From 1fb94a689c146a9dfd4004131d36de3ac82bdffc Mon Sep 17 00:00:00 2001 From: Zeke Date: Tue, 30 Sep 2025 11:59:00 -0400 Subject: [PATCH 1/4] Add GitHub secrets detection parser and tests --- .../__init__.py | 0 .../github_secrets_detection_report/parser.py | 151 ++++++ ...hub_secrets_detection_report_many_vul.json | 431 ++++++++++++++++++ ...thub_secrets_detection_report_one_vul.json | 164 +++++++ ...hub_secrets_detection_report_zero_vul.json | 1 + ..._github_secrets_detection_report_parser.py | 109 +++++ 6 files changed, 856 insertions(+) create mode 100644 dojo/tools/github_secrets_detection_report/__init__.py create mode 100644 dojo/tools/github_secrets_detection_report/parser.py create mode 100644 unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json create mode 100644 unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json create mode 100644 unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json create mode 100644 unittests/tools/test_github_secrets_detection_report_parser.py diff --git a/dojo/tools/github_secrets_detection_report/__init__.py b/dojo/tools/github_secrets_detection_report/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/tools/github_secrets_detection_report/parser.py b/dojo/tools/github_secrets_detection_report/parser.py new file mode 100644 index 00000000000..5f4198b620a --- /dev/null +++ b/dojo/tools/github_secrets_detection_report/parser.py @@ -0,0 +1,151 @@ +import json +from dojo.models import Finding + + +class GithubSecretsDetectionReportParser(object): + """ + Import secrets detection report from GitHub + """ + + def get_scan_types(self): + return ["Github Secrets Detection Report Scan"] + + def get_label_for_scan_types(self, scan_type): + return "Github Secrets Detection Report Scan" + + def get_description_for_scan_types(self, scan_type): + return "Github Secrets Detection Report report file can be imported in JSON format (option --json)." + + def get_findings(self, file, test): + data = json.load(file) + + if not isinstance(data, list): + error_msg = "Invalid GitHub secrets detection report format, expected a JSON list of alerts." + raise TypeError(error_msg) + + findings = [] + for alert in data: + # Extract basic alert information + alert_number = alert.get("number") + state = alert.get("state", "open") + secret_type = alert.get("secret_type", "Unknown") + secret_type_display_name = alert.get("secret_type_display_name", secret_type) + html_url = alert.get("html_url", "") + + # Create title + title = f"Exposed Secret Detected: {secret_type_display_name}" + + # Build description + desc_lines = [] + if html_url: + desc_lines.append(f"**GitHub Alert**: [{html_url}]({html_url})") + + desc_lines.append(f"**Secret Type**: {secret_type_display_name}") + desc_lines.append(f"**Alert State**: {state}") + + # Add repository information + repository = alert.get("repository", {}) + if repository: + repo_full_name = repository.get("full_name") + if repo_full_name: + desc_lines.append(f"**Repository**: {repo_full_name}") + + # Add location information + first_location = alert.get("first_location_detected", {}) + if first_location: + file_path = first_location.get("path") + start_line = first_location.get("start_line") + end_line = first_location.get("end_line") + + if file_path: + desc_lines.append(f"**File Path**: {file_path}") + if start_line: + if end_line and end_line != start_line: + desc_lines.append(f"**Lines**: {start_line}-{end_line}") + else: + desc_lines.append(f"**Line**: {start_line}") + + # Add resolution information + resolution = alert.get("resolution") + if resolution: + desc_lines.append(f"**Resolution**: {resolution}") + + resolved_by = alert.get("resolved_by") + if resolved_by: + resolved_by_login = resolved_by.get("login", "Unknown") + desc_lines.append(f"**Resolved By**: {resolved_by_login}") + + resolved_at = alert.get("resolved_at") + if resolved_at: + desc_lines.append(f"**Resolved At**: {resolved_at}") + + resolution_comment = alert.get("resolution_comment") + if resolution_comment: + desc_lines.append(f"**Resolution Comment**: {resolution_comment}") + + # Add push protection information + push_protection_bypassed = alert.get("push_protection_bypassed", False) + if push_protection_bypassed: + desc_lines.append("**Push Protection Bypassed**: True") + + bypassed_by = alert.get("push_protection_bypassed_by") + if bypassed_by: + bypassed_by_login = bypassed_by.get("login", "Unknown") + desc_lines.append(f"**Bypassed By**: {bypassed_by_login}") + + bypassed_at = alert.get("push_protection_bypassed_at") + if bypassed_at: + desc_lines.append(f"**Bypassed At**: {bypassed_at}") + else: + desc_lines.append("**Push Protection Bypassed**: False") + + # Add additional metadata + validity = alert.get("validity", "unknown") + desc_lines.append(f"**Validity**: {validity}") + + publicly_leaked = alert.get("publicly_leaked", False) + desc_lines.append(f"**Publicly Leaked**: {'Yes' if publicly_leaked else 'No'}") + + multi_repo = alert.get("multi_repo", False) + desc_lines.append(f"**Multi-Repository**: {'Yes' if multi_repo else 'No'}") + + has_more_locations = alert.get("has_more_locations", False) + if has_more_locations: + desc_lines.append("**Note**: This secret has been detected in multiple locations") + + description = "\n\n".join(desc_lines) + + # Determine severity based on state and other factors + if state == "resolved": + severity = "Info" + else: + if validity == "active" and publicly_leaked: + severity = "Critical" + elif validity == "active": + severity = "High" + else: + severity = "Medium" + + # Create finding + finding = Finding( + title=title, + test=test, + description=description, + severity=severity, + static_finding=True, + dynamic_finding=False, + vuln_id_from_tool=str(alert_number) if alert_number else None, + ) + + # Set file path and line information + if first_location: + finding.file_path = first_location.get("path") + finding.line = first_location.get("start_line") + + # Set external URL + if html_url: + finding.url = html_url + + findings.append(finding) + + return findings diff --git a/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json new file mode 100644 index 00000000000..bc8c17b6236 --- /dev/null +++ b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json @@ -0,0 +1,431 @@ +[ + { + "number":2, + "created_at":"2020-11-06T18:48:51Z", + "url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2", + "html_url":"https://github.com/owner/private-repo/security/secret-scanning/2", + "locations_url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2/locations", + "state":"resolved", + "resolution":"false_positive", + "resolved_at":"2020-11-07T02:47:13Z", + "resolved_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "secret_type":"adafruit_io_key", + "secret_type_display_name":"Adafruit IO Key", + "secret":"aio_XXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "repository":{ + "id":1296269, + "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name":"Hello-World", + "full_name":"octocat/Hello-World", + "owner":{ + "login":"octocat", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://github.com/images/error/octocat_happy.gif", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":false + }, + "private":false, + "html_url":"https://github.com/octocat/Hello-World", + "description":"This your first repo!", + "fork":false, + "url":"https://api.github.com/repos/octocat/Hello-World", + "archive_url":"https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url":"https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url":"https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url":"https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url":"https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url":"https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url":"https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url":"https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url":"https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url":"https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url":"https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url":"https://api.github.com/repos/octocat/Hello-World/events", + "forks_url":"https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url":"https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url":"https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url":"https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url":"https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url":"https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url":"https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url":"https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url":"https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url":"https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url":"https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url":"https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url":"https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url":"https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url":"https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url":"https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url":"https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url":"https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url":"https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url":"https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url":"https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url":"https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url":"https://api.github.com/repos/octocat/Hello-World/hooks" + }, + "push_protection_bypassed_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypassed":true, + "push_protection_bypassed_at":"2020-11-06T21:48:51Z", + "push_protection_bypass_request_reviewer":{ + "login":"octocat", + "id":3, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/3?", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypass_request_reviewer_comment":"Example response", + "push_protection_bypass_request_comment":"Example comment", + "push_protection_bypass_request_html_url":"https://github.com/owner/repo/secret_scanning_exemptions/1", + "resolution_comment":"Example comment", + "validity":"active", + "publicly_leaked":false, + "multi_repo":false, + "is_base64_encoded":false, + "first_location_detected":{ + "path":"/example/secrets.txt", + "start_line":1, + "end_line":1, + "start_column":1, + "end_column":64, + "blob_sha":"af5626b4a114abcb82d63db7c8082c3c4756e51b", + "blob_url":"https://api.github.com/repos/octocat/hello-world/git/blobs/af5626b4a114abcb82d63db7c8082c3c4756e51b", + "commit_sha":"f14d7debf9775f957cf4f1e8176da0786431f72b", + "commit_url":"https://api.github.com/repos/octocat/hello-world/git/commits/f14d7debf9775f957cf4f1e8176da0786431f72b" + }, + "has_more_locations":true + }, + { + "number":3, + "created_at":"2021-03-15T10:30:25Z", + "url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/3", + "html_url":"https://github.com/owner/private-repo/security/secret-scanning/3", + "locations_url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/3/locations", + "state":"open", + "resolution":null, + "resolved_at":null, + "resolved_by":null, + "secret_type":"aws_access_key_id", + "secret_type_display_name":"AWS Access Key ID", + "secret":"AKIAIOSFODNN7EXAMPLE", + "repository":{ + "id":1296269, + "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name":"Hello-World", + "full_name":"octocat/Hello-World", + "owner":{ + "login":"octocat", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://github.com/images/error/octocat_happy.gif", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":false + }, + "private":false, + "html_url":"https://github.com/octocat/Hello-World", + "description":"This your first repo!", + "fork":false, + "url":"https://api.github.com/repos/octocat/Hello-World", + "archive_url":"https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url":"https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url":"https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url":"https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url":"https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url":"https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url":"https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url":"https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url":"https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url":"https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url":"https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url":"https://api.github.com/repos/octocat/Hello-World/events", + "forks_url":"https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url":"https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url":"https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url":"https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url":"https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url":"https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url":"https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url":"https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url":"https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url":"https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url":"https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url":"https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url":"https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url":"https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url":"https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url":"https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url":"https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url":"https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url":"https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url":"https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url":"https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url":"https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url":"https://api.github.com/repos/octocat/Hello-World/hooks" + }, + "push_protection_bypassed_by":null, + "push_protection_bypassed":false, + "push_protection_bypassed_at":null, + "push_protection_bypass_request_reviewer":null, + "push_protection_bypass_request_reviewer_comment":null, + "push_protection_bypass_request_comment":null, + "push_protection_bypass_request_html_url":null, + "resolution_comment":null, + "validity":"active", + "publicly_leaked":true, + "multi_repo":false, + "is_base64_encoded":false, + "first_location_detected":{ + "path":"/config/aws-credentials.env", + "start_line":5, + "end_line":5, + "start_column":15, + "end_column":40, + "blob_sha":"b5626b4a114abcb82d63db7c8082c3c4756e51c", + "blob_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs/b5626b4a114abcb82d63db7c8082c3c4756e51c", + "commit_sha":"g14d7debf9775f957cf4f1e8176da0786431f72c", + "commit_url":"https://api.github.com/repos/octocat/Hello-World/git/commits/g14d7debf9775f957cf4f1e8176da0786431f72c" + }, + "has_more_locations":false + }, + { + "number":4, + "created_at":"2021-06-20T14:45:10Z", + "url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/4", + "html_url":"https://github.com/owner/private-repo/security/secret-scanning/4", + "locations_url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/4/locations", + "state":"resolved", + "resolution":"revoked", + "resolved_at":"2021-06-21T09:15:30Z", + "resolved_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "secret_type":"github_personal_access_token", + "secret_type_display_name":"GitHub Personal Access Token", + "secret":"ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "repository":{ + "id":1296269, + "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name":"Hello-World", + "full_name":"octocat/Hello-World", + "owner":{ + "login":"octocat", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://github.com/images/error/octocat_happy.gif", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":false + }, + "private":false, + "html_url":"https://github.com/octocat/Hello-World", + "description":"This your first repo!", + "fork":false, + "url":"https://api.github.com/repos/octocat/Hello-World", + "archive_url":"https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url":"https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url":"https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url":"https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url":"https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url":"https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url":"https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url":"https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url":"https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url":"https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url":"https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url":"https://api.github.com/repos/octocat/Hello-World/events", + "forks_url":"https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url":"https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url":"https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url":"https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url":"https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url":"https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url":"https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url":"https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url":"https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url":"https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url":"https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url":"https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url":"https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url":"https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url":"https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url":"https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url":"https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url":"https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url":"https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url":"https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url":"https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url":"https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url":"https://api.github.com/repos/octocat/Hello-World/hooks" + }, + "push_protection_bypassed_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypassed":true, + "push_protection_bypassed_at":"2021-06-20T17:45:10Z", + "push_protection_bypass_request_reviewer":{ + "login":"octocat", + "id":3, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/3?", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypass_request_reviewer_comment":"Approved for emergency deployment", + "push_protection_bypass_request_comment":"Emergency deployment required", + "push_protection_bypass_request_html_url":"https://github.com/owner/repo/secret_scanning_exemptions/2", + "resolution_comment":"Token has been revoked and regenerated", + "validity":"inactive", + "publicly_leaked":false, + "multi_repo":true, + "is_base64_encoded":false, + "first_location_detected":{ + "path":"/scripts/deploy.sh", + "start_line":12, + "end_line":12, + "start_column":8, + "end_column":50, + "blob_sha":"c5626b4a114abcb82d63db7c8082c3c4756e51d", + "blob_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs/c5626b4a114abcb82d63db7c8082c3c4756e51d", + "commit_sha":"h14d7debf9775f957cf4f1e8176da0786431f72d", + "commit_url":"https://api.github.com/repos/octocat/Hello-World/git/commits/h14d7debf9775f957cf4f1e8176da0786431f72d" + }, + "has_more_locations":true + } +] \ No newline at end of file diff --git a/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json new file mode 100644 index 00000000000..ad97f2b13d9 --- /dev/null +++ b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json @@ -0,0 +1,164 @@ +[ + { + "number":2, + "created_at":"2020-11-06T18:48:51Z", + "url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2", + "html_url":"https://github.com/owner/private-repo/security/secret-scanning/2", + "locations_url":"https://api.github.com/repos/owner/private-repo/secret-scanning/alerts/2/locations", + "state":"resolved", + "resolution":"false_positive", + "resolved_at":"2020-11-07T02:47:13Z", + "resolved_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "secret_type":"adafruit_io_key", + "secret_type_display_name":"Adafruit IO Key", + "secret":"aio_XXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "repository":{ + "id":1296269, + "node_id":"MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name":"Hello-World", + "full_name":"octocat/Hello-World", + "owner":{ + "login":"octocat", + "id":1, + "node_id":"MDQ6VXNlcjE=", + "avatar_url":"https://github.com/images/error/octocat_happy.gif", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":false + }, + "private":false, + "html_url":"https://github.com/octocat/Hello-World", + "description":"This your first repo!", + "fork":false, + "url":"https://api.github.com/repos/octocat/Hello-World", + "archive_url":"https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url":"https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url":"https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url":"https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url":"https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url":"https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url":"https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url":"https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url":"https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url":"https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url":"https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url":"https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url":"https://api.github.com/repos/octocat/Hello-World/events", + "forks_url":"https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url":"https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url":"https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url":"https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "issue_comment_url":"https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url":"https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url":"https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url":"https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url":"https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url":"https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url":"https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url":"https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url":"https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url":"https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url":"https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "stargazers_url":"https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url":"https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url":"https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url":"https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url":"https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url":"https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url":"https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url":"https://api.github.com/repos/octocat/Hello-World/hooks" + }, + "push_protection_bypassed_by":{ + "login":"monalisa", + "id":2, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/2?", + "gravatar_id":"", + "url":"https://api.github.com/users/monalisa", + "html_url":"https://github.com/monalisa", + "followers_url":"https://api.github.com/users/monalisa/followers", + "following_url":"https://api.github.com/users/monalisa/following{/other_user}", + "gists_url":"https://api.github.com/users/monalisa/gists{/gist_id}", + "starred_url":"https://api.github.com/users/monalisa/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/monalisa/subscriptions", + "organizations_url":"https://api.github.com/users/monalisa/orgs", + "repos_url":"https://api.github.com/users/monalisa/repos", + "events_url":"https://api.github.com/users/monalisa/events{/privacy}", + "received_events_url":"https://api.github.com/users/monalisa/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypassed":true, + "push_protection_bypassed_at":"2020-11-06T21:48:51Z", + "push_protection_bypass_request_reviewer":{ + "login":"octocat", + "id":3, + "node_id":"MDQ6VXNlcjI=", + "avatar_url":"https://alambic.github.com/avatars/u/3?", + "gravatar_id":"", + "url":"https://api.github.com/users/octocat", + "html_url":"https://github.com/octocat", + "followers_url":"https://api.github.com/users/octocat/followers", + "following_url":"https://api.github.com/users/octocat/following{/other_user}", + "gists_url":"https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url":"https://api.github.com/users/octocat/subscriptions", + "organizations_url":"https://api.github.com/users/octocat/orgs", + "repos_url":"https://api.github.com/users/octocat/repos", + "events_url":"https://api.github.com/users/octocat/events{/privacy}", + "received_events_url":"https://api.github.com/users/octocat/received_events", + "type":"User", + "site_admin":true + }, + "push_protection_bypass_request_reviewer_comment":"Example response", + "push_protection_bypass_request_comment":"Example comment", + "push_protection_bypass_request_html_url":"https://github.com/owner/repo/secret_scanning_exemptions/1", + "resolution_comment":"Example comment", + "validity":"active", + "publicly_leaked":false, + "multi_repo":false, + "is_base64_encoded":false, + "first_location_detected":{ + "path":"/example/secrets.txt", + "start_line":1, + "end_line":1, + "start_column":1, + "end_column":64, + "blob_sha":"af5626b4a114abcb82d63db7c8082c3c4756e51b", + "blob_url":"https://api.github.com/repos/octocat/hello-world/git/blobs/af5626b4a114abcb82d63db7c8082c3c4756e51b", + "commit_sha":"f14d7debf9775f957cf4f1e8176da0786431f72b", + "commit_url":"https://api.github.com/repos/octocat/hello-world/git/commits/f14d7debf9775f957cf4f1e8176da0786431f72b" + }, + "has_more_locations":true + } + ] \ No newline at end of file diff --git a/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/unittests/tools/test_github_secrets_detection_report_parser.py b/unittests/tools/test_github_secrets_detection_report_parser.py new file mode 100644 index 00000000000..0a1eb565d6e --- /dev/null +++ b/unittests/tools/test_github_secrets_detection_report_parser.py @@ -0,0 +1,109 @@ +from django.test import TestCase +from dojo.tools.github_secrets_detection_report.parser import GithubSecretsDetectionReportParser +from dojo.models import Test + + +class TestGithubSecretsDetectionReportParser(TestCase): + + def test_github_secrets_detection_report_parser_with_no_vuln_has_no_findings(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + self.assertEqual(0, len(findings)) + + def test_github_secrets_detection_report_parser_with_one_vuln_has_one_finding(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + self.assertEqual(1, len(findings)) + + finding = findings[0] + self.assertEqual("Exposed Secret Detected: Adafruit IO Key", finding.title) + self.assertEqual("Info", finding.severity) + self.assertTrue(finding.static_finding) + self.assertFalse(finding.dynamic_finding) + self.assertEqual("2", finding.vuln_id_from_tool) + self.assertIn("**Secret Type**: Adafruit IO Key", finding.description) + self.assertIn("**Alert State**: resolved", finding.description) + self.assertIn("**Repository**: octocat/Hello-World", finding.description) + self.assertIn("**File Path**: /example/secrets.txt", finding.description) + self.assertIn("**Line**: 1", finding.description) + self.assertIn("**Resolution**: false_positive", finding.description) + self.assertIn("**Push Protection Bypassed**: True", finding.description) + self.assertIn("**Validity**: active", finding.description) + self.assertIn("**Publicly Leaked**: No", finding.description) + self.assertIn("**Multi-Repository**: No", finding.description) + + def test_github_secrets_detection_report_parser_with_many_vuln_has_many_findings(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + self.assertEqual(3, len(findings)) + + # Test first finding (resolved false positive) + finding1 = findings[0] + self.assertEqual("Exposed Secret Detected: Adafruit IO Key", finding1.title) + self.assertEqual("Info", finding1.severity) # resolved false positive + self.assertEqual("2", finding1.vuln_id_from_tool) + self.assertIn("**Resolution**: false_positive", finding1.description) + + # Test second finding (open AWS key) + finding2 = findings[1] + self.assertEqual("Exposed Secret Detected: AWS Access Key ID", finding2.title) + self.assertEqual("Critical", finding2.severity) + self.assertEqual("3", finding2.vuln_id_from_tool) + self.assertIn("**Alert State**: open", finding2.description) + self.assertIn("**Publicly Leaked**: Yes", finding2.description) + + # Test third finding (resolved revoked token) + finding3 = findings[2] + self.assertEqual("Exposed Secret Detected: GitHub Personal Access Token", finding3.title) + self.assertEqual("Info", finding3.severity) # resolved + self.assertEqual("4", finding3.vuln_id_from_tool) + self.assertIn("**Resolution**: revoked", finding3.description) + self.assertIn("**Multi-Repository**: Yes", finding3.description) + + def test_github_secrets_detection_report_parser_invalid_format(self): + with self.assertRaises(TypeError) as context: + testfile = open("unittests/scans/github_secrets_detection_report/empty.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + self.assertIn("Invalid GitHub secrets detection report format", str(context.exception)) + + def test_github_secrets_detection_report_parser_severity_assignment(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + # Check severity assignments + severities = [finding.severity for finding in findings] + self.assertIn("Info", severities) + self.assertIn("Critical", severities) + self.assertEqual(3, len(severities)) + + def test_github_secrets_detection_report_parser_file_path_and_line(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + finding = findings[0] + self.assertEqual("/example/secrets.txt", finding.file_path) + self.assertEqual(1, finding.line) + + def test_github_secrets_detection_report_parser_url_setting(self): + testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + testfile.close() + + finding = findings[0] + self.assertIn("https://github.com/owner/private-repo/security/secret-scanning/2", finding.url) From 549c09f276c5366ed9eb4dd360eaf4b0b6459f16 Mon Sep 17 00:00:00 2001 From: Zeke Date: Tue, 30 Sep 2025 12:25:42 -0400 Subject: [PATCH 2/4] Ruff fixes --- .../github_secrets_detection_report/parser.py | 71 +++--- ...thub_secrets_detection_report_invalid.json | 1 + ..._github_secrets_detection_report_parser.py | 227 ++++++++++-------- 3 files changed, 161 insertions(+), 138 deletions(-) create mode 100644 unittests/scans/github_secrets_detection_report/github_secrets_detection_report_invalid.json diff --git a/dojo/tools/github_secrets_detection_report/parser.py b/dojo/tools/github_secrets_detection_report/parser.py index 5f4198b620a..4b2f0eedddc 100644 --- a/dojo/tools/github_secrets_detection_report/parser.py +++ b/dojo/tools/github_secrets_detection_report/parser.py @@ -1,12 +1,9 @@ import json -from dojo.models import Finding +from dojo.models import Finding -class GithubSecretsDetectionReportParser(object): - """ - Import secrets detection report from GitHub - """ +class GithubSecretsDetectionReportParser: def get_scan_types(self): return ["Github Secrets Detection Report Scan"] @@ -18,7 +15,7 @@ def get_description_for_scan_types(self, scan_type): def get_findings(self, file, test): data = json.load(file) - + if not isinstance(data, list): error_msg = "Invalid GitHub secrets detection report format, expected a JSON list of alerts." raise TypeError(error_msg) @@ -31,32 +28,31 @@ def get_findings(self, file, test): secret_type = alert.get("secret_type", "Unknown") secret_type_display_name = alert.get("secret_type_display_name", secret_type) html_url = alert.get("html_url", "") - + # Create title title = f"Exposed Secret Detected: {secret_type_display_name}" - + # Build description desc_lines = [] if html_url: desc_lines.append(f"**GitHub Alert**: [{html_url}]({html_url})") - - desc_lines.append(f"**Secret Type**: {secret_type_display_name}") - desc_lines.append(f"**Alert State**: {state}") - + + desc_lines.extend([f"**Secret Type**: {secret_type_display_name}", f"**Alert State**: {state}"]) + # Add repository information repository = alert.get("repository", {}) if repository: repo_full_name = repository.get("full_name") if repo_full_name: desc_lines.append(f"**Repository**: {repo_full_name}") - + # Add location information first_location = alert.get("first_location_detected", {}) if first_location: file_path = first_location.get("path") start_line = first_location.get("start_line") end_line = first_location.get("end_line") - + if file_path: desc_lines.append(f"**File Path**: {file_path}") if start_line: @@ -64,68 +60,67 @@ def get_findings(self, file, test): desc_lines.append(f"**Lines**: {start_line}-{end_line}") else: desc_lines.append(f"**Line**: {start_line}") - + # Add resolution information resolution = alert.get("resolution") if resolution: desc_lines.append(f"**Resolution**: {resolution}") - + resolved_by = alert.get("resolved_by") if resolved_by: resolved_by_login = resolved_by.get("login", "Unknown") desc_lines.append(f"**Resolved By**: {resolved_by_login}") - + resolved_at = alert.get("resolved_at") if resolved_at: desc_lines.append(f"**Resolved At**: {resolved_at}") - + resolution_comment = alert.get("resolution_comment") if resolution_comment: desc_lines.append(f"**Resolution Comment**: {resolution_comment}") - + # Add push protection information push_protection_bypassed = alert.get("push_protection_bypassed", False) if push_protection_bypassed: desc_lines.append("**Push Protection Bypassed**: True") - + bypassed_by = alert.get("push_protection_bypassed_by") if bypassed_by: bypassed_by_login = bypassed_by.get("login", "Unknown") desc_lines.append(f"**Bypassed By**: {bypassed_by_login}") - + bypassed_at = alert.get("push_protection_bypassed_at") if bypassed_at: desc_lines.append(f"**Bypassed At**: {bypassed_at}") else: desc_lines.append("**Push Protection Bypassed**: False") - + # Add additional metadata validity = alert.get("validity", "unknown") desc_lines.append(f"**Validity**: {validity}") - + publicly_leaked = alert.get("publicly_leaked", False) desc_lines.append(f"**Publicly Leaked**: {'Yes' if publicly_leaked else 'No'}") - + multi_repo = alert.get("multi_repo", False) desc_lines.append(f"**Multi-Repository**: {'Yes' if multi_repo else 'No'}") - + has_more_locations = alert.get("has_more_locations", False) if has_more_locations: desc_lines.append("**Note**: This secret has been detected in multiple locations") - + description = "\n\n".join(desc_lines) - + # Determine severity based on state and other factors if state == "resolved": severity = "Info" + elif validity == "active" and publicly_leaked: + severity = "Critical" + elif validity == "active": + severity = "High" else: - if validity == "active" and publicly_leaked: - severity = "Critical" - elif validity == "active": - severity = "High" - else: - severity = "Medium" - + severity = "Medium" + # Create finding finding = Finding( title=title, @@ -136,16 +131,16 @@ def get_findings(self, file, test): dynamic_finding=False, vuln_id_from_tool=str(alert_number) if alert_number else None, ) - + # Set file path and line information if first_location: finding.file_path = first_location.get("path") finding.line = first_location.get("start_line") - + # Set external URL if html_url: finding.url = html_url - + findings.append(finding) - + return findings diff --git a/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_invalid.json b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_invalid.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/unittests/scans/github_secrets_detection_report/github_secrets_detection_report_invalid.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/unittests/tools/test_github_secrets_detection_report_parser.py b/unittests/tools/test_github_secrets_detection_report_parser.py index 0a1eb565d6e..bf2b989b5df 100644 --- a/unittests/tools/test_github_secrets_detection_report_parser.py +++ b/unittests/tools/test_github_secrets_detection_report_parser.py @@ -1,109 +1,136 @@ -from django.test import TestCase -from dojo.tools.github_secrets_detection_report.parser import GithubSecretsDetectionReportParser +import io + from dojo.models import Test +from dojo.tools.github_secrets_detection_report.parser import GithubSecretsDetectionReportParser +from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path -class TestGithubSecretsDetectionReportParser(TestCase): +class TestGithubSecretsDetectionReportParser(DojoTestCase): + def test_parse_file_with_no_vuln_has_no_findings(self): + """Empty list should yield no findings""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_zero_vul.json" + ).open( + encoding="utf-8", + ) as testfile: + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(0, len(findings)) - def test_github_secrets_detection_report_parser_with_no_vuln_has_no_findings(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_zero_vul.json") - parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - self.assertEqual(0, len(findings)) + def test_parse_file_with_one_vuln_parsed_correctly(self): + """Single secret alert entry parsed correctly""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_one_vul.json" + ).open(encoding="utf-8") as testfile: + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(1, len(findings)) + finding = findings[0] + for ep in getattr(finding, "unsaved_endpoints", []): + ep.clean() - def test_github_secrets_detection_report_parser_with_one_vuln_has_one_finding(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") - parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - - self.assertEqual(1, len(findings)) - - finding = findings[0] - self.assertEqual("Exposed Secret Detected: Adafruit IO Key", finding.title) - self.assertEqual("Info", finding.severity) - self.assertTrue(finding.static_finding) - self.assertFalse(finding.dynamic_finding) - self.assertEqual("2", finding.vuln_id_from_tool) - self.assertIn("**Secret Type**: Adafruit IO Key", finding.description) - self.assertIn("**Alert State**: resolved", finding.description) - self.assertIn("**Repository**: octocat/Hello-World", finding.description) - self.assertIn("**File Path**: /example/secrets.txt", finding.description) - self.assertIn("**Line**: 1", finding.description) - self.assertIn("**Resolution**: false_positive", finding.description) - self.assertIn("**Push Protection Bypassed**: True", finding.description) - self.assertIn("**Validity**: active", finding.description) - self.assertIn("**Publicly Leaked**: No", finding.description) - self.assertIn("**Multi-Repository**: No", finding.description) - - def test_github_secrets_detection_report_parser_with_many_vuln_has_many_findings(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json") - parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - - self.assertEqual(3, len(findings)) - - # Test first finding (resolved false positive) - finding1 = findings[0] - self.assertEqual("Exposed Secret Detected: Adafruit IO Key", finding1.title) - self.assertEqual("Info", finding1.severity) # resolved false positive - self.assertEqual("2", finding1.vuln_id_from_tool) - self.assertIn("**Resolution**: false_positive", finding1.description) - - # Test second finding (open AWS key) - finding2 = findings[1] - self.assertEqual("Exposed Secret Detected: AWS Access Key ID", finding2.title) - self.assertEqual("Critical", finding2.severity) - self.assertEqual("3", finding2.vuln_id_from_tool) - self.assertIn("**Alert State**: open", finding2.description) - self.assertIn("**Publicly Leaked**: Yes", finding2.description) - - # Test third finding (resolved revoked token) - finding3 = findings[2] - self.assertEqual("Exposed Secret Detected: GitHub Personal Access Token", finding3.title) - self.assertEqual("Info", finding3.severity) # resolved - self.assertEqual("4", finding3.vuln_id_from_tool) - self.assertIn("**Resolution**: revoked", finding3.description) - self.assertIn("**Multi-Repository**: Yes", finding3.description) - - def test_github_secrets_detection_report_parser_invalid_format(self): - with self.assertRaises(TypeError) as context: - testfile = open("unittests/scans/github_secrets_detection_report/empty.json") + expected_title = "Exposed Secret Detected: Adafruit IO Key" + self.assertEqual(expected_title, finding.title) + self.assertEqual("/example/secrets.txt", finding.file_path) + self.assertEqual(1, finding.line) + self.assertEqual("2", finding.vuln_id_from_tool) + self.assertEqual("Info", finding.severity) + self.assertEqual("https://github.com/owner/private-repo/security/secret-scanning/2", finding.url) + self.assertIn("**Secret Type**: Adafruit IO Key", finding.description) + self.assertIn("**Alert State**: resolved", finding.description) + self.assertIn("**Repository**: octocat/Hello-World", finding.description) + self.assertIn("**Resolution**: false_positive", finding.description) + self.assertIn("**Push Protection Bypassed**: True", finding.description) + self.assertIn("**Validity**: active", finding.description) + self.assertIn("**Publicly Leaked**: No", finding.description) + self.assertIn("**Multi-Repository**: No", finding.description) + + def test_parse_file_with_multiple_vulns_has_multiple_findings(self): + """Multiple entries produce corresponding findings""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_many_vul.json" + ).open( + encoding="utf-8", + ) as testfile: parser = GithubSecretsDetectionReportParser() findings = parser.get_findings(testfile, Test()) - testfile.close() - - self.assertIn("Invalid GitHub secrets detection report format", str(context.exception)) + self.assertEqual(3, len(findings)) - def test_github_secrets_detection_report_parser_severity_assignment(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_many_vul.json") - parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - - # Check severity assignments - severities = [finding.severity for finding in findings] - self.assertIn("Info", severities) - self.assertIn("Critical", severities) - self.assertEqual(3, len(severities)) - - def test_github_secrets_detection_report_parser_file_path_and_line(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") - parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - - finding = findings[0] - self.assertEqual("/example/secrets.txt", finding.file_path) - self.assertEqual(1, finding.line) - - def test_github_secrets_detection_report_parser_url_setting(self): - testfile = open("unittests/scans/github_secrets_detection_report/github_secrets_detection_report_one_vul.json") + # Test first finding (resolved false positive) + finding1 = findings[0] + self.assertEqual("Exposed Secret Detected: Adafruit IO Key", finding1.title) + self.assertEqual("Info", finding1.severity) + self.assertEqual("2", finding1.vuln_id_from_tool) + self.assertIn("**Resolution**: false_positive", finding1.description) + + # Test second finding (open AWS key) + finding2 = findings[1] + self.assertEqual("Exposed Secret Detected: AWS Access Key ID", finding2.title) + self.assertEqual("Critical", finding2.severity) + self.assertEqual("3", finding2.vuln_id_from_tool) + self.assertIn("**Alert State**: open", finding2.description) + self.assertIn("**Publicly Leaked**: Yes", finding2.description) + + # Test third finding (resolved revoked token) + finding3 = findings[2] + self.assertEqual("Exposed Secret Detected: GitHub Personal Access Token", finding3.title) + self.assertEqual("Info", finding3.severity) + self.assertEqual("4", finding3.vuln_id_from_tool) + self.assertIn("**Resolution**: revoked", finding3.description) + self.assertIn("**Multi-Repository**: Yes", finding3.description) + + def test_parse_file_invalid_format_raises(self): + """Non-list JSON should raise""" + bad_json = io.StringIO('{"not": "a list"}') parser = GithubSecretsDetectionReportParser() - findings = parser.get_findings(testfile, Test()) - testfile.close() - - finding = findings[0] - self.assertIn("https://github.com/owner/private-repo/security/secret-scanning/2", finding.url) + with self.assertRaises(TypeError): + parser.get_findings(bad_json, Test()) + + def test_severity_assignment(self): + """Test severity assignment logic""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_many_vul.json" + ).open( + encoding="utf-8", + ) as testfile: + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + + # Check severity assignments + severities = [finding.severity for finding in findings] + self.assertIn("Info", severities) # resolved findings + self.assertIn("Critical", severities) # active + publicly leaked + self.assertEqual(3, len(severities)) + + def test_file_path_and_line_assignment(self): + """Test file path and line number extraction""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_one_vul.json" + ).open( + encoding="utf-8", + ) as testfile: + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + + finding = findings[0] + self.assertEqual("/example/secrets.txt", finding.file_path) + self.assertEqual(1, finding.line) + + def test_url_setting(self): + """Test URL assignment from GitHub alert""" + with ( + get_unit_tests_scans_path("github_secrets_detection_report") + / "github_secrets_detection_report_one_vul.json" + ).open( + encoding="utf-8", + ) as testfile: + parser = GithubSecretsDetectionReportParser() + findings = parser.get_findings(testfile, Test()) + + finding = findings[0] + self.assertEqual("https://github.com/owner/private-repo/security/secret-scanning/2", finding.url) From 5f7a7f15c5e153038df84b1abc16af33f0e2b62f Mon Sep 17 00:00:00 2001 From: Zeke Date: Tue, 30 Sep 2025 12:40:19 -0400 Subject: [PATCH 3/4] Add docs and dedupe algo --- .../parsers/file/github_secrets_detection_report.md | 9 +++++++++ dojo/settings/settings.dist.py | 2 ++ 2 files changed, 11 insertions(+) create mode 100644 docs/content/en/connecting_your_tools/parsers/file/github_secrets_detection_report.md diff --git a/docs/content/en/connecting_your_tools/parsers/file/github_secrets_detection_report.md b/docs/content/en/connecting_your_tools/parsers/file/github_secrets_detection_report.md new file mode 100644 index 00000000000..f32fef42575 --- /dev/null +++ b/docs/content/en/connecting_your_tools/parsers/file/github_secrets_detection_report.md @@ -0,0 +1,9 @@ +--- +title: "Github Secrets Detection Report" +toc_hide: true +--- +Import findings in JSON format from Github Secret Scanning REST API: + + +### Sample Scan Data +Sample Github SAST scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/github_secrets_detection_report_many_vul.json). \ No newline at end of file diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 3b66121d62f..54b88b432a8 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1303,6 +1303,7 @@ def saml2_attrib_map_format(din): "Scout Suite Scan": ["file_path", "vuln_id_from_tool"], # for now we use file_path as there is no attribute for "service" "Meterian Scan": ["cwe", "component_name", "component_version", "description", "severity"], "Github Vulnerability Scan": ["title", "severity", "component_name", "vulnerability_ids", "file_path"], + "Github Secrets Detection Report": ["title", "severity", "file_path", "line"], "Solar Appscreener Scan": ["title", "file_path", "line", "severity"], "pip-audit Scan": ["vuln_id_from_tool", "component_name", "component_version"], "Rubocop Scan": ["vuln_id_from_tool", "file_path", "line"], @@ -1545,6 +1546,7 @@ def saml2_attrib_map_format(din): "AWS Security Hub Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL, "Meterian Scan": DEDUPE_ALGO_HASH_CODE, "Github Vulnerability Scan": DEDUPE_ALGO_HASH_CODE, + "Github Secrets Detection Report": DEDUPE_ALGO_HASH_CODE, "Cloudsploit Scan": DEDUPE_ALGO_HASH_CODE, "SARIF": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE, "Azure Security Center Recommendations Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL, From c2e1408561c1f79527d5a56a5f29725bd996b547 Mon Sep 17 00:00:00 2001 From: Zeke Date: Tue, 30 Sep 2025 13:53:09 -0400 Subject: [PATCH 4/4] Rm severity from hash_code --- dojo/settings/settings.dist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 54b88b432a8..ca9cc0f2345 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -1303,7 +1303,7 @@ def saml2_attrib_map_format(din): "Scout Suite Scan": ["file_path", "vuln_id_from_tool"], # for now we use file_path as there is no attribute for "service" "Meterian Scan": ["cwe", "component_name", "component_version", "description", "severity"], "Github Vulnerability Scan": ["title", "severity", "component_name", "vulnerability_ids", "file_path"], - "Github Secrets Detection Report": ["title", "severity", "file_path", "line"], + "Github Secrets Detection Report": ["title", "file_path", "line"], "Solar Appscreener Scan": ["title", "file_path", "line", "severity"], "pip-audit Scan": ["vuln_id_from_tool", "component_name", "component_version"], "Rubocop Scan": ["vuln_id_from_tool", "file_path", "line"],