Files
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

207 lines
8.4 KiB
Python

#!/usr/bin/env python3
"""Microsoft Entra ID passwordless authentication audit agent using MS Graph API."""
import json
import sys
import argparse
from datetime import datetime
try:
import requests
from msal import ConfidentialClientApplication
except ImportError:
print("Install: pip install msal requests")
sys.exit(1)
GRAPH_URL = "https://graph.microsoft.com/v1.0"
GRAPH_BETA = "https://graph.microsoft.com/beta"
def get_access_token(tenant_id, client_id, client_secret):
"""Authenticate to Microsoft Graph using client credentials."""
app = ConfidentialClientApplication(
client_id, authority=f"https://login.microsoftonline.com/{tenant_id}",
client_credential=client_secret)
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if "access_token" in result:
return result["access_token"]
print(f"[!] Auth failed: {result.get('error_description', 'Unknown error')}")
sys.exit(1)
def graph_get(token, endpoint, beta=False):
"""Make authenticated GET request to Microsoft Graph."""
base = GRAPH_BETA if beta else GRAPH_URL
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
resp = requests.get(f"{base}{endpoint}", headers=headers, timeout=30)
resp.raise_for_status()
return resp.json()
def get_auth_methods_policy(token):
"""Get authentication methods policy to check passwordless configuration."""
return graph_get(token, "/policies/authenticationMethodsPolicy", beta=True)
def get_fido2_policy(token):
"""Get FIDO2 security key configuration."""
policy = get_auth_methods_policy(token)
for method in policy.get("authenticationMethodConfigurations", []):
if method.get("id") == "fido2":
return {
"state": method.get("state"),
"is_attestation_enforced": method.get("isAttestationEnforced"),
"is_self_service_allowed": method.get("isSelfServiceRegistrationAllowed"),
"key_restrictions": method.get("keyRestrictions", {}),
}
return {"state": "not_configured"}
def get_microsoft_authenticator_policy(token):
"""Get Microsoft Authenticator configuration."""
policy = get_auth_methods_policy(token)
for method in policy.get("authenticationMethodConfigurations", []):
if method.get("id") == "microsoftAuthenticator":
return {
"state": method.get("state"),
"feature_settings": method.get("featureSettings", {}),
}
return {"state": "not_configured"}
def get_windows_hello_policy(token):
"""Get Windows Hello for Business configuration."""
policy = get_auth_methods_policy(token)
for method in policy.get("authenticationMethodConfigurations", []):
if method.get("id") == "windowsHelloForBusiness":
return {"state": method.get("state")}
return {"state": "not_configured"}
def list_user_auth_methods(token, user_id):
"""List authentication methods registered by a specific user."""
try:
methods = graph_get(token, f"/users/{user_id}/authentication/methods")
return [{"id": m.get("id"), "type": m.get("@odata.type", "").split(".")[-1]}
for m in methods.get("value", [])]
except Exception as e:
return [{"error": str(e)}]
def get_users_without_passwordless(token, max_users=100):
"""Identify users who have not registered any passwordless method."""
users = graph_get(token, f"/users?$top={max_users}&$select=id,displayName,userPrincipalName")
users_without = []
for user in users.get("value", []):
methods = list_user_auth_methods(token, user["id"])
passwordless_types = {"fido2AuthenticationMethod", "microsoftAuthenticatorAuthenticationMethod",
"windowsHelloForBusinessAuthenticationMethod"}
user_types = {m.get("type") for m in methods if not m.get("error")}
if not user_types.intersection(passwordless_types):
users_without.append({
"user": user["userPrincipalName"],
"name": user.get("displayName", ""),
"methods": [m.get("type") for m in methods if not m.get("error")],
})
return users_without
def get_sign_in_logs(token, days=7, passwordless_only=False):
"""Get recent sign-in logs to analyze authentication methods used."""
filter_str = ""
if passwordless_only:
filter_str = "?$filter=authenticationDetails/any(a:a/authenticationMethod eq 'FIDO2 security key')"
try:
logs = graph_get(token, f"/auditLogs/signIns{filter_str}", beta=True)
return [{"user": log.get("userPrincipalName"),
"app": log.get("appDisplayName"),
"status": log.get("status", {}).get("errorCode", 0),
"auth_detail": [d.get("authenticationMethod") for d in log.get("authenticationDetails", [])],
"time": log.get("createdDateTime")}
for log in logs.get("value", [])[:50]]
except Exception as e:
return [{"error": str(e)}]
def get_conditional_access_policies(token):
"""List conditional access policies related to authentication strength."""
try:
policies = graph_get(token, "/identity/conditionalAccess/policies", beta=True)
auth_strength_policies = []
for p in policies.get("value", []):
grant = p.get("grantControls", {})
if grant and "authenticationStrength" in json.dumps(grant):
auth_strength_policies.append({
"name": p.get("displayName"),
"state": p.get("state"),
"grant_controls": grant,
})
return auth_strength_policies
except Exception as e:
return [{"error": str(e)}]
def run_passwordless_audit(token):
"""Run comprehensive passwordless authentication audit."""
print(f"\n{'='*60}")
print(f" MICROSOFT ENTRA PASSWORDLESS AUTH AUDIT")
print(f" Generated: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
print(f"{'='*60}\n")
fido2 = get_fido2_policy(token)
print(f"--- FIDO2 SECURITY KEYS ---")
print(f" State: {fido2.get('state', 'unknown')}")
print(f" Attestation Enforced: {fido2.get('is_attestation_enforced', 'N/A')}")
print(f" Self-Service Registration: {fido2.get('is_self_service_allowed', 'N/A')}")
authenticator = get_microsoft_authenticator_policy(token)
print(f"\n--- MICROSOFT AUTHENTICATOR ---")
print(f" State: {authenticator.get('state', 'unknown')}")
hello = get_windows_hello_policy(token)
print(f"\n--- WINDOWS HELLO FOR BUSINESS ---")
print(f" State: {hello.get('state', 'unknown')}")
ca_policies = get_conditional_access_policies(token)
print(f"\n--- CONDITIONAL ACCESS (Auth Strength) ({len(ca_policies)}) ---")
for p in ca_policies:
print(f" {p.get('name', 'N/A')}: {p.get('state', 'N/A')}")
users_without = get_users_without_passwordless(token, max_users=50)
print(f"\n--- USERS WITHOUT PASSWORDLESS ({len(users_without)}) ---")
for u in users_without[:10]:
print(f" {u['user']} - Current methods: {', '.join(u['methods']) or 'none'}")
print(f"\n{'='*60}\n")
return {"fido2": fido2, "authenticator": authenticator, "hello": hello,
"users_without_passwordless": len(users_without)}
def main():
parser = argparse.ArgumentParser(description="Microsoft Entra Passwordless Auth Agent")
parser.add_argument("--tenant-id", required=True, help="Azure AD tenant ID")
parser.add_argument("--client-id", required=True, help="App registration client ID")
parser.add_argument("--client-secret", required=True, help="App registration secret")
parser.add_argument("--audit", action="store_true", help="Run passwordless audit")
parser.add_argument("--user-methods", help="List auth methods for specific user")
parser.add_argument("--output", help="Save report to JSON")
args = parser.parse_args()
token = get_access_token(args.tenant_id, args.client_id, args.client_secret)
if args.audit:
report = run_passwordless_audit(token)
if args.output:
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
elif args.user_methods:
methods = list_user_auth_methods(token, args.user_methods)
print(json.dumps(methods, indent=2))
else:
parser.print_help()
if __name__ == "__main__":
main()