diff --git a/CleanupMonster.psd1 b/CleanupMonster.psd1 index 5331e8e..0ad434f 100644 --- a/CleanupMonster.psd1 +++ b/CleanupMonster.psd1 @@ -6,7 +6,7 @@ CompatiblePSEditions = @('Desktop', 'Core') Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' Description = 'This module provides an easy way to cleanup Active Directory from dead/old objects based on various criteria. It can also disable, move or delete objects. It can utilize Azure AD, Intune and Jamf to get additional information about objects before deleting them.' - FunctionsToExport = @('Invoke-ADComputersCleanup', 'Invoke-ADSIDHistoryCleanup') + FunctionsToExport = @('Invoke-ADComputersCleanup', 'Invoke-ADSIDHistoryCleanup', 'Invoke-ADServiceAccountsCleanup') GUID = 'cd1f9987-6242-452c-a7db-6337d4a6b639' ModuleVersion = '3.1.6' PowerShellVersion = '5.1' diff --git a/Private/Get-ADServiceAccountsToProcess.ps1 b/Private/Get-ADServiceAccountsToProcess.ps1 new file mode 100644 index 0000000..ba8914b --- /dev/null +++ b/Private/Get-ADServiceAccountsToProcess.ps1 @@ -0,0 +1,53 @@ +function Get-ADServiceAccountsToProcess { + [CmdletBinding()] + param( + [parameter(Mandatory)][ValidateSet('Disable','Delete')][string] $Type, + [Array] $Accounts, + [alias('DeleteOnlyIf','DisableOnlyIf')][System.Collections.IDictionary] $ActionIf, + [Array] $Exclusions + ) + Write-Color -Text "[i] ", "Applying following rules to $Type action: " -Color Yellow, Cyan, Green + foreach ($Key in $ActionIf.Keys) { + if ($null -eq $ActionIf[$Key]) { + Write-Color -Text " [>] ", $Key, " is ", 'Not Set' -Color Yellow, Cyan, Yellow + } else { + Write-Color -Text " [>] ", $Key, " is ", $ActionIf[$Key] -Color Yellow, Cyan, Green + } + } + Write-Color -Text "[i] ", "Looking for service accounts to $Type" -Color Yellow, Cyan, Green + $Today = Get-Date + [Array] $Output = foreach ($Account in $Accounts) { + foreach ($Exclude in $Exclusions) { + if ($Account.DistinguishedName -like $Exclude -or $Account.Name -like $Exclude) { + continue 2 + } + } + $Include = $true + if ($ActionIf.LastLogonDateMoreThan) { + if ($Account.LastLogonDate) { + $LastLogonDays = (New-TimeSpan -Start $Account.LastLogonDate -End $Today).Days + $Account | Add-Member -NotePropertyName LastLogonDays -NotePropertyValue $LastLogonDays -Force + if ($LastLogonDays -le $ActionIf.LastLogonDateMoreThan) { $Include = $false } + } + } + if ($ActionIf.PasswordLastSetMoreThan) { + if ($Account.PasswordLastSet) { + $PasswordDays = (New-TimeSpan -Start $Account.PasswordLastSet -End $Today).Days + $Account | Add-Member -NotePropertyName PasswordLastChangedDays -NotePropertyValue $PasswordDays -Force + if ($PasswordDays -le $ActionIf.PasswordLastSetMoreThan) { $Include = $false } + } + } + if ($ActionIf.WhenCreatedMoreThan) { + if ($Account.WhenCreated) { + $CreatedDays = (New-TimeSpan -Start $Account.WhenCreated -End $Today).Days + $Account | Add-Member -NotePropertyName WhenCreatedDays -NotePropertyValue $CreatedDays -Force + if ($CreatedDays -le $ActionIf.WhenCreatedMoreThan) { $Include = $false } + } + } + if ($Include) { + $Account | Add-Member -NotePropertyName Action -NotePropertyValue $Type -Force + $Account + } + } + $Output +} diff --git a/Private/Get-InitialADServiceAccounts.ps1 b/Private/Get-InitialADServiceAccounts.ps1 new file mode 100644 index 0000000..d730cfb --- /dev/null +++ b/Private/Get-InitialADServiceAccounts.ps1 @@ -0,0 +1,25 @@ +function Get-InitialADServiceAccounts { + [CmdletBinding()] + param( + [System.Collections.IDictionary] $ForestInformation, + [string[]] $IncludeAccounts, + [string[]] $ExcludeAccounts + ) + $Report = [ordered]@{} + foreach ($Domain in $ForestInformation.Domains) { + $Server = $ForestInformation['QueryServers'][$Domain].HostName[0] + Write-Color -Text "[i] ", "Getting service accounts for domain ", $Domain, " from ", $Server -Color Yellow, Magenta, Yellow, Magenta + $Accounts = Get-ADServiceAccount -Filter * -Server $Server -Properties SamAccountName,Enabled,LastLogonDate,PasswordLastSet,WhenCreated,DistinguishedName,ObjectClass | + Select-Object *, @{Name='DomainName';Expression={$Domain}}, @{Name='Server';Expression={$Server}} + if ($IncludeAccounts) { + $IncludePattern = [string]::Join('|', ($IncludeAccounts | ForEach-Object { [regex]::Escape($_).Replace('\*','.*') })) + $Accounts = $Accounts | Where-Object { $_.SamAccountName -match "^($IncludePattern)$" } + } + if ($ExcludeAccounts) { + $ExcludePattern = [string]::Join('|', ($ExcludeAccounts | ForEach-Object { [regex]::Escape($_).Replace('\*','.*') })) + $Accounts = $Accounts | Where-Object { $_.SamAccountName -notmatch "^($ExcludePattern)$" } + } + $Report[$Domain] = [ordered]@{ Server = $Server; Accounts = @($Accounts) } + } + $Report +} diff --git a/Private/New-HTMLProcessedServiceAccounts.ps1 b/Private/New-HTMLProcessedServiceAccounts.ps1 new file mode 100644 index 0000000..f8fbb3b --- /dev/null +++ b/Private/New-HTMLProcessedServiceAccounts.ps1 @@ -0,0 +1,153 @@ +function New-HTMLProcessedServiceAccounts { + [CmdletBinding()] + param( + [System.Collections.IDictionary] $Export, + [System.Collections.IDictionary] $DisableOnlyIf, + [System.Collections.IDictionary] $DeleteOnlyIf, + [Array] $AccountsToProcess, + [string] $FilePath, + [switch] $Online, + [switch] $ShowHTML, + [string] $LogFile, + [switch] $Disable, + [switch] $Delete, + [switch] $ReportOnly + ) + + New-HTML { + New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey -BackgroundColor BlizzardBlue + New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow + New-HTMLPanelStyle -BorderRadius 0px + New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin + + New-HTMLHeader { + New-HTMLSection -Invisible { + New-HTMLSection { + New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue + } -JustifyContent flex-start -Invisible + New-HTMLSection { + New-HTMLText -Text "Cleanup Monster - $($Export['Version'])" -Color Blue + } -JustifyContent flex-end -Invisible + } + } + + if (-not $ReportOnly) { + New-HTMLTab -Name 'Service Accounts Current Run' { + New-HTMLSection { + [Array] $ListAll = $Export.CurrentRun + New-HTMLPanel { + New-HTMLToast -TextHeader 'Total in this run' -Text "Actions (disable & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen + } -Invisible + + [Array] $ListDisabled = $Export.CurrentRun | Where-Object { $_.Action -eq 'Disable' } + New-HTMLPanel { + New-HTMLToast -TextHeader 'Disable' -Text "Accounts disabled: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel + } -Invisible + + [Array] $ListDeleted = $Export.CurrentRun | Where-Object { $_.Action -eq 'Delete' } + New-HTMLPanel { + New-HTMLToast -TextHeader 'Delete' -Text "Accounts deleted: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed + } -Invisible + } -Invisible + + New-HTMLTable -DataTable $Export.CurrentRun -Filtering -ScrollX { + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue + } -WarningAction SilentlyContinue + } + + New-HTMLTab -Name 'Service Accounts History' { + New-HTMLSection { + [Array] $ListAll = $Export.History + New-HTMLPanel { + New-HTMLToast -TextHeader 'Total History' -Text "Actions (disable & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen + } -Invisible + + [Array] $ListDisabled = $Export.History | Where-Object { $_.Action -eq 'Disable' } + New-HTMLPanel { + New-HTMLToast -TextHeader 'Disabled History' -Text "Accounts disabled so far: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel + } -Invisible + + [Array] $ListDeleted = $Export.History | Where-Object { $_.Action -eq 'Delete' } + New-HTMLPanel { + New-HTMLToast -TextHeader 'Deleted History' -Text "Accounts deleted so far: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed + } -Invisible + } -Invisible + New-HTMLTable -DataTable $Export.History -Filtering -ScrollX { + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue + } -WarningAction SilentlyContinue -AllProperties + } + } + + New-HTMLPanel { + if ($Disable) { + New-HTMLText -Text "Service accounts will be disabled only if: " -FontWeight bold + New-HTMLList { + foreach ($Key in $DisableOnlyIf.Keys) { + New-HTMLListItem -Text @( + if ($null -eq $DisableOnlyIf[$Key]) { + $($Key), " is ", 'Not Set' + $ColorInUse = 'Cinnabar' + } else { + $($Key), " is ", $($DisableOnlyIf[$Key]) + $ColorInUse = 'Apple' + } + ) -FontWeight bold, normal, bold -Color $ColorInUse, None, CornflowerBlue + } + } + } else { + New-HTMLText -Text "Service accounts will not be disabled, as the disable functionality was not enabled." -FontWeight bold + } + } + + New-HTMLPanel { + if ($Delete) { + New-HTMLText -Text "Service accounts will be deleted only if: " -FontWeight bold + New-HTMLList { + foreach ($Key in $DeleteOnlyIf.Keys) { + New-HTMLListItem -Text @( + if ($null -eq $DeleteOnlyIf[$Key]) { + $($Key), " is ", 'Not Set' + $ColorInUse = 'Cinnabar' + } else { + $($Key), " is ", $($DeleteOnlyIf[$Key]) + $ColorInUse = 'Apple' + } + ) -FontWeight bold, normal, bold -Color $ColorInUse, None, CornflowerBlue + } + } + } else { + New-HTMLText -Text "Service accounts will not be deleted, as the delete functionality was not enabled." -FontWeight bold + } + } + + if ($AccountsToProcess) { + New-HTMLTable -DataTable $AccountsToProcess -Filtering -ScrollX { + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace + New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon + New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue + } -WarningAction SilentlyContinue -ExcludeProperty 'TimeOnPendingList', 'TimeToLeavePendingList' + } + + try { + if ($LogFile -and (Test-Path -LiteralPath $LogFile -ErrorAction Stop)) { + $LogContent = Get-Content -Raw -LiteralPath $LogFile -ErrorAction Stop + New-HTMLTab -Name 'Log' { + New-HTMLCodeBlock -Code $LogContent -Style generic + } + } + } catch { + Write-Color -Text "[e] ", "Couldn't read the log file. Skipping adding log to HTML. Error: $($_.Exception.Message)" -Color Yellow, Red + } + } -FilePath $FilePath -Online:$Online.IsPresent -ShowHTML:$ShowHTML.IsPresent +} + diff --git a/Private/Request-ADServiceAccountsDelete.ps1 b/Private/Request-ADServiceAccountsDelete.ps1 new file mode 100644 index 0000000..e664ab2 --- /dev/null +++ b/Private/Request-ADServiceAccountsDelete.ps1 @@ -0,0 +1,36 @@ +function Request-ADServiceAccountsDelete { + [CmdletBinding(SupportsShouldProcess)] + param( + [Array] $Accounts, + [switch] $ReportOnly, + [switch] $WhatIfDelete, + [DateTime] $Today, + [switch] $DontWriteToEventLog + ) + foreach ($Account in $Accounts) { + if ($Account.Action -ne 'Delete') { continue } + if ($ReportOnly) { + $Account + continue + } + $Server = $Account.Server + $Success = $false + if ($PSCmdlet.ShouldProcess($Account.DistinguishedName, 'Delete service account')) { + try { + Remove-ADObject -Identity $Account.DistinguishedName -Server $Server -Confirm:$false -WhatIf:$WhatIfDelete -ErrorAction Stop + $Success = $true + Write-Color -Text "[+] ", "Deleting service account ", $Account.SamAccountName, " (WhatIf: $($WhatIfDelete.IsPresent)) successful." -Color Yellow, Green, Yellow + } catch { + Write-Color -Text "[-] ", "Deleting service account ", $Account.SamAccountName, " failed: ", $_.Exception.Message -Color Yellow, Red, Yellow, Red + $Account.ActionComment = $_.Exception.Message + } + } + $Account.ActionDate = $Today + if ($WhatIfDelete.IsPresent) { + $Account.ActionStatus = 'WhatIf' + } else { + $Account.ActionStatus = $Success + } + $Account + } +} diff --git a/Private/Request-ADServiceAccountsDisable.ps1 b/Private/Request-ADServiceAccountsDisable.ps1 new file mode 100644 index 0000000..3d1d4a6 --- /dev/null +++ b/Private/Request-ADServiceAccountsDisable.ps1 @@ -0,0 +1,36 @@ +function Request-ADServiceAccountsDisable { + [CmdletBinding(SupportsShouldProcess)] + param( + [Array] $Accounts, + [switch] $ReportOnly, + [switch] $WhatIfDisable, + [DateTime] $Today, + [switch] $DontWriteToEventLog + ) + foreach ($Account in $Accounts) { + if ($Account.Action -ne 'Disable') { continue } + if ($ReportOnly) { + $Account + continue + } + $Server = $Account.Server + $Success = $false + if ($PSCmdlet.ShouldProcess($Account.DistinguishedName, 'Disable service account')) { + try { + Disable-ADAccount -Identity $Account.DistinguishedName -Server $Server -WhatIf:$WhatIfDisable -ErrorAction Stop + $Success = $true + Write-Color -Text "[+] ", "Disabling service account ", $Account.SamAccountName, " (WhatIf: $($WhatIfDisable.IsPresent)) successful." -Color Yellow, Green, Yellow + } catch { + Write-Color -Text "[-] ", "Disabling service account ", $Account.SamAccountName, " failed: ", $_.Exception.Message -Color Yellow, Red, Yellow, Red + $Account.ActionComment = $_.Exception.Message + } + } + $Account.ActionDate = $Today + if ($WhatIfDisable.IsPresent) { + $Account.ActionStatus = 'WhatIf' + } else { + $Account.ActionStatus = $Success + } + $Account + } +} diff --git a/Public/Invoke-ADServiceAccountsCleanup.ps1 b/Public/Invoke-ADServiceAccountsCleanup.ps1 new file mode 100644 index 0000000..98ad485 --- /dev/null +++ b/Public/Invoke-ADServiceAccountsCleanup.ps1 @@ -0,0 +1,179 @@ +function Invoke-ADServiceAccountsCleanup { + <# + .SYNOPSIS + Cleans up stale Active Directory service accounts. + + .DESCRIPTION + Enumerates managed service accounts in Active Directory and disables or deletes + accounts based on inactivity or age criteria. + + .PARAMETER Forest + Forest to use when connecting to Active Directory. + + .PARAMETER IncludeDomains + List of domains to include in the process. + + .PARAMETER ExcludeDomains + List of domains to exclude from the process. + + .PARAMETER IncludeAccounts + Include only service accounts that match these names (supports wildcards). + + .PARAMETER ExcludeAccounts + Exclude service accounts that match these names (supports wildcards). + + .PARAMETER Disable + Enable disabling of matching service accounts. + + .PARAMETER Delete + Enable deletion of matching service accounts. + + .PARAMETER DisableLastLogonDateMoreThan + Disable accounts that have not logged on for the specified number of days. + + .PARAMETER DisablePasswordLastSetMoreThan + Disable accounts where password has not been changed for the specified number of days. + + .PARAMETER DisableWhenCreatedMoreThan + Disable accounts created more than the specified number of days ago. + + .PARAMETER DeleteLastLogonDateMoreThan + Delete accounts that have not logged on for the specified number of days. + + .PARAMETER DeletePasswordLastSetMoreThan + Delete accounts where password has not been changed for the specified number of days. + + .PARAMETER DeleteWhenCreatedMoreThan + Delete accounts created more than the specified number of days ago. + + .PARAMETER ReportOnly + Only report accounts that would be processed. + + .PARAMETER ReportPath + Path to save optional HTML report. + + .PARAMETER ShowHTML + Show HTML report in a browser. + + .PARAMETER Online + Use online resources (CDN) for HTML report. + + .PARAMETER WhatIfDisable + Shows what would happen if accounts were disabled. + + .PARAMETER WhatIfDelete + Shows what would happen if accounts were deleted. + + .EXAMPLE + Invoke-ADServiceAccountsCleanup -Disable -DisableLastLogonDateMoreThan 90 -ReportOnly + + .EXAMPLE + Invoke-ADServiceAccountsCleanup -Delete -DeleteLastLogonDateMoreThan 180 -WhatIfDelete + #> + [CmdletBinding(SupportsShouldProcess)] + param( + [string] $Forest, + [alias('Domain')][string[]] $IncludeDomains, + [string[]] $ExcludeDomains, + [string[]] $IncludeAccounts, + [string[]] $ExcludeAccounts, + [switch] $Disable, + [switch] $Delete, + [int] $DisableLastLogonDateMoreThan, + [int] $DisablePasswordLastSetMoreThan, + [int] $DisableWhenCreatedMoreThan, + [int] $DeleteLastLogonDateMoreThan, + [int] $DeletePasswordLastSetMoreThan, + [int] $DeleteWhenCreatedMoreThan, + [switch] $ReportOnly, + [string] $ReportPath, + [switch] $ShowHTML, + [switch] $Online, + [switch] $WhatIfDisable, + [switch] $WhatIfDelete, + [switch] $DontWriteToEventLog, + [switch] $Suppress, + [string] $LogPath, + [int] $LogMaximum = 5, + [switch] $LogShowTime, + [string] $LogTimeFormat + ) + + Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName + + $Export = [ordered]@{ + Date = Get-Date + Version = Get-GitHubVersion -Cmdlet 'Invoke-ADServiceAccountsCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster' + CurrentRun = @() + History = @() + } + + Write-Color -Text "[i] ", "Started process of cleaning up service accounts" -Color Yellow, White + Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White + + try { + $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains + } catch { + Write-Color -Text "[i] ", "Couldn't get forest. Terminating. Error: $($_.Exception.Message)." -Color Yellow, Red + return + } + + if (-not $Disable -and -not $Delete) { + Write-Color -Text "[i] ", "No action can be taken. Enable Disable and/or Delete." -Color Yellow, Red + return + } + + $Report = Get-InitialADServiceAccounts -ForestInformation $ForestInformation -IncludeAccounts $IncludeAccounts -ExcludeAccounts $ExcludeAccounts + + $Today = Get-Date + [Array]$Processed = @() + foreach ($Domain in $Report.Keys) { + $Accounts = $Report[$Domain]['Accounts'] + if ($Disable) { + $DisableOnlyIf = @{ LastLogonDateMoreThan = $DisableLastLogonDateMoreThan; PasswordLastSetMoreThan = $DisablePasswordLastSetMoreThan; WhenCreatedMoreThan = $DisableWhenCreatedMoreThan } + $ToDisable = Get-ADServiceAccountsToProcess -Type 'Disable' -Accounts $Accounts -ActionIf $DisableOnlyIf -Exclusions $ExcludeAccounts + $Report[$Domain]['AccountsToBeDisabled'] = $ToDisable.Count + $Processed += Request-ADServiceAccountsDisable -Accounts $ToDisable -ReportOnly:$ReportOnly -WhatIfDisable:$WhatIfDisable -Today $Today -DontWriteToEventLog:$DontWriteToEventLog + } + if ($Delete) { + $DeleteOnlyIf = @{ LastLogonDateMoreThan = $DeleteLastLogonDateMoreThan; PasswordLastSetMoreThan = $DeletePasswordLastSetMoreThan; WhenCreatedMoreThan = $DeleteWhenCreatedMoreThan } + $ToDelete = Get-ADServiceAccountsToProcess -Type 'Delete' -Accounts $Accounts -ActionIf $DeleteOnlyIf -Exclusions $ExcludeAccounts + $Report[$Domain]['AccountsToBeDeleted'] = $ToDelete.Count + $Processed += Request-ADServiceAccountsDelete -Accounts $ToDelete -ReportOnly:$ReportOnly -WhatIfDelete:$WhatIfDelete -Today $Today -DontWriteToEventLog:$DontWriteToEventLog + } + } + + $Export.CurrentRun = $Processed + $Export.History = $Processed + + foreach ($Domain in $Report.Keys) { + if ($Disable) { + Write-Color -Text "[i] ", "Accounts to be disabled for domain $Domain`: ", $Report[$Domain]['AccountsToBeDisabled'] -Color Yellow, Cyan, Green + } + if ($Delete) { + Write-Color -Text "[i] ", "Accounts to be deleted for domain $Domain`: ", $Report[$Domain]['AccountsToBeDeleted'] -Color Yellow, Cyan, Green + } + } + + if ($Export -and $ReportPath) { + [Array] $AccountsToProcess = foreach ($Domain in $Report.Keys) { $Report[$Domain]['Accounts'] } + $newHTMLProcessedServiceAccountsSplat = @{ + Export = $Export + FilePath = $ReportPath + Online = $Online.IsPresent + ShowHTML = $ShowHTML.IsPresent + LogFile = $LogPath + AccountsToProcess = $AccountsToProcess + DisableOnlyIf = $DisableOnlyIf + DeleteOnlyIf = $DeleteOnlyIf + Disable = $Disable + Delete = $Delete + ReportOnly = $ReportOnly + } + Write-Color -Text "[i] ", "Generating HTML report ($ReportPath)" -Color Yellow, Magenta + New-HTMLProcessedServiceAccounts @newHTMLProcessedServiceAccountsSplat + } + + Write-Color -Text "[i] Finished process of cleaning up service accounts" -Color Green + if (-not $Suppress) { $Export } +} diff --git a/Tests/Invoke-ADServiceAccountsCleanup.Tests.ps1 b/Tests/Invoke-ADServiceAccountsCleanup.Tests.ps1 new file mode 100644 index 0000000..d599b37 --- /dev/null +++ b/Tests/Invoke-ADServiceAccountsCleanup.Tests.ps1 @@ -0,0 +1,46 @@ +BeforeAll { + $Public = Join-Path $PSScriptRoot '..' 'Public' 'Invoke-ADServiceAccountsCleanup.ps1' + $Private = Get-ChildItem -Path (Join-Path $PSScriptRoot '..' 'Private') -Filter '*ServiceAccounts*.ps1' + . $Public + foreach ($P in $Private) { . $P.FullName } + + function Write-Color { param([Parameter(ValueFromRemainingArguments=$true)]$Text,[object[]]$Color) } + function Set-LoggingCapabilities {} + function Get-GitHubVersion { param($Cmdlet,$RepositoryOwner,$RepositoryName) '0.0.0' } + function Get-WinADForestDetails { param([string]$Forest,[string[]]$IncludeDomains,[string[]]$ExcludeDomains) @{ Domains=@('domain.local'); QueryServers=@{ 'domain.local'=@{ HostName=@('localhost') } }; DomainsExtended=@{}; Forest='domain.local' } } + function Get-ADServiceAccount { param([string]$Filter,[string]$Server,[string[]]$Properties) @() } + function New-HTML { param([scriptblock]$Body,[string]$FilePath,[switch]$Online,[switch]$ShowHTML) & $Body } + function New-HTMLProcessedServiceAccounts {} +} + +Describe 'Invoke-ADServiceAccountsCleanup' { + It 'exports the function' { + Get-Command Invoke-ADServiceAccountsCleanup -ErrorAction Stop | Should -Not -BeNullOrEmpty + } + It 'filters service accounts by last logon date' { + $acc1 = [pscustomobject]@{ SamAccountName='gmsa1'; DistinguishedName='CN=gmsa1,DC=lab,DC=local'; LastLogonDate=(Get-Date).AddDays(-100); PasswordLastSet=(Get-Date).AddDays(-100); WhenCreated=(Get-Date).AddYears(-1) } + $acc2 = [pscustomobject]@{ SamAccountName='gmsa2'; DistinguishedName='CN=gmsa2,DC=lab,DC=local'; LastLogonDate=(Get-Date).AddDays(-5); PasswordLastSet=(Get-Date).AddDays(-5); WhenCreated=(Get-Date).AddDays(-10) } + $res = Get-ADServiceAccountsToProcess -Type 'Disable' -Accounts @($acc1,$acc2) -ActionIf @{ LastLogonDateMoreThan = 30 } + $res.SamAccountName | Should -Be @('gmsa1') + } + It 'supports WhatIf' { + { Invoke-ADServiceAccountsCleanup -Disable -ReportOnly -WhatIfDisable } | Should -Not -Throw + } + It 'generates HTML report when ReportPath is specified' { + Mock -CommandName New-HTMLProcessedServiceAccounts + Invoke-ADServiceAccountsCleanup -Disable -ReportOnly -ReportPath 'out.html' + Assert-MockCalled New-HTMLProcessedServiceAccounts -Times 1 + } + + It 'respects include and exclude account patterns' { + Mock -CommandName Get-ADServiceAccount -MockWith { + @( + [pscustomobject]@{ SamAccountName='gmsa1'; DistinguishedName='CN=gmsa1,DC=lab,DC=local'; LastLogonDate=$null; PasswordLastSet=$null; WhenCreated=$null }, + [pscustomobject]@{ SamAccountName='gmsa2'; DistinguishedName='CN=gmsa2,DC=lab,DC=local'; LastLogonDate=$null; PasswordLastSet=$null; WhenCreated=$null } + ) + } + $forest = @{ Domains=@('lab.local'); QueryServers=@{ 'lab.local'=@{ HostName=@('dc1') } } } + $report = Get-InitialADServiceAccounts -ForestInformation $forest -IncludeAccounts @('gmsa*') -ExcludeAccounts @('gmsa2') + $report['lab.local'].Accounts.SamAccountName | Should -Be @('gmsa1') + } +}