Expose matched CVEs on the outofdate and the package pages

This commit is contained in:
Christoph Reiter 2024-03-24 11:32:36 +01:00
parent b86d0a3b0b
commit 9bdd3d22b1
10 changed files with 183 additions and 2 deletions

View File

@ -39,6 +39,11 @@ PYPI_URLS = [
"https://github.com/msys2/MSYS2-packages/releases/download/srcinfo-cache/pypi.json.gz", "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" 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 # Check for updates every 5 minutes by default, at max 1 time every minute even if triggered

View File

@ -13,6 +13,7 @@ from urllib.parse import quote_plus, quote
from typing import NamedTuple, Any from typing import NamedTuple, Any
from collections.abc import Sequence from collections.abc import Sequence
from pydantic import BaseModel from pydantic import BaseModel
from dataclasses import dataclass
from .appconfig import REPOSITORIES from .appconfig import REPOSITORIES
from .utils import vercmp, version_is_newer_than, extract_upstream_version, split_depends, \ 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]] = [] 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: class AppState:
def __init__(self) -> None: def __init__(self) -> None:
@ -171,6 +200,7 @@ class AppState:
self._pkgextra: PkgExtra = PkgExtra(packages={}) self._pkgextra: PkgExtra = PkgExtra(packages={})
self._ext_infos: dict[ExtId, dict[str, ExtInfo]] = {} self._ext_infos: dict[ExtId, dict[str, ExtInfo]] = {}
self._build_status: BuildStatus = BuildStatus() self._build_status: BuildStatus = BuildStatus()
self._vulnerabilities: dict[str, list[Vulnerability]] = {}
self._update_etag() self._update_etag()
def _update_etag(self) -> None: def _update_etag(self) -> None:
@ -232,6 +262,15 @@ class AppState:
self._build_status = build_status self._build_status = build_status
self._update_etag() 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: class Package:
@ -394,6 +433,16 @@ class Source:
def _package(self) -> Package: def _package(self) -> Package:
return sorted(self.packages.items())[0][1] 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 @property
def repos(self) -> list[str]: def repos(self) -> list[str]:
return sorted({p.repo for p in self.packages.values()}) return sorted({p.repo for p in self.packages.values()})

57
app/fetch/cdx.py Normal file
View 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

View File

@ -18,6 +18,7 @@ from .cygwin import update_cygwin_versions
from .gentoo import update_gentoo_versions from .gentoo import update_gentoo_versions
from .source import update_source from .source import update_source
from .sourceinfos import update_sourceinfos from .sourceinfos import update_sourceinfos
from .cdx import update_cdx
_rate_limit = AsyncLimiter(UPDATE_MIN_RATE, UPDATE_MIN_INTERVAL) _rate_limit = AsyncLimiter(UPDATE_MIN_RATE, UPDATE_MIN_INTERVAL)
@ -63,6 +64,7 @@ async def update_loop() -> None:
update_source(), update_source(),
update_sourceinfos(), update_sourceinfos(),
update_build_status(), update_build_status(),
update_cdx(),
] ]
await asyncio.gather(*awaitables) await asyncio.gather(*awaitables)
state.ready = True state.ready = True

File diff suppressed because one or more lines are too long

View File

@ -94,6 +94,17 @@
</dd> </dd>
{% endfor %} {% 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> <dt class="col-sm-3 text-sm-end"></dt>
<dd class="col-sm-9"><hr></dd> <dd class="col-sm-9"><hr></dd>

View File

@ -44,6 +44,7 @@
<th>Repo Version</th> <th>Repo Version</th>
<th></th> <th></th>
<th>New Version</th> <th>New Version</th>
<th></th>
</tr> </tr>
</thead> </thead>
<tbody> <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 class="text-version">{{ myver }}{% if gitver %} <span class="text-muted small px-2">({{ gitver }} in git)</span>{% endif %}</td>
<td></td> <td></td>
<td class="text-version"><a href="{{ url }}">{{ ver }}</a></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> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@ -79,6 +79,17 @@
</dd> </dd>
{% endfor %} {% 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> <dt class="col-sm-3 text-sm-end"></dt>
<dd class="col-sm-9"><hr></dd> <dd class="col-sm-9"><hr></dd>

View File

@ -21,7 +21,7 @@ from fastapi_etag import Etag
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi_etag import add_exception_handler as add_etag_exception_handler 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 .appconfig import DEFAULT_REPO
from .utils import extract_upstream_version, version_is_newer_than from .utils import extract_upstream_version, version_is_newer_than
@ -82,6 +82,16 @@ def update_timestamp(request: Request) -> float:
return state.last_update 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") @context_function("package_url")
def package_url(request: Request, package: Package, name: str | None = None) -> str: def package_url(request: Request, package: Package, name: str | None = None) -> str:
res: str = "" res: str = ""

View File

@ -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; window.App = App;