Skip to content

Instantly share code, notes, and snippets.

@RajChowdhury240
Created February 25, 2026 13:29
Show Gist options
  • Select an option

  • Save RajChowdhury240/2c5326d8f6295f608755bfebff6b9ee4 to your computer and use it in GitHub Desktop.

Select an option

Save RajChowdhury240/2c5326d8f6295f608755bfebff6b9ee4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
"""
AWS Identity Center (SSO) Comprehensive Report Generator
Enumerates users, groups, memberships, permission sets, and account assignments.
Outputs a modern, interactive HTML dashboard.
"""
import boto3
import json
import html
import sys
from datetime import datetime, timezone
from collections import defaultdict
# ─── AWS API Helpers ────────────────────────────────────────────────────────
def paginate(client, method, result_key, **kwargs):
"""Generic paginator for AWS APIs."""
items = []
paginator = client.get_paginator(method)
for page in paginator.paginate(**kwargs):
items.extend(page.get(result_key, []))
return items
def get_sso_instance(sso_admin):
"""Discover the SSO instance ARN and Identity Store ID."""
resp = sso_admin.list_instances()
instances = resp.get("Instances", [])
if not instances:
print("ERROR: No AWS Identity Center instance found in this account/region.")
sys.exit(1)
inst = instances[0]
return inst["InstanceArn"], inst["IdentityStoreId"]
# ─── Data Collection ────────────────────────────────────────────────────────
def collect_users(identity_store, store_id):
"""List all users in the identity store."""
print(" [*] Listing users...")
users = paginate(identity_store, "list_users", "Users", IdentityStoreId=store_id)
print(f" Found {len(users)} users")
return users
def collect_groups(identity_store, store_id):
"""List all groups in the identity store."""
print(" [*] Listing groups...")
groups = paginate(identity_store, "list_groups", "Groups", IdentityStoreId=store_id)
print(f" Found {len(groups)} groups")
return groups
def collect_group_memberships(identity_store, store_id, groups, user_map):
"""For each group, list its members."""
print(" [*] Collecting group memberships...")
memberships = {}
for g in groups:
gid = g["GroupId"]
members = paginate(
identity_store, "list_group_memberships", "GroupMemberships",
IdentityStoreId=store_id, GroupId=gid
)
member_list = []
for m in members:
member_id_obj = m.get("MemberId", {})
uid = member_id_obj.get("UserId")
if uid and uid in user_map:
member_list.append(uid)
memberships[gid] = member_list
total = sum(len(v) for v in memberships.values())
print(f" Found {total} total memberships across {len(memberships)} groups")
return memberships
def collect_permission_sets(sso_admin, instance_arn):
"""List all permission sets and describe each one."""
print(" [*] Listing permission sets...")
ps_arns = paginate(
sso_admin, "list_permission_sets", "PermissionSets",
InstanceArn=instance_arn
)
permission_sets = {}
for arn in ps_arns:
desc = sso_admin.describe_permission_set(
InstanceArn=instance_arn, PermissionSetArn=arn
).get("PermissionSet", {})
# Get inline policy
try:
inline = sso_admin.get_inline_policy_for_permission_set(
InstanceArn=instance_arn, PermissionSetArn=arn
).get("InlinePolicy", "")
except Exception:
inline = ""
# Get managed policies
try:
managed = paginate(
sso_admin, "list_managed_policies_in_permission_set",
"AttachedManagedPolicies",
InstanceArn=instance_arn, PermissionSetArn=arn
)
except Exception:
managed = []
# Get customer managed policies
try:
customer_managed = paginate(
sso_admin, "list_customer_managed_policy_references_in_permission_set",
"CustomerManagedPolicyReferences",
InstanceArn=instance_arn, PermissionSetArn=arn
)
except Exception:
customer_managed = []
# Get permission boundary
try:
boundary = sso_admin.get_permissions_boundary_for_permission_set(
InstanceArn=instance_arn, PermissionSetArn=arn
).get("PermissionsBoundary", {})
except Exception:
boundary = {}
permission_sets[arn] = {
"details": desc,
"inline_policy": inline,
"managed_policies": managed,
"customer_managed_policies": customer_managed,
"permissions_boundary": boundary,
}
print(f" Found {len(permission_sets)} permission sets")
return permission_sets
def collect_account_assignments(sso_admin, instance_arn, permission_sets, account_map):
"""For each account + permission set, list all assignments."""
print(" [*] Collecting account assignments (this may take a while)...")
assignments = []
account_ids = list(account_map.keys())
ps_arns = list(permission_sets.keys())
total_combos = len(account_ids) * len(ps_arns)
done = 0
for acct_id in account_ids:
for ps_arn in ps_arns:
done += 1
if done % 20 == 0 or done == total_combos:
print(f" Progress: {done}/{total_combos} combinations checked...")
try:
acct_assignments = paginate(
sso_admin,
"list_account_assignments",
"AccountAssignments",
InstanceArn=instance_arn,
AccountId=acct_id,
PermissionSetArn=ps_arn
)
for a in acct_assignments:
a["PermissionSetArn"] = ps_arn
assignments.append(a)
except Exception as e:
print(f" Warning: Could not list assignments for {acct_id}/{ps_arn}: {e}")
print(f" Found {len(assignments)} total assignments")
return assignments
def collect_accounts(org_client):
"""List all AWS accounts in the organization."""
print(" [*] Listing AWS accounts...")
try:
accounts = paginate(org_client, "list_accounts", "Accounts")
print(f" Found {len(accounts)} accounts")
return {a["Id"]: a for a in accounts}
except Exception as e:
print(f" Warning: Could not list org accounts ({e}). Account names may be unavailable.")
return {}
# ─── HTML Report ────────────────────────────────────────────────────────────
def esc(text):
"""HTML-escape a string."""
if text is None:
return ""
return html.escape(str(text))
def generate_html(users, groups, memberships, permission_sets, assignments, account_map, user_map, group_map):
"""Generate the full interactive HTML report."""
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
# Pre-process data for the report
# Build user -> groups mapping
user_groups = defaultdict(list)
for gid, member_uids in memberships.items():
for uid in member_uids:
user_groups[uid].append(gid)
# Build structured assignments
# assignment_by_account[account_id] = [ {principal, principal_type, permission_set_name, ps_arn} ]
assignment_by_account = defaultdict(list)
assignment_by_principal = defaultdict(list)
for a in assignments:
acct = a.get("AccountId", "")
pid = a.get("PrincipalId", "")
ptype = a.get("PrincipalType", "")
ps_arn = a.get("PermissionSetArn", "")
ps_name = permission_sets.get(ps_arn, {}).get("details", {}).get("Name", ps_arn.split("/")[-1])
if ptype == "USER":
principal_name = user_map.get(pid, {}).get("UserName", pid)
elif ptype == "GROUP":
principal_name = group_map.get(pid, {}).get("DisplayName", pid)
else:
principal_name = pid
entry = {
"account_id": acct,
"account_name": account_map.get(acct, {}).get("Name", acct),
"principal_id": pid,
"principal_name": principal_name,
"principal_type": ptype,
"permission_set_name": ps_name,
"permission_set_arn": ps_arn,
}
assignment_by_account[acct].append(entry)
assignment_by_principal[pid].append(entry)
# Stats
total_users = len(users)
total_groups = len(groups)
total_ps = len(permission_sets)
total_accounts = len(account_map)
total_assignments = len(assignments)
# ── Build HTML ──
parts = []
parts.append(f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AWS Identity Center Report</title>
<style>
:root {{
--bg: #0f1117;
--surface: #161b22;
--surface2: #1c2129;
--border: #30363d;
--text: #e6edf3;
--text-muted: #8b949e;
--accent: #58a6ff;
--accent2: #3fb950;
--accent3: #d2a8ff;
--accent4: #f0883e;
--accent5: #f778ba;
--danger: #f85149;
--badge-user: #1f3a5f;
--badge-group: #1a3828;
--badge-ps: #2d1f4e;
--badge-acct: #3d2b10;
}}
* {{ margin:0; padding:0; box-sizing:border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}}
.container {{ max-width: 1400px; margin: 0 auto; padding: 20px; }}
/* Header */
.header {{
background: linear-gradient(135deg, #0d1117 0%, #161b22 50%, #1a1e2e 100%);
border: 1px solid var(--border);
border-radius: 12px;
padding: 32px;
margin-bottom: 24px;
}}
.header h1 {{
font-size: 28px;
font-weight: 700;
background: linear-gradient(90deg, var(--accent), var(--accent3));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 8px;
}}
.header .meta {{ color: var(--text-muted); font-size: 14px; }}
/* Stats bar */
.stats {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}}
.stat-card {{
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 20px;
text-align: center;
transition: transform 0.15s, border-color 0.15s;
}}
.stat-card:hover {{ transform: translateY(-2px); border-color: var(--accent); }}
.stat-card .number {{ font-size: 36px; font-weight: 700; }}
.stat-card .label {{ font-size: 13px; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; margin-top: 4px; }}
.stat-card:nth-child(1) .number {{ color: var(--accent); }}
.stat-card:nth-child(2) .number {{ color: var(--accent2); }}
.stat-card:nth-child(3) .number {{ color: var(--accent3); }}
.stat-card:nth-child(4) .number {{ color: var(--accent4); }}
.stat-card:nth-child(5) .number {{ color: var(--accent5); }}
/* Tabs */
.tabs {{
display: flex;
gap: 4px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 6px;
margin-bottom: 24px;
overflow-x: auto;
}}
.tab {{
padding: 10px 20px;
border: none;
background: transparent;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
cursor: pointer;
border-radius: 8px;
transition: all 0.15s;
white-space: nowrap;
}}
.tab:hover {{ color: var(--text); background: var(--surface2); }}
.tab.active {{ color: var(--text); background: var(--accent); color: #fff; }}
.tab-content {{ display: none; }}
.tab-content.active {{ display: block; }}
/* Search */
.search-bar {{
width: 100%;
padding: 12px 16px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 14px;
margin-bottom: 16px;
outline: none;
transition: border-color 0.15s;
}}
.search-bar:focus {{ border-color: var(--accent); }}
.search-bar::placeholder {{ color: var(--text-muted); }}
/* Tables */
.table-wrap {{
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
}}
table {{
width: 100%;
border-collapse: collapse;
font-size: 14px;
}}
thead th {{
background: var(--surface2);
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
font-size: 12px;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--border);
cursor: pointer;
user-select: none;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 1;
}}
thead th:hover {{ color: var(--accent); }}
thead th .sort-icon {{ margin-left: 4px; opacity: 0.4; }}
thead th.sorted .sort-icon {{ opacity: 1; color: var(--accent); }}
tbody td {{
padding: 10px 16px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}}
tbody tr {{ transition: background 0.1s; }}
tbody tr:hover {{ background: var(--surface2); }}
tbody tr:last-child td {{ border-bottom: none; }}
/* Badges */
.badge {{
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
margin: 2px 4px 2px 0;
white-space: nowrap;
}}
.badge-user {{ background: var(--badge-user); color: var(--accent); }}
.badge-group {{ background: var(--badge-group); color: var(--accent2); }}
.badge-ps {{ background: var(--badge-ps); color: var(--accent3); }}
.badge-acct {{ background: var(--badge-acct); color: var(--accent4); }}
.badge-type {{ background: #2d333b; color: var(--text-muted); }}
/* Expandable rows */
.expand-btn {{
background: none;
border: 1px solid var(--border);
color: var(--accent);
padding: 4px 10px;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
transition: all 0.15s;
}}
.expand-btn:hover {{ background: var(--accent); color: #fff; }}
.detail-row {{ display: none; }}
.detail-row.show {{ display: table-row; }}
.detail-row td {{
background: var(--surface2);
padding: 16px 24px;
}}
.detail-content {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 16px;
}}
.detail-section h4 {{
font-size: 13px;
color: var(--accent);
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
.detail-section pre {{
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 12px;
font-size: 12px;
overflow-x: auto;
max-height: 300px;
overflow-y: auto;
color: var(--text-muted);
}}
.detail-section ul {{ list-style: none; }}
.detail-section ul li {{
padding: 4px 0;
font-size: 13px;
color: var(--text-muted);
}}
.detail-section ul li::before {{
content: "→ ";
color: var(--accent);
}}
/* No results */
.no-results {{
text-align: center;
padding: 40px;
color: var(--text-muted);
font-size: 14px;
}}
/* Scrollable table container */
.table-scroll {{
max-height: 70vh;
overflow-y: auto;
}}
.table-scroll::-webkit-scrollbar {{ width: 8px; }}
.table-scroll::-webkit-scrollbar-track {{ background: var(--surface); }}
.table-scroll::-webkit-scrollbar-thumb {{ background: var(--border); border-radius: 4px; }}
.table-scroll::-webkit-scrollbar-thumb:hover {{ background: var(--text-muted); }}
/* Count badge */
.count {{ color: var(--text-muted); font-size: 12px; margin-left: 4px; }}
/* Tooltip */
.tip {{
position: relative;
cursor: help;
border-bottom: 1px dotted var(--text-muted);
}}
.tip:hover::after {{
content: attr(data-tip);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #000;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
z-index: 10;
}}
@media (max-width: 768px) {{
.container {{ padding: 12px; }}
.header {{ padding: 20px; }}
.header h1 {{ font-size: 22px; }}
.stats {{ grid-template-columns: repeat(2, 1fr); }}
.tabs {{ flex-wrap: wrap; }}
table {{ font-size: 13px; }}
thead th, tbody td {{ padding: 8px 10px; }}
}}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>AWS Identity Center Report</h1>
<div class="meta">Generated: {now}</div>
</div>
<div class="stats">
<div class="stat-card"><div class="number">{total_users}</div><div class="label">Users</div></div>
<div class="stat-card"><div class="number">{total_groups}</div><div class="label">Groups</div></div>
<div class="stat-card"><div class="number">{total_ps}</div><div class="label">Permission Sets</div></div>
<div class="stat-card"><div class="number">{total_accounts}</div><div class="label">Accounts</div></div>
<div class="stat-card"><div class="number">{total_assignments}</div><div class="label">Assignments</div></div>
</div>
<div class="tabs">
<button class="tab active" data-tab="tab-users">Users</button>
<button class="tab" data-tab="tab-groups">Groups</button>
<button class="tab" data-tab="tab-permsets">Permission Sets</button>
<button class="tab" data-tab="tab-accounts">Accounts</button>
<button class="tab" data-tab="tab-assignments">All Assignments</button>
<button class="tab" data-tab="tab-matrix">Access Matrix</button>
</div>
""")
# ── TAB: Users ──
parts.append("""
<div id="tab-users" class="tab-content active">
<input class="search-bar" type="text" placeholder="Search users by name, email, ID..." onkeyup="filterTable(this, 'users-table')">
<div class="table-wrap"><div class="table-scroll">
<table id="users-table">
<thead><tr>
<th onclick="sortTable('users-table',0)">Username <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('users-table',1)">Display Name <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('users-table',2)">Email <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('users-table',3)">Groups <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('users-table',4)">Account Access <span class="sort-icon">⇅</span></th>
<th>Details</th>
</tr></thead>
<tbody>
""")
for u in sorted(users, key=lambda x: x.get("UserName", "")):
uid = u["UserId"]
uname = esc(u.get("UserName", ""))
display = esc(u.get("DisplayName", ""))
emails = u.get("Emails", [])
email = esc(emails[0].get("Value", "")) if emails else ""
grps = user_groups.get(uid, [])
grp_badges = "".join(
f'<span class="badge badge-group">{esc(group_map.get(gid, {}).get("DisplayName", gid))}</span>'
for gid in grps
) or '<span style="color:var(--text-muted)">None</span>'
acct_access = assignment_by_principal.get(uid, [])
# Also include access via groups
for gid in grps:
acct_access.extend(assignment_by_principal.get(gid, []))
unique_accts = list({a["account_id"] for a in acct_access})
acct_badges = "".join(
f'<span class="badge badge-acct">{esc(account_map.get(aid, {}).get("Name", aid))}</span>'
for aid in sorted(unique_accts)
) or '<span style="color:var(--text-muted)">None</span>'
row_id = f"user-{uid}"
parts.append(f"""<tr>
<td><strong>{uname}</strong></td>
<td>{display}</td>
<td>{email}</td>
<td>{grp_badges}</td>
<td>{acct_badges}</td>
<td><button class="expand-btn" onclick="toggleDetail('{row_id}')">Details</button></td>
</tr>
<tr class="detail-row" id="{row_id}"><td colspan="6"><div class="detail-content">
<div class="detail-section"><h4>User Info</h4><ul>
<li>User ID: {esc(uid)}</li>
<li>Username: {uname}</li>
<li>Display Name: {display}</li>
<li>Email: {email}</li>
</ul></div>
<div class="detail-section"><h4>Direct & Inherited Access</h4><ul>
""")
if acct_access:
seen = set()
for a in acct_access:
key = (a["account_id"], a["permission_set_name"], a["principal_name"])
if key in seen:
continue
seen.add(key)
parts.append(f' <li>{esc(a["account_name"])} — <span class="badge badge-ps">{esc(a["permission_set_name"])}</span> (via {esc(a["principal_type"].lower())} {esc(a["principal_name"])})</li>\n')
else:
parts.append(" <li>No account access</li>\n")
parts.append(" </ul></div>\n</div></td></tr>\n")
parts.append("</tbody></table></div></div></div>\n")
# ── TAB: Groups ──
parts.append("""
<div id="tab-groups" class="tab-content">
<input class="search-bar" type="text" placeholder="Search groups..." onkeyup="filterTable(this, 'groups-table')">
<div class="table-wrap"><div class="table-scroll">
<table id="groups-table">
<thead><tr>
<th onclick="sortTable('groups-table',0)">Group Name <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('groups-table',1)">Description <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('groups-table',2)">Members <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('groups-table',3)">Account Access <span class="sort-icon">⇅</span></th>
<th>Details</th>
</tr></thead>
<tbody>
""")
for g in sorted(groups, key=lambda x: x.get("DisplayName", "")):
gid = g["GroupId"]
gname = esc(g.get("DisplayName", ""))
gdesc = esc(g.get("Description", ""))
members = memberships.get(gid, [])
member_badges = "".join(
f'<span class="badge badge-user">{esc(user_map.get(uid, {}).get("UserName", uid))}</span>'
for uid in members
) or '<span style="color:var(--text-muted)">None</span>'
grp_access = assignment_by_principal.get(gid, [])
acct_badges = "".join(
f'<span class="badge badge-acct">{esc(a["account_name"])}</span> <span class="badge badge-ps">{esc(a["permission_set_name"])}</span> '
for a in grp_access
) or '<span style="color:var(--text-muted)">None</span>'
row_id = f"group-{gid}"
parts.append(f"""<tr>
<td><strong>{gname}</strong></td>
<td>{gdesc}</td>
<td>{member_badges} <span class="count">({len(members)})</span></td>
<td>{acct_badges}</td>
<td><button class="expand-btn" onclick="toggleDetail('{row_id}')">Details</button></td>
</tr>
<tr class="detail-row" id="{row_id}"><td colspan="5"><div class="detail-content">
<div class="detail-section"><h4>Members ({len(members)})</h4><ul>
""")
for uid in members:
u = user_map.get(uid, {})
parts.append(f' <li>{esc(u.get("UserName", uid))} — {esc(u.get("DisplayName", ""))}</li>\n')
if not members:
parts.append(" <li>No members</li>\n")
parts.append(f""" </ul></div>
<div class="detail-section"><h4>Account Assignments</h4><ul>
""")
for a in grp_access:
parts.append(f' <li>{esc(a["account_name"])} ({esc(a["account_id"])}) — {esc(a["permission_set_name"])}</li>\n')
if not grp_access:
parts.append(" <li>No assignments</li>\n")
parts.append(" </ul></div>\n</div></td></tr>\n")
parts.append("</tbody></table></div></div></div>\n")
# ── TAB: Permission Sets ──
parts.append("""
<div id="tab-permsets" class="tab-content">
<input class="search-bar" type="text" placeholder="Search permission sets..." onkeyup="filterTable(this, 'ps-table')">
<div class="table-wrap"><div class="table-scroll">
<table id="ps-table">
<thead><tr>
<th onclick="sortTable('ps-table',0)">Name <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('ps-table',1)">Description <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('ps-table',2)">Session Duration <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('ps-table',3)">Managed Policies <span class="sort-icon">⇅</span></th>
<th>Details</th>
</tr></thead>
<tbody>
""")
for ps_arn, ps_data in sorted(permission_sets.items(), key=lambda x: x[1]["details"].get("Name", "")):
d = ps_data["details"]
name = esc(d.get("Name", ""))
desc = esc(d.get("Description", ""))
duration = d.get("SessionDuration", "")
managed = ps_data["managed_policies"]
managed_badges = "".join(
f'<span class="badge badge-type">{esc(p.get("Name", p.get("Arn", "")))}</span>'
for p in managed
) or '<span style="color:var(--text-muted)">None</span>'
row_id = f"ps-{name}"
inline = ps_data["inline_policy"]
customer = ps_data["customer_managed_policies"]
boundary = ps_data["permissions_boundary"]
parts.append(f"""<tr>
<td><strong><span class="badge badge-ps">{name}</span></strong></td>
<td>{desc}</td>
<td>{esc(duration)}</td>
<td>{managed_badges}</td>
<td><button class="expand-btn" onclick="toggleDetail('{row_id}')">Details</button></td>
</tr>
<tr class="detail-row" id="{row_id}"><td colspan="5"><div class="detail-content">
<div class="detail-section"><h4>ARN</h4><pre>{esc(ps_arn)}</pre></div>
""")
if inline:
try:
formatted = json.dumps(json.loads(inline), indent=2)
except Exception:
formatted = inline
parts.append(f' <div class="detail-section"><h4>Inline Policy</h4><pre>{esc(formatted)}</pre></div>\n')
if managed:
parts.append(' <div class="detail-section"><h4>AWS Managed Policies</h4><ul>\n')
for p in managed:
parts.append(f' <li>{esc(p.get("Name", ""))} — {esc(p.get("Arn", ""))}</li>\n')
parts.append(" </ul></div>\n")
if customer:
parts.append(' <div class="detail-section"><h4>Customer Managed Policies</h4><ul>\n')
for p in customer:
parts.append(f' <li>{esc(p.get("Name", ""))} (path: {esc(p.get("Path", "/"))})</li>\n')
parts.append(" </ul></div>\n")
if boundary:
parts.append(f' <div class="detail-section"><h4>Permissions Boundary</h4><pre>{esc(json.dumps(boundary, indent=2, default=str))}</pre></div>\n')
parts.append("</div></td></tr>\n")
parts.append("</tbody></table></div></div></div>\n")
# ── TAB: Accounts ──
parts.append("""
<div id="tab-accounts" class="tab-content">
<input class="search-bar" type="text" placeholder="Search accounts..." onkeyup="filterTable(this, 'accounts-table')">
<div class="table-wrap"><div class="table-scroll">
<table id="accounts-table">
<thead><tr>
<th onclick="sortTable('accounts-table',0)">Account ID <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('accounts-table',1)">Account Name <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('accounts-table',2)">Email <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('accounts-table',3)">Status <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('accounts-table',4)">Assignments <span class="sort-icon">⇅</span></th>
<th>Details</th>
</tr></thead>
<tbody>
""")
for acct_id, acct in sorted(account_map.items(), key=lambda x: x[1].get("Name", "")):
aname = esc(acct.get("Name", ""))
aemail = esc(acct.get("Email", ""))
astatus = esc(acct.get("Status", ""))
acct_assigns = assignment_by_account.get(acct_id, [])
row_id = f"acct-{acct_id}"
parts.append(f"""<tr>
<td><code>{esc(acct_id)}</code></td>
<td><strong>{aname}</strong></td>
<td>{aemail}</td>
<td>{astatus}</td>
<td><span class="count">{len(acct_assigns)} assignments</span></td>
<td><button class="expand-btn" onclick="toggleDetail('{row_id}')">Details</button></td>
</tr>
<tr class="detail-row" id="{row_id}"><td colspan="6"><div class="detail-content">
<div class="detail-section"><h4>Principals with Access</h4><ul>
""")
for a in acct_assigns:
ptype_badge = "badge-user" if a["principal_type"] == "USER" else "badge-group"
parts.append(
f' <li><span class="badge {ptype_badge}">{esc(a["principal_type"])}</span> '
f'{esc(a["principal_name"])} → <span class="badge badge-ps">{esc(a["permission_set_name"])}</span></li>\n'
)
if not acct_assigns:
parts.append(" <li>No assignments</li>\n")
parts.append(" </ul></div>\n</div></td></tr>\n")
parts.append("</tbody></table></div></div></div>\n")
# ── TAB: All Assignments ──
parts.append("""
<div id="tab-assignments" class="tab-content">
<input class="search-bar" type="text" placeholder="Search assignments..." onkeyup="filterTable(this, 'assign-table')">
<div class="table-wrap"><div class="table-scroll">
<table id="assign-table">
<thead><tr>
<th onclick="sortTable('assign-table',0)">Account <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('assign-table',1)">Principal Type <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('assign-table',2)">Principal <span class="sort-icon">⇅</span></th>
<th onclick="sortTable('assign-table',3)">Permission Set <span class="sort-icon">⇅</span></th>
</tr></thead>
<tbody>
""")
all_assign_entries = []
for acct_assigns in assignment_by_account.values():
all_assign_entries.extend(acct_assigns)
for a in sorted(all_assign_entries, key=lambda x: (x["account_name"], x["principal_type"], x["principal_name"])):
ptype_badge = "badge-user" if a["principal_type"] == "USER" else "badge-group"
parts.append(f"""<tr>
<td><span class="badge badge-acct">{esc(a["account_name"])}</span> <span class="count">{esc(a["account_id"])}</span></td>
<td><span class="badge {ptype_badge}">{esc(a["principal_type"])}</span></td>
<td>{esc(a["principal_name"])}</td>
<td><span class="badge badge-ps">{esc(a["permission_set_name"])}</span></td>
</tr>
""")
parts.append("</tbody></table></div></div></div>\n")
# ── TAB: Access Matrix ──
# Rows = users, Columns = accounts, Cells = permission sets (direct + inherited)
parts.append("""
<div id="tab-matrix" class="tab-content">
<input class="search-bar" type="text" placeholder="Search matrix..." onkeyup="filterTable(this, 'matrix-table')">
<p style="color:var(--text-muted);font-size:13px;margin-bottom:12px;">Effective access per user (direct + group-inherited). Cells show permission set names.</p>
<div class="table-wrap"><div class="table-scroll">
<table id="matrix-table">
<thead><tr>
<th>User</th>
""")
sorted_accts = sorted(account_map.items(), key=lambda x: x[1].get("Name", ""))
for acct_id, acct in sorted_accts:
parts.append(f' <th style="font-size:11px;writing-mode:vertical-lr;text-orientation:mixed;max-width:40px;">{esc(acct.get("Name", acct_id))}</th>\n')
parts.append(" </tr></thead>\n <tbody>\n")
for u in sorted(users, key=lambda x: x.get("UserName", "")):
uid = u["UserId"]
uname = esc(u.get("UserName", ""))
# Compute effective access: direct + via group memberships
effective = defaultdict(set) # account_id -> set of ps_names
for a in assignment_by_principal.get(uid, []):
effective[a["account_id"]].add(a["permission_set_name"])
for gid in user_groups.get(uid, []):
for a in assignment_by_principal.get(gid, []):
effective[a["account_id"]].add(a["permission_set_name"])
parts.append(f' <tr><td><strong>{uname}</strong></td>\n')
for acct_id, _ in sorted_accts:
ps_set = effective.get(acct_id, set())
if ps_set:
cell = " ".join(f'<span class="badge badge-ps" style="font-size:10px;">{esc(p)}</span>' for p in sorted(ps_set))
else:
cell = '<span style="color:#30363d;">—</span>'
parts.append(f' <td>{cell}</td>\n')
parts.append(" </tr>\n")
parts.append("</tbody></table></div></div></div>\n")
# ── JavaScript ──
parts.append("""
</div><!-- container -->
<script>
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.tab).classList.add('active');
});
});
// Detail row toggle
function toggleDetail(id) {
const row = document.getElementById(id);
if (row) row.classList.toggle('show');
}
// Table search/filter
function filterTable(input, tableId) {
const filter = input.value.toLowerCase();
const table = document.getElementById(tableId);
const rows = table.querySelectorAll('tbody tr:not(.detail-row)');
rows.forEach(row => {
const text = row.textContent.toLowerCase();
const match = text.includes(filter);
row.style.display = match ? '' : 'none';
// hide detail row too
const next = row.nextElementSibling;
if (next && next.classList.contains('detail-row')) {
if (!match) next.classList.remove('show');
next.style.display = match ? '' : 'none';
}
});
}
// Column sorting
let sortState = {};
function sortTable(tableId, colIdx) {
const table = document.getElementById(tableId);
const tbody = table.querySelector('tbody');
const rows = Array.from(tbody.querySelectorAll('tr:not(.detail-row)'));
// Determine direction
const key = tableId + '-' + colIdx;
sortState[key] = sortState[key] === 'asc' ? 'desc' : 'asc';
const dir = sortState[key] === 'asc' ? 1 : -1;
// Update header styling
table.querySelectorAll('th').forEach(th => th.classList.remove('sorted'));
table.querySelectorAll('th')[colIdx]?.classList.add('sorted');
rows.sort((a, b) => {
const aText = a.cells[colIdx]?.textContent.trim().toLowerCase() || '';
const bText = b.cells[colIdx]?.textContent.trim().toLowerCase() || '';
return aText.localeCompare(bText) * dir;
});
// Rebuild tbody preserving detail rows
const frag = document.createDocumentFragment();
rows.forEach(row => {
frag.appendChild(row);
const next = row.nextElementSibling;
// Find matching detail row by checking original DOM
const detailId = row.querySelector('.expand-btn')?.getAttribute('onclick')?.match(/'([^']+)'/)?.[1];
if (detailId) {
const detailRow = document.getElementById(detailId);
if (detailRow) frag.appendChild(detailRow);
}
});
tbody.appendChild(frag);
}
</script>
</body>
</html>
""")
return "".join(parts)
# ─── Main ───────────────────────────────────────────────────────────────────
def main():
print("=" * 60)
print(" AWS Identity Center — Comprehensive Report Generator")
print("=" * 60)
print()
session = boto3.Session()
sso_admin = session.client("sso-admin")
identity_store = session.client("identitystore")
org_client = session.client("organizations")
# 1. Discover instance
print("[1/7] Discovering SSO instance...")
instance_arn, store_id = get_sso_instance(sso_admin)
print(f" Instance ARN: {instance_arn}")
print(f" Identity Store ID: {store_id}")
print()
# 2. Accounts
print("[2/7] Collecting accounts...")
account_map = collect_accounts(org_client)
print()
# 3. Users
print("[3/7] Collecting users...")
users = collect_users(identity_store, store_id)
user_map = {u["UserId"]: u for u in users}
print()
# 4. Groups
print("[4/7] Collecting groups...")
groups = collect_groups(identity_store, store_id)
group_map = {g["GroupId"]: g for g in groups}
print()
# 5. Memberships
print("[5/7] Collecting group memberships...")
memberships = collect_group_memberships(identity_store, store_id, groups, user_map)
print()
# 6. Permission sets
print("[6/7] Collecting permission sets...")
permission_sets = collect_permission_sets(sso_admin, instance_arn)
print()
# 7. Account assignments
print("[7/7] Collecting account assignments...")
assignments = collect_account_assignments(sso_admin, instance_arn, permission_sets, account_map)
print()
# Generate report
print("Generating HTML report...")
html_content = generate_html(
users, groups, memberships, permission_sets,
assignments, account_map, user_map, group_map
)
output_file = "aws_identity_center_report.html"
with open(output_file, "w", encoding="utf-8") as f:
f.write(html_content)
print()
print("=" * 60)
print(f" Report saved to: {output_file}")
print(f" Open in browser: file://{output_file}")
print("=" * 60)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment