Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/content/supported_tools/parsers/file/fortify.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ toc_hide: true
You can either import the findings in .xml or in .fpr file format. </br>
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.

### Fortify Scan v2
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.

### Sample Scan Data
Sample Fortify scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/fortify).

Expand Down
17 changes: 17 additions & 0 deletions dojo/tools/fortify/fpr_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,20 @@ def compute_line(self, vulnerability, snippet) -> str:
if snippet and snippet.start_line:
return snippet.start_line
return vulnerability.source_location_line


class FortifyFPRParserV2(FortifyFPRParser):

"""
FPR parser for the "Fortify Scan v2" scan type.

Stores the line Fortify reports for the vulnerability itself (the SourceLocation
"line" attribute) instead of the snippet StartLine used by the v1 parser, which
includes leading context lines and does not point at the finding. Kept as a
separate scan type so hashcodes of existing "Fortify Scan" findings are unaffected.
"""

def compute_line(self, vulnerability, snippet) -> str:
if vulnerability.source_location_line:
return vulnerability.source_location_line
return super().compute_line(vulnerability, snippet)
21 changes: 20 additions & 1 deletion dojo/tools/fortify/parser.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dojo.tools.fortify.fpr_parser import FortifyFPRParser
from dojo.tools.fortify.fpr_parser import FortifyFPRParser, FortifyFPRParserV2
from dojo.tools.fortify.xml_parser import FortifyXMLParser


Expand All @@ -19,3 +19,22 @@ def get_findings(self, filename, test):
return FortifyFPRParser().parse_fpr(filename, test)
msg = "Filename extension not recognized. Use .xml or .fpr"
raise ValueError(msg)


class FortifyParserV2:
def get_scan_types(self):
return ["Fortify Scan v2"]

def get_label_for_scan_types(self, scan_type):
return scan_type # no custom label for now

def get_description_for_scan_types(self, scan_type):
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."

def get_findings(self, filename, test):
if str(filename.name).endswith(".xml"):
return FortifyXMLParser().parse_xml(filename, test)
if str(filename.name).endswith(".fpr"):
return FortifyFPRParserV2().parse_fpr(filename, test)
msg = "Filename extension not recognized. Use .xml or .fpr"
raise ValueError(msg)
64 changes: 63 additions & 1 deletion unittests/tools/test_fortify_parser.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@

from dojo.models import Test
from dojo.tools.fortify.parser import FortifyParser
from dojo.tools.fortify.parser import FortifyParser, FortifyParserV2
from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path


Expand Down Expand Up @@ -189,3 +189,65 @@ def test_fortify_hello_world_fpr_rule_without_metainfo(self):
self.assertEqual("D3166922519EDD92D132761602EB71B4", finding.unique_id_from_tool)
self.assertEqual("src/main/java/hello/HelloWorld.java", finding.file_path)
self.assertEqual(13, finding.line)


class TestFortifyV2Parser(DojoTestCase):

"""
Regression: the v1 FPR parser stores the snippet context StartLine (Fortify's
reported line minus up to 3 context lines) instead of the reported SourceLocation
line, which breaks file_path + line deduplication against other tools. The
"Fortify Scan v2" scan type stores the reported line; v1 is intentionally left
unchanged so existing hashcodes are unaffected.
"""

def test_fortify_v2_scan_type(self):
self.assertEqual(["Fortify Scan v2"], FortifyParserV2().get_scan_types())

def test_fortify_v2_fpr_uses_true_source_location_line(self):
with (get_unit_tests_scans_path("fortify") / "many_findings.fpr").open(encoding="utf-8") as testfile:
parser = FortifyParserV2()
findings = parser.get_findings(testfile, Test())
self.assertEqual(61, len(findings))
self.validate_locations(findings)
finding = findings[0]
# control: the description records Fortify's reported line verbatim
self.assertIn("**SourceLocationLine:** 222", finding.description)
# the stored line must match the reported line, not the snippet start (219)
self.assertEqual(
222, finding.line,
msg=f"expected line=222 (SourceLocationLine), stored line={finding.line}",
)
self.assertEqual("Cross-Site Request Forgery - category.html: 222 (114E5A67-3446-4DD5-B578-D0E6FDBB304E)", finding.title)

def test_fortify_v2_fpr_clamped_snippet(self):
with (get_unit_tests_scans_path("fortify") / "hello_world.fpr").open(encoding="utf-8") as testfile:
parser = FortifyParserV2()
findings = parser.get_findings(testfile, Test())
self.assertEqual(4, len(findings))
with self.subTest(i=0):
finding = findings[0]
self.assertIn("**SourceLocationLine:** 8", finding.description)
self.assertEqual(
8, finding.line,
msg=f"expected line=8 (SourceLocationLine), stored line={finding.line}",
)
with self.subTest(i=2):
# near the top of a file the snippet start clamps to 1, so the v1
# offset is not even constant: reported line 3, snippet start 1
finding = findings[2]
self.assertIn("**SourceLocationLine:** 3", finding.description)
self.assertEqual(
3, finding.line,
msg=f"expected line=3 (SourceLocationLine), stored line={finding.line}",
)

def test_fortify_v2_xml_unchanged_from_v1(self):
# the XML report path already uses the reported line in v1; v2 must match
with (get_unit_tests_scans_path("fortify") / "fortify_many_findings.xml").open(encoding="utf-8") as testfile:
parser = FortifyParserV2()
findings = parser.get_findings(testfile, Test())
self.assertEqual(324, len(findings))
finding = findings[0]
self.assertEqual("src/main/java/org/joychou/controller/XXE.java", finding.file_path)
self.assertEqual(81, finding.line)
Loading