From e0dfd4645b27f9bcb04dcd3aebf52115f6e38d1d Mon Sep 17 00:00:00 2001 From: Frank Niessink Date: Mon, 20 Jan 2025 13:56:47 +0100 Subject: [PATCH] When measuring security warnings with Trivy JSON as source, be prepared for optional fields not being present. Fixes #10672. --- .../collector/src/source_collectors/trivy/base.py | 13 ++++++------- .../source_collectors/trivy/security_warnings.py | 8 +++++--- .../tests/source_collectors/trivy/base.py | 6 ++++++ .../trivy/test_security_warnings.py | 15 +++++++++++++-- docs/src/changelog.md | 1 + 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/components/collector/src/source_collectors/trivy/base.py b/components/collector/src/source_collectors/trivy/base.py index b3b4eea8b5..e9a29a9398 100644 --- a/components/collector/src/source_collectors/trivy/base.py +++ b/components/collector/src/source_collectors/trivy/base.py @@ -1,6 +1,6 @@ """Base classes for Trivy JSON collectors.""" -from typing import TypedDict +from typing import NotRequired, TypedDict # The types below are based on https://aquasecurity.github.io/trivy/v0.45/docs/configuration/reporting/#json. # That documentation says: "VulnerabilityID, PkgName, InstalledVersion, and Severity in Vulnerabilities are always @@ -13,20 +13,20 @@ class TrivyJSONVulnerability(TypedDict): """Trivy JSON for one vulnerability.""" VulnerabilityID: str - Title: str - Description: str + Title: NotRequired[str] + Description: NotRequired[str] Severity: str PkgName: str InstalledVersion: str - FixedVersion: str - References: list[str] + FixedVersion: NotRequired[str] + References: NotRequired[list[str]] class TrivyJSONResult(TypedDict): """Trivy JSON for one dependency repository.""" Target: str - Vulnerabilities: list[TrivyJSONVulnerability] | None # The examples in the Trivy docs show this key can be null + Vulnerabilities: NotRequired[list[TrivyJSONVulnerability]] # Examples in the Trivy docs show this key can be null # Trivy JSON reports come in two different forms, following schema version 1 or schema version 2. @@ -34,7 +34,6 @@ class TrivyJSONResult(TypedDict): # See https://aquasecurity.github.io/trivy/v0.55/docs/configuration/reporting/#json. # Schema version 2 is not explicitly documented as a schema either. The only thing available seems to be a GitHub # discussion: https://github.com/aquasecurity/trivy/discussions/1050. -# Issue to improve the documentation: https://github.com/aquasecurity/trivy/discussions/7552 TriviJSONSchemaVersion1 = list[TrivyJSONResult] diff --git a/components/collector/src/source_collectors/trivy/security_warnings.py b/components/collector/src/source_collectors/trivy/security_warnings.py index c06e566888..6ae6d7bd84 100644 --- a/components/collector/src/source_collectors/trivy/security_warnings.py +++ b/components/collector/src/source_collectors/trivy/security_warnings.py @@ -27,17 +27,19 @@ def _parse_json(self, json: JSON, filename: str) -> Entities: for vulnerability in result.get("Vulnerabilities") or []: vulnerability_id = vulnerability["VulnerabilityID"] package_name = vulnerability["PkgName"] + references = vulnerability.get("References", []) + url = references[0] if references else "" # Assume the 1st link is at least as relevant as the others entities.append( Entity( key=f"{vulnerability_id}@{package_name}@{target}", vulnerability_id=vulnerability_id, - title=vulnerability["Title"], - description=vulnerability["Description"], + title=vulnerability.get("Title", vulnerability_id), + description=vulnerability.get("Description", ""), level=vulnerability["Severity"], package_name=package_name, installed_version=vulnerability["InstalledVersion"], fixed_version=vulnerability.get("FixedVersion", ""), - url=vulnerability["References"][0], # Assume the 1st link is at least as relevant as the others + url=url, ), ) return entities diff --git a/components/collector/tests/source_collectors/trivy/base.py b/components/collector/tests/source_collectors/trivy/base.py index f77a5a23de..b82076d426 100644 --- a/components/collector/tests/source_collectors/trivy/base.py +++ b/components/collector/tests/source_collectors/trivy/base.py @@ -53,6 +53,12 @@ def vulnerabilities_json(self, schema_version: int = 2): "Severity": "LOW", "References": ["https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5432"], }, + { + "VulnerabilityID": "CVE-2025-6298", + "PkgName": "This vulnerability has no optional fields", + "InstalledVersion": "3.4.1", + "Severity": "LOW", + }, ], }, ] diff --git a/components/collector/tests/source_collectors/trivy/test_security_warnings.py b/components/collector/tests/source_collectors/trivy/test_security_warnings.py index 2676a72113..3e5deea62f 100644 --- a/components/collector/tests/source_collectors/trivy/test_security_warnings.py +++ b/components/collector/tests/source_collectors/trivy/test_security_warnings.py @@ -44,6 +44,17 @@ def expected_entities(self): "fixed_version": "", "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5432", }, + { + "key": "CVE-2025-6298@This vulnerability has no optional fields@trivy-ci-test (alpine 3_7_1)", + "vulnerability_id": "CVE-2025-6298", + "title": "CVE-2025-6298", + "description": "", + "package_name": "This vulnerability has no optional fields", + "installed_version": "3.4.1", + "level": "LOW", + "fixed_version": "", + "url": "", + }, ] async def test_warnings(self): @@ -51,7 +62,7 @@ async def test_warnings(self): for schema_version in self.SCHEMA_VERSIONS: with self.subTest(schema_version=schema_version): response = await self.collect(get_request_json_return_value=self.vulnerabilities_json(schema_version)) - self.assert_measurement(response, value="3", entities=self.expected_entities()) + self.assert_measurement(response, value="4", entities=self.expected_entities()) async def test_warning_levels(self): """Test the number of security warnings when specifying a level.""" @@ -75,4 +86,4 @@ async def test_fix_not_available(self): for schema_version in self.SCHEMA_VERSIONS: with self.subTest(schema_version=schema_version): response = await self.collect(get_request_json_return_value=self.vulnerabilities_json(schema_version)) - self.assert_measurement(response, value="2", entities=self.expected_entities()[1:]) + self.assert_measurement(response, value="3", entities=self.expected_entities()[1:]) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index 4f28056654..f3ac21cc5c 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -16,6 +16,7 @@ If your currently installed *Quality-time* version is not the latest version, pl ### Fixed +- When measuring security warnings with Trivy JSON as source, be prepared for optional fields not being present. Fixes [#10672](https://github.com/ICTU/quality-time/issues/10672). - Docker compose has been integrated into Docker as a subcommand for a while, but the developer documentation did not reflect that. Change `docker-compose` to `docker compose` in the documentation. Fixes [#10684](https://github.com/ICTU/quality-time/issues/10684). ### Changed