Add executive summary with prioritized risk findings to both reports

Surfaces blank-password, AS-REP roastable, unconstrained delegation,
lockout, stale-account, empty-group, and password-never-expires
counts as a severity-ranked findings table at the top of the report,
ahead of the full detail tables -- useful as a reorg-planning summary.
This commit is contained in:
2026-08-21 16:17:44 -04:00
parent 60cbfadbae
commit 903f64a40b
2 changed files with 214 additions and 59 deletions
+137 -22
View File
@@ -257,6 +257,121 @@ def audit_object_counts(conn, base_dn):
return counter
def compute_stats(data, args):
ous = data["ous"]
users = data["users"]
computers = data["computers"]
groups = data["groups"]
max_depth = max((o["depth"] for o in ous), default=0)
total_users = len(users)
disabled = sum(1 for u in users if u["disabled"])
stats = {
"total_users": total_users,
"enabled": total_users - disabled,
"disabled": disabled,
"locked": sum(1 for u in users if u["locked"]),
"pwd_never_expires": sum(1 for u in users if u["pwd_never_expires"]),
"pwd_not_required": sum(1 for u in users if u["pwd_not_required"]),
"never_logged_on": sum(1 for u in users if u["never_logged_on"] and not u["disabled"]),
"stale": sum(1 for u in users if u["stale"]),
"admin_count_flagged": sum(1 for u in users if u["admin_count"]),
"trusted_deleg": sum(1 for u in users if u["trusted_for_delegation"]),
"no_preauth": sum(1 for u in users if u["kerberos_preauth_disabled"]),
}
total_comp = len(computers)
comp_disabled = sum(1 for c in computers if c["disabled"])
comp_stats = {
"total": total_comp,
"disabled": comp_disabled,
"stale": sum(1 for c in computers if c["stale"]),
"os_counter": Counter(c["os"] or "Unknown" for c in computers),
}
total_groups = len(groups)
security_groups = sum(1 for g in groups if g["type"] == "Security")
group_stats = {
"total": total_groups,
"security": security_groups,
"distribution": total_groups - security_groups,
"empty": sum(1 for g in groups if g["empty"]),
"scope_counter": Counter(g["scope"] for g in groups),
}
return {
"max_ou_depth": max_depth,
"users": stats,
"computers": comp_stats,
"groups": group_stats,
}
def build_executive_summary(stats, args):
u = stats["users"]
c = stats["computers"]
g = stats["groups"]
findings = []
if u["pwd_not_required"]:
findings.append(("Critical", "User accounts allowing blank passwords", u["pwd_not_required"],
"PASSWD_NOTREQD flag set; remove unless there is a specific reason"))
if u["no_preauth"]:
findings.append(("Critical", "AS-REP roastable accounts (Kerberos pre-auth disabled)", u["no_preauth"],
"Offline password cracking risk; re-enable pre-auth unless required"))
if u["trusted_deleg"]:
findings.append(("Critical", "Accounts trusted for unconstrained delegation", u["trusted_deleg"],
"High-value targets for credential theft; move to constrained/no delegation"))
if u["locked"]:
findings.append(("High", "Currently locked-out user accounts", u["locked"],
"May indicate attack activity or stale service credentials"))
if u["stale"]:
findings.append(("Medium", f"Stale enabled user accounts (>{args.stale_days}d inactive)", u["stale"],
"Candidates for disable/offboarding review"))
if c["stale"]:
findings.append(("Medium", f"Stale enabled computer accounts (>{args.stale_days}d inactive)", c["stale"],
"Likely decommissioned hardware still trusted in the domain"))
if g["empty"]:
findings.append(("Medium", "Empty security/distribution groups", g["empty"],
"Cleanup candidates ahead of OU/group reorg"))
if u["pwd_never_expires"]:
findings.append(("Medium", "Accounts with password-never-expires set", u["pwd_never_expires"],
"Review against password policy; exempt only where justified"))
if u["never_logged_on"]:
findings.append(("Low", "Enabled accounts that have never logged on", u["never_logged_on"],
"Possibly unused/orphaned provisioning; verify before disabling"))
if u["admin_count_flagged"]:
findings.append(("Info", "Accounts with adminCount=1 (current or former privileged)", u["admin_count_flagged"],
"SDProp-protected ACLs persist even after privilege is removed; review membership"))
severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3, "Info": 4}
findings.sort(key=lambda f: severity_order[f[0]])
lines = []
a = lines.append
a("## Executive Summary")
a("")
a(f"- Users: {u['total_users']} total ({u['enabled']} enabled, {u['disabled']} disabled)")
a(f"- Computers: {c['total']} total ({c['disabled']} disabled)")
a(f"- Groups: {g['total']} total ({g['security']} security, {g['distribution']} distribution)")
a(f"- Max OU nesting depth: {stats['max_ou_depth']}")
a("")
if findings:
a("### Risk & Cleanup Findings")
a("")
a("| Severity | Finding | Count | Notes |")
a("|---|---|---|---|")
for severity, finding, count, notes in findings:
a(f"| {severity} | {finding} | {count} | {notes} |")
a("")
else:
a("No notable risk or cleanup findings surfaced by this audit's checks.")
a("")
return "\n".join(lines)
def build_report(data, args):
ous = data["ous"]
users = data["users"]
@@ -264,6 +379,8 @@ def build_report(data, args):
groups = data["groups"]
obj_counts = data["object_counts"]
stats = compute_stats(data, args)
lines = []
a = lines.append
a(f"# Active Directory Audit Report")
@@ -274,6 +391,8 @@ def build_report(data, args):
a(f"- Stale-account threshold: {args.stale_days} days of inactivity")
a(f"")
a(build_executive_summary(stats, args))
a("## Object Type Counts")
a("")
a("| Object Class | Count |")
@@ -285,8 +404,7 @@ def build_report(data, args):
a("## Organizational Units")
a("")
a(f"- Total OUs: {len(ous)}")
max_depth = max((o["depth"] for o in ous), default=0)
a(f"- Maximum nesting depth: {max_depth}")
a(f"- Maximum nesting depth: {stats['max_ou_depth']}")
a("")
a("| OU DN | Depth | Description |")
a("|---|---|---|")
@@ -296,17 +414,16 @@ def build_report(data, args):
a("## Users")
a("")
total_users = len(users)
disabled = sum(1 for u in users if u["disabled"])
enabled = total_users - disabled
locked = sum(1 for u in users if u["locked"])
pwd_never_expires = sum(1 for u in users if u["pwd_never_expires"])
pwd_not_required = sum(1 for u in users if u["pwd_not_required"])
never_logged_on = sum(1 for u in users if u["never_logged_on"] and not u["disabled"])
stale = sum(1 for u in users if u["stale"])
admin_count_flagged = sum(1 for u in users if u["admin_count"])
trusted_deleg = sum(1 for u in users if u["trusted_for_delegation"])
no_preauth = sum(1 for u in users if u["kerberos_preauth_disabled"])
u = stats["users"]
total_users, enabled, disabled = u["total_users"], u["enabled"], u["disabled"]
locked = u["locked"]
pwd_never_expires = u["pwd_never_expires"]
pwd_not_required = u["pwd_not_required"]
never_logged_on = u["never_logged_on"]
stale = u["stale"]
admin_count_flagged = u["admin_count_flagged"]
trusted_deleg = u["trusted_deleg"]
no_preauth = u["no_preauth"]
a(f"- Total user objects: {total_users}")
a(f"- Enabled: {enabled}")
@@ -332,10 +449,9 @@ def build_report(data, args):
a("## Computers")
a("")
total_comp = len(computers)
comp_disabled = sum(1 for c in computers if c["disabled"])
comp_stale = sum(1 for c in computers if c["stale"])
os_counter = Counter(c["os"] or "Unknown" for c in computers)
c = stats["computers"]
total_comp, comp_disabled, comp_stale = c["total"], c["disabled"], c["stale"]
os_counter = c["os_counter"]
a(f"- Total computer objects: {total_comp}")
a(f"- Disabled: {comp_disabled}")
@@ -351,11 +467,10 @@ def build_report(data, args):
a("## Groups")
a("")
total_groups = len(groups)
security_groups = sum(1 for g in groups if g["type"] == "Security")
distribution_groups = total_groups - security_groups
empty_groups = sum(1 for g in groups if g["empty"])
scope_counter = Counter(g["scope"] for g in groups)
gs = stats["groups"]
total_groups, security_groups, distribution_groups = gs["total"], gs["security"], gs["distribution"]
empty_groups = gs["empty"]
scope_counter = gs["scope_counter"]
a(f"- Total groups: {total_groups}")
a(f"- Security groups: {security_groups}")