55import cvss .parser
66import dateutil .parser
77from cpe import CPE
8+ from cvss .exceptions import CVSSError
89from django .core .exceptions import ImproperlyConfigured
910
1011from dojo .models import Endpoint , Finding
@@ -41,6 +42,35 @@ def escape_if_needed(x):
4142 return "" .join ([escape_if_needed (c ) for c in s ])
4243
4344
45+ def cvss_score_to_severity (score : float , version : int ) -> str :
46+ """
47+ Maps a CVSS score with a given version to a severity level.
48+ Mapping from https://nvd.nist.gov/vuln-metrics/cvss (modified slightly to have "Info" in range [0.0, 0.1) for CVSS
49+ v3/v4)
50+ """
51+ cvss_score = float (score )
52+ if version == 2 :
53+ if cvss_score >= 7.0 :
54+ severity = "High"
55+ elif cvss_score >= 4.0 :
56+ severity = "Medium"
57+ else :
58+ severity = "Low"
59+ else :
60+ if cvss_score >= 9.0 :
61+ severity = "Critical"
62+ elif cvss_score >= 7.0 :
63+ severity = "High"
64+ elif cvss_score >= 4.0 :
65+ severity = "Medium"
66+ elif cvss_score >= 0.1 :
67+ severity = "Low"
68+ else :
69+ severity = "Info"
70+
71+ return severity
72+
73+
4474#######
4575# Field parsing helper classes
4676#######
@@ -122,7 +152,6 @@ class BaseEngineParser:
122152 * status -> active/false_p/risk_accepted (depending on value)
123153 * cves -> unsaved_vulnerability_ids (vulnerability_ids)
124154 * cpe -> component name/version
125- * cvss_vector -> severity (determined using CVSS package)
126155 * notes -> appended to Finding description
127156 * details -> appended to Finding description
128157
@@ -143,7 +172,6 @@ class BaseEngineParser:
143172 "status" : Method ("parse_status" ),
144173 "cves" : Method ("parse_cves" ),
145174 "cpe" : Method ("parse_components" ),
146- "cvss_vector" : Method ("parse_severity" ),
147175 # These should be listed after the 'description' entry; they append to it
148176 "notes" : Method ("parse_notes" ),
149177 "details" : Method ("parse_details" )}
@@ -176,7 +204,7 @@ def parse_initial_date(self, finding: Finding, value: str) -> None:
176204 def is_cve (self , c : str ) -> bool :
177205 return bool (c and isinstance (c , str ) and self .CVE_PATTERN .fullmatch (c ))
178206
179- def parse_cves (self , finding : Finding , value : [str ]) -> None :
207+ def parse_cves (self , finding : Finding , value : list [str ]) -> None :
180208 finding .unsaved_vulnerability_ids = [c .upper () for c in value if self .is_cve (c )]
181209
182210 #####
@@ -192,19 +220,6 @@ def parse_status(self, finding: Finding, value: str) -> None:
192220 elif value == "acceptable_risk" :
193221 finding .risk_accepted = True
194222
195- #####
196- # For severity (extracted from cvss vector)
197- #####
198- def get_severity (self , value : str ) -> Optional [str ]:
199- if cvss_obj := cvss .parser .parse_cvss_from_text (value ):
200- if (severity := cvss_obj [0 ].severities ()[0 ].title ()) in Finding .SEVERITIES :
201- return severity
202- return None
203-
204- def parse_severity (self , finding : Finding , value : str ) -> None :
205- if severity := self .get_severity (value ):
206- finding .severity = severity
207-
208223 #####
209224 # For parsing component data
210225 #####
@@ -217,7 +232,7 @@ def parse_cpe(self, cpe_str: str) -> (Optional[str], Optional[str]):
217232 (cpe_obj .get_version () and cpe_obj .get_version ()[0 ]) or None ,
218233 )
219234
220- def parse_components (self , finding : Finding , value : [str ]) -> None :
235+ def parse_components (self , finding : Finding , value : list [str ]) -> None :
221236 # Only use the first entry
222237 finding .component_name , finding .component_version = self .parse_cpe (value [0 ])
223238
@@ -236,12 +251,12 @@ def append_description(self, finding: Finding, addendum: dict[str, str]) -> None
236251 def parse_notes (self , finding : Finding , value : str ) -> None :
237252 self .append_description (finding , {"Notes" : value })
238253
239- def extract_details (self , value : Union [str , dict [str , Union [str , dict [str , [str ]]]]]) -> dict [str , str ]:
254+ def extract_details (self , value : Union [str , dict [str , Union [str , dict [str , list [str ]]]]]) -> dict [str , str ]:
240255 if isinstance (value , dict ):
241256 return {k : v for k , v in value .items () if k != "_meta" }
242257 return {"Details" : str (value )}
243258
244- def parse_details (self , finding : Finding , value : dict [str , Union [str , dict [str , [str ]]]]) -> None :
259+ def parse_details (self , finding : Finding , value : dict [str , Union [str , dict [str , list [str ]]]]) -> None :
245260 self .append_description (finding , self .extract_details (value ))
246261
247262 #####
@@ -282,6 +297,44 @@ def set_endpoints(self, finding: Finding, item: Any) -> None:
282297 endpoints = self .parse_endpoints (item )
283298 finding .unsaved_endpoints .extend (endpoints )
284299
300+ #####
301+ # For severity (extracted from various cvss vectors)
302+ #####
303+ def parse_cvss_vector (self , value : str ) -> Optional [str ]:
304+ # CVSS4 vectors don't parse with the handy-danty parse method :(
305+ try :
306+ if (severity := cvss .CVSS4 (value ).severity ) in Finding .SEVERITIES :
307+ return severity
308+ except CVSSError :
309+ pass
310+
311+ if cvss_obj := cvss .parser .parse_cvss_from_text (value ):
312+ if (severity := cvss_obj [0 ].severities ()[0 ].title ()) in Finding .SEVERITIES :
313+ return severity
314+ return None
315+
316+ def set_severity (self , finding : Finding , item : Any ) -> None :
317+ for base_score_entry , cvss_version in [
318+ ("cvss_v4_base_score" , 4 ),
319+ ("cvss_v3_base_score" , 3 ),
320+ ("cvss_base_score" , 2 ),
321+ ]:
322+ if base_score := item .get (base_score_entry ):
323+ finding .severity = cvss_score_to_severity (base_score , cvss_version )
324+ return
325+
326+ for vector_type in ["cvss_v4_vector" , "cvss_v3_vector" , "cvss_vector" ]:
327+ if vector := item .get (vector_type ):
328+ if severity := self .parse_cvss_vector (vector ):
329+ finding .severity = severity
330+ return
331+
332+ finding .severity = "Info"
333+
334+ def process_whole_item (self , finding : Finding , item : Any ) -> None :
335+ self .set_severity (finding , item )
336+ self .set_endpoints (finding , item )
337+
285338 # Returns the complete field processing map: common fields plus any engine-specific
286339 def get_engine_fields (self ) -> dict [str , FieldType ]:
287340 return {
@@ -302,7 +355,7 @@ def parse_finding(self, item: dict[str, Any]) -> Tuple[Finding, Tuple]:
302355 # Check first whether the field even exists on this item entry; if not, skip it
303356 if value := item .get (field ):
304357 field_handler (self , finding , value )
305- self .set_endpoints (finding , item )
358+ self .process_whole_item (finding , item )
306359 # Make a note of what scanning engine was used for this Finding
307360 self .append_description (finding , {"Scanning Engine" : self .SCANNING_ENGINE })
308361 return finding , self .get_finding_key (finding )
0 commit comments