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]

Reply via email to