Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/content/supported_tools/parsers/file/anchore_grype.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
24 changes: 24 additions & 0 deletions dojo/tools/anchore_grype/parser.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import logging
from datetime import datetime

from cvss import parser as cvss_parser
from cvss.cvss3 import CVSS3
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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":
Expand Down Expand Up @@ -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:
Expand Down
148 changes: 148 additions & 0 deletions unittests/scans/anchore_grype/check_kev_date.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
24 changes: 24 additions & 0 deletions unittests/tools/test_anchore_grype_parser.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Loading