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/aws_password_policy/evaluate_policy.py · raw
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* Most numeric items are treated as **minimums** (actual >= expected → PASS).
12* Maximum password age is treated as a **maximum** (actual <= expected → PASS),
13 because a lower ceiling is the stricter/more‑secure setting.
14* Boolean items are treated as **exact matches** (actual == expected → PASS).
15* A required item that is absent from the policy is reported as FAIL rather
16 than crashing (AWS omits e.g. MaxPasswordAge when password expiry is off).
17* The CSV begins with a small metadata block (same data that the Bash script
18 captured) so the audit trail is self‑contained.
19* Usage:
20 python3 evaluate_policy.py policy_report.json
21"""
22
23import csv
24import json
25import sys
26from datetime import datetime, timezone
27from pathlib import Path
28from typing import Any
29
30# ----------------------------------------------------------------------
31# Mapping of the 10 password‑policy fields we care about
32# (rule_no, json_key, friendly_name, datatype)
33# ----------------------------------------------------------------------
34# Numeric datatypes carry a direction:
35# "int_min" – actual must be >= expected (higher is stricter)
36# "int_max" – actual must be <= expected (lower is stricter)
37POLICY_FIELDS = [
38 (1, "MinimumPasswordLength", "Minimum password length", "int_min"),
39 (2, "RequireSymbols", "Require symbols (!@#$…)", "bool"),
40 (3, "RequireNumbers", "Require numbers (0‑9)", "bool"),
41 (4, "RequireUppercaseCharacters", "Require uppercase letters (A‑Z)", "bool"),
42 (5, "RequireLowercaseCharacters", "Require lowercase letters (a‑z)", "bool"),
43 (6, "AllowUsersToChangePassword", "Allow users to change password", "bool"),
44 (7, "ExpirePasswords", "Expire passwords (enable aging)", "bool"),
45 (8, "MaxPasswordAge", "Maximum password age (days)", "int_max"),
46 (9, "PasswordReusePrevention", "Prevent password reuse (last N)", "int_min"),
47 (10, "HardExpiry", "Hard expiry (no grace period)", "bool"),
48]
49
50
51# ----------------------------------------------------------------------
52# Helper functions
53# ----------------------------------------------------------------------
54def utc_now() -> datetime:
55 """Return a timezone‑aware UTC datetime."""
56 return datetime.now(timezone.utc)
57
58
59def prompt_expected(field_type: str, description: str) -> Any | None:
60 """
61 Ask the auditor for the expected value.
62 Returns:
63 - int / bool : the entered expectation
64 - None : user pressed Enter → rule is N/A
65 """
66 while True:
67 raw = input(
68 f"Enter expected value for '{description}' ({field_type}) or press <Enter> to skip: "
69 ).strip()
70 if raw == "":
71 return None # N/A
72 if field_type.startswith("int"):
73 if raw.isdigit():
74 return int(raw)
75 print("Please enter a whole number (or leave blank).")
76 elif field_type == "bool":
77 lowered = raw.lower()
78 if lowered in {"true", "t", "yes", "y", "1"}:
79 return True
80 if lowered in {"false", "f", "no", "n", "0"}:
81 return False
82 print("Boolean expected – type yes/no (or true/false).")
83 else:
84 # Should never happen
85 return raw
86
87
88def evaluate(expect: Any | None, actual: Any, field_type: str) -> str:
89 """Return PASS / FAIL / N/A.
90
91 A required item (expectation set) that is absent or of the wrong type in
92 the policy is a FAIL, never a crash.
93 """
94 if expect is None:
95 return "N/A"
96 if field_type == "bool":
97 # bool is a subclass of int, so guard against ints sneaking through.
98 return "PASS" if isinstance(actual, bool) and actual == expect else "FAIL"
99 if field_type.startswith("int"):
100 # Reject non‑numbers (e.g. a missing field rendered as a string) and
101 # booleans (a subclass of int that must not satisfy a numeric rule).
102 if isinstance(actual, bool) or not isinstance(actual, (int, float)):
103 return "FAIL"
104 if field_type == "int_max":
105 return "PASS" if actual <= expect else "FAIL"
106 return "PASS" if actual >= expect else "FAIL"
107 return "FAIL"
108
109
110def load_json(path: Path) -> dict[str, Any]:
111 """Read the JSON file generated by the Bash script."""
112 resolved = path.resolve()
113 if not resolved.is_file():
114 sys.exit(f"Could not read JSON file {path}: not a regular file")
115 try:
116 with resolved.open("r", encoding="utf-8") as fh:
117 return json.load(fh)
118 except Exception as exc:
119 sys.exit(f"Could not read JSON file {path}: {exc}")
120
121
122def write_csv(
123 out_path: Path,
124 metadata: dict[str, Any],
125 rows: list[list[Any]],
126) -> None:
127 """Write the CSV report, including a metadata header block."""
128 with out_path.open("w", newline="", encoding="utf-8") as csvfile:
129 writer = csv.writer(csvfile)
130
131 # ---- metadata block (prefixed with #) ----
132 for key, val in metadata.items():
133 writer.writerow([f"# {key}: {val}"])
134 writer.writerow([]) # blank line
135
136 # ---- column header ----
137 writer.writerow(["Rule#", "Policy‑Item", "Expected", "Actual", "Result"])
138
139 # ---- data rows ----
140 for row in rows:
141 writer.writerow(row)
142
143
144# ----------------------------------------------------------------------
145# Main workflow
146# ----------------------------------------------------------------------
147def main() -> None:
148 if len(sys.argv) != 2:
149 sys.exit("Usage: python3 evaluate_policy.py <policy_report.json>")
150 json_path = Path(sys.argv[1])
151 data = load_json(json_path)
152
153 # Extract the two top‑level sections we expect
154 metadata = data.get("metadata", {})
155 policy = data.get("PasswordPolicy", {})
156
157 # --------------------------------------------------------------
158 # 1. Prompt the auditor for expectations
159 # --------------------------------------------------------------
160 expectations: dict[str, Any | None] = {}
161 print("\n=== Expected / Minimum Values (press <Enter> for N/A) ===\n")
162 for _, key, friendly, typ in POLICY_FIELDS:
163 expectations[key] = prompt_expected(typ, friendly)
164
165 # --------------------------------------------------------------
166 # 2. Build the CSV rows (including PASS/FAIL)
167 # --------------------------------------------------------------
168 csv_rows: list[list[Any]] = []
169 for rule_no, key, friendly, typ in POLICY_FIELDS:
170 expected = expectations[key]
171 actual = policy.get(key, "(missing)")
172 result = evaluate(expected, actual, typ)
173
174 # Normalise booleans for nicer CSV output
175 actual_str = str(actual).lower() if isinstance(actual, bool) else str(actual)
176 expected_str = "" if expected is None else str(expected).lower()
177
178 csv_rows.append([rule_no, friendly, expected_str, actual_str, result])
179
180 # --------------------------------------------------------------
181 # 3. Write the CSV file (timestamped)
182 # --------------------------------------------------------------
183 timestamp = utc_now().strftime("%Y%m%dT%H%M%SZ")
184 out_csv = Path(f"policy_audit_{timestamp}.csv")
185 write_csv(out_csv, metadata, csv_rows)
186
187 print(f"\nAudit CSV written to: {out_csv}\n")
188 # Simple on‑screen summary
189 print("Summary:")
190 for row in csv_rows:
191 print(f" {row[0]:2}. {row[1]:35} → {row[4]}")
192
193 print("\n--- End of report ---\n")
194
195
196if __name__ == "__main__":
197 main()