Skip to content

Commit d4b6163

Browse files
committed
OpenReports: Add Dedup and non-CVE support
1 parent 6be57e5 commit d4b6163

File tree

5 files changed

+90
-19
lines changed

5 files changed

+90
-19
lines changed

dojo/settings/settings.dist.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1395,6 +1395,7 @@ def saml2_attrib_map_format(din):
13951395
"Cycognito Scan": ["title", "severity"],
13961396
"OpenVAS Parser v2": ["title", "severity", "vuln_id_from_tool", "endpoints"],
13971397
"Snyk Issue API Scan": ["vuln_id_from_tool", "file_path"],
1398+
"OpenReports": ["vulnerability_ids", "component_name", "component_version", "severity"],
13981399
}
13991400

14001401
# Override the hardcoded settings here via the env var
@@ -1467,6 +1468,7 @@ def saml2_attrib_map_format(din):
14671468
"AWS Inspector2 Scan": True,
14681469
"Cyberwatch scan (Galeax)": True,
14691470
"OpenVAS Parser v2": True,
1471+
"OpenReports": True,
14701472
}
14711473

14721474
# List of fields that are known to be usable in hash_code computation)
@@ -1657,6 +1659,7 @@ def saml2_attrib_map_format(din):
16571659
"Cyberwatch scan (Galeax)": DEDUPE_ALGO_HASH_CODE,
16581660
"OpenVAS Parser v2": DEDUPE_ALGO_HASH_CODE,
16591661
"Snyk Issue API Scan": DEDUPE_ALGO_HASH_CODE,
1662+
"OpenReports": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL,
16601663
}
16611664

16621665
# Override the hardcoded settings here via the env var

dojo/tools/openreports/parser.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,13 @@
3131

3232
class OpenreportsParser:
3333
def get_scan_types(self):
34-
return ["OpenReports Scan"]
34+
return ["OpenReports"]
3535

3636
def get_label_for_scan_types(self, scan_type):
37-
return "OpenReports Scan"
37+
return "OpenReports"
3838

3939
def get_description_for_scan_types(self, scan_type):
40-
return "Import OpenReports JSON scan report."
40+
return "Import OpenReports JSON report."
4141

4242
def get_findings(self, scan_file, test):
4343
scan_data = scan_file.read()
@@ -79,6 +79,7 @@ def _parse_report(self, test, report):
7979
metadata = report.get("metadata", {})
8080
report_name = metadata.get("name", "")
8181
namespace = metadata.get("namespace", "")
82+
report_uid = metadata.get("uid", "")
8283

8384
# Extract scope information
8485
scope = report.get("scope", {})
@@ -95,13 +96,13 @@ def _parse_report(self, test, report):
9596
if not isinstance(result, dict):
9697
continue
9798

98-
finding = self._create_finding_from_result(test, result, service_name, report_name)
99+
finding = self._create_finding_from_result(test, result, service_name, report_name, report_uid)
99100
if finding:
100101
findings.append(finding)
101102

102103
return findings
103104

104-
def _create_finding_from_result(self, test, result, service_name, report_name):
105+
def _create_finding_from_result(self, test, result, service_name, report_name, report_uid):
105106
try:
106107
# Extract basic fields
107108
message = result.get("message", "")
@@ -175,8 +176,15 @@ def _create_finding_from_result(self, test, result, service_name, report_name):
175176
# Add vulnerability ID if it's a CVE
176177
if policy.startswith("CVE-"):
177178
finding.unsaved_vulnerability_ids = [policy]
178-
else:
179-
return finding
179+
180+
# Create unique_id_from_tool for deduplication
181+
# Use the report UID if available (from metadata.uid), otherwise fall back to service_name
182+
# Format: report_uid:policy:package_name (preferred) or policy:package_name:service_name (fallback)
183+
# This uses the stable UID from the OpenReports API that won't change on reimport
184+
unique_id_components = [report_uid, policy, pkg_name] if report_uid else [policy, pkg_name, service_name]
185+
finding.unique_id_from_tool = ":".join(unique_id_components)
186+
187+
return finding # noqa: TRY300 - This is intentional
180188

181189
except KeyError as exc:
182190
logger.warning("Failed to parse OpenReports result due to missing key: %r", exc)

unittests/scans/openreports/openreports_list_format.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,20 @@
9191
"result": "fail",
9292
"severity": "high",
9393
"source": "image-scanner"
94+
},
95+
{
96+
"category": "configuration scan",
97+
"message": "Container running as root user",
98+
"policy": "SECURITY-001",
99+
"properties": {
100+
"fixedVersion": "",
101+
"installedVersion": "latest",
102+
"pkgName": "container-config",
103+
"primaryURL": "https://security.example.com/policies/SECURITY-001"
104+
},
105+
"result": "warn",
106+
"severity": "medium",
107+
"source": "policy-scanner"
94108
}
95109
],
96110
"scope": {
@@ -102,7 +116,7 @@
102116
"summary": {
103117
"fail": 1,
104118
"skip": 0,
105-
"warn": 0
119+
"warn": 1
106120
}
107121
}
108122
],

unittests/scans/openreports/openreports_single_report.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,20 @@
5050
"result": "fail",
5151
"severity": "high",
5252
"source": "image-scanner"
53+
},
54+
{
55+
"category": "compliance check",
56+
"message": "Missing security headers in HTTP response",
57+
"policy": "CIS-BENCH-001",
58+
"properties": {
59+
"fixedVersion": "Configure proper security headers",
60+
"installedVersion": "N/A",
61+
"pkgName": "web-server",
62+
"primaryURL": "https://www.cisecurity.org/benchmark/docker"
63+
},
64+
"result": "fail",
65+
"severity": "low",
66+
"source": "compliance-scanner"
5367
}
5468
],
5569
"scope": {
@@ -59,7 +73,7 @@
5973
"uid": "d0cbd625-d495-415e-bf39-b4e3c4f4366e"
6074
},
6175
"summary": {
62-
"fail": 1,
76+
"fail": 2,
6377
"skip": 0,
6478
"warn": 1
6579
}

unittests/tools/test_openreports_parser.py

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ def sample_path(file_name):
88

99

1010
class TestOpenreportsParser(DojoTestCase):
11-
1211
def test_no_results(self):
1312
with sample_path("openreports_no_results.json").open(encoding="utf-8") as test_file:
1413
parser = OpenreportsParser()
@@ -19,7 +18,7 @@ def test_single_report(self):
1918
with sample_path("openreports_single_report.json").open(encoding="utf-8") as test_file:
2019
parser = OpenreportsParser()
2120
findings = parser.get_findings(test_file, Test())
22-
self.assertEqual(len(findings), 2)
21+
self.assertEqual(len(findings), 3)
2322

2423
# Test first finding (warn/low severity)
2524
finding1 = findings[0]
@@ -35,6 +34,9 @@ def test_single_report(self):
3534
self.assertTrue(finding1.fix_available)
3635
self.assertEqual(1, len(finding1.unsaved_vulnerability_ids))
3736
self.assertEqual("CVE-2025-9232", finding1.unsaved_vulnerability_ids[0])
37+
self.assertEqual(
38+
"b1fcca57-2efd-44d3-89e9-949e29b61936:CVE-2025-9232:libcrypto3", finding1.unique_id_from_tool
39+
)
3840
self.assertIn("vulnerability scan", finding1.tags)
3941
self.assertIn("image-scanner", finding1.tags)
4042
self.assertIn("Deployment", finding1.tags)
@@ -53,31 +55,61 @@ def test_single_report(self):
5355
self.assertTrue(finding2.fix_available)
5456
self.assertEqual(1, len(finding2.unsaved_vulnerability_ids))
5557
self.assertEqual("CVE-2025-47907", finding2.unsaved_vulnerability_ids[0])
58+
self.assertEqual("b1fcca57-2efd-44d3-89e9-949e29b61936:CVE-2025-47907:stdlib", finding2.unique_id_from_tool)
59+
60+
# Test third finding (non-CVE policy, fail/low severity)
61+
finding3 = findings[2]
62+
self.assertEqual("CIS-BENCH-001: Missing security headers in HTTP response", finding3.title)
63+
self.assertEqual("Low", finding3.severity)
64+
self.assertEqual("web-server", finding3.component_name)
65+
self.assertEqual("N/A", finding3.component_version)
66+
self.assertEqual("Upgrade to version: Configure proper security headers", finding3.mitigation)
67+
self.assertEqual("https://www.cisecurity.org/benchmark/docker", finding3.references)
68+
self.assertEqual("test/Deployment/test-app", finding3.service)
69+
self.assertTrue(finding3.active)
70+
self.assertTrue(finding3.verified)
71+
self.assertTrue(finding3.fix_available)
72+
# Non-CVE policies should not have vulnerability IDs
73+
self.assertIsNone(finding3.unsaved_vulnerability_ids)
74+
self.assertEqual(
75+
"b1fcca57-2efd-44d3-89e9-949e29b61936:CIS-BENCH-001:web-server", finding3.unique_id_from_tool
76+
)
77+
self.assertIn("compliance check", finding3.tags)
78+
self.assertIn("compliance-scanner", finding3.tags)
79+
self.assertIn("Deployment", finding3.tags)
5680

5781
def test_list_format(self):
5882
with sample_path("openreports_list_format.json").open(encoding="utf-8") as test_file:
5983
parser = OpenreportsParser()
6084
findings = parser.get_findings(test_file, Test())
61-
self.assertEqual(len(findings), 2)
85+
self.assertEqual(len(findings), 3)
6286

6387
# Verify findings from different reports have different services
6488
services = {finding.service for finding in findings}
6589
self.assertEqual(len(services), 2)
6690
self.assertIn("test/Deployment/app1", services)
6791
self.assertIn("test/Deployment/app2", services)
6892

69-
# Verify CVE IDs
70-
cve_ids = [finding.unsaved_vulnerability_ids[0] for finding in findings]
93+
# Verify CVE IDs - only findings with CVE policies should have vulnerability IDs
94+
cve_findings = [finding for finding in findings if finding.unsaved_vulnerability_ids]
95+
self.assertEqual(len(cve_findings), 2)
96+
cve_ids = [finding.unsaved_vulnerability_ids[0] for finding in cve_findings]
7197
self.assertIn("CVE-2025-9232", cve_ids)
7298
self.assertIn("CVE-2025-47907", cve_ids)
7399

100+
# Verify there's at least one non-CVE finding
101+
non_cve_findings = [finding for finding in findings if not finding.unsaved_vulnerability_ids]
102+
self.assertEqual(len(non_cve_findings), 1)
103+
non_cve_finding = non_cve_findings[0]
104+
self.assertEqual("SECURITY-001: Container running as root user", non_cve_finding.title)
105+
74106
def test_parser_metadata(self):
75107
parser = OpenreportsParser()
76108
scan_types = parser.get_scan_types()
77-
self.assertEqual(["OpenReports Scan"], scan_types)
109+
self.assertEqual(["OpenReports"], scan_types)
78110

79-
label = parser.get_label_for_scan_types("OpenReports Scan")
80-
self.assertEqual("OpenReports Scan", label)
111+
label = parser.get_label_for_scan_types("OpenReports")
112+
self.assertEqual("OpenReports", label)
81113

82-
description = parser.get_description_for_scan_types("OpenReports Scan")
83-
self.assertEqual("Import OpenReports JSON scan report.", description)
114+
description = parser.get_description_for_scan_types("OpenReports")
115+
self.assertEqual("Import OpenReports JSON report.", description)

0 commit comments

Comments
 (0)