diff --git a/components/collector/.vulture_ignore_list.py b/components/collector/.vulture_ignore_list.py index 77c799c539..3049e8cced 100644 --- a/components/collector/.vulture_ignore_list.py +++ b/components/collector/.vulture_ignore_list.py @@ -118,7 +118,6 @@ PipDependencies # unused class (src/source_collectors/pip/dependencies.py:10) PyupioSafetySecurityWarnings # unused class (src/source_collectors/pyupio_safety/security_warnings.py:12) QualityTimeMetrics # unused class (src/source_collectors/quality_time/metrics.py:18) -QualityTimeMissingMetrics # unused class (src/source_collectors/quality_time/missing_metrics.py:11) QualityTimeSourceUpToDateness # unused class (src/source_collectors/quality_time/source_up_to_dateness.py:12) QualityTimeSourceVersion # unused class (src/source_collectors/quality_time/source_version.py:9) RobotFrameworkSourceUpToDateness # unused class (src/source_collectors/robot_framework/source_up_to_dateness.py:13) @@ -153,7 +152,9 @@ References # unused variable (src/source_collectors/trivy/security_warnings.py:26) Target # unused variable (src/source_collectors/trivy/security_warnings.py:32) Vulnerabilities # unused variable (src/source_collectors/trivy/security_warnings.py:33) -TrivyJSONSecurityWarnings # unused class (src/source_collectors/trivy/security_warnings.py:39) +SchemaVersion # unused variable (src/source_collectors/trivy/security_warnings.py:42) +Results # unused variable (src/source_collectors/trivy/security_warnings.py:43) +TrivyJSONSecurityWarnings # unused class (src/source_collectors/trivy/security_warnings.py:49) totalCount # unused variable (tests/source_collectors/github/test_merge_requests.py:16) baseRefName # unused variable (tests/source_collectors/github/test_merge_requests.py:24) createdAt # unused variable (tests/source_collectors/github/test_merge_requests.py:27) diff --git a/components/collector/src/source_collectors/trivy/security_warnings.py b/components/collector/src/source_collectors/trivy/security_warnings.py index 458c13d93b..4ed78be5b4 100644 --- a/components/collector/src/source_collectors/trivy/security_warnings.py +++ b/components/collector/src/source_collectors/trivy/security_warnings.py @@ -26,14 +26,31 @@ class TrivyJSONVulnerability(TypedDict): References: list[str] -class TrivyJSONDependencyRepository(TypedDict): +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 -TrivyJSON = list[TrivyJSONDependencyRepository] +# Trivy JSON reports come in two different forms, following schema version 1 or schema version 2. +# Schema version 1 is not explicitly documented as a schema. The Trivy docs only give an example report. +# 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] + + +class TrivyJSONSchemaVersion2(TypedDict): + """Trivy JSON conform schema version 2.""" + + SchemaVersion: int + Results: list[TrivyJSONResult] + + +TrivyJSON = TriviJSONSchemaVersion1 | TrivyJSONSchemaVersion2 class TrivyJSONSecurityWarnings(SecurityWarningsSourceCollector, JSONFileSourceCollector): @@ -46,9 +63,11 @@ class TrivyJSONSecurityWarnings(SecurityWarningsSourceCollector, JSONFileSourceC def _parse_json(self, json: JSON, filename: str) -> Entities: """Override to parse the vulnerabilities from the Trivy JSON.""" entities = Entities() - for dependency_repository in cast(TrivyJSON, json): - target = dependency_repository["Target"] - for vulnerability in dependency_repository.get("Vulnerabilities") or []: + trivy_json = cast(TrivyJSON, json) + results = trivy_json["Results"] if isinstance(trivy_json, dict) else trivy_json + for result in results: + target = result["Target"] + for vulnerability in result.get("Vulnerabilities") or []: vulnerability_id = vulnerability["VulnerabilityID"] package_name = vulnerability["PkgName"] entities.append( 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 cd3f96bf67..26f25988dd 100644 --- a/components/collector/tests/source_collectors/trivy/test_security_warnings.py +++ b/components/collector/tests/source_collectors/trivy/test_security_warnings.py @@ -1,9 +1,5 @@ """Unit tests for the Trivy JSON security warnings collector.""" -from typing import ClassVar - -from source_collectors.trivy.security_warnings import TrivyJSON - from tests.source_collectors.source_collector_test_case import SourceCollectorTestCase @@ -12,74 +8,88 @@ class TrivyJSONSecurityWarningsTest(SourceCollectorTestCase): SOURCE_TYPE = "trivy_json" METRIC_TYPE = "security_warnings" - VULNERABILITIES_JSON: ClassVar[TrivyJSON] = [ - { - "Target": "php-app/composer.lock", - "Vulnerabilities": None, - }, - { - "Target": "trivy-ci-test (alpine 3.7.1)", - "Vulnerabilities": [ - { - "VulnerabilityID": "CVE-2018-16840", - "PkgName": "curl", - "InstalledVersion": "7.61.0-r0", - "FixedVersion": "7.61.1-r1", - "Title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', - "Description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", - "Severity": "HIGH", - "References": [ - "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", - ], - }, - { - "VulnerabilityID": "CVE-2019-3822", - "PkgName": "curl", - "InstalledVersion": "7.61.1-r0", - "FixedVersion": "7.61.2-r2", - "Title": "curl: NTLMv2 type-3 header stack buffer overflow", - "Description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", - "Severity": "MEDIUM", - "References": [ - "https://curl.haxx.se/docs/CVE-2019-3822.html", - "https://lists.apache.org/thread.html", - ], - }, - ], - }, - ] - EXPECTED_ENTITIES: ClassVar[list[dict[str, str]]] = [ - { - "key": "CVE-2018-16840@curl@trivy-ci-test (alpine 3_7_1)", - "vulnerability_id": "CVE-2018-16840", - "title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', - "description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", - "level": "HIGH", - "package_name": "curl", - "installed_version": "7.61.0-r0", - "fixed_version": "7.61.1-r1", - "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", - }, - { - "key": "CVE-2019-3822@curl@trivy-ci-test (alpine 3_7_1)", - "vulnerability_id": "CVE-2019-3822", - "title": "curl: NTLMv2 type-3 header stack buffer overflow", - "description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", - "level": "MEDIUM", - "package_name": "curl", - "installed_version": "7.61.1-r0", - "fixed_version": "7.61.2-r2", - "url": "https://curl.haxx.se/docs/CVE-2019-3822.html", - }, - ] + SCHEMA_VERSIONS = (1, 2) + + def vulnerabilities_json(self, schema_version: int = 1): + """Return the Trivy Vulnerabilities JSON.""" + results = [ + { + "Target": "php-app/composer.lock", + "Vulnerabilities": None, + }, + { + "Target": "trivy-ci-test (alpine 3.7.1)", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2018-16840", + "PkgName": "curl", + "InstalledVersion": "7.61.0-r0", + "FixedVersion": "7.61.1-r1", + "Title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', + "Description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", + "Severity": "HIGH", + "References": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", + ], + }, + { + "VulnerabilityID": "CVE-2019-3822", + "PkgName": "curl", + "InstalledVersion": "7.61.1-r0", + "FixedVersion": "7.61.2-r2", + "Title": "curl: NTLMv2 type-3 header stack buffer overflow", + "Description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", + "Severity": "MEDIUM", + "References": [ + "https://curl.haxx.se/docs/CVE-2019-3822.html", + "https://lists.apache.org/thread.html", + ], + }, + ], + }, + ] + if schema_version == 1: + return results + return {"SchemaVersion": 2, "Results": results} + + def expected_entities(self): + """Return the expected entities.""" + return [ + { + "key": "CVE-2018-16840@curl@trivy-ci-test (alpine 3_7_1)", + "vulnerability_id": "CVE-2018-16840", + "title": 'curl: Use-after-free when closing "easy" handle in Curl_close()', + "description": "A heap use-after-free flaw was found in curl versions from 7.59.0 through ...", + "level": "HIGH", + "package_name": "curl", + "installed_version": "7.61.0-r0", + "fixed_version": "7.61.1-r1", + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-16840", + }, + { + "key": "CVE-2019-3822@curl@trivy-ci-test (alpine 3_7_1)", + "vulnerability_id": "CVE-2019-3822", + "title": "curl: NTLMv2 type-3 header stack buffer overflow", + "description": "libcurl versions from 7.36.0 to before 7.64.0 are vulnerable to ...", + "level": "MEDIUM", + "package_name": "curl", + "installed_version": "7.61.1-r0", + "fixed_version": "7.61.2-r2", + "url": "https://curl.haxx.se/docs/CVE-2019-3822.html", + }, + ] async def test_warnings(self): """Test the number of security warnings.""" - response = await self.collect(get_request_json_return_value=self.VULNERABILITIES_JSON) - self.assert_measurement(response, value="2", entities=self.EXPECTED_ENTITIES) + 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()) async def test_warning_levels(self): """Test the number of security warnings when specifying a level.""" self.set_source_parameter("levels", ["high", "critical"]) - response = await self.collect(get_request_json_return_value=self.VULNERABILITIES_JSON) - self.assert_measurement(response, value="1", entities=[self.EXPECTED_ENTITIES[0]]) + 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="1", entities=[self.expected_entities()[0]]) diff --git a/docs/src/changelog.md b/docs/src/changelog.md index a7cf422c36..70c52903bf 100644 --- a/docs/src/changelog.md +++ b/docs/src/changelog.md @@ -27,6 +27,7 @@ If your currently installed *Quality-time* version is not v5.15.0, please first - Allow for configuring Jenkins as source for the metric 'CI-pipeline duration' (GitLab CI was already supported, Azure DevOps will follow later). Partially implements [#6423](https://github.com/ICTU/quality-time/issues/6423). - Show the number of ignored measurement entities (entities marked as "False positive, "Won't fix" or "Will be fixed") in the measurement value popup. Closes [#7626](https://github.com/ICTU/quality-time/issues/7626). - Add GitHub as possible source for the 'merge requests' metric. Patch contributed by Tobias Termeczky (the/experts). Closes [#9323](https://github.com/ICTU/quality-time/issues/9323). +- Support schema version 2 of the Trivy JSON format. Closes [#9711](https://github.com/ICTU/quality-time/issues/9711). ### Changed