security: show the fixed versions for each vuln

the data quality is bad, but let's see
This commit is contained in:
Christoph Reiter 2025-09-07 10:13:00 +02:00
parent 3f17254ab1
commit 4ad9115d46
8 changed files with 58 additions and 55 deletions

View File

@ -14,7 +14,7 @@ from typing import NamedTuple, Any
from collections.abc import Iterable
from collections.abc import Sequence
from pydantic import BaseModel
from dataclasses import dataclass
from dataclasses import dataclass, field
from packageurl import PackageURL
from .appconfig import REPOSITORIES
@ -200,6 +200,7 @@ class Vulnerability:
url: str
severity: Severity
ignored: bool = False
unaffected_versions: list[str] = field(default_factory=list)
@property
def sort_key(self) -> tuple[bool, int, str, str]:

View File

@ -29,10 +29,19 @@ def parse_cdx(data: bytes) -> dict[str, list[Vulnerability]]:
for ratings in vuln["ratings"]:
severity = Severity(ratings["severity"])
break
unaffected_versions = []
for affects in vuln["affects"]:
versions = affects.get("versions", [])
for version in versions:
if version.get("status") == "unaffected" and "version" in version:
unaffected_versions.append(version["version"])
return Vulnerability(
id=vuln["id"],
url=vuln["source"]["url"],
severity=severity)
severity=severity,
unaffected_versions=unaffected_versions)
vuln_mapping: dict[str, list[Vulnerability]] = {}
for vuln in cdx["vulnerabilities"]:

View File

@ -1,6 +1,7 @@
{% extends "layout.html" %}
{% block title %}Base Package: {{ sources[0].name if sources else '' }}{% endblock %}
{% block inner_content %}
{% from 'macros.html' import vulnerability_list %}
{% for s in sources %}
<div class="card mb-3">
@ -85,11 +86,7 @@
<dt class="col-sm-3 text-sm-end">Vulnerabilities:</dt>
<dd class="col-sm-9">
{% if s.all_vulnerabilities %}
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
{% endfor %}
</ul>
{{ vulnerability_list(s) }}
{% elif not s.can_have_vulnerabilities %}
<span class="text-muted">Not enough metadata for vulnerability reporting</span>
{% else %}

32
app/templates/macros.html Normal file
View File

@ -0,0 +1,32 @@
{% macro vulnerability_color(vuln) %}
{%- if vuln.severity|string == "critical" -%}
danger
{%- elif vuln.severity|string == "high" -%}
warning
{%- else -%}
secondary
{%- endif -%}
{% endmacro %}
{% macro vulnerability_tooltip(s) %}
<span class="mytooltip-onclick">
<span role="button" class="text-{{vulnerability_color(s.worst_active_vulnerability)}}"></span>
<template class="mytooltip-content">
{{ vulnerability_list(s) }}
</template>
</span>
{% endmacro %}
{% macro vulnerability_list(s) %}
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}>
<a href="{{ vuln.url }}">{{ vuln.id }}</a>
<span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span>
{% if not vuln.ignored and vuln.unaffected_versions %}
<br><span>(fixed in {{ vuln.unaffected_versions|join(', ') }})</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% endmacro %}

View File

@ -1,6 +1,7 @@
{% extends "layout.html" %}
{% block title %}Outdated Packages{% endblock %}
{% block inner_content %}
{% from 'macros.html' import vulnerability_tooltip %}
<div class="card mb-3">
<div class="card-header">
@ -64,20 +65,11 @@
<td class="text-version">{{ myver }}{% if gitver %} <span class="text-muted small align-text-bottom ps-1">({{ gitver }} in git)</span>{% endif %}</td>
<td></td>
<td class="text-version"><a href="{{ url }}">{{ ver }}</a></td>
<td>
{% if s.active_vulnerabilities %}
<td class="mytooltip-onclick">
<span role="button" class="text-{{vulnerability_color(s.worst_active_vulnerability)}}"></span>
<template class="mytooltip-content">
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}><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>
{{ vulnerability_tooltip(s) }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
@ -87,16 +79,7 @@
{% for s in missing %}
<a href="{{ url_for('base', base_name=s.name) }}">{{ s.realname }}</a>
{%- if s.active_vulnerabilities %}
<span class="mytooltip-onclick">
<span role="button" class="text-{{vulnerability_color(s.worst_active_vulnerability)}}"></span>
<template class="mytooltip-content">
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
{% endfor %}
</ul>
</template>
</span>
{{ vulnerability_tooltip(s) }}
{% endif -%}
{{ ", " if not loop.last else '' }}
{% endfor %}

View File

@ -1,6 +1,7 @@
{% extends "layout.html" %}
{% block title %}Package: {{ packages[0][1].name if packages else '' }}{% endblock %}
{% block inner_content %}
{% from 'macros.html' import vulnerability_list %}
{% for s, p in packages %}
<div class="card mb-3">
@ -79,11 +80,7 @@
{% if s.all_vulnerabilities %}
<dt class="col-sm-3 text-sm-end">Vulnerabilities:</dt>
<dd class="col-sm-9">
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
{% endfor %}
</ul>
{{ vulnerability_list(s) }}
</dd>
{% endif%}

View File

@ -1,6 +1,7 @@
{% extends "layout.html" %}
{% block title %}Security {% endblock %}
{% block inner_content %}
{% from 'macros.html' import vulnerability_tooltip %}
<div class="card mb-3">
<div class="card-header">
@ -35,15 +36,8 @@
<td><a href="{{ url_for('base', base_name=s.name) }}">{{ s.name }}</a></td>
<td class="text-version">{{ s.version }}{% if s.version != s.git_version %} <span class="text-muted small align-text-bottom ps-1">({{ s.git_version }} in git)</span>{% endif %}</td>
<td class="text-version">{{ s.upstream_version if s.is_outdated_in_git else '' }}</td>
<td class="mytooltip-onclick">
<span role="button" class="text-{{vulnerability_color(s.worst_active_vulnerability)}}"></span>
<template class="mytooltip-content">
<ul class="list-unstyled">
{% for vuln in s.all_vulnerabilities %}
<li {% if vuln.ignored %}style="text-decoration: line-through"{% endif %}><a href="{{ vuln.url }}">{{ vuln.id }}</a> <span class="opacity-75 text-{{vulnerability_color(vuln)}}">({{ vuln.severity }})</span></li>
{% endfor %}
</ul>
</template>
<td>
{{ vulnerability_tooltip(s) }}
</td>
</tr>
{% endfor %}

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, Vulnerability, Severity, PackageKey, find_packages
from .appstate import state, get_repositories, Package, Source, DepType, SrcInfoPackage, get_base_group_name, Vulnerability, PackageKey, find_packages
from .utils import extract_upstream_version, version_is_newer_than
router = APIRouter(default_response_class=HTMLResponse)
@ -92,16 +92,6 @@ 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 = ""