$npx -y skills add BagelHole/DevOps-Security-Agent-Skills --skill access-reviewConduct periodic access reviews and certifications. Implement access governance and recertification workflows. Use when managing access compliance.
| 1 | # Access Review |
| 2 | |
| 3 | Implement periodic access review processes for AWS IAM, GitHub, Okta, and other identity providers, including automated reporting, certification workflows, and unused permission detection. |
| 4 | |
| 5 | ## When to Use |
| 6 | |
| 7 | - Conducting quarterly or annual access reviews for compliance (SOC 2, HIPAA, PCI DSS, ISO 27001) |
| 8 | - Identifying and removing stale accounts and unused credentials |
| 9 | - Certifying that current access levels match job responsibilities |
| 10 | - Detecting excessive privileges and dormant service accounts |
| 11 | - Generating evidence for auditor requests on access governance |
| 12 | |
| 13 | ## Access Review Process |
| 14 | |
| 15 | ```yaml |
| 16 | access_review_workflow: |
| 17 | 1_scope: |
| 18 | actions: |
| 19 | - Define systems in scope for the review cycle |
| 20 | - Identify review owners (managers, system owners) |
| 21 | - Set review timeline and deadlines |
| 22 | - Generate access inventory from all identity sources |
| 23 | frequency: |
| 24 | privileged_access: Quarterly |
| 25 | standard_access: Semi-annually |
| 26 | service_accounts: Quarterly |
| 27 | api_keys: Monthly |
| 28 | |
| 29 | 2_extract: |
| 30 | actions: |
| 31 | - Pull current access data from all systems |
| 32 | - Correlate identities across platforms (SSO mapping) |
| 33 | - Enrich with last login and activity data |
| 34 | - Flag accounts for review (inactive, over-privileged, orphaned) |
| 35 | |
| 36 | 3_review: |
| 37 | actions: |
| 38 | - Assign review items to appropriate managers |
| 39 | - Manager certifies each user's access (approve/revoke/modify) |
| 40 | - Risk-based prioritization (privileged users reviewed first) |
| 41 | - Escalate non-responses after deadline |
| 42 | decisions: |
| 43 | approve: "Access is appropriate for current role" |
| 44 | modify: "Access needs adjustment (reduce/change scope)" |
| 45 | revoke: "Access is no longer needed" |
| 46 | |
| 47 | 4_remediate: |
| 48 | actions: |
| 49 | - Revoke access flagged for removal |
| 50 | - Modify access as directed by reviewers |
| 51 | - Document exceptions with justification |
| 52 | - Confirm changes with system owners |
| 53 | sla: |
| 54 | revocations: "Complete within 5 business days of decision" |
| 55 | modifications: "Complete within 10 business days" |
| 56 | exceptions: "Approved by security team, documented, time-limited" |
| 57 | |
| 58 | 5_report: |
| 59 | actions: |
| 60 | - Generate completion metrics (% reviewed, % on time) |
| 61 | - Document all decisions and actions taken |
| 62 | - Archive evidence for compliance audits |
| 63 | - Identify process improvements for next cycle |
| 64 | ``` |
| 65 | |
| 66 | ## AWS IAM Access Review Scripts |
| 67 | |
| 68 | ```bash |
| 69 | #!/usr/bin/env bash |
| 70 | # aws-iam-review.sh - Comprehensive IAM access review report |
| 71 | |
| 72 | OUTPUT_DIR="./access-review/$(date +%Y-%m)" |
| 73 | mkdir -p "$OUTPUT_DIR" |
| 74 | |
| 75 | echo "=== AWS IAM Access Review ===" |
| 76 | |
| 77 | # Generate credential report |
| 78 | aws iam generate-credential-report > /dev/null |
| 79 | sleep 10 |
| 80 | aws iam get-credential-report --output text --query Content | \ |
| 81 | base64 -d > "$OUTPUT_DIR/credential-report.csv" |
| 82 | |
| 83 | echo "--- Users Without MFA ---" |
| 84 | aws iam get-credential-report --output text --query Content | base64 -d | \ |
| 85 | awk -F, 'NR>1 && $4=="true" && $8=="false" {print $1}' | \ |
| 86 | tee "$OUTPUT_DIR/users-without-mfa.txt" |
| 87 | |
| 88 | echo "--- Inactive Users (90+ days) ---" |
| 89 | THRESHOLD=$(date -d '90 days ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-90d +%Y-%m-%dT%H:%M:%S) |
| 90 | aws iam get-credential-report --output text --query Content | base64 -d | \ |
| 91 | awk -F, -v t="$THRESHOLD" 'NR>1 && $5!="N/A" && $5!="no_information" && $5<t { |
| 92 | print $1","$5 |
| 93 | }' | tee "$OUTPUT_DIR/inactive-users.csv" |
| 94 | |
| 95 | echo "--- Stale Access Keys (90+ days unused) ---" |
| 96 | for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do |
| 97 | for key_id in $(aws iam list-access-keys --user-name "$user" \ |
| 98 | --query 'AccessKeyMetadata[?Status==`Active`].AccessKeyId' --output text); do |
| 99 | last_used=$(aws iam get-access-key-last-used --access-key-id "$key_id" \ |
| 100 | --query 'AccessKeyLastUsed.LastUsedDate' --output text) |
| 101 | if [ "$last_used" = "None" ] || [ "$last_used" \< "$THRESHOLD" ]; then |
| 102 | echo "$user,$key_id,$last_used" |
| 103 | fi |
| 104 | done |
| 105 | done | tee "$OUTPUT_DIR/stale-access-keys.csv" |
| 106 | |
| 107 | echo "--- Users With Admin Policies ---" |
| 108 | for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do |
| 109 | policies=$(aws iam list-attached-user-policies --user-name "$user" \ |
| 110 | --query 'AttachedPolicies[*].PolicyName' --output text) |
| 111 | if echo "$policies" | grep -qi "admin\|fullaccess"; then |
| 112 | groups=$(aws iam list-groups-for-user --user-name "$user" \ |
| 113 | --query 'Groups[*].GroupName' --output text) |
| 114 | echo "$user|policies:$policies|groups:$groups" |
| 115 | fi |
| 116 | done | tee "$OUTPUT_DIR/admin-users.txt" |
| 117 | |
| 118 | echo "--- IAM Roles With Cross-Account Trust ---" |
| 119 | for role in $(aws iam list-roles --query 'Roles[*].RoleName' --output text); do |
| 120 | trust=$(aws iam get-role --role-name "$role" \ |
| 121 | --query 'Role.AssumeRolePolicyDocument' --output json 2>/dev/null) |
| 122 | if echo "$t |