Skip to main content
skill CX Codex

access-review — Claude Code skill: access audits and certification

Runs periodic access reviews across AWS IAM and GitHub: flags dormant accounts, unused keys and missing MFA. Useful when you need evidence for SOC 2, HIPAA or ISO 27001.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add BagelHole/DevOps-Security-Agent-Skills --skill access-review -a claude-code
Open on GitHub

What access-review does

The skill lays out a five-step review cycle: define the systems in scope, pull current access data, hand items to managers for certification, revoke what is no longer needed, then report. Each step comes with actions and deadlines, and frequency depends on the access type — privileged and service accounts get checked more often than standard ones.

Two bash scripts do the heavy lifting. The AWS one generates a credential report and pulls users without MFA, accounts idle past 90 days, access keys that were never used, users holding admin policies and roles with cross-account trust. The GitHub one walks the org through gh api: members, owners, outside collaborators, per-repo access, teams and pending invitations.

Both expect a configured AWS CLI and an authenticated gh, and they write results into ./access-review/ split by month. You need read access to IAM and to the GitHub org up front, otherwise some reports come back empty. Okta and other providers appear only as a general workflow, with no ready-made commands.

What it helps with

  • list AWS users who never enabled MFA
  • find access keys untouched for 90 days
  • export outside collaborators from a GitHub org
  • spot IAM roles trusted by another account
  • put together audit evidence for access governance

Example prompts for the agent

  • run a quarterly access review on AWS IAM and write the report to ./access-review/
  • find inactive members and pending invitations in our GitHub org
  • draft a certification plan: who owns each review item, deadlines, what gets revoked

How to install access-review

1Skills CLI (Vercel)

npx skills add BagelHole/DevOps-Security-Agent-Skills --skill access-review -a claude-code

2Manually: put the files in place

~/.claude/skills/access-review/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/access-review/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about access-review

How do I install access-review in Claude?

Run npx skills add BagelHole/DevOps-Security-Agent-Skills --skill access-review -a claude-code. The skill file then lands at ~/.claude/skills/access-review/SKILL.md.

What does access-review do?

It documents the access review cycle and ships scripts for AWS IAM and GitHub: credential reporting, idle user detection, keys without MFA and roles with cross-account trust.

What do the scripts need to run?

A configured AWS CLI and an authenticated gh with org access. Without read permissions on IAM and the org data, some reports will not build.

Source text

SKILL.md · MIT
---
name: access-review
description: Conduct periodic access reviews and certifications. Implement access governance and recertification workflows. Use when managing access compliance.
license: MIT
metadata:
  author: devops-skills
  version: "1.0"
---

# Access Review

Implement periodic access review processes for AWS IAM, GitHub, Okta, and other identity providers, including automated reporting, certification workflows, and unused permission detection.

## When to Use

- Conducting quarterly or annual access reviews for compliance (SOC 2, HIPAA, PCI DSS, ISO 27001)
- Identifying and removing stale accounts and unused credentials
- Certifying that current access levels match job responsibilities
- Detecting excessive privileges and dormant service accounts
- Generating evidence for auditor requests on access governance

## Access Review Process

```yaml
access_review_workflow:
  1_scope:
    actions:
      - Define systems in scope for the review cycle
      - Identify review owners (managers, system owners)
      - Set review timeline and deadlines
      - Generate access inventory from all identity sources
    frequency:
      privileged_access: Quarterly
      standard_access: Semi-annually
      service_accounts: Quarterly
      api_keys: Monthly

  2_extract:
    actions:
      - Pull current access data from all systems
      - Correlate identities across platforms (SSO mapping)
      - Enrich with last login and activity data
      - Flag accounts for review (inactive, over-privileged, orphaned)

  3_review:
    actions:
      - Assign review items to appropriate managers
      - Manager certifies each user's access (approve/revoke/modify)
      - Risk-based prioritization (privileged users reviewed first)
      - Escalate non-responses after deadline
    decisions:
      approve: "Access is appropriate for current role"
      modify: "Access needs adjustment (reduce/change scope)"
      revoke: "Access is no longer needed"

  4_remediate:
    actions:
      - Revoke access flagged for removal
      - Modify access as directed by reviewers
      - Document exceptions with justification
      - Confirm changes with system owners
    sla:
      revocations: "Complete within 5 business days of decision"
      modifications: "Complete within 10 business days"
      exceptions: "Approved by security team, documented, time-limited"

  5_report:
    actions:
      - Generate completion metrics (% reviewed, % on time)
      - Document all decisions and actions taken
      - Archive evidence for compliance audits
      - Identify process improvements for next cycle
```

## AWS IAM Access Review Scripts

```bash
#!/usr/bin/env bash
# aws-iam-review.sh - Comprehensive IAM access review report

OUTPUT_DIR="./access-review/$(date +%Y-%m)"
mkdir -p "$OUTPUT_DIR"

echo "=== AWS IAM Access Review ==="

# Generate credential report
aws iam generate-credential-report > /dev/null
sleep 10
aws iam get-credential-report --output text --query Content | \
  base64 -d > "$OUTPUT_DIR/credential-report.csv"

echo "--- Users Without MFA ---"
aws iam get-credential-report --output text --query Content | base64 -d | \
  awk -F, 'NR>1 && $4=="true" && $8=="false" {print $1}' | \
  tee "$OUTPUT_DIR/users-without-mfa.txt"

echo "--- Inactive Users (90+ days) ---"
THRESHOLD=$(date -d '90 days ago' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -v-90d +%Y-%m-%dT%H:%M:%S)
aws iam get-credential-report --output text --query Content | base64 -d | \
  awk -F, -v t="$THRESHOLD" 'NR>1 && $5!="N/A" && $5!="no_information" && $5<t {
    print $1","$5
  }' | tee "$OUTPUT_DIR/inactive-users.csv"

echo "--- Stale Access Keys (90+ days unused) ---"
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
  for key_id in $(aws iam list-access-keys --user-name "$user" \
    --query 'AccessKeyMetadata[?Status==`Active`].AccessKeyId' --output text); do
    last_used=$(aws iam get-access-key-last-used --access-key-id "$key_id" \
      --query 'AccessKeyLastUsed.LastUsedDate' --output text)
    if [ "$last_used" = "None" ] || [ "$last_used" \< "$THRESHOLD" ]; then
      echo "$user,$key_id,$last_used"
    fi
  done
done | tee "$OUTPUT_DIR/stale-access-keys.csv"

echo "--- Users With Admin Policies ---"
for user in $(aws iam list-users --query 'Users[*].UserName' --output text); do
  policies=$(aws iam list-attached-user-policies --user-name "$user" \
    --query 'AttachedPolicies[*].PolicyName' --output text)
  if echo "$policies" | grep -qi "admin\|fullaccess"; then
    groups=$(aws iam list-groups-for-user --user-name "$user" \
      --query 'Groups[*].GroupName' --output text)
    echo "$user|policies:$policies|groups:$groups"
  fi
done | tee "$OUTPUT_DIR/admin-users.txt"

echo "--- IAM Roles With Cross-Account Trust ---"
for role in $(aws iam list-roles --query 'Roles[*].RoleName' --output text); do
  trust=$(aws iam get-role --role-name "$role" \
    --query 'Role.AssumeRolePolicyDocument' --output json 2>/dev/null)
  if echo "$trust" | grep -q '"AWS"' && echo "$trust" | grep -qv "$(aws sts get-caller-identity --query Account --output text)"; then
    echo "$role: $trust" | jq -c '.Statement[].Principal'
  fi
done | tee "$OUTPUT_DIR/cross-account-roles.txt"

echo "--- Service Accounts (Programmatic Only) ---"
aws iam get-credential-report --output text --query Content | base64 -d | \
  awk -F, 'NR>1 && $4=="false" && $9!="N/A" {print $1","$11","$16}' | \
  tee "$OUTPUT_DIR/service-accounts.csv"

echo "Report generated in $OUTPUT_DIR"
```

## GitHub Access Review

```bash
#!/usr/bin/env bash
# github-access-review.sh - GitHub organization access audit

ORG="your-org"
OUTPUT_DIR="./access-review/github/$(date +%Y-%m)"
mkdir -p "$OUTPUT_DIR"

echo "=== GitHub Organization Access Review ==="

echo "--- Organization Members ---"
gh api orgs/$ORG/members --paginate \
  --jq '.[] | [.login, .site_admin] | @csv' \
  > "$OUTPUT_DIR/org-members.csv"

echo "--- Organization Owners ---"
gh api "orgs/$ORG/members?role=admin" --paginate \
  --jq '.[] | .login' \
  > "$OUTPUT_DIR/org-owners.txt"

echo "--- Outside Collaborators ---"
gh api orgs/$ORG/outside_collaborators --paginate \
  --jq '.[] | .login' \
  > "$OUTPUT_DIR/outside-collaborators.txt"

echo "--- Repository Access Per Repo ---"
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
  echo "Repo: $repo"
  gh api "repos/$ORG/$repo/collaborators" --paginate \
    --jq '.[] | [.login, .role_name] | @csv' \
    > "$OUTPUT_DIR/repo-$repo-access.csv" 2>/dev/null
done

echo "--- Team Memberships ---"
for team in $(gh api orgs/$ORG/teams --paginate --jq '.[].slug'); do
  echo "Team: $team"
  gh api "orgs/$ORG/teams/$team/members" --paginate \
    --jq '.[] | .login' \
    > "$OUTPUT_DIR/team-$team-members.txt"
done

echo "--- Pending Invitations ---"
gh api orgs/$ORG/invitations --paginate \
  --jq '.[] | [.login, .email, .role, .created_at] | @csv' \
  > "$OUTPUT_DIR/pending-invitations.csv"

echo "--- Deploy Keys ---"
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
  keys=$(gh api "repos/$ORG/$repo/keys" --jq '.[].title' 2>/dev/null)
  if [ -n "$keys" ]; then
    echo "$repo: $keys"
  fi
done > "$OUTPUT_DIR/deploy-keys.txt"

echo "--- Branch Protection Rules ---"
for repo in $(gh repo list $ORG --json name -q '.[].name' --limit 500); do
  protection=$(gh api "repos/$ORG/$repo/branches/main/protection" 2>/dev/null)
  if [ $? -eq 0 ]; then
    echo "$repo: protected"
    echo "$protection" | jq '{required_reviews: .required_pull_request_reviews.required_approving_review_count, dismiss_stale: .required_pull_request_reviews.dismiss_stale_reviews}' \
      > "$OUTPUT_DIR/branch-protection-$repo.json"
  else
    echo "$repo: NOT protected" >> "$OUTPUT_DIR/unprotected-repos.txt"
  fi
done

echo "Report generated in $OUTPUT_DIR"
```

## Okta Access Review

```bash
#!/usr/bin/env bash
# okta-access-review.sh - Okta user and application access audit
# Requires OKTA_DOMAIN and OKTA_API_TOKEN environment variables

OUTPUT_DIR="./access-review/okta/$(date +%Y-%m)"
mkdir -p "$OUTPUT_DIR"
BASE_URL="https://${OKTA_DOMAIN}/api/v1"

echo "=== Okta Access Review ==="

echo "--- Active Users ---"
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
  "$BASE_URL/users?filter=status+eq+%22ACTIVE%22&limit=200" | \
  jq -r '.[] | [.profile.email, .profile.firstName, .profile.lastName, .lastLogin, .created] | @csv' \
  > "$OUTPUT_DIR/active-users.csv"

echo "--- Suspended/Deprovisioned Users ---"
for status in SUSPENDED DEPROVISIONED; do
  curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
    "$BASE_URL/users?filter=status+eq+%22$status%22&limit=200" | \
    jq -r '.[] | [.profile.email, .status, .statusChanged] | @csv'
done > "$OUTPUT_DIR/inactive-users.csv"

echo "--- Users Without MFA Enrolled ---"
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
  "$BASE_URL/users?limit=200" | \
  jq -r '.[] | .id' | while read -r uid; do
    factors=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
      "$BASE_URL/users/$uid/factors" | jq 'length')
    if [ "$factors" -eq 0 ]; then
      curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
        "$BASE_URL/users/$uid" | jq -r '.profile.email'
    fi
  done > "$OUTPUT_DIR/users-without-mfa.txt"

echo "--- Application Assignments ---"
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
  "$BASE_URL/apps?limit=200" | \
  jq -r '.[] | [.id, .label, .status] | @csv' | while IFS=, read -r app_id app_name status; do
    echo "App: $app_name"
    curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
      "$BASE_URL/apps/$app_id/users?limit=200" | \
      jq -r '.[] | [.credentials.userName // .profile.email, .status] | @csv'
  done > "$OUTPUT_DIR/app-assignments.csv"

echo "--- Admin Role Assignments ---"
curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
  "$BASE_URL/users?limit=200" | \
  jq -r '.[] | .id' | while read -r uid; do
    roles=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
      "$BASE_URL/users/$uid/roles" | jq -r '.[].type' 2>/dev/null)
    if [ -n "$roles" ]; then
      email=$(curl -s -H "Authorization: SSWS $OKTA_API_TOKEN" \
        "$BASE_URL/users/$uid" | jq -r '.profile.email')
      echo "$email: $roles"
    fi
  done > "$OUTPUT_DIR/admin-roles.txt"

echo "Report generated in $OUTPUT_DIR"
```

## Unused Permission Detection

```python
"""
Detect unused IAM permissions using CloudTrail and IAM Access Analyzer.
Generates recommendations for right-sizing access.
"""
import boto3
import json
import time
from datetime import datetime, timedelta, timezone


def analyze_iam_usage(days_lookback=90):
    """Analyze IAM user and role activity against granted permissions."""
    iam = boto3.client("iam")

    report = {
        "generated_at": datetime.now(timezone.utc).isoformat(),
        "lookback_days": days_lookback,
        "findings": [],
    }

    users = iam.list_users()["Users"]
    for user in users:
        username = user["UserName"]

        # Get service last accessed data
        job_id = iam.generate_service_last_accessed_details(
            Arn=user["Arn"]
        )["JobId"]

        while True:
            result = iam.get_service_last_accessed_details(JobId=job_id)
            if result["JobStatus"] == "COMPLETED":
                break
            time.sleep(2)

        threshold = datetime.now(timezone.utc) - timedelta(days=days_lookback)
        unused_services = []

        for service in result["ServicesLastAccessed"]:
            last_accessed = service.get("LastAuthenticated")
            if last_accessed is None or last_accessed < threshold:
                unused_services.append({
                    "service": service["ServiceNamespace"],
                    "last_accessed": str(last_accessed) if last_accessed else "Never",
                })

        if unused_services:
            report["findings"].append({
                "type": "unused_permissions",
                "user": username,
                "arn": user["Arn"],
                "unused_service_count": len(unused_services),
                "unused_services": unused_services[:10],
                "recommendation": "Review and remove unused service permissions",
            })

    return report


def detect_overprivileged_roles():
    """Use IAM Access Analyzer to find overprivileged roles."""
    analyzer = boto3.client("accessanalyzer")

    findings = analyzer.list_findings(
        analyzerArn="arn:aws:access-analyzer:us-east-1:123456789012:analyzer/org-analyzer",
        filter={
            "status": {"eq": ["ACTIVE"]},
            "resourceType": {"eq": ["AWS::IAM::Role"]},
        },
    )

    return [
        {
            "resource": f["resource"],
            "resource_type": f["resourceType"],
            "condition": f.get("condition", {}),
            "principal": f.get("principal", {}),
            "action": f.get("action", []),
            "created_at": str(f["createdAt"]),
        }
        for f in findings.get("findings", [])
    ]
```

## Certification Workflow Automation

```yaml
# GitHub Actions - Automated access review reminder and tracking
name: Quarterly Access Review
on:
  schedule:
    - cron: '0 9 1 1,4,7,10 *'  # First day of each quarter
  workflow_dispatch:

jobs:
  generate-review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Generate access reports
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AUDIT_AWS_KEY }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AUDIT_AWS_SECRET }}
          OKTA_DOMAIN: ${{ secrets.OKTA_DOMAIN }}
          OKTA_API_TOKEN: ${{ secrets.OKTA_API_TOKEN }}
        run: |
          bash scripts/aws-iam-review.sh
          bash scripts/github-access-review.sh
          bash scripts/okta-access-review.sh

      - name: Create review issue
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          QUARTER="Q$(( ($(date +%-m) - 1) / 3 + 1 )) $(date +%Y)"
          MFA_MISSING=$(wc -l < access-review/$(date +%Y-%m)/users-without-mfa.txt)
          INACTIVE=$(wc -l < access-review/$(date +%Y-%m)/inactive-users.csv)
          STALE_KEYS=$(wc -l < access-review/$(date +%Y-%m)/stale-access-keys.csv)

          gh issue create \
            --title "Access Review - $QUARTER" \
            --label "compliance,access-review" \
            --body "## Quarterly Access Review - $QUARTER

          ### Summary
          - Users without MFA: **$MFA_MISSING**
          - Inactive users (90+ days): **$INACTIVE**
          - Stale access keys: **$STALE_KEYS**

          ### Required Actions
          - [ ] Review and disable inactive users
          - [ ] Enforce MFA for non-compliant users
          - [ ] Rotate or deactivate stale access keys
          - [ ] Review admin/privileged access assignments
          - [ ] Review outside collaborators on GitHub
          - [ ] Certify remaining access is appropriate
          - [ ] Document exceptions with justification

          ### Deadline
          Complete within 30 days."

      - name: Upload reports as artifact
        uses: actions/upload-artifact@v4
        with:
          name: access-review-reports
          path: access-review/
          retention-days: 365
```

## Access Review Checklist

```yaml
access_review_checklist:
  preparation:
    - [ ] Define scope (systems, user populations, review period)
    - [ ] Assign review owners for each system
    - [ ] Extract current access data from all identity sources
    - [ ] Correlate identities across platforms via SSO mapping
    - [ ] Generate review packages for each manager

  execution:
    - [ ] Managers notified with review assignments and deadline
    - [ ] Privileged access reviewed first (admin, root, service accounts)
    - [ ] Each user's access certified (approve, modify, or revoke)
    - [ ] Inactive accounts flagged for disable/removal
    - [ ] Stale credentials (keys, tokens) flagged for rotation
    - [ ] Outside collaborators and contractors verified
    - [ ] Service account ownership confirmed

  remediation:
    - [ ] Revocations executed within SLA (5 business days)
    - [ ] Access modifications completed within SLA (10 business days)
    - [ ] Exceptions documented with business justification
    - [ ] Exception approvals recorded from security team
    - [ ] Changes verified in target systems

  reporting:
    - [ ] Review completion rate documented (target: 100%)
    - [ ] Non-response escalations documented
    - [ ] Remediation actions summarized
    - [ ] Exception register updated
    - [ ] Evidence archived for audit (retained 3+ years)
    - [ ] Metrics compared to prior review cycle
```

## Best Practices

- Automate access data extraction to eliminate manual data gathering and reduce errors
- Integrate access review with HR systems to automatically flag accounts for departed employees
- Use risk-based review frequency: privileged access quarterly, standard access semi-annually
- Provide managers with clear context: show last login date, permissions, and role to inform decisions
- Set firm deadlines with escalation for non-response (no certification = automatic revocation)
- Detect and eliminate orphaned accounts from contractors, former employees, and decommissioned services
- Review service accounts and API keys alongside human accounts to prevent credential sprawl
- Document all exceptions with business justification, approver, and expiration date
- Track review metrics over time: completion rates, revocation rates, time to remediate
- Archive all access review evidence for a minimum of 3 years for audit purposes

© devops-skills · MIT · BagelHole/DevOps-Security-Agent-Skills

Similar skills

Everything for Security →

From the same set127

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor