Skip to content

Commit 35c08a0

Browse files
authored
create fortify parser V2 - prefer true line (#15228)
* change fortify parser to prefer true line * create v2 scan
1 parent 97139fa commit 35c08a0

4 files changed

Lines changed: 103 additions & 2 deletions

File tree

docs/content/supported_tools/parsers/file/fortify.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ toc_hide: true
55
You can either import the findings in .xml or in .fpr file format. </br>
66
If you import a .fpr file, the parser will look for the file 'audit.fvdl' and analyze it. An extracted example can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/fortify/audit.fvdl). The optional `audit.xml` is also parsed. All vulnerabilities marked with `suppressed="true"` will be marked as false positive.
77

8+
### Fortify Scan v2
9+
The `Fortify Scan v2` scan type behaves identically to `Fortify Scan` except for .fpr imports: findings store the line number Fortify reports for the vulnerability (the FVDL `SourceLocation` line) instead of the first line of the surrounding code snippet, which includes up to 3 leading context lines. Use `Fortify Scan v2` when deduplicating on file path + line number, especially across tools. The two scan types produce different hashcodes for .fpr findings, so keep using `Fortify Scan` on existing products if you want to preserve deduplication history.
10+
811
### Sample Scan Data
912
Sample Fortify scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/fortify).
1013

dojo/tools/fortify/fpr_parser.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,20 @@ def compute_line(self, vulnerability, snippet) -> str:
333333
if snippet and snippet.start_line:
334334
return snippet.start_line
335335
return vulnerability.source_location_line
336+
337+
338+
class FortifyFPRParserV2(FortifyFPRParser):
339+
340+
"""
341+
FPR parser for the "Fortify Scan v2" scan type.
342+
343+
Stores the line Fortify reports for the vulnerability itself (the SourceLocation
344+
"line" attribute) instead of the snippet StartLine used by the v1 parser, which
345+
includes leading context lines and does not point at the finding. Kept as a
346+
separate scan type so hashcodes of existing "Fortify Scan" findings are unaffected.
347+
"""
348+
349+
def compute_line(self, vulnerability, snippet) -> str:
350+
if vulnerability.source_location_line:
351+
return vulnerability.source_location_line
352+
return super().compute_line(vulnerability, snippet)

dojo/tools/fortify/parser.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dojo.tools.fortify.fpr_parser import FortifyFPRParser
1+
from dojo.tools.fortify.fpr_parser import FortifyFPRParser, FortifyFPRParserV2
22
from dojo.tools.fortify.xml_parser import FortifyXMLParser
33

44

@@ -19,3 +19,22 @@ def get_findings(self, filename, test):
1919
return FortifyFPRParser().parse_fpr(filename, test)
2020
msg = "Filename extension not recognized. Use .xml or .fpr"
2121
raise ValueError(msg)
22+
23+
24+
class FortifyParserV2:
25+
def get_scan_types(self):
26+
return ["Fortify Scan v2"]
27+
28+
def get_label_for_scan_types(self, scan_type):
29+
return scan_type # no custom label for now
30+
31+
def get_description_for_scan_types(self, scan_type):
32+
return "Import Findings in FPR or XML file format. Unlike Fortify Scan, FPR findings use the line reported by Fortify rather than the start of the code snippet."
33+
34+
def get_findings(self, filename, test):
35+
if str(filename.name).endswith(".xml"):
36+
return FortifyXMLParser().parse_xml(filename, test)
37+
if str(filename.name).endswith(".fpr"):
38+
return FortifyFPRParserV2().parse_fpr(filename, test)
39+
msg = "Filename extension not recognized. Use .xml or .fpr"
40+
raise ValueError(msg)

unittests/tools/test_fortify_parser.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11

22
from dojo.models import Test
3-
from dojo.tools.fortify.parser import FortifyParser
3+
from dojo.tools.fortify.parser import FortifyParser, FortifyParserV2
44
from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path
55

66

@@ -189,3 +189,65 @@ def test_fortify_hello_world_fpr_rule_without_metainfo(self):
189189
self.assertEqual("D3166922519EDD92D132761602EB71B4", finding.unique_id_from_tool)
190190
self.assertEqual("src/main/java/hello/HelloWorld.java", finding.file_path)
191191
self.assertEqual(13, finding.line)
192+
193+
194+
class TestFortifyV2Parser(DojoTestCase):
195+
196+
"""
197+
Regression: the v1 FPR parser stores the snippet context StartLine (Fortify's
198+
reported line minus up to 3 context lines) instead of the reported SourceLocation
199+
line, which breaks file_path + line deduplication against other tools. The
200+
"Fortify Scan v2" scan type stores the reported line; v1 is intentionally left
201+
unchanged so existing hashcodes are unaffected.
202+
"""
203+
204+
def test_fortify_v2_scan_type(self):
205+
self.assertEqual(["Fortify Scan v2"], FortifyParserV2().get_scan_types())
206+
207+
def test_fortify_v2_fpr_uses_true_source_location_line(self):
208+
with (get_unit_tests_scans_path("fortify") / "many_findings.fpr").open(encoding="utf-8") as testfile:
209+
parser = FortifyParserV2()
210+
findings = parser.get_findings(testfile, Test())
211+
self.assertEqual(61, len(findings))
212+
self.validate_locations(findings)
213+
finding = findings[0]
214+
# control: the description records Fortify's reported line verbatim
215+
self.assertIn("**SourceLocationLine:** 222", finding.description)
216+
# the stored line must match the reported line, not the snippet start (219)
217+
self.assertEqual(
218+
222, finding.line,
219+
msg=f"expected line=222 (SourceLocationLine), stored line={finding.line}",
220+
)
221+
self.assertEqual("Cross-Site Request Forgery - category.html: 222 (114E5A67-3446-4DD5-B578-D0E6FDBB304E)", finding.title)
222+
223+
def test_fortify_v2_fpr_clamped_snippet(self):
224+
with (get_unit_tests_scans_path("fortify") / "hello_world.fpr").open(encoding="utf-8") as testfile:
225+
parser = FortifyParserV2()
226+
findings = parser.get_findings(testfile, Test())
227+
self.assertEqual(4, len(findings))
228+
with self.subTest(i=0):
229+
finding = findings[0]
230+
self.assertIn("**SourceLocationLine:** 8", finding.description)
231+
self.assertEqual(
232+
8, finding.line,
233+
msg=f"expected line=8 (SourceLocationLine), stored line={finding.line}",
234+
)
235+
with self.subTest(i=2):
236+
# near the top of a file the snippet start clamps to 1, so the v1
237+
# offset is not even constant: reported line 3, snippet start 1
238+
finding = findings[2]
239+
self.assertIn("**SourceLocationLine:** 3", finding.description)
240+
self.assertEqual(
241+
3, finding.line,
242+
msg=f"expected line=3 (SourceLocationLine), stored line={finding.line}",
243+
)
244+
245+
def test_fortify_v2_xml_unchanged_from_v1(self):
246+
# the XML report path already uses the reported line in v1; v2 must match
247+
with (get_unit_tests_scans_path("fortify") / "fortify_many_findings.xml").open(encoding="utf-8") as testfile:
248+
parser = FortifyParserV2()
249+
findings = parser.get_findings(testfile, Test())
250+
self.assertEqual(324, len(findings))
251+
finding = findings[0]
252+
self.assertEqual("src/main/java/org/joychou/controller/XXE.java", finding.file_path)
253+
self.assertEqual(81, finding.line)

0 commit comments

Comments
 (0)