Add AD audit scripts (Python/LDAP and PowerShell/RSAT)
Read-only structural/health audit of Active Directory: OU tree, user and computer account status/hygiene flags, and group breakdown. Two equivalent implementations depending on available access -- raw LDAP via ldap3, or Get-AD* cmdlets via RSAT on a domain-joined machine.
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Read-only Active Directory structure/health audit using the RSAT ActiveDirectory module.
|
||||
|
||||
.DESCRIPTION
|
||||
Domain-joined-machine counterpart to ad_audit.py (the LDAP/ldap3 version). Produces the
|
||||
same data points -- OUs, users, computers, groups, object type counts -- using
|
||||
Get-AD* cmdlets instead of raw LDAP. Requires the ActiveDirectory PowerShell module
|
||||
(RSAT) and only performs reads; no changes are made to the directory.
|
||||
|
||||
.PARAMETER Server
|
||||
Domain controller to query. Defaults to the domain of the current user's logon.
|
||||
|
||||
.PARAMETER SearchBase
|
||||
Distinguished name to scope the search to. Defaults to the domain root.
|
||||
|
||||
.PARAMETER StaleDays
|
||||
Days of inactivity (LastLogonDate) before an enabled account is flagged stale. Default 90.
|
||||
|
||||
.PARAMETER Credential
|
||||
Optional PSCredential to bind with. If omitted, uses the current logon session
|
||||
(typical when run interactively on a domain-joined machine as a normal user).
|
||||
|
||||
.PARAMETER OutDir
|
||||
Directory to write the Markdown report and raw JSON into. Default .\reports relative
|
||||
to the current working directory.
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-ADAudit.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-ADAudit.ps1 -Server dc01.corp.example.com -SearchBase "OU=Corp,DC=corp,DC=example,DC=com" -StaleDays 120
|
||||
|
||||
.EXAMPLE
|
||||
.\Invoke-ADAudit.ps1 -Credential (Get-Credential)
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Server,
|
||||
[string]$SearchBase,
|
||||
[int]$StaleDays = 90,
|
||||
[System.Management.Automation.PSCredential]$Credential,
|
||||
[string]$OutDir = ".\reports"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not (Get-Module -ListAvailable -Name ActiveDirectory)) {
|
||||
Write-Error "ActiveDirectory module not found. Install RSAT: Add-WindowsCapability -Online -Name 'Rsat.ActiveDirectory.DS-LDS.Tools~~~~0.0.1.0'"
|
||||
exit 1
|
||||
}
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
|
||||
$adParams = @{}
|
||||
if ($Server) { $adParams["Server"] = $Server }
|
||||
if ($Credential) { $adParams["Credential"] = $Credential }
|
||||
|
||||
if (-not $SearchBase) {
|
||||
$domain = Get-ADDomain @adParams
|
||||
$SearchBase = $domain.DistinguishedName
|
||||
}
|
||||
|
||||
Write-Host "Auditing base: $SearchBase"
|
||||
if ($Server) { Write-Host "Domain controller: $Server" }
|
||||
|
||||
# --- UserAccountControl bit flags ---
|
||||
$UAC_PASSWD_NOTREQD = 0x0020
|
||||
$UAC_DONT_EXPIRE_PASSWD = 0x10000
|
||||
$UAC_SMARTCARD_REQUIRED = 0x40000
|
||||
$UAC_TRUSTED_FOR_DELEGATION = 0x80000
|
||||
$UAC_DONT_REQ_PREAUTH = 0x400000
|
||||
|
||||
$now = Get-Date
|
||||
$staleCutoff = $now.AddDays(-$StaleDays)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Object type counts (whole subtree)
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - object type counts..."
|
||||
$allObjects = Get-ADObject -SearchBase $SearchBase -Filter * -Properties objectClass @adParams
|
||||
$objectCounts = $allObjects | Group-Object -Property { $_.ObjectClass } | Sort-Object Count -Descending |
|
||||
ForEach-Object { [PSCustomObject]@{ ObjectClass = $_.Name; Count = $_.Count } }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OUs
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - organizational units..."
|
||||
$ous = Get-ADOrganizationalUnit -SearchBase $SearchBase -Filter * -Properties Description, whenCreated @adParams |
|
||||
ForEach-Object {
|
||||
[PSCustomObject]@{
|
||||
DN = $_.DistinguishedName
|
||||
Name = $_.Name
|
||||
Description = $_.Description
|
||||
Created = $_.whenCreated
|
||||
Depth = ([regex]::Matches($_.DistinguishedName, "OU=")).Count
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Users
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - users..."
|
||||
$userProps = @(
|
||||
"sAMAccountName", "userPrincipalName", "userAccountControl", "LastLogonDate",
|
||||
"PasswordLastSet", "whenCreated", "adminCount", "MemberOf", "Enabled", "LockedOut"
|
||||
)
|
||||
$users = Get-ADUser -SearchBase $SearchBase -Filter * -Properties $userProps @adParams | ForEach-Object {
|
||||
$uac = [int]$_.userAccountControl
|
||||
$lastLogon = $_.LastLogonDate
|
||||
$neverLoggedOn = -not $lastLogon
|
||||
$stale = ($_.Enabled) -and $lastLogon -and ($lastLogon -lt $staleCutoff)
|
||||
|
||||
[PSCustomObject]@{
|
||||
DN = $_.DistinguishedName
|
||||
SamAccountName = $_.sAMAccountName
|
||||
UPN = $_.userPrincipalName
|
||||
Disabled = -not $_.Enabled
|
||||
Locked = [bool]$_.LockedOut
|
||||
PwdNeverExpires = [bool]($uac -band $UAC_DONT_EXPIRE_PASSWD)
|
||||
PwdNotRequired = [bool]($uac -band $UAC_PASSWD_NOTREQD)
|
||||
SmartcardRequired = [bool]($uac -band $UAC_SMARTCARD_REQUIRED)
|
||||
TrustedForDelegation = [bool]($uac -band $UAC_TRUSTED_FOR_DELEGATION)
|
||||
KerberosPreAuthDisabled = [bool]($uac -band $UAC_DONT_REQ_PREAUTH)
|
||||
AdminCount = [bool]($_.adminCount -gt 0)
|
||||
LastLogon = $lastLogon
|
||||
NeverLoggedOn = $neverLoggedOn -and $_.Enabled
|
||||
Stale = $stale
|
||||
PasswordLastSet = $_.PasswordLastSet
|
||||
Created = $_.whenCreated
|
||||
GroupCount = (@($_.MemberOf)).Count
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Computers
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - computers..."
|
||||
$compProps = @("sAMAccountName", "userAccountControl", "LastLogonDate", "OperatingSystem",
|
||||
"OperatingSystemVersion", "whenCreated", "Enabled")
|
||||
$computers = Get-ADComputer -SearchBase $SearchBase -Filter * -Properties $compProps @adParams | ForEach-Object {
|
||||
$lastLogon = $_.LastLogonDate
|
||||
$stale = ($_.Enabled) -and $lastLogon -and ($lastLogon -lt $staleCutoff)
|
||||
|
||||
[PSCustomObject]@{
|
||||
DN = $_.DistinguishedName
|
||||
SamAccountName = $_.sAMAccountName
|
||||
OS = $_.OperatingSystem
|
||||
OSVersion = $_.OperatingSystemVersion
|
||||
Disabled = -not $_.Enabled
|
||||
LastLogon = $lastLogon
|
||||
Stale = $stale
|
||||
Created = $_.whenCreated
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Groups
|
||||
# ---------------------------------------------------------------------------
|
||||
Write-Host " - groups..."
|
||||
$groups = Get-ADGroup -SearchBase $SearchBase -Filter * -Properties Description, whenCreated, Members, GroupCategory, GroupScope @adParams |
|
||||
ForEach-Object {
|
||||
$memberCount = (@($_.Members)).Count
|
||||
[PSCustomObject]@{
|
||||
DN = $_.DistinguishedName
|
||||
SamAccountName = $_.SamAccountName
|
||||
Type = $_.GroupCategory.ToString()
|
||||
Scope = $_.GroupScope.ToString()
|
||||
MemberCount = $memberCount
|
||||
Empty = ($memberCount -eq 0)
|
||||
Description = $_.Description
|
||||
Created = $_.whenCreated
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Write raw data
|
||||
# ---------------------------------------------------------------------------
|
||||
if (-not (Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null }
|
||||
$ts = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||
|
||||
$raw = [PSCustomObject]@{
|
||||
ObjectCounts = $objectCounts
|
||||
OUs = $ous
|
||||
Users = $users
|
||||
Computers = $computers
|
||||
Groups = $groups
|
||||
}
|
||||
$rawPath = Join-Path $OutDir "ad_audit_raw_$ts.json"
|
||||
$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)"
|
||||
$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
|
||||
$locked = ($users | Where-Object Locked).Count
|
||||
$pwdNeverExpires = ($users | Where-Object PwdNeverExpires).Count
|
||||
$pwdNotRequired = ($users | Where-Object PwdNotRequired).Count
|
||||
$neverLoggedOn = ($users | Where-Object NeverLoggedOn).Count
|
||||
$staleUsers = ($users | Where-Object Stale).Count
|
||||
$adminCountFlagged = ($users | Where-Object AdminCount).Count
|
||||
$trustedDeleg = ($users | Where-Object TrustedForDelegation).Count
|
||||
$noPreauth = ($users | Where-Object KerberosPreAuthDisabled).Count
|
||||
|
||||
Add-Line "- Total user objects: $totalUsers"
|
||||
Add-Line "- Enabled: $enabled"
|
||||
Add-Line "- Disabled: $disabled"
|
||||
Add-Line "- Currently locked out: $locked"
|
||||
Add-Line "- Password never expires: $pwdNeverExpires"
|
||||
Add-Line "- Password not required (blank password allowed): **$pwdNotRequired**"
|
||||
Add-Line "- Enabled but never logged on: $neverLoggedOn"
|
||||
Add-Line "- Stale (enabled, inactive > $StaleDays days): $staleUsers"
|
||||
Add-Line "- adminCount=1 (protected/privileged, incl. historical): $adminCountFlagged"
|
||||
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 ""
|
||||
$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"
|
||||
Add-Line ""
|
||||
Add-Line "### OS breakdown"
|
||||
Add-Line ""
|
||||
Add-Line "| Operating System | Count |"
|
||||
Add-Line "|---|---|"
|
||||
$osGroups = $computers | Group-Object -Property { if ($_.OS) { $_.OS } else { "Unknown" } } | Sort-Object Count -Descending
|
||||
foreach ($g in $osGroups) { Add-Line "| $($g.Name) | $($g.Count) |" }
|
||||
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"
|
||||
Add-Line "- Empty groups (0 members): $emptyGroups"
|
||||
Add-Line ""
|
||||
Add-Line "| Scope | Count |"
|
||||
Add-Line "|---|---|"
|
||||
$scopeGroups = $groups | Group-Object -Property Scope | Sort-Object Count -Descending
|
||||
foreach ($g in $scopeGroups) { Add-Line "| $($g.Name) | $($g.Count) |" }
|
||||
Add-Line ""
|
||||
|
||||
Add-Line "### Largest groups (top 15 by member count)"
|
||||
Add-Line ""
|
||||
Add-Line "| Group | Type | Scope | Members |"
|
||||
Add-Line "|---|---|---|---|"
|
||||
foreach ($g in ($groups | Sort-Object -Property MemberCount -Descending | Select-Object -First 15)) {
|
||||
Add-Line "| $($g.SamAccountName) | $($g.Type) | $($g.Scope) | $($g.MemberCount) |"
|
||||
}
|
||||
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
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done."
|
||||
Write-Host " Raw data: $rawPath"
|
||||
Write-Host " Report: $reportPath"
|
||||
Reference in New Issue
Block a user