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()