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:
+137
-22
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user