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
Empty file.
130 changes: 130 additions & 0 deletions dojo/tools/betterleaks/parser.py
Original file line number Diff line number Diff line change
@@ -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"""

Check failure on line 10 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (incorrect-blank-line-before-class)

dojo/tools/betterleaks/parser.py:10:5: incorrect-blank-line-before-class: 1 blank line required before class docstring help: Insert 1 blank line before class docstring

Check failure on line 10 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (incorrect-blank-line-before-class)

dojo/tools/betterleaks/parser.py:10:5: incorrect-blank-line-before-class: 1 blank line required before class docstring help: Insert 1 blank line before class docstring

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]

Check failure on line 95 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (slice-to-remove-prefix-or-suffix)

dojo/tools/betterleaks/parser.py:94:9: slice-to-remove-prefix-or-suffix: Prefer `str.removesuffix()` over conditionally replacing with slice. help: Use removesuffix instead of assignment conditional upon endswith.

Check failure on line 95 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (slice-to-remove-prefix-or-suffix)

dojo/tools/betterleaks/parser.py:94:9: slice-to-remove-prefix-or-suffix: Prefer `str.removesuffix()` over conditionally replacing with slice. help: Use removesuffix instead of assignment conditional upon endswith.

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

Check failure on line 130 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (missing-newline-at-end-of-file)

dojo/tools/betterleaks/parser.py:130:38: missing-newline-at-end-of-file: No newline at end of file help: Add trailing newline

Check failure on line 130 in dojo/tools/betterleaks/parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (missing-newline-at-end-of-file)

dojo/tools/betterleaks/parser.py:130:38: missing-newline-at-end-of-file: No newline at end of file help: Add trailing newline
34 changes: 34 additions & 0 deletions unittests/scans/betterleaks/betterleaks_one_finding.json
Original file line number Diff line number Diff line change
@@ -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": ""
}
]
16 changes: 16 additions & 0 deletions unittests/tools/test_betterleaks_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.test import TestCase
from dojo.models import Test
from dojo.tools.betterleaks.parser import BetterleaksParser

Check failure on line 3 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (unsorted-imports)

unittests/tools/test_betterleaks_parser.py:1:1: unsorted-imports: Import block is un-sorted or un-formatted help: Organize imports

Check failure on line 3 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (unsorted-imports)

unittests/tools/test_betterleaks_parser.py:1:1: unsorted-imports: Import block is un-sorted or un-formatted help: Organize imports


class TestBetterleaksParser(TestCase):
def test_betterleaks_parser_one_finding(self):
testfile = open("unittests/scans/betterleaks/betterleaks_one_finding.json")

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (builtin-open)

unittests/tools/test_betterleaks_parser.py:8:20: builtin-open: `open()` should be replaced by `Path.open()` help: Replace with `Path.open()`

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (open-file-with-context-handler)

unittests/tools/test_betterleaks_parser.py:8:20: open-file-with-context-handler: Use a context manager for opening files

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (unspecified-encoding)

unittests/tools/test_betterleaks_parser.py:8:20: unspecified-encoding: `open` in text mode without explicit `encoding` argument help: Add explicit `encoding` argument

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (builtin-open)

unittests/tools/test_betterleaks_parser.py:8:20: builtin-open: `open()` should be replaced by `Path.open()` help: Replace with `Path.open()`

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (open-file-with-context-handler)

unittests/tools/test_betterleaks_parser.py:8:20: open-file-with-context-handler: Use a context manager for opening files

Check failure on line 8 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (unspecified-encoding)

unittests/tools/test_betterleaks_parser.py:8:20: unspecified-encoding: `open` in text mode without explicit `encoding` argument help: Add explicit `encoding` argument
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)

Check failure on line 16 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff-linting

ruff (missing-newline-at-end-of-file)

unittests/tools/test_betterleaks_parser.py:16:43: missing-newline-at-end-of-file: No newline at end of file help: Add trailing newline

Check failure on line 16 in unittests/tools/test_betterleaks_parser.py

View workflow job for this annotation

GitHub Actions / ruff / ruff-linting

ruff (missing-newline-at-end-of-file)

unittests/tools/test_betterleaks_parser.py:16:43: missing-newline-at-end-of-file: No newline at end of file help: Add trailing newline
Loading