audit-labs/audit-tools

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

Commit 2dc0c40b11

2dc0c40b11360382595200cf8f35531326a59df1

parent: 0455eb80af

Unsigned

cmc <hello@cleberg.net> · 2026-07-04 17:36 UTC

fix: update ruff formatting and yaml dependencies

Layout: unified · split

.github/workflows/ruff.yml +6 −16
@@ -12,26 +12,16 @@ jobs:
1212 matrix:
1313 python-version: ["3.x"]
1414 steps:
15 - uses: actions/checkout@v4
15 - uses: actions/checkout@v5
1616 - name: Set up Python ${{ matrix.python-version }}
17 uses: actions/setup-python@v5
17 uses: actions/setup-python@v6
1818 with:
1919 python-version: ${{ matrix.python-version }}
20 ref: ${{ github.event.pull_request.head.ref }}
2120 - name: Install dependencies
2221 run: |
2322 python -m pip install --upgrade pip
24 pip install pandas dash plotly.express
25 - name: Install Ruff
26 uses: astral-sh/ruff-action@v3.2.2
27 - name: Ruff Actions
23 pip install pandas dash plotly.express ruff
24 - name: Ruff
2825 run: |
29 ruff check --fix
30 ruff format
31 - name: Add and Commit
32 uses: EndBug/add-and-commit@v9
33 env:
34 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 with:
36 default_author: github_actions
37 pathspec_error_handling: ignore
26 ruff check .
27 ruff format --check .
applications/github/audit.py +53 −10
@@ -90,8 +90,10 @@ def run():
9090 sections.append((label, len(rows)))
9191 return rows
9292
93 collect("Member roster", members.member_roster, "member_roster.csv", org, cfg)
94 collect("2FA disabled", members.two_factor_disabled, "two_factor_disabled.csv", org, cfg)
93 collect("Member roster", members.member_roster, "member_roster.csv", org, cfg)
94 collect(
95 "2FA disabled", members.two_factor_disabled, "two_factor_disabled.csv", org, cfg
96 )
9597
9698 print("Fetching repo collaborators (shared cache)...")
9799 try:
@@ -100,14 +102,55 @@ def run():
100102 print(f" Error fetching collaborators: {e}", file=sys.stderr)
101103 repo_collabs = []
102104
103 collect("Outside collaborators", members.outside_collaborators, "outside_collaborators.csv", org, cfg, repo_collabs)
104 collect("Privileged access", members.privileged_access, "privileged_access.csv", org, cfg, repo_collabs)
105 collect("Pending invitations", members.pending_invitations, "pending_invitations.csv", org, cfg)
106 collect("Team permissions", members.team_permissions, "team_permissions.csv", org, cfg)
107 collect("Permission matrix", members.permission_matrix, "permission_matrix.csv", org, cfg, repo_collabs)
108 collect("Branch protections", branch_protections.branch_protections, "branch_protections.csv", org, cfg)
109 collect("Commits", commits.commits, "commits.csv", org, cfg, args.branch)
110 collect("Audit log branch/ruleset changes", audit_log.audit_log, "audit_log.csv", org, cfg)
105 collect(
106 "Outside collaborators",
107 members.outside_collaborators,
108 "outside_collaborators.csv",
109 org,
110 cfg,
111 repo_collabs,
112 )
113 collect(
114 "Privileged access",
115 members.privileged_access,
116 "privileged_access.csv",
117 org,
118 cfg,
119 repo_collabs,
120 )
121 collect(
122 "Pending invitations",
123 members.pending_invitations,
124 "pending_invitations.csv",
125 org,
126 cfg,
127 )
128 collect(
129 "Team permissions", members.team_permissions, "team_permissions.csv", org, cfg
130 )
131 collect(
132 "Permission matrix",
133 members.permission_matrix,
134 "permission_matrix.csv",
135 org,
136 cfg,
137 repo_collabs,
138 )
139 collect(
140 "Branch protections",
141 branch_protections.branch_protections,
142 "branch_protections.csv",
143 org,
144 cfg,
145 )
146 collect("Commits", commits.commits, "commits.csv", org, cfg, args.branch)
147 collect(
148 "Audit log branch/ruleset changes",
149 audit_log.audit_log,
150 "audit_log.csv",
151 org,
152 cfg,
153 )
111154
112155 print()
113156 csv_reporter.write_summary(output_dir, org, sections)
applications/github/collectors/api.py +3 −1
@@ -12,7 +12,9 @@ def paginate(url, cfg, params=None):
1212
1313 while True:
1414 p["page"] = page
15 resp = requests.get(url, headers=cfg["headers"], params=p, timeout=cfg["timeout"])
15 resp = requests.get(
16 url, headers=cfg["headers"], params=p, timeout=cfg["timeout"]
17 )
1618 resp.raise_for_status()
1719 data = resp.json()
1820 if not data:
applications/github/collectors/audit_log.py +13 −11
@@ -88,17 +88,19 @@ def audit_log(org, cfg, actions=None, lookback_days=DEFAULT_LOOKBACK_DAYS):
8888
8989 rows = []
9090 for e in _dedupe_events(events):
91 rows.append({
92 "action": e.get("action", ""),
93 "actor": e.get("actor", ""),
94 "repo": e.get("repo", ""),
95 "branch_or_pattern": _branch_or_pattern(e),
96 "operation_type": e.get("operation_type", ""),
97 "summary": _event_summary(e),
98 "details": _event_details(e),
99 "created_at": _format_created_at(e.get("created_at", "")),
100 "org": e.get("org", ""),
101 })
91 rows.append(
92 {
93 "action": e.get("action", ""),
94 "actor": e.get("actor", ""),
95 "repo": e.get("repo", ""),
96 "branch_or_pattern": _branch_or_pattern(e),
97 "operation_type": e.get("operation_type", ""),
98 "summary": _event_summary(e),
99 "details": _event_details(e),
100 "created_at": _format_created_at(e.get("created_at", "")),
101 "org": e.get("org", ""),
102 }
103 )
102104 return rows
103105
104106
applications/github/collectors/branch_protections.py +33 −23
@@ -27,7 +27,10 @@ def branch_protections(org, cfg):
2727 )
2828 except requests.HTTPError as e:
2929 if e.response is not None and e.response.status_code == 403:
30 print(f" Skipping {repo_name}: branches endpoint returned 403", file=sys.stderr)
30 print(
31 f" Skipping {repo_name}: branches endpoint returned 403",
32 file=sys.stderr,
33 )
3134 continue
3235 raise
3336
@@ -40,17 +43,19 @@ def branch_protections(org, cfg):
4043 resp = requests.get(url, headers=cfg["headers"], timeout=cfg["timeout"])
4144
4245 if resp.status_code in (403, 404):
43 rows.append({
44 "repo": repo_name,
45 "branch": branch_name,
46 "protected": False,
47 "required_reviews": None,
48 "dismiss_stale_reviews": None,
49 "require_code_owner_reviews": None,
50 "required_status_checks": None,
51 "enforce_admins": None,
52 "restrictions": None,
53 })
46 rows.append(
47 {
48 "repo": repo_name,
49 "branch": branch_name,
50 "protected": False,
51 "required_reviews": None,
52 "dismiss_stale_reviews": None,
53 "require_code_owner_reviews": None,
54 "required_status_checks": None,
55 "enforce_admins": None,
56 "restrictions": None,
57 }
58 )
5459 continue
5560
5661 resp.raise_for_status()
@@ -58,16 +63,21 @@ def branch_protections(org, cfg):
5863 reviews = p.get("required_pull_request_reviews", {})
5964 checks = p.get("required_status_checks", {})
6065
61 rows.append({
62 "repo": repo_name,
63 "branch": branch_name,
64 "protected": True,
65 "required_reviews": reviews.get("required_approving_review_count"),
66 "dismiss_stale_reviews": reviews.get("dismiss_stale_reviews"),
67 "require_code_owner_reviews": reviews.get("require_code_owner_reviews"),
68 "required_status_checks": ", ".join(checks.get("contexts", [])) or None,
69 "enforce_admins": p.get("enforce_admins", {}).get("enabled"),
70 "restrictions": bool(p.get("restrictions")),
71 })
66 rows.append(
67 {
68 "repo": repo_name,
69 "branch": branch_name,
70 "protected": True,
71 "required_reviews": reviews.get("required_approving_review_count"),
72 "dismiss_stale_reviews": reviews.get("dismiss_stale_reviews"),
73 "require_code_owner_reviews": reviews.get(
74 "require_code_owner_reviews"
75 ),
76 "required_status_checks": ", ".join(checks.get("contexts", []))
77 or None,
78 "enforce_admins": p.get("enforce_admins", {}).get("enabled"),
79 "restrictions": bool(p.get("restrictions")),
80 }
81 )
7282
7383 return rows
applications/github/collectors/commits.py +13 −11
@@ -31,16 +31,18 @@ def commits(org, cfg, branch="main"):
3131 commit = c.get("commit", {})
3232 author = commit.get("author", {})
3333 stats = c.get("stats", {})
34 rows.append({
35 "repo": repo_name,
36 "branch": branch,
37 "sha": c.get("sha", "")[:12],
38 "author_name": author.get("name", ""),
39 "author_email": author.get("email", ""),
40 "date": author.get("date", ""),
41 "message": commit.get("message", "").splitlines()[0],
42 "additions": stats.get("additions", ""),
43 "deletions": stats.get("deletions", ""),
44 })
34 rows.append(
35 {
36 "repo": repo_name,
37 "branch": branch,
38 "sha": c.get("sha", "")[:12],
39 "author_name": author.get("name", ""),
40 "author_email": author.get("email", ""),
41 "date": author.get("date", ""),
42 "message": commit.get("message", "").splitlines()[0],
43 "additions": stats.get("additions", ""),
44 "deletions": stats.get("deletions", ""),
45 }
46 )
4547
4648 return rows
applications/github/collectors/members.py +67 −42
@@ -50,22 +50,31 @@ def fetch_repo_collaborators(org, cfg):
5050 )
5151 except requests.HTTPError as e:
5252 if e.response is not None and e.response.status_code == 403:
53 print(f" Skipping {repo_name}: collaborators endpoint returned 403", file=sys.stderr)
53 print(
54 f" Skipping {repo_name}: collaborators endpoint returned 403",
55 file=sys.stderr,
56 )
5457 continue
5558 raise
56 results.append({
57 "repo": repo_name,
58 "visibility": repo["visibility"],
59 "collaborators": collabs,
60 })
59 results.append(
60 {
61 "repo": repo_name,
62 "visibility": repo["visibility"],
63 "collaborators": collabs,
64 }
65 )
6166 return results
6267
6368
6469def member_roster(org, cfg):
65 members = paginate(f"https://api.github.com/orgs/{org}/members", cfg, {"role": "all"})
70 members = paginate(
71 f"https://api.github.com/orgs/{org}/members", cfg, {"role": "all"}
72 )
6673 owners = {
6774 m["login"]
68 for m in paginate(f"https://api.github.com/orgs/{org}/members", cfg, {"role": "owner"})
75 for m in paginate(
76 f"https://api.github.com/orgs/{org}/members", cfg, {"role": "owner"}
77 )
6978 }
7079 return [
7180 {
@@ -104,18 +113,22 @@ def outside_collaborators(org, cfg, repo_collabs):
104113 """
105114 outside = {
106115 m["login"]
107 for m in paginate(f"https://api.github.com/orgs/{org}/outside_collaborators", cfg)
116 for m in paginate(
117 f"https://api.github.com/orgs/{org}/outside_collaborators", cfg
118 )
108119 }
109120 rows = []
110121 for entry in repo_collabs:
111122 for c in entry["collaborators"]:
112123 if c["login"] in outside:
113 rows.append({
114 "login": c["login"],
115 "repo": entry["repo"],
116 "permission": _permission_level(c.get("permissions", {})),
117 "repo_visibility": entry["visibility"],
118 })
124 rows.append(
125 {
126 "login": c["login"],
127 "repo": entry["repo"],
128 "permission": _permission_level(c.get("permissions", {})),
129 "repo_visibility": entry["visibility"],
130 }
131 )
119132 return rows
120133
121134
@@ -128,12 +141,14 @@ def privileged_access(org, cfg, repo_collabs):
128141 for entry in repo_collabs:
129142 for c in entry["collaborators"]:
130143 if c.get("permissions", {}).get("admin"):
131 rows.append({
132 "login": c["login"],
133 "repo": entry["repo"],
134 "permission": "admin",
135 "repo_visibility": entry["visibility"],
136 })
144 rows.append(
145 {
146 "login": c["login"],
147 "repo": entry["repo"],
148 "permission": "admin",
149 "repo_visibility": entry["visibility"],
150 }
151 )
137152 return rows
138153
139154
@@ -146,13 +161,15 @@ def pending_invitations(org, cfg):
146161 if created:
147162 dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
148163 age_days = (now - dt).days
149 rows.append({
150 "login": inv.get("login") or inv.get("email", "unknown"),
151 "role": inv.get("role", ""),
152 "invited_by": inv.get("inviter", {}).get("login", ""),
153 "created_at": created,
154 "age_days": age_days,
155 })
164 rows.append(
165 {
166 "login": inv.get("login") or inv.get("email", "unknown"),
167 "role": inv.get("role", ""),
168 "invited_by": inv.get("inviter", {}).get("login", ""),
169 "created_at": created,
170 "age_days": age_days,
171 }
172 )
156173 return rows
157174
158175
@@ -160,16 +177,22 @@ def team_permissions(org, cfg):
160177 rows = []
161178 for team in paginate(f"https://api.github.com/orgs/{org}/teams", cfg):
162179 slug = team["slug"]
163 team_members = paginate(f"https://api.github.com/orgs/{org}/teams/{slug}/members", cfg)
164 team_repos = paginate(f"https://api.github.com/orgs/{org}/teams/{slug}/repos", cfg)
180 team_members = paginate(
181 f"https://api.github.com/orgs/{org}/teams/{slug}/members", cfg
182 )
183 team_repos = paginate(
184 f"https://api.github.com/orgs/{org}/teams/{slug}/repos", cfg
185 )
165186 member_logins = ", ".join(m["login"] for m in team_members) or "(none)"
166187 for repo in team_repos:
167 rows.append({
168 "team": team["name"],
169 "repo": repo["name"],
170 "permission": _permission_level(repo.get("permissions", {})),
171 "members": member_logins,
172 })
188 rows.append(
189 {
190 "team": team["name"],
191 "repo": repo["name"],
192 "permission": _permission_level(repo.get("permissions", {})),
193 "members": member_logins,
194 }
195 )
173196 return rows
174197
175198
@@ -181,10 +204,12 @@ def permission_matrix(org, cfg, repo_collabs):
181204 rows = []
182205 for entry in repo_collabs:
183206 for c in entry["collaborators"]:
184 rows.append({
185 "repo": entry["repo"],
186 "login": c["login"],
187 "permission": _permission_level(c.get("permissions", {})),
188 "visibility": entry["visibility"],
189 })
207 rows.append(
208 {
209 "repo": entry["repo"],
210 "login": c["login"],
211 "permission": _permission_level(c.get("permissions", {})),
212 "visibility": entry["visibility"],
213 }
214 )
190215 return rows
applications/github/reporters/csv_reporter.py +3 −3
@@ -31,10 +31,10 @@ def write_summary(output_dir, org, sections):
3131 """
3232 path = os.path.join(output_dir, "summary.txt")
3333 lines = [
34 f"GitHub Audit Package",
34 "GitHub Audit Package",
3535 f"Org: {org}",
36 f"",
37 f"Section Rows",
36 "",
37 "Section Rows",
3838 f"{'─' * 40}",
3939 ]
4040 for label, count in sections: