Expose matched CVEs on the outofdate and the package pages
This commit is contained in:
parent
b86d0a3b0b
commit
9bdd3d22b1
@ -39,6 +39,11 @@ PYPI_URLS = [
|
||||
"https://github.com/msys2/MSYS2-packages/releases/download/srcinfo-cache/pypi.json.gz",
|
||||
]
|
||||
|
||||
CDX_URLS = [
|
||||
"https://github.com/msys2/MINGW-packages/releases/download/srcinfo-cache/sbom.vuln.cdx.json",
|
||||
"https://github.com/msys2/MSYS2-packages/releases/download/srcinfo-cache/sbom.vuln.cdx.json"
|
||||
]
|
||||
|
||||
GENTOO_SNAPSHOT_URL = "https://mirror.leaseweb.com/gentoo/snapshots/gentoo-latest.tar.xz"
|
||||
|
||||
# Check for updates every 5 minutes by default, at max 1 time every minute even if triggered
|
||||
|
||||
@ -13,6 +13,7 @@ from urllib.parse import quote_plus, quote
|
||||
from typing import NamedTuple, Any
|
||||
from collections.abc import Sequence
|
||||
from pydantic import BaseModel
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .appconfig import REPOSITORIES
|
||||
from .utils import vercmp, version_is_newer_than, extract_upstream_version, split_depends, \
|
||||
@ -158,6 +159,34 @@ class BuildStatus(BaseModel):
|
||||
cycles: list[tuple[str, str]] = []
|
||||
|
||||
|
||||
class Severity(Enum):
|
||||
|
||||
UNKNOWN = "unknown"
|
||||
LOW = "low"
|
||||
MEDIUM = "medium"
|
||||
HIGH = "high"
|
||||
CRITICAL = "critical"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
@property
|
||||
def sort_key(self) -> int:
|
||||
return list(Severity).index(self)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Vulnerability:
|
||||
|
||||
id: str
|
||||
url: str
|
||||
severity: Severity
|
||||
|
||||
@property
|
||||
def sort_key(self) -> tuple[int, str, str]:
|
||||
return (self.severity.sort_key, self.id, self.url)
|
||||
|
||||
|
||||
class AppState:
|
||||
|
||||
def __init__(self) -> None:
|
||||
@ -171,6 +200,7 @@ class AppState:
|
||||
self._pkgextra: PkgExtra = PkgExtra(packages={})
|
||||
self._ext_infos: dict[ExtId, dict[str, ExtInfo]] = {}
|
||||
self._build_status: BuildStatus = BuildStatus()
|
||||
self._vulnerabilities: dict[str, list[Vulnerability]] = {}
|
||||
self._update_etag()
|
||||
|
||||
def _update_etag(self) -> None:
|
||||
@ -232,6 +262,15 @@ class AppState:
|
||||
self._build_status = build_status
|
||||
self._update_etag()
|
||||
|
||||
@property
|
||||
def vulnerabilities(self) -> dict[str, list[Vulnerability]]:
|
||||
return self._vulnerabilities
|
||||
|
||||
@vulnerabilities.setter
|
||||
def vulnerabilities(self, vulnerabilities: dict[str, list[Vulnerability]]) -> None:
|
||||
self._vulnerabilities = vulnerabilities
|
||||
self._update_etag()
|
||||
|
||||
|
||||
class Package:
|
||||
|
||||
@ -394,6 +433,16 @@ class Source:
|
||||
def _package(self) -> Package:
|
||||
return sorted(self.packages.items())[0][1]
|
||||
|
||||
@property
|
||||
def vulnerabilities(self) -> list[Vulnerability]:
|
||||
return sorted(state.vulnerabilities.get(self.name, []), key=lambda v: v.sort_key, reverse=True)
|
||||
|
||||
@property
|
||||
def worst_vulnerability(self) -> Vulnerability | None:
|
||||
if not self.vulnerabilities:
|
||||
return None
|
||||
return sorted(self.vulnerabilities, key=lambda v: v.severity.sort_key)[-1]
|
||||
|
||||
@property
|
||||
def repos(self) -> list[str]:
|
||||
return sorted({p.repo for p in self.packages.values()})
|
||||
|
||||
57
app/fetch/cdx.py
Normal file
57
app/fetch/cdx.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright 2024 Christoph Reiter
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import json
|
||||
|
||||
from ..appconfig import CDX_URLS, REQUEST_TIMEOUT
|
||||
from ..appstate import Severity, Vulnerability, state
|
||||
from ..utils import logger
|
||||
from .utils import check_needs_update, get_content_cached
|
||||
|
||||
|
||||
def parse_cdx(data: bytes) -> dict[str, list[Vulnerability]]:
|
||||
"""Parse the cdx data and returns a mapping of pkgbase names to a list of
|
||||
vulnerabilities."""
|
||||
|
||||
cdx = json.loads(data)
|
||||
|
||||
mapping = {}
|
||||
for component in cdx["components"]:
|
||||
name = component["name"]
|
||||
bom_ref = component["bom-ref"]
|
||||
mapping[bom_ref] = name
|
||||
|
||||
def parse_vuln(vuln: dict) -> Vulnerability:
|
||||
severity = Severity.UNKNOWN
|
||||
for ratings in vuln["ratings"]:
|
||||
severity = Severity(ratings["severity"])
|
||||
break
|
||||
return Vulnerability(
|
||||
id=vuln["id"],
|
||||
url=vuln["source"]["url"],
|
||||
severity=severity)
|
||||
|
||||
vuln_mapping: dict[str, list[Vulnerability]] = {}
|
||||
for vuln in cdx["vulnerabilities"]:
|
||||
for affected in vuln["affects"]:
|
||||
bom_ref = affected["ref"]
|
||||
name = mapping[bom_ref]
|
||||
vuln_mapping.setdefault(name, []).append(parse_vuln(vuln))
|
||||
|
||||
return vuln_mapping
|
||||
|
||||
|
||||
async def update_cdx() -> None:
|
||||
urls = CDX_URLS
|
||||
if not await check_needs_update(urls):
|
||||
return
|
||||
|
||||
logger.info("update cdx")
|
||||
vuln_mapping = {}
|
||||
for url in urls:
|
||||
logger.info("Loading %r" % url)
|
||||
data = await get_content_cached(url, timeout=REQUEST_TIMEOUT)
|
||||
logger.info(f"Done: {url!r}")
|
||||
vuln_mapping.update(parse_cdx(data))
|
||||
|
||||
state.vulnerabilities = vuln_mapping
|
||||
@ -18,6 +18,7 @@ from .cygwin import update_cygwin_versions
|
||||
from .gentoo import update_gentoo_versions
|
||||
from .source import update_source
|
||||
from .sourceinfos import update_sourceinfos
|
||||
from .cdx import update_cdx
|
||||
|
||||
|
||||
_rate_limit = AsyncLimiter(UPDATE_MIN_RATE, UPDATE_MIN_INTERVAL)
|
||||
@ -63,6 +64,7 @@ async def update_loop() -> None:
|
||||
update_source(),
|
||||
update_sourceinfos(),
|
||||
update_build_status(),
|
||||
update_cdx(),
|
||||
]
|
||||
await asyncio.gather(*awaitables)
|
||||
state.ready = True
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -94,6 +94,17 @@
|
||||
</dd>
|
||||
{% endfor %}
|
||||
|
||||
{% if s.vulnerabilities %}
|
||||
<dt class="col-sm-3 text-sm-end">Vulnerabilities:</dt>
|
||||
<dd class="col-sm-9">
|
||||
<ul class="list-unstyled">
|
||||
{% for vuln in s.vulnerabilities %}
|
||||
<li><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</dd>
|
||||
{% endif%}
|
||||
|
||||
<dt class="col-sm-3 text-sm-end"></dt>
|
||||
<dd class="col-sm-9"><hr></dd>
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
<th>Repo Version</th>
|
||||
<th></th>
|
||||
<th>New Version</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -63,6 +64,20 @@
|
||||
<td class="text-version">{{ myver }}{% if gitver %} <span class="text-muted small px-2">({{ gitver }} in git)</span>{% endif %}</td>
|
||||
<td>→</td>
|
||||
<td class="text-version"><a href="{{ url }}">{{ ver }}</a></td>
|
||||
{% if s.vulnerabilities %}
|
||||
<td class="mytooltip-onclick">
|
||||
<span role="button" class="text-{{vulnerability_color(s.worst_vulnerability)}}">⚠</span>
|
||||
<template class="mytooltip-content">
|
||||
<ul class="list-unstyled">
|
||||
{% for vuln in s.vulnerabilities %}
|
||||
<li><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</template>
|
||||
</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -79,6 +79,17 @@
|
||||
</dd>
|
||||
{% endfor %}
|
||||
|
||||
{% if s.vulnerabilities %}
|
||||
<dt class="col-sm-3 text-sm-end">Vulnerabilities:</dt>
|
||||
<dd class="col-sm-9">
|
||||
<ul class="list-unstyled">
|
||||
{% for vuln in s.vulnerabilities %}
|
||||
<li><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</dd>
|
||||
{% endif%}
|
||||
|
||||
<dt class="col-sm-3 text-sm-end"></dt>
|
||||
<dd class="col-sm-9"><hr></dd>
|
||||
|
||||
|
||||
12
app/web.py
12
app/web.py
@ -21,7 +21,7 @@ from fastapi_etag import Etag
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi_etag import add_exception_handler as add_etag_exception_handler
|
||||
|
||||
from .appstate import state, get_repositories, Package, Source, DepType, SrcInfoPackage, get_base_group_name
|
||||
from .appstate import state, get_repositories, Package, Source, DepType, SrcInfoPackage, get_base_group_name, Vulnerability, Severity
|
||||
from .appconfig import DEFAULT_REPO
|
||||
from .utils import extract_upstream_version, version_is_newer_than
|
||||
|
||||
@ -82,6 +82,16 @@ def update_timestamp(request: Request) -> float:
|
||||
return state.last_update
|
||||
|
||||
|
||||
@context_function("vulnerability_color")
|
||||
def vulnerability_color(request: Request, vuln: Vulnerability) -> str:
|
||||
if vuln.severity == Severity.CRITICAL:
|
||||
return "danger"
|
||||
elif vuln.severity == Severity.HIGH:
|
||||
return "warning"
|
||||
else:
|
||||
return "secondary"
|
||||
|
||||
|
||||
@context_function("package_url")
|
||||
def package_url(request: Request, package: Package, name: str | None = None) -> str:
|
||||
res: str = ""
|
||||
|
||||
@ -35,4 +35,25 @@ tippy('.mytooltip', {
|
||||
},
|
||||
});
|
||||
|
||||
tippy('.mytooltip-onclick', {
|
||||
allowHTML: true,
|
||||
theme: 'bootstrap',
|
||||
placement: 'top',
|
||||
interactive: true,
|
||||
trigger: 'click',
|
||||
popperOptions: {
|
||||
modifiers: [
|
||||
{
|
||||
name: 'flip',
|
||||
options: {
|
||||
fallbackPlacements: ['top', 'bottom', 'left'],
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
content(reference) {
|
||||
return reference.querySelector(".mytooltip-content").innerHTML;
|
||||
},
|
||||
});
|
||||
|
||||
window.App = App;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user