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",
|
"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
|
||||||
|
|||||||
@ -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
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 .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
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
12
app/web.py
12
app/web.py
@ -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 = ""
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user