Skip to content

Commit 7d0f185

Browse files
🔨 RustyHog: handle empty reports correctly to fix #10584 (#12129)
* 🔨 Rework RustyHog to fix #10584 * Update docs/content/en/connecting_your_tools/parsers/file/rusty_hog.md Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com> * update --------- Co-authored-by: Cody Maffucci <46459665+Maffooch@users.noreply.github.com>
1 parent 5ccbef0 commit 7d0f185

File tree

3 files changed

+71
-64
lines changed

3 files changed

+71
-64
lines changed

docs/content/en/connecting_your_tools/parsers/file/rusty_hog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ DefectDojo currently supports the parsing of the following Rusty Hog JSON output
1414
RustyHog scans only one target at a time. This is not efficient if you want to scan all targets (e.g. all JIRA tickets) and upload each single report to DefectDojo.
1515
[Rusty-Hog-Wrapper](https://github.com/manuel-sommer/Rusty-Hog-Wrapper) deals with this and scans a whole JIRA Project or Confluence Space, merges the findings into a valid file which can be uploaded to DefectDojo. (This is no official recommendation from DefectDojo, but rather a pointer in a direction on how to use this vulnerability scanner in a more efficient way.)
1616

17+
You can either select "Rusty Hog Scan" directly, or specify the sub scanner (e.g. "Duroc Hog Scan"). If you choose "Rusty Hog Scan", we recommend to re-import scans into the same test. For more information look at [this issue](https://github.com/DefectDojo/django-DefectDojo/issues/10584).
18+
1719
### Sample Scan Data
1820
Sample Rusty Hog parser scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/rusty_hog).

dojo/tools/rusty_hog/parser.py

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,68 +6,66 @@
66

77
class RustyhogParser:
88
def get_scan_types(self):
9-
return ["Rusty Hog Scan"]
9+
return ["Rusty Hog Scan", "Choctaw Hog Scan", "Duroc Hog Scan", "Gottingen Hog Scan", "Essex Hog Scan"]
1010

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

1414
def get_description_for_scan_types(self, scan_type):
1515
return "Rusty Hog Scan - JSON Report"
1616

17-
def get_findings(self, json_output, test):
18-
tree = json.load(json_output)
19-
return self.get_items(tree, test)
20-
21-
def parse_json(self, json_output):
22-
return json.load(json_output)
23-
24-
def get_items(self, json_output, scanner, test):
17+
def get_findings(self, filename, test):
18+
if filename:
19+
tree = filename.read()
20+
try:
21+
json_output = json.loads(str(tree, "utf-8"))
22+
except Exception:
23+
json_output = json.loads(tree)
24+
if not json_output:
25+
json_output = []
2526
items = {}
26-
findings = self.__getitem(
27-
vulnerabilities=self.parse_json(json_output), scanner=scanner,
28-
)
27+
findings = self.__getitem(vulnerabilities=json_output, scanner=test)
2928
for finding in findings:
3029
unique_key = f"Finding {finding}"
3130
items[unique_key] = finding
3231
return list(items.values())
3332

3433
def get_tests(self, scan_type, handle):
35-
tree = self.parse_json(handle)
34+
tree = json.load(handle)
3635
tests = []
37-
parsername = "Rusty Hog"
38-
for node in tree:
39-
if (
40-
"commit" in node
41-
or "commitHash" in node
42-
or "parent_commit_hash" in node
43-
or "old_file_id" in node
44-
or "new_file_id" in node
45-
):
46-
parsername = "Choctaw Hog"
47-
break
48-
if "linenum" in node or "diff" in node:
49-
parsername = "Duroc Hog"
50-
break
51-
if "issue_id" in node or "location" in node:
52-
parsername = "Gottingen Hog"
53-
break
54-
if "page_id" in node:
55-
parsername = "Essex Hog"
56-
break
36+
if scan_type == "Rusty Hog Scan":
37+
parsername = "Rusty Hog"
38+
for node in tree:
39+
if (
40+
"commit" in node
41+
or "commitHash" in node
42+
or "parent_commit_hash" in node
43+
or "old_file_id" in node
44+
or "new_file_id" in node
45+
):
46+
parsername = "Choctaw Hog"
47+
break
48+
if "linenum" in node or "diff" in node:
49+
parsername = "Duroc Hog"
50+
break
51+
if "issue_id" in node or "location" in node:
52+
parsername = "Gottingen Hog"
53+
break
54+
if "page_id" in node:
55+
parsername = "Essex Hog"
56+
break
57+
else:
58+
parsername = scan_type.replace(" Scan", "")
5759
test = ParserTest(
5860
name=parsername,
5961
parser_type=parsername,
6062
version="",
6163
)
62-
if (
63-
parsername == "Rusty Hog"
64-
): # The outputfile is empty. A subscanner can't be classified
64+
if parsername == "Rusty Hog": # The outputfile is empty. A subscanner can't be classified
6565
test.description = "The exact scanner within Rusty Hog could not be determined due to missing information within the scan result."
6666
else:
6767
test.description = parsername
68-
test.findings = self.__getitem(
69-
vulnerabilities=tree, scanner=parsername,
70-
)
68+
test.findings = self.__getitem(vulnerabilities=tree, scanner=parsername)
7169
tests.append(test)
7270
return tests
7371

@@ -85,13 +83,15 @@ def __getitem(self, vulnerabilities, scanner):
8583
break
8684
if scanner == "Choctaw Hog":
8785
"""Choctaw Hog"""
86+
if vulnerability.get("commitHash") is None:
87+
raise ValueError("You chose the wrong scan type: " + scanner + ". A commitHash is expected.")
8888
found_secret_string = str(vulnerability.get("stringsFound") or "")
89-
description += f"**This string was found:** {found_secret_string}"
89+
description += f"\n**This string was found:** {found_secret_string}"
9090
if vulnerability.get("commit") is not None:
9191
description += "\n**Commit message:** {}".format(
9292
vulnerability.get("commit"),
9393
)
94-
if vulnerability.get("commitHash") is not None:
94+
if vulnerability.get("commitHash"):
9595
description += "\n**Commit hash:** {}".format(
9696
vulnerability.get("commitHash"),
9797
)
@@ -121,13 +121,15 @@ def __getitem(self, vulnerabilities, scanner):
121121
)
122122
elif scanner == "Duroc Hog":
123123
"""Duroc Hog"""
124+
if vulnerability.get("linenum") is None:
125+
raise ValueError("You chose the wrong scan type: " + scanner + ". A linenum is expected.")
124126
found_secret_string = str(vulnerability.get("stringsFound") or "")
125-
description += f"**This string was found:** {found_secret_string}"
127+
description += f"\n**This string was found:** {found_secret_string}"
126128
if vulnerability.get("path") is not None:
127129
description += "\n**Path of Issue:** {}".format(
128130
vulnerability.get("path"),
129131
)
130-
if vulnerability.get("linenum") is not None:
132+
if vulnerability.get("linenum"):
131133
description += "\n**Linenum of Issue:** {}".format(
132134
vulnerability.get("linenum"),
133135
)
@@ -137,9 +139,11 @@ def __getitem(self, vulnerabilities, scanner):
137139
)
138140
elif scanner == "Gottingen Hog":
139141
"""Gottingen Hog"""
142+
if vulnerability.get("issue_id") is None:
143+
raise ValueError("You chose the wrong scan type: " + scanner + ". An issue_id is expected.")
140144
found_secret_string = str(vulnerability.get("stringsFound") or "")
141-
description += f"**This string was found:** {found_secret_string}"
142-
if vulnerability.get("issue_id") is not None:
145+
description += f"\n**This string was found:** {found_secret_string}"
146+
if vulnerability.get("issue_id"):
143147
description += "\n**JIRA Issue ID:** {}".format(
144148
vulnerability.get("issue_id"),
145149
)
@@ -152,9 +156,11 @@ def __getitem(self, vulnerabilities, scanner):
152156
vulnerability.get("url"), vulnerability.get("url"),
153157
)
154158
elif scanner == "Essex Hog":
159+
if vulnerability.get("page_id") is None:
160+
raise ValueError("You chose the wrong scan type: " + scanner + ". A page_id is expected.")
155161
found_secret_string = str(vulnerability.get("stringsFound") or "")
156-
description += f"**This string was found:** {found_secret_string}"
157-
if vulnerability.get("page_id") is not None:
162+
description += f"\n**This string was found:** {found_secret_string}"
163+
if vulnerability.get("page_id"):
158164
description += "\n**Confluence URL:** [{}]({})".format(
159165
vulnerability.get("url"), vulnerability.get("url"),
160166
)

unittests/tools/test_rusty_hog_parser.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from dojo.models import Test
21
from dojo.tools.rusty_hog.parser import RustyhogParser
32
from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path
43

@@ -7,25 +6,25 @@ class TestRustyhogParser(DojoTestCase):
76
def test_parse_file_with_no_vuln_has_no_finding_choctawhog(self):
87
with open(get_unit_tests_scans_path("rusty_hog") / "choctawhog_no_vuln.json", encoding="utf-8") as testfile:
98
parser = RustyhogParser()
10-
findings = parser.get_items(testfile, "Rusty Hog", Test()) # The outputfile is empty. A subscanner can't be classified
9+
findings = parser.get_findings(testfile, "Rusty Hog") # The outputfile is empty. A subscanner can't be classified
1110
self.assertEqual(0, len(findings))
1211

1312
def test_parse_file_with_one_vuln_has_one_finding_choctawhog(self):
1413
with open(get_unit_tests_scans_path("rusty_hog") / "choctawhog_one_vuln.json", encoding="utf-8") as testfile:
1514
parser = RustyhogParser()
16-
findings = parser.get_items(testfile, "Choctaw Hog", Test())
15+
findings = parser.get_findings(testfile, "Choctaw Hog")
1716
self.assertEqual(1, len(findings))
1817

1918
def test_parse_file_with_multiple_vuln_has_multiple_finding_choctawhog(self):
2019
with open(get_unit_tests_scans_path("rusty_hog") / "choctawhog_many_vulns.json", encoding="utf-8") as testfile:
2120
parser = RustyhogParser()
22-
findings = parser.get_items(testfile, "Choctaw Hog", Test())
21+
findings = parser.get_findings(testfile, "Choctaw Hog")
2322
self.assertEqual(13, len(findings))
2423

2524
def test_parse_file_with_multiple_vuln_has_multiple_finding_choctawhog_content(self):
2625
with open(get_unit_tests_scans_path("rusty_hog") / "choctawhog_many_vulns.json", encoding="utf-8") as testfile:
2726
parser = RustyhogParser()
28-
findings = parser.get_items(testfile, "Choctaw Hog", Test())
27+
findings = parser.get_findings(testfile, "Choctaw Hog")
2928
self.assertEqual(findings[0].title, "Email address found in Git path .github/workflows/main.yml (a7bce96377c4ff2ac16cd51fb0da7fe7ea678829)")
3029
self.assertIn("**This string was found:** ['dojo-helpers@this-repo.com']", findings[0].description)
3130
self.assertIn("**Commit message:** removing action", findings[0].description)
@@ -38,25 +37,25 @@ def test_parse_file_with_multiple_vuln_has_multiple_finding_choctawhog_content(s
3837
def test_parse_file_with_no_vuln_has_no_finding_duorchog(self):
3938
with open(get_unit_tests_scans_path("rusty_hog") / "durochog_no_vuln.json", encoding="utf-8") as testfile:
4039
parser = RustyhogParser()
41-
findings = parser.get_items(testfile, "Rusty Hog", Test()) # The outputfile is empty. A subscanner can't be classified
40+
findings = parser.get_findings(testfile, "Rusty Hog") # The outputfile is empty. A subscanner can't be classified
4241
self.assertEqual(0, len(findings))
4342

4443
def test_parse_file_with_one_vuln_has_one_finding_durochog(self):
4544
with open(get_unit_tests_scans_path("rusty_hog") / "durochog_one_vuln.json", encoding="utf-8") as testfile:
4645
parser = RustyhogParser()
47-
findings = parser.get_items(testfile, "Duroc Hog", Test())
46+
findings = parser.get_findings(testfile, "Duroc Hog")
4847
self.assertEqual(1, len(findings))
4948

5049
def test_parse_file_with_multiple_vuln_has_multiple_finding_durochog(self):
5150
with open(get_unit_tests_scans_path("rusty_hog") / "durochog_many_vulns.json", encoding="utf-8") as testfile:
5251
parser = RustyhogParser()
53-
findings = parser.get_items(testfile, "Duroc Hog", Test())
52+
findings = parser.get_findings(testfile, "Duroc Hog")
5453
self.assertEqual(4, len(findings))
5554

5655
def test_parse_file_with_multiple_vuln_has_multiple_finding_durochog_content(self):
5756
with open(get_unit_tests_scans_path("rusty_hog") / "durochog_many_vulns.json", encoding="utf-8") as testfile:
5857
parser = RustyhogParser()
59-
findings = parser.get_items(testfile, "Duroc Hog", Test())
58+
findings = parser.get_findings(testfile, "Duroc Hog")
6059
self.assertEqual(findings[0].title, "password (Password) found in path /scan_folder/unittests/scans/sonarqube/sonar-no-finding.html")
6160
self.assertIn("**This string was found:** ['password = getEncryptedPass()']", findings[0].description)
6261
self.assertIn("**Path of Issue:** /scan_folder/unittests/scans/sonarqube/sonar-no-finding.html", findings[0].description)
@@ -67,25 +66,25 @@ def test_parse_file_with_multiple_vuln_has_multiple_finding_durochog_content(sel
6766
def test_parse_file_with_no_vuln_has_no_finding_gottingenhog(self):
6867
with open(get_unit_tests_scans_path("rusty_hog") / "gottingenhog_no_vuln.json", encoding="utf-8") as testfile:
6968
parser = RustyhogParser()
70-
findings = parser.get_items(testfile, "Rusty Hog", Test()) # The outputfile is empty. A subscanner can't be classified
69+
findings = parser.get_findings(testfile, "Rusty Hog") # The outputfile is empty. A subscanner can't be classified
7170
self.assertEqual(0, len(findings))
7271

7372
def test_parse_file_with_one_vuln_has_one_finding_gottingenhog(self):
7473
with open(get_unit_tests_scans_path("rusty_hog") / "gottingenhog_one_vuln.json", encoding="utf-8") as testfile:
7574
parser = RustyhogParser()
76-
findings = parser.get_items(testfile, "Gottingen Hog", Test())
75+
findings = parser.get_findings(testfile, "Gottingen Hog")
7776
self.assertEqual(1, len(findings))
7877

7978
def test_parse_file_with_multiple_vuln_has_multiple_finding_gottingenhog(self):
8079
with open(get_unit_tests_scans_path("rusty_hog") / "gottingenhog_many_vulns.json", encoding="utf-8") as testfile:
8180
parser = RustyhogParser()
82-
findings = parser.get_items(testfile, "Gottingen Hog", Test())
81+
findings = parser.get_findings(testfile, "Gottingen Hog")
8382
self.assertEqual(10, len(findings))
8483

8584
def test_parse_file_with_multiple_vuln_has_multiple_finding_gottingenhog_content(self):
8685
with open(get_unit_tests_scans_path("rusty_hog") / "gottingenhog_many_vulns.json", encoding="utf-8") as testfile:
8786
parser = RustyhogParser()
88-
findings = parser.get_items(testfile, "Gottingen Hog", Test())
87+
findings = parser.get_findings(testfile, "Gottingen Hog")
8988
self.assertEqual(findings[0].title, "password found in Jira ID TEST-123 (Issue Description)")
9089
self.assertIn("**This string was found:** ['password: jeans']", findings[0].description)
9190
self.assertIn("**JIRA Issue ID:** TEST-123", findings[0].description)
@@ -96,19 +95,19 @@ def test_parse_file_with_multiple_vuln_has_multiple_finding_gottingenhog_content
9695
def test_parse_file_with_no_vuln_has_no_finding_essexhog(self):
9796
with open(get_unit_tests_scans_path("rusty_hog") / "essexhog_no_vuln.json", encoding="utf-8") as testfile:
9897
parser = RustyhogParser()
99-
findings = parser.get_items(testfile, "Rusty Hog", Test()) # The outputfile is empty. A subscanner can't be classified
98+
findings = parser.get_findings(testfile, "Rusty Hog") # The outputfile is empty. A subscanner can't be classified
10099
self.assertEqual(0, len(findings))
101100

102101
def test_parse_file_with_one_vuln_has_one_finding_essexhog(self):
103102
with open(get_unit_tests_scans_path("rusty_hog") / "essexhog_one_vuln.json", encoding="utf-8") as testfile:
104103
parser = RustyhogParser()
105-
findings = parser.get_items(testfile, "Essex Hog", Test())
104+
findings = parser.get_findings(testfile, "Essex Hog")
106105
self.assertEqual(1, len(findings))
107106

108107
def test_parse_file_with_multiple_vuln_has_multiple_finding_essexhog(self):
109108
with open(get_unit_tests_scans_path("rusty_hog") / "essexhog_many_vulns.json", encoding="utf-8") as testfile:
110109
parser = RustyhogParser()
111-
findings = parser.get_items(testfile, "Essex Hog", Test())
110+
findings = parser.get_findings(testfile, "Essex Hog")
112111
self.assertEqual(3, len(findings))
113112
self.assertEqual("https://confluence.com/pages/viewpage.action?pageId=12345", findings[0].file_path)
114113
self.assertEqual("['-----BEGIN EC PRIVATE KEY-----']", findings[0].payload)
@@ -117,7 +116,7 @@ def test_parse_file_with_multiple_vuln_has_multiple_finding_essexhog(self):
117116
def test_parse_file_with_multiple_vuln_has_multiple_finding_essexhog_content(self):
118117
with open(get_unit_tests_scans_path("rusty_hog") / "essexhog_many_vulns.json", encoding="utf-8") as testfile:
119118
parser = RustyhogParser()
120-
findings = parser.get_items(testfile, "Essex Hog", Test())
119+
findings = parser.get_findings(testfile, "Essex Hog")
121120
self.assertEqual(findings[0].title, "SSH (EC) private key found in Confluence Page ID 12345")
122121
self.assertIn("-----BEGIN EC PRIVATE KEY-----", findings[0].description)
123122
self.assertIn("**Confluence URL:** [https://confluence.com/pages/viewpage.action?pageId=12345](https://confluence.com/pages/viewpage.action?pageId=12345)", findings[0].description)

0 commit comments

Comments
 (0)