Skip to content

Commit ab59763

Browse files
Maffoochcneill
andauthored
Fortify Parser: Fortification of the the FPR parsing (#10901)
* Fortify Parser: Fortification of the the FPR parsing * Update dojo/tools/fortify/parser.py Co-authored-by: Charles Neill <1749665+cneill@users.noreply.github.com> --------- Co-authored-by: Charles Neill <1749665+cneill@users.noreply.github.com>
1 parent d1da8aa commit ab59763

2 files changed

Lines changed: 148 additions & 58 deletions

File tree

dojo/tools/fortify/fpr_parser.py

Lines changed: 145 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import re
22
import zipfile
3+
from xml.etree.ElementTree import Element
34

45
from defusedxml import ElementTree
56

6-
from dojo.models import Finding
7+
from dojo.models import Finding, Test
78

89

910
class 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

dojo/tools/fortify/parser.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,6 @@ def get_findings(self, filename, test):
1717
return FortifyXMLParser().parse_xml(filename, test)
1818
elif str(filename.name).endswith(".fpr"):
1919
return FortifyFPRParser().parse_fpr(filename, test)
20+
else:
21+
msg = "Filename extension not recognized. Use .xml or .fpr"
22+
raise ValueError(msg)

0 commit comments

Comments
 (0)