Commit 0e167b5102
Unsigned
Layout: unified · split
README.org +1 −1
| @@ -18,7 +18,7 @@ connection details, choose which checks to run, and watch live progress. | |||
| 18 | | Directory | Description | | 18 | | Directory | Description | |
| 19 | |----------------------+------------------------------------------------------------------------------| | 19 | |----------------------+------------------------------------------------------------------------------| |
| 20 | | =applications/aws/= | AWS IAM users, password policy, and S3 bucket analysis | | 20 | | =applications/aws/= | AWS IAM users, password policy, and S3 bucket analysis | |
| 21 | | =applications/github/= | GitHub admin enumeration, audit log, branch protections, and commit analysis | | 21 | | =applications/github/= | GitHub admin enumeration, org security settings, webhooks, deploy keys, secret-scanning/Dependabot alerts, audit log, branch protections, commits | |
| 22 | | =applications/gitlab/= | GitLab group/project members, branch protections, approvals, pipelines, audit events | | 22 | | =applications/gitlab/= | GitLab group/project members, branch protections, approvals, pipelines, audit events | |
| 23 | | =databases/mongo/= | MongoDB admin enumeration | | 23 | | =databases/mongo/= | MongoDB admin enumeration | |
| 24 | | =databases/mysql/= | MySQL admin and password queries | | 24 | | =databases/mysql/= | MySQL admin and password queries | |
applications/github/README.md +9 −1
| @@ -1,6 +1,9 @@ | |||
| 1 | > **NOTE**: The PAT used across all scripts needs the following minimum permissions: | 1 | > **NOTE**: The PAT used across all scripts needs the following minimum permissions: |
| 2 | > - Repository: Actions (read), Contents (read), Metadata (read), Workflows (read) | 2 | > - Repository: Actions (read), Contents (read), Metadata (read), Workflows (read) |
| 3 | > - Organization: Administration (read), Members (read) | 3 | > - Organization: Administration (read), Members (read), Webhooks (read) |
| 4 | > - Secret scanning and Dependabot alerts require GitHub Advanced Security and | ||
| 5 | > the corresponding read permissions; they are skipped with a warning if | ||
| 6 | > unavailable. | ||
| 4 | > - Audit log collection also requires GitHub Enterprise Cloud. Classic PATs need | 7 | > - Audit log collection also requires GitHub Enterprise Cloud. Classic PATs need |
| 5 | > `read:audit_log`; fine-grained tokens need Organization Administration (read). | 8 | > `read:audit_log`; fine-grained tokens need Organization Administration (read). |
| 6 | 9 | ||
| @@ -46,6 +49,11 @@ Creates a directory: `<out>/github_audit_<org>_<YYYY-MM-DD>/` | |||
| 46 | | `permission_matrix.csv` | Full user/repo/permission cross-reference | | 49 | | `permission_matrix.csv` | Full user/repo/permission cross-reference | |
| 47 | | `branch_protections.csv` | Branch protection settings across all repos | | 50 | | `branch_protections.csv` | Branch protection settings across all repos | |
| 48 | | `commits.csv` | Commit history across all repos for the target branch | | 51 | | `commits.csv` | Commit history across all repos for the target branch | |
| 52 | | `org_security.csv` | Org security settings (2FA requirement, default permission, repo creation, secret scanning defaults) | | ||
| 53 | | `webhooks.csv` | Org and per-repo webhooks, flagging plain-HTTP delivery and disabled SSL verification | | ||
| 54 | | `deploy_keys.csv` | Deploy keys across all repos (read-only vs read-write, last used) | | ||
| 55 | | `secret_scanning.csv` | Open secret-scanning alerts (Advanced Security) | | ||
| 56 | | `dependabot_alerts.csv` | Open Dependabot alerts with severity (Advanced Security) | | ||
| 49 | | `audit_log.csv` | Branch protection and repository ruleset audit-log changes from the last 180 days (Enterprise Cloud only) | | 57 | | `audit_log.csv` | Branch protection and repository ruleset audit-log changes from the last 180 days (Enterprise Cloud only) | |
| 50 | | `summary.txt` | Row counts per section | | 58 | | `summary.txt` | Row counts per section | |
| 51 | 59 | ||
applications/github/audit.py +29 −1
| @@ -34,7 +34,16 @@ import sys | |||
| 34 | from datetime import date | 34 | from datetime import date |
| 35 | 35 | ||
| 36 | import config | 36 | import config |
| 37 | from collectors import audit_log, branch_protections, commits, members | 37 | from collectors import ( |
| 38 | audit_log, | ||
| 39 | branch_protections, | ||
| 40 | commits, | ||
| 41 | deploy_keys, | ||
| 42 | members, | ||
| 43 | org_settings, | ||
| 44 | security_alerts, | ||
| 45 | webhooks, | ||
| 46 | ) | ||
| 38 | from reporters import csv_reporter | 47 | from reporters import csv_reporter |
| 39 | 48 | ||
| 40 | 49 | ||
| @@ -144,6 +153,25 @@ def run(): | |||
| 144 | cfg, | 153 | cfg, |
| 145 | ) | 154 | ) |
| 146 | collect("Commits", commits.commits, "commits.csv", org, cfg, args.branch) | 155 | collect("Commits", commits.commits, "commits.csv", org, cfg, args.branch) |
| 156 | collect( | ||
| 157 | "Org security settings", org_settings.org_security, "org_security.csv", org, cfg | ||
| 158 | ) | ||
| 159 | collect("Webhooks", webhooks.webhooks, "webhooks.csv", org, cfg) | ||
| 160 | collect("Deploy keys", deploy_keys.deploy_keys, "deploy_keys.csv", org, cfg) | ||
| 161 | collect( | ||
| 162 | "Secret scanning alerts", | ||
| 163 | security_alerts.secret_scanning, | ||
| 164 | "secret_scanning.csv", | ||
| 165 | org, | ||
| 166 | cfg, | ||
| 167 | ) | ||
| 168 | collect( | ||
| 169 | "Dependabot alerts", | ||
| 170 | security_alerts.dependabot_alerts, | ||
| 171 | "dependabot_alerts.csv", | ||
| 172 | org, | ||
| 173 | cfg, | ||
| 174 | ) | ||
| 147 | collect( | 175 | collect( |
| 148 | "Audit log branch/ruleset changes", | 176 | "Audit log branch/ruleset changes", |
| 149 | audit_log.audit_log, | 177 | audit_log.audit_log, |
applications/github/collectors/deploy_keys.py added +36
| @@ -0,0 +1,36 @@ | |||
| 1 | """Collect deploy keys across all repositories in an org.""" | ||
| 2 | |||
| 3 | import sys | ||
| 4 | |||
| 5 | import requests | ||
| 6 | |||
| 7 | from .api import paginate | ||
| 8 | |||
| 9 | |||
| 10 | def deploy_keys(org, cfg): | ||
| 11 | rows = [] | ||
| 12 | for repo in paginate(f"https://api.github.com/orgs/{org}/repos", cfg): | ||
| 13 | name = repo["name"] | ||
| 14 | try: | ||
| 15 | keys = paginate(f"https://api.github.com/repos/{org}/{name}/keys", cfg) | ||
| 16 | except requests.HTTPError as e: | ||
| 17 | if e.response is not None and e.response.status_code in (403, 404): | ||
| 18 | print( | ||
| 19 | f" Skipping {name}: keys endpoint returned " | ||
| 20 | f"{e.response.status_code}", | ||
| 21 | file=sys.stderr, | ||
| 22 | ) | ||
| 23 | continue | ||
| 24 | raise | ||
| 25 | for k in keys: | ||
| 26 | rows.append( | ||
| 27 | { | ||
| 28 | "repo": name, | ||
| 29 | "title": k.get("title", ""), | ||
| 30 | "read_only": k.get("read_only"), | ||
| 31 | "created_at": k.get("created_at", ""), | ||
| 32 | "last_used": k.get("last_used") or "", | ||
| 33 | "added_by": k.get("added_by") or "", | ||
| 34 | } | ||
| 35 | ) | ||
| 36 | return rows | ||
applications/github/collectors/org_settings.py added +40
| @@ -0,0 +1,40 @@ | |||
| 1 | """ | ||
| 2 | Collect organization-level security settings. | ||
| 3 | |||
| 4 | Reads the org object; several fields are only populated when the token has org | ||
| 5 | admin access. | ||
| 6 | """ | ||
| 7 | |||
| 8 | import requests | ||
| 9 | |||
| 10 | |||
| 11 | def org_security(org, cfg): | ||
| 12 | resp = requests.get( | ||
| 13 | f"https://api.github.com/orgs/{org}", | ||
| 14 | headers=cfg["headers"], | ||
| 15 | timeout=cfg["timeout"], | ||
| 16 | ) | ||
| 17 | resp.raise_for_status() | ||
| 18 | o = resp.json() | ||
| 19 | return [ | ||
| 20 | { | ||
| 21 | "org": org, | ||
| 22 | "two_factor_required": o.get("two_factor_requirement_enabled"), | ||
| 23 | "default_repo_permission": o.get("default_repository_permission"), | ||
| 24 | "members_can_create_repos": o.get("members_can_create_repositories"), | ||
| 25 | "members_can_create_public_repos": o.get( | ||
| 26 | "members_can_create_public_repositories" | ||
| 27 | ), | ||
| 28 | "members_can_create_pages": o.get("members_can_create_pages"), | ||
| 29 | "web_commit_signoff_required": o.get("web_commit_signoff_required"), | ||
| 30 | "advanced_security_for_new_repos": o.get( | ||
| 31 | "advanced_security_enabled_for_new_repositories" | ||
| 32 | ), | ||
| 33 | "secret_scanning_for_new_repos": o.get( | ||
| 34 | "secret_scanning_enabled_for_new_repositories" | ||
| 35 | ), | ||
| 36 | "secret_scanning_push_protection_for_new_repos": o.get( | ||
| 37 | "secret_scanning_push_protection_enabled_for_new_repositories" | ||
| 38 | ), | ||
| 39 | } | ||
| 40 | ] | ||
applications/github/collectors/security_alerts.py added +63
| @@ -0,0 +1,63 @@ | |||
| 1 | """ | ||
| 2 | Collect open secret-scanning and Dependabot alerts across the organization. | ||
| 3 | |||
| 4 | Both require GitHub Advanced Security (or public repos) and admin access. If the | ||
| 5 | org or token can't reach them, the collector returns an empty list with a | ||
| 6 | warning instead of failing the run. | ||
| 7 | """ | ||
| 8 | |||
| 9 | import sys | ||
| 10 | |||
| 11 | import requests | ||
| 12 | |||
| 13 | from .api import paginate | ||
| 14 | |||
| 15 | |||
| 16 | def secret_scanning(org, cfg): | ||
| 17 | return _org_alerts(org, cfg, "secret-scanning", _secret_row, "secret scanning") | ||
| 18 | |||
| 19 | |||
| 20 | def dependabot_alerts(org, cfg): | ||
| 21 | return _org_alerts(org, cfg, "dependabot", _dependabot_row, "Dependabot") | ||
| 22 | |||
| 23 | |||
| 24 | def _org_alerts(org, cfg, kind, row_fn, label): | ||
| 25 | try: | ||
| 26 | alerts = paginate( | ||
| 27 | f"https://api.github.com/orgs/{org}/{kind}/alerts", cfg, {"state": "open"} | ||
| 28 | ) | ||
| 29 | except requests.HTTPError as e: | ||
| 30 | if e.response is not None and e.response.status_code in (403, 404): | ||
| 31 | print( | ||
| 32 | f"Warning: {label} alerts require GitHub Advanced Security and " | ||
| 33 | "org admin access -- skipping.", | ||
| 34 | file=sys.stderr, | ||
| 35 | ) | ||
| 36 | return [] | ||
| 37 | raise | ||
| 38 | return [row_fn(a) for a in alerts] | ||
| 39 | |||
| 40 | |||
| 41 | def _secret_row(alert): | ||
| 42 | return { | ||
| 43 | "repo": alert.get("repository", {}).get("full_name", ""), | ||
| 44 | "secret_type": alert.get("secret_type_display_name") | ||
| 45 | or alert.get("secret_type", ""), | ||
| 46 | "state": alert.get("state", ""), | ||
| 47 | "created_at": alert.get("created_at", ""), | ||
| 48 | "html_url": alert.get("html_url", ""), | ||
| 49 | } | ||
| 50 | |||
| 51 | |||
| 52 | def _dependabot_row(alert): | ||
| 53 | dependency = alert.get("dependency", {}) | ||
| 54 | advisory = alert.get("security_advisory", {}) | ||
| 55 | return { | ||
| 56 | "repo": alert.get("repository", {}).get("full_name", ""), | ||
| 57 | "package": dependency.get("package", {}).get("name", ""), | ||
| 58 | "severity": advisory.get("severity", ""), | ||
| 59 | "summary": advisory.get("summary", ""), | ||
| 60 | "state": alert.get("state", ""), | ||
| 61 | "created_at": alert.get("created_at", ""), | ||
| 62 | "html_url": alert.get("html_url", ""), | ||
| 63 | } | ||
applications/github/collectors/webhooks.py added +52
| @@ -0,0 +1,52 @@ | |||
| 1 | """ | ||
| 2 | Collect organization and repository webhooks. | ||
| 3 | |||
| 4 | Flags webhooks that deliver over plain HTTP or with SSL verification disabled. | ||
| 5 | """ | ||
| 6 | |||
| 7 | import sys | ||
| 8 | from urllib.parse import urlparse | ||
| 9 | |||
| 10 | import requests | ||
| 11 | |||
| 12 | from .api import paginate | ||
| 13 | |||
| 14 | |||
| 15 | def webhooks(org, cfg): | ||
| 16 | rows = [ | ||
| 17 | _hook_row("org", h) | ||
| 18 | for h in paginate(f"https://api.github.com/orgs/{org}/hooks", cfg) | ||
| 19 | ] | ||
| 20 | |||
| 21 | for repo in paginate(f"https://api.github.com/orgs/{org}/repos", cfg): | ||
| 22 | name = repo["name"] | ||
| 23 | try: | ||
| 24 | hooks = paginate(f"https://api.github.com/repos/{org}/{name}/hooks", cfg) | ||
| 25 | except requests.HTTPError as e: | ||
| 26 | if e.response is not None and e.response.status_code in (403, 404): | ||
| 27 | print( | ||
| 28 | f" Skipping {name}: hooks endpoint returned " | ||
| 29 | f"{e.response.status_code}", | ||
| 30 | file=sys.stderr, | ||
| 31 | ) | ||
| 32 | continue | ||
| 33 | raise | ||
| 34 | rows.extend(_hook_row(f"repo:{name}", h) for h in hooks) | ||
| 35 | |||
| 36 | return rows | ||
| 37 | |||
| 38 | |||
| 39 | def _hook_row(scope, hook): | ||
| 40 | config = hook.get("config", {}) | ||
| 41 | url = config.get("url", "") | ||
| 42 | return { | ||
| 43 | "scope": scope, | ||
| 44 | "url": url, | ||
| 45 | "insecure_url": urlparse(url).scheme == "http", | ||
| 46 | "ssl_verification": "disabled" | ||
| 47 | if str(config.get("insecure_ssl", "0")) == "1" | ||
| 48 | else "enabled", | ||
| 49 | "content_type": config.get("content_type", ""), | ||
| 50 | "events": ", ".join(hook.get("events", [])), | ||
| 51 | "active": hook.get("active"), | ||
| 52 | } | ||
tui/github_runner.py +29 −1
| @@ -27,7 +27,11 @@ from applications.github.collectors import ( | |||
| 27 | audit_log, | 27 | audit_log, |
| 28 | branch_protections, | 28 | branch_protections, |
| 29 | commits, | 29 | commits, |
| 30 | deploy_keys, | ||
| 30 | members, | 31 | members, |
| 32 | org_settings, | ||
| 33 | security_alerts, | ||
| 34 | webhooks, | ||
| 31 | ) | 35 | ) |
| 32 | from applications.github.reporters import csv_reporter | 36 | from applications.github.reporters import csv_reporter |
| 33 | 37 | ||
| @@ -85,6 +89,28 @@ CHECKS: list[Check] = [ | |||
| 85 | "branch_protections.csv", | 89 | "branch_protections.csv", |
| 86 | ), | 90 | ), |
| 87 | Check("commits", "Commits", commits.commits, "commits.csv", arg="branch"), | 91 | Check("commits", "Commits", commits.commits, "commits.csv", arg="branch"), |
| 92 | Check( | ||
| 93 | "org_security", | ||
| 94 | "Org security settings", | ||
| 95 | org_settings.org_security, | ||
| 96 | "org_security.csv", | ||
| 97 | ), | ||
| 98 | Check("webhooks", "Webhooks", webhooks.webhooks, "webhooks.csv"), | ||
| 99 | Check("deploy_keys", "Deploy keys", deploy_keys.deploy_keys, "deploy_keys.csv"), | ||
| 100 | Check( | ||
| 101 | "secret_scanning", | ||
| 102 | "Secret scanning alerts", | ||
| 103 | security_alerts.secret_scanning, | ||
| 104 | "secret_scanning.csv", | ||
| 105 | note="requires GitHub Advanced Security", | ||
| 106 | ), | ||
| 107 | Check( | ||
| 108 | "dependabot_alerts", | ||
| 109 | "Dependabot alerts", | ||
| 110 | security_alerts.dependabot_alerts, | ||
| 111 | "dependabot_alerts.csv", | ||
| 112 | note="requires GitHub Advanced Security", | ||
| 113 | ), | ||
| 88 | Check( | 114 | Check( |
| 89 | "audit_log", | 115 | "audit_log", |
| 90 | "Audit log (branch/ruleset changes)", | 116 | "Audit log (branch/ruleset changes)", |
| @@ -94,7 +120,9 @@ CHECKS: list[Check] = [ | |||
| 94 | ), | 120 | ), |
| 95 | ] | 121 | ] |
| 96 | 122 | ||
| 97 | DEFAULT_SELECTION = [c.key for c in CHECKS if c.key != "audit_log"] | 123 | # Off by default: checks needing Advanced Security or Enterprise Cloud. |
| 124 | _OFF_BY_DEFAULT = {"secret_scanning", "dependabot_alerts", "audit_log"} | ||
| 125 | DEFAULT_SELECTION = [c.key for c in CHECKS if c.key not in _OFF_BY_DEFAULT] | ||
| 98 | 126 | ||
| 99 | 127 | ||
| 100 | # --- Config + output helpers ------------------------------------------------ | 128 | # --- Config + output helpers ------------------------------------------------ |
tui/tests/test_github_collectors.py added +172
| @@ -0,0 +1,172 @@ | |||
| 1 | """Unit tests for the new GitHub security collectors, mocking the HTTP layer.""" | ||
| 2 | |||
| 3 | import types | ||
| 4 | |||
| 5 | import pytest | ||
| 6 | import requests | ||
| 7 | |||
| 8 | from applications.github.collectors import ( | ||
| 9 | deploy_keys, | ||
| 10 | org_settings, | ||
| 11 | security_alerts, | ||
| 12 | webhooks, | ||
| 13 | ) | ||
| 14 | |||
| 15 | CFG = {"headers": {}, "timeout": 30} | ||
| 16 | |||
| 17 | |||
| 18 | class _Resp: | ||
| 19 | def __init__(self, payload): | ||
| 20 | self._payload = payload | ||
| 21 | |||
| 22 | def raise_for_status(self): | ||
| 23 | pass | ||
| 24 | |||
| 25 | def json(self): | ||
| 26 | return self._payload | ||
| 27 | |||
| 28 | |||
| 29 | def _http_error(status): | ||
| 30 | err = requests.HTTPError() | ||
| 31 | err.response = types.SimpleNamespace(status_code=status) | ||
| 32 | return err | ||
| 33 | |||
| 34 | |||
| 35 | # --- org_security ----------------------------------------------------------- | ||
| 36 | |||
| 37 | |||
| 38 | def test_org_security_row(monkeypatch): | ||
| 39 | payload = { | ||
| 40 | "two_factor_requirement_enabled": True, | ||
| 41 | "default_repository_permission": "read", | ||
| 42 | "members_can_create_repositories": False, | ||
| 43 | } | ||
| 44 | monkeypatch.setattr(org_settings.requests, "get", lambda *a, **k: _Resp(payload)) | ||
| 45 | rows = org_settings.org_security("acme", CFG) | ||
| 46 | assert len(rows) == 1 | ||
| 47 | assert rows[0]["two_factor_required"] is True | ||
| 48 | assert rows[0]["default_repo_permission"] == "read" | ||
| 49 | assert rows[0]["members_can_create_repos"] is False | ||
| 50 | |||
| 51 | |||
| 52 | # --- webhooks --------------------------------------------------------------- | ||
| 53 | |||
| 54 | |||
| 55 | def test_webhooks_flags_insecure(monkeypatch): | ||
| 56 | def fake_paginate(url, cfg, params=None): | ||
| 57 | if url.endswith("/orgs/acme/hooks"): | ||
| 58 | return [ | ||
| 59 | { | ||
| 60 | "config": {"url": "http://hook.example", "insecure_ssl": "1"}, | ||
| 61 | "events": ["push"], | ||
| 62 | "active": True, | ||
| 63 | } | ||
| 64 | ] | ||
| 65 | if url.endswith("/orgs/acme/repos"): | ||
| 66 | return [{"name": "repo1"}] | ||
| 67 | if url.endswith("/repos/acme/repo1/hooks"): | ||
| 68 | return [ | ||
| 69 | { | ||
| 70 | "config": {"url": "https://secure.example", "insecure_ssl": "0"}, | ||
| 71 | "events": ["pull_request"], | ||
| 72 | "active": True, | ||
| 73 | } | ||
| 74 | ] | ||
| 75 | return [] | ||
| 76 | |||
| 77 | monkeypatch.setattr(webhooks, "paginate", fake_paginate) | ||
| 78 | rows = webhooks.webhooks("acme", CFG) | ||
| 79 | |||
| 80 | org_hook = next(r for r in rows if r["scope"] == "org") | ||
| 81 | assert org_hook["insecure_url"] is True | ||
| 82 | assert org_hook["ssl_verification"] == "disabled" | ||
| 83 | |||
| 84 | repo_hook = next(r for r in rows if r["scope"] == "repo:repo1") | ||
| 85 | assert repo_hook["insecure_url"] is False | ||
| 86 | assert repo_hook["ssl_verification"] == "enabled" | ||
| 87 | |||
| 88 | |||
| 89 | def test_webhooks_skips_forbidden_repo(monkeypatch): | ||
| 90 | def fake_paginate(url, cfg, params=None): | ||
| 91 | if url.endswith("/orgs/acme/hooks"): | ||
| 92 | return [] | ||
| 93 | if url.endswith("/orgs/acme/repos"): | ||
| 94 | return [{"name": "locked"}] | ||
| 95 | raise _http_error(403) | ||
| 96 | |||
| 97 | monkeypatch.setattr(webhooks, "paginate", fake_paginate) | ||
| 98 | assert webhooks.webhooks("acme", CFG) == [] | ||
| 99 | |||
| 100 | |||
| 101 | # --- deploy_keys ------------------------------------------------------------ | ||
| 102 | |||
| 103 | |||
| 104 | def test_deploy_keys_rows(monkeypatch): | ||
| 105 | def fake_paginate(url, cfg, params=None): | ||
| 106 | if url.endswith("/orgs/acme/repos"): | ||
| 107 | return [{"name": "repo1"}] | ||
| 108 | if url.endswith("/repos/acme/repo1/keys"): | ||
| 109 | return [{"title": "ci", "read_only": False, "created_at": "2026-01-01"}] | ||
| 110 | return [] | ||
| 111 | |||
| 112 | monkeypatch.setattr(deploy_keys, "paginate", fake_paginate) | ||
| 113 | rows = deploy_keys.deploy_keys("acme", CFG) | ||
| 114 | assert rows == [ | ||
| 115 | { | ||
| 116 | "repo": "repo1", | ||
| 117 | "title": "ci", | ||
| 118 | "read_only": False, | ||
| 119 | "created_at": "2026-01-01", | ||
| 120 | "last_used": "", | ||
| 121 | "added_by": "", | ||
| 122 | } | ||
| 123 | ] | ||
| 124 | |||
| 125 | |||
| 126 | # --- security alerts -------------------------------------------------------- | ||
| 127 | |||
| 128 | |||
| 129 | def test_secret_scanning_rows(monkeypatch): | ||
| 130 | monkeypatch.setattr( | ||
| 131 | security_alerts, | ||
| 132 | "paginate", | ||
| 133 | lambda url, cfg, params=None: [ | ||
| 134 | { | ||
| 135 | "repository": {"full_name": "acme/repo1"}, | ||
| 136 | "secret_type_display_name": "AWS Key", | ||
| 137 | "state": "open", | ||
| 138 | } | ||
| 139 | ], | ||
| 140 | ) | ||
| 141 | rows = security_alerts.secret_scanning("acme", CFG) | ||
| 142 | assert rows[0]["repo"] == "acme/repo1" | ||
| 143 | assert rows[0]["secret_type"] == "AWS Key" | ||
| 144 | |||
| 145 | |||
| 146 | def test_dependabot_rows(monkeypatch): | ||
| 147 | monkeypatch.setattr( | ||
| 148 | security_alerts, | ||
| 149 | "paginate", | ||
| 150 | lambda url, cfg, params=None: [ | ||
| 151 | { | ||
| 152 | "repository": {"full_name": "acme/repo1"}, | ||
| 153 | "dependency": {"package": {"name": "requests"}}, | ||
| 154 | "security_advisory": {"severity": "high", "summary": "RCE"}, | ||
| 155 | "state": "open", | ||
| 156 | } | ||
| 157 | ], | ||
| 158 | ) | ||
| 159 | rows = security_alerts.dependabot_alerts("acme", CFG) | ||
| 160 | assert rows[0]["package"] == "requests" | ||
| 161 | assert rows[0]["severity"] == "high" | ||
| 162 | |||
| 163 | |||
| 164 | @pytest.mark.parametrize( | ||
| 165 | "fn", [security_alerts.secret_scanning, security_alerts.dependabot_alerts] | ||
| 166 | ) | ||
| 167 | def test_alerts_skip_without_advanced_security(monkeypatch, fn): | ||
| 168 | def raise_403(url, cfg, params=None): | ||
| 169 | raise _http_error(403) | ||
| 170 | |||
| 171 | monkeypatch.setattr(security_alerts, "paginate", raise_403) | ||
| 172 | assert fn("acme", CFG) == [] | ||