11import re
22import zipfile
3+ from xml .etree .ElementTree import Element
34
45from defusedxml import ElementTree
56
6- from dojo .models import Finding
7+ from dojo .models import Finding , Test
78
89
910class FortifyFPRParser :
@@ -12,70 +13,156 @@ def parse_fpr(self, filename, test):
1213 input_zip = zipfile .ZipFile (filename .name , "r" )
1314 else :
1415 input_zip = zipfile .ZipFile (filename , "r" )
15- zipdata = {name : input_zip .read (name ) for name in input_zip .namelist ()}
16- root = ElementTree .fromstring (zipdata ["audit.fvdl" ].decode ("utf-8" ))
16+ # Read each file from the zip artifact into a dict with the format of
17+ # filename: file_content
18+ zip_data = {name : input_zip .read (name ) for name in input_zip .namelist ()}
19+ root = self .identify_root (zip_data )
20+ return self .parse_vulnerabilities_and_convert_to_findings (root , test )
21+
22+ def identify_root (self , zip_data : dict ) -> Element :
23+ """Iterate through the zip data to determine which file in the zip could be the XMl to be parsed."""
24+ # Determine where the "audit.fvdl" could be
25+ audit_file = None
26+ for file_name in zip_data :
27+ if file_name .endswith ("audit.fvdl" ):
28+ audit_file = file_name
29+ break
30+ # Make sure we have an audit file
31+ if audit_file is None :
32+ msg = 'A search for an "audit.fvdl" file was not successful. '
33+ raise ValueError (msg )
34+ # Parser the XML file and determine the name space, if present
35+ root = ElementTree .fromstring (zip_data .get (audit_file ).decode ("utf-8" ))
36+ self .identify_namespace (root )
37+ return root
38+
39+ def identify_namespace (self , root : Element ) -> None :
40+ """Determine what the namespace could be, and then set the value in a class var labeled `namespace`"""
1741 regex = r"{.*}"
1842 matches = re .match (regex , root .tag )
1943 try :
20- namespace = matches .group (0 )
44+ self . namespace = matches .group (0 )
2145 except BaseException :
22- namespace = ""
46+ self .namespace = ""
47+
48+ def parse_vulnerabilities_and_convert_to_findings (self , root : Element , test : Test ) -> list [Finding ]:
49+ """Parse the XML and generate a list of findings."""
2350 items = []
2451 for child in root :
2552 if "Vulnerabilities" in child .tag :
2653 for vuln in child :
27- ClassID = vuln .find (f"{ namespace } ClassInfo" ).find (f"{ namespace } ClassID" ).text
28- Kingdom = vuln .find (f"{ namespace } ClassInfo" ).find (f"{ namespace } Kingdom" ).text
29- Type = vuln .find (f"{ namespace } ClassInfo" ).find (f"{ namespace } Type" ).text
30- AnalyzerName = vuln .find (f"{ namespace } ClassInfo" ).find (f"{ namespace } AnalyzerName" ).text
31- DefaultSeverity = vuln .find (f"{ namespace } ClassInfo" ).find (f"{ namespace } DefaultSeverity" ).text
32- InstanceID = vuln .find (f"{ namespace } InstanceInfo" ).find (f"{ namespace } InstanceID" ).text
33- InstanceSeverity = vuln .find (f"{ namespace } InstanceInfo" ).find (f"{ namespace } InstanceSeverity" ).text
34- Confidence = vuln .find (f"{ namespace } InstanceInfo" ).find (f"{ namespace } Confidence" ).text
35- SourceLocationpath = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("path" )
36- SourceLocationline = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("line" )
37- SourceLocationlineEnd = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("lineEnd" )
38- SourceLocationcolStart = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("colStart" )
39- SourceLocationcolEnd = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("colEnd" )
40- SourceLocationsnippet = vuln .find (f"{ namespace } AnalysisInfo" ).find (f"{ namespace } Unified" ).find (f"{ namespace } Trace" ).find (f"{ namespace } Primary" ).find (f"{ namespace } Entry" ).find (f"{ namespace } Node" ).find (f"{ namespace } SourceLocation" ).attrib .get ("snippet" )
41- description = Type + "\n "
42- severity = self .fpr_severity (Confidence , InstanceSeverity )
43- description += "**ClassID:** " + ClassID + "\n "
44- description += "**Kingdom:** " + Kingdom + "\n "
45- description += "**AnalyzerName:** " + AnalyzerName + "\n "
46- description += "**DefaultSeverity:** " + DefaultSeverity + "\n "
47- description += "**InstanceID:** " + InstanceID + "\n "
48- description += "**InstanceSeverity:** " + InstanceSeverity + "\n "
49- description += "**Confidence:** " + Confidence + "\n "
50- description += "**SourceLocationpath:** " + str (SourceLocationpath ) + "\n "
51- description += "**SourceLocationline:** " + str (SourceLocationline ) + "\n "
52- description += "**SourceLocationlineEnd:** " + str (SourceLocationlineEnd ) + "\n "
53- description += "**SourceLocationcolStart:** " + str (SourceLocationcolStart ) + "\n "
54- description += "**SourceLocationcolEnd:** " + str (SourceLocationcolEnd ) + "\n "
55- description += "**SourceLocationsnippet:** " + str (SourceLocationsnippet ) + "\n "
56- items .append (
57- Finding (
58- title = Type + " " + ClassID ,
59- severity = severity ,
60- static_finding = True ,
61- test = test ,
62- description = description ,
63- unique_id_from_tool = ClassID ,
64- file_path = SourceLocationpath ,
65- line = SourceLocationline ,
66- ),
67- )
54+ finding_context = {
55+ "title" : "" ,
56+ "description" : "" ,
57+ "static_finding" : True ,
58+ "test" : test ,
59+ }
60+ self .parse_class_information (vuln , finding_context )
61+ self .parse_instance_information (vuln , finding_context )
62+ self .parse_analysis_information (vuln , finding_context )
63+ self .parse_severity_and_convert (vuln , finding_context )
64+ items .append (Finding (** finding_context ))
6865 return items
6966
70- def fpr_severity (self , Confidence , InstanceSeverity ):
71- if float (Confidence ) >= 2.5 and float (InstanceSeverity ) >= 2.5 :
72- severity = "Critical"
73- elif float (Confidence ) >= 2.5 and float (InstanceSeverity ) < 2.5 :
74- severity = "High"
75- elif float (Confidence ) < 2.5 and float (InstanceSeverity ) >= 2.5 :
76- severity = "Medium"
77- elif float (Confidence ) < 2.5 and float (InstanceSeverity ) < 2.5 :
78- severity = "Low"
79- else :
80- severity = "Info"
81- return severity
67+ def parse_severity_and_convert (self , vulnerability : Element , finding_context : dict ) -> None :
68+ """Convert the the float representation of severity and confidence to a string severity."""
69+ # Default info severity in the case of an error
70+ severity = "Info"
71+ instance_severity = None
72+ confidence = None
73+ # Attempt to fetch the confidence and instance severity
74+ if (instance_info := vulnerability .find (f"{ self .namespace } InstanceInfo" )) is not None :
75+ instance_severity = getattr (instance_info .find (f"{ self .namespace } InstanceSeverity" ), "text" , None )
76+ confidence = getattr (instance_info .find (f"{ self .namespace } Confidence" ), "text" , None )
77+ # Make sure we have something to work with
78+ if confidence is not None and instance_severity is not None :
79+ if float (confidence ) >= 2.5 and float (instance_severity ) >= 2.5 :
80+ severity = "Critical"
81+ elif float (confidence ) >= 2.5 and float (instance_severity ) < 2.5 :
82+ severity = "High"
83+ elif float (confidence ) < 2.5 and float (instance_severity ) >= 2.5 :
84+ severity = "Medium"
85+ elif float (confidence ) < 2.5 and float (instance_severity ) < 2.5 :
86+ severity = "Low"
87+ # Return either info, or the calculated severity
88+ finding_context ["severity" ] = severity
89+
90+ def parse_class_information (self , vulnerability : Element , finding_context : dict ) -> None :
91+ """Appends the description with any class information that can be extracted."""
92+ if (class_info := vulnerability .find (f"{ self .namespace } ClassInfo" )) is not None :
93+ if (namespace_type := class_info .find (f"{ self .namespace } Type" )) is not None :
94+ finding_context ["description" ] += f"{ namespace_type .text } \n "
95+ finding_context ["title" ] += f"{ namespace_type .text } "
96+ if (class_id := class_info .find (f"{ self .namespace } ClassID" )) is not None :
97+ finding_context ["description" ] += f"**ClassID:** { class_id .text } \n "
98+ finding_context ["unique_id_from_tool" ] = class_id .text
99+ finding_context ["title" ] += f" { class_id .text } "
100+ if (kingdom := class_info .find (f"{ self .namespace } Kingdom" )) is not None :
101+ finding_context ["description" ] += f"**Kingdom:** { kingdom .text } \n "
102+ if (analyzer_name := class_info .find (f"{ self .namespace } AnalyzerName" )) is not None :
103+ finding_context ["description" ] += f"**AnalyzerName:** { analyzer_name .text } \n "
104+ if (default_severity := class_info .find (f"{ self .namespace } DefaultSeverity" )) is not None :
105+ finding_context ["description" ] += f"**DefaultSeverity:** { default_severity .text } \n "
106+
107+ def parse_instance_information (self , vulnerability : Element , finding_context : dict ) -> None :
108+ """Appends the description with any instance information that can be extracted."""
109+ if (instance_info := vulnerability .find (f"{ self .namespace } InstanceInfo" )) is not None :
110+ if (instance_id := instance_info .find (f"{ self .namespace } InstanceID" )) is not None :
111+ finding_context ["description" ] += f"**InstanceID:** { instance_id .text } \n "
112+ if (instance_severity := instance_info .find (f"{ self .namespace } InstanceSeverity" )) is not None :
113+ finding_context ["description" ] += f"**InstanceSeverity:** { instance_severity .text } \n "
114+ if (confidence := instance_info .find (f"{ self .namespace } Confidence" )) is not None :
115+ finding_context ["description" ] += f"**Confidence:** { confidence .text } \n "
116+
117+ def parse_analysis_information (self , vulnerability : Element , finding_context : dict ) -> None :
118+ """Appends the description with any analysis information that can be extracted."""
119+ if (analysis_info := vulnerability .find (f"{ self .namespace } AnalysisInfo" )) is not None :
120+ # See if we can get a SourceLocation from this
121+ if (source_location := self .get_source_location (analysis_info )) is not None :
122+ path = source_location .attrib .get ("path" )
123+ line = source_location .attrib .get ("line" )
124+ # Managed the description
125+ finding_context ["description" ] += f"**SourceLocationPath:** { path } \n "
126+ finding_context ["description" ] += f"**SourceLocationLine:** { line } \n "
127+ finding_context ["description" ] += (
128+ f"**SourceLocationLineEnd:** { source_location .attrib .get ('lineEnd' )} \n "
129+ )
130+ finding_context ["description" ] += (
131+ f"**SourceLocationColStart:** { source_location .attrib .get ('colStart' )} \n "
132+ )
133+ finding_context ["description" ] += f"**SourceLocationColEnd:** { source_location .attrib .get ('colEnd' )} \n "
134+ finding_context ["description" ] += (
135+ f"**SourceLocationSnippet:** { source_location .attrib .get ('snippet' )} \n "
136+ )
137+ # manage the other metadata
138+ finding_context ["file_path" ] = path
139+ finding_context ["line" ] = line
140+
141+ def get_source_location (self , analysis_info : Element ) -> Element | None :
142+ """Return the SourceLocation element if we are able to reach it."""
143+ # The order of this list is very important. Do not reorder it!
144+ key_path = [
145+ "Unified" ,
146+ "Trace" ,
147+ "Primary" ,
148+ "Entry" ,
149+ "Node" ,
150+ "SourceLocation" ,
151+ ]
152+ # iterate of the keys until we find something that cannot be fulfilled
153+ current_element = analysis_info
154+ # Traverse the key path up to "Entry" to fetch all Entry elements
155+ for key in key_path [:- 3 ]: # stop before "Entry" level
156+ if (next_current_element := current_element .find (f"{ self .namespace } { key } " )) is not None :
157+ current_element = next_current_element
158+ else :
159+ return None
160+ # Iterate over all "Entry" elements
161+ entries = current_element .findall (f"{ self .namespace } Entry" )
162+ for entry in entries :
163+ # Continue the search for "Node" and "SourceLocation" within each entry
164+ if (node := entry .find (f"{ self .namespace } Node" )) is not None :
165+ if (source_location := node .find (f"{ self .namespace } SourceLocation" )) is not None :
166+ return source_location
167+ # Return None if no SourceLocation was found in any Entry
168+ return None
0 commit comments