From cdd30c002dbf780265fa9c0889f80e0c5b5f64be Mon Sep 17 00:00:00 2001 From: siddhardh-codemonk Date: Sun, 13 Sep 2026 16:19:56 +0530 Subject: [PATCH] feat(parser): add Betterleaks scanner parser --- dojo/tools/betterleaks/__init__.py | 0 dojo/tools/betterleaks/parser.py | 130 ++++++++++++++++++ .../betterleaks/betterleaks_one_finding.json | 34 +++++ unittests/tools/test_betterleaks_parser.py | 16 +++ 4 files changed, 180 insertions(+) create mode 100644 dojo/tools/betterleaks/__init__.py create mode 100644 dojo/tools/betterleaks/parser.py create mode 100644 unittests/scans/betterleaks/betterleaks_one_finding.json create mode 100644 unittests/tools/test_betterleaks_parser.py diff --git a/dojo/tools/betterleaks/__init__.py b/dojo/tools/betterleaks/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/betterleaks/parser.py b/dojo/tools/betterleaks/parser.py new file mode 100644 index 00000000000..7490d122ae2 --- /dev/null +++ b/dojo/tools/betterleaks/parser.py @@ -0,0 +1,130 @@ +import hashlib +import json + +from dojo.location.feature import locations_enabled +from dojo.models import Finding +from dojo.tools.locations import LocationData + + +class BetterleaksParser: + """A class that can be used to parse the Betterleaks JSON report files""" + + def get_fields(self) -> list[str]: + return [ + "title", + "description", + "severity", + "file_path", + "line", + "dynamic_finding", + "static_finding", + "nb_occurences", + ] + + def get_dedupe_fields(self) -> list[str]: + return [ + "title", + "line", + "file_path", + "description", + ] + + def get_scan_types(self): + return ["Betterleaks Scan"] + + def get_label_for_scan_types(self, scan_type): + return scan_type + + def get_description_for_scan_types(self, scan_type): + return "Import Betterleaks Scan findings in JSON format." + + def get_findings(self, filename, test): + """Converts a Betterleaks report to DefectDojo findings""" + issues = json.load(filename) + if issues is None: + return [] + + dupes = {} + + for issue in issues: + if issue.get("Description") or issue.get("RuleID"): + self.get_finding(issue, test, dupes) + else: + msg = "Format is not recognized for Betterleaks" + raise ValueError(msg) + + return list(dupes.values()) + + def get_finding(self, issue, test, dupes): + reason = issue.get("Description", "Secret Leak") + line = issue.get("StartLine") + line = int(line) if line else 0 + match = issue.get("Match") + secret = issue.get("Secret") + file_path = issue.get("File") + commit = issue.get("Commit") + date = issue.get("Date") + message = issue.get("Message") + tags = issue.get("Tags") + rule_id = issue.get("RuleID") + + title = f"Hard coded {reason} found in {file_path}" + + description = "" + if secret: + description += f"**Secret:** {secret}\n" + if match: + description += f"**Match:** {match}\n" + if message: + if len(message.split("\n")) > 1: + description += ( + "**Commit message:**\n```\n" + + message.replace("```", "\\`\\`\\`") + + "\n```\n" + ) + else: + description += f"**Commit message:** {message}\n" + if commit: + description += f"**Commit hash:** {commit}\n" + if date: + description += f"**Commit date:** {date}\n" + if rule_id: + description += f"**Rule Id:** {rule_id}\n" + + if description.endswith("\n"): + description = description[:-1] + + severity = "High" + + dupe_key = hashlib.md5( + (title + (secret or "") + str(line)).encode("utf-8"), + usedforsecurity=False, + ).hexdigest() + + if dupe_key in dupes: + finding = dupes[dupe_key] + finding.description = ( + finding.description + "\n\n***\n\n" + description + ) + finding.nb_occurences += 1 + dupes[dupe_key] = finding + else: + finding = Finding( + title=title, + test=test, + cwe=798, # Use of Hard-coded Credentials + description=description, + severity=severity, + file_path=file_path, + line=line, + dynamic_finding=False, + static_finding=True, + nb_occurences=1, + ) + if tags: + finding.unsaved_tags = tags + if locations_enabled() and file_path: + finding.unsaved_locations.append( + LocationData.code(file_path=file_path, line=line), + ) + dupes[dupe_key] = finding \ No newline at end of file diff --git a/unittests/scans/betterleaks/betterleaks_one_finding.json b/unittests/scans/betterleaks/betterleaks_one_finding.json new file mode 100644 index 00000000000..9c5d2f5faf8 --- /dev/null +++ b/unittests/scans/betterleaks/betterleaks_one_finding.json @@ -0,0 +1,34 @@ +[ + { + "RuleID": "generic-api-key", + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 53, + "EndLine": 53, + "StartColumn": 9, + "EndColumn": 37, + "Match": "XXXXXXXXX", + "Secret": "XXXXXXXXX", + "Attributes": { + "confidence": "low", + "git.author_email": "XXXXXXX", + "git.author_name": "XXXXXXX", + "git.date": "2026-04-23T14:25:51Z", + "git.message": "", + "git.platform": "unknown", + "git.remote_url": "XXXXXXXX", + "git.sha": "XXXXXXXXX", + "path": "XXXXXXXXX", + "resource": "git.patch_content" + }, + "Tags": [], + "Fingerprint": "XXXXXXX:file.cfg:generic-api-key:53", + "File": "file.cfg", + "SymlinkFile": "", + "Commit": "XXXXXXXX", + "Entropy": 3.625, + "Author": "XXXXXXXXXX", + "Email": "XXXXXXXXXX", + "Date": "2026-04-23T14:25:51Z", + "Message": "" + } +] \ No newline at end of file diff --git a/unittests/tools/test_betterleaks_parser.py b/unittests/tools/test_betterleaks_parser.py new file mode 100644 index 00000000000..a5578a94aae --- /dev/null +++ b/unittests/tools/test_betterleaks_parser.py @@ -0,0 +1,16 @@ +from django.test import TestCase +from dojo.models import Test +from dojo.tools.betterleaks.parser import BetterleaksParser + + +class TestBetterleaksParser(TestCase): + def test_betterleaks_parser_one_finding(self): + testfile = open("unittests/scans/betterleaks/betterleaks_one_finding.json") + parser = BetterleaksParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(len(findings), 1) + finding = findings[0] + self.assertEqual(finding.severity, "High") + self.assertEqual(finding.file_path, "file.cfg") + self.assertEqual(finding.line, 53) + self.assertEqual(finding.cwe, 798) \ No newline at end of file