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:
+77
-37
@@ -193,38 +193,11 @@ $raw | ConvertTo-Json -Depth 6 | Out-File -FilePath $rawPath -Encoding utf8
|
||||
# Build Markdown report
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - building report..."
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
|
||||
function Add-Line([string]$text = "") { [void]$sb.AppendLine($text) }
|
||||
|
||||
Add-Line "# Active Directory Audit Report"
|
||||
Add-Line ""
|
||||
Add-Line "- Search base: ``$SearchBase``"
|
||||
if ($Server) { Add-Line "- Domain controller: ``$Server``" }
|
||||
Add-Line "- Generated: $($now.ToUniversalTime().ToString("o"))"
|
||||
Add-Line "- Stale-account threshold: $StaleDays days of inactivity"
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Object Type Counts"
|
||||
Add-Line ""
|
||||
Add-Line "| Object Class | Count |"
|
||||
Add-Line "|---|---|"
|
||||
foreach ($row in $objectCounts) { Add-Line "| $($row.ObjectClass) | $($row.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Organizational Units"
|
||||
Add-Line ""
|
||||
Add-Line "- Total OUs: $($ous.Count)"
|
||||
# Aggregate stats up front so the executive summary and the detailed
|
||||
# sections below both draw from the same computed values.
|
||||
$maxDepth = ($ous | Measure-Object -Property Depth -Maximum).Maximum
|
||||
Add-Line "- Maximum nesting depth: $maxDepth"
|
||||
Add-Line ""
|
||||
Add-Line "| OU DN | Depth | Description |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($o in ($ous | Sort-Object DN)) { Add-Line "| $($o.DN) | $($o.Depth) | $($o.Description) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Users"
|
||||
Add-Line ""
|
||||
$totalUsers = $users.Count
|
||||
$disabled = ($users | Where-Object Disabled).Count
|
||||
$enabled = $totalUsers - $disabled
|
||||
@@ -237,6 +210,81 @@ $adminCountFlagged = ($users | Where-Object AdminCount).Count
|
||||
$trustedDeleg = ($users | Where-Object TrustedForDelegation).Count
|
||||
$noPreauth = ($users | Where-Object KerberosPreAuthDisabled).Count
|
||||
|
||||
$totalComp = $computers.Count
|
||||
$compDisabled = ($computers | Where-Object Disabled).Count
|
||||
$compStale = ($computers | Where-Object Stale).Count
|
||||
|
||||
$totalGroups = $groups.Count
|
||||
$securityGroups = ($groups | Where-Object { $_.Type -eq "Security" }).Count
|
||||
$distributionGroups = $totalGroups - $securityGroups
|
||||
$emptyGroups = ($groups | Where-Object Empty).Count
|
||||
|
||||
$sb = New-Object System.Text.StringBuilder
|
||||
|
||||
function Add-Line([string]$text = "") { [void]$sb.AppendLine($text) }
|
||||
|
||||
Add-Line "# Active Directory Audit Report"
|
||||
Add-Line ""
|
||||
Add-Line "- Search base: ``$SearchBase``"
|
||||
if ($Server) { Add-Line "- Domain controller: ``$Server``" }
|
||||
Add-Line "- Generated: $($now.ToUniversalTime().ToString("o"))"
|
||||
Add-Line "- Stale-account threshold: $StaleDays days of inactivity"
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Executive Summary"
|
||||
Add-Line ""
|
||||
Add-Line "- Users: $totalUsers total ($enabled enabled, $disabled disabled)"
|
||||
Add-Line "- Computers: $totalComp total ($compDisabled disabled)"
|
||||
Add-Line "- Groups: $totalGroups total ($securityGroups security, $distributionGroups distribution)"
|
||||
Add-Line "- Max OU nesting depth: $maxDepth"
|
||||
Add-Line ""
|
||||
|
||||
$findings = New-Object System.Collections.Generic.List[Object]
|
||||
if ($pwdNotRequired -gt 0) { $findings.Add(@("Critical", "User accounts allowing blank passwords", $pwdNotRequired, "PASSWD_NOTREQD flag set; remove unless there is a specific reason")) }
|
||||
if ($noPreauth -gt 0) { $findings.Add(@("Critical", "AS-REP roastable accounts (Kerberos pre-auth disabled)", $noPreauth, "Offline password cracking risk; re-enable pre-auth unless required")) }
|
||||
if ($trustedDeleg -gt 0) { $findings.Add(@("Critical", "Accounts trusted for unconstrained delegation", $trustedDeleg, "High-value targets for credential theft; move to constrained/no delegation")) }
|
||||
if ($locked -gt 0) { $findings.Add(@("High", "Currently locked-out user accounts", $locked, "May indicate attack activity or stale service credentials")) }
|
||||
if ($staleUsers -gt 0) { $findings.Add(@("Medium", "Stale enabled user accounts (>$StaleDays days inactive)", $staleUsers, "Candidates for disable/offboarding review")) }
|
||||
if ($compStale -gt 0) { $findings.Add(@("Medium", "Stale enabled computer accounts (>$StaleDays days inactive)", $compStale, "Likely decommissioned hardware still trusted in the domain")) }
|
||||
if ($emptyGroups -gt 0) { $findings.Add(@("Medium", "Empty security/distribution groups", $emptyGroups, "Cleanup candidates ahead of OU/group reorg")) }
|
||||
if ($pwdNeverExpires -gt 0) { $findings.Add(@("Medium", "Accounts with password-never-expires set", $pwdNeverExpires, "Review against password policy; exempt only where justified")) }
|
||||
if ($neverLoggedOn -gt 0) { $findings.Add(@("Low", "Enabled accounts that have never logged on", $neverLoggedOn, "Possibly unused/orphaned provisioning; verify before disabling")) }
|
||||
if ($adminCountFlagged -gt 0) { $findings.Add(@("Info", "Accounts with adminCount=1 (current or former privileged)", $adminCountFlagged, "SDProp-protected ACLs persist even after privilege is removed; review membership")) }
|
||||
|
||||
$severityOrder = @{ "Critical" = 0; "High" = 1; "Medium" = 2; "Low" = 3; "Info" = 4 }
|
||||
$findings = $findings | Sort-Object { $severityOrder[$_[0]] }
|
||||
|
||||
if ($findings.Count -gt 0) {
|
||||
Add-Line "### Risk & Cleanup Findings"
|
||||
Add-Line ""
|
||||
Add-Line "| Severity | Finding | Count | Notes |"
|
||||
Add-Line "|---|---|---|---|"
|
||||
foreach ($f in $findings) { Add-Line "| $($f[0]) | $($f[1]) | $($f[2]) | $($f[3]) |" }
|
||||
Add-Line ""
|
||||
} else {
|
||||
Add-Line "No notable risk or cleanup findings surfaced by this audit's checks."
|
||||
Add-Line ""
|
||||
}
|
||||
|
||||
Add-Line "## Object Type Counts"
|
||||
Add-Line ""
|
||||
Add-Line "| Object Class | Count |"
|
||||
Add-Line "|---|---|"
|
||||
foreach ($row in $objectCounts) { Add-Line "| $($row.ObjectClass) | $($row.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Organizational Units"
|
||||
Add-Line ""
|
||||
Add-Line "- Total OUs: $($ous.Count)"
|
||||
Add-Line "- Maximum nesting depth: $maxDepth"
|
||||
Add-Line ""
|
||||
Add-Line "| OU DN | Depth | Description |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($o in ($ous | Sort-Object DN)) { Add-Line "| $($o.DN) | $($o.Depth) | $($o.Description) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "## Users"
|
||||
Add-Line ""
|
||||
Add-Line "- Total user objects: $totalUsers"
|
||||
Add-Line "- Enabled: $enabled"
|
||||
Add-Line "- Disabled: $disabled"
|
||||
@@ -263,9 +311,6 @@ if ($staleUsers -gt 0) {
|
||||
|
||||
Add-Line "## Computers"
|
||||
Add-Line ""
|
||||
$totalComp = $computers.Count
|
||||
$compDisabled = ($computers | Where-Object Disabled).Count
|
||||
$compStale = ($computers | Where-Object Stale).Count
|
||||
Add-Line "- Total computer objects: $totalComp"
|
||||
Add-Line "- Disabled: $compDisabled"
|
||||
Add-Line "- Stale (enabled, inactive > $StaleDays days): $compStale"
|
||||
@@ -280,11 +325,6 @@ Add-Line ""
|
||||
|
||||
Add-Line "## Groups"
|
||||
Add-Line ""
|
||||
$totalGroups = $groups.Count
|
||||
$securityGroups = ($groups | Where-Object { $_.Type -eq "Security" }).Count
|
||||
$distributionGroups = $totalGroups - $securityGroups
|
||||
$emptyGroups = ($groups | Where-Object Empty).Count
|
||||
|
||||
Add-Line "- Total groups: $totalGroups"
|
||||
Add-Line "- Security groups: $securityGroups"
|
||||
Add-Line "- Distribution groups: $distributionGroups"
|
||||
|
||||
Reference in New Issue
Block a user