audit-labs/audit-tools

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

Commit f12763587f

f12763587f1c5c680a7a1c80054470cab9ad8360

parent: e6f5e670bf

Unsigned

cmc <hello@cleberg.net> · 2026-07-29 17:10 UTC

feat: add AWS account, network, and logging security checks

Deepen the AWS audit with four new collectors:

- account_security: IAM account summary (root MFA, root access keys, resource
  counts).
- security_groups: security-group ingress rules open to 0.0.0.0/0 or ::/0,
  scanned across all enabled regions (one row per open rule).
- cloudtrail: trail logging status, multi-region, log-file validation.
- config_recorders: AWS Config recording status per region, flagging gaps.

Add an enabled_regions() helper for the region-scoped checks (each region is
skipped independently on error). Wire the checks into aws_runner and audit.py,
add mocked-boto3 unit tests, and update the README scope/permission notes.

Layout: unified · split

README.org +1 −1
@@ -17,7 +17,7 @@ connection details, choose which checks to run, and watch live progress.
1717
1818| Directory | Description |
1919|----------------------+------------------------------------------------------------------------------|
20| =applications/aws/= | AWS IAM users, password policy, and S3 bucket analysis |
20| =applications/aws/= | AWS IAM users, account/root security, password policy, S3 public access, open security groups, CloudTrail, Config, SSO |
2121| =applications/github/= | GitHub admin enumeration, org security settings, webhooks, deploy keys, secret-scanning/Dependabot alerts, audit log, branch protections, commits |
2222| =applications/gitlab/= | GitLab group/project members, branch protections, approvals, pipelines, audit events |
2323| =databases/mongo/= | MongoDB admin enumeration |
applications/aws/README.md +11 −2
@@ -1,9 +1,11 @@
11> **NOTE**: Authentication uses the standard AWS credential chain (environment
22> variables, shared config/credentials, SSO profiles, instance roles). This tool
33> never handles access keys directly. Read-only permissions are enough — IAM
4> `Get*`/`List*`, S3 `s3:GetBucket*` + `s3:ListAllMyBuckets`, and for the SSO
4> `Get*`/`List*`, S3 `s3:GetBucket*` + `s3:ListAllMyBuckets`, EC2
5> `ec2:DescribeRegions`/`DescribeSecurityGroups`, `cloudtrail:DescribeTrails` +
6> `GetTrailStatus`, `config:DescribeConfigurationRecorders*`, and for the SSO
57> check `sso:List*`/`sso:Describe*`, `identitystore:Describe*`, and
6> `organizations:ListAccounts`.
8> `organizations:ListAccounts`. The SecurityAudit managed policy covers these.
79
810---
911
@@ -42,10 +44,17 @@ Creates a directory: `<out>/aws_audit_<profile>_<YYYY-MM-DD>/`
4244|---|---|
4345| `iam_users.csv` | IAM users with MFA status, access-key count/age, console password, last use |
4446| `password_policy.csv` | Account IAM password policy (length, complexity, rotation, reuse) |
47| `account_security.csv` | Account summary — root MFA, root access keys, and resource counts |
4548| `s3_public_access.csv` | Per-bucket Public Access Block, policy public status, and ACL public exposure |
49| `open_security_groups.csv` | Security-group ingress rules open to `0.0.0.0/0` or `::/0`, across all regions |
50| `cloudtrail.csv` | CloudTrail trails — logging status, multi-region, log-file validation |
51| `config_recorders.csv` | AWS Config recording status per region (gaps are flagged) |
4652| `sso_assignments.csv` | IAM Identity Center permission-set assignments per account (Identity Center + Organizations) |
4753| `summary.txt` | Row counts per section |
4854
55`open_security_groups.csv` and `config_recorders.csv` scan every enabled region,
56so they take longer on accounts with many regions.
57
4958Checks that aren't available (no password policy, no Identity Center instance,
5059missing permissions) are skipped with a warning; the rest still run.
5160
applications/aws/audit.py +9 −1
@@ -31,7 +31,7 @@ import sys
3131from datetime import date
3232
3333import config
34from collectors import iam, s3, sso
34from collectors import iam, monitoring, s3, security_groups, sso
3535from reporters import csv_reporter
3636
3737
@@ -86,7 +86,15 @@ def run():
8686
8787 collect("IAM users", iam.iam_users, "iam_users.csv")
8888 collect("Password policy", iam.password_policy, "password_policy.csv")
89 collect("Account security", iam.account_security, "account_security.csv")
8990 collect("S3 public access", s3.s3_public_access, "s3_public_access.csv")
91 collect(
92 "Open security groups",
93 security_groups.security_groups,
94 "open_security_groups.csv",
95 )
96 collect("CloudTrail", monitoring.cloudtrail, "cloudtrail.csv")
97 collect("AWS Config recorders", monitoring.config_recorders, "config_recorders.csv")
9098 collect("SSO assignments", sso.sso_assignments, "sso_assignments.csv")
9199
92100 print()
applications/aws/collectors/api.py +6
@@ -35,3 +35,9 @@ def account_id(cfg):
3535 return cfg["session"].client("sts").get_caller_identity()["Account"]
3636 except Exception:
3737 return ""
38
39
40def enabled_regions(cfg):
41 """Return the region names enabled for the account (for region-scoped checks)."""
42 ec2 = cfg["session"].client("ec2", region_name=cfg.get("region") or "us-east-1")
43 return [r["RegionName"] for r in ec2.describe_regions().get("Regions", [])]
applications/aws/collectors/iam.py +25 −1
@@ -1,5 +1,6 @@
11"""
2Collect IAM user hygiene and the account password policy.
2Collect IAM user hygiene, the account password policy, and account-level
3security summary (root MFA, root access keys).
34"""
45
56import sys
@@ -8,6 +9,29 @@ from datetime import datetime, timezone
89from botocore.exceptions import ClientError
910
1011
12def account_security(cfg):
13 """
14 One row of account-level security signals from the IAM account summary:
15 whether the root user has MFA and access keys, plus resource counts.
16 """
17 iam = cfg["session"].client("iam")
18 s = iam.get_account_summary()["SummaryMap"]
19 return [
20 {
21 "root_mfa_enabled": bool(s.get("AccountMFAEnabled", 0)),
22 "root_access_keys_present": bool(s.get("AccountAccessKeysPresent", 0)),
23 "root_signing_certs_present": bool(
24 s.get("AccountSigningCertificatesPresent", 0)
25 ),
26 "mfa_devices": s.get("MFADevices", 0),
27 "users": s.get("Users", 0),
28 "groups": s.get("Groups", 0),
29 "roles": s.get("Roles", 0),
30 "policies": s.get("Policies", 0),
31 }
32 ]
33
34
1135def iam_users(cfg):
1236 """
1337 One row per IAM user: MFA status, access-key count and oldest key age,
applications/aws/collectors/monitoring.py added +81
@@ -0,0 +1,81 @@
1"""
2Collect audit-logging posture: CloudTrail trails and AWS Config recorders.
3"""
4
5import sys
6
7from botocore.exceptions import ClientError
8
9from .api import enabled_regions
10
11
12def cloudtrail(cfg):
13 """
14 One row per CloudTrail trail: whether it is logging, multi-region, and has
15 log-file validation. An empty result means no trails are configured.
16 """
17 region = cfg.get("region") or "us-east-1"
18 ct = cfg["session"].client("cloudtrail", region_name=region)
19 rows = []
20 for trail in ct.describe_trails(includeShadowTrails=False).get("trailList", []):
21 try:
22 status = ct.get_trail_status(Name=trail["TrailARN"])
23 except ClientError:
24 status = {}
25 rows.append(
26 {
27 "name": trail.get("Name", ""),
28 "home_region": trail.get("HomeRegion", ""),
29 "multi_region": trail.get("IsMultiRegionTrail"),
30 "log_file_validation": trail.get("LogFileValidationEnabled"),
31 "is_logging": status.get("IsLogging"),
32 "s3_bucket": trail.get("S3BucketName", ""),
33 }
34 )
35 return rows
36
37
38def config_recorders(cfg):
39 """
40 One row per region: whether AWS Config is recording. Regions with no
41 recorder are reported so gaps are visible.
42 """
43 session = cfg["session"]
44 rows = []
45 for region in enabled_regions(cfg):
46 try:
47 cc = session.client("config", region_name=region)
48 recorders = cc.describe_configuration_recorders().get(
49 "ConfigurationRecorders", []
50 )
51 statuses = {
52 s["name"]: s
53 for s in cc.describe_configuration_recorder_status().get(
54 "ConfigurationRecordersStatus", []
55 )
56 }
57 except ClientError as e:
58 print(f" Skipping {region}: config returned {e}", file=sys.stderr)
59 continue
60
61 if not recorders:
62 rows.append(
63 {
64 "region": region,
65 "recorder": "(none)",
66 "recording": False,
67 "last_status": "",
68 }
69 )
70 continue
71 for r in recorders:
72 st = statuses.get(r["name"], {})
73 rows.append(
74 {
75 "region": region,
76 "recorder": r["name"],
77 "recording": st.get("recording"),
78 "last_status": st.get("lastStatus", ""),
79 }
80 )
81 return rows
applications/aws/collectors/security_groups.py added +63
@@ -0,0 +1,63 @@
1"""
2Collect security-group ingress rules open to the internet, across all regions.
3
4Only rules allowing 0.0.0.0/0 or ::/0 are reported — one row per open rule.
5"""
6
7import sys
8
9from botocore.exceptions import ClientError
10
11from .api import enabled_regions
12
13OPEN_V4 = "0.0.0.0/0"
14OPEN_V6 = "::/0"
15
16
17def security_groups(cfg):
18 session = cfg["session"]
19 rows = []
20 for region in enabled_regions(cfg):
21 try:
22 ec2 = session.client("ec2", region_name=region)
23 groups = _all_groups(ec2)
24 except ClientError as e:
25 print(f" Skipping {region}: ec2 returned {e}", file=sys.stderr)
26 continue
27 for sg in groups:
28 rows.extend(_open_rules(region, sg))
29 return rows
30
31
32def _all_groups(ec2):
33 groups = []
34 for page in ec2.get_paginator("describe_security_groups").paginate():
35 groups.extend(page.get("SecurityGroups", []))
36 return groups
37
38
39def _open_rules(region, sg):
40 rows = []
41 for perm in sg.get("IpPermissions", []):
42 open_to = [
43 r["CidrIp"] for r in perm.get("IpRanges", []) if r.get("CidrIp") == OPEN_V4
44 ]
45 open_to += [
46 r["CidrIpv6"]
47 for r in perm.get("Ipv6Ranges", [])
48 if r.get("CidrIpv6") == OPEN_V6
49 ]
50 if not open_to:
51 continue
52 rows.append(
53 {
54 "region": region,
55 "group_id": sg.get("GroupId", ""),
56 "group_name": sg.get("GroupName", ""),
57 "protocol": perm.get("IpProtocol", ""),
58 "from_port": perm.get("FromPort", "all"),
59 "to_port": perm.get("ToPort", "all"),
60 "open_to": ", ".join(open_to),
61 }
62 )
63 return rows
tui/aws_runner.py +29 −1
@@ -21,7 +21,14 @@ _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
2121if _REPO_ROOT not in sys.path:
2222 sys.path.insert(0, _REPO_ROOT)
2323
24from applications.aws.collectors import api, iam, s3, sso
24from applications.aws.collectors import (
25 api,
26 iam,
27 monitoring,
28 s3,
29 security_groups,
30 sso,
31)
2532from applications.aws.reporters import csv_reporter
2633
2734# --- Check registry ---------------------------------------------------------
@@ -34,12 +41,33 @@ CHECKS: list[Check] = [
3441 iam.password_policy,
3542 "password_policy.csv",
3643 ),
44 Check(
45 "account_security",
46 "Account security (root MFA)",
47 iam.account_security,
48 "account_security.csv",
49 ),
3750 Check(
3851 "s3_public_access",
3952 "S3 public access",
4053 s3.s3_public_access,
4154 "s3_public_access.csv",
4255 ),
56 Check(
57 "security_groups",
58 "Open security groups",
59 security_groups.security_groups,
60 "open_security_groups.csv",
61 note="scans all regions",
62 ),
63 Check("cloudtrail", "CloudTrail", monitoring.cloudtrail, "cloudtrail.csv"),
64 Check(
65 "config_recorders",
66 "AWS Config recorders",
67 monitoring.config_recorders,
68 "config_recorders.csv",
69 note="scans all regions",
70 ),
4371 Check(
4472 "sso_assignments",
4573 "SSO assignments",
tui/tests/test_aws_collectors.py added +166
@@ -0,0 +1,166 @@
1"""Unit tests for the new AWS security collectors, mocking boto3 clients."""
2
3from unittest.mock import MagicMock
4
5from applications.aws.collectors import iam as aws_iam
6from applications.aws.collectors import monitoring
7from applications.aws.collectors import security_groups as sg
8
9
10class FakeSession:
11 """Dispatch .client(service, region_name=...) to preconfigured mocks."""
12
13 def __init__(self, clients):
14 self._clients = clients
15
16 def client(self, service, region_name=None):
17 return self._clients[service]
18
19
20def _cfg(clients):
21 return {"session": FakeSession(clients), "region": "us-east-1"}
22
23
24# --- account_security -------------------------------------------------------
25
26
27def test_account_security():
28 iam = MagicMock()
29 iam.get_account_summary.return_value = {
30 "SummaryMap": {
31 "AccountMFAEnabled": 1,
32 "AccountAccessKeysPresent": 0,
33 "Users": 5,
34 "Roles": 12,
35 }
36 }
37 rows = aws_iam.account_security(_cfg({"iam": iam}))
38 assert rows[0]["root_mfa_enabled"] is True
39 assert rows[0]["root_access_keys_present"] is False
40 assert rows[0]["users"] == 5
41 assert rows[0]["roles"] == 12
42
43
44# --- cloudtrail -------------------------------------------------------------
45
46
47def test_cloudtrail():
48 ct = MagicMock()
49 ct.describe_trails.return_value = {
50 "trailList": [
51 {
52 "Name": "org-trail",
53 "TrailARN": "arn:aws:cloudtrail:...:trail/org-trail",
54 "HomeRegion": "us-east-1",
55 "IsMultiRegionTrail": True,
56 "LogFileValidationEnabled": True,
57 "S3BucketName": "logs",
58 }
59 ]
60 }
61 ct.get_trail_status.return_value = {"IsLogging": True}
62 rows = monitoring.cloudtrail(_cfg({"cloudtrail": ct}))
63 assert rows[0]["is_logging"] is True
64 assert rows[0]["multi_region"] is True
65 assert rows[0]["s3_bucket"] == "logs"
66
67
68# --- config_recorders -------------------------------------------------------
69
70
71def _ec2_one_region():
72 ec2 = MagicMock()
73 ec2.describe_regions.return_value = {"Regions": [{"RegionName": "us-east-1"}]}
74 return ec2
75
76
77def test_config_recorders_recording():
78 config = MagicMock()
79 config.describe_configuration_recorders.return_value = {
80 "ConfigurationRecorders": [{"name": "default"}]
81 }
82 config.describe_configuration_recorder_status.return_value = {
83 "ConfigurationRecordersStatus": [
84 {"name": "default", "recording": True, "lastStatus": "SUCCESS"}
85 ]
86 }
87 rows = monitoring.config_recorders(
88 _cfg({"ec2": _ec2_one_region(), "config": config})
89 )
90 assert rows == [
91 {
92 "region": "us-east-1",
93 "recorder": "default",
94 "recording": True,
95 "last_status": "SUCCESS",
96 }
97 ]
98
99
100def test_config_recorders_reports_gap():
101 config = MagicMock()
102 config.describe_configuration_recorders.return_value = {
103 "ConfigurationRecorders": []
104 }
105 config.describe_configuration_recorder_status.return_value = {
106 "ConfigurationRecordersStatus": []
107 }
108 rows = monitoring.config_recorders(
109 _cfg({"ec2": _ec2_one_region(), "config": config})
110 )
111 assert rows[0]["recorder"] == "(none)"
112 assert rows[0]["recording"] is False
113
114
115# --- security_groups --------------------------------------------------------
116
117
118def _ec2_with_groups(groups):
119 ec2 = _ec2_one_region()
120 paginator = MagicMock()
121 paginator.paginate.return_value = [{"SecurityGroups": groups}]
122 ec2.get_paginator.return_value = paginator
123 return ec2
124
125
126def test_security_groups_flags_open_ingress():
127 groups = [
128 {
129 "GroupId": "sg-1",
130 "GroupName": "web",
131 "IpPermissions": [
132 {
133 "IpProtocol": "tcp",
134 "FromPort": 22,
135 "ToPort": 22,
136 "IpRanges": [{"CidrIp": "0.0.0.0/0"}],
137 "Ipv6Ranges": [],
138 }
139 ],
140 }
141 ]
142 rows = sg.security_groups(_cfg({"ec2": _ec2_with_groups(groups)}))
143 assert len(rows) == 1
144 assert rows[0]["group_id"] == "sg-1"
145 assert rows[0]["from_port"] == 22
146 assert rows[0]["open_to"] == "0.0.0.0/0"
147
148
149def test_security_groups_ignores_scoped_ingress():
150 groups = [
151 {
152 "GroupId": "sg-2",
153 "GroupName": "internal",
154 "IpPermissions": [
155 {
156 "IpProtocol": "tcp",
157 "FromPort": 5432,
158 "ToPort": 5432,
159 "IpRanges": [{"CidrIp": "10.0.0.0/8"}],
160 "Ipv6Ranges": [],
161 }
162 ],
163 }
164 ]
165 rows = sg.security_groups(_cfg({"ec2": _ec2_with_groups(groups)}))
166 assert rows == []