Files

303 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Device Posture Assessment - Compliance Audit Tool
Queries CrowdStrike ZTA scores, Microsoft Intune compliance status,
and generates a consolidated device posture compliance report.
Requirements:
pip install requests msal pandas
"""
import json
import sys
from datetime import datetime, timezone
from typing import Any
import requests
class DevicePostureAuditor:
"""Audit device posture compliance across EDR and MDM platforms."""
def __init__(self, cs_client_id: str, cs_client_secret: str,
azure_tenant_id: str, azure_client_id: str, azure_client_secret: str):
self.cs_client_id = cs_client_id
self.cs_client_secret = cs_client_secret
self.azure_tenant_id = azure_tenant_id
self.azure_client_id = azure_client_id
self.azure_client_secret = azure_client_secret
self.cs_token = None
self.azure_token = None
def authenticate_crowdstrike(self):
"""Get CrowdStrike API bearer token."""
resp = requests.post(
"https://api.crowdstrike.com/oauth2/token",
data={
"client_id": self.cs_client_id,
"client_secret": self.cs_client_secret
},
timeout=30
)
resp.raise_for_status()
self.cs_token = resp.json()["access_token"]
print("[AUTH] CrowdStrike authenticated")
def authenticate_azure(self):
"""Get Microsoft Graph API token."""
resp = requests.post(
f"https://login.microsoftonline.com/{self.azure_tenant_id}/oauth2/v2.0/token",
data={
"client_id": self.azure_client_id,
"client_secret": self.azure_client_secret,
"scope": "https://graph.microsoft.com/.default",
"grant_type": "client_credentials"
},
timeout=30
)
resp.raise_for_status()
self.azure_token = resp.json()["access_token"]
print("[AUTH] Microsoft Graph authenticated")
def get_zta_scores(self) -> dict[str, Any]:
"""Get CrowdStrike ZTA score distribution."""
print("\n[1/4] Querying CrowdStrike ZTA scores...")
headers = {"Authorization": f"Bearer {self.cs_token}"}
# Get all device AIDs with ZTA assessments
resp = requests.get(
"https://api.crowdstrike.com/zero-trust-assessment/queries/assessments/v1",
headers=headers,
params={"limit": 5000},
timeout=60
)
resp.raise_for_status()
device_ids = resp.json().get("resources", [])
if not device_ids:
print(" No ZTA assessments found")
return {"total": 0, "distribution": {}}
# Get detailed assessments in batches of 100
scores = []
for i in range(0, len(device_ids), 100):
batch = device_ids[i:i+100]
resp = requests.get(
"https://api.crowdstrike.com/zero-trust-assessment/entities/assessments/v1",
headers=headers,
params={"ids": batch},
timeout=60
)
if resp.status_code == 200:
for resource in resp.json().get("resources", []):
assessment = resource.get("assessment", {})
scores.append({
"aid": resource.get("aid"),
"overall": assessment.get("overall", 0),
"os_score": assessment.get("os", 0),
"sensor_score": assessment.get("sensor_config", 0)
})
# Calculate distribution
distribution = {
"critical_90_100": sum(1 for s in scores if s["overall"] >= 90),
"high_80_89": sum(1 for s in scores if 80 <= s["overall"] < 90),
"medium_65_79": sum(1 for s in scores if 65 <= s["overall"] < 80),
"low_50_64": sum(1 for s in scores if 50 <= s["overall"] < 65),
"blocked_below_50": sum(1 for s in scores if s["overall"] < 50),
}
avg_score = sum(s["overall"] for s in scores) / len(scores) if scores else 0
print(f" Total devices: {len(scores)}")
print(f" Average ZTA score: {avg_score:.1f}")
print(f" Distribution: {distribution}")
return {
"total": len(scores),
"average_score": round(avg_score, 1),
"distribution": distribution,
"below_threshold_50": distribution["blocked_below_50"]
}
def get_intune_compliance(self) -> dict[str, Any]:
"""Get Microsoft Intune device compliance status."""
print("\n[2/4] Querying Intune compliance status...")
headers = {"Authorization": f"Bearer {self.azure_token}"}
resp = requests.get(
"https://graph.microsoft.com/v1.0/deviceManagement/managedDevices",
headers=headers,
params={
"$select": "id,deviceName,userPrincipalName,complianceState,"
"operatingSystem,osVersion,isEncrypted,lastSyncDateTime,"
"managementAgent",
"$top": 999
},
timeout=60
)
resp.raise_for_status()
devices = resp.json().get("value", [])
stats = {
"total": len(devices),
"compliant": 0,
"noncompliant": 0,
"in_grace_period": 0,
"unknown": 0,
"os_distribution": {},
"encryption_status": {"encrypted": 0, "not_encrypted": 0},
"stale_devices": 0,
"noncompliant_details": []
}
now = datetime.now(timezone.utc)
for device in devices:
compliance = device.get("complianceState", "unknown")
os_name = device.get("operatingSystem", "unknown")
encrypted = device.get("isEncrypted", False)
stats["os_distribution"][os_name] = stats["os_distribution"].get(os_name, 0) + 1
if encrypted:
stats["encryption_status"]["encrypted"] += 1
else:
stats["encryption_status"]["not_encrypted"] += 1
if compliance == "compliant":
stats["compliant"] += 1
elif compliance == "noncompliant":
stats["noncompliant"] += 1
stats["noncompliant_details"].append({
"device": device.get("deviceName"),
"user": device.get("userPrincipalName"),
"os": f"{os_name} {device.get('osVersion', '')}",
"encrypted": encrypted
})
elif compliance == "inGracePeriod":
stats["in_grace_period"] += 1
else:
stats["unknown"] += 1
# Check for stale devices (no sync in 30 days)
last_sync = device.get("lastSyncDateTime")
if last_sync:
try:
sync_dt = datetime.fromisoformat(last_sync.replace("Z", "+00:00"))
if (now - sync_dt).days > 30:
stats["stale_devices"] += 1
except (ValueError, TypeError):
pass
compliance_rate = (stats["compliant"] / stats["total"] * 100) if stats["total"] else 0
print(f" Total: {stats['total']}, Compliant: {stats['compliant']} ({compliance_rate:.1f}%)")
print(f" Non-compliant: {stats['noncompliant']}, Grace period: {stats['in_grace_period']}")
print(f" Encryption: {stats['encryption_status']}")
print(f" Stale devices (>30d no sync): {stats['stale_devices']}")
return stats
def correlate_posture(self, zta: dict, intune: dict) -> dict[str, Any]:
"""Correlate ZTA and MDM compliance for overall posture score."""
print("\n[3/4] Correlating posture signals...")
total_devices = max(zta["total"], intune["total"])
zta_passing = zta["total"] - zta.get("below_threshold_50", 0)
intune_passing = intune["compliant"] + intune["in_grace_period"]
overall_compliance = min(
(zta_passing / zta["total"] * 100) if zta["total"] else 0,
(intune_passing / intune["total"] * 100) if intune["total"] else 0
)
summary = {
"estimated_total_devices": total_devices,
"zta_passing_rate": round((zta_passing / zta["total"] * 100) if zta["total"] else 0, 1),
"intune_passing_rate": round((intune_passing / intune["total"] * 100) if intune["total"] else 0, 1),
"overall_compliance_rate": round(overall_compliance, 1),
"risk_level": "LOW" if overall_compliance >= 90 else "MEDIUM" if overall_compliance >= 75 else "HIGH"
}
print(f" ZTA passing: {summary['zta_passing_rate']}%")
print(f" Intune passing: {summary['intune_passing_rate']}%")
print(f" Overall compliance: {summary['overall_compliance_rate']}%")
print(f" Risk level: {summary['risk_level']}")
return summary
def generate_report(self, zta: dict, intune: dict, correlated: dict) -> str:
"""Generate consolidated posture report."""
print("\n[4/4] Generating report...")
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
report = f"""
Device Posture Compliance Report
{'=' * 55}
Generated: {now}
1. CROWDSTRIKE ZTA SCORES
Total devices assessed: {zta['total']}
Average ZTA score: {zta['average_score']}
Score >= 90 (Critical OK): {zta['distribution'].get('critical_90_100', 0)}
Score 80-89 (High OK): {zta['distribution'].get('high_80_89', 0)}
Score 65-79 (Medium OK): {zta['distribution'].get('medium_65_79', 0)}
Score 50-64 (Low): {zta['distribution'].get('low_50_64', 0)}
Score < 50 (BLOCKED): {zta['distribution'].get('blocked_below_50', 0)}
2. INTUNE COMPLIANCE
Total managed devices: {intune['total']}
Compliant: {intune['compliant']}
Non-compliant: {intune['noncompliant']}
In grace period: {intune['in_grace_period']}
Stale (>30d no sync): {intune['stale_devices']}
Encrypted: {intune['encryption_status']['encrypted']}
Not encrypted: {intune['encryption_status']['not_encrypted']}
3. OVERALL POSTURE
ZTA passing rate: {correlated['zta_passing_rate']}%
Intune passing rate: {correlated['intune_passing_rate']}%
Combined compliance: {correlated['overall_compliance_rate']}%
Risk level: {correlated['risk_level']}
4. RECOMMENDATIONS
"""
recs = []
if zta["distribution"].get("blocked_below_50", 0) > 0:
recs.append(f" - {zta['distribution']['blocked_below_50']} devices below ZTA 50 - investigate immediately")
if intune["encryption_status"]["not_encrypted"] > 0:
recs.append(f" - {intune['encryption_status']['not_encrypted']} devices lack encryption - enforce BitLocker/FileVault")
if intune["stale_devices"] > 0:
recs.append(f" - {intune['stale_devices']} stale devices - verify active use or remove")
if correlated["overall_compliance_rate"] < 95:
recs.append(f" - Overall compliance {correlated['overall_compliance_rate']}% below 95% target")
if not recs:
recs.append(" - All devices meet compliance requirements")
report += "\n".join(recs)
return report
def main():
if len(sys.argv) < 6:
print("Usage: python process.py <cs_client_id> <cs_client_secret> "
"<azure_tenant_id> <azure_client_id> <azure_client_secret>")
sys.exit(1)
auditor = DevicePostureAuditor(*sys.argv[1:6])
auditor.authenticate_crowdstrike()
auditor.authenticate_azure()
zta = auditor.get_zta_scores()
intune = auditor.get_intune_compliance()
correlated = auditor.correlate_posture(zta, intune)
report = auditor.generate_report(zta, intune, correlated)
print(report)
filename = f"device_posture_report_{datetime.now().strftime('%Y%m%d')}.txt"
with open(filename, "w") as f:
f.write(report)
print(f"\nReport saved to: {filename}")
if __name__ == "__main__":
main()