A serverless, open-source dashboard to audit IAM Identity Center (SSO) permission assignments, permission set configurations, and security risks across your entire AWS Organization — deployed in minutes with Terraform.
Full-org SSO crawl | Risk scoring | Historical snapshots | Audit trail | Export to CSV/PDF | Dark/light mode | ~$0.10-$5/month
- Quick Start
- Configuration Reference
- Cost Estimate
- Dashboard Guide
- Troubleshooting
- Advanced Setup
- Architecture
- Reference
- Contributing
- Uninstalling
- License
| Requirement | Version / Detail |
|---|---|
| AWS Account | IAM Identity Center enabled with AWS Organizations |
| Terraform | >= 1.5 |
| Node.js | >= 18 |
| Python | 3.12 |
| AWS CLI | v2, configured with deploy credentials |
git clone https://github.com/alfredkzr/aws-iam-identity-center-security-governance-dashboard.git
cd aws-iam-identity-center-security-governance-dashboard
cp terraform.tfvars.example terraform/terraform.tfvarsEdit terraform/terraform.tfvars and set the three required values:
resource_prefix = "myorg-idc-gov" # Must be globally unique (used in S3 bucket names)
sso_instance_arn = "arn:aws:sso:::instance/ssoins-xxxxxxxx" # IAM Identity Center → Settings → ARN
identity_store_id = "d-xxxxxxxxxx" # IAM Identity Center → Settings → Identity sourcecd terraform
terraform init && terraform applyTerraform provisions all infrastructure, builds the React frontend, and uploads it to S3. The frontend_url output is your dashboard URL. First-time CloudFront creation takes 5-10 minutes.
Trigger the initial crawl to populate the dashboard:
aws stepfunctions start-execution \
--region $(terraform output -raw aws_region) \
--state-machine-arn $(terraform output -raw step_functions_arn)Open the frontend_url and log in with admin / admin123. Data appears within 1-3 minutes. Subsequent crawls run automatically every 6 hours.
For production SSO authentication, see Advanced Setup.
All variables are set in terraform/terraform.tfvars. Required variables are marked with R.
| Variable | Type | Default | Description |
|---|---|---|---|
| Core | |||
resource_prefix R |
string |
— | Prefix for all resource names (3-31 chars, lowercase alphanumeric + hyphens, globally unique for S3) |
sso_instance_arn R |
string |
— | ARN of your IAM Identity Center instance |
identity_store_id R |
string |
— | Identity Store ID associated with the SSO instance |
aws_region |
string |
"us-east-1" |
AWS deployment region |
project_name |
string |
"idc-governance" |
Tag applied to all resources |
environment |
string |
"production" |
Environment tag |
| SSO Authentication | |||
okta_domain |
string |
"" |
Okta domain (e.g. your-org.okta.com) |
okta_client_id |
string |
"" |
Okta OIDC client ID |
azure_tenant_id |
string |
"" |
Azure AD / Entra ID tenant ID |
azure_client_id |
string |
"" |
Azure AD / Entra ID OIDC client ID |
| Local Auth | |||
local_admin_username |
string |
"admin" |
Dashboard login username (when no IdP is configured) |
local_admin_password |
string |
"admin123" |
Dashboard login password (when no IdP is configured) |
local_api_key |
string |
"" |
Backend API key (auto-generated if empty; only used without IdP) |
| Audit Trail | |||
cloudtrail_bucket |
string |
"" |
S3 bucket with Organization CloudTrail logs (enables Audit Trail tab) |
cloudtrail_prefix |
string |
"AWSLogs" |
S3 key prefix for CloudTrail logs |
organization_id |
string |
"" |
AWS Organization ID (required with cloudtrail_bucket) |
management_account_id |
string |
"" |
Management account ID (required with cloudtrail_bucket) |
| Schedule & Lifecycle | |||
crawler_schedule_interval |
string |
"6 hours" |
Crawl frequency (EventBridge rate syntax: "6 hours", "1 day") |
inventory_lifecycle_days |
number |
7 |
Days before raw snapshots auto-delete from S3 |
athena_results_lifecycle_days |
number |
1 |
Days before Athena query results auto-delete |
cache_lifecycle_days |
number |
1 |
Days before cached API responses auto-delete |
log_retention_days |
number |
7 |
CloudWatch Logs retention (days) |
| Cost Controls | |||
worker_reserved_concurrency |
number |
10 |
Max concurrent worker Lambda executions |
athena_proxy_reserved_concurrency |
number |
5 |
Max concurrent API Lambda executions |
force_destroy_buckets |
bool |
false |
Allow terraform destroy to delete non-empty S3 buckets (dev/demo only) |
Fully serverless — you only pay when things run. No fixed costs (no NAT Gateways, no RDS, no Glue Crawlers).
| Service | Role | Cost Driver |
|---|---|---|
| Step Functions | Crawl orchestration | State transitions (biggest cost at scale) |
| Lambda | Crawl + API | Invocation count + duration |
| S3 | Storage | Object count + storage (minimal with lifecycle) |
| Athena | SQL queries | Per TB scanned (typically KB-MB range) |
| CloudFront | CDN + API gateway | Data transfer out (cache hits are free) |
| EventBridge | Scheduler | Negligible (4 invocations/day) |
| CloudWatch | Logs | Retention-based (default: 7 days) |
| Scale | Accounts | Crawls/Day | Estimated Cost |
|---|---|---|---|
| Small | 20 | 4 | ~$0.10 |
| Medium | 100 | 4 | ~$0.50 |
| Large | 500 | 4 | ~$2.75 |
Most small-to-medium deployments fall within the AWS Free Tier. Set
crawler_schedule_interval = "1 day"to crawl once daily and cut Step Functions costs by ~75%.
- Search and filter by principal name, account, or permission set
- Sort by any column header (click to toggle ascending/descending)
- Snapshot selector to browse historical crawl dates
- Access Heatmap shows principal counts per account x permission set matrix
- Export visible assignments to CSV or PDF
- Columns: Name (with AWS Console deep link), Description, Provisioned count, Session Duration, Policies, Boundary, Inline Policy, Tags
- Policy labels: AWS managed policies as blue links; customer managed policies with yellow badge
- Inline Policy: expandable syntax-highlighted JSON viewer
- Resizable columns: drag any column header divider
- Risk Overview: summary cards for Critical, High, Medium, and No Issues categories
- Flagged Permission Sets: all sets above "No Issues", showing matched rules and reasons
- Rule Editor: add, edit, or delete custom risk rules (exact and wildcard matching)
- Default rules based on CIS and Rhino Security research, restorable at any time
- Export risk policy rules to CSV or PDF
- Requires CloudTrail integration — see Advanced Setup
- Date range picker to scope the query (default: last 7 days)
- Category filter for All Changes, Assignment Changes, or Permission Set Changes
- Search by actor name, event type, or IP address
- Expandable details with full CloudTrail request parameters as JSON
- Export events to CSV
The dashboard needs at least one completed crawl. Trigger it manually:
aws stepfunctions start-execution \
--region $(terraform output -raw aws_region) \
--state-machine-arn $(terraform output -raw step_functions_arn)Wait 1-3 minutes, then refresh the dashboard.
Ensure the AWS account where you deploy has IAM Identity Center enabled and the sso_instance_arn and identity_store_id are correct. Check the Worker Lambda CloudWatch logs:
aws logs tail /aws/lambda/<resource_prefix>-worker --followThis is normal during the first deployment — CloudFront takes 5-10 minutes to propagate. If it persists, check that the S3 bucket policy allows CloudFront OAC access (Terraform manages this automatically).
Clear sessionStorage in your browser (DevTools -> Application -> Session Storage -> Clear). This removes stale tokens.
The Athena Proxy Lambda has a 60-second timeout. For very large organisations (1000+ accounts), the first query after a crawl may take longer as Athena scans new partitions. Subsequent queries use the cache and return instantly.
aws stepfunctions list-executions \
--region $(terraform output -raw aws_region) \
--state-machine-arn $(terraform output -raw step_functions_arn) \
--max-results 5Optional. Without an identity provider, the dashboard uses local auth with credentials embedded in the client-side JS bundle. Local auth is a development convenience, not a security boundary. Always configure SSO for production use.
Configure one identity provider at a time.
- Log into your Okta Admin Console
- Go to Applications -> Create App Integration
- Select OIDC - OpenID Connect -> Single-Page Application (SPA)
- Configure:
| Setting | Value |
|---|---|
| App name | IAM Governance Dashboard |
| Grant type | Authorization Code |
| Sign-in redirect URI (dev) | http://localhost:3000/callback |
| Sign-in redirect URI (prod) | https://your-cloudfront-domain.cloudfront.net/callback |
| Sign-out redirect URI (dev) | http://localhost:3000 |
| Sign-out redirect URI (prod) | https://your-cloudfront-domain.cloudfront.net |
- Copy the Client ID, then add to
terraform/terraform.tfvars:
okta_domain = "your-org.okta.com"
okta_client_id = "0oaXXXXXXXXXXXXXXXXX"- Redeploy with
terraform apply.
- Go to Microsoft Entra admin center -> Identity -> Applications -> App registrations -> New registration
- Name it
IAM Governance Dashboard, set Supported account types to Accounts in this organizational directory only (Single tenant), click Register - From the app's Overview page, copy the Application (client) ID and Directory (tenant) ID
- Go to Manage -> Authentication -> Add a platform -> Single-page application
- Set Redirect URIs:
- Dev:
http://localhost:3000/callback - Prod:
https://your-cloudfront-domain.cloudfront.net/callback
- Dev:
- Do not check either checkbox under Implicit grant and hybrid flows (the dashboard uses Authorization Code + PKCE — no client secret required)
- Under API permissions, verify
Microsoft Graph > User.Read(delegated) is present (added by default) - Add to
terraform/terraform.tfvars:
azure_tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
azure_client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"- Redeploy with
terraform apply.
After deploying, add the production callback URL (https://<cloudfront-domain>/callback) to your IdP's redirect URI configuration.
Create frontend/.env with your provider's variables:
# Okta
REACT_APP_OKTA_DOMAIN=your-org.okta.com
REACT_APP_OKTA_CLIENT_ID=0oaXXXXXXXXXXXXXXXXX
# OR Azure AD
REACT_APP_AZURE_TENANT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
REACT_APP_AZURE_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxThe frontend uses Authorization Code + PKCE with auto-detected provider. Tokens are stored in sessionStorage. The backend validates tokens by calling the provider's OIDC userinfo endpoint (with in-memory caching). Tokens are passed via the X-Auth-Token header because CloudFront OAC replaces the Authorization header with its own SigV4 signature.
Optional. The Audit Trail tab shows who assigned/removed permission sets and who created/modified permission set configurations. It also enriches the Assignments and Permission Sets tabs with "Assigned By" and "Created/Updated By" columns. Queries CloudTrail via Athena at zero fixed cost. Attribution data only appears for changes made after the Organization Trail is created.
aws cloudtrail describe-trails --query 'trailList[?IsOrganizationTrail==`true`].[Name,S3BucketName]' --output tableIf you see a trail, skip to Step 3.
Run from the management account. The first management event trail is free.
# Set your management account profile
export AWS_PROFILE=your-management-account-profile
# Get your Organization ID
ORG_ID=$(aws organizations describe-organization --query 'Organization.Id' --output text)
MGMT_ACCOUNT=$(aws sts get-caller-identity --query 'Account' --output text)
BUCKET_NAME="your-org-cloudtrail"
REGION="ap-southeast-1" # Use your preferred region
# 1. Create S3 bucket
aws s3api create-bucket \
--bucket $BUCKET_NAME \
--region $REGION \
--create-bucket-configuration LocationConstraint=$REGION
# 2. Secure the bucket
aws s3api put-public-access-block \
--bucket $BUCKET_NAME \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api put-bucket-encryption \
--bucket $BUCKET_NAME \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"},"BucketKeyEnabled":true}]}'
# 3. Set lifecycle (1 year retention)
aws s3api put-bucket-lifecycle-configuration \
--bucket $BUCKET_NAME \
--lifecycle-configuration \
'{"Rules":[{"ID":"ExpireCloudTrailLogs","Status":"Enabled","Filter":{"Prefix":"AWSLogs/"},"Expiration":{"Days":365}}]}'
# 4. Set bucket policy (CloudTrail write access)
aws s3api put-bucket-policy --bucket $BUCKET_NAME --policy "{
\"Version\": \"2012-10-17\",
\"Statement\": [
{
\"Sid\": \"AWSCloudTrailAclCheck\",
\"Effect\": \"Allow\",
\"Principal\": {\"Service\": \"cloudtrail.amazonaws.com\"},
\"Action\": \"s3:GetBucketAcl\",
\"Resource\": \"arn:aws:s3:::$BUCKET_NAME\",
\"Condition\": {\"StringEquals\": {\"AWS:SourceArn\": \"arn:aws:cloudtrail:$REGION:$MGMT_ACCOUNT:trail/org-management-trail\"}}
},
{
\"Sid\": \"AWSCloudTrailWrite\",
\"Effect\": \"Allow\",
\"Principal\": {\"Service\": \"cloudtrail.amazonaws.com\"},
\"Action\": \"s3:PutObject\",
\"Resource\": \"arn:aws:s3:::$BUCKET_NAME/AWSLogs/$MGMT_ACCOUNT/*\",
\"Condition\": {\"StringEquals\": {\"s3:x-amz-acl\": \"bucket-owner-full-control\", \"AWS:SourceArn\": \"arn:aws:cloudtrail:$REGION:$MGMT_ACCOUNT:trail/org-management-trail\"}}
},
{
\"Sid\": \"AWSCloudTrailOrgWrite\",
\"Effect\": \"Allow\",
\"Principal\": {\"Service\": \"cloudtrail.amazonaws.com\"},
\"Action\": \"s3:PutObject\",
\"Resource\": \"arn:aws:s3:::$BUCKET_NAME/AWSLogs/$ORG_ID/*\",
\"Condition\": {\"StringEquals\": {\"s3:x-amz-acl\": \"bucket-owner-full-control\", \"AWS:SourceArn\": \"arn:aws:cloudtrail:$REGION:$MGMT_ACCOUNT:trail/org-management-trail\"}}
}
]
}"
# 5. Enable CloudTrail service access for Organizations
aws organizations enable-aws-service-access --service-principal cloudtrail.amazonaws.com
# 6. Create the Organization Trail
aws cloudtrail create-trail \
--name org-management-trail \
--s3-bucket-name $BUCKET_NAME \
--is-organization-trail \
--is-multi-region-trail \
--region $REGION
# 7. Start logging
aws cloudtrail start-logging --name org-management-trail --region $REGIONIf the dashboard is deployed in a delegated admin account (not the management account), add a read policy to the CloudTrail bucket so the dashboard Lambda can query the logs.
Run from the management account — add this statement to the existing bucket policy:
{
"Sid": "AllowDashboardLambdaRead",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<delegated-admin-account-id>:role/<resource-prefix>-athena-proxy-lambda-role"
},
"Action": [
"s3:GetObject",
"s3:ListBucket",
"s3:GetBucketLocation"
],
"Resource": [
"arn:aws:s3:::<cloudtrail-bucket>",
"arn:aws:s3:::<cloudtrail-bucket>/*"
]
}Add to terraform/terraform.tfvars:
cloudtrail_bucket = "your-org-cloudtrail"
organization_id = "o-xxxxxxxxxx"
management_account_id = "123456789012"Find these values:
# Organization ID
aws organizations describe-organization --query 'Organization.Id' --output text
# Management account ID
aws organizations describe-organization --query 'Organization.MasterAccountId' --output textRedeploy with terraform apply. The Audit Trail tab appears automatically.
How it works: CloudTrail logs every IAM Identity Center API call with actor attribution. Logs arrive in S3 within 5-15 minutes. The dashboard queries them via Athena. Only SSO-related events are queried (eventsource = sso.amazonaws.com). Cost: first management trail is free; Athena queries cost ~$0.001-0.01 each.
| Action | Why | How |
|---|---|---|
| Configure SSO | Local auth credentials are in the JS bundle | See SSO Authentication |
| Attach AWS WAFv2 | DDoS, bots, OWASP Top 10 protection | Create aws_wafv2_web_acl with AWS Managed Rules, associate with CloudFront |
| Custom domain + TLS | Replace *.cloudfront.net |
ACM certificate in us-east-1 + Route 53 alias + viewer_certificate block |
| Geo-restriction | Limit access to operating regions | restrictions.geo_restriction in CloudFront |
| Security headers | HSTS, X-Frame-Options, CSP | aws_cloudfront_response_headers_policy with security headers |
| Restrict CORS | Lock API to your domain | Update CORS headers in Athena Proxy handler |
flowchart TD
user(["Admin / Auditor"])
okta(["Okta IdP"])
subgraph presentation ["Presentation & API Layer"]
CF["CloudFront + S3\nReact UI"]
OAC["CloudFront OAC\nAWS_IAM Auth"]
Proxy["Athena Proxy Lambda\nFunction URL"]
end
subgraph datalake ["Analytics & Data Lake"]
Athena["Amazon Athena\nSQL Engine"]
Glue["Glue Catalog\nPartition Projection"]
Cache["S3 Cache Bucket\nsummary.json"]
S3["S3 Inventory Bucket\nCSV + JSON"]
end
subgraph pipeline ["Ingestion & Crawl Pipeline"]
EB["EventBridge Scheduler\nrate(6 hours)"]
SFN["Step Functions\nDistributed Map"]
Workers["Worker Lambdas\n1 per Account"]
end
subgraph org ["AWS Organization"]
IAMOrg["AWS Organizations"]
IDC["IAM Identity Center"]
end
user --> okta
okta --> CF
user --> CF
CF --> OAC
OAC --> Proxy
Proxy --> Cache
Proxy --> Athena
Athena --> Glue
Glue --> S3
EB --> SFN
SFN --> Workers
Workers --> S3
Workers --> IAMOrg
Workers --> IDC
Data flow:
- Crawl — EventBridge triggers Step Functions on a schedule. Worker Lambdas crawl each AWS account's assignments in parallel via Distributed Map, then crawl all permission set details. Results are written to S3 as Hive-partitioned CSV and JSON.
- Query — Athena queries the data via Glue Catalog (partition projection, no Glue Crawlers). The Athena Proxy Lambda serves a cached
summary.jsonfrom S3 first, falling back to Athena SQL on cache miss. - Display — The React SPA is served from S3 via CloudFront. API requests go through CloudFront's
/api*path behaviour to the Athena Proxy Lambda Function URL (secured with CloudFront OAC + AWS_IAM auth).
Built-in security controls:
| Control | Detail |
|---|---|
| S3 Encryption | AES-256 server-side encryption with bucket keys on all buckets |
| Public Access Blocked | All S3 buckets block public ACLs, policies, and restrict public access |
| CloudFront OAC | Origin Access Control secures both S3 and Lambda Function URL origins with SigV4 |
| AWS_IAM Auth | Lambda Function URL requires IAM authorization — no anonymous access |
| OIDC Token Validation | Backend validates tokens against the configured IdP's userinfo endpoint |
| SQL Injection Prevention | Table names validated with regex ^[a-zA-Z_][a-zA-Z0-9_]*$ |
| Query Allowlist | API only accepts predefined query types |
| Least-Privilege IAM | All Lambda roles scoped to exactly the required actions and resources |
| Auto-Expiry Lifecycle | S3 objects auto-delete (inventory: 7d, results: 1d, cache: 1d) |
| Reserved Concurrency | Lambda concurrency limits control blast radius and cost |
| PKCE Flow | Authorization Code + PKCE (no client secret in browser) |
| Session Storage | Auth tokens in sessionStorage (cleared on tab close) |
These are the exact IAM permissions the deployed tool uses at runtime (not what the deployer needs). All roles follow least privilege.
Worker Lambda — crawls assignments and permission sets:
sso:ListAccountAssignments sso:ListPermissionSets
sso:DescribePermissionSet sso:ListAccountsForProvisionedPermissionSet
sso:ListManagedPoliciesInPermissionSet
sso:GetInlinePolicyForPermissionSet
sso:ListCustomerManagedPolicyReferencesInPermissionSet
sso:GetPermissionsBoundaryForPermissionSet
sso:ListTagsForResource
identitystore:DescribeUser identitystore:DescribeGroup
identitystore:ListGroupMemberships
organizations:ListAccounts organizations:DescribeAccount
s3:PutObject s3:GetObject (inventory bucket only)
logs:CreateLogGroup/Stream logs:PutLogEvents
Athena Proxy Lambda — serves API, queries Athena, manages cache:
athena:StartQueryExecution athena:GetQueryExecution
athena:GetQueryResults athena:StopQueryExecution
glue:GetTable glue:GetDatabase glue:GetPartitions
glue:GetDatabases glue:GetTables
s3:GetObject s3:PutObject s3:ListBucket s3:GetBucketLocation
(inventory, athena-results, and cache buckets)
logs:CreateLogGroup/Stream logs:PutLogEvents
Step Functions — orchestrates the 3-phase crawl:
lambda:InvokeFunction (worker Lambda only)
organizations:ListAccounts
states:StartExecution states:DescribeExecution states:StopExecution
logs:CreateLogDelivery logs:PutResourcePolicy logs:DescribeLogGroups
Deployer permissions — the IAM principal running terraform apply needs permissions to create:
| Service | Resources Created |
|---|---|
| S3 | 4 buckets (inventory, cache, athena-results, frontend) |
| Lambda | 2 functions (worker, athena-proxy) + Function URL |
| IAM | 4 roles + policies (worker, proxy, step-functions, scheduler) |
| Step Functions | 1 state machine (Standard type) |
| Athena | 1 workgroup |
| Glue | 1 database + 2 tables (catalog only, no crawlers) |
| CloudFront | 1 distribution + OAC |
| EventBridge | 1 scheduler rule |
| CloudWatch Logs | 3 log groups |
Assignments Table (CSV, Hive-partitioned at s3://{inventory_bucket}/assignments/snapshot_date=YYYY-MM-DD/{account_id}.csv):
| Column | Type | Description |
|---|---|---|
account_id |
string | AWS account ID |
account_name |
string | AWS account name |
principal_type |
string | USER, GROUP, or USER_VIA_GROUP |
principal_id |
string | Identity Store principal ID |
principal_name |
string | Resolved display name |
principal_email |
string | Email address (empty for groups) |
permission_set_name |
string | Permission set name |
permission_set_arn |
string | Permission set ARN |
group_name |
string | Group name (if user is assigned via group) |
created_date |
string | Assignment creation date |
Permission Sets Table (JSON, Hive-partitioned at s3://{inventory_bucket}/permission_sets/snapshot_date=YYYY-MM-DD/permission_sets.json):
| Field | Type | Description |
|---|---|---|
name |
string | Permission set name |
arn |
string | Permission set ARN |
description |
string | Description |
session_duration |
string | ISO 8601 duration (e.g. PT4H) |
created_date |
string | Creation timestamp |
aws_managed_policies |
array | List of {name, arn} objects |
customer_managed_policies |
array | List of {name, path} objects |
inline_policy |
string | JSON policy document (stringified) |
permissions_boundary |
object | Boundary policy reference (or null) |
tags |
array | List of {Key, Value} objects |
provisioned_accounts |
number | Number of accounts this set is provisioned to |
Both tables use Glue partition projection — no Glue Crawlers needed, partitions are auto-discovered.
Base URL: https://{cloudfront_domain}/api
All requests require the X-Auth-Token header when SSO is configured.
| Parameter | Required | Description |
|---|---|---|
type |
Yes | Query type (see below) |
date |
No | Snapshot date YYYY-MM-DD (defaults to latest) |
force |
No | Set to true to bypass cache and query Athena directly |
| Type | Method | Description |
|---|---|---|
all |
GET | All assignments for a snapshot date |
summary |
GET | Aggregated stats (cached) |
dates |
GET | Available assignment snapshot dates |
permission_sets |
GET | All permission sets for a snapshot date |
permission_sets_dates |
GET | Available permission set snapshot dates |
risk_policies |
GET | Current risk policy rules (custom or defaults) |
save_risk_policies |
POST | Save custom risk policy rules (JSON body) |
All responses return JSON with CORS headers. Error responses return {"error": "message"}. Cache TTL is 1 hour; use force=true to bypass.
aws-iam-identity-center-security-governance-dashboard/
├── terraform/ # Infrastructure as Code
│ ├── main.tf # Provider & backend configuration
│ ├── variables.tf # All configurable input variables
│ ├── outputs.tf # Terraform outputs (URLs, ARNs, bucket names)
│ ├── s3.tf # S3 buckets (inventory, cache, athena-results)
│ ├── frontend_hosting.tf # S3 + CloudFront + OAC (auto-builds & deploys frontend)
│ ├── lambda.tf # Lambda functions (worker + athena proxy) + Function URL
│ ├── iam.tf # IAM roles & policies (least privilege)
│ ├── athena.tf # Athena workgroup + Glue catalog (partition projection)
│ ├── stepfunctions.tf # Step Functions state machine (3-phase crawler)
│ └── eventbridge.tf # EventBridge scheduled trigger
├── backend/
│ ├── worker/ # Crawler Lambda (Python 3.12, ARM64)
│ │ ├── handler.py # Lambda handler (list accounts, crawl assignments, crawl permission sets)
│ │ └── default_risk_policies.py # Default risk scoring rules
│ └── athena_proxy/ # API Lambda (Python 3.12, ARM64)
│ ├── handler.py # Lambda handler (query, cache, token validation)
│ └── default_risk_policies.py # Default risk scoring rules (must stay in sync with worker/)
├── frontend/ # React 18 SPA (plain JavaScript, no TypeScript)
│ ├── public/
│ │ └── index.html # HTML shell with FOUC-prevention theme script
│ └── src/
│ ├── App.js # Main app (tabs, data fetching, demo data fallback)
│ ├── index.css # Design system (CSS custom properties, light + dark themes)
│ ├── auth/
│ │ └── AuthContext.js # Multi-IdP OIDC (Okta/Azure AD) + PKCE + local auth fallback
│ ├── hooks/
│ │ └── useTheme.js # Dark/light mode hook (localStorage + OS preference)
│ └── components/
│ ├── Header.js # Top navigation + theme toggle
│ ├── LoginPage.js # Authentication page
│ ├── Dashboard.js # Assignments tab (table, toolbar, stats)
│ ├── GovernanceCharts.js # Visualisations (bar charts, access heatmap)
│ ├── PermissionSetsTable.js # Permission sets tab (resizable, expandable)
│ └── SecurityTab.js # Security risk tab (scoring, rule editor)
├── terraform.tfvars.example # Configuration template — copy to terraform/terraform.tfvars
└── README.md
Contributions are welcome. See CLAUDE.md for code conventions, architecture details, and common commands.
- Fork the repository
- Create a feature branch:
git checkout -b feature/your-feature - Commit your changes:
git commit -m 'feat: add your feature' - Push:
git push origin feature/your-feature - Open a Pull Request
Please open an issue with:
- A clear description of the problem
- Steps to reproduce
- Expected vs actual behaviour
- Relevant logs or Terraform output
To remove all deployed resources:
- Add to
terraform/terraform.tfvars:
force_destroy_buckets = true- Apply the change, then destroy:
cd terraform
terraform apply # Applies the force_destroy flag
terraform destroy # Removes all resourcesCloudFront distributions can take 10-15 minutes to fully delete. Terraform waits for this automatically.
MIT — free to use, modify, and distribute.
