Coverage for /home/benjarobin/Bootlin/projects/Schneider-Electric-Senux/sbom-cve-check/src/sbom_cve_check/export/export_yocto.py: 97%
61 statements
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-28 15:37 +0100
« prev ^ index » next coverage.py v7.11.1, created at 2025-11-28 15:37 +0100
1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: GPL-2.0-only
4import json
5import pathlib
6from collections.abc import Generator
7from datetime import UTC
8from typing import Any
10from ..cve_db.annot_aggregate import AggregateAnnotEntry
11from ..sbom.component import CompBuild
12from ..sbom.sbom_base import Sbom
13from ..vuln.cve import CveVexStatus
14from ..vuln.cvss import CvssMetric, CvssVersion
15from .export_base import BaseExport
16from .registry import register_export
19def _group_cvss_metrics_by_vers(metric: CvssMetric) -> CvssVersion:
20 return (
21 CvssVersion.V3_0 if (metric.cvss_ver == CvssVersion.V3_1) else metric.cvss_ver
22 )
25def _get_cvss_metric_score(
26 metrics: dict[CvssVersion, tuple[CvssMetric, ...]], vers: CvssVersion
27) -> str:
28 m = metrics.get(vers)
29 return f"{m[0].score:.1f}" if m else "0.0"
32def _get_modified_date(annotation: AggregateAnnotEntry) -> str | None:
33 d = annotation.date_modified
34 if d:
35 return d.astimezone(UTC).replace(tzinfo=None).isoformat(timespec="milliseconds")
36 return None
39def _get_cve_status(annotation: AggregateAnnotEntry) -> str | None:
40 s = annotation.vex_assessment.status
41 if s == CveVexStatus.FIXED:
42 return "Patched"
43 if s in (CveVexStatus.AFFECTED, CveVexStatus.UNDER_INVESTIGATION):
44 return "Unpatched"
45 if s == CveVexStatus.NOT_AFFECTED:
46 return "Ignored"
47 return None
50def _gen_issue_info(annotation: AggregateAnnotEntry) -> dict[str, str | None]:
51 metrics = annotation.group_cvss_metrics(key=_group_cvss_metrics_by_vers)
52 metric_vector = (
53 metrics.get(CvssVersion.V4_0)
54 or metrics.get(CvssVersion.V3_0)
55 or metrics.get(CvssVersion.V2_0)
56 )
57 attack_vector = "UNKNOWN"
58 vector_str = None
59 if metric_vector:
60 vector_str = metric_vector[0].vector_str
61 enum_av = metric_vector[0].decode_vector().get("AV")
62 if enum_av is not None:
63 attack_vector = enum_av.name
65 return {
66 "id": annotation.identifier.id,
67 "status": _get_cve_status(annotation),
68 "link": f"https://nvd.nist.gov/vuln/detail/{annotation.identifier.id}",
69 "summary": annotation.description or "",
70 "scorev2": _get_cvss_metric_score(metrics, CvssVersion.V2_0),
71 "scorev3": _get_cvss_metric_score(metrics, CvssVersion.V3_0),
72 "scorev4": _get_cvss_metric_score(metrics, CvssVersion.V4_0),
73 "modified": _get_modified_date(annotation),
74 "vector": attack_vector,
75 "vectorString": vector_str,
76 "detail": annotation.vex_assessment.status_notes,
77 }
80@register_export("yocto-cve-check-manifest")
81class YoctoCveCheckExport(BaseExport):
82 def __init__(self, sbom: Sbom, out_path: pathlib.Path) -> None:
83 super().__init__(sbom, out_path)
84 self._packages: list[dict[str, Any]] = []
86 def start_export(self) -> Generator[None, None, None]:
87 yield
88 json_obj = {"version": 1, "package": self._packages}
89 with self._out_path.open("w", encoding="utf-8") as f:
90 json.dump(json_obj, f, indent=2)
92 def export_comp_info(
93 self, comp_build: CompBuild
94 ) -> Generator[None, tuple[bool, AggregateAnnotEntry], None]:
95 issues = []
96 try:
97 while True:
98 is_filtered, annotation = yield
99 if not is_filtered:
100 issues.append(_gen_issue_info(annotation))
101 finally:
102 cves_in_rec = "Yes" if issues else "No"
104 self._packages.append(
105 {
106 "name": comp_build.build_name,
107 "layer": "",
108 "version": comp_build.version,
109 "products": [
110 {"product": n, "cvesInRecord": cves_in_rec}
111 for n in sorted({c.name for c in comp_build.identifiers})
112 ],
113 "issue": issues,
114 "cpes": [
115 str(c.build_cpe(version=comp_build.version))
116 for c in comp_build.identifiers
117 ],
118 }
119 )