mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-12 22:24:56 +03:00
212 lines
7.8 KiB
Python
212 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Active Directory Vulnerability Assessment Analyzer.
|
|
|
|
Parses PingCastle and BloodHound output to generate consolidated
|
|
AD security assessment reports with prioritized remediation actions.
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import re
|
|
import sys
|
|
import xml.etree.ElementTree as ET
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
|
|
RISK_WEIGHTS = {
|
|
"kerberoastable_admin": 10,
|
|
"unconstrained_delegation": 9,
|
|
"as_rep_roastable": 8,
|
|
"password_never_expires_admin": 8,
|
|
"adminsdholder_modified": 8,
|
|
"dcsync_non_dc": 9,
|
|
"gpo_abuse_path": 7,
|
|
"stale_admin_account": 6,
|
|
"ldap_signing_disabled": 6,
|
|
"ntlm_not_restricted": 5,
|
|
"excessive_domain_admins": 7,
|
|
"sid_history_present": 6,
|
|
"trust_sid_filtering_disabled": 7,
|
|
"unsupported_os_dc": 9,
|
|
"password_policy_weak": 5,
|
|
}
|
|
|
|
|
|
def parse_pingcastle_xml(xml_path):
|
|
"""Parse PingCastle HTML/XML health check report."""
|
|
findings = []
|
|
try:
|
|
tree = ET.parse(xml_path)
|
|
root = tree.getroot()
|
|
for risk in root.iter("RiskRule"):
|
|
finding = {
|
|
"source": "PingCastle",
|
|
"category": risk.findtext("Category", ""),
|
|
"rule_id": risk.findtext("RiskId", ""),
|
|
"title": risk.findtext("Rationale", ""),
|
|
"description": risk.findtext("Detail", ""),
|
|
"points": int(risk.findtext("Points", "0")),
|
|
}
|
|
findings.append(finding)
|
|
except ET.ParseError:
|
|
print(f"[-] Could not parse PingCastle XML: {xml_path}")
|
|
print(" Try exporting PingCastle results as XML format")
|
|
return findings
|
|
|
|
|
|
def parse_bloodhound_json(json_path):
|
|
"""Parse BloodHound CE exported data for critical findings."""
|
|
findings = []
|
|
with open(json_path, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
|
|
if isinstance(data, dict):
|
|
nodes = data.get("nodes", [])
|
|
edges = data.get("edges", [])
|
|
elif isinstance(data, list):
|
|
nodes = data
|
|
edges = []
|
|
else:
|
|
return findings
|
|
|
|
for node in nodes:
|
|
props = node.get("properties", node.get("Properties", {}))
|
|
kind = node.get("kind", node.get("label", ""))
|
|
|
|
if kind == "User":
|
|
if props.get("hasspn", False) and props.get("admincount", False):
|
|
findings.append({
|
|
"source": "BloodHound",
|
|
"category": "Kerberos",
|
|
"rule_id": "kerberoastable_admin",
|
|
"title": f"Kerberoastable admin account: {props.get('name', 'unknown')}",
|
|
"description": f"User {props.get('name')} has SPN set and is in admin group",
|
|
"risk_weight": RISK_WEIGHTS["kerberoastable_admin"],
|
|
})
|
|
if props.get("dontreqpreauth", False):
|
|
findings.append({
|
|
"source": "BloodHound",
|
|
"category": "Kerberos",
|
|
"rule_id": "as_rep_roastable",
|
|
"title": f"AS-REP roastable account: {props.get('name', 'unknown')}",
|
|
"description": f"User {props.get('name')} has Kerberos pre-auth disabled",
|
|
"risk_weight": RISK_WEIGHTS["as_rep_roastable"],
|
|
})
|
|
if props.get("pwdneverexpires", False) and props.get("admincount", False):
|
|
findings.append({
|
|
"source": "BloodHound",
|
|
"category": "Privileged Accounts",
|
|
"rule_id": "password_never_expires_admin",
|
|
"title": f"Admin with non-expiring password: {props.get('name', 'unknown')}",
|
|
"description": f"Privileged user {props.get('name')} has password set to never expire",
|
|
"risk_weight": RISK_WEIGHTS["password_never_expires_admin"],
|
|
})
|
|
|
|
elif kind == "Computer":
|
|
if props.get("unconstraineddelegation", False):
|
|
name = props.get("name", "unknown")
|
|
if "DC" not in name.upper():
|
|
findings.append({
|
|
"source": "BloodHound",
|
|
"category": "Kerberos",
|
|
"rule_id": "unconstrained_delegation",
|
|
"title": f"Unconstrained delegation on non-DC: {name}",
|
|
"description": f"Computer {name} has unconstrained delegation enabled",
|
|
"risk_weight": RISK_WEIGHTS["unconstrained_delegation"],
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def consolidate_findings(pingcastle_findings, bloodhound_findings):
|
|
"""Merge and deduplicate findings from multiple tools."""
|
|
all_findings = pingcastle_findings + bloodhound_findings
|
|
|
|
for finding in all_findings:
|
|
if "risk_weight" not in finding:
|
|
points = finding.get("points", 0)
|
|
if points >= 30:
|
|
finding["risk_weight"] = 10
|
|
elif points >= 20:
|
|
finding["risk_weight"] = 8
|
|
elif points >= 10:
|
|
finding["risk_weight"] = 6
|
|
elif points >= 5:
|
|
finding["risk_weight"] = 4
|
|
else:
|
|
finding["risk_weight"] = 2
|
|
|
|
rule_id = finding.get("rule_id", "")
|
|
if rule_id in RISK_WEIGHTS:
|
|
finding["risk_weight"] = max(finding["risk_weight"], RISK_WEIGHTS[rule_id])
|
|
|
|
all_findings.sort(key=lambda f: f.get("risk_weight", 0), reverse=True)
|
|
return all_findings
|
|
|
|
|
|
def generate_report(findings, output_path):
|
|
"""Generate consolidated AD assessment report."""
|
|
report = {
|
|
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
"total_findings": len(findings),
|
|
"critical": len([f for f in findings if f.get("risk_weight", 0) >= 9]),
|
|
"high": len([f for f in findings if 7 <= f.get("risk_weight", 0) < 9]),
|
|
"medium": len([f for f in findings if 4 <= f.get("risk_weight", 0) < 7]),
|
|
"low": len([f for f in findings if f.get("risk_weight", 0) < 4]),
|
|
"findings": findings,
|
|
"categories": {},
|
|
}
|
|
|
|
for f in findings:
|
|
cat = f.get("category", "Other")
|
|
if cat not in report["categories"]:
|
|
report["categories"][cat] = 0
|
|
report["categories"][cat] += 1
|
|
|
|
with open(output_path, "w", encoding="utf-8") as fh:
|
|
json.dump(report, fh, indent=2)
|
|
|
|
print(f"[+] AD Assessment Report: {output_path}")
|
|
print(f" Total findings: {report['total_findings']}")
|
|
print(f" Critical: {report['critical']}")
|
|
print(f" High: {report['high']}")
|
|
print(f" Medium: {report['medium']}")
|
|
print(f" Low: {report['low']}")
|
|
print(f" Categories: {json.dumps(report['categories'], indent=6)}")
|
|
return report
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="AD Vulnerability Assessment Analyzer")
|
|
parser.add_argument("--pingcastle", help="PingCastle XML report path")
|
|
parser.add_argument("--bloodhound", help="BloodHound JSON export path")
|
|
parser.add_argument("--output", default="ad_assessment_report.json", help="Output report path")
|
|
args = parser.parse_args()
|
|
|
|
pc_findings = []
|
|
bh_findings = []
|
|
|
|
if args.pingcastle:
|
|
print(f"[*] Parsing PingCastle report: {args.pingcastle}")
|
|
pc_findings = parse_pingcastle_xml(args.pingcastle)
|
|
print(f" Found {len(pc_findings)} PingCastle findings")
|
|
|
|
if args.bloodhound:
|
|
print(f"[*] Parsing BloodHound data: {args.bloodhound}")
|
|
bh_findings = parse_bloodhound_json(args.bloodhound)
|
|
print(f" Found {len(bh_findings)} BloodHound findings")
|
|
|
|
if not pc_findings and not bh_findings:
|
|
print("[-] No input files provided. Use --pingcastle and/or --bloodhound")
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
findings = consolidate_findings(pc_findings, bh_findings)
|
|
generate_report(findings, args.output)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|