vulnerability management

Performing Active Directory Vulnerability Assessment

Assess Active Directory security posture using PingCastle, BloodHound, and Purple Knight to identify misconfigurations, privilege escalation paths, and attack vectors.

active-directoryad-securitybloodhoundkerberosldappingcastleprivilege-escalationpurple-knight
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Active Directory (AD) is the primary identity and access management system in most enterprise environments, making it a critical attack target. This skill covers comprehensive AD security assessment using PingCastle for health checks, BloodHound for attack path analysis, and Purple Knight for security posture scoring. These tools identify misconfigurations, excessive privileges, Kerberos weaknesses, and lateral movement opportunities.

When to Use

  • When conducting security assessments that involve performing active directory vulnerability assessment
  • When following incident response procedures for related security events
  • When performing scheduled security testing or auditing activities
  • When validating security controls through hands-on testing

Prerequisites

  • Domain-joined workstation or domain admin access for scanning
  • PingCastle (https://github.com/netwrix/pingcastle)
  • BloodHound Community Edition with SharpHound collector
  • Purple Knight from Semperis (free community tool)
  • Python 3.9+ for analysis scripts
  • .NET Framework 4.7+ for PingCastle on Windows

Tool 1: PingCastle Health Check

Installation and Execution

# Download PingCastle
Invoke-WebRequest -Uri "https://github.com/netwrix/pingcastle/releases/latest/download/PingCastle.zip" `
  -OutFile "PingCastle.zip"
Expand-Archive PingCastle.zip -DestinationPath C:\Tools\PingCastle
 
# Run health check against current domain
cd C:\Tools\PingCastle
.\PingCastle.exe --healthcheck
 
# Run health check against specific domain
.\PingCastle.exe --healthcheck --server dc01.corp.local --user CORP\scanner_account --password P@ssw0rd
 
# Run in scanner mode for multiple domains
.\PingCastle.exe --scanner --scannerlp
 
# Generate consolidated report
.\PingCastle.exe --healthcheck --level Full

PingCastle Scoring Categories

Category Description Risk Areas
Stale Objects Inactive accounts, old passwords, obsolete OS Ghost accounts, expired credentials
Privileged Accounts Excessive admin rights, nested groups Domain Admin sprawl, SID history
Trusts Forest and domain trust configurations Transitive trust abuse, SID filtering
Anomalies Security setting deviations GPO misconfigurations, schema issues

Key PingCastle Checks

# Critical items to review in PingCastle report:
- Accounts with "Password Never Expires" flag
- Accounts with Kerberos pre-authentication disabled (AS-REP roastable)
- Accounts with Kerberos delegation (unconstrained/constrained)
- Domain Controllers running unsupported OS versions
- AdminSDHolder permission modifications
- Accounts in privileged groups (Domain Admins, Enterprise Admins, Schema Admins)
- Trust relationships with SID filtering disabled
- GPO vulnerabilities allowing privilege escalation

Tool 2: BloodHound Attack Path Analysis

SharpHound Data Collection

# Download SharpHound collector
# https://github.com/SpecterOps/BloodHound/tree/main/packages/csharp/SharpHound
 
# Run SharpHound collection (all methods)
.\SharpHound.exe --collectionmethods All --domain corp.local --zipfilename bloodhound_data.zip
 
# Stealthy collection (minimal noise)
.\SharpHound.exe --collectionmethods Session,LoggedOn --domain corp.local --stealth
 
# Collection with specific domain controller
.\SharpHound.exe --collectionmethods All --domain corp.local --domaincontroller dc01.corp.local
 
# Run via PowerShell
Import-Module .\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -Domain corp.local -OutputDirectory C:\BH_Data

BloodHound CE Setup

# Deploy BloodHound Community Edition with Docker
curl -L https://ghst.ly/getbhce -o docker-compose.yml
docker compose up -d
 
# Access BloodHound CE at http://localhost:8080
# Default credentials shown in docker compose logs
 
# Upload SharpHound data through web UI or API
curl -X POST "http://localhost:8080/api/v2/file-upload/start" \
  -H "Authorization: Bearer $BH_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"fileName": "bloodhound_data.zip"}'

Critical BloodHound Queries

# Find shortest path to Domain Admin
MATCH p=shortestPath((u:User)-[*1..]->(g:Group {name:"DOMAIN ADMINS@CORP.LOCAL"}))
WHERE u.name <> "ADMINISTRATOR@CORP.LOCAL"
RETURN p
 
# Find Kerberoastable accounts with admin privileges
MATCH (u:User {hasspn:true})-[:MemberOf*1..]->(g:Group)
WHERE g.name CONTAINS "ADMIN"
RETURN u.name, u.serviceprincipalnames
 
# Find computers where Domain Admins are logged in
MATCH (c:Computer)-[:HasSession]->(u:User)-[:MemberOf*1..]->(g:Group {name:"DOMAIN ADMINS@CORP.LOCAL"})
RETURN c.name, u.name
 
# Find AS-REP roastable accounts
MATCH (u:User {dontreqpreauth:true})
RETURN u.name, u.description
 
# Find unconstrained delegation hosts
MATCH (c:Computer {unconstraineddelegation:true})
WHERE NOT c.name CONTAINS "DC"
RETURN c.name
 
# Find GPO abuse paths
MATCH p=(u:User)-[:GenericAll|GenericWrite|WriteOwner|WriteDacl]->(g:GPO)
RETURN p

Tool 3: Purple Knight Assessment

# Download Purple Knight from https://www.purple-knight.com/
# Run as domain admin or with appropriate read permissions
 
.\PurpleKnight.exe
 
# Purple Knight checks 130+ security indicators across:
# - Account Security (password policies, privileged accounts)
# - AD Infrastructure (replication, DNS, LDAP signing)
# - Group Policy (GPO permissions, security settings)
# - Kerberos Security (delegation, encryption types, SPN)
# - AD Delegation (AdminSDHolder, OU permissions)

Purple Knight Score Categories

Score Range Rating Action Required
90-100 Excellent Maintain current posture
75-89 Good Address high-risk findings
60-74 Fair Prioritize remediation plan
40-59 Poor Immediate remediation required
0-39 Critical Emergency response needed

Common AD Vulnerabilities

1. Kerberoasting Exposure

# Find SPNs assigned to user accounts (Kerberoasting targets)
Get-ADUser -Filter {ServicePrincipalName -ne "$null"} -Properties ServicePrincipalName |
  Select-Object Name, ServicePrincipalName, PasswordLastSet, Enabled

2. AS-REP Roasting Exposure

# Find accounts with pre-auth disabled
Get-ADUser -Filter {DoesNotRequirePreAuth -eq $true} -Properties DoesNotRequirePreAuth |
  Select-Object Name, DoesNotRequirePreAuth, Enabled

3. LLMNR/NBT-NS Poisoning Risk

# Check if LLMNR is disabled via GPO
Get-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\DNSClient" -Name EnableMulticast -ErrorAction SilentlyContinue

4. Excessive Privileged Group Membership

# Count members in critical groups
$groups = @("Domain Admins", "Enterprise Admins", "Schema Admins", "Account Operators", "Backup Operators")
foreach ($group in $groups) {
    $count = (Get-ADGroupMember -Identity $group -Recursive).Count
    Write-Output "$group : $count members"
}

Remediation Priorities

Finding Risk Remediation
Kerberoastable admin accounts Critical Remove SPNs or use MSA/gMSA
Unconstrained delegation on non-DCs Critical Switch to constrained/RBCD
Password Never Expires on admins High Enable password rotation policy
AS-REP roastable accounts High Enable Kerberos pre-authentication
AdminSDHolder modification High Audit and restore default ACLs
Stale computer accounts (90+ days) Medium Disable and move to quarantine OU
LDAP signing not enforced Medium Enable via GPO on all DCs

References

Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md2.5 KB

Active Directory Vulnerability Assessment - API Reference

PingCastle XML Report

PingCastle generates XML health check reports with scoring across four categories:

Category Description
StaleObjects Dormant accounts, old computer objects, unused groups
PrivilegiedGroup Excessive admin memberships, unprotected privileged accounts
Trust Insecure trust relationships, SID filtering
Anomaly Unusual configurations, legacy protocols

Score Interpretation

  • 0-20: Excellent
  • 21-50: Good
  • 51-75: Needs improvement
  • 76-100: Critical

XML Structure

<HealthcheckData>
  <GlobalScore>45</GlobalScore>
  <StaleObjectsScore>15</StaleObjectsScore>
  <PrivilegiedGroupScore>20</PrivilegiedGroupScore>
  <RiskRules>
    <HealthcheckRiskRule>
      <RiskId>P-AdminCount</RiskId>
      <Category>PrivilegiedGroup</Category>
      <Points>30</Points>
      <Rationale>15 accounts with adminCount=1</Rationale>
    </HealthcheckRiskRule>
  </RiskRules>
</HealthcheckData>

LDAP Checks (ldap3)

Password Policy Attributes

Attribute Type Description
minPwdLength int Minimum password length
lockoutThreshold int Failed attempts before lockout (0 = disabled)
pwdHistoryLength int Number of remembered passwords
maxPwdAge interval Maximum password age

krbtgt Account

The krbtgt account key encrypts all Kerberos tickets. If compromised (Golden Ticket attack), all tickets must be invalidated by resetting krbtgt password twice. Recommended rotation: every 180 days.

conn.search(base_dn, "(&(objectClass=user)(sAMAccountName=krbtgt))",
            attributes=["pwdLastSet"])

Vulnerability Categories

Critical

  • No account lockout policy
  • krbtgt password > 180 days old
  • PingCastle risk rules with 50+ points

High

  • Minimum password length < 12
  • Accounts with adminCount=1 not in privileged groups
  • Legacy protocols enabled (NTLMv1, LM hashes)

Medium

  • Password history < 12
  • Stale computer objects > 90 days
  • Unconstrained delegation

Output Schema

{
  "report": "ad_vulnerability_assessment",
  "pingcastle_scores": {"global": 45, "staleobjects": 15},
  "total_findings": 20,
  "severity_summary": {"critical": 3, "high": 10, "medium": 7}
}

CLI Usage

python agent.py --pingcastle-xml report.xml --server ldaps://dc.example.com --username "DOMAIN\\user" --password "pass" --output report.json
standards.md1.6 KB

Standards and References - AD Vulnerability Assessment

Primary Standards

MITRE ATT&CK - Active Directory Techniques

  • URL: https://attack.mitre.org/
  • Key Techniques:
    • T1558.003 - Kerberoasting
    • T1558.004 - AS-REP Roasting
    • T1557.001 - LLMNR/NBT-NS Poisoning
    • T1484.001 - Group Policy Modification
    • T1134.005 - SID-History Injection
    • T1003.006 - DCSync

CIS Benchmark for Active Directory

Microsoft Security Compliance Toolkit

NIST SP 800-63B

NSA Active Directory Security Guidance

  • Title: Detecting and Preventing Active Directory Attacks
  • Relevance: NSA recommendations for securing AD against common attack techniques

Tools

Tool License URL
PingCastle Open Source (Netwrix) https://github.com/netwrix/pingcastle
BloodHound CE Apache 2.0 https://github.com/SpecterOps/BloodHound
SharpHound Apache 2.0 https://github.com/SpecterOps/BloodHound
Purple Knight Free Community https://www.purple-knight.com/
ADRecon MIT https://github.com/adrecon/ADRecon
Testimo MIT https://github.com/EvotecIT/Testimo
workflows.md2.0 KB

Workflows - AD Vulnerability Assessment

Workflow 1: Comprehensive AD Security Assessment

Steps

  1. Run PingCastle health check to get overall AD security score
  2. Review PingCastle report for stale objects, privilege issues, trust problems, and anomalies
  3. Run SharpHound data collection against the domain
  4. Upload SharpHound data to BloodHound CE
  5. Execute critical BloodHound queries (shortest path to DA, Kerberoastable admins, delegation issues)
  6. Run Purple Knight for additional security indicator checks
  7. Consolidate findings from all three tools into unified report
  8. Prioritize findings by risk severity and attack path impact
  9. Generate remediation plan with specific PowerShell/GPO fix commands

Workflow 2: Attack Path Remediation

Steps

  1. Identify top 5 shortest attack paths to Domain Admin from BloodHound
  2. For each path, determine the weakest link (misconfigured ACL, session reuse, etc.)
  3. Remediate weakest links to break attack paths
  4. Re-run BloodHound collection to verify paths are eliminated
  5. Document remediated paths and remaining risk

Workflow 3: Privileged Account Hardening

Steps

  1. Export all members of privileged groups from PingCastle report
  2. Validate each account has legitimate business need for privilege
  3. Remove unnecessary privileged group memberships
  4. Implement tiered administration model (Tier 0/1/2)
  5. Enable Protected Users group for sensitive accounts
  6. Configure AdminSDHolder with correct ACLs
  7. Verify changes with follow-up PingCastle scan

Workflow 4: Kerberos Security Hardening

Steps

  1. Identify all Kerberoastable accounts from BloodHound
  2. Convert user-assigned SPNs to Managed Service Accounts (MSA/gMSA) where possible
  3. For remaining SPNs, ensure 25+ character passwords with rotation
  4. Disable DES and RC4 encryption for Kerberos
  5. Enable AES-256 encryption for all accounts
  6. Enable Kerberos pre-authentication for all accounts
  7. Configure constrained delegation to replace unconstrained delegation

Scripts 2

agent.py6.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Active Directory Vulnerability Assessment agent - parses PingCastle XML
reports and performs LDAP checks to assess AD security posture against
common vulnerability categories."""

import argparse
import json
import xml.etree.ElementTree as ET
from collections import Counter
from datetime import datetime
from pathlib import Path

try:
    from ldap3 import Server, Connection, ALL, SUBTREE
except ImportError:
    Connection = None


def parse_pingcastle_report(xml_path: str) -> dict:
    """Parse PingCastle XML health check report."""
    tree = ET.parse(xml_path)
    root = tree.getroot()

    scores = {}
    for score_elem in root.iter("GlobalScore"):
        scores["global"] = int(score_elem.text) if score_elem.text else 0
    for cat in ("StaleObjectsScore", "PrivilegiedGroupScore",
                "TrustScore", "AnomalyScore"):
        elem = root.find(f".//{cat}")
        if elem is not None and elem.text:
            scores[cat.replace("Score", "").lower()] = int(elem.text)

    risks = []
    for rule in root.iter("HealthcheckRiskRule"):
        rationale = rule.find("Rationale")
        category = rule.find("Category")
        points = rule.find("Points")
        risks.append({
            "rule": rule.find("RiskId").text if rule.find("RiskId") is not None else "",
            "category": category.text if category is not None else "",
            "points": int(points.text) if points is not None and points.text else 0,
            "rationale": rationale.text if rationale is not None else "",
        })

    return {"scores": scores, "risks": risks}


def check_password_policy_ldap(server_url: str, username: str, password: str) -> list[dict]:
    """Check domain password policy via LDAP."""
    if Connection is None:
        return [{"error": "ldap3 not installed"}]
    srv = Server(server_url, get_info=ALL, use_ssl=True)
    conn = Connection(srv, user=username, password=password, auto_bind=True)
    base_dn = conn.server.info.other.get("defaultNamingContext", [""])[0]

    conn.search(base_dn, "(objectClass=domain)", search_scope=SUBTREE,
                attributes=["minPwdLength", "lockoutThreshold", "pwdHistoryLength",
                             "maxPwdAge", "minPwdAge"])
    findings = []
    if conn.entries:
        entry = conn.entries[0]
        min_len = int(str(entry.minPwdLength)) if hasattr(entry, "minPwdLength") else 0
        lockout = int(str(entry.lockoutThreshold)) if hasattr(entry, "lockoutThreshold") else 0
        history = int(str(entry.pwdHistoryLength)) if hasattr(entry, "pwdHistoryLength") else 0

        if min_len < 12:
            findings.append({"check": "min_password_length", "value": min_len,
                             "severity": "high", "detail": f"Min length {min_len} < 12"})
        if lockout == 0:
            findings.append({"check": "account_lockout", "value": lockout,
                             "severity": "critical", "detail": "No account lockout policy"})
        if history < 12:
            findings.append({"check": "password_history", "value": history,
                             "severity": "medium", "detail": f"History {history} < 12"})
    conn.unbind()
    return findings


def check_krbtgt_age(server_url: str, username: str, password: str) -> list[dict]:
    """Check krbtgt account password age."""
    if Connection is None:
        return []
    srv = Server(server_url, get_info=ALL, use_ssl=True)
    conn = Connection(srv, user=username, password=password, auto_bind=True)
    base_dn = conn.server.info.other.get("defaultNamingContext", [""])[0]

    conn.search(base_dn, "(&(objectClass=user)(sAMAccountName=krbtgt))",
                search_scope=SUBTREE, attributes=["pwdLastSet"])
    findings = []
    if conn.entries:
        pwd_set = conn.entries[0].pwdLastSet.value
        if pwd_set:
            age_days = (datetime.utcnow() - pwd_set.replace(tzinfo=None)).days
            if age_days > 180:
                findings.append({
                    "check": "krbtgt_password_age",
                    "value": age_days,
                    "severity": "critical",
                    "detail": f"krbtgt password is {age_days} days old (reset recommended every 180 days)",
                })
    conn.unbind()
    return findings


def assess_pingcastle_risks(risks: list[dict]) -> list[dict]:
    """Convert PingCastle risk rules into standardized findings."""
    findings = []
    for risk in risks:
        severity = "critical" if risk["points"] >= 50 else "high" if risk["points"] >= 20 else "medium" if risk["points"] >= 5 else "low"
        findings.append({
            "type": f"pingcastle_{risk['rule']}",
            "severity": severity,
            "category": risk["category"],
            "points": risk["points"],
            "detail": risk["rationale"],
        })
    return findings


def generate_report(xml_path: str = None, server_url: str = None,
                    username: str = None, password: str = None) -> dict:
    """Run all assessments and build consolidated report."""
    findings = []
    scores = {}

    if xml_path:
        pc_data = parse_pingcastle_report(xml_path)
        scores = pc_data["scores"]
        findings.extend(assess_pingcastle_risks(pc_data["risks"]))

    if server_url and username and password:
        findings.extend(check_password_policy_ldap(server_url, username, password))
        findings.extend(check_krbtgt_age(server_url, username, password))

    severity_counts = Counter(f.get("severity", "info") for f in findings)
    return {
        "report": "ad_vulnerability_assessment",
        "generated_at": datetime.utcnow().isoformat() + "Z",
        "pingcastle_scores": scores,
        "total_findings": len(findings),
        "severity_summary": dict(severity_counts),
        "findings": findings,
    }


def main():
    parser = argparse.ArgumentParser(description="AD Vulnerability Assessment Agent")
    parser.add_argument("--pingcastle-xml", help="PingCastle XML report file")
    parser.add_argument("--server", help="LDAP server URL for live checks")
    parser.add_argument("--username", help="Domain username")
    parser.add_argument("--password", help="Password")
    parser.add_argument("--output", help="Output JSON file path")
    args = parser.parse_args()

    if not args.pingcastle_xml and not args.server:
        parser.error("At least --pingcastle-xml or --server is required")

    report = generate_report(args.pingcastle_xml, args.server, args.username, args.password)
    output = json.dumps(report, indent=2)
    if args.output:
        Path(args.output).write_text(output, encoding="utf-8")
        print(f"Report written to {args.output}")
    else:
        print(output)


if __name__ == "__main__":
    main()
process.py7.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/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()

Assets 1

template.mdtext/markdown · 2.4 KB
Keep exploring