Commit 599361577d
Unsigned
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 |
| 6 | chmod +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 | |||
| 99 | To test a password policy against AWS, I have created two steps: | ||
| 100 | |||
| 101 | **Step 1: Gather AWS Policy** | ||
| 102 | |||
| 103 | Run the script on your CloudShell or using the `aws` command | ||
| 104 | |||
| 105 | ``` bash | ||
| 106 | chmod +x gather_policy.sh | ||
| 107 | ./gather_policy.sh | ||
| 108 | ``` | ||
| 109 | |||
| 110 | This 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 | |||
| 144 | Use 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 | ||
| 147 | uv run evaluate_policy.py policy_report.json | ||
| 148 | ``` | ||
| 149 | |||
| 150 | This 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 | |||
| 157 | Enter expected value for 'Minimum password length' (int) or press <Enter> to skip: 8 | ||
| 158 | Enter expected value for 'Require symbols (!@#$…)' (bool) or press <Enter> to skip: true | ||
| 159 | Enter expected value for 'Require numbers (0‑9)' (bool) or press <Enter> to skip: true | ||
| 160 | Enter expected value for 'Require uppercase letters (A‑Z)' (bool) or press <Enter> to skip: true | ||
| 161 | Enter expected value for 'Require lowercase letters (a‑z)' (bool) or press <Enter> to skip: true | ||
| 162 | Enter expected value for 'Allow users to change password' (bool) or press <Enter> to skip: true | ||
| 163 | Enter expected value for 'Expire passwords (enable aging)' (bool) or press <Enter> to skip: true | ||
| 164 | Enter expected value for 'Maximum password age (days)' (int) or press <Enter> to skip: 90 | ||
| 165 | Enter expected value for 'Prevent password reuse (last N)' (int) or press <Enter> to skip: 4 | ||
| 166 | Enter expected value for 'Hard expiry (no grace period)' (bool) or press <Enter> to skip: false | ||
| 167 | |||
| 168 | Audit CSV written to: policy_audit_20251215T014323Z.csv | ||
| 169 | |||
| 170 | Summary: | ||
| 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 | |||
| 196 | Rule#,Policy‑Item,Expected,Actual,Result | ||
| 197 | 1,Minimum password length,8,8,PASS | ||
| 198 | 2,Require symbols (!@#$…),true,true,PASS | ||
| 199 | 3,Require numbers (0‑9),true,true,PASS | ||
| 200 | 4,Require uppercase letters (A‑Z),true,true,PASS | ||
| 201 | 5,Require lowercase letters (a‑z),true,true,PASS | ||
| 202 | 6,Allow users to change password,true,true,PASS | ||
| 203 | 7,Expire passwords (enable aging),true,true,PASS | ||
| 204 | 8,Maximum password age (days),90,90,PASS | ||
| 205 | 9,Prevent password reuse (last N),4,4,PASS | ||
| 206 | 10,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 | """ | ||
| 3 | evaluate_policy.py | ||
| 4 | ------------------ | ||
| 5 | Read the JSON file produced by `gather_policy.sh`, ask the auditor for the | ||
| 6 | expected/minimum values for each of the 10 IAM password‑policy items, and emit | ||
| 7 | a CSV audit report. | ||
| 8 | |||
| 9 | Features | ||
| 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 | |||
| 19 | import csv | ||
| 20 | import json | ||
| 21 | import sys | ||
| 22 | from datetime import datetime, timezone | ||
| 23 | from pathlib import Path | ||
| 24 | from 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 | # ---------------------------------------------------------------------- | ||
| 30 | POLICY_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 | # ---------------------------------------------------------------------- | ||
| 47 | def 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 | |||
| 52 | def 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 | |||
| 81 | def 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 | |||
| 92 | def 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 | |||
| 101 | def 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 | # ---------------------------------------------------------------------- | ||
| 126 | def 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 | |||
| 181 | if __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 | |||
| 21 | set -euo pipefail | ||
| 22 | |||
| 23 | # ---------- Helper ---------- | ||
| 24 | die() { echo "ERROR: $*" >&2; exit 1; } | ||
| 25 | |||
| 26 | # ---------- Argument parsing ---------- | ||
| 27 | OUTFILE="policy_report.json" | ||
| 28 | while [[ $# -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 | ||
| 44 | done | ||
| 45 | |||
| 46 | # ---------- Verify prerequisites ---------- | ||
| 47 | command -v aws >/dev/null || die "AWS CLI not found in PATH" | ||
| 48 | command -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 | ||
| 52 | if ! 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)." | ||
| 54 | fi | ||
| 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 | ||
| 63 | METADATA=$(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 | } | ||
| 75 | EOF | ||
| 76 | ) | ||
| 77 | |||
| 78 | # ---------- 3. Merge policy + metadata ---------- | ||
| 79 | # The final JSON will have two top‑level keys: "metadata" and "PasswordPolicy" | ||
| 80 | FINAL_JSON=$(jq -s 'reduce .[] as $item ({}; . * $item)' <(echo "$METADATA") <(echo "$POLICY_JSON")) | ||
| 81 | |||
| 82 | # ---------- 4. Write output ---------- | ||
| 83 | echo "$FINAL_JSON" | jq '.' > "$OUTFILE" | ||
| 84 | |||
| 85 | echo "Password‑policy report written to: $OUTFILE" | ||