Add 12 AI-generated PowerShell scripts with documentation
⚠️ IMPORTANT: These scripts are AI-GENERATED and UNTESTED Exchange Scripts (5): - Get-MailboxPermissions.ps1: Audit delegate access permissions - Get-InactiveMailboxes.ps1: Identify stale mailboxes - Compare-MailboxDatabases.ps1: Database health comparison - Export-DistributionGroups.ps1: Distribution group inventory - Get-MailflowStats.ps1: Transport log analysis Active Directory Scripts (3): - Get-ADUserLastLogon.ps1: True LastLogon across all DCs - Export-OUStructure.ps1: OU hierarchy with GPO links - Compare-ADGroupMemberships.ps1: Compare user group memberships System Maintenance Scripts (4): - Get-ServerInventory.ps1: Hardware/software inventory report - Monitor-DiskSpace.ps1: Disk space monitoring with alerts - Backup-ExchangeCertificates.ps1: Certificate backup to PFX - Test-ExchangeHealth.ps1: Aggregated Exchange health checks Documentation: - Updated CLAUDE.md with AI-generated scripts section - Added AI-GENERATED-SCRIPTS.md with warnings and testing guide All scripts include prominent warnings and follow established patterns from existing scripts. Require thorough testing before production use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
215
ActiveDirectory/Compare-ADGroupMemberships.ps1
Normal file
215
ActiveDirectory/Compare-ADGroupMemberships.ps1
Normal file
@ -0,0 +1,215 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Compare group memberships between two Active Directory users
|
||||
|
||||
.DESCRIPTION
|
||||
Shows differences in group memberships between two users. Useful for
|
||||
access troubleshooting, onboarding/offboarding validation, or role comparison.
|
||||
|
||||
.PARAMETER User1
|
||||
First user (SamAccountName, UserPrincipalName, or DistinguishedName)
|
||||
|
||||
.PARAMETER User2
|
||||
Second user (SamAccountName, UserPrincipalName, or DistinguishedName)
|
||||
|
||||
.PARAMETER OutputFolder
|
||||
Destination folder for reports. Default: .\GroupComparison-<date>
|
||||
|
||||
.PARAMETER ShowCommonGroups
|
||||
Include groups common to both users in report (default: $true)
|
||||
|
||||
.NOTES
|
||||
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||
This script was generated by Claude AI and has not been tested in production.
|
||||
Review and test thoroughly in a non-production environment before use.
|
||||
|
||||
- Requires Active Directory PowerShell module
|
||||
- Run with appropriate AD permissions
|
||||
- Includes nested group memberships
|
||||
|
||||
.EXAMPLE
|
||||
.\Compare-ADGroupMemberships.ps1 -User1 "jsmith" -User2 "jdoe"
|
||||
|
||||
.EXAMPLE
|
||||
.\Compare-ADGroupMemberships.ps1 -User1 "john.smith@domain.com" -User2 "jane.doe@domain.com" -ShowCommonGroups $false
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$User1,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$User2,
|
||||
|
||||
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("GroupComparison-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||
|
||||
[bool]$ShowCommonGroups = $true
|
||||
)
|
||||
|
||||
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||
|
||||
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||
Write-Host "[$(NowTag)] Comparing AD group memberships..." -ForegroundColor Green
|
||||
|
||||
# Import AD module
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create output folder
|
||||
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||
|
||||
# Get user objects
|
||||
Write-Host "[$(NowTag)] Retrieving user information..."
|
||||
try {
|
||||
$adUser1 = Get-ADUser -Identity $User1 -Properties MemberOf -ErrorAction Stop
|
||||
Write-Host "[$(NowTag)] User 1: $($adUser1.Name) ($($adUser1.SamAccountName))"
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] ERROR: Could not find user: $User1" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
$adUser2 = Get-ADUser -Identity $User2 -Properties MemberOf -ErrorAction Stop
|
||||
Write-Host "[$(NowTag)] User 2: $($adUser2.Name) ($($adUser2.SamAccountName))"
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] ERROR: Could not find user: $User2" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Get group memberships
|
||||
Write-Host "[$(NowTag)] Retrieving group memberships..."
|
||||
|
||||
$user1Groups = @()
|
||||
if ($adUser1.MemberOf) {
|
||||
foreach ($groupDN in $adUser1.MemberOf) {
|
||||
try {
|
||||
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
|
||||
if ($group) {
|
||||
$user1Groups += [PSCustomObject]@{
|
||||
Name = $group.Name
|
||||
DN = $group.DistinguishedName
|
||||
Description = $group.Description
|
||||
GroupScope = $group.GroupScope
|
||||
GroupCategory = $group.GroupCategory
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
$user2Groups = @()
|
||||
if ($adUser2.MemberOf) {
|
||||
foreach ($groupDN in $adUser2.MemberOf) {
|
||||
try {
|
||||
$group = Get-ADGroup -Identity $groupDN -Properties Description -ErrorAction SilentlyContinue
|
||||
if ($group) {
|
||||
$user2Groups += [PSCustomObject]@{
|
||||
Name = $group.Name
|
||||
DN = $group.DistinguishedName
|
||||
Description = $group.Description
|
||||
GroupScope = $group.GroupScope
|
||||
GroupCategory = $group.GroupCategory
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[$(NowTag)] User 1 group count: $($user1Groups.Count)"
|
||||
Write-Host "[$(NowTag)] User 2 group count: $($user2Groups.Count)"
|
||||
|
||||
# Compare memberships
|
||||
$user1GroupNames = $user1Groups | Select-Object -ExpandProperty Name
|
||||
$user2GroupNames = $user2Groups | Select-Object -ExpandProperty Name
|
||||
|
||||
$uniqueToUser1 = $user1Groups | Where-Object { $_.Name -notin $user2GroupNames }
|
||||
$uniqueToUser2 = $user2Groups | Where-Object { $_.Name -notin $user1GroupNames }
|
||||
$commonGroups = $user1Groups | Where-Object { $_.Name -in $user2GroupNames }
|
||||
|
||||
# Export results
|
||||
Write-Host "[$(NowTag)] Exporting comparison results..."
|
||||
|
||||
# Unique to User 1
|
||||
if ($uniqueToUser1.Count -gt 0) {
|
||||
$file1 = Join-Path $OutputFolder "Groups-UniqueToUser1-$($adUser1.SamAccountName).csv"
|
||||
$uniqueToUser1 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file1
|
||||
Write-Host "[$(NowTag)] Groups unique to $($adUser1.Name): $file1" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Unique to User 2
|
||||
if ($uniqueToUser2.Count -gt 0) {
|
||||
$file2 = Join-Path $OutputFolder "Groups-UniqueToUser2-$($adUser2.SamAccountName).csv"
|
||||
$uniqueToUser2 | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file2
|
||||
Write-Host "[$(NowTag)] Groups unique to $($adUser2.Name): $file2" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
# Common groups
|
||||
if ($ShowCommonGroups -and $commonGroups.Count -gt 0) {
|
||||
$fileCommon = Join-Path $OutputFolder "Groups-Common.csv"
|
||||
$commonGroups | Sort-Object Name | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $fileCommon
|
||||
Write-Host "[$(NowTag)] Common groups: $fileCommon" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Summary report
|
||||
$summaryFile = Join-Path $OutputFolder "Comparison-Summary.txt"
|
||||
$summary = @"
|
||||
Active Directory Group Membership Comparison
|
||||
Generated: $(Get-Date)
|
||||
|
||||
User 1: $($adUser1.Name) ($($adUser1.SamAccountName))
|
||||
User 2: $($adUser2.Name) ($($adUser2.SamAccountName))
|
||||
|
||||
SUMMARY:
|
||||
User 1 Total Groups: $($user1Groups.Count)
|
||||
User 2 Total Groups: $($user2Groups.Count)
|
||||
Common Groups: $($commonGroups.Count)
|
||||
Groups Unique to User 1: $($uniqueToUser1.Count)
|
||||
Groups Unique to User 2: $($uniqueToUser2.Count)
|
||||
|
||||
GROUPS UNIQUE TO USER 1 ($($adUser1.Name)):
|
||||
$($uniqueToUser1 | ForEach-Object { " - $($_.Name)" } | Out-String)
|
||||
|
||||
GROUPS UNIQUE TO USER 2 ($($adUser2.Name)):
|
||||
$($uniqueToUser2 | ForEach-Object { " - $($_.Name)" } | Out-String)
|
||||
|
||||
$(if ($ShowCommonGroups) {
|
||||
"COMMON GROUPS:
|
||||
$($commonGroups | ForEach-Object { " - $($_.Name)" } | Out-String)"
|
||||
} else { "" })
|
||||
"@
|
||||
|
||||
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||
Write-Host "[$(NowTag)] Summary report: $summaryFile" -ForegroundColor Green
|
||||
|
||||
# Console output
|
||||
Write-Host "`nCOMPARISON RESULTS:" -ForegroundColor Cyan
|
||||
Write-Host " Common Groups: $($commonGroups.Count)" -ForegroundColor Green
|
||||
Write-Host " Unique to $($adUser1.Name): $($uniqueToUser1.Count)" -ForegroundColor Yellow
|
||||
Write-Host " Unique to $($adUser2.Name): $($uniqueToUser2.Count)" -ForegroundColor Yellow
|
||||
|
||||
if ($uniqueToUser1.Count -gt 0) {
|
||||
Write-Host "`nGroups Only in $($adUser1.Name):" -ForegroundColor Yellow
|
||||
$uniqueToUser1 | Select-Object -First 10 | ForEach-Object {
|
||||
Write-Host " - $($_.Name)"
|
||||
}
|
||||
if ($uniqueToUser1.Count -gt 10) {
|
||||
Write-Host " ... and $($uniqueToUser1.Count - 10) more (see CSV)"
|
||||
}
|
||||
}
|
||||
|
||||
if ($uniqueToUser2.Count -gt 0) {
|
||||
Write-Host "`nGroups Only in $($adUser2.Name):" -ForegroundColor Yellow
|
||||
$uniqueToUser2 | Select-Object -First 10 | ForEach-Object {
|
||||
Write-Host " - $($_.Name)"
|
||||
}
|
||||
if ($uniqueToUser2.Count -gt 10) {
|
||||
Write-Host " ... and $($uniqueToUser2.Count - 10) more (see CSV)"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "`n[$(NowTag)] Comparison complete! Output folder: $OutputFolder"
|
||||
201
ActiveDirectory/Export-OUStructure.ps1
Normal file
201
ActiveDirectory/Export-OUStructure.ps1
Normal file
@ -0,0 +1,201 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Export Active Directory OU structure with GPO links
|
||||
|
||||
.DESCRIPTION
|
||||
Documents the complete OU hierarchy including GPO links, inheritance settings,
|
||||
and description. Useful for documentation, disaster recovery, or comparing
|
||||
environments.
|
||||
|
||||
.PARAMETER OutputFolder
|
||||
Destination folder for reports. Default: .\OU-Structure-<date>
|
||||
|
||||
.PARAMETER IncludeGPODetails
|
||||
Include detailed GPO information (default: $true)
|
||||
|
||||
.PARAMETER SearchBase
|
||||
Optional starting point for OU export (default: domain root)
|
||||
|
||||
.NOTES
|
||||
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||
This script was generated by Claude AI and has not been tested in production.
|
||||
Review and test thoroughly in a non-production environment before use.
|
||||
|
||||
- Requires Active Directory PowerShell module
|
||||
- Run with appropriate AD permissions
|
||||
- Exports to both CSV (detailed) and TXT (tree view)
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-OUStructure.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Export-OUStructure.ps1 -SearchBase "OU=Departments,DC=domain,DC=com"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("OU-Structure-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||
[bool]$IncludeGPODetails = $true,
|
||||
[string]$SearchBase = ""
|
||||
)
|
||||
|
||||
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||
|
||||
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||
Write-Host "[$(NowTag)] Exporting OU structure..." -ForegroundColor Green
|
||||
|
||||
# Import AD module
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create output folder
|
||||
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||
|
||||
# Get domain info
|
||||
$domain = Get-ADDomain
|
||||
if (-not $SearchBase) {
|
||||
$SearchBase = $domain.DistinguishedName
|
||||
}
|
||||
|
||||
Write-Host "[$(NowTag)] Domain: $($domain.DNSRoot)"
|
||||
Write-Host "[$(NowTag)] Search Base: $SearchBase"
|
||||
|
||||
# Get all OUs
|
||||
Write-Host "[$(NowTag)] Retrieving organizational units..."
|
||||
$ouParams = @{
|
||||
Filter = '*'
|
||||
SearchBase = $SearchBase
|
||||
Properties = @('Description', 'gPLink', 'gPOptions', 'ProtectedFromAccidentalDeletion', 'WhenCreated', 'WhenChanged')
|
||||
}
|
||||
|
||||
$ous = Get-ADOrganizationalUnit @ouParams | Sort-Object DistinguishedName
|
||||
|
||||
$ouCount = ($ous | Measure-Object).Count
|
||||
Write-Host "[$(NowTag)] Found $ouCount organizational unit(s)"
|
||||
|
||||
# Build GPO name lookup if requested
|
||||
$gpoLookup = @{}
|
||||
if ($IncludeGPODetails) {
|
||||
Write-Host "[$(NowTag)] Building GPO lookup table..."
|
||||
try {
|
||||
Import-Module GroupPolicy -ErrorAction Stop
|
||||
$gpos = Get-GPO -All -ErrorAction SilentlyContinue
|
||||
foreach ($gpo in $gpos) {
|
||||
$gpoLookup[$gpo.Id.ToString()] = $gpo.DisplayName
|
||||
}
|
||||
Write-Host "[$(NowTag)] Found $($gpos.Count) GPO(s)"
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] WARNING: Could not load GroupPolicy module - GPO names will show as GUIDs" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Process OUs
|
||||
$ouDetails = @()
|
||||
$treeLines = @()
|
||||
|
||||
foreach ($ou in $ous) {
|
||||
# Calculate depth for tree view
|
||||
$depth = ($ou.DistinguishedName -split ',OU=').Count - 1
|
||||
$indent = " " * $depth
|
||||
|
||||
# Parse GPO links
|
||||
$gpoLinks = @()
|
||||
$gpoNames = @()
|
||||
|
||||
if ($ou.gPLink) {
|
||||
# gPLink format: [LDAP://cn={GUID},cn=policies,cn=system,DC=domain,DC=com;0]
|
||||
$links = $ou.gPLink -split '\]\[' -replace '^\[' -replace '\]$'
|
||||
|
||||
foreach ($link in $links) {
|
||||
if ($link -match '\{([^\}]+)\}') {
|
||||
$gpoGuid = $matches[1]
|
||||
$gpoName = if ($gpoLookup.ContainsKey($gpoGuid)) {
|
||||
$gpoLookup[$gpoGuid]
|
||||
} else {
|
||||
$gpoGuid
|
||||
}
|
||||
|
||||
# Parse link order and enforcement
|
||||
$linkOrder = if ($link -match ';(\d+)') { $matches[1] } else { "0" }
|
||||
$enforced = $linkOrder -band 1 # Bit 0 indicates enforcement
|
||||
$disabled = $linkOrder -band 2 # Bit 1 indicates disabled
|
||||
|
||||
$gpoNames += $gpoName
|
||||
$gpoLinks += [PSCustomObject]@{
|
||||
OU = $ou.Name
|
||||
GPOName = $gpoName
|
||||
GPOGUID = $gpoGuid
|
||||
LinkOrder = $linkOrder
|
||||
Enforced = ($enforced -eq 1)
|
||||
Disabled = ($disabled -eq 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# GPO inheritance blocked?
|
||||
$inheritanceBlocked = ($ou.gPOptions -eq 1)
|
||||
|
||||
# Tree view line
|
||||
$protectedMark = if ($ou.ProtectedFromAccidentalDeletion) { "[P]" } else { "" }
|
||||
$inheritMark = if ($inheritanceBlocked) { "[BLOCKED]" } else { "" }
|
||||
$gpoMark = if ($gpoNames.Count -gt 0) { " (GPOs: $($gpoNames -join ', '))" } else { "" }
|
||||
|
||||
$treeLines += "$indent$($ou.Name) $protectedMark$inheritMark$gpoMark"
|
||||
|
||||
# Detailed record
|
||||
$ouDetails += [PSCustomObject]@{
|
||||
Name = $ou.Name
|
||||
DistinguishedName = $ou.DistinguishedName
|
||||
Description = $ou.Description
|
||||
ProtectedFromAccidentalDeletion = $ou.ProtectedFromAccidentalDeletion
|
||||
GPOInheritanceBlocked = $inheritanceBlocked
|
||||
LinkedGPOs = ($gpoNames -join "; ")
|
||||
LinkedGPOCount = $gpoNames.Count
|
||||
WhenCreated = $ou.WhenCreated
|
||||
WhenChanged = $ou.WhenChanged
|
||||
}
|
||||
}
|
||||
|
||||
# Export detailed CSV
|
||||
$csvFile = Join-Path $OutputFolder "OU-Structure.csv"
|
||||
$ouDetails | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||
Write-Host "[$(NowTag)] OU details exported: $csvFile" -ForegroundColor Green
|
||||
|
||||
# Export GPO links if available
|
||||
if ($IncludeGPODetails -and $gpoLinks.Count -gt 0) {
|
||||
$gpoLinksFile = Join-Path $OutputFolder "OU-GPO-Links.csv"
|
||||
$gpoLinks | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $gpoLinksFile
|
||||
Write-Host "[$(NowTag)] GPO links exported: $gpoLinksFile" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Export tree view
|
||||
$treeFile = Join-Path $OutputFolder "OU-Tree.txt"
|
||||
$treeHeader = @"
|
||||
Active Directory OU Structure
|
||||
Domain: $($domain.DNSRoot)
|
||||
Search Base: $SearchBase
|
||||
Generated: $(Get-Date)
|
||||
|
||||
Legend:
|
||||
[P] = Protected from Accidental Deletion
|
||||
[BLOCKED] = GPO Inheritance Blocked
|
||||
|
||||
"@
|
||||
|
||||
$treeHeader | Out-File -FilePath $treeFile -Encoding UTF8
|
||||
$treeLines | Out-File -FilePath $treeFile -Encoding UTF8 -Append
|
||||
Write-Host "[$(NowTag)] Tree view exported: $treeFile" -ForegroundColor Green
|
||||
|
||||
# Summary
|
||||
Write-Host "`nOU STRUCTURE SUMMARY:" -ForegroundColor Cyan
|
||||
Write-Host " Total OUs: $ouCount"
|
||||
Write-Host " Protected OUs: $(($ouDetails | Where-Object ProtectedFromAccidentalDeletion).Count)"
|
||||
Write-Host " OUs with GPO Inheritance Blocked: $(($ouDetails | Where-Object GPOInheritanceBlocked).Count)"
|
||||
Write-Host " OUs with Linked GPOs: $(($ouDetails | Where-Object { $_.LinkedGPOCount -gt 0 }).Count)"
|
||||
|
||||
Write-Host "`n[$(NowTag)] Export complete! Output folder: $OutputFolder"
|
||||
185
ActiveDirectory/Get-ADUserLastLogon.ps1
Normal file
185
ActiveDirectory/Get-ADUserLastLogon.ps1
Normal file
@ -0,0 +1,185 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Report on inactive Active Directory user accounts based on true LastLogon
|
||||
|
||||
.DESCRIPTION
|
||||
Queries all domain controllers to find the most recent LastLogon time for users.
|
||||
The LastLogon attribute is not replicated, so each DC must be queried individually
|
||||
to get accurate results. Identifies accounts inactive for specified days.
|
||||
|
||||
.PARAMETER InactiveDays
|
||||
Number of days without logon to consider account inactive (default: 90)
|
||||
|
||||
.PARAMETER OutputFolder
|
||||
Destination folder for reports. Default: .\InactiveADUsers-<date>
|
||||
|
||||
.PARAMETER SearchBase
|
||||
Optional OU to limit search scope (default: entire domain)
|
||||
|
||||
.PARAMETER IncludeDisabled
|
||||
Include disabled accounts in the report (default: $false)
|
||||
|
||||
.NOTES
|
||||
⚠️ AI-GENERATED SCRIPT - UNTESTED
|
||||
This script was generated by Claude AI and has not been tested in production.
|
||||
Review and test thoroughly in a non-production environment before use.
|
||||
|
||||
- Requires Active Directory PowerShell module
|
||||
- Run with appropriate AD permissions
|
||||
- Can be slow in large environments (queries each DC)
|
||||
- LastLogon attribute is not replicated between DCs
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-ADUserLastLogon.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Get-ADUserLastLogon.ps1 -InactiveDays 180 -SearchBase "OU=Users,DC=domain,DC=com"
|
||||
#>
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[int]$InactiveDays = 90,
|
||||
[string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("InactiveADUsers-" + (Get-Date -Format "yyyyMMdd-HHmm"))),
|
||||
[string]$SearchBase = "",
|
||||
[bool]$IncludeDisabled = $false
|
||||
)
|
||||
|
||||
function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") }
|
||||
|
||||
Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow
|
||||
Write-Host "[$(NowTag)] Starting AD user lastlogon analysis..." -ForegroundColor Green
|
||||
|
||||
# Import AD module
|
||||
try {
|
||||
Import-Module ActiveDirectory -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Host "[$(NowTag)] ERROR: Active Directory module not available" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Create output folder
|
||||
New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null
|
||||
|
||||
$cutoffDate = (Get-Date).AddDays(-$InactiveDays)
|
||||
Write-Host "[$(NowTag)] Cutoff date: $($cutoffDate.ToString('yyyy-MM-dd'))"
|
||||
|
||||
# Get all domain controllers
|
||||
Write-Host "[$(NowTag)] Discovering domain controllers..."
|
||||
$domainControllers = Get-ADDomainController -Filter * | Select-Object -ExpandProperty HostName
|
||||
Write-Host "[$(NowTag)] Found $($domainControllers.Count) domain controller(s)"
|
||||
|
||||
# Get all users
|
||||
Write-Host "[$(NowTag)] Retrieving AD users..."
|
||||
$getUserParams = @{
|
||||
Filter = "ObjectClass -eq 'user' -and Enabled -eq 'True'"
|
||||
Properties = @('LastLogon', 'LastLogonDate', 'WhenCreated', 'Department', 'Title', 'Description', 'PasswordLastSet', 'PasswordNeverExpires', 'Enabled')
|
||||
}
|
||||
|
||||
if ($SearchBase) {
|
||||
$getUserParams['SearchBase'] = $SearchBase
|
||||
}
|
||||
|
||||
if ($IncludeDisabled) {
|
||||
$getUserParams['Filter'] = "ObjectClass -eq 'user'"
|
||||
}
|
||||
|
||||
$users = Get-ADUser @getUserParams
|
||||
$userCount = ($users | Measure-Object).Count
|
||||
Write-Host "[$(NowTag)] Found $userCount user(s) to analyze"
|
||||
|
||||
# Query each DC for LastLogon and find the most recent
|
||||
$userLogonData = @()
|
||||
$current = 0
|
||||
|
||||
foreach ($user in $users) {
|
||||
$current++
|
||||
$pct = [int](($current / $userCount) * 100)
|
||||
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Status "Processing $($user.SamAccountName) ($current/$userCount)" -PercentComplete $pct
|
||||
|
||||
$mostRecentLogon = $null
|
||||
|
||||
foreach ($dc in $domainControllers) {
|
||||
try {
|
||||
$userFromDC = Get-ADUser -Identity $user.SamAccountName -Server $dc -Properties LastLogon -ErrorAction SilentlyContinue
|
||||
|
||||
if ($userFromDC.LastLogon) {
|
||||
$logonDate = [DateTime]::FromFileTime($userFromDC.LastLogon)
|
||||
|
||||
if ($null -eq $mostRecentLogon -or $logonDate -gt $mostRecentLogon) {
|
||||
$mostRecentLogon = $logonDate
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Verbose "Could not query $dc for $($user.SamAccountName)"
|
||||
}
|
||||
}
|
||||
|
||||
# Determine if inactive
|
||||
$isInactive = $false
|
||||
$reason = ""
|
||||
$daysSinceLogon = "N/A"
|
||||
|
||||
if ($null -eq $mostRecentLogon) {
|
||||
$isInactive = $true
|
||||
$reason = "Never logged on"
|
||||
} elseif ($mostRecentLogon -lt $cutoffDate) {
|
||||
$isInactive = $true
|
||||
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
|
||||
$reason = "Last logon: $($mostRecentLogon.ToString('yyyy-MM-dd'))"
|
||||
} else {
|
||||
$daysSinceLogon = [int]((Get-Date) - $mostRecentLogon).TotalDays
|
||||
}
|
||||
|
||||
if ($isInactive) {
|
||||
$userLogonData += [PSCustomObject]@{
|
||||
SamAccountName = $user.SamAccountName
|
||||
DisplayName = $user.Name
|
||||
UserPrincipalName = $user.UserPrincipalName
|
||||
Enabled = $user.Enabled
|
||||
LastLogon = $mostRecentLogon
|
||||
DaysSinceLogon = $daysSinceLogon
|
||||
LastLogonDate = $user.LastLogonDate # Replicated attribute for comparison
|
||||
WhenCreated = $user.WhenCreated
|
||||
PasswordLastSet = $user.PasswordLastSet
|
||||
PasswordNeverExpires = $user.PasswordNeverExpires
|
||||
Department = $user.Department
|
||||
Title = $user.Title
|
||||
Description = $user.Description
|
||||
DistinguishedName = $user.DistinguishedName
|
||||
Reason = $reason
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Progress -Activity "Querying LastLogon from Domain Controllers" -Completed
|
||||
|
||||
# Export results
|
||||
Write-Host "[$(NowTag)] Found $($userLogonData.Count) inactive user(s)"
|
||||
|
||||
if ($userLogonData.Count -gt 0) {
|
||||
$csvFile = Join-Path $OutputFolder "Inactive-ADUsers.csv"
|
||||
$userLogonData | Sort-Object DaysSinceLogon -Descending |
|
||||
Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile
|
||||
Write-Host "[$(NowTag)] Report exported: $csvFile" -ForegroundColor Green
|
||||
|
||||
# Summary statistics
|
||||
Write-Host "`nINACTIVE USER SUMMARY:" -ForegroundColor Cyan
|
||||
Write-Host " Total Inactive Users: $($userLogonData.Count)"
|
||||
Write-Host " Never Logged On: $(($userLogonData | Where-Object { $_.DaysSinceLogon -eq 'N/A' }).Count)"
|
||||
Write-Host " Enabled Accounts: $(($userLogonData | Where-Object Enabled).Count)"
|
||||
Write-Host " Disabled Accounts: $(($userLogonData | Where-Object { -not $_.Enabled }).Count)"
|
||||
Write-Host " Password Never Expires: $(($userLogonData | Where-Object PasswordNeverExpires).Count)"
|
||||
|
||||
# By department
|
||||
$byDept = $userLogonData | Where-Object Department | Group-Object Department | Sort-Object Count -Descending | Select-Object -First 5
|
||||
if ($byDept) {
|
||||
Write-Host "`nTop 5 Departments with Inactive Users:" -ForegroundColor Cyan
|
||||
$byDept | ForEach-Object {
|
||||
Write-Host " $($_.Name): $($_.Count)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "[$(NowTag)] No inactive users found!" -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"
|
||||
Reference in New Issue
Block a user