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: applications/aws/collectors/iam.py · raw
1"""
2Collect IAM user hygiene, the account password policy, and account-level
3security summary (root MFA, root access keys).
4"""
5
6import sys
7from datetime import datetime, timezone
8
9from botocore.exceptions import ClientError
10
11
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
35def iam_users(cfg):
36 """
37 One row per IAM user: MFA status, access-key count and oldest key age,
38 whether a console password is set, and last password use.
39 """
40 iam = cfg["session"].client("iam")
41 now = datetime.now(timezone.utc)
42 rows = []
43 for page in iam.get_paginator("list_users").paginate():
44 rows.extend(_user_row(iam, u, now) for u in page["Users"])
45 return rows
46
47
48def _user_row(iam, user, now):
49 name = user["UserName"]
50 mfa = iam.list_mfa_devices(UserName=name).get("MFADevices", [])
51 keys = iam.list_access_keys(UserName=name).get("AccessKeyMetadata", [])
52 key_ages = [(now - k["CreateDate"]).days for k in keys]
53 last_used = user.get("PasswordLastUsed")
54 created = user.get("CreateDate")
55 return {
56 "user": name,
57 "mfa_enabled": bool(mfa),
58 "access_keys": len(keys),
59 "oldest_key_age_days": max(key_ages) if key_ages else "",
60 "console_password": _has_console_password(iam, name),
61 "password_last_used": last_used.isoformat() if last_used else "",
62 "created": created.isoformat() if created else "",
63 }
64
65
66def _has_console_password(iam, name):
67 try:
68 iam.get_login_profile(UserName=name)
69 return True
70 except ClientError as e:
71 if e.response["Error"]["Code"] == "NoSuchEntity":
72 return False
73 raise
74
75
76def password_policy(cfg):
77 """
78 One row describing the account IAM password policy. Returns an empty list
79 with a warning if no policy is set.
80 """
81 iam = cfg["session"].client("iam")
82 try:
83 p = iam.get_account_password_policy()["PasswordPolicy"]
84 except ClientError as e:
85 if e.response["Error"]["Code"] == "NoSuchEntity":
86 print(
87 "Warning: no IAM password policy is set for this account -- skipping.",
88 file=sys.stderr,
89 )
90 return []
91 raise
92
93 return [
94 {
95 "minimum_length": p.get("MinimumPasswordLength"),
96 "require_symbols": p.get("RequireSymbols"),
97 "require_numbers": p.get("RequireNumbers"),
98 "require_uppercase": p.get("RequireUppercaseCharacters"),
99 "require_lowercase": p.get("RequireLowercaseCharacters"),
100 "allow_users_to_change": p.get("AllowUsersToChangePassword"),
101 "max_age_days": p.get("MaxPasswordAge", "N/A"),
102 "reuse_prevention": p.get("PasswordReusePrevention", "N/A"),
103 "hard_expiry": p.get("HardExpiry", False),
104 }
105 ]