This is an automated email from the ASF dual-hosted git repository.
arm pushed a commit to branch previous_sbom_results
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/previous_sbom_results by this
push:
new 2d9da11 Highlight new/updated vulnerabilities and colour code
severities
2d9da11 is described below
commit 2d9da11f9e034c135ea1899b67a8fe927768542f
Author: Alastair McFarlane <[email protected]>
AuthorDate: Tue Dec 23 11:10:51 2025 +0000
Highlight new/updated vulnerabilities and colour code severities
---
atr/get/sbom.py | 167 +++++++++++++++++++++++++++++++++---------------
atr/sbom/models/base.py | 2 +-
2 files changed, 116 insertions(+), 53 deletions(-)
diff --git a/atr/get/sbom.py b/atr/get/sbom.py
index 7bda641..0da25bb 100644
--- a/atr/get/sbom.py
+++ b/atr/get/sbom.py
@@ -400,15 +400,35 @@ def _license_tally(
)
-def _vulnerability_component_details_osv(block: htm.Block, component:
results.OSVComponent) -> None:
- details_content = []
- summary_element = htm.summary[
-
htm.span(".badge.bg-danger.me-2.font-monospace")[str(len(component.vulnerabilities))],
- htm.strong[component.purl],
- ]
- details_content.append(summary_element)
-
+def _severity_to_style(severity: str) -> str:
+ match severity.lower():
+ case "critical":
+ return ".bg-danger.text-light"
+ case "high":
+ return ".bg-danger.text-light"
+ case "medium":
+ return ".bg-warning.text-dark"
+ case "moderate":
+ return ".bg-warning.text-dark"
+ case "low":
+ return ".bg-warning.text-dark"
+ case "info":
+ return ".bg-info.text-light"
+ return ".bg-info.text-light"
+
+
+def _vulnerability_component_details_osv(
+ block: htm.Block,
+ component: results.OSVComponent,
+ previous_vulns: dict[str, str] | None, # id: severity
+) -> int:
+ severities = ["critical", "high", "medium", "moderate", "low", "info",
"none", "unknown"]
+ new = 0
+ worst = 99
+
+ vuln_details = []
for vuln in component.vulnerabilities:
+ is_new = False
vuln_id = vuln.id or "Unknown"
vuln_summary = vuln.summary
vuln_refs = []
@@ -416,11 +436,21 @@ def _vulnerability_component_details_osv(block:
htm.Block, component: results.OS
vuln_refs = [r for r in vuln.references if r.get("type", "") ==
"WEB"]
vuln_primary_ref = vuln_refs[0] if (len(vuln_refs) > 0) else {}
vuln_modified = vuln.modified or "Unknown"
+
vuln_severity = _extract_vulnerability_severity(vuln)
+ sev_index = severities.index(vuln_severity)
+ worst = min(worst, sev_index)
+
+ if previous_vulns is not None:
+ if vuln_id not in previous_vulns.keys() or previous_vulns[vuln_id]
!= vuln_severity:
+ is_new = True
+ new = new + 1
vuln_header = [htm.a(href=vuln_primary_ref.get("url", ""),
target="_blank")[htm.strong(".me-2")[vuln_id]]]
- if vuln_severity != "Unknown":
-
vuln_header.append(htm.span(".badge.bg-warning.text-dark")[vuln_severity])
+ style = f".badge.me-2{_severity_to_style(vuln_severity)}"
+ vuln_header.append(htm.span(style)[vuln_severity])
+ if is_new:
+ vuln_header.append(htm.span(".badge.bg-info.text-light")["new"])
details =
markupsafe.Markup(cmarkgfm.github_flavored_markdown_to_html(vuln.details))
vuln_div =
htm.div(".ms-3.mb-3.border-start.border-warning.border-3.ps-3")[
@@ -432,13 +462,22 @@ def _vulnerability_component_details_osv(block:
htm.Block, component: results.OS
],
htm.div(".mt-2.text-muted")[details or "No additional details
available."],
]
- details_content.append(vuln_div)
-
+ vuln_details.append(vuln_div)
+
+ badge_style = ""
+ if worst < len(severities):
+ badge_style = _severity_to_style(severities[worst])
+ summary_elements =
[htm.span(f".badge{badge_style}.me-2.font-monospace")[str(len(component.vulnerabilities))]]
+ if new > 0:
+ summary_elements.append(htm.span(".badge.me-2.bg-info")[f"{new!s}
new"])
+ summary_elements.append(htm.strong[component.purl])
+ details_content = [htm.summary[*summary_elements], *vuln_details]
block.append(htm.details(".mb-3.rounded")[*details_content])
+ return new
def _vulnerability_scan_button(block: htm.Block) -> None:
- block.p["No new vulnerability scan has been performed for this revision."]
+ block.p["You can perform a new vulnerability scan."]
form.render_block(
block,
@@ -476,58 +515,82 @@ def _vulnerability_scan_results(
vulns: list[osv.CdxVulnerabilityDetail],
scans: list[str],
task: sql.Task | None,
- prev: list[osv.CdxVulnerabilityDetail], # TODO: highlight new
vulnerabilities
+ prev: list[osv.CdxVulnerabilityDetail] | None,
) -> None:
+ previous_vulns = None
+ if prev is not None:
+ previous_osv = [_cdx_to_osv(v) for v in prev]
+ previous_vulns = {v.id: _extract_vulnerability_severity(v) for v in
previous_osv}
if task is not None:
- task_result = task.result
- if not isinstance(task_result, results.SBOMOSVScan):
- block.p["Invalid scan result format."]
- return
+ _vulnerability_results_from_scan(task, block, previous_vulns)
+ else:
+ _vulnerability_results_from_bom(vulns, block, scans, previous_vulns)
- components = task_result.components
- ignored = task_result.ignored
- ignored_count = len(ignored)
- if not components:
- block.p["No vulnerabilities found."]
- if ignored_count > 0:
- component_word = "component was" if (ignored_count == 1) else
"components were"
- block.p[f"{ignored_count} {component_word} ignored due to
missing PURL or version information:"]
- block.p[f"{','.join(ignored)}"]
- return
+def _vulnerability_results_from_bom(
+ vulns: list[osv.CdxVulnerabilityDetail], block: htm.Block, scans:
list[str], previous_vulns: dict[str, str] | None
+) -> None:
+ total_new = 0
+ new_block = htm.Block()
+ if len(vulns) == 0:
+ block.p["No vulnerabilities listed in this SBOM."]
+ return
+ components = {a.get("ref", "") for v in vulns if v.affects is not None for
a in v.affects}
+
+ if len(scans) > 0:
+ block.p["This SBOM was scanned for vulnerabilities at revision ",
htm.code[scans[-1]], "."]
+
+ for component in components:
+ new = _vulnerability_component_details_osv(
+ new_block,
+ results.OSVComponent(
+ purl=component,
+ vulnerabilities=[
+ _cdx_to_osv(v)
+ for v in vulns
+ if (v.affects is not None) and (component in [a.get("ref")
for a in v.affects])
+ ],
+ ),
+ previous_vulns,
+ )
+ total_new = total_new + new
+
+ new_str = f" ({total_new!s} new since last release)" if (total_new > 0)
else ""
+ block.p[f"Vulnerabilities{new_str} found in {len(components)} components:"]
+ block.append(new_block)
+
- block.p[f"Scan found vulnerabilities in {len(components)} components:"]
+def _vulnerability_results_from_scan(task: sql.Task, block: htm.Block,
previous_vulns: dict[str, str] | None) -> None:
+ total_new = 0
+ new_block = htm.Block()
+ task_result = task.result
+ if not isinstance(task_result, results.SBOMOSVScan):
+ block.p["Invalid scan result format."]
+ return
- for component in components:
- _vulnerability_component_details_osv(block, component)
+ components = task_result.components
+ ignored = task_result.ignored
+ ignored_count = len(ignored)
+ if not components:
+ block.p["No vulnerabilities found."]
if ignored_count > 0:
component_word = "component was" if (ignored_count == 1) else
"components were"
block.p[f"{ignored_count} {component_word} ignored due to missing
PURL or version information:"]
block.p[f"{','.join(ignored)}"]
- else:
- if len(vulns) == 0:
- block.p["No vulnerabilities listed in this SBOM."]
- return
- components = {a.get("ref", "") for v in vulns if v.affects is not None
for a in v.affects}
+ return
- if len(scans) > 0:
- block.p["This SBOM was scanned for vulnerabilities at revision ",
htm.code[scans[-1]], "."]
+ for component in components:
+ new = _vulnerability_component_details_osv(new_block, component,
previous_vulns)
+ total_new = total_new + new
- block.p[f"Vulnerabilities found in {len(components)} components:"]
+ new_str = f" ({total_new!s} new since last release)" if (total_new > 0)
else ""
+ block.p[f"Scan found vulnerabilities{new_str} in {len(components)}
components:"]
- for component in components:
- _vulnerability_component_details_osv(
- block,
- results.OSVComponent(
- purl=component,
- vulnerabilities=[
- _cdx_to_osv(v)
- for v in vulns
- if (v.affects is not None) and (component in
[a.get("ref") for a in v.affects])
- ],
- ),
- )
+ if ignored_count > 0:
+ component_word = "component was" if (ignored_count == 1) else
"components were"
+ block.p[f"{ignored_count} {component_word} ignored due to missing PURL
or version information:"]
+ block.p[f"{','.join(ignored)}"]
def _cdx_to_osv(cdx: osv.CdxVulnerabilityDetail) -> osv.VulnerabilityDetails:
@@ -577,7 +640,7 @@ def _vulnerability_scan_section(
sbom.models.osv.CdxVulnAdapter.validate_python(json.loads(e)) for
e in task_result.prev_vulnerabilities
]
else:
- prev_vulnerabilities = []
+ prev_vulnerabilities = None
if task_result.atr_props is not None:
scans = [t.get("value", "") for t in task_result.atr_props if
t.get("name", "") == "asf:atr:osv-scan"]
_vulnerability_scan_results(block, vulnerabilities, scans, completed_task,
prev_vulnerabilities)
diff --git a/atr/sbom/models/base.py b/atr/sbom/models/base.py
index d392d17..2e34024 100644
--- a/atr/sbom/models/base.py
+++ b/atr/sbom/models/base.py
@@ -21,7 +21,7 @@ import pydantic
class Lax(pydantic.BaseModel):
- model_config = pydantic.ConfigDict(extra="allow", strict=False)
+ model_config = pydantic.ConfigDict(extra="allow", strict=False,
validate_assignment=True, validate_by_name=True)
class Strict(pydantic.BaseModel):
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]