11import csv
22import io
3+ import json
34from urllib .parse import urlparse
45
56from dojo .models import Endpoint , Finding
89class 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 )
0 commit comments