Skip to content

Commit deb198d

Browse files
🎉 Implement json part for Cloudflare insights parser (#14096)
1 parent a8479e1 commit deb198d

5 files changed

Lines changed: 158 additions & 33 deletions

File tree

docs/content/supported_tools/parsers/file/cloudflare_insights.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: "Cloudflare Insights"
33
toc_hide: true
44
---
55

6-
Import Cloudflare Insights findings using the **CSV export** provided by Cloudflare.
6+
Import Cloudflare Insights findings using the **CSV export** or via api the **JSON output** provided by Cloudflare.
77

88
### Sample Scan Data
99
Sample Cloudflare Insights files can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/cloudflare_insights).
Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import csv
22
import io
3+
import json
34
from urllib.parse import urlparse
45

56
from dojo.models import Endpoint, Finding
@@ -8,19 +9,27 @@
89
class CloudflareInsightsParser:
910

1011
"""
11-
DefectDojo parser for Cloudflare Insights CSV exports.
12+
DefectDojo parser for Cloudflare Insights CSV or JSON exports.
1213
13-
Expected columns:
14+
CSV expected columns:
1415
- severity
1516
- issue_class
16-
- subject (used as Endpoint host; not repeated in description)
17+
- subject
1718
- issue_type
18-
- scan_performed_on (ignored)
1919
- status
20-
- insight (optional)
21-
- detection_method (optional)
22-
- risk (optional)
23-
- recommended_action (used as mitigation if present)
20+
- insight (optional)
21+
- detection_method (optional)
22+
- risk (optional)
23+
- recommended_action (optional)
24+
25+
JSON expected fields:
26+
- severity
27+
- issue_class
28+
- subject
29+
- issue_type
30+
- dismissed (maps to status)
31+
- resolve_text (optional mitigation)
32+
- risk (optional)
2433
"""
2534

2635
def get_scan_types(self):
@@ -30,15 +39,15 @@ def get_label_for_scan_types(self, scan_type):
3039
return scan_type
3140

3241
def get_description_for_scan_types(self, scan_type):
33-
return "Import Cloudflare Insights (CSV export)."
42+
return "Import Cloudflare Insights (CSV or JSON export)."
3443

3544
def _map_severity(self, value):
3645
normalized = value.strip().lower()
3746
mapping = {
3847
"low": "Low",
3948
"moderate": "Medium",
4049
"critical": "Critical",
41-
"high": "High", # optional: Cloudflare occasionally uses this
50+
"high": "High",
4251
}
4352
return mapping.get(normalized, "Info")
4453

@@ -56,25 +65,14 @@ def _extract_host_from_subject(self, subject: str) -> str | None:
5665
host = netloc or s
5766
if ":" in host:
5867
host = host.split(":", 1)[0]
59-
host = host.strip().strip("/").strip()
60-
61-
return host or None
68+
return host.strip().strip("/") or None
6269

6370
def _is_inactive_status(self, status: str) -> bool:
6471
inactive_markers = {"resolved", "mitigated", "closed", "fixed"}
6572
return bool(status) and status.strip().lower() in inactive_markers
6673

67-
def get_findings(self, filename, test):
68-
content = filename.read()
69-
if isinstance(content, bytes):
70-
content = content.decode("utf-8", errors="replace")
71-
72-
reader = csv.DictReader(
73-
io.StringIO(content),
74-
delimiter=",",
75-
quotechar='"',
76-
skipinitialspace=True,
77-
)
74+
def _parse_csv(self, content: str, test):
75+
reader = csv.DictReader(io.StringIO(content), delimiter=",", quotechar='"', skipinitialspace=True)
7876
findings = []
7977
for row in reader:
8078
severity_raw = (row.get("severity") or "").strip()
@@ -86,15 +84,10 @@ def get_findings(self, filename, test):
8684
detection_method = (row.get("detection_method") or "").strip()
8785
risk = (row.get("risk") or "").strip()
8886
recommended_action = (row.get("recommended_action") or "").strip()
87+
8988
mapped_severity = self._map_severity(severity_raw)
90-
if issue_type and subject:
91-
title = f"{issue_type}: {subject}"
92-
elif issue_type:
93-
title = issue_type
94-
elif subject:
95-
title = subject
96-
else:
97-
title = "Cloudflare Insight"
89+
title = f"{issue_type}: {subject}" if issue_type and subject else issue_type or subject or "Cloudflare Insight"
90+
9891
description_parts = []
9992
if issue_class:
10093
description_parts.append(f"**Issue class**: {issue_class}")
@@ -109,6 +102,7 @@ def get_findings(self, filename, test):
109102
if risk:
110103
description_parts.append(f"**Risk**: {risk}")
111104
description = "\n\n".join(description_parts)
105+
112106
finding = Finding(
113107
test=test,
114108
title=title,
@@ -124,5 +118,54 @@ def get_findings(self, filename, test):
124118
if host:
125119
finding.unsaved_endpoints = [Endpoint(host=host, port=None)]
126120
findings.append(finding)
121+
return findings
122+
123+
def _parse_json(self, content: str, test):
124+
data = json.loads(content)
125+
findings = []
126+
for item in data:
127+
severity_raw = (item.get("severity") or "").strip()
128+
issue_class = (item.get("issue_class") or "").strip()
129+
subject = (item.get("subject") or "").strip()
130+
issue_type = (item.get("issue_type") or "").strip()
131+
dismissed = item.get("dismissed", False)
132+
risk = (item.get("risk") or "").strip()
133+
recommended_action = (item.get("resolve_text") or "").strip()
134+
135+
mapped_severity = self._map_severity(severity_raw)
136+
title = f"{issue_type}: {subject}" if issue_type and subject else issue_type or subject or "Cloudflare Insight"
137+
138+
description_parts = []
139+
if issue_class:
140+
description_parts.append(f"**Issue class**: {issue_class}")
141+
if issue_type:
142+
description_parts.append(f"**Issue type**: {issue_type}")
143+
if risk:
144+
description_parts.append(f"**Risk**: {risk}")
145+
description = "\n\n".join(description_parts)
127146

147+
finding = Finding(
148+
test=test,
149+
title=title,
150+
severity=mapped_severity,
151+
description=description,
152+
mitigation=recommended_action,
153+
references="Not provided!",
154+
static_finding=False,
155+
dynamic_finding=True,
156+
)
157+
finding.active = not dismissed
158+
host = self._extract_host_from_subject(subject)
159+
if host:
160+
finding.unsaved_endpoints = [Endpoint(host=host, port=None)]
161+
findings.append(finding)
128162
return findings
163+
164+
def get_findings(self, filename, test):
165+
content = filename.read()
166+
if isinstance(content, bytes):
167+
content = content.decode("utf-8", errors="replace")
168+
content_strip = content.strip()
169+
if content_strip.startswith("["):
170+
return self._parse_json(content_strip, test)
171+
return self._parse_csv(content_strip, test)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
[
2+
{
3+
"id": "asdf-security_txt_not_enabled",
4+
"issue_class": "security_txt_not_enabled",
5+
"issue_type": "configuration_suggestion",
6+
"payload": {
7+
"zone_tag": "asdffwe"
8+
},
9+
"severity": "Low",
10+
"subject": "test.de",
11+
"timestamp": "2024-12-01T05:43:45.712676Z",
12+
"dismissed": false,
13+
"since": "2024-10-03T07:46:55.613682Z",
14+
"resolve_link": "",
15+
"resolve_text": ""
16+
},
17+
{
18+
"id": "fwefwef-security_txt_not_enabled",
19+
"issue_class": "security_txt_not_enabled",
20+
"issue_type": "configuration_suggestion",
21+
"payload": {
22+
"zone_tag": "klklkl"
23+
},
24+
"severity": "Low",
25+
"subject": "nannana.com",
26+
"timestamp": "2024-12-01T05:43:44.175313Z",
27+
"dismissed": false,
28+
"since": "2024-10-03T08:48:44.581718Z",
29+
"resolve_link": "",
30+
"resolve_text": ""
31+
},
32+
{
33+
"id": "yxcv-security_txt_not_enabled",
34+
"issue_class": "security_txt_not_enabled",
35+
"issue_type": "configuration_suggestion",
36+
"payload": {
37+
"zone_tag": "yxcv"
38+
},
39+
"severity": "Low",
40+
"subject": "asdf.de",
41+
"timestamp": "2024-12-01T05:43:44.252529Z",
42+
"dismissed": false,
43+
"since": "2024-10-03T08:48:43.923198Z",
44+
"resolve_link": "",
45+
"resolve_text": ""
46+
}
47+
]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"id": "123-security_txt_not_enabled",
4+
"issue_class": "security_txt_not_enabled",
5+
"issue_type": "configuration_suggestion",
6+
"payload": {
7+
"zone_tag": "12"
8+
},
9+
"severity": "Low",
10+
"subject": "domain.com",
11+
"timestamp": "2024-12-01T05:43:45.712676Z",
12+
"dismissed": false,
13+
"since": "2024-10-03T07:46:55.613682Z",
14+
"resolve_link": "",
15+
"resolve_text": ""
16+
}
17+
]

unittests/tools/test_cloudflare_insights_parser.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,21 @@ def test_cloudflare_insights_parser_with_many_findings(self):
2222
finding = findings[0]
2323
self.assertEqual("Exposed infrastructure: domain1.com", finding.title)
2424
self.assertEqual("Medium", finding.severity)
25+
26+
def test_cloudflare_insights_parser_with_one_finding_json(self):
27+
with (get_unit_tests_scans_path("cloudflare_insights") / "one_finding.json").open(encoding="utf-8") as testfile:
28+
parser = CloudflareInsightsParser()
29+
findings = parser.get_findings(testfile, Test())
30+
self.assertEqual(1, len(findings))
31+
finding = findings[0]
32+
self.assertEqual("configuration_suggestion: domain.com", finding.title)
33+
self.assertEqual("Low", finding.severity)
34+
35+
def test_cloudflare_insights_parser_with_many_findings_json(self):
36+
with (get_unit_tests_scans_path("cloudflare_insights") / "many_findings.json").open(encoding="utf-8") as testfile:
37+
parser = CloudflareInsightsParser()
38+
findings = parser.get_findings(testfile, Test())
39+
self.assertEqual(3, len(findings))
40+
finding = findings[0]
41+
self.assertEqual("configuration_suggestion: test.de", finding.title)
42+
self.assertEqual("Low", finding.severity)

0 commit comments

Comments
 (0)