CleanupMonster.psm1
function ConvertFrom-DistinguishedName { <# .SYNOPSIS Converts a Distinguished Name to CN, OU, Multiple OUs or DC .DESCRIPTION Converts a Distinguished Name to CN, OU, Multiple OUs or DC .PARAMETER DistinguishedName Distinguished Name to convert .PARAMETER ToOrganizationalUnit Converts DistinguishedName to Organizational Unit .PARAMETER ToDC Converts DistinguishedName to DC .PARAMETER ToDomainCN Converts DistinguishedName to Domain Canonical Name (CN) .PARAMETER ToCanonicalName Converts DistinguishedName to Canonical Name .PARAMETER ToFQDN Converts DistinguishedName to Fully Qualified Domain Name (FQDN) This will only work for very specific cases, and will not really convert all Distinguished Names to FQDN .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName Output: Przemyslaw Klys .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent Output: OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit Output: OU=Production,DC=ad,DC=evotec,DC=xyz .EXAMPLE $Con = @( 'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz' 'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz' 'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz' 'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl' 'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz' ) ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName Output: Windows Authorization Access Group Mmm e6d5fd00-385d-4e65-b02d-9da3493ed850 Domain Controllers Microsoft Exchange Security Groups .EXAMPLEE ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName Output: ad.evotec.xyz ad.evotec.xyz\Production\Users ad.evotec.xyz\Production\Users\test .NOTES General notes #> [CmdletBinding(DefaultParameterSetName = 'Default')] param( [Parameter(ParameterSetName = 'ToOrganizationalUnit')] [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')] [Parameter(ParameterSetName = 'ToDC')] [Parameter(ParameterSetName = 'ToDomainCN')] [Parameter(ParameterSetName = 'Default')] [Parameter(ParameterSetName = 'ToLastName')] [Parameter(ParameterSetName = 'ToCanonicalName')] [Parameter(ParameterSetName = 'ToFQDN')] [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName, [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit, [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent, [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC, [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN, [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName, [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName, [Parameter(ParameterSetName = 'ToFQDN')][switch] $ToFQDN ) Process { foreach ($Distinguished in $DistinguishedName) { if ($ToDomainCN) { $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' $CN = $DN -replace ',DC=', '.' -replace "DC=" if ($CN) { $CN } } elseif ($ToOrganizationalUnit) { if ($Distinguished -match '^CN=[^,\\]+(?:\\,[^,\\]+)*,(.+)$') { $matches[1] } elseif ($Distinguished -match '^(OU=|CN=)') { $Distinguished } } elseif ($ToMultipleOrganizationalUnit) { $Parts = $Distinguished -split '(?<!\\),' $Results = [System.Collections.ArrayList]::new() if ($IncludeParent) { $null = $Results.Add($Distinguished) } for ($i = 1; $i -lt $Parts.Count; $i++) { $CurrentPath = $Parts[$i..($Parts.Count - 1)] -join ',' if ($CurrentPath -match '^(OU=|CN=)' -and $CurrentPath -notmatch '^DC=') { $null = $Results.Add($CurrentPath) } } foreach ($R in $Results) { if ($R -match '^(OU=|CN=)') { $R } } } elseif ($ToDC) { $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1' if ($Value) { $Value } } elseif ($ToLastName) { $NewDN = $Distinguished -split ",DC=" if ($NewDN[0].Contains(",OU=")) { [Array] $ChangedDN = $NewDN[0] -split ",OU=" } elseif ($NewDN[0].Contains(",CN=")) { [Array] $ChangedDN = $NewDN[0] -split ",CN=" } else { [Array] $ChangedDN = $NewDN[0] } if ($ChangedDN[0].StartsWith('CN=')) { $ChangedDN[0] -replace 'CN=', '' } else { $ChangedDN[0] -replace 'OU=', '' } } elseif ($ToCanonicalName) { $Domain = $null $Rest = $null foreach ($O in $Distinguished -split '(?<!\\),') { if ($O -match '^DC=') { $Domain += $O.Substring(3) + '.' } else { $Rest = $O.Substring(3) + '\' + $Rest } } if ($Domain -and $Rest) { $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',') } elseif ($Domain) { $Domain.Trim('.') } elseif ($Rest) { $Rest.TrimEnd('\') -replace '\\,', ',' } } elseif ($ToFQDN) { if ($Distinguished -match '^CN=(.+?),(?:(?:OU|CN).+,)*((?:DC=.+,?)+)$') { $cnPart = $matches[1] -replace '\\,', ',' $dcPart = $matches[2] -replace 'DC=', '' -replace ',', '.' "$cnPart.$dcPart" } elseif ($Distinguished -match '^CN=(.+?),((?:DC=.+,?)+)$') { $cnPart = $matches[1] -replace '\\,', ',' $dcPart = $matches[2] -replace 'DC=', '' -replace ',', '.' "$cnPart.$dcPart" } } else { $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$' $Found = $Distinguished -match $Regex if ($Found) { $Matches.cn } } } } } function ConvertTo-OperatingSystem { <# .SYNOPSIS Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .DESCRIPTION Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD .PARAMETER OperatingSystem Operating System as returned by Active Directory .PARAMETER OperatingSystemVersion Operating System Version as returned by Active Directory .EXAMPLE $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object { $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force $_ } $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table .EXAMPLE $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion' ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber .NOTES General notes #> [CmdletBinding()] param( [string] $OperatingSystem, [string] $OperatingSystemVersion ) if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') { $Systems = @{ '10.0 (22621)' = 'Windows 11 22H2' '10.0 (22000)' = 'Windows 11 21H2' '10.0 (19045)' = 'Windows 10 22H2' '10.0 (19044)' = 'Windows 10 21H2' '10.0 (19043)' = 'Windows 10 21H1' '10.0 (19042)' = 'Windows 10 20H2' '10.0 (19041)' = 'Windows 10 2004' '10.0 (18898)' = 'Windows 10 Insider Preview' '10.0 (18363)' = "Windows 10 1909" '10.0 (18362)' = "Windows 10 1903" '10.0 (17763)' = "Windows 10 1809" '10.0 (17134)' = "Windows 10 1803" '10.0 (16299)' = "Windows 10 1709" '10.0 (15063)' = "Windows 10 1703" '10.0 (14393)' = "Windows 10 1607" '10.0 (10586)' = "Windows 10 1511" '10.0 (10240)' = "Windows 10 1507" '10.0.22621' = 'Windows 11 22H2' '10.0.22000' = 'Windows 11 21H2' '10.0.19045' = 'Windows 10 22H2' '10.0.19044' = 'Windows 10 21H2' '10.0.19043' = 'Windows 10 21H1' '10.0.19042' = 'Windows 10 20H2' '10.0.19041' = 'Windows 10 2004' '10.0.18898' = 'Windows 10 Insider Preview' '10.0.18363' = "Windows 10 1909" '10.0.18362' = "Windows 10 1903" '10.0.17763' = "Windows 10 1809" '10.0.17134' = "Windows 10 1803" '10.0.16299' = "Windows 10 1709" '10.0.15063' = "Windows 10 1703" '10.0.14393' = "Windows 10 1607" '10.0.10586' = "Windows 10 1511" '10.0.10240' = "Windows 10 1507" '22621' = 'Windows 11 22H2' '22000' = 'Windows 11 21H2' '19045' = 'Windows 10 22H2' '19044' = 'Windows 10 21H2' '19043' = 'Windows 10 21H1' '19042' = 'Windows 10 20H2' '19041' = 'Windows 10 2004' '18898' = 'Windows 10 Insider Preview' '18363' = "Windows 10 1909" '18362' = "Windows 10 1903" '17763' = "Windows 10 1809" '17134' = "Windows 10 1803" '16299' = "Windows 10 1709" '15063' = "Windows 10 1703" '14393' = "Windows 10 1607" '10586' = "Windows 10 1511" '10240' = "Windows 10 1507" } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystemVersion } } elseif ($OperatingSystem -like 'Windows Server*') { $Systems = @{ '10.0 (20348)' = 'Windows Server 2022' '10.0 (19042)' = 'Windows Server 2019 20H2' '10.0 (19041)' = 'Windows Server 2019 2004' '10.0 (18363)' = 'Windows Server 2019 1909' '10.0 (18362)' = "Windows Server 2019 1903" '10.0 (17763)' = "Windows Server 2019 1809" '10.0 (17134)' = "Windows Server 2016 1803" '10.0 (14393)' = "Windows Server 2016 1607" '6.3 (9600)' = 'Windows Server 2012 R2' '6.1 (7601)' = 'Windows Server 2008 R2' '5.2 (3790)' = 'Windows Server 2003' '10.0.20348' = 'Windows Server 2022' '10.0.19042' = 'Windows Server 2019 20H2' '10.0.19041' = 'Windows Server 2019 2004' '10.0.18363' = 'Windows Server 2019 1909' '10.0.18362' = "Windows Server 2019 1903" '10.0.17763' = "Windows Server 2019 1809" '10.0.17134' = "Windows Server 2016 1803" '10.0.14393' = "Windows Server 2016 1607" '6.3.9600' = 'Windows Server 2012 R2' '6.1.7601' = 'Windows Server 2008 R2' '5.2.3790' = 'Windows Server 2003' '20348' = 'Windows Server 2022' '19042' = 'Windows Server 2019 20H2' '19041' = 'Windows Server 2019 2004' '18363' = 'Windows Server 2019 1909' '18362' = "Windows Server 2019 1903" '17763' = "Windows Server 2019 1809" '17134' = "Windows Server 2016 1803" '14393' = "Windows Server 2016 1607" '9600' = 'Windows Server 2012 R2' '7601' = 'Windows Server 2008 R2' '3790' = 'Windows Server 2003' } $System = $Systems[$OperatingSystemVersion] if (-not $System) { $System = $OperatingSystemVersion } } else { $System = $OperatingSystem } if ($System) { $System } else { 'Unknown' } } function Get-GitHubVersion { <# .SYNOPSIS Get the latest version of a GitHub repository and compare with local version .DESCRIPTION Get the latest version of a GitHub repository and compare with local version .PARAMETER Cmdlet Cmdlet to find module for .PARAMETER RepositoryOwner Repository owner .PARAMETER RepositoryName Repository name .EXAMPLE Get-GitHubVersion -Cmdlet 'Start-DelegationModel' -RepositoryOwner 'evotecit' -RepositoryName 'DelegationModel' .NOTES General notes #> [cmdletBinding()] param( [Parameter(Mandatory)][string] $Cmdlet, [Parameter(Mandatory)][string] $RepositoryOwner, [Parameter(Mandatory)][string] $RepositoryName ) $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue if ($App) { [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false) $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($App.Version -eq $LatestVersion.Version) { "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($App.Version -lt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($App.Version -gt $LatestVersion.Version) { "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { "Current: $($App.Version)" } } else { "Current: Unknown" } } function Get-WinADForestDetails { <# .SYNOPSIS Get details about Active Directory Forest, Domains and Domain Controllers in a single query .DESCRIPTION Get details about Active Directory Forest, Domains and Domain Controllers in a single query .PARAMETER Forest Target different Forest, by default current forest is used .PARAMETER ExcludeDomains Exclude domain from search, by default whole forest is scanned .PARAMETER IncludeDomains Include only specific domains, by default whole forest is scanned .PARAMETER ExcludeDomainControllers Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. .PARAMETER IncludeDomainControllers Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored. .PARAMETER SkipRODC Skip Read-Only Domain Controllers. By default all domain controllers are included. .PARAMETER ExtendedForestInformation Ability to provide Forest Information from another command to speed up processing .PARAMETER Filter Filter for Get-ADDomainController .PARAMETER TestAvailability Check if Domain Controllers are available .PARAMETER Test Pick what to check for availability. Options are: All, Ping, WinRM, PortOpen, Ping+WinRM, Ping+PortOpen, WinRM+PortOpen. Default is All .PARAMETER Ports Ports to check for availability. Default is 135 .PARAMETER PortsTimeout Ports timeout for availability check. Default is 100 .PARAMETER PingCount How many pings to send. Default is 1 .PARAMETER PreferWritable Prefer writable domain controllers over read-only ones when returning Query Servers .PARAMETER Extended Return extended information about domains with NETBIOS names .EXAMPLE Get-WinADForestDetails | Format-Table .EXAMPLE Get-WinADForestDetails -Forest 'ad.evotec.xyz' | Format-Table .NOTES General notes #> [CmdletBinding()] param( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers, [switch] $SkipRODC, [string] $Filter = '*', [switch] $TestAvailability, [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All', [int[]] $Ports = 135, [int] $PortsTimeout = 100, [int] $PingCount = 1, [switch] $PreferWritable, [switch] $Extended, [System.Collections.IDictionary] $ExtendedForestInformation ) if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } if (-not $ExtendedForestInformation) { $Findings = [ordered] @{ } try { if ($Forest) { $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest } else { $ForestInformation = Get-ADForest -ErrorAction Stop } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)" return } if (-not $ForestInformation) { return } $Findings['Forest'] = $ForestInformation $Findings['ForestDomainControllers'] = @() $Findings['QueryServers'] = @{ } $Findings['DomainDomainControllers'] = @{ } [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) { if ($IncludeDomains) { if ($Domain -in $IncludeDomains) { $Domain.ToLower() } continue } if ($Domain -notin $ExcludeDomains) { $Domain.ToLower() } } [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) { try { $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent $OrderedDC = [ordered] @{ Domain = $DC.Domain Forest = $DC.Forest HostName = [Array] $DC.HostName IPv4Address = $DC.IPv4Address IPv6Address = $DC.IPv6Address Name = $DC.Name Site = $DC.Site } } catch { Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)" continue } if ($Domain -eq $Findings['Forest']['Name']) { $Findings['QueryServers']['Forest'] = $OrderedDC } $Findings['QueryServers']["$Domain"] = $OrderedDC $Domain } [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) { if ($Domain -notin $DomainsActive) { Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping." continue } $Domain } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0] [Array] $AllDC = try { try { $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop } catch { Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)" continue } foreach ($S in $DomainControllers) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $DSAGuid = (Get-ADObject -Identity $S.NTDSSettingsObjectDN -Server $QueryServer).ObjectGUID $Server = [ordered] @{ Domain = $Domain HostName = $S.HostName Name = $S.Name Forest = $ForestInformation.RootDomain Site = $S.Site IPV4Address = $S.IPV4Address IPV6Address = $S.IPV6Address IsGlobalCatalog = $S.IsGlobalCatalog IsReadOnly = $S.IsReadOnly IsSchemaMaster = ($S.OperationMasterRoles -contains 'SchemaMaster') IsDomainNamingMaster = ($S.OperationMasterRoles -contains 'DomainNamingMaster') IsPDC = ($S.OperationMasterRoles -contains 'PDCEmulator') IsRIDMaster = ($S.OperationMasterRoles -contains 'RIDMaster') IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster') OperatingSystem = $S.OperatingSystem OperatingSystemVersion = $S.OperatingSystemVersion OperatingSystemLong = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion LdapPort = $S.LdapPort SslPort = $S.SslPort DistinguishedName = $S.ComputerObjectDN NTDSSettingsObjectDN = $S.NTDSSettingsObjectDN DsaGuid = $DSAGuid DsaGuidName = "$DSAGuid._msdcs.$($ForestInformation.RootDomain)" Pingable = $null WinRM = $null PortOpen = $null Comment = '' } if ($TestAvailability) { if ($Test -eq 'All' -or $Test -like 'Ping*') { $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount } if ($Test -eq 'All' -or $Test -like '*WinRM*') { $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status } if ($Test -eq 'All' -or '*PortOpen*') { $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status } } [PSCustomObject] $Server } } catch { [PSCustomObject]@{ Domain = $Domain HostName = '' Name = '' Forest = $ForestInformation.RootDomain IPV4Address = '' IPV6Address = '' IsGlobalCatalog = '' IsReadOnly = '' Site = '' SchemaMaster = $false DomainNamingMasterMaster = $false PDCEmulator = $false RIDMaster = $false InfrastructureMaster = $false LdapPort = '' SslPort = '' DistinguishedName = '' NTDSSettingsObjectDN = '' DsaGuid = '' DsaGuidName = '' Pingable = $null WinRM = $null PortOpen = $null Comment = $_.Exception.Message -replace "`n", " " -replace "`r", " " } } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } if ($null -ne $Findings['DomainDomainControllers'][$Domain]) { [Array] $Findings['DomainDomainControllers'][$Domain] } } if ($Extended) { $Findings['DomainsExtended'] = @{ } $Findings['DomainsExtendedNetBIOS'] = @{ } foreach ($DomainEx in $Findings['Domains']) { try { $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object { [ordered] @{ AllowedDNSSuffixes = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ } ChildDomains = $_.ChildDomains | ForEach-Object -Process { $_ } ComputersContainer = $_.ComputersContainer DeletedObjectsContainer = $_.DeletedObjectsContainer DistinguishedName = $_.DistinguishedName DNSRoot = $_.DNSRoot DomainControllersContainer = $_.DomainControllersContainer DomainMode = $_.DomainMode DomainSID = $_.DomainSID.Value ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer Forest = $_.Forest InfrastructureMaster = $_.InfrastructureMaster LastLogonReplicationInterval = $_.LastLogonReplicationInterval LinkedGroupPolicyObjects = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ } LostAndFoundContainer = $_.LostAndFoundContainer ManagedBy = $_.ManagedBy Name = $_.Name NetBIOSName = $_.NetBIOSName ObjectClass = $_.ObjectClass ObjectGUID = $_.ObjectGUID ParentDomain = $_.ParentDomain PDCEmulator = $_.PDCEmulator PublicKeyRequiredPasswordRolling = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ } QuotasContainer = $_.QuotasContainer ReadOnlyReplicaDirectoryServers = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ } ReplicaDirectoryServers = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ } RIDMaster = $_.RIDMaster SubordinateReferences = $_.SubordinateReferences | ForEach-Object -Process { $_ } SystemsContainer = $_.SystemsContainer UsersContainer = $_.UsersContainer } } $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName'] $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx] } catch { Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)" continue } } } if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } $Findings } else { $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) { if ($IncludeDomains) { if ($_ -in $IncludeDomains) { $_.ToLower() } continue } if ($_ -notin $ExcludeDomains) { $_.ToLower() } } foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainDomainControllers.Remove($_) } } foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) { if ($_ -notin $Findings.Domains) { $Findings.DomainsExtended.Remove($_) $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName' if ($NetBiosName) { $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName) } } } [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) { [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) { if ($IncludeDomainControllers.Count -gt 0) { If (-not $IncludeDomainControllers[0].Contains('.')) { if ($S.Name -notin $IncludeDomainControllers) { continue } } else { if ($S.HostName -notin $IncludeDomainControllers) { continue } } } if ($ExcludeDomainControllers.Count -gt 0) { If (-not $ExcludeDomainControllers[0].Contains('.')) { if ($S.Name -in $ExcludeDomainControllers) { continue } } else { if ($S.HostName -in $ExcludeDomainControllers) { continue } } } $S } if ($SkipRODC) { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false } } else { [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC } [Array] $Findings['DomainDomainControllers'][$Domain] } $Findings } } function Get-WinADSIDHistory { <# .SYNOPSIS Retrieves SID History information for objects in Active Directory forest. .DESCRIPTION This function collects and analyzes SID History information for all objects in the Active Directory forest. It provides detailed information about internal and external SID history values, including statistics about users, groups, and computers that have SID history attributes. .PARAMETER Forest The name of the Active Directory forest to analyze. If not specified, uses the current forest. .PARAMETER ExcludeDomains An array of domain names to exclude from the analysis. .PARAMETER IncludeDomains An array of domain names to include in the analysis. Also aliased as 'Domain' or 'Domains'. .PARAMETER ExtendedForestInformation A hashtable containing extended forest information. Usually provided by Get-WinADForestDetails. .PARAMETER All Switch to return all information including domain SIDs and statistics. If not specified, returns only object information. .EXAMPLE Get-WinADSIDHistory -Forest "contoso.com" Returns a list of all objects with SID history in the specified forest. .EXAMPLE Get-WinADSIDHistory -IncludeDomains "domain1.local","domain2.local" -All Returns detailed SID history information including statistics for specified domains. .EXAMPLE Get-WinADSIDHistory -ExcludeDomains "legacy.local" -All Returns detailed SID history information for all domains except the specified excluded domain. .NOTES The function returns: - Object details (Name, Domain, Enabled status, etc.) - SID History count and values - Internal vs External vs Unknown SID information - Domain translation for SID values - Statistics about object types and status #> [CmdletBinding()] param ( [alias('ForestName')][string] $Forest, [string[]] $ExcludeDomains, [alias('Domain', 'Domains')][string[]] $IncludeDomains, [System.Collections.IDictionary] $ExtendedForestInformation, [switch] $All ) $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-ADEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials' # Lets get all information about the forest $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation -Extended # Lets create an output object $Output = [ordered] @{ 'All' = [System.Collections.Generic.List[PSCustomObject]]::new() 'DomainSIDs' = $null 'Statistics' = [ordered] @{ 'TotalObjects' = 0 'TotalUsers' = 0 'TotalGroups' = 0 'TotalComputers' = 0 'EnabledObjects' = 0 'DisabledObjects' = 0 'InternalSIDs' = 0 'ExternalSIDs' = 0 'UnknownSIDs' = 0 } 'Trusts' = [System.Collections.Generic.List[PSCustomObject]]::new() 'DuplicateSIDs' = [System.Collections.Generic.List[PSCustomObject]]::new() } # Lets find out all trusts in the forest $Output['Trusts'] = Get-WinADTrust -Forest $Forest -Recursive -SkipValidation # Lets find out all SIDs that are in the forest and in trusts $DomainSIDs = [ordered]@{} $ForestDomainSIDs = [ordered]@{} $TrustDomainSIDs = [ordered]@{} # Add forest domains foreach ($Domain in $ForestInformation.DomainsExtended.Keys) { $SID = $ForestInformation.DomainsExtended[$Domain].DomainSID $DomainSIDs[$SID] = [PSCustomObject] @{ Domain = $Domain Type = 'Domain' SID = $SID } $ForestDomainSIDs[$SID] = $Domain } # Add trusted domains foreach ($Trust in $Output['Trusts']) { if ($Trust.TrustTarget -in $ForestInformation.DomainsExtended.Keys) { continue } $SID = $Trust.DomainSID $DomainSIDs[$SID] = [PSCustomObject] @{ Domain = $Trust.TrustTarget Type = 'Trust' SID = $SID } $TrustDomainSIDs[$SID] = $Trust.TrustTarget } # Lets get all objects with SIDHistory $AllUsers = foreach ($Domain in $ForestInformation.Domains) { $QueryServer = $ForestInformation['QueryServers'][$Domain].HostName[0] $getADObjectSplat = @{ LDAPFilter = "(sidHistory=*)" Properties = 'sidHistory', 'userPrincipalName', 'WhenCreated', 'WhenChanged', 'userAccountControl', 'mail', 'sAMAccountName', 'lastLogonTimestamp', 'objectClass', 'distinguishedName', 'name', 'pwdLastSet' Server = $QueryServer } $Objects = Get-ADObject @getADObjectSplat foreach ($Object in $Objects) { $SidDomains = [System.Collections.Generic.List[string]]::new() $SidHistoryValues = [System.Collections.Generic.List[string]]::new() $SidHistoryDomainsTranslated = [System.Collections.Generic.List[string]]::new() $SidHistoryInternal = [System.Collections.Generic.List[string]]::new() $SIDHistoryExternal = [System.Collections.Generic.List[string]]::new() $SIDHistoryUnknown = [System.Collections.Generic.List[string]]::new() foreach ($Sid in $Object.sidHistory) { $SidHistoryValues.Add($Sid.Value) $DomainSID = $Sid.AccountDomainSid.Value # Check if this is from an internal forest domain if ($ForestDomainSIDs.Contains($DomainSID)) { $SIDHistoryInternal.Add($Sid.Value) $Output['Statistics']['InternalSIDs']++ } # Check if this is from a known trust elseif ($TrustDomainSIDs.Contains($DomainSID)) { $SIDHistoryExternal.Add($Sid.Value) $Output['Statistics']['ExternalSIDs']++ } # Otherwise it's unknown else { $SIDHistoryUnknown.Add($Sid.Value) $Output['Statistics']['UnknownSIDs']++ } if (-not $SidDomains.Contains($DomainSID)) { $SidDomains.Add($DomainSID) } $DomainInternal = $DomainSIDs[$DomainSID].Domain if ($DomainInternal) { if (-not $SidHistoryDomainsTranslated.Contains($DomainInternal)) { $SidHistoryDomainsTranslated.Add($DomainInternal) } } else { if (-not $SidHistoryDomainsTranslated.Contains($DomainSID)) { $SidHistoryDomainsTranslated.Add($DomainSID) } } } $UAC = Convert-UserAccountControl -UserAccountControl $Object.UserAccountControl $LastLogonTime = if ($Object.lastLogonTimestamp) { [datetime]::FromFileTime($Object.lastLogonTimestamp) } else { $null } $LastLogonTimeDays = if ($LastLogonTime) { [math]::Round((New-TimeSpan -Start $LastLogonTime -End (Get-Date)).TotalDays, 0) } else { $null } $PasswordLastSet = if ($Object.pwdLastSet) { [datetime]::FromFileTime($Object.pwdLastSet) } else { $null } $PasswordLastSetDays = if ($PasswordLastSet) { [math]::Round((New-TimeSpan -Start $PasswordLastSet -End (Get-Date)).TotalDays, 0) } else { $null } $O = [PSCustomObject] @{ Name = $Object.Name UserPrincipalName = $Object.UserPrincipalName Domain = $Domain SamAccountName = $Object.sAMAccountName ObjectClass = $Object.ObjectClass Enabled = if ($UAC -contains 'ACCOUNTDISABLE') { $false } else { $true } PasswordDays = $PasswordLastSetDays LogonDays = $LastLogonTimeDays Count = $SidHistoryValues.Count SIDHistory = $SidHistoryValues Domains = $SidDomains DomainsExpanded = $SidHistoryDomainsTranslated Internal = $SIDHistoryInternal InternalCount = $SIDHistoryInternal.Count External = $SIDHistoryExternal ExternalCount = $SIDHistoryExternal.Count Unknown = $SIDHistoryUnknown UnknownCount = $SIDHistoryUnknown.Count WhenCreated = $Object.WhenCreated WhenChanged = $Object.WhenChanged LastLogon = $LastLogonTime PasswordLastSet = $PasswordLastSet OrganizationalUnit = ConvertFrom-DistinguishedName -DistinguishedName $Object.DistinguishedName -ToOrganizationalUnit DistinguishedName = $Object.DistinguishedName } if ($O.Enabled) { $Output['Statistics']['EnabledObjects']++ } else { $Output['Statistics']['DisabledObjects']++ } $Output['Statistics']['TotalObjects']++ switch ($O.ObjectClass) { 'user' { $Output['Statistics']['TotalUsers']++ } 'group' { $Output['Statistics']['TotalGroups']++ } 'computer' { $Output['Statistics']['TotalComputers']++ } } $O if ($All) { foreach ($Sid in $SidDomains) { if (-not $Output[$Sid]) { $Output[$Sid] = [System.Collections.Generic.List[PSCustomObject]]::new() } $Output[$Sid].Add($O) } } } } if ($All) { $Output['DomainSIDs'] = $DomainSIDs $Output['Statistics'] = $Output['Statistics'] $Output['All'] = $AllUsers $Output } else { $AllUsers } } function Remove-EmptyValue { <# .SYNOPSIS Removes empty values from a hashtable recursively. .DESCRIPTION This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values. .PARAMETER Hashtable The hashtable from which empty values will be removed. .PARAMETER ExcludeParameter An array of keys to exclude from the removal process. .PARAMETER Recursive Indicates whether to recursively remove empty values from nested hashtables. .PARAMETER Rerun Specifies the number of times to rerun the removal process recursively. .PARAMETER DoNotRemoveNull If specified, null values will not be removed. .PARAMETER DoNotRemoveEmpty If specified, empty string values will not be removed. .PARAMETER DoNotRemoveEmptyArray If specified, empty array values will not be removed. .PARAMETER DoNotRemoveEmptyDictionary If specified, empty dictionary values will not be removed. .EXAMPLE $hashtable = @{ 'Key1' = ''; 'Key2' = $null; 'Key3' = @(); 'Key4' = @{} } Remove-EmptyValue -Hashtable $hashtable -Recursive Description ----------- This example removes empty values from the $hashtable recursively. .EXAMPLE $SplatDictionary = [ordered] @{ Test = $NotExistingParameter Test1 = 'Existing Entry' Test2 = $null Test3 = '' Test5 = 0 Test6 = 6 Test7 = @{} } Remove-EmptyValue -Splat $SplatDictionary -Recursive -ExcludeParameter 'Test7' $SplatDictionary #> [alias('Remove-EmptyValues')] [CmdletBinding()] param( [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable, [string[]] $ExcludeParameter, [switch] $Recursive, [int] $Rerun, [switch] $DoNotRemoveNull, [switch] $DoNotRemoveEmpty, [switch] $DoNotRemoveEmptyArray, [switch] $DoNotRemoveEmptyDictionary ) foreach ($Key in [string[]] $Hashtable.Keys) { if ($Key -notin $ExcludeParameter) { if ($Recursive) { if ($Hashtable[$Key] -is [System.Collections.IDictionary]) { if ($Hashtable[$Key].Count -eq 0) { if (-not $DoNotRemoveEmptyDictionary) { $Hashtable.Remove($Key) } } else { Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } else { if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') { $Hashtable.Remove($Key) } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) { $Hashtable.Remove($Key) } } } } if ($Rerun) { for ($i = 0; $i -lt $Rerun; $i++) { Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive -ExcludeParameter $ExcludeParameter -DoNotRemoveNull:$DoNotRemoveNull -DoNotRemoveEmpty:$DoNotRemoveEmpty -DoNotRemoveEmptyArray:$DoNotRemoveEmptyArray -DoNotRemoveEmptyDictionary:$DoNotRemoveEmptyDictionary } } } function Set-LoggingCapabilities { <# .SYNOPSIS Sets up logging capabilities by managing log files. .DESCRIPTION This function sets up logging capabilities by creating the necessary directories and managing the number of log files based on the specified maximum. .PARAMETER LogPath The path where the log files will be stored. .PARAMETER ScriptPath The path of the script that generates the logs. .PARAMETER LogMaximum The maximum number of log files to keep. Older files will be deleted if this limit is exceeded. .PARAMETER ShowTime Switch to include timestamps in the log entries. .PARAMETER TimeFormat The format of the timestamps in the log entries. .PARAMETER ParameterPSDefaultParameterValues The hashtable of default parameter values for the Write-Color function. If this parameter is not provided, the function will create a new hashtable. This will only work properly if the function is nested as private function in another module. It's advised to provide the hashtable from the parent function for this to work always. .EXAMPLE Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss" -ParameterPSDefaultParameterValues $Script:PSDefaultParameterValues .EXAMPLE Set-LoggingCapabilities -LogPath "C:\Logs\log.log" -ScriptPath "C:\Scripts\script.ps1" -LogMaximum 10 -ShowTime -TimeFormat "yyyy-MM-dd HH:mm:ss" .NOTES This function is used in: - CleanupMonster - PasswordSolution - SharePointEssentials And many other scripts. #> [CmdletBinding()] param( [Alias('Path', 'Log', 'Folder', 'LiteralPath', 'FilePath')][string] $LogPath, [string] $ScriptPath, [Alias('Maximum')][int] $LogMaximum, [switch] $ShowTime, [string] $TimeFormat, [System.Collections.IDictionary] $ParameterPSDefaultParameterValues ) if (-not $ParameterPSDefaultParameterValues) { $Script:PSDefaultParameterValues = @{ "Write-Color:LogFile" = $LogPath "Write-Color:ShowTime" = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } "Write-Color:TimeFormat" = $TimeFormat } } else { $ParameterPSDefaultParameterValues["Write-Color:LogFile"] = $LogPath $ParameterPSDefaultParameterValues["Write-Color:ShowTime"] = if ($PSBoundParameters.ContainsKey('ShowTime')) { $ShowTime.IsPresent } else { $null } $ParameterPSDefaultParameterValues["Write-Color:TimeFormat"] = $TimeFormat } if ($LogPath) { try { $FolderPath = [io.path]::GetDirectoryName($LogPath) if (-not (Test-Path -LiteralPath $FolderPath)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false } if ($LogMaximum -gt 0) { if ($ScriptPath) { $ScriptPathFolder = [io.path]::GetDirectoryName($ScriptPath) if ($ScriptPathFolder -eq $FolderPath) { Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but log files are in the same folder as the script. Cleanup disabled." -Color Yellow, White, DarkCyan, White return } $LogPathExtension = [io.path]::GetExtension($LogPath) if ($LogPathExtension) { $CurrentLogs = Get-ChildItem -LiteralPath $FolderPath -Filter "*$LogPathExtension" -ErrorAction Stop | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $LogMaximum } else { $CurrentLogs = $null Write-Color -Text '[i] ', "Log file has no extension (?!). Cleanup disabled." -Color Yellow, White, DarkCyan, White } if ($CurrentLogs) { Write-Color -Text '[i] ', "Logs directory has more than ", $LogMaximum, " log files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Log in $CurrentLogs) { try { Remove-Item -LiteralPath $Log.FullName -Confirm:$false -WhatIf:$false Write-Color -Text '[+] ', "Deleted ", "$($Log.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete log file $($Log.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } else { Write-Color -Text '[i] ', "LogMaximum is set to ", $LogMaximum, " but no script path detected. Most likely running interactively. Cleanup disabled." -Color Yellow, White, DarkCyan, White } } else { Write-Color -Text '[i] ', "LogMaximum is set to 0 (Unlimited). No log files will be deleted." -Color Yellow, DarkCyan } } catch { Write-Color -Text "[e] ", "Couldn't create the log directory. Error: $($_.Exception.Message)" -Color Yellow, Red $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null } } else { $Script:PSDefaultParameterValues["Write-Color:LogFile"] = $null } Remove-EmptyValue -Hashtable $Script:PSDefaultParameterValues } function Set-ReportingCapabilities { <# .SYNOPSIS Sets up reporting capabilities by managing report files. .DESCRIPTION This function sets up reporting capabilities by creating the necessary directories and managing the number of report files based on the specified maximum. .PARAMETER ReportPath The path where the report files will be stored. .PARAMETER ScriptPath The path of the script that generates the reports. .PARAMETER ReportMaximum The maximum number of report files to keep. Older files will be deleted if this limit is exceeded. .EXAMPLE Set-ReportingCapabilities -ReportPath "C:\Reports\report.log" -ScriptPath "C:\Scripts\script.ps1" -ReportMaximum 10 .NOTES This function is used in: - CleanupMonster - PasswordSolution - SharePointEssentials And many other scripts. #> [CmdletBinding()] param( [alias('Path', 'LiteralPath', 'FilePath')][string] $ReportPath, [string] $ScriptPath, [Alias('Maximum', 'Count')][int] $ReportMaximum ) if ($ReportPath) { try { $FolderPath = [io.path]::GetDirectoryName($ReportPath) if (-not (Test-Path -LiteralPath $FolderPath -ErrorAction Stop)) { $null = New-Item -Path $FolderPath -ItemType Directory -Force -WhatIf:$false -ErrorAction Stop } if ($ReportMaximum -gt 0) { if ($ScriptPath) { $ScriptPathFolder = [io.path]::GetDirectoryName($ScriptPath) if ($ScriptPathFolder -eq $FolderPath) { Write-Color -Text '[i] ', "ReportMaximum is set to ", $ReportMaximum, " but report files are in the same folder as the script. Cleanup disabled." -Color Yellow, White, DarkCyan, White return } } $ReportPathExtension = [io.path]::GetExtension($ReportPath) if ($ReportPathExtension) { $CurrentReports = Get-ChildItem -LiteralPath $FolderPath -Filter "*$ReportPathExtension" -ErrorAction Stop | Sort-Object -Property CreationTime -Descending | Select-Object -Skip $ReportMaximum } else { $CurrentReports = $null Write-Color -Text '[i] ', "Report file has no extension (?!). Cleanup disabled." -Color Yellow, White, DarkCyan, White } if ($CurrentReports) { Write-Color -Text '[i] ', "Reporting directory has more than ", $ReportMaximum, " report files. Cleanup required..." -Color Yellow, DarkCyan, Red, DarkCyan foreach ($Report in $CurrentReports) { try { Remove-Item -LiteralPath $Report.FullName -Confirm:$false -WhatIf:$false Write-Color -Text '[+] ', "Deleted ", "$($Report.FullName)" -Color Yellow, White, Green } catch { Write-Color -Text '[-] ', "Couldn't delete report file $($Report.FullName). Error: ', "$($_.Exception.Message) -Color Yellow, White, Red } } } } else { Write-Color -Text '[i] ', "ReportMaximum is set to 0 (Unlimited). No report files will be deleted." -Color Yellow, DarkCyan } } catch { Write-Color -Text "[e] ", "Couldn't create the reporting directory. Error: $($_.Exception.Message)" -Color Yellow, Red } } } function Write-Color { <# .SYNOPSIS Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. .DESCRIPTION Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options. It provides: - Easy manipulation of colors, - Logging output to file (log) - Nice formatting options out of the box. - Ability to use aliases for parameters .PARAMETER Text Text to display on screen and write to log file if specified. Accepts an array of strings. .PARAMETER Color Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER BackGroundColor Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string. If there are more strings than colors it will start from the beginning. Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White .PARAMETER StartTab Number of tabs to add before text. Default is 0. .PARAMETER LinesBefore Number of empty lines before text. Default is 0. .PARAMETER LinesAfter Number of empty lines after text. Default is 0. .PARAMETER StartSpaces Number of spaces to add before text. Default is 0. .PARAMETER LogFile Path to log file. If not specified no log file will be created. .PARAMETER DateTimeFormat Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss .PARAMETER LogTime If set to $true it will add time to log file. Default is $true. .PARAMETER LogRetry Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2. .PARAMETER Encoding Encoding of the log file. Default is Unicode. .PARAMETER ShowTime Switch to add time to console output. Default is not set. .PARAMETER NoNewLine Switch to not add new line at the end of the output. Default is not set. .PARAMETER NoConsoleOutput Switch to not output to console. Default all output goes to console. .EXAMPLE Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan .EXAMPLE Write-Color -Text "This is text in Green ", "followed by red ", "and then we have Magenta... ", "isn't it fun? ", "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1 .EXAMPLE Write-Color "1. ", "Option 1" -Color Yellow, Green Write-Color "2. ", "Option 2" -Color Yellow, Green Write-Color "3. ", "Option 3" -Color Yellow, Green Write-Color "4. ", "Option 4" -Color Yellow, Green Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1 .EXAMPLE Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss" Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." ` -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" .EXAMPLE Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow Write-Color -t "my text" -c yellow -b green Write-Color -text "my text" -c red .EXAMPLE Write-Color -Text "Testuję czy się ładnie zapisze, czy będą problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput .NOTES Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings Project support: https://github.com/EvotecIT/PSWriteColor Original idea: Josh (https://stackoverflow.com/users/81769/josh) #> [alias('Write-Colour')] [CmdletBinding()] param ( [alias ('T')] [String[]]$Text, [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White, [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null, [alias ('Indent')][int] $StartTab = 0, [int] $LinesBefore = 0, [int] $LinesAfter = 0, [int] $StartSpaces = 0, [alias ('L')] [string] $LogFile = '', [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss', [alias ('LogTimeStamp')][bool] $LogTime = $true, [int] $LogRetry = 2, [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode', [switch] $ShowTime, [switch] $NoNewLine, [alias('HideConsole')][switch] $NoConsoleOutput ) if (-not $NoConsoleOutput) { $DefaultColor = $Color[0] if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) { Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated." return } if ($LinesBefore -ne 0) { for ($i = 0; $i -lt $LinesBefore; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line before if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } } # Add TABS before text if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } } # Add SPACES before text if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline } # Add Time before output if ($Text.Count -ne 0) { if ($Color.Count -ge $Text.Count) { # the real deal coloring if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } } else { for ($i = 0; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } } } else { if ($null -eq $BackGroundColor) { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline } } else { for ($i = 0; $i -lt $Color.Length ; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline } for ($i = $Color.Length; $i -lt $Text.Length; $i++) { Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline } } } } if ($NoNewLine -eq $true) { Write-Host -NoNewline } else { Write-Host } # Support for no new line if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } } # Add empty line after } if ($Text.Count -and $LogFile) { # Save to file $TextToFile = "" for ($i = 0; $i -lt $Text.Length; $i++) { $TextToFile += $Text[$i] } $Saved = $false $Retry = 0 Do { $Retry++ try { if ($LogTime) { "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } else { "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false } $Saved = $true } catch { if ($Saved -eq $false -and $Retry -eq $LogRetry) { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))" } else { Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)" } } } Until ($Saved -eq $true -or $Retry -ge $LogRetry) } } function Convert-UserAccountControl { <# .SYNOPSIS Converts the UserAccountControl flags to their corresponding names. .DESCRIPTION This function takes a UserAccountControl value and converts it into a human-readable format by matching the flags to their corresponding names. .PARAMETER UserAccountControl Specifies the UserAccountControl value to be converted. .PARAMETER Separator Specifies the separator to use when joining the converted flags. If not provided, the flags will be returned as a list. .EXAMPLE Convert-UserAccountControl -UserAccountControl 66048 Outputs: "DONT_EXPIRE_PASSWORD, PASSWORD_EXPIRED" .EXAMPLE Convert-UserAccountControl -UserAccountControl 512 -Separator ', ' Outputs: "NORMAL_ACCOUNT" #> [cmdletBinding()] param( [alias('UAC')][int] $UserAccountControl, [string] $Separator ) $UserAccount = [ordered] @{ "SCRIPT" = 1 "ACCOUNTDISABLE" = 2 "HOMEDIR_REQUIRED" = 8 "LOCKOUT" = 16 "PASSWD_NOTREQD" = 32 "ENCRYPTED_TEXT_PWD_ALLOWED" = 128 "TEMP_DUPLICATE_ACCOUNT" = 256 "NORMAL_ACCOUNT" = 512 "INTERDOMAIN_TRUST_ACCOUNT" = 2048 "WORKSTATION_TRUST_ACCOUNT" = 4096 "SERVER_TRUST_ACCOUNT" = 8192 "DONT_EXPIRE_PASSWORD" = 65536 "MNS_LOGON_ACCOUNT" = 131072 "SMARTCARD_REQUIRED" = 262144 "TRUSTED_FOR_DELEGATION" = 524288 "NOT_DELEGATED" = 1048576 "USE_DES_KEY_ONLY" = 2097152 "DONT_REQ_PREAUTH" = 4194304 "PASSWORD_EXPIRED" = 8388608 "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216 "PARTIAL_SECRETS_ACCOUNT" = 67108864 } $Output = foreach ($_ in $UserAccount.Keys) { $binaryAnd = $UserAccount[$_] -band $UserAccountControl if ($binaryAnd -ne "0") { $_ } } if ($Separator) { $Output -join $Separator } else { $Output } } function Copy-DictionaryManual { <# .SYNOPSIS Copies a dictionary recursively, handling nested dictionaries and lists. .DESCRIPTION This function copies a dictionary recursively, handling nested dictionaries and lists. It creates a deep copy of the input dictionary, ensuring that modifications to the copied dictionary do not affect the original dictionary. .PARAMETER Dictionary The dictionary to be copied. .EXAMPLE $originalDictionary = @{ 'Key1' = 'Value1' 'Key2' = @{ 'NestedKey1' = 'NestedValue1' } } $copiedDictionary = Copy-DictionaryManual -Dictionary $originalDictionary This example demonstrates how to copy a dictionary with nested values. #> [CmdletBinding()] param( [System.Collections.IDictionary] $Dictionary ) $clone = [ordered] @{} foreach ($Key in $Dictionary.Keys) { $value = $Dictionary.$Key $clonedValue = switch ($Dictionary.$Key) { { $null -eq $_ } { $null continue } { $_ -is [System.Collections.IDictionary] } { Copy-DictionaryManual -Dictionary $_ continue } { $type = $_.GetType() $type.IsPrimitive -or $type.IsValueType -or $_ -is [string] } { $_ continue } default { $_ | Select-Object -Property * } } if ($value -is [System.Collections.IList]) { $clone[$Key] = @($clonedValue) } else { $clone[$Key] = $clonedValue } } $clone } function Get-GitHubLatestRelease { <# .SYNOPSIS Gets one or more releases from GitHub repository .DESCRIPTION Gets one or more releases from GitHub repository .PARAMETER Url Url to github repository .EXAMPLE Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table .NOTES General notes #> [CmdLetBinding()] param( [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url ) $ProgressPreference = 'SilentlyContinue' $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1 if ($Responds) { Try { [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json) foreach ($JsonContent in $JsonOutput) { [PSCustomObject] @{ PublishDate = [DateTime] $JsonContent.published_at CreatedDate = [DateTime] $JsonContent.created_at PreRelease = [bool] $JsonContent.prerelease Version = [version] ($JsonContent.name -replace 'v', '') Tag = $JsonContent.tag_name Branch = $JsonContent.target_commitish Errors = '' } } } catch { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = $_.Exception.Message } } } else { [PSCustomObject] @{ PublishDate = $null CreatedDate = $null PreRelease = $null Version = $null Tag = $null Branch = $null Errors = "No connection (ping) to $($Url.Host)" } } $ProgressPreference = 'Continue' } function Get-WinADTrust { <# .SYNOPSIS Retrieves trust relationships within an Active Directory forest. .DESCRIPTION This cmdlet retrieves and displays trust relationships within an Active Directory forest. It can be used to identify the trust relationships between domains and forests, including the type of trust, direction, and other properties. The cmdlet can also recursively explore trust relationships across multiple forests. .PARAMETER Forest Specifies the target forest to retrieve trust relationships from. If not specified, the current forest is used. .PARAMETER Recursive Indicates that the cmdlet should recursively explore trust relationships across multiple forests. .PARAMETER Nesting This parameter is used internally to track the nesting level of recursive calls. It should not be used directly. .PARAMETER UniqueTrusts This parameter is used internally to keep track of unique trust relationships encountered during recursive exploration. It should not be used directly. .PARAMETER SkipValidation Indicates that the cmdlet should skip the validation of trust relationships. By default, the cmdlet validates trust relationships by checking the existence of the "Domain Admins" group in the trusted domain. .EXAMPLE Get-WinADTrust -Recursive This example retrieves all trust relationships within the current forest and recursively explores trust relationships across multiple forests. .NOTES This cmdlet is designed to provide detailed information about trust relationships within an Active Directory environment. It can be used for auditing, troubleshooting, and planning purposes. #> [alias('Get-WinADTrusts')] [cmdletBinding()] param( [string] $Forest, [switch] $Recursive, [Parameter(DontShow)][int] $Nesting = -1, [Parameter(DontShow)][System.Collections.IDictionary] $UniqueTrusts, [switch] $SkipValidation ) Begin { if ($Nesting -eq -1) { $UniqueTrusts = [ordered]@{} } } Process { $Nesting++ $ForestInformation = Get-WinADForest -Forest $Forest [Array] $Trusts = @( try { $TrustRelationship = $ForestInformation.GetAllTrustRelationships() foreach ($Trust in $TrustRelationship) { [ordered] @{ Type = 'Forest' Details = $Trust ExecuteObject = $ForestInformation } } } catch { Write-Warning "Get-WinADForest - Can't process trusts for $Forest, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" } foreach ($Domain in $ForestInformation.Domains) { $DomainInformation = Get-WinADDomain -Domain $Domain.Name try { $TrustRelationship = $DomainInformation.GetAllTrustRelationships() foreach ($Trust in $TrustRelationship) { [ordered] @{ Type = 'Domain' Details = $Trust ExecuteObject = $DomainInformation } } } catch { Write-Warning "Get-WinADForest - Can't process trusts for $Domain, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" } } ) [Array] $Output = foreach ($Trust in $Trusts) { Write-Verbose "Get-WinADTrust - From: $($Trust.Details.SourceName) To: $($Trust.Details.TargetName) Nesting: $Nesting" $UniqueID1 = -join ($Trust.Details.SourceName, $Trust.Details.TargetName) $UniqueID2 = -join ($Trust.Details.TargetName, $Trust.Details.SourceName) if (-not $UniqueTrusts[$UniqueID1]) { $UniqueTrusts[$UniqueID1] = $true } else { Write-Verbose "Get-WinADTrust - Trust already on the list (From: $($Trust.Details.SourceName) To: $($Trust.Details.TargetName) Nesting: $Nesting)" continue } if (-not $UniqueTrusts[$UniqueID2]) { $UniqueTrusts[$UniqueID2] = $true } else { Write-Verbose "Get-WinADTrust - Trust already on the list (Reverse) (From: $($Trust.Details.TargetName) To: $($Trust.Details.SourceName) Nesting: $Nesting" continue } $TrustObject = Get-WinADTrustObject -Domain $Trust.ExecuteObject.Name -AsHashTable # https://github.com/vletoux/pingcastle/issues/9 if ($TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Enable TGT DELEGATION") { $TGTDelegation = $true } elseif ($TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "No TGT DELEGATION") { $TGTDelegation = $false } else { # Assuming all patches are installed (past July 2019) $TGTDelegation = $false } if (-not $SkipValidation) { $TrustStatus = Test-DomainTrust -Domain $Trust.Details.SourceName -TrustedDomain $Trust.Details.TargetName $GroupExists = Get-WinADObject -Identity 'S-1-5-32-544' -DomainName $Trust.Details.TargetName } else { $TrustStatus = [PSCustomObject] @{ TrustStatus = 'Validation skipped' TrustSourceDC = '' TrustTargetDC = '' } $GroupExists = $null } [PsCustomObject] @{ 'TrustSource' = $Trust.Details.SourceName #$Domain 'TrustTarget' = $Trust.Details.TargetName #$Trust.Target 'TrustDirection' = $Trust.Details.TrustDirection.ToString() #$Trust.Direction.ToString() 'TrustBase' = $Trust.Type 'TrustType' = $Trust.Details.TrustType.ToString() 'TrustTypeAD' = $TrustObject[$Trust.Details.TargetName].TrustType 'CreatedDaysAgo' = ((Get-Date) - $TrustObject[$Trust.Details.TargetName].WhenCreated).Days 'ModifiedDaysAgo' = ((Get-Date) - $TrustObject[$Trust.Details.TargetName].WhenChanged).Days 'NetBiosName' = if ($Trust.Details.TrustedDomainInformation.NetBiosName) { $Trust.Details.TrustedDomainInformation.NetBiosName } else { $TrustObject[$Trust.Details.TargetName].TrustPartnerNetBios } 'DomainSID' = if ($Trust.Details.TrustedDomainInformation.DomainSid) { $Trust.Details.TrustedDomainInformation.DomainSid } else { $TrustObject[$Trust.Details.TargetName].ObjectSID } 'Status' = if ($null -ne $Trust.Details.TrustedDomainInformation.Status) { $Trust.Details.TrustedDomainInformation.Status.ToString() } else { 'Internal' } 'Level' = $Nesting 'SuffixesIncluded' = (($Trust.Details.TopLevelNames | Where-Object { $_.Status -eq 'Enabled' }).Name) -join ', ' 'SuffixesExcluded' = $Trust.Details.ExcludedTopLevelNames.Name 'TrustAttributes' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -join ', ' 'TrustStatus' = $TrustStatus.TrustStatus 'QueryStatus' = if ($null -eq $GroupExists) { 'Skipped' } elseif ($GroupExists) { 'OK' } else { 'NOT OK' } 'ForestTransitive' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Forest Transitive" 'SelectiveAuthentication' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Cross Organization" #'SIDFilteringForestAware' = $null 'SIDFilteringQuarantined' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Quarantined Domain" 'DisallowTransitivity' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Non Transitive" 'IntraForest' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Within Forest" #'IsTreeParent' = $null #$Trust.IsTreeParent #'IsTreeRoot' = $Trust.Details.TrustType.ToString() -eq 'TreeRoot' 'IsTGTDelegationEnabled' = $TGTDelegation #'TrustedPolicy' = $null #$Trust.TrustedPolicy #'TrustingPolicy' = $null #$Trust.TrustingPolicy 'UplevelOnly' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "UpLevel Only" 'UsesAESKeys' = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -contains "AES128-CTS-HMAC-SHA1-96" -or $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -contains 'AES256-CTS-HMAC-SHA1-96' 'UsesRC4Encryption' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Uses RC4 Encryption" 'EncryptionTypes' = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -join ', ' 'TrustSourceDC' = $TrustStatus.TrustSourceDC 'TrustTargetDC' = $TrustStatus.TrustTargetDC 'ObjectGUID' = $TrustObject[$Trust.Details.TargetName].ObjectGuid #'ObjectSID' = $TrustObject[$Trust.Details.TargetName].ObjectSID 'Created' = $TrustObject[$Trust.Details.TargetName].WhenCreated 'Modified' = $TrustObject[$Trust.Details.TargetName].WhenChanged 'TrustDirectionText' = $TrustObject[$Trust.Details.TargetName].TrustDirectionText 'TrustTypeText' = $TrustObject[$Trust.Details.TargetName].TrustTypeText 'AdditionalInformation' = [ordered] @{ 'msDSSupportedEncryptionTypes' = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes 'msDSTrustForestTrustInfo' = $TrustObject[$Trust.Details.TargetName].msDSTrustForestTrustInfo 'SuffixesInclude' = $Trust.Details.TopLevelNames 'SuffixesExclude' = $Trust.Details.ExcludedTopLevelNames 'TrustObject' = $TrustObject 'GroupExists' = $GroupExists } } } if ($Output -and $Output.Count -gt 0) { $Output } if ($Recursive) { foreach ($Trust in $Output) { if ($Trust.TrustType -notin 'TreeRoot', 'ParentChild') { Get-WinADTrust -Forest $Trust.TrustTarget -Recursive -Nesting $Nesting -UniqueTrusts $UniqueTrusts -SkipValidation:$SkipValidation.IsPresent } } } } } function Test-ComputerPort { <# .SYNOPSIS Tests the connectivity of a computer on specified TCP and UDP ports. .DESCRIPTION The Test-ComputerPort function tests the connectivity of a computer on specified TCP and UDP ports. It checks if the specified ports are open and reachable on the target computer. .PARAMETER ComputerName Specifies the name of the computer to test the port connectivity. .PARAMETER PortTCP Specifies an array of TCP ports to test connectivity. .PARAMETER PortUDP Specifies an array of UDP ports to test connectivity. .PARAMETER Timeout Specifies the timeout value in milliseconds for the connection test. Default is 5000 milliseconds. .EXAMPLE Test-ComputerPort -ComputerName "Server01" -PortTCP 80,443 -PortUDP 53 -Timeout 3000 Tests the connectivity of Server01 on TCP ports 80 and 443, UDP port 53 with a timeout of 3000 milliseconds. .EXAMPLE Test-ComputerPort -ComputerName "Server02" -PortTCP 3389 -PortUDP 123 Tests the connectivity of Server02 on TCP port 3389, UDP port 123 with the default timeout of 5000 milliseconds. #> [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName, [int[]] $PortTCP, [int[]] $PortUDP, [int]$Timeout = 5000 ) begin { if ($Global:ProgressPreference -ne 'SilentlyContinue') { $TemporaryProgress = $Global:ProgressPreference $Global:ProgressPreference = 'SilentlyContinue' } } process { foreach ($Computer in $ComputerName) { foreach ($P in $PortTCP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'TCP' 'Status' = $null 'Summary' = $null 'Response' = $null } $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue if ($TcpClient.TcpTestSucceeded) { $Output['Status'] = $TcpClient.TcpTestSucceeded $Output['Summary'] = "TCP $P Successful" } else { $Output['Status'] = $false $Output['Summary'] = "TCP $P Failed" $Output['Response'] = $Warnings } [PSCustomObject]$Output } foreach ($P in $PortUDP) { $Output = [ordered] @{ 'ComputerName' = $Computer 'Port' = $P 'Protocol' = 'UDP' 'Status' = $null 'Summary' = $null } $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P) $UdpClient.Client.ReceiveTimeout = $Timeout $Encoding = [System.Text.ASCIIEncoding]::new() $byte = $Encoding.GetBytes("Evotec") [void]$UdpClient.Send($byte, $byte.length) $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0) try { $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint) [string]$Data = $Encoding.GetString($Bytes) If ($Data) { $Output['Status'] = $true $Output['Summary'] = "UDP $P Successful" $Output['Response'] = $Data } } catch { $Output['Status'] = $false $Output['Summary'] = "UDP $P Failed" $Output['Response'] = $_.Exception.Message } $UdpClient.Close() $UdpClient.Dispose() [PSCustomObject]$Output } } } end { if ($TemporaryProgress) { $Global:ProgressPreference = $TemporaryProgress } } } function Test-WinRM { <# .SYNOPSIS Tests the WinRM connectivity on the specified computers. .DESCRIPTION The Test-WinRM function tests the WinRM connectivity on the specified computers and returns the status of the connection. .PARAMETER ComputerName Specifies the names of the computers to test WinRM connectivity on. .EXAMPLE Test-WinRM -ComputerName "Server01", "Server02" Tests the WinRM connectivity on Server01 and Server02. .EXAMPLE Test-WinRM -ComputerName "Server03" Tests the WinRM connectivity on Server03. #> [CmdletBinding()] param ( [alias('Server')][string[]] $ComputerName ) $Output = foreach ($Computer in $ComputerName) { $Test = [PSCustomObject] @{ Output = $null Status = $null ComputerName = $Computer } try { $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop $Test.Status = $true } catch { $Test.Status = $false } $Test } $Output } function Get-WinADDomain { <# .SYNOPSIS Retrieves information about a specified Active Directory domain. .DESCRIPTION This function retrieves detailed information about the specified Active Directory domain. It queries the domain to gather information such as domain controllers, domain name, and other domain-related details. .PARAMETER Domain Specifies the target domain to retrieve information from. .EXAMPLE Get-WinADDomain -Domain "example.com" .NOTES This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory domain. #> [cmdletBinding()] param( [string] $Domain ) try { if ($Domain) { $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Domain) $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context) } else { $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain() } } catch { Write-Warning "Get-WinADDomain - Can't get $Domain information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" } $DomainInformation } function Get-WinADForest { <# .SYNOPSIS Retrieves information about a specified Active Directory forest. .DESCRIPTION This function retrieves detailed information about the specified Active Directory forest. It queries the forest to gather information such as domain controllers, sites, and other forest-related details. .PARAMETER Forest Specifies the target forest to retrieve information from. .EXAMPLE Get-WinADForest -Forest "example.com" .NOTES This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest. #> [cmdletBinding()] param( [string] $Forest ) try { if ($Forest) { $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Forest) $ForestInformation = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($Context) } else { $ForestInformation = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()) } } catch { Write-Warning "Get-WinADForest - Can't get $Forest information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" } $ForestInformation } function Get-WinADObject { <# .SYNOPSIS Gets Active Directory Object .DESCRIPTION Returns Active Directory Object (Computers, Groups, Users or ForeignSecurityPrincipal) using ADSI .PARAMETER Identity Identity of an object. It can be SamAccountName, SID, DistinguishedName or multiple other options .PARAMETER DomainName Choose domain name the objects resides in. This is optional for most objects .PARAMETER Credential Parameter description .PARAMETER IncludeGroupMembership Queries for group members when object is a group .PARAMETER IncludeAllTypes Allows functions to return all objects types and not only Computers, Groups, Users or ForeignSecurityPrincipal .EXAMPLE Get-WinADObject -Identity 'TEST\Domain Admins' -Verbose Get-WinADObject -Identity 'EVOTEC\Domain Admins' -Verbose Get-WinADObject -Identity 'Domain Admins' -DomainName 'DC=AD,DC=EVOTEC,DC=PL' -Verbose Get-WinADObject -Identity 'Domain Admins' -DomainName 'ad.evotec.pl' -Verbose Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=pl' Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz' .NOTES General notes #> [cmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][Array] $Identity, [string] $DomainName, [pscredential] $Credential, [switch] $IncludeGroupMembership, [switch] $IncludeAllTypes, [switch] $AddType, [switch] $Cache, [string[]] $Properties ) Begin { if ($Cache -and -not $Script:CacheObjectsWinADObject) { $Script:CacheObjectsWinADObject = @{} } # This is purely for calling group workaround Add-Type -AssemblyName System.DirectoryServices.AccountManagement $GroupTypes = @{ '2' = @{ Name = 'Distribution Group - Global' # distribution Type = 'Distribution' Scope = 'Global' } '4' = @{ Name = 'Distribution Group - Domain Local' # distribution Type = 'Distribution' Scope = 'Domain local' } '8' = @{ Name = 'Distribution Group - Universal' Type = 'Distribution' Scope = 'Universal' } '-2147483640' = @{ Name = 'Security Group - Universal' Type = 'Security' Scope = 'Universal' } '-2147483643' = @{ Name = 'Security Group - Builtin Local' # Builtin local Security Group Type = 'Security' Scope = 'Builtin local' } '-2147483644' = @{ Name = 'Security Group - Domain Local' Type = 'Security' Scope = 'Domain local' } '-2147483646' = @{ Name = 'Security Group - Global' # security Type = 'Security' Scope = 'Global' } } } process { foreach ($Ident in $Identity) { if (-not $Ident) { Write-Warning -Message "Get-WinADObject - Identity is empty. Skipping" continue } $ResolvedIdentity = $null # If it's an object we need to make sure we pass only DN if ($Ident.DistinguishedName) { $Ident = $Ident.DistinguishedName } # we reset domain name to it's given value if at all $TemporaryName = $Ident $TemporaryDomainName = $DomainName # Since we change $Ident below to different names we need to be sure we use original query for cache if ($Cache -and $Script:CacheObjectsWinADObject[$TemporaryName]) { Write-Verbose "Get-WinADObject - Requesting $TemporaryName from Cache" $Script:CacheObjectsWinADObject[$TemporaryName] continue } # if Domain Name is provided we don't check for anything as it's most likely already good Ident value if (-not $TemporaryDomainName) { $MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-|){1,14}\d+") if ($MatchRegex.Success) { $ResolvedIdentity = ConvertFrom-SID -SID $MatchRegex.Value $TemporaryDomainName = $ResolvedIdentity.DomainName $Ident = $MatchRegex.Value } elseif ($Ident -like '*\*') { $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false if ($ResolvedIdentity.SID) { $TemporaryDomainName = $ResolvedIdentity.DomainName $Ident = $ResolvedIdentity.SID } else { $NetbiosConversion = ConvertFrom-NetbiosName -Identity $Ident if ($NetbiosConversion.DomainName) { $TemporaryDomainName = $NetbiosConversion.DomainName $Ident = $NetbiosConversion.Name } } } elseif ($Ident -like '*DC=*') { $DNConversion = ConvertFrom-DistinguishedName -DistinguishedName $Ident -ToDomainCN $TemporaryDomainName = $DNConversion } elseif ($Ident -like '*@*') { $CNConversion = $Ident -split '@', 2 $TemporaryDomainName = $CNConversion[1] $Ident = $CNConversion[0] } elseif ($Ident -like '*.*') { $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false if ($ResolvedIdentity.SID) { $TemporaryDomainName = $ResolvedIdentity.DomainName $Ident = $ResolvedIdentity.SID } else { $CNConversion = $Ident -split '\.', 2 $Ident = $CNConversion[0] $TemporaryDomainName = $CNConversion[1] } } else { $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false if ($ResolvedIdentity.SID) { $TemporaryDomainName = $ResolvedIdentity.DomainName $Ident = $ResolvedIdentity.SID } else { $NetbiosConversion = ConvertFrom-NetbiosName -Identity $Ident if ($NetbiosConversion.DomainName) { $TemporaryDomainName = $NetbiosConversion.DomainName $Ident = $NetbiosConversion.Name } } } } # Building up ADSI call $Search = [System.DirectoryServices.DirectorySearcher]::new() #$Search.SizeLimit = $SizeLimit if ($TemporaryDomainName) { try { $Context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Domain', $TemporaryDomainName) } catch { Write-Warning "Get-WinADObject - Building context failed ($TemporaryDomainName), error: $($_.Exception.Message)" } } else { try { $Context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Domain') } catch { Write-Warning "Get-WinADObject - Building context failed, error: $($_.Exception.Message)" } } #Convert Identity Input String to HEX, if possible Try { $IdentityGUID = "" ([System.Guid]$Ident).ToByteArray() | ForEach-Object { $IdentityGUID += $("\{0:x2}" -f $_) } } Catch { $IdentityGUID = "null" } # Building search filter $Search.filter = "(|(DistinguishedName=$Ident)(Name=$Ident)(SamAccountName=$Ident)(UserPrincipalName=$Ident)(objectGUID=$IdentityGUID)(objectSid=$Ident))" if ($TemporaryDomainName) { $Search.SearchRoot = "LDAP://$TemporaryDomainName" } if ($PSBoundParameters['Credential']) { $Cred = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$TemporaryDomainName", $($Credential.UserName), $($Credential.GetNetworkCredential().password)) $Search.SearchRoot = $Cred } Write-Verbose "Get-WinADObject - Requesting $Ident ($TemporaryDomainName)" try { $SearchResults = $($Search.FindAll()) } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "Get-WinADObject - Requesting $Ident ($TemporaryDomainName) failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" } else { Write-Warning "Get-WinADObject - Requesting $Ident ($TemporaryDomainName) failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))" continue } } if ($SearchResults.Count -lt 1) { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw "Requesting $Ident ($TemporaryDomainName) failed with no results." } } foreach ($Object in $SearchResults) { $UAC = Convert-UserAccountControl -UserAccountControl ($Object.properties.useraccountcontrol -as [string]) $ObjectClass = ($Object.properties.objectclass -as [array])[-1] if ($ObjectClass -notin 'group', 'contact', 'inetOrgPerson', 'computer', 'user', 'foreignSecurityPrincipal', 'msDS-ManagedServiceAccount', 'msDS-GroupManagedServiceAccount' -and (-not $IncludeAllTypes)) { Write-Warning "Get-WinADObject - Unsupported object ($Ident) of type $ObjectClass. Only user,computer,group, foreignSecurityPrincipal, msDS-ManagedServiceAccount, msDS-GroupManagedServiceAccount are displayed by default. Use IncludeAllTypes switch to display all if nessecary." continue } $Members = $Object.properties.member -as [array] if ($ObjectClass -eq 'group') { # we only do this additional step when requested. It's not nessecary for day to day use but can hurt performance real bad for normal use cases # This was especially visible for group with 50k members and Get-WinADObjectMember which doesn't even require this data if ($IncludeGroupMembership) { # This is weird case but for some reason $Object.properties.member doesn't always return all values # the workaround is to do additional query for group and assing it $GroupMembers = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($Context, $Ident).Members try { $Members = [System.Collections.Generic.List[string]]::new() foreach ($Member in $Object.properties.member) { if ($Member) { $Members.Add($Member) } } foreach ($Member in $GroupMembers) { if ($Member.DistinguishedName) { if ($Member.DistinguishedName -notin $Members) { $Members.Add($Member.DistinguishedName) } } elseif ($Member.DisplayName) { $Members.Add($Member.DisplayName) } else { $Members.Add($Member.Sid) } } } catch { if ($PSBoundParameters.ErrorAction -eq 'Stop') { throw return } else { Write-Warning -Message "Error while parsing group members for $($Ident): $($_.Exception.Message)" } } } } $ObjectDomainName = ConvertFrom-DistinguishedName -DistinguishedName ($Object.properties.distinguishedname -as [string]) -ToDomainCN $DisplayName = $Object.properties.displayname -as [string] $SamAccountName = $Object.properties.samaccountname -as [string] $Name = $Object.properties.name -as [string] if ($ObjectClass -eq 'foreignSecurityPrincipal' -and $DisplayName -eq '') { # If object is foreignSecurityPrincipal (which shouldn't happen at this point) we need to set it to temporary name we # used before. Usually this is to fix 'NT AUTHORITY\INTERACTIVE' # I have no clue if there's better way to do it $DisplayName = $ResolvedIdentity.Name if ($DisplayName -like '*\*') { $NetbiosWithName = $DisplayName -split '\\' if ($NetbiosWithName.Count -eq 2) { #$NetbiosName = $NetbiosWithName[0] $NetbiosUser = $NetbiosWithName[1] $Name = $NetbiosUser $SamAccountName = $NetbiosUser } else { $Name = $DisplayName } } else { $Name = $DisplayName } } $GroupType = $Object.properties.grouptype -as [string] if ($Object.Properties.objectsid) { try { $ObjectSID = [System.Security.Principal.SecurityIdentifier]::new($Object.Properties.objectsid[0], 0).Value } catch { Write-Warning "Get-WinADObject - Getting objectsid failed, error: $($_.Exception.Message)" $ObjectSID = $null } } else { $ObjectSID = $null } $ReturnObject = [ordered] @{ DisplayName = $DisplayName Name = $Name SamAccountName = $SamAccountName ObjectClass = $ObjectClass Enabled = if ($ObjectClass -in 'group', 'contact') { $null } else { $UAC -notcontains 'ACCOUNTDISABLE' } PasswordNeverExpire = if ($ObjectClass -in 'group', 'contact') { $null } else { $UAC -contains 'DONT_EXPIRE_PASSWORD' } DomainName = $ObjectDomainName Distinguishedname = $Object.properties.distinguishedname -as [string] #Adspath = $Object.properties.adspath -as [string] WhenCreated = $Object.properties.whencreated -as [string] WhenChanged = $Object.properties.whenchanged -as [string] #Deleted = $Object.properties.isDeleted -as [string] #Recycled = $Object.properties.isRecycled -as [string] UserPrincipalName = $Object.properties.userprincipalname -as [string] ObjectSID = $ObjectSID MemberOf = $Object.properties.memberof -as [array] Members = $Members DirectReports = $Object.Properties.directreports GroupScopedType = $GroupTypes[$GroupType].Name GroupScope = $GroupTypes[$GroupType].Scope GroupType = $GroupTypes[$GroupType].Type #Administrative = if ($Object.properties.admincount -eq '1') { $true } else { $false } #Type = $ResolvedIdentity.Type Description = $Object.properties.description -as [string] } if ($Properties -contains 'LastLogonDate') { $LastLogon = [int64] $Object.properties.lastlogontimestamp[0] if ($LastLogon -ne 9223372036854775807) { $ReturnObject['LastLogonDate'] = [datetime]::FromFileTimeUtc($LastLogon) } else { $ReturnObject['LastLogonDate'] = $null } } if ($Properties -contains 'PasswordLastSet') { $PasswordLastSet = [int64] $Object.properties.pwdlastset[0] if ($PasswordLastSet -ne 9223372036854775807) { $ReturnObject['PasswordLastSet'] = [datetime]::FromFileTimeUtc($PasswordLastSet) } else { $ReturnObject['PasswordLastSet'] = $null } } if ($Properties -contains 'AccountExpirationDate') { $ExpirationDate = [int64] $Object.properties.accountexpires[0] if ($ExpirationDate -ne 9223372036854775807) { $ReturnObject['AccountExpirationDate'] = [datetime]::FromFileTimeUtc($ExpirationDate) } else { $ReturnObject['AccountExpirationDate'] = $null } } if ($AddType) { if (-not $ResolvedIdentity) { # This is purely to get special types $ResolvedIdentity = ConvertFrom-SID -SID $ReturnObject['ObjectSID'] } $ReturnObject['Type'] = $ResolvedIdentity.Type } if ($ReturnObject['Type'] -eq 'WellKnownAdministrative') { if (-not $TemporaryDomainName) { # This is so BUILTIN\Administrators would not report domain name that's always related to current one, while it could be someone expects it to be from different forest # this is to mainly address issues with Get-ADACL IdentityReference returning data that's hard to manage otherwise $ReturnObject['DomainName'] = '' } } <# $LastLogon = $Object.properties.lastlogon -as [string] if ($LastLogon) { $LastLogonDate = [datetime]::FromFileTime($LastLogon) } else { $LastLogonDate = $null } $AccountExpires = $Object.Properties.accountexpires -as [string] $AccountExpiresDate = ConvertTo-Date -accountExpires $AccountExpires $PasswordLastSet = $Object.Properties.pwdlastset -as [string] if ($PasswordLastSet) { $PasswordLastSetDate = [datetime]::FromFileTime($PasswordLastSet) } else { $PasswordLastSetDate = $null } $BadPasswordTime = $Object.Properties.badpasswordtime -as [string] if ($BadPasswordTime) { $BadPasswordDate = [datetime]::FromFileTime($BadPasswordTime) } else { $BadPasswordDate = $null } $ReturnObject['LastLogonDate'] = $LastLogonDate $ReturnObject['PasswordLastSet'] = $PasswordLastSetDate $ReturnObject['BadPasswordTime'] = $BadPasswordDate $ReturnObject['AccountExpiresDate'] = $AccountExpiresDate #> if ($Cache) { $Script:CacheObjectsWinADObject[$TemporaryName] = [PSCustomObject] $ReturnObject $Script:CacheObjectsWinADObject[$TemporaryName] } else { [PSCustomObject] $ReturnObject } } } } } function Get-WinADTrustObject { <# .SYNOPSIS Retrieves trust relationship information for a specified domain in Active Directory. .DESCRIPTION This function retrieves trust relationship information for the specified domain in Active Directory. It provides details about the trust type, trust direction, trust attributes, and security identifier. .PARAMETER Identity Specifies the domain identity for which trust relationship information is to be retrieved. .PARAMETER AsHashTable Indicates whether the output should be returned as a hashtable. .NOTES Author: Your Name Date: Current Date Version: 1.0 #> [cmdletBinding()] param( [Parameter(Mandatory, Position = 0)][alias('Domain')][string] $Identity, [switch] $AsHashTable ) $Summary = [ordered] @{} # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.trusttype?view=dotnet-plat-ext-3.1 $TrustType = @{ CrossLink = 'The trust relationship is a shortcut between two domains that exists to optimize the authentication processing between two domains that are in separate domain trees.' # 2 External = 'The trust relationship is with a domain outside of the current forest.' # 3 Forest = 'The trust relationship is between two forest root domains in separate Windows Server 2003 forests.' # 4 Kerberos = 'The trusted domain is an MIT Kerberos realm.' # 5 ParentChild = 'The trust relationship is between a parent and a child domain.' # 1 TreeRoot = 'One of the domains in the trust relationship is a tree root.' # 0 Unknown = 'The trust is a non-specific type.' #6 } # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.trustdirection?view=dotnet-plat-ext-3.1 $TrustDirection = @{ Bidirectional = 'Each domain or forest has access to the resources of the other domain or forest.' # 3 Inbound = 'This is a trusting domain or forest. The other domain or forest has access to the resources of this domain or forest. This domain or forest does not have access to resources that belong to the other domain or forest.' # 1 Outbound = 'This is a trusted domain or forest. This domain or forest has access to resources of the other domain or forest. The other domain or forest does not have access to the resources of this domain or forest.' # 2 } if ($Identity -contains 'DC=') { $DomainName = "LDAP://$Domain" $TrustSource = ConvertFrom-DistinguishedName -DistinguishedName $DomainName -ToDomainCN } else { $DomainDN = ConvertTo-DistinguishedName -CanonicalName $Identity -ToDomain $DomainName = "LDAP://$DomainDN" $TrustSource = $Identity } $searcher = [adsisearcher]'(objectClass=trustedDomain)' $searcher.SearchRoot = [adsi] $DomainName #'LDAP://DC=TEST,DC=EVOTEC,DC=PL' $Trusts = $searcher.FindAll() foreach ($Trust in $Trusts) { $TrustD = [System.DirectoryServices.ActiveDirectory.TrustDirection] $Trust.properties.trustdirection[0] $TrustT = [System.DirectoryServices.ActiveDirectory.TrustType] $Trust.properties.trusttype[0] if ($Trust.properties.'msds-trustforesttrustinfo') { $msDSTrustForestTrustInfo = Convert-TrustForestTrustInfo -msDSTrustForestTrustInfo $Trust.properties.'msds-trustforesttrustinfo'[0] } else { $msDSTrustForestTrustInfo = $null } if ($Trust.properties.trustattributes) { $TrustAttributes = Get-ADTrustAttributes -Value ([int] $Trust.properties.trustattributes[0]) } else { $TrustAttributes = $null } if ($Trust.properties.securityidentifier) { try { $ObjectSID = [System.Security.Principal.SecurityIdentifier]::new($Trust.properties.securityidentifier[0], 0).Value } catch { $ObjectSID = $null } } else { $ObjectSID = $null } $TrustObject = [PSCustomObject] @{ #Name = [string] $Trust.properties.name # {ad.evotec.xyz} TrustSource = $TrustSource TrustPartner = [string] $Trust.properties.trustpartner # {ad.evotec.xyz} TrustPartnerNetBios = [string] $Trust.properties.flatname # {EVOTEC} TrustDirection = $TrustD.ToString() # {3} TrustType = $TrustT.ToString() # {2} TrustAttributes = $TrustAttributes # {32} TrustDirectionText = $TrustDirection[$TrustD.ToString()] TrustTypeText = $TrustType[$TrustT.ToString()] WhenCreated = [DateTime] $Trust.properties.whencreated[0] # {26.07.2018 10:59:52} WhenChanged = [DateTime] $Trust.properties.whenchanged[0] # {14.08.2020 22:23:14} ObjectSID = $ObjectSID Distinguishedname = [string] $Trust.properties.distinguishedname # {CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl} IsCriticalSystemObject = [bool]::Parse($Trust.properties.iscriticalsystemobject[0]) # {True} ObjectGuid = [guid]::new($Trust.properties.objectguid[0]) ObjectCategory = [string] $Trust.properties.objectcategory # {CN=Trusted-Domain,CN=Schema,CN=Configuration,DC=ad,DC=evotec,DC=xyz} ObjectClass = ([array] $Trust.properties.objectclass)[-1] # {top, leaf, trustedDomain} UsnCreated = [string] $Trust.properties.usncreated # {14149} UsnChanged = [string] $Trust.properties.usnchanged # {4926091} ShowInAdvancedViewOnly = [bool]::Parse($Trust.properties.showinadvancedviewonly) # {True} TrustPosixOffset = [string] $Trust.properties.trustposixoffset # {-2147483648} msDSTrustForestTrustInfo = $msDSTrustForestTrustInfo msDSSupportedEncryptionTypes = if ($Trust.properties.'msds-supportedencryptiontypes') { Get-ADEncryptionTypes -Value ([int] $Trust.properties.'msds-supportedencryptiontypes'[0]) } else { $null } #SecurityIdentifier = [string] $Trust.properties.securityidentifier # {1 4 0 0 0 0 0 5 21 0 0 0 113 37 225 50 27 133 23 171 67 175 144 188} #InstanceType = $Trust.properties.instancetype # {4} #AdsPath = [string] $Trust.properties.adspath # {LDAP://CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl} #CN = [string] $Trust.properties.cn # {ad.evotec.xyz} #ObjectGuid = $Trust.properties.objectguid # {193 58 187 220 218 30 146 77 162 218 90 74 159 98 153 219} #dscorepropagationdata = $Trust.properties.dscorepropagationdata # {01.01.1601 00:00:00} } if ($AsHashTable) { $Summary[$TrustObject.trustpartner] = $TrustObject } else { $TrustObject } } if ($AsHashTable) { $Summary } } function Test-DomainTrust { <# .SYNOPSIS Test the trust relationship between two domains. .DESCRIPTION This function tests the trust relationship between the specified domain and a trusted domain. .PARAMETER Domain Specifies the domain to test the trust relationship for. .PARAMETER TrustedDomain Specifies the trusted domain to test the trust relationship against. .EXAMPLE Test-DomainTrust -Domain "contoso.com" -TrustedDomain "fabrikam.com" Tests the trust relationship between the "contoso.com" domain and the "fabrikam.com" trusted domain. .NOTES Author: Your Name Date: Date Version: 1.0 #> [cmdletBinding()] param( [string] $Domain, [string] $TrustedDomain ) #$DomainPDC = $ForestInformation['DomainDomainControllers'][$Domain] | Where-Object { $_.IsPDC -eq $true } $DomainInformation = Get-WinADDomain -Domain $Domain $DomainPDC = $DomainInformation.PdcRoleOwner.Name $PropertiesTrustWMI = @( 'FlatName', 'SID', 'TrustAttributes', 'TrustDirection', 'TrustedDCName', 'TrustedDomain', 'TrustIsOk', 'TrustStatus', 'TrustStatusString', # TrustIsOk/TrustStatus are covered by this 'TrustType' ) $getCimInstanceSplat = @{ ClassName = 'Microsoft_DomainTrustStatus' Namespace = 'root\MicrosoftActiveDirectory' ComputerName = $DomainPDC ErrorAction = 'SilentlyContinue' Property = $PropertiesTrustWMI Verbose = $false } if ($TrustedDomain) { $getCimInstanceSplat['Filter'] = "TrustedDomain = `"$TrustedDomain`"" } $TrustStatatuses = Get-CimInstance @getCimInstanceSplat if ($TrustStatatuses) { foreach ($Status in $TrustStatatuses) { [PSCustomObject] @{ 'TrustSource' = $DomainInformation.Name 'TrustPartner' = $Status.TrustedDomain 'TrustAttributes' = if ($Status.TrustAttributes) { Get-ADTrustAttributes -Value $Status.TrustAttributes } else { 'Error - needs fixing' } 'TrustStatus' = if ($null -ne $Status) { $Status.TrustStatusString } else { 'N/A' } 'TrustSourceDC' = if ($null -ne $Status) { $Status.PSComputerName } else { '' } 'TrustTargetDC' = if ($null -ne $Status) { $Status.TrustedDCName.Replace('\\', '') } else { '' } #'TrustOK' = if ($null -ne $Status) { $Status.TrustIsOK } else { $false } #'TrustStatusInt' = if ($null -ne $Status) { $Status.TrustStatus } else { -1 } } } } else { [PSCustomObject] @{ 'TrustSource' = $DomainInformation.Name 'TrustPartner' = $TrustedDomain 'TrustAttributes' = 'Error - needs fixing' 'TrustStatus' = 'N/A' 'TrustSourceDC' = '' 'TrustTargetDC' = '' #'TrustOK' = $false #'TrustStatusInt' = -1 } } } function Convert-Identity { <# .SYNOPSIS Small command that tries to resolve any given object .DESCRIPTION Small command that tries to resolve any given object - be it SID, DN, FSP or Netbiosname .PARAMETER Identity Type to resolve in form of Identity, DN, SID .PARAMETER SID Allows to pass SID directly, rather then going thru verification process .PARAMETER Name Allows to pass Name directly, rather then going thru verification process .PARAMETER Force Allows to clear cache, useful when you want to force refresh .EXAMPLE $Identity = @( 'S-1-5-4' 'S-1-5-4' 'S-1-5-11' 'S-1-5-32-549' 'S-1-5-32-550' 'S-1-5-32-548' 'S-1-5-64-10' 'S-1-5-64-14' 'S-1-5-64-21' 'S-1-5-18' 'S-1-5-19' 'S-1-5-32-544' 'S-1-5-20-20-10-51' # Wrong SID 'S-1-5-21-853615985-2870445339-3163598659-512' 'S-1-5-21-3661168273-3802070955-2987026695-512' 'S-1-5-21-1928204107-2710010574-1926425344-512' 'CN=Test Test 2,OU=Users,OU=Production,DC=ad,DC=evotec,DC=pl' 'Test Local Group' 'przemyslaw.klys@evotec.pl' 'test2' 'NT AUTHORITY\NETWORK' 'NT AUTHORITY\SYSTEM' 'S-1-5-21-853615985-2870445339-3163598659-519' 'TEST\some' 'EVOTECPL\Domain Admins' 'NT AUTHORITY\INTERACTIVE' 'INTERACTIVE' 'EVOTEC\Domain Admins' 'EVOTECPL\Domain Admins' 'Test\Domain Admins' 'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # Valid 'CN=S-1-5-21-1928204107-2710010574-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # not valid 'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # cached ) $TestOutput = Convert-Identity -Identity $Identity -Verbose Output: Name SID DomainName Type Error ---- --- ---------- ---- ----- NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup NT AUTHORITY\Authenticated Users S-1-5-11 WellKnownGroup BUILTIN\Server Operators S-1-5-32-549 WellKnownGroup BUILTIN\Print Operators S-1-5-32-550 WellKnownGroup BUILTIN\Account Operators S-1-5-32-548 WellKnownGroup NT AUTHORITY\NTLM Authentication S-1-5-64-10 WellKnownGroup NT AUTHORITY\SChannel Authentication S-1-5-64-14 WellKnownGroup NT AUTHORITY\Digest Authentication S-1-5-64-21 WellKnownGroup NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative NT AUTHORITY\NETWORK SERVICE S-1-5-19 WellKnownGroup BUILTIN\Administrators S-1-5-32-544 WellKnownAdministrative S-1-5-20-20-10-51 S-1-5-20-20-10-51 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated." EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative EVOTECPL\TestingAD S-1-5-21-3661168273-3802070955-2987026695-1111 ad.evotec.pl NotAdministrative EVOTEC\Test Local Group S-1-5-21-853615985-2870445339-3163598659-3610 ad.evotec.xyz NotAdministrative EVOTEC\przemyslaw.klys S-1-5-21-853615985-2870445339-3163598659-1105 ad.evotec.xyz NotAdministrative test2 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated." NT AUTHORITY\NETWORK S-1-5-2 WellKnownGroup NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative EVOTEC\Enterprise Admins S-1-5-21-853615985-2870445339-3163598659-519 ad.evotec.xyz Administrative TEST\some S-1-5-21-1928204107-2710010574-1926425344-1106 test.evotec.pl NotAdministrative EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative S-1-5-21-1928204107-2710010574-512 S-1-5-21-1928204107-2710010574-512 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated." TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative .NOTES General notes #> [cmdletBinding(DefaultParameterSetName = 'Identity')] param( [parameter(ParameterSetName = 'Identity', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)][string[]] $Identity, [parameter(ParameterSetName = 'SID', Mandatory)][System.Security.Principal.SecurityIdentifier[]] $SID, [parameter(ParameterSetName = 'Name', Mandatory)][string[]] $Name, [switch] $Force ) Begin { if (-not $Script:GlobalCacheSidConvert -or $Force) { $Script:GlobalCacheSidConvert = @{ 'NT AUTHORITY\SYSTEM' = [PSCustomObject] @{ Name = 'BUILTIN\Administrators' SID = 'S-1-5-18' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'BUILTIN\Administrators' = [PSCustomObject] @{ Name = 'BUILTIN\Administrators' SID = 'S-1-5-32-544' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'BUILTIN\Users' = [PSCustomObject] @{ Name = 'BUILTIN\Users' SID = 'S-1-5-32-545' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Guests' = [PSCustomObject] @{ Name = 'BUILTIN\Guests' SID = 'S-1-5-32-546' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Power Users' = [PSCustomObject] @{ Name = 'BUILTIN\Power Users' SID = 'S-1-5-32-547' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Account Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Account Operators' SID = 'S-1-5-32-548' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Server Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Server Operators' SID = 'S-1-5-32-549' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Print Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Print Operators' SID = 'S-1-5-32-550' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Backup Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Backup Operators' SID = 'S-1-5-32-551' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Replicator' = [PSCustomObject] @{ Name = 'BUILTIN\Replicators' SID = 'S-1-5-32-552' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Pre-Windows 2000 Compatible Access' = [PSCustomObject] @{ Name = 'BUILTIN\Pre-Windows 2000 Compatible Access' SID = 'S-1-5-32-554' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Remote Desktop Users' = [PSCustomObject] @{ Name = 'BUILTIN\Remote Desktop Users' SID = 'S-1-5-32-555' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Network Configuration Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Network Configuration Operators' SID = 'S-1-5-32-556' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Incoming Forest Trust Builders' = [PSCustomObject] @{ Name = 'BUILTIN\Incoming Forest Trust Builders' SID = 'S-1-5-32-557' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Performance Monitor Users' = [PSCustomObject] @{ Name = 'BUILTIN\Performance Monitor Users' SID = 'S-1-5-32-558' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Performance Log Users' = [PSCustomObject] @{ Name = 'BUILTIN\Performance Log Users' SID = 'S-1-5-32-559' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Windows Authorization Access Group' = [PSCustomObject] @{ Name = 'BUILTIN\Windows Authorization Access Group' SID = 'S-1-5-32-560' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Terminal Server License Servers' = [PSCustomObject] @{ Name = 'BUILTIN\Terminal Server License Servers' SID = 'S-1-5-32-561' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Distributed COM Users' = [PSCustomObject] @{ Name = 'BUILTIN\Distributed COM Users' SID = 'S-1-5-32-562' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\IIS_IUSRS' = [PSCustomObject] @{ Name = 'BUILTIN\IIS_IUSRS' SID = 'S-1-5-32-568' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Cryptographic Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Cryptographic Operators' SID = 'S-1-5-32-569' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Event Log Readers' = [PSCustomObject] @{ Name = 'BUILTIN\Event Log Readers' SID = 'S-1-5-32-573' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Certificate Service DCOM Access' = [PSCustomObject] @{ Name = 'BUILTIN\Certificate Service DCOM Access' SID = 'S-1-5-32-574' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\RDS Remote Access Servers' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Remote Access Servers' SID = 'S-1-5-32-575' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\RDS Endpoint Servers' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Endpoint Servers' SID = 'S-1-5-32-576' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\RDS Management Servers' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Management Servers' SID = 'S-1-5-32-577' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Hyper-V Administrators' = [PSCustomObject] @{ Name = 'BUILTIN\Hyper-V Administrators' SID = 'S-1-5-32-578' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Access Control Assistance Operators' = [PSCustomObject] @{ Name = 'BUILTIN\Access Control Assistance Operators' SID = 'S-1-5-32-579' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'BUILTIN\Remote Management Users' = [PSCustomObject] @{ Name = 'BUILTIN\Remote Management Users' SID = 'S-1-5-32-580' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'Window Manager\Window Manager Group' = [PSCustomObject] @{ Name = 'Window Manager\Window Manager Group' SID = 'S-1-5-90-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT SERVICE\WdiServiceHost' = [PSCustomObject] @{ Name = 'NT SERVICE\WdiServiceHost' SID = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT SERVICE\MSSQLSERVER' = [PSCustomObject] @{ Name = 'NT SERVICE\MSSQLSERVER' SID = 'S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT SERVICE\SQLSERVERAGENT' = [PSCustomObject] @{ Name = 'NT SERVICE\SQLSERVERAGENT' SID = 'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT SERVICE\SQLTELEMETRY' = [PSCustomObject] @{ Name = 'NT SERVICE\SQLTELEMETRY' SID = 'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT SERVICE\ADSync' = [PSCustomObject] @{ Name = 'NT SERVICE\ADSync' SID = 'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'NT Service\himds' = [PSCustomObject] @{ Name = 'NT Service\himds' SID = 'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083' DomainName = '' Type = 'WellKnownGroup' Error = '' } } } } Process { if ($Identity) { foreach ($Ident in $Identity) { $MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-|){1,14}\d+") if ($Script:GlobalCacheSidConvert[$Ident]) { Write-Verbose "Convert-Identity - Processing $Ident (Cache)" $Script:GlobalCacheSidConvert[$Ident] } elseif ($MatchRegex.Success) { Write-Verbose "Convert-Identity - Processing $Ident (SID)" if ($MatchRegex.Value -ne $Ident) { $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $MatchRegex.Value } else { $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $Ident } $Script:GlobalCacheSidConvert[$Ident] } elseif ($Ident -like '*DC=*') { Write-Verbose "Convert-Identity - Processing $Ident (DistinguishedName)" try { $Object = [adsi]"LDAP://$($Ident)" $SIDValue = [System.Security.Principal.SecurityIdentifier]::new($Object.objectSid.Value, 0).Value $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $SIDValue } catch { $Script:GlobalCacheSidConvert[$Ident] = [PSCustomObject] @{ Name = $Ident SID = $null DomainName = '' Type = 'Unknown' Error = $_.Exception.Message -replace [environment]::NewLine, ' ' } } $Script:GlobalCacheSidConvert[$Ident] } else { Write-Verbose "Convert-Identity - Processing $Ident (Other)" try { $SIDValue = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $SIDValue } catch { $Script:GlobalCacheSidConvert[$Ident] = [PSCustomObject] @{ Name = $Ident SID = $null DomainName = '' Type = 'Unknown' Error = $_.Exception.Message -replace [environment]::NewLine, ' ' } } $Script:GlobalCacheSidConvert[$Ident] } } } else { if ($SID) { foreach ($S in $SID) { if ($Script:GlobalCacheSidConvert[$S]) { $Script:GlobalCacheSidConvert[$S] } else { $Script:GlobalCacheSidConvert[$S] = ConvertFrom-SID -SID $S $Script:GlobalCacheSidConvert[$S] } } } else { foreach ($Ident in $Name) { if ($Script:GlobalCacheSidConvert[$Ident]) { $Script:GlobalCacheSidConvert[$Ident] } else { $Script:GlobalCacheSidConvert[$Ident] = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value $Script:GlobalCacheSidConvert[$Ident] } } } } } End { } } function Convert-TrustForestTrustInfo { <# .SYNOPSIS Converts the binary data of forest trust information into a readable format. .DESCRIPTION This function takes the binary data of forest trust information and converts it into a readable format, providing details about the trust status, domain names, SIDs, and creation timestamps. .PARAMETER msDSTrustForestTrustInfo The binary data containing forest trust information. .EXAMPLE Convert-TrustForestTrustInfo -msDSTrustForestTrustInfo $binaryData Converts the binary forest trust information into a readable format. .NOTES Author: Your Name Date: Current Date Version: 1.0 #> [CmdletBinding()] param( [byte[]] $msDSTrustForestTrustInfo ) # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/66387402-cb2b-490c-bf2a-f4ad687397e4 $Flags = [ordered] @{ '0' = 'Enabled' 'LsaTlnDisabledNew' = 'Not yet enabled' 'LsaTlnDisabledAdmin' = 'Disabled by administrator' 'LsaTlnDisabledConflict' = 'Disabled due to a conflict with another trusted domain' 'LsaSidDisabledAdmin' = 'Disabled for SID, NetBIOS, and DNS name-based matches by the administrator' 'LsaSidDisabledConflict' = 'Disabled for SID, NetBIOS, and DNS name-based matches due to a SID or DNS name-based conflict with another trusted domain' 'LsaNBDisabledAdmin' = 'Disabled for NetBIOS name-based matches by the administrator' 'LsaNBDisabledConflict' = 'Disabled for NetBIOS name-based matches due to a NetBIOS domain name conflict with another trusted domain' } if ($msDSTrustForestTrustInfo) { $Read = Get-ForestTrustInfo -Byte $msDSTrustForestTrustInfo $ForestTrustDomainInfo = [ordered]@{} [Array] $Records = foreach ($Record in $Read.Records) { if ($Record.RecordType -ne 'ForestTrustDomainInfo') { # ForestTrustTopLevelName, ForestTrustTopLevelNameEx if ($Record.RecordType -eq 'ForestTrustTopLevelName') { $Type = 'Included' } else { $Type = 'Excluded' } [PSCustomObject] @{ DnsName = $null NetbiosName = $null Sid = $null Type = $Type Suffix = $Record.ForestTrustData Status = $Flags["$($Record.Flags)"] StatusFlag = $Record.Flags WhenCreated = $Record.Timestamp } } else { $ForestTrustDomainInfo['DnsName'] = $Record.ForestTrustData.DnsName $ForestTrustDomainInfo['NetbiosName'] = $Record.ForestTrustData.NetbiosName $ForestTrustDomainInfo['Sid'] = $Record.ForestTrustData.Sid } } foreach ($Record in $Records) { $Record.DnsName = $ForestTrustDomainInfo['DnsName'] $Record.NetbiosName = $ForestTrustDomainInfo['NetbiosName'] $Record.Sid = $ForestTrustDomainInfo['Sid'] } $Records } } function ConvertFrom-NetbiosName { <# .SYNOPSIS Converts a NetBIOS name to its corresponding domain name and object name. .DESCRIPTION This function takes a NetBIOS name in the format 'Domain\Object' and converts it to the corresponding domain name and object name. .PARAMETER Identity Specifies the NetBIOS name(s) to convert. .EXAMPLE 'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins' | ConvertFrom-NetbiosName Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names. .EXAMPLE ConvertFrom-NetbiosName -Identity 'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins' Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names. #> [cmdletBinding()] param( [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] [string[]] $Identity ) process { foreach ($Ident in $Identity) { if ($Ident -like '*\*') { $NetbiosWithObject = $Ident -split "\\" if ($NetbiosWithObject.Count -eq 2) { $LDAPQuery = ([ADSI]"LDAP://$($NetbiosWithObject[0])") $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $LDAPQuery.distinguishedName -ToDomainCN [PSCustomObject] @{ DomainName = $DomainName Name = $NetbiosWithObject[1] } } else { [PSCustomObject] @{ DomainName = '' Name = $Ident } } } else { [PSCustomObject] @{ DomainName = '' Name = $Ident } } } } } function ConvertFrom-SID { <# .SYNOPSIS Small command that can resolve SID values .DESCRIPTION Small command that can resolve SID values .PARAMETER SID Value to resolve .PARAMETER OnlyWellKnown Only resolve SID when it's well know SID. Otherwise return $null .PARAMETER OnlyWellKnownAdministrative Only resolve SID when it's administrative well know SID. Otherwise return $null .PARAMETER DoNotResolve Uses only dicrionary values without querying AD .EXAMPLE ConvertFrom-SID -SID 'S-1-5-8', 'S-1-5-9', 'S-1-5-11', 'S-1-5-18', 'S-1-1-0' -DoNotResolve .NOTES General notes #> [cmdletbinding(DefaultParameterSetName = 'Standard')] param( [Parameter(ParameterSetName = 'Standard')] [Parameter(ParameterSetName = 'OnlyWellKnown')] [Parameter(ParameterSetName = 'OnlyWellKnownAdministrative')] [string[]] $SID, [Parameter(ParameterSetName = 'OnlyWellKnown')][switch] $OnlyWellKnown, [Parameter(ParameterSetName = 'OnlyWellKnownAdministrative')][switch] $OnlyWellKnownAdministrative, [Parameter(ParameterSetName = 'Standard')][switch] $DoNotResolve ) $WellKnownAdministrative = @{ 'S-1-5-18' = [PSCustomObject] @{ Name = 'NT AUTHORITY\SYSTEM' SID = 'S-1-5-18' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'S-1-5-32-544' = [PSCustomObject] @{ Name = 'BUILTIN\Administrators' SID = 'S-1-5-32-544' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } } $wellKnownSIDs = @{ 'S-1-0' = [PSCustomObject] @{ Name = 'Null AUTHORITY' SID = 'S-1-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-0-0' = [PSCustomObject] @{ Name = 'NULL SID' SID = 'S-1-0-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-1' = [PSCustomObject] @{ Name = 'WORLD AUTHORITY' SID = 'S-1-1' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-1-0' = [PSCustomObject] @{ Name = 'Everyone' SID = 'S-1-1-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-2' = [PSCustomObject] @{ Name = 'LOCAL AUTHORITY' SID = 'S-1-2' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-2-0' = [PSCustomObject] @{ Name = 'LOCAL' SID = 'S-1-2-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-2-1' = [PSCustomObject] @{ Name = 'CONSOLE LOGON' SID = 'S-1-2-1' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-3' = [PSCustomObject] @{ Name = 'CREATOR AUTHORITY' SID = 'S-1-3' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-3-0' = [PSCustomObject] @{ Name = 'CREATOR OWNER' SID = 'S-1-3-0' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'S-1-3-1' = [PSCustomObject] @{ Name = 'CREATOR GROUP' SID = 'S-1-3-1' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-3-2' = [PSCustomObject] @{ Name = 'CREATOR OWNER SERVER' SID = 'S-1-3-2' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-3-3' = [PSCustomObject] @{ Name = 'CREATOR GROUP SERVER' SID = 'S-1-3-3' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-3-4' = [PSCustomObject] @{ Name = 'OWNER RIGHTS' SID = 'S-1-3-4' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-0' = [PSCustomObject] @{ Name = 'NT SERVICE\ALL SERVICES' SID = 'S-1-5-80-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-4' = [PSCustomObject] @{ Name = 'Non-unique Authority' SID = 'S-1-4' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5' = [PSCustomObject] @{ Name = 'NT AUTHORITY' SID = 'S-1-5' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-1' = [PSCustomObject] @{ Name = 'NT AUTHORITY\DIALUP' SID = 'S-1-5-1' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-2' = [PSCustomObject] @{ Name = 'NT AUTHORITY\NETWORK' SID = 'S-1-5-2' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-3' = [PSCustomObject] @{ Name = 'NT AUTHORITY\BATCH' SID = 'S-1-5-3' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-4' = [PSCustomObject] @{ Name = 'NT AUTHORITY\INTERACTIVE' SID = 'S-1-5-4' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-6' = [PSCustomObject] @{ Name = 'NT AUTHORITY\SERVICE' SID = 'S-1-5-6' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-7' = [PSCustomObject] @{ Name = 'NT AUTHORITY\ANONYMOUS LOGON' SID = 'S-1-5-7' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-8' = [PSCustomObject] @{ Name = 'NT AUTHORITY\PROXY' SID = 'S-1-5-8' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-9' = [PSCustomObject] @{ Name = 'NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS' SID = 'S-1-5-9' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-10' = [PSCustomObject] @{ Name = 'NT AUTHORITY\SELF' SID = 'S-1-5-10' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-11' = [PSCustomObject] @{ Name = 'NT AUTHORITY\Authenticated Users' SID = 'S-1-5-11' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-12' = [PSCustomObject] @{ Name = 'NT AUTHORITY\RESTRICTED' SID = 'S-1-5-12' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-13' = [PSCustomObject] @{ Name = 'NT AUTHORITY\TERMINAL SERVER USER' SID = 'S-1-5-13' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-14' = [PSCustomObject] @{ Name = 'NT AUTHORITY\REMOTE INTERACTIVE LOGON' SID = 'S-1-5-14' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-15' = [PSCustomObject] @{ Name = 'NT AUTHORITY\This Organization' SID = 'S-1-5-15' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-17' = [PSCustomObject] @{ Name = 'NT AUTHORITY\IUSR' SID = 'S-1-5-17' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-18' = [PSCustomObject] @{ Name = 'NT AUTHORITY\SYSTEM' SID = 'S-1-5-18' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'S-1-5-19' = [PSCustomObject] @{ Name = 'NT AUTHORITY\LOCAL SERVICE' SID = 'S-1-5-19' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-20' = [PSCustomObject] @{ Name = 'NT AUTHORITY\NETWORK SERVICE' SID = 'S-1-5-20' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-544' = [PSCustomObject] @{ Name = 'BUILTIN\Administrators' SID = 'S-1-5-32-544' DomainName = '' Type = 'WellKnownAdministrative' Error = '' } 'S-1-5-32-545' = [PSCustomObject] @{ Name = 'BUILTIN\Users' SID = 'S-1-5-32-545' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-546' = [PSCustomObject] @{ Name = 'BUILTIN\Guests' SID = 'S-1-5-32-546' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-547' = [PSCustomObject] @{ Name = 'BUILTIN\Power Users' SID = 'S-1-5-32-547' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-548' = [PSCustomObject] @{ Name = 'BUILTIN\Account Operators' SID = 'S-1-5-32-548' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-549' = [PSCustomObject] @{ Name = 'BUILTIN\Server Operators' SID = 'S-1-5-32-549' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-550' = [PSCustomObject] @{ Name = 'BUILTIN\Print Operators' SID = 'S-1-5-32-550' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-551' = [PSCustomObject] @{ Name = 'BUILTIN\Backup Operators' SID = 'S-1-5-32-551' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-552' = [PSCustomObject] @{ Name = 'BUILTIN\Replicators' SID = 'S-1-5-32-552' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-64-10' = [PSCustomObject] @{ Name = 'NT AUTHORITY\NTLM Authentication' SID = 'S-1-5-64-10' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-64-14' = [PSCustomObject] @{ Name = 'NT AUTHORITY\SChannel Authentication' SID = 'S-1-5-64-14' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-64-21' = [PSCustomObject] @{ Name = 'NT AUTHORITY\Digest Authentication' SID = 'S-1-5-64-21' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80' = [PSCustomObject] @{ Name = 'NT SERVICE' SID = 'S-1-5-80' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-83-0' = [PSCustomObject] @{ Name = 'NT VIRTUAL MACHINE\Virtual Machines' SID = 'S-1-5-83-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-0' = [PSCustomObject] @{ Name = 'Untrusted Mandatory Level' SID = 'S-1-16-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-4096' = [PSCustomObject] @{ Name = 'Low Mandatory Level' SID = 'S-1-16-4096' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-8192' = [PSCustomObject] @{ Name = 'Medium Mandatory Level' SID = 'S-1-16-8192' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-8448' = [PSCustomObject] @{ Name = 'Medium Plus Mandatory Level' SID = 'S-1-16-8448' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-12288' = [PSCustomObject] @{ Name = 'High Mandatory Level' SID = 'S-1-16-12288' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-16384' = [PSCustomObject] @{ Name = 'System Mandatory Level' SID = 'S-1-16-16384' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-20480' = [PSCustomObject] @{ Name = 'Protected Process Mandatory Level' SID = 'S-1-16-20480' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-16-28672' = [PSCustomObject] @{ Name = 'Secure Process Mandatory Level' SID = 'S-1-16-28672' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-554' = [PSCustomObject] @{ Name = 'BUILTIN\Pre-Windows 2000 Compatible Access' SID = 'S-1-5-32-554' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-555' = [PSCustomObject] @{ Name = 'BUILTIN\Remote Desktop Users' SID = 'S-1-5-32-555' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-556' = [PSCustomObject] @{ Name = 'BUILTIN\Network Configuration Operators' SID = 'S-1-5-32-556' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-557' = [PSCustomObject] @{ Name = 'BUILTIN\Incoming Forest Trust Builders' SID = 'S-1-5-32-557' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-558' = [PSCustomObject] @{ Name = 'BUILTIN\Performance Monitor Users' SID = 'S-1-5-32-558' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-559' = [PSCustomObject] @{ Name = 'BUILTIN\Performance Log Users' SID = 'S-1-5-32-559' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-560' = [PSCustomObject] @{ Name = 'BUILTIN\Windows Authorization Access Group' SID = 'S-1-5-32-560' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-561' = [PSCustomObject] @{ Name = 'BUILTIN\Terminal Server License Servers' SID = 'S-1-5-32-561' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-562' = [PSCustomObject] @{ Name = 'BUILTIN\Distributed COM Users' SID = 'S-1-5-32-562' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-568' = [PSCustomObject] @{ Name = 'BUILTIN\IIS_IUSRS' SID = 'S-1-5-32-568' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-569' = [PSCustomObject] @{ Name = 'BUILTIN\Cryptographic Operators' SID = 'S-1-5-32-569' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-573' = [PSCustomObject] @{ Name = 'BUILTIN\Event Log Readers' SID = 'S-1-5-32-573' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-574' = [PSCustomObject] @{ Name = 'BUILTIN\Certificate Service DCOM Access' SID = 'S-1-5-32-574' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-575' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Remote Access Servers' SID = 'S-1-5-32-575' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-576' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Endpoint Servers' SID = 'S-1-5-32-576' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-577' = [PSCustomObject] @{ Name = 'BUILTIN\RDS Management Servers' SID = 'S-1-5-32-577' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-578' = [PSCustomObject] @{ Name = 'BUILTIN\Hyper-V Administrators' SID = 'S-1-5-32-578' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-579' = [PSCustomObject] @{ Name = 'BUILTIN\Access Control Assistance Operators' SID = 'S-1-5-32-579' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-32-580' = [PSCustomObject] @{ Name = 'BUILTIN\Remote Management Users' SID = 'S-1-5-32-580' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-90-0' = [PSCustomObject] @{ Name = 'Window Manager\Window Manager Group' SID = 'S-1-5-90-0' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420' = [PSCustomObject] @{ Name = 'NT SERVICE\WdiServiceHost' SID = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003' = [PSCustomObject] @{ Name = 'NT SERVICE\MSSQLSERVER' SID = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430' = [PSCustomObject] @{ Name = 'NT SERVICE\SQLSERVERAGENT' SID = 'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775' = [PSCustomObject] @{ Name = 'NT SERVICE\SQLTELEMETRY' SID = 'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451' = [PSCustomObject] @{ Name = 'NT SERVICE\ADSync' SID = 'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451' DomainName = '' Type = 'WellKnownGroup' Error = '' } 'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083' = [PSCustomObject] @{ Name = 'NT Service\himds' SID = 'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083' DomainName = '' Type = 'WellKnownGroup' Error = '' } } foreach ($S in $SID) { if ($OnlyWellKnownAdministrative) { if ($WellKnownAdministrative[$S]) { $WellKnownAdministrative[$S] } } elseif ($OnlyWellKnown) { if ($wellKnownSIDs[$S]) { $wellKnownSIDs[$S] } } else { if ($wellKnownSIDs[$S]) { $wellKnownSIDs[$S] } else { if ($DoNotResolve) { if ($S -like "S-1-5-21-*-519" -or $S -like "S-1-5-21-*-512" -or $S -like "S-1-5-21-*-518") { [PSCustomObject] @{ Name = $S SID = $S DomainName = '' Type = 'Administrative' Error = '' } } else { [PSCustomObject] @{ Name = $S SID = $S DomainName = '' Error = '' Type = 'NotAdministrative' } } } else { if (-not $Script:LocalComputerSID) { $Script:LocalComputerSID = Get-LocalComputerSid } try { if ($S.Length -le 18) { $Type = 'NotAdministrative' $Name = (([System.Security.Principal.SecurityIdentifier]::new($S)).Translate([System.Security.Principal.NTAccount])).Value [PSCustomObject] @{ Name = $Name SID = $S DomainName = '' Type = $Type Error = '' } } else { if ($S -like "S-1-5-21-*-519" -or $S -like "S-1-5-21-*-512" -or $S -like "S-1-5-21-*-518") { $Type = 'Administrative' } else { $Type = 'NotAdministrative' } $Name = (([System.Security.Principal.SecurityIdentifier]::new($S)).Translate([System.Security.Principal.NTAccount])).Value [PSCustomObject] @{ Name = $Name SID = $S DomainName = if ($S -like "$Script:LocalComputerSID*") { '' } else { (ConvertFrom-NetbiosName -Identity $Name).DomainName } Type = $Type Error = '' } } } catch { [PSCustomObject] @{ Name = $S SID = $S DomainName = '' Error = $_.Exception.Message -replace [environment]::NewLine, ' ' Type = 'Unknown' } } } } } } } function ConvertTo-DistinguishedName { <# .SYNOPSIS Converts CanonicalName to DistinguishedName .DESCRIPTION Converts CanonicalName to DistinguishedName for 3 different options .PARAMETER CanonicalName One or multiple canonical names .PARAMETER ToOU Converts CanonicalName to OrganizationalUnit DistinguishedName .PARAMETER ToObject Converts CanonicalName to Full Object DistinguishedName .PARAMETER ToDomain Converts CanonicalName to Domain DistinguishedName .EXAMPLE $CanonicalObjects = @( 'ad.evotec.xyz/Production/Groups/Security/ITR03_AD Admins' 'ad.evotec.xyz/Production/Accounts/Special/SADM Testing 2' ) $CanonicalOU = @( 'ad.evotec.xyz/Production/Groups/Security/NetworkAdministration' 'ad.evotec.xyz/Production' ) $CanonicalDomain = @( 'ad.evotec.xyz/Production/Groups/Security/ITR03_AD Admins' 'ad.evotec.pl' 'ad.evotec.xyz' 'test.evotec.pl' 'ad.evotec.xyz/Production' ) $CanonicalObjects | ConvertTo-DistinguishedName -ToObject $CanonicalOU | ConvertTo-DistinguishedName -ToOU $CanonicalDomain | ConvertTo-DistinguishedName -ToDomain Output: CN=ITR03_AD Admins,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz CN=SADM Testing 2,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz Output2: OU=NetworkAdministration,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz OU=Production,DC=ad,DC=evotec,DC=xyz Output3: DC=ad,DC=evotec,DC=xyz DC=ad,DC=evotec,DC=pl DC=ad,DC=evotec,DC=xyz DC=test,DC=evotec,DC=pl DC=ad,DC=evotec,DC=xyz .NOTES General notes #> [cmdletBinding(DefaultParameterSetName = 'ToDomain')] param( [Parameter(ParameterSetName = 'ToOU')] [Parameter(ParameterSetName = 'ToObject')] [Parameter(ParameterSetName = 'ToDomain')] [alias('Identity', 'CN')][Parameter(ValueFromPipeline, Mandatory, ValueFromPipelineByPropertyName, Position = 0)][string[]] $CanonicalName, [Parameter(ParameterSetName = 'ToOU')][switch] $ToOU, [Parameter(ParameterSetName = 'ToObject')][switch] $ToObject, [Parameter(ParameterSetName = 'ToDomain')][switch] $ToDomain ) Process { foreach ($CN in $CanonicalName) { if ($ToObject) { $ADObject = $CN.Replace(',', '\,').Split('/') [string]$DN = "CN=" + $ADObject[$ADObject.count - 1] for ($i = $ADObject.count - 2; $i -ge 1; $i--) { $DN += ",OU=" + $ADObject[$i] } $ADObject[0].split(".") | ForEach-Object { $DN += ",DC=" + $_ } } elseif ($ToOU) { $ADObject = $CN.Replace(',', '\,').Split('/') [string]$DN = "OU=" + $ADObject[$ADObject.count - 1] for ($i = $ADObject.count - 2; $i -ge 1; $i--) { $DN += ",OU=" + $ADObject[$i] } $ADObject[0].split(".") | ForEach-Object { $DN += ",DC=" + $_ } } else { $ADObject = $CN.Replace(',', '\,').Split('/') $DN = 'DC=' + $ADObject[0].Replace('.', ',DC=') } $DN } } } function Get-ADEncryptionTypes { <# .SYNOPSIS Retrieves the supported encryption types based on the specified value. .DESCRIPTION This function returns the list of encryption types supported by Active Directory based on the provided value. Each encryption type is represented by a string. .PARAMETER Value Specifies the integer value representing the encryption types to retrieve. .EXAMPLE Get-ADEncryptionTypes -Value 24 Retrieves the following encryption types: - AES128-CTS-HMAC-SHA1-96 - AES256-CTS-HMAC-SHA1-96 .NOTES This function is designed to provide information about encryption types supported by Active Directory. #> [cmdletbinding()] Param( [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value ) [String[]]$EncryptionTypes = @( Foreach ($V in $Value) { if ([int32]$V -band 0x00000001) { "DES-CBC-CRC" } if ([int32]$V -band 0x00000002) { "DES-CBC-MD5" } if ([int32]$V -band 0x00000004) { "RC4-HMAC" } if ([int32]$V -band 0x00000008) { "AES128-CTS-HMAC-SHA1-96" } if ([int32]$V -band 0x00000010) { "AES256-CTS-HMAC-SHA1-96" } if ([int32]$V -band 0x00000020) { "FAST-supported" } if ([int32]$V -band 0x00000040) { "Compound-identity-supported" } if ([int32]$V -band 0x00000080) { "Claims-supported" } if ([int32]$V -band 0x00000200) { "Resource-SID-compression-disabled" } } ) $EncryptionTypes } function Get-ADTrustAttributes { <# .SYNOPSIS Retrieves and interprets Active Directory trust attributes based on the provided value. .DESCRIPTION This function retrieves and interprets Active Directory trust attributes based on the provided value. It decodes the binary value into human-readable trust attributes. .PARAMETER Value Specifies the integer value representing the trust attributes. .EXAMPLE Get-ADTrustAttributes -Value 1 Retrieves and interprets the trust attributes for the value 1. .EXAMPLE 1, 2, 4 | Get-ADTrustAttributes Retrieves and interprets the trust attributes for the values 1, 2, and 4. .NOTES This function provides a convenient way to decode Active Directory trust attributes. #> [cmdletbinding()] Param( [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value ) [String[]]$TrustAttributes = @( Foreach ($V in $Value) { if ([int32]$V -band 0x00000001) { "Non Transitive" } if ([int32]$V -band 0x00000002) { "UpLevel Only" } if ([int32]$V -band 0x00000004) { "Quarantined Domain" } if ([int32]$V -band 0x00000008) { "Forest Transitive" } if ([int32]$V -band 0x00000010) { "Cross Organization" } if ([int32]$V -band 0x00000020) { "Within Forest" } if ([int32]$V -band 0x00000040) { "Treat as External" } if ([int32]$V -band 0x00000080) { "Uses RC4 Encryption" } if ([int32]$V -band 0x00000200) { "No TGT DELEGATION" } if ([int32]$V -band 0x00000800) { "Enable TGT DELEGATION" } if ([int32]$V -band 0x00000400) { "PIM Trust" } } ) return $TrustAttributes } function Get-ForestTrustInfo { <# .SYNOPSIS Retrieves and processes forest trust information from an array of bytes. .DESCRIPTION This function retrieves and processes forest trust information from the provided array of bytes. .PARAMETER Byte An array of bytes containing the forest trust information to be processed. .EXAMPLE Get-ForestTrustInfo -Byte $ByteData Retrieves and processes forest trust information from the specified array of bytes. .NOTES Author: Chris Dent #> [CmdletBinding()] param ( [Parameter(Mandatory)][byte[]]$Byte ) $reader = [System.IO.BinaryReader][System.IO.MemoryStream]$Byte $trustInfo = [PSCustomObject]@{ Version = $reader.ReadUInt32() RecordCount = $reader.ReadUInt32() Records = $null } $trustInfo.Records = for ($i = 0; $i -lt $trustInfo.RecordCount; $i++) { Get-ForestTrustRecord -BinaryReader $reader } $trustInfo } function Get-LocalComputerSid { <# .SYNOPSIS Get the SID of the local computer. .DESCRIPTION Get the SID of the local computer. .EXAMPLE Get-LocalComputerSid .NOTES General notes #> [cmdletBinding()] param() try { Add-Type -AssemblyName System.DirectoryServices.AccountManagement $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Machine) $UserPrincipal = [System.DirectoryServices.AccountManagement.UserPrincipal]::new($PrincipalContext) $Searcher = [System.DirectoryServices.AccountManagement.PrincipalSearcher]::new() $Searcher.QueryFilter = $UserPrincipal $User = $Searcher.FindAll() foreach ($U in $User) { if ($U.Sid.Value -like "*-500") { return $U.Sid.Value.TrimEnd("-500") } } } catch { Write-Warning -Message "Get-LocalComputerSid - Error: $($_.Exception.Message)" } } function Get-ForestTrustRecord { <# .SYNOPSIS Retrieves and processes forest trust record information. .DESCRIPTION This function retrieves and processes forest trust record information from the provided BinaryReader. .PARAMETER BinaryReader Specifies the BinaryReader object containing the forest trust record information. .EXAMPLE Get-ForestTrustRecord -BinaryReader $BinaryReader Retrieves and processes forest trust record information from the BinaryReader object. .NOTES Author: Chris Dent #> [CmdletBinding()] param ( [Parameter(Mandatory)][System.IO.BinaryReader]$BinaryReader ) [Flags()] enum TrustFlags { LsaTlnDisabledNew = 0x1 LsaTlnDisabledAdmin = 0x2 LsaTlnDisabledConflict = 0x4 } [Flags()] enum ForestTrustFlags { LsaSidDisabledAdmin = 0x1 LsaSidDisabledConflict = 0x2 LsaNBDisabledAdmin = 0x4 LsaNBDisabledConflict = 0x8 } enum RecordType { ForestTrustTopLevelName ForestTrustTopLevelNameEx ForestTrustDomainInfo } $record = [PSCustomObject]@{ RecordLength = $BinaryReader.ReadUInt32() Flags = $BinaryReader.ReadUInt32() Timestamp = $BinaryReader.ReadUInt32(), $BinaryReader.ReadUInt32() RecordType = $BinaryReader.ReadByte() -as [RecordType] ForestTrustData = $null } $record.Timestamp = [DateTime]::FromFileTimeUtc( ($record.Timestamp[0] -as [UInt64] -shl 32) + $record.Timestamp[1] ) $record.Flags = switch ($record.RecordType) { ([RecordType]::ForestTrustDomainInfo) { $record.Flags -as [ForestTrustFlags] } default { $record.Flags -as [TrustFlags] } } if ($record.RecordLength -gt 11) { switch ($record.RecordType) { ([RecordType]::ForestTrustDomainInfo) { $record.ForestTrustData = [PSCustomObject]@{ Sid = $null DnsName = $null NetbiosName = $null } $sidLength = $BinaryReader.ReadUInt32() if ($sidLength -gt 0) { $record.ForestTrustData.Sid = [System.Security.Principal.SecurityIdentifier]::new( $BinaryReader.ReadBytes($sidLength), 0 ) } $dnsNameLen = $BinaryReader.ReadUInt32() if ($dnsNameLen -gt 0) { $record.ForestTrustData.DnsName = [string]::new($BinaryReader.ReadBytes($dnsNameLen) -as [char[]]) } $NetbiosNameLen = $BinaryReader.ReadUInt32() if ($NetbiosNameLen -gt 0) { $record.ForestTrustData.NetbiosName = [string]::new($BinaryReader.ReadBytes($NetbiosNameLen) -as [char[]]) } } default { $nameLength = $BinaryReader.ReadUInt32() if ($nameLength -gt 0) { $record.ForestTrustData = [String]::new($BinaryReader.ReadBytes($nameLength) -as [char[]]) } } } } $record } function Assert-InitialSettings { [cmdletbinding()] param( [System.Collections.IDictionary] $DisableOnlyIf, [System.Collections.IDictionary] $MoveOnlyIf, [System.Collections.IDictionary] $DeleteOnlyIf ) $AzureRequired = $false $IntuneRequired = $false $JamfRequired = $false if ($DisableOnlyIf) { if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($MoveOnlyIf) { if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($DeleteOnlyIf) { if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($AzureRequired -or $IntuneRequired) { $ModuleAvailable = Get-Module -Name GraphEssentials -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 if (-not $ModuleAvailable) { Write-Color -Text "[e] ", "'GraphEssentials' module is required but not available. Terminating." -Color Yellow, Red return $false } $ModuleVersion = [version]'0.0.46' if ($ModuleAvailable.Version -lt $ModuleVersion) { Write-Color -Text "[e] ", "'GraphEssentials' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red return $false } } if ($JamfRequired) { $ModuleAvailable = Get-Module -Name PowerJamf -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1 if (-not $ModuleAvailable) { Write-Color -Text "[e] ", "'PowerJamf' module is required but not available. Terminating." -Color Yellow, Red return $false } $ModuleVersion = [version]'0.3.1' if ($ModuleAvailable.Version -lt $ModuleVersion) { Write-Color -Text "[e] ", "'PowerJamf' module is outdated. Please update to the latest version minimum '$ModuleVersion'. Terminating." -Color Yellow, Red return $false } } } function Convert-ListProcessed { <# .SYNOPSIS This function converts old format of writting down pending list with DN to new format with SamAccountName with domain. .DESCRIPTION This function converts old format of writting down pending list with DN to new format with SamAccountName with domain. The old way would probably break if someone would move computer after disabling which would cause the computer to be removed from the list. .PARAMETER FileImport Hashtable with PendingDeletion and History keys. .EXAMPLE $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop # convert old format to new format $FileImport = Convert-ListProcessed -FileImport $FileImport .NOTES General notes #> [CmdletBinding()] param( [System.Collections.IDictionary] $FileImport ) if (-not $FileImport.Contains('PendingDeletion') -and $FileImport.PendingDeletion.Keys.Count -eq 0) { return $FileImport } if ($FileImport.PendingDeletion.Keys[0] -like "*@*") { # Write-Color -Text "[i] ", "List is already converted. Terminating." -Color Yellow, Green return $FileImport } Write-Color -Text "[i] ", "Converting list to new format." -Color Yellow, Green foreach ($Key in [string[]] $FileImport.PendingDeletion.Keys) { $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $FileImport.PendingDeletion[$Key].DistinguishedName -ToDomainCN $NewKey = -join ($FileImport.PendingDeletion[$Key].SamAccountName, "@", $DomainName) $FileImport.PendingDeletion[$NewKey] = $FileImport.PendingDeletion[$Key] $null = $FileImport.PendingDeletion.Remove($Key) } Write-Color -Text "[i] ", "List converted." -Color Yellow, Green $FileImport } function ConvertTo-PreparedComputer { [CmdletBinding()] param( [Microsoft.ActiveDirectory.Management.ADComputer[]] $Computers, [System.Collections.IDictionary] $AzureInformationCache, [System.Collections.IDictionary] $JamfInformationCache, [switch] $IncludeAzureAD, [switch] $IncludeIntune, [switch] $IncludeJamf ) foreach ($Computer in $Computers) { if ($IncludeAzureAD) { $AzureADComputer = $AzureInformationCache['AzureAD']["$($Computer.Name)"] $DataAzureAD = [ordered] @{ 'AzureLastSeen' = $AzureADComputer.LastSeen 'AzureLastSeenDays' = $AzureADComputer.LastSeenDays 'AzureLastSync' = $AzureADComputer.LastSynchronized 'AzureLastSyncDays' = $AzureADComputer.LastSynchronizedDays 'AzureOwner' = $AzureADComputer.OwnerDisplayName 'AzureOwnerStatus' = $AzureADComputer.OwnerEnabled 'AzureOwnerUPN' = $AzureADComputer.OwnerUserPrincipalName } } if ($IncludeIntune) { # data was requested from Intune $IntuneComputer = $AzureInformationCache['Intune']["$($Computer.Name)"] $DataIntune = [ordered] @{ 'IntuneLastSeen' = $IntuneComputer.LastSeen 'IntuneLastSeenDays' = $IntuneComputer.LastSeenDays 'IntuneUser' = $IntuneComputer.UserDisplayName 'IntuneUserUPN' = $IntuneComputer.UserPrincipalName 'IntuneUserEmail' = $IntuneComputer.EmailAddress } } if ($IncludeJamf) { $JamfComputer = $JamfInformationCache["$($Computer.Name)"] $DataJamf = [ordered] @{ JamfLastContactTime = $JamfComputer.lastContactTime JamfLastContactTimeDays = $JamfComputer.lastContactTimeDays JamfCapableUsers = $JamfComputer.mdmCapableCapableUsers } } $LastLogonDays = if ($null -ne $Computer.LastLogonDate) { - $($Computer.LastLogonDate - $Today).Days } else { $null } $PasswordLastChangedDays = if ($null -ne $Computer.PasswordLastSet) { - $($Computer.PasswordLastSet - $Today).Days } else { $null } $DataStart = [ordered] @{ 'DNSHostName' = $Computer.DNSHostName 'SamAccountName' = $Computer.SamAccountName 'DomainName' = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN 'Enabled' = $Computer.Enabled 'Action' = 'Not required' 'ActionStatus' = $null 'ActionDate' = $null 'ActionComment' = $null 'OperatingSystem' = $Computer.OperatingSystem 'OperatingSystemVersion' = $Computer.OperatingSystemVersion 'OperatingSystemLong' = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion 'LastLogonDate' = $Computer.LastLogonDate 'LastLogonDays' = $LastLogonDays 'PasswordLastSet' = $Computer.PasswordLastSet 'PasswordLastChangedDays' = $PasswordLastChangedDays 'ProtectedFromAccidentalDeletion' = $Computer.ProtectedFromAccidentalDeletion } $DataEnd = [ordered] @{ 'PasswordExpired' = $Computer.PasswordExpired 'LogonCount' = $Computer.logonCount 'ManagedBy' = $Computer.ManagedBy 'DistinguishedName' = $Computer.DistinguishedName 'OrganizationalUnit' = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToOrganizationalUnit 'Description' = $Computer.Description 'WhenCreated' = $Computer.WhenCreated 'WhenChanged' = $Computer.WhenChanged 'ServicePrincipalName' = $Computer.servicePrincipalName #-join [System.Environment]::NewLine 'DistinguishedNameAfterMove' = $null 'TimeOnPendingList' = $null 'TimeToLeavePendingList' = $null } if ($IncludeAzureAD -and $IncludeIntune -and $IncludeJamf) { $Data = $DataStart + $DataAzureAD + $DataIntune + $DataJamf + $DataEnd } elseif ($IncludeAzureAD -and $IncludeIntune) { $Data = $DataStart + $DataAzureAD + $DataIntune + $DataEnd } elseif ($IncludeAzureAD -and $IncludeJamf) { $Data = $DataStart + $DataAzureAD + $DataJamf + $DataEnd } elseif ($IncludeIntune -and $IncludeJamf) { $Data = $DataStart + $DataIntune + $DataJamf + $DataEnd } elseif ($IncludeAzureAD) { $Data = $DataStart + $DataAzureAD + $DataEnd } elseif ($IncludeIntune) { $Data = $DataStart + $DataIntune + $DataEnd } elseif ($IncludeJamf) { $Data = $DataStart + $DataJamf + $DataEnd } else { $Data = $DataStart + $DataEnd } [PSCustomObject] $Data } } function Disable-WinADComputer { [CmdletBinding(SupportsShouldProcess)] param( [bool] $Success, [switch] $WhatIfDisable, [switch] $DontWriteToEventLog, [PSCustomObject] $Computer, [string] $Server ) if ($Success) { if ($Computer.Enabled -eq $true) { 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 try { if ($Computer.DistinguishedNameAfterMove) { $DN = $Computer.DistinguishedNameAfterMove } else { $DN = $Computer.DistinguishedName } Disable-ADAccount -Identity $DN -Server $Server -WhatIf:$WhatIfDisable -ErrorAction Stop Write-Color -Text "[+] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) successful." -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Success = $true } catch { $Computer.ActionComment = $_.Exception.Message $Success = $false Write-Color -Text "[-] Disabling computer ", $DN, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } } } else { Write-Color -Text "[i] Computer ", $Computer.SamAccountName, " is already disabled." -Color Yellow, Green, Yellow } } $Success } function Get-ADComputersToProcess { [CmdletBinding()] param( [parameter(Mandatory)][ValidateSet('Disable', 'Move', 'Delete')][string] $Type, [Array] $Computers, [alias('DeleteOnlyIf', 'DisableOnlyIf', 'MoveOnlyIf')][System.Collections.IDictionary] $ActionIf, [Array] $Exclusions, [System.Collections.IDictionary] $DomainInformation, [System.Collections.IDictionary] $ProcessedComputers, [System.Collections.IDictionary] $AzureInformationCache, [System.Collections.IDictionary] $JamfInformationCache, [switch] $IncludeAzureAD, [switch] $IncludeIntune, [switch] $IncludeJamf ) Write-Color -Text "[i] ", "Applying following rules to $Type action: " -Color Yellow, Cyan, Green foreach ($Key in $ActionIf.Keys) { if ($null -eq $ActionIf[$Key] -or $ActionIf[$Key].Count -eq 0) { Write-Color -Text " [>] ", $($Key), " is ", 'Not Set' -Color Yellow, Cyan, Yellow } else { if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') { Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never logged on" -Color Yellow, Cyan, Green } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') { Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never changed" -Color Yellow, Cyan, Green } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') { Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]), " or ", "Never synced/seen" -Color Yellow, Cyan, Green } else { Write-Color -Text " [>] ", $($Key), " is ", $($ActionIf[$Key]) -Color Yellow, Cyan, Green } } } $Count = 0 $Today = Get-Date # Let's cache the destination move, if we have them $CachedDestinationMove = @{} if ($ActionIf.MoveTargetOrganizationalUnit -is [string]) { $Domain = ConvertFrom-DistinguishedName -DistinguishedName $ActionIf.MoveTargetOrganizationalUnit -ToDomainCN $CachedDestinationMove[$ActionIf.MoveTargetOrganizationalUnit] = $Domain } elseif ($ActionIf.MoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) { foreach ($Domain in $ActionIf.MoveTargetOrganizationalUnit.Keys) { $OU = $ActionIf.MoveTargetOrganizationalUnit[$Domain] $CachedDestinationMove[$OU] = $Domain } } Write-Color -Text "[i] ", "Looking for computers to $Type" -Color Yellow, Cyan, Green :SkipComputer foreach ($Computer in $Computers) { if ($Type -eq 'Delete') { # actions to happen only if we are deleting computers if ($null -ne $ActionIf.ListProcessedMoreThan) { # if more then 0 this means computer has to be on list of disabled computers for that number of days. if ($ProcessedComputers.Count -gt 0) { $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)" $FoundComputer = $ProcessedComputers[$FullComputerName] if ($FoundComputer) { if ($FoundComputer.ActionDate -is [DateTime]) { $TimeSpan = New-TimeSpan -Start $FoundComputer.ActionDate -End $Today # Lets calculate how many days it's been on the list $ProcessedComputers[$FullComputerName].TimeToLeavePendingList = $ActionIf.ListProcessedMoreThan - $TimeSpan.Days if ($TimeSpan.Days -gt $ActionIf.ListProcessedMoreThan) { } else { continue SkipComputer } } else { continue SkipComputer } } else { continue SkipComputer } } else { # ListProcessed doesn't have members, and it's part of requirement break } } } if ($Type -eq 'Disable') { # actions to happen only if we are disabling computers if ($ProcessedComputers.Count -gt 0) { $FullComputerName = "$($Computer.SamAccountName)@$($Computer.DomainName)" $FoundComputer = $ProcessedComputers[$FullComputerName] if ($FoundComputer) { if ($Computer.Enabled -eq $true) { # We checked and it seems the computer has been enabled since it was added to list, we remove it from the list and reprocess Write-Color -Text "[*] Removing computer from pending list (computer is enabled) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow $ProcessedComputers.Remove($FullComputerName) } elseif ($ActionIf.DisableAndMove -and $Computer.Enabled -eq $false) { if ($ActionIf.MoveTargetOrganizationalUnit) { if (-not $CachedDestinationMove[$Computer.OrganizationalUnit]) { # We checked and it seems the computer has been moved since it was added to list, we remove it from the list and reprocess Write-Color -Text "[*] Removing computer from pending list (computer is moved out of pending deletion OU) ", $FoundComputer.SamAccountName, " ($($FoundComputer.DistinguishedName))" -Color DarkYellow, Green, DarkYellow $ProcessedComputers.Remove($FullComputerName) } else { # We checked and it seems the computer is in place where it's supposed to, we skip to next computer continue SkipComputer } } else { # We checked and it seems the computer is in place where it's supposed to, we skip to next computer continue SkipComputer } } else { # we skip adding to disabled because it's already on the list for removing continue SkipComputer } } } } # rest of actions are same for all types foreach ($PartialExclusion in $Exclusions) { if ($Computer.DistinguishedName -like "$PartialExclusion") { $Computer.'Action' = 'ExcludedByFilter' continue SkipComputer } if ($Computer.SamAccountName -like "$PartialExclusion") { $Computer.'Action' = 'ExcludedByFilter' continue SkipComputer } if ($Computer.DNSHostName -like "$PartialExclusion") { $Computer.'Action' = 'ExcludedByFilter' continue SkipComputer } } if ($ActionIf.IncludeSystems.Count -gt 0) { $FoundInclude = $false foreach ($Include in $ActionIf.IncludeSystems) { if ($Computer.OperatingSystem -like $Include) { $FoundInclude = $true break } } # If not found in includes we need to skip the computer if (-not $FoundInclude) { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } if ($ActionIf.ExcludeServicePrincipalName.Count -gt 0) { foreach ($ExcludeSPN in $ActionIf.ExcludeServicePrincipalName) { if ($Computer.servicePrincipalName -like "$ExcludeSPN") { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } } if ($ActionIf.IncludeServicePrincipalName.Count -gt 0) { $FoundInclude = $false foreach ($IncludeSPN in $ActionIf.IncludeServicePrincipalName) { if ($Computer.servicePrincipalName -like "$IncludeSPN") { $FoundInclude = $true break } } # If not found in includes we need to skip the computer if (-not $FoundInclude) { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } if ($ActionIf.ExcludeSystems.Count -gt 0) { foreach ($Exclude in $ActionIf.ExcludeSystems) { if ($Computer.OperatingSystem -like $Exclude) { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } } if ($ActionIf.NoServicePrincipalName -eq $true) { # action computer only if it has no service principal names defined if ($Computer.servicePrincipalName.Count -gt 0) { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } elseif ($ActionIf.NoServicePrincipalName -eq $false) { # action computer only if it has service principal names defined if ($Computer.servicePrincipalName.Count -eq 0) { $Computer.'Action' = 'ExcludedBySetting' continue SkipComputer } } if ($ActionIf.RequireWhenCreatedMoreThan) { # This runs only if more than 0 if ($Computer.WhenCreated) { # We ignore empty $TimeToCompare = ($Computer.WhenCreated).AddDays($ActionIf.RequireWhenCreatedMoreThan) if ($TimeToCompare -gt $Today) { continue SkipComputer } } } if ($ActionIf.IsEnabled -eq $true) { # action computer only if it's Enabled if ($Computer.Enabled -eq $false) { continue SkipComputer } } elseif ($ActionIf.IsEnabled -eq $false) { # action computer only if it's Disabled if ($Computer.Enabled -eq $true) { continue SkipComputer } } if ($ActionIf.LastLogonDateMoreThan) { # This runs only if more than 0 if ($Computer.LastLogonDate) { # We ignore empty $TimeToCompare = ($Computer.LastLogonDate).AddDays($ActionIf.LastLogonDateMoreThan) if ($TimeToCompare -gt $Today) { continue SkipComputer } } } if ($ActionIf.PasswordLastSetMoreThan) { # This runs only if more than 0 if ($Computer.PasswordLastSet) { # We ignore empty $TimeToCompare = ($Computer.PasswordLastSet).AddDays($ActionIf.PasswordLastSetMoreThan) if ($TimeToCompare -gt $Today) { continue SkipComputer } } } if ($ActionIf.PasswordLastSetOlderThan) { # This runs only if not null if ($Computer.PasswordLastSet) { # We ignore empty if ($ActionIf.PasswordLastSetOlderThan -le $Computer.PasswordLastSet) { continue SkipComputer } } } if ($ActionIf.LastLogonDateOlderThan) { # This runs only if not null if ($Computer.LastLogonDate) { # We ignore empty if ($ActionIf.LastLogonDateOlderThan -le $Computer.LastLogonDate) { continue SkipComputer } } } if ($IncludeAzureAD) { if ($null -ne $ActionIf.LastSeenAzureMoreThan -and $null -ne $Computer.AzureLastSeenDays) { if ($Computer.AzureLastSeenDays -le $ActionIf.LastSeenAzureMoreThan) { continue SkipComputer } } if ($null -ne $ActionIf.LastSyncAzureMoreThan -and $null -ne $Computer.AzureLastSyncDays) { if ($Computer.AzureLastSyncDays -le $ActionIf.LastSyncAzureMoreThan) { continue SkipComputer } } } if ($IncludeIntune) { if ($null -ne $ActionIf.LastSeenIntuneMoreThan -and $null -ne $Computer.IntuneLastSeenDays) { if ($Computer.IntuneLastSeenDays -le $ActionIf.LastSeenIntuneMoreThan) { continue SkipComputer } } } if ($IncludeJamf) { if ($null -ne $ActionIf.LastContactJamfMoreThan -and $null -ne $Computer.JamfLastContactTimeDays) { if ($Computer.JamfLastContactTimeDays -le $ActionIf.LastContactJamfMoreThan) { continue SkipComputer } } } $Computer.'Action' = $Type $Count++ } $Count } function Get-InitialADComputers { [CmdletBinding()] param( [System.Collections.IDictionary] $Report, [System.Collections.IDictionary] $ForestInformation, [object] $Filter, [object] $SearchBase, [string[]] $Properties, [bool] $Disable, [bool] $Delete, [bool] $Move, [System.Collections.IDictionary] $DisableOnlyIf, [System.Collections.IDictionary] $DeleteOnlyIf, [System.Collections.IDictionary] $MoveOnlyIf, [Array] $Exclusions, [System.Collections.IDictionary] $ProcessedComputers, [nullable[int]] $SafetyADLimit, [System.Collections.IDictionary] $AzureInformationCache, [System.Collections.IDictionary] $JamfInformationCache, [object] $TargetServers ) $AllComputers = [ordered] @{} $AzureRequired = $false $IntuneRequired = $false $JamfRequired = $false if ($DisableOnlyIf) { if ($null -ne $DisableOnlyIf.LastSyncAzureMoreThan -or $null -ne $DisableOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $DisableOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $DisableOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($MoveOnlyIf) { if ($null -ne $MoveOnlyIf.LastSyncAzureMoreThan -or $null -ne $MoveOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $MoveOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $MoveOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($DeleteOnlyIf) { if ($null -ne $DeleteOnlyIf.LastSyncAzureMoreThan -or $null -ne $DeleteOnlyIf.LastSeenAzureMoreThan) { $AzureRequired = $true } if ($null -ne $DeleteOnlyIf.LastContactJamfMoreThan) { $JamfRequired = $true } if ($null -ne $DeleteOnlyIf.LastSeenIntuneMoreThan) { $IntuneRequired = $true } } if ($TargetServers) { # User provided target servers/server. If there is only one we assume user wants to use it for all domains (hopefully just one domain) # If there are multiple we assume user wants to use different servers for different domains using hashtable/dictionary # If there is no server for a domain we will use the default server, as detected if ($TargetServers -is [string]) { $TargetServer = $TargetServers } if ($TargetServers -is [System.Collections.IDictionary]) { $TargetServerDictionary = $TargetServers[$Domain] } } $CountDomains = 0 foreach ($Domain in $ForestInformation.Domains) { $CountDomains++ $Report["$Domain"] = [ordered] @{ } $Server = $ForestInformation['QueryServers'][$Domain].HostName[0] if (-not $Server) { Write-Color "[e] ", "No server found for domain $Domain" -Color Yellow, Red continue } if ($TargetServer) { Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServer -Color Yellow, Magenta $Server = $TargetServer } elseif ($TargetServerDictionary) { if ($TargetServerDictionary[$Domain]) { Write-Color -Text "Overwritting target server for domain ", $Domain, ": ", $TargetServerDictionary[$Domain] -Color Yellow, Magenta $Server = $TargetServerDictionary[$Domain] } } $DomainInformation = $ForestInformation.DomainsExtended[$Domain] $Report["$Domain"]['Server'] = $Server 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 if ($Filter) { if ($Filter -is [string]) { $FilterToUse = $Filter } elseif ($Filter -is [System.Collections.IDictionary]) { $FilterToUse = $Filter[$Domain] } else { Write-Color "[e] ", "Filter must be a string or a hashtable/ordereddictionary" -Color Yellow, Red return $false } } else { $FilterToUse = "*" } if ($SearchBase) { if ($SearchBase -is [string]) { $SearchBaseToUse = $SearchBase } elseif ($SearchBase -is [System.Collections.IDictionary]) { $SearchBaseToUse = $SearchBase[$Domain] } else { Write-Color "[e] ", "SearchBase must be a string or a hashtable/ordereddictionary" -Color Yellow, Red return $false } } else { $SearchBaseToUse = $DomainInformation.DistinguishedName } $getADComputerSplat = @{ Filter = $FilterToUse Server = $Server Properties = $Properties ErrorAction = 'Stop' } if ($SearchBaseToUse) { $getADComputerSplat.SearchBase = $SearchBaseToUse } try { [Array] $Computers = Get-ADComputer @getADComputerSplat } catch { if ($_.Exception.Message -like "*distinguishedName must belong to one of the following partition*") { Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red 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 } else { Write-Color "[e] ", "Error getting computers for domain $($Domain): ", $_.Exception.Message -Color Yellow, Red } return $false } foreach ($Computer in $Computers) { # we will be using it later to just check if computer exists in AD $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Computer.DistinguishedName -ToDomainCN $ComputerFullName = -join ($Computer.SamAccountName, "@", $DomainName) # initially we used DN, but DN changes when moving so it wouldn't work $AllComputers[$ComputerFullName] = $Computer } $Report["$Domain"]['Computers'] = @( $convertToPreparedComputerSplat = @{ Computers = $Computers AzureInformationCache = $AzureInformationCache JamfInformationCache = $JamfInformationCache IncludeAzureAD = $AzureRequired IncludeJamf = $JamfRequired IncludeIntune = $IntuneRequired } ConvertTo-PreparedComputer @convertToPreparedComputerSplat ) Write-Color "[i] ", "Computers found for domain $Domain`: ", $($Computers.Count) -Color Yellow, Cyan, Green if ($Disable) { Write-Color "[i] ", "Processing computers to disable for domain $Domain" -Color Yellow, Cyan, Green $getADComputersToDisableSplat = @{ Computers = $Report["$Domain"]['Computers'] DisableOnlyIf = $DisableOnlyIf Exclusions = $Exclusions DomainInformation = $DomainInformation ProcessedComputers = $ProcessedComputers AzureInformationCache = $AzureInformationCache JamfInformationCache = $JamfInformationCache IncludeAzureAD = $AzureRequired IncludeJamf = $JamfRequired IncludeIntune = $IntuneRequired Type = 'Disable' } $Report["$Domain"]['ComputersToBeDisabled'] = Get-ADComputersToProcess @getADComputersToDisableSplat #Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDisabled'].Count) -Color Yellow, Cyan, Green } if ($Move) { Write-Color "[i] ", "Processing computers to move for domain $Domain" -Color Yellow, Cyan, Green $getADComputersToDeleteSplat = @{ Computers = $Report["$Domain"]['Computers'] MoveOnlyIf = $MoveOnlyIf Exclusions = $Exclusions DomainInformation = $DomainInformation ProcessedComputers = $ProcessedComputers AzureInformationCache = $AzureInformationCache JamfInformationCache = $JamfInformationCache IncludeAzureAD = $AzureRequired IncludeJamf = $JamfRequired IncludeIntune = $IntuneRequired Type = 'Move' } $Report["$Domain"]['ComputersToBeMoved'] = Get-ADComputersToProcess @getADComputersToDeleteSplat #Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeMoved'].Count) -Color Yellow, Cyan, Green } if ($Delete) { Write-Color "[i] ", "Processing computers to delete for domain $Domain" -Color Yellow, Cyan, Green $getADComputersToDeleteSplat = @{ Computers = $Report["$Domain"]['Computers'] DeleteOnlyIf = $DeleteOnlyIf Exclusions = $Exclusions DomainInformation = $DomainInformation ProcessedComputers = $ProcessedComputers AzureInformationCache = $AzureInformationCache JamfInformationCache = $JamfInformationCache IncludeAzureAD = $AzureRequired IncludeJamf = $JamfRequired IncludeIntune = $IntuneRequired Type = 'Delete' } $Report["$Domain"]['ComputersToBeDeleted'] = Get-ADComputersToProcess @getADComputersToDeleteSplat #Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $($Report["$Domain"]['ComputersToBeDeleted'].Count) -Color Yellow, Cyan, Green } } if ($null -ne $SafetyADLimit -and $AllComputers.Count -lt $SafetyADLimit) { 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 return $false } $AllComputers } function Get-InitialGraphComputers { [CmdletBinding()] param( [nullable[int]] $SafetyAzureADLimit, [nullable[int]] $SafetyIntuneLimit, [nullable[int]] $DeleteLastSeenAzureMoreThan, [nullable[int]] $DeleteLastSeenIntuneMoreThan, [nullable[int]] $DeleteLastSyncAzureMoreThan, [nullable[int]] $DisableLastSeenAzureMoreThan, [nullable[int]] $DisableLastSeenIntuneMoreThan, [nullable[int]] $DisableLastSyncAzureMoreThan, [nullable[int]] $MoveLastSeenAzureMoreThan, [nullable[int]] $MoveLastSeenIntuneMoreThan, [nullable[int]] $MoveLastSyncAzureMoreThan ) $AzureInformationCache = [ordered] @{ AzureAD = [ordered] @{} Intune = [ordered] @{} } if ($PSBoundParameters.ContainsKey('DisableLastSeenAzureMoreThan') -or $PSBoundParameters.ContainsKey('DisableLastSyncAzureMoreThan') -or $PSBoundParameters.ContainsKey('DeleteLastSeenAzureMoreThan') -or $PSBoundParameters.ContainsKey('DeleteLastSyncAzureMoreThan') -or $PSBoundParameters.ContainsKey('MoveLastSeenAzureMoreThan') -or $PSBoundParameters.ContainsKey('MoveLastSyncAzureMoreThan')) { Write-Color "[i] ", "Getting all computers from AzureAD" -Color Yellow, Cyan, Green [Array] $Devices = Get-MyDevice -Synchronized -WarningAction SilentlyContinue -WarningVariable WarningVar if ($WarningVar) { Write-Color "[e] ", "Error getting computers from AzureAD: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red return $false } if ($Devices.Count -eq 0) { Write-Color "[e] ", "No computers found in AzureAD, terminating! Please disable Azure AD integration or fix connectivity." -Color Yellow, Red return $false } foreach ($Device in $Devices) { $AzureInformationCache.AzureAD[$Device.Name] = $Device } if ($null -ne $SafetyAzureADLimit -and $Devices.Count -lt $SafetyAzureADLimit) { 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 return $false } Write-Color "[i] ", "Synchronized Computers found in AzureAD`: ", $($Devices.Count) -Color Yellow, Cyan, Green } if ($PSBoundParameters.ContainsKey('DisableLastSeenIntuneMoreThan') -or $PSBoundParameters.ContainsKey('DeleteLastSeenIntuneMoreThan') -or $PSBoundParameters.ContainsKey('MoveLastSeenIntuneMoreThan')) { Write-Color "[i] ", "Getting all computers from Intune" -Color Yellow, Cyan, Green [Array] $DevicesIntune = Get-MyDeviceIntune -WarningAction SilentlyContinue -WarningVariable WarningVar -Synchronized if ($WarningVar) { Write-Color "[e] ", "Error getting computers from Intune: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red return $false } if ($DevicesIntune.Count -eq 0) { Write-Color "[e] ", "No computers found in Intune, terminating! Please disable Intune integration or fix connectivity." -Color Yellow, Red return $false } foreach ($device in $DevicesIntune) { $AzureInformationCache.Intune[$Device.Name] = $device } if ($null -ne $SafetyIntuneLimit -and $DevicesIntune.Count -lt $SafetyIntuneLimit) { 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 return $false } Write-Color "[i] ", "Synchronized Computers found in Intune`: ", $($DevicesIntune.Count) -Color Yellow, Cyan, Green } $AzureInformationCache } function Get-InitialJamfComputers { [CmdletBinding()] param( [bool] $DisableLastContactJamfMoreThan, [bool] $MoveLastContactJamfMoreThan, [bool] $DeleteLastContactJamfMoreThan, [nullable[int]] $SafetyJamfLimit ) $JamfCache = [ordered] @{} if ($PSBoundParameters.ContainsKey('DisableLastContactJamfMoreThan') -or $PSBoundParameters.ContainsKey('DeleteLastContactJamfMoreThan') -or $PSBoundParameters.ContainsKey('MoveLastContactJamfMoreThan') ) { Write-Color "[i] ", "Getting all computers from Jamf" -Color Yellow, Cyan, Green [Array] $Jamf = Get-JamfDevice -WarningAction SilentlyContinue -WarningVariable WarningVar if ($WarningVar) { Write-Color "[e] ", "Error getting computers from Jamf: ", $WarningVar, " Terminating!" -Color Yellow, Red, Yellow, Red return $false } if ($Jamf.Count -eq 0) { Write-Color "[e] ", "No computers found in Jamf, terminating! Please disable Jamf integration or fix connectivity." -Color Yellow, Red return $false } else { Write-Color "[i] ", "Computers found in Jamf`: ", $($Jamf.Count) -Color Yellow, Cyan, Green } if ($null -ne $SafetyJamfLimit -and $Jamf.Count -lt $SafetyJamfLimit) { 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 return $false } foreach ($device in $Jamf) { $JamfCache[$Device.Name] = $device } } $JamfCache } function Import-ComputersData { [CmdletBinding()] param( [string] $DataStorePath, [System.Collections.IDictionary] $Export ) $ProcessedComputers = [ordered] @{ } $Today = Get-Date try { if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) { $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop # convert old format to new format $FileImport = Convert-ListProcessed -FileImport $FileImport if ($FileImport.PendingDeletion) { if ($FileImport.PendingDeletion.GetType().Name -notin 'Hashtable', 'OrderedDictionary') { Write-Color -Text "[e] ", "Incorrect XML format. PendingDeletion is not a hashtable/ordereddictionary. Terminating." -Color Yellow, Red return $false } } if ($FileImport.History) { if ($FileImport.History.GetType().Name -ne 'ArrayList') { Write-Color -Text "[e] ", "Incorrect XML format. History is not a ArrayList. Terminating." -Color Yellow, Red return $False } } $ProcessedComputers = $FileImport.PendingDeletion foreach ($ComputerFullName in $ProcessedComputers.Keys) { $Computer = $ProcessedComputers[$ComputerFullName] if ($Computer.PSObject.Properties.Name -notcontains 'TimeOnPendingList') { $TimeOnPendingList = if ($Computer.ActionDate) { - $($Computer.ActionDate - $Today).Days } else { $null } # We need to add this property to the object, as it may not exist on the old exports Add-Member -MemberType NoteProperty -Name 'TimeOnPendingList' -Value $TimeOnPendingList -Force -InputObject $Computer Add-Member -MemberType NoteProperty -Name 'TimeToLeavePendingList' -Value $null -Force -InputObject $Computer } else { $TimeOnPendingList = if ($Computer.ActionDate) { - $($Computer.ActionDate - $Today).Days } else { $null } $Computer.TimeOnPendingList = $TimeOnPendingList } } $Export['History'] = $FileImport.History } if (-not $ProcessedComputers) { $ProcessedComputers = [ordered] @{ } } } catch { Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red return $false } $ProcessedComputers } function Import-SIDHistory { [CmdletBinding()] param( [string] $DataStorePath, [System.Collections.IDictionary] $Export ) try { if ($DataStorePath -and (Test-Path -LiteralPath $DataStorePath -ErrorAction Stop)) { $FileImport = Import-Clixml -LiteralPath $DataStorePath -ErrorAction Stop if ($FileImport.History) { $Export.History = $FileImport.History } } } catch { Write-Color -Text "[e] ", "Couldn't read the list or wrong format. Error: $($_.Exception.Message)" -Color Yellow, Red return $false } $Export } function Move-WinADComputer { [CmdletBinding(SupportsShouldProcess)] param( [bool] $Success, [bool] $DisableAndMove, [System.Collections.IDictionary] $OrganizationalUnit, [PSCustomObject] $Computer, [switch] $WhatIfDisable, [switch] $DontWriteToEventLog, [string] $Server, [switch] $RemoveProtectedFromAccidentalDeletionFlag ) if ($Success -and $DisableAndMove) { # we only move if we successfully disabled the computer if ($OrganizationalUnit[$Domain]) { if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) { Write-Color -Text "[i] Computer ", $Computer.DistinguishedName, " is already in the correct OU." -Color Yellow, Green, Yellow } else { if ($Computer.ProtectedFromAccidentalDeletion) { if ($RemoveProtectedFromAccidentalDeletionFlag) { try { 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 Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfDisable if (-not $DontWriteToEventLog) { 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 } $Success = $true } catch { $Success = $false Write-Color -Text "[-] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } } } else { Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental move. Move skipped.' -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } $Success = $false } } else { $Success = $true } if ($Success) { $Success = $false try { $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfDisable -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) successful." -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName $Success = $true } catch { $Success = $false Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDisable.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message } } } } } $Success } function New-ADComputersStatistics { [CmdletBinding()] param( [Array] $ComputersToProcess ) $Statistics = [ordered] @{ All = $ComputersToProcess.Count ToMove = 0 ToMoveComputerWorkstation = 0 ToMoveComputerServer = 0 ToMoveComputerUnknown = 0 ToDisable = 0 ToDisableComputerUnknown = 0 ToDisableComputerWorkstation = 0 ToDisableComputerServer = 0 ToDelete = 0 ToDeleteComputerWorkstation = 0 ToDeleteComputerServer = 0 ToDeleteComputerUnknown = 0 TotalWindowsServers = 0 TotalWindowsWorkstations = 0 TotalMacOS = 0 TotalLinux = 0 TotalUnknown = 0 Delete = [ordered] @{ LastLogonDays = [ordered ]@{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } Move = [ordered] @{ LastLogonDays = [ordered] @{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } Disable = [ordered] @{ LastLogonDays = [ordered] @{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } 'Not required' = [ordered] @{ LastLogonDays = [ordered] @{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } 'ExcludedBySetting' = [ordered] @{ LastLogonDays = [ordered] @{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } 'ExcludedByFilter' = [ordered] @{ LastLogonDays = [ordered] @{} PasswordLastChangedDays = [ordered] @{} Systems = [ordered] @{} } } foreach ($Computer in $ComputersToProcess) { if ($Computer.OperatingSystem -like "Windows Server*") { $Statistics.TotalWindowsServers++ } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { $Statistics.TotalWindowsWorkstations++ } elseif ($Computer.OperatingSystem -like "Mac*") { $Statistics.TotalMacOS++ } elseif ($Computer.OperatingSystem -like "Linux*") { $Statistics.TotalLinux++ } else { $Statistics.TotalUnknown++ } if ($Computer.Action -eq 'Disable') { $Statistics.ToDisable++ if ($Computer.OperatingSystem -like "Windows Server*") { $Statistics.ToDisableComputerServer++ } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { $Statistics.ToDisableComputerWorkstation++ } else { $Statistics.ToDisableComputerUnknown++ } } elseif ($Computer.Action -eq 'Move') { $Statistics.ToMove++ if ($Computer.OperatingSystem -like "Windows Server*") { $Statistics.ToMoveComputerServer++ } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { $Statistics.ToMoveComputerWorkstation++ } else { $Statistics.ToMoveComputerUnknown++ } } elseif ($Computer.Action -eq 'Delete') { $Statistics.ToDelete++ if ($Computer.OperatingSystem -like "Windows Server*") { $Statistics.ToDeleteComputerServer++ } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") { $Statistics.ToDeleteComputerWorkstation++ } else { $Statistics.ToDeleteComputerUnknown++ } } if ($Computer.OperatingSystem) { $Statistics[$Computer.Action]['Systems'][$Computer.OperatingSystem]++ } else { $Statistics[$Computer.Action]['Systems']['Unknown']++ } if ($Computer.LastLogonDays -gt 720) { $Statistics[$Computer.Action]['LastLogonDays']['Over 720 days']++ } elseif ($Computer.LastLogonDays -gt 360) { $Statistics[$Computer.Action]['LastLogonDays']['Over 360 days']++ } elseif ($Computer.LastLogonDays -gt 180) { $Statistics[$Computer.Action]['LastLogonDays']['Over 180 days']++ } elseif ($Computer.LastLogonDays -gt 90) { $Statistics[$Computer.Action]['LastLogonDays']['Over 90 days']++ } elseif ($Computer.LastLogonDays -gt 30) { $Statistics[$Computer.Action]['LastLogonDays']['Over 30 days']++ } else { $Statistics[$Computer.Action]['LastLogonDays']['Under 30 days']++ } if ($Computer.PasswordLastChangedDays -gt 720) { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 720 days']++ } elseif ($Computer.PasswordLastChangedDays -gt 360) { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 360 days']++ } elseif ($Computer.PasswordLastChangedDays -gt 180) { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 180 days']++ } elseif ($Computer.PasswordLastChangedDays -gt 90) { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 90 days']++ } elseif ($Computer.PasswordLastChangedDays -gt 30) { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Over 30 days']++ } else { $Statistics[$Computer.Action]['PasswordLastChangedDays']['Under 30 days']++ } } $Statistics } function New-EmailBodyComputers { [CmdletBinding()] param( [Array] $CurrentRun ) Write-Color -Text "[i] ", "Generating email body" -Color Yellow, White [Array] $DisabledObjects = $CurrentRun | Where-Object { $_.Action -eq 'Disable' } [Array] $DeletedObjects = $CurrentRun | Where-Object { $_.Action -eq 'Delete' } $EmailBody = EmailBody -EmailBody { EmailText -Text "Hello," EmailText -LineBreak 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 EmailText -LineBreak EmailText -Text "Following is a summary for the computer object cleanup:" -FontWeight bold EmailList { EmailListItem -Text "Objects actioned: ", $Output.CurrentRun.Count -Color None, Green -FontWeight normal, bold EmailListItem -Text "Objects deleted: ", $DeletedObjects.Count -Color None, Salmon -FontWeight normal, bold EmailListItem -Text "Objects disabled: ", $DisabledObjects.Count -Color None, Orange -FontWeight normal, bold } EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon EmailTable -DataTable $Output.CurrentRun -HideFooter { New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackGroundColor PinkLace -Inline New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackGroundColor EnergyYellow -Inline New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackGroundColor LightGreen -Inline New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackGroundColor Salmon -Inline New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackGroundColor LightBlue -Inline } EmailText -LineBreak EmailText -Text "Regards," EmailText -Text "Automations Team" -FontWeight bold } Write-Color -Text "[i] ", "Email body generated" -Color Yellow, White $EmailBody } function New-EmailBodySidHistory { [CmdletBinding()] param( [System.Collections.IDictionary] $Export ) $EmailBody = EmailBody -EmailBody { EmailText -Text "Hello," EmailText -LineBreak 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 EmailText -LineBreak 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 $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled } $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled } EmailList { EmailListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold EmailListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold EmailListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal EmailListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal } -FontSize 10pt EmailText -Text "Following objects were actioned:" -LineBreak -FontWeight bold -Color Salmon EmailTable -DataTable $Export.CurrentRun { EmailTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon -Inline EmailTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightCoral -FailBackgroundColor LightGreen -Inline EmailTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor LightGreen -FailBackgroundColor Salmon -Inline EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink -Inline EmailTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral -Inline EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen -Inline EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon -Inline EmailTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue -Inline } -HideFooter -PrettifyObject EmailText -LineBreak EmailText -Text "Regards," EmailText -Text "Automations Team" -FontWeight bold } $EmailBody } function New-HTMLProcessedComputers { [CmdletBinding()] param( [System.Collections.IDictionary] $Export, [System.Collections.IDictionary] $DisableOnlyIf, [System.Collections.IDictionary] $DeleteOnlyIf, [System.Collections.IDictionary] $MoveOnlyIf, [Array] $ComputersToProcess, [string] $FilePath, [switch] $Online, [switch] $ShowHTML, [string] $LogFile, [switch] $Disable, [switch] $Delete, [switch] $Move, [switch] $ReportOnly ) New-HTML { New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey -BackgroundColor BlizzardBlue New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLPanelStyle -BorderRadius 0px New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Cleanup Monster - $($Export['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } if (-not $ReportOnly) { New-HTMLTab -Name 'Devices Current Run' { New-HTMLSection { [Array] $ListAll = $($Export.CurrentRun) New-HTMLPanel { New-HTMLToast -TextHeader 'Total in this run' -Text "Actions (disable & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen } -Invisible [Array] $ListDisabled = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Disable' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Disable' -Text "Computers disabled: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible [Array] $ListMoved = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Move' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Move' -Text "Computers moved: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible [Array] $ListDeleted = $($($Export.CurrentRun | Where-Object { $_.Action -eq 'Delete' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Delete' -Text "Computers deleted: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed } -Invisible } -Invisible New-HTMLTable -DataTable $Export.CurrentRun -Filtering -ScrollX { New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon } -WarningAction SilentlyContinue } New-HTMLTab -Name 'Devices History' { New-HTMLSection { [Array] $ListAll = $($Export.History) New-HTMLPanel { New-HTMLToast -TextHeader 'Total History' -Text "Actions (disable & move & delete): $($ListAll.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen } -Invisible [Array] $ListDisabled = $($($Export.History | Where-Object { $_.Action -eq 'Disable' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Disabled History' -Text "Computers disabled so far: $($ListDisabled.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible [Array] $ListMoved = $($($Export.History | Where-Object { $_.Action -eq 'Move' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Moved History' -Text "Computers moved so far: $($ListMoved.Count)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible [Array] $ListDeleted = $($($Export.History | Where-Object { $_.Action -eq 'Delete' })) New-HTMLPanel { New-HTMLToast -TextHeader 'Deleted History' -Text "Computers deleted so far: $($ListDeleted.Count)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed } -Invisible } -Invisible New-HTMLTable -DataTable $Export.History -Filtering -ScrollX { New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue } -WarningAction SilentlyContinue -AllProperties } New-HTMLTab -Name 'Devices Pending' { New-HTMLTable -DataTable $Export.PendingDeletion.Values -Filtering -ScrollX { New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon } -WarningAction SilentlyContinue -AllProperties } } New-HTMLTab -Name 'Devices' { New-HTMLSection { New-HTMLPanel { New-HTMLToast -TextHeader 'Total' -Text "Computers Total: $($ComputersToProcess.Count)" -BarColorLeft MintGreen -IconSolid info-circle -IconColor MintGreen } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'To disable' -Text "Computers to be disabled: $($Export.Statistics.ToDisable)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'To move' -Text "Computers to be moved: $($Export.Statistics.ToMove)" -BarColorLeft OrangePeel -IconSolid info-circle -IconColor OrangePeel } -Invisible New-HTMLPanel { New-HTMLToast -TextHeader 'To delete' -Text "Computers to be deleted: $($Export.Statistics.ToDelete)" -BarColorLeft OrangeRed -IconSolid info-circle -IconColor OrangeRed } -Invisible } -Invisible New-HTMLSection -HeaderText 'General statistics' -CanCollapse { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name 'To be disabled' -Value $Export.Statistics.ToDisable New-ChartPie -Name 'To be moved' -Value $Export.Statistics.ToMove New-ChartPie -Name 'To be deleted' -Value $Export.Statistics.ToDelete } -Title "Computers to be disabled or deleted" } if ($Export.Statistics.ToDisableComputerWorkstation -or $Export.Statistics.ToDisableComputerServer -or $Export.Statistics.ToDisableComputerUnknown) { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name "Disable workstations" -Value $Export.Statistics.ToDisableComputerWorkstation New-ChartPie -Name "Disable servers" -Value $Export.Statistics.ToDisableComputerServer New-ChartPie -Name "Disable unknown" -Value $Export.Statistics.ToDisableComputerUnknown } -Title "Computers to be disabled by type" } } if ($Export.Statistics.ToDeleteComputerWorkstation -or $Export.Statistics.ToDeleteComputerServer -or $Export.Statistics.ToDeleteComputerUnknown) { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name "Delete workstations" -Value $Export.Statistics.ToDeleteComputerWorkstation New-ChartPie -Name "Delete servers" -Value $Export.Statistics.ToDeleteComputerServer New-ChartPie -Name "Delete unknown" -Value $Export.Statistics.ToDeleteComputerUnknown } -Title "Computers to be deleted by type" } } } New-HTMLText -LineBreak New-HTMLHeading -Heading h3 -HeadingText "Full list of computers that will be processed if there are no limits to processing. " New-HTMLText -LineBreak New-HTMLSection -Invisible { New-HTMLPanel { if ($Disable) { New-HTMLText -Text "Computers will be disabled (and moved) only if: " -FontWeight bold New-HTMLList { foreach ($Key in $DisableOnlyIf.Keys) { $newHTMLListItemSplat = @{ Text = @( if ($null -eq $DisableOnlyIf[$Key] -or $DisableOnlyIf[$Key].Count -eq 0) { $($Key), " is ", 'Not Set' $ColorInUse = 'Cinnabar' } else { if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') { $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never logged on" } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') { $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never changed" } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') { $($Key), " is ", $($DisableOnlyIf[$Key]), " or ", "Never synced/seen" } elseif ($Key -in 'MoveTargetOrganizationalUnit') { if ($DisableOnlyIf[$Key] -is [string]) { $($Key), " is ", $MoveOnlyIf[$Key] } elseif ($DisableOnlyIf[$Key] -is [System.Collections.IDictionary]) { $Key } } else { $($Key), " is ", $($DisableOnlyIf[$Key]) } $ColorInUse = 'Apple' } ) FontWeight = 'bold', 'normal', 'bold', 'normal', 'bold' Color = $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue' } if ($Key -eq 'MoveTargetOrganizationalUnit') { $newHTMLListItemSplat.NestedListItems = { New-HTMLList { foreach ($MoveKey in $DisableOnlyIf[$Key].Keys) { New-HTMLListItem -Text @( $MoveKey, " to ", $DisableOnlyIf[$Key][$MoveKey] ) -FontWeight 'bold', 'normal', 'bold', 'normal', 'bold' -Color $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue' } } } } New-HTMLListItem @newHTMLListItemSplat } } } else { New-HTMLText -Text "Computers will not be disabled, as the disable functionality was not enabled. " -FontWeight bold } } New-HTMLPanel { if ($Move) { New-HTMLText -Text "Computers will be moved only if: " -FontWeight bold New-HTMLList { foreach ($Key in $MoveOnlyIf.Keys) { $newHTMLListItemSplat = @{ Text = @( if ($null -eq $MoveOnlyIf[$Key] -or $MoveOnlyIf[$Key].Count -eq 0) { $($Key), " is ", 'Not Set' $ColorInUse = 'Cinnabar' } else { if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') { $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never logged on" } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') { $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never changed" } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') { $($Key), " is ", $($MoveOnlyIf[$Key]), " or ", "Never synced/seen" } elseif ($Key -in 'TargetOrganizationalUnit') { if ($MoveOnlyIf[$Key] -is [string]) { $($Key), " is ", $MoveOnlyIf[$Key] } elseif ($MoveOnlyIf[$Key] -is [System.Collections.IDictionary]) { $Key } } else { $($Key), " is ", $($MoveOnlyIf[$Key]) } $ColorInUse = 'Apple' } ) FontWeight = 'bold', 'normal', 'bold', 'normal', 'bold' Color = $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue' } if ($Key -eq 'TargetOrganizationalUnit') { $newHTMLListItemSplat.NestedListItems = { New-HTMLList { foreach ($MoveKey in $MoveOnlyIf[$Key].Keys) { New-HTMLListItem -Text @( $MoveKey, " to ", $MoveOnlyIf[$Key][$MoveKey] ) -FontWeight 'bold', 'normal', 'bold', 'normal', 'bold' -Color $ColorInUse, 'None', 'CornflowerBlue', 'None', 'CornflowerBlue' } } } } New-HTMLListItem @newHTMLListItemSplat } } } else { New-HTMLText -Text "Computers will not be moved, as the move functionality was not enabled. " -FontWeight bold } } New-HTMLPanel { if ($Delete) { New-HTMLText -Text "Computers will be deleted only if: " -FontWeight bold New-HTMLList { foreach ($Key in $DeleteOnlyIf.Keys) { New-HTMLListItem -Text @( if ($null -eq $DeleteOnlyIf[$Key] -or $DeleteOnlyIf[$Key].Count -eq 0) { $($Key), " is ", 'Not Set' $ColorInUse = 'Cinnabar' } else { if ($Key -in 'LastLogonDateMoreThan', 'LastLogonDateOlderThan') { $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never logged on" } elseif ($Key -in 'PasswordLastSetMoreThan', 'PasswordLastSetOlderThan') { $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never changed" } elseif ($Key -in 'LastSeenAzureMoreThan', 'LastSeenIntuneMoreThan', 'LastSyncAzureMoreThan', 'LastContactJamfMoreThan') { $($Key), " is ", $($DeleteOnlyIf[$Key]), " or ", "Never synced/seen" } else { $($Key), " is ", $($DeleteOnlyIf[$Key]) } $ColorInUse = 'Apple' } ) -FontWeight bold, normal, bold, normal, bold -Color $ColorInUse, None, CornflowerBlue, None, CornflowerBlue } } } else { New-HTMLText -Text "Computers will not be deleted, as the delete functionality was not enabled. " -FontWeight bold } } } New-HTMLTable -DataTable $ComputersToProcess -Filtering -ScrollX { New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Delete' -BackgroundColor PinkLace New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Move' -BackgroundColor Yellow New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'Disable' -BackgroundColor EnergyYellow New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'True' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'False' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Whatif' -BackgroundColor LightBlue New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'ExcludedByFilter' -BackgroundColor LightBlue New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'ExcludedBySetting' -BackgroundColor LightPink New-HTMLTableCondition -Name 'ProtectedFromAccidentalDeletion' -ComparisonType string -Value $false -BackgroundColor LightBlue -FailBackgroundColor Salmon } -WarningAction SilentlyContinue -ExcludeProperty 'TimeOnPendingList', 'TimeToLeavePendingList', 'DistinguishedNameAfterMove' } try { if ($LogFile -and (Test-Path -LiteralPath $LogFile -ErrorAction Stop)) { $LogContent = Get-Content -Raw -LiteralPath $LogFile -ErrorAction Stop New-HTMLTab -Name 'Log' { New-HTMLCodeBlock -Code $LogContent -Style generic } } } catch { Write-Color -Text "[e] ", "Couldn't read the log file. Skipping adding log to HTML. Error: $($_.Exception.Message)" -Color Yellow, Red } } -FilePath $FilePath -Online:$Online.IsPresent -ShowHTML:$ShowHTML.IsPresent } function New-HTMLProcessedSIDHistory { [CmdletBinding()] param( $Export, [System.Collections.IDictionary] $ForestInformation, [System.Collections.IDictionary] $Output, [string] $FilePath, [switch] $HideHTML, [switch] $Online, [string] $LogPath, [System.Collections.IDictionary] $Configuration ) New-HTML { New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLTableOption -DataStore JavaScript -ArrayJoin -ArrayJoinString ", " -BoolAsString New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text "Cleanup Monster - $($Export['Version'])" -Color Blue } -JustifyContent flex-end -Invisible } } 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 New-HTMLSection -HeaderText "SID History Report for $($ForestInformation.Forest)" { New-HTMLPanel { New-HTMLText -Text @( "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. ", "The report is divided into three tabs: Overview, Current Deletion Status, and History Deletion Status. ", "The following report shows 3 tabs:" ) -FontSize 10pt New-HTMLList { New-HTMLListItem -Text "Overview", " - ", "provides an overview of the SID history in the forest" -FontWeight bold, normal, normal 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 New-HTMLListItem -Text "History Deletion Status", " - ", "shows the history deletion status of SID history values over time" -FontWeight bold, normal, normal } -FontSize 10pt New-HTMLText -Text "The following statistics provide insights into the SID history in the forest:" -FontSize 10pt New-HTMLList { New-HTMLListItem -Text "$($Output.All.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.TotalUsers)", " users with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.TotalGroups)", " groups with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.TotalComputers)", " computers with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.EnabledObjects)", " enabled objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.DisabledObjects)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Keys.Count - 2)", " different domains with SID history values" -Color BlueViolet, None -FontWeight bold, normal } -LineBreak -FontSize 10pt New-HTMLText -Text "The following statistics provide insights into the SID history categories:" -FontSize 10pt New-HTMLList { # Add statistics for the three SID history categories New-HTMLListItem -Text "$($Output.Statistics.InternalSIDs)", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.ExternalSIDs)", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal New-HTMLListItem -Text "$($Output.Statistics.UnknownSIDs)", " SID history values from unknown domains (deleted or broken trusts)" -Color Crimson, None -FontWeight bold, normal } -FontSize 10pt } New-HTMLPanel { 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 New-HTMLList { foreach ($SID in $Output.DomainSIDs.Keys) { $DomainSID = $Output.DomainSIDs[$SID] 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 } } -FontSize 10pt } } } [Array] $DomainNames = foreach ($Key in $Output.Keys) { if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs')) { continue } $Key } New-HTMLTab -Name 'Overview' { foreach ($Domain in $DomainNames) { [Array] $Objects = $Output[$Domain] $EnabledObjects = $Objects | Where-Object { $_.Enabled } $DisabledObjects = $Objects | Where-Object { -not $_.Enabled } $Types = $Objects | Group-Object -Property ObjectClass -NoElement if ($Domain -eq 'All') { $Name = 'All' } else { if ($Output.DomainSIDs[$Domain]) { $DomainName = $Output.DomainSIDs[$Domain].Domain $Name = "$DomainName ($($Objects.Count))" } else { $Name = "$Domain ($($Objects.Count))" } } New-HTMLTab -Name $Name { New-HTMLSection -HeaderText "Domain $Domain" { New-HTMLPanel -Invisible { New-HTMLText -Text "Overview for ", $Domain -Color Blue, BattleshipGrey -FontSize 10pt New-HTMLList { New-HTMLListItem -Text "$($Objects.Count)", " objects with SID history values" -Color BlueViolet, None -FontWeight bold, normal New-HTMLListItem -Text "$($EnabledObjects.Count)", " enabled objects with SID history values" -Color Green, None -FontWeight bold, normal New-HTMLListItem -Text "$($DisabledObjects.Count)", " disabled objects with SID history values" -Color Salmon, None -FontWeight bold, normal # Calculate SID history categories for this domain $InternalSIDsForDomain = ($Objects | ForEach-Object { $_.InternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum $ExternalSIDsForDomain = ($Objects | ForEach-Object { $_.ExternalCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum $UnknownSIDsForDomain = ($Objects | ForEach-Object { $_.UnknownCount }) | Measure-Object -Sum | Select-Object -ExpandProperty Sum New-HTMLListItem -Text "$InternalSIDsForDomain", " SID history values from internal forest domains" -Color ForestGreen, None -FontWeight bold, normal New-HTMLListItem -Text "$ExternalSIDsForDomain", " SID history values from external trusted domains" -Color DodgerBlue, None -FontWeight bold, normal New-HTMLListItem -Text "$UnknownSIDsForDomain", " SID history values from unknown domains" -Color Crimson, None -FontWeight bold, normal New-HTMLListItem -Text "Object types:" { New-HTMLList { foreach ($Type in $Types) { New-HTMLListItem -Text "$($Type.Count)", " ", $Type.Name, " objects with SID history values" -Color BlueViolet, None, BlueViolet, None -FontWeight bold, normal, bold, normal } } } -FontSize 10pt } -FontSize 10pt } New-HTMLPanel -Invisible { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -Text "Domain", " - ", "this column shows the domain of the object" -FontWeight bold, normal, normal New-HTMLListItem -Text "ObjectClass", " - ", "this column shows the object class of the object (user, device, group)" -FontWeight bold, normal, normal New-HTMLListItem -Text "Internal", " - ", "this column shows SIDs from domains within the current forest" -FontWeight bold, normal, normal New-HTMLListItem -Text "External", " - ", "this column shows SIDs from domains that are trusted by the current forest" -FontWeight bold, normal, normal New-HTMLListItem -Text "Unknown", " - ", "this column shows SIDs from domains that no longer exist or have broken trusts" -FontWeight bold, normal, normal New-HTMLListItem -Text "Enabled", " - ", "this column shows if the object is enabled" -FontWeight bold, normal, normal New-HTMLListItem -Text "SIDHistory", " - ", "this column shows the SID history values of the object" -FontWeight bold, normal, normal New-HTMLListItem -Text "Domains", " - ", "this column shows the domains of the SID history values" -FontWeight bold, normal, normal 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 } -FontSize 10pt } } New-HTMLTable -DataTable $Objects -Filtering { New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'InternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor ForestGreen New-HTMLTableCondition -Name 'ExternalCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor DodgerBlue New-HTMLTableCondition -Name 'UnknownCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor Crimson } -ScrollX } -TextTransform uppercase } } New-HTMLTab -Name 'Current Deletion Status' { New-HTMLSection -HeaderText "SID History Report" { New-HTMLPanel -Invisible { 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 $Enabled = $Export.CurrentRun | Where-Object { $_.Enabled } $Disabled = $Export.CurrentRun | Where-Object { -not $_.Enabled } New-HTMLList { New-HTMLListItem -Text "$($Enabled.Count)", " enabled objects" -FontWeight normal, bold New-HTMLListItem -Text "$($Disabled.Count)", " disabled objects" -FontWeight normal, bold New-HTMLListItem -Text "Processed ", $($Export.ProcessedObjects), " total objects" -FontWeight normal, bold, normal New-HTMLListItem -Text "Processed ", $($Export.ProcessedSIDs), " total SID history values" -FontWeight normal, bold, normal } -FontSize 10pt New-HTMLText -Text "The following table lists all objects with SID history values and their current deletion status." -FontSize 10pt } } New-HTMLTable -DataTable $Export.CurrentRun -Filtering { New-HTMLTableCondition -Name 'Enabled' -ComparisonType bool -Operator eq -Value $true -BackgroundColor MintGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'SIDBeforeCount' -ComparisonType number -Operator gt -Value 0 -BackgroundColor LightCoral -FailBackgroundColor LightGreen New-HTMLTableCondition -Name 'SIDAfterCount' -ComparisonType number -Operator eq -Value 0 -BackgroundColor LightGreen -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue } -ScrollX } New-HTMLTab -Name 'History Deletion Status' { New-HTMLSection -HeaderText "SID History Report" { New-HTMLPanel -Invisible { New-HTMLText -Text "The following table lists all actions that were taken on given objects while removing SID History over time." -FontSize 10pt } } New-HTMLTable -DataTable $Export.History -Filtering { New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Success' -BackgroundColor LightGreen New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'Failed' -BackgroundColor Salmon New-HTMLTableCondition -Name 'ActionStatus' -ComparisonType string -Value 'WhatIf' -BackgroundColor LightBlue New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemoveAll' -BackgroundColor LightPink New-HTMLTableCondition -Name 'Action' -ComparisonType string -Value 'RemovePerSID' -BackgroundColor LightCoral } -ScrollX } New-HTMLTab -Name "Configuration" { New-HTMLSection -HeaderText "Configuration" { New-HTMLPanel -Invisible { New-HTMLText -Text "The following table lists all configuration settings used in the script." -FontSize 10pt } } New-HTMLTable -DataTable $Configuration -ScrollX } if ($LogPath) { $LogsContent = Get-Content -Path $LogPath -Raw -ErrorAction SilentlyContinue if ($LogsContent) { New-HTMLTab -Name 'Logs' { New-HTMLCodeBlock -Code $LogsContent -Style generic } } } } -FilePath $FilePath -ShowHTML:(-not $HideHTML) -Online:$Online.IsPresent } function Remove-ADSIDHistory { [CmdletBinding(SupportsShouldProcess)] param( [Array] $ObjectsToProcess, [System.Collections.IDictionary] $Export, [int] $RemoveLimitSID ) Write-Color -Text "[i] ", "Starting process of removing SID History entries from ", $ObjectsToProcess.Count, " objects" -Color Yellow, White, Green $GlobalLimitSID = 0 $ProcessedSIDs = 0 $ProcessedObjects = 0 $Export['CurrentRun'] = [System.Collections.Generic.List[PSCustomObject]]::new() :TopLoop foreach ($Item in $ObjectsToProcess) { $Object = $Item.Object $QueryServer = $Item.QueryServer $CurrentRunObject = [PSCustomObject] @{ ObjectName = $Object.Name ObjectDomain = $Object.Domain Enabled = $Object.Enabled SIDBefore = $Object.SIDHistory -join ", " SIDBeforeCount = $Object.SIDHistory.Count Action = $null ActionDate = $null ActionStatus = $null ActionError = '' SIDRemoved = @() SIDAfter = @() SIDAfterCount = 0 ObjectDN = $Object.DistinguishedName } $ProcessedObjects++ if ($LimitPerSID) { # Process individual SIDs for this object foreach ($SID in $Object.SIDHistory) { $CurrentDate = Get-Date if ($PSCmdlet.ShouldProcess("$($Object.Name) ($($Object.Domain))", "Remove SID History entry $SID")) { Write-Color -Text "[i] ", "Removing SID History entry $SID from ", $Object.Name -Color Yellow, White, Green try { Set-ADObject -Identity $Object.DistinguishedName -Remove @{ SIDHistory = $SID } -Server $QueryServer -ErrorAction Stop $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain Enabled = $Object.Enabled SID = $SID Action = 'RemovePerSID' ActionDate = $CurrentDate ActionStatus = 'Success' ActionError = '' ObjectDN = $Object.DistinguishedName } $CurrentRunObject.Action = 'RemovePerSID' $CurrentRunObject.ActionDate = $CurrentDate $CurrentRunObject.ActionStatus = 'Success' $CurrentRunObject.ActionError = '' $CurrentRunObject.SIDRemoved += $SID Write-Color -Text "[+] ", "Removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green } catch { Write-Color -Text "[!] ", "Failed to remove SID History entry $SID from ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain SID = $SID Action = 'RemovePerSID' ActionDate = $CurrentDate ActionStatus = 'Failed' ActionError = $_.Exception.Message ObjectDN = $Object.DistinguishedName } $CurrentRunObject.Action = 'RemovePerSID' $CurrentRunObject.ActionDate = $CurrentDate $CurrentRunObject.ActionStatus = 'Failed' $CurrentRunObject.ActionError = $_.Exception.Message } } else { Write-Color -Text "[i] ", "Would have removed SID History entry $SID from ", $Object.Name -Color Yellow, White, Green $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain SID = $SID Action = 'RemovePerSID' ActionDate = Get-Date ActionStatus = 'WhatIf' ActionError = '' ObjectDN = $Object.DistinguishedName } $CurrentRunObject.SIDRemoved += $SID $CurrentRunObject.Action = 'RemovePerSID' $CurrentRunObject.ActionDate = $CurrentDate $CurrentRunObject.ActionStatus = 'WhatIf' $CurrentRunObject.ActionError = '' } $null = $Export.History.Add($Result) try { $RefreshedObject = Get-ADObject -Identity $Object.DistinguishedName -Properties SIDHistory -Server $QueryServer -ErrorAction Stop } catch { Write-Color -Text "[!] ", "Failed to refresh object ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red, Red $RefreshedObject = $null } if ($RefreshedObject -and $RefreshedObject.SIDHistory) { $CurrentRunObject.SIDAfter = $RefreshedObject.SIDHistory -join ", " } else { $CurrentRunObject.SIDAfter = $null } $CurrentRunObject.SIDAfterCount = $RefreshedObject.SIDHistory.Count $Export.CurrentRun.Add($CurrentRunObject) $GlobalLimitSID++ $ProcessedSIDs++ if ($GlobalLimitSID -ge $RemoveLimitSID) { Write-Color -Text "[i] ", "Reached SID limit of ", $RemoveLimitSID, ". Stopping processing." -Color Yellow, White, Green, White break TopLoop } } } else { $CurrentDate = Get-Date # Process all SIDs for this object at once if ($PSCmdlet.ShouldProcess("$($Object.Name) ($($Object.Domain))", "Remove all SID History entries")) { Write-Color -Text "[i] ", "Removing all SID History entries from ", $Object.Name -Color Yellow, White, Green try { Set-ADObject -Identity $Object.DistinguishedName -Clear SIDHistory -Server $QueryServer -ErrorAction Stop $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain SID = $Object.SIDHistory -join ", " Action = 'RemoveAll' ActionDate = $CurrentDate ActionStatus = 'Success' ActionError = '' ObjectDN = $Object.DistinguishedName } $CurrentRunObject.ActionStatus = 'Success' $CurrentRunObject.ActionError = '' Write-Color -Text "[+] ", "Removed all SID History entries from ", $Object.Name -Color Yellow, White, Green } catch { Write-Color -Text "[!] ", "Failed to remove SID History entries from ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain SID = $Object.SIDHistory -join ", " Action = 'RemoveAll' ActionDate = $CurrentDate ActionStatus = 'Failed' ActionError = $_.Exception.Message ObjectDN = $Object.DistinguishedName } $CurrentRunObject.ActionStatus = 'Failed' $CurrentRunObject.ActionError = $_.Exception.Message } } else { Write-Color -Text "[i] ", "Would have removed all SID History entries from ", $Object.Name -Color Yellow, White, Green $Result = [PSCustomObject]@{ ObjectName = $Object.Name ObjectDomain = $Object.Domain SID = $Object.SIDHistory -join ", " Action = 'RemoveAll' ActionDate = $CurrentDate ActionStatus = 'WhatIf' ActionError = '' ObjectDN = $Object.DistinguishedName } $CurrentRunObject.ActionStatus = 'WhatIf' $CurrentRunObject.ActionError = '' } $CurrentRunObject.SIDRemoved += $Object.SIDHistory $CurrentRunObject.Action = 'RemoveAll' $CurrentRunObject.ActionDate = $CurrentDate $CurrentRunObject.ActionError = '' $null = $Export.History.Add($Result) try { $RefreshedObject = Get-ADObject -Identity $Object.DistinguishedName -Properties SIDHistory -Server $QueryServer -ErrorAction Stop } catch { Write-Color -Text "[!] ", "Failed to refresh object ", $Object.Name, " exception: ", $_.Exception.Message -Color Yellow, White, Red, Red $RefreshedObject = $null } if ($RefreshedObject -and $RefreshedObject.SIDHistory) { $CurrentRunObject.SIDAfter = $RefreshedObject.SIDHistory -join ", " } else { $CurrentRunObject.SIDAfter = $null } $CurrentRunObject.SIDAfterCount = $RefreshedObject.SIDHistory.Count $Export.CurrentRun.Add($CurrentRunObject) $ProcessedSIDs += $Object.SIDHistory.Count } } $Export['ProcessedObjects'] = $ProcessedObjects $Export['ProcessedSIDs'] = $ProcessedSIDs 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 } function Request-ADComputersDelete { [cmdletBinding(SupportsShouldProcess)] param( [System.Collections.IDictionary] $Report, [switch] $ReportOnly, [switch] $WhatIfDelete, [int] $DeleteLimit, [System.Collections.IDictionary] $ProcessedComputers, [DateTime] $Today, [switch] $DontWriteToEventLog, [switch] $RemoveProtectedFromAccidentalDeletionFlag ) $CountDeleteLimit = 0 # :top means name of the loop, so we can break it :topLoop foreach ($Domain in $Report.Keys) { foreach ($Computer in $Report["$Domain"]['Computers']) { $Server = $Report["$Domain"]['Server'] if ($Computer.Action -ne 'Delete') { continue } if ($ReportOnly) { $Computer } else { if ($Computer.ProtectedFromAccidentalDeletion) { if ($RemoveProtectedFromAccidentalDeletionFlag) { try { 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 Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfDelete if (-not $DontWriteToEventLog) { 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 } $Success = $true } catch { $Success = $false Write-Color -Text "[-] Removing protected from accidental deletion flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } } } else { Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental deletion. Deletion skipped.' -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } $Success = $false } } else { $Success = $true } if ($Success) { $Success = $false 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 try { $Success = $true Remove-ADObject -Identity $Computer.DistinguishedName -Recursive -WhatIf:$WhatIfDelete -Server $Server -ErrorAction Stop -Confirm:$false Write-Color -Text "[+] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) successful." -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } } catch { $Success = $false Write-Color -Text "[-] Deleting computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfDelete.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Computer.ActionComment = $_.Exception.Message } } $Computer.ActionDate = $Today if ($WhatIfDelete.IsPresent) { $Computer.ActionStatus = 'WhatIf' } else { if ($Success) { # lets remove computer from $ProcessedComputers # but only if it's not WhatIf and only if it's successful $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain) $ProcessedComputers.Remove("$ComputerOnTheList") } $Computer.ActionStatus = $Success } # return computer to $ReportDeleted so we can see summary just in case $Computer $CountDeleteLimit++ if ($DeleteLimit) { if ($DeleteLimit -eq $CountDeleteLimit) { break topLoop # this breaks top loop } } } } } } function Request-ADComputersDisable { [cmdletbinding(SupportsShouldProcess)] param( [nullable[bool]] $Delete, [nullable[bool]] $Move, [nullable[bool]] $DisableAndMove, [System.Collections.IDictionary] $Report, [switch] $WhatIfDisable, [switch] $DisableModifyDescription, [switch] $DisableModifyAdminDescription, [int] $DisableLimit, [switch] $ReportOnly, [DateTime] $Today, [switch] $DontWriteToEventLog, [Object] $DisableMoveTargetOrganizationalUnit, [switch] $DoNotAddToPendingList, [ValidateSet( 'DisableAndMove', 'MoveAndDisable' )][string] $DisableAndMoveOrder = 'DisableAndMove', [switch] $RemoveProtectedFromAccidentalDeletionFlag ) if ($DisableAndMove -and $DisableMoveTargetOrganizationalUnit) { if ($DisableMoveTargetOrganizationalUnit -is [System.Collections.IDictionary]) { $OrganizationalUnit = $DisableMoveTargetOrganizationalUnit } elseif ($DisableMoveTargetOrganizationalUnit -is [string]) { $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $DisableMoveTargetOrganizationalUnit -ToDomainCN $OrganizationalUnit = [ordered] @{ $DomainCN = $DisableMoveTargetOrganizationalUnit } } else { Write-Color -Text "[-] DisableMoveTargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red return } } $CountDisable = 0 # :top means name of the loop, so we can break it :topLoop foreach ($Domain in $Report.Keys) { Write-Color "[i] ", "Starting process of disabling computers for domain $Domain" -Color Yellow, Green foreach ($Computer in $Report["$Domain"]['Computers']) { $Server = $Report["$Domain"]['Server'] if ($Computer.Action -ne 'Disable') { continue } if ($ReportOnly) { $Computer } else { $Success = $true if ($DisableAndMoveOrder -eq 'DisableAndMove') { $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server -RemoveProtectedFromAccidentalDeletionFlag:$RemoveProtectedFromAccidentalDeletionFlag.IsPresent } else { $Success = Move-WinADComputer -Success $Success -DisableAndMove $DisableAndMove -OrganizationalUnit $OrganizationalUnit -Computer $Computer -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Server $Server -RemoveProtectedFromAccidentalDeletionFlag:$RemoveProtectedFromAccidentalDeletionFlag.IsPresent $Success = Disable-WinADComputer -Success $Success -WhatIfDisable:$WhatIfDisable -DontWriteToEventLog:$DontWriteToEventLog -Computer $Computer -Server $Server } if ($Success) { if ($DisableModifyDescription -eq $true) { $DisableModifyDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))" try { Set-ADComputer -Identity $Computer.DistinguishedName -Description $DisableModifyDescriptionText -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server Write-Color -Text "[+] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyDescriptionText -Color Yellow, Green, Yellow, Green, Yellow } catch { $Computer.ActionComment = $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message Write-Color -Text "[-] ", "Setting description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow } } if ($DisableModifyAdminDescription) { $DisableModifyAdminDescriptionText = "Disabled by a script, LastLogon $($Computer.LastLogonDate) ($($DisableOnlyIf.LastLogonDateMoreThan)), PasswordLastSet $($Computer.PasswordLastSet) ($($DisableOnlyIf.PasswordLastSetMoreThan))" try { Set-ADObject -Identity $Computer.DistinguishedName -Replace @{ AdminDescription = $DisableModifyAdminDescriptionText } -WhatIf:$WhatIfDisable -ErrorAction Stop -Server $Server Write-Color -Text "[+] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) successful. Set to: ", $DisableModifyAdminDescriptionText -Color Yellow, Green, Yellow, Green, Yellow } catch { $Computer.ActionComment + [System.Environment]::NewLine + $_.Exception.Message Write-Color -Text "[-] ", "Setting admin description on disabled computer ", $Computer.DistinguishedName, " (WhatIf: $WhatIfDisable) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow } } } # this is to store actual disabling time - we can't trust WhenChanged date $Computer.ActionDate = $Today if ($WhatIfDisable.IsPresent) { $Computer.ActionStatus = 'WhatIf' } else { $Computer.ActionStatus = $Success } # We add computer to pending list in all cases because otherwise we would be going in circles # if move or delete were not enabled # please use -DoNotAddToPendingList if you don't want to add computer to pending list if (-not $DoNotAddToPendingList) { $FullComputerName = -join ($Computer.SamAccountName, '@', $Domain) # Lets add computer to pending list, and lets set time how long it's there so it can be easily visible in reports $Computer.TimeOnPendingList = 0 $ProcessedComputers[$FullComputerName] = $Computer } # return computer to $ReportDisabled so we can see summary just in case $Computer $CountDisable++ if ($DisableLimit) { if ($DisableLimit -eq $CountDisable) { break topLoop # this breaks top loop } } } } } } function Request-ADComputersMove { [cmdletBinding(SupportsShouldProcess)] param( [nullable[bool]] $Delete, [System.Collections.IDictionary] $Report, [switch] $ReportOnly, [switch] $WhatIfMove, [int] $MoveLimit, [System.Collections.IDictionary] $ProcessedComputers, [DateTime] $Today, [Object] $TargetOrganizationalUnit, [switch] $DontWriteToEventLog, [switch] $DoNotAddToPendingList, [switch] $RemoveProtectedFromAccidentalDeletionFlag ) if ($TargetOrganizationalUnit -is [System.Collections.IDictionary]) { $OrganizationalUnit = $TargetOrganizationalUnit } elseif ($TargetOrganizationalUnit -is [string]) { $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $TargetOrganizationalUnit -ToDomainCN $OrganizationalUnit = [ordered] @{ $DomainCN = $TargetOrganizationalUnit } } else { Write-Color -Text "[-] TargetOrganizationalUnit is not a string or hashtable. Skipping moving to proper OU." -Color Yellow, Red return } $CountMoveLimit = 0 # :top means name of the loop, so we can break it :topLoop foreach ($Domain in $Report.Keys) { foreach ($Computer in $Report["$Domain"]['Computers']) { $Server = $Report["$Domain"]['Server'] if ($Computer.Action -ne 'Move') { continue } if ($ReportOnly) { $Computer } else { if ($OrganizationalUnit[$Domain]) { # we check if the computer is already in the correct OU if ($Computer.OrganizationalUnit -eq $OrganizationalUnit[$Domain]) { # this shouldn't really happen as we should have filtered it out earlier } else { if ($Computer.ProtectedFromAccidentalDeletion) { if ($RemoveProtectedFromAccidentalDeletionFlag) { try { 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 Set-ADObject -ProtectedFromAccidentalDeletion $false -Identity $Computer.DistinguishedName -Server $Server -ErrorAction Stop -Confirm:$false -WhatIf:$WhatIfMove if (-not $DontWriteToEventLog) { 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 } $Success = $true } catch { $Success = $false Write-Color -Text "[-] Removing protected from accidental move flag for computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } } } else { Write-Color -Text "[i] Computer ", $Computer.SamAccountName, ' DN: ', $Computer.DistinguishedName, ' is protected from accidental move. Move skipped.' -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } $Success = $false } } else { $Success = $true } if ($Success) { $Success = $false 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 try { $MovedObject = Move-ADObject -Identity $Computer.DistinguishedName -WhatIf:$WhatIfMove -Server $Server -ErrorAction Stop -Confirm:$false -TargetPath $OrganizationalUnit[$Domain] -PassThru $Computer.DistinguishedNameAfterMove = $MovedObject.DistinguishedName Write-Color -Text "[+] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) successful." -Color Yellow, Green, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } if (-not $Delete) { # lets remove computer from $ProcessedComputers # we only remove it if Delete is not part of the removal process and move is the last step if (-not $DoNotAddToPendingList) { $ComputerOnTheList = -join ($Computer.SamAccountName, "@", $Domain) $ProcessedComputers.Remove("$ComputerOnTheList") } } $Success = $true } catch { $Success = $false Write-Color -Text "[-] Moving computer ", $Computer.DistinguishedName, " (WhatIf: $($WhatIfMove.IsPresent)) failed. Error: $($_.Exception.Message)" -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Computer.ActionComment = $_.Exception.Message } } $Computer.ActionDate = $Today if ($WhatIfMove.IsPresent) { $Computer.ActionStatus = 'WhatIf' } else { $Computer.ActionStatus = $Success } # return computer to $ReportMoved so we can see summary just in case $Computer $CountMoveLimit++ if ($MoveLimit) { if ($MoveLimit -eq $CountMoveLimit) { break topLoop # this breaks top loop } } } } else { Write-Color -Text "[-] Moving computer ", $Computer.SamAccountName, " failed. TargetOrganizationalUnit for domain $Domain not found." -Color Yellow, Red, Yellow if (-not $DontWriteToEventLog) { 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 } foreach ($W in $Warnings) { Write-Color -Text "[-] ", "Warning: ", $W -Color Yellow, Cyan, Red } $Computer.ActionComment = "TargetOrganizationalUnit for domain $Domain not found." $Computer.ActionDate = $Today $Computer.ActionStatus = $false # return computer to $ReportMoved so we can see summary just in case $Computer $CountMoveLimit++ if ($MoveLimit) { if ($MoveLimit -eq $CountMoveLimit) { break topLoop # this breaks top loop } } } } } } } function Request-ADSIDHistory { [CmdletBinding()] param( [Array] $DomainNames, [System.Collections.IDictionary] $Output, [System.Collections.IDictionary] $Export, [System.Collections.IDictionary] $ForestInformation, [string[]] $IncludeOrganizationalUnit, [string[]] $ExcludeOrganizationalUnit, [string[]] $IncludeSIDHistoryDomain, [string[]] $ExcludeSIDHistoryDomain, [nullable[int]] $RemoveLimitObject, [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType, [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType, [switch] $DisabledOnly, [switch] $DontWriteToEventLog, [bool] $LimitPerObject, [bool] $LimitPerSID, [System.Collections.Generic.List[PSCustomObject]] $ObjectsToProcess ) $GlobalLimitObject = 0 # Process each domain SID :TopLoop foreach ($Domain in $DomainNames) { [Array] $Objects = $Output[$Domain] # Skip if we've already hit our object limit if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) { Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping processing." -Color Yellow, White, Green, White break TopLoop } # Check if this domain SID matches our type filters $DomainInfo = $Output.DomainSIDs[$Domain] if (-not $DomainInfo) { $DomainType = "Unknown" } else { $DomainType = $DomainInfo.Type if ($DomainType -eq 'Domain') { $DomainType = "Internal" } elseif ($DomainType -eq 'Trust') { $DomainType = "External" } } # Apply type filters if ($ExcludeType.Count -gt 0 -and $ExcludeType -contains $DomainType) { Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is excluded." -Color Yellow, White, Red, White, Red, White continue } if ($IncludeType.Count -gt 0 -and $IncludeType -notcontains $DomainType) { Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's type ", $DomainType, " is not included." -Color Yellow, White, Red, White, Red, White continue } # Apply SID domain filters if ($IncludeSIDHistoryDomain -and $IncludeSIDHistoryDomain -notcontains $Domain) { Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's not in the included SID history domains." -Color Yellow, White, Red, White continue } if ($ExcludeSIDHistoryDomain -and $ExcludeSIDHistoryDomain -contains $Domain) { Write-Color -Text "[s] ", "Skipping ", $Domain, " as it's in the excluded SID history domains." -Color Yellow, White, Red, White continue } # Display which domain we're processing $DomainDisplayName = if ($DomainInfo) { $DomainInfo.Domain } else { "Unknown" } Write-Color -Text "[i] ", "Processing domain SID ", $Domain, " (", $DomainDisplayName, " - ", $DomainType, ")" -Color Yellow, White, Green, White, Green, White, Green # Process each object in this domain foreach ($Object in $Objects) { $QueryServer = $ForestInformation['QueryServers'][$Object.Domain].HostName[0] if ($DisabledOnly) { if ($Object.Enabled) { Write-Color -Text "[s] ", "Skipping ", $Object.Name, " as it is enabled and DisabledOnly filter is set." -Color Yellow, White, Red, White continue } } # Check if we need to filter by OU if ($IncludeOrganizationalUnit -and $IncludeOrganizationalUnit -notcontains $Object.OrganizationalUnit) { continue } if ($ExcludeOrganizationalUnit -and $ExcludeOrganizationalUnit -contains $Object.OrganizationalUnit) { continue } 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 # Add to our collection of objects to process $ObjectsToProcess.Add( [PSCustomObject]@{ Object = $Object QueryServer = $QueryServer Domain = $Domain DomainInfo = $DomainInfo DomainType = $DomainType } ) # Increment counter and check limits $GlobalLimitObject++ if ($LimitPerObject -and $GlobalLimitObject -ge $RemoveLimitObject) { Write-Color -Text "[i] ", "Reached object limit of ", $RemoveLimitObject, ". Stopping object collection." -Color Yellow, White, Green, White break TopLoop } } } } function Invoke-ADComputersCleanup { <# .SYNOPSIS Active Directory Cleanup function that can disable or delete computers that have not been logged on for a certain amount of time. .DESCRIPTION Active Directory Cleanup function that can disable or delete computers that have not been logged on for a certain amount of time. It has many options to customize the cleanup process. .PARAMETER Forest Forest to use when connecting to Active Directory. .PARAMETER IncludeDomains List of domains to include in the process. .PARAMETER ExcludeDomains List of domains to exclude from the process. .PARAMETER Disable Enable the disable process, meaning the computers that meet the criteria will be disabled. .PARAMETER DisableAndMove Enable the disable and move process, meaning the computers that meet the criteria will be disabled and moved (in that order). This is useful if you want to disable computers first and then move them to a different OU right after. It's integral part of disabling process. If you want Move as a separate process, use Move settings. .PARAMETER DisableAndMoveOrder Order of the Disable and Move process. Default is 'DisableAndMove'. If you want to move computers first and then disable them, use 'MoveAndDisable'. .PARAMETER DisableIsEnabled Disable computer only if it's Enabled or only if it's Disabled. By default it will try to disable all computers that are either disabled or enabled. While counter-intuitive for already disabled computers, this is useful if you want preproceess computers for deletion and need to get them on the list. .PARAMETER DisableNoServicePrincipalName Disable computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName. By default it doesn't care if it has a ServicePrincipalName or not. .PARAMETER DisableLastLogonDateMoreThan Disable computer only if it has a LastLogonDate that is more than the specified number of days. .PARAMETER DisablePasswordLastSetMoreThan Disable computer only if it has a PasswordLastSet that is more than the specified number of days. .PARAMETER DisableRequireWhenCreatedMoreThan Disable computer only if it was created more than the specified number of days ago. .PARAMETER DisablePasswordLastSetOlderThan Disable computer only if it has a PasswordLastSet that is older than the specified date. .PARAMETER DisableLastLogonDateOlderThan Disable computer only if it has a LastLogonDate that is older than the specified date. .PARAMETER DisableLastSeenAzureMoreThan Disable computer only if it Last Seen in Azure is more than the specified number of days. Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER DisableLastSeenIntuneMoreThan Disable computer only if it Last Seen in Intune is more than the specified number of days. Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER DisableLastSyncAzureMoreThan Disable computer only if it Last Synced in Azure is more than the specified number of days. Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER DisableLastContactJamfMoreThan Disable computer only if it Last Contacted in Jamf is more than the specified number of days. Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first. Additionally you will need PowerJamf PowerShell Module installed. .PARAMETER DisableExcludeSystems Disable computer only if it's not on the list of excluded operating systems. If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. It's using OperatingSystem property of the computer object for comparison. .PARAMETER DisableIncludeSystems Disable computer only if it's on the list of included operating systems. If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. .PARAMETER DisableExcludeServicePrincipalName Disable computer only if it's not on the list of excluded ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER DisableIncludeServicePrincipalName Disable computer only if it's on the list of included ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER DisableMoveTargetOrganizationalUnit Move computer to the specified OU after it's disabled. It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName. If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable. .PARAMETER DisableDoNotAddToPendingList By default, computers that are disabled are added to the list of computers that will be actioned later (moved/deleted). If you want to disable computers, but not add them to the list of computers that will be actioned later (aka pending list), use this switch. .PARAMETER Delete Enable the delete process, meaning the computers that meet the criteria will be deleted. .PARAMETER DeleteIsEnabled Delete computer only if it's Enabled or only if it's Disabled. By default it will try to delete all computers that are either disabled or enabled. .PARAMETER DeleteNoServicePrincipalName Delete computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName. By default it doesn't care if it has a ServicePrincipalName or not. .PARAMETER DeleteLastLogonDateMoreThan Delete computer only if it has a LastLogonDate that is more than the specified number of days. .PARAMETER DeletePasswordLastSetMoreThan Delete computer only if it has a PasswordLastSet that is more than the specified number of days. .PARAMETER DeleteRequireWhenCreatedMoreThan Delete computer only if it was created more than the specified number of days ago. .PARAMETER DeleteListProcessedMoreThan Delete computer only if it has been processed by this script more than the specified number of days ago. This is useful if you want to delete computers that have been disabled for a certain amount of time. It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over. .PARAMETER DeletePasswordLastSetOlderThan Delete computer only if it has a PasswordLastSet that is older than the specified date. .PARAMETER DeleteLastLogonDateOlderThan Delete computer only if it has a LastLogonDate that is older than the specified date. .PARAMETER DeleteLastSeenAzureMoreThan Delete computer only if it Last Seen in Azure is more than the specified number of days. Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first. Additionally yopu will need GraphEssentials PowerShell Module installed. .PARAMETER DeleteLastSeenIntuneMoreThan Delete computer only if it Last Seen in Intune is more than the specified number of days. Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER DeleteLastSyncAzureMoreThan Delete computer only if it Last Synced in Azure is more than the specified number of days. Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER DeleteLastContactJamfMoreThan Delete computer only if it Last Contacted in Jamf is more than the specified number of days. Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first. Additionally you will need PowerJamf PowerShell Module installed. .PARAMETER DeleteExcludeSystems Delete computer only if it's not on the list of excluded operating systems. If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. It's using OperatingSystem property of the computer object for comparison. .PARAMETER DeleteIncludeSystems Delete computer only if it's on the list of included operating systems. If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. .PARAMETER DeleteExcludeServicePrincipalName Delete computer only if it's not on the list of excluded ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER DeleteIncludeServicePrincipalName Delete computer only if it's on the list of included ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER DeleteLimit Limit the number of computers that will be deleted. 0 = unlimited. Default is 1. This is to prevent accidental deletion of all computers that meet the criteria. Adjust the limit to your needs. .PARAMETER MoveIsEnabled Move computer only if it's Enabled or only if it's Disabled. By default it will try to Move all computers that are either disabled or enabled. .PARAMETER MoveNoServicePrincipalName Move computer only if it has a ServicePrincipalName or only if it doesn't have a ServicePrincipalName. By default it doesn't care if it has a ServicePrincipalName or not. .PARAMETER MoveLastLogonDateMoreThan Move computer only if it has a LastLogonDate that is more than the specified number of days. .PARAMETER MovePasswordLastSetMoreThan Move computer only if it has a PasswordLastSet that is more than the specified number of days. .PARAMETER MoveListProcessedMoreThan Move computer only if it has been processed by this script more than the specified number of days ago. This is useful if you want to Move computers that have been disabled for a certain amount of time. It uses XML file to store the list of processed computers, so please make sure to not remove it or it will start over. .PARAMETER MovePasswordLastSetOlderThan Move computer only if it has a PasswordLastSet that is older than the specified date. .PARAMETER MoveRequireWhenCreatedMoreThan Move computer only if it was created more than the specified number of days ago. .PARAMETER MoveLastLogonDateOlderThan Move computer only if it has a LastLogonDate that is older than the specified date. .PARAMETER MoveLastSeenAzureMoreThan Move computer only if it Last Seen in Azure is more than the specified number of days. Please note that you need to make connection to Azure using Connect-MgGraph with proper permissions first. Additionally yopu will need GraphEssentials PowerShell Module installed. .PARAMETER MoveLastSeenIntuneMoreThan Move computer only if it Last Seen in Intune is more than the specified number of days. Please note that you need to make connection to Intune using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER MoveLastSyncAzureMoreThan Move computer only if it Last Synced in Azure is more than the specified number of days. Please note that you need to make connection to Azure AD using Connect-MgGraph with proper permissions first. Additionally you will need GraphEssentials PowerShell Module installed. .PARAMETER MoveLastContactJamfMoreThan Move computer only if it Last Contacted in Jamf is more than the specified number of days. Please note that you need to make connection to Jamf using PowerJamf PowerShell Module first. Additionally you will need PowerJamf PowerShell Module installed. .PARAMETER MoveExcludeSystems Move computer only if it's not on the list of excluded operating systems. If you want to exclude Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. It's using OperatingSystem property of the computer object for comparison. .PARAMETER MoveIncludeSystems Move computer only if it's on the list of included operating systems. If you want to include Windows 10, you can specify 'Windows 10' or 'Windows 10*' or 'Windows 10*' or '*Windows 10*' or '*Windows 10*'. You can also specify multiple operating systems by separating them with a comma. It's using the -like operator, so you can use wildcards. .PARAMETER MoveExcludeServicePrincipalName Move computer only if it's not on the list of excluded ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER MoveIncludeServicePrincipalName Move computer only if it's on the list of included ServicePrincipalNames. You can also specify multiple ServicePrincipalNames by providing an array of entries. It's using the -like operator, so you can use wildcards. .PARAMETER MoveTargetOrganizationalUnit Target Organizational Unit where the computer will be moved as part of Move action. It can take a string with DistinguishedName, or hashtable with key being the domain, and value being the DistinguishedName. If you have a forest with multiple domains and want to move computers to different OUs based on their domain, you can use hashtable. .PARAMETER MoveLimit Limit the number of computers that will be moved. 0 = unlimited. Default is 1. This is to prevent accidental move of all computers that meet the criteria. Adjust the limit to your needs. .PARAMETER MoveDoNotAddToPendingList By default the script will add computers that are moved to a list of computers that will be actioned later (deleted). If you want to move computers, but not add them to the list of computers that will be action later (aka pending list), use this switch. .PARAMETER DeleteLimit Limit the number of computers that will be deleted. 0 = unlimited. Default is 1. This is to prevent accidental deletion of all computers that meet the criteria. Adjust the limit to your needs. .PARAMETER DisableLimit Limit the number of computers that will be disabled. 0 = unlimited. Default is 1. This is to prevent accidental disabling of all computers that meet the criteria. Adjust the limit to your needs. .PARAMETER Exclusions List of computers to exclude from the process. You can specify multiple computers by separating them with a comma. It's using the -like operator, so you can use wildcards. You can use SamAccoutName (remember about ending $), DistinguishedName, or DNSHostName property of the computer object for comparison. .PARAMETER DisableModifyDescription Modify the description of the computer object to include the date and time when it was disabled. By default it will not modify the description. .PARAMETER DisableModifyAdminDescription Modify the admin description of the computer object to include the date and time when it was disabled. By default it will not modify the admin description. .PARAMETER Filter Filter to use when searching for computers in Get-ADComputer cmdlet. Default is '*' .PARAMETER SearchBase SearchBase to use when searching for computers in Get-ADComputer cmdlet. Default is not set. It will search the whole domain. You can provide a string or hashtable of domains with their SearchBase. .PARAMETER DataStorePath Path to the XML file that will be used to store the list of processed computers, current run, and history data. Default is $PSScriptRoot\ProcessedComputers.xml .PARAMETER ReportOnly Only generate the report, don't disable or delete computers. .PARAMETER ReportMaximum Maximum number of reports to keep. Default is Unlimited (0). .PARAMETER WhatIfDelete WhatIf parameter for the Delete process. It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes. .PARAMETER WhatIfDisable WhatIf parameter for the Disable process. It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes. .PARAMETER WhatIfMove WhatIf parameter for the Move process. It's not nessessary to specify this parameter if you use WhatIf parameter which applies to all processes. .PARAMETER LogPath Path to the log file. Default is no logging to file. .PARAMETER LogMaximum Maximum number of log files to keep. Default is 5. .PARAMETER LogShowTime Show time in the log file. Default is $false .PARAMETER LogTimeFormat Time format to use when logging to file. Default is 'yyyy-MM-dd HH:mm:ss' .PARAMETER Suppress Suppress output of the object and only display to console .PARAMETER ShowHTML Show HTML report in the browser once the function is complete .PARAMETER Online Online parameter causes HTML report to use CDN for CSS and JS files. This can be useful to minimize the size of the HTML report. Otherwise the report will start with at least 2MB in size. .PARAMETER ReportPath Path to the HTML report file. Default is $PSScriptRoot\ProcessedComputers.html .PARAMETER SafetyADLimit Minimum number of computers that must be returned by AD cmdlets to proceed with the process. Default is not to check. This is there to prevent accidental deletion of all computers if there is a problem with AD. .PARAMETER SafetyAzureADLimit Minimum number of computers that must be returned by AzureAD cmdlets to proceed with the process. Default is not to check. This is there to prevent accidental deletion of all computers if there is a problem with AzureAD. It only applies if Azure AD parameters are used. .PARAMETER SafetyIntuneLimit Minimum number of computers that must be returned by Intune cmdlets to proceed with the process. Default is not to check. This is there to prevent accidental deletion of all computers if there is a problem with Intune. It only applies if Intune parameters are used. .PARAMETER SafetyJamfLimit Minimum number of computers that must be returned by Jamf cmdlets to proceed with the process. Default is not to check. This is there to prevent accidental deletion of all computers if there is a problem with Jamf. It only applies if Jamf parameters are used. .PARAMETER DontWriteToEventLog By default the function will write to the event log making sure the cleanup process is logged. This parameter will prevent the function from writing to the event log. .PARAMETER TargetServers Target servers to use when connecting to Active Directory. It can take a string with server name, or hashtable with key being the domain, and value being the server name. If you have a forest with multiple domains and want to use different servers for different domains, you can use hashtable. It will use the default server if no server is provided for a domain, which is default approach. This feature is only nessecary if you have specific requirments per domain/forest rather than using the automatic detection. .PARAMETER RemoveProtectedFromAccidentalDeletionFlag Remove the ProtectedFromAccidentalDeletion flag from the computer object before deleting it. By default it will not remove the flag, and require it to be removed manually. .EXAMPLE $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 $Output .EXAMPLE $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 $Output .EXAMPLE # this is a fresh run and it will provide report only according to it's defaults $Output = Invoke-ADComputersCleanup -WhatIf -ReportOnly -Disable -Delete -ShowHTML $Output .EXAMPLE # this is a fresh run and it will try to disable computers according to it's defaults # read documentation to understand what it does $Output = Invoke-ADComputersCleanup -Disable -ShowHTML -WhatIfDisable -WhatIfDelete -Delete $Output .EXAMPLE # this is a fresh run and it will try to delete computers according to it's defaults # read documentation to understand what it does $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 $Output .EXAMPLE # Run the script $Configuration = @{ Disable = $true DisableNoServicePrincipalName = $null DisableIsEnabled = $true DisableLastLogonDateMoreThan = 90 DisablePasswordLastSetMoreThan = 90 DisableExcludeSystems = @( # 'Windows Server*' ) DisableIncludeSystems = @() DisableLimit = 2 # 0 means unlimited, ignored for reports DisableModifyDescription = $false DisableAdminModifyDescription = $true Delete = $true DeleteIsEnabled = $false DeleteNoServicePrincipalName = $null DeleteLastLogonDateMoreThan = 180 DeletePasswordLastSetMoreThan = 180 DeleteListProcessedMoreThan = 90 # 90 days since computer was added to list DeleteExcludeSystems = @( # 'Windows Server*' ) DeleteIncludeSystems = @( ) DeleteLimit = 2 # 0 means unlimited, ignored for reports Exclusions = @( '*OU=Domain Controllers*' '*OU=Servers,OU=Production*' 'EVOMONSTER$' 'EVOMONSTER.AD.EVOTEC.XYZ' ) Filter = '*' WhatIfDisable = $true WhatIfDelete = $true LogPath = "$PSScriptRoot\Logs\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).log" DataStorePath = "$PSScriptRoot\DeleteComputers_ListProcessed.xml" ReportPath = "$PSScriptRoot\Reports\DeleteComputers_$((Get-Date).ToString('yyyy-MM-dd_HH_mm_ss')).html" ShowHTML = $true } # Run one time as admin: Write-Event -ID 10 -LogName 'Application' -EntryType Information -Category 0 -Message 'Initialize' -Source 'CleanupComputers' $Output = Invoke-ADComputersCleanup @Configuration $Output .NOTES General notes #> [CmdletBinding(SupportsShouldProcess)] param( [string] $Forest, [alias('Domain')][string[]] $IncludeDomains, [string[]] $ExcludeDomains, # Disable options [switch] $Disable, [switch] $DisableAndMove, [ValidateSet( 'DisableAndMove', 'MoveAndDisable' )][string] $DisableAndMoveOrder = 'DisableAndMove', [nullable[bool]] $DisableIsEnabled, [nullable[bool]] $DisableNoServicePrincipalName, [nullable[int]] $DisableLastLogonDateMoreThan = 180, [nullable[int]] $DisablePasswordLastSetMoreThan = 180, [nullable[int]] $DisableRequireWhenCreatedMoreThan = 90, [nullable[DateTime]] $DisablePasswordLastSetOlderThan, [nullable[DateTime]] $DisableLastLogonDateOlderThan, [nullable[int]] $DisableLastSeenAzureMoreThan, [nullable[int]] $DisableLastSeenIntuneMoreThan, [nullable[int]] $DisableLastSyncAzureMoreThan, [nullable[int]] $DisableLastContactJamfMoreThan, [Array] $DisableExcludeSystems = @(), [Array] $DisableIncludeSystems = @(), [Array] $DisableExcludeServicePrincipalName = @(), [Array] $DisableIncludeServicePrincipalName = @(), [int] $DisableLimit = 1, # 0 = unlimited [Object] $DisableMoveTargetOrganizationalUnit, [switch] $DisableDoNotAddToPendingList, # Move options [switch] $Move, [nullable[bool]] $MoveIsEnabled, [nullable[bool]] $MoveNoServicePrincipalName, [nullable[int]] $MoveLastLogonDateMoreThan, [nullable[int]] $MovePasswordLastSetMoreThan, [nullable[int]] $MoveListProcessedMoreThan, [nullable[int]] $MoveRequireWhenCreatedMoreThan, [nullable[DateTime]] $MovePasswordLastSetOlderThan, [nullable[DateTime]] $MoveLastLogonDateOlderThan, [nullable[int]] $MoveLastSeenAzureMoreThan, [nullable[int]] $MoveLastSeenIntuneMoreThan, [nullable[int]] $MoveLastSyncAzureMoreThan, [nullable[int]] $MoveLastContactJamfMoreThan, [Array] $MoveExcludeSystems = @(), [Array] $MoveIncludeSystems = @(), [Array] $MoveExcludeServicePrincipalName = @(), [Array] $MoveIncludeServicePrincipalName = @(), [int] $MoveLimit = 1, # 0 = unlimited [Object] $MoveTargetOrganizationalUnit, # Delete options [switch] $Delete, [nullable[bool]] $DeleteIsEnabled, [nullable[bool]] $DeleteNoServicePrincipalName, [nullable[int]] $DeleteLastLogonDateMoreThan = 180, [nullable[int]] $DeletePasswordLastSetMoreThan = 180, [nullable[int]] $DeleteRequireWhenCreatedMoreThan = 90, [nullable[int]] $DeleteListProcessedMoreThan, [nullable[DateTime]] $DeletePasswordLastSetOlderThan, [nullable[DateTime]] $DeleteLastLogonDateOlderThan, [nullable[int]] $DeleteLastSeenAzureMoreThan, [nullable[int]] $DeleteLastSeenIntuneMoreThan, [nullable[int]] $DeleteLastSyncAzureMoreThan, [nullable[int]] $DeleteLastContactJamfMoreThan, [Array] $DeleteExcludeSystems = @(), [Array] $DeleteIncludeSystems = @(), [Array] $DeleteExcludeServicePrincipalName = @(), [Array] $DeleteIncludeServicePrincipalName = @(), [int] $DeleteLimit = 1, # 0 = unlimited # General options [Array] $Exclusions = @( # default globalexclusions '*OU=Domain Controllers*' ), [switch] $DisableModifyDescription, [alias('DisableAdminModifyDescription')][switch] $DisableModifyAdminDescription, [object] $Filter = '*', [object] $SearchBase, [string] $DataStorePath, [switch] $ReportOnly, [int] $ReportMaximum, [switch] $WhatIfDelete, [switch] $WhatIfDisable, [switch] $WhatIfMove, [string] $LogPath, [int] $LogMaximum = 5, [switch] $LogShowTime, [string] $LogTimeFormat, [switch] $Suppress, [switch] $ShowHTML, [switch] $Online, [string] $ReportPath, [nullable[int]] $SafetyADLimit, [nullable[int]] $SafetyAzureADLimit, [nullable[int]] $SafetyIntuneLimit, [nullable[int]] $SafetyJamfLimit, [switch] $DontWriteToEventLog, [Object] $TargetServers, [switch] $RemoveProtectedFromAccidentalDeletionFlag ) # we will use it to check for intune/azuread/jamf functionality $Script:CleanupOptions = [ordered] @{} # just in case user wants to use -WhatIf instead of -WhatIfDelete and -WhatIfDisable if (-not $WhatIfDelete -and -not $WhatIfDisable) { $WhatIfDelete = $WhatIfDisable = $WhatIfPreference } # lets enable global logging Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName # prepare configuration $DisableOnlyIf = [ordered] @{ # Active directory IsEnabled = $DisableIsEnabled NoServicePrincipalName = $DisableNoServicePrincipalName LastLogonDateMoreThan = $DisableLastLogonDateMoreThan PasswordLastSetMoreThan = $DisablePasswordLastSetMoreThan ExcludeSystems = $DisableExcludeSystems IncludeSystems = $DisableIncludeSystems ExcludeServicePrincipalName = $DisableExcludeServicePrincipalName IncludeServicePrincipalName = $DisableIncludeServicePrincipalName PasswordLastSetOlderThan = $DisablePasswordLastSetOlderThan LastLogonDateOlderThan = $DisableLastLogonDateOlderThan # Intune LastSeenIntuneMoreThan = $DisableLastSeenIntuneMoreThan # Azure LastSyncAzureMoreThan = $DisableLastSyncAzureMoreThan LastSeenAzureMoreThan = $DisableLastSeenAzureMoreThan # Jamf LastContactJamfMoreThan = $DisableLastContactJamfMoreThan DoNotAddToPendingList = $DisableDoNotAddToPendingList MoveTargetOrganizationalUnit = $DisableMoveTargetOrganizationalUnit DisableAndMove = $DisableAndMove.IsPresent RequireWhenCreatedMoreThan = $DisableRequireWhenCreatedMoreThan } $MoveOnlyIf = [ordered] @{ # Active directory IsEnabled = $MoveIsEnabled NoServicePrincipalName = $MoveNoServicePrincipalName LastLogonDateMoreThan = $MoveLastLogonDateMoreThan PasswordLastSetMoreThan = $MovePasswordLastSetMoreThan ListProcessedMoreThan = $MoveListProcessedMoreThan ExcludeSystems = $MoveExcludeSystems IncludeSystems = $MoveIncludeSystems ExcludeServicePrincipalName = $MoveExcludeServicePrincipalName IncludeServicePrincipalName = $MoveIncludeServicePrincipalName PasswordLastSetOlderThan = $MovePasswordLastSetOlderThan LastLogonDateOlderThan = $MoveLastLogonDateOlderThan # Intune LastSeenIntuneMoreThan = $MoveLastSeenIntuneMoreThan # Azure LastSeenAzureMoreThan = $MoveLastSeenAzureMoreThan LastSyncAzureMoreThan = $MoveLastSyncAzureMoreThan # Jamf LastContactJamfMoreThan = $MoveLastContactJamfMoreThan # special option for move only TargetOrganizationalUnit = $MoveTargetOrganizationalUnit DoNotAddToPendingList = $MoveDoNotAddToPendingList #MoveTargetOrganizationalUnit = $MoveTargetOrganizationalUnit RequireWhenCreatedMoreThan = $MoveRequireWhenCreatedMoreThan } $DeleteOnlyIf = [ordered] @{ # Active directory IsEnabled = $DeleteIsEnabled NoServicePrincipalName = $DeleteNoServicePrincipalName LastLogonDateMoreThan = $DeleteLastLogonDateMoreThan PasswordLastSetMoreThan = $DeletePasswordLastSetMoreThan ListProcessedMoreThan = $DeleteListProcessedMoreThan ExcludeSystems = $DeleteExcludeSystems IncludeSystems = $DeleteIncludeSystems ExcludeServicePrincipalName = $DeleteExcludeServicePrincipalName IncludeServicePrincipalName = $DeleteIncludeServicePrincipalName PasswordLastSetOlderThan = $DeletePasswordLastSetOlderThan LastLogonDateOlderThan = $DeleteLastLogonDateOlderThan # Intune LastSeenIntuneMoreThan = $DeleteLastSeenIntuneMoreThan # Azure LastSeenAzureMoreThan = $DeleteLastSeenAzureMoreThan LastSyncAzureMoreThan = $DeleteLastSyncAzureMoreThan # Jamf LastContactJamfMoreThan = $DeleteLastContactJamfMoreThan RequireWhenCreatedMoreThan = $DeleteRequireWhenCreatedMoreThan } if (-not $DataStorePath) { $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.xml' } if (-not $ReportPath) { $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedComputers.html' } # lets create report path, reporting is enabled by default Set-ReportingCapabilities -ReportPath $ReportPath -ReportMaximum $ReportMaximum -ScriptPath $MyInvocation.ScriptName $Success = Assert-InitialSettings -DisableOnlyIf $DisableOnlyIf -MoveOnlyIf $MoveOnlyIf -DeleteOnlyIf $DeleteOnlyIf if ($Success -contains $false) { return } $Today = Get-Date $Properties = 'DistinguishedName', 'DNSHostName', 'SamAccountName', 'Enabled', 'OperatingSystem', 'OperatingSystemVersion', 'LastLogonDate', 'PasswordLastSet', 'PasswordExpired', 'servicePrincipalName', 'logonCount', 'ManagedBy', 'Description', 'WhenCreated', 'WhenChanged', 'ProtectedFromAccidentalDeletion' $Export = [ordered] @{ Version = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster' CurrentRun = $null History = $null PendingDeletion = $null } Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color -Text "[i] ", "Started process of cleaning up stale computers" -Color Yellow, White Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White try { $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest -Extended -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains } catch { Write-Color -Text "[i] ", "Couldn't get forest. Terminating. Lack of domain contact? Error: $($_.Exception.Message)." -Color Yellow, Red return } if (-not $ReportOnly) { $ProcessedComputers = Import-ComputersData -Export $Export -DataStorePath $DataStorePath if ($ProcessedComputers -eq $false) { return } Write-Color -Text "[i] ", "Loaded ", $($ProcessedComputers.Count), " computers from $($DataStorePath) and added to pending list of computers." -Color Yellow, White, Green, White } if (-not $Disable -and -not $Delete) { Write-Color -Text "[i] ", "No action can be taken. You need to enable Disable or/and Delete feature to have any action." -Color Yellow, Red return } $Report = [ordered] @{} $getInitialGraphComputersSplat = [ordered] @{ SafetyAzureADLimit = $SafetyAzureADLimit SafetyIntuneLimit = $SafetyIntuneLimit DeleteLastSeenAzureMoreThan = $DeleteLastSeenAzureMoreThan DeleteLastSeenIntuneMoreThan = $DeleteLastSeenIntuneMoreThan DeleteLastSyncAzureMoreThan = $DeleteLastSyncAzureMoreThan DisableLastSeenAzureMoreThan = $DisableLastSeenAzureMoreThan DisableLastSeenIntuneMoreThan = $DisableLastSeenIntuneMoreThan DisableLastSyncAzureMoreThan = $DisableLastSyncAzureMoreThan MoveLastSeenAzureMoreThan = $MoveLastSeenAzureMoreThan MoveLastSeenIntuneMoreThan = $MoveLastSeenIntuneMoreThan MoveLastSyncAzureMoreThan = $MoveLastSyncAzureMoreThan } Remove-EmptyValue -Hashtable $getInitialGraphComputersSplat $AzureInformationCache = Get-InitialGraphComputers @getInitialGraphComputersSplat if ($AzureInformationCache -eq $false) { return } $getInitialJamf = @{ DisableLastContactJamfMoreThan = $DisableLastContactJamfMoreThan DeleteLastContactJamfMoreThan = $DeleteLastContactJamfMoreThan MoveLastContactJamfMoreThan = $MoveLastContactJamfMoreThan SafetyJamfLimit = $SafetyJamfLimit } Remove-EmptyValue -Hashtable $getInitialJamf $JamfInformationCache = Get-InitialJamfComputers @getInitialJamf if ($JamfInformationCache -eq $false) { return } $SplatADComputers = [ordered] @{ Report = $Report ForestInformation = $ForestInformation Filter = $Filter SearchBase = $SearchBase Properties = $Properties Disable = $Disable Delete = $Delete Move = $Move DisableOnlyIf = $DisableOnlyIf DeleteOnlyIf = $DeleteOnlyIf MoveOnlyIf = $MoveOnlyIf Exclusions = $Exclusions ProcessedComputers = $ProcessedComputers SafetyADLimit = $SafetyADLimit AzureInformationCache = $AzureInformationCache JamfInformationCache = $JamfInformationCache TargetServers = $TargetServers } $AllComputers = Get-InitialADComputers @SplatADComputers if ($AllComputers -eq $false) { return } foreach ($Domain in $Report.Keys) { if ($Disable -or $DisableAndMove) { if ($DisableLimit -eq 0) { $DisableLimitText = 'Unlimited' } else { $DisableLimitText = $DisableLimit } Write-Color "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'], ". Current disable limit: ", $DisableLimitText -Color Yellow, Cyan, Green, Cyan, Yellow } if ($Move) { if ($MoveLimit -eq 0) { $MoveLimitText = 'Unlimited' } else { $MoveLimitText = $MoveLimit } Write-Color "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'], ". Current move limit: ", $MoveLimitText -Color Yellow, Cyan, Green, Cyan, Yellow } if ($Delete) { if ($DeleteLimit -eq 0) { $DeleteLimitText = 'Unlimited' } else { $DeleteLimitText = $DeleteLimit } Write-Color "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'], ". Current delete limit: ", $DeleteLimitText -Color Yellow, Cyan, Green, Cyan, Yellow } } if ($Disable -or $DisableAndMove) { $requestADComputersDisableSplat = @{ # those 2 are added only to make sure we don't add to processing list # if there is no process later on Delete = $Delete Move = $Move # we can disable and move on one go DisableAndMove = $DisableAndMove Report = $Report WhatIfDisable = $WhatIfDisable WhatIf = $WhatIfPreference DisableModifyDescription = $DisableModifyDescription.IsPresent DisableModifyAdminDescription = $DisableModifyAdminDescription.IsPresent DisableLimit = $DisableLimit ReportOnly = $ReportOnly Today = $Today DontWriteToEventLog = $DontWriteToEventLog DisableMoveTargetOrganizationalUnit = $DisableMoveTargetOrganizationalUnit DoNotAddToPendingList = $DisableDoNotAddToPendingList DisableAndMoveOrder = $DisableAndMoveOrder RemoveProtectedFromAccidentalDeletionFlag = $RemoveProtectedFromAccidentalDeletionFlag.IsPresent } [Array] $ReportDisabled = Request-ADComputersDisable @requestADComputersDisableSplat } if ($Move) { $requestADComputersMoveSplat = @{ Report = $Report WhatIfMove = $WhatIfMove WhatIf = $WhatIfPreference MoveLimit = $MoveLimit ReportOnly = $ReportOnly Today = $Today ProcessedComputers = $ProcessedComputers TargetOrganizationalUnit = $MoveTargetOrganizationalUnit DontWriteToEventLog = $DontWriteToEventLog Delete = $Delete DoNotAddToPendingList = $MoveDoNotAddToPendingList RemoveProtectedFromAccidentalDeletionFlag = $RemoveProtectedFromAccidentalDeletionFlag.IsPresent } [Array] $ReportMoved = Request-ADComputersMove @requestADComputersMoveSplat } if ($Delete) { $requestADComputersDeleteSplat = @{ Report = $Report WhatIfDelete = $WhatIfDelete WhatIf = $WhatIfPreference DeleteLimit = $DeleteLimit ReportOnly = $ReportOnly Today = $Today ProcessedComputers = $ProcessedComputers DontWriteToEventLog = $DontWriteToEventLog RemoveProtectedFromAccidentalDeletionFlag = $RemoveProtectedFromAccidentalDeletionFlag.IsPresent } [Array] $ReportDeleted = Request-ADComputersDelete @requestADComputersDeleteSplat } Write-Color "[i] ", "Cleanup process for processed computers that no longer exists in AD" -Color Yellow, Green foreach ($FullName in [string[]] $ProcessedComputers.Keys) { if (-not $AllComputers["$($FullName)"]) { Write-Color -Text "[*] Removing computer from pending list ", $ProcessedComputers[$FullName].SamAccountName, " ($($ProcessedComputers[$FullName].DistinguishedName))" -Color Yellow, Green, Yellow $ProcessedComputers.Remove("$($FullName)") } } # Building up summary $Export.PendingDeletion = $ProcessedComputers $Export.CurrentRun = @( if ($ReportDisabled.Count -gt 0) { $ReportDisabled } if ($ReportMoved.Count -gt 0) { $ReportMoved } if ($ReportDeleted.Count -gt 0) { $ReportDeleted } ) $Export.History = @( if ($Export.History) { $Export.History } if ($ReportDisabled.Count -gt 0) { $ReportDisabled } if ($ReportMoved.Count -gt 0) { $ReportMoved } if ($ReportDeleted.Count -gt 0) { $ReportDeleted } ) Write-Color "[i] ", "Exporting Processed List" -Color Yellow, Magenta if (-not $ReportOnly) { try { $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop } catch { Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red } } Write-Color -Text "[i] ", "Summary of cleaning up stale computers" -Color Yellow, Cyan foreach ($Domain in $Report.Keys) { if ($Disable -or $DisableAndMove) { Write-Color -Text "[i] ", "Computers to be disabled for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDisabled'] -Color Yellow, Cyan, Green } if ($Move) { Write-Color -Text "[i] ", "Computers to be moved for domain $Domain`: ", $Report["$Domain"]['ComputersToBeMoved'] -Color Yellow, Cyan, Green } if ($Delete) { Write-Color -Text "[i] ", "Computers to be deleted for domain $Domain`: ", $Report["$Domain"]['ComputersToBeDeleted'] -Color Yellow, Cyan, Green } } if (-not $ReportOnly) { Write-Color -Text "[i] ", "Computers on pending list`: ", $Export['PendingDeletion'].Count -Color Yellow, Cyan, Green } if (($Disable -or $DisableAndMove) -and -not $ReportOnly) { Write-Color -Text "[i] ", "Computers disabled in this run`: ", $ReportDisabled.Count -Color Yellow, Cyan, Green } if ($Move -and -not $ReportOnly) { Write-Color -Text "[i] ", "Computers moved in this run`: ", $ReportMoved.Count -Color Yellow, Cyan, Green } if ($Delete -and -not $ReportOnly) { Write-Color -Text "[i] ", "Computers deleted in this run`: ", $ReportDeleted.Count -Color Yellow, Cyan, Green } if ($Export -and $ReportPath) { [Array] $ComputersToProcess = foreach ($Domain in $Report.Keys) { if ($Report["$Domain"]['Computers'].Count -gt 0) { $Report["$Domain"]['Computers'] } } Write-Color -Text "[i] ", "Computers to be processed for HTML report`: ", $ComputersToProcess.Count -Color Yellow, Cyan, Green $Export.Statistics = New-ADComputersStatistics -ComputersToProcess $ComputersToProcess $newHTMLProcessedComputersSplat = @{ Export = $Export FilePath = $ReportPath Online = $Online.IsPresent ShowHTML = $ShowHTML.IsPresent LogFile = $LogPath ComputersToProcess = $ComputersToProcess DisableOnlyIf = $DisableOnlyIf DeleteOnlyIf = $DeleteOnlyIf MoveOnlyIf = $MoveOnlyIf Delete = $Delete Disable = $Disable Move = $Move ReportOnly = $ReportOnly } Write-Color "[i] ", "Generating HTML report ($ReportPath)" -Color Yellow, Magenta New-HTMLProcessedComputers @newHTMLProcessedComputersSplat } Write-Color -Text "[i] Finished process of cleaning up stale computers" -Color Green if (-not $Suppress) { $Export.EmailBody = New-EmailBodyComputers -CurrentRun $Export.CurrentRun $Export } } function Invoke-ADSIDHistoryCleanup { <# .SYNOPSIS Cleans up SID history entries in Active Directory based on various filtering criteria. .DESCRIPTION This function identifies and removes SID history entries from AD objects based on specified filters. It can target internal domains (same forest), external domains (trusted), or unknown domains. The function allows for detailed reporting before making any changes. .PARAMETER Forest The name of the forest to process. If not specified, uses the current forest. .PARAMETER IncludeDomains An array of domain names to include in the cleanup process. .PARAMETER ExcludeDomains An array of domain names to exclude from the cleanup process. .PARAMETER IncludeOrganizationalUnit An array of organizational units to include in the cleanup process. .PARAMETER ExcludeOrganizationalUnit An array of organizational units to exclude from the cleanup process. .PARAMETER IncludeSIDHistoryDomain An array of domain SIDs to include when cleaning up SID history. .PARAMETER ExcludeSIDHistoryDomain An array of domain SIDs to exclude when cleaning up SID history. .PARAMETER RemoveLimitSID Limits the total number of SID history entries to remove. .PARAMETER RemoveLimitObject Limits the total number of objects to process for SID history removal. Defaults to 1 to prevent accidental mass deletions. .PARAMETER IncludeType Specifies which types of SID history to include: 'Internal', 'External', or 'Unknown'. Defaults to all three types if not specified. .PARAMETER ExcludeType Specifies which types of SID history to exclude: 'Internal', 'External', or 'Unknown'. .PARAMETER DisabledOnly Only processes objects that are disabled. .PARAMETER SafetyADLimit Stops processing if the number of objects with SID history in AD is less than the specified limit. .PARAMETER LogPath The path to the log file to write. .PARAMETER LogMaximum The maximum number of log files to keep. .PARAMETER LogShowTime If specified, includes the time in the log entries. .PARAMETER LogTimeFormat The format to use for the time in the log entries. .PARAMETER Suppress Suppresses the output of the function and only returns the summary information. .PARAMETER ShowHTML If specified, shows the HTML report in the default browser. .PARAMETER Online If specified, uses online resources in HTML report (CSS/JS is loaded from CDN). Otherwise local resources are used (bigger HTML file). .PARAMETER DataStorePath Path to the XML file used to store processed SID history entries. .PARAMETER ReportOnly If specified, only generates a report without making any changes. .PARAMETER Report Generates a report of affected objects without making any changes. .PARAMETER ReportPath The path where the HTML report should be saved. Used with the -Report parameter. .PARAMETER WhatIf Shows what would happen if the function runs. The SID history entries aren't actually removed. .EXAMPLE Invoke-ADSIDHistoryCleanup -Forest "contoso.com" -IncludeType "External" -ReportOnly -ReportPath "C:\Temp\SIDHistoryReport.html" -WhatIf Generates a report of external SID history entries in the contoso.com forest without making any changes. .EXAMPLE Invoke-ADSIDHistoryCleanup -IncludeDomains "domain1.local" -IncludeType "Internal" -RemoveLimitSID 2 -WhatIf Removes up to 2 internal SID history entries from objects in domain1.local. .EXAMPLE Invoke-ADSIDHistoryCleanup -ExcludeSIDHistoryDomain "S-1-5-21-1234567890-1234567890-1234567890" -WhatIf -RemoveLimitObject 2 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. .EXAMPLE # Prepare splat $invokeADSIDHistoryCleanupSplat = @{ Verbose = $true WhatIf = $true IncludeSIDHistoryDomain = @( 'S-1-5-21-3661168273-3802070955-2987026695' 'S-1-5-21-853615985-2870445339-3163598659' ) IncludeType = 'External' RemoveLimitSID = 1 RemoveLimitObject = 2 SafetyADLimit = 1 ShowHTML = $true Online = $true DisabledOnly = $true #ReportOnly = $true LogPath = "C:\Temp\ProcessedSIDHistory.log" ReportPath = "$PSScriptRoot\ProcessedSIDHistory.html" DataStorePath = "$PSScriptRoot\ProcessedSIDHistory.xml" } # Run the script $Output = Invoke-ADSIDHistoryCleanup @invokeADSIDHistoryCleanupSplat $Output | Format-Table -AutoSize # Lets send an email $EmailBody = $Output.EmailBody Connect-MgGraph -Scopes 'Mail.Send' -NoWelcome Send-EmailMessage -To 'przemyslaw.klys@test.pl' -From 'przemyslaw.klys@test.pl' -MgGraphRequest -Subject "Automated SID Cleanup Report" -Body $EmailBody -Priority Low -Verbose #> [CmdletBinding(SupportsShouldProcess)] param ( [string] $Forest, [alias('Domain')][string[]] $IncludeDomains, [string[]] $ExcludeDomains, [string[]] $IncludeOrganizationalUnit, [string[]] $ExcludeOrganizationalUnit, [string[]] $IncludeSIDHistoryDomain, [string[]] $ExcludeSIDHistoryDomain, [nullable[int]] $RemoveLimitSID, [nullable[int]] $RemoveLimitObject = 1, [ValidateSet('Internal', 'External', 'Unknown')][string[]] $IncludeType = @('Internal', 'External', 'Unknown'), [ValidateSet('Internal', 'External', 'Unknown')][string[]] $ExcludeType = @(), [string] $ReportPath, [string] $DataStorePath, [switch] $ReportOnly, [string] $LogPath, [int] $LogMaximum = 5, [switch] $LogShowTime, [string] $LogTimeFormat, [switch] $Suppress, [switch] $ShowHTML, [switch] $Online, [switch] $DisabledOnly, [nullable[int]] $SafetyADLimit, [switch] $DontWriteToEventLog ) if (-not $DataStorePath) { $DataStorePath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.xml' } if (-not $ReportPath) { $ReportPath = $($MyInvocation.PSScriptRoot) + '\ProcessedSIDHistory.html' } # lets enable global logging Set-LoggingCapabilities -LogPath $LogPath -LogMaximum $LogMaximum -ShowTime:$LogShowTime -TimeFormat $LogTimeFormat -ScriptPath $MyInvocation.ScriptName $Export = [ordered] @{ Date = Get-Date Version = Get-GitHubVersion -Cmdlet 'Invoke-ADComputersCleanup' -RepositoryOwner 'evotecit' -RepositoryName 'CleanupMonster' ObjectsToProcess = $null CurrentRun = $null History = [System.Collections.Generic.List[PSCustomObject]]::new() } Write-Color '[i] ', "[CleanupMonster] ", 'Version', ' [Informative] ', $Export['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta Write-Color -Text "[i] ", "Started process of cleaning up SID History for AD Objects" -Color Yellow, White Write-Color -Text "[i] ", "Executed by: ", $Env:USERNAME, ' from domain ', $Env:USERDNSDOMAIN -Color Yellow, White, Green, White $Export = Import-SIDHistory -DataStorePath $DataStorePath -Export $Export # Determine if we're using limits if ($Null -eq $RemoveLimitSID -and $null -eq $RemoveLimitObject) { $LimitPerObject = $false $LimitPerSID = $false } elseif ($Null -eq $RemoveLimitSID) { $LimitPerObject = $true $LimitPerSID = $false } elseif ($Null -eq $RemoveLimitObject) { $LimitPerObject = $false $LimitPerSID = $true } else { $LimitPerObject = $true $LimitPerSID = $true } $Configuration = [ordered] @{ Forest = $Forest IncludeDomains = $IncludeDomains ExcludeDomains = $ExcludeDomains IncludeOrganizationalUnit = $IncludeOrganizationalUnit ExcludeOrganizationalUnit = $ExcludeOrganizationalUnit IncludeSIDHistoryDomain = $IncludeSIDHistoryDomain ExcludeSIDHistoryDomain = $ExcludeSIDHistoryDomain RemoveLimitSID = $RemoveLimitSID RemoveLimitObject = $RemoveLimitObject IncludeType = $IncludeType ExcludeType = $ExcludeType LimitPerObject = $LimitPerObject LimitPerSID = $LimitPerSID SafetyADLimit = $SafetyADLimit DisabledOnly = $DisabledOnly LogPath = $LogPath LogMaximum = $LogMaximum LogShowTime = $LogShowTime LogTimeFormat = $LogTimeFormat DontWriteToEventLog = $DontWriteToEventLog } # Initialize collections to store objects for processing or reporting $ObjectsToProcess = [System.Collections.Generic.List[PSCustomObject]]::new() # Get forest details using existing function $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -PreferWritable # Get SID history information $Output = Get-WinADSIDHistory -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -All if ($SafetyADLimit -and $Output.All.Count -le $SafetyADLimit) { Write-Color -Text "[i] ", "Number of objects returned with SIDHistory in AD is less than SafetyADLimit ", $SafetyADLimit, ". Stopping processing." -Color Yellow, White, Green, White return } # Extract domain names from the output [Array] $DomainNames = foreach ($Key in $Output.Keys) { if ($Key -in @('Statistics', 'Trusts', 'DomainSIDs', 'DuplicateSIDs', 'All')) { continue } $Key } $requestADSIDHistorySplat = @{ DomainNames = $DomainNames Output = $Output Export = $Export IncludeSIDHistoryDomain = $IncludeSIDHistoryDomain ExcludeSIDHistoryDomain = $ExcludeSIDHistoryDomain IncludeType = $IncludeType ExcludeType = $ExcludeType RemoveLimitObject = $RemoveLimitObject LimitPerObject = $LimitPerObject LimitPerSID = $LimitPerSID ObjectsToProcess = $ObjectsToProcess DisabledOnly = $DisabledOnly ForestInformation = $ForestInformation } Request-ADSIDHistory @requestADSIDHistorySplat if (-not $ReportOnly) { # Process the collected objects for SID removal Remove-ADSIDHistory -ObjectsToProcess $ObjectsToProcess -Export $Export -RemoveLimitSID $RemoveLimitSID } $Export['TotalObjectsFound'] = $ObjectsToProcess.Count $Export['TotalSIDsFound'] = ($ObjectsToProcess | ForEach-Object { $_.Object.SIDHistory.Count } | Measure-Object -Sum).Sum if (-not $ReportOnly) { try { $Export | Export-Clixml -LiteralPath $DataStorePath -Encoding Unicode -WhatIf:$false -ErrorAction Stop } catch { Write-Color -Text "[-] Exporting Processed List failed. Error: $($_.Exception.Message)" -Color Yellow, Red } } $Export['ObjectsToProcess'] = $ObjectsToProcess New-HTMLProcessedSIDHistory -Export $Export -FilePath $ReportPath -Output $Output -ForestInformation $ForestInformation -Online:$Online.IsPresent -HideHTML:(-not $ShowHTML.IsPresent) -LogPath $LogPath -Configuration $Configuration # Return summary information if (-not $Suppress) { $Export.EmailBody = New-EmailBodySIDHistory -Export $Export $Export } } # Export functions and aliases as required Export-ModuleMember -Function @('Invoke-ADComputersCleanup', 'Invoke-ADSIDHistoryCleanup') -Alias @() # SIG # Begin signature block # MIItqwYJKoZIhvcNAQcCoIItnDCCLZgCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC9LL777InjY/DR # fjgItLi0yHouN09CbSaxsJ2L3yxO5qCCJq4wggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG # SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD # VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq # hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4 # XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ # aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg # X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk # apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL # FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy # 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u # KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54 # zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8 # 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8 # aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w # ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT # MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1 # c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG # SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS # g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9 # /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn # HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0 # VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f # sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj # gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0 # QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv # mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T # /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk # 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r # mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E # FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n # P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG # CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu # Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln # aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV # HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB # AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp # wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl # zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ # cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe # Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j # Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh # IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6 # OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw # N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR # 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2 # VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ # CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV # MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t # MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw # MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF # AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k # jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9 # NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9 # URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY # E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS # 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa # wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w # c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR # Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2 # 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK # ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC # AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2 # O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P # MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB # AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr # BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH # BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6 # mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/ # SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY # gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9 # kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ # 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew # Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm # Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA # SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr # y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR # ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu # v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGvDCCBKSgAwIBAgIQC65mvFq6f5WHxvnp # BOMzBDANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5 # NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTI0MDkyNjAwMDAwMFoXDTM1MTEy # NTIzNTk1OVowQjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERpZ2lDZXJ0MSAwHgYD # VQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQAD # ggIPADCCAgoCggIBAL5qc5/2lSGrljC6W23mWaO16P2RHxjEiDtqmeOlwf0KMCBD # Er4IxHRGd7+L660x5XltSVhhK64zi9CeC9B6lUdXM0s71EOcRe8+CEJp+3R2O8oo # 76EO7o5tLuslxdr9Qq82aKcpA9O//X6QE+AcaU/byaCagLD/GLoUb35SfWHh43rO # H3bpLEx7pZ7avVnpUVmPvkxT8c2a2yC0WMp8hMu60tZR0ChaV76Nhnj37DEYTX9R # eNZ8hIOYe4jl7/r419CvEYVIrH6sN00yx49boUuumF9i2T8UuKGn9966fR5X6kgX # j3o5WHhHVO+NBikDO0mlUh902wS/Eeh8F/UFaRp1z5SnROHwSJ+QQRZ1fisD8UTV # DSupWJNstVkiqLq+ISTdEjJKGjVfIcsgA4l9cbk8Smlzddh4EfvFrpVNnes4c16J # idj5XiPVdsn5n10jxmGpxoMc6iPkoaDhi6JjHd5ibfdp5uzIXp4P0wXkgNs+CO/C # acBqU0R4k+8h6gYldp4FCMgrXdKWfM4N0u25OEAuEa3JyidxW48jwBqIJqImd93N # Rxvd1aepSeNeREXAu2xUDEW8aqzFQDYmr9ZONuc2MhTMizchNULpUEoA6Vva7b1X # CB+1rxvbKmLqfY/M/SdV6mwWTyeVy5Z/JkvMFpnQy5wR14GJcv6dQ4aEKOX5AgMB # AAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUB # Af8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFJ9X # LAN3DigVkGalY17uT5IfdqBbMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwz # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1l # U3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhho # dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNl # cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZU # aW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAD2tHh92mVvjOIQS # R9lDkfYR25tOCB3RKE/P09x7gUsmXqt40ouRl3lj+8QioVYq3igpwrPvBmZdrlWB # b0HvqT00nFSXgmUrDKNSQqGTdpjHsPy+LaalTW0qVjvUBhcHzBMutB6HzeledbDC # zFzUy34VarPnvIWrqVogK0qM8gJhh/+qDEAIdO/KkYesLyTVOoJ4eTq7gj9UFAL1 # UruJKlTnCVaM2UeUUW/8z3fvjxhN6hdT98Vr2FYlCS7Mbb4Hv5swO+aAXxWUm3Wp # ByXtgVQxiBlTVYzqfLDbe9PpBKDBfk+rabTFDZXoUke7zPgtd7/fvWTlCs30VAGE # sshJmLbJ6ZbQ/xll/HjO9JbNVekBv2Tgem+mLptR7yIrpaidRJXrI+UzB6vAlk/8 # a1u7cIqV0yef4uaZFORNekUgQHTqddmsPCEIYQP7xGxZBIhdmm4bhYsVA6G2WgNF # YagLDBzpmk9104WQzYuVNsxyoVLObhx3RugaEGru+SojW4dHPoWrUhftNpFC5H7Q # EY7MhKRyrBe7ucykW7eaCuWBsBb4HOKRFVDcrZgdwaSIqMDiCLg4D+TPVgKx2EgE # deoHNHT9l3ZDBD+XgbF+23/zBjeCtxz+dL/9NWR6P2eZRi7zcEO1xwcdcqJsyz/J # ceENc2Sg8h3KeFUCS7tpFk7CrDqkMIIHXzCCBUegAwIBAgIQB8JSdCgUotar/iTq # F+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln # aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT # aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAwMDAwMFoX # DTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1pa2/FgsOz # dzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYDVQQDDBhQ # cnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw # ggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmVOrRBVRQA # 8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVEh0C/Daeh # vxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNdGVXRYOLn # 47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0235CN4Rr # W+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuAo3+jVB8w # iUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw8/FNzGNP # lAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP0ib98XLf # QpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxiW4oHYO28 # eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFKRqwvSSr4 # fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKABGoIqSW0 # 5nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQIDAQABo4IC # AzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYDVR0OBBYE # FHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK # BggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz # ODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5j # cmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0cDovL3d3 # dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmlu # Z1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqGSIb3DQEB # CwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50ZHzoWs6E # BlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa1W47YSrc # 5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2CbE3JroJ # f2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0djvQSx51 # 0MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N9E8hUVev # xALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgizpwBasrx # h6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38wwtaJ3KY # D0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Yn8kQMB6/ # Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Zn3exUAKq # G+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe6nB6bSYH # v8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGCBlMwggZP # AgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEw # PwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2 # IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgBZQMEAgEF # AKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgor # BgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3 # DQEJBDEiBCB0blI2wKHpF/2c1eIuDOoNjNq/+OHFmW/c9kQoDNF9JzANBgkqhkiG # 9w0BAQEFAASCAgBNGrJEbOFaYTP2uoHOpJ/YnsDcU9VdrREHhwJcKaFVPX5OERoK # m6NrB7r26voUHflioIo8BJ/QnbFIq5MQsZC3rHA7tQPDJLcFnPlMVRASadhEbbII # pg731JeboeNb6jJSeCa9lmkiNej5rbfIMKabsGCVKVpVYdmQzZLyDOxHnZ69H1F5 # rgGRA0v2AioribKKhNBoXk0ax13smyKzah2bM9Hl1bALSWPUN9MhIWaRMMHkENU1 # mux+nQ5YXQ7ARZ9knBWslmE7pI4A2cslwAnyBWCns3gEKhJYNVPWeUCfAkKHST8F # AzwAm9y7mDRRzDLU7Z714ouC2tQRgchO4l9kYY72+R6+Pq4iZJg9wSS/gSqkvDoi # ZubMZZQkghQx83RDoTxbXDXdhHRNG0H0X4hFmqBPeYmCtAB+c1PJxeC0SfMm+k// # ZvIYf877KF9r1NgHU2DWrXfDS+WZWuKRHyZQj8bGU/EKZNP6itnghO4cZS52jPpC # /xtqm4wp6t6fEzgaoq5zRtZiXqBmtzrAFCOTxDpHdCDjGvdMgx9GkEUJEqhD3mk9 # qgpd8Z57ZbM/zKIy2BR1SUAoBeBWF3vQ6Imf0cIVjMC1gzPwo4eR4e0ANh+mPYN8 # e5hWgEMSvPMF0iDyRv55EE3hkc6T+Kv4mF/Ccnye+oktlYf1bKoafLA3bKGCAyAw # ggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVTMRcwFQYD # VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH # NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAuuZrxaun+Vh8b56QTj # MwQwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNTA0MDExODI4MTBaMC8GCSqGSIb3DQEJBDEiBCDe2y6h # iRnBwC2BnmZYb+Bw+7AKdNfe5kYmc61UvgwZ/jANBgkqhkiG9w0BAQEFAASCAgCI # ckCL/F+lssL2r17NXOQAm6PLEoqvjQZ5MMGvjMd12ECZA2jkZLQHoo8AmYPP7E8r # SSOT9yPf8SdKKT+d0ON3ibzfIThOArjuhOVFHodXrXw+FZTdcjhUwIdEfBkSN1Ji # 9Z47nyJxiMEopvB7InHwGksfDLPzMr3A6MjpqzaUBZTIahpiM6qoc7j4nLhNwDoG # +pu7GmwI1UPoqpSjXmIABEOH5NLINTwBYpBQIHzyc1vvz9dEQa5BMo6dbsrNPg08 # 4VpNGUXAmWfmSmRXeUEHMIlw7gjCcoZahPdTxQETKiGy15uennytm4XeexyxckFf # Cl+AVzyBhDsol5ahNVxIhumJt8zktopI0uBOCjxQB9tgOgSrGJ3cKYzgj+vSm4c+ # Rnio9RPEAmF8UqGv2TWbn8AnVujZqqqjHh5DB9JGrpANacDcqV6hzq/wAmFoWwRd # Y3dx1Spnxr9ZCms7tQikVuUf17Vb+WEAMjvVwI3KL1b4CkxuX8KGWqlLNmCpeSfJ # KyzJDeLFZtWCDkGj+J/1A3aYmQ1YlfQU50WqFf8IgLjxC4omARYlAbJyIl/ocn99 # BCs86nR0Q+2pZhgbsW5EVPOvB6IqecuDfYkNEZqLNWT+TLYe4YCoQUMTt3LzsxAV # SH8G+hbUF17ngg1CRtjZbqX9CPxQeE+3HFhiFiH1zQ== # SIG # End signature block |