audit-labs/audit-tools

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

Commit 7f54baf67e

7f54baf67ea1cf422440226bd66405e75c524346

parent: b9a6394f09

Unsigned

cmc <hello@cleberg.net> · 2026-03-23 04:13 UTC

Add unified GitHub audit tool

- Add audit.py CLI entrypoint (argparse, env var config)
- Add collectors/ package: members, branch_protections, commits, audit_log
- Add reporters/ package: csv_reporter
- Fetch repo collaborators once, share cache across three sections
- Add two_factor_disabled collector
- Remove legacy standalone scripts
- Update .gitignore: __pycache__, *.pyc, output/
- Add os/windows/ and databases/snowflake/ READMEs (planned)

Layout: unified · split

.gitignore +7
@@ -1,3 +1,10 @@
1.venv 1.venv
2venv 2venv
3readme.html 3readme.html
4
5# Python
6__pycache__/
7*.py[cod]
8
9# Audit output
10applications/github/output/
applications/github/README.md +36 −96
@@ -1,113 +1,53 @@
1**NOTE**: I used the same 1> **NOTE**: The PAT used across all scripts needs the following minimum permissions:
2[PAT](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) 2> - Repository: Actions (read), Contents (read), Metadata (read), Workflows (read)
3for all scripts within this folder. Note that you can likely reduce 3> - Organization: Administration (read), Members (read)
4permissions for certain scripts - it's best practice to define a PAT for
5a specific purpose and avoid using a single PAT with broad permissions.
6 4
7- Personal Access Token: 5---
8 - \[x\] Repository Permissions
9 - \[x\] Actions: read-only
10 - \[x\] Contents: read-only
11 - \[x\] Metadata: read-only
12 - \[x\] Workflows: read-only
13 - \[x\] Organization Permissions
14 - \[x\] Administration: read-only
15 6
16# `github_admins.py` 7# `audit.py` — Unified GitHub Audit Tool
17 8
18``` bash 9Runs all collectors against a GitHub organization and writes a timestamped
19python ./github_admins.py 10audit package to disk.
20```
21
22``` text
23Members of the organization 'your_organization':
24 11
25Repositories in the organization 'your_organization': 12## Setup
26- demo-repository
27 13
28Collaborators for the repository 'demo-repository': 14```bash
29- user1: admin 15export GITHUB_TOKEN=your_token
16export GITHUB_ORG=your_organization
30``` 17```
31 18
32# `github_audit_log.py` 19## Usage
33 20
34**NOTE**: Requires an active GitHub Enterprise subscription. 21```bash
22# Basic run — uses GITHUB_TOKEN and GITHUB_ORG from environment
23python audit.py
35 24
36``` bash 25# Override org, set output directory
37python ./github_audit_log.py 26python audit.py --org my-org --out ./output
38```
39 27
40``` text 28# Collect commits from a non-default branch
41TODO: Need to get an Enterprise subscription to test this script. 29python audit.py --branch develop
42```
43 30
44# `github_branch_protections.py` 31# Include audit log (requires GitHub Enterprise)
45 32python audit.py --include-audit-log
46``` bash
47python ./github_branch_protections.py
48``` 33```
49 34
50``` text 35## Output
51Total branches in the repository 'demo-repository': 1
52 36
53Branch: main 37Creates a directory: `<out>/github_audit_<org>_<YYYY-MM-DD>/`
54No protection settings
55 38
56Repository rulesets for 'demo-repository': 39| File | Contents |
57[{'id': 2311373, 'name': 'default', 'target': 'branch', 'source_type': 'Repository', 'source': 'phryq/demo-repository', 'enforcement': 'active', 'node_id': 'RRS_lACqUmVwb3NpdG9yec40LV1PzgAjRM0', '_links': {'self': {'href': 'https://api.github.com/repos/phryq/demo-repository/rulesets/2311373'}, 'html': {'href': 'https://github.com/phryq/demo-repository/rules/2311373'}}, 'created_at': '2024-10-19T15:59:35.200-05:00', 'updated_at': '2024-10-19T15:59:35.200-05:00'}] 40|---|---|
58``` 41| `member_roster.csv` | All org members with role (owner vs member) |
42| `two_factor_disabled.csv` | Org members without 2FA enabled |
43| `outside_collaborators.csv` | Non-org members with direct repo access |
44| `privileged_access.csv` | All users with admin permission on any repo |
45| `pending_invitations.csv` | Invitations not yet accepted, with age in days |
46| `team_permissions.csv` | Teams, their repos, permissions, and members |
47| `permission_matrix.csv` | Full user/repo/permission cross-reference |
48| `branch_protections.csv` | Branch protection settings across all repos |
49| `commits.csv` | Commit history across all repos for the target branch |
50| `audit_log.csv` | Org-level audit events (Enterprise only, opt-in) |
51| `summary.txt` | Row counts per section |
59 52
60# `github_commits.py`
61 53
62``` bash
63python ./github_commits.py
64```
65
66``` text
67Total commits in the repository 'demo-repository' on branch 'main': 3
68
69Commit SHA: 13c488a2cdda08e4043f8ef36ced5fdd429e9718
70Author: Christian Cleberg <156287552+ccleberg@users.noreply.github.com>
71Date: 2024-10-19T20:57:55Z
72Message: Merge pull request #2 from phryq/1-test-issue
73
74fixes
75URL: https://github.com/phryq/demo-repository/commit/13c488a2cdda08e4043f8ef36ced5fdd429e9718
76Files changed:
77 - .gitignore (added)
78 Additions: 0, Deletions: 0, Changes: 0
79 - README.md (removed)
80 Additions: 0, Deletions: 4, Changes: 4
81 - README.org (added)
82 Additions: 7, Deletions: 0, Changes: 7
83
84Commit SHA: 6bfde238a2a34a93ce8ee02082eaf4ab3c189368
85Author: Christian Cleberg <hello@cmc.pub>
86Date: 2024-10-19T20:56:50Z
87Message: fixes
88URL: https://github.com/phryq/demo-repository/commit/6bfde238a2a34a93ce8ee02082eaf4ab3c189368
89Files changed:
90 - .gitignore (added)
91 Additions: 0, Deletions: 0, Changes: 0
92 - README.md (removed)
93 Additions: 0, Deletions: 4, Changes: 4
94 - README.org (added)
95 Additions: 7, Deletions: 0, Changes: 7
96
97Commit SHA: be1ddf31e08fc790f54d68f8067b7b2f3805f999
98Author: Christian Cleberg <156287552+ccleberg@users.noreply.github.com>
99Date: 2024-10-19T20:54:08Z
100Message: Initial commit
101URL: https://github.com/phryq/demo-repository/commit/be1ddf31e08fc790f54d68f8067b7b2f3805f999
102Files changed:
103 - .github/workflows/auto-assign.yml (added)
104 Additions: 19, Deletions: 0, Changes: 19
105 - .github/workflows/proof-html.yml (added)
106 Additions: 11, Deletions: 0, Changes: 11
107 - README.md (added)
108 Additions: 4, Deletions: 0, Changes: 4
109 - index.html (added)
110 Additions: 1, Deletions: 0, Changes: 1
111 - package.json (added)
112 Additions: 9, Deletions: 0, Changes: 9
113```
applications/github/audit.py added +121
@@ -0,0 +1,121 @@
1"""
2GitHub audit CLI.
3
4Runs all collectors against a GitHub organization and writes a timestamped
5audit package to an output directory.
6
7Usage:
8 export GITHUB_TOKEN=your_token
9 export GITHUB_ORG=your_org
10
11 python audit.py
12 python audit.py --org my-org
13 python audit.py --org my-org --out ./output
14 python audit.py --org my-org --branch main --include-audit-log
15
16Output:
17 <out>/github_audit_<org>_<date>/
18 member_roster.csv
19 two_factor_disabled.csv
20 outside_collaborators.csv
21 privileged_access.csv
22 pending_invitations.csv
23 team_permissions.csv
24 permission_matrix.csv
25 branch_protections.csv
26 commits.csv
27 audit_log.csv (only with --include-audit-log)
28 summary.txt
29"""
30
31import argparse
32import os
33import sys
34from datetime import date
35
36import config
37from collectors import members, branch_protections, commits, audit_log
38from reporters import csv_reporter
39
40
41def parse_args():
42 parser = argparse.ArgumentParser(
43 description="Generate a GitHub audit package for an organization."
44 )
45 parser.add_argument(
46 "--org",
47 help="GitHub organization name. Overrides GITHUB_ORG env var.",
48 )
49 parser.add_argument(
50 "--out",
51 default="./output",
52 help="Directory to write the audit package into. Default: ./output",
53 )
54 parser.add_argument(
55 "--branch",
56 default="main",
57 help="Branch to collect commits from. Default: main",
58 )
59 parser.add_argument(
60 "--include-audit-log",
61 action="store_true",
62 help="Include audit log collection (requires GitHub Enterprise).",
63 )
64 return parser.parse_args()
65
66
67def run():
68 args = parse_args()
69 cfg = config.load(org_override=args.org)
70 org = cfg["org"]
71
72 output_dir = os.path.join(
73 args.out, f"github_audit_{org}_{date.today().isoformat()}"
74 )
75
76 print(f"GitHub Audit — {org}")
77 print(f"Output directory: {output_dir}")
78 print()
79
80 sections = []
81
82 def collect(label, fn, filename, *fn_args):
83 print(f"Collecting: {label}...")
84 try:
85 rows = fn(*fn_args)
86 except Exception as e:
87 print(f" Error: {e}", file=sys.stderr)
88 rows = []
89 csv_reporter.write(output_dir, filename, rows)
90 sections.append((label, len(rows)))
91 return rows
92
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)
95
96 print("Fetching repo collaborators (shared cache)...")
97 try:
98 repo_collabs = members.fetch_repo_collaborators(org, cfg)
99 except Exception as e:
100 print(f" Error fetching collaborators: {e}", file=sys.stderr)
101 repo_collabs = []
102
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
111 if args.include_audit_log:
112 collect("Audit log", audit_log.audit_log, "audit_log.csv", org, cfg)
113
114 print()
115 csv_reporter.write_summary(output_dir, org, sections)
116 print()
117 print("Done.")
118
119
120if __name__ == "__main__":
121 run()
applications/github/collectors/__init__.py added +1
@@ -0,0 +1 @@
1"""Package init files."""
applications/github/collectors/api.py added +25
@@ -0,0 +1,25 @@
1"""Shared GitHub API helper."""
2
3import requests
4
5
6def paginate(url, cfg, params=None):
7 """Fetch all pages from a GitHub API endpoint and return combined results."""
8 results = []
9 p = dict(params or {})
10 p["per_page"] = 100
11 page = 1
12
13 while True:
14 p["page"] = page
15 resp = requests.get(url, headers=cfg["headers"], params=p, timeout=cfg["timeout"])
16 resp.raise_for_status()
17 data = resp.json()
18 if not data:
19 break
20 results.extend(data)
21 if "next" not in resp.links:
22 break
23 page += 1
24
25 return results
applications/github/collectors/audit_log.py added +53
@@ -0,0 +1,53 @@
1"""
2Collect GitHub audit log events.
3
4Requires GitHub Enterprise. Skips gracefully with a warning if not available.
5"""
6
7import sys
8
9from .api import paginate
10
11# Default event categories relevant to a security audit
12DEFAULT_ACTIONS = [
13 "org.add_member",
14 "org.remove_member",
15 "org.update_member",
16 "protected_branch",
17 "repo.access",
18 "repo.create",
19 "repo.destroy",
20 "team.add_member",
21 "team.remove_member",
22]
23
24
25def audit_log(org, cfg, actions=None):
26 """
27 Return audit log events filtered by action list.
28 Returns an empty list with a warning if the org is not on GitHub Enterprise.
29 """
30 actions = actions or DEFAULT_ACTIONS
31 url = f"https://api.github.com/orgs/{org}/audit-log"
32
33 try:
34 events = paginate(url, cfg, {"action": ",".join(actions)})
35 except Exception as e:
36 if "403" in str(e) or "404" in str(e):
37 print(
38 "Warning: audit log requires GitHub Enterprise -- skipping.",
39 file=sys.stderr,
40 )
41 return []
42 raise
43
44 rows = []
45 for e in events:
46 rows.append({
47 "action": e.get("action", ""),
48 "actor": e.get("actor", ""),
49 "repo": e.get("repo", ""),
50 "created_at": e.get("created_at", ""),
51 "org": e.get("org", ""),
52 })
53 return rows
applications/github/collectors/branch_protections.py added +73
@@ -0,0 +1,73 @@
1"""
2Collect branch protection and ruleset data across all repos in an org.
3"""
4
5import sys
6
7import requests
8
9from .api import paginate
10
11
12def branch_protections(org, cfg):
13 """
14 For each repo, return protection settings per branch and any rulesets.
15 Branches with no protection are included with protected=False.
16 Repos that return 403 on the branches endpoint are skipped with a warning.
17 """
18 repos = paginate(f"https://api.github.com/orgs/{org}/repos", cfg)
19 rows = []
20
21 for repo in repos:
22 repo_name = repo["name"]
23
24 try:
25 branches = paginate(
26 f"https://api.github.com/repos/{org}/{repo_name}/branches", cfg
27 )
28 except requests.HTTPError as e:
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)
31 continue
32 raise
33
34 for branch in branches:
35 branch_name = branch["name"]
36 url = (
37 f"https://api.github.com/repos/{org}/{repo_name}"
38 f"/branches/{branch_name}/protection"
39 )
40 resp = requests.get(url, headers=cfg["headers"], timeout=cfg["timeout"])
41
42 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 })
54 continue
55
56 resp.raise_for_status()
57 p = resp.json()
58 reviews = p.get("required_pull_request_reviews", {})
59 checks = p.get("required_status_checks", {})
60
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 })
72
73 return rows
applications/github/collectors/commits.py added +46
@@ -0,0 +1,46 @@
1"""
2Collect commit history for all repos in an org.
3"""
4
5from .api import paginate
6
7
8def commits(org, cfg, branch="main"):
9 """
10 Return commits across all repos. Each row includes repo, branch, sha,
11 author, date, message (first line), and change counts.
12
13 Skips repos where the branch doesn't exist.
14 """
15 repos = paginate(f"https://api.github.com/orgs/{org}/repos", cfg)
16 rows = []
17
18 for repo in repos:
19 repo_name = repo["name"]
20 try:
21 repo_commits = paginate(
22 f"https://api.github.com/repos/{org}/{repo_name}/commits",
23 cfg,
24 {"sha": branch},
25 )
26 except Exception:
27 # Branch doesn't exist in this repo or other API error -- skip
28 continue
29
30 for c in repo_commits:
31 commit = c.get("commit", {})
32 author = commit.get("author", {})
33 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 })
45
46 return rows
applications/github/collectors/members.py added +190
@@ -0,0 +1,190 @@
1"""
2Collect org membership, access, and permission data.
3
4Covers:
5- Org member roster with roles (owner vs member)
6- Outside collaborators
7- Privileged access (admin permission on any repo)
8- Pending org invitations
9- Team memberships and repo permissions
10- Full per-repo permission matrix
11- Members with 2FA disabled
12"""
13
14import sys
15from datetime import datetime, timezone
16
17import requests
18
19from .api import paginate
20
21
22def _permission_level(perms):
23 for level in ("admin", "maintain", "push", "triage", "pull"):
24 if perms.get(level):
25 return "write" if level == "push" else level
26 return "unknown"
27
28
29def _repos(org, cfg):
30 return paginate(f"https://api.github.com/orgs/{org}/repos", cfg)
31
32
33def fetch_repo_collaborators(org, cfg):
34 """
35 Fetch collaborators for every repo once (affiliation=all).
36 Returns a list of dicts: {repo, visibility, collaborators}.
37 Repos that 403 are skipped with a warning.
38 This cache is passed into outside_collaborators, privileged_access,
39 and permission_matrix to avoid redundant API calls.
40 """
41 repos = _repos(org, cfg)
42 results = []
43 for repo in repos:
44 repo_name = repo["name"]
45 try:
46 collabs = paginate(
47 f"https://api.github.com/repos/{org}/{repo_name}/collaborators",
48 cfg,
49 {"affiliation": "all"},
50 )
51 except requests.HTTPError as e:
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)
54 continue
55 raise
56 results.append({
57 "repo": repo_name,
58 "visibility": repo["visibility"],
59 "collaborators": collabs,
60 })
61 return results
62
63
64def member_roster(org, cfg):
65 members = paginate(f"https://api.github.com/orgs/{org}/members", cfg, {"role": "all"})
66 owners = {
67 m["login"]
68 for m in paginate(f"https://api.github.com/orgs/{org}/members", cfg, {"role": "owner"})
69 }
70 return [
71 {
72 "login": m["login"],
73 "org_role": "owner" if m["login"] in owners else "member",
74 "profile_url": m["html_url"],
75 }
76 for m in members
77 ]
78
79
80def two_factor_disabled(org, cfg):
81 """
82 List org members who do not have 2FA enabled.
83 Requires the token to have read:org scope.
84 Note: GitHub only exposes this to org owners.
85 """
86 members = paginate(
87 f"https://api.github.com/orgs/{org}/members",
88 cfg,
89 {"filter": "2fa_disabled"},
90 )
91 return [
92 {
93 "login": m["login"],
94 "profile_url": m["html_url"],
95 }
96 for m in members
97 ]
98
99
100def outside_collaborators(org, cfg, repo_collabs):
101 """
102 Non-org members with direct repo access.
103 Accepts pre-fetched repo_collabs from fetch_repo_collaborators().
104 """
105 outside = {
106 m["login"]
107 for m in paginate(f"https://api.github.com/orgs/{org}/outside_collaborators", cfg)
108 }
109 rows = []
110 for entry in repo_collabs:
111 for c in entry["collaborators"]:
112 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 })
119 return rows
120
121
122def privileged_access(org, cfg, repo_collabs):
123 """
124 All users with admin permission on any repo.
125 Accepts pre-fetched repo_collabs from fetch_repo_collaborators().
126 """
127 rows = []
128 for entry in repo_collabs:
129 for c in entry["collaborators"]:
130 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 })
137 return rows
138
139
140def pending_invitations(org, cfg):
141 now = datetime.now(timezone.utc)
142 rows = []
143 for inv in paginate(f"https://api.github.com/orgs/{org}/invitations", cfg):
144 created = inv.get("created_at", "")
145 age_days = None
146 if created:
147 dt = datetime.fromisoformat(created.replace("Z", "+00:00"))
148 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 })
156 return rows
157
158
159def team_permissions(org, cfg):
160 rows = []
161 for team in paginate(f"https://api.github.com/orgs/{org}/teams", cfg):
162 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)
165 member_logins = ", ".join(m["login"] for m in team_members) or "(none)"
166 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 })
173 return rows
174
175
176def permission_matrix(org, cfg, repo_collabs):
177 """
178 Full per-repo/per-user permission cross-reference.
179 Accepts pre-fetched repo_collabs from fetch_repo_collaborators().
180 """
181 rows = []
182 for entry in repo_collabs:
183 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 })
190 return rows
applications/github/config.py added +40
@@ -0,0 +1,40 @@
1"""
2Configuration loader for the GitHub audit tool.
3
4Reads GITHUB_TOKEN and GITHUB_ORG from environment variables.
5
6Usage:
7 export GITHUB_TOKEN=your_token
8 export GITHUB_ORG=your_organization
9"""
10
11import os
12import sys
13
14
15def load(org_override=None):
16 """
17 Return a config dict. Exits with an error if required values are missing.
18 """
19 token = os.environ.get("GITHUB_TOKEN", "").strip()
20 org = org_override or os.environ.get("GITHUB_ORG", "").strip()
21
22 missing = []
23 if not token:
24 missing.append("GITHUB_TOKEN")
25 if not org:
26 missing.append("GITHUB_ORG (or pass --org)")
27
28 if missing:
29 print(f"Error: missing required values: {', '.join(missing)}", file=sys.stderr)
30 sys.exit(1)
31
32 return {
33 "token": token,
34 "org": org,
35 "headers": {
36 "Authorization": f"token {token}",
37 "Accept": "application/vnd.github.v3+json",
38 },
39 "timeout": 30,
40 }
applications/github/github_admins.py deleted −83
@@ -1,83 +0,0 @@
1"""
2Gather all members of a GitHub organization, all repos within that organization,
3and list each user's permission per repo.
4"""
5
6import requests
7
8GITHUB_TOKEN = "your_personal_access_token"
9ORGANIZATION = "your_organization"
10TIMEOUT = 30
11
12# Headers for authentication
13headers = {
14 "Authorization": f"token {GITHUB_TOKEN}",
15 "Accept": "application/vnd.github.v3+json",
16}
17
18
19def get_org_members(org):
20 """
21 Get members of an organization
22 """
23 url = f"https://api.github.com/orgs/{org}/members"
24 response = requests.get(url, headers=headers, timeout=TIMEOUT)
25 response.raise_for_status()
26 return response.json()
27
28
29def get_org_repos(org):
30 """
31 Get repositories of an organization
32 """
33 url = f"https://api.github.com/orgs/{org}/repos"
34 response = requests.get(url, headers=headers, timeout=TIMEOUT)
35 response.raise_for_status()
36 return response.json()
37
38
39def get_repo_collaborators(org, repo):
40 """
41 Get collaborators of a repository with their permissions
42 """
43 url = f"https://api.github.com/repos/{org}/{repo}/collaborators"
44 response = requests.get(url, headers=headers, timeout=TIMEOUT)
45 response.raise_for_status()
46 return response.json()
47
48
49def get_user_permissions(org, repo, user):
50 """
51 Get a user's permissions for a repository
52 """
53 url = f"https://api.github.com/repos/{org}/{repo}/collaborators/{user}/permission"
54 response = requests.get(url, headers=headers, timeout=TIMEOUT)
55 response.raise_for_status()
56 return response.json()
57
58
59# Main script
60if __name__ == "__main__":
61 # Get organization members
62 members = get_org_members(ORGANIZATION)
63 print(f"Members of the organization '{ORGANIZATION}':")
64 for member in members:
65 print(f"- {member['login']}")
66
67 # Get organization repositories
68 repositories = get_org_repos(ORGANIZATION)
69 print(f"\nRepositories in the organization '{ORGANIZATION}':")
70 for repository in repositories:
71 print(f"- {repository['name']}")
72
73 # Get collaborators for each repository and their permissions
74 for repository in repositories:
75 repository_name = repository["name"]
76 collaborators = get_repo_collaborators(ORGANIZATION, repository_name)
77 print(f"\nCollaborators for the repository '{repository_name}':")
78 for collaborator in collaborators:
79 user_login = collaborator["login"]
80 permissions = get_user_permissions(
81 ORGANIZATION, repository_name, user_login
82 )
83 print(f"- {user_login}: {permissions['permission']}")
applications/github/github_audit_log.py deleted −65
@@ -1,65 +0,0 @@
1"""
2Extract a specific list of events from the GitHub Audit Log API.
3
4NOTE: REQUIRES A GITHUB ENTERPRISE SUBSCRIPTION TO ACCESS THE API.
5"""
6
7import requests
8
9GITHUB_TOKEN = "your_personal_access_token"
10ORGANIZATION = "your_organization"
11TIMEOUT = 30
12
13# Headers for authentication
14headers = {
15 "Authorization": f"token {GITHUB_TOKEN}",
16 "Accept": "application/vnd.github.v3+json",
17}
18
19
20def get_audit_log_events(org, actions):
21 """
22 Get audit log events for specific actions
23 """
24 events = []
25 page = 1
26 while True:
27 url = (
28 f"https://api.github.com/orgs/{org}/audit-log?page={page}&per_page=100"
29 f"&action={','.join(actions)}"
30 )
31 response = requests.get(url, headers=headers, timeout=TIMEOUT)
32 response.raise_for_status()
33 page_events = response.json()
34 if not page_events:
35 break
36 events.extend(page_events)
37 page += 1
38 return events
39
40
41if __name__ == "__main__":
42 try:
43 # Define the actions to filter
44 action_filters = [
45 "protected_branch",
46 "repository_branch_protection_evaluation",
47 "repository_ruleset",
48 ]
49
50 # Get audit log events for the specified actions
51 audit_log_events = get_audit_log_events(ORGANIZATION, action_filters)
52 print(f"Total audit log events for specified actions: {len(audit_log_events)}")
53
54 # Print detailed information for each event
55 for event in audit_log_events:
56 print(f"\nEvent ID: {event['@id']}")
57 print(f"Action: {event['action']}")
58 print(f"Actor: {event['actor']}")
59 print(f"Repository: {event.get('repo', 'N/A')}")
60 print(f"Created At: {event['created_at']}")
61 print(f"Details: {event}")
62 except requests.exceptions.Timeout:
63 print("The request timed out")
64 except requests.exceptions.RequestException as e:
65 print(f"An error occurred: {e}")
applications/github/github_branch_protections.py deleted −84
@@ -1,84 +0,0 @@
1"""
2Gathers branch protection rules for a repository.
3"""
4
5import requests
6
7GITHUB_TOKEN = "your_personal_access_token"
8ORGANIZATION = "your_organization"
9REPOSITORY = "your_repository"
10TIMEOUT = 30
11
12headers = {
13 "Authorization": f"token {GITHUB_TOKEN}",
14 "Accept": "application/vnd.github.v3+json",
15}
16
17
18def get_all_branches(org, repo):
19 """
20 Get all branches in a repository
21 """
22 all_branches = []
23 page = 1
24 while True:
25 url = f"https://api.github.com/repos/{org}/{repo}/branches?page={page}&per_page=100"
26 response = requests.get(url, headers=headers, timeout=TIMEOUT)
27 response.raise_for_status()
28 page_branches = response.json()
29 if not page_branches:
30 break
31 all_branches.extend(page_branches)
32 page += 1
33 return all_branches
34
35
36def get_branch_protection(org, repo, repo_branch):
37 """
38 Get branch protection settings
39 """
40 url = f"https://api.github.com/repos/{org}/{repo}/branches/{repo_branch}/protection"
41 response = requests.get(url, headers=headers, timeout=TIMEOUT)
42 if response.status_code == 404:
43 return None # No protection settings for this branch
44 response.raise_for_status()
45 return response.json()
46
47
48def get_repository_rulesets(org, repo):
49 """
50 Get repository rulesets
51 """
52 url = f"https://api.github.com/repos/{org}/{repo}/rulesets"
53 response = requests.get(url, headers=headers, timeout=TIMEOUT)
54 response.raise_for_status()
55 return response.json()
56
57
58if __name__ == "__main__":
59 try:
60 # Get all branches in the repository
61 branches = get_all_branches(ORGANIZATION, REPOSITORY)
62 print(f"Total branches in the repository '{REPOSITORY}': {len(branches)}")
63
64 # Get protection settings for each branch
65 for branch in branches:
66 branch_name = branch["name"]
67 protection_settings = get_branch_protection(
68 ORGANIZATION, REPOSITORY, branch_name
69 )
70 print(f"\nBranch: {branch_name}")
71 if protection_settings:
72 print(f"Protection settings: {protection_settings}")
73 else:
74 print("No protection settings")
75
76 # Get repository rulesets
77 rulesets = get_repository_rulesets(ORGANIZATION, REPOSITORY)
78 print(f"\nRepository rulesets for '{REPOSITORY}':")
79 print(rulesets)
80
81 except requests.exceptions.Timeout:
82 print("The request timed out")
83 except requests.exceptions.RequestException as e:
84 print(f"An error occurred: {e}")
applications/github/github_commits.py deleted −85
@@ -1,85 +0,0 @@
1"""
2Gather all commits from a specific branch of a repository in a GitHub organization.
3"""
4
5import requests
6
7GITHUB_TOKEN = "your_personal_access_token"
8ORGANIZATION = "your_organization"
9REPOSITORY = "your_repository"
10BRANCH = "your_branch"
11
12# Headers for authentication
13headers = {
14 "Authorization": f"token {GITHUB_TOKEN}",
15 "Accept": "application/vnd.github.v3+json",
16}
17
18# Define a timeout value (in seconds)
19TIMEOUT = 10
20
21
22def get_commit_log(org, repo, branch):
23 """
24 Get the full commit log for a repository branch
25 """
26 commits = []
27 page = 1
28 while True:
29 url = (
30 f"https://api.github.com/repos/{org}/{repo}/commits?sha={branch}"
31 f"&page={page}&per_page=100"
32 )
33 response = requests.get(url, headers=headers, timeout=TIMEOUT)
34 response.raise_for_status()
35 page_commits = response.json()
36 if not page_commits:
37 break
38 commits.extend(page_commits)
39 page += 1
40 return commits
41
42
43def get_commit_details(org, repo, sha):
44 """
45 Get detailed information for a specific commit
46 """
47 url = f"https://api.github.com/repos/{org}/{repo}/commits/{sha}"
48 response = requests.get(url, headers=headers, timeout=TIMEOUT)
49 response.raise_for_status()
50 return response.json()
51
52
53if __name__ == "__main__":
54 try:
55 # Get the full commit log for the specified branch
56 commit_log = get_commit_log(ORGANIZATION, REPOSITORY, BRANCH)
57 print(
58 f"Total commits in the repository '{REPOSITORY}' on branch "
59 f"'{BRANCH}': {len(commit_log)}"
60 )
61
62 # Get detailed information for each commit
63 for commit in commit_log:
64 sha_hash = commit["sha"]
65 commit_details = get_commit_details(ORGANIZATION, REPOSITORY, sha_hash)
66 print(f"\nCommit SHA: {commit_details['sha']}")
67 print(
68 f"Author: {commit_details['commit']['author']['name']} "
69 f"<{commit_details['commit']['author']['email']}>"
70 )
71 print(f"Date: {commit_details['commit']['author']['date']}")
72 print(f"Message: {commit_details['commit']['message']}")
73 print(f"URL: {commit_details['html_url']}")
74 print("Files changed:")
75 for file in commit_details["files"]:
76 print(f" - {file['filename']} ({file['status']})")
77 print(
78 f" Additions: {file['additions']}, "
79 f"Deletions: {file['deletions']}, "
80 f"Changes: {file['changes']}"
81 )
82 except requests.exceptions.Timeout:
83 print("The request timed out")
84 except requests.exceptions.RequestException as e:
85 print(f"An error occurred: {e}")
applications/github/reporters/__init__.py added +1
@@ -0,0 +1 @@
1"""Package init files."""
applications/github/reporters/csv_reporter.py added +46
@@ -0,0 +1,46 @@
1"""CSV reporter: writes one CSV file per data section into an output directory."""
2
3import csv
4import os
5
6
7def write(output_dir, filename, rows):
8 """
9 Write a list of dicts to a CSV file in output_dir.
10 Skips writing if rows is empty, but logs the skip.
11 """
12 if not rows:
13 print(f" {filename}: no data, skipping")
14 return
15
16 os.makedirs(output_dir, exist_ok=True)
17 path = os.path.join(output_dir, filename)
18
19 with open(path, "w", newline="", encoding="utf-8") as f:
20 writer = csv.DictWriter(f, fieldnames=rows[0].keys())
21 writer.writeheader()
22 writer.writerows(rows)
23
24 print(f" {filename}: {len(rows)} rows -> {path}")
25
26
27def write_summary(output_dir, org, sections):
28 """
29 Write a plain-text summary file listing section names and row counts.
30 sections: list of (label, row_count) tuples
31 """
32 path = os.path.join(output_dir, "summary.txt")
33 lines = [
34 f"GitHub Audit Package",
35 f"Org: {org}",
36 f"",
37 f"Section Rows",
38 f"{'─' * 40}",
39 ]
40 for label, count in sections:
41 lines.append(f"{label:<35}{count}")
42
43 with open(path, "w", encoding="utf-8") as f:
44 f.write("\n".join(lines) + "\n")
45
46 print(f" summary.txt -> {path}")
databases/snowflake/README.md added +30
@@ -0,0 +1,30 @@
1# databases/snowflake
2
3> Planned SQL scripts for Snowflake security audits. Mirrors the style of `databases/postgres/`
4> and `databases/mysql/`. All queries target `SNOWFLAKE.ACCOUNT_USAGE` views, which require
5> the ACCOUNTADMIN role or a role granted the SNOWFLAKE database privilege.
6
7## Planned Scripts
8
9### `admins.sql`
10List users and roles holding `ACCOUNTADMIN`, `SECURITYADMIN`, or `SYSADMIN` via
11`SNOWFLAKE.ACCOUNT_USAGE.GRANTS_TO_ROLES` and `GRANTS_TO_USERS`.
12
13### `passwords.sql`
14Read account-level password policy parameters from `SNOWFLAKE.ACCOUNT_USAGE.ACCOUNT_PARAMETERS`
15(min length, max age, lockout attempts, MFA enforcement).
16
17### `users.sql`
18List all users from `SNOWFLAKE.ACCOUNT_USAGE.USERS` with `last_success_login`, `disabled`,
19`must_change_password`, and `has_password` flags.
20
21### `network_policies.sql`
22List all network policies and their assignments. Flag users with no network policy attached.
23
24### `stale_users.sql`
25Filter `SNOWFLAKE.ACCOUNT_USAGE.USERS` for accounts inactive for 90+ days or that have
26never logged in.
27
28### `service_accounts.sql`
29Identify likely service accounts: no email set and `has_rsa_public_key = TRUE`.
30Join against role grants to show what access each holds.
os/windows/README.md added +36
@@ -0,0 +1,36 @@
1# os/windows
2
3> Planned PowerShell scripts for Windows security audits. Mirrors the structure of `os/linux/`.
4>
5> Open architectural decision: scripts can target local accounts only (`Get-LocalUser`),
6> Active Directory (`Get-ADUser`), or both. This affects cmdlet choices across most scripts
7> below and should be settled before implementation.
8
9## Planned Scripts
10
11### `local_admins.ps1`
12List members of the local Administrators group via `Get-LocalGroupMember`.
13
14### `passwords.ps1`
15Dump local password policy via `net accounts`. If domain-joined, also pull
16`Get-ADDefaultDomainPasswordPolicy` (min length, max age, lockout threshold, history).
17
18### `audit_policy.ps1`
19Read the Windows audit policy via `auditpol /get /category:*`. Flag whether logon,
20account management, and privilege use events are being logged.
21
22### `rdp_settings.ps1`
23Check if RDP is enabled, whether NLA is required, and which users/groups hold
24"Allow log on through Remote Desktop Services" rights.
25
26### `inactive_users.ps1`
27List local user accounts with last logon date. Flag accounts inactive past a
28configurable threshold (e.g., 90 days).
29
30### `ad_admins.ps1`
31If domain-joined: list members of Domain Admins, Enterprise Admins, and Schema Admins.
32AD equivalent of `../../../applications/github/github_admins.py`.
33
34### `scheduled_tasks.ps1`
35List scheduled tasks running as SYSTEM or with stored credentials.
36Windows analog of a cron audit.