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/github_runner.py · raw

  1"""
  2Drive the existing GitHub audit collectors from the TUI.
  3
  4This module reuses the collectors and CSV reporter under
  5``applications/github`` unchanged. It exposes:
  6
  7- ``CHECKS``: the list of available audit checks the UI presents.
  8- ``run_audit``: run the selected checks, write the same output package
  9  ``audit.py`` produces, and report progress through a callback.
 10"""
 11
 12import os
 13import sys
 14from collections.abc import Iterable
 15from datetime import date
 16
 17from tui.common import Check, ProgressCallback, ProgressEvent
 18
 19# Import the GitHub collectors as a namespaced package so the GitHub and GitLab
 20# collector packages (both named ``collectors`` on disk) can coexist in one
 21# process. Requires the repo root on sys.path.
 22_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 23if _REPO_ROOT not in sys.path:
 24    sys.path.insert(0, _REPO_ROOT)
 25
 26from applications.github.collectors import (
 27    audit_log,
 28    branch_protections,
 29    commits,
 30    deploy_keys,
 31    members,
 32    org_settings,
 33    security_alerts,
 34    webhooks,
 35)
 36from applications.github.reporters import csv_reporter
 37
 38# --- Check registry ---------------------------------------------------------
 39
 40# For GitHub, ``arg`` is "base" -> fn(org, cfg), "collabs" -> fn(org, cfg,
 41# repo_collabs), or "branch" -> fn(org, cfg, branch).
 42
 43CHECKS: list[Check] = [
 44    Check("member_roster", "Member roster", members.member_roster, "member_roster.csv"),
 45    Check(
 46        "two_factor",
 47        "2FA disabled",
 48        members.two_factor_disabled,
 49        "two_factor_disabled.csv",
 50        note="requires org owner token",
 51    ),
 52    Check(
 53        "outside_collaborators",
 54        "Outside collaborators",
 55        members.outside_collaborators,
 56        "outside_collaborators.csv",
 57        arg="collabs",
 58    ),
 59    Check(
 60        "privileged_access",
 61        "Privileged access",
 62        members.privileged_access,
 63        "privileged_access.csv",
 64        arg="collabs",
 65    ),
 66    Check(
 67        "pending_invitations",
 68        "Pending invitations",
 69        members.pending_invitations,
 70        "pending_invitations.csv",
 71    ),
 72    Check(
 73        "team_permissions",
 74        "Team permissions",
 75        members.team_permissions,
 76        "team_permissions.csv",
 77    ),
 78    Check(
 79        "permission_matrix",
 80        "Permission matrix",
 81        members.permission_matrix,
 82        "permission_matrix.csv",
 83        arg="collabs",
 84    ),
 85    Check(
 86        "branch_protections",
 87        "Branch protections",
 88        branch_protections.branch_protections,
 89        "branch_protections.csv",
 90    ),
 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    ),
114    Check(
115        "audit_log",
116        "Audit log (branch/ruleset changes)",
117        audit_log.audit_log,
118        "audit_log.csv",
119        note="requires GitHub Enterprise Cloud",
120    ),
121]
122
123# Off by default: checks needing Advanced Security or Enterprise Cloud.
124_OFF_BY_DEFAULT = {"secret_scanning", "dependabot_alerts", "audit_log"}
125DEFAULT_SELECTION = [c.key for c in CHECKS if c.key not in _OFF_BY_DEFAULT]
126
127
128# --- Config + output helpers ------------------------------------------------
129
130
131def build_cfg(token: str) -> dict:
132    """Build the config dict the collectors expect (mirrors config.load())."""
133    return {
134        "token": token,
135        "headers": {
136            "Authorization": f"token {token}",
137            "Accept": "application/vnd.github.v3+json",
138        },
139        "timeout": 30,
140    }
141
142
143def default_output_dir(out: str, org: str) -> str:
144    """Match the folder naming used by audit.py."""
145    return os.path.join(out, f"github_audit_{org}_{date.today().isoformat()}")
146
147
148# --- Runner -----------------------------------------------------------------
149
150
151def run_audit(
152    *,
153    org: str,
154    token: str,
155    output_dir: str,
156    branch: str,
157    selected_keys: Iterable[str],
158    on_event: ProgressCallback,
159) -> list[tuple[str, int]]:
160    """
161    Run the selected checks and write the audit package to ``output_dir``.
162
163    A collector that raises is reported as an error and recorded with a count
164    of 0, matching audit.py's behavior of never aborting the whole run.
165
166    Returns the list of (label, row_count) sections that was written to the
167    summary file.
168    """
169    cfg = build_cfg(token)
170    selected = set(selected_keys)
171    checks = [c for c in CHECKS if c.key in selected]
172
173    repo_collabs: list | None = None
174    if any(c.arg == "collabs" for c in checks):
175        on_event(ProgressEvent("fetch", "Repo collaborators (shared cache)"))
176        try:
177            repo_collabs = members.fetch_repo_collaborators(org, cfg)
178        except Exception as e:
179            on_event(
180                ProgressEvent(
181                    "error", "Repo collaborators (shared cache)", message=str(e)
182                )
183            )
184            repo_collabs = []
185
186    sections: list[tuple[str, int]] = []
187    for c in checks:
188        on_event(ProgressEvent("start", c.label))
189        try:
190            if c.arg == "collabs":
191                rows = c.fn(org, cfg, repo_collabs or [])
192            elif c.arg == "branch":
193                rows = c.fn(org, cfg, branch)
194            else:
195                rows = c.fn(org, cfg)
196        except Exception as e:
197            on_event(ProgressEvent("error", c.label, message=str(e)))
198            sections.append((c.label, 0))
199            continue
200
201        csv_reporter.write(output_dir, c.filename, rows)
202        sections.append((c.label, len(rows)))
203        on_event(ProgressEvent("done", c.label, count=len(rows)))
204
205    csv_reporter.write_summary(output_dir, org, sections)
206    total = sum(n for _, n in sections)
207    on_event(ProgressEvent("summary", output_dir, count=total))
208    return sections