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    ]