Files
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

825 lines
32 KiB
Bash

#!/usr/bin/env bash
#########################################################################################
#### iam-audit.sh — Audit AWS IAM users, roles, policies, and access keys ####
#### Finds stale access keys, users without MFA, unused roles, overly broad policies ####
#### Requires: bash 4+, aws-cli v2, jq ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version 1.01 ####
#### ####
#### Usage: ####
#### export AWS_PROFILE="production" ####
#### ./iam-audit.sh --full ####
#### ####
#### See --help for all options. ####
#########################################################################################
set -euo pipefail
# ── Defaults ──────────────────────────────────────────────────────────
AWS_REGION="${AWS_REGION:-}"
KEY_AGE_WARN_DAYS="${KEY_AGE_WARN_DAYS:-90}"
KEY_AGE_CRIT_DAYS="${KEY_AGE_CRIT_DAYS:-180}"
UNUSED_DAYS="${UNUSED_DAYS:-90}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
VERBOSE="${VERBOSE:-false}"
COLOR="${COLOR:-auto}"
# ── State ─────────────────────────────────────────────────────────────
SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_NAME
RUN_MODE=""
START_TIME=""
WARNINGS=0
TOTAL_PASS=0
TOTAL_WARN=0
TOTAL_CRIT=0
# ── Colors ────────────────────────────────────────────────────────────
setup_colors() {
if [[ "$COLOR" == "never" ]]; then
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
return
fi
if [[ "$COLOR" == "always" ]] || [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED="" GREEN="" YELLOW="" BLUE="" BOLD="" RESET=""
fi
}
# ── Logging ───────────────────────────────────────────────────────────
log() { echo -e "${BLUE}[INFO]${RESET} $*"; }
warn() { echo -e "${YELLOW}[WARN]${RESET} $*" >&2; ((WARNINGS++)) || true; }
err() { echo -e "${RED}[ERROR]${RESET} $*" >&2; }
verbose() { if [[ "$VERBOSE" == "true" ]]; then echo -e "${BLUE}[DEBUG]${RESET} $*"; fi; }
pass() { ((TOTAL_PASS++)) || true; }
flag_warn() { ((TOTAL_WARN++)) || true; }
flag_crit() { ((TOTAL_CRIT++)) || true; }
# ── AWS CLI wrapper ───────────────────────────────────────────────────
aws_cmd() {
local args=("$@")
[[ -n "$AWS_REGION" ]] && args+=(--region "$AWS_REGION")
verbose "aws ${args[*]}"
aws "${args[@]}"
}
# ── Date math (portable) ─────────────────────────────────────────────
now_epoch() {
date +%s
}
iso_to_epoch() {
local iso_date="$1"
if date -d "$iso_date" +%s &>/dev/null; then
date -d "$iso_date" +%s
elif date -jf "%Y-%m-%dT%H:%M:%S" "${iso_date%%+*}" +%s &>/dev/null; then
date -jf "%Y-%m-%dT%H:%M:%S" "${iso_date%%+*}" +%s
else
# Fallback: parse YYYY-MM-DD with awk
echo "$iso_date" | awk -F'[-T:+]' '{
y=$1; m=$2; d=$3
t = mktime(y " " m " " d " 0 0 0")
print t
}'
fi
}
days_since() {
local iso_date="$1"
local then_epoch now_epoch_val
then_epoch=$(iso_to_epoch "$iso_date")
now_epoch_val=$(now_epoch)
echo $(( (now_epoch_val - then_epoch) / 86400 ))
}
# ── Credential check ─────────────────────────────────────────────────
check_deps() {
for cmd in aws jq; do
if ! command -v "$cmd" &>/dev/null; then
err "${cmd} is required but not installed"
exit 1
fi
done
local identity
identity=$(aws sts get-caller-identity 2>&1) || {
err "AWS credentials not configured, expired, or invalid"
echo "" >&2
echo "Supported credential methods:" >&2
echo " • AWS_PROFILE — named profile from ~/.aws/credentials" >&2
echo " • AWS SSO — run 'aws sso login --profile your-profile'" >&2
echo " • Environment vars — AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_SESSION_TOKEN" >&2
echo " • Instance profile — automatic on EC2/ECS" >&2
echo " • AWS_ROLE_ARN — assume role via STS" >&2
exit 1
}
local arn
arn=$(echo "$identity" | jq -r '.Arn')
verbose "Identity: ${arn}"
if [[ -n "${AWS_SESSION_TOKEN:-}" ]]; then
verbose "Using temporary credentials (session token present)"
fi
if [[ -z "$AWS_REGION" ]]; then
AWS_REGION=$(aws configure get region 2>/dev/null || echo "")
if [[ -n "$AWS_REGION" ]]; then
verbose "Using region from config: ${AWS_REGION}"
fi
fi
log "Authenticated as ${arn}"
}
# ══════════════════════════════════════════════════════════════════════
# ACCESS KEY AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_access_keys() {
log "Auditing access keys..."
echo ""
printf " %-24s %-22s %-10s %-10s %-20s %s\n" \
"USER" "KEY_ID" "STATUS" "AGE_DAYS" "LAST_USED" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..100})"
local users_json
users_json=$(aws_cmd iam list-users --output json)
local user_count
user_count=$(echo "$users_json" | jq '.Users | length')
if [[ "$user_count" -eq 0 ]]; then
log "No IAM users found"
return
fi
local user_name
echo "$users_json" | jq -r '.Users[].UserName' | while IFS= read -r user_name; do
local keys_json
keys_json=$(aws_cmd iam list-access-keys --user-name "$user_name" --output json 2>/dev/null) || continue
local key_count
key_count=$(echo "$keys_json" | jq '.AccessKeyMetadata | length')
if [[ "$key_count" -eq 0 ]]; then
verbose "User ${user_name}: no access keys"
continue
fi
echo "$keys_json" | jq -c '.AccessKeyMetadata[]' | while IFS= read -r key; do
local key_id status create_date age_days last_used_date severity last_used_display
key_id=$(echo "$key" | jq -r '.AccessKeyId')
status=$(echo "$key" | jq -r '.Status')
create_date=$(echo "$key" | jq -r '.CreateDate')
age_days=$(days_since "$create_date")
# Get last used info
local last_used_json
last_used_json=$(aws_cmd iam get-access-key-last-used --access-key-id "$key_id" --output json 2>/dev/null) || true
last_used_date=$(echo "$last_used_json" | jq -r '.AccessKeyLastUsed.LastUsedDate // "N/A"')
if [[ "$last_used_date" == "N/A" ]]; then
last_used_display="Never"
else
last_used_display="${last_used_date:0:10}"
fi
# Determine severity
if [[ "$status" == "Inactive" ]]; then
severity="INFO"
elif [[ "$age_days" -ge "$KEY_AGE_CRIT_DAYS" ]]; then
severity="CRITICAL"
flag_crit
elif [[ "$age_days" -ge "$KEY_AGE_WARN_DAYS" ]]; then
severity="WARN"
flag_warn
elif [[ "$last_used_display" == "Never" && "$age_days" -ge 30 ]]; then
severity="WARN"
flag_warn
else
severity="OK"
pass
fi
local color=""
case "$severity" in
CRITICAL) color="$RED" ;;
WARN) color="$YELLOW" ;;
OK) color="$GREEN" ;;
*) color="" ;;
esac
printf " %-24s %-22s %-10s %-10s %-20s %b%s%b\n" \
"$user_name" "$key_id" "$status" "$age_days" "$last_used_display" \
"$color" "$severity" "$RESET"
done
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# MFA AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_mfa() {
log "Auditing MFA status..."
echo ""
printf " %-24s %-18s %-14s %s\n" \
"USER" "CONSOLE_ACCESS" "MFA_ENABLED" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..75})"
local users_json
users_json=$(aws_cmd iam list-users --output json)
echo "$users_json" | jq -r '.Users[].UserName' | while IFS= read -r user_name; do
local has_console="No"
local has_mfa="No"
local severity="OK"
# Check for login profile (console access)
if aws_cmd iam get-login-profile --user-name "$user_name" &>/dev/null; then
has_console="Yes"
fi
# Check MFA devices
local mfa_json
mfa_json=$(aws_cmd iam list-mfa-devices --user-name "$user_name" --output json 2>/dev/null) || true
local mfa_count
mfa_count=$(echo "$mfa_json" | jq '.MFADevices | length')
if [[ "$mfa_count" -gt 0 ]]; then
has_mfa="Yes"
fi
# Severity: console access without MFA is critical
if [[ "$has_console" == "Yes" && "$has_mfa" == "No" ]]; then
severity="CRITICAL"
flag_crit
elif [[ "$has_console" == "Yes" && "$has_mfa" == "Yes" ]]; then
severity="OK"
pass
else
severity="OK"
pass
fi
local color=""
case "$severity" in
CRITICAL) color="$RED" ;;
WARN) color="$YELLOW" ;;
OK) color="$GREEN" ;;
*) color="" ;;
esac
printf " %-24s %-18s %-14s %b%s%b\n" \
"$user_name" "$has_console" "$has_mfa" \
"$color" "$severity" "$RESET"
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# UNUSED USERS AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_unused_users() {
log "Auditing user activity (inactive > ${UNUSED_DAYS} days)..."
echo ""
printf " %-24s %-16s %-16s %s\n" \
"USER" "LAST_CONSOLE" "LAST_API" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..75})"
local users_json
users_json=$(aws_cmd iam list-users --output json)
echo "$users_json" | jq -c '.Users[]' | while IFS= read -r user; do
local user_name password_last_used last_console last_api severity
user_name=$(echo "$user" | jq -r '.UserName')
password_last_used=$(echo "$user" | jq -r '.PasswordLastUsed // "N/A"')
if [[ "$password_last_used" == "N/A" ]]; then
last_console="Never"
else
last_console="${password_last_used:0:10}"
fi
# Check access key last used
last_api="Never"
local keys_json
keys_json=$(aws_cmd iam list-access-keys --user-name "$user_name" --output json 2>/dev/null) || true
echo "$keys_json" | jq -r '.AccessKeyMetadata[].AccessKeyId' 2>/dev/null | while IFS= read -r key_id; do
local lu
lu=$(aws_cmd iam get-access-key-last-used --access-key-id "$key_id" --output json 2>/dev/null | \
jq -r '.AccessKeyLastUsed.LastUsedDate // "N/A"') || true
if [[ "$lu" != "N/A" ]]; then
echo "${lu:0:10}"
fi
done | sort -r | head -1 | read -r latest_api || true
if [[ -n "${latest_api:-}" ]]; then
last_api="$latest_api"
fi
# Determine severity
severity="OK"
local console_inactive=false api_inactive=false
if [[ "$last_console" == "Never" ]]; then
console_inactive=true
else
local console_days
console_days=$(days_since "$password_last_used")
if [[ "$console_days" -ge "$UNUSED_DAYS" ]]; then
console_inactive=true
fi
fi
if [[ "$last_api" == "Never" ]]; then
api_inactive=true
else
local api_days
api_days=$(days_since "${last_api}T00:00:00Z")
if [[ "$api_days" -ge "$UNUSED_DAYS" ]]; then
api_inactive=true
fi
fi
if [[ "$console_inactive" == "true" && "$api_inactive" == "true" ]]; then
severity="WARN"
flag_warn
else
pass
fi
local color=""
case "$severity" in
WARN) color="$YELLOW" ;;
OK) color="$GREEN" ;;
*) color="" ;;
esac
printf " %-24s %-16s %-16s %b%s%b\n" \
"$user_name" "$last_console" "$last_api" \
"$color" "$severity" "$RESET"
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# UNUSED ROLES AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_unused_roles() {
log "Auditing role usage (inactive > ${UNUSED_DAYS} days)..."
echo ""
printf " %-40s %-16s %-10s %s\n" \
"ROLE" "LAST_USED" "AGE_DAYS" "SEVERITY"
printf " %s\n" "$(printf '%.0s─' {1..80})"
local roles_json
roles_json=$(aws_cmd iam list-roles --output json)
echo "$roles_json" | jq -c '.Roles[]' | while IFS= read -r role; do
local role_name role_path last_used create_date age_days severity
role_name=$(echo "$role" | jq -r '.RoleName')
role_path=$(echo "$role" | jq -r '.Path')
create_date=$(echo "$role" | jq -r '.CreateDate')
age_days=$(days_since "$create_date")
# Skip service-linked roles
if [[ "$role_path" == /aws-service-role/* ]]; then
verbose "Skipping service-linked role: ${role_name}"
continue
fi
# Skip AWS-managed roles
if [[ "$role_name" == AWS* || "$role_name" == aws* ]]; then
verbose "Skipping AWS-managed role: ${role_name}"
continue
fi
# Check last used
local last_used_date
last_used_date=$(echo "$role" | jq -r '.RoleLastUsed.LastUsedDate // "N/A"')
if [[ "$last_used_date" == "N/A" ]]; then
last_used="Never"
if [[ "$age_days" -ge "$UNUSED_DAYS" ]]; then
severity="WARN"
flag_warn
else
severity="OK"
pass
fi
else
last_used="${last_used_date:0:10}"
local used_days
used_days=$(days_since "$last_used_date")
if [[ "$used_days" -ge "$UNUSED_DAYS" ]]; then
severity="WARN"
flag_warn
else
severity="OK"
pass
fi
fi
local color=""
case "$severity" in
WARN) color="$YELLOW" ;;
OK) color="$GREEN" ;;
*) color="" ;;
esac
printf " %-40s %-16s %-10s %b%s%b\n" \
"$role_name" "$last_used" "$age_days" \
"$color" "$severity" "$RESET"
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# POLICY AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_policies() {
log "Auditing IAM policies..."
echo ""
printf " %-28s %-10s %-30s %s\n" \
"ENTITY" "TYPE" "POLICY" "ISSUE"
printf " %s\n" "$(printf '%.0s─' {1..90})"
local users_json
users_json=$(aws_cmd iam list-users --output json)
# Check users with direct policy attachments
echo "$users_json" | jq -r '.Users[].UserName' | while IFS= read -r user_name; do
# Attached managed policies directly on user
local attached
attached=$(aws_cmd iam list-attached-user-policies --user-name "$user_name" --output json 2>/dev/null) || continue
echo "$attached" | jq -c '.AttachedPolicies[]' 2>/dev/null | while IFS= read -r pol; do
local policy_name
policy_name=$(echo "$pol" | jq -r '.PolicyName')
# Flag direct attachment
printf " %-28s %-10s %-30s %b%s%b\n" \
"$user_name" "User" "$policy_name" \
"$YELLOW" "Direct attachment (use groups)" "$RESET"
flag_warn
# Flag admin access
if [[ "$policy_name" == "AdministratorAccess" ]]; then
printf " %-28s %-10s %-30s %b%s%b\n" \
"$user_name" "User" "$policy_name" \
"$RED" "Full admin access" "$RESET"
flag_crit
fi
done
# Inline policies on user
local inline
inline=$(aws_cmd iam list-user-policies --user-name "$user_name" --output json 2>/dev/null) || continue
echo "$inline" | jq -r '.PolicyNames[]' 2>/dev/null | while IFS= read -r pol_name; do
local pol_doc
pol_doc=$(aws_cmd iam get-user-policy --user-name "$user_name" --policy-name "$pol_name" --output json 2>/dev/null) || continue
# Check for wildcard
local has_star_action has_star_resource
has_star_action=$(echo "$pol_doc" | jq '[.PolicyDocument.Statement[] | select(.Effect == "Allow") | .Action] | flatten | any(. == "*")' 2>/dev/null) || has_star_action="false"
has_star_resource=$(echo "$pol_doc" | jq '[.PolicyDocument.Statement[] | select(.Effect == "Allow") | .Resource] | flatten | any(. == "*")' 2>/dev/null) || has_star_resource="false"
if [[ "$has_star_action" == "true" && "$has_star_resource" == "true" ]]; then
printf " %-28s %-10s %-30s %b%s%b\n" \
"$user_name" "Inline" "$pol_name" \
"$RED" "Action:* Resource:*" "$RESET"
flag_crit
fi
done
done
# Check roles with AdministratorAccess
local roles_json
roles_json=$(aws_cmd iam list-roles --output json)
echo "$roles_json" | jq -c '.Roles[]' | while IFS= read -r role; do
local role_name role_path
role_name=$(echo "$role" | jq -r '.RoleName')
role_path=$(echo "$role" | jq -r '.Path')
[[ "$role_path" == /aws-service-role/* ]] && continue
local attached
attached=$(aws_cmd iam list-attached-role-policies --role-name "$role_name" --output json 2>/dev/null) || continue
echo "$attached" | jq -c '.AttachedPolicies[]' 2>/dev/null | while IFS= read -r pol; do
local policy_name
policy_name=$(echo "$pol" | jq -r '.PolicyName')
if [[ "$policy_name" == "AdministratorAccess" ]]; then
printf " %-28s %-10s %-30s %b%s%b\n" \
"$role_name" "Role" "$policy_name" \
"$RED" "Full admin access" "$RESET"
flag_crit
fi
done
done
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# ROOT ACCOUNT AUDIT
# ══════════════════════════════════════════════════════════════════════
audit_root_account() {
log "Auditing root account and account settings..."
echo ""
# Account summary
local summary
summary=$(aws_cmd iam get-account-summary --output json 2>/dev/null) || {
warn "Unable to retrieve account summary (may require root or admin permissions)"
return
}
local root_keys root_mfa
root_keys=$(echo "$summary" | jq '.SummaryMap.AccountAccessKeysPresent')
root_mfa=$(echo "$summary" | jq '.SummaryMap.AccountMFAEnabled')
echo " Root Account Status:"
echo " $(printf '%.0s─' {1..50})"
if [[ "$root_keys" -gt 0 ]]; then
printf " %-30s %b%s%b\n" "Root access keys:" "$RED" "PRESENT (remove them!)" "$RESET"
flag_crit
else
printf " %-30s %b%s%b\n" "Root access keys:" "$GREEN" "None" "$RESET"
pass
fi
if [[ "$root_mfa" -eq 1 ]]; then
printf " %-30s %b%s%b\n" "Root MFA:" "$GREEN" "Enabled" "$RESET"
pass
else
printf " %-30s %b%s%b\n" "Root MFA:" "$RED" "DISABLED" "$RESET"
flag_crit
fi
# Password policy
echo ""
log "Checking password policy..."
local policy
if policy=$(aws_cmd iam get-account-password-policy --output json 2>/dev/null); then
local min_len require_upper require_lower require_numbers require_symbols max_age
min_len=$(echo "$policy" | jq '.PasswordPolicy.MinimumPasswordLength')
require_upper=$(echo "$policy" | jq '.PasswordPolicy.RequireUppercaseCharacters')
require_lower=$(echo "$policy" | jq '.PasswordPolicy.RequireLowercaseCharacters')
require_numbers=$(echo "$policy" | jq '.PasswordPolicy.RequireNumbers')
require_symbols=$(echo "$policy" | jq '.PasswordPolicy.RequireSymbols')
max_age=$(echo "$policy" | jq '.PasswordPolicy.MaxPasswordAge // 0')
printf " %-30s %s\n" "Min password length:" "$min_len"
printf " %-30s %s\n" "Require uppercase:" "$require_upper"
printf " %-30s %s\n" "Require lowercase:" "$require_lower"
printf " %-30s %s\n" "Require numbers:" "$require_numbers"
printf " %-30s %s\n" "Require symbols:" "$require_symbols"
if [[ "$max_age" -gt 0 ]]; then
printf " %-30s %s days\n" "Max password age:" "$max_age"
else
printf " %-30s %s\n" "Max password age:" "No expiry"
fi
if [[ "$min_len" -lt 14 ]]; then
flag_warn
printf "\n %b%s%b\n" "$YELLOW" "Recommendation: Set minimum password length to 14+" "$RESET"
else
pass
fi
else
printf " %-30s %b%s%b\n" "Password policy:" "$YELLOW" "Not configured (using AWS defaults)" "$RESET"
flag_warn
fi
echo ""
}
# ══════════════════════════════════════════════════════════════════════
# SUMMARY
# ══════════════════════════════════════════════════════════════════════
print_summary() {
local elapsed
elapsed=$(( $(now_epoch) - START_TIME ))
echo ""
echo " ══════════════════════════════════════════"
echo " IAM Audit Summary"
echo " ══════════════════════════════════════════"
printf " %-20s %b%d%b\n" "PASS:" "$GREEN" "$TOTAL_PASS" "$RESET"
printf " %-20s %b%d%b\n" "WARN:" "$YELLOW" "$TOTAL_WARN" "$RESET"
printf " %-20s %b%d%b\n" "CRITICAL:" "$RED" "$TOTAL_CRIT" "$RESET"
echo " ──────────────────────────────────────────"
printf " Completed in %ds\n" "$elapsed"
echo ""
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
echo -e " ${RED}${BOLD}Action required:${RESET} ${TOTAL_CRIT} critical finding(s)"
echo ""
echo " Top recommendations:"
echo " • Remove root access keys immediately"
echo " • Enable MFA for all console users"
echo " • Rotate access keys older than ${KEY_AGE_CRIT_DAYS} days"
echo " • Replace Action:*/Resource:* policies with least-privilege"
echo ""
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
echo -e " ${YELLOW}Review recommended:${RESET} ${TOTAL_WARN} warning(s)"
echo ""
echo " Suggestions:"
echo " • Rotate access keys older than ${KEY_AGE_WARN_DAYS} days"
echo " • Remove or disable inactive users"
echo " • Use groups instead of direct policy attachments"
echo ""
else
echo -e " ${GREEN}All checks passed${RESET}"
echo ""
fi
}
# ══════════════════════════════════════════════════════════════════════
# USAGE
# ══════════════════════════════════════════════════════════════════════
usage() {
cat <<EOF
${SCRIPT_NAME} — Audit AWS IAM users, roles, policies, and access keys
USAGE:
${SCRIPT_NAME} [OPTIONS]
MODES:
--full Run all audits
--keys Audit access keys (age, usage, status)
--mfa Audit MFA status for all users
--users Find inactive users
--roles Find unused roles
--policies Audit policy attachments and wildcards
--root Audit root account and password policy
OPTIONS:
--key-age-warn DAYS Warn threshold for key age (default: ${KEY_AGE_WARN_DAYS})
--key-age-crit DAYS Critical threshold for key age (default: ${KEY_AGE_CRIT_DAYS})
--unused-days DAYS Inactive threshold for users/roles (default: ${UNUSED_DAYS})
--format FORMAT Output format: text, csv, json (default: text)
--verbose Enable debug output
--no-color Disable colored output
--help Show this help
ENVIRONMENT VARIABLES:
AWS_PROFILE AWS CLI profile name
AWS_REGION AWS region
AWS_ACCESS_KEY_ID Access key (with AWS_SECRET_ACCESS_KEY)
AWS_SESSION_TOKEN Session token for temporary credentials
KEY_AGE_WARN_DAYS Warn threshold (default: 90)
KEY_AGE_CRIT_DAYS Critical threshold (default: 180)
UNUSED_DAYS Inactive threshold (default: 90)
OUTPUT_FORMAT Output format (default: text)
EXAMPLES:
# Full audit with default thresholds
AWS_PROFILE=prod ./iam-audit.sh --full
# Check only access keys, flag anything older than 60 days
./iam-audit.sh --keys --key-age-warn 60 --key-age-crit 120
# Check MFA status only
./iam-audit.sh --mfa
# Find users and roles inactive for 60+ days
./iam-audit.sh --users --roles --unused-days 60
EOF
}
# ══════════════════════════════════════════════════════════════════════
# ARGUMENT PARSING
# ══════════════════════════════════════════════════════════════════════
parse_args() {
local modes=()
while [[ $# -gt 0 ]]; do
case "$1" in
--full)
modes=(keys mfa users roles policies root)
shift ;;
--keys)
modes+=(keys); shift ;;
--mfa)
modes+=(mfa); shift ;;
--users)
modes+=(users); shift ;;
--roles)
modes+=(roles); shift ;;
--policies)
modes+=(policies); shift ;;
--root)
modes+=(root); shift ;;
--key-age-warn)
KEY_AGE_WARN_DAYS="$2"; shift 2 ;;
--key-age-crit)
KEY_AGE_CRIT_DAYS="$2"; shift 2 ;;
--unused-days)
UNUSED_DAYS="$2"; shift 2 ;;
--format)
OUTPUT_FORMAT="$2"; shift 2 ;;
--verbose)
VERBOSE="true"; shift ;;
--no-color)
COLOR="never"; shift ;;
--help|-h)
setup_colors
usage
exit 0 ;;
*)
err "Unknown option: $1"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1 ;;
esac
done
if [[ ${#modes[@]} -eq 0 ]]; then
err "No audit mode specified"
echo "Run ${SCRIPT_NAME} --help for usage" >&2
exit 1
fi
RUN_MODE="${modes[*]}"
}
# ══════════════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════════════
main() {
parse_args "$@"
setup_colors
check_deps
START_TIME=$(now_epoch)
echo ""
echo -e "${BOLD}IAM Audit${RESET}"
echo -e "Mode: ${RUN_MODE}"
echo -e "Time: $(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo -e "Thresholds: key-warn=${KEY_AGE_WARN_DAYS}d key-crit=${KEY_AGE_CRIT_DAYS}d unused=${UNUSED_DAYS}d"
echo ""
for mode in $RUN_MODE; do
case "$mode" in
keys) audit_access_keys ;;
mfa) audit_mfa ;;
users) audit_unused_users ;;
roles) audit_unused_roles ;;
policies) audit_policies ;;
root) audit_root_account ;;
esac
done
print_summary
if [[ "$TOTAL_CRIT" -gt 0 ]]; then
exit 2
elif [[ "$TOTAL_WARN" -gt 0 ]]; then
exit 1
fi
exit 0
}
main "$@"