├── .github └── FUNDING.yml ├── .gitignore ├── Build └── Build-Module.ps1 ├── CHANGELOG.MD ├── CleanupMonster.psd1 ├── CleanupMonster.psm1 ├── Docs ├── Invoke-ADComputersCleanup.md └── Readme.md ├── Examples ├── DeleteComputers.ps1 ├── DeleteComputersEnableSource.ps1 ├── DeleteComputersInteractive.ps1 ├── DeleteComputersInteractive02.ps1 ├── DeleteComputersWithJamfAndO365.ps1 ├── DeleteComputersWithMoveAndEmail.ps1 ├── DeleteComputersWithO365.ps1 ├── DeleteComputersWithO365andJAMF.ps1 ├── DeleteSIDHistory.ps1 ├── HowToChangeTaskToGMSA.ps1 ├── HowToPreparePasswordForUse.ps1 ├── HowToReEnableComputer.ps1 ├── HowToRemoveProtectedFromDeletionAll.ps1 ├── HowToRemoveProtectedFromDeletionFew.ps1 └── Images │ ├── CleanupDevicesAllRemaining.png │ ├── CleanupDevicesCurrentRun.png │ ├── CleanupDevicesHistory.png │ ├── CleanupDevicesPending.png │ ├── CleanupDevicesReport.png │ ├── SIDHistoryEmail.png │ ├── SIDHistoryReportAll.png │ ├── SIDHistoryReportCurrentRun.png │ ├── SIDHistoryReportHistory.png │ └── SIDHistoryReportLogs.png ├── Private ├── Assert-InitialSettings.ps1 ├── Convert-ListProcessed.ps1 ├── ConvertTo-PreparedComputer.ps1 ├── Disable-WinADComputer.ps1 ├── Get-ADComputersToProcess.ps1 ├── Get-InitialADComputers.ps1 ├── Get-InitialGraphComputers.ps1 ├── Get-InitialJamfComputers.ps1 ├── Import-ComputersData.ps1 ├── Import-SIDHistory.ps1 ├── Move-WinADComputer.ps1 ├── New-ADComputersStatistics.ps1 ├── New-EmailBodyComputers.ps1 ├── New-EmailBodySidHistory.ps1 ├── New-HTMLProcessedComputers.ps1 ├── New-HTMLProcessedSIDHistory.ps1 ├── Remove-ADSIDHistory.ps1 ├── Request-ADComputersDelete.ps1 ├── Request-ADComputersDisable.ps1 ├── Request-ADComputersMove.ps1 └── Request-ADSIDHistory.ps1 ├── Public ├── Invoke-ADComputersCleanup.ps1 └── Invoke-ADSIDHistoryCleanup.ps1 └── README.MD /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: PrzemyslawKlys 2 | custom: ["https://paypal.me/PrzemyslawKlys"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | Ignore/* 2 | .vs/* 3 | .vscode/* 4 | Examples/Output/* 5 | Examples/Reports/* 6 | Examples/Logs/* 7 | Examples/Package/* 8 | Examples/*.xml 9 | Examples/*.html 10 | Releases/* 11 | Artefacts/* 12 | ReleasedUnpacked/* 13 | Sources/.vs 14 | Sources/*/.vs 15 | Sources/*/obj 16 | Sources/*/bin 17 | Sources/*/*/obj 18 | Sources/*/*/bin 19 | Sources/packages/* 20 | Lib/Default/* 21 | Lib/Standard/* 22 | Lib/Core/* -------------------------------------------------------------------------------- /Build/Build-Module.ps1: -------------------------------------------------------------------------------- 1 | Clear-Host 2 | 3 | Invoke-ModuleBuild -ModuleName 'CleanupMonster' { 4 | # Usual defaults as per standard module 5 | $Manifest = [ordered] @{ 6 | ModuleVersion = '3.1.X' 7 | CompatiblePSEditions = @('Desktop', 'Core') 8 | GUID = 'cd1f9987-6242-452c-a7db-6337d4a6b639' 9 | Author = 'Przemyslaw Klys' 10 | CompanyName = 'Evotec' 11 | Copyright = "(c) 2011 - $((Get-Date).Year) Przemyslaw Klys @ Evotec. All rights reserved." 12 | 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." 13 | PowerShellVersion = '5.1' 14 | Tags = 'windows', 'activedirectory' 15 | IconUri = 'https://evotec.xyz/wp-content/uploads/2023/04/CleanupMonster.png' 16 | ProjectUri = 'https://github.com/EvotecIT/CleanupMonster' 17 | #DotNetFrameworkVersion = '4.5.2' 18 | } 19 | New-ConfigurationManifest @Manifest 20 | 21 | New-ConfigurationModule -Type RequiredModule -Name 'PSSharedGoods', 'PSWriteHTML', 'PSWriteColor', 'PSEventViewer', 'ADEssentials' -Guid Auto -Version Latest 22 | New-ConfigurationModule -Type ExternalModule -Name @( 23 | 'ActiveDirectory', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management' 24 | 'Microsoft.WSMan.Management', 'NetTCPIP', 'CimCmdlets' 25 | ) 26 | New-ConfigurationModule -Type ApprovedModule -Name 'PSSharedGoods', 'PSWriteColor', 'Connectimo', 'PSUnifi', 'PSWebToolbox', 'PSMyPassword', 'PSPublishModule', 'ADEssentials' 27 | New-ConfigurationModuleSkip -IgnoreModuleName 'PowerJamf', 'GraphEssentials' -IgnoreFunctionName @( 28 | 'Get-JamfDevice', 'Get-MyDevice', 'Get-MyDeviceIntune' 29 | ) 30 | 31 | $ConfigurationFormat = [ordered] @{ 32 | RemoveComments = $false 33 | 34 | PlaceOpenBraceEnable = $true 35 | PlaceOpenBraceOnSameLine = $true 36 | PlaceOpenBraceNewLineAfter = $true 37 | PlaceOpenBraceIgnoreOneLineBlock = $true 38 | 39 | PlaceCloseBraceEnable = $true 40 | PlaceCloseBraceNewLineAfter = $false 41 | PlaceCloseBraceIgnoreOneLineBlock = $true 42 | PlaceCloseBraceNoEmptyLineBefore = $false 43 | 44 | UseConsistentIndentationEnable = $true 45 | UseConsistentIndentationKind = 'space' 46 | UseConsistentIndentationPipelineIndentation = 'IncreaseIndentationAfterEveryPipeline' 47 | UseConsistentIndentationIndentationSize = 4 48 | 49 | UseConsistentWhitespaceEnable = $true 50 | UseConsistentWhitespaceCheckInnerBrace = $true 51 | UseConsistentWhitespaceCheckOpenBrace = $true 52 | UseConsistentWhitespaceCheckOpenParen = $true 53 | UseConsistentWhitespaceCheckOperator = $true 54 | UseConsistentWhitespaceCheckPipe = $true 55 | UseConsistentWhitespaceCheckSeparator = $true 56 | 57 | AlignAssignmentStatementEnable = $true 58 | AlignAssignmentStatementCheckHashtable = $true 59 | 60 | UseCorrectCasingEnable = $true 61 | } 62 | # format PSD1 and PSM1 files when merging into a single file 63 | # enable formatting is not required as Configuration is provided 64 | New-ConfigurationFormat -ApplyTo 'OnMergePSM1', 'OnMergePSD1' -Sort None @ConfigurationFormat 65 | # format PSD1 and PSM1 files within the module 66 | # enable formatting is required to make sure that formatting is applied (with default settings) 67 | New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'DefaultPSM1' -EnableFormatting -Sort None 68 | # when creating PSD1 use special style without comments and with only required parameters 69 | New-ConfigurationFormat -ApplyTo 'DefaultPSD1', 'OnMergePSD1' -PSD1Style 'Minimal' 70 | # configuration for documentation, at the same time it enables documentation processing 71 | New-ConfigurationDocumentation -Enable:$false -StartClean -UpdateWhenNew -PathReadme 'Docs\Readme.md' -Path 'Docs' 72 | 73 | #New-ConfigurationImportModule -ImportSelf 74 | 75 | New-ConfigurationBuild -Enable:$true -SignModule -MergeModuleOnBuild -MergeFunctionsFromApprovedModules -CertificateThumbprint '483292C9E317AA13B07BB7A96AE9D1A5ED9E7703' 76 | 77 | New-ConfigurationArtefact -Type Unpacked -Enable -Path "$PSScriptRoot\..\Artefacts\Unpacked" -ModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\Modules" -RequiredModulesPath "$PSScriptRoot\..\Artefacts\Unpacked\Modules" -AddRequiredModules 78 | New-ConfigurationArtefact -Type Packed -Enable -Path "$PSScriptRoot\..\Artefacts\Packed" -ArtefactName '.v.zip' 79 | 80 | # options for publishing to github/psgallery 81 | #New-ConfigurationPublish -Type PowerShellGallery -FilePath 'C:\Support\Important\PowerShellGalleryAPI.txt' -Enabled 82 | #New-ConfigurationPublish -Type GitHub -FilePath 'C:\Support\Important\GitHubAPI.txt' -UserName 'EvotecIT' -Enabled 83 | } -------------------------------------------------------------------------------- /CHANGELOG.MD: -------------------------------------------------------------------------------- 1 |  2 | ### 3.1.1 - 2025.04.01 3 | #### What's Changed 4 | * Improve SIDHistory Cleanup by removing not working code by @PrzemyslawKlys in https://github.com/EvotecIT/CleanupMonster/pull/27 5 | 6 | **Full Changelog**: https://github.com/EvotecIT/CleanupMonster/compare/v3.1.0...v3.1.1 7 | 8 | ### 3.1.0 - 2025.04.01 9 | 10 | #### What's Changed 11 | * Add protected from accidental deletion removal by @PrzemyslawKlys in https://github.com/EvotecIT/CleanupMonster/pull/25 12 | * feat(Build): ✨ update `ModuleVersion` to `3.1.0` by @PrzemyslawKlys in https://github.com/EvotecIT/CleanupMonster/pull/26 13 | 14 | **Full Changelog**: https://github.com/EvotecIT/CleanupMonster/compare/v3.0.5...v3.1.0 15 | 16 | ### 3.0.5 - 2025.03.28 17 | - Remove `ADEssentials` dependency (in PSGallery only, development still requires it), as it was included by mistake 18 | 19 | ### 3.0.4 - 2025.03.25 20 | - Bump dependencies to `GraphEssentials` 21 | 22 | ### 3.0.3 - 2025.03.24 23 | - Bump dependencies to `GraphEssentials` 24 | 25 | ### 3.0.2 - 2025.03.16 26 | - Improvement to `Invoke-ADSIDHistoryCleanup` 27 | 28 | ### 3.0.1 - 2025.03.16 29 | - Improvement to `Invoke-ADSIDHistoryCleanup` 30 | 31 | ### 3.0.0 - 2025.03.16 32 | - Added `Invoke-ADSIDHistoryCleanup` to cleanup SIDHistory. Please [read blog post about it](https://evotec.xyz/mastering-active-directory-hygiene-automating-sidhistory-cleanup-with-cleanupmonster/) before trying to use it. 33 | 34 | THIS IS DANGEROUS COMMAND. USE WITH CAUTION. IT WILL REMOVE SIDHISTORY FROM ALL USERS IN THE DOMAIN IF CONFIGURED TO DO SO. 35 | 36 | ### 2.8.8 - 2025.03.09 37 | - Fix deletion of log files/reports to specific extensions only [#24](https://github.com/EvotecIT/PasswordSolution/issues/24) 38 | - Added `TimeOnPendingList` to HTML report and passthru object 39 | - Added `TimeToLeavePendingList` to HTML report and passthru object 40 | - Improve detection of 'PowerJamf' and 'GraphEssentials' requiring minimum specific versions 41 | - Bump versions of other modules that are integral part of this module 42 | 43 | ### 2.8.7 - 2024.10.21 44 | - Do verification for missing modules PowerJamf, GraphEssentials when using AzureAD, Intune, Jamf Pro 45 | - Add TargetServers to be able to manually choose servers to target 46 | 47 | ### 2.8.6 - 2024.09.02 48 | - Filter now takes string or hashtable so it can be configured per domain 49 | - SearchBase was added. It can take string or hashtable so it can be configured per domain. If you use a string remember that it will only target a single domain and rest of the domains have to be excluded - closes [#13](https://github.com/EvotecIT/CleanupMonster/issues/13) 50 | 51 | ```powershell 52 | SearchBase = @{ 53 | 'ad.evotec.xyz' = 'DC=ad,DC=evotec,DC=xyz' 54 | 'ad.evotec.pl' = 'DC=ad,DC=evotec,DC=pl' 55 | } 56 | ``` 57 | 58 | ### 2.8.5 - 2024.09.02 59 | -Fixes for error in logging when using module interactively in console instead as a script 60 | 61 | ### 2.8.4 - 2024.08.30 62 | - Update HTML report with additional colors 63 | 64 | ### 2.8.3 - 2024.08.30 65 | - Fix wrong usage of string in `DisableMoveTargetOrganizationalUnit` 66 | 67 | ### 2.8.2 - 2024.08.21 68 | - Added to display `ExcludedByFilter`, `ExcludedBySetting` to HTML report. Keep in mind that standard filtering such as LastPassword/LastLogon/WhenCreated are ignored and are treated as "Not required". 69 | 70 | ### 2.8.1 - 2024.08.20 71 | - Add `ProtectedFromAccidentalDeletion` to the report 72 | 73 | ### 2.8.0 - 2024.08.19 74 | Added 3 new parameters: 75 | - `DisableRequireWhenCreatedMoreThan` - disable computers only if they were created more than X days ago 76 | - `MoveRequireWhenCreatedMoreThan` - move computers only if they were created more than X days ago 77 | - `DeleteRequireWhenCreatedMoreThan` - delete computers only if they were created more than X days ago 78 | This is to prevent completly new computers being disabled, moved or deleted. By default Disable/Delete are set to old than 90 days old, and Move is not set at all. 79 | 80 | ### 2.7.2 - 2024.08.12 81 | - Fixes for processing computer over and over again when moving computers 82 | 83 | ### 2.7.1 - 2024.08.07 84 | - Fixes bad export of aliases in module 85 | 86 | ### 2.7.0 - 2024.07.31 87 | - Added `DisableAndMoveOrder` to allow for moving computers before disabling them when DisableAndMove is enabled. Options are: `DisableAndMove`, `MoveAndDisable`. Default is `DisableAndMove` (current setting) 88 | - Improve handling of processed list when moving computers 89 | - Improve output in HTML report 90 | 91 | ### 2.6.2 92 | - Fix typo in HTML report 93 | 94 | ### 2.6.1 95 | - Improve Logging Capabilities to not delete log if it's the same folder 96 | - Add additional loggin options 97 | 98 | ### 2.6.0 99 | - Fix display issue with HTML report 100 | 101 | ### 2.5.0 102 | - Fix for Jamf Pro where it would only process 100 computers 103 | - Fix for processed list during deletion would use DN instead of FullName as expected, preventing deletion 104 | 105 | ### 2.4.1 - 2023.06.06 106 | - Fixes typo in a report 107 | 108 | ### 2.4.0 - 2023.05.30 109 | - Fixes removing computers from processed list after list conversion 110 | 111 | ### 2.2.0 - 2023.05.30 112 | - Fixes conversion logic 113 | 114 | ### 2.1.0 - 2023.05.30 115 | - Fixes bug around AzureAD, Intune, Jamf processing wrong computers 116 | - Fixed wrong function placement on import 117 | - Small improvements 118 | 119 | ### 2.0.0 120 | Issues resolved: 121 | - Implement move and move on disable [#2](https://github.com/EvotecIT/CleanupMonster/issues/2) 122 | - Allow exclude or include for Disable/Delete or Move and Service Principal Name [#7](https://github.com/EvotecIT/CleanupMonster/issues/7) 123 | 124 | Improvements: 125 | - Add ability to move objects (disable, move or/and delete) as separate action 126 | - disable, move and delete all have their separate rules to act upon 127 | - Add ability to move objects as part of the disable process (disable and move right after) - `DisableAndMove` switch 128 | - Fix handling processing list (using custom search rather then DN which would not work if object was moved) 129 | - Add `DontWriteToEventLog` switch to disable writing to event log 130 | - Processed lists now only remove items if delete or move is successful 131 | - Computers is removed from processed list if it's moved, and there's no delete action, otherwise it's removed only on delete. Make sure to not use `Delete` switch if you don't plan to delete objects 132 | - Add ability to exclude or include computers based on specific SPN (Service Principal Name) - `DisableExcludeServicePrincipalName` and `DisableIncludeServicePrincipalName` 133 | - Add ability to exclude or include computers based on specific SPN (Service Principal Name) - `MoveExcludeServicePrincipalName` and `MoveIncludeServicePrincipalName` 134 | - Add ability to exclude or include computers based on specific SPN (Service Principal Name) - `DeleteExcludeServicePrincipalName` and `DeleteIncludeServicePrincipalName` 135 | - Added `DoNotAddToPendingList` switch to disable adding computers to pending list when disabling or moving, and when deletion is enabled 136 | 137 | ### 1.6.0 - 2023.05.12 138 | Issues resolved: 139 | - Fixes [#4](https://github.com/EvotecIT/CleanupMonster/issues/4) 140 | 141 | Following features are added: 142 | - Add support for Forest 143 | - Add support for IncludeDomains 144 | - Add support for ExcludeDomain 145 | 146 | ### 1.5.0 - 2023.05.12 147 | - Fixes an error when using processed list when deleting computers 148 | - Fixes an +1 in the count of objects displayed in HTML 149 | 150 | ### 1.4.0 - 2023.05.08 151 | - Add an alias to Invoke-ADComputersCleanup 152 | - Show AllProperties for History & Pending values in HTML report 153 | - Fix small issues with HTML 154 | - Fixes critical logic flaw for AzureAD, Intune, Jamf 155 | 156 | ### 1.3.0 - 2023.05.08 157 | - Remove verbose from Jamf queries 158 | 159 | ### 1.2.0 160 | - Renamed PowerShell module to CleanupMonster 161 | - Invoke-ADComputersCleanup 162 | - Support for more AD controlled parameters 163 | - Added `DisablePasswordLastSetOlderThan` 164 | - Added `DisableLastLogonDateOlderThan` 165 | - Added `DeletePasswordLastSetOlderThan` 166 | - Added `DeleteLastLogonDateOlderThan` 167 | - Support for Azure AD and Intune (via Graph API, requires GraphEssentials module, not installed by default) 168 | - Added `DisableLastSeenAzureMoreThan` 169 | - Added `DisableLastSyncAzureMoreThan` 170 | - Added `DisableLastSeenIntuneMoreThan` 171 | - Added `DeleteLastSeenAzureMoreThan` 172 | - Added `DeleteLastSyncAzureMoreThan` 173 | - Added `DeleteLastSeenIntuneMoreThan` 174 | - Support for Jamf Pro (via Jamf Pro API, requires PowerJamf module, not installed by default) 175 | - Added `DisableLastContactJamfMoreThan` 176 | - Added `DeleteLastContactJamfMoreThan` 177 | - Support for Safety Limits (cleanup will cancel if conditions are not matched, by default disabled) 178 | - Added `SafetyADLimit` - minimum X number of computers to be returned from the AD 179 | - Added `SafetyAzureADLimit` - minimum X number of computers to be returned from the Azure AD 180 | - Added `SafetyIntuneLimit` - minimum X number of computers to be returned from the Intune 181 | - Added `SafetyJamfLimit` - minimum X number of computers to be returned from the Jamf Pro 182 | - Reworked HTML reports to be more readable and some logic changes 183 | 184 | Showing new options 185 | 186 | ```powershell 187 | $DateTime = Get-Date -Year 2021 -Month 8 -Day 19 -Hour 0 -Minute 0 -Second 0 188 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -ShowHTML -DisablePasswordLastSetOlderThan $DateTime -DisableLastLogonDateOlderThan $DateTime -DeletePasswordLastSetOlderThan $DateTime -DeleteLastLogonDateOlderThan $DateTime 189 | $Output 190 | ``` 191 | 192 | With support for Jamf and Azure AD 193 | 194 | ```powershell 195 | Connect-MgGraph -Scopes Device.Read.All, DeviceManagementManagedDevices.Read.All, Directory.ReadWrite.All, DeviceManagementConfiguration.Read.All 196 | Connect-Jamf -Organization 'aaa' -UserName 'aaaa' -Suppress -Force -Password '01000000d08c9ddf0115d1118c7a0' 197 | 198 | # this is a fresh run and it will provide report only according to it's defaults 199 | $Output = Invoke-ADComputersCleanup -ReportOnly -SafetyJamfLimit 99 -WhatIf -Disable -ShowHTML -DisableLastSeenAzureMoreThan 80 -DisableLastSyncAzureMoreThan 80 -DisableLastSeenIntuneMoreThan 80 -DisableLastContactJamfMoreThan 80 #-Delete -DeleteListProcessedMoreThan 80 200 | $Output 201 | ``` 202 | 203 | ### 1.1.4 - 2023.04.03 204 | - Clarify HTML information when it comes to PasswordLastSet and LastLogonDays 205 | 206 | ### 1.1.3 - 2023.04.01 207 | - Improved reporting when using `ReportOnly` switch 208 | - Improved logging and error handling 209 | - Added some statistics to the report 210 | - Added ability to delete older reports 211 | 212 | ### 1.1.2 - 2023.03.31 213 | - Improved reporting when using `ReportOnly` switch 214 | - Added some more logging and error handling 215 | 216 | ### 1.1.1 - 2023.03.21 217 | - Improved reporting 218 | - Fixes [#1](https://github.com/EvotecIT/CleanupMonster/issues/1) where it would not add computers to IsProcessedList 219 | 220 | ### 1.0.0 221 | - Initial release -------------------------------------------------------------------------------- /CleanupMonster.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | AliasesToExport = @() 3 | Author = 'Przemyslaw Klys' 4 | CmdletsToExport = @() 5 | CompanyName = 'Evotec' 6 | CompatiblePSEditions = @('Desktop', 'Core') 7 | Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' 8 | 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.' 9 | FunctionsToExport = @('Invoke-ADComputersCleanup', 'Invoke-ADSIDHistoryCleanup') 10 | GUID = 'cd1f9987-6242-452c-a7db-6337d4a6b639' 11 | ModuleVersion = '3.1.1' 12 | PowerShellVersion = '5.1' 13 | PrivateData = @{ 14 | PSData = @{ 15 | ExternalModuleDependencies = @('ActiveDirectory', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management', 'Microsoft.WSMan.Management', 'NetTCPIP', 'CimCmdlets') 16 | IconUri = 'https://evotec.xyz/wp-content/uploads/2023/04/CleanupMonster.png' 17 | ProjectUri = 'https://github.com/EvotecIT/CleanupMonster' 18 | Tags = @('windows', 'activedirectory') 19 | } 20 | } 21 | RequiredModules = @(@{ 22 | Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' 23 | ModuleName = 'PSSharedGoods' 24 | ModuleVersion = '0.0.306' 25 | }, @{ 26 | Guid = 'a7bdf640-f5cb-4acf-9de0-365b322d245c' 27 | ModuleName = 'PSWriteHTML' 28 | ModuleVersion = '1.28.0' 29 | }, @{ 30 | Guid = '0b0ba5c5-ec85-4c2b-a718-874e55a8bc3f' 31 | ModuleName = 'PSWriteColor' 32 | ModuleVersion = '1.0.1' 33 | }, @{ 34 | Guid = '5df72a79-cdf6-4add-b38d-bcacf26fb7bc' 35 | ModuleName = 'PSEventViewer' 36 | ModuleVersion = '2.4.3' 37 | }, @{ 38 | Guid = '9fc9fd61-7f11-4f4b-a527-084086f1905f' 39 | ModuleName = 'ADEssentials' 40 | ModuleVersion = '0.0.231' 41 | }, 'ActiveDirectory', 'Microsoft.PowerShell.Utility', 'Microsoft.PowerShell.Management', 'Microsoft.WSMan.Management', 'NetTCPIP', 'CimCmdlets') 42 | RootModule = 'CleanupMonster.psm1' 43 | } -------------------------------------------------------------------------------- /CleanupMonster.psm1: -------------------------------------------------------------------------------- 1 | # Get public and private function definition files. 2 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 3 | $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 4 | $Classes = @( Get-ChildItem -Path $PSScriptRoot\Classes\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 5 | $Enums = @( Get-ChildItem -Path $PSScriptRoot\Enums\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 6 | # Get all assemblies 7 | $AssemblyFolders = Get-ChildItem -Path $PSScriptRoot\Lib -Directory -ErrorAction SilentlyContinue 8 | 9 | # Lets find which libraries we need to load 10 | $Default = $false 11 | $Core = $false 12 | $Standard = $false 13 | foreach ($A in $AssemblyFolders.Name) { 14 | if ($A -eq 'Default') { 15 | $Default = $true 16 | } elseif ($A -eq 'Core') { 17 | $Core = $true 18 | } elseif ($A -eq 'Standard') { 19 | $Standard = $true 20 | } 21 | } 22 | if ($Standard -and $Core -and $Default) { 23 | $FrameworkNet = 'Default' 24 | $Framework = 'Standard' 25 | } elseif ($Standard -and $Core) { 26 | $Framework = 'Standard' 27 | $FrameworkNet = 'Standard' 28 | } elseif ($Core -and $Default) { 29 | $Framework = 'Core' 30 | $FrameworkNet = 'Default' 31 | } elseif ($Standard -and $Default) { 32 | $Framework = 'Standard' 33 | $FrameworkNet = 'Default' 34 | } elseif ($Standard) { 35 | $Framework = 'Standard' 36 | $FrameworkNet = 'Standard' 37 | } elseif ($Core) { 38 | $Framework = 'Core' 39 | $FrameworkNet = '' 40 | } elseif ($Default) { 41 | $Framework = '' 42 | $FrameworkNet = 'Default' 43 | } else { 44 | #Write-Error -Message 'No assemblies found' 45 | } 46 | 47 | $Assembly = @( 48 | if ($Framework -and $PSEdition -eq 'Core') { 49 | Get-ChildItem -Path $PSScriptRoot\Lib\$Framework\*.dll -ErrorAction SilentlyContinue -Recurse 50 | } 51 | if ($FrameworkNet -and $PSEdition -ne 'Core') { 52 | Get-ChildItem -Path $PSScriptRoot\Lib\$FrameworkNet\*.dll -ErrorAction SilentlyContinue -Recurse 53 | } 54 | ) 55 | $FoundErrors = @( 56 | Foreach ($Import in @($Assembly)) { 57 | try { 58 | Write-Verbose -Message $Import.FullName 59 | Add-Type -Path $Import.Fullname -ErrorAction Stop 60 | # } 61 | } catch [System.Reflection.ReflectionTypeLoadException] { 62 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 63 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 64 | foreach ($E in $LoaderExceptions) { 65 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 66 | } 67 | $true 68 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 69 | } catch { 70 | Write-Warning "Processing $($Import.Name) Exception: $($_.Exception.Message)" 71 | $LoaderExceptions = $($_.Exception.LoaderExceptions) | Sort-Object -Unique 72 | foreach ($E in $LoaderExceptions) { 73 | Write-Warning "Processing $($Import.Name) LoaderExceptions: $($E.Message)" 74 | } 75 | $true 76 | #Write-Error -Message "StackTrace: $($_.Exception.StackTrace)" 77 | } 78 | } 79 | #Dot source the files 80 | Foreach ($Import in @($Classes + $Enums + $Private + $Public)) { 81 | Try { 82 | . $Import.Fullname 83 | } Catch { 84 | Write-Error -Message "Failed to import functions from $($import.Fullname): $_" 85 | $true 86 | } 87 | } 88 | ) 89 | 90 | if ($FoundErrors.Count -gt 0) { 91 | $ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName 92 | Write-Warning "Importing module $ModuleName failed. Fix errors before continuing." 93 | break 94 | } 95 | 96 | Export-ModuleMember -Function '*' -Alias '*' -------------------------------------------------------------------------------- /Docs/Invoke-ADComputersCleanup.md: -------------------------------------------------------------------------------- 1 | --- 2 | external help file: CleanupMonster-help.xml 3 | Module Name: CleanupMonster 4 | online version: 5 | schema: 2.0.0 6 | --- 7 | 8 | # Invoke-ADComputersCleanup 9 | 10 | ## SYNOPSIS 11 | Active Directory Cleanup function that can disable or delete computers 12 | that have not been logged on for a certain amount of time. 13 | 14 | ## SYNTAX 15 | 16 | ``` 17 | Invoke-ADComputersCleanup [-Disable] [-Delete] [[-DisableIsEnabled] ] 18 | [[-DisableNoServicePrincipalName] ] [[-DisableLastLogonDateMoreThan] ] 19 | [[-DisablePasswordLastSetMoreThan] ] [[-DisableExcludeSystems] ] 20 | [[-DisableIncludeSystems] ] [[-DeleteIsEnabled] ] [[-DeleteNoServicePrincipalName] ] 21 | [[-DeleteLastLogonDateMoreThan] ] [[-DeletePasswordLastSetMoreThan] ] 22 | [[-DeleteListProcessedMoreThan] ] [[-DeleteExcludeSystems] ] [[-DeleteIncludeSystems] ] 23 | [[-DeleteLimit] ] [[-DisableLimit] ] [[-Exclusions] ] [-DisableModifyDescription] 24 | [-DisableModifyAdminDescription] [[-Filter] ] [[-DataStorePath] ] [-ReportOnly] 25 | [[-ReportMaximum] ] [-WhatIfDelete] [-WhatIfDisable] [[-LogPath] ] [[-LogMaximum] ] 26 | [-Suppress] [-ShowHTML] [-Online] [[-ReportPath] ] [-WhatIf] [-Confirm] [] 27 | ``` 28 | 29 | ## DESCRIPTION 30 | Active Directory Cleanup function that can disable or delete computers 31 | that have not been logged on for a certain amount of time. 32 | It has many options to customize the cleanup process. 33 | 34 | ## EXAMPLES 35 | 36 | ### EXAMPLE 1 37 | ``` 38 | $Output = Invoke-ADComputersCleanup -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 39 | ``` 40 | 41 | $Output 42 | 43 | ### EXAMPLE 2 44 | ``` 45 | $Output = Invoke-ADComputersCleanup -DeleteListProcessedMoreThan 100 -Disable -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 46 | ``` 47 | 48 | $Output 49 | 50 | ### EXAMPLE 3 51 | ``` 52 | # this is a fresh run and it will provide report only according to it's defaults 53 | ``` 54 | 55 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML 56 | $Output 57 | 58 | ### EXAMPLE 4 59 | ``` 60 | # this is a fresh run and it will try to disable computers according to it's defaults 61 | ``` 62 | 63 | # read documentation to understand what it does 64 | $Output = Invoke-ADComputersCleanup -Disable -ShowHTML -WhatIfDisable -WhatIfDelete -Delete 65 | $Output 66 | 67 | ### EXAMPLE 5 68 | ``` 69 | # this is a fresh run and it will try to delete computers according to it's defaults 70 | ``` 71 | 72 | # read documentation to understand what it does 73 | $Output = Invoke-ADComputersCleanup -Delete -WhatIfDelete -ShowHTML -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 74 | $Output 75 | 76 | ### EXAMPLE 6 77 | ``` 78 | # Run the script 79 | ``` 80 | 81 | $Configuration = @{ 82 | Disable = $true 83 | DisableNoServicePrincipalName = $null 84 | DisableIsEnabled = $true 85 | DisableLastLogonDateMoreThan = 90 86 | DisablePasswordLastSetMoreThan = 90 87 | DisableExcludeSystems = @( 88 | # 'Windows Server*' 89 | ) 90 | DisableIncludeSystems = @() 91 | DisableLimit = 2 # 0 means unlimited, ignored for reports 92 | DisableModifyDescription = $false 93 | DisableAdminModifyDescription = $true 94 | 95 | Delete = $true 96 | DeleteIsEnabled = $false 97 | DeleteNoServicePrincipalName = $null 98 | DeleteLastLogonDateMoreThan = 180 99 | DeletePasswordLastSetMoreThan = 180 100 | DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list 101 | DeleteExcludeSystems = @( 102 | # 'Windows Server*' 103 | ) 104 | DeleteIncludeSystems = @( 105 | 106 | ) 107 | DeleteLimit = 2 # 0 means unlimited, ignored for reports 108 | 109 | Exclusions = @( 110 | '*OU=Domain Controllers*' 111 | '*OU=Servers,OU=Production*' 112 | 'EVOMONSTER$' 113 | 'EVOMONSTER.AD.EVOTEC.XYZ' 114 | ) 115 | 116 | Filter = '*' 117 | WhatIfDisable = $true 118 | WhatIfDelete = $true 119 | LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 120 | DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml" 121 | ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 122 | ShowHTML = $true 123 | } 124 | 125 | # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers' 126 | $Output = Invoke-ADComputersCleanup @Configuration 127 | $Output 128 | 129 | ## PARAMETERS 130 | 131 | ### -Disable 132 | Enable the disable process, meaning the computers that meet the criteria will be disabled. 133 | 134 | ```yaml 135 | Type: SwitchParameter 136 | Parameter Sets: (All) 137 | Aliases: 138 | 139 | Required: False 140 | Position: Named 141 | Default value: False 142 | Accept pipeline input: False 143 | Accept wildcard characters: False 144 | ``` 145 | 146 | ### -Delete 147 | Enable the delete process, meaning the computers that meet the criteria will be deleted. 148 | 149 | ```yaml 150 | Type: SwitchParameter 151 | Parameter Sets: (All) 152 | Aliases: 153 | 154 | Required: False 155 | Position: Named 156 | Default value: False 157 | Accept pipeline input: False 158 | Accept wildcard characters: False 159 | ``` 160 | 161 | ### -DisableIsEnabled 162 | Disable computer only if it's Enabled or only if it's Disabled. 163 | By default it will try to disable all computers that are either disabled or enabled. 164 | While counter-intuitive for already disabled computers, 165 | this is useful if you want preproceess computers for deletion and need to get them on the list. 166 | 167 | ```yaml 168 | Type: Boolean 169 | Parameter Sets: (All) 170 | Aliases: 171 | 172 | Required: False 173 | Position: 1 174 | Default value: None 175 | Accept pipeline input: False 176 | Accept wildcard characters: False 177 | ``` 178 | 179 | ### -DisableNoServicePrincipalName 180 | Disable computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName. 181 | By default it doesn't care if it has a ServicePrincipalName or not. 182 | 183 | ```yaml 184 | Type: Boolean 185 | Parameter Sets: (All) 186 | Aliases: 187 | 188 | Required: False 189 | Position: 2 190 | Default value: None 191 | Accept pipeline input: False 192 | Accept wildcard characters: False 193 | ``` 194 | 195 | ### -DisableLastLogonDateMoreThan 196 | Disable computer only if it has a LastLogonDate that is more than the specified number of days. 197 | 198 | ```yaml 199 | Type: Int32 200 | Parameter Sets: (All) 201 | Aliases: 202 | 203 | Required: False 204 | Position: 3 205 | Default value: 180 206 | Accept pipeline input: False 207 | Accept wildcard characters: False 208 | ``` 209 | 210 | ### -DisablePasswordLastSetMoreThan 211 | Disable computer only if it has a PasswordLastSet that is more than the specified number of days. 212 | 213 | ```yaml 214 | Type: Int32 215 | Parameter Sets: (All) 216 | Aliases: 217 | 218 | Required: False 219 | Position: 4 220 | Default value: 180 221 | Accept pipeline input: False 222 | Accept wildcard characters: False 223 | ``` 224 | 225 | ### -DisableExcludeSystems 226 | Disable computer only if it's not on the list of excluded operating systems. 227 | If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. 228 | You can also specify multiple operating systems by separating them with a comma. 229 | It's using the -like operator, so you can use wildcards. 230 | It's using OperatingSystem property of the computer object for comparison. 231 | 232 | ```yaml 233 | Type: Array 234 | Parameter Sets: (All) 235 | Aliases: 236 | 237 | Required: False 238 | Position: 5 239 | Default value: @() 240 | Accept pipeline input: False 241 | Accept wildcard characters: False 242 | ``` 243 | 244 | ### -DisableIncludeSystems 245 | Disable computer only if it's on the list of included operating systems. 246 | If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*' 247 | or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. 248 | You can also specify multiple operating systems by separating them with a comma. 249 | It's using the -like operator, so you can use wildcards. 250 | 251 | ```yaml 252 | Type: Array 253 | Parameter Sets: (All) 254 | Aliases: 255 | 256 | Required: False 257 | Position: 6 258 | Default value: @() 259 | Accept pipeline input: False 260 | Accept wildcard characters: False 261 | ``` 262 | 263 | ### -DeleteIsEnabled 264 | Delete computer only if it's Enabled or only if it's Disabled. 265 | By default it will try to delete all computers that are either disabled or enabled. 266 | 267 | ```yaml 268 | Type: Boolean 269 | Parameter Sets: (All) 270 | Aliases: 271 | 272 | Required: False 273 | Position: 7 274 | Default value: None 275 | Accept pipeline input: False 276 | Accept wildcard characters: False 277 | ``` 278 | 279 | ### -DeleteNoServicePrincipalName 280 | Delete computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName. 281 | By default it doesn't care if it has a ServicePrincipalName or not. 282 | 283 | ```yaml 284 | Type: Boolean 285 | Parameter Sets: (All) 286 | Aliases: 287 | 288 | Required: False 289 | Position: 8 290 | Default value: None 291 | Accept pipeline input: False 292 | Accept wildcard characters: False 293 | ``` 294 | 295 | ### -DeleteLastLogonDateMoreThan 296 | Delete computer only if it has a LastLogonDate that is more than the specified number of days. 297 | 298 | ```yaml 299 | Type: Int32 300 | Parameter Sets: (All) 301 | Aliases: 302 | 303 | Required: False 304 | Position: 9 305 | Default value: 180 306 | Accept pipeline input: False 307 | Accept wildcard characters: False 308 | ``` 309 | 310 | ### -DeletePasswordLastSetMoreThan 311 | Delete computer only if it has a PasswordLastSet that is more than the specified number of days. 312 | 313 | ```yaml 314 | Type: Int32 315 | Parameter Sets: (All) 316 | Aliases: 317 | 318 | Required: False 319 | Position: 10 320 | Default value: 180 321 | Accept pipeline input: False 322 | Accept wildcard characters: False 323 | ``` 324 | 325 | ### -DeleteListProcessedMoreThan 326 | Delete computer only if it has been processed by this script more than the specified number of days ago. 327 | This is useful if you want to delete computers that have been disabled for a certain amount of time. 328 | It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over. 329 | 330 | ```yaml 331 | Type: Int32 332 | Parameter Sets: (All) 333 | Aliases: 334 | 335 | Required: False 336 | Position: 11 337 | Default value: None 338 | Accept pipeline input: False 339 | Accept wildcard characters: False 340 | ``` 341 | 342 | ### -DeleteExcludeSystems 343 | Delete computer only if it's not on the list of excluded operating systems. 344 | If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' 345 | or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. 346 | You can also specify multiple operating systems by separating them with a comma. 347 | It's using the -like operator, so you can use wildcards. 348 | It's using OperatingSystem property of the computer object for comparison. 349 | 350 | ```yaml 351 | Type: Array 352 | Parameter Sets: (All) 353 | Aliases: 354 | 355 | Required: False 356 | Position: 12 357 | Default value: @() 358 | Accept pipeline input: False 359 | Accept wildcard characters: False 360 | ``` 361 | 362 | ### -DeleteIncludeSystems 363 | Delete computer only if it's on the list of included operating systems. 364 | If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*' 365 | or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. 366 | You can also specify multiple operating systems by separating them with a comma. 367 | It's using the -like operator, so you can use wildcards. 368 | 369 | ```yaml 370 | Type: Array 371 | Parameter Sets: (All) 372 | Aliases: 373 | 374 | Required: False 375 | Position: 13 376 | Default value: @() 377 | Accept pipeline input: False 378 | Accept wildcard characters: False 379 | ``` 380 | 381 | ### -DeleteLimit 382 | Limit the number of computers that will be deleted. 383 | 0 = unlimited. 384 | Default is 1. 385 | This is to prevent accidental deletion of all computers that meet the criteria. 386 | Adjust the limit to your needs. 387 | 388 | ```yaml 389 | Type: Int32 390 | Parameter Sets: (All) 391 | Aliases: 392 | 393 | Required: False 394 | Position: 14 395 | Default value: 1 396 | Accept pipeline input: False 397 | Accept wildcard characters: False 398 | ``` 399 | 400 | ### -DisableLimit 401 | Limit the number of computers that will be disabled. 402 | 0 = unlimited. 403 | Default is 1. 404 | This is to prevent accidental disabling of all computers that meet the criteria. 405 | Adjust the limit to your needs. 406 | 407 | ```yaml 408 | Type: Int32 409 | Parameter Sets: (All) 410 | Aliases: 411 | 412 | Required: False 413 | Position: 15 414 | Default value: 1 415 | Accept pipeline input: False 416 | Accept wildcard characters: False 417 | ``` 418 | 419 | ### -Exclusions 420 | List of computers to exclude from the process. 421 | You can specify multiple computers by separating them with a comma. 422 | It's using the -like operator, so you can use wildcards. 423 | You can use SamAccoutName (remember about ending $), DistinguishedName, 424 | or DNSHostName property of the computer object for comparison. 425 | 426 | ```yaml 427 | Type: Array 428 | Parameter Sets: (All) 429 | Aliases: 430 | 431 | Required: False 432 | Position: 16 433 | Default value: @( 434 | # default exclusions 435 | '*OU=Domain Controllers*' 436 | ) 437 | Accept pipeline input: False 438 | Accept wildcard characters: False 439 | ``` 440 | 441 | ### -DisableModifyDescription 442 | Modify the description of the computer object to include the date and time when it was disabled. 443 | By default it will not modify the description. 444 | 445 | ```yaml 446 | Type: SwitchParameter 447 | Parameter Sets: (All) 448 | Aliases: 449 | 450 | Required: False 451 | Position: Named 452 | Default value: False 453 | Accept pipeline input: False 454 | Accept wildcard characters: False 455 | ``` 456 | 457 | ### -DisableModifyAdminDescription 458 | Modify the admin description of the computer object to include the date and time when it was disabled. 459 | By default it will not modify the admin description. 460 | 461 | ```yaml 462 | Type: SwitchParameter 463 | Parameter Sets: (All) 464 | Aliases: 465 | 466 | Required: False 467 | Position: Named 468 | Default value: False 469 | Accept pipeline input: False 470 | Accept wildcard characters: False 471 | ``` 472 | 473 | ### -Filter 474 | Filter to use when searching for computers in Get-ADComputer cmdlet. 475 | Default is '*' 476 | 477 | ```yaml 478 | Type: String 479 | Parameter Sets: (All) 480 | Aliases: 481 | 482 | Required: False 483 | Position: 17 484 | Default value: * 485 | Accept pipeline input: False 486 | Accept wildcard characters: False 487 | ``` 488 | 489 | ### -DataStorePath 490 | Path to the XML file that will be used to store the list of processed computers, current run, and history data. 491 | Default is $PSScriptRoot\ProcessedComputers.xml 492 | 493 | ```yaml 494 | Type: String 495 | Parameter Sets: (All) 496 | Aliases: 497 | 498 | Required: False 499 | Position: 18 500 | Default value: None 501 | Accept pipeline input: False 502 | Accept wildcard characters: False 503 | ``` 504 | 505 | ### -ReportOnly 506 | Only generate the report, don't disable or delete computers. 507 | 508 | ```yaml 509 | Type: SwitchParameter 510 | Parameter Sets: (All) 511 | Aliases: 512 | 513 | Required: False 514 | Position: Named 515 | Default value: False 516 | Accept pipeline input: False 517 | Accept wildcard characters: False 518 | ``` 519 | 520 | ### -ReportMaximum 521 | Maximum number of reports to keep. 522 | Default is Unlimited (0). 523 | 524 | ```yaml 525 | Type: Int32 526 | Parameter Sets: (All) 527 | Aliases: 528 | 529 | Required: False 530 | Position: 19 531 | Default value: 0 532 | Accept pipeline input: False 533 | Accept wildcard characters: False 534 | ``` 535 | 536 | ### -WhatIfDelete 537 | WhatIf parameter for the Delete process. 538 | It's not nessessary to specify this parameter if you use WhatIf parameter which applies to both processes. 539 | 540 | ```yaml 541 | Type: SwitchParameter 542 | Parameter Sets: (All) 543 | Aliases: 544 | 545 | Required: False 546 | Position: Named 547 | Default value: False 548 | Accept pipeline input: False 549 | Accept wildcard characters: False 550 | ``` 551 | 552 | ### -WhatIfDisable 553 | WhatIf parameter for the Disable process. 554 | It's not nessessary to specify this parameter if you use WhatIf parameter which applies to both processes. 555 | 556 | ```yaml 557 | Type: SwitchParameter 558 | Parameter Sets: (All) 559 | Aliases: 560 | 561 | Required: False 562 | Position: Named 563 | Default value: False 564 | Accept pipeline input: False 565 | Accept wildcard characters: False 566 | ``` 567 | 568 | ### -LogPath 569 | Path to the log file. 570 | Default is no logging to file. 571 | 572 | ```yaml 573 | Type: String 574 | Parameter Sets: (All) 575 | Aliases: 576 | 577 | Required: False 578 | Position: 20 579 | Default value: None 580 | Accept pipeline input: False 581 | Accept wildcard characters: False 582 | ``` 583 | 584 | ### -LogMaximum 585 | Maximum number of log files to keep. 586 | Default is 5. 587 | 588 | ```yaml 589 | Type: Int32 590 | Parameter Sets: (All) 591 | Aliases: 592 | 593 | Required: False 594 | Position: 21 595 | Default value: 5 596 | Accept pipeline input: False 597 | Accept wildcard characters: False 598 | ``` 599 | 600 | ### -Suppress 601 | Suppress output of the object and only display to console 602 | 603 | ```yaml 604 | Type: SwitchParameter 605 | Parameter Sets: (All) 606 | Aliases: 607 | 608 | Required: False 609 | Position: Named 610 | Default value: False 611 | Accept pipeline input: False 612 | Accept wildcard characters: False 613 | ``` 614 | 615 | ### -ShowHTML 616 | Show HTML report in the browser once the function is complete 617 | 618 | ```yaml 619 | Type: SwitchParameter 620 | Parameter Sets: (All) 621 | Aliases: 622 | 623 | Required: False 624 | Position: Named 625 | Default value: False 626 | Accept pipeline input: False 627 | Accept wildcard characters: False 628 | ``` 629 | 630 | ### -Online 631 | Online parameter causes HTML report to use CDN for CSS and JS files. 632 | This can be useful to minimize the size of the HTML report. 633 | Otherwise the report will start with at least 2MB in size. 634 | 635 | ```yaml 636 | Type: SwitchParameter 637 | Parameter Sets: (All) 638 | Aliases: 639 | 640 | Required: False 641 | Position: Named 642 | Default value: False 643 | Accept pipeline input: False 644 | Accept wildcard characters: False 645 | ``` 646 | 647 | ### -ReportPath 648 | Path to the HTML report file. 649 | Default is $PSScriptRoot\ProcessedComputers.html 650 | 651 | ```yaml 652 | Type: String 653 | Parameter Sets: (All) 654 | Aliases: 655 | 656 | Required: False 657 | Position: 22 658 | Default value: None 659 | Accept pipeline input: False 660 | Accept wildcard characters: False 661 | ``` 662 | 663 | ### -WhatIf 664 | Shows what would happen if the cmdlet runs. 665 | The cmdlet is not run. 666 | 667 | ```yaml 668 | Type: SwitchParameter 669 | Parameter Sets: (All) 670 | Aliases: wi 671 | 672 | Required: False 673 | Position: Named 674 | Default value: None 675 | Accept pipeline input: False 676 | Accept wildcard characters: False 677 | ``` 678 | 679 | ### -Confirm 680 | Prompts you for confirmation before running the cmdlet. 681 | 682 | ```yaml 683 | Type: SwitchParameter 684 | Parameter Sets: (All) 685 | Aliases: cf 686 | 687 | Required: False 688 | Position: Named 689 | Default value: None 690 | Accept pipeline input: False 691 | Accept wildcard characters: False 692 | ``` 693 | 694 | ### CommonParameters 695 | This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). 696 | 697 | ## INPUTS 698 | 699 | ## OUTPUTS 700 | 701 | ## NOTES 702 | General notes 703 | 704 | ## RELATED LINKS 705 | -------------------------------------------------------------------------------- /Docs/Readme.md: -------------------------------------------------------------------------------- 1 | --- 2 | Module Name: CleanupMonster 3 | Module Guid: 9b7b0f50-950b-401d-aa7a-90e39b54a6ef 4 | Download Help Link: {{ Update Download Link }} 5 | Help Version: {{ Please enter version of help manually (X.X.X.X) format }} 6 | Locale: en-US 7 | --- 8 | 9 | # CleanupMonster Module 10 | ## Description 11 | {{ Fill in the Description }} 12 | 13 | ## CleanupMonster Cmdlets 14 | ### [Invoke-ADComputersCleanup](Invoke-ADComputersCleanup.md) 15 | {{ Fill in the Synopsis }} 16 | 17 | -------------------------------------------------------------------------------- /Examples/DeleteComputers.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | # Run the script 4 | $Configuration = @{ 5 | Disable = $true 6 | DisableNoServicePrincipalName = $null 7 | DisableIsEnabled = $true 8 | DisableLastLogonDateMoreThan = 90 9 | DisablePasswordLastSetMoreThan = 90 10 | DisableExcludeSystems = @( 11 | # 'Windows Server*' 12 | ) 13 | DisableIncludeSystems = @() 14 | DisableLimit = 1 # 0 means unlimited, ignored for reports 15 | DisableModifyDescription = $false 16 | DisableModifyAdminDescription = $true 17 | 18 | Delete = $true 19 | DeleteIsEnabled = $false 20 | DeleteNoServicePrincipalName = $null 21 | DeleteLastLogonDateMoreThan = 180 22 | DeletePasswordLastSetMoreThan = 180 23 | DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list 24 | DeleteExcludeSystems = @( 25 | # 'Windows Server*' 26 | ) 27 | DeleteIncludeSystems = @( 28 | 29 | ) 30 | DeleteLimit = 1 # 0 means unlimited, ignored for reports 31 | 32 | Exclusions = @( 33 | '*OU=Domain Controllers*' 34 | '*OU=Servers,OU=Production*' 35 | 'EVOMONSTER$' 36 | 'EVOMONSTER.AD.EVOTEC.XYZ' 37 | ) 38 | 39 | Filter = '*' 40 | WhatIfDisable = $true 41 | WhatIfDelete = $true 42 | LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 43 | DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml" 44 | ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 45 | ShowHTML = $true 46 | 47 | RemoveProtectedFromAccidentalDeletionFlag = $true 48 | } 49 | 50 | # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers' 51 | $Output = Invoke-ADComputersCleanup @Configuration 52 | $Output -------------------------------------------------------------------------------- /Examples/DeleteComputersEnableSource.ps1: -------------------------------------------------------------------------------- 1 | Import-Module PSEventViewer -Force 2 | 3 | Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers' -------------------------------------------------------------------------------- /Examples/DeleteComputersInteractive.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | # this is a fresh run and it will provide report only according to it's defaults 4 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML 5 | $Output 6 | 7 | # this is a fresh run and it will provide report only according to its defaults 8 | # the defaults are 180 days last logon and 180 days password last set 9 | # but we also now add the requirement that computer hasn't changed it's password before 2021-08-19 00:00:00 10 | # and it hasn't logged on before 2021-08-19 00:00:00 11 | $DateTime = Get-Date -Year 2021 -Month 8 -Day 19 -Hour 0 -Minute 0 -Second 0 12 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -ShowHTML -DisablePasswordLastSetOlderThan $DateTime -DisableLastLogonDateOlderThan $DateTime -DeletePasswordLastSetOlderThan $DateTime -DeleteLastLogonDateOlderThan $DateTime 13 | $Output 14 | 15 | # this is a fresh run and it will try to disable computers according to it's defaults 16 | # read documentation to understand what it does 17 | $Output = Invoke-ADComputersCleanup -Disable -ShowHTML -WhatIfDisable -WhatIfDelete -Delete 18 | $Output 19 | 20 | # this is a fresh run and it will try to delete computers according to it's defaults 21 | # read documentation to understand what it does 22 | $Output = Invoke-ADComputersCleanup -Delete -WhatIfDelete -ShowHTML -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 23 | $Output -------------------------------------------------------------------------------- /Examples/DeleteComputersInteractive02.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | $Output = Invoke-ADComputersCleanup -LogMaximum 4 -ReportMaximum 4 -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 4 | $Output 5 | 6 | $Output = Invoke-ADComputersCleanup -DeleteListProcessedMoreThan 100 -Disable -DeleteIsEnabled $false -Delete -WhatIfDelete -ShowHTML -ReportOnly -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 7 | $Output -------------------------------------------------------------------------------- /Examples/DeleteComputersWithJamfAndO365.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | Connect-MgGraph -Scopes Device.Read.All, DeviceManagementManagedDevices.Read.All, Directory.ReadWrite.All, DeviceManagementConfiguration.Read.All 4 | #Connect-Jamf -Organization 'Evotec' -UserName 'API_AD_CLEANUP' -Suppress -Force -PasswordEncrypted 'dsdsdd' 5 | 6 | # this is a fresh run and it will provide report only according to it's defaults 7 | $invokeADComputersCleanupSplat = @{ 8 | # safety limits (minimum amount of computers that has to be returned from each source) 9 | SafetyADLimit = 40 10 | SafetyAzureADLimit = 5 11 | SafetyIntuneLimit = 2 12 | #SafetyJamfLimit = 50 13 | # disable settings 14 | Disable = $true 15 | DisableLimit = 10 16 | DisableLastLogonDateMoreThan = 90 17 | DisablePasswordLastSetMoreThan = 90 18 | DisableLastSeenAzureMoreThan = 90 19 | DisableLastSyncAzureMoreThan = 90 20 | #DisableLastContactJamfMoreThan = 90 21 | DisableLastSeenIntuneMoreThan = 90 22 | # delete settings 23 | Delete = $true 24 | DeleteLimit = 10 25 | DeleteLastLogonDateMoreThan = 180 26 | DeletePasswordLastSetMoreThan = 180 27 | DeleteLastSeenAzureMoreThan = 180 28 | DeleteLastSyncAzureMoreThan = 180 29 | # DeleteLastContactJamfMoreThan = 180 30 | DeleteLastSeenIntuneMoreThan = 180 31 | DeleteListProcessedMoreThan = 90 32 | DeleteIsEnabled = $false # Computer has to be disabled to be deleted 33 | # global exclusions 34 | Exclusions = @( 35 | '*OU=Domain Controllers*' # exclude Domain Controllers 36 | ) 37 | # filter for AD search 38 | Filter = '*' 39 | # logs, reports and datastores 40 | LogPath = "$PSScriptRoot\Logs\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 41 | DataStorePath = "$PSScriptRoot\CleanupComputers_ListProcessed.xml" 42 | ReportPath = "$PSScriptRoot\Reports\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 43 | # WhatIf settings 44 | #ReportOnly = $true 45 | WhatIfDisable = $true 46 | WhatIfDelete = $true 47 | ShowHTML = $true 48 | } 49 | 50 | $Output = Invoke-ADComputersCleanup @invokeADComputersCleanupSplat 51 | $Output -------------------------------------------------------------------------------- /Examples/DeleteComputersWithMoveAndEmail.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | # connect to graph for Email sending 4 | Connect-MgGraph -Scopes Mail.Send -NoWelcome 5 | 6 | $invokeADComputersCleanupSplat = @{ 7 | #ExcludeDomains = 'ad.evotec.xyz' 8 | # safety limits (minimum amount of computers that has to be returned from each source) 9 | SafetyADLimit = 30 10 | #SafetyAzureADLimit = 5 11 | #SafetyIntuneLimit = 3 12 | #SafetyJamfLimit = 50 13 | # disable settings 14 | Disable = $true 15 | DisableAndMove = $true 16 | DisableAndMoveOrder = 'MoveAndDisable' # DisableAndMove, MoveAndDisable 17 | #DisableIsEnabled = $true 18 | DisableLimit = 1 19 | DisableLastLogonDateMoreThan = 90 20 | DisablePasswordLastSetMoreThan = 2280 21 | #DisableLastSeenAzureMoreThan = 90 22 | DisableRequireWhenCreatedMoreThan = 90 23 | #DisablePasswordLastSetOlderThan = Get-Date -Year 2023 -Month 1 -Day 1 24 | #DisableLastSyncAzureMoreThan = 90 25 | #DisableLastContactJamfMoreThan = 90 26 | #DisableLastSeenIntuneMoreThan = 90 27 | DisableMoveTargetOrganizationalUnit = @{ 28 | 'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz' 29 | 'ad.evotec.pl' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl' 30 | } 31 | SearchBase = @{ 32 | 'ad.evotec.xyz' = 'DC=ad,DC=evotec,DC=xyz' 33 | 'ad.evotec.pl' = 'DC=ad,DC=evotec,DC=pl' 34 | } 35 | # move settings 36 | Move = $false 37 | MoveLimit = 1 38 | MoveLastLogonDateMoreThan = 90 39 | MovePasswordLastSetMoreThan = 90 40 | #MoveLastSeenAzureMoreThan = 180 41 | #MoveLastSyncAzureMoreThan = 180 42 | #MoveLastContactJamfMoreThan = 180 43 | #MoveLastSeenIntuneMoreThan = 180 44 | #MoveListProcessedMoreThan = 90 # disabled computer has to spend 90 days in list before it can be deleted 45 | MoveIsEnabled = $false # Computer has to be disabled to be moved 46 | MoveTargetOrganizationalUnit = @{ 47 | 'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz' 48 | 'ad.evotec.pl' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl' 49 | } 50 | 51 | # delete settings 52 | Delete = $false 53 | DeleteLimit = 2 54 | DeleteLastLogonDateMoreThan = 180 55 | DeletePasswordLastSetMoreThan = 180 56 | #DeleteLastSeenAzureMoreThan = 180 57 | #DeleteLastSyncAzureMoreThan = 180 58 | #DeleteLastContactJamfMoreThan = 180 59 | #DeleteLastSeenIntuneMoreThan = 180 60 | #DeleteListProcessedMoreThan = 90 # disabled computer has to spend 90 days in list before it can be deleted 61 | DeleteIsEnabled = $false # Computer has to be disabled to be deleted 62 | # global exclusions 63 | Exclusions = @( 64 | '*OU=Domain Controllers*' # exclude Domain Controllers 65 | ) 66 | # filter for AD search 67 | Filter = '*' 68 | # logs, reports and datastores 69 | LogPath = "$PSScriptRoot\Logs\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 70 | DataStorePath = "$PSScriptRoot\CleanupComputers_ListProcessed.xml" 71 | ReportPath = "$PSScriptRoot\Reports\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 72 | # WhatIf settings 73 | ReportOnly = $false 74 | WhatIfDisable = $true 75 | WhatIfMove = $true 76 | WhatIfDelete = $true 77 | ShowHTML = $true 78 | 79 | DontWriteToEventLog = $true 80 | } 81 | 82 | $Output = Invoke-ADComputersCleanup @invokeADComputersCleanupSplat 83 | 84 | # Now lets send email using Graph 85 | [Array] $DisabledObjects = $Output.CurrentRun | Where-Object { $_.Action -eq 'Disable' } 86 | [Array] $DeletedObjects = $Output.CurrentRun | Where-Object { $_.Action -eq 'Delete' } 87 | 88 | $EmailBody = EmailBody -EmailBody { 89 | EmailText -Text "Hello," 90 | 91 | EmailText -LineBreak 92 | 93 | EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold 94 | 95 | EmailText -LineBreak 96 | 97 | EmailText -Text "Following is a summary for the computer object cleanup:" -FontWeight bold 98 | EmailList { 99 | EmailListItem -Text "Objects actioned: ", $Output.CurrentRun.Count -Color None, Green -FontWeight normal, bold 100 | EmailListItem -Text "Objects deleted: ", $DeletedObjects.Count -Color None, Salmon -FontWeight normal, bold 101 | EmailListItem -Text "Objects disabled: ", $DisabledObjects.Count -Color None, Orange -FontWeight normal, bold 102 | } 103 | 104 | EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon 105 | EmailTable -DataTable $Output.CurrentRun -HideFooter { 106 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace -Inline 107 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow -Inline 108 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen -Inline 109 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon -Inline 110 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue -Inline 111 | } 112 | 113 | EmailText -LineBreak 114 | 115 | EmailText -Text "Regards," 116 | EmailText -Text "Automations Team" -FontWeight bold 117 | } 118 | #Save-Html -HTML $EmailBody -ShowHTML 119 | # send email using Mailozaurr 120 | #Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated Computer Cleanup Report" -Body $EmailBody -Priority Low -Verbose -WhatIf -------------------------------------------------------------------------------- /Examples/DeleteComputersWithO365.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | Connect-MgGraph -Scopes Device.Read.All, DeviceManagementManagedDevices.Read.All, Directory.ReadWrite.All, DeviceManagementConfiguration.Read.All 4 | 5 | # this is a fresh run and it will provide report only according to it's defaults 6 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -ShowHTML -DisableLastSeenAzureMoreThan 80 -DisableLastSyncAzureMoreThan 80 -DisableLastSeenIntuneMoreThan 80 7 | $Output -------------------------------------------------------------------------------- /Examples/DeleteComputersWithO365andJAMF.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | # connect to graph for Azure AD, Intune (requires GraphEssentials module) 4 | Connect-MgGraph -Scopes Device.Read.All, DeviceManagementManagedDevices.Read.All, Directory.ReadWrite.All, DeviceManagementConfiguration.Read.All 5 | # connect to jamf (requires PowerJamf module) 6 | #Connect-Jamf -Organization 'aaa' -UserName 'aaa' -Suppress -Force -PasswordEncrypted 'aaaaa' 7 | 8 | $invokeADComputersCleanupSplat = @{ 9 | # safety limits (minimum amount of computers that has to be returned from each source) 10 | SafetyADLimit = 30 11 | SafetyAzureADLimit = 5 12 | SafetyIntuneLimit = 3 13 | SafetyJamfLimit = 50 14 | # disable settings 15 | Disable = $true 16 | DisableAndMove = $false 17 | #DisableIsEnabled = $true 18 | DisableLimit = 2 19 | DisableLastLogonDateMoreThan = 90 20 | DisablePasswordLastSetMoreThan = 90 21 | DisableLastSeenAzureMoreThan = 90 22 | 23 | DisablePasswordLastSetOlderThan = Get-Date -Year 2023 -Month 1 -Day 1 24 | #DisableLastSyncAzureMoreThan = 90 25 | #DisableLastContactJamfMoreThan = 90 26 | DisableLastSeenIntuneMoreThan = 90 27 | DisableMoveTargetOrganizationalUnit = @{ 28 | 'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz' 29 | 'ad.evotec.pl' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl' 30 | } 31 | # move settings 32 | Move = $false 33 | MoveLimit = 1 34 | MoveLastLogonDateMoreThan = 90 35 | MovePasswordLastSetMoreThan = 90 36 | #MoveLastSeenAzureMoreThan = 180 37 | #MoveLastSyncAzureMoreThan = 180 38 | #MoveLastContactJamfMoreThan = 180 39 | #MoveLastSeenIntuneMoreThan = 180 40 | #MoveListProcessedMoreThan = 90 # disabled computer has to spend 90 days in list before it can be deleted 41 | MoveIsEnabled = $false # Computer has to be disabled to be moved 42 | MoveTargetOrganizationalUnit = @{ 43 | 'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz' 44 | 'ad.evotec.pl' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl' 45 | } 46 | 47 | # delete settings 48 | Delete = $true 49 | DeleteLimit = 2 50 | DeleteLastLogonDateMoreThan = 180 51 | DeletePasswordLastSetMoreThan = 180 52 | DeleteLastSeenAzureMoreThan = 180 53 | #DeleteLastSyncAzureMoreThan = 180 54 | #DeleteLastContactJamfMoreThan = 180 55 | #DeleteLastSeenIntuneMoreThan = 180 56 | #DeleteListProcessedMoreThan = 90 # disabled computer has to spend 90 days in list before it can be deleted 57 | DeleteIsEnabled = $false # Computer has to be disabled to be deleted 58 | # global exclusions 59 | Exclusions = @( 60 | '*OU=Domain Controllers*' # exclude Domain Controllers 61 | ) 62 | # filter for AD search 63 | Filter = '*' 64 | # logs, reports and datastores 65 | LogPath = "$PSScriptRoot\Logs\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 66 | DataStorePath = "$PSScriptRoot\CleanupComputers_ListProcessed.xml" 67 | ReportPath = "$PSScriptRoot\Reports\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 68 | # WhatIf settings 69 | ReportOnly = $false 70 | WhatIfDisable = $true 71 | WhatIfMove = $true 72 | WhatIfDelete = $true 73 | ShowHTML = $true 74 | 75 | DontWriteToEventLog = $true 76 | } 77 | 78 | $Output = Invoke-ADComputersCleanup @invokeADComputersCleanupSplat 79 | $Output -------------------------------------------------------------------------------- /Examples/DeleteSIDHistory.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\CleanupMonster.psd1 -Force 2 | 3 | # Prepare splat 4 | $invokeADSIDHistoryCleanupSplat = @{ 5 | Verbose = $true 6 | WhatIf = $true 7 | IncludeSIDHistoryDomain = @( 8 | 'S-1-5-21-3661168273-3802070955-2987026695' 9 | 'S-1-5-21-853615985-2870445339-3163598659' 10 | ) 11 | #IncludeType = 'External' 12 | RemoveLimitSID = 1 13 | RemoveLimitObject = 2 14 | SafetyADLimit = 1 15 | ShowHTML = $true 16 | Online = $true 17 | DisabledOnly = $false 18 | #ReportOnly = $true 19 | LogPath = "C:\Temp\ProcessedSIDHistory.log" 20 | ReportPath = "$PSScriptRoot\ProcessedSIDHistory.html" 21 | DataStorePath = "$PSScriptRoot\ProcessedSIDHistory.xml" 22 | } 23 | 24 | # Run the script 25 | $Output = Invoke-ADSIDHistoryCleanup @invokeADSIDHistoryCleanupSplat 26 | $Output | Format-Table -AutoSize 27 | 28 | # Lets send an email 29 | $EmailBody = $Output.EmailBody 30 | 31 | #Connect-MgGraph -Scopes 'Mail.Send' -NoWelcome 32 | #Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated SID Cleanup Report" -Body $EmailBody -Priority Low -Verbose -------------------------------------------------------------------------------- /Examples/HowToChangeTaskToGMSA.ps1: -------------------------------------------------------------------------------- 1 | schtasks /Change /TN Automation-CleanupComputers /RU "gmsa-cleanup$" /RP "" -------------------------------------------------------------------------------- /Examples/HowToPreparePasswordForUse.ps1: -------------------------------------------------------------------------------- 1 | ConvertTo-SecureString -String 'PasswordToProtect' -AsPlainText -Force | ConvertFrom-SecureString | Set-Clipboard -------------------------------------------------------------------------------- /Examples/HowToReEnableComputer.ps1: -------------------------------------------------------------------------------- 1 | $Computers = @( 2 | 'de-muc-01' 3 | ) 4 | $Test = $Computers | Sort-Object -Unique 5 | 6 | foreach ($Computer in $Test) { 7 | Set-ADComputer -Identity $Computer -Server 'domain.loc' -Enabled $true 8 | } -------------------------------------------------------------------------------- /Examples/HowToRemoveProtectedFromDeletionAll.ps1: -------------------------------------------------------------------------------- 1 | $Domain = 'ad.evotec.xyz' 2 | $ADComputers = Get-ADComputer -Filter * -Properties ProtectedFromAccidentalDeletion -Server $Domain 3 | foreach ($ADComputer in $ADComputers) { 4 | if ($ADComputer.ProtectedFromAccidentalDeletion) { 5 | Write-Host "Removing ProtectedFromAccidentalDeletion from $($ADComputer.Name)" 6 | Set-ADObject -Server $Domain -Identity $ADComputer.DistinguishedName -ProtectedFromAccidentalDeletion $false -Verbose -WhatIf 7 | } 8 | } -------------------------------------------------------------------------------- /Examples/HowToRemoveProtectedFromDeletionFew.ps1: -------------------------------------------------------------------------------- 1 | $Computers = @( 2 | 'ComputerToRemoveProtected$' 3 | ) 4 | $Server = 'domain.com' 5 | foreach ($Computer in $Computers) { 6 | $ADComputer = Get-ADComputer -Identity $Computer -Server $Server 7 | Set-ADObject -Server $Server -Identity $ADComputer.DistinguishedName -ProtectedFromAccidentalDeletion $false -Verbose -WhatIf 8 | } -------------------------------------------------------------------------------- /Examples/Images/CleanupDevicesAllRemaining.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/CleanupDevicesAllRemaining.png -------------------------------------------------------------------------------- /Examples/Images/CleanupDevicesCurrentRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/CleanupDevicesCurrentRun.png -------------------------------------------------------------------------------- /Examples/Images/CleanupDevicesHistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/CleanupDevicesHistory.png -------------------------------------------------------------------------------- /Examples/Images/CleanupDevicesPending.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/CleanupDevicesPending.png -------------------------------------------------------------------------------- /Examples/Images/CleanupDevicesReport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/CleanupDevicesReport.png -------------------------------------------------------------------------------- /Examples/Images/SIDHistoryEmail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/SIDHistoryEmail.png -------------------------------------------------------------------------------- /Examples/Images/SIDHistoryReportAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/SIDHistoryReportAll.png -------------------------------------------------------------------------------- /Examples/Images/SIDHistoryReportCurrentRun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/SIDHistoryReportCurrentRun.png -------------------------------------------------------------------------------- /Examples/Images/SIDHistoryReportHistory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/SIDHistoryReportHistory.png -------------------------------------------------------------------------------- /Examples/Images/SIDHistoryReportLogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EvotecIT/CleanupMonster/d1328202e00ef2ee492ee60e12a83f72bfcbdc13/Examples/Images/SIDHistoryReportLogs.png -------------------------------------------------------------------------------- /Private/Assert-InitialSettings.ps1: -------------------------------------------------------------------------------- 1 | function Assert-InitialSettings { 2 | [cmdletbinding()] 3 | param( 4 | [System.Collections.IDictionary] $DisableOnlyIf, 5 | [System.Collections.IDictionary] $MoveOnlyIf, 6 | [System.Collections.IDictionary] $DeleteOnlyIf 7 | ) 8 | 9 | $AzureRequired = $false 10 | $IntuneRequired = $false 11 | $JamfRequired = $false 12 | 13 | if ($DisableOnlyIf) { 14 | if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) { 15 | $AzureRequired = $true 16 | } 17 | if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) { 18 | $JamfRequired = $true 19 | } 20 | if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) { 21 | $IntuneRequired = $true 22 | } 23 | } 24 | if ($MoveOnlyIf) { 25 | if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) { 26 | $AzureRequired = $true 27 | } 28 | if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) { 29 | $JamfRequired = $true 30 | } 31 | if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) { 32 | $IntuneRequired = $true 33 | } 34 | } 35 | if ($DeleteOnlyIf) { 36 | if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) { 37 | $AzureRequired = $true 38 | } 39 | if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) { 40 | $JamfRequired = $true 41 | } 42 | if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) { 43 | $IntuneRequired = $true 44 | } 45 | } 46 | 47 | if ($AzureRequired -or $IntuneRequired) { 48 | $ModuleAvailable = Get-Module -Name GraphEssentials -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 49 | if (-not $ModuleAvailable) { 50 | Write-Color -Text "[e] ", "'GraphEssentials' module is required but not available. Terminating." -Color Yellow, Red 51 | return $false 52 | } 53 | $ModuleVersion = [version]'0.0.46' 54 | if ($ModuleAvailable.Version -lt $ModuleVersion) { 55 | Write-Color -Text "[e] ", "'GraphEssentials' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red 56 | return $false 57 | } 58 | } 59 | if ($JamfRequired) { 60 | $ModuleAvailable = Get-Module -Name PowerJamf -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 61 | if (-not $ModuleAvailable) { 62 | Write-Color -Text "[e] ", "'PowerJamf' module is required but not available. Terminating." -Color Yellow, Red 63 | return $false 64 | } 65 | $ModuleVersion = [version]'0.3.1' 66 | if ($ModuleAvailable.Version -lt $ModuleVersion) { 67 | Write-Color -Text "[e] ", "'PowerJamf' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red 68 | return $false 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Private/Convert-ListProcessed.ps1: -------------------------------------------------------------------------------- 1 | function Convert-ListProcessed { 2 | <# 3 | .SYNOPSIS 4 | This function converts old format of writting down pending list with DN to new format with SamAccountName with domain. 5 | 6 | .DESCRIPTION 7 | This function converts old format of writting down pending list with DN to new format with SamAccountName with domain. 8 | The old way would probably break if someone would move computer after disabling which would cause the computer to be removed from the list. 9 | 10 | .PARAMETER FileImport 11 | Hashtable with PendingDeletion and History keys. 12 | 13 | .EXAMPLE 14 | $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop 15 | # convert old format to new format 16 | $FileImport = Convert-ListProcessed -FileImport $FileImport 17 | 18 | .NOTES 19 | General notes 20 | #> 21 | [CmdletBinding()] 22 | param( 23 | [System.Collections.IDictionary] $FileImport 24 | ) 25 | if (-not $FileImport.Contains('PendingDeletion') -and $FileImport.PendingDeletion.Keys.Count -eq 0) { 26 | return $FileImport 27 | } 28 | if ($FileImport.PendingDeletion.Keys[0] -like "*@*") { 29 | # Write-Color -Text "[i] ", "List is already converted. Terminating." -Color Yellow, Green 30 | return $FileImport 31 | } 32 | Write-Color -Text "[i] ", "Converting list to new format." -Color Yellow, Green 33 | foreach ($Key in [string[]] $FileImport.PendingDeletion.Keys) { 34 | $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $FileImport.PendingDeletion[$Key].DistinguishedName -ToDomainCN 35 | $NewKey = -join ($FileImport.PendingDeletion[$Key].SamAccountName, "@", $DomainName) 36 | $FileImport.PendingDeletion[$NewKey] = $FileImport.PendingDeletion[$Key] 37 | $null = $FileImport.PendingDeletion.Remove($Key) 38 | } 39 | Write-Color -Text "[i] ", "List converted." -Color Yellow, Green 40 | $FileImport 41 | } -------------------------------------------------------------------------------- /Private/ConvertTo-PreparedComputer.ps1: -------------------------------------------------------------------------------- 1 | function ConvertTo-PreparedComputer { 2 | [CmdletBinding()] 3 | param( 4 | [Microsoft.ActiveDirectory.Management.ADComputer[]] $Computers, 5 | [System.Collections.IDictionary] $AzureInformationCache, 6 | [System.Collections.IDictionary] $JamfInformationCache, 7 | [switch] $IncludeAzureAD, 8 | [switch] $IncludeIntune, 9 | [switch] $IncludeJamf 10 | ) 11 | 12 | foreach ($Computer in $Computers) { 13 | if ($IncludeAzureAD) { 14 | $AzureADComputer = $AzureInformationCache['AzureAD']["$($Computer.Name)"] 15 | $DataAzureAD = [ordered] @{ 16 | 'AzureLastSeen' = $AzureADComputer.LastSeen 17 | 'AzureLastSeenDays' = $AzureADComputer.LastSeenDays 18 | 'AzureLastSync' = $AzureADComputer.LastSynchronized 19 | 'AzureLastSyncDays' = $AzureADComputer.LastSynchronizedDays 20 | 'AzureOwner' = $AzureADComputer.OwnerDisplayName 21 | 'AzureOwnerStatus' = $AzureADComputer.OwnerEnabled 22 | 'AzureOwnerUPN' = $AzureADComputer.OwnerUserPrincipalName 23 | } 24 | } 25 | if ($IncludeIntune) { 26 | # data was requested from Intune 27 | $IntuneComputer = $AzureInformationCache['Intune']["$($Computer.Name)"] 28 | $DataIntune = [ordered] @{ 29 | 'IntuneLastSeen' = $IntuneComputer.LastSeen 30 | 'IntuneLastSeenDays' = $IntuneComputer.LastSeenDays 31 | 'IntuneUser' = $IntuneComputer.UserDisplayName 32 | 'IntuneUserUPN' = $IntuneComputer.UserPrincipalName 33 | 'IntuneUserEmail' = $IntuneComputer.EmailAddress 34 | } 35 | } 36 | if ($IncludeJamf) { 37 | $JamfComputer = $JamfInformationCache["$($Computer.Name)"] 38 | $DataJamf = [ordered] @{ 39 | JamfLastContactTime = $JamfComputer.lastContactTime 40 | JamfLastContactTimeDays = $JamfComputer.lastContactTimeDays 41 | JamfCapableUsers = $JamfComputer.mdmCapableCapableUsers 42 | } 43 | } 44 | $LastLogonDays = if ($null -ne $Computer.LastLogonDate) { 45 | - $($Computer.LastLogonDate - $Today).Days 46 | } else { 47 | $null 48 | } 49 | $PasswordLastChangedDays = if ($null -ne $Computer.PasswordLastSet) { 50 | - $($Computer.PasswordLastSet - $Today).Days 51 | } else { 52 | $null 53 | } 54 | 55 | 56 | $DataStart = [ordered] @{ 57 | 'DNSHostName' = $Computer.DNSHostName 58 | 'SamAccountName' = $Computer.SamAccountName 59 | 'DomainName' = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN 60 | 'Enabled' = $Computer.Enabled 61 | 'Action' = 'Not required' 62 | 'ActionStatus' = $null 63 | 'ActionDate' = $null 64 | 'ActionComment' = $null 65 | 'OperatingSystem' = $Computer.OperatingSystem 66 | 'OperatingSystemVersion' = $Computer.OperatingSystemVersion 67 | 'OperatingSystemLong' = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion 68 | 'LastLogonDate' = $Computer.LastLogonDate 69 | 'LastLogonDays' = $LastLogonDays 70 | 'PasswordLastSet' = $Computer.PasswordLastSet 71 | 'PasswordLastChangedDays' = $PasswordLastChangedDays 72 | 'ProtectedFromAccidentalDeletion' = $Computer.ProtectedFromAccidentalDeletion 73 | } 74 | $DataEnd = [ordered] @{ 75 | 'PasswordExpired' = $Computer.PasswordExpired 76 | 'LogonCount' = $Computer.logonCount 77 | 'ManagedBy' = $Computer.ManagedBy 78 | 'DistinguishedName' = $Computer.DistinguishedName 79 | 'OrganizationalUnit' = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToOrganizationalUnit 80 | 'Description' = $Computer.Description 81 | 'WhenCreated' = $Computer.WhenCreated 82 | 'WhenChanged' = $Computer.WhenChanged 83 | 'ServicePrincipalName' = $Computer.servicePrincipalName #-join [System.Environment]::NewLine 84 | 'DistinguishedNameAfterMove' = $null 85 | 'TimeOnPendingList' = $null 86 | 'TimeToLeavePendingList' = $null 87 | } 88 | if ($IncludeAzureAD -and $IncludeIntune -and $IncludeJamf) { 89 | $Data = $DataStart + $DataAzureAD + $DataIntune + $DataJamf + $DataEnd 90 | } elseif ($IncludeAzureAD -and $IncludeIntune) { 91 | $Data = $DataStart + $DataAzureAD + $DataIntune + $DataEnd 92 | } elseif ($IncludeAzureAD -and $IncludeJamf) { 93 | $Data = $DataStart + $DataAzureAD + $DataJamf + $DataEnd 94 | } elseif ($IncludeIntune -and $IncludeJamf) { 95 | $Data = $DataStart + $DataIntune + $DataJamf + $DataEnd 96 | } elseif ($IncludeAzureAD) { 97 | $Data = $DataStart + $DataAzureAD + $DataEnd 98 | } elseif ($IncludeIntune) { 99 | $Data = $DataStart + $DataIntune + $DataEnd 100 | } elseif ($IncludeJamf) { 101 | $Data = $DataStart + $DataJamf + $DataEnd 102 | } else { 103 | $Data = $DataStart + $DataEnd 104 | } 105 | [PSCustomObject] $Data 106 | } 107 | } -------------------------------------------------------------------------------- /Private/Disable-WinADComputer.ps1: -------------------------------------------------------------------------------- 1 | function Disable-WinADComputer { 2 | [CmdletBinding(SupportsShouldProcess)] 3 | param( 4 | [bool] $Success, 5 | [switch] $WhatIfDisable, 6 | [switch] $DontWriteToEventLog, 7 | [PSCustomObject] $Computer, 8 | [string] $Server 9 | ) 10 | if ($Success) { 11 | if ($Computer.Enabled -eq $true) { 12 | Write-Color -Text "[i] Disabling computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 13 | try { 14 | if ($Computer.DistinguishedNameAfterMove) { 15 | $DN = $Computer.DistinguishedNameAfterMove 16 | } else { 17 | $DN = $Computer.DistinguishedName 18 | } 19 | 20 | Disable-ADAccount -Identity $DN -Server $Server -WhatIf:$WhatIfDisable -ErrorAction Stop 21 | Write-Color -Text "[+] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) successful." -Color Yellow, Green, Yellow 22 | if (-not $DontWriteToEventLog) { 23 | Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 1000 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) successful." -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings 24 | } 25 | foreach ($W in $Warnings) { 26 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 27 | } 28 | $Success = $true 29 | } catch { 30 | $Computer.ActionComment = $_.Exception.Message 31 | $Success = $false 32 | Write-Color -Text "[-] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 33 | if (-not $DontWriteToEventLog) { 34 | Write-Event -ID 10 -LogName 'Application' -EntryType Error -Category 1001 -Source 'CleanupComputers' -Message "Disabling computer $($Computer.SamAccountName) failed. Error: $($_.Exception.Message)" -AdditionalFields @('Disable', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 35 | } 36 | foreach ($W in $Warnings) { 37 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 38 | } 39 | } 40 | } else { 41 | Write-Color -Text "[i] Computer ", $Computer.SamAccountName, " is already disabled." -Color Yellow, Green, Yellow 42 | } 43 | } 44 | $Success 45 | } -------------------------------------------------------------------------------- /Private/Get-ADComputersToProcess.ps1: -------------------------------------------------------------------------------- 1 | function Get-ADComputersToProcess { 2 | [CmdletBinding()] 3 | param( 4 | [parameter(Mandatory)][ValidateSet('Disable', 'Move', 'Delete')][string] $Type, 5 | [Array] $Computers, 6 | [alias('DeleteOnlyIf', 'DisableOnlyIf', 'MoveOnlyIf')][System.Collections.IDictionary] $ActionIf, 7 | [Array] $Exclusions, 8 | [System.Collections.IDictionary] $DomainInformation, 9 | [System.Collections.IDictionary] $ProcessedComputers, 10 | [System.Collections.IDictionary] $AzureInformationCache, 11 | [System.Collections.IDictionary] $JamfInformationCache, 12 | [switch] $IncludeAzureAD, 13 | [switch] $IncludeIntune, 14 | [switch] $IncludeJamf 15 | ) 16 | Write-Color -Text "[i] ", "Applying following rules to $Type action: " -Color Yellow, Cyan, Green 17 | foreach ($Key in $ActionIf.Keys) { 18 | if ($null -eq $ActionIf[$Key] -or $ActionIf[$Key].Count -eq 0) { 19 | Write-Color -Text " [>] ", $($Key), " is ", 'Not Set' -Color Yellow, Cyan, Yellow 20 | } else { 21 | if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') { 22 | Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never logged on" -Color Yellow, Cyan, Green 23 | } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') { 24 | Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never changed" -Color Yellow, Cyan, Green 25 | } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') { 26 | Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never synced/seen" -Color Yellow, Cyan, Green 27 | } else { 28 | Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]) -Color Yellow, Cyan, Green 29 | } 30 | } 31 | } 32 | $Count = 0 33 | $Today = Get-Date 34 | 35 | # Let's cache the destination move, if we have them 36 | $CachedDestinationMove = @{} 37 | if ($ActionIf.MoveTargetOrganizationalUnit -is [string]) { 38 | $Domain = ConvertFrom-DistinguishedName -DistinguishedName $ActionIf.MoveTargetOrganizationalUnit -ToDomainCN 39 | $CachedDestinationMove[$ActionIf.MoveTargetOrganizationalUnit] = $Domain 40 | } elseif ($ActionIf.MoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) { 41 | foreach ($Domain in $ActionIf.MoveTargetOrganizationalUnit.Keys) { 42 | $OU = $ActionIf.MoveTargetOrganizationalUnit[$Domain] 43 | $CachedDestinationMove[$OU] = $Domain 44 | } 45 | } 46 | 47 | Write-Color -Text "[i] ", "Looking for computers to $Type" -Color Yellow, Cyan, Green 48 | :SkipComputer foreach ($Computer in $Computers) { 49 | if ($Type -eq 'Delete') { 50 | # actions to happen only if we are deleting computers 51 | if ($null -ne $ActionIf.ListProcessedMoreThan) { 52 | # if more then 0 this means computer has to be on list of disabled computers for that number of days. 53 | if ($ProcessedComputers.Count -gt 0) { 54 | $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)" 55 | $FoundComputer = $ProcessedComputers[$FullComputerName] 56 | if ($FoundComputer) { 57 | if ($FoundComputer.ActionDate -is [DateTime]) { 58 | $TimeSpan = New-TimeSpan -Start $FoundComputer.ActionDate -End $Today 59 | # Lets calculate how many days it's been on the list 60 | $ProcessedComputers[$FullComputerName].TimeToLeavePendingList = $ActionIf.ListProcessedMoreThan - $TimeSpan.Days 61 | if ($TimeSpan.Days -gt $ActionIf.ListProcessedMoreThan) { 62 | 63 | } else { 64 | continue SkipComputer 65 | } 66 | } else { 67 | continue SkipComputer 68 | } 69 | } else { 70 | continue SkipComputer 71 | } 72 | } else { 73 | # ListProcessed doesn't have members, and it's part of requirement 74 | break 75 | } 76 | } 77 | } 78 | if ($Type -eq 'Disable') { 79 | # actions to happen only if we are disabling computers 80 | if ($ProcessedComputers.Count -gt 0) { 81 | $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)" 82 | $FoundComputer = $ProcessedComputers[$FullComputerName] 83 | if ($FoundComputer) { 84 | if ($Computer.Enabled -eq $true) { 85 | # We checked and it seems the computer has been enabled since it was added to list, we remove it from the list and reprocess 86 | Write-Color -Text "[*] Removing computer from pending list (computer is enabled) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow 87 | $ProcessedComputers.Remove($FullComputerName) 88 | } elseif ($ActionIf.DisableAndMove -and $Computer.Enabled -eq $false) { 89 | if ($ActionIf.MoveTargetOrganizationalUnit) { 90 | if (-not $CachedDestinationMove[$Computer.OrganizationalUnit]) { 91 | # We checked and it seems the computer has been moved since it was added to list, we remove it from the list and reprocess 92 | Write-Color -Text "[*] Removing computer from pending list (computer is moved out of pending deletion OU) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow 93 | $ProcessedComputers.Remove($FullComputerName) 94 | } else { 95 | # We checked and it seems the computer is in place where it's supposed to, we skip to next computer 96 | continue SkipComputer 97 | } 98 | } else { 99 | # We checked and it seems the computer is in place where it's supposed to, we skip to next computer 100 | continue SkipComputer 101 | } 102 | } else { 103 | # we skip adding to disabled because it's already on the list for removing 104 | continue SkipComputer 105 | } 106 | } 107 | } 108 | } 109 | 110 | # rest of actions are same for all types 111 | foreach ($PartialExclusion in $Exclusions) { 112 | if ($Computer.DistinguishedName -like "$PartialExclusion") { 113 | $Computer.'Action' = 'ExcludedByFilter' 114 | continue SkipComputer 115 | } 116 | if ($Computer.SamAccountName -like "$PartialExclusion") { 117 | $Computer.'Action' = 'ExcludedByFilter' 118 | continue SkipComputer 119 | } 120 | if ($Computer.DNSHostName -like "$PartialExclusion") { 121 | $Computer.'Action' = 'ExcludedByFilter' 122 | continue SkipComputer 123 | } 124 | } 125 | if ($ActionIf.IncludeSystems.Count -gt 0) { 126 | $FoundInclude = $false 127 | foreach ($Include in $ActionIf.IncludeSystems) { 128 | if ($Computer.OperatingSystem -like $Include) { 129 | $FoundInclude = $true 130 | break 131 | } 132 | } 133 | # If not found in includes we need to skip the computer 134 | if (-not $FoundInclude) { 135 | $Computer.'Action' = 'ExcludedBySetting' 136 | continue SkipComputer 137 | } 138 | } 139 | if ($ActionIf.ExcludeServicePrincipalName.Count -gt 0) { 140 | foreach ($ExcludeSPN in $ActionIf.ExcludeServicePrincipalName) { 141 | if ($Computer.servicePrincipalName -like "$ExcludeSPN") { 142 | $Computer.'Action' = 'ExcludedBySetting' 143 | continue SkipComputer 144 | } 145 | } 146 | } 147 | if ($ActionIf.IncludeServicePrincipalName.Count -gt 0) { 148 | $FoundInclude = $false 149 | foreach ($IncludeSPN in $ActionIf.IncludeServicePrincipalName) { 150 | if ($Computer.servicePrincipalName -like "$IncludeSPN") { 151 | $FoundInclude = $true 152 | break 153 | } 154 | } 155 | # If not found in includes we need to skip the computer 156 | if (-not $FoundInclude) { 157 | $Computer.'Action' = 'ExcludedBySetting' 158 | continue SkipComputer 159 | } 160 | } 161 | if ($ActionIf.ExcludeSystems.Count -gt 0) { 162 | foreach ($Exclude in $ActionIf.ExcludeSystems) { 163 | if ($Computer.OperatingSystem -like $Exclude) { 164 | $Computer.'Action' = 'ExcludedBySetting' 165 | continue SkipComputer 166 | } 167 | } 168 | } 169 | if ($ActionIf.NoServicePrincipalName -eq $true) { 170 | # action computer only if it has no service principal names defined 171 | if ($Computer.servicePrincipalName.Count -gt 0) { 172 | $Computer.'Action' = 'ExcludedBySetting' 173 | continue SkipComputer 174 | } 175 | } elseif ($ActionIf.NoServicePrincipalName -eq $false) { 176 | # action computer only if it has service principal names defined 177 | if ($Computer.servicePrincipalName.Count -eq 0) { 178 | $Computer.'Action' = 'ExcludedBySetting' 179 | continue SkipComputer 180 | } 181 | } 182 | if ($ActionIf.RequireWhenCreatedMoreThan) { 183 | # This runs only if more than 0 184 | if ($Computer.WhenCreated) { 185 | # We ignore empty 186 | 187 | $TimeToCompare = ($Computer.WhenCreated).AddDays($ActionIf.RequireWhenCreatedMoreThan) 188 | if ($TimeToCompare -gt $Today) { 189 | continue SkipComputer 190 | } 191 | } 192 | } 193 | if ($ActionIf.IsEnabled -eq $true) { 194 | # action computer only if it's Enabled 195 | if ($Computer.Enabled -eq $false) { 196 | continue SkipComputer 197 | } 198 | } elseif ($ActionIf.IsEnabled -eq $false) { 199 | # action computer only if it's Disabled 200 | if ($Computer.Enabled -eq $true) { 201 | continue SkipComputer 202 | } 203 | } 204 | 205 | if ($ActionIf.LastLogonDateMoreThan) { 206 | # This runs only if more than 0 207 | if ($Computer.LastLogonDate) { 208 | # We ignore empty 209 | 210 | $TimeToCompare = ($Computer.LastLogonDate).AddDays($ActionIf.LastLogonDateMoreThan) 211 | if ($TimeToCompare -gt $Today) { 212 | continue SkipComputer 213 | } 214 | } 215 | } 216 | if ($ActionIf.PasswordLastSetMoreThan) { 217 | # This runs only if more than 0 218 | if ($Computer.PasswordLastSet) { 219 | # We ignore empty 220 | 221 | $TimeToCompare = ($Computer.PasswordLastSet).AddDays($ActionIf.PasswordLastSetMoreThan) 222 | if ($TimeToCompare -gt $Today) { 223 | continue SkipComputer 224 | } 225 | } 226 | } 227 | 228 | if ($ActionIf.PasswordLastSetOlderThan) { 229 | # This runs only if not null 230 | if ($Computer.PasswordLastSet) { 231 | # We ignore empty 232 | 233 | if ($ActionIf.PasswordLastSetOlderThan -le $Computer.PasswordLastSet) { 234 | continue SkipComputer 235 | } 236 | } 237 | } 238 | if ($ActionIf.LastLogonDateOlderThan) { 239 | # This runs only if not null 240 | if ($Computer.LastLogonDate) { 241 | # We ignore empty 242 | 243 | if ($ActionIf.LastLogonDateOlderThan -le $Computer.LastLogonDate) { 244 | continue SkipComputer 245 | } 246 | } 247 | } 248 | if ($IncludeAzureAD) { 249 | if ($null -ne $ActionIf.LastSeenAzureMoreThan -and $null -ne $Computer.AzureLastSeenDays) { 250 | if ($Computer.AzureLastSeenDays -le $ActionIf.LastSeenAzureMoreThan) { 251 | continue SkipComputer 252 | } 253 | 254 | } 255 | if ($null -ne $ActionIf.LastSyncAzureMoreThan -and $null -ne $Computer.AzureLastSyncDays) { 256 | if ($Computer.AzureLastSyncDays -le $ActionIf.LastSyncAzureMoreThan) { 257 | continue SkipComputer 258 | } 259 | } 260 | } 261 | if ($IncludeIntune) { 262 | if ($null -ne $ActionIf.LastSeenIntuneMoreThan -and $null -ne $Computer.IntuneLastSeenDays) { 263 | if ($Computer.IntuneLastSeenDays -le $ActionIf.LastSeenIntuneMoreThan) { 264 | continue SkipComputer 265 | } 266 | } 267 | } 268 | if ($IncludeJamf) { 269 | if ($null -ne $ActionIf.LastContactJamfMoreThan -and $null -ne $Computer.JamfLastContactTimeDays) { 270 | if ($Computer.JamfLastContactTimeDays -le $ActionIf.LastContactJamfMoreThan) { 271 | continue SkipComputer 272 | } 273 | } 274 | } 275 | $Computer.'Action' = $Type 276 | $Count++ 277 | } 278 | $Count 279 | } -------------------------------------------------------------------------------- /Private/Get-InitialADComputers.ps1: -------------------------------------------------------------------------------- 1 | function Get-InitialADComputers { 2 | [CmdletBinding()] 3 | param( 4 | [System.Collections.IDictionary] $Report, 5 | [System.Collections.IDictionary] $ForestInformation, 6 | [object] $Filter, 7 | [object] $SearchBase, 8 | [string[]] $Properties, 9 | [bool] $Disable, 10 | [bool] $Delete, 11 | [bool] $Move, 12 | [System.Collections.IDictionary] $DisableOnlyIf, 13 | [System.Collections.IDictionary] $DeleteOnlyIf, 14 | [System.Collections.IDictionary] $MoveOnlyIf, 15 | [Array] $Exclusions, 16 | [System.Collections.IDictionary] $ProcessedComputers, 17 | [nullable[int]] $SafetyADLimit, 18 | [System.Collections.IDictionary] $AzureInformationCache, 19 | [System.Collections.IDictionary] $JamfInformationCache, 20 | [object] $TargetServers 21 | ) 22 | $AllComputers = [ordered] @{} 23 | 24 | $AzureRequired = $false 25 | $IntuneRequired = $false 26 | $JamfRequired = $false 27 | 28 | if ($DisableOnlyIf) { 29 | if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) { 30 | $AzureRequired = $true 31 | } 32 | if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) { 33 | $JamfRequired = $true 34 | } 35 | if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) { 36 | $IntuneRequired = $true 37 | } 38 | } 39 | if ($MoveOnlyIf) { 40 | if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) { 41 | $AzureRequired = $true 42 | } 43 | if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) { 44 | $JamfRequired = $true 45 | } 46 | if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) { 47 | $IntuneRequired = $true 48 | } 49 | } 50 | if ($DeleteOnlyIf) { 51 | if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) { 52 | $AzureRequired = $true 53 | } 54 | if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) { 55 | $JamfRequired = $true 56 | } 57 | if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) { 58 | $IntuneRequired = $true 59 | } 60 | } 61 | 62 | if ($TargetServers) { 63 | # User provided target servers/server. If there is only one we assume user wants to use it for all domains (hopefully just one domain) 64 | # If there are multiple we assume user wants to use different servers for different domains using hashtable/dictionary 65 | # If there is no server for a domain we will use the default server, as detected 66 | if ($TargetServers -is [string]) { 67 | $TargetServer = $TargetServers 68 | } 69 | if ($TargetServers -is [System.Collections.IDictionary]) { 70 | $TargetServerDictionary = $TargetServers[$Domain] 71 | } 72 | } 73 | 74 | $CountDomains = 0 75 | foreach ($Domain in $ForestInformation.Domains) { 76 | $CountDomains++ 77 | $Report["$Domain"] = [ordered] @{ } 78 | $Server = $ForestInformation['QueryServers'][$Domain].HostName[0] 79 | if (-not $Server) { 80 | Write-Color "[e] ", "No server found for domain $Domain" -Color Yellow, Red 81 | continue 82 | } 83 | if ($TargetServer) { 84 | Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServer -Color Yellow, Magenta 85 | $Server = $TargetServer 86 | } elseif ($TargetServerDictionary) { 87 | if ($TargetServerDictionary[$Domain]) { 88 | Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServerDictionary[$Domain] -Color Yellow, Magenta 89 | $Server = $TargetServerDictionary[$Domain] 90 | } 91 | } 92 | $DomainInformation = $ForestInformation.DomainsExtended[$Domain] 93 | $Report["$Domain"]['Server'] = $Server 94 | Write-Color "[i] Getting all computers for domain ", $Domain, " [", $CountDomains, "/", $ForestInformation.Domains.Count, "]", " from ", $Server -Color Yellow, Magenta, Yellow, Magenta, Yellow, Magenta, Yellow, Yellow, Magenta 95 | 96 | if ($Filter) { 97 | if ($Filter -is [string]) { 98 | $FilterToUse = $Filter 99 | } elseif ($Filter -is [System.Collections.IDictionary]) { 100 | $FilterToUse = $Filter[$Domain] 101 | } else { 102 | Write-Color "[e] ", "Filter must be a string or a hashtable/ordereddictionary" -Color Yellow, Red 103 | return $false 104 | } 105 | } else { 106 | $FilterToUse = "*" 107 | } 108 | 109 | if ($SearchBase) { 110 | if ($SearchBase -is [string]) { 111 | $SearchBaseToUse = $SearchBase 112 | } elseif ($SearchBase -is [System.Collections.IDictionary]) { 113 | $SearchBaseToUse = $SearchBase[$Domain] 114 | } else { 115 | Write-Color "[e] ", "SearchBase must be a string or a hashtable/ordereddictionary" -Color Yellow, Red 116 | return $false 117 | } 118 | } else { 119 | $SearchBaseToUse = $DomainInformation.DistinguishedName 120 | } 121 | 122 | $getADComputerSplat = @{ 123 | Filter = $FilterToUse 124 | Server = $Server 125 | Properties = $Properties 126 | ErrorAction = 'Stop' 127 | } 128 | if ($SearchBaseToUse) { 129 | $getADComputerSplat.SearchBase = $SearchBaseToUse 130 | } 131 | try { 132 | [Array] $Computers = Get-ADComputer @getADComputerSplat 133 | } catch { 134 | if ($_.Exception.Message -like "*distinguishedName must belong to one of the following partition*") { 135 | Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red 136 | Write-Color "[e] ", "Please check if the distinguishedName for SearchBase is correct for the domain. If you have multiple domains please use Hashtable/Dictionary to provide relevant data or using IncludeDomains/ExcludeDomains functionality" -Color Yellow, Red 137 | } else { 138 | Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red 139 | } 140 | return $false 141 | } 142 | foreach ($Computer in $Computers) { 143 | # we will be using it later to just check if computer exists in AD 144 | $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN 145 | $ComputerFullName = -join ($Computer.SamAccountName, "@", $DomainName) 146 | # initially we used DN, but DN changes when moving so it wouldn't work 147 | $AllComputers[$ComputerFullName] = $Computer 148 | } 149 | $Report["$Domain"]['Computers'] = @( 150 | $convertToPreparedComputerSplat = @{ 151 | Computers = $Computers 152 | AzureInformationCache = $AzureInformationCache 153 | JamfInformationCache = $JamfInformationCache 154 | IncludeAzureAD = $AzureRequired 155 | IncludeJamf = $JamfRequired 156 | IncludeIntune = $IntuneRequired 157 | } 158 | 159 | ConvertTo-PreparedComputer @convertToPreparedComputerSplat 160 | ) 161 | Write-Color "[i] ", "Computers found for domain $Domain`: ", $($Computers.Count) -Color Yellow, Cyan, Green 162 | if ($Disable) { 163 | Write-Color "[i] ", "Processing computers to disable for domain $Domain" -Color Yellow, Cyan, Green 164 | $getADComputersToDisableSplat = @{ 165 | Computers = $Report["$Domain"]['Computers'] 166 | DisableOnlyIf = $DisableOnlyIf 167 | Exclusions = $Exclusions 168 | DomainInformation = $DomainInformation 169 | ProcessedComputers = $ProcessedComputers 170 | AzureInformationCache = $AzureInformationCache 171 | JamfInformationCache = $JamfInformationCache 172 | IncludeAzureAD = $AzureRequired 173 | IncludeJamf = $JamfRequired 174 | IncludeIntune = $IntuneRequired 175 | Type = 'Disable' 176 | } 177 | $Report["$Domain"]['ComputersToBeDisabled'] = Get-ADComputersToProcess @getADComputersToDisableSplat 178 | #Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDisabled'].Count) -Color Yellow, Cyan, Green 179 | } 180 | if ($Move) { 181 | Write-Color "[i] ", "Processing computers to move for domain $Domain" -Color Yellow, Cyan, Green 182 | $getADComputersToDeleteSplat = @{ 183 | Computers = $Report["$Domain"]['Computers'] 184 | MoveOnlyIf = $MoveOnlyIf 185 | Exclusions = $Exclusions 186 | DomainInformation = $DomainInformation 187 | ProcessedComputers = $ProcessedComputers 188 | AzureInformationCache = $AzureInformationCache 189 | JamfInformationCache = $JamfInformationCache 190 | IncludeAzureAD = $AzureRequired 191 | IncludeJamf = $JamfRequired 192 | IncludeIntune = $IntuneRequired 193 | Type = 'Move' 194 | } 195 | $Report["$Domain"]['ComputersToBeMoved'] = Get-ADComputersToProcess @getADComputersToDeleteSplat 196 | #Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeMoved'].Count) -Color Yellow, Cyan, Green 197 | } 198 | if ($Delete) { 199 | Write-Color "[i] ", "Processing computers to delete for domain $Domain" -Color Yellow, Cyan, Green 200 | $getADComputersToDeleteSplat = @{ 201 | Computers = $Report["$Domain"]['Computers'] 202 | DeleteOnlyIf = $DeleteOnlyIf 203 | Exclusions = $Exclusions 204 | DomainInformation = $DomainInformation 205 | ProcessedComputers = $ProcessedComputers 206 | AzureInformationCache = $AzureInformationCache 207 | JamfInformationCache = $JamfInformationCache 208 | IncludeAzureAD = $AzureRequired 209 | IncludeJamf = $JamfRequired 210 | IncludeIntune = $IntuneRequired 211 | Type = 'Delete' 212 | } 213 | $Report["$Domain"]['ComputersToBeDeleted'] = Get-ADComputersToProcess @getADComputersToDeleteSplat 214 | #Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDeleted'].Count) -Color Yellow, Cyan, Green 215 | } 216 | } 217 | if ($null -ne $SafetyADLimit -and $AllComputers.Count -lt $SafetyADLimit) { 218 | Write-Color "[e] ", "Only ", $($AllComputers.Count), " computers found in AD, this is less than the safety limit of ", $SafetyADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan 219 | return $false 220 | } 221 | $AllComputers 222 | } -------------------------------------------------------------------------------- /Private/Get-InitialGraphComputers.ps1: -------------------------------------------------------------------------------- 1 | function Get-InitialGraphComputers { 2 | [CmdletBinding()] 3 | param( 4 | [nullable[int]] $SafetyAzureADLimit, 5 | [nullable[int]] $SafetyIntuneLimit, 6 | [nullable[int]] $DeleteLastSeenAzureMoreThan, 7 | [nullable[int]] $DeleteLastSeenIntuneMoreThan, 8 | [nullable[int]] $DeleteLastSyncAzureMoreThan, 9 | [nullable[int]] $DisableLastSeenAzureMoreThan, 10 | [nullable[int]] $DisableLastSeenIntuneMoreThan, 11 | [nullable[int]] $DisableLastSyncAzureMoreThan, 12 | [nullable[int]] $MoveLastSeenAzureMoreThan, 13 | [nullable[int]] $MoveLastSeenIntuneMoreThan, 14 | [nullable[int]] $MoveLastSyncAzureMoreThan 15 | ) 16 | 17 | $AzureInformationCache = [ordered] @{ 18 | AzureAD = [ordered] @{} 19 | Intune = [ordered] @{} 20 | } 21 | 22 | if ($PSBoundParameters.ContainsKey('DisableLastSeenAzureMoreThan') -or 23 | $PSBoundParameters.ContainsKey('DisableLastSyncAzureMoreThan') -or 24 | $PSBoundParameters.ContainsKey('DeleteLastSeenAzureMoreThan') -or 25 | $PSBoundParameters.ContainsKey('DeleteLastSyncAzureMoreThan') -or 26 | $PSBoundParameters.ContainsKey('MoveLastSeenAzureMoreThan') -or 27 | $PSBoundParameters.ContainsKey('MoveLastSyncAzureMoreThan')) { 28 | Write-Color "[i] ", "Getting all computers from AzureAD" -Color Yellow, Cyan, Green 29 | 30 | [Array] $Devices = Get-MyDevice -Synchronized -WarningAction SilentlyContinue -WarningVariable WarningVar 31 | if ($WarningVar) { 32 | Write-Color "[e] ", "Error getting computers from AzureAD: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red 33 | return $false 34 | } 35 | if ($Devices.Count -eq 0) { 36 | Write-Color "[e] ", "No computers found in AzureAD, terminating! Please disable Azure AD integration or fix connectivity." -Color Yellow, Red 37 | return $false 38 | } 39 | foreach ($Device in $Devices) { 40 | $AzureInformationCache.AzureAD[$Device.Name] = $Device 41 | } 42 | 43 | if ($null -ne $SafetyAzureADLimit -and $Devices.Count -lt $SafetyAzureADLimit) { 44 | Write-Color "[e] ", "Only ", $($Devices.Count), " computers found in AzureAD, this is less than the safety limit of ", $SafetyAzureADLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan 45 | return $false 46 | } 47 | Write-Color "[i] ", "Synchronized Computers found in AzureAD`: ", $($Devices.Count) -Color Yellow, Cyan, Green 48 | } 49 | if ($PSBoundParameters.ContainsKey('DisableLastSeenIntuneMoreThan') -or 50 | $PSBoundParameters.ContainsKey('DeleteLastSeenIntuneMoreThan') -or 51 | $PSBoundParameters.ContainsKey('MoveLastSeenIntuneMoreThan')) { 52 | Write-Color "[i] ", "Getting all computers from Intune" -Color Yellow, Cyan, Green 53 | 54 | [Array] $DevicesIntune = Get-MyDeviceIntune -WarningAction SilentlyContinue -WarningVariable WarningVar -Synchronized 55 | if ($WarningVar) { 56 | Write-Color "[e] ", "Error getting computers from Intune: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red 57 | return $false 58 | } 59 | if ($DevicesIntune.Count -eq 0) { 60 | Write-Color "[e] ", "No computers found in Intune, terminating! Please disable Intune integration or fix connectivity." -Color Yellow, Red 61 | return $false 62 | } 63 | 64 | foreach ($device in $DevicesIntune) { 65 | $AzureInformationCache.Intune[$Device.Name] = $device 66 | } 67 | 68 | if ($null -ne $SafetyIntuneLimit -and $DevicesIntune.Count -lt $SafetyIntuneLimit) { 69 | Write-Color "[e] ", "Only ", $($DevicesIntune.Count), " computers found in Intune, this is less than the safety limit of ", $SafetyIntuneLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan 70 | return $false 71 | } 72 | 73 | Write-Color "[i] ", "Synchronized Computers found in Intune`: ", $($DevicesIntune.Count) -Color Yellow, Cyan, Green 74 | } 75 | 76 | $AzureInformationCache 77 | } -------------------------------------------------------------------------------- /Private/Get-InitialJamfComputers.ps1: -------------------------------------------------------------------------------- 1 | function Get-InitialJamfComputers { 2 | [CmdletBinding()] 3 | param( 4 | [bool] $DisableLastContactJamfMoreThan, 5 | [bool] $MoveLastContactJamfMoreThan, 6 | [bool] $DeleteLastContactJamfMoreThan, 7 | [nullable[int]] $SafetyJamfLimit 8 | ) 9 | $JamfCache = [ordered] @{} 10 | if ($PSBoundParameters.ContainsKey('DisableLastContactJamfMoreThan') -or 11 | $PSBoundParameters.ContainsKey('DeleteLastContactJamfMoreThan') -or 12 | $PSBoundParameters.ContainsKey('MoveLastContactJamfMoreThan') 13 | ) { 14 | Write-Color "[i] ", "Getting all computers from Jamf" -Color Yellow, Cyan, Green 15 | [Array] $Jamf = Get-JamfDevice -WarningAction SilentlyContinue -WarningVariable WarningVar 16 | if ($WarningVar) { 17 | Write-Color "[e] ", "Error getting computers from Jamf: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red 18 | return $false 19 | } 20 | if ($Jamf.Count -eq 0) { 21 | Write-Color "[e] ", "No computers found in Jamf, terminating! Please disable Jamf integration or fix connectivity." -Color Yellow, Red 22 | return $false 23 | } else { 24 | Write-Color "[i] ", "Computers found in Jamf`: ", $($Jamf.Count) -Color Yellow, Cyan, Green 25 | } 26 | 27 | if ($null -ne $SafetyJamfLimit -and $Jamf.Count -lt $SafetyJamfLimit) { 28 | Write-Color "[e] ", "Only ", $($Jamf.Count), " computers found in Jamf, this is less than the safety limit of ", $SafetyJamfLimit, ". Terminating!" -Color Yellow, Cyan, Red, Cyan 29 | return $false 30 | } 31 | 32 | foreach ($device in $Jamf) { 33 | $JamfCache[$Device.Name] = $device 34 | } 35 | } 36 | $JamfCache 37 | } -------------------------------------------------------------------------------- /Private/Import-ComputersData.ps1: -------------------------------------------------------------------------------- 1 | function Import-ComputersData { 2 | [CmdletBinding()] 3 | param( 4 | [string] $DataStorePath, 5 | [System.Collections.IDictionary] $Export 6 | ) 7 | 8 | $ProcessedComputers = [ordered] @{ } 9 | $Today = Get-Date 10 | try { 11 | if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) { 12 | $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop 13 | # convert old format to new format 14 | $FileImport = Convert-ListProcessed -FileImport $FileImport 15 | 16 | if ($FileImport.PendingDeletion) { 17 | if ($FileImport.PendingDeletion.GetType().Name -notin 'Hashtable', 'OrderedDictionary') { 18 | Write-Color -Text "[e] ", "Incorrect XML format. PendingDeletion is not a hashtable/ordereddictionary. Terminating." -Color Yellow, Red 19 | return $false 20 | } 21 | } 22 | if ($FileImport.History) { 23 | if ($FileImport.History.GetType().Name -ne 'ArrayList') { 24 | Write-Color -Text "[e] ", "Incorrect XML format. History is not a ArrayList. Terminating." -Color Yellow, Red 25 | return $False 26 | } 27 | } 28 | $ProcessedComputers = $FileImport.PendingDeletion 29 | foreach ($ComputerFullName in $ProcessedComputers.Keys) { 30 | $Computer = $ProcessedComputers[$ComputerFullName] 31 | if ($Computer.PSObject.Properties.Name -notcontains 'TimeOnPendingList') { 32 | $TimeOnPendingList = if ($Computer.ActionDate) { 33 | - $($Computer.ActionDate - $Today).Days 34 | } else { 35 | $null 36 | } 37 | # We need to add this property to the object, as it may not exist on the old exports 38 | Add-Member -MemberType NoteProperty -Name 'TimeOnPendingList' -Value $TimeOnPendingList -Force -InputObject $Computer 39 | Add-Member -MemberType NoteProperty -Name 'TimeToLeavePendingList' -Value $null -Force -InputObject $Computer 40 | } else { 41 | $TimeOnPendingList = if ($Computer.ActionDate) { 42 | - $($Computer.ActionDate - $Today).Days 43 | } else { 44 | $null 45 | } 46 | $Computer.TimeOnPendingList = $TimeOnPendingList 47 | } 48 | } 49 | $Export['History'] = $FileImport.History 50 | } 51 | if (-not $ProcessedComputers) { 52 | $ProcessedComputers = [ordered] @{ } 53 | } 54 | } catch { 55 | Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red 56 | return $false 57 | } 58 | $ProcessedComputers 59 | } -------------------------------------------------------------------------------- /Private/Import-SIDHistory.ps1: -------------------------------------------------------------------------------- 1 | function Import-SIDHistory { 2 | [CmdletBinding()] 3 | param( 4 | [string] $DataStorePath, 5 | [System.Collections.IDictionary] $Export 6 | ) 7 | try { 8 | if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) { 9 | $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop 10 | if ($FileImport.History) { 11 | $Export.History = $FileImport.History 12 | } 13 | } 14 | } catch { 15 | Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red 16 | return $false 17 | } 18 | 19 | $Export 20 | } -------------------------------------------------------------------------------- /Private/Move-WinADComputer.ps1: -------------------------------------------------------------------------------- 1 | function Move-WinADComputer { 2 | [CmdletBinding(SupportsShouldProcess)] 3 | param( 4 | [bool] $Success, 5 | [bool] $DisableAndMove, 6 | [System.Collections.IDictionary] $OrganizationalUnit, 7 | [PSCustomObject] $Computer, 8 | [switch] $WhatIfDisable, 9 | [switch] $DontWriteToEventLog, 10 | [string] $Server, 11 | [switch] $RemoveProtectedFromAccidentalDeletionFlag 12 | ) 13 | if ($Success -and $DisableAndMove) { 14 | # we only move if we successfully disabled the computer 15 | if ($OrganizationalUnit[$Domain]) { 16 | if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) { 17 | Write-Color -Text "[i] Computer ", $Computer.DistinguishedName, " is already in the correct OU." -Color Yellow, Green, Yellow 18 | } else { 19 | if ($Computer.ProtectedFromAccidentalDeletion) { 20 | if ($RemoveProtectedFromAccidentalDeletionFlag) { 21 | try { 22 | Write-Color -Text "[i] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 23 | Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfDisable 24 | if (-not $DontWriteToEventLog) { 25 | Write-Event -ID 15 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental move flag for computer $($Computer.SamAccountName) successful." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings 26 | } 27 | $Success = $true 28 | } catch { 29 | $Success = $false 30 | Write-Color -Text "[-] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 31 | if (-not $DontWriteToEventLog) { 32 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental move flag for computer $($Computer.SamAccountName) failed." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 33 | } 34 | foreach ($W in $Warnings) { 35 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 36 | } 37 | } 38 | } else { 39 | Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental move. Move skipped.' -Color Yellow, Green, Yellow 40 | if (-not $DontWriteToEventLog) { 41 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Computer $($Computer.SamAccountName) is protected from accidental move. Move skipped." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings 42 | } 43 | $Success = $false 44 | } 45 | } else { 46 | $Success = $true 47 | } 48 | if ($Success) { 49 | $Success = $false 50 | try { 51 | $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfDisable -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru 52 | Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) successful." -Color Yellow, Green, Yellow 53 | if (-not $DontWriteToEventLog) { 54 | Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable) -WarningAction SilentlyContinue -WarningVariable warnings 55 | } 56 | foreach ($W in $Warnings) { 57 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 58 | } 59 | $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName 60 | $Success = $true 61 | } catch { 62 | $Success = $false 63 | Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 64 | if (-not $DontWriteToEventLog) { 65 | Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDisable, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 66 | } 67 | foreach ($W in $Warnings) { 68 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 69 | } 70 | $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message 71 | } 72 | } 73 | } 74 | } 75 | } 76 | $Success 77 | } -------------------------------------------------------------------------------- /Private/New-ADComputersStatistics.ps1: -------------------------------------------------------------------------------- 1 | function New-ADComputersStatistics { 2 | [CmdletBinding()] 3 | param( 4 | [Array] $ComputersToProcess 5 | ) 6 | $Statistics = [ordered] @{ 7 | All = $ComputersToProcess.Count 8 | 9 | ToMove = 0 10 | ToMoveComputerWorkstation = 0 11 | ToMoveComputerServer = 0 12 | ToMoveComputerUnknown = 0 13 | 14 | ToDisable = 0 15 | ToDisableComputerUnknown = 0 16 | ToDisableComputerWorkstation = 0 17 | ToDisableComputerServer = 0 18 | 19 | ToDelete = 0 20 | ToDeleteComputerWorkstation = 0 21 | ToDeleteComputerServer = 0 22 | ToDeleteComputerUnknown = 0 23 | 24 | TotalWindowsServers = 0 25 | TotalWindowsWorkstations = 0 26 | TotalMacOS = 0 27 | TotalLinux = 0 28 | TotalUnknown = 0 29 | 30 | Delete = [ordered] @{ 31 | LastLogonDays = [ordered ]@{} 32 | PasswordLastChangedDays = [ordered] @{} 33 | Systems = [ordered] @{} 34 | } 35 | Move = [ordered] @{ 36 | LastLogonDays = [ordered] @{} 37 | PasswordLastChangedDays = [ordered] @{} 38 | Systems = [ordered] @{} 39 | } 40 | Disable = [ordered] @{ 41 | LastLogonDays = [ordered] @{} 42 | PasswordLastChangedDays = [ordered] @{} 43 | Systems = [ordered] @{} 44 | } 45 | 'Not required' = [ordered] @{ 46 | LastLogonDays = [ordered] @{} 47 | PasswordLastChangedDays = [ordered] @{} 48 | Systems = [ordered] @{} 49 | } 50 | 'ExcludedBySetting' = [ordered] @{ 51 | LastLogonDays = [ordered] @{} 52 | PasswordLastChangedDays = [ordered] @{} 53 | Systems = [ordered] @{} 54 | } 55 | 'ExcludedByFilter' = [ordered] @{ 56 | LastLogonDays = [ordered] @{} 57 | PasswordLastChangedDays = [ordered] @{} 58 | Systems = [ordered] @{} 59 | } 60 | 61 | } 62 | foreach ($Computer in $ComputersToProcess) { 63 | if ($Computer.OperatingSystem -like "Windows Server*") { 64 | $Statistics.TotalWindowsServers++ 65 | } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { 66 | $Statistics.TotalWindowsWorkstations++ 67 | } elseif ($Computer.OperatingSystem -like "Mac*") { 68 | $Statistics.TotalMacOS++ 69 | } elseif ($Computer.OperatingSystem -like "Linux*") { 70 | $Statistics.TotalLinux++ 71 | } else { 72 | $Statistics.TotalUnknown++ 73 | } 74 | if ($Computer.Action -eq 'Disable') { 75 | $Statistics.ToDisable++ 76 | if ($Computer.OperatingSystem -like "Windows Server*") { 77 | $Statistics.ToDisableComputerServer++ 78 | } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { 79 | $Statistics.ToDisableComputerWorkstation++ 80 | } else { 81 | $Statistics.ToDisableComputerUnknown++ 82 | } 83 | } elseif ($Computer.Action -eq 'Move') { 84 | $Statistics.ToMove++ 85 | if ($Computer.OperatingSystem -like "Windows Server*") { 86 | $Statistics.ToMoveComputerServer++ 87 | } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { 88 | $Statistics.ToMoveComputerWorkstation++ 89 | } else { 90 | $Statistics.ToMoveComputerUnknown++ 91 | } 92 | } elseif ($Computer.Action -eq 'Delete') { 93 | $Statistics.ToDelete++ 94 | if ($Computer.OperatingSystem -like "Windows Server*") { 95 | $Statistics.ToDeleteComputerServer++ 96 | } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { 97 | $Statistics.ToDeleteComputerWorkstation++ 98 | } else { 99 | $Statistics.ToDeleteComputerUnknown++ 100 | } 101 | } 102 | if ($Computer.OperatingSystem) { 103 | $Statistics[$Computer.Action]['Systems'][$Computer.OperatingSystem]++ 104 | } else { 105 | $Statistics[$Computer.Action]['Systems']['Unknown']++ 106 | } 107 | if ($Computer.LastLogonDays -gt 720) { 108 | $Statistics[$Computer.Action]['LastLogonDays']['Over 720 days']++ 109 | } elseif ($Computer.LastLogonDays -gt 360) { 110 | $Statistics[$Computer.Action]['LastLogonDays']['Over 360 days']++ 111 | } elseif ($Computer.LastLogonDays -gt 180) { 112 | $Statistics[$Computer.Action]['LastLogonDays']['Over 180 days']++ 113 | } elseif ($Computer.LastLogonDays -gt 90) { 114 | $Statistics[$Computer.Action]['LastLogonDays']['Over 90 days']++ 115 | } elseif ($Computer.LastLogonDays -gt 30) { 116 | $Statistics[$Computer.Action]['LastLogonDays']['Over 30 days']++ 117 | } else { 118 | $Statistics[$Computer.Action]['LastLogonDays']['Under 30 days']++ 119 | } 120 | if ($Computer.PasswordLastChangedDays -gt 720) { 121 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 720 days']++ 122 | } elseif ($Computer.PasswordLastChangedDays -gt 360) { 123 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 360 days']++ 124 | } elseif ($Computer.PasswordLastChangedDays -gt 180) { 125 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 180 days']++ 126 | } elseif ($Computer.PasswordLastChangedDays -gt 90) { 127 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 90 days']++ 128 | } elseif ($Computer.PasswordLastChangedDays -gt 30) { 129 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 30 days']++ 130 | } else { 131 | $Statistics[$Computer.Action]['PasswordLastChangedDays']['Under 30 days']++ 132 | } 133 | } 134 | $Statistics 135 | } -------------------------------------------------------------------------------- /Private/New-EmailBodyComputers.ps1: -------------------------------------------------------------------------------- 1 | function New-EmailBodyComputers { 2 | [CmdletBinding()] 3 | param( 4 | [Array] $CurrentRun 5 | ) 6 | 7 | Write-Color -Text "[i] ", "Generating email body" -Color Yellow, White 8 | 9 | [Array] $DisabledObjects = $CurrentRun | Where-Object { $_.Action -eq 'Disable' } 10 | [Array] $DeletedObjects = $CurrentRun | Where-Object { $_.Action -eq 'Delete' } 11 | 12 | $EmailBody = EmailBody -EmailBody { 13 | EmailText -Text "Hello," 14 | 15 | EmailText -LineBreak 16 | 17 | EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold 18 | 19 | EmailText -LineBreak 20 | 21 | EmailText -Text "Following is a summary for the computer object cleanup:" -FontWeight bold 22 | EmailList { 23 | EmailListItem -Text "Objects actioned: ", $Output.CurrentRun.Count -Color None, Green -FontWeight normal, bold 24 | EmailListItem -Text "Objects deleted: ", $DeletedObjects.Count -Color None, Salmon -FontWeight normal, bold 25 | EmailListItem -Text "Objects disabled: ", $DisabledObjects.Count -Color None, Orange -FontWeight normal, bold 26 | } 27 | 28 | EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon 29 | EmailTable -DataTable $Output.CurrentRun -HideFooter { 30 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace -Inline 31 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow -Inline 32 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen -Inline 33 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon -Inline 34 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue -Inline 35 | } 36 | 37 | EmailText -LineBreak 38 | 39 | EmailText -Text "Regards," 40 | EmailText -Text "Automations Team" -FontWeight bold 41 | } 42 | 43 | Write-Color -Text "[i] ", "Email body generated" -Color Yellow, White 44 | 45 | $EmailBody 46 | } -------------------------------------------------------------------------------- /Private/New-EmailBodySidHistory.ps1: -------------------------------------------------------------------------------- 1 | function New-EmailBodySidHistory { 2 | [CmdletBinding()] 3 | param( 4 | [System.Collections.IDictionary] $Export 5 | ) 6 | 7 | $EmailBody = EmailBody -EmailBody { 8 | EmailText -Text "Hello," 9 | 10 | EmailText -LineBreak 11 | 12 | EmailText -Text "This is an automated email from Automations run on ", $Env:COMPUTERNAME, " on ", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), " by ", $Env:UserName -Color None, Green, None, Green, None, Green -FontWeight normal, bold, normal, bold, normal, bold 13 | 14 | EmailText -LineBreak 15 | 16 | New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History. The following statistics provide insights into processed SID history in the forest:" -FontSize 10pt 17 | 18 | $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled } 19 | $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled } 20 | 21 | EmailList { 22 | EmailListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold 23 | EmailListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold 24 | EmailListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal 25 | EmailListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal 26 | } -FontSize 10pt 27 | 28 | EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon 29 | 30 | EmailTable -DataTable $Export.CurrentRun { 31 | EmailTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackGroundColor MintGreen -FailBackgroundColor Salmon -Inline 32 | EmailTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackGroundColor LightCoral -FailBackgroundColor LightGreen -Inline 33 | EmailTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackGroundColor LightGreen -FailBackgroundColor Salmon -Inline 34 | 35 | EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackGroundColor LightPink -Inline 36 | EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackGroundColor LightCoral -Inline 37 | 38 | EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackGroundColor LightGreen -Inline 39 | EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackGroundColor Salmon -Inline 40 | EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackGroundColor LightBlue -Inline 41 | } -HideFooter -PrettifyObject 42 | 43 | EmailText -LineBreak 44 | 45 | EmailText -Text "Regards," 46 | EmailText -Text "Automations Team" -FontWeight bold 47 | } 48 | $EmailBody 49 | } -------------------------------------------------------------------------------- /Private/New-HTMLProcessedSIDHistory.ps1: -------------------------------------------------------------------------------- 1 | function New-HTMLProcessedSIDHistory { 2 | [CmdletBinding()] 3 | param( 4 | $Export, 5 | [System.Collections.IDictionary] $ForestInformation, 6 | [System.Collections.IDictionary] $Output, 7 | [string] $FilePath, 8 | [switch] $HideHTML, 9 | [switch] $Online, 10 | [string] $LogPath, 11 | [System.Collections.IDictionary] $Configuration 12 | ) 13 | New-HTML { 14 | New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow 15 | New-HTMLTableOption -DataStore JavaScript -ArrayJoin -ArrayJoinString ", " -BoolAsString 16 | New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey 17 | 18 | New-HTMLHeader { 19 | New-HTMLSection -Invisible { 20 | New-HTMLSection -Invisible { 21 | New-HTMLSection { 22 | New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue 23 | } -JustifyContent flex-start -Invisible 24 | New-HTMLSection { 25 | New-HTMLText -Text "Cleanup Monster - $($Export['Version'])" -Color Blue 26 | } -JustifyContent flex-end -Invisible 27 | } 28 | } 29 | 30 | New-HTMLText -Text "Overview of cleanup process for the SID History in the forest ", $($ForestInformation.Forest) -Color None, None -FontSize 14pt -FontWeight normal, bold -Alignment center 31 | 32 | New-HTMLSection -HeaderText "SID History Report for $($ForestInformation.Forest)" { 33 | New-HTMLPanel { 34 | New-HTMLText -Text @( 35 | "This report provides an overview of the SID history in the forest along with the current and history deletion status of SID history values as configured in the script. ", 36 | "The report is divided into three tabs: Overview, Current Deletion Status, and History Deletion Status. ", 37 | "The following report shows 3 tabs:" 38 | ) -FontSize 10pt 39 | 40 | New-HTMLList { 41 | New-HTMLListItem -Text "Overview", " - ", "provides an overview of the SID history in the forest" -FontWeight bold, normal, normal 42 | New-HTMLListItem -Text "Current Deletion Status", " - ", "shows the current deletion status of SID history values for given day (this report only)" -FontWeight bold, normal, normal 43 | New-HTMLListItem -Text "History Deletion Status", " - ", "shows the history deletion status of SID history values over time" -FontWeight bold, normal, normal 44 | } -FontSize 10pt 45 | 46 | New-HTMLText -Text "The following statistics provide insights into the SID history in the forest:" -FontSize 10pt 47 | 48 | New-HTMLList { 49 | New-HTMLListItem -Text "$($Output.All.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal 50 | New-HTMLListItem -Text "$($Output.Statistics.TotalUsers)", " users with SID history values" -Color BlueViolet, None -FontWeight bold, normal 51 | New-HTMLListItem -Text "$($Output.Statistics.TotalGroups)", " groups with SID history values" -Color BlueViolet, None -FontWeight bold, normal 52 | New-HTMLListItem -Text "$($Output.Statistics.TotalComputers)", " computers with SID history values" -Color BlueViolet, None -FontWeight bold, normal 53 | New-HTMLListItem -Text "$($Output.Statistics.EnabledObjects)", " enabled objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal 54 | New-HTMLListItem -Text "$($Output.Statistics.DisabledObjects)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal 55 | New-HTMLListItem -Text "$($Output.Keys.Count - 2)", " different domains with SID history values" -Color BlueViolet, None -FontWeight bold, normal 56 | } -LineBreak -FontSize 10pt 57 | 58 | 59 | New-HTMLText -Text "The following statistics provide insights into the SID history categories:" -FontSize 10pt 60 | 61 | New-HTMLList { 62 | # Add statistics for the three SID history categories 63 | New-HTMLListItem -Text "$($Output.Statistics.InternalSIDs)", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal 64 | New-HTMLListItem -Text "$($Output.Statistics.ExternalSIDs)", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal 65 | New-HTMLListItem -Text "$($Output.Statistics.UnknownSIDs)", " SID history values from unknown domains (deleted or broken trusts)" -Color Crimson, None -FontWeight bold, normal 66 | } -FontSize 10pt 67 | } 68 | New-HTMLPanel { 69 | New-HTMLText -Text "The following table lists all domains in the forest and active trusts, and their respective domain SID values, along with their types." -FontSize 10pt 70 | New-HTMLList { 71 | foreach ($SID in $Output.DomainSIDs.Keys) { 72 | $DomainSID = $Output.DomainSIDs[$SID] 73 | New-HTMLListItem -Text "Domain ", $($DomainSID.Domain), ", SID: ", $($DomainSID.SID), ", Type: ", $($DomainSID.Type) -Color None, BlueViolet, None, BlueViolet, None, BlueViolet -FontWeight normal, bold, normal, bold, normal, bold 74 | } 75 | } -FontSize 10pt 76 | } 77 | } 78 | 79 | } 80 | [Array] $DomainNames = foreach ($Key in $Output.Keys) { 81 | if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs')) { 82 | continue 83 | } 84 | $Key 85 | } 86 | 87 | New-HTMLTab -Name 'Overview' { 88 | foreach ($Domain in $DomainNames) { 89 | [Array] $Objects = $Output[$Domain] 90 | $EnabledObjects = $Objects | Where-Object { $_.Enabled } 91 | $DisabledObjects = $Objects | Where-Object { -not $_.Enabled } 92 | $Types = $Objects | Group-Object -Property ObjectClass -NoElement 93 | 94 | 95 | if ($Domain -eq 'All') { 96 | $Name = 'All' 97 | } else { 98 | if ($Output.DomainSIDs[$Domain]) { 99 | $DomainName = $Output.DomainSIDs[$Domain].Domain 100 | $Name = "$DomainName ($($Objects.Count))" 101 | } else { 102 | $Name = "$Domain ($($Objects.Count))" 103 | } 104 | } 105 | 106 | New-HTMLTab -Name $Name { 107 | New-HTMLSection -HeaderText "Domain $Domain" { 108 | New-HTMLPanel -Invisible { 109 | New-HTMLText -Text "Overview for ", $Domain -Color Blue, BattleshipGrey -FontSize 10pt 110 | New-HTMLList { 111 | New-HTMLListItem -Text "$($Objects.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal 112 | New-HTMLListItem -Text "$($EnabledObjects.Count)", " enabled objects with SID history values" -Color Green, None -FontWeight bold, normal 113 | New-HTMLListItem -Text "$($DisabledObjects.Count)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal 114 | 115 | # Calculate SID history categories for this domain 116 | $InternalSIDsForDomain = ($Objects | ForEach-Object { $_.InternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum 117 | $ExternalSIDsForDomain = ($Objects | ForEach-Object { $_.ExternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum 118 | $UnknownSIDsForDomain = ($Objects | ForEach-Object { $_.UnknownCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum 119 | 120 | New-HTMLListItem -Text "$InternalSIDsForDomain", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal 121 | New-HTMLListItem -Text "$ExternalSIDsForDomain", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal 122 | New-HTMLListItem -Text "$UnknownSIDsForDomain", " SID history values from unknown domains" -Color Crimson, None -FontWeight bold, normal 123 | 124 | New-HTMLListItem -Text "Object types:" { 125 | New-HTMLList { 126 | foreach ($Type in $Types) { 127 | New-HTMLListItem -Text "$($Type.Count)", " ", $Type.Name, " objects with SID history values" -Color BlueViolet, None, BlueViolet, None -FontWeight bold, normal, bold, normal 128 | } 129 | } 130 | } -FontSize 10pt 131 | } -FontSize 10pt 132 | } 133 | New-HTMLPanel -Invisible { 134 | New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt 135 | New-HTMLList { 136 | New-HTMLListItem -Text "Domain", " - ", "this column shows the domain of the object" -FontWeight bold, normal, normal 137 | New-HTMLListItem -Text "ObjectClass", " - ", "this column shows the object class of the object (user, device, group)" -FontWeight bold, normal, normal 138 | New-HTMLListItem -Text "Internal", " - ", "this column shows SIDs from domains within the current forest" -FontWeight bold, normal, normal 139 | New-HTMLListItem -Text "External", " - ", "this column shows SIDs from domains that are trusted by the current forest" -FontWeight bold, normal, normal 140 | New-HTMLListItem -Text "Unknown", " - ", "this column shows SIDs from domains that no longer exist or have broken trusts" -FontWeight bold, normal, normal 141 | New-HTMLListItem -Text "Enabled", " - ", "this column shows if the object is enabled" -FontWeight bold, normal, normal 142 | New-HTMLListItem -Text "SIDHistory", " - ", "this column shows the SID history values of the object" -FontWeight bold, normal, normal 143 | New-HTMLListItem -Text "Domains", " - ", "this column shows the domains of the SID history values" -FontWeight bold, normal, normal 144 | New-HTMLListItem -Text "DomainsExpanded", " - ", "this column shows the expanded domains of the SID history values (if possible), including SID if not possible to expand" -FontWeight bold, normal, normal 145 | } -FontSize 10pt 146 | } 147 | } 148 | New-HTMLTable -DataTable $Objects -Filtering { 149 | New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon 150 | New-HTMLTableCondition -Name 'InternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor ForestGreen 151 | New-HTMLTableCondition -Name 'ExternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor DodgerBlue 152 | New-HTMLTableCondition -Name 'UnknownCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor Crimson 153 | } -ScrollX 154 | } -TextTransform uppercase 155 | } 156 | } 157 | New-HTMLTab -Name 'Current Deletion Status' { 158 | New-HTMLSection -HeaderText "SID History Report" { 159 | New-HTMLPanel -Invisible { 160 | New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History. The following statistics provide insights into processed SID history in the forest:" -FontSize 10pt 161 | 162 | $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled } 163 | $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled } 164 | 165 | New-HTMLList { 166 | New-HTMLListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold 167 | New-HTMLListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold 168 | New-HTMLListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal 169 | New-HTMLListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal 170 | } -FontSize 10pt 171 | 172 | 173 | New-HTMLText -Text "The following table lists all objects with SID history values and their current deletion status." -FontSize 10pt 174 | } 175 | } 176 | New-HTMLTable -DataTable $Export.CurrentRun -Filtering { 177 | New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon 178 | New-HTMLTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightCoral -FailBackgroundColor LightGreen 179 | New-HTMLTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor LightGreen -FailBackgroundColor Salmon 180 | 181 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink 182 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral 183 | 184 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen 185 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon 186 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue 187 | } -ScrollX 188 | } 189 | New-HTMLTab -Name 'History Deletion Status' { 190 | New-HTMLSection -HeaderText "SID History Report" { 191 | New-HTMLPanel -Invisible { 192 | New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History over time." -FontSize 10pt 193 | } 194 | } 195 | New-HTMLTable -DataTable $Export.History -Filtering { 196 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen 197 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon 198 | New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue 199 | 200 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink 201 | New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral 202 | } -ScrollX 203 | } 204 | New-HTMLTab -Name "Configuration" { 205 | New-HTMLSection -HeaderText "Configuration" { 206 | New-HTMLPanel -Invisible { 207 | New-HTMLText -Text "The following table lists all configuration settings used in the script." -FontSize 10pt 208 | } 209 | } 210 | New-HTMLTable -DataTable $Configuration -ScrollX 211 | } 212 | if ($LogPath) { 213 | $LogsContent = Get-Content -Path $LogPath -Raw -ErrorAction SilentlyContinue 214 | if ($LogsContent) { 215 | New-HTMLTab -Name 'Logs' { 216 | New-HTMLCodeBlock -Code $LogsContent -Style generic 217 | } 218 | } 219 | } 220 | } -FilePath $FilePath -ShowHTML:(-not $HideHTML) -Online:$Online.IsPresent 221 | } -------------------------------------------------------------------------------- /Private/Remove-ADSIDHistory.ps1: -------------------------------------------------------------------------------- 1 | function Remove-ADSIDHistory { 2 | [CmdletBinding(SupportsShouldProcess)] 3 | param( 4 | [Array] $ObjectsToProcess, 5 | [System.Collections.IDictionary] $Export, 6 | [int] $RemoveLimitSID 7 | ) 8 | Write-Color -Text "[i] ", "Starting process of removing SID History entries from ", $ObjectsToProcess.Count, " objects" -Color Yellow, White, Green 9 | 10 | $GlobalLimitSID = 0 11 | $ProcessedSIDs = 0 12 | $ProcessedObjects = 0 13 | 14 | $Export['CurrentRun'] = [System.Collections.Generic.List[PSCustomObject]]::new() 15 | 16 | :TopLoop foreach ($Item in $ObjectsToProcess) { 17 | $Object = $Item.Object 18 | $QueryServer = $Item.QueryServer 19 | 20 | $CurrentRunObject = [PSCustomObject] @{ 21 | ObjectName = $Object.Name 22 | ObjectDomain = $Object.Domain 23 | Enabled = $Object.Enabled 24 | SIDBefore = $Object.SIDHistory -join ", " 25 | SIDBeforeCount = $Object.SIDHistory.Count 26 | Action = $null 27 | ActionDate = $null 28 | ActionStatus = $null 29 | ActionError = '' 30 | SIDRemoved = @() 31 | SIDAfter = @() 32 | SIDAfterCount = 0 33 | ObjectDN = $Object.DistinguishedName 34 | } 35 | 36 | $ProcessedObjects++ 37 | 38 | # Process individual SIDs for this object 39 | foreach ($SID in $Object.SIDHistory) { 40 | $CurrentDate = Get-Date 41 | if ($PSCmdlet.ShouldProcess("$($Object.Name) ($($Object.Domain))", "Remove SID History entry $SID")) { 42 | Write-Color -Text "[i] ", "Removing SID History entry $SID from ", $Object.Name -Color Yellow, White, Green 43 | try { 44 | Set-ADObject -Identity $Object.DistinguishedName -Remove @{ SIDHistory = $SID } -Server $QueryServer -ErrorAction Stop 45 | $Result = [PSCustomObject]@{ 46 | ObjectName = $Object.Name 47 | ObjectDomain = $Object.Domain 48 | Enabled = $Object.Enabled 49 | SID = $SID 50 | Action = 'RemovePerSID' 51 | ActionDate = $CurrentDate 52 | ActionStatus = 'Success' 53 | ActionError = '' 54 | ObjectDN = $Object.DistinguishedName 55 | } 56 | $CurrentRunObject.Action = 'RemovePerSID' 57 | $CurrentRunObject.ActionDate = $CurrentDate 58 | $CurrentRunObject.ActionStatus = 'Success' 59 | $CurrentRunObject.ActionError = '' 60 | $CurrentRunObject.SIDRemoved += $SID 61 | Write-Color -Text "[+] ", "Removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green 62 | } catch { 63 | Write-Color -Text "[!] ", "Failed to remove SID History entry $SID from ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red 64 | $Result = [PSCustomObject]@{ 65 | ObjectName = $Object.Name 66 | ObjectDomain = $Object.Domain 67 | SID = $SID 68 | Action = 'RemovePerSID' 69 | ActionDate = $CurrentDate 70 | ActionStatus = 'Failed' 71 | ActionError = $_.Exception.Message 72 | ObjectDN = $Object.DistinguishedName 73 | } 74 | $CurrentRunObject.Action = 'RemovePerSID' 75 | $CurrentRunObject.ActionDate = $CurrentDate 76 | $CurrentRunObject.ActionStatus = 'Failed' 77 | $CurrentRunObject.ActionError = $_.Exception.Message 78 | } 79 | } else { 80 | Write-Color -Text "[i] ", "Would have removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green 81 | $Result = [PSCustomObject]@{ 82 | ObjectName = $Object.Name 83 | ObjectDomain = $Object.Domain 84 | SID = $SID 85 | Action = 'RemovePerSID' 86 | ActionDate = Get-Date 87 | ActionStatus = 'WhatIf' 88 | ActionError = '' 89 | ObjectDN = $Object.DistinguishedName 90 | } 91 | $CurrentRunObject.SIDRemoved += $SID 92 | $CurrentRunObject.Action = 'RemovePerSID' 93 | $CurrentRunObject.ActionDate = $CurrentDate 94 | $CurrentRunObject.ActionStatus = 'WhatIf' 95 | $CurrentRunObject.ActionError = '' 96 | } 97 | $null = $Export.History.Add($Result) 98 | 99 | try { 100 | $RefreshedObject = Get-ADObject -Identity $Object.DistinguishedName -Properties SIDHistory -Server $QueryServer -ErrorAction Stop 101 | } catch { 102 | Write-Color -Text "[!] ", "Failed to refresh object ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red, Red 103 | $RefreshedObject = $null 104 | } 105 | if ($RefreshedObject -and $RefreshedObject.SIDHistory) { 106 | $CurrentRunObject.SIDAfter = $RefreshedObject.SIDHistory -join ", " 107 | } else { 108 | $CurrentRunObject.SIDAfter = $null 109 | } 110 | 111 | $CurrentRunObject.SIDAfterCount = $RefreshedObject.SIDHistory.Count 112 | $Export.CurrentRun.Add($CurrentRunObject) 113 | $GlobalLimitSID++ 114 | $ProcessedSIDs++ 115 | 116 | if ($GlobalLimitSID -ge $RemoveLimitSID) { 117 | Write-Color -Text "[i] ", "Reached SID limit of ", $RemoveLimitSID, ". Stopping processing." -Color Yellow, White, Green, White 118 | break TopLoop 119 | } 120 | } 121 | } 122 | $Export['ProcessedObjects'] = $ProcessedObjects 123 | $Export['ProcessedSIDs'] = $ProcessedSIDs 124 | 125 | Write-Color -Text "[i] ", "Processed ", $ProcessedObjects, " objects out of ", $ObjectsToProcess.Count, " and removed ", $ProcessedSIDs, " SID History entries" -Color Yellow, White, Green, White, Green, White, Green, White 126 | } -------------------------------------------------------------------------------- /Private/Request-ADComputersDelete.ps1: -------------------------------------------------------------------------------- 1 | function Request-ADComputersDelete { 2 | [cmdletBinding(SupportsShouldProcess)] 3 | param( 4 | [System.Collections.IDictionary] $Report, 5 | [switch] $ReportOnly, 6 | [switch] $WhatIfDelete, 7 | [int] $DeleteLimit, 8 | [System.Collections.IDictionary] $ProcessedComputers, 9 | [DateTime] $Today, 10 | [switch] $DontWriteToEventLog, 11 | [switch] $RemoveProtectedFromAccidentalDeletionFlag 12 | ) 13 | 14 | $CountDeleteLimit = 0 15 | # :top means name of the loop, so we can break it 16 | :topLoop foreach ($Domain in $Report.Keys) { 17 | foreach ($Computer in $Report["$Domain"]['Computers']) { 18 | $Server = $Report["$Domain"]['Server'] 19 | if ($Computer.Action -ne 'Delete') { 20 | continue 21 | } 22 | if ($ReportOnly) { 23 | $Computer 24 | } else { 25 | if ($Computer.ProtectedFromAccidentalDeletion) { 26 | if ($RemoveProtectedFromAccidentalDeletionFlag) { 27 | try { 28 | Write-Color -Text "[i] Removing protected from accidental deletion flag for computer ", $Computer.DistinguishedName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 29 | Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfDelete 30 | if (-not $DontWriteToEventLog) { 31 | Write-Event -ID 15 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental deletion flag for computer $($Computer.SamAccountName) successful." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete) -WarningAction SilentlyContinue -WarningVariable warnings 32 | } 33 | $Success = $true 34 | } catch { 35 | $Success = $false 36 | Write-Color -Text "[-] Removing protected from accidental deletion flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 37 | if (-not $DontWriteToEventLog) { 38 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental deletion flag for computer $($Computer.SamAccountName) failed." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 39 | } 40 | foreach ($W in $Warnings) { 41 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 42 | } 43 | } 44 | } else { 45 | Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental deletion. Deletion skipped.' -Color Yellow, Green, Yellow 46 | if (-not $DontWriteToEventLog) { 47 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Computer $($Computer.SamAccountName) is protected from accidental deletion. Deletion skipped." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete) -WarningAction SilentlyContinue -WarningVariable warnings 48 | } 49 | $Success = $false 50 | } 51 | } else { 52 | $Success = $true 53 | } 54 | if ($Success) { 55 | $Success = $false 56 | Write-Color -Text "[i] Deleting computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 57 | try { 58 | $Success = $true 59 | Remove-ADObject -Identity $Computer.DistinguishedName -Recursive -WhatIf:$WhatIfDelete -Server $Server -ErrorAction Stop -Confirm:$false 60 | Write-Color -Text "[+] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) successful." -Color Yellow, Green, Yellow 61 | if (-not $DontWriteToEventLog) { 62 | Write-Event -ID 12 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) successful." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete) -WarningAction SilentlyContinue -WarningVariable warnings 63 | } 64 | foreach ($W in $Warnings) { 65 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 66 | } 67 | } catch { 68 | $Success = $false 69 | Write-Color -Text "[-] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 70 | if (-not $DontWriteToEventLog) { 71 | Write-Event -ID 12 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Deleting computer $($Computer.SamAccountName) failed." -AdditionalFields @('Delete', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfDelete, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 72 | } 73 | foreach ($W in $Warnings) { 74 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 75 | } 76 | $Computer.ActionComment = $_.Exception.Message 77 | } 78 | } 79 | $Computer.ActionDate = $Today 80 | if ($WhatIfDelete.IsPresent) { 81 | $Computer.ActionStatus = 'WhatIf' 82 | } else { 83 | if ($Success) { 84 | # lets remove computer from $ProcessedComputers 85 | # but only if it's not WhatIf and only if it's successful 86 | $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain) 87 | $ProcessedComputers.Remove("$ComputerOnTheList") 88 | } 89 | $Computer.ActionStatus = $Success 90 | } 91 | # return computer to $ReportDeleted so we can see summary just in case 92 | $Computer 93 | $CountDeleteLimit++ 94 | if ($DeleteLimit) { 95 | if ($DeleteLimit -eq $CountDeleteLimit) { 96 | break topLoop # this breaks top loop 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /Private/Request-ADComputersDisable.ps1: -------------------------------------------------------------------------------- 1 | function Request-ADComputersDisable { 2 | [cmdletbinding(SupportsShouldProcess)] 3 | param( 4 | [nullable[bool]] $Delete, 5 | [nullable[bool]] $Move, 6 | [nullable[bool]] $DisableAndMove, 7 | [System.Collections.IDictionary] $Report, 8 | [switch] $WhatIfDisable, 9 | [switch] $DisableModifyDescription, 10 | [switch] $DisableModifyAdminDescription, 11 | [int] $DisableLimit, 12 | [switch] $ReportOnly, 13 | [DateTime] $Today, 14 | [switch] $DontWriteToEventLog, 15 | [Object] $DisableMoveTargetOrganizationalUnit, 16 | [switch] $DoNotAddToPendingList, 17 | [ValidateSet( 18 | 'DisableAndMove', 19 | 'MoveAndDisable' 20 | )][string] $DisableAndMoveOrder = 'DisableAndMove', 21 | [switch] $RemoveProtectedFromAccidentalDeletionFlag 22 | ) 23 | 24 | if ($DisableAndMove -and $DisableMoveTargetOrganizationalUnit) { 25 | if ($DisableMoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) { 26 | $OrganizationalUnit = $DisableMoveTargetOrganizationalUnit 27 | } elseif ($DisableMoveTargetOrganizationalUnit -is [string]) { 28 | $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $DisableMoveTargetOrganizationalUnit -ToDomainCN 29 | $OrganizationalUnit = [ordered] @{ 30 | $DomainCN = $DisableMoveTargetOrganizationalUnit 31 | } 32 | } else { 33 | Write-Color -Text "[-] DisableMoveTargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red 34 | return 35 | } 36 | } 37 | 38 | $CountDisable = 0 39 | # :top means name of the loop, so we can break it 40 | :topLoop foreach ($Domain in $Report.Keys) { 41 | Write-Color "[i] ", "Starting process of disabling computers for domain $Domain" -Color Yellow, Green 42 | foreach ($Computer in $Report["$Domain"]['Computers']) { 43 | $Server = $Report["$Domain"]['Server'] 44 | if ($Computer.Action -ne 'Disable') { 45 | continue 46 | } 47 | if ($ReportOnly) { 48 | $Computer 49 | } else { 50 | $Success = $true 51 | if ($DisableAndMoveOrder -eq 'DisableAndMove') { 52 | $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server 53 | $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server -RemoveProtectedFromAccidentalDeletionFlag:$RemoveProtectedFromAccidentalDeletionFlag.IsPresent 54 | } else { 55 | $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server -RemoveProtectedFromAccidentalDeletionFlag:$RemoveProtectedFromAccidentalDeletionFlag.IsPresent 56 | $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server 57 | } 58 | if ($Success) { 59 | if ($DisableModifyDescription -eq $true) { 60 | $DisableModifyDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))" 61 | try { 62 | Set-ADComputer -Identity $Computer.DistinguishedName -Description $DisableModifyDescriptionText -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server 63 | Write-Color -Text "[+] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyDescriptionText -Color Yellow, Green, Yellow, Green, Yellow 64 | } catch { 65 | $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message 66 | Write-Color -Text "[-] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 67 | } 68 | } 69 | if ($DisableModifyAdminDescription) { 70 | $DisableModifyAdminDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))" 71 | try { 72 | Set-ADObject -Identity $Computer.DistinguishedName -Replace @{ AdminDescription = $DisableModifyAdminDescriptionText } -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server 73 | Write-Color -Text "[+] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyAdminDescriptionText -Color Yellow, Green, Yellow, Green, Yellow 74 | } catch { 75 | $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message 76 | Write-Color -Text "[-] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 77 | } 78 | } 79 | } 80 | 81 | # this is to store actual disabling time - we can't trust WhenChanged date 82 | $Computer.ActionDate = $Today 83 | if ($WhatIfDisable.IsPresent) { 84 | $Computer.ActionStatus = 'WhatIf' 85 | } else { 86 | $Computer.ActionStatus = $Success 87 | } 88 | 89 | # We add computer to pending list in all cases because otherwise we would be going in circles 90 | # if move or delete were not enabled 91 | # please use -DoNotAddToPendingList if you don't want to add computer to pending list 92 | if (-not $DoNotAddToPendingList) { 93 | $FullComputerName = -join ($Computer.SamAccountName, '@', $Domain) 94 | # Lets add computer to pending list, and lets set time how long it's there so it can be easily visible in reports 95 | $Computer.TimeOnPendingList = 0 96 | $ProcessedComputers[$FullComputerName] = $Computer 97 | } 98 | 99 | # return computer to $ReportDisabled so we can see summary just in case 100 | $Computer 101 | $CountDisable++ 102 | if ($DisableLimit) { 103 | if ($DisableLimit -eq $CountDisable) { 104 | break topLoop # this breaks top loop 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /Private/Request-ADComputersMove.ps1: -------------------------------------------------------------------------------- 1 | function Request-ADComputersMove { 2 | [cmdletBinding(SupportsShouldProcess)] 3 | param( 4 | [nullable[bool]] $Delete, 5 | [System.Collections.IDictionary] $Report, 6 | [switch] $ReportOnly, 7 | [switch] $WhatIfMove, 8 | [int] $MoveLimit, 9 | [System.Collections.IDictionary] $ProcessedComputers, 10 | [DateTime] $Today, 11 | [Object] $TargetOrganizationalUnit, 12 | [switch] $DontWriteToEventLog, 13 | [switch] $DoNotAddToPendingList, 14 | [switch] $RemoveProtectedFromAccidentalDeletionFlag 15 | ) 16 | 17 | if ($TargetOrganizationalUnit -is [System.Collections.IDictionary]) { 18 | $OrganizationalUnit = $TargetOrganizationalUnit 19 | } elseif ($TargetOrganizationalUnit -is [string]) { 20 | $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $TargetOrganizationalUnit -ToDomainCN 21 | $OrganizationalUnit = [ordered] @{ 22 | $DomainCN = $TargetOrganizationalUnit 23 | } 24 | } else { 25 | Write-Color -Text "[-] TargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red 26 | return 27 | } 28 | $CountMoveLimit = 0 29 | # :top means name of the loop, so we can break it 30 | :topLoop foreach ($Domain in $Report.Keys) { 31 | foreach ($Computer in $Report["$Domain"]['Computers']) { 32 | $Server = $Report["$Domain"]['Server'] 33 | if ($Computer.Action -ne 'Move') { 34 | continue 35 | } 36 | if ($ReportOnly) { 37 | $Computer 38 | } else { 39 | if ($OrganizationalUnit[$Domain]) { 40 | # we check if the computer is already in the correct OU 41 | if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) { 42 | # this shouldn't really happen as we should have filtered it out earlier 43 | } else { 44 | if ($Computer.ProtectedFromAccidentalDeletion) { 45 | if ($RemoveProtectedFromAccidentalDeletionFlag) { 46 | try { 47 | Write-Color -Text "[i] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 48 | Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfMove 49 | if (-not $DontWriteToEventLog) { 50 | Write-Event -ID 15 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental move flag for computer $($Computer.SamAccountName) successful." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove) -WarningAction SilentlyContinue -WarningVariable warnings 51 | } 52 | $Success = $true 53 | } catch { 54 | $Success = $false 55 | Write-Color -Text "[-] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 56 | if (-not $DontWriteToEventLog) { 57 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Removing protected from accidental move flag for computer $($Computer.SamAccountName) failed." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 58 | } 59 | foreach ($W in $Warnings) { 60 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 61 | } 62 | } 63 | } else { 64 | Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental move. Move skipped.' -Color Yellow, Green, Yellow 65 | if (-not $DontWriteToEventLog) { 66 | Write-Event -ID 15 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Computer $($Computer.SamAccountName) is protected from accidental move. Move skipped." -AdditionalFields @('RemoveProtection', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove) -WarningAction SilentlyContinue -WarningVariable warnings 67 | } 68 | $Success = $false 69 | } 70 | } else { 71 | $Success = $true 72 | } 73 | if ($Success) { 74 | $Success = $false 75 | Write-Color -Text "[i] Moving computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' Enabled: ', $Computer.Enabled, ' Operating System: ', $Computer.OperatingSystem, ' LastLogon: ', $Computer.LastLogonDate, " / " , $Computer.LastLogonDays , ' days, PasswordLastSet: ', $Computer.PasswordLastSet, " / ", $Computer.PasswordLastChangedDays, " days" -Color Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green, Yellow, Green 76 | try { 77 | $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfMove -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru 78 | $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName 79 | Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) successful." -Color Yellow, Green, Yellow 80 | if (-not $DontWriteToEventLog) { 81 | Write-Event -ID 11 -LogName 'Application' -EntryType Warning -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) successful." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove) -WarningAction SilentlyContinue -WarningVariable warnings 82 | } 83 | foreach ($W in $Warnings) { 84 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 85 | } 86 | if (-not $Delete) { 87 | # lets remove computer from $ProcessedComputers 88 | # we only remove it if Delete is not part of the removal process and move is the last step 89 | if (-not $DoNotAddToPendingList) { 90 | $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain) 91 | $ProcessedComputers.Remove("$ComputerOnTheList") 92 | } 93 | } 94 | $Success = $true 95 | } catch { 96 | $Success = $false 97 | Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow 98 | if (-not $DontWriteToEventLog) { 99 | Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, $($_.Exception.Message)) -WarningAction SilentlyContinue -WarningVariable warnings 100 | } 101 | foreach ($W in $Warnings) { 102 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 103 | } 104 | $Computer.ActionComment = $_.Exception.Message 105 | } 106 | } 107 | $Computer.ActionDate = $Today 108 | if ($WhatIfMove.IsPresent) { 109 | $Computer.ActionStatus = 'WhatIf' 110 | } else { 111 | $Computer.ActionStatus = $Success 112 | } 113 | # return computer to $ReportMoved so we can see summary just in case 114 | $Computer 115 | $CountMoveLimit++ 116 | if ($MoveLimit) { 117 | if ($MoveLimit -eq $CountMoveLimit) { 118 | break topLoop # this breaks top loop 119 | } 120 | } 121 | } 122 | } else { 123 | Write-Color -Text "[-] Moving computer ", $Computer.SamAccountName, " failed. TargetOrganizationalUnit for domain $Domain not found." -Color Yellow, Red, Yellow 124 | if (-not $DontWriteToEventLog) { 125 | Write-Event -ID 11 -LogName 'Application' -EntryType Error -Category 1000 -Source 'CleanupComputers' -Message "Moving computer $($Computer.SamAccountName) failed." -AdditionalFields @('Move', $Computer.SamAccountName, $Computer.DistinguishedName, $Computer.Enabled, $Computer.OperatingSystem, $Computer.LastLogonDate, $Computer.PasswordLastSet, $WhatIfMove, "TargetOrganizationalUnit for domain $Domain not found.") -WarningAction SilentlyContinue -WarningVariable warnings 126 | } 127 | foreach ($W in $Warnings) { 128 | Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red 129 | } 130 | $Computer.ActionComment = "TargetOrganizationalUnit for domain $Domain not found." 131 | $Computer.ActionDate = $Today 132 | $Computer.ActionStatus = $false 133 | # return computer to $ReportMoved so we can see summary just in case 134 | $Computer 135 | $CountMoveLimit++ 136 | if ($MoveLimit) { 137 | if ($MoveLimit -eq $CountMoveLimit) { 138 | break topLoop # this breaks top loop 139 | } 140 | } 141 | } 142 | } 143 | } 144 | } 145 | } -------------------------------------------------------------------------------- /Private/Request-ADSIDHistory.ps1: -------------------------------------------------------------------------------- 1 | function Request-ADSIDHistory { 2 | [CmdletBinding()] 3 | param( 4 | [Array] $DomainNames, 5 | [System.Collections.IDictionary] $Output, 6 | [System.Collections.IDictionary] $Export, 7 | [System.Collections.IDictionary] $ForestInformation, 8 | [string[]] $IncludeOrganizationalUnit, 9 | [string[]] $ExcludeOrganizationalUnit, 10 | [string[]] $IncludeSIDHistoryDomain, 11 | [string[]] $ExcludeSIDHistoryDomain, 12 | [nullable[int]] $RemoveLimitObject, 13 | [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType, 14 | [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType, 15 | [switch] $DisabledOnly, 16 | [switch] $DontWriteToEventLog, 17 | [bool] $LimitPerObject, 18 | [bool] $LimitPerSID, 19 | [System.Collections.Generic.List[PSCustomObject]] $ObjectsToProcess 20 | ) 21 | $GlobalLimitObject = 0 22 | 23 | # Process each domain SID 24 | :TopLoop foreach ($Domain in $DomainNames) { 25 | [Array] $Objects = $Output[$Domain] 26 | 27 | # Skip if we've already hit our object limit 28 | if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) { 29 | Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping processing." -Color Yellow, White, Green, White 30 | break TopLoop 31 | } 32 | 33 | # Check if this domain SID matches our type filters 34 | $DomainInfo = $Output.DomainSIDs[$Domain] 35 | 36 | if (-not $DomainInfo) { 37 | $DomainType = "Unknown" 38 | } else { 39 | $DomainType = $DomainInfo.Type 40 | if ($DomainType -eq 'Domain') { 41 | $DomainType = "Internal" 42 | } elseif ($DomainType -eq 'Trust') { 43 | $DomainType = "External" 44 | } 45 | } 46 | 47 | # Apply type filters 48 | if ($ExcludeType.Count -gt 0 -and $ExcludeType -contains $DomainType) { 49 | Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is excluded." -Color Yellow, White, Red, White, Red, White 50 | continue 51 | } 52 | 53 | if ($IncludeType.Count -gt 0 -and $IncludeType -notcontains $DomainType) { 54 | Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is not included." -Color Yellow, White, Red, White, Red, White 55 | continue 56 | } 57 | 58 | # Apply SID domain filters 59 | if ($IncludeSIDHistoryDomain -and $IncludeSIDHistoryDomain -notcontains $Domain) { 60 | Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's not in the included SID history domains." -Color Yellow, White, Red, White 61 | continue 62 | } 63 | 64 | if ($ExcludeSIDHistoryDomain -and $ExcludeSIDHistoryDomain -contains $Domain) { 65 | Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's in the excluded SID history domains." -Color Yellow, White, Red, White 66 | continue 67 | } 68 | 69 | # Display which domain we're processing 70 | $DomainDisplayName = if ($DomainInfo) { $DomainInfo.Domain } else { "Unknown" } 71 | Write-Color -Text "[i] ", "Processing domain SID ", $Domain, " (", $DomainDisplayName, " - ", $DomainType, ")" -Color Yellow, White, Green, White, Green, White, Green 72 | 73 | # Process each object in this domain 74 | foreach ($Object in $Objects) { 75 | $QueryServer = $ForestInformation['QueryServers'][$Object.Domain].HostName[0] 76 | 77 | if ($DisabledOnly) { 78 | if ($Object.Enabled) { 79 | Write-Color -Text "[s] ", "Skipping ", $Object.Name, " as it is enabled and DisabledOnly filter is set." -Color Yellow, White, Red, White 80 | continue 81 | } 82 | } 83 | 84 | # Check if we need to filter by OU 85 | if ($IncludeOrganizationalUnit -and $IncludeOrganizationalUnit -notcontains $Object.OrganizationalUnit) { 86 | continue 87 | } 88 | 89 | if ($ExcludeOrganizationalUnit -and $ExcludeOrganizationalUnit -contains $Object.OrganizationalUnit) { 90 | continue 91 | } 92 | 93 | Write-Color -Text "[i] ", "Processing ", $Object.Name, " (", $Object.ObjectClass, " in ", $Object.Domain, ", SID History Count: ", $Object.SIDHistory.Count, ")" -Color Yellow, White, Green, White, Green, White, Green, White, Green 94 | 95 | # Add to our collection of objects to process 96 | $ObjectsToProcess.Add( 97 | [PSCustomObject]@{ 98 | Object = $Object 99 | QueryServer = $QueryServer 100 | Domain = $Domain 101 | DomainInfo = $DomainInfo 102 | DomainType = $DomainType 103 | } 104 | ) 105 | 106 | # Increment counter and check limits 107 | $GlobalLimitObject++ 108 | if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) { 109 | Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping object collection." -Color Yellow, White, Green, White 110 | break TopLoop 111 | } 112 | } 113 | } 114 | } -------------------------------------------------------------------------------- /Public/Invoke-ADSIDHistoryCleanup.ps1: -------------------------------------------------------------------------------- 1 | function Invoke-ADSIDHistoryCleanup { 2 | <# 3 | .SYNOPSIS 4 | Cleans up SID history entries in Active Directory based on various filtering criteria. 5 | 6 | .DESCRIPTION 7 | This function identifies and removes SID history entries from AD objects based on specified filters. 8 | It can target internal domains (same forest), external domains (trusted), or unknown domains. 9 | The function allows for detailed reporting before making any changes. 10 | 11 | .PARAMETER Forest 12 | The name of the forest to process. If not specified, uses the current forest. 13 | 14 | .PARAMETER IncludeDomains 15 | An array of domain names to include in the cleanup process. 16 | 17 | .PARAMETER ExcludeDomains 18 | An array of domain names to exclude from the cleanup process. 19 | 20 | .PARAMETER IncludeOrganizationalUnit 21 | An array of organizational units to include in the cleanup process. 22 | 23 | .PARAMETER ExcludeOrganizationalUnit 24 | An array of organizational units to exclude from the cleanup process. 25 | 26 | .PARAMETER IncludeSIDHistoryDomain 27 | An array of domain SIDs to include when cleaning up SID history. 28 | 29 | .PARAMETER ExcludeSIDHistoryDomain 30 | An array of domain SIDs to exclude when cleaning up SID history. 31 | 32 | .PARAMETER RemoveLimitSID 33 | Limits the total number of SID history entries to remove. 34 | 35 | .PARAMETER RemoveLimitObject 36 | Limits the total number of objects to process for SID history removal. Defaults to 1 to prevent accidental mass deletions. 37 | 38 | .PARAMETER IncludeType 39 | Specifies which types of SID history to include: 'Internal', 'External', or 'Unknown'. 40 | Defaults to all three types if not specified. 41 | 42 | .PARAMETER ExcludeType 43 | Specifies which types of SID history to exclude: 'Internal', 'External', or 'Unknown'. 44 | 45 | .PARAMETER DisabledOnly 46 | Only processes objects that are disabled. 47 | 48 | .PARAMETER SafetyADLimit 49 | Stops processing if the number of objects with SID history in AD is less than the specified limit. 50 | 51 | .PARAMETER LogPath 52 | The path to the log file to write. 53 | 54 | .PARAMETER LogMaximum 55 | The maximum number of log files to keep. 56 | 57 | .PARAMETER LogShowTime 58 | If specified, includes the time in the log entries. 59 | 60 | .PARAMETER LogTimeFormat 61 | The format to use for the time in the log entries. 62 | 63 | .PARAMETER Suppress 64 | Suppresses the output of the function and only returns the summary information. 65 | 66 | .PARAMETER ShowHTML 67 | If specified, shows the HTML report in the default browser. 68 | 69 | .PARAMETER Online 70 | If specified, uses online resources in HTML report (CSS/JS is loaded from CDN). Otherwise local resources are used (bigger HTML file). 71 | 72 | .PARAMETER DataStorePath 73 | Path to the XML file used to store processed SID history entries. 74 | 75 | .PARAMETER ReportOnly 76 | If specified, only generates a report without making any changes. 77 | 78 | .PARAMETER Report 79 | Generates a report of affected objects without making any changes. 80 | 81 | .PARAMETER ReportPath 82 | The path where the HTML report should be saved. Used with the -Report parameter. 83 | 84 | .PARAMETER WhatIf 85 | Shows what would happen if the function runs. The SID history entries aren't actually removed. 86 | 87 | .EXAMPLE 88 | Invoke-ADSIDHistoryCleanup -Forest "contoso.com" -IncludeType "External" -ReportOnly -ReportPath "C:\Temp\SIDHistoryReport.html" -WhatIf 89 | 90 | Generates a report of external SID history entries in the contoso.com forest without making any changes. 91 | 92 | .EXAMPLE 93 | Invoke-ADSIDHistoryCleanup -IncludeDomains "domain1.local" -IncludeType "Internal" -RemoveLimitSID 2 -WhatIf 94 | 95 | Removes up to 2 internal SID history entries from objects in domain1.local. 96 | 97 | .EXAMPLE 98 | Invoke-ADSIDHistoryCleanup -ExcludeSIDHistoryDomain "S-1-5-21-1234567890-1234567890-1234567890" -WhatIf -RemoveLimitObject 2 99 | 100 | Shows what SID history entries would be removed while excluding entries from the specified domain SID. Limits the number of objects to process to 2. 101 | 102 | .EXAMPLE 103 | # Prepare splat 104 | $invokeADSIDHistoryCleanupSplat = @{ 105 | Verbose = $true 106 | WhatIf = $true 107 | IncludeSIDHistoryDomain = @( 108 | 'S-1-5-21-3661168273-3802070955-2987026695' 109 | 'S-1-5-21-853615985-2870445339-3163598659' 110 | ) 111 | IncludeType = 'External' 112 | RemoveLimitSID = 1 113 | RemoveLimitObject = 2 114 | 115 | SafetyADLimit = 1 116 | ShowHTML = $true 117 | Online = $true 118 | DisabledOnly = $true 119 | #ReportOnly = $true 120 | LogPath = "C:\Temp\ProcessedSIDHistory.log" 121 | ReportPath = "$PSScriptRoot\ProcessedSIDHistory.html" 122 | DataStorePath = "$PSScriptRoot\ProcessedSIDHistory.xml" 123 | } 124 | 125 | # Run the script 126 | $Output = Invoke-ADSIDHistoryCleanup @invokeADSIDHistoryCleanupSplat 127 | $Output | Format-Table -AutoSize 128 | 129 | # Lets send an email 130 | $EmailBody = $Output.EmailBody 131 | 132 | Connect-MgGraph -Scopes 'Mail.Send' -NoWelcome 133 | Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated SID Cleanup Report" -Body $EmailBody -Priority Low -Verbose 134 | #> 135 | [CmdletBinding(SupportsShouldProcess)] 136 | param ( 137 | [string] $Forest, 138 | [alias('Domain')][string[]] $IncludeDomains, 139 | [string[]] $ExcludeDomains, 140 | [string[]] $IncludeOrganizationalUnit, 141 | [string[]] $ExcludeOrganizationalUnit, 142 | [string[]] $IncludeSIDHistoryDomain, 143 | [string[]] $ExcludeSIDHistoryDomain, 144 | [nullable[int]] $RemoveLimitSID, 145 | [nullable[int]] $RemoveLimitObject = 1, 146 | [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType = @('Internal', 'External', 'Unknown'), 147 | [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType = @(), 148 | [string] $ReportPath, 149 | [string] $DataStorePath, 150 | [switch] $ReportOnly, 151 | [string] $LogPath, 152 | [int] $LogMaximum = 5, 153 | [switch] $LogShowTime, 154 | [string] $LogTimeFormat, 155 | [switch] $Suppress, 156 | [switch] $ShowHTML, 157 | [switch] $Online, 158 | [switch] $DisabledOnly, 159 | [nullable[int]] $SafetyADLimit, 160 | [switch] $DontWriteToEventLog 161 | ) 162 | 163 | if (-not $DataStorePath) { 164 | $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.xml' 165 | } 166 | if (-not $ReportPath) { 167 | $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.html' 168 | } 169 | 170 | # lets enable global logging 171 | Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName 172 | 173 | $Export = [ordered] @{ 174 | Date = Get-Date 175 | Version = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster' 176 | ObjectsToProcess = $null 177 | CurrentRun = $null 178 | History = [System.Collections.Generic.List[PSCustomObject]]::new() 179 | } 180 | 181 | Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta 182 | Write-Color -Text "[i] ", "Started process of cleaning up SID History for AD Objects" -Color Yellow, White 183 | Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White 184 | 185 | $Export = Import-SIDHistory -DataStorePath $DataStorePath -Export $Export 186 | 187 | # Determine if we're using limits 188 | if ($Null -eq $RemoveLimitSID -and $null -eq $RemoveLimitObject) { 189 | $LimitPerObject = $false 190 | $LimitPerSID = $false 191 | } elseif ($Null -eq $RemoveLimitSID) { 192 | $LimitPerObject = $true 193 | $LimitPerSID = $false 194 | } elseif ($Null -eq $RemoveLimitObject) { 195 | $LimitPerObject = $false 196 | $LimitPerSID = $true 197 | } else { 198 | $LimitPerObject = $true 199 | $LimitPerSID = $true 200 | } 201 | 202 | $Configuration = [ordered] @{ 203 | Forest = $Forest 204 | IncludeDomains = $IncludeDomains 205 | ExcludeDomains = $ExcludeDomains 206 | IncludeOrganizationalUnit = $IncludeOrganizationalUnit 207 | ExcludeOrganizationalUnit = $ExcludeOrganizationalUnit 208 | IncludeSIDHistoryDomain = $IncludeSIDHistoryDomain 209 | ExcludeSIDHistoryDomain = $ExcludeSIDHistoryDomain 210 | RemoveLimitSID = $RemoveLimitSID 211 | RemoveLimitObject = $RemoveLimitObject 212 | IncludeType = $IncludeType 213 | ExcludeType = $ExcludeType 214 | LimitPerObject = $LimitPerObject 215 | LimitPerSID = $LimitPerSID 216 | SafetyADLimit = $SafetyADLimit 217 | DisabledOnly = $DisabledOnly 218 | LogPath = $LogPath 219 | LogMaximum = $LogMaximum 220 | LogShowTime = $LogShowTime 221 | LogTimeFormat = $LogTimeFormat 222 | DontWriteToEventLog = $DontWriteToEventLog 223 | } 224 | 225 | 226 | # Initialize collections to store objects for processing or reporting 227 | $ObjectsToProcess = [System.Collections.Generic.List[PSCustomObject]]::new() 228 | 229 | # Get forest details using existing function 230 | $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -PreferWritable 231 | 232 | # Get SID history information 233 | $Output = Get-WinADSIDHistory -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -All 234 | 235 | if ($SafetyADLimit -and $Output.All.Count -le $SafetyADLimit) { 236 | Write-Color -Text "[i] ", "Number of objects returned with SIDHistory in AD is less than SafetyADLimit ", $SafetyADLimit, ". Stopping processing." -Color Yellow, White, Green, White 237 | return 238 | } 239 | 240 | # Extract domain names from the output 241 | [Array] $DomainNames = foreach ($Key in $Output.Keys) { 242 | if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs', 'All')) { 243 | continue 244 | } 245 | $Key 246 | } 247 | 248 | $requestADSIDHistorySplat = @{ 249 | DomainNames = $DomainNames 250 | Output = $Output 251 | Export = $Export 252 | IncludeSIDHistoryDomain = $IncludeSIDHistoryDomain 253 | ExcludeSIDHistoryDomain = $ExcludeSIDHistoryDomain 254 | IncludeType = $IncludeType 255 | ExcludeType = $ExcludeType 256 | RemoveLimitObject = $RemoveLimitObject 257 | LimitPerObject = $LimitPerObject 258 | LimitPerSID = $LimitPerSID 259 | ObjectsToProcess = $ObjectsToProcess 260 | DisabledOnly = $DisabledOnly 261 | ForestInformation = $ForestInformation 262 | } 263 | 264 | Request-ADSIDHistory @requestADSIDHistorySplat 265 | 266 | if (-not $ReportOnly) { 267 | # Process the collected objects for SID removal 268 | Remove-ADSIDHistory -ObjectsToProcess $ObjectsToProcess -Export $Export -RemoveLimitSID $RemoveLimitSID 269 | } 270 | 271 | $Export['TotalObjectsFound'] = $ObjectsToProcess.Count 272 | $Export['TotalSIDsFound'] = ($ObjectsToProcess | ForEach-Object { $_.Object.SIDHistory.Count } | Measure-Object -Sum).Sum 273 | 274 | if (-not $ReportOnly) { 275 | try { 276 | $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop 277 | } catch { 278 | Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red 279 | } 280 | } 281 | $Export['ObjectsToProcess'] = $ObjectsToProcess 282 | 283 | New-HTMLProcessedSIDHistory -Export $Export -FilePath $ReportPath -Output $Output -ForestInformation $ForestInformation -Online:$Online.IsPresent -HideHTML:(-not $ShowHTML.IsPresent) -LogPath $LogPath -Configuration $Configuration 284 | 285 | # Return summary information 286 | if (-not $Suppress) { 287 | $Export.EmailBody = New-EmailBodySIDHistory -Export $Export 288 | $Export 289 | } 290 | } -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # CleanupMonster - PowerShell Module 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 |

15 | 16 |

17 | 18 | 19 | 20 | Discord 21 |

22 | 23 | `CleanupMonster` is a PowerShell module to that helps you clean up **Active Directory**. 24 | 25 | It has multiple functionalities currently & planned: 26 | - [x] Cleanup stale Computer objects from Active Directory 27 | - [x] Cleanup SID History from Active Directory 28 | - [ ] Cleanup stale User objects from Active Directory 29 | - [ ] Cleanup stale Group objects from Active Directory 30 | - [ ] Cleanup GMSA/MSA objects from Active Directory 31 | 32 | There are 2 blog posts that explain how to use the module: 33 | - [Mastering Active Directory Hygiene: Automating Stale Computer Cleanup with CleanupMonster](https://evotec.xyz/mastering-active-directory-hygiene-automating-stale-computer-cleanup-with-cleanupmonster/) 34 | - [Mastering Active Directory Hygiene: Automating SID History Cleanup with CleanupMonster](https://evotec.xyz/mastering-active-directory-hygiene-automating-sidhistory-cleanup-with-cleanupmonster/) 35 | 36 | The solution is really thought through and has many options to customize it to your needs. 37 | It's a complete solution for cleaning up Active Directory. 38 | **Please make sure to run this module with proper permissions or you may get wrong results.** 39 | By default Active Directory domain allows a standard user to read LastLogonDate and LastPasswordSet attributes. 40 | If you have changed those settings you may need to run the module with elevated permissions even for reporting needs. 41 | 42 | ## Support This Project 43 | 44 | If you find this project helpful, please consider supporting its development. 45 | Your sponsorship will help the maintainers dedicate more time to maintenance and new feature development for everyone. 46 | 47 | It takes a lot of time and effort to create and maintain this project. 48 | By becoming a sponsor, you can help ensure that it stays free and accessible to everyone who needs it. 49 | 50 | To become a sponsor, you can choose from the following options: 51 | 52 | - [Become a sponsor via GitHub Sponsors :heart:](https://github.com/sponsors/PrzemyslawKlys) 53 | - [Become a sponsor via PayPal :heart:](https://paypal.me/PrzemyslawKlys) 54 | 55 | Your sponsorship is completely optional and not required for using this project. 56 | We want this project to remain open-source and available for anyone to use for free, 57 | regardless of whether they choose to sponsor it or not. 58 | 59 | If you work for a company that uses our .NET libraries or PowerShell Modules, 60 | please consider asking your manager or marketing team if your company would be interested in supporting this project. 61 | Your company's support can help us continue to maintain and improve this project for the benefit of everyone. 62 | 63 | Thank you for considering supporting this project! 64 | 65 | # Installation 66 | 67 | ```powershell 68 | Install-Module -Name CleanupMonster -Force -Verbose 69 | ``` 70 | 71 | ## Cleaning Active Directory 72 | 73 | ### Cleaning Computers 74 | 75 | #### Report Only 76 | 77 | The first thing you should do is to run the module in a report only mode. 78 | It will show you how many computers are there to disable and delete. 79 | 80 | ```powershell 81 | $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML 82 | $Output 83 | ``` 84 | 85 | Keep in mind it works with default values such as 180 days for LastLogonDate and LastPasswordSet. 86 | You can change those values by using parameters. 87 | 88 | ![ReportOnlyMode](https://raw.githubusercontent.com/EvotecIT/CleanupMonster/master/Examples/Images/CleanupDevicesReport.png) 89 | 90 | #### Interactively 91 | 92 | This is a sample script that you can use to run the module interactively. 93 | It's good idea to run it interactively first to clean your AD and then run it in a scheduled task. 94 | 95 | ```powershell 96 | # this is a fresh run and it will try to disable computers according to it's defaults 97 | $Output = Invoke-ADComputersCleanup -Disable -WhatIfDisable -ShowHTML 98 | $Output 99 | ``` 100 | 101 | When you run cleanup the module will deliver HTML report on every run. 102 | It will show you: 103 | 104 | - Devices in Current Run (Actioned) 105 | 106 | ![CurrentRun](https://raw.githubusercontent.com/EvotecIT/CleanupMonster/master/Examples/Images/CleanupDevicesCurrentRun.png) 107 | 108 | - Devices in Previous Runs (History) 109 | 110 | ![PreviousRuns](https://raw.githubusercontent.com/EvotecIT/CleanupMonster/master/Examples/Images/CleanupDevicesHistory.png) 111 | 112 | - Devices on Pending List (Pending deletion) 113 | 114 | ![PendingList](https://raw.githubusercontent.com/EvotecIT/CleanupMonster/master/Examples/Images/CleanupDevicesPending.png) 115 | 116 | - All Devices (All) remaining 117 | 118 | ![AllRemaining](https://raw.githubusercontent.com/EvotecIT/CleanupMonster/master/Examples/Images/CleanupDevicesAllRemaining.png) 119 | 120 | Another example with log settings and custom report path 121 | 122 | ```powershell 123 | # this is a fresh run and it will try to delete computers according to it's defaults 124 | $Output = Invoke-ADComputersCleanup -Delete -WhatIfDelete -ShowHTML -LogPath $PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log -ReportPath $PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html 125 | $Output 126 | ``` 127 | 128 | #### Non-interactively (scheduled task) 129 | 130 | This is a sample script that you can use to run the module in a scheduled task. It's a good idea to run it as a scheduled task as it will log all the actions and you can easily review them. It's very advanced with many options and you can easily customize it to your needs. 131 | 132 | ```powershell 133 | # Run the script 134 | $Configuration = @{ 135 | Disable = $true 136 | DisableNoServicePrincipalName = $null 137 | DisableIsEnabled = $true 138 | DisableLastLogonDateMoreThan = 90 139 | DisablePasswordLastSetMoreThan = 90 140 | DisableExcludeSystems = @( 141 | # 'Windows Server*' 142 | ) 143 | DisableIncludeSystems = @() 144 | DisableLimit = 2 # 0 means unlimited, ignored for reports 145 | DisableModifyDescription = $false 146 | DisableModifyAdminDescription = $true 147 | 148 | Delete = $true 149 | DeleteIsEnabled = $false 150 | DeleteNoServicePrincipalName = $null 151 | DeleteLastLogonDateMoreThan = 180 152 | DeletePasswordLastSetMoreThan = 180 153 | DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list 154 | DeleteExcludeSystems = @( 155 | # 'Windows Server*' 156 | ) 157 | DeleteIncludeSystems = @( 158 | 159 | ) 160 | DeleteLimit = 2 # 0 means unlimited, ignored for reports 161 | 162 | Exclusions = @( 163 | '*OU=Domain Controllers*' 164 | '*OU=Servers,OU=Production*' 165 | 'EVOMONSTER$' 166 | 'EVOMONSTER.AD.EVOTEC.XYZ' 167 | ) 168 | 169 | Filter = '*' 170 | WhatIfDisable = $true 171 | WhatIfDelete = $true 172 | LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 173 | DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml" 174 | ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 175 | ShowHTML = $true 176 | } 177 | 178 | # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers' 179 | $Output = Invoke-ADComputersCleanup @Configuration 180 | $Output 181 | ``` 182 | 183 | #### Non-interactively (scheduled task) 184 | 185 | This is a sample script that you can use to run the module in a scheduled task. It's a good idea to run it as a scheduled task as it will log all the actions and you can easily review them. It's very advanced with many options and you can easily customize it to your needs. 186 | 187 | Thi example shows how to use AzureAD, Intune and Jamf to clean up computers in Active Directory where computer also needs needs to be non-existant in AzureAD, Intune and Jamf or have last seen date matches in AzureAD, Intune and Jamf. 188 | 189 | This example also moves computers to different OU's as part of the disable process. 190 | 191 | ```powershell 192 | # connect to graph for Azure AD, Intune (requires GraphEssentials module) 193 | Connect-MgGraph -Scopes Device.Read.All, DeviceManagementManagedDevices.Read.All, Directory.ReadWrite.All, DeviceManagementConfiguration.Read.All 194 | # connect to jamf (requires PowerJamf module) 195 | Connect-Jamf -Organization 'aaa' -UserName 'aaa' -Suppress -Force -PasswordEncrypted 'aaaaa' 196 | 197 | $invokeADComputersCleanupSplat = @{ 198 | # safety limits (minimum amount of computers that has to be returned from each source) 199 | SafetyADLimit = 30 200 | SafetyAzureADLimit = 5 201 | SafetyIntuneLimit = 3 202 | SafetyJamfLimit = 50 203 | # disable settings 204 | Disable = $true 205 | DisableLimit = 3 206 | DisableLastLogonDateMoreThan = 90 207 | DisablePasswordLastSetMoreThan = 90 208 | DisableLastSeenAzureMoreThan = 90 209 | DisableLastSyncAzureMoreThan = 90 210 | DisableLastContactJamfMoreThan = 90 211 | DisableLastSeenIntuneMoreThan = 90 212 | DisableAndMove = $true 213 | DisableMoveTargetOrganizationalUnit = @{ 214 | 'ad.evotec.xyz' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz' 215 | 'ad.evotec.pl' = 'OU=Disabled,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=pl' 216 | } 217 | # delete settings 218 | Delete = $true 219 | DeleteLimit = 3 220 | DeleteLastLogonDateMoreThan = 180 221 | DeletePasswordLastSetMoreThan = 180 222 | DeleteLastSeenAzureMoreThan = 180 223 | DeleteLastSyncAzureMoreThan = 180 224 | DeleteLastContactJamfMoreThan = 180 225 | DeleteLastSeenIntuneMoreThan = 180 226 | DeleteListProcessedMoreThan = 90 # disabled computer has to spend 90 days in list before it can be deleted 227 | DeleteIsEnabled = $false # Computer has to be disabled to be deleted 228 | # global exclusions 229 | Exclusions = @( 230 | '*OU=Domain Controllers*' # exclude Domain Controllers 231 | ) 232 | # filter for AD search 233 | Filter = '*' 234 | # logs, reports and datastores 235 | LogPath = "$PSScriptRoot\Logs\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" 236 | DataStorePath = "$PSScriptRoot\CleanupComputers_ListProcessed.xml" 237 | ReportPath = "$PSScriptRoot\Reports\CleanupComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" 238 | # WhatIf settings 239 | #ReportOnly = $true 240 | WhatIfDisable = $true 241 | WhatIfDelete = $true 242 | ShowHTML = $true 243 | } 244 | 245 | $Output = Invoke-ADComputersCleanup @invokeADComputersCleanupSplat 246 | $Output 247 | ``` 248 | 249 | ### Cleaning SID History 250 | 251 | #### Report Only 252 | 253 | The first thing you should do is to run the module in a report only mode. 254 | It will show you how many SID History entries are there to remove. 255 | 256 | ```powershell 257 | $Output = Invoke-ADSIDHistoryCleanup -WhatIf -ReportOnly 258 | ``` 259 | 260 | #### 261 | Cleanup of specific SID History entries along with report with email 262 | 263 | ```powershell 264 | $invokeADSIDHistoryCleanupSplat = @{ 265 | Verbose = $true 266 | WhatIf = $true 267 | IncludeSIDHistoryDomain = @( 268 | 'S-1-5-21-3661168273-3802070955-2987026695' 269 | 'S-1-5-21-853615985-2870445339-3163598659' 270 | ) 271 | #IncludeType = 'External' 272 | RemoveLimitSID = 2 273 | RemoveLimitObject = 2 274 | SafetyADLimit = 1 275 | ShowHTML = $true 276 | Online = $true 277 | DisabledOnly = $false 278 | LogPath = "$PSScriptROot\ProcessedSIDHistory.log" 279 | ReportPath = "$PSScriptRoot\ProcessedSIDHistory.html" 280 | DataStorePath = "$PSScriptRoot\ProcessedSIDHistory.xml" 281 | } 282 | 283 | # Run the script 284 | $Output = Invoke-ADSIDHistoryCleanup @invokeADSIDHistoryCleanupSplat 285 | $Output | Format-Table -AutoSize 286 | 287 | # Lets send an email 288 | $EmailBody = $Output.EmailBody 289 | 290 | # Send an email with the report 291 | Connect-MgGraph -Scopes 'Mail.Send' -NoWelcome 292 | Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated SID Cleanup Report" -Body $EmailBody -Priority Low -Verbose 293 | ``` --------------------------------------------------------------------------------