Files

269 lines
11 KiB
Python

#!/usr/bin/env python3
"""
Duo MFA Configuration Auditor and Health Checker
Audits Duo MFA deployment configuration, checks policy compliance,
detects MFA fatigue patterns, and monitors authentication health.
"""
import json
import datetime
from typing import Dict, List, Optional
from dataclasses import dataclass, field
from collections import defaultdict
@dataclass
class DuoPolicy:
"""Duo authentication policy configuration."""
policy_name: str
group: str
allowed_methods: List[str] # push, verified_push, webauthn, totp, sms, phone
remembered_devices_days: int = 0
require_device_health: bool = False
require_encryption: bool = False
min_os_version: str = ""
trusted_networks: List[str] = field(default_factory=list)
failmode: str = "secure" # secure or safe
@dataclass
class AuthEvent:
"""Authentication event from Duo logs."""
timestamp: str
username: str
factor: str # push, verified_push, webauthn, totp, sms, phone, bypass
result: str # success, denied, fraud, timeout
ip_address: str
device_os: str = ""
application: str = ""
reason: str = ""
@dataclass
class MFAAuditFinding:
"""MFA audit finding."""
severity: str
category: str
title: str
description: str
recommendation: str = ""
class DuoMFAAuditor:
"""Audits Duo MFA configuration and detects anomalies."""
PHISHING_RESISTANT = {"verified_push", "webauthn", "fido2"}
WEAK_METHODS = {"sms", "phone"}
AAL2_METHODS = {"push", "verified_push", "webauthn", "totp", "fido2"}
def __init__(self):
self.policies: List[DuoPolicy] = []
self.auth_events: List[AuthEvent] = []
self.findings: List[MFAAuditFinding] = []
def load_policies(self, policies: List[Dict]):
for p in policies:
self.policies.append(DuoPolicy(**p))
def load_auth_events(self, events: List[Dict]):
for e in events:
self.auth_events.append(AuthEvent(**e))
def audit_all(self) -> List[MFAAuditFinding]:
self.findings = []
self._audit_policy_strength()
self._audit_weak_methods()
self._audit_device_health()
self._audit_failmode()
self._audit_remembered_devices()
self._detect_mfa_fatigue()
self._detect_bypass_usage()
self._audit_method_distribution()
return self.findings
def _audit_policy_strength(self):
for policy in self.policies:
has_phishing_resistant = any(
m in self.PHISHING_RESISTANT for m in policy.allowed_methods
)
if "privileged" in policy.group.lower() or "admin" in policy.group.lower():
if not has_phishing_resistant:
self.findings.append(MFAAuditFinding(
severity="critical",
category="Policy Strength",
title=f"Privileged group '{policy.group}' lacks phishing-resistant MFA",
description=f"Policy '{policy.policy_name}' allows only: {', '.join(policy.allowed_methods)}",
recommendation="Enable Verified Push or WebAuthn for privileged users per CISA guidance"
))
def _audit_weak_methods(self):
for policy in self.policies:
weak = set(policy.allowed_methods) & self.WEAK_METHODS
if weak:
self.findings.append(MFAAuditFinding(
severity="high",
category="Weak Methods",
title=f"Weak MFA methods enabled for '{policy.group}'",
description=f"SMS and/or phone call enabled: {', '.join(weak)}. "
"These are vulnerable to SIM swapping and social engineering.",
recommendation="Disable SMS/phone for users with smartphone access. Use push or WebAuthn."
))
def _audit_device_health(self):
for policy in self.policies:
if not policy.require_device_health:
self.findings.append(MFAAuditFinding(
severity="medium",
category="Device Health",
title=f"Device health not enforced for '{policy.group}'",
description="Unmanaged or unhealthy devices can authenticate without restriction.",
recommendation="Enable device health policy to check OS version, encryption, and firewall."
))
def _audit_failmode(self):
for policy in self.policies:
if policy.failmode == "safe":
self.findings.append(MFAAuditFinding(
severity="high",
category="Failmode",
title=f"Failmode set to 'safe' for '{policy.policy_name}'",
description="When Duo is unreachable, users are allowed in without MFA.",
recommendation="Set failmode to 'secure' to deny access when Duo is unavailable."
))
def _audit_remembered_devices(self):
for policy in self.policies:
if policy.remembered_devices_days > 30:
self.findings.append(MFAAuditFinding(
severity="medium",
category="Remembered Devices",
title=f"Long remembered device period for '{policy.group}'",
description=f"Devices remembered for {policy.remembered_devices_days} days. "
"Stolen devices retain MFA bypass.",
recommendation="Reduce remembered device period to 7 days or less."
))
def _detect_mfa_fatigue(self):
"""Detect potential MFA prompt bombing / fatigue attacks."""
user_denials = defaultdict(list)
for event in self.auth_events:
if event.result in ("denied", "timeout", "fraud"):
user_denials[event.username].append(event)
for user, denials in user_denials.items():
if len(denials) >= 5:
# Check if denials happened within a short window
timestamps = sorted(denials, key=lambda e: e.timestamp)
if len(timestamps) >= 5:
self.findings.append(MFAAuditFinding(
severity="critical",
category="MFA Fatigue",
title=f"Potential MFA fatigue attack on user '{user}'",
description=f"{len(denials)} denied/timeout MFA attempts detected. "
"This pattern indicates potential MFA prompt bombing.",
recommendation=f"Lock account '{user}', verify with user, enable Verified Push, "
"investigate source IPs."
))
def _detect_bypass_usage(self):
bypass_events = [e for e in self.auth_events if e.factor == "bypass"]
if bypass_events:
users = set(e.username for e in bypass_events)
self.findings.append(MFAAuditFinding(
severity="high",
category="Bypass Usage",
title=f"{len(bypass_events)} MFA bypass authentications detected",
description=f"Users with bypass codes: {', '.join(users)}. "
"Bypass codes circumvent MFA protection.",
recommendation="Review bypass usage. Ensure bypass codes are single-use and time-limited."
))
def _audit_method_distribution(self):
method_counts = defaultdict(int)
for event in self.auth_events:
if event.result == "success":
method_counts[event.factor] += 1
total = sum(method_counts.values())
if total > 0:
weak_pct = sum(method_counts.get(m, 0) for m in self.WEAK_METHODS) / total * 100
if weak_pct > 10:
self.findings.append(MFAAuditFinding(
severity="medium",
category="Method Distribution",
title=f"{weak_pct:.1f}% of authentications use weak MFA methods",
description="Significant portion of users still using SMS/phone.",
recommendation="Migrate users to Duo Push, Verified Push, or WebAuthn."
))
def generate_report(self) -> str:
if not self.findings:
self.audit_all()
lines = [
"=" * 70,
"DUO MFA CONFIGURATION AUDIT REPORT",
"=" * 70,
f"Report Date: {datetime.datetime.now().isoformat()}",
f"Policies Audited: {len(self.policies)}",
f"Auth Events Analyzed: {len(self.auth_events)}",
f"Findings: {len(self.findings)}",
"-" * 70, ""
]
severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
for f in sorted(self.findings, key=lambda x: severity_order.get(x.severity, 5)):
lines.append(f"[{f.severity.upper()}] {f.title}")
lines.append(f" Category: {f.category}")
lines.append(f" {f.description}")
if f.recommendation:
lines.append(f" Fix: {f.recommendation}")
lines.append("")
lines.append("=" * 70)
critical = sum(1 for f in self.findings if f.severity == "critical")
lines.append(f"OVERALL: {'FAIL' if critical > 0 else 'PASS WITH FINDINGS' if self.findings else 'PASS'}")
lines.append("=" * 70)
return "\n".join(lines)
def main():
auditor = DuoMFAAuditor()
auditor.load_policies([
{"policy_name": "Standard Users", "group": "standard_users",
"allowed_methods": ["push", "totp", "sms"], "remembered_devices_days": 7,
"failmode": "secure"},
{"policy_name": "Privileged Admins", "group": "privileged_admins",
"allowed_methods": ["push", "totp"], "remembered_devices_days": 0,
"require_device_health": True, "failmode": "safe"},
{"policy_name": "Contractors", "group": "contractors",
"allowed_methods": ["push", "phone", "sms"], "remembered_devices_days": 45,
"failmode": "secure"},
])
auditor.load_auth_events([
{"timestamp": "2026-02-23T08:00:00", "username": "alice", "factor": "push",
"result": "success", "ip_address": "10.0.1.50", "application": "VPN"},
{"timestamp": "2026-02-23T08:01:00", "username": "bob", "factor": "sms",
"result": "success", "ip_address": "192.168.1.100", "application": "VPN"},
{"timestamp": "2026-02-23T08:02:00", "username": "charlie", "factor": "push",
"result": "denied", "ip_address": "203.0.113.50", "application": "RDP"},
{"timestamp": "2026-02-23T08:02:30", "username": "charlie", "factor": "push",
"result": "denied", "ip_address": "203.0.113.50", "application": "RDP"},
{"timestamp": "2026-02-23T08:03:00", "username": "charlie", "factor": "push",
"result": "denied", "ip_address": "203.0.113.50", "application": "RDP"},
{"timestamp": "2026-02-23T08:03:30", "username": "charlie", "factor": "push",
"result": "timeout", "ip_address": "203.0.113.50", "application": "RDP"},
{"timestamp": "2026-02-23T08:04:00", "username": "charlie", "factor": "push",
"result": "denied", "ip_address": "203.0.113.50", "application": "RDP"},
{"timestamp": "2026-02-23T09:00:00", "username": "dave", "factor": "bypass",
"result": "success", "ip_address": "10.0.2.75", "application": "SSH"},
])
print(auditor.generate_report())
if __name__ == "__main__":
main()