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:
12 matrix: 12 matrix:
13 python-version: ["3.x"] 13 python-version: ["3.x"]
14 steps: 14 steps:
15 - uses: actions/checkout@v4 15 - uses: actions/checkout@v5
16 - name: Set up Python ${{ matrix.python-version }} 16 - name: Set up Python ${{ matrix.python-version }}
17 uses: actions/setup-python@v5 17 uses: actions/setup-python@v6
18 with: 18 with:
19 python-version: ${{ matrix.python-version }} 19 python-version: ${{ matrix.python-version }}
20 ref: ${{ github.event.pull_request.head.ref }}
21 - name: Install dependencies 20 - name: Install dependencies
22 run: | 21 run: |
23 python -m pip install --upgrade pip 22 python -m pip install --upgrade pip
24 pip install pandas dash plotly.express 23 pip install pandas dash plotly.express ruff
25 - name: Install Ruff 24 - name: Ruff
26 uses: astral-sh/ruff-action@v3.2.2
27 - name: Ruff Actions
28 run: | 25 run: |
29 ruff check --fix 26 ruff check .
30 ruff format 27 ruff format --check .
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
applications/github/audit.py +53 −10
@@ -90,8 +90,10 @@ def run():
90 sections.append((label, len(rows))) 90 sections.append((label, len(rows)))
91 return rows 91 return rows
92 92
93 collect("Member roster", members.member_roster, "member_roster.csv", org, cfg) 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) 94 collect(
95 "2FA disabled", members.two_factor_disabled, "two_factor_disabled.csv", org, cfg
96 )
95 97
96 print("Fetching repo collaborators (shared cache)...") 98 print("Fetching repo collaborators (shared cache)...")
97 try: 99 try:
@@ -100,14 +102,55 @@ def run():
100 print(f" Error fetching collaborators: {e}", file=sys.stderr) 102 print(f" Error fetching collaborators: {e}", file=sys.stderr)
101 repo_collabs = [] 103 repo_collabs = []
102 104
103 collect("Outside collaborators", members.outside_collaborators, "outside_collaborators.csv", org, cfg, repo_collabs) 105 collect(
104 collect("Privileged access", members.privileged_access, "privileged_access.csv", org, cfg, repo_collabs) 106 "Outside collaborators",
105 collect("Pending invitations", members.pending_invitations, "pending_invitations.csv", org, cfg) 107 members.outside_collaborators,
106 collect("Team permissions", members.team_permissions, "team_permissions.csv", org, cfg) 108 "outside_collaborators.csv",
107 collect("Permission matrix", members.permission_matrix, "permission_matrix.csv", org, cfg, repo_collabs) 109 org,
108 collect("Branch protections", branch_protections.branch_protections, "branch_protections.csv", org, cfg) 110 cfg,
109 collect("Commits", commits.commits, "commits.csv", org, cfg, args.branch) 111 repo_collabs,
110 collect("Audit log branch/ruleset changes", audit_log.audit_log, "audit_log.csv", org, cfg) 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 )
111 154
112 print() 155 print()
113 csv_reporter.write_summary(output_dir, org, sections) 156 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):
12 12
13 while True: 13 while True:
14 p["page"] = page 14 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 )
16 resp.raise_for_status() 18 resp.raise_for_status()
17 data = resp.json() 19 data = resp.json()
18 if not data: 20 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):
88 88
89 rows = [] 89 rows = []
90 for e in _dedupe_events(events): 90 for e in _dedupe_events(events):
91 rows.append({ 91 rows.append(
92 "action": e.get("action", ""), 92 {
93 "actor": e.get("actor", ""), 93 "action": e.get("action", ""),
94 "repo": e.get("repo", ""), 94 "actor": e.get("actor", ""),
95 "branch_or_pattern": _branch_or_pattern(e), 95 "repo": e.get("repo", ""),
96 "operation_type": e.get("operation_type", ""), 96 "branch_or_pattern": _branch_or_pattern(e),
97 "summary": _event_summary(e), 97 "operation_type": e.get("operation_type", ""),
98 "details": _event_details(e), 98 "summary": _event_summary(e),
99 "created_at": _format_created_at(e.get("created_at", "")), 99 "details": _event_details(e),
100 "org": e.get("org", ""), 100 "created_at": _format_created_at(e.get("created_at", "")),
101 }) 101 "org": e.get("org", ""),
102 }
103 )
102 return rows 104 return rows
103 105
104 106
applications/github/collectors/branch_protections.py +33 −23
@@ -27,7 +27,10 @@ def branch_protections(org, cfg):
27 ) 27 )
28 except requests.HTTPError as e: 28 except requests.HTTPError as e:
29 if e.response is not None and e.response.status_code == 403: 29 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 )
31 continue 34 continue
32 raise 35 raise
33 36
@@ -40,17 +43,19 @@ def branch_protections(org, cfg):
40 resp = requests.get(url, headers=cfg["headers"], timeout=cfg["timeout"]) 43 resp = requests.get(url, headers=cfg["headers"], timeout=cfg["timeout"])
41 44
42 if resp.status_code in (403, 404): 45 if resp.status_code in (403, 404):
43 rows.append({ 46 rows.append(
44 "repo": repo_name, 47 {
45 "branch": branch_name, 48 "repo": repo_name,
46 "protected": False, 49 "branch": branch_name,
47 "required_reviews": None, 50 "protected": False,
48 "dismiss_stale_reviews": None, 51 "required_reviews": None,
49 "require_code_owner_reviews": None, 52 "dismiss_stale_reviews": None,
50 "required_status_checks": None, 53 "require_code_owner_reviews": None,
51 "enforce_admins": None, 54 "required_status_checks": None,
52 "restrictions": None, 55 "enforce_admins": None,
53 }) 56 "restrictions": None,
57 }
58 )
54 continue 59 continue
55 60
56 resp.raise_for_status() 61 resp.raise_for_status()
@@ -58,16 +63,21 @@ def branch_protections(org, cfg):
58 reviews = p.get("required_pull_request_reviews", {}) 63 reviews = p.get("required_pull_request_reviews", {})
59 checks = p.get("required_status_checks", {}) 64 checks = p.get("required_status_checks", {})
60 65
61 rows.append({ 66 rows.append(
62 "repo": repo_name, 67 {
63 "branch": branch_name, 68 "repo": repo_name,
64 "protected": True, 69 "branch": branch_name,
65 "required_reviews": reviews.get("required_approving_review_count"), 70 "protected": True,
66 "dismiss_stale_reviews": reviews.get("dismiss_stale_reviews"), 71 "required_reviews": reviews.get("required_approving_review_count"),
67 "require_code_owner_reviews": reviews.get("require_code_owner_reviews"), 72 "dismiss_stale_reviews": reviews.get("dismiss_stale_reviews"),
68 "required_status_checks": ", ".join(checks.get("contexts", [])) or None, 73 "require_code_owner_reviews": reviews.get(
69 "enforce_admins": p.get("enforce_admins", {}).get("enabled"), 74 "require_code_owner_reviews"
70 "restrictions": bool(p.get("restrictions")), 75 ),
71 }) 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 )
72 82
73 return rows 83 return rows
applications/github/collectors/commits.py +13 −11
@@ -31,16 +31,18 @@ def commits(org, cfg, branch="main"):
31 commit = c.get("commit", {}) 31 commit = c.get("commit", {})
32 author = commit.get("author", {}) 32 author = commit.get("author", {})
33 stats = c.get("stats", {}) 33 stats = c.get("stats", {})
34 rows.append({ 34 rows.append(
35 "repo": repo_name, 35 {
36 "branch": branch, 36 "repo": repo_name,
37 "sha": c.get("sha", "")[:12], 37 "branch": branch,
38 "author_name": author.get("name", ""), 38 "sha": c.get("sha", "")[:12],
39 "author_email": author.get("email", ""), 39 "author_name": author.get("name", ""),
40 "date": author.get("date", ""), 40 "author_email": author.get("email", ""),
41 "message": commit.get("message", "").splitlines()[0], 41 "date": author.get("date", ""),
42 "additions": stats.get("additions", ""), 42 "message": commit.get("message", "").splitlines()[0],
43 "deletions": stats.get("deletions", ""), 43 "additions": stats.get("additions", ""),
44 }) 44 "deletions": stats.get("deletions", ""),
45 }
46 )
45 47
46 return rows 48 return rows
applications/github/collectors/members.py +67 −42
@@ -50,22 +50,31 @@ def fetch_repo_collaborators(org, cfg):
50 ) 50 )
51 except requests.HTTPError as e: 51 except requests.HTTPError as e:
52 if e.response is not None and e.response.status_code == 403: 52 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 )
54 continue 57 continue
55 raise 58 raise
56 results.append({ 59 results.append(
57 "repo": repo_name, 60 {
58 "visibility": repo["visibility"], 61 "repo": repo_name,
59 "collaborators": collabs, 62 "visibility": repo["visibility"],
60 }) 63 "collaborators": collabs,
64 }
65 )
61 return results 66 return results
62 67
63 68
64def member_roster(org, cfg): 69def 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 )
66 owners = { 73 owners = {
67 m["login"] 74 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 )
69 } 78 }
70 return [ 79 return [
71 { 80 {
@@ -104,18 +113,22 @@ def outside_collaborators(org, cfg, repo_collabs):
104 """ 113 """
105 outside = { 114 outside = {
106 m["login"] 115 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 )
108 } 119 }
109 rows = [] 120 rows = []
110 for entry in repo_collabs: 121 for entry in repo_collabs:
111 for c in entry["collaborators"]: 122 for c in entry["collaborators"]:
112 if c["login"] in outside: 123 if c["login"] in outside:
113 rows.append({ 124 rows.append(
114 "login": c["login"], 125 {
115 "repo": entry["repo"], 126 "login": c["login"],
116 "permission": _permission_level(c.get("permissions", {})), 127 "repo": entry["repo"],
117 "repo_visibility": entry["visibility"], 128 "permission": _permission_level(c.get("permissions", {})),
118 }) 129 "repo_visibility": entry["visibility"],
130 }
131 )
119 return rows 132 return rows
120 133
121 134
@@ -128,12 +141,14 @@ def privileged_access(org, cfg, repo_collabs):
128 for entry in repo_collabs: 141 for entry in repo_collabs:
129 for c in entry["collaborators"]: 142 for c in entry["collaborators"]:
130 if c.get("permissions", {}).get("admin"): 143 if c.get("permissions", {}).get("admin"):
131 rows.append({ 144 rows.append(
132 "login": c["login"], 145 {
133 "repo": entry["repo"], 146 "login": c["login"],
134 "permission": "admin", 147 "repo": entry["repo"],
135 "repo_visibility": entry["visibility"], 148 "permission": "admin",
136 }) 149 "repo_visibility": entry["visibility"],
150 }
151 )
137 return rows 152 return rows
138 153
139 154
@@ -146,13 +161,15 @@ def pending_invitations(org, cfg):
146 if created: 161 if created:
147 dt = datetime.fromisoformat(created.replace("Z", "+00:00")) 162 dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
148 age_days = (now - dt).days 163 age_days = (now - dt).days
149 rows.append({ 164 rows.append(
150 "login": inv.get("login") or inv.get("email", "unknown"), 165 {
151 "role": inv.get("role", ""), 166 "login": inv.get("login") or inv.get("email", "unknown"),
152 "invited_by": inv.get("inviter", {}).get("login", ""), 167 "role": inv.get("role", ""),
153 "created_at": created, 168 "invited_by": inv.get("inviter", {}).get("login", ""),
154 "age_days": age_days, 169 "created_at": created,
155 }) 170 "age_days": age_days,
171 }
172 )
156 return rows 173 return rows
157 174
158 175
@@ -160,16 +177,22 @@ def team_permissions(org, cfg):
160 rows = [] 177 rows = []
161 for team in paginate(f"https://api.github.com/orgs/{org}/teams", cfg): 178 for team in paginate(f"https://api.github.com/orgs/{org}/teams", cfg):
162 slug = team["slug"] 179 slug = team["slug"]
163 team_members = paginate(f"https://api.github.com/orgs/{org}/teams/{slug}/members", cfg) 180 team_members = paginate(
164 team_repos = paginate(f"https://api.github.com/orgs/{org}/teams/{slug}/repos", cfg) 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 )
165 member_logins = ", ".join(m["login"] for m in team_members) or "(none)" 186 member_logins = ", ".join(m["login"] for m in team_members) or "(none)"
166 for repo in team_repos: 187 for repo in team_repos:
167 rows.append({ 188 rows.append(
168 "team": team["name"], 189 {
169 "repo": repo["name"], 190 "team": team["name"],
170 "permission": _permission_level(repo.get("permissions", {})), 191 "repo": repo["name"],
171 "members": member_logins, 192 "permission": _permission_level(repo.get("permissions", {})),
172 }) 193 "members": member_logins,
194 }
195 )
173 return rows 196 return rows
174 197
175 198
@@ -181,10 +204,12 @@ def permission_matrix(org, cfg, repo_collabs):
181 rows = [] 204 rows = []
182 for entry in repo_collabs: 205 for entry in repo_collabs:
183 for c in entry["collaborators"]: 206 for c in entry["collaborators"]:
184 rows.append({ 207 rows.append(
185 "repo": entry["repo"], 208 {
186 "login": c["login"], 209 "repo": entry["repo"],
187 "permission": _permission_level(c.get("permissions", {})), 210 "login": c["login"],
188 "visibility": entry["visibility"], 211 "permission": _permission_level(c.get("permissions", {})),
189 }) 212 "visibility": entry["visibility"],
213 }
214 )
190 return rows 215 return rows
applications/github/reporters/csv_reporter.py +3 −3
@@ -31,10 +31,10 @@ def write_summary(output_dir, org, sections):
31 """ 31 """
32 path = os.path.join(output_dir, "summary.txt") 32 path = os.path.join(output_dir, "summary.txt")
33 lines = [ 33 lines = [
34 f"GitHub Audit Package", 34 "GitHub Audit Package",
35 f"Org: {org}", 35 f"Org: {org}",
36 f"", 36 "",
37 f"Section Rows", 37 "Section Rows",
38 f"{'─' * 40}", 38 f"{'─' * 40}",
39 ] 39 ]
40 for label, count in sections: 40 for label, count in sections: