A Python CLI tool that pulls identity security data from the Google Workspace Admin SDK and generates a self-contained HTML dashboard — no web server required.
What it covers:
| Data point | Source |
|---|---|
| MFA enrollment & enforcement per user | Directory API |
| Password policy (org-level settings + per-user compliance) | Directory API / Usage Report |
| Login activity — auth methods, 2FA challenges, suspicious logins | Reports API |
| MFA enable/disable event history | Reports API |
| Third-party OAuth app grants — scope risk level + history | Directory API / Reports API |
| Password change events | Reports API |
| Account recovery info change events | Reports API |
Output is a single .html file you can open in any browser or share with auditors.
![Dashboard screenshot placeholder — shows 9 KPI cards + tabbed detail views]
- Python 3.11+
- A Google Workspace super-admin account (Business Starter / Standard / Plus / Enterprise)
- A Google Cloud project (the free tier is sufficient — this tool makes read-only API calls)
- Go to console.cloud.google.com
- Click the project selector → New Project
- Name it
workspace-posture(or anything you like)
- In your project, go to APIs & Services → Library
- Search for Admin SDK API
- Click Enable
This single enable covers the Directory, Reports, and Token sub-APIs.
- Go to IAM & Admin → Service Accounts
- Click Create Service Account
- Name:
identity-posture-sa - Description:
Reads Workspace identity data for security posture reporting
- Name:
- Click Create and continue, then Skip the next two optional steps, then Done
- Click the service account you just created → Keys tab
- Click Add Key → Create new key → JSON → Download and save the file
- Place the file at
credentials/service-account.jsonin this project directory - Still on the service account details page, note the Unique ID — a long numeric string (e.g.
112233445566778899001). You'll need this in the next step.
Warning: The Unique ID is the long number on the service account details page. It is not the SHA-1 fingerprint shown on the Keys tab.
This grants the service account read-only access to your Workspace data.
- Go to admin.google.com signed in as a super-admin
- Navigate to: Security → Access and data control → API controls
- Click Manage Domain Wide Delegation
- Click Add new
- Fill in:
- Client ID: the numeric Unique ID from Step 3
- OAuth Scopes: copy and paste the entire line below
https://www.googleapis.com/auth/admin.directory.user.readonly,https://www.googleapis.com/auth/admin.reports.audit.readonly,https://www.googleapis.com/auth/admin.reports.usage.readonly,https://www.googleapis.com/auth/admin.directory.user.security,https://www.googleapis.com/auth/admin.directory.customer.readonly
- Click Authorise
Allow up to 5 minutes for the delegation to propagate before running the tool.
Why these scopes?
| Scope | Used for |
|---|---|
admin.directory.user.readonly |
List users and read real-time MFA status (isEnrolledIn2Sv, isEnforcedIn2Sv) |
admin.reports.audit.readonly |
Login events, MFA disable events, OAuth grant/revoke events, password change events |
admin.reports.usage.readonly |
Per-user password compliance and MFA snapshot from usage report |
admin.directory.user.security |
Read OAuth tokens currently granted per user |
admin.directory.customer.readonly |
Read org-level password policy settings |
# Clone the repo
git clone https://github.com/YOUR_USERNAME/identity-posture.git
cd identity-posture
# Create and activate a virtual environment
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# Install dependencies
pip install -r requirements.txtcp .env.example .envEdit .env and fill in your values:
# Path to the service account JSON key (from Step 3)
GOOGLE_SERVICE_ACCOUNT_FILE=credentials/service-account.json
# Email of a super-admin account to impersonate via domain-wide delegation
GOOGLE_DELEGATED_ADMIN=admin@yourdomain.com
# Your Google Workspace primary domain
GOOGLE_DOMAIN=yourdomain.com
# SQLite database path (created automatically on first run)
DB_PATH=data/posture.db
# Directory where HTML reports are written
REPORT_OUTPUT_DIR=reports/
# Lookback windows for event history (in days)
LOGIN_LOOKBACK_DAYS=7
MFA_LOOKBACK_DAYS=30
OAUTH_LOOKBACK_DAYS=30
USER_ACCOUNT_LOOKBACK_DAYS=30Security:
.envandcredentials/*.jsonare in.gitignoreand must never be committed.
python main.py runAdd --open to open the report in your browser automatically:
python main.py run --open# Regenerate the HTML report from already-stored data (no API calls)
python main.py report --open
# Only collect data, skip report generation
python main.py collect
# Collect only specific data sources
python main.py run --collectors users,logins,mfaAvailable collectors: users, policy, logins, mfa, oauth, user_accounts
| File | Description |
|---|---|
reports/dashboard_latest.html |
Always the most recent run — share this with auditors |
reports/dashboard_YYYYMMDD_HHMMSS.html |
Timestamped archive of each run |
data/posture.db |
SQLite database with all collected data |
The dashboard opens with a landing page of 9 KPI cards covering the most security-relevant signals at a glance:
| Card | Green when | Red/Amber when |
|---|---|---|
| MFA Enrolled % | 100% | < 100% |
| MFA Protected % | 100% | < 100% |
| Admins Without MFA | 0 | > 0 (critical) |
| Password Non-Compliant | 0 | > 0 |
| Suspicious Logins | 0 | > 0 |
| High-Risk OAuth Apps | 0 | > 0 |
| Password Changes | — | > 0 shown in amber (informational) |
| Recovery Info Changes | — | > 0 shown in amber (potential persistence signal) |
| MFA Disabled Events | 0 | > 0 shown in red |
Detail tabs:
| Tab | What it shows |
|---|---|
| Password Policy | Org-level settings (min length, complexity, reuse, expiry) + per-user compliance |
| MFA Status | Enrolled / Enforced / Protected columns per user; admin alert if any admin lacks MFA |
| Login Activity | Login events with auth method, 2FA challenge type, success/failure, suspicious flag |
| MFA Events | Timeline of MFA enroll and disable events (user-initiated and admin-initiated) |
| OAuth Apps | Active OAuth grants with scope risk rating (HIGH / MEDIUM / LOW) + grant/revoke history |
| Account Events | Password changes and account recovery info changes by user and actor |
For consistent data, run the tool at least once per day. Event data (logins, MFA events, OAuth events) is typically available via the API within 1–2 hours of the event occurring.
# Edit your crontab
crontab -e
# Run daily at 06:00, log output to a file
0 6 * * * cd /path/to/identity-posture && /path/to/.venv/bin/python main.py run >> logs/cron.log 2>&1Create ~/Library/LaunchAgents/com.identity-posture.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.identity-posture</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/.venv/bin/python</string>
<string>/path/to/identity-posture/main.py</string>
<string>run</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>6</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>WorkingDirectory</key>
<string>/path/to/identity-posture</string>
<key>StandardOutPath</key>
<string>/path/to/identity-posture/logs/launchd.log</string>
<key>StandardErrorPath</key>
<string>/path/to/identity-posture/logs/launchd.log</string>
</dict>
</plist>Load it:
launchctl load ~/Library/LaunchAgents/com.identity-posture.plistFor fully automated, off-laptop execution:
- Containerise with the provided
Dockerfile(if present) or build one based onpython:3.12-slim - Store
credentials/service-account.jsonin Google Secret Manager - Deploy as a Cloud Run Job in your GCP project
- Schedule it with Cloud Scheduler (cron syntax, calls the Cloud Run Job trigger)
- Mount the secret as an environment variable or file at startup
- Write reports to Google Cloud Storage and serve via a signed URL or Cloud Run web service
This requires no additional IAM setup if the Cloud Run service account is granted the same domain-wide delegation as the local service account.
- Enrolled (
isEnrolledIn2Sv) — whether a second factor is registered. Comes from the Directory API; real-time. - Enforced (
isEnforcedIn2Sv) — whether 2FA is enforced on the account. Comes from the Directory API; real-time. - Protected — whether 2FA was actually challenged at last sign-in (from
userUsageReport). A user can be enrolled but bypass 2FA via trusted devices or SSO. userUsageReportdata is typically 24–48 hours behind. New Workspace deployments may have no data yet.
Google does not expose password hashes or plaintext via any API. This tool can show:
- Org-level policy settings (minimum length, complexity toggle, reuse, expiry)
- Per-user compliance (
password_length_ok) sourced fromuserUsageReport - Limitation: Users whose passwords were set via GCDS hash sync will show
Unknownfor compliance — this is a Google design decision, not a tool limitation
- Reports API data has a minimum 1–2 hour ingestion lag
- Historical data is available for up to 180 days via the API
- The
is_suspiciousflag is set by Google's risk assessment, not by this tool
- Current grants show what is currently authorised, not when tokens were last used
- An authorised app may be dormant
403 Forbidden errors
- Verify domain-wide delegation was added correctly in Step 4
- Confirm
GOOGLE_DELEGATED_ADMINis a super-admin email (not a regular user or delegated admin) - Allow up to 5 minutes after configuring delegation for propagation
- Confirm the Admin SDK API is enabled in your GCP project (Step 2)
Invalid Client ID when adding delegation
- Make sure you're entering the numeric Unique ID from the service account details page, not the SHA-1 key fingerprint shown on the Keys tab
FileNotFoundError for service account
- Confirm the JSON file is at
credentials/service-account.json(or the path inGOOGLE_SERVICE_ACCOUNT_FILE)
MFA showing 0% enrolled when you know users are enrolled
- Check that
GOOGLE_DELEGATED_ADMINis an account on your primary domain (the one that owns the Workspace subscription), not a secondary/alias domain - The tool reads MFA status from the Directory API; delegating through a secondary domain can return incomplete results
Empty userUsageReport results
- Usage reports are typically available for the previous 2 days
- New Workspace deployments may have no historical data yet
quota exceeded errors
- The tool avoids per-user filtered queries for event data, but very large orgs (10,000+ users) may still hit limits
- Consider running during off-peak hours
- Reduce
LOGIN_LOOKBACK_DAYSorOAUTH_LOOKBACK_DAYS
main.py (CLI)
└── collectors/
│ ├── users.py Directory API users.list() → users table
│ ├── password_policy.py Directory API customers.get() → password_policy table
│ ├── login_activity.py Reports API login application → login_events table
│ ├── mfa_events.py Reports API admin application → mfa_events table
│ ├── oauth_tokens.py Directory API tokens.list() → oauth_tokens table
│ │ Reports API token application → oauth_events table
│ └── user_account_events.py Reports API user_accounts app → user_account_events table
└── storage/
│ ├── schema.sql SQLite schema (auto-created)
│ └── db.py upsert / query helpers
└── dashboard/
├── renderer.py Pulls from SQLite, renders Jinja2 template
└── templates/index.html Self-contained HTML (Tailwind CSS + vanilla JS)
- No web server required — the dashboard is a single static HTML file
- SQLite stores history across runs — re-running only appends new events;
INSERT OR IGNOREon event IDs prevents duplicates - OAuth tokens are fetched per-user (
tokens.list) in parallel with a thread pool capped at 10 workers; each thread builds its own API client to avoid connection sharing issues - Event collectors use broad time-range queries (no per-user filter) to stay within the Reports API filter quota cap of 250 requests/minute
- Credentials never leave your machine — the service account key and
.envare.gitignored; the generated HTML contains no credentials
- The service account key JSON is a long-lived credential — treat it like a password. Store it securely and rotate it if it may have been exposed.
- Domain-wide delegation grants broad read access to your Workspace data. Audit who can access the machine where this tool runs.
- The generated HTML reports contain user email addresses, login activity, and OAuth grant details. Treat them as confidential and restrict access accordingly.
- Consider using Workload Identity Federation instead of a service account key if running in GCP — eliminates the need for a key file entirely.
MIT