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

v1.0.0: tui/gitlab_runner.py · raw

  1"""
  2Drive the GitLab audit collectors from the TUI.
  3
  4Reuses the collectors and CSV reporter under ``applications/gitlab`` unchanged.
  5Mirrors github_runner: a ``CHECKS`` registry plus ``run_audit`` that writes the
  6same package ``applications/gitlab/audit.py`` produces and reports progress
  7through a callback.
  8"""
  9
 10import os
 11import sys
 12from collections.abc import Iterable
 13from datetime import date
 14
 15from tui.common import Check, ProgressCallback, ProgressEvent
 16
 17# Namespaced import so the GitHub and GitLab collector packages (both named
 18# ``collectors`` on disk) can coexist in one process.
 19_REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 20if _REPO_ROOT not in sys.path:
 21    sys.path.insert(0, _REPO_ROOT)
 22
 23from applications.gitlab.collectors import (
 24    approvals,
 25    audit_events,
 26    branch_protections,
 27    members,
 28    pipelines,
 29    projects,
 30    settings,
 31)
 32from applications.gitlab.reporters import csv_reporter
 33
 34# --- Check registry ---------------------------------------------------------
 35
 36# For GitLab, ``arg`` is "base" -> fn(group, cfg) or "projects" -> fn(group,
 37# cfg, projects), where the project cache is fetched once and shared.
 38
 39CHECKS: list[Check] = [
 40    Check("group_members", "Group members", members.group_members, "group_members.csv"),
 41    Check(
 42        "projects", "Projects", projects.project_list, "projects.csv", arg="projects"
 43    ),
 44    Check(
 45        "project_members",
 46        "Project members",
 47        members.project_members,
 48        "project_members.csv",
 49        arg="projects",
 50    ),
 51    Check(
 52        "branch_protections",
 53        "Branch protections",
 54        branch_protections.branch_protections,
 55        "branch_protections.csv",
 56        arg="projects",
 57    ),
 58    Check(
 59        "pipelines",
 60        "Pipelines",
 61        pipelines.pipelines,
 62        "pipelines.csv",
 63        arg="projects",
 64    ),
 65    Check(
 66        "approval_rules",
 67        "Approval rules",
 68        approvals.approval_rules,
 69        "approval_rules.csv",
 70        arg="projects",
 71        note="requires Premium/Ultimate",
 72    ),
 73    Check(
 74        "audit_events",
 75        "Audit events",
 76        audit_events.audit_events,
 77        "audit_events.csv",
 78        note="requires Premium/Ultimate",
 79    ),
 80    Check(
 81        "password_policy",
 82        "Password policy",
 83        settings.password_policy,
 84        "password_policy.csv",
 85        note="self-hosted, admin token",
 86    ),
 87]
 88
 89_PREMIUM = {"approval_rules", "audit_events", "password_policy"}
 90DEFAULT_SELECTION = [c.key for c in CHECKS if c.key not in _PREMIUM]
 91
 92
 93# --- Config + output helpers ------------------------------------------------
 94
 95
 96def build_cfg(token: str, base_url: str) -> dict:
 97    """Build the config dict the collectors expect (mirrors config.load())."""
 98    return {
 99        "token": token,
100        "base_url": base_url.rstrip("/"),
101        "headers": {"PRIVATE-TOKEN": token},
102        "timeout": 30,
103    }
104
105
106def default_output_dir(out: str, group: str) -> str:
107    """Match the folder naming used by applications/gitlab/audit.py."""
108    safe_group = group.replace("/", "-")
109    return os.path.join(out, f"gitlab_audit_{safe_group}_{date.today().isoformat()}")
110
111
112# --- Runner -----------------------------------------------------------------
113
114
115def run_audit(
116    *,
117    group: str,
118    token: str,
119    base_url: str,
120    output_dir: str,
121    selected_keys: Iterable[str],
122    on_event: ProgressCallback,
123) -> list[tuple[str, int]]:
124    """
125    Run the selected checks and write the audit package to ``output_dir``.
126
127    A collector that raises is reported as an error and recorded with a count
128    of 0, so one bad check never aborts the whole run.
129    """
130    cfg = build_cfg(token, base_url)
131    selected = set(selected_keys)
132    checks = [c for c in CHECKS if c.key in selected]
133
134    project_cache: list | None = None
135    if any(c.arg == "projects" for c in checks):
136        on_event(ProgressEvent("fetch", "Projects (shared cache)"))
137        try:
138            project_cache = projects.fetch_projects(group, cfg)
139        except Exception as e:
140            on_event(ProgressEvent("error", "Projects (shared cache)", message=str(e)))
141            project_cache = []
142
143    sections: list[tuple[str, int]] = []
144    for c in checks:
145        on_event(ProgressEvent("start", c.label))
146        try:
147            if c.arg == "projects":
148                rows = c.fn(group, cfg, project_cache or [])
149            else:
150                rows = c.fn(group, cfg)
151        except Exception as e:
152            on_event(ProgressEvent("error", c.label, message=str(e)))
153            sections.append((c.label, 0))
154            continue
155
156        csv_reporter.write(output_dir, c.filename, rows)
157        sections.append((c.label, len(rows)))
158        on_event(ProgressEvent("done", c.label, count=len(rows)))
159
160    csv_reporter.write_summary(output_dir, group, sections)
161    total = sum(n for _, n in sections)
162    on_event(ProgressEvent("summary", output_dir, count=total))
163    return sections