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",
]
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

View File

@ -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
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 .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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 = ""

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;