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

92 lines
3.6 KiB
Python

#!/usr/bin/env python3
"""FIDO2 Passwordless Auth Agent - audits FIDO2 deployment readiness and credential status."""
import json
import argparse
import logging
import subprocess
from datetime import datetime
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
def graph_api(token, endpoint):
cmd = ["curl", "-s", "-H", f"Authorization: Bearer {token}",
f"https://graph.microsoft.com/v1.0{endpoint}"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
return json.loads(result.stdout) if result.stdout else {}
def get_fido2_policy(token):
return graph_api(token, "/policies/authenticationMethodsPolicy/authenticationMethodConfigurations/fido2")
def get_registrations(token):
return graph_api(token, "/reports/authenticationMethods/userRegistrationDetails")
def audit_fido2_policy(policy):
findings = []
if policy.get("state") != "enabled":
findings.append({"issue": f"FIDO2 policy state: {policy.get('state', 'unknown')}", "severity": "high"})
if not policy.get("keyRestrictions", {}).get("aaGuids"):
findings.append({"issue": "No AAGUID restrictions set", "severity": "medium"})
if not policy.get("isAttestationEnforced"):
findings.append({"issue": "Attestation not enforced", "severity": "medium"})
return findings
def analyze_adoption(registrations):
users = registrations.get("value", [])
total = len(users)
fido2 = sum(1 for u in users if "fido2" in str(u.get("methodsRegistered", [])).lower())
passwordless = sum(1 for u in users if u.get("isPasswordlessCapable", False))
return {
"total_users": total, "fido2_registered": fido2, "passwordless_capable": passwordless,
"fido2_adoption_rate": round(fido2 / max(total, 1) * 100, 1),
}
def check_rp_config(rp_url):
cmd = ["curl", "-s", f"{rp_url}/.well-known/webauthn"]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
findings = []
try:
config = json.loads(result.stdout)
except json.JSONDecodeError:
config = {}
findings.append({"issue": "WebAuthn well-known not configured", "severity": "high"})
return {"config": config, "findings": findings}
def generate_report(policy, policy_findings, adoption, rp):
return {
"timestamp": datetime.utcnow().isoformat(),
"fido2_policy_state": policy.get("state", "unknown"),
"policy_findings": policy_findings, "adoption_metrics": adoption,
"rp_config": rp,
"total_findings": len(policy_findings) + len(rp.get("findings", [])),
}
def main():
parser = argparse.ArgumentParser(description="FIDO2 Passwordless Authentication Audit Agent")
parser.add_argument("--token", required=True, help="Graph API bearer token")
parser.add_argument("--rp-url", help="WebAuthn relying party URL")
parser.add_argument("--output", default="fido2_audit_report.json")
args = parser.parse_args()
policy = get_fido2_policy(args.token)
policy_findings = audit_fido2_policy(policy)
registrations = get_registrations(args.token)
adoption = analyze_adoption(registrations)
rp = check_rp_config(args.rp_url) if args.rp_url else {"config": {}, "findings": []}
report = generate_report(policy, policy_findings, adoption, rp)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
logger.info("FIDO2: adoption %.1f%%, %d findings", adoption["fido2_adoption_rate"], report["total_findings"])
print(json.dumps(report, indent=2, default=str))
if __name__ == "__main__":
main()