Add per-object detail lists to each risk finding
Each finding in the Risk & Cleanup table now expands into a full list of the matching accounts/computers/groups (sAMAccountName, last logon or type, DN) so the report can be acted on directly instead of just citing counts. Removed the now-redundant standalone stale-user and empty-group sections since they're covered by the finding lists.
This commit is contained in:
+60
-34
@@ -239,28 +239,76 @@ Add-Line "- Groups: $totalGroups total ($securityGroups security, $distributionG
|
||||
Add-Line "- Max OU nesting depth: $maxDepth"
|
||||
Add-Line ""
|
||||
|
||||
# Each finding: Severity, Title, Notes, column labels for the detail table, matching rows.
|
||||
$USER_COLS = @("Account", "Last Logon", "DN")
|
||||
function Format-Finding($Severity, $Title, $Notes, $Cols, $Rows) {
|
||||
[PSCustomObject]@{ Severity = $Severity; Title = $Title; Notes = $Notes; Cols = $Cols; Rows = @($Rows) }
|
||||
}
|
||||
function UserRows($Predicate) {
|
||||
$users | Where-Object $Predicate | Sort-Object SamAccountName | ForEach-Object {
|
||||
[PSCustomObject]@{ Col0 = $_.SamAccountName; Col1 = $_.LastLogon; DN = $_.DN }
|
||||
}
|
||||
}
|
||||
|
||||
$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")) }
|
||||
if ($pwdNotRequired -gt 0) {
|
||||
$findings.Add((Format-Finding "Critical" "User accounts allowing blank passwords" "PASSWD_NOTREQD flag set; remove unless there is a specific reason" $USER_COLS (UserRows { $_.PwdNotRequired })))
|
||||
}
|
||||
if ($noPreauth -gt 0) {
|
||||
$findings.Add((Format-Finding "Critical" "AS-REP roastable accounts (Kerberos pre-auth disabled)" "Offline password cracking risk; re-enable pre-auth unless required" $USER_COLS (UserRows { $_.KerberosPreAuthDisabled })))
|
||||
}
|
||||
if ($trustedDeleg -gt 0) {
|
||||
$findings.Add((Format-Finding "Critical" "Accounts trusted for unconstrained delegation" "High-value targets for credential theft; move to constrained/no delegation" $USER_COLS (UserRows { $_.TrustedForDelegation })))
|
||||
}
|
||||
if ($locked -gt 0) {
|
||||
$findings.Add((Format-Finding "High" "Currently locked-out user accounts" "May indicate attack activity or stale service credentials" $USER_COLS (UserRows { $_.Locked })))
|
||||
}
|
||||
if ($staleUsers -gt 0) {
|
||||
$findings.Add((Format-Finding "Medium" "Stale enabled user accounts (>$StaleDays days inactive)" "Candidates for disable/offboarding review" $USER_COLS (UserRows { $_.Stale })))
|
||||
}
|
||||
if ($compStale -gt 0) {
|
||||
$compRows = $computers | Where-Object Stale | Sort-Object SamAccountName | ForEach-Object {
|
||||
[PSCustomObject]@{ Col0 = $_.SamAccountName; Col1 = $_.LastLogon; DN = $_.DN }
|
||||
}
|
||||
$findings.Add((Format-Finding "Medium" "Stale enabled computer accounts (>$StaleDays days inactive)" "Likely decommissioned hardware still trusted in the domain" @("Computer", "Last Logon", "DN") $compRows))
|
||||
}
|
||||
if ($emptyGroups -gt 0) {
|
||||
$emptyRows = $groups | Where-Object Empty | Sort-Object SamAccountName | ForEach-Object {
|
||||
[PSCustomObject]@{ Col0 = $_.SamAccountName; Col1 = $_.Type; DN = $_.DN }
|
||||
}
|
||||
$findings.Add((Format-Finding "Medium" "Empty security/distribution groups" "Cleanup candidates ahead of OU/group reorg" @("Group", "Type", "DN") $emptyRows))
|
||||
}
|
||||
if ($pwdNeverExpires -gt 0) {
|
||||
$findings.Add((Format-Finding "Medium" "Accounts with password-never-expires set" "Review against password policy; exempt only where justified" $USER_COLS (UserRows { $_.PwdNeverExpires })))
|
||||
}
|
||||
if ($neverLoggedOn -gt 0) {
|
||||
$findings.Add((Format-Finding "Low" "Enabled accounts that have never logged on" "Possibly unused/orphaned provisioning; verify before disabling" $USER_COLS (UserRows { $_.NeverLoggedOn })))
|
||||
}
|
||||
if ($adminCountFlagged -gt 0) {
|
||||
$findings.Add((Format-Finding "Info" "Accounts with adminCount=1 (current or former privileged)" "SDProp-protected ACLs persist even after privilege is removed; review membership" $USER_COLS (UserRows { $_.AdminCount })))
|
||||
}
|
||||
|
||||
$severityOrder = @{ "Critical" = 0; "High" = 1; "Medium" = 2; "Low" = 3; "Info" = 4 }
|
||||
$findings = $findings | Sort-Object { $severityOrder[$_[0]] }
|
||||
$findings = $findings | Sort-Object { $severityOrder[$_.Severity] }
|
||||
|
||||
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]) |" }
|
||||
foreach ($f in $findings) { Add-Line "| $($f.Severity) | $($f.Title) | $($f.Rows.Count) | $($f.Notes) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "### Finding Detail Lists"
|
||||
Add-Line ""
|
||||
foreach ($f in $findings) {
|
||||
Add-Line "#### [$($f.Severity)] $($f.Title)"
|
||||
Add-Line ""
|
||||
Add-Line "| $($f.Cols[0]) | $($f.Cols[1]) | $($f.Cols[2]) |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($row in $f.Rows) { Add-Line "| $($row.Col0) | $($row.Col1) | $($row.DN) |" }
|
||||
Add-Line ""
|
||||
}
|
||||
} else {
|
||||
Add-Line "No notable risk or cleanup findings surfaced by this audit's checks."
|
||||
Add-Line ""
|
||||
@@ -298,17 +346,6 @@ Add-Line "- Trusted for unconstrained delegation: **$trustedDeleg**"
|
||||
Add-Line "- Kerberos pre-auth disabled (AS-REP roastable): **$noPreauth**"
|
||||
Add-Line ""
|
||||
|
||||
if ($staleUsers -gt 0) {
|
||||
Add-Line "### Stale user accounts"
|
||||
Add-Line ""
|
||||
Add-Line "| sAMAccountName | Last Logon | DN |"
|
||||
Add-Line "|---|---|---|"
|
||||
foreach ($u in ($users | Where-Object Stale | Sort-Object LastLogon)) {
|
||||
Add-Line "| $($u.SamAccountName) | $($u.LastLogon) | $($u.DN) |"
|
||||
}
|
||||
Add-Line ""
|
||||
}
|
||||
|
||||
Add-Line "## Computers"
|
||||
Add-Line ""
|
||||
Add-Line "- Total computer objects: $totalComp"
|
||||
@@ -345,17 +382,6 @@ foreach ($g in ($groups | Sort-Object -Property MemberCount -Descending | Select
|
||||
}
|
||||
Add-Line ""
|
||||
|
||||
if ($emptyGroups -gt 0) {
|
||||
Add-Line "### Empty groups (candidates for cleanup)"
|
||||
Add-Line ""
|
||||
Add-Line "| Group | DN |"
|
||||
Add-Line "|---|---|"
|
||||
foreach ($g in ($groups | Where-Object Empty | Sort-Object SamAccountName)) {
|
||||
Add-Line "| $($g.SamAccountName) | $($g.DN) |"
|
||||
}
|
||||
Add-Line ""
|
||||
}
|
||||
|
||||
$reportPath = Join-Path $OutDir "ad_audit_report_$ts.md"
|
||||
$sb.ToString() | Out-File -FilePath $reportPath -Encoding utf8
|
||||
|
||||
|
||||
Reference in New Issue
Block a user