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
commit 837d1296ccc84cf29e27b7cd503b2feb032800f4 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 | 170 +++++++++++++++++++++++++++++++++--------------- atr/sbom/models/base.py | 2 +- 2 files changed, 119 insertions(+), 53 deletions(-) diff --git a/atr/get/sbom.py b/atr/get/sbom.py index 7bda641..b76cd68 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,24 @@ 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) + try: + sev_index = severities.index(vuln_severity) + except ValueError: + sev_index = 99 + 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 +465,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 +518,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 +643,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]
