Testimo.psm1
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) } } $ComputersUnsupported = @{ Name = 'DomainComputersUnsupported' Enable = $true Scope = 'Domain' Source = @{ Name = "Computers Unsupported" Data = { $Computers = Get-ADComputer -Filter { ( operatingsystem -like "*xp*") -or (operatingsystem -like "*vista*") -or ( operatingsystem -like "*Windows NT*") -or ( operatingsystem -like "*2000*") -or ( operatingsystem -like "*2003*") } -Property Name, OperatingSystem, OperatingSystemServicePack, lastlogontimestamp -Server $Domain $Computers | Select-Object Name, OperatingSystem, OperatingSystemServicePack, @{name = "lastlogontimestamp"; expression = { [datetime]::fromfiletime($_.lastlogontimestamp) } } } Details = [ordered] @{ Area = 'Objects' Category = 'Cleanup' Importance = 3 Description = 'Computers running an unsupported operating system.' Resolution = 'Upgrade or remove computers from Domain.' Resources = @() } ExpectedOutput = $false } } $ComputersUnsupportedMainstream = @{ Name = 'DomainComputersUnsupportedMainstream' Enable = $true Scope = 'Domain' Source = @{ Name = "Computers Unsupported Mainstream Only" Data = { $Computers = Get-ADComputer -Filter { ( operatingsystem -like "*2008*") } -Property Name, OperatingSystem, OperatingSystemServicePack, lastlogontimestamp -Server $Domain $Computers | Select-Object Name, OperatingSystem, OperatingSystemServicePack, @{name = "lastlogontimestamp"; expression = { [datetime]::fromfiletime($_.lastlogontimestamp) } } } Details = [ordered] @{ Area = 'Objects' Category = 'Cleanup' Importance = 3 Description = 'Computers running an unsupported operating system, but with possibly Microsoft support.' Resolution = 'Consider upgrading computers running Windows Server 2008 or Windows Server 2008 R2 to a version that still offers mainstream support from Microsoft.' Resources = @() } ExpectedOutput = $false } } $DHCPAuthorized = @{ Name = 'DomainDHCPAuthorized' Enable = $false Scope = 'Domain' Source = @{ Name = "DHCP authorized in domain" Data = { #$DomainInformation = Get-ADDomain -Identity 'ad.evotec.pl' $SearchBase = 'cn=configuration,{0}' -f $DomainInformation.DistinguishedName Get-ADObject -SearchBase $searchBase -Filter "objectclass -eq 'dhcpclass' -AND Name -ne 'dhcproot'" #| select name } Requirements = @{ IsDomainRoot = $true } Details = [ordered] @{ Area = 'DHCP' Category = 'Configuration' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ DHCPAuthorized = @{ Enable = $true Name = 'At least 1 DHCP Server Authorized' Parameters = @{ ExpectedCount = '1' OperationType = 'ge' } } } } $DNSForwaders = @{ Name = 'DomainDNSForwaders' Enable = $true Scope = 'Domain' Source = @{ Name = "DNS Forwarders" Data = { [Array] $Forwarders = Get-WinADDnsServerForwarder -Forest $ForestName -Domain $Domain -WarningAction SilentlyContinue if ($Forwarders.Count -gt 1) { $Comparision = Compare-MultipleObjects -Objects $Forwarders -FormatOutput -CompareSorted:$true -ExcludeProperty GatheredFrom -SkipProperties -Property 'IpAddress' -WarningAction SilentlyContinue [PSCustomObject] @{ Source = $Comparision.Source -join ', ' Status = $Comparision.Status } } elseif ($Forwarders.Count -eq 0) { [PSCustomObject] @{ # This code takes care of no forwarders Source = 'No forwarders set' Status = $false } } else { # This code takes care of only 1 server within a domain. If there is 1 server available (as others may be dead/unavailable at the time it assumes Pass) [PSCustomObject] @{ Source = $Forwarders[0].IPAddress -join ', ' Status = $true } } } Details = [ordered] @{ Area = 'DNS' Category = 'Configuration' Importance = 3 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ SameForwarders = @{ Enable = $true Name = 'Same DNS Forwarders' Parameters = @{ Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Source' } Description = 'DNS forwarders within one domain should have identical setup' } } } $DNSScavengingForPrimaryDNSServer = @{ Name = 'DomainDNSScavengingForPrimaryDNSServer' Enable = $true Scope = 'Domain' Source = @{ Name = "DNS Scavenging - Primary DNS Server" Data = { Get-WinADDnsServerScavenging -Forest $ForestName -IncludeDomains $Domain } Details = [ordered] @{ Area = 'DNS' Category = 'Configuration' Importance = 3 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ ScavengingCount = @{ Enable = $true Name = 'Scavenging DNS Servers Count' Parameters = @{ WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } ExpectedCount = 1 OperationType = 'eq' } Description = 'Scavenging Count should be 1. There should be 1 DNS server per domain responsible for scavenging. If this returns false, every other test fails.' } ScavengingInterval = @{ Enable = $true Name = 'Scavenging Interval' Parameters = @{ WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } Property = 'ScavengingInterval', 'Days' ExpectedValue = 7 OperationType = 'le' } } 'Scavenging State' = @{ Enable = $true Name = 'Scavenging State' Parameters = @{ WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } Property = 'ScavengingState' ExpectedValue = $true OperationType = 'eq' } Description = 'Scavenging State is responsible for enablement of scavenging for all new zones created.' RecommendedValue = $true DescriptionRecommended = 'It should be enabled so all new zones are subject to scavanging.' DefaultValue = $false } 'Last Scavenge Time' = @{ Enable = $true Name = 'Last Scavenge Time' Parameters = @{ WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } # this date should be the same as in Scavending Interval Property = 'LastScavengeTime' # we need to use string which will be converted to ScriptBlock later on due to configuration export to JSON ExpectedValue = '(Get-Date).AddDays(-7)' OperationType = 'gt' } } } } $DnsZonesAging = @{ Name = 'DomainDnsZonesAging' Enable = $true Scope = 'Domain' Source = @{ Name = "Aging primary DNS Zone" Data = { Get-WinDnsServerZones -Forest $ForestName -ZoneName $Domain -IncludeDomains $Domain } Details = [ordered] @{ Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ EnabledAgingEnabledAndIdentical = @{ Enable = $true Name = 'Zone DNS aging should be identical on all DCs' Parameters = @{ WhereObject = { $_.AgingEnabled -eq $false } ExpectedCount = 0 } Description = 'Primary DNS zone should have aging enabled, on all DNS servers.' } } } $DNSZonesDomain0ADEL = @{ Name = 'DomainDNSZonesDomain0ADEL' Enable = $true Scope = 'Domain' Source = @{ Name = "DomainDNSZones should have proper FSMO Owner (0ADEL)" Data = { #$DomainController = 'ad.evotec.pl' #$DomainInformation = Get-ADDomain -Server $DomainController $IdentityDomain = "CN=Infrastructure,DC=DomainDnsZones,$(($DomainInformation).DistinguishedName)" $FSMORoleOwner = (Get-ADObject -Identity $IdentityDomain -Properties fSMORoleOwner -Server $Domain) $FSMORoleOwner } Details = [ordered] @{ Area = 'DNS' Category = 'Configuration' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @( 'https://blogs.technet.microsoft.com/the_9z_by_chris_davis/2011/12/20/forestdnszones-or-domaindnszones-fsmo-says-the-role-owner-attribute-could-not-be-read/' 'https://support.microsoft.com/en-us/help/949257/error-message-when-you-run-the-adprep-rodcprep-command-in-windows-serv' 'https://social.technet.microsoft.com/Forums/en-US/8b4a7794-13b2-4ef0-90c8-16799e9fd529/orphaned-fsmoroleowner-entry-for-domaindnszones?forum=winserverDS' ) } ExpectedOutput = $true } Tests = [ordered] @{ DNSZonesDomain0ADEL = @{ Enable = $true Name = 'DomainDNSZones should have proper FSMO Owner (0ADEL)' Parameters = @{ ExpectedValue = '0ADEL:' Property = 'fSMORoleOwner' OperationType = 'notmatch' } } } } $DNSZonesForest0ADEL = @{ Name = 'DomainDNSZonesForest0ADEL' Enable = $true Scope = 'Domain' Source = @{ Name = "ForestDNSZones should have proper FSMO Owner (0ADEL)" Data = { #$DomainController = 'ad.evotec.xyz' #$DomainInformation = Get-ADDomain -Server $DomainController $IdentityForest = "CN=Infrastructure,DC=ForestDnsZones,$(($DomainInformation).DistinguishedName)" $FSMORoleOwner = (Get-ADObject -Identity $IdentityForest -Properties fSMORoleOwner -Server $Domain) $FSMORoleOwner } Requirements = @{ IsDomainRoot = $true } Details = [ordered] @{ Area = 'DNS' Category = 'Configuration' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @( 'https://blogs.technet.microsoft.com/the_9z_by_chris_davis/2011/12/20/forestdnszones-or-domaindnszones-fsmo-says-the-role-owner-attribute-could-not-be-read/' 'https://support.microsoft.com/en-us/help/949257/error-message-when-you-run-the-adprep-rodcprep-command-in-windows-serv' 'https://social.technet.microsoft.com/Forums/en-US/8b4a7794-13b2-4ef0-90c8-16799e9fd529/orphaned-fsmoroleowner-entry-for-domaindnszones?forum=winserverDS' ) } ExpectedOutput = $true } Tests = [ordered] @{ DNSZonesForest0ADEL = @{ Enable = $true Name = 'ForestDNSZones should have proper FSMO Owner (0ADEL)' Parameters = @{ ExpectedValue = '0ADEL:' Property = 'fSMORoleOwner' OperationType = 'notmatch' } } } } $DomainDomainControllers = @{ Name = 'DomainDomainControllers' Enable = $true Scope = 'Domain' Source = @{ Name = "Domain Controller Objects" Data = { Get-WinADForestControllerInformation -Forest $ForestName -Domain $Domain } Requirements = @{} Details = [ordered] @{ Category = 'Cleanup', 'Security' Importance = 0 ActionType = 0 Description = "Following test verifies Domain Controller status in Active Directory. It verifies critical aspects of Domain Controler such as Domain Controller Owner and Domain Controller Manager. It also checks if Domain Controller is enabled, ip address matches dns ip address, verifies whether LastLogonDate and LastPasswordDate are within thresholds. Those additional checks are there to find dead or offline DCs that could potentially impact Active Directory functionality. " Resources = @( '[Domain member: Maximum machine account password age](https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/domain-member-maximum-machine-account-password-age)' '[Machine Account Password Process](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/machine-account-password-process/ba-p/396026)' '[How to Configure DNS on a Domain Controller with Two IP Addresses](https://petri.com/configure-dns-on-domain-controller-two-ip-addresses)' '[USN rollback](https://docs.microsoft.com/en-us/troubleshoot/windows-server/identity/detect-and-recover-from-usn-rollback)' '[Active Directory Replication Overview & USN Rollback: What It Is & How It Happens](https://adsecurity.org/?p=515)' ) StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{ Enabled = @{ Enable = $true Name = 'DC object should be enabled' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.Enabled -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 0 StatusTrue = 1 StatusFalse = 3 } } OwnerType = @{ Enable = $true Name = 'DC OwnerType should be Administrative' Parameters = @{ #ExpectedValue = 'Administrative' #Property = 'OwnerType' #OperationType = 'eq' ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.OwnerType -ne 'Administrative' } } Details = [ordered] @{ Category = 'Security' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } } ManagedBy = @{ Enable = $true Name = 'DC field ManagedBy should be empty' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ManagerNotSet -ne $true } } Details = [ordered] @{ Category = 'Security' Importance = 3 ActionType = 2 StatusTrue = 1 StatusFalse = 2 } } DNSStatus = @{ Enable = $true Name = 'DNS should return IP Address for DC' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.DNSStatus -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 0 StatusTrue = 1 StatusFalse = 2 } } IPAddressStatusV4 = @{ Enable = $true Name = 'DNS returned IPAddressV4 should match AD' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.IPAddressStatusV4 -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 0 StatusTrue = 1 StatusFalse = 2 } } IPAddressStatusV6 = @{ Enable = $true Name = 'DNS returned IPAddressV6 should match AD' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.IPAddressStatusV6 -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 0 StatusTrue = 1 StatusFalse = 2 } } IPAddressSingleV4 = @{ Enable = $true Name = 'There should be single IPv4 address set' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.IPAddressHasOneIpV4 -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 1 StatusTrue = 1 StatusFalse = 2 } } IPAddressSingleV6 = @{ Enable = $true Name = 'There should be single IPv6 address set' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.IPAddressHasOneipV6 -ne $true } } Details = [ordered] @{ Category = 'Cleanup' Importance = 0 ActionType = 1 StatusTrue = 1 StatusFalse = 2 } } PasswordNotRequired = @{ Enable = $true Name = "PasswordNotRequired shouldn't be set" Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.PasswordNotRequired -ne $false } } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } PasswordNeverExpires = @{ Enable = $true Name = "PasswordNeverExpires shouldn't be set" Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.PasswordNeverExpires -ne $false } } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } PasswordLastChange = @{ Enable = $true Name = 'DC Password Change Less Than X days' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.PasswordLastChangedDays -ge 60 } } Details = [ordered] @{ Category = 'Cleanup' Importance = 1 ActionType = 1 StatusTrue = 1 StatusFalse = 4 } } LastLogonDays = @{ Enable = $true Name = 'DC Last Logon Less Than X days' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.LastLogonDays -ge 15 } } Details = [ordered] @{ Category = 'Cleanup' Importance = 1 ActionType = 1 StatusTrue = 1 StatusFalse = 4 } } } DataInformation = { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -FontWeight bold, normal -Text "Enabled", " - means Domain Controller is enabled. If it's disabled it should be removed using proper cleanup method and according to company operation procedures. " New-HTMLListItem -FontWeight bold, normal -Text "DNSStatus", " - means Domain Controller IP address is available in DNS. If it's not registrered this means DC may not be functioning properly. " New-HTMLListItem -FontWeight bold, normal -Text "IPAddressStatusV4", " - means Domain Controller IP matches the one returned by DNS for IPV4. " New-HTMLListItem -FontWeight bold, normal -Text "IPAddressStatusV6", " - means Domain Controller IP matches the one returned by DNS for IPV6. " New-HTMLListItem -FontWeight bold, normal -Text "IPAddressHasOneIpV4", " - means Domain Controller has only one 1 IPV4 ipaddress (or not set at all). If it has more than 1 it's bad. " New-HTMLListItem -FontWeight bold, normal -Text "IPAddressHasOneipV6", " - means Domain Controller has only one 1 IPV6 ipaddress (or not set at all). If it has more than 1 it's bad. " New-HTMLListItem -FontWeight bold, normal -Text "ManagerNotSet", " - means ManagedBy property is not set (as required). If it's set it's bad. " New-HTMLListItem -FontWeight bold, normal -Text "OwnerType", " - means Domain Controller Owner is of certain type. Required type is Administrative. If it's different that means there's security risk involved. " New-HTMLListItem -FontWeight bold, normal -Text "PasswordNotRequired", " - should not be set. If it's set it can affect replication and security of Domain Controller. " New-HTMLListItem -FontWeight bold, normal -Text "PasswordLastChangedDays", " - displays last password change by Domain Controller. If it's more than 60 days it usually means DC is down or otherwise affected. " New-HTMLListItem -FontWeight bold, normal -Text "LastLogonDays", " - display last logon days of DC. If it's more than 15 days it usually means DC is down or otherwise affected. " } -FontSize 10pt } DataHighlights = { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'DNSStatus' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'DNSStatus' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'ManagerNotSet' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'ManagerNotSet' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'IPAddressStatusV4' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'IPAddressStatusV4' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'IPAddressStatusV6' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'IPAddressStatusV6' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'IPAddressHasOneIpV4' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'IPAddressHasOneIpV4' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'IPAddressHasOneipV6' -ComparisonType string -BackgroundColor Salmon -Value $false New-HTMLTableCondition -Name 'IPAddressHasOneipV6' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor Salmon -Value 'Administrative' -Operator ne New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor PaleGreen -Value 'Administrative' -Operator eq New-HTMLTableCondition -Name 'ManagedBy' -ComparisonType string -Color Salmon -Value '' -Operator ne New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -BackgroundColor PaleGreen -Value $false -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordNeverExpires' -ComparisonType string -BackgroundColor PaleGreen -Value $false -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -BackgroundColor PaleGreen -Value 40 -Operator le New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -BackgroundColor OrangePeel -Value 41 -Operator ge New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -BackgroundColor Crimson -Value 60 -Operator ge New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -BackgroundColor PaleGreen -Value 15 -Operator lt New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -BackgroundColor OrangePeel -Value 15 -Operator ge New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -BackgroundColor Crimson -Value 30 -Operator ge } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { #New-HTMLText -Text 'Following steps will guide you how to fix permissions consistency' New-HTMLWizard { New-HTMLWizardStep -Name 'Prepare environment' { New-HTMLText -Text "To be able to execute actions in automated way please install required modules. Those modules will be installed straight from Microsoft PowerShell Gallery." New-HTMLCodeBlock -Code { Install-Module ADEssentials -Force Import-Module ADEssentials -Force } -Style powershell New-HTMLText -Text "Using force makes sure newest version is downloaded from PowerShellGallery regardless of what is currently installed. Once installed you're ready for next step." } New-HTMLWizardStep -Name 'Prepare report' { New-HTMLText -Text "Depending when this report was run you may want to prepare new report before proceeding fixing permissions inconsistencies. To generate new report please use:" New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoBefore.DomainDomainControllers.html -Type DomainDomainControllers } New-HTMLText -Text @( "When executed it will take a while to generate all data and provide you with new report depending on size of environment." "Once confirmed that data is still showing issues and requires fixing please proceed with next step." ) New-HTMLText -Text "Alternatively if you prefer working with console you can run: " New-HTMLCodeBlock -Code { $Output = Get-WinADForestControllerInformation -IncludeDomains 'TargetDomain' $Output | Format-Table # do your actions as desired } New-HTMLText -Text "It provides same data as you see in table above just doesn't prettify it for you." } New-HTMLWizardStep -Name 'Fix Domain Controller Owner' { New-HTMLText -Text @( "Domain Controller Owner should always be set to Domain Admins. " "When non Domain Admin adds computer to domain that later on gets promoted to Domain Controller that person becomes the owner of the AD object. " "This is very dangerous and requires fixing. " "Following command when executed fixes domain controller owner. " "It makes sure each DC is owned by Domain Admins. " "If it's owned by Domain Admins already it will be skipped. " "Make sure when running it for the first time to run it with ", "WhatIf", " parameter as shown below to prevent accidental overwrite." ) -FontWeight normal, normal, normal, normal, normal, normal, normal, bold, normal -Color Black, Black, Black, Black, Black, Black, Black, Red, Black New-HTMLText -Text "Make sure to fill in TargetDomain to match your Domain Admin permission account" New-HTMLCodeBlock -Code { Repair-WinADForestControllerInformation -Verbose -LimitProcessing 3 -Type Owner -IncludeDomains "TargetDomain" -WhatIf } New-HTMLText -TextBlock { "After execution please make sure there are no errors, make sure to review provided output, and confirm that what is about to be fixed matches expected data. Once happy with results please follow with command: " } New-HTMLCodeBlock -Code { Repair-WinADForestControllerInformation -Verbose -LimitProcessing 3 -Type Owner -IncludeDomains "TargetDomain" } New-HTMLText -TextBlock { "This command when executed repairs only first X domain controller owners. Use LimitProcessing parameter to prevent mass fixing and increase the counter when no errors occur. " "Repeat step above as much as needed increasing LimitProcessing count till there's nothing left. In case of any issues please review and action accordingly. " } } New-HTMLWizardStep -Name 'Fix Domain Controller Manager' { New-HTMLText -Text @( "Domain Controller Manager should not be set. " "There's no reason for anyone outside of Domain Admins group to be manager of Domain Controller object. " "Since Domain Admins are by design Owners of Domain Controller object ManagedBy field should not be set. " "Following command fixes this by clearing ManagedBy field. " ) New-HTMLCodeBlock -Code { Repair-WinADForestControllerInformation -Verbose -LimitProcessing 3 -Type Manager -IncludeDomains "TargetDomain" -WhatIf } New-HTMLText -TextBlock { "After execution please make sure there are no errors, make sure to review provided output, and confirm that what is about to be fixed matches expected data. Once happy with results please follow with command: " } New-HTMLCodeBlock -Code { Repair-WinADForestControllerInformation -Verbose -LimitProcessing 3 -Type Manager -IncludeDomains "TargetDomain" } New-HTMLText -TextBlock { "This command when executed repairs only first X domain controller managers. Use LimitProcessing parameter to prevent mass fixing and increase the counter when no errors occur. " "Repeat step above as much as needed increasing LimitProcessing count till there's nothing left. In case of any issues please review and action accordingly. " } } New-HTMLWizardStep -Name 'Remaining Problems' { New-HTMLText -Text @( "If there are any Domain Controllers that are disabled, or last logon date or last password set are above thresholds those should be investigated if those are still up and running. " "In Active Directory based domains, each device has an account and password. " "By default, the domain members submit a password change every 30 days. " "If last password change is above threshold that means DC may already be offline. " "If last logon date is above threshold that also means DC may already be offline. " "Bringing back DC to life after longer downtime period can cause serious issues when done improperly. " "Please investigate and decide with other Domain Admins how to deal with dead/offline DC. " ) New-HTMLText -LineBreak New-HTMLText -Text @( "Additionally DNS should return IP Address of DC when asked, and it should be the same IP Address as the one stored in Active Directory. " "If those do not match or IP Address is not set/returned it needs investigation why is it so. " "It's possible the DC is down/dead and should be safely removed from Active Directory to prevent potential issues. " "Alternatively it's possible there are some network issues with it. " ) } New-HTMLWizardStep -Name 'Verification report' { New-HTMLText -TextBlock { "Once cleanup task was executed properly, we need to verify that report now shows no problems." } New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoAfter.DomainDomainControllers.html -Type DomainDomainControllers } New-HTMLText -Text "If everything is healthy in the report you're done! Enjoy rest of the day!" -Color BlueDiamond } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $DomainFSMORoles = @{ Name = 'DomainRoles' Enable = $true Scope = 'Domain' Source = @{ Name = 'Domain Roles Availability' Data = { Test-ADRolesAvailability -Domain $Domain } Details = [ordered] @{ Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ PDCEmulator = @{ Enable = $true Name = 'PDC Emulator Availability' Parameters = @{ ExpectedValue = $true Property = 'PDCEmulatorAvailability' OperationType = 'eq' PropertyExtendedValue = 'PDCEmulator' } } RIDMaster = @{ Enable = $true Name = 'RID Master Availability' Parameters = @{ ExpectedValue = $true Property = 'RIDMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'RIDMaster' } } InfrastructureMaster = @{ Enable = $true Name = 'Infrastructure Master Availability' Parameters = @{ ExpectedValue = $true Property = 'InfrastructureMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'InfrastructureMaster' } } } } $DomainLDAP = @{ Name = 'DomainLDAP' Enable = $true Scope = 'Domain' Source = @{ Name = 'LDAP Connectivity' Data = { Test-LDAP -Forest $ForestName -IncludeDomains $Domain -SkipRODC:$SkipRODC -WarningAction SilentlyContinue -VerifyCertificate } Details = [ordered] @{ Category = 'Health' Description = 'Domain Controllers require certain ports to be open, and serving proper certificate for SSL connectivity. ' Importance = 0 ActionType = 0 Resources = @( "[Testing LDAP and LDAPS connectivity with PowerShell](https://evotec.xyz/testing-ldap-and-ldaps-connectivity-with-powershell/)" "[2020 LDAP channel binding and LDAP signing requirements for Windows](https://support.microsoft.com/en-us/topic/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows-ef185fb8-00f7-167d-744c-f299a66fc00a)" ) StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{ PortLDAP = @{ Enable = $true Name = 'LDAP Port is Available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.LDAP -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 3 } } PortLDAPS = @{ Enable = $true Name = 'LDAP SSL Port is Available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.LDAPS -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } PortLDAP_GC = @{ Enable = $true Name = 'LDAP GC Port is Available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.GlobalCatalogLDAP -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } PortLDAPS_GC = @{ Enable = $true Name = 'LDAP SSL GC Port is Available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.GlobalCatalogLDAPS -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } BindLDAPS = @{ Enable = $true Name = 'LDAP SSL Bind available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.LDAPSBind -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } BindLDAPS_GC = @{ Enable = $true Name = 'LDAP SSL GC Bind is Available' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.GlobalCatalogLDAPSBind -eq $false } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } X509NotBeforeDays = @{ Enable = $true Name = 'Not Before Days should be less/equal 0' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.X509NotBeforeDays -gt 0 } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } X509NotAfterDaysWarning = @{ Enable = $true Name = 'Not After Days should be more than 10 days' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.X509NotAfterDays -lt 10 } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 1 StatusTrue = 1 StatusFalse = 4 } } X509NotAfterDaysCritical = @{ Enable = $true Name = 'Not After Days should be more than 0 days' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.X509NotAfterDays -lt 0 } } Details = [ordered] @{ Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @( 'Domain Controllers require certain ports for LDAP connectivity to be open, and serving proper certificate for SSL connectivity. ' 'Following ports are required to be available: ' ) New-HTMLList { New-HTMLListItem -Text 'LDAP port 389' New-HTMLListItem -Text 'LDAP SSL port 636' New-HTMLListItem -Text 'LDAP Global Catalog port 3268' New-HTMLListItem -Text 'LDAP Global Catalog SLL port 3269' } New-HTMLText -Text @( "If any/all of those ports are unavailable for any of the Domain Controllers " "it means that either DC is not available from location it's getting tested from (" "$Env:COMPUTERNAME" ") or those ports are down, or DC doesn't have a proper certificate installed. " "Please make sure to verify Domain Controllers that are reporting errors and talk to network team if required to make sure " "proper ports are open thru firewall. " ) -Color None, None, BilobaFlower, None, None, None } } DataHighlights = { New-HTMLTableCondition -Name 'GlobalCatalogLDAP' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'GlobalCatalogLDAP' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'GlobalCatalogLDAPS' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'GlobalCatalogLDAPS' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'GlobalCatalogLDAPSBind' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'GlobalCatalogLDAPSBind' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'LDAP' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'LDAP' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'LDAPS' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'LDAPS' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'LDAPSBind' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'LDAPSBind' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'X509NotBeforeDays' -ComparisonType number -BackgroundColor PaleGreen -Value 0 -Operator le New-HTMLTableCondition -Name 'X509NotBeforeDays' -ComparisonType number -BackgroundColor Salmon -Value 0 -Operator gt New-HTMLTableCondition -Name 'X509NotAfterDays' -ComparisonType number -BackgroundColor PaleGreen -Value 0 -Operator gt New-HTMLTableCondition -Name 'X509NotAfterDays' -ComparisonType number -BackgroundColor Salmon -Value 0 -Operator lt } } $DuplicateObjects = @{ Name = 'DomainDuplicateObjects' Enable = $true Scope = 'Domain' Source = @{ Name = "Duplicate Objects: 0ACNF (Duplicate RDN)" <# Alternative: dsquery * forestroot -gc -attr distinguishedName -scope subtree -filter "(|(cn=*\0ACNF:*)(ou=*OACNF:*))" #> Data = { Get-WinADDuplicateObject -IncludeDomains $Domain } Details = [ordered] @{ Category = 'Cleanup' Description = "When two objects are created with the same Relative Distinguished Name (RDN) in the same parent Organizational Unit or container, the conflict is recognized by the system when one of the new objects replicates to another domain controller. When this happens, one of the objects is renamed. Some sources say the RDN is mangled to make it unique. The new RDN will be <Old RDN>\0ACNF:<objectGUID>" Importance = 5 ActionType = 2 Resources = @( 'https://social.technet.microsoft.com/wiki/contents/articles/15435.active-directory-duplicate-object-name-resolution.aspx' 'https://ourwinblog.blogspot.com/2011/05/resolving-computer-object-replication.html' 'https://kickthatcomputer.wordpress.com/2014/11/22/seek-and-destroy-duplicate-ad-objects-with-cnf-in-the-name/' 'https://jorgequestforknowledge.wordpress.com/2014/09/17/finding-conflicting-objects-in-your-ad/' 'https://social.technet.microsoft.com/Forums/en-US/e9327be6-922c-4b9f-8357-417c3ab6a1af/cnf-remove-from-ad?forum=winserverDS' 'https://kickthatcomputer.wordpress.com/2014/11/22/seek-and-destroy-duplicate-ad-objects-with-cnf-in-the-name/' 'https://community.spiceworks.com/topic/2113346-active-directory-replication-cnf-guid-entries' ) StatusTrue = 1 StatusFalse = 2 } ExpectedOutput = $false } DataHighlights = { } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { New-HTMLWizard { New-HTMLWizardStep -Name 'Prepare environment' { New-HTMLText -Text "To be able to execute actions in automated way please install required modules. Those modules will be installed straight from Microsoft PowerShell Gallery." New-HTMLCodeBlock -Code { Install-Module ADEssentials -Force Import-Module ADEssentials -Force } -Style powershell New-HTMLText -Text "Using force makes sure newest version is downloaded from PowerShellGallery regardless of what is currently installed. Once installed you're ready for next step." } New-HTMLWizardStep -Name 'Prepare report' { New-HTMLText -Text "Depending when this report was run you may want to prepare new report before proceeding fixing permissions inconsistencies. To generate new report please use:" New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoBefore.DomainDuplicateObjects.html -Type DomainDuplicateObjects } New-HTMLText -Text @( "When executed it will take a while to generate all data and provide you with new report depending on size of environment." "Once confirmed that data is still showing issues and requires fixing please proceed with next step." ) New-HTMLText -Text "Alternatively if you prefer working with console you can run: " New-HTMLCodeBlock -Code { $Output = Get-WinADDuplicateObject -IncludeDomains 'TargetDomain' $Output | Format-Table # do your actions as desired } New-HTMLText -Text "It provides same data as you see in table above just doesn't prettify it for you." } New-HTMLWizardStep -Name 'Remove Domain Duplicate Objects' { New-HTMLText -Text @( "CNF objects, Conflict objects or Duplicate Objects are created in Active Directory when there is simultaneous creation of an AD object under the same container " "on two separate Domain Controllers near about the same time or before the replication occurs. " "This results in a conflict and the same is exhibited by a CNF (Duplicate) object. " "While it doesn't nessecary has a huge impact on Active Directory it's important to keep Active Directory in proper, healthy state. " ) -FontWeight normal, normal, normal, normal, normal, normal, normal, bold, normal -Color Black, Black, Black, Black, Black, Black, Black, Red, Black New-HTMLText -Text "Make sure to fill in TargetDomain to match your Domain Admin permission account" New-HTMLCodeBlock -Code { Remove-WinADDuplicateObject -Verbose -LimitProcessing 1 -IncludeDomains "TargetDomain" -WhatIf } New-HTMLText -TextBlock { "After execution please make sure there are no errors, make sure to review provided output, and confirm that what is about to be fixed matches expected data. Once happy with results please follow with command: " } New-HTMLCodeBlock -Code { Remove-WinADDuplicateObject -Verbose -LimitProcessing 1 -IncludeDomains "TargetDomain" } New-HTMLText -TextBlock { "This command when executed removes only first X duplicate/CNF objects. Use LimitProcessing parameter to prevent mass remove and increase the counter when no errors occur. " "Repeat step above as much as needed increasing LimitProcessing count till there's nothing left. In case of any issues please review and action accordingly. " } } New-HTMLWizardStep -Name 'Verification report' { New-HTMLText -TextBlock { "Once cleanup task was executed properly, we need to verify that report now shows no problems." } New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoAfter.DomainDuplicateObjects.html -Type DomainDuplicateObjects } New-HTMLText -Text "If everything is healthy in the report you're done! Enjoy rest of the day!" -Color BlueDiamond } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $ExchangeUsers = @{ Name = 'DomainExchangeUsers' Enable = $false Scope = 'Domain' Source = @{ Name = "Exchange Users: Missing MailNickName" Data = { Get-ADUser -Filter { Mail -like '*' -and MailNickName -notlike '*' } -Properties mailNickName, mail -Server $Domain } Details = [ordered] @{ Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( 'https://evotec.xyz/office-365-msexchhidefromaddresslists-does-not-synchronize-with-office-365/' ) } ExpectedOutput = $false } } $GroupPolicyAssessment = @{ Name = 'DomainGroupPolicyAssessment' Enable = $true Scope = 'Domain' Source = @{ Name = "Group Policy Assessment" Data = { Get-GPOZaurr -Forest $ForestName -IncludeDomains $Domain } Implementation = { } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Cleanup' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ Empty = @{ Enable = $true Name = 'Group Policy Empty' Parameters = @{ #Bundle = $true WhereObject = { $_.Empty -eq $true } ExpectedCount = 0 } } Linked = @{ Enable = $true Name = 'Group Policy Unlinked' Parameters = @{ #Bundle = $true WhereObject = { $_.Linked -eq $false } ExpectedCount = 0 } } Enabled = @{ Enable = $true Name = 'Group Policy Disabled' Parameters = @{ #Bundle = $true WhereObject = { $_.Enabled -eq $false } ExpectedCount = 0 } } Problem = @{ Enable = $true Name = 'Group Policy with Problem' Parameters = @{ #Bundle = $true WhereObject = { $_.Problem -eq $true } ExpectedCount = 0 } } Optimized = @{ Enable = $true Name = 'Group Policy Not Optimized' Parameters = @{ #Bundle = $true WhereObject = { $_.Optimized -eq $false } ExpectedCount = 0 } } ApplyPermission = @{ Enable = $true Name = 'Group Policy No Apply Permission' Parameters = @{ # Bundle = $true WhereObject = { $_.ApplyPermissioon -eq $false } ExpectedCount = 0 } } } DataHighlights = { New-HTMLTableCondition -Name 'Empty' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'Linked' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'Optimized' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'Problem' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'ApplyPermission' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon } } $GroupPolicyADM = @{ Name = 'DomainGroupPolicyADM' Enable = $true Scope = 'Domain' Source = @{ Name = 'Group Policy Legacy ADM Files' Data = { Get-GPOZaurrLegacyFiles -Forest $ForestName -IncludeDomains $Domain } Implementation = { Remove-GPOZaurrLegacyFiles -Verbose -WhatIf } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Cleanup' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( 'https://support.microsoft.com/en-us/help/816662/recommendations-for-managing-group-policy-administrative-template-adm' 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-vista/cc709647(v=ws.10)?redirectedfrom=MSDN' 'https://sdmsoftware.com/group-policy-blog/tips-tricks/understanding-the-role-of-admx-and-adm-files-in-group-policy/' 'https://social.technet.microsoft.com/Forums/en-US/bbbe04f5-218b-4526-ae67-cf82a20d49fc/deleting-adm-templates?forum=winserverGP' 'https://gallery.technet.microsoft.com/scriptcenter/Removing-ADM-files-from-b532e3b6#content' ) } ExpectedOutput = $false } } $GroupPolicyOwner = @{ Name = 'DomainGroupPolicyOwner' Enable = $true Scope = 'Domain' Source = @{ Name = "GPO: Owner" Data = { Get-GPOZaurrOwner -Forest $ForestName -IncludeSysvol -IncludeDomains $Domain } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Security' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ OwnerConsistent = @{ Enable = $true Name = 'GPO: Owner Consistent' Parameters = @{ WhereObject = { $_.IsOwnerConsistent -ne $true } ExpectedResult = $false # this tests things in bundle rather then per object of array } } OwnerAdministrative = @{ Enable = $true Name = 'GPO: Owner Administrative' Parameters = @{ WhereObject = { $_.OwnerType -ne 'Administrative' -or $_.SysvolType -ne 'Administrative' } ExpectedResult = $false # this tests things in bundle rather then per object of array } } } } <# ExpectedCount = 0,1,2,3 and so on ExpectedValue = [object] ExpectedResult = $true # just checks if there is result or there is not #> $GroupPolicyPermissions = @{ Name = 'DomainGroupPolicyPermissions' Enable = $true Scope = 'Domain' Source = @{ Name = "Group Policy Required Permissions" Data = { Get-GPOZaurrPermissionAnalysis -Forest $ForestName -Domain $Domain } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Security' Severity = '' Importance = 0 Description = "Group Policy permissions should always have Authenticated Users and Domain Computers gropup" Resolution = 'Do not remove Authenticated Users, Domain Computers from Group Policies.' Resources = @( 'https://secureinfra.blog/2018/12/31/most-common-mistakes-in-active-directory-and-domain-services-part-1/' 'https://support.microsoft.com/en-us/help/3163622/ms16-072-security-update-for-group-policy-june-14-2016' ) } ExpectedOutput = $true } Tests = [ordered] @{ Administrative = @{ Enable = $true Name = 'GPO: Administrative Permissions' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.Administrative -eq $false } } } AuthenticatedUsers = @{ Enable = $true Name = 'GPO: Authenticated Permissions' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.AuthenticatedUsers -eq $false } } } System = @{ Enable = $true Name = 'GPO: System Permissions' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.System -eq $false } } } Unknown = @{ Enable = $true Name = 'GPO: Unknown Permissions' Parameters = @{ ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.Unknown -eq $true } } } } <# Another way to do the same thing as above Tests = [ordered] @{ Administrative = @{ Enable = $true Name = 'GPO: Administrative Permissions' Parameters = @{ Bundle = $true Property = 'Administrative' ExpectedValue = $false OperationType = 'notcontains' DisplayResult = $false } } AuthenticatedUsers = @{ Enable = $true Name = 'GPO: Authenticated Permissions' Parameters = @{ Bundle = $true Property = 'AuthenticatedUsers' ExpectedValue = $false OperationType = 'notcontains' DisplayResult = $false } } System = @{ Enable = $true Name = 'GPO: System Permissions' Parameters = @{ Bundle = $true Property = 'System' ExpectedValue = $false OperationType = 'notcontains' DisplayResult = $false } } Unknown = @{ Enable = $true Name = 'GPO: Unknown Permissions' Parameters = @{ Bundle = $true Property = 'Unknown' ExpectedValue = $false OperationType = 'notcontains' DisplayResult = $false } } } #> } $GroupPolicyPermissionConsistency = @{ Name = 'DomainGroupPolicyPermissionConsistency' Enable = $true Scope = 'Domain' Source = @{ Name = "GPO: Permission Consistency" Data = { Get-GPOZaurrPermissionConsistency -Forest $ForestName -VerifyInheritance -Type Inconsistent -IncludeDomains $Domain } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Security' Severity = '' Importance = 0 Description = "GPO Permissions are stored in Active Directory and SYSVOL at the same time. Setting up permissions for GPO should replicate itself to SYSVOL and those permissions should be consistent. However, sometimes this doesn't happen or is done on purpose." Resolution = '' Resources = @( ) } ExpectedOutput = $false } } $GroupPolicySysvol = @{ Name = 'DomainGroupPolicySysvol' Enable = $true Scope = 'Domain' Source = @{ Name = "GPO: Sysvol folder existance" Data = { Get-GPOZaurrSysvol -Forest $ForestName -IncludeDomains $Domain } Details = [ordered] @{ Area = 'GroupPolicy' Category = 'Security' Severity = '' Importance = 0 Description = "GPO Permissions are stored in Active Directory and SYSVOL at the same time. Sometimes when deleting GPO or due to replication issues GPO becomes orphaned (no SYSVOL files) or SYSVOL files exists but no GPO." Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ SysvolExists = @{ Enable = $true Name = 'GPO: Files on SYSVOL are not Orphaned' Parameters = @{ WhereObject = { $_.SysvolStatus -ne 'Exists' -or $_.Status -ne 'Exists' } ExpectedResult = $false # this tests things in bundle rather then per object of array } } } } $MachineQuota = @{ Name = 'DomainMachineQuota' Enable = $true Scope = 'Domain' Source = @{ Name = "Machine Quota: Gathering ms-DS-MachineAccountQuota" Data = { Get-ADObject -Identity ((Get-ADDomain -Identity $Domain).distinguishedname) -Properties 'ms-DS-MachineAccountQuota' -Server $Domain | Select-Object DistinguishedName, Name, ObjectClass, ObjectGUID, ms-DS-MachineAccountQuota } Details = [ordered] @{ Category = 'Security' Importance = 0 Description = "By default, In the Microsoft Active Directory, members of the authenticated user group can join up to 10 computer accounts in the domain. This value is defined in the attribute ms-DS-MachineAccountQuota on the domain-DNS object for a domain." Resources = @( '[ms-DS-MachineAccountQuota](https://docs.microsoft.com/en-us/windows/win32/adschema/a-ms-ds-machineaccountquota)' "[MachineAccountQuota is USEFUL Sometimes: Exploiting One of Active Directory's Oddest Settings](https://www.netspi.com/blog/technical/network-penetration-testing/machineaccountquota-is-useful-sometimes/)" "[How to change the attribute ms-DS-MachineAccountQuota](https://www.jorgebernhardt.com/how-to-change-attribute-ms-ds-machineaccountquota/)" "[Default limit to number of workstations a user can join to the domain](https://docs.microsoft.com/pl-PL/troubleshoot/windows-server/identity/default-workstation-numbers-join-domain)" ) Tags = 'Security', 'Configuration' StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{ MachineQuotaIsZero = @{ Enable = $true Name = 'Machine Quota: Should be set to 0' Parameters = @{ ExpectedValue = 0 Property = 'ms-DS-MachineAccountQuota' OperationType = 'eq' } Details = [ordered] @{ Category = 'Configuration' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @( "By default, In the Microsoft Active Directory, members of the authenticated user group can join up to 10 computer accounts in the domain. " "This value is defined in the attribute " "ms-DS-MachineAccountQuota" " on the domain-DNS object for a domain. " "This value should always be ", "0" " and permissions to add computers to domain should be managed on Active Directory Delegation level." ) -FontWeight normal, normal, bold, normal } } DataHighlights = { New-HTMLTableCondition -Name 'ms-DS-MachineAccountQuota' -ComparisonType number -BackgroundColor PaleGreen -Value 0 -Operator eq New-HTMLTableCondition -Name 'ms-DS-MachineAccountQuota' -ComparisonType number -BackgroundColor Salmon -Value 0 -Operator gt } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { New-HTMLWizard { New-HTMLWizard { New-HTMLWizardStep -Name 'Gather information about ms-DS-MachineAccountQuota' { New-HTMLText -Text @( "ms-DS-MachineAccountQuota " "should always be set to 0 to prevent any users adding computers to domain. This is security risk and should be fixed for all domains in a forest!" "To make sure you can easily revert this setting if something goes wrong you should first get this information before doing any changes." ) -FontWeight bold, normal New-HTMLCodeBlock { Get-ADObject -Identity ((Get-ADDomain -Identity $Domain).distinguishedname) -Properties 'ms-DS-MachineAccountQuota' -Server $Domain } } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors New-HTMLWizardStep -Name 'Fix ms-DS-MachineAccountQuota' { New-HTMLText -Text @( "ms-DS-MachineAccountQuota " "should always be set to 0 to prevent any users adding computers to domain. This is security risk and should be fixed for all domains in a forest!" "This can be done using following cmdlet. Please make sure to use WhatIf to verify what will change." ) -FontWeight bold, normal New-HTMLCodeBlock { Set-ADDomain -Identity $Domain -Replace @{"ms-DS-MachineAccountQuota" = "0" } -WhatIf } } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $NetLogonOwner = @{ Name = 'DomainNetLogonOwner' Enable = $true Scope = 'Domain' Source = @{ Name = "NetLogon Owner" Data = { Get-GPOZaurrNetLogon -Forest $ForestName -OwnerOnly -IncludeDomains $Domain } Implementation = { } Details = [ordered] @{ Area = 'FileSystem' Category = 'Cleanup' Severity = '' Importance = 6 Description = "" Resolution = '' Resources = @( ) Tags = 'netlogon', 'grouppolicy', 'gpo', 'sysvol' } ExpectedOutput = $null } Tests = [ordered] @{ Empty = @{ Enable = $true Name = 'Owner should be BUILTIN\Administrators' Parameters = @{ #Bundle = $true WhereObject = { $_.OwnerSid -ne 'S-1-5-32-544' } ExpectedCount = 0 ExpectedOutput = $true } } } } $OrganizationalUnitsEmpty = @{ Name = 'DomainOrganizationalUnitsEmpty' Enable = $true Scope = 'Domain' Source = @{ Name = "Organizational Units: Orphaned/Empty" Data = { <# We should replace it with better alternative ([adsisearcher]'(objectcategory=organizationalunit)').FindAll() | Where-Object { -not (-join $_.GetDirectoryEntry().psbase.children) } #> $OrganizationalUnits = Get-ADOrganizationalUnit -Filter * -Properties distinguishedname -Server $Domain | Select-Object -ExpandProperty distinguishedname $WellKnownContainers = Get-ADDomain | Select-Object *Container $AllUsedOU = Get-ADObject -Filter "ObjectClass -eq 'user' -or ObjectClass -eq 'computer' -or ObjectClass -eq 'group' -or ObjectClass -eq 'contact'" -Server $Domain | ` Where-Object { ($_.DistinguishedName -notlike '*LostAndFound*') -and ($_.DistinguishedName -match 'OU=(.*)') } | ` ForEach-Object { $matches[0] } | ` Select-Object -Unique $OrganizationalUnits | Where-Object { ($AllUsedOU -notcontains $_) -and -not (Get-ADOrganizationalUnit -Filter * -SearchBase $_ -SearchScope 1 -Server $Domain) -and (($_ -notlike $WellKnownContainers.UsersContainer) -or ($_ -notlike $WellKnownContainers.ComputersContainer)) } } Details = [ordered] @{ Category = 'Configuration' Importance = 3 ActionType = 1 Description = '' Resolution = '' Resources = @( "[Active Directory Friday: Find empty Organizational Unit](https://www.jaapbrasser.com/active-directory-friday-find-empty-organizational-unit/)" ) StatusTrue = 1 StatusFalse = 2 } ExpectedOutput = $false } } $OrganizationalUnitsProtected = @{ Name = 'DomainOrganizationalUnitsProtected' Enable = $true Scope = 'Domain' Source = @{ Name = "Organizational Units: Protected" Data = { $OUs = Get-ADOrganizationalUnit -Properties ProtectedFromAccidentalDeletion, CanonicalName -Filter * -Server $Domain $FilteredOus = foreach ($OU in $OUs) { if ($OU.ProtectedFromAccidentalDeletion -eq $false) { $OU } } $FilteredOus | Select-Object -Property Name, CanonicalName, DistinguishedName, ProtectedFromAccidentalDeletion } Details = [ordered] @{ Area = 'Objects' Category = 'Cleanup' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $false } } $OrphanedForeignSecurityPrincipals = @{ Name = 'DomainOrphanedForeignSecurityPrincipals' Enable = $true Scope = 'Domain' Source = @{ Name = "Orphaned Foreign Security Principals" Data = { $AllFSP = Get-WinADUsersForeignSecurityPrincipalList -Domain $Domain $OrphanedObjects = $AllFSP | Where-Object { $_.TranslatedName -eq $null } $OrphanedObjects } Details = [ordered] @{ Category = 'Cleanup' Area = 'Objects' Importance = 0 ActionType = 0 Description = 'An FSP is an Active Directory (AD) security principal that points to a security principal (a user, computer, or group) from a domain of another forest. AD automatically and transparently creates them in a domain the first time after adding a security principal from another forest to a group from that domain. AD creates FSPs in a domain the first time after adding a security principal of a domain from another forest to a group. And when someone removes the security principal the FSP is pointing to, the FSP becomes an orphan because it points to a non-existent security principal.' Resolution = '' Resources = @( '[Clean up orphaned Foreign Security Principals](https://4sysops.com/archives/clean-up-orphaned-foreign-security-principals/)' '[Foreign Security Principals and Well-Known SIDS, a.k.a. the curly red arrow problem](https://docs.microsoft.com/en-us/archive/blogs/389thoughts/foreign-security-principals-and-well-known-sids-a-k-a-the-curly-red-arrow-problem)' '[Active Directory: Foreign Security Principals and Special Identities](https://social.technet.microsoft.com/wiki/contents/articles/51367.active-directory-foreign-security-principals-and-special-identities.aspx)' '[Find orphaned foreign security principals and remove them from groups](https://serverfault.com/questions/320840/find-orphaned-foreign-security-principals-and-remove-them-from-groups)' ) StatusTrue = 1 StatusFalse = 3 } ExpectedOutput = $false } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { #New-HTMLText -Text 'Following steps will guide you how to fix permissions consistency' New-HTMLWizard { <# New-HTMLWizardStep -Name 'Prepare environment' { New-HTMLText -Text "To be able to execute actions in automated way please install required modules. Those modules will be installed straight from Microsoft PowerShell Gallery." New-HTMLCodeBlock -Code { Install-Module ADEssentials -Force Import-Module ADEssentials -Force } -Style powershell New-HTMLText -Text "Using force makes sure newest version is downloaded from PowerShellGallery regardless of what is currently installed. Once installed you're ready for next step." } #> New-HTMLWizardStep -Name 'Prepare report' { New-HTMLText -Text "Depending when this report was run you may want to prepare new report before proceeding fixing permissions inconsistencies. To generate new report please use:" New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoBefore.DomainOrphanedForeignSecurityPrincipals.html -Type DomainOrphanedForeignSecurityPrincipals } New-HTMLText -Text @( "When executed it will take a while to generate all data and provide you with new report depending on size of environment." "Once confirmed that data is still showing issues and requires fixing please proceed with next step." ) New-HTMLText -Text "Alternatively if you prefer working with console you can run: " New-HTMLCodeBlock -Code { $Output = Get-WinADUsersForeignSecurityPrincipalList -IncludeDomains 'TargetDomain' $Output | Where-Object { $_.TranslatedName -eq $null } | Format-Table } New-HTMLText -Text "It provides same data as you see in table above just doesn't prettify it for you." } New-HTMLWizardStep -Name 'Verify Trusts' { New-HTMLText -Text @( "It's important before deleting any FSP that all trusts are working correctly. " "If trusts are down, translation FSP objects doesn't happen and therefore it would look like that FSP or orphaned. " "Please run following command " ) New-HTMLCodeBlock -Code { Show-WinADTrust -Online -Recursive -Verbose } New-HTMLText -Text @( "Zero level trusts are required to be functional and responding. " "First level and above are optional, but should be verified if that's expected before removing FSP objects. " ) } New-HTMLWizardStep -Name 'Remove Orphaned FSP Objects (manual)' { New-HTMLText -Text @( "You can find all FSPs in the Active Directory Users and Computers (ADUC) console in a container named ForeignSecurityPrincipals. " "However, you must first enable Advanced Features in the console. Otherwise the container won't show anything." "You can recognize orphan FSPs by empty readable names in the ADUC console. " "" "However, there is a potential issue you need to be aware of. If, at the same time you are looking for orphaned FSPs, " "there is a network connectivity issue between domain controllers and domain controllers from other trusted forests, " "you won't be able to see the readable names. Thus the script and you will incorrectly deduce that they are orphans." "When cleaning up, please consult other Domain Admins and confirm the trusts with other domains are working as required before proceeding." ) -FontWeight normal, normal, normal, normal, normal, normal, normal, bold, normal -Color Black, Black, Black, Black, Black, Black, Black, Red, Black } New-HTMLWizardStep -Name 'Restore FSP object' { New-HTMLText -Text @( "If you've deleted FSP object by accident it's possible to restore such object from Active Directory Recycle Bin." ) } New-HTMLWizardStep -Name 'Verification report' { New-HTMLText -TextBlock { "Once cleanup task was executed properly, we need to verify that report now shows no problems." } New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoAfter.DomainOrphanedForeignSecurityPrincipals.html -Type DomainOrphanedForeignSecurityPrincipals } New-HTMLText -Text "If everything is healthy in the report you're done! Enjoy rest of the day!" -Color BlueDiamond } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $PasswordComplexity = @{ Name = 'DomainPasswordComplexity' Enable = $true Scope = 'Domain' Source = @{ Name = 'Password Complexity Requirements' Data = { Get-ADDefaultDomainPasswordPolicy -Server $Domain } Details = [ordered] @{ Area = 'Objects' Category = 'Security' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] @{ ComplexityEnabled = @{ Enable = $true Name = 'Complexity Enabled' Details = [ordered] @{ Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @( ) } Parameters = @{ Property = 'ComplexityEnabled' ExpectedValue = $true OperationType = 'eq' } } 'LockoutDuration' = @{ Enable = $true Name = 'Lockout Duration' Parameters = @{ Property = 'LockoutDuration' ExpectedValue = 30 OperationType = 'ge' } } 'LockoutObservationWindow' = @{ Enable = $true Name = 'Lockout Observation Window' Parameters = @{ #PropertyExtendedValue = 'LockoutObservationWindow' Property = 'LockoutObservationWindow', 'TotalMinutes' ExpectedValue = 30 OperationType = 'ge' } } 'LockoutThreshold' = @{ Enable = $true Name = 'Lockout Threshold' Parameters = @{ Property = 'LockoutThreshold' ExpectedValue = 4 OperationType = 'gt' } } 'MaxPasswordAge' = @{ Enable = $true Name = 'Maximum Password Age' Parameters = @{ Property = 'MaxPasswordAge', 'TotalDays' ExpectedValue = 60 OperationType = 'le' } } 'MinPasswordLength' = @{ Enable = $true Name = 'Minimum Password Length' Parameters = @{ Property = 'MinPasswordLength' ExpectedValue = 8 OperationType = 'gt' } } 'MinPasswordAge' = @{ Enable = $true Name = 'Minimum Password Age' Parameters = @{ #PropertyExtendedValue = 'MinPasswordAge', 'TotalDays' Property = 'MinPasswordAge', 'TotalDays' ExpectedValue = 1 OperationType = 'le' } } 'PasswordHistoryCount' = @{ Enable = $true Name = 'Password History Count' Parameters = @{ Property = 'PasswordHistoryCount' ExpectedValue = 10 OperationType = 'ge' } } 'ReversibleEncryptionEnabled' = @{ Enable = $true Name = 'Reversible Encryption Enabled' Parameters = @{ Property = 'ReversibleEncryptionEnabled' ExpectedValue = $false OperationType = 'eq' } } } } $DomainSecurityComputers = @{ Name = 'DomainSecurityComputers' Enable = $true Scope = 'Domain' Source = @{ Name = "Computers: Standard" Data = { $Properties = @( 'SamAccountName' 'UserPrincipalName' 'Enabled' 'PasswordNotRequired' 'AllowReversiblePasswordEncryption' 'UseDESKeyOnly' 'PasswordLastSet' 'LastLogonDate' 'PasswordNeverExpires' 'PrimaryGroup' 'PrimaryGroupID' 'DistinguishedName' 'Name' 'SID' ) Get-ADComputer -Filter { (PasswordNeverExpires -eq $true -or AllowReversiblePasswordEncryption -eq $true -or UseDESKeyOnly -eq $true -or (PrimaryGroupID -ne '515' -and PrimaryGroupID -ne '516' -and PrimaryGroupID -ne '521') -or PasswordNotRequired -eq $true) } -Properties $Properties -Server $Domain | Where-Object { $_.SamAccountName -ne 'AZUREADSSOACC$' } | Select-Object -Property $Properties } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 0 ActionType = 0 Description = 'Account by default have certain settings that make sure the account is fairly safe and can be used within Active Directory.' Resources = @( '[Understanding and Remediating "PASSWD_NOTREQD](https://docs.microsoft.com/en-us/archive/blogs/russellt/passwd_notreqd)' '[Miscellaneous facts about computer passwords in Active Directory](https://blog.joeware.net/2012/09/12/2590/)' '[Domain member: Maximum machine account password age](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/jj852252(v=ws.11)?redirectedfrom=MSDN)' '[Machine Account Password Process](https://techcommunity.microsoft.com/t5/ask-the-directory-services-team/machine-account-password-process/ba-p/396026)' ) StatusTrue = 1 StatusFalse = 0 } ExpectedOutput = $false } Tests = [ordered] @{ KeberosDES = @{ Enable = $true Name = 'Kerberos DES detection' Parameters = @{ WhereObject = { $_.UseDESKeyOnly -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "Computer accounts shouldn't use DES encryption. Having UseDESKeyOnly forces the Kerberos encryption to be DES instead of RC4 which is the Microsoft default. DES is 56 bit encryption and is regarded as weak these days so this setting is not recommended." } AllowReversiblePasswordEncryption = @{ Enable = $true Name = 'Reversible Password detection' Parameters = @{ WhereObject = { $_.AllowReversiblePasswordEncryption -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "Computer accounts shouldn't use Reversible Password Encryption. Having AllowReversiblePasswordEncryption allows for easy password decryption." } PasswordNeverExpires = @{ Enable = $true Name = 'PasswordNeverExpires detection' Parameters = @{ WhereObject = { $_.PasswordNeverExpires -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "Computer accounts shouldn't use PasswordNeverExpires. Having PasswordNeverExpires is dangerous and shoudn't be used." } PasswordNotRequired = @{ Enable = $true Name = 'PasswordNotRequired detection' Parameters = @{ WhereObject = { $_.PasswordNotRequired -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "Computer accounts shouldn't use PasswordNotRequired. Having PasswordNotRequired is dangerous and shoudn't be used." } PrimaryGroup = @{ Enable = $true Name = "Domain Computers or Domain Controllers or Read-Only Domain Controllers." Parameters = @{ #WhereObject = { $_.PrimaryGroupID -ne 513 -and $_.SID -ne "$((Get-ADDomain).DomainSID.Value)-501" } WhereObject = { $_.PrimaryGroupID -notin 515, 516, 521 } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } Description = "Computer accounts shouldn't have different group then Domain Computers or Domain Controllers or Read-Only Domain Controllers as their primary group." } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @( "Account by default have certain settings that make sure the account is fairly safe and can be used within Active Directory. " "Those settings are: " ) New-HTMLList { New-HTMLListItem -Text "Password is always required" New-HTMLListItem -Text "Password is expiring" New-HTMLListItem -Text "Password is not reverisble" New-HTMLListItem -Text "Keberos Encryption is set to RC4" New-HTMLListItem -Text "Primary Group is always Domain Computers/Domain Cotrollers or Domain Read-Only Controllers" } New-HTMLText -Text @( "It's important that all those settings are set as expected." ) } } DataHighlights = { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordNeverExpires' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'AllowReversiblePasswordEncryption' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'UseDESKeyOnly' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PrimaryGroupID' -ComparisonType number -BackgroundColor PaleGreen -Value 515, 516, 521 -Operator in -FailBackgroundColor Salmon -HighlightHeaders 'PrimaryGroupID', 'PrimaryGroup' New-HTMLTableCondition -Name 'PasswordLastSet' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' New-HTMLTableCondition -Name 'LastLogonDate' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' } DataInformation = { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -FontWeight bold, normal -Text "PasswordNotRequired", " - means password is not required by the account. This should be investigated right away. " New-HTMLListItem -FontWeight bold, normal -Text "PasswordNeverExpires", " - means password is not required by the account. This should be investigated right away. " New-HTMLListItem -FontWeight bold, normal -Text "AllowReversiblePasswordEncryption", " - means the password is stored insecurely in Active Directory. Removing this flag is required. " New-HTMLListItem -FontWeight bold, normal -Text "UseDESKeyOnly", " - means the kerberos encryption is set to DES which is very weak. Removing flag is required. " New-HTMLListItem -FontWeight bold, normal -Text "PrimaryGroupID", " - if primary group ID is something else then 513 it means someone made a primary group change to something else than Domain Users. This should be fixed. " } -FontSize 10pt } } $DomainSecurityDelegatedObjects = @{ Name = 'DomainSecurityDelegatedObjects' Enable = $true Scope = 'Domain' Source = @{ Name = "Security: Delegated Objects" Data = { Get-WinADDelegatedAccounts -Forest $ForestName -IncludeDomains $Domain } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 0 ActionType = 0 Description = '' Resources = @( '[What is KERBEROS DELEGATION? An overview of kerberos delegation](https://stealthbits.com/blog/what-is-kerberos-delegation-an-overview-of-kerberos-delegation/)' ) StatusTrue = 1 StatusFalse = 0 } ExpectedOutput = $null } Tests = [ordered] @{ FullDelegation = @{ Enable = $true Name = 'There should be no full delegation' Parameters = @{ WhereObject = { $_.FullDelegation -eq $true -and $_.IsDC -eq $false } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 9 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "" } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @( "There are a few flavors of Kerberos delegation since it has evolved over the years. The original implementation is unconstrained delegation, this was what existed in Windows Server 2000. Since then, more strict versions of delegation have come along. Constrained delegation, which was available in Windows Server 2003, and Resource-Based Constrained delegation which was made available in 2012, both have improved the security and implementation of Kerberos delegation. " "Those settings are: " ) New-HTMLList { New-HTMLListItem -Text "Unconstrained (Full) delegation ", " is most When a privileged account authenticates to a host with unconstrained delegation configured, you now can access any configured service within the domain as that privileged user. " -FontWeight bold, normal New-HTMLListItem -Text "Constrained delegation ", " takes it a step further by allowing you to configure which services an account can be delegated to. This, in theory, would limit the potential exposure if a compromise occurred." -FontWeight bold, normal New-HTMLListItem -Text "Resource-Based Constrained Delegation ", " changes how you can configure constrained delegation, and it will work across a trust. Instead of specifying which object can delegate to which service, the resource hosting the service specifies which objects can delegate to it. From an administrative standpoint, this allows the resource owner to control who can access it. " -FontWeight bold, normal } New-HTMLText -Text @( "It's important that there are no objects with unconstrained delegation anywhere else than on Domain Controller objects." ) } } DataHighlights = { New-HTMLTableConditionGroup { New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Value $true -Operator eq New-HTMLTableCondition -Name 'FullDelegation' -ComparisonType string -Value $true -Operator eq } -BackgroundColor PaleGreen -HighlightHeaders IsDC, FullDelegation New-HTMLTableConditionGroup { New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Value $false -Operator eq New-HTMLTableCondition -Name 'FullDelegation' -ComparisonType string -Value $true -Operator eq } -BackgroundColor Salmon -HighlightHeaders IsDC, FullDelegation New-HTMLTableConditionGroup { New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Value $false -Operator eq New-HTMLTableCondition -Name 'FullDelegation' -ComparisonType string -Value $false -Operator eq } -BackgroundColor PaleGreen -HighlightHeaders IsDC, FullDelegation New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordLastSet' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' New-HTMLTableCondition -Name 'LastLogonDate' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' } DataInformation = { } } $SecurityGroupsAccountOperators = @{ Name = 'DomainSecurityGroupsAccountOperators' Enable = $true Scope = 'Domain' Source = @{ Name = "Groups: Account operators should be empty" Data = { Get-ADGroupMember -Identity 'S-1-5-32-548' -Recursive -Server $Domain } Details = [ordered] @{ Area = 'Objects' Category = 'Cleanup', 'Security' Severity = '' Importance = 0 Description = "The Account Operators group should not be used. Custom delegate instead. This group is a great 'backdoor' priv group for attackers. Microsoft even says don't use this group!" Resolution = '' Resources = @() } ExpectedOutput = $false } } $SecurityGroupsSchemaAdmins = @{ Name = 'DomainSecurityGroupsSchemaAdmins' Enable = $true Scope = 'Domain' Source = @{ Name = "Groups: Schema Admins should be empty" Data = { $DomainSID = (Get-ADDomain -Server $Domain).DomainSID Get-ADGroupMember -Recursive -Server $Domain -Identity "$DomainSID-518" } Requirements = @{ IsDomainRoot = $true } Details = [ordered] @{ Area = 'Objects' Category = 'Cleanup', 'Security' Severity = '' Importance = 0 Description = "Schema Admins group should be empty. If you need to manage schema you can always add user for the time of modification." Resolution = 'Keep Schema group empty.' Resources = @( 'https://www.stigviewer.com/stig/active_directory_forest/2016-12-19/finding/V-72835' ) } ExpectedOutput = $false } } $SecurityKRBGT = @{ Name = 'DomainSecurityKrbtgt' Enable = $true Scope = 'Domain' Source = @{ Name = "Security: Krbtgt password" Data = { #Get-ADUser -Filter { name -like "krbtgt*" } -Property Name, Created, logonCount, Modified, PasswordLastSet, PasswordExpired, msDS-KeyVersionNumber, CanonicalName, msDS-KrbTgtLinkBl -Server $Domain Get-ADUser -Filter { name -like "krbtgt*" } -Property Name, Created, Modified, PasswordLastSet, PasswordExpired, msDS-KeyVersionNumber, CanonicalName, msDS-KrbTgtLinkBl, Description -Server $Domain | Select-Object Name, Enabled, Description, PasswordLastSet, PasswordExpired, msDS-KrbTgtLinkBl, msDS-KeyVersionNumber, CanonicalName, Created, Modified } Details = [ordered] @{ Category = 'Security' Importance = 10 ActionType = 1 Description = 'A stolen krbtgt account password can wreak havoc on an organization because it can be used to impersonate authentication throughout the organization thereby giving an attacker access to sensitive data.' Resources = @( '[AD Forest Recovery - Resetting the krbtgt password](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/ad-forest-recovery-resetting-the-krbtgt-password)' '[KRBTGT Account Password Reset Scripts now available for customers](https://www.microsoft.com/security/blog/2015/02/11/krbtgt-account-password-reset-scripts-now-available-for-customers/)' "[Kerberos & KRBTGT: Active Directory's Domain Kerberos Service Account](https://adsecurity.org/?p=483)" "[Attacking Read-Only Domain Controllers to Own Active Directory](https://adsecurity.org/?p=3592)" '[DETECTING AND PREVENTING A GOLDEN TICKET ATTACK](https://frsecure.com/blog/golden-ticket-attack/)' '[Adversary techniques for credential theft and data compromise - Golden Ticket](https://attack.stealthbits.com/how-golden-ticket-attack-works)' '[Do You Need to Update KRBTGT Account Password?](https://www.kjctech.net/do-you-need-to-update-krbtgt-account-password/)' ) StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{ PasswordLastSet = @{ Enable = $false Name = 'Krbtgt Last Password Change should changed frequently' Parameters = @{ Property = 'PasswordLastSet' ExpectedValue = '(Get-Date).AddDays(-180)' OperationType = 'gt' } Details = [ordered] @{ Category = 'Security' Importance = 8 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = 'LastPasswordChange should be less than 180 days ago.' } PasswordLastSetPrimary = @{ Enable = $true Name = 'Krbtgt DC password should be changed frequently' Parameters = @{ WhereObject = { $_.Name -eq 'krbtgt' -and $_.PasswordLastSet -lt (Get-Date).AddDays(-180) } ExpectedCount = 0 OperationType = 'eq' } Details = [ordered] @{ Category = 'Security' Importance = 8 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = 'LastPasswordChange should be less than 180 days ago.' } PasswordLastSetAzure = @{ Enable = $true Name = 'Krbtgt Azure AD password should be changed frequently' Parameters = @{ WhereObject = { $_.Name -eq 'krbtgt_AzureAD' -and $_.PasswordLastSet -lt (Get-Date).AddDays(-180) } ExpectedCount = 0 OperationType = 'eq' } Details = [ordered] @{ Category = 'Security' Importance = 8 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = 'LastPasswordChange should be less than 180 days ago.' } PasswordLastSetRODC = @{ Enable = $true Name = 'Krbtgt RODC password should be changed frequently' Parameters = @{ WhereObject = { $_.Name -ne 'krbtgt' -and $_.Name -ne 'krbtgt_AzureAD' -and $_.PasswordLastSet -lt (Get-Date).AddDays(-180) } ExpectedCount = 0 OperationType = 'eq' } Details = [ordered] @{ Category = 'Security' Importance = 8 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = 'LastPasswordChange should be less than 180 days ago.' } DeadKerberosAccount = @{ Enable = $true Name = 'Krbtgt RODC account without RODC' Parameters = @{ WhereObject = { $_.Name -ne 'krbtgt' -and $_.Name -ne 'krbtgt_AzureAD' -and $_.'msDS-KrbTgtLinkBl'.Count -eq 0 } ExpectedCount = 0 OperationType = 'eq' } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 2 } Description = 'Kerberos accounts for dead RODCs should be removed' } } DataInformation = { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -FontWeight bold, normal -Text "PasswordLastSet", " - shows the last date password for Kerberos was changed." New-HTMLListItem -FontWeight bold, normal -Text "msDS-KrbTgtLinkBl", " - shows linked RODC. If name contains numbers and msDS-KrbTgtLinkBl is empty the kerberos account is not required." } -FontSize 10pt New-HTMLText -Text "Please keep in mind that if there are more than one keberos account it means there are RODC having own krbtgt account. " -FontSize 10pt } DataHighlights = { New-HTMLTableConditionGroup { New-HTMLTableCondition -Name 'Name' -Value 'krbtgt' -Operator ne -ComparisonType string New-HTMLTableCondition -Name 'msDS-KrbTgtLinkBl' -Value '' -Operator eq -ComparisonType string } -Row -BackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordLastSet' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon } } $SecurityUsers = @{ Name = 'DomainSecurityUsers' Enable = $true Scope = 'Domain' Source = @{ Name = "Users: Standard" Data = { $Properties = @( 'SamAccountName' 'UserPrincipalName' 'Enabled' 'PasswordNotRequired' 'AllowReversiblePasswordEncryption' 'UseDESKeyOnly' 'PasswordLastSet' 'LastLogonDate' 'PrimaryGroup' 'PrimaryGroupID' 'DistinguishedName' 'Name' #'ObjectClass' #'ObjectGUID' 'SID' 'SamAccountType' #'GivenName' #'Surname' ) $GuestSID = "$($DomainInformation.DomainSID)-501" # Skipping trusts with SamAccountType and Guests # Skipping Exchange_Online-ApplicationAccount because it doesn't require password by default (also disabled) Get-ADUser -Filter { (AllowReversiblePasswordEncryption -eq $true -or UseDESKeyOnly -eq $true -or PrimaryGroupID -ne '513' -or PasswordNotRequired -eq $true) -and (SID -ne $GuestSID -and SamAccountType -ne 805306370) } -Properties $Properties -Server $Domain | Where-Object { $_.UserPrincipalName -notlike 'Exchange_Online-ApplicationAccount*' } | Select-Object -Property $Properties } Details = [ordered] @{ Category = 'Security', 'Cleanup' Importance = 0 ActionType = 0 Description = 'Account by default have certain settings that make sure the account is fairly safe and can be used within Active Directory.' Resources = @( ) StatusTrue = 1 StatusFalse = 0 } ExpectedOutput = $false } Tests = [ordered] @{ KeberosDES = @{ Enable = $true Name = 'Kerberos DES detection' Parameters = @{ WhereObject = { $_.UseDESKeyOnly -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "User accounts shouldn't use DES encryption. Having UseDESKeyOnly forces the Kerberos encryption to be DES instead of RC4 which is the Microsoft default. DES is 56 bit encryption and is regarded as weak these days so this setting is not recommended." } AllowReversiblePasswordEncryption = @{ Enable = $true Name = 'Reversible Password detection' Parameters = @{ WhereObject = { $_.AllowReversiblePasswordEncryption -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "User accounts shouldn't use Reversible Password Encryption. Having AllowReversiblePasswordEncryption allows for easy password decryption." } PasswordNotRequired = @{ Enable = $true Name = 'PasswordNotRequired detection' Parameters = @{ WhereObject = { $_.PasswordNotRequired -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "User accounts shouldn't use PasswordNotRequired. Having PasswordNotRequired is dangerous and shoudn't be used." } PrimaryGroup = @{ Enable = $true Name = "Primary Group shouldn't be changed from default Domain Users." Parameters = @{ #WhereObject = { $_.PrimaryGroupID -ne 513 -and $_.SID -ne "$((Get-ADDomain).DomainSID.Value)-501" } WhereObject = { $_.PrimaryGroupID -ne 513 -and $_.SID -ne "$($DomainInformation.DomainSID)-501" } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{ Category = 'Security' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } Description = "User accounts shouldn't have different group then Domain Users as their primary group." } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @( "Account by default have certain settings that make sure the account is fairly safe and can be used within Active Directory. " "Those settings are: " ) New-HTMLList { New-HTMLListItem -Text "Password is always required" New-HTMLListItem -Text "Password is not reverisble" New-HTMLListItem -Text "Keberos Encryption is set to RC4" New-HTMLListItem -Text "Primary Group is always Domain Users with exception of Domain Guests" } New-HTMLText -Text @( "It's important that all those settings are set as expected." ) } } DataHighlights = { New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'AllowReversiblePasswordEncryption' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'UseDESKeyOnly' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon New-HTMLTableCondition -Name 'PrimaryGroupID' -ComparisonType string -BackgroundColor PaleGreen -Value '513' -Operator eq -FailBackgroundColor Salmon -HighlightHeaders 'PrimaryGroupID', 'PrimaryGroup' New-HTMLTableCondition -Name 'PasswordLastSet' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' New-HTMLTableCondition -Name 'LastLogonDate' -ComparisonType date -BackgroundColor PaleGreen -Value (Get-Date).AddDays(-180) -Operator gt -FailBackgroundColor Salmon -DateTimeFormat 'DD.MM.YYYY HH:mm:ss' } DataInformation = { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -FontWeight bold, normal -Text "PasswordNotRequired", " - means password is not required by the account. This should be investigated right away. " New-HTMLListItem -FontWeight bold, normal -Text "AllowReversiblePasswordEncryption", " - means the password is stored insecurely in Active Directory. Removing this flag is required. " New-HTMLListItem -FontWeight bold, normal -Text "UseDESKeyOnly", " - means the kerberos encryption is set to DES which is very weak. Removing flag is required. " New-HTMLListItem -FontWeight bold, normal -Text "PrimaryGroupID", " - if primary group ID is something else then 513 it means someone made a primary group change to something else than Domain Users. This should be fixed. " } -FontSize 10pt } } $SecurityUsersAcccountAdministrator = @{ Name = 'DomainSecurityUsersAcccountAdministrator' Enable = $true Scope = 'Domain' Source = @{ Name = "Users: Administrator (SID-500)" Data = { # this test is kind of special # basically when account is disabled it doesn't make sense to check for PasswordLastSet # therefore i'm adding setting PasswordLastSet to current date to be able to test just that field # At least until support for multiple checks is added $DomainSID = (Get-ADDomain -Server $Domain).DomainSID $User = Get-ADUser -Identity "$DomainSID-500" -Properties PasswordLastSet, LastLogonDate, servicePrincipalName -Server $Domain if ($User.Enabled -eq $false) { [PSCustomObject] @{ Name = $User.SamAccountName Enabled = $User.Enabled PasswordLastSet = Get-Date ServicePrincipalName = $User.ServicePrincipalName LastLogonDate = $User.LastLogonDate DistinguishedName = $User.DistinguishedName SID = $User.SID } } else { [PSCustomObject] @{ Name = $User.SamAccountName Enabled = $User.Enabled PasswordLastSet = $User.PasswordLastSet ServicePrincipalName = $User.ServicePrincipalName LastLogonDate = $User.LastLogonDate DistinguishedName = $User.DistinguishedName SID = $User.SID } } } Details = [ordered] @{ Category = 'Security' Importance = 0 ActionType = 0 Description = "Administrator (SID-500) account is critical account in Active Directory. Due to it's role it shouldn't be used as a daily driver, and only as emeregency account." Resources = @( ) StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{ LastLogonDate = @{ Enable = $true Name = 'Last Logon Date should not be recent' Parameters = @{ Property = 'LastLogonDate' ExpectedValue = (Get-Date).AddDays(-60) OperationType = 'lt' } Details = [ordered] @{ Category = 'Security' Importance = 9 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "" } ServicePrincipalName = @{ Enable = $true Name = 'Service Principal Name should be empty' Parameters = @{ Property = 'servicePrincipalName' ExpectedValue = $null OperationType = 'eq' } Details = [ordered] @{ Category = 'Security' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } Description = "" } PasswordLastSet = @{ Enable = $true Name = 'Administrator Last Password Change Should be less than 360 days ago' Parameters = @{ Property = 'PasswordLastSet' ExpectedValue = '(Get-Date).AddDays(-360)' OperationType = 'gt' } Description = 'Administrator account should be disabled or LastPasswordChange should be less than 1 year ago.' } } } $SysVolDFSR = @{ Name = 'DomainSysVolDFSR' Enable = $true Scope = 'Domain' Source = @{ Name = "DFSR Flags" Data = { $DistinguishedName = (Get-ADDomain -Server $Domain).DistinguishedName $ADObject = "CN=DFSR-GlobalSettings,CN=System,$DistinguishedName" $Object = Get-ADObject -Identity $ADObject -Properties * -Server $Domain if ($Object.'msDFSR-Flags' -gt 47) { [PSCustomObject] @{ 'SysvolMode' = 'DFS-R' 'Flags' = $Object.'msDFSR-Flags' } } else { [PSCustomObject] @{ 'SysvolMode' = 'Not DFS-R' 'Flags' = $Object.'msDFSR-Flags' } } } Details = [ordered] @{ Category = 'Health' Area = 'SYSVOL' Severity = '' Importance = 0 Description = 'Checks if DFS-R is available.' Resolution = '' Resources = @( 'https://blogs.technet.microsoft.com/askds/2009/01/05/dfsr-sysvol-migration-faq-useful-trivia-that-may-save-your-follicles/' 'https://dirteam.com/sander/2019/04/10/knowledgebase-in-place-upgrading-domain-controllers-to-windows-server-2019-while-still-using-ntfrs-breaks-sysvol-replication-and-dslocator/' ) } ExpectedOutput = $true } Tests = [ordered] @{ DFSRSysvolState = @{ Enable = $true Name = 'DFSR Sysvol State' Parameters = @{ Property = 'SysvolMode' ExpectedValue = 'DFS-R' OperationType = 'eq' PropertyExtendedValue = 'Flags' } } } } $WellKnownFolders = @{ Name = 'DomainWellKnownFolders' Enable = $true Scope = 'Domain' Source = @{ Name = 'Well known folders' Data = { $DomainInformation = Get-ADDomain -Server $Domain $WellKnownFolders = $DomainInformation | Select-Object -Property UsersContainer, ComputersContainer, DomainControllersContainer, DeletedObjectsContainer, SystemsContainer, LostAndFoundContainer, QuotasContainer, ForeignSecurityPrincipalsContainer $CurrentWellKnownFolders = [ordered] @{ } $DomainDistinguishedName = $DomainInformation.DistinguishedName $DefaultWellKnownFolders = [ordered] @{ UsersContainer = "CN=Users,$DomainDistinguishedName" ComputersContainer = "CN=Computers,$DomainDistinguishedName" DomainControllersContainer = "OU=Domain Controllers,$DomainDistinguishedName" DeletedObjectsContainer = "CN=Deleted Objects,$DomainDistinguishedName" SystemsContainer = "CN=System,$DomainDistinguishedName" LostAndFoundContainer = "CN=LostAndFound,$DomainDistinguishedName" QuotasContainer = "CN=NTDS Quotas,$DomainDistinguishedName" ForeignSecurityPrincipalsContainer = "CN=ForeignSecurityPrincipals,$DomainDistinguishedName" } foreach ($_ in $WellKnownFolders.PSObject.Properties.Name) { $CurrentWellKnownFolders[$_] = $DomainInformation.$_ } Compare-MultipleObjects -Objects @($DefaultWellKnownFolders, $CurrentWellKnownFolders) -SkipProperties } Details = [ordered] @{ Area = 'Objects' Category = 'Configuration' Severity = 'Low' Importance = 5 Description = 'Verifies whether well-known folders are at their defaults or not.' Resolution = 'Follow given resources to redirect users and computers containers to managable Organizational Units. If other Well Known folers are wrong - investigate.' Resources = @( 'https://support.microsoft.com/en-us/help/324949/redirecting-the-users-and-computers-containers-in-active-directory-dom' ) } ExpectedOutput = $true } Tests = [ordered] @{ UsersContainer = @{ Enable = $true Name = "Users Container shouldn't be at default" Parameters = @{ WhereObject = { $_.Name -eq 'UsersContainer' } ExpectedValue = $false Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } ComputersContainer = @{ Enable = $true Name = "Computers Container shouldn't be at default" Parameters = @{ WhereObject = { $_.Name -eq 'ComputersContainer' } ExpectedValue = $false Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } DomainControllersContainer = @{ Enable = $true Name = "Domain Controllers Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'DomainControllersContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } DeletedObjectsContainer = @{ Enable = $true Name = "Deleted Objects Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'DeletedObjectsContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } SystemsContainer = @{ Enable = $true Name = "Systems Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'SystemsContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } LostAndFoundContainer = @{ Enable = $true Name = "Lost And Found Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'LostAndFoundContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } QuotasContainer = @{ Enable = $true Name = "Quotas Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'QuotasContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } ForeignSecurityPrincipalsContainer = @{ Enable = $true Name = "Foreign Security Principals Container should be at default location" Parameters = @{ WhereObject = { $_.Name -eq 'ForeignSecurityPrincipalsContainer' } ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = '1' } } } } $DCDNSForwaders = @{ Name = 'DCDNSForwaders' Enable = $true Scope = 'DC' Source = @{ Name = "DC DNS Forwarders" Data = { $Forwarders = Get-WinADDnsServerForwarder -Forest $ForestName -Domain $Domain -IncludeDomainControllers $DomainController -WarningAction SilentlyContinue -Formatted $Forwarders } Details = [ordered] @{ Category = 'Configuration' Area = 'DNS' Importance = 5 Description = '' Resolution = '' Resources = @( ) } ExpectedOutput = $true } Tests = [ordered] |