Commit 599361577d
Unsigned
Layout: unified · split
applications/aws/README.md +113
| @@ -3,6 +3,7 @@ | ||
| 3 | 3 | *Note*: This example uses an account titled `cmc`, which has access provisioned to it through IAM. |
| 4 | 4 | |
| 5 | 5 | ``` bash |
| 6 | chmod +x aws_iam_users.sh | |
| 6 | 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" | |