audit-labs/audit-tools

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

Commit 599361577d

599361577dd71fa14392a6dcd993ca925372bcc6

parent: ded42e2f5f

Unsigned

cmc <hello@cleberg.net> · 2025-12-15 01:45 UTC

add aws password testing

Layout: unified · split

applications/aws/README.md +113
@@ -3,6 +3,7 @@
3*Note*: This example uses an account titled `cmc`, which has access provisioned to it through IAM. 3*Note*: This example uses an account titled `cmc`, which has access provisioned to it through IAM.
4 4
5``` bash 5``` bash
6chmod +x aws_iam_users.sh
6./aws_iam_users.sh 7./aws_iam_users.sh
7``` 8```
8 9
@@ -92,3 +93,115 @@ cat report_cmc.json
92 } 93 }
93] 94]
94``` 95```
96
97# `aws_password_policy`
98
99To test a password policy against AWS, I have created two steps:
100
101**Step 1: Gather AWS Policy**
102
103Run the script on your CloudShell or using the `aws` command
104
105``` bash
106chmod +x gather_policy.sh
107./gather_policy.sh
108```
109
110This will produce a JSON file as the output, with both metadata and the password policy/
111
112``` json
113{
114 "metadata": {
115 "report_timestamp_utc": "2025-12-15T01:29:52Z",
116 "os_user": "cloudshell-user",
117 "hostname": "",
118 "working_directory": "/home/cloudshell-user",
119 "aws_profile": "default",
120 "aws_region": "eu-west-1",
121 "aws_caller_identity": {
122 "UserId": "214941490075",
123 "Account": "214941490075",
124 "Arn": "arn:aws:iam::214941490075:root"
125 }
126 },
127 "PasswordPolicy": {
128 "MinimumPasswordLength": 8,
129 "RequireSymbols": true,
130 "RequireNumbers": true,
131 "RequireUppercaseCharacters": true,
132 "RequireLowercaseCharacters": true,
133 "AllowUsersToChangePassword": true,
134 "ExpirePasswords": true,
135 "MaxPasswordAge": 90,
136 "PasswordReusePrevention": 4,
137 "HardExpiry": false
138 }
139}
140```
141
142**Step 2: Test AWS**
143
144Use this file as the input to the `evaluate_policy.py` script. This Python script will ask you what you expect the values to be (e.g., what are the requirements in the company's policy?).
145
146``` bash
147uv run evaluate_policy.py policy_report.json
148```
149
150This will ask you for inputs dynamically (all are optional) and will return both a table of results in the shell, as well as a CSV file for further testing and/or documentation.
151
152*Shell Output:*
153
154``` text
155=== Expected / Minimum Values (press <Enter> for N/A) ===
156
157Enter expected value for 'Minimum password length' (int) or press <Enter> to skip: 8
158Enter expected value for 'Require symbols (!@#$…)' (bool) or press <Enter> to skip: true
159Enter expected value for 'Require numbers (0‑9)' (bool) or press <Enter> to skip: true
160Enter expected value for 'Require uppercase letters (A‑Z)' (bool) or press <Enter> to skip: true
161Enter expected value for 'Require lowercase letters (a‑z)' (bool) or press <Enter> to skip: true
162Enter expected value for 'Allow users to change password' (bool) or press <Enter> to skip: true
163Enter expected value for 'Expire passwords (enable aging)' (bool) or press <Enter> to skip: true
164Enter expected value for 'Maximum password age (days)' (int) or press <Enter> to skip: 90
165Enter expected value for 'Prevent password reuse (last N)' (int) or press <Enter> to skip: 4
166Enter expected value for 'Hard expiry (no grace period)' (bool) or press <Enter> to skip: false
167
168Audit CSV written to: policy_audit_20251215T014323Z.csv
169
170Summary:
171 1. Minimum password length → PASS
172 2. Require symbols (!@#$…) → PASS
173 3. Require numbers (0‑9) → PASS
174 4. Require uppercase letters (A‑Z) → PASS
175 5. Require lowercase letters (a‑z) → PASS
176 6. Allow users to change password → PASS
177 7. Expire passwords (enable aging) → PASS
178 8. Maximum password age (days) → PASS
179 9. Prevent password reuse (last N) → PASS
180 10. Hard expiry (no grace period) → PASS
181
182--- End of report ---
183```
184
185*CSV Output:*
186
187``` csv
188# report_timestamp_utc: 2025-12-15T01:29:52Z
189# os_user: cloudshell-user
190# hostname:
191# working_directory: /home/cloudshell-user
192# aws_profile: default
193# aws_region: eu-west-1
194"# aws_caller_identity: {'UserId': '214941490075', 'Account': '214941490075', 'Arn': 'arn:aws:iam::214941490075:root'}"
195
196Rule#,Policy‑Item,Expected,Actual,Result
1971,Minimum password length,8,8,PASS
1982,Require symbols (!@#$…),true,true,PASS
1993,Require numbers (0‑9),true,true,PASS
2004,Require uppercase letters (A‑Z),true,true,PASS
2015,Require lowercase letters (a‑z),true,true,PASS
2026,Allow users to change password,true,true,PASS
2037,Expire passwords (enable aging),true,true,PASS
2048,Maximum password age (days),90,90,PASS
2059,Prevent password reuse (last N),4,4,PASS
20610,Hard expiry (no grace period),false,false,PASS
207```\ No newline at end of file
applications/aws/aws_password_policy/evaluate_policy.py added +183
@@ -0,0 +1,183 @@
1#!/usr/bin/env python3
2"""
3evaluate_policy.py
4------------------
5Read the JSON file produced by `gather_policy.sh`, ask the auditor for the
6expected/minimum values for each of the 10 IAM password‑policy items, and emit
7a CSV audit report.
8
9Features
10* Interactive prompts – press <Enter> to mark a rule as N/A.
11* Numeric items are treated as **minimums** (actual >= expected → PASS).
12* Boolean items are treated as **exact matches** (actual == expected → PASS).
13* The CSV begins with a small metadata block (same data that the Bash script
14 captured) so the audit trail is self‑contained.
15* Usage:
16 python3 evaluate_policy.py policy_report.json
17"""
18
19import csv
20import json
21import sys
22from datetime import datetime, timezone
23from pathlib import Path
24from typing import Any, Dict, List, Optional
25
26# ----------------------------------------------------------------------
27# Mapping of the 10 password‑policy fields we care about
28# (rule_no, json_key, friendly_name, datatype)
29# ----------------------------------------------------------------------
30POLICY_FIELDS = [
31 (1, "MinimumPasswordLength", "Minimum password length", "int"),
32 (2, "RequireSymbols", "Require symbols (!@#$…)", "bool"),
33 (3, "RequireNumbers", "Require numbers (0‑9)", "bool"),
34 (4, "RequireUppercaseCharacters", "Require uppercase letters (A‑Z)", "bool"),
35 (5, "RequireLowercaseCharacters", "Require lowercase letters (a‑z)", "bool"),
36 (6, "AllowUsersToChangePassword", "Allow users to change password", "bool"),
37 (7, "ExpirePasswords", "Expire passwords (enable aging)", "bool"),
38 (8, "MaxPasswordAge", "Maximum password age (days)", "int"),
39 (9, "PasswordReusePrevention", "Prevent password reuse (last N)", "int"),
40 (10, "HardExpiry", "Hard expiry (no grace period)", "bool"),
41]
42
43
44# ----------------------------------------------------------------------
45# Helper functions
46# ----------------------------------------------------------------------
47def utc_now() -> datetime:
48 """Return a timezone‑aware UTC datetime (compatible with all Python 3.x)."""
49 return datetime.datetime.now(timezone.utc)
50
51
52def prompt_expected(field_type: str, description: str) -> Optional[Any]:
53 """
54 Ask the auditor for the expected value.
55 Returns:
56 - int / bool : the entered expectation
57 - None : user pressed Enter → rule is N/A
58 """
59 while True:
60 raw = input(
61 f"Enter expected value for '{description}' ({field_type}) or press <Enter> to skip: "
62 ).strip()
63 if raw == "":
64 return None # N/A
65 if field_type == "int":
66 if raw.isdigit():
67 return int(raw)
68 print("Please enter a whole number (or leave blank).")
69 elif field_type == "bool":
70 lowered = raw.lower()
71 if lowered in {"true", "t", "yes", "y", "1"}:
72 return True
73 if lowered in {"false", "f", "no", "n", "0"}:
74 return False
75 print("Boolean expected – type yes/no (or true/false).")
76 else:
77 # Should never happen
78 return raw
79
80
81def evaluate(expect: Optional[Any], actual: Any, field_type: str) -> str:
82 """Return PASS / FAIL / N/A."""
83 if expect is None:
84 return "N/A"
85 if field_type == "int":
86 return "PASS" if actual >= expect else "FAIL"
87 if field_type == "bool":
88 return "PASS" if actual is expect else "FAIL"
89 return "FAIL"
90
91
92def load_json(path: Path) -> Dict[str, Any]:
93 """Read the JSON file generated by the Bash script."""
94 try:
95 with path.open("r", encoding="utf-8") as fh:
96 return json.load(fh)
97 except Exception as exc:
98 sys.exit(f"Could not read JSON file {path}: {exc}")
99
100
101def write_csv(
102 out_path: Path,
103 metadata: Dict[str, Any],
104 rows: List[List[Any]],
105) -> None:
106 """Write the CSV report, including a metadata header block."""
107 with out_path.open("w", newline="", encoding="utf-8") as csvfile:
108 writer = csv.writer(csvfile)
109
110 # ---- metadata block (prefixed with #) ----
111 for key, val in metadata.items():
112 writer.writerow([f"# {key}: {val}"])
113 writer.writerow([]) # blank line
114
115 # ---- column header ----
116 writer.writerow(["Rule#", "Policy‑Item", "Expected", "Actual", "Result"])
117
118 # ---- data rows ----
119 for row in rows:
120 writer.writerow(row)
121
122
123# ----------------------------------------------------------------------
124# Main workflow
125# ----------------------------------------------------------------------
126def main() -> None:
127 if len(sys.argv) != 2:
128 sys.exit("Usage: python3 evaluate_policy.py <policy_report.json>")
129 json_path = Path(sys.argv[1])
130 data = load_json(json_path)
131
132 # Extract the two top‑level sections we expect
133 metadata = data.get("metadata", {})
134 policy = data.get("PasswordPolicy", {})
135
136 # --------------------------------------------------------------
137 # 1. Prompt the auditor for expectations
138 # --------------------------------------------------------------
139 expectations: Dict[str, Optional[Any]] = {}
140 print("\n=== Expected / Minimum Values (press <Enter> for N/A) ===\n")
141 for _, key, friendly, typ in POLICY_FIELDS:
142 expectations[key] = prompt_expected(typ, friendly)
143
144 # --------------------------------------------------------------
145 # 2. Build the CSV rows (including PASS/FAIL)
146 # --------------------------------------------------------------
147 csv_rows: List[List[Any]] = []
148 for rule_no, key, friendly, typ in POLICY_FIELDS:
149 expected = expectations[key]
150 actual = policy.get(key, "(missing)")
151 result = evaluate(expected, actual, typ)
152
153 # Normalise booleans for nicer CSV output
154 actual_str = (
155 str(actual).lower()
156 if isinstance(actual, bool)
157 else str(actual)
158 )
159 expected_str = "" if expected is None else str(expected).lower()
160
161 csv_rows.append(
162 [rule_no, friendly, expected_str, actual_str, result]
163 )
164
165 # --------------------------------------------------------------
166 # 3. Write the CSV file (timestamped)
167 # --------------------------------------------------------------
168 timestamp = utc_now().strftime("%Y%m%dT%H%M%SZ")
169 out_csv = Path(f"policy_audit_{timestamp}.csv")
170 write_csv(out_csv, metadata, csv_rows)
171
172 print(f"\nAudit CSV written to: {out_csv}\n")
173 # Simple on‑screen summary
174 print("Summary:")
175 for row in csv_rows:
176 print(f" {row[0]:2}. {row[1]:35} → {row[4]}")
177
178 print("\n--- End of report ---\n")
179
180
181if __name__ == "__main__":
182 import datetime # imported here to keep the top of file tidy
183 main()
applications/aws/aws_password_policy/gather_policy.sh added +85
@@ -0,0 +1,85 @@
1#!/usr/bin/env bash
2#
3# gather_policy.sh
4# ----------------
5# 1. Calls AWS CLI to fetch the current IAM password policy.
6# 2. Captures execution metadata (date, user, host, AWS profile/region, etc.).
7# 3. Writes a single JSON document (policy_report.json) that the Python
8# script can consume.
9#
10# Prerequisites:
11# • AWS CLI v2 installed and configured (credentials, default region, etc.)
12# • jq installed (used to merge JSON objects). If jq is missing the script
13# will abort with a helpful message.
14#
15# Usage:
16# $ chmod +x gather_policy.sh
17# $ ./gather_policy.sh # creates policy_report.json in the cwd
18# $ ./gather_policy.sh -o /tmp/my_report.json # custom output path
19#
20
21set -euo pipefail
22
23# ---------- Helper ----------
24die() { echo "ERROR: $*" >&2; exit 1; }
25
26# ---------- Argument parsing ----------
27OUTFILE="policy_report.json"
28while [[ $# -gt 0 ]]; do
29 case "$1" in
30 -o|--output)
31 shift
32 [[ -z "${1:-}" ]] && die "Missing argument for -o|--output"
33 OUTFILE="$1"
34 ;;
35 -h|--help)
36 echo "Usage: $0 [-o|--output <path-to-json>]"
37 exit 0
38 ;;
39 *)
40 die "Unknown option: $1"
41 ;;
42 esac
43 shift
44done
45
46# ---------- Verify prerequisites ----------
47command -v aws >/dev/null || die "AWS CLI not found in PATH"
48command -v jq >/dev/null || die "jq not found in PATH – install it (e.g. sudo dnf install jq)"
49
50# ---------- 1. Pull the IAM password policy ----------
51# If no policy exists, AWS returns a NoSuchEntity error – we capture that
52if ! POLICY_JSON=$(aws iam get-account-password-policy 2>/dev/null); then
53 die "No password policy is defined for this AWS account (AWS returned NoSuchEntity)."
54fi
55
56# ---------- 2. Gather metadata ----------
57# * timestamp (UTC)
58# * OS user running the script
59# * hostname
60# * current working directory (useful for traceability)
61# * AWS profile & region (if set)
62# * AWS caller identity (ARN, account id, user id) – proves *who* ran the command
63METADATA=$(cat <<EOF
64{
65 "metadata": {
66 "report_timestamp_utc": "$(date -u +"%Y-%m-%dT%H:%M:%SZ")",
67 "os_user": "$(id -un)",
68 "hostname": "$(hostname)",
69 "working_directory": "$(pwd)",
70 "aws_profile": "${AWS_PROFILE:-default}",
71 "aws_region": "${AWS_DEFAULT_REGION:-unknown}",
72 "aws_caller_identity": $(aws sts get-caller-identity 2>/dev/null || echo "null")
73 }
74}
75EOF
76)
77
78# ---------- 3. Merge policy + metadata ----------
79# The final JSON will have two top‑level keys: "metadata" and "PasswordPolicy"
80FINAL_JSON=$(jq -s 'reduce .[] as $item ({}; . * $item)' <(echo "$METADATA") <(echo "$POLICY_JSON"))
81
82# ---------- 4. Write output ----------
83echo "$FINAL_JSON" | jq '.' > "$OUTFILE"
84
85echo "Password‑policy report written to: $OUTFILE"