Skip to content

Commit 8fb9fd7

Browse files
authored
fortify fpr_parser: allow optional fields to be optional (#13160)
1 parent 2fe00d6 commit 8fb9fd7

3 files changed

Lines changed: 46 additions & 19 deletions

File tree

dojo/tools/fortify/fpr_parser.py

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -133,14 +133,14 @@ def convert_vulnerabilities_to_findings(self, root: Element, audit_log: Element,
133133
finding = Finding(test=test, static_finding=True)
134134

135135
finding.active, finding.false_p = self.compute_status(related_data, vuln_data)
136-
finding.title = self.format_title(vuln_data, snippet, description, rule)
136+
finding.title = self.format_title(vuln_data, snippet)
137137
finding.description = self.format_description(vuln_data, snippet, description, rule)
138138
finding.mitigation = self.format_mitigation(vuln_data, snippet, description, rule)
139-
finding.severity = self.compute_severity(vuln_data, snippet, description, rule)
139+
finding.severity = self.compute_severity(vuln_data, rule)
140140
finding.impact = self.format_impact(related_data, vuln_data)
141141

142142
finding.file_path = vuln_data.source_location_path
143-
finding.line = int(self.compute_line(vuln_data, snippet, description, rule))
143+
finding.line = int(self.compute_line(vuln_data, snippet))
144144
finding.unique_id_from_tool = vuln_data.instance_id
145145

146146
findings.append(finding)
@@ -225,26 +225,28 @@ def parse_description_information(self, description: Element) -> DescriptionData
225225
def parse_rule_information(self, rule: Element) -> RuleData:
226226
"""Parse the rule information and return a RuleData object."""
227227
rule_data = RuleData()
228-
rule_data.accuracy = rule.findtext("Group[@name='Accuracy']", None, self.namespaces)
229-
rule_data.impact = rule.findtext("Group[@name='Impact']", None, self.namespaces)
230-
rule_data.probability = rule.findtext("Group[@name='Probability']", None, self.namespaces)
231-
rule_data.impact_bias = rule.findtext("Group[@name='ImpactBias']", None, self.namespaces)
232-
rule_data.confidentiality_impact = rule.findtext("Group[@name='ConfidentialityImpact']", None, self.namespaces)
233-
rule_data.integrity_impact = rule.findtext("Group[@name='IntegrityImpact']", None, self.namespaces)
234-
rule_data.remediation_effort = rule.findtext("Group[@name='Recommendations']", None, self.namespaces)
235-
logger.debug(f"Rule Impact: {rule_data.impact}")
228+
if rule is not None:
229+
rule_data.accuracy = rule.findtext("Group[@name='Accuracy']", None, self.namespaces)
230+
rule_data.impact = rule.findtext("Group[@name='Impact']", None, self.namespaces)
231+
rule_data.probability = rule.findtext("Group[@name='Probability']", None, self.namespaces)
232+
rule_data.impact_bias = rule.findtext("Group[@name='ImpactBias']", None, self.namespaces)
233+
rule_data.confidentiality_impact = rule.findtext("Group[@name='ConfidentialityImpact']", None, self.namespaces)
234+
rule_data.integrity_impact = rule.findtext("Group[@name='IntegrityImpact']", None, self.namespaces)
235+
rule_data.remediation_effort = rule.findtext("Group[@name='Recommendations']", None, self.namespaces)
236+
logger.debug(f"Rule Impact: {rule_data.impact}")
236237
return rule_data
237238

238-
def format_title(self, vulnerability, snippet, description, rule) -> str:
239+
def format_title(self, vulnerability, snippet) -> str:
239240
# defaults for when there is no snippet (shouldn't happen, future improvement: parser might also parse ReplacementDefinitions and/or Context elements)
240241
file_name = vulnerability.source_location_path.split("/")[-1]
241-
line = self.compute_line(vulnerability, snippet, description, rule)
242+
line = self.compute_line(vulnerability, snippet)
242243

243244
return f"{vulnerability.vulnerability_type} - {file_name}: {line} ({vulnerability.class_id})"
244245

245246
def format_description(self, vulnerability, snippet, description, rule) -> str:
246247
desc = f"##Catagory: {vulnerability.vulnerability_type}\n"
247-
desc += f"###Abstract:\n{description.abstract}\n"
248+
if description:
249+
desc += f"###Abstract:\n{description.abstract}\n"
248250

249251
desc += f"**SourceLocationPath:** {vulnerability.source_location_path}\n"
250252
desc += f"**SourceLocationLine:** {vulnerability.source_location_line}\n"
@@ -258,7 +260,8 @@ def format_description(self, vulnerability, snippet, description, rule) -> str:
258260
"leads to this finding. \n")
259261
desc += f"###Snippet:\n**File: {snippet.file_name}: {snippet.start_line}**\n```\n{snippet.text}\n```\n"
260262

261-
desc += f"##Explanation:\n {description.explanation}"
263+
if description:
264+
desc += f"##Explanation:\n {description.explanation}"
262265

263266
desc += f"##Details: {vulnerability.instance_id}\n"
264267
desc += f"**InstanceID:** {vulnerability.instance_id}\n"
@@ -273,14 +276,14 @@ def format_description(self, vulnerability, snippet, description, rule) -> str:
273276

274277
def format_mitigation(self, vulnerability, snippet, description, rule) -> str:
275278
mitigation = ""
276-
if description.recommendations:
279+
if description and description.recommendations:
277280
mitigation += f"###Recommendation:\n {description.recommendations}\n"
278281

279-
if description.tips:
282+
if description and description.tips:
280283
mitigation += f"###Tips:\n {description.tips}"
281284
return mitigation
282285

283-
def compute_severity(self, vulnerability, snippet, description, rule) -> str:
286+
def compute_severity(self, vulnerability, rule) -> str:
284287
"""Convert the the float representation of severity and confidence to a string severity."""
285288
if not rule.impact:
286289
logger.debug("No rule impact found, setting severity to Informational")
@@ -330,7 +333,7 @@ def compute_status(self, related_data, vulnerability) -> tuple[bool, bool]:
330333
return False, True
331334
return True, False
332335

333-
def compute_line(self, vulnerability, snippet, description, rule) -> str:
336+
def compute_line(self, vulnerability, snippet) -> str:
334337
if snippet and snippet.start_line:
335338
return snippet.start_line
336339
return vulnerability.source_location_line
24.1 KB
Binary file not shown.

unittests/tools/test_fortify_parser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,3 +154,27 @@ def test_fortify_fpr_suppressed_finding(self):
154154
self.assertFalse(finding.active)
155155
self.assertTrue(finding.false_p)
156156
self.assertEqual("Threaded Comments:\n2025-03-10T20:52:28.964+05:30 - (testuser): Not an issue. Handled in server config to refer to internal Artifactory\n", finding.impact)
157+
158+
def test_fortify_hello_world_fpr_rule_without_metainfo(self):
159+
with (get_unit_tests_scans_path("fortify") / "hello_world_no_metainfo.fpr").open(encoding="utf-8") as testfile:
160+
parser = FortifyParser()
161+
findings = parser.get_findings(testfile, Test())
162+
self.assertEqual(4, len(findings))
163+
# for i in range(len(findings)):
164+
# print(f"{i}: {findings[i]}: {findings[i].severity}")
165+
166+
with self.subTest(i=0):
167+
finding = findings[0]
168+
self.assertEqual("Password Management - HelloWorld.java: 5 (720E3A66-55AC-4D2D-8DB9-DC30E120A52F)", finding.title)
169+
# Info as rule has no metainfo/impact
170+
self.assertEqual("Informational", finding.severity)
171+
self.assertEqual("A5338E223E737FF81F8A806C50A05969", finding.unique_id_from_tool)
172+
self.assertEqual("src/main/java/hello/HelloWorld.java", finding.file_path)
173+
self.assertEqual(5, finding.line)
174+
with self.subTest(i=1):
175+
finding = findings[1]
176+
self.assertEqual("Password Management - HelloWorld.java: 13 (9C5BD1B5-C296-48d4-B5F5-5D2958661BC4)", finding.title)
177+
self.assertEqual("High", finding.severity)
178+
self.assertEqual("D3166922519EDD92D132761602EB71B4", finding.unique_id_from_tool)
179+
self.assertEqual("src/main/java/hello/HelloWorld.java", finding.file_path)
180+
self.assertEqual(13, finding.line)

0 commit comments

Comments
 (0)