audit-labs/audit-tools

A collection of scripts, queries, and other goodies you can use in an audit.

clone: git clone https://gitbay.org/audit-labs/audit-tools.git

main: tui/tests/test_github_collectors.py · raw

  1"""Unit tests for the new GitHub security collectors, mocking the HTTP layer."""
  2
  3import types
  4
  5import pytest
  6import requests
  7
  8from applications.github.collectors import (
  9    deploy_keys,
 10    org_settings,
 11    security_alerts,
 12    webhooks,
 13)
 14
 15CFG = {"headers": {}, "timeout": 30}
 16
 17
 18class _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
 29def _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
 38def 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
 55def 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
 89def 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
104def 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
129def 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
146def 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)
167def 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) == []