Skip to content

Instantly share code, notes, and snippets.

@egre55
Created March 29, 2026 13:03
Show Gist options
  • Select an option

  • Save egre55/8e02a43f49e52f1ada5d1bb42bd174f9 to your computer and use it in GitHub Desktop.

Select an option

Save egre55/8e02a43f49e52f1ada5d1bb42bd174f9 to your computer and use it in GitHub Desktop.
"""
Google Workspace MFA Reporting (Internal)
--- INSTALL ---
pip install google-api-python-client google-auth-oauthlib --break-system-packages
--- USAGE ---
python3 gws_mfa_report.py --credentials client_secret.json
python3 gws_mfa_report.py --credentials client_secret.json --csv mfa_report.csv
python3 gws_mfa_report.py --credentials client_secret.json --domain <domain>
First run opens a browser for Google sign-in. Token is cached in token.json
for subsequent runs. If you have an existing stale token.json you can delete it.
"""
import argparse
import csv
import json
import os
import sys
from datetime import datetime
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/admin.directory.user.readonly"]
TOKEN_FILE = "token.json"
def authenticate(credentials_file: str) -> Credentials:
creds = None
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(credentials_file, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_FILE, "w") as f:
f.write(creds.to_json())
return creds
def fetch_users(service, domain: str = None) -> list:
users = []
params = {
"customer": "my_customer",
"maxResults": 500,
"orderBy": "email",
"projection": "full",
}
if domain:
params["domain"] = domain
request = service.users().list(**params)
while request is not None:
response = request.execute()
users.extend(response.get("users", []))
request = service.users().list_next(request, response)
return users
def print_report(users: list):
active = [u for u in users if not u.get("suspended", False)]
suspended = [u for u in users if u.get("suspended", False)]
enrolled = [u for u in active if u.get("isEnrolledIn2Sv", False)]
enforced = [u for u in active if u.get("isEnforcedIn2Sv", False)]
not_enrolled = [u for u in active if not u.get("isEnrolledIn2Sv", False)]
def pct(n, total):
return f"{(n / total * 100):.0f}%" if total else "0%"
print()
print("=" * 65)
print(" MFA STATUS REPORT")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M')}")
print("=" * 65)
print()
print(f" Total users: {len(users)}")
print(f" Active: {len(active)}")
print(f" Suspended: {len(suspended)}")
print()
print(f" MFA enrolled: {len(enrolled)}/{len(active)} ({pct(len(enrolled), len(active))})")
print(f" MFA enforced: {len(enforced)}/{len(active)} ({pct(len(enforced), len(active))})")
print(f" MFA not enrolled: {len(not_enrolled)}/{len(active)} ({pct(len(not_enrolled), len(active))})")
if not_enrolled:
print()
print("-" * 65)
print(" ACTIVE USERS WITHOUT MFA")
print("-" * 65)
for u in sorted(not_enrolled, key=lambda x: x.get("primaryEmail", "")):
email = u.get("primaryEmail", "")
last = u.get("lastLoginTime", "Never")
if last != "Never":
last = last[:10]
print(f" {email:<45} last login: {last}")
if enrolled:
print()
print("-" * 65)
print(" ACTIVE USERS WITH MFA")
print("-" * 65)
for u in sorted(enrolled, key=lambda x: x.get("primaryEmail", "")):
email = u.get("primaryEmail", "")
enforced_str = "enforced" if u.get("isEnforcedIn2Sv", False) else "not enforced"
print(f" {email:<45} {enforced_str}")
print()
def export_csv(users: list, filename: str):
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow([
"Email", "Name", "OU", "Suspended",
"MFA Enrolled", "MFA Enforced", "Last Login"
])
for u in sorted(users, key=lambda x: x.get("primaryEmail", "")):
writer.writerow([
u.get("primaryEmail", ""),
u.get("name", {}).get("fullName", ""),
u.get("orgUnitPath", "/"),
u.get("suspended", False),
u.get("isEnrolledIn2Sv", False),
u.get("isEnforcedIn2Sv", False),
u.get("lastLoginTime", "Never"),
])
print(f" Exported to {filename}")
print()
def main():
parser = argparse.ArgumentParser(description="Google Workspace MFA Status Report")
parser.add_argument(
"--credentials", required=True,
help="Path to OAuth client secret JSON (download from GCP Console)"
)
parser.add_argument("--domain", default=None, help="Filter by domain")
parser.add_argument("--csv", default=None, help="Export to CSV")
args = parser.parse_args()
if not os.path.exists(args.credentials):
print(f"Error: {args.credentials} not found")
sys.exit(1)
creds = authenticate(args.credentials)
service = build("admin", "directory_v1", credentials=creds)
print("Fetching users...")
users = fetch_users(service, domain=args.domain)
if not users:
print("No users found. Check that you're signing in as a Workspace admin.")
sys.exit(1)
print_report(users)
if args.csv:
export_csv(users, args.csv)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment