Skip to content

Commit fa04e22

Browse files
committed
fix: cyclonedx import of grype scan results
1 parent f1540b3 commit fa04e22

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

  • backend
    • application/import_observations/parsers/cyclone_dx
    • unittests/import_observations/parsers/cyclone_dx

backend/application/import_observations/parsers/cyclone_dx/parser.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def get_observations(self, data: dict, product: Product, branch: Optional[Branch
117117
payload = base64.b64decode(cosign_output["payload"]).decode("utf-8")
118118
sbom_data = json.loads(payload)["predicate"]
119119

120-
self.components = self._get_components(sbom_data or data)
120+
self.components = self._get_components(data, sbom_data)
121121
self.dependencies = self._get_dependencies(sbom_data or data)
122122
observations = self._create_observations(data)
123123

@@ -163,7 +163,7 @@ def _add_license_component_evidence(
163163
evidence.append(dumps(component.json))
164164
license_component.unsaved_evidences.append(evidence)
165165

166-
def _get_components(self, data: dict) -> dict[str, Component]:
166+
def _get_components(self, data: dict, sbom_data: Optional[dict] = None) -> dict[str, Component]:
167167
components_dict = {}
168168
components_list: list[Component] = []
169169

@@ -175,6 +175,15 @@ def _get_components(self, data: dict) -> dict[str, Component]:
175175
components = self._get_sbom_component_with_subs(sbom_component)
176176
components_list.extend(components)
177177

178+
# The scan report (e.g. Grype) and the attested SBOM (Trivy) use different bom-refs for the
179+
# same package, so the vulnerabilities reference refs that only exist in the scan report.
180+
# Merge both component sets so those refs resolve, while still keeping the richer SBOM
181+
# components for dependency relations and license/location enrichment.
182+
if sbom_data:
183+
components_list.extend(self._get_root_component_with_subs(sbom_data))
184+
for sbom_component in sbom_data.get("components", []):
185+
components_list.extend(self._get_sbom_component_with_subs(sbom_component))
186+
178187
for component in components_list:
179188
components_dict[component.bom_ref] = component
180189

backend/unittests/import_observations/parsers/cyclone_dx/test_parser.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import base64
2+
import json
13
from os import path
24
from unittest import TestCase
5+
from unittest.mock import MagicMock, patch
36

47
from application.core.models import Product
58
from application.core.types import Severity
@@ -8,6 +11,41 @@
811

912

1013
class TestCycloneDXParser(TestCase):
14+
def test_grype_observations_kept_when_sbom_attestation_resolves(self):
15+
# The scan report (Grype) and the attested SBOM (Trivy) use different bom-refs for the same
16+
# package. When the cosign attestation lookup succeeds, the SBOM components must be *merged*
17+
# with the scan components, not replace them. Otherwise the vulnerabilities reference refs
18+
# that only exist in the scan report, no component is found, and every observation is
19+
# silently dropped.
20+
attested_sbom = {
21+
"bomFormat": "CycloneDX",
22+
"metadata": {"component": {"bom-ref": "sbom-only-ref", "type": "container", "name": "x"}},
23+
"components": [
24+
{
25+
"bom-ref": "sbom-only-ref",
26+
"type": "library",
27+
"name": "sbom-only-package",
28+
"version": "1.0.0",
29+
"purl": "pkg:generic/sbom-only-package@1.0.0",
30+
}
31+
],
32+
"dependencies": [],
33+
}
34+
payload = base64.b64encode(json.dumps({"predicate": attested_sbom}).encode()).decode()
35+
cosign_result = MagicMock(returncode=0, stdout=json.dumps({"payload": payload}).encode())
36+
37+
with open(path.dirname(__file__) + "/files/grype.json") as testfile:
38+
parser, parser_instance, data = detect_parser(testfile)
39+
40+
with patch(
41+
"application.import_observations.parsers.cyclone_dx.parser.subprocess.run",
42+
return_value=cosign_result,
43+
):
44+
observations, scanner = parser_instance.get_observations(data, Product(name="product"), None)
45+
46+
self.assertEqual("grype / 0.59.1", scanner)
47+
self.assertEqual(8, len(observations))
48+
1149
def test_grype_no_bom_link(self):
1250
with open(path.dirname(__file__) + "/files/grype.json") as testfile:
1351
parser, parser_instance, data = detect_parser(testfile)

0 commit comments

Comments
 (0)