diff --git a/docs/content/supported_tools/parsers/file/anchore_grype.md b/docs/content/supported_tools/parsers/file/anchore_grype.md index 5e80da5293a..84e169d7920 100644 --- a/docs/content/supported_tools/parsers/file/anchore_grype.md +++ b/docs/content/supported_tools/parsers/file/anchore_grype.md @@ -233,3 +233,5 @@ A typical case is a package installed at multiple paths in a container image (e. | `fix_version` | `vulnerability.fix.versions[0]` (or comma-joined if multiple) | | `cvssv3` | `vulnerability.cvss` or `relatedVulnerabilities[0].cvss` | | `epss_score` / `epss_percentile` | `vulnerability.epss` or `relatedVulnerabilities[0].epss` | +| `kev_date` | `vulnerability.knownExploited[].dateAdded` (matched by CVE) | +| `known_exploited` | `true` if `kev_date` is present, `false` otherwise | diff --git a/dojo/tools/anchore_grype/parser.py b/dojo/tools/anchore_grype/parser.py index 0eba674114d..2a6435bc7b1 100644 --- a/dojo/tools/anchore_grype/parser.py +++ b/dojo/tools/anchore_grype/parser.py @@ -1,5 +1,6 @@ import json import logging +from datetime import datetime from cvss import parser as cvss_parser from cvss.cvss3 import CVSS3 @@ -59,6 +60,8 @@ def get_findings(self, file, test): vuln_fix_versions = vulnerability["fix"].get("versions") vuln_cvss = vulnerability.get("cvss") vuln_epss = vulnerability.get("epss") + vuln_known_exploited = vulnerability.get("knownExploited") + finding_kev_date = None rel_datasource = None rel_urls = None @@ -176,6 +179,9 @@ def get_findings(self, file, test): if finding_epss_score is None and rel_vuln_id: finding_epss_score, finding_epss_percentile = self.get_epss_values(vuln_id, vuln_epss) + if vuln_known_exploited and vulnerability_ids: + finding_kev_date = self.get_kev_date(vuln_id, vuln_known_exploited, vulnerability_ids) + if self.mode == "detailed": dupe_key = f"{vuln_id}|{artifact_name}|{artifact_version}|{file_path}" else: @@ -202,6 +208,8 @@ def get_findings(self, file, test): file_path=file_path, fix_available=fix_available, fix_version=fix_version, + kev_date=finding_kev_date, + known_exploited=bool(finding_kev_date), ) if self.mode == "detailed": @@ -253,6 +261,22 @@ def get_epss_values(self, vuln_id, epss_list): logger.debug("epss not found for vuln_id: %s in epss_list: %s", vuln_id, epss_list) return None, None + def get_kev_date(self, vuln_id, known_exploited_list, vulnerability_ids): + if not isinstance(known_exploited_list, list): + return None + + for known_exploited_data in known_exploited_list: + known_exploited_cve = known_exploited_data.get("cve") + if known_exploited_cve in vulnerability_ids: + kev_date_str = known_exploited_data.get("dateAdded") + if kev_date_str: + try: + return datetime.strptime(kev_date_str, "%Y-%m-%d").date() + except (TypeError, ValueError): + logger.debug("kev_date_str is not a valid date: %s", kev_date_str) + logger.debug("kev_date not found for vuln_id: %s", vuln_id) + return None + def get_vulnerability_ids(self, vuln_id, related_vulnerabilities): vulnerability_ids = [] if vuln_id: diff --git a/unittests/scans/anchore_grype/check_kev_date.json b/unittests/scans/anchore_grype/check_kev_date.json new file mode 100644 index 00000000000..a6ee5d2df67 --- /dev/null +++ b/unittests/scans/anchore_grype/check_kev_date.json @@ -0,0 +1,148 @@ +{ + "matches": [ + { + "vulnerability": { + "id": "CVE-2021-44228", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228", + "namespace": "nvd", + "severity": "Critical", + "urls": [ + "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + ], + "description": "Apache Log4j2 JNDI features do not protect against attacker controlled LDAP and other JNDI related endpoints.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H", + "metrics": { + "baseScore": 10.0, + "exploitabilityScore": 3.9, + "impactScore": 6.0 + }, + "vendorMetadata": {} + } + ], + "fix": { + "versions": ["2.15.0"], + "state": "fixed" + }, + "advisories": [], + "epss": [ + { + "cve": "CVE-2021-44228", + "epss": "0.97565", + "percentile": "1.00000" + } + ], + "knownExploited": [ + { + "cve": "CVE-2021-44228", + "vendorProject": "Apache", + "product": "Log4j", + "vulnerabilityName": "Apache Log4j2 Remote Code Execution Vulnerability", + "dateAdded": "2021-12-10", + "shortDescription": "Apache Log4j2 contains a vulnerability where JNDI features do not protect against attacker-controlled LDAP and other JNDI related endpoints.", + "requiredAction": "Apply updates per vendor instructions.", + "dueDate": "2021-12-24", + "notes": "" + } + ] + }, + "relatedVulnerabilities": [], + "matchDetails": [ + { + "matcher": "java-matcher", + "searchedBy": { + "namespace": "nvd", + "package": { + "name": "log4j-core", + "version": "2.14.1" + } + }, + "found": { + "versionConstraint": "< 2.15.0" + } + } + ], + "artifact": { + "name": "log4j-core", + "version": "2.14.1", + "type": "java-archive", + "locations": [ + { + "path": "/app/lib/log4j-core-2.14.1.jar" + } + ], + "purl": "pkg:maven/org.apache.logging.log4j/log4j-core@2.14.1" + } + }, + { + "vulnerability": { + "id": "CVE-2021-45046", + "dataSource": "https://nvd.nist.gov/vuln/detail/CVE-2021-45046", + "namespace": "nvd", + "severity": "Critical", + "urls": [ + "https://nvd.nist.gov/vuln/detail/CVE-2021-45046" + ], + "description": "It was found that the fix to address CVE-2021-44228 in Apache Log4j 2.15.0 was incomplete in certain non-default configurations.", + "cvss": [ + { + "version": "3.1", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H", + "metrics": { + "baseScore": 9.0, + "exploitabilityScore": 2.2, + "impactScore": 6.0 + }, + "vendorMetadata": {} + } + ], + "fix": { + "versions": ["2.16.0"], + "state": "fixed" + }, + "advisories": [] + }, + "relatedVulnerabilities": [], + "matchDetails": [ + { + "matcher": "java-matcher", + "searchedBy": { + "namespace": "nvd", + "package": { + "name": "log4j-core", + "version": "2.14.1" + } + }, + "found": { + "versionConstraint": "< 2.16.0" + } + } + ], + "artifact": { + "name": "log4j-api", + "version": "2.14.1", + "type": "java-archive", + "locations": [ + { + "path": "/app/lib/log4j-api-2.14.1.jar" + } + ], + "purl": "pkg:maven/org.apache.logging.log4j/log4j-api@2.14.1" + } + } + ], + "source": { + "type": "image", + "target": { + "userInput": "test-image:latest", + "imageID": "sha256:abc123" + } + }, + "distro": {}, + "descriptor": { + "name": "grype", + "version": "0.70.0" + } +} diff --git a/unittests/tools/test_anchore_grype_parser.py b/unittests/tools/test_anchore_grype_parser.py index 86a99847eae..883e704eeac 100644 --- a/unittests/tools/test_anchore_grype_parser.py +++ b/unittests/tools/test_anchore_grype_parser.py @@ -1,3 +1,5 @@ +from datetime import date + from dojo.models import Finding, Test from dojo.tools.anchore_grype.parser import AnchoreGrypeParser from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path @@ -366,3 +368,25 @@ def test_detailed_mode_separates_same_cve_different_paths(self): file_paths = {f.file_path for f in findings} self.assertIn("/usr/lib/x86_64-linux-gnu/libc.so.6", file_paths) self.assertIn("/lib/x86_64-linux-gnu/libc.so.6", file_paths) + + def test_grype_kev_date_parsing(self): + """ + Test that KEV (Known Exploited Vulnerabilities) dates are correctly parsed from Grype reports. + KEV dates should be converted from string format (YYYY-MM-DD) to Python date objects. + The file contains two findings: CVE-2021-44228 (has knownExploited) and CVE-2021-45046 (no knownExploited). + """ + with (get_unit_tests_scans_path("anchore_grype") / "check_kev_date.json").open(encoding="utf-8") as testfile: + parser = AnchoreGrypeParser() + findings = parser.get_findings(testfile, Test()) + + self.assertEqual(2, len(findings)) + + finding_with_kev = next(f for f in findings if f.vuln_id_from_tool == "CVE-2021-44228") + self.assertIsNotNone(finding_with_kev.kev_date) + self.assertIsInstance(finding_with_kev.kev_date, date) + self.assertEqual(date(2021, 12, 10), finding_with_kev.kev_date) + self.assertTrue(finding_with_kev.known_exploited) + + finding_without_kev = next(f for f in findings if f.vuln_id_from_tool == "CVE-2021-45046") + self.assertIsNone(finding_without_kev.kev_date) + self.assertFalse(finding_without_kev.known_exploited)