Testimo.psm1
$ComputersUnsupported = @{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 = 'Cleanup' Category = '' Severity = '' Importance = 0 Description = 'Computers running an unsupported operating system.' Resolution = 'Upgrade or remove computers from Domain.' Resources = @() } ExpectedOutput = $false } } $ComputersUnsupportedMainstream = @{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 = 'Cleanup' Category = '' Severity = '' Importance = 0 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 = @{Enable = $false Scope = 'Domain' Source = @{Name = "DHCP authorized in domain" Data = { $SearchBase = 'cn=configuration,{0}' -f $DomainInformation.DistinguishedName Get-ADObject -SearchBase $searchBase -Filter "objectclass -eq 'dhcpclass' -AND Name -ne 'dhcproot'" } Details = [ordered] @{Area = 'Configuration' Category = 'DHCP' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{DHCPAuthorized = @{Enable = $true Name = 'At least 1 DHCP Server Authorized' Parameters = @{ExpectedCount = '1' OperationType = 'gt' } } } } $DNSForwaders = @{Enable = $true Scope = 'Domain' Source = @{Name = "DNS Forwarders" Data = { [Array] $Forwarders = Get-WinDnsServerForwarder -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] @{Source = 'No forwarders set' Status = $false } } else { [PSCustomObject] @{Source = $Forwarders[0].IPAddress -join ', ' Status = $true } } } Details = [ordered] @{Area = 'Configuration' Category = '' Severity = '' Importance = 0 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "DNS Scavenging - Primary DNS Server" Data = { Get-WinDnsServerScavenging -Forest $ForestName -IncludeDomains $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' Importance = 0 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 } Property = 'LastScavengeTime' ExpectedValue = '(Get-Date).AddDays(-7)' OperationType = 'gt' } } } } $DnsZonesAging = @{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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "DomainDNSZones should have proper FSMO Owner (0ADEL)" Data = { $IdentityDomain = "CN=Infrastructure,DC=DomainDnsZones,$(($DomainInformation).DistinguishedName)" $FSMORoleOwner = (Get-ADObject -Identity $IdentityDomain -Properties fSMORoleOwner -Server $Domain) $FSMORoleOwner } Details = [ordered] @{Area = 'Configuration' Category = 'DNS' 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "ForestDNSZones should have proper FSMO Owner (0ADEL)" Data = { $IdentityForest = "CN=Infrastructure,DC=ForestDnsZones,$(($DomainInformation).DistinguishedName)" $FSMORoleOwner = (Get-ADObject -Identity $IdentityForest -Properties fSMORoleOwner -Server $Domain) $FSMORoleOwner } Requirements = @{IsDomainRoot = $true } Details = [ordered] @{Area = 'Configuration' Category = 'DNS' 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 = @{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 = @{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 } } 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 "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 '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-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 } 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = '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 = @{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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "Duplicate Objects: 0ACNF (Duplicate RDN)" 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 } 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 = @{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 } } $GroupPolicyAssesment = @{Enable = $true Scope = 'Domain' Source = @{Name = "Group Policy Assesment" Data = { Get-GPOZaurr -Forest $ForestName -IncludeDomains $Domain } Implementation = {} Details = [ordered] @{Area = 'Cleanup' Category = 'Group Policy' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{Empty = @{Enable = $true Name = 'Group Policy Empty' Parameters = @{WhereObject = { $_.Empty -eq $false } ExpectedCount = 0 ExpectedOutput = $true } } Linked = @{Enable = $true Name = 'Group Policy Unlinked' Parameters = @{WhereObject = { $_.Linked -eq $false } ExpectedCount = 0 } } Enabled = @{Enable = $true Name = 'Group Policy Disabled' Parameters = @{WhereObject = { $_.Enabled -eq $false } ExpectedCount = 0 ExpectedOutput = $true } } Problem = @{Enable = $true Name = 'Group Policy with Problem' Parameters = @{WhereObject = { $_.Problem -eq $false } ExpectedCount = 0 } } Optimized = @{Enable = $true Name = 'Group Policy Not Optimized' Parameters = @{WhereObject = { $_.Optimized -eq $false } ExpectedCount = 0 } } ApplyPermission = @{Enable = $true Name = 'Group Policy No Apply Permission' Parameters = @{WhereObject = { $_.ApplyPermissioon -eq $false } ExpectedCount = 0 } } } } $GroupPolicyADM = @{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 = 'Cleanup' Category = 'Group Policy' 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "GPO: Owner" Data = { Get-GPOZaurrOwner -Forest $ForestName -IncludeSysvol -IncludeDomains $Domain } Details = [ordered] @{Area = 'Security' Category = '' Severity = '' Importance = 0 Description = "" Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{OwnerConsistent = @{Enable = $true Name = 'GPO: Owner Consistent' Parameters = @{WhereObject = { $_.IsOwnerConsistent -ne $true } ExpectedResult = $false } } OwnerAdministrative = @{Enable = $true Name = 'GPO: Owner Administrative' Parameters = @{WhereObject = { $_.OwnerType -ne 'Administrative' -or $_.SysvolType -ne 'Administrative' } ExpectedResult = $false } } } } $GroupPolicyPermissions = @{Enable = $true Scope = 'Domain' Source = @{Name = "Group Policy Required Permissions" Data = { Get-GPOZaurrPermissionAnalysis -Forest $ForestName -Domain $Domain } Details = [ordered] @{Area = 'Security' Category = '' 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 $false } } } } } $GroupPolicyPermissionConsistency = @{Enable = $true Scope = 'Domain' Source = @{Name = "GPO: Permission Consistency" Data = { Get-GPOZaurrPermissionConsistency -Forest $ForestName -VerifyInheritance -Type Inconsistent -IncludeDomains $Domain } Details = [ordered] @{Area = 'Security' Category = '' 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "GPO: Sysvol folder existance" Data = { Get-GPOZaurrSysvol -Forest $ForestName -IncludeDomains $Domain } Details = [ordered] @{Area = 'Security' Category = '' 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 } } } } $NetLogonOwner = @{Enable = $true Scope = 'Domain' Source = @{Name = "NetLogon Owner" Data = { Get-GPOZaurrNetLogon -Forest $ForestName -OwnerOnly -IncludeDomains $Domain } Implementation = {} Details = [ordered] @{Area = 'Cleanup' Category = 'SYSVOL' Severity = '' Importance = 10 Description = "" Resolution = '' Resources = @() Tags = 'netlogon', 'grouppolicy', 'gpo', 'sysvol' } ExpectedOutput = $true } Tests = [ordered] @{Empty = @{Enable = $true Name = 'Owner should be BUILTIN\Administrators' Parameters = @{WhereObject = { $_.OwnerSid -ne 'S-1-5-32-544' } ExpectedCount = 0 ExpectedOutput = $true } } } } $OrganizationalUnitsEmpty = @{Enable = $true Scope = 'Domain' Source = @{Name = "Organizational Units: Orphaned/Empty" Data = { $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] @{Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $false } } $OrganizationalUnitsProtected = @{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 = 'Cleanup' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $false } } $OrphanedForeignSecurityPrincipals = @{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' 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-HTMLWizard { 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = 'Password Complexity Requirements' Data = { Get-ADDefaultDomainPasswordPolicy -Server $Domain } Details = [ordered] @{Area = 'Security' Category = '' 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 = @{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 = @{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' } } } } $SecurityGroupsAccountOperators = @{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 = 'Cleanup', 'Security' Category = '' 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 = @{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 = 'Cleanup', 'Security' Category = '' 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 = @{Enable = $true Scope = 'Domain' Source = @{Name = "Security: Krbtgt password" Data = { Get-ADUser -Identity "krbtgt" -Property Name, Created, logonCount, Modified, PasswordLastSet, PasswordExpired, msDS-KeyVersionNumber, CanonicalName, msDS-KrbTgtLinkBl -Server $Domain | Select-Object Name, Created, logonCount, Modified, PasswordLastSet, PasswordExpired, msDS-KeyVersionNumber, CanonicalName, msDS-KrbTgtLinkBl } Details = [ordered] @{Area = 'Security', 'Cleanup' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @('https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/ad-forest-recovery-resetting-the-krbtgt-password' 'https://gallery.technet.microsoft.com/Reset-the-krbtgt-account-581a9e51' 'https://www.microsoft.com/security/blog/2015/02/11/krbtgt-account-password-reset-scripts-now-available-for-customers/') } ExpectedOutput = $true } Tests = [ordered] @{PasswordLastSet = @{Enable = $true Name = 'Krbtgt Last Password Change Should be less than 180 days ago' Parameters = @{Property = 'PasswordLastSet' ExpectedValue = '(Get-Date).AddDays(-180)' OperationType = 'gt' } Description = 'User account should be disabled or LastPasswordChange should be less than 1 year ago.' } } } $SecurityUsers = @{Enable = $true Scope = 'Domain' Source = @{Name = "Users: Standard" Data = { $Properties = @('AllowReversiblePasswordEncryption' 'DistinguishedName' 'Enabled' 'GivenName' 'Name' 'ObjectClass' 'ObjectGUID' 'PasswordLastSet' 'PrimaryGroup' 'PrimaryGroupID' 'SamAccountName' 'SID' 'Surname' 'UseDESKeyOnly' 'UserPrincipalName') $GuestSID = "$($DomainInformation.DomainSID)-501" Get-ADUser -Filter { (AllowReversiblePasswordEncryption -eq $true -or UseDESKeyOnly -eq $true -or PrimaryGroupID -ne '513') -and (SID -ne $GuestSID) } -Properties AllowReversiblePasswordEncryption, UseDESKeyOnly, PrimaryGroup, PrimaryGroupID, PasswordLastSet, Enabled -Server $Domain | Select-Object -Property $Properties } Details = [ordered] @{Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $false } Tests = [ordered] @{KeberosDES = @{Enable = $true Name = 'Kerberos DES detection' Parameters = @{WhereObject = { $_.UseDESKeyOnly -eq $true } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } 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 } Description = "User accounts shouldn't use Reversible Password Encryption. Having AllowReversiblePasswordEncryption allows for easy password decryption." } PrimaryGroup = @{Enable = $true Name = "Primary Group shouldn't be changed from default Domain Users." Parameters = @{WhereObject = { $_.PrimaryGroupID -ne 513 -and $_.SID -ne "$($DomainInformation.DomainSID)-501" } ExpectedCount = 0 OperationType = 'eq' ExpectedOutput = $false } Description = "User accounts shouldn't have different group then Domain Users as their primary group." } } } $SecurityUsersAcccountAdministrator = @{Enable = $true Scope = 'Domain' Source = @{Name = "Users: Administrator" Data = { $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 = 'Administrator' PasswordLastSet = Get-Date } } else { [PSCustomObject] @{Name = 'Administrator' PasswordLastSet = $User.PasswordLastSet } } } Details = [ordered] @{Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{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 = @{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] @{Area = 'Health' Category = '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 = @{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 -Object @($DefaultWellKnownFolders, $CurrentWellKnownFolders) -SkipProperties } Details = [ordered] @{Area = 'Configuration' Category = 'OrganizationalUnits' 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 = @{Enable = $true Scope = 'DC' Source = @{Name = "DC DNS Forwarders" Data = { $Forwarders = Get-WinDnsServerForwarder -Forest $ForestName -Domain $Domain -IncludeDomainControllers $DomainController -WarningAction SilentlyContinue -Formatted $Forwarders } Details = [ordered] @{Area = 'Configuration' Category = 'DNS' Severity = 'Medium' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{SameForwarders = @{Enable = $true Name = 'Multiple DNS Forwarders' Parameters = @{Property = 'ForwardersCount' ExpectedValue = 1 OperationType = 'gt' PropertyExtendedValue = 'IPAddress' } Description = 'DNS: More than one forwarding server should be configured' } } } $DFS = @{Enable = $true Scope = 'DC' Source = @{Name = "SYSVOL/DFS Verification" Data = { Get-WinADDFSHealth -Forest $ForestName -Domains $Domain -DomainControllers $DomainController -EventDays $EventDays } Parameters = @{EventDays = 3 } Details = [ordered] @{Area = 'Health' Category = 'DFS' Severity = 'High' Importance = 0 Description = "Provides health verification of SYSVOL/DFS on Domain Controller." Resolution = '' Resources = @('https://support.microsoft.com/en-us/help/2218556/how-to-force-an-authoritative-and-non-authoritative-synchronization-fo' 'https://www.itprotoday.com/windows-78/fixing-broken-sysvol-replication' 'https://www.brisk-it.net/when-dfs-replication-goes-wrong-and-how-to-fix-it/' 'https://gallery.technet.microsoft.com/scriptcenter/AD-DFS-Replication-Auto-812a88bc' 'https://www.reddit.com/r/sysadmin/comments/7gey4k/resuming_dfs_replication_after_4_years_of_no/' 'https://kimconnect.com/fix-dfs-replication-problems/' 'https://community.spiceworks.com/topic/2205945-repairing-broken-dfs-replication' 'https://support.microsoft.com/en-us/help/2958414/dfs-replication-how-to-troubleshoot-missing-sysvol-and-netlogon-shares' 'https://noobient.com/2013/11/11/fixing-sysvol-replication-on-windows-server-2012/' 'https://jackstromberg.com/2014/07/sysvol-and-group-policy-out-of-sync-on-server-2012-r2-dcs-using-dfsr/') } ExpectedOutput = $true } Tests = [ordered] @{Status = @{Enable = $true Name = 'DFS should be Healthy' Parameters = @{ExpectedValue = $true Property = 'Status' OperationType = 'eq' } } ReplicationState = @{Enable = $true Name = 'Replication State should be NORMAL' Parameters = @{ExpectedValue = 'Normal' Property = 'ReplicationState' OperationType = 'eq' } } CentralRepository = @{Enable = $true Name = 'Central Repository for GPO for Domain should be available' Parameters = @{ExpectedValue = $true Property = 'CentralRepository' OperationType = 'eq' } } CentralRepositoryDC = @{Enable = $true Name = 'Central Repository for GPO for DC should be available' Parameters = @{ExpectedValue = $true Property = 'CentralRepositoryDC' OperationType = 'eq' } } IdenticalCount = @{Enable = $true Name = 'GPO Count should match folder count' Parameters = @{ExpectedValue = $true Property = 'IdenticalCount' OperationType = 'eq' } } MemberReference = @{Enable = $true Name = 'MemberReference should return TRUE' Parameters = @{ExpectedValue = $true Property = 'MemberReference' OperationType = 'eq' } } DFSErrors = @{Enable = $true Name = 'DFSErrors should be 0' Parameters = @{ExpectedValue = 0 Property = 'DFSErrors' OperationType = 'eq' } } DFSLocalSetting = @{Enable = $true Name = 'DFSLocalSetting should be TRUE' Parameters = @{ExpectedValue = $true Property = 'DFSLocalSetting' OperationType = 'eq' } } DomainSystemVolume = @{Enable = $true Name = 'DomainSystemVolume should be TRUE' Parameters = @{ExpectedValue = $true Property = 'DomainSystemVolume' OperationType = 'eq' } } SYSVOLSubscription = @{Enable = $true Name = 'SYSVOLSubscription should be TRUE' Parameters = @{ExpectedValue = $true Property = 'SYSVOLSubscription' OperationType = 'eq' } } DFSRAutoRecovery = @{Enable = $true Name = 'DFSR AutoRecovery should be enabled (not stopped)' Parameters = @{Property = 'StopReplicationOnAutoRecovery' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://secureinfra.blog/2019/04/30/field-notes-a-quick-tip-on-dfsr-automatic-recovery-while-you-prepare-for-an-ad-domain-upgrade/' 'https://richardjgreen.net/active-directory-dfs-r-auto-recovery/') } } } } $Diagnostics = @{Enable = $true Scope = 'DC' Source = @{Name = 'Diagnostics (DCIAG)' Data = { Test-ADDomainController -Forest $ForestName -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = 'Health' Category = 'Overall' Description = '' Resolution = '' Importance = 10 Severity = 'Medium' Resources = @('https://social.technet.microsoft.com/Forums/en-US/b48ee073-eb71-4852-8f56-ecf6f76b3fff/how-could-i-change-result-of-dcdiag-language-to-english-?forum=winserver8gen') } ExpectedOutput = $true } Tests = [ordered] @{Connectivity = @{Enable = $true Name = 'DCDiag Connectivity' Parameters = @{WhereObject = { $_.Test -eq 'Connectivity' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } Advertising = @{Enable = $true Name = 'DCDiag Advertising' Parameters = @{WhereObject = { $_.Test -eq 'Advertising' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } CheckSecurityError = @{Enable = $true Name = 'DCDiag CheckSecurityError' Parameters = @{WhereObject = { $_.Test -eq 'CheckSecurityError' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } CutoffServers = @{Enable = $true Name = 'DCDiag CutoffServers' Parameters = @{WhereObject = { $_.Test -eq 'CutoffServers' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } FrsEvent = @{Enable = $true Name = 'DCDiag FrsEvent' Parameters = @{WhereObject = { $_.Test -eq 'FrsEvent' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } DFSREvent = @{Enable = $true Name = 'DCDiag DFSREvent' Parameters = @{WhereObject = { $_.Test -eq 'DFSREvent' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } SysVolCheck = @{Enable = $true Name = 'DCDiag SysVolCheck' Parameters = @{WhereObject = { $_.Test -eq 'SysVolCheck' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } FrsSysVol = @{Enable = $true Name = 'DCDiag FrsSysVol' Parameters = @{WhereObject = { $_.Test -eq 'FrsSysVol' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } KccEvent = @{Enable = $true Name = 'DCDiag KccEvent' Parameters = @{WhereObject = { $_.Test -eq 'KccEvent' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } KnowsOfRoleHolders = @{Enable = $true Name = 'DCDiag KnowsOfRoleHolders' Parameters = @{WhereObject = { $_.Test -eq 'KnowsOfRoleHolders' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } MachineAccount = @{Enable = $true Name = 'DCDiag MachineAccount' Parameters = @{WhereObject = { $_.Test -eq 'MachineAccount' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } NCSecDesc = @{Enable = $true Name = 'DCDiag NCSecDesc' Parameters = @{WhereObject = { $_.Test -eq 'NCSecDesc' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } NetLogons = @{Enable = $true Name = 'DCDiag NetLogons' Parameters = @{WhereObject = { $_.Test -eq 'NetLogons' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } ObjectsReplicated = @{Enable = $true Name = 'DCDiag ObjectsReplicated' Parameters = @{WhereObject = { $_.Test -eq 'ObjectsReplicated' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } Replications = @{Enable = $true Name = 'DCDiag Replications' Parameters = @{WhereObject = { $_.Test -eq 'Replications' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } RidManager = @{Enable = $true Name = 'DCDiag RidManager' Parameters = @{WhereObject = { $_.Test -eq 'RidManager' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } Services = @{Enable = $true Name = 'DCDiag Services' Parameters = @{WhereObject = { $_.Test -eq 'Services' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } SystemLog = @{Enable = $true Name = 'DCDiag SystemLog' Parameters = @{WhereObject = { $_.Test -eq 'SystemLog' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } Topology = @{Enable = $true Name = 'DCDiag Topology' Parameters = @{WhereObject = { $_.Test -eq 'Topology' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } VerifyEnterpriseReferences = @{Enable = $true Name = 'DCDiag VerifyEnterpriseReferences' Parameters = @{WhereObject = { $_.Test -eq 'VerifyEnterpriseReferences' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } VerifyReferences = @{Enable = $true Name = 'DCDiag VerifyReferences' Parameters = @{WhereObject = { $_.Test -eq 'VerifyReferences' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } VerifyReplicas = @{Enable = $true Name = 'DCDiag VerifyReplicas' Parameters = @{WhereObject = { $_.Test -eq 'VerifyReplicas' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } DNS = @{Enable = $true Name = 'DCDiag DNS' Parameters = @{WhereObject = { $_.Test -eq 'DNS' -and $_.Target -ne $Domain } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } ForestDnsZonesCheckSDRefDom = @{Enable = $true Name = 'DCDiag ForestDnsZones CheckSDRefDom' Parameters = @{WhereObject = { $_.Test -eq 'CheckSDRefDom' -and $_.Target -eq 'ForestDnsZones' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } ForestDnsZonesCrossRefValidation = @{Enable = $true Name = 'DCDiag ForestDnsZones CrossRefValidation' Parameters = @{WhereObject = { $_.Test -eq 'CrossRefValidation' -and $_.Target -eq 'ForestDnsZones' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } DomainDnsZonesCheckSDRefDom = @{Enable = $true Name = 'DCDiag DomainDnsZones CheckSDRefDom' Parameters = @{WhereObject = { $_.Test -eq 'CheckSDRefDom' -and $_.Target -eq 'DomainDnsZones' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } DomainDnsZonesCrossRefValidation = @{Enable = $true Name = 'DCDiag DomainDnsZones CrossRefValidation' Parameters = @{WhereObject = { $_.Test -eq 'CrossRefValidation' -and $_.Target -eq 'DomainDnsZones' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } SchemaCheckSDRefDom = @{Enable = $true Name = 'DCDiag Schema CheckSDRefDom' Parameters = @{WhereObject = { $_.Test -eq 'CheckSDRefDom' -and $_.Target -eq 'Schema' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } SchemaCrossRefValidation = @{Enable = $true Name = 'DCDiag Schema CrossRefValidation' Parameters = @{WhereObject = { $_.Test -eq 'CrossRefValidation' -and $_.Target -eq 'Schema' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } ConfigurationCheckSDRefDom = @{Enable = $true Name = 'DCDiag Configuration CheckSDRefDom' Parameters = @{WhereObject = { $_.Test -eq 'CheckSDRefDom' -and $_.Target -eq 'Configuration' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } ConfigurationCrossRefValidation = @{Enable = $true Name = 'DCDiag Configuration CrossRefValidation' Parameters = @{WhereObject = { $_.Test -eq 'CrossRefValidation' -and $_.Target -eq 'Configuration' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } NetbiosCheckSDRefDom = @{Enable = $true Name = 'DCDiag NETBIOS CheckSDRefDom' Parameters = @{WhereObject = { $_.Test -eq 'CheckSDRefDom' -and ($_.Target -ne 'Configuration' -and $_.Target -ne 'ForestDnsZones' -and $_.Target -ne 'DomainDnsZones' -and $_.Target -ne 'Schema') } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } NetbiosCrossRefValidation = @{Enable = $true Name = 'DCDiag NETBIOS CrossRefValidation' Parameters = @{WhereObject = { $_.Test -eq 'CrossRefValidation' -and ($_.Target -ne 'Configuration' -and $_.Target -ne 'ForestDnsZones' -and $_.Target -ne 'DomainDnsZones' -and $_.Target -ne 'Schema') } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } DNSDomain = @{Enable = $true Name = 'DCDiag DNS' Parameters = @{WhereObject = { $_.Test -eq 'DNS' -and $_.Target -eq $Domain } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } LocatorCheck = @{Enable = $true Name = 'DCDiag LocatorCheck' Parameters = @{WhereObject = { $_.Test -eq 'LocatorCheck' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } FsmoCheck = @{Enable = $true Name = 'DCDiag FsmoCheck' Parameters = @{WhereObject = { $_.Test -eq 'FsmoCheck' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } Intersite = @{Enable = $true Name = 'DCDiag Intersite' Parameters = @{WhereObject = { $_.Test -eq 'Intersite' } Property = 'Result' ExpectedValue = $true OperationType = 'eq' } } } } $DiskSpace = @{Enable = $true Scope = 'DC' Source = @{Name = 'Disk Free' Data = { Get-ComputerDiskLogical -ComputerName $DomainController -OnlyLocalDisk -WarningAction SilentlyContinue } Details = [ordered] @{Area = 'Health' Category = 'Disk' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{FreeSpace = @{Enable = $true Name = 'Free Space in GB' Parameters = @{Property = 'FreeSpace' PropertyExtendedValue = 'FreeSpace' ExpectedValue = 10 OperationType = 'gt' } } FreePercent = @{Enable = $true Name = 'Free Space Percent' Parameters = @{Property = 'FreePercent' PropertyExtendedValue = 'FreePercent' ExpectedValue = 10 OperationType = 'gt' } } } } $DNSNameServers = @{Enable = $true Scope = 'DC' Source = @{Name = "Name servers for primary domain zone" Data = { Test-DNSNameServers -Domain $Domain -DomainController $DomainController } Details = [ordered] @{Area = 'Configuration' Category = 'DNS' Severity = 'Medium' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{DnsNameServersIdentical = @{Enable = $true Name = 'DNS Name servers for primary zone are identical' Parameters = @{Property = 'Status' ExpectedValue = $True OperationType = 'eq' PropertyExtendedValue = 'Comment' } Description = 'DNS Name servers for primary zone should be equal to Domain Controllers for a Domain.' } } } $DNSResolveExternal = @{Enable = $true Scope = 'DC' Source = @{Name = "Resolves external DNS queries" Data = { $Output = Invoke-Command -ComputerName $DomainController -ErrorAction Stop { Resolve-DnsName -Name 'evotec.xyz' -ErrorAction SilentlyContinue } $Output } Details = [ordered] @{Area = 'Health' Category = 'DNS' Severity = 'High' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{ResolveDNSExternal = @{Enable = $true Name = 'Should resolve External DNS' Parameters = @{Property = 'IPAddress' ExpectedValue = '37.59.176.139' OperationType = 'eq' } Description = 'DNS should resolve external queries properly.' } } } $DNSResolveInternal = @{Enable = $true Scope = 'DC' Source = @{Name = "Resolves internal DNS queries" Data = { $Output = Invoke-Command -ComputerName $DomainController -ErrorAction Stop { param([string] $DomainController) $AllDomainControllers = Get-ADDomainController -Identity $DomainController -Server $DomainController $IPs = $AllDomainControllers.IPv4Address | Sort-Object $Output = Resolve-DnsName -Name $DomainController -ErrorAction SilentlyContinue @{'Result' = 'IP Comparison' 'Status' = if ($null -eq (Compare-Object -ReferenceObject $IPs -DifferenceObject ($Output.IP4Address | Sort-Object))) { $true } else { $false } 'IPAddresses' = $Output.IP4Address } } -ArgumentList $DomainController $Output } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{ResolveDNSInternal = @{Enable = $true Name = 'Should resolve Internal DNS' Parameters = @{Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'IPAddresses' } Description = 'DNS should resolve internal domains correctly.' } } } $EventLogs = @{Enable = $true Scope = 'DC' Source = @{Name = "Event Logs" Data = { Get-EventsInformation -LogName 'Application', 'System', 'Security', 'Microsoft-Windows-PowerShell/Operational' -Machine $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{ApplicationLogMode = @{Enable = $true Name = 'Application Log mode is set to AutoBackup' Parameters = @{WhereObject = { $_.LogName -eq 'Application' } Property = 'LogMode' ExpectedValue = 'AutoBackup' OperationType = 'eq' } } ApplicationLogFull = @{Enable = $true Name = 'Application log is not full' Parameters = @{WhereObject = { $_.LogName -eq 'Application' } Property = 'IsLogFull' ExpectedValue = $false OperationType = 'eq' } } PowershellLogMode = @{Enable = $true Name = 'PowerShell Log mode is set to AutoBackup' Parameters = @{WhereObject = { $_.LogName -eq 'Microsoft-Windows-PowerShell/Operational' } Property = 'LogMode' ExpectedValue = 'AutoBackup' OperationType = 'eq' } } PowerShellLogFull = @{Enable = $true Name = 'PowerShell log is not full' Parameters = @{WhereObject = { $_.LogName -eq 'Microsoft-Windows-PowerShell/Operational' } Property = 'IsLogFull' ExpectedValue = $false OperationType = 'eq' } } SystemLogMode = @{Enable = $true Name = 'System Log mode is set to AutoBackup' Parameters = @{WhereObject = { $_.LogName -eq 'System' } Property = 'LogMode' ExpectedValue = 'AutoBackup' OperationType = 'eq' } } SystemLogFull = @{Enable = $true Name = 'System log is not full' Parameters = @{WhereObject = { $_.LogName -eq 'System' } Property = 'IsLogFull' ExpectedValue = $false OperationType = 'eq' } } SecurityLogMode = @{Enable = $true Name = 'Security Log mode is set to AutoBackup' Parameters = @{WhereObject = { $_.LogName -eq 'Security' } Property = 'LogMode' ExpectedValue = 'AutoBackup' OperationType = 'eq' } } SecurityLogFull = @{Enable = $true Name = 'Security log is not full' Parameters = @{WhereObject = { $_.LogName -eq 'Security' } Property = 'IsLogFull' ExpectedValue = $false OperationType = 'eq' } } SecurityMaximumLogSize = @{Enable = $true Name = 'Security Log Maximum Size smaller then 4GB' Parameters = @{WhereObject = { $_.LogName -eq 'Security' } Property = 'FileSizeMaximumMB' ExpectedValue = 4000 OperationType = 'le' } } SecurityCurrentLogSize = @{Enable = $true Name = 'Security Log Current Size smaller then 4GB' Parameters = @{WhereObject = { $_.LogName -eq 'Security' } Property = 'FileSizeCurrentMB' ExpectedValue = 4000 OperationType = 'le' } } SecurityPermissionsDefaultNetworkService = @{Enable = $true Name = 'Security Log has NT AUTHORITY\NETWORK SERVICE with AccessAllowed' Parameters = @{WhereObject = { $_.LogName -eq 'Security' -and $_.SecurityDescriptorDiscretionaryAcl -contains 'NT AUTHORITY\NETWORK SERVICE: AccessAllowed (ListDirectory)' } ExpectedCount = 1 OperationType = 'eq' } } SecurityPermissionsDefaultSYSTEM = @{Enable = $true Name = 'Security Log has NT AUTHORITY\SYSTEM with AccessAllowed' Parameters = @{WhereObject = { $_.LogName -eq 'Security' -and $_.SecurityDescriptorDiscretionaryAcl -contains 'NT AUTHORITY\SYSTEM: AccessAllowed (ChangePermissions, CreateDirectories, Delete, GenericExecute, ListDirectory, ReadPermissions, TakeOwnership)' } ExpectedCount = 1 OperationType = 'eq' } } SecurityPermissionsNDefaultBuiltinAdministrators = @{Enable = $true Name = 'Security Log has BUILTIN\Administrators with AccessAllowed' Parameters = @{WhereObject = { $_.LogName -eq 'Security' -and $_.SecurityDescriptorDiscretionaryAcl -contains 'BUILTIN\Administrators: AccessAllowed (CreateDirectories, ListDirectory)' } ExpectedCount = 1 OperationType = 'eq' } } SecurityPermissionsDefaultBuiltinEventLogReaders = @{Enable = $true Name = 'Security Log has BUILTIN\Event Log Readers with AccessAllowed' Parameters = @{WhereObject = { $_.LogName -eq 'Security' -and $_.SecurityDescriptorDiscretionaryAcl -contains 'BUILTIN\Event Log Readers: AccessAllowed (ListDirectory)' } ExpectedCount = 1 OperationType = 'eq' } } } } $FileSystem = @{Enable = $true Scope = 'DC' Source = @{Name = "FileSystem" Data = { Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Control\FileSystem' -ComputerName $DomainController } Details = [ordered] @{Type = 'Security' Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('') } Requirements = @{CommandAvailable = 'Get-WinADLMSettings' } ExpectedOutput = $true } Tests = [ordered] @{NtfsDisable8dot3NameCreation = @{Enable = $true Name = 'NtfsDisable8dot3NameCreation' Parameters = @{Property = 'NtfsDisable8dot3NameCreation' ExpectedValue = 0 OperationType = 'gt' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://guyrleech.wordpress.com/2014/04/15/ntfs-8-3-short-names-solving-the-issues/' 'https://blogs.technet.microsoft.com/josebda/2012/11/13/windows-server-2012-file-server-tip-disable-8-3-naming-and-strip-those-short-names-too/' 'https://support.microsoft.com/en-us/help/121007/how-to-disable-8-3-file-name-creation-on-ntfs-partitions') } } } } $GroupPolicySYSVOLDC = @{Enable = $true Scope = 'DC' Source = @{Name = "Group Policy SYSVOL Verification" Data = { Get-GPOZaurrSysvol -IncludeDomains $Domain -IncludeDomainControllers $DomainController -VerifyDomainControllers | Where-Object { $_.SysvolStatus -ne 'Exists' -or $_.Status -ne 'Exists' } } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $false } } $Information = @{Enable = $true Scope = 'DC' Source = @{Name = "Domain Controller Information" Data = { Get-ADDomainController -Server $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{IsEnabled = @{Enable = $true Name = 'Is Enabled' Parameters = @{Property = 'Enabled' ExpectedValue = $True OperationType = 'eq' } } IsGlobalCatalog = @{Enable = $true Name = 'Is Global Catalog' Parameters = @{Property = 'IsGlobalCatalog' ExpectedValue = $True OperationType = 'eq' } } } } $LanManagerSettings = @{Enable = $true Scope = 'DC' Source = @{Name = "Lan Manager Settings" Data = { Get-WinADLMSettings -DomainController $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://adsecurity.org/?p=3377') } Requirements = @{CommandAvailable = 'Get-WinADLMSettings' } ExpectedOutput = $true } Tests = [ordered] @{Level = @{Enable = $true Name = 'LM Level' Parameters = @{Property = 'Level' ExpectedValue = 5 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } AuditBaseObjects = @{Enable = $true Name = 'Audit Base Objects' Parameters = @{Property = 'AuditBaseObjects' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-gpac/262a2bed-93d4-4c04-abec-cf06e9ec72fd') } } CrashOnAuditFail = @{Enable = $true Name = 'Crash On Audit Fail' Parameters = @{Property = 'CrashOnAuditFail' ExpectedValue = 0 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('http://systemmanager.ru/win2k_regestry.en/46686.htm') } } EveryoneIncludesAnonymous = @{Enable = $true Name = 'Everyone Includes Anonymous' Parameters = @{Property = 'EveryoneIncludesAnonymous' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Title = 'Disable and Enforce the Setting "Network access: Let Everyone permissions apply to anonymous users"' Area = '' Description = 'This setting helps to prevent an unauthorized user could from anonymously listing account names and shared resources and use using the information to attempt to guess passwords, perform social engineering attacks, or launch DoS attacks.' Resolution = '' Importance = 10 Resources = @('https://www.stigviewer.com/stig/windows_7/2014-04-02/finding/V-3377') } } SecureBoot = @{Enable = $true Name = 'Secure Boot' Parameters = @{Property = 'SecureBoot' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } LSAProtectionCredentials = @{Enable = $true Name = 'LSAProtectionCredentials' Parameters = @{Property = 'LSAProtectionCredentials' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } LimitBlankPasswordUse = @{Enable = $true Name = 'LimitBlankPasswordUse' Parameters = @{Property = 'LimitBlankPasswordUse' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } NoLmHash = @{Enable = $true Name = 'NoLmHash' Parameters = @{Property = 'NoLmHash' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } DisableDomainCreds = @{Enable = $true Name = 'DisableDomainCreds' Parameters = @{Property = 'DisableDomainCreds' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://www.stigviewer.com/stig/windows_8/2014-01-07/finding/V-3376') } } ForceGuest = @{Enable = $true Name = 'ForceGuest' Parameters = @{Property = 'ForceGuest' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } } } $LanManServer = @{Enable = $true Scope = 'DC' Source = @{Name = "Lan Man Server" Data = { Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\LanManServer\Parameters' -ComputerName $DomainController } Details = [ordered] @{Area = 'Security' Description = 'Lan Man Server' Resolution = '' Importance = 10 Resources = @() } Requirements = @{CommandAvailable = 'Get-PSRegistry' } ExpectedOutput = $true } Tests = [ordered] @{DisableCompression = @{Enable = $false Name = 'Disable Compression SMBv3' Parameters = @{Property = 'DisableCompression' ExpectedValue = 1 OperationType = 'eq' } Details = [ordered] @{Area = 'Security' Description = 'Microsoft is aware of a remote code execution vulnerability in the way that the Microsoft Server Message Block 3.1.1 (SMBv3) protocol handles certain requests. An attacker who successfully exploited the vulnerability could gain the ability to execute code on the target SMB Server or SMB Client. To exploit the vulnerability against an SMB Server, an unauthenticated attacker could send a specially crafted packet to a targeted SMBv3 Server. To exploit the vulnerability against an SMB Client, an unauthenticated attacker would need to configure a malicious SMBv3 Server and convince a user to connect to it.' Resolution = 'Disable SMBv3 compression or apply patch. Since patch is available disabling is not nessecary.' Importance = 10 Resources = @('https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/adv200005') } } EnableForcedLogoff = @{Enable = $true Name = 'Enable Forced Logoff' Parameters = @{Property = 'EnableForcedLogoff' ExpectedValue = 1 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Users are not forcibly disconnected when logon hours expire.' Resolution = '' Importance = 10 Resources = @('https://www.stigviewer.com/stig/windows_7/2012-07-02/finding/V-1136') } } EnableSecuritySignature = @{Enable = $true Name = 'Enable Security Signature' Parameters = @{Property = 'EnableSecuritySignature' ExpectedValue = 1 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Microsoft network server: Digitally sign communications (if client agrees)' Resolution = '' Importance = 10 Resources = @('https://support.microsoft.com/en-us/help/887429/overview-of-server-message-block-signing' 'https://community.spiceworks.com/topic/2131862-how-to-set-microsoft-network-server-digitally-sign-communications-always' 'https://www.stigviewer.com/stig/windows_server_2016/2017-11-20/finding/V-73663') } } RequireSecuritySignature = @{Enable = $true Name = 'Require Security Signature' Parameters = @{Property = 'RequireSecuritySignature' ExpectedValue = 1 OperationType = 'eq' } Details = [ordered] @{Type = 'Security' Area = '' Description = 'Microsoft network server: Digitally sign communications (always)' Vulnerability = 'Session hijacking uses tools that allow attackers who have access to the same network as the client computer or server to interrupt, end, or steal a session in progress. Attackers can potentially intercept and modify unsigned Server Message Block (SMB) packets and then modify the traffic and forward it so that the server might perform objectionable actions. Alternatively, the attacker could pose as the server or client after legitimate authentication and gain unauthorized access to data. SMB is the resource-sharing protocol that is supported by many Windows operating systems. It is the basis of NetBIOS and many other protocols. SMB signatures authenticate both users and the servers that host the data. If either side fails the authentication process, data transmission does not take place.' PotentialImpact = 'The Windows implementation of the SMB file and print-sharing protocol support mutual authentication, which prevents session hijacking attacks and supports message authentication to prevent man-in-the-middle attacks. SMB signing provides this authentication by placing a digital signature into each SMB, which is then verified by both the client computer and the server. Implementing SMB signing may negatively affect performance because each packet must be signed and verified. If these policy settings are enabled on a server that is performing multiple roles, such as a small business server that is serving as a domain controller, file server, print server, and application server, performance may be substantially slowed. Additionally, if you configure computers to ignore all unsigned SMB communications, older applications and operating systems cannot connect. However, if you completely disable all SMB signing, computers are vulnerable to session-hijacking attacks.' Resolution = '' Importance = 10 Resources = @('https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/hh125918(v=ws.10)?redirectedfrom=MSDN#vulnerability' 'https://support.microsoft.com/en-us/help/887429/overview-of-server-message-block-signing' 'https://community.spiceworks.com/topic/2131862-how-to-set-microsoft-network-server-digitally-sign-communications-always') } } } } $LDAP = @{Enable = $false Scope = 'DC' Source = @{Name = 'LDAP Connectivity' Data = { Test-LDAP -ComputerName $DomainController -WarningAction SilentlyContinue -VerifyCertificate } Details = [ordered] @{Category = 'Health' Description = '' Resolution = '' Importance = 0 ActionType = 0 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{PortLDAP = @{Enable = $true Name = 'LDAP Port is Available' Parameters = @{Property = 'LDAP' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } PortLDAPS = @{Enable = $true Name = 'LDAP SSL Port is Available' Parameters = @{Property = 'LDAPS' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } PortLDAP_GC = @{Enable = $true Name = 'LDAP GC Port is Available' Parameters = @{Property = 'GlobalCatalogLDAP' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } PortLDAPS_GC = @{Enable = $true Name = 'LDAP SSL GC Port is Available' Parameters = @{Property = 'GlobalCatalogLDAPS' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } BindLDAPS = @{Enable = $true Name = 'LDAP SSL Bind available' Parameters = @{Property = 'LDAPSBind' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } BindLDAPS_GC = @{Enable = $true Name = 'LDAP SSL GC Bind is Available' Parameters = @{Property = 'GlobalCatalogLDAPSBind' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } X509NotBefore = @{Enable = $true Name = 'Not Before Date should be within required range' Parameters = @{Property = 'X509NotBefore' ExpectedValue = Get-Date OperationType = 'lt' } Details = [ordered] @{Category = 'Health' Importance = 0 ActionType = 0 } } X509NotAfter = @{Enable = $true Name = 'Not After Date should be within required range' Parameters = @{Property = 'X509NotAfter' ExpectedValue = Get-Date OperationType = 'gt' } Details = [ordered] @{Category = 'Health' Importance = 0 ActionType = 0 } } X509NotBeforeDays = @{Enable = $true Name = 'Not Before Days should be less/equal 0' Parameters = @{Property = 'X509NotBeforeDays' ExpectedValue = 0 OperationType = 'le' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } X509NotAfterDaysWarning = @{Enable = $true Name = 'Not After Days should be more than 10 days' Parameters = @{Property = 'X509NotAfterDays' ExpectedValue = 10 OperationType = 'gt' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 1 } } X509NotAfterDaysCritical = @{Enable = $true Name = 'Not After Days should be more than 0 days' Parameters = @{Property = 'X509NotAfterDays' ExpectedValue = 0 OperationType = 'gt' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 } } } } $LDAPInsecureBindings = @{Enable = $true Scope = 'DC' Source = @{Name = 'LDAP Insecure Bindings' Data = { Get-WinADLDAPBindingsSummary -IncludeDomainControllers $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = 'Security' Category = 'LDAP' Description = 'LDAP channel binding and LDAP signing provide ways to increase the security of network communications between an Active Directory Domain Services (AD DS) or an Active Directory Lightweight Directory Services (AD LDS) and its clients. There is a vulerability in the default configuration for Lightweight Directory Access Protocol (LDAP) channel binding and LDAP signing and may expose Active directory domain controllers to elevation of privilege vulnerabilities. Microsoft Security Advisory ADV190023 address the issue by recommending the administrators enable LDAP channel binding and LDAP signing on Active Directory Domain Controllers. This hardening must be done manually until the release of the security update that will enable these settings by default.' Resolution = 'Make sure to remove any Clients performing simple or unsigned bindings.' Importance = 10 Resources = @('https://docs.microsoft.com/en-us/archive/blogs/russellt/identifying-clear-text-ldap-binds-to-your-dcs' 'https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirement-for-windows') } ExpectedOutput = $false } Tests = [ordered] @{SimpleBinds = @{Enable = $true Name = 'Simple binds performed without SSL/TLS is 0' Parameters = @{Property = 'Number of simple binds performed without SSL/TLS' ExpectedValue = 0 OperationType = 'eq' } } UnsignedBinds = @{Enable = $true Name = 'Negotiate/Kerberos/NTLM/Digest binds performed without signing is 0' Parameters = @{Property = 'Number of Negotiate/Kerberos/NTLM/Digest binds performed without signing' ExpectedValue = 0 OperationType = 'eq' } } } } $MSSLegacy = @{Enable = $true Scope = 'DC' Source = @{Name = "MSS (Legacy)" Data = { Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -ComputerName $DomainController } Details = [ordered] @{Area = 'Security' Category = 'Network' Description = 'Provides verification of MSS Network Settings on Domain Controllers' Resolution = '' Importance = 10 Resources = @('https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/') } Requirements = @{CommandAvailable = 'Get-PSRegistry' } ExpectedOutput = $true } Tests = [ordered] @{DisableIPSourceRouting = @{Enable = $true Name = 'DisableIPSourceRouting' Parameters = @{Property = 'DisableIPSourceRouting' ExpectedValue = 2 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Highest protection, source routing is completely disabled' Resolution = '' Importance = 10 Resources = @('https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/') } } EnableICMPRedirect = @{Enable = $true Name = 'EnableICMPRedirect' Parameters = @{Property = 'EnableICMPRedirect' ExpectedValue = 0 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/') } } } } $NetSessionEnumeration = @{Enable = $true Scope = 'DC' Source = @{Name = "Net Session Enumeration" Data = { $Registry = Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\DefaultSecurity" -ComputerName $DomainController $CSD = [System.Security.AccessControl.CommonSecurityDescriptor]::new($true, $false, $Registry.SrvsvcSessionInfo, 0) $CSD.DiscretionaryAcl.SecurityIdentifier | Where-Object { $_ -eq 'S-1-5-11' } } Details = [ordered] @{Type = 'Security' Area = '' Description = 'Net Session Enumeration is a method used to retrieve information about established sessions on a server. Any domain user can query a server for its established sessions.' Resolution = 'Hardening Net Session Enumeration' Importance = 10 Resources = @('https://gallery.technet.microsoft.com/Net-Cease-Blocking-Net-1e8dcb5b') } Requirements = @{CommandAvailable = 'Get-PSRegistry' } ExpectedOutput = $false } } $NetworkCardSettings = @{Enable = $true Scope = 'DC' Source = @{Name = "Get all network interfaces and firewall status" Data = { Get-ComputerNetwork -ComputerName $DomainController } Details = [ordered] @{Area = 'Connectivity' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{NETBIOSOverTCIP = @{Enable = $true Name = 'NetBIOS over TCIP should be disabled.' Parameters = @{Property = 'NetBIOSOverTCPIP' ExpectedValue = 'Disabled' OperationType = 'eq' } Details = @{Area = 'Connectivity' Category = 'Legacy Protocols' Severity = 'Critical' Importance = 90 Description = @' NetBIOS over TCP/IP is a networking protocol that allows legacy computer applications relying on the NetBIOS to be used on modern TCP/IP networks. Enabling NetBios might help an attackers access shared directories, files and also gain sensitive information such as computer name, domain, or workgroup. '@ Resolution = 'Disable NetBIOS over TCPIP' Resources = @('http://woshub.com/how-to-disable-netbios-over-tcpip-and-llmnr-using-gpo/') } } Loopbackpresent = @{Enable = $true Name = 'Loopback IP address should be list in DNS servers on network card' Parameters = @{Property = 'DNSServerSearchOrder' ExpectedValue = '127.0.0.1' OperationType = 'Contains' } } WindowsFirewall = @{Enable = $true Name = 'Windows Firewall should be enabled on network card' Parameters = @{Property = 'FirewallStatus' ExpectedValue = $true OperationType = 'eq' } } WindowsFirewallProfile = @{Enable = $true Name = 'Windows Firewall should be set on domain network profile' Parameters = @{Property = 'FirewallProfile' ExpectedValue = 'DomainAuthenticated' OperationType = 'eq' } } DHCPDisabled = @{Enable = $false Name = 'DHCP should be disabled on network card' Parameters = @{Property = 'DHCPEnabled' ExpectedValue = $false OperationType = 'eq' } } } } $NTDSParameters = @{Enable = $true Scope = 'DC' Source = @{Name = "NTDS Parameters" Data = { Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{DsaNotWritable = @{Enable = $true Name = 'Domain Controller should be writeable' Parameters = @{Property = 'Dsa Not Writable' ExpectedOutput = $false } } } } $OperatingSystem = @{Enable = $true Scope = 'DC' Source = @{Name = 'Operating System' Data = { Get-ComputerOperatingSystem -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{OperatingSystem = @{Enable = $true Name = 'Operating system Windows Server 2012 and up' Parameters = @{Property = 'OperatingSystem' ExpectedValue = @('Microsoft Windows Server 2019*', 'Microsoft Windows Server 2016*', 'Microsoft Windows Server 2012*') OperationType = 'like' OperationResult = 'OR' PropertyExtendedValue = 'OperatingSystem' } } } } $Pingable = @{Enable = $true Scope = 'DC' Source = @{Name = 'Ping Connectivity' Data = { Test-NetConnection -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{Ping = @{Enable = $true Name = 'Responding to PING' Parameters = @{Property = 'PingSucceeded' PropertyExtendedValue = 'PingReplyDetails', 'RoundtripTime' ExpectedValue = $true OperationType = 'eq' } } } } $Ports = [ordered] @{Enable = $true Scope = 'DC' Source = [ordered] @{Name = 'TCP Ports are open/closed as required' Data = { $TcpPorts = @(53, 88, 135, 139, 389, 445, 464, 636, 3268, 3269, 9389) Test-ComputerPort -ComputerName $DomainController -PortTCP $TcpPorts -WarningAction SilentlyContinue } Requirements = @{CommandAvailable = 'Test-NetConnection' } Details = [ordered] @{Area = '' Category = '' Severity = '' Importance = 0 Description = '' Resolution = '' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{Port53 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '53' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port88 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '88' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port135 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '135' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port139 = [ordered] @{Enable = $true Name = 'Port is CLOSED' Parameters = @{WhereObject = { $_.Port -eq '139' } Property = 'Status' ExpectedValue = $false OperationType = 'eq' PropertyExtendedValue = 'Summary' } Details = [ordered] @{Area = '' Category = '' Severity = '' Importance = 0 Description = @' NetBIOS over TCP/IP is a networking protocol that allows legacy computer applications relying on the NetBIOS to be used on modern TCP/IP networks. Enabling NetBios might help an attackers access shared directories, files and also gain sensitive information such as computer name, domain, or workgroup. '@ Resolution = 'Disable NETBIOS over TCPIP' Resources = @('http://woshub.com/how-to-disable-netbios-over-tcpip-and-llmnr-using-gpo/') } } Port445 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '445' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port464 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '464' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port636 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '636' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port3268 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '3268' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port3269 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '3269' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } Port9389 = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{WhereObject = { $_.Port -eq '9389' } Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } } } $RDPPorts = [ordered] @{Enable = $false Scope = 'DC' Source = [ordered] @{Name = 'RDP Port is open' Data = { Test-ComputerPort -ComputerName $DomainController -PortTCP 3389 -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{PortOpen = [ordered] @{Enable = $false Name = 'Port is OPEN' Parameters = @{Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } } } $RDPSecurity = [ordered] @{Enable = $true Scope = 'DC' Source = [ordered] @{Name = 'RDP Security' Data = { Get-ComputerRDP -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = 'Connectivity' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{PortOpen = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{Property = 'Connectivity' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'ConnectivitySummary' } Details = [ordered] @{Area = 'Connectivity' Description = '' Resolution = '' Importance = 10 Resources = @('https://lazywinadmin.com/2014/04/powershell-getset-network-level.html' 'https://devblogs.microsoft.com/scripting/weekend-scripter-report-on-network-level-authentication/') } } NLAAuthenticationEnabled = [ordered] @{Enable = $true Name = 'NLA Authentication is Enabled' Parameters = @{Property = 'UserAuthenticationRequired' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = 'Connectivity' Description = '' Resolution = '' Importance = 10 Resources = @('https://lazywinadmin.com/2014/04/powershell-getset-network-level.html' 'https://devblogs.microsoft.com/scripting/weekend-scripter-report-on-network-level-authentication/') } } MinimalEncryptionLevel = [ordered] @{Enable = $true Name = 'Minimal Encryption Level is set to at least High' Parameters = @{Property = 'MinimalEncryptionLevelValue' ExpectedValue = 3 OperationType = 'ge' PropertyExtendedValue = 'MinimalEncryptionLevel' } Details = [ordered] @{Area = 'Connectivity' Description = 'Remote connections must be encrypted to prevent interception of data or sensitive information. Selecting "High Level" will ensure encryption of Remote Desktop Services sessions in both directions.' Resolution = '' Importance = 10 Resources = @('https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-3454') } } } } $Services = [ordered] @{Enable = $true Scope = 'DC' Source = @{Name = 'Service Status' Data = { $Services = @('ADWS', 'DNS', 'DFS', 'DFSR', 'Eventlog', 'EventSystem', 'KDC', 'LanManWorkstation', 'LanManServer', 'NetLogon', 'NTDS', 'RPCSS', 'SAMSS', 'Spooler', 'W32Time', 'XblGameSave', 'XblAuthManager') Get-PSService -Computers $DomainController -Services $Services } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{ADWSServiceStatus = @{Enable = $true Name = 'ADWS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'ADWS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } ADWSServiceStartType = @{Enable = $true Name = 'ADWS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'ADWS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } DNSServiceStatus = @{Enable = $true Name = 'DNS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'DNS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } DNSServiceStartType = @{Enable = $true Name = 'DNS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'DNS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } DFSServiceStatus = @{Enable = $true Name = 'DFS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'DFS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } DFSServiceStartType = @{Enable = $true Name = 'DFS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'DFS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } DFSRServiceStatus = @{Enable = $true Name = 'DFSR Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'DFSR' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } DFSRServiceStartType = @{Enable = $true Name = 'DFSR Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'DFSR' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } EventlogServiceStatus = @{Enable = $true Name = 'Eventlog Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'Eventlog' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } EventlogServiceStartType = @{Enable = $true Name = 'Eventlog Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'Eventlog' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } EventSystemServiceStatus = @{Enable = $true Name = 'EventSystem Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'EventSystem' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } EventSystemServiceStartType = @{Enable = $true Name = 'EventSystem Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'EventSystem' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } KDCServiceStatus = @{Enable = $true Name = 'KDC Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'KDC' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } KDCServiceStartType = @{Enable = $true Name = 'KDC Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'KDC' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } LanManWorkstationServiceStatus = @{Enable = $true Name = 'LanManWorkstation Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'LanManWorkstation' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } LanManWorkstationServiceStartType = @{Enable = $true Name = 'LanManWorkstation Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'LanManWorkstation' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } LanManServerServiceStatus = @{Enable = $true Name = 'LanManServer Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'LanManServer' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } LanManServerServiceStartType = @{Enable = $true Name = 'LanManServer Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'LanManServer' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } NetLogonServiceStatus = @{Enable = $true Name = 'NetLogon Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'NetLogon' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } NetLogonServiceStartType = @{Enable = $true Name = 'NetLogon Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'NetLogon' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } NTDSServiceStatus = @{Enable = $true Name = 'NTDS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'NTDS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } NTDSServiceStartType = @{Enable = $true Name = 'NTDS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'NTDS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } RPCSSServiceStatus = @{Enable = $true Name = 'RPCSS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'RPCSS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } RPCSSServiceStartType = @{Enable = $true Name = 'RPCSS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'RPCSS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } SAMSSServiceStatus = @{Enable = $true Name = 'SAMSS Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'SAMSS' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } SAMSSServiceStartType = @{Enable = $true Name = 'SAMSS Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'SAMSS' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } SpoolerServiceStatus = @{Enable = $true Name = 'Spooler Service is STOPPED' Parameters = @{WhereObject = { $_.Name -eq 'Spooler' } Property = 'Status' ExpectedValue = 'Stopped' OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{Area = 'Security' Category = 'Services' Severity = '' Importance = 0 Description = 'Due to security concerns SPOOLER should be disabled and stopped. However in some cases it may be required to have SPOOLER service up and running to cleanup stale printer objects from AD.' Resolution = '' Resources = @('https://adsecurity.org/?p=4056' 'https://docs.microsoft.com/en-us/windows-server/security/windows-services/security-guidelines-for-disabling-system-services-in-windows-server#print-spooler') } } SpoolerServiceStartType = @{Enable = $true Name = 'Spooler Service START TYPE is DISABLED' Parameters = @{WhereObject = { $_.Name -eq 'Spooler' } Property = 'StartType' ExpectedValue = 'Disabled' OperationType = 'eq' ExpectedOutput = $false } Details = [ordered] @{Area = 'Security' Category = 'Services' Severity = '' Importance = 0 Description = 'Due to security concerns SPOOLER should be disabled and stopped. However in some cases it may be required to have SPOOLER service up and running to cleanup stale printer objects from AD.' Resolution = '' Resources = @('https://adsecurity.org/?p=4056' 'https://docs.microsoft.com/en-us/windows-server/security/windows-services/security-guidelines-for-disabling-system-services-in-windows-server#print-spooler') } } W32TimeServiceStatus = @{Enable = $true Name = 'W32Time Service is RUNNING' Parameters = @{WhereObject = { $_.Name -eq 'W32Time' } Property = 'Status' ExpectedValue = 'Running' OperationType = 'eq' } } W32TimeServiceStartType = @{Enable = $true Name = 'W32Time Service START TYPE is Automatic' Parameters = @{WhereObject = { $_.Name -eq 'W32Time' } Property = 'StartType' ExpectedValue = 'Auto' OperationType = 'eq' } } XblAuthManagerServiceStatus = @{Enable = $true Name = 'XblAuthManager Service is STOPPED' Parameters = @{WhereObject = { $_.Name -eq 'XblAuthManager' } Property = 'Status' ExpectedValue = 'Stopped', 'N/A' OperationType = 'in' ExpectedOutput = $false } } XblAuthManagerStartupType = @{Enable = $true Name = 'XblAuthManager Service START TYPE is Disabled' Parameters = @{WhereObject = { $_.Name -eq 'XblAuthManager' } Property = 'StartType' ExpectedValue = 'Disabled', 'N/A' OperationType = 'in' ExpectedOutput = $false } } XblGameSaveServiceStatus = @{Enable = $true Name = 'XblGameSave Service is STOPPED' Parameters = @{WhereObject = { $_.Name -eq 'XblGameSave' } Property = 'Status' ExpectedValue = 'Stopped', 'N/A' OperationType = 'in' ExpectedOutput = $false } } XblGameSaveStartupType = @{Enable = $true Name = 'XblGameSave Service START TYPE is Disabled' Parameters = @{WhereObject = { $_.Name -eq 'XblGameSave' } Property = 'StartType' ExpectedValue = 'Disabled', 'N/A' OperationType = 'in' ExpectedOutput = $false } } } } $ServiceWINRM = @{Enable = $true Scope = 'DC' Source = @{Name = "Service WINRM" Data = { Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service' -ComputerName $DomainController } Details = [ordered] @{Type = 'Security' Area = '' Description = 'Storage of administrative credentials could allow unauthorized access. Disallowing the storage of RunAs credentials for Windows Remote Management will prevent them from being used with plug-ins. The Windows Remote Management (WinRM) service must not store RunAs credentials.' Resolution = '' Importance = 10 Resources = @() } Requirements = @{CommandAvailable = 'Get-PSRegistry' } ExpectedOutput = $true } Tests = [ordered] @{DisableRunAs = @{Enable = $true Name = 'DisableRunAs' Parameters = @{Property = 'DisableRunAs' ExpectedValue = 1 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Storage of administrative credentials could allow unauthorized access. Disallowing the storage of RunAs credentials for Windows Remote Management will prevent them from being used with plug-ins. The Windows Remote Management (WinRM) service must not store RunAs credentials.' Resolution = '' Importance = 10 Resources = @('https://www.stigviewer.com/stig/windows_server_2016/2018-03-07/finding/V-73603') } } } } $SMBProtocols = @{Enable = $true Scope = 'DC' Source = @{Name = 'SMB Protocols' Data = { Get-ComputerSMB -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @('https://community.spiceworks.com/topic/2153374-bpa-on-windows-server-2016-warns-about-smb-not-in-a-default-configuration') } Requirements = @{CommandAvailable = 'Get-ComputerSMB' } ExpectedOutput = $true } Tests = [ordered] @{AsynchronousCredits = @{Enable = $true Name = 'AsynchronousCredits' Parameters = @{Property = 'AsynchronousCredits' ExpectedValue = 64 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'AsynchronousCredits should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } AutoDisconnectTimeout = @{Enable = $true Name = 'AutoDisconnectTimeout' Parameters = @{Property = 'AutoDisconnectTimeout' ExpectedValue = 0 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'AutoDisconnectTimeout should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } CachedOpenLimit = @{Enable = $true Name = 'CachedOpenLimit' Parameters = @{Property = 'CachedOpenLimit' ExpectedValue = 5 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'CachedOpenLimit should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } DurableHandleV2TimeoutInSeconds = @{Enable = $true Name = 'DurableHandleV2TimeoutInSeconds' Parameters = @{Property = 'DurableHandleV2TimeoutInSeconds' ExpectedValue = 30 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'DurableHandleV2TimeoutInSeconds should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } EnableSMB1Protocol = @{Enable = $true Name = 'SMB v1 Protocol should be disabled' Parameters = @{Property = 'EnableSMB1Protocol' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } EnableSMB2Protocol = @{Enable = $true Name = 'SMB v2 Protocol should be enabled' Parameters = @{Property = 'EnableSMB2Protocol' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } MaxThreadsPerQueue = @{Enable = $true Name = 'MaxThreadsPerQueue' Parameters = @{Property = 'MaxThreadsPerQueue' ExpectedValue = 20 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'MaxThreadsPerQueue should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } Smb2CreditsMin = @{Enable = $true Name = 'Smb2CreditsMin' Parameters = @{Property = 'Smb2CreditsMin' ExpectedValue = 128 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Smb2CreditsMin should have the recommended value' Resolution = '' Importance = 10 Resources = @() } } Smb2CreditsMax = @{Enable = $true Name = 'Smb2CreditsMax' Parameters = @{Property = 'Smb2CreditsMax' ExpectedValue = 2048 OperationType = 'eq' } Details = [ordered] @{Area = '' Description = 'Smb2CreditsMax should have the recommended value' Resolution = '' Importance = 10 Resources = @('https://github.com/EvotecIT/Testimo/issues/50') } } RequireSecuritySignature = @{Enable = $true Name = 'SMB v2 Require Security Signature' Parameters = @{Property = 'RequireSecuritySignature' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } } } } $SMBShares = @{Enable = $true Scope = 'DC' Source = @{Name = 'Default SMB Shares' Data = { Get-ComputerSMBShare -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } Requirements = @{CommandAvailable = 'Get-ComputerSMBShare' } ExpectedOutput = $true } Tests = [ordered] @{AdminShare = @{Enable = $true Name = 'Remote Admin Share is available' Parameters = @{WhereObject = { $_.Name -eq 'ADMIN$' } ExpectedCount = 1 PropertyExtendedValue = 'Path' } } DefaultShare = @{Enable = $true Name = 'Default Share is available' Parameters = @{WhereObject = { $_.Name -eq 'C$' } ExpectedCount = 1 PropertyExtendedValue = 'Path' } } RemoteIPC = @{Enable = $true Name = 'Remote IPC Share is available' Parameters = @{WhereObject = { $_.Name -eq 'IPC$' } ExpectedCount = 1 PropertyExtendedValue = 'Path' } } NETLOGON = @{Enable = $true Name = 'NETLOGON Share is available' Parameters = @{WhereObject = { $_.Name -eq 'NETLOGON' } ExpectedCount = 1 PropertyExtendedValue = 'Path' } } SYSVOL = @{Enable = $true Name = 'SYSVOL Share is available' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' } ExpectedCount = 1 PropertyExtendedValue = 'Path' } } } } $SMBSharesPermissions = @{Enable = $true Scope = 'DC' Source = @{Name = 'Default SMB Shares Permissions' Data = { Get-ComputerSMBSharePermissions -ComputerName $DomainController -ShareName 'Netlogon', 'Sysvol' } Details = [ordered] @{Area = 'Security' Description = "SMB Shares for Sysvol and Netlogon should be at their defaults. That means 2 permissions for Netlogon and 3 for SysVol." Resolution = 'Add/Remove unnecessary permissions.' Importance = 20 Resources = @() } Requirements = @{CommandAvailable = 'Get-ComputerSMBSharePermissions' } ExpectedOutput = $true } Tests = [ordered] @{OverallCount = @{Enable = $true Name = 'Should only have default number of permissions' Parameters = @{ExpectedCount = 5 } Details = [ordered] @{Area = 'Security' Description = "SMB Shares for Sysvol and Netlogon should be at their defaults. That means 2 permissions for Netlogon and 3 for SysVol." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } } NetlogonEveryone = @{Enable = $true Name = 'Netlogon Share Permissions - Everyone' Parameters = @{WhereObject = { $_.Name -eq 'NETLOGON' -and $_.AccountName -eq 'Everyone' } ExpectedCount = 1 } Area = 'Security' Description = "SMB Shares for NETLOGON should contain Everyone with Read access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } NetlogonAdministrators = @{Enable = $true Name = 'Netlogon Share Permissions - BUILTIN\Administrators' Parameters = @{WhereObject = { $_.Name -eq 'NETLOGON' -and $_.AccountName -eq 'BUILTIN\Administrators' } ExpectedCount = 1 } Area = 'Security' Description = "SMB Shares for NETLOGON should contain BUILTIN\Administrators with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolEveryone = @{Enable = $true Name = 'SysVol Share Permissions - Everyone' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'Everyone' } ExpectedCount = 1 } Area = 'Security' Description = "SMB Shares for SYSVOL should contain Everyone with Read access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolAdministrators = @{Enable = $true Name = 'SysVol Share Permissions - BUILTIN\Administrators' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'BUILTIN\Administrators' } ExpectedCount = 1 } Area = 'Security' Description = "SMB Shares for SYSVOL should contain BUILTIN\Administrators with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolAuthenticatedUsers = @{Enable = $true Name = 'SysVol Share Permissions - NT AUTHORITY\Authenticated Users' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'NT AUTHORITY\Authenticated Users' } ExpectedCount = 1 } Area = 'Security' Description = "SMB Shares for SYSVOL should contain NT AUTHORITY\Authenticated Users with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } NetlogonEveryoneValue = @{Enable = $true Name = 'Netlogon Share Permissions Value - Everyone' Parameters = @{WhereObject = { $_.Name -eq 'NETLOGON' -and $_.AccountName -eq 'Everyone' } Property = 'AccessRight' ExpectedValue = 'Read' OperationType = 'eq' } Area = 'Security' Description = "SMB Shares for NETLOGON should contain Everyone with Read access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } NetlogonAdministratorsValue = @{Enable = $true Name = 'Netlogon Share Permissions Value - BUILTIN\Administrators' Parameters = @{WhereObject = { $_.Name -eq 'NETLOGON' -and $_.AccountName -eq 'BUILTIN\Administrators' } Property = 'AccessRight' ExpectedValue = 'Full' OperationType = 'eq' } Area = 'Security' Description = "SMB Shares for NETLOGON should contain BUILTIN\Administrators with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolEveryoneValue = @{Enable = $true Name = 'SysVol Share Permissions Value - Everyone' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'Everyone' } Property = 'AccessRight' ExpectedValue = 'Read' OperationType = 'eq' } Area = 'Security' Description = "SMB Shares for SYSVOL should contain Everyone with Read access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolAdministratorsValue = @{Enable = $true Name = 'SysVol Share Permissions Value - BUILTIN\Administrators' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'BUILTIN\Administrators' } Property = 'AccessRight' ExpectedValue = 'Full' OperationType = 'eq' } Area = 'Security' Description = "SMB Shares for SYSVOL should contain BUILTIN\Administrators with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } SysvolAuthenticatedUsersValue = @{Enable = $true Name = 'SysVol Share Permissions Value - NT AUTHORITY\Authenticated Users' Parameters = @{WhereObject = { $_.Name -eq 'SYSVOL' -and $_.AccountName -eq 'NT AUTHORITY\Authenticated Users' } Property = 'AccessRight' ExpectedValue = 'Full' OperationType = 'eq' } Area = 'Security' Description = "SMB Shares for SYSVOL should contain NT AUTHORITY\Authenticated Users with Full access rights." Resolution = 'Add/Remove unnecessary permissions.' Importance = 5 Resources = @() } } } $TimeSettings = [ordered] @{Enable = $true Scope = 'DC' Source = @{Name = "Time Settings" Data = { Get-TimeSetttings -ComputerName $DomainController -Domain $Domain } Details = [ordered] @{Area = 'Configuration' Description = '' Resolution = '' Importance = 2 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{NTPServerEnabled = @{Enable = $true Name = 'NtpServer must be enabled.' Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpServerEnabled' ExpectedValue = $true OperationType = 'eq' } } NTPServerIntervalMissing = @{Enable = $true Name = 'Ntp Server Interval should be set' Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpServerIntervals' ExpectedValue = 'Missing' OperationType = 'notcontains' } } NTPServerIntervalIncorrect = @{Enable = $true Name = 'Ntp Server Interval should be within known settings' Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpServerIntervals' ExpectedValue = 'Incorrect' OperationType = 'notcontains' } } VMTimeProvider = @{Enable = $true Name = 'Virtual Machine Time Provider should be disabled.' Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'VMTimeProvider' ExpectedValue = $false OperationType = 'eq' } } NtpTypeNonPDC = [ordered] @{Enable = $true Name = 'NTP Server should be set to Domain Hierarchy' Requirements = @{IsPDC = $false } Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpType' ExpectedValue = 'NT5DS' OperationType = 'eq' } } NtpTypePDC = [ordered] @{Enable = $true Name = 'NTP Server should be set to NTP' Requirements = @{IsPDC = $true } Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpType' ExpectedValue = 'NTP' OperationType = 'eq' } } } } $TimeSynchronizationExternal = @{Enable = $true Scope = 'DC' Source = @{Name = "Time Synchronization External" Data = { Get-ComputerTime -TimeTarget $DomainController -WarningAction SilentlyContinue -TimeSource $TimeSource } Parameters = @{TimeSource = 'pool.ntp.org' } Details = [ordered] @{Area = '' Category = 'Configuration' Description = '' Resolution = '' Importance = 2 Resources = @('[How to: Fix Time Sync in your Domain](https://community.spiceworks.com/how_to/166215-fix-time-sync-in-your-domain-use-w32time)' '[Windows Time Settings in a Domain](https://www.concurrency.com/blog/october-2018/windows-time-settings-in-a-domain)') } ExpectedOutput = $true } Tests = [ordered] @{TimeSynchronizationTest = @{Enable = $true Name = 'Time Difference' Details = [ordered] @{Area = '' Category = 'Configuration' Description = '' Importance = 2 Resources = @() } Parameters = @{Property = 'TimeDifferenceSeconds' ExpectedValue = 1 OperationType = 'le' PropertyExtendedValue = 'TimeDifferenceSeconds' } } } MicrosoftMaterials = 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc773263(v=ws.10)#w2k3tr_times_tools_uhlp' } $TimeSynchronizationInternal = @{Enable = $true Scope = 'DC' Source = @{Name = "Time Synchronization Internal" Data = { Get-ComputerTime -TimeTarget $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Category = 'Configuration' Area = '' Description = '' Resolution = '' Importance = 2 Resources = @('https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc773263(v=ws.10)#w2k3tr_times_tools_uhlp') } ExpectedOutput = $true } Tests = [ordered] @{LastBootUpTime = @{Enable = $true Name = 'Last Boot Up time should be less than X days' Parameters = @{Property = 'LastBootUpTime' ExpectedValue = '(Get-Date).AddDays(-60)' OperationType = 'gt' } } TimeSynchronizationTest = @{Enable = $true Name = 'Time Difference' Parameters = @{Property = 'TimeDifferenceSeconds' ExpectedValue = 1 OperationType = 'le' PropertyExtendedValue = 'TimeDifferenceSeconds' } } } } $UNCHardenedPaths = @{Enable = $true Scope = 'DC' Source = @{Name = "Hardened UNC Paths" Data = { Get-PSRegistry -RegistryPath "HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths" -ComputerName $DomainController } Details = [ordered] @{Type = 'Security' Area = '' Description = 'Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares.' Resolution = 'Harden UNC Paths for SYSVOL and NETLOGON' Importance = 10 Resources = @('https://docs.microsoft.com/en-us/archive/blogs/leesteve/demystifying-the-unc-hardening-dilemma' 'https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63577' 'https://support.microsoft.com/en-us/help/3000483/ms15-011-vulnerability-in-group-policy-could-allow-remote-code-executi') } Requirements = @{CommandAvailable = 'Get-PSRegistry' } Implementation = {} Rollback = { Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths" -Name "*" } ExpectedOutput = $true } Tests = [ordered] @{NetLogonUNCPath = @{Enable = $true Name = 'Netlogon UNC Hardening' Parameters = @{Property = '\\*\NETLOGON' ExpectedValue = 'RequireMutualAuthentication=1, RequireIntegrity=1', 'RequireMutualAuthentication=1,RequireIntegrity=1' OperationType = 'in' } Description = "Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares." } SysVolUNCPath = @{Enable = $true Name = 'SysVol UNC Hardening' Parameters = @{Property = '\\*\SYSVOL' ExpectedValue = 'RequireMutualAuthentication=1, RequireIntegrity=1', 'RequireMutualAuthentication=1,RequireIntegrity=1' OperationType = 'in' } Description = "Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares." } } } $WindowsRemoteManagement = @{Enable = $true Scope = 'DC' Source = @{Name = 'Windows Remote Management' Data = { Test-WinRM -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{WindowsRemoteManagement = @{Enable1 = $true Name = 'Test submits an identification request that determines whether the WinRM service is running.' Parameters = @{Property = 'Status' ExpectedValue = $true OperationType = 'eq' } } } } $WindowsRolesAndFeatures = @{Enable = $true Scope = 'DC' Source = @{Name = "Windows Roles and Features" Data = { Get-WindowsFeature -ComputerName $DomainController } ExpectedOutput = $true } Tests = [ordered] @{ActiveDirectoryDomainServices = @{Enable = $true Name = 'Active Directory Domain Services is installed' Parameters = @{WhereObject = { $_.Name -eq 'AD-Domain-Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } DNSServer = @{Enable = $true Name = 'DNS Server is installed' Parameters = @{WhereObject = { $_.Name -eq 'DNS' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileandStorageServices = @{Enable = $true Name = 'File and Storage Services is installed' Parameters = @{WhereObject = { $_.Name -eq 'FileAndStorage-Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileandiSCSIServices = @{Enable = $true Name = 'File and iSCSI Services is installed' Parameters = @{WhereObject = { $_.Name -eq 'File-Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileServer = @{Enable = $true Name = 'File Server is installed' Parameters = @{WhereObject = { $_.Name -eq 'FS-FileServer' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } StorageServices = @{Enable = $true Name = 'Storage Services is installed' Parameters = @{WhereObject = { $_.Name -eq 'Storage-Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } WindowsPowerShell51 = @{Enable = $true Name = 'Windows PowerShell 5.1 is installed' Parameters = @{WhereObject = { $_.Name -eq 'PowerShell' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } } } $WindowsUpdates = @{Enable = $true Scope = 'DC' Source = @{Name = "Windows Updates" Data = { Get-HotFix -ComputerName $DomainController | Sort-Object -Property InstalledOn -Descending | Select-Object -First 1 -Property HotFixID, InstalledOn, Description, InstalledBy } Details = [ordered] @{Area = '' Description = '' Resolution = '' Importance = 10 Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{WindowsUpdates = @{Enable = $true Name = 'Last Windows Updates should be less than X days ago' Parameters = @{Property = 'InstalledOn' ExpectedValue = '(Get-Date).AddDays(-60)' OperationType = 'gt' } } } } $Backup = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Forest Backup' Data = { Get-WinADLastBackup -Forest $ForestName } Details = [ordered] @{Category = 'Configuration' Description = 'Active Directory is critical system for any company. Having a proper, up to date backup in place is crucial.' Importance = 0 ActionType = 0 Resources = @('[Backing Up and Restoring an Active Directory Server](https://docs.microsoft.com/en-us/windows/win32/ad/backing-up-and-restoring-an-active-directory-server)' '[Backup Active Directory (Full and Incremental Backup)](https://activedirectorypro.com/backup-active-directory/)') Tags = 'Backup', 'Configuration' StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{LastBackupTests = @{Enable = $true Name = 'Forest Last Backup Time' Parameters = @{ExpectedValue = 2 OperationType = 'lt' Property = 'LastBackupDaysAgo' PropertyExtendedValue = 'LastBackup' OverwriteName = { "Last Backup $($_.NamingContext)" } } Details = [ordered] @{Category = 'Configuration' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @('Active Directory is critical system for any company. Having a proper, up to date backup is crucial. ' 'Last backup should be maximum few days old, if not less than 24 hours old. ' "Please keep in mind that this test doesn't verifies the backup, nor provides information if the backup was saved to proper place and will be available for restore operations. " "This tests merely checks what was reported by Active Directory - that backup did happen. " "You should make sure that your backup, and more importantly restore process actually works! ") } } DataHighlights = { New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor PaleGreen -Value 2 -Operator lt New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Salmon -Value 2 -Operator ge New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Tomato -Value 10 -Operator ge } } $ForestConfigurationPartitionOwners = @{Enable = $true Scope = 'Forest' Source = @{Name = "Configuration Partitions: Owners" Data = { Get-WinADACLConfiguration -Forest $ForestName -Owner -ObjectType site, subnet, siteLink } Details = [ordered] @{Category = 'Security' Severity = '' Importance = 5 Description = "The configuration partition contains replication topology and other configuration data that must be replicated throughout the forest. Every domain controller in the forest has a replica of the same configuration partition. Just like schema partition, there is just one master configuration partition per forest and a second one on all DCs in a forest. It contains the forest-wide active directory topology including DCs, sites, services, subnets and sitelinks. It is replicated to all DCs in a forest. Owners of Active Directory Configuration Partition, and more specifically Sites, Subnets and Sitelinks should always be set to Administrative (Domain Admins / Enterprise Admins). Being an owner of a site, subnet or sitelink is potentially dangerous and can lead to domain compromise. In comparison to ForestConfigurationPartitionOwnersContainers this test focuses only on chosen object types and nothing else. If there are issues reported in this test you may consider running Testimo with ForestConfigurationPartitionOwnersContainers check to verify if everything is as required. " Resources = @('[Escalating privileges with ACLs in Active Directory](https://blog.fox-it.com/2018/04/26/escalating-privileges-with-acls-in-active-directory/)' '[Site Topology Owner Role](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/site-topology-owner-role)') StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{SiteOwners = @{Enable = $true Name = 'Site Owners should be Administrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'Site' -and $_.OwnerType -ne 'Administrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } SubnetOwners = @{Enable = $true Name = 'Subnet Owners should be Administrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'Subnet' -and $_.OwnerType -ne 'Administrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } SiteLinkOwners = @{Enable = $true Name = 'SiteLink Owners should be Administrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'SiteLink' -and $_.OwnerType -ne 'Administrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } } DataHighlights = { New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor Salmon -Value 'Administrative' -Operator ne -Row New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor PaleGreen -Value 'Administrative' -Operator eq -Row } 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 owners. To generate new report please use:" New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoBefore.ForestConfigurationPartitionOwners.html -Type ForestConfigurationPartitionOwners } 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-WinADACLConfiguration -ObjectType site, subnet, siteLink -Owner -Verbose $Output | 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 'Fix AD Partition Configuration Owners' { New-HTMLText -Text @("Configuration partition contains important AD Objects. Those are among other objects Subnets, Sites and SiteLinks. " "Those objects should have proper owners which usually means being owned by Domain Admins/Enterprise Admins or at some cases by NT AUTHORITY\SYSTEM account. " "Following command when executed fixes owners of those types. " "If the object has proper owner, the owner change is skipped. " "It makes sure each critical AD Object is owned Administrative or WellKnownAdministrative account. " "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, bold, normal -Color Black, Black, Black, Black, Black, Black, Red, Black New-HTMLCodeBlock -Code { Repair-WinADACLConfigurationOwner -ObjectType site, siteLink, subnet -Verbose -WhatIf -LimitProcessing 2 } 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-WinADACLConfigurationOwner -ObjectType site, siteLink, subnet -Verbose -WhatIf } New-HTMLText -TextBlock { "This command when executed repairs only first X object 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 '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.ForestConfigurationPartitionOwners.html -Type ForestConfigurationPartitionOwners } New-HTMLText -TextBlock { "If there were issues reported by this test you may consider running additional test " "ForestConfigurationPartitionOwnersContainers " "which focuses on whole containers rather than just specific objects. " "This is to make sure most of configuration partition is as expected when it comes to object owners." } -FontWeight normal, bold, normal, normal -Color Black, Amaranth, Black, Black New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoAfter.ForestConfigurationPartitionOwnersContainers.html -Type ForestConfigurationPartitionOwnersContainers } 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 } } } } $ForestConfigurationPartitionOwnersContainers = @{Enable = $false Scope = 'Forest' Source = @{Name = "Configuration Partitions: Container Owners" Data = { Get-WinADACLConfiguration -Forest $ForestName -Owner -ContainerType site, subnet, siteLink } Details = [ordered] @{Category = 'Security' Importance = 5 Description = "The configuration partition contains replication topology and other configuration data that must be replicated throughout the forest. Every domain controller in the forest has a replica of the same configuration partition. Just like schema partition, there is just one master configuration partition per forest and a second one on all DCs in a forest. It contains the forest-wide active directory topology including DCs, sites, services, subnets and sitelinks. It is replicated to all DCs in a forest. Owners of Active Directory Configuration Partition, and more specifically Sites, Subnets and Sitelinks should always be set to Administrative (Domain Admins / Enterprise Admins). Being an owner of a site, subnet or sitelink is potentially dangerous and can lead to domain compromise. While ForestConfigurationPartitionOwners test checks only specific objects for ownership this test checks all objects within specific containers. This means every single object is required to have proper membership. " Resources = @('[Escalating privileges with ACLs in Active Directory](https://blog.fox-it.com/2018/04/26/escalating-privileges-with-acls-in-active-directory/)') StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{SiteOwners = @{Enable = $true Name = 'Site Container Owners should be Administrative or WellKnownAdministrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'Site' -and $_.OwnerType -notin 'Administrative', 'WellKnownAdministrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } SubnetOwners = @{Enable = $true Name = 'Subnet Container Owners should be Administrative or WellKnownAdministrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'Subnet' -and $_.OwnerType -notin 'Administrative', 'WellKnownAdministrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } SiteLinkOwners = @{Enable = $true Name = 'SiteLink Container Owners should be Administrative or WellKnownAdministrative' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.ObjectType -eq 'SiteLink' -and $_.OwnerType -notin 'Administrative', 'WellKnownAdministrative' } } Details = [ordered] @{Category = 'Security' Importance = 5 StatusTrue = 1 StatusFalse = 3 } } } DataHighlights = { New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor Salmon -Value 'Administrative' -Operator ne -Row New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor PaleGreen -Value 'Administrative' -Operator eq -Row New-HTMLTableCondition -Name 'OwnerType' -ComparisonType string -BackgroundColor PaleGreen -Value 'WellKnownAdministrative' -Operator eq -Row } 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 owners. To generate new report please use:" New-HTMLCodeBlock -Code { Invoke-Testimo -FilePath $Env:UserProfile\Desktop\TestimoBefore.ForestConfigurationPartitionOwnersContainers.html -Type ForestConfigurationPartitionOwnersContainers } 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-WinADACLConfiguration -ContainerType site, subnet, siteLink -Owner -Verbose $Output | 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 'Fix AD Partition Configuration (Containers) Owners' { New-HTMLText -Text @("Configuration partition contains important AD Objects. Those are among other objects Subnets, Sites and SiteLinks. " "Those objects should have proper owners which usually means being owned by Domain Admins/Enterprise Admins or at some cases by NT AUTHORITY\SYSTEM account. " "Following command when executed fixes owners of those types. " "If the object has proper owner, the owner change is skipped. " "It makes sure each critical AD Object is owned Administrative or WellKnownAdministrative account. " "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, bold, normal -Color 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-WinADACLConfigurationOwner -ContainerType site, siteLink, subnet -Verbose -WhatIf -LimitProcessing 2 } 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-WinADACLConfigurationOwner -ContainerType site, siteLink, subnet -Verbose -WhatIf } New-HTMLText -TextBlock { "This command when executed repairs only first X object 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 '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.ForestConfigurationPartitionOwnersContainers.html -Type ForestConfigurationPartitionOwnersContainers } 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 } } } } $ForestFSMORoles = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Roles availability' Data = { Test-ADRolesAvailability -Forest $ForestName } Details = [ordered] @{Category = 'Health' Description = '' Resolution = '' Importance = 0 ActionType = 0 Severity = 'High' Resources = @() StatusTrue = 0 StatusFalse = 2 } ExpectedOutput = $true } Tests = [ordered] @{SchemaMasterAvailability = @{Enable = $true Name = 'Schema Master Availability' Parameters = @{ExpectedValue = $true Property = 'SchemaMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'SchemaMaster' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 10 } } DomainNamingMasterAvailability = @{Enable = $true Name = 'Domain Master Availability' Parameters = @{ExpectedValue = $true Property = 'DomainNamingMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'DomainNamingMaster' } Details = [ordered] @{Category = 'Health' Importance = 10 ActionType = 2 StatusTrue = 1 StatusFalse = 10 } } } } $ForestSubnets = @{Enable = $true Scope = 'Forest' Source = [ordered] @{Name = 'Subnets verification' Data = { Get-WinADForestSubnet -VerifyOverlap -Forest $ForestName } Details = [ordered] @{Category = 'Configuration' Description = "AD subnets are used so that a machine can work out which AD site they should be in. If you have a subnet that hasn’t been defined to Active Directory, any machines will have difficulty identifying which AD site they should be in. This can easily lead to them authenticating against a domain controller that’s inappropriate from a network standpoint, which will cause a poor logon experience for those users." Importance = 3 ActionType = 1 Resources = @("[Configuring Active Directory Sites and Subnets](https://theitbros.com/active-directory-sites-and-subnets/)" "[How to Create an Active Directory Subnet/Site with /32 or /128 and Why](https://techcommunity.microsoft.com/t5/core-infrastructure-and-security/how-to-create-an-active-directory-subnet-site-with-32-or-128-and/ba-p/256105)" "[Active Directory subnets, sites, and site links](https://www.windows-active-directory.com/active-directory-subnets-sites-and-site-links.html)" "[Chapter 16. Managing sites and subnets](https://livebook.manning.com/book/learn-active-directory-management-in-a-month-of-lunches/chapter-16/44)") StatusTrue = 1 StatusFalse = 2 } ExpectedOutput = $true } Tests = [ordered] @{SubnetsWithoutSites = @{Enable = $true Name = 'Subnets without Sites' Description = 'Verify each subnet is attached to a site' Parameters = @{WhereObject = { $_.SiteStatus -eq $false } ExpectedCount = 0 } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 3 } } SubnetsOverlapping = @{Enable = $true Name = 'Subnets overlapping' Parameters = @{WhereObject = { $_.Overlap -eq $true } ExpectedCount = 0 } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 4 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @("AD subnets are used so that a machine can work out which AD site they should be in. " "If you have a subnet that hasn’t been defined to Active Directory, any machines will have difficulty identifying which AD site they should be in. " "This can easily lead to them authenticating against a domain controller that’s inappropriate from a network standpoint, which will cause a poor logon experience for those users. " "There are 3 stages to this test: ") New-HTMLList { New-HTMLListItem -Text "Gather data about subnets. It should return at least one subnet to pass a test. " New-HTMLListItem -Text "Find subnets that are not attached to any sites. " New-HTMLListItem -Text "Find subnets that are overlapping with other subnets. " } New-HTMLText -Text @("All three tests are required to pass for properly configured Active Directory. ") } } DataHighlights = { New-HTMLTableCondition -Name 'SiteStatus' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'SiteStatus' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq New-HTMLTableCondition -Name 'Overlap' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq New-HTMLTableCondition -Name 'Overlap' -ComparisonType string -BackgroundColor Salmon -Value $true -Operator eq } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { New-HTMLWizard { New-HTMLWizardStep -Name 'Investigate Subnets without Sites' { New-HTMLText -Text @("Subnets without sites are pretty uncommon. " "This usually happens if site is deleted while the subnets are still attached to it. " "Subnets without sites have no use. " "" "Please move subnet to proper site, or if it's no longer needed, remove it totally. ") } New-HTMLWizardStep -Name 'Investigate Subnets overlapping' { New-HTMLText -Text @("Subnets are supposed to be unique across forest. " "You can assign only one subnet to only one site. " "However it's possible to define subnets that overlap already defined subnets such as 10.0.0.0/8 will overlap with 10.0.20.32/32. " "This shouldn't happen as it will influence authentication process and cause poor logon experience. " "" "Investigate why subnets are added with overlap and fix it. " "Please make sure to consult it with appriopriate people or/and network team. ") -FontWeight normal, bold, normal, normal, normal, normal, bold, bold } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $OptionalFeatures = [ordered] @{Enable = $true Scope = 'Forest' Source = [ordered] @{Name = 'Optional Features' Data = { Get-WinADForestOptionalFeatures } Details = [ordered] @{Category = 'Configuration' Description = "Verifies availability of Recycle Bin, LAPS and PAM in the Active Directory Forest." Importance = 0 ActionType = 0 Resources = @() StatusTrue = 0 StatusFalse = 5 } ExpectedOutput = $true } Tests = [ordered] @{RecycleBinEnabled = @{Enable = $true Name = 'Recycle Bin Enabled' Parameters = @{Property = 'Recycle Bin Enabled' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Description = "The AD Recycle bin allows you to quickly restore deleted objects without the need of a system state or 3rd party backup. The recycle bin feature preserves all link valued and non link valued attributes. This means that a restored object will retain all it's settings when restored." Importance = 5 ActionType = 2 Resources = @('[How to Enable Active Directory Recycle Bin (Server 2016)](https://activedirectorypro.com/enable-active-directory-recycle-bin-server-2016/)') StatusTrue = 1 StatusFalse = 4 } } LapsAvailable = @{Enable = $true Name = 'LAPS Schema Extended' Parameters = @{Property = 'Laps Enabled' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Description = "Microsoft Local Administrator Password Solution (LAPS) is a password manager that utilizes Active Directory to manage and rotate passwords for local Administrator accounts across all of your Windows endpoints. LAPS is a great mitigation tool against lateral movement and privilege escalation, by forcing all local Administrator accounts to have unique, complex passwords, so an attacker compromising one local Administrator account can’t move laterally to other endpoints and accounts that may share that same password." Importance = 10 ActionType = 2 Resources = @('[Running LAPS in the race to security](https://blog.stealthbits.com/running-laps-in-the-race-to-security/)' '[Lithnet LAPS Web App](https://github.com/lithnet/laps-web)' '[Lithnet Access Manager](https://github.com/lithnet/access-manager)' '[Getting Bitlocker and LAPS summary report with PowerShell](https://evotec.xyz/getting-bitlocker-and-laps-summary-report-with-powershell/)' '[Backing up Bitlocker Keys and LAPS passwords from Active Directory](https://evotec.xyz/backing-up-bitlocker-keys-and-laps-passwords-from-active-directory/)') StatusTrue = 1 StatusFalse = 4 } } PrivAccessManagement = @{Enable = $true Name = 'Privileged Access Management Enabled' Parameters = @{Property = 'Privileged Access Management Feature Enabled' ExpectedValue = $true OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Description = "Privileged Access Management (PAM) is a solution that helps organizations restrict privileged access within an existing Active Directory environment. Consider introducing PAM to your environment." Importance = 5 ActionType = 0 Resources = @('[Privileged Access Management for Active Directory Domain Services](https://docs.microsoft.com/en-us/microsoft-identity-manager/pam/privileged-identity-management-for-active-directory-domain-services)') StatusTrue = 1 StatusFalse = 0 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @("Following test verifies availability of Recycle Bin, LAPS and PAM in the Active Directory Forest. " "While LAPS and RecycleBin are quite critical for properly functioning Active Directory, PAM is just a recommendation and is not so easy to implement. " "Therefore only 2 out of 3 tests are considered critical. PAM test is optional. ") } } DataHighlights = { New-HTMLTableCondition -Name 'Value' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq New-HTMLTableCondition -Name 'Value' -ComparisonType string -BackgroundColor Salmon -Value $false -Operator eq } } $OrphanedAdmins = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Orphaned Administrative Objects (AdminCount)' Data = { Get-WinADPrivilegedObjects -Forest $ForestName } Details = [ordered] @{Category = 'Security' Description = "Active Directory user, group, and computer objects possess an AdminCount attribute. The AdminCount attribute’s value defaults to NOT SET. Its utility comes from the fact when a user, group, or computer is added, either directly or transitively, to any of a specific set of protected groups its value is updated to 1. This can provide a relatively simple method by which objects with inherited administrative privileges may be identified. Consider this: a user is stamped with an AdminCount of 1, as a result of being added to Domain Admins; the user is removed from Domain Admins; the AdminCount value persists. In this instance the user is considered as orphaned. The ramifications? The AdminSDHolder ACL will be stamped upon this user every hour to protect against tampering. In turn, this can cause unexpected issues with delegation and application permissions." Importance = 4 ActionType = 1 Resources = @('[Security Focus: Orphaned AdminCount -eq 1 AD Users](https://blogs.technet.microsoft.com/poshchap/2016/07/29/security-focus-orphaned-admincount-eq-1-ad-users/)' "[Fun with Active Directory's AdminCount Attiribute](https://stealthbits.com/blog/fun-with-active-directorys-admincount-attribute/)" '[AdminSDHolder, Protected Groups and SDPROP](https://technet.microsoft.com/en-us/magazine/2009.09.sdadminholder.aspx)' '[Scanning for Active Directory Privileges & Privileged Accounts](https://adsecurity.org/?p=3658)' 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn535495(v=ws.11)') StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{Enabled = @{Enable = $true Name = 'No orphaned AdminCount' Parameters = @{ExpectedCount = 0 OperationType = 'eq' WhereObject = { $_.IsOrphaned -ne $false } } Details = [ordered] @{Category = 'Security' Importance = 4 StatusTrue = 1 StatusFalse = 2 } } } DataInformation = { New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt New-HTMLList { New-HTMLListItem -FontWeight bold, normal -Text "IsOrphaned", " - means object contains AdminCount set to 1, while not being a critical object or direct or indirect member of any critical system groups. " New-HTMLListItem -FontWeight bold, normal -Text "IsMember", " - means object is memberof (direct or indirect) of critical system groups. " New-HTMLListItem -FontWeight bold, normal -Text "IsCriticalSystemObject", " - means object is critical system object. " } -FontSize 10pt } DataHighlights = { New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor Salmon -Value $true New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor PaleGreen -Value $false New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor PaleGreen -Value $true New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor TangerineYellow -Value $false } } $Replication = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Forest Replication' Data = { Get-WinADForestReplication -WarningAction SilentlyContinue -Forest $ForestName } Details = [ordered] @{Area = 'Health' Category = 'Replication' Description = '' Resolution = '' Importance = 10 Severity = 'High' Resources = @() } ExpectedOutput = $null } Tests = [ordered] @{ReplicationTests = @{Enable = $true Name = 'Replication Test' Parameters = @{ExpectedValue = $true Property = 'Status' OperationType = 'eq' PropertyExtendedValue = 'StatusMessage' OverwriteName = { "Replication from $($_.Server) to $($_.ServerPartner)" } } } } } $ReplicationStatus = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Forest Replication using RepAdmin' Data = { $Header = '"showrepl_COLUMNS","Destination DSA Site","Destination DSA","Naming Context","Source DSA Site","Source DSA","Transport Type","Number of Failures","Last Failure Time","Last Success Time","Last Failure Status"' $data = repadmin /showrepl * /csv $data[0] = $Header $data | ConvertFrom-Csv } Details = [ordered] @{Area = 'Health' Category = 'Replication' Description = '' Resolution = '' Importance = 10 Severity = 'High' Resources = @() } Requirements = @{CommandAvailable = 'repadmin' IsInternalForest = $true } ExpectedOutput = $null } Tests = [ordered] @{ReplicationTests = @{Enable = $true Name = 'Replication Test' Parameters = @{ExpectedValue = 0 Property = 'Number of Failures' OperationType = 'eq' PropertyExtendedValue = 'Last Success Time' OverwriteName = { "Replication from $($_.'Source DSA') to $($_.'Destination DSA'), Naming Context: $($_.'Naming Context')" } } } } } $SiteLinks = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Site Links' Data = { Get-WinADSiteLinks -Forest $ForestName } Details = [ordered] @{Area = 'Configuration' Category = 'Sites' Description = '' Resolution = '' Importance = 10 Severity = 'Informational' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{MinimalReplicationFrequency = @{Enable = $true Name = 'Replication Frequency should be set to maximum 60 minutes' Parameters = @{Property = 'ReplicationFrequencyInMinutes' ExpectedValue = 60 OperationType = 'le' } } UseNotificationsForLinks = @{Enable = $true Name = 'Automatic site links should use notifications' Parameters = @{Property = 'Options' ExpectedValue = 'UseNotify' OperationType = 'contains' PropertyExtendedValue = 'Options' } } } } $SiteLinksConnections = @{Enable = $true Scope = 'Forest' Source = @{Name = 'Site Links Connections' Data = { Test-ADSiteLinks -Splitter ', ' -Forest $ForestName } Details = [ordered] @{Area = 'Configuration' Category = 'Sites' Description = '' Resolution = '' Importance = 10 Severity = 'Informational' Resources = @() } ExpectedOutput = $true } Tests = [ordered] @{AutomaticSiteLinks = @{Enable = $true Name = 'All site links are automatic' Description = 'Verify there are no manually configured sitelinks' Parameters = @{Property = 'SiteLinksManualCount' ExpectedValue = 0 OperationType = 'eq' } } SiteLinksCrossSiteNotifications = @{Enable = $true Name = 'All cross-site links use notifications' Parameters = @{Property = 'SiteLinksCrossSiteNotUseNotifyCount' ExpectedValue = 0 OperationType = 'eq' } } SiteLinksSameSiteNotifications = @{Enable = $true Name = 'All same-site links have no notifications' Parameters = @{Property = 'SiteLinksSameSiteUseNotifyCount' ExpectedValue = 0 OperationType = 'eq' } } NoDisabledLinks = @{Enable = $true Name = 'All links are enabled' Parameters = @{Property = 'SiteLinksDisabledCount' ExpectedValue = 0 OperationType = 'eq' } } } } $Sites = @{Enable = $true Scope = 'Forest' Source = [ordered] @{Name = 'Forest Sites' Data = { Get-WinADForestSites -Forest $ForestName } Details = [ordered] @{Category = 'Configuration' Description = 'Sites are Active Directory objects that represent one or more TCP/IP subnets with highly reliable and fast network connections. Site information allows administrators to configure Active Directory access and replication to optimize usage of the physical network. Site objects are associated with a set of subnets, and each domain controller in a forest is associated with an Active Directory site according to its IP address. Sites can host domain controllers from more than one domain, and a domain can be represented in more than one site.' Importance = 0 ActionType = 0 Resources = @("[Site Functions](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/plan/site-functions)" "[Active Directory Sites](https://www.windows-active-directory.com/active-directory-sites.html)") StatusTrue = 0 StatusFalse = 0 } ExpectedOutput = $true } Tests = [ordered] @{SitesWithoutDC = @{Enable = $true Name = 'Sites without Domain Controllers' Description = 'Verify each `site has at least [one subnet configured]`' Parameters = @{WhereObject = { $_.DomainControllersCount -eq 0 } ExpectedCount = 0 } Details = [ordered] @{Category = 'Configuration' Importance = 0 ActionType = 0 StatusTrue = 1 StatusFalse = 0 } } SitesWithoutSubnets = @{Enable = $true Name = 'Sites without Subnets' Parameters = @{WhereObject = { $_.SubnetsCount -eq 0 } ExpectedCount = 0 } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 2 } } } DataDescription = { New-HTMLSpanStyle -FontSize 10pt { New-HTMLText -Text @("Sites are Active Directory objects that represent one or more TCP/IP subnets with highly reliable and fast network connections. " "Site information allows administrators to configure Active Directory access and replication to optimize usage of the physical network. " "Site objects are associated with a set of subnets, and each domain controller in a forest is associated with an Active Directory site according to its IP address. " "Sites can host domain controllers from more than one domain, and a domain can be represented in more than one site.") New-HTMLText -Text @("Sites without subnets have no role and just stay there unused. " "Sites without Domain Controllers still have their role in the Active Directory Topology. " "Following tests finds " "sites without subnets " "and Domain Admins role is to asses whether such stie is still needed and is just missing a subnet, or should be deleted because it's no longer required. " "Following tests also finds " "sites without Domain Controllers" ", but this test is just informational - although if Domain Admin is aware of a site that is no longer required it should be deleted. ") -FontWeight normal, normal, normal, bold, normal, normal, bold, normal } } DataHighlights = { New-HTMLTableCondition -Name 'SubnetsCount' -ComparisonType number -BackgroundColor PaleGreen -Value 0 -Operator gt New-HTMLTableCondition -Name 'SubnetsCount' -ComparisonType number -BackgroundColor Salmon -Value 0 -Operator eq New-HTMLTableCondition -Name 'DomainControllersCount' -ComparisonType number -BackgroundColor PaleGreen -Value 0 -Operator gt } Solution = { New-HTMLContainer { New-HTMLSpanStyle -FontSize 10pt { New-HTMLWizard { New-HTMLWizardStep -Name 'Investigate Sites without Subnets' { New-HTMLText -Text @("Sites without subnets have no use. " "It can mean the site is no longer in use and can be safely deleted. " "" "Please investigate and find out if that's really the case. " "Otherwise you should create proper subnet for given site. ") } New-HTMLWizardStep -Name 'Investigate Sites without Domain Controllers (optional)' { New-HTMLText -Text @("Sites without Domain Controllers do happen and are quite common. " "But this isn't always true. " "Consider investigating whether sites without Domain Controller are as expected. ") -FontWeight normal, bold, normal, normal, normal, normal, bold, bold } } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors } } } } $TombstoneLifetime = @{Enable = $true Scope = 'Forest' Source = [ordered]@{Name = 'Tombstone Lifetime' Data = { $Output = (Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$((Get-ADRootDSE -Server $ForestName).configurationNamingContext)" -Server $ForestName -Properties tombstoneLifetime) | Select-Object -Property DistinguishedName, Name, objectClass, ObjectGuid, tombstoneLifetime if ($null -eq $Output) { [PSCustomObject] @{TombstoneLifeTime = 60 } } else { $Output } } Details = [ordered] @{Category = 'Configuration' Description = "A tombstone is a container object consisting of the deleted objects from AD. These objects have not been physically removed from the database. When an AD object, such as a user is deleted, the object technically remains in the directory for a given period of time; known as the Tombstone Lifetime. At that point, Active Directory sets the ‘isDeleted' attribute of the deleted object to TRUE and moves it to a special container called Tombstone (previously known as CN=Deleted Objects.) Once the object is older than the tombstone lifetime, it will be removed (physically deleted) by the garbage collection process." Importance = 0 ActionType = 0 Resources = @('[Understanding Tombstones, Active Directory, and How To Protect It](https://support.storagecraft.com/s/article/Understanding-Tombstones-Active-Directory-and-How-To-Protect-It?language=en_US)' '[Adjust Active Directory Tombstone Lifetime](https://helpcenter.netwrix.com/NA/Configure_IT_Infrastructure/AD/AD_Tombstone.html)') StatusTrue = 0 StatusFalse = 2 } ExpectedOutput = $true } Tests = [ordered] @{TombstoneLifetime = [ordered] @{Enable = $true Name = 'TombstoneLifetime should be set to minimum of 180 days' Parameters = @{ExpectedValue = 180 Property = 'TombstoneLifeTime' OperationType = 'ge' } Details = [ordered] @{Category = 'Configuration' Importance = 7 ActionType = 2 StatusTrue = 1 StatusFalse = 3 } } } DataHighlights = { New-HTMLTableCondition -Name 'tombstoneLifetime' -ComparisonType number -BackgroundColor PaleGreen -Value 180 -Operator ge -Row New-HTMLTableCondition -Name 'tombstoneLifetime' -ComparisonType number -BackgroundColor Orange -Value 180 -Operator lt -Row New-HTMLTableCondition -Name 'tombstoneLifetime' -ComparisonType number -BackgroundColor Salmon -Value 60 -Operator le -Row } } $Trusts = @{Enable = $true Scope = 'Forest' Source = @{Name = "Trust Availability" Data = { Get-WinADTrust -Forest $ForestName } Details = [ordered] @{Category = 'Health', 'Configuration' Importance = 4 ActionType = 0 Description = 'Verifies if trusts are available and tests for trust unconstrained TGTDelegation' Resolution = '' Resources = @('[Changes to Ticket-Granting Ticket (TGT) Delegation Across Trusts in Windows Server (CIS edition)](https://techcommunity.microsoft.com/t5/core-infrastructure-and-security/changes-to-ticket-granting-ticket-tgt-delegation-across-trusts/ba-p/440261)' "[Visually display Active Directory Trusts using PowerShell](https://evotec.xyz/visually-display-active-directory-trusts-using-powershell/)") StatusTrue = 0 StatusFalse = 3 } ExpectedOutput = $null } Tests = [ordered] @{TrustsConnectivity = @{Enable = $true Name = 'Trust status' Parameters = @{OverwriteName = { "Trust status | Source $($_.'TrustSource'), Target $($_.'TrustTarget'), Direction $($_.'TrustDirection')" } Property = 'TrustStatus' ExpectedValue = 'OK' OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 0 } } TrustsQueryStatus = @{Enable = $true Name = 'Trust Query Status' Parameters = @{OverwriteName = { "Trust query | Source $($_.'TrustSource'), Target $($_.'TrustTarget'), Direction $($_.'TrustDirection')" } Property = 'QueryStatus' ExpectedValue = 'OK' OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } } TrustsUnconstrainedDelegation = @{Enable = $true Name = 'Trust unconstrained TGTDelegation' Parameters = @{OverwriteName = { "Trust unconstrained TGTDelegation | Source $($_.'TrustSource'), Target $($_.'TrustTarget'), Direction $($_.'TrustDirection')" } WhereObject = { ($_.'TrustAttributes' -ne 'Within Forest') -and ($_.'TrustDirection' -eq 'BiDirectional' -or $_.'TrustDirection' -eq 'InBound') } Property = 'IsTGTDelegationEnabled' ExpectedValue = $false OperationType = 'eq' } Details = [ordered] @{Category = 'Configuration' Importance = 5 ActionType = 2 StatusTrue = 1 StatusFalse = 5 } } } } function Get-TestimoDomainControllers { [CmdletBinding()] param([string] $Domain, [switch] $SkipRODC) try { $DC = Get-ADDomainController -Discover -DomainName $Domain $DomainControllers = Get-ADDomainController -Server $DC.HostName[0] -Filter * -ErrorAction Stop if ($SkipRODC) { $DomainControllers = $DomainControllers | Where-Object { $_.IsReadOnly -eq $false } } foreach ($_ in $DomainControllers) { if ($Script:TestimoConfiguration['Inclusions']['DomainControllers']) { if ($_ -in $Script:TestimoConfiguration['Inclusions']['DomainControllers']) { [PSCustomObject] @{Name = $($_.HostName).ToLower() IsPDC = $_.OperationMasterRoles -contains 'PDCEmulator' } } continue } if ($_.HostName -notin $Script:TestimoConfiguration['Exclusions']['DomainControllers']) { [PSCustomObject] @{Name = $($_.HostName).ToLower() IsPDC = $_.OperationMasterRoles -contains 'PDCEmulator' } } } } catch { return } } function Get-TestimoSourcesStatus { [cmdletbinding()] param([string] $Scope) $AllTests = foreach ($Source in $($Script:TestimoConfiguration.ActiveDirectory.Keys)) { if ($Scope -ne $Script:TestimoConfiguration.ActiveDirectory[$Source].Scope) { continue } $Script:TestimoConfiguration.ActiveDirectory["$Source"].Enable } $AllTests -contains $true } function Import-TestimoConfiguration { [CmdletBinding()] param([Object] $Configuration) if ($Configuration) { if ($Configuration -is [System.Collections.IDictionary]) { $Option = 'Hashtable' $LoadedConfiguration = $Configuration } elseif ($Configuration -is [string]) { if (Test-Path -LiteralPath $Configuration) { $Option = 'File' $FileContent = Get-Content -LiteralPath $Configuration } else { $Option = 'JSON' $FileContent = $Configuration } try { $LoadedConfiguration = $FileContent | ConvertFrom-Json } catch { Out-Informative -OverrideTitle 'Testimo' -Text "Loading configuration from JSON failed. Skipping." -Level 0 -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Not JSON or syntax is incorrect.") return } } else { Out-Informative -OverrideTitle 'Testimo' -Text "Loading configuratio failed. Skipping." -Level 0 -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Not JSON/Hashtable or syntax is incorrect.") } Out-Informative -OverrideTitle 'Testimo' -Text "Using configuration provided by user" -Level 0 -Start if ($LoadedConfiguration -is [System.Collections.IDictionary]) { foreach ($Key in ($LoadedConfiguration).Keys) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Enable'] = $LoadedConfiguration.$Key.Enable if ($null -ne $LoadedConfiguration[$Key]['Source']) { if ($null -ne $LoadedConfiguration[$Key]['Source']['ExpectedOutput']) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Source']['ExpectedOutput'] = $LoadedConfiguration.$Key['Source']['ExpectedOutput'] } if ($null -ne $LoadedConfiguration[$Key]['Source']['Parameters']) { foreach ($Parameter in [string] $LoadedConfiguration[$Key]['Source']['Parameters'].Keys) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Source']['Parameters'][$Parameter] = $LoadedConfiguration[$Key]['Source']['Parameters'][$Parameter] } } } foreach ($Test in $LoadedConfiguration.$Key.Tests.Keys) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Enable'] = $LoadedConfiguration.$Key.Tests.$Test.Enable if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedValue) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['ExpectedValue'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedValue } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedCount) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['ExpectedCount'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedCount } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.Property) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['Property'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.Property } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.OperationType) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['OperationType'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.OperationType } } } } else { foreach ($Key in ($LoadedConfiguration).PSObject.Properties.Name) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Enable'] = $LoadedConfiguration.$Key.Enable if ($null -ne $LoadedConfiguration.$Key.'Source') { if ($null -ne $LoadedConfiguration.$Key.'Source'.'ExpectedOutput') { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Source']['ExpectedOutput'] = $LoadedConfiguration.$Key.'Source'.'ExpectedOutput' } if ($null -ne $LoadedConfiguration.$Key.'Source'.'Parameters') { foreach ($Parameter in $LoadedConfiguration.$Key.'Source'.'Parameters'.PSObject.Properties.Name) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Source']['Parameters'][$Parameter] = $LoadedConfiguration.$Key.'Source'.'Parameters'.$Parameter } } } foreach ($Test in $LoadedConfiguration.$Key.Tests.PSObject.Properties.Name) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Enable'] = $LoadedConfiguration.$Key.Tests.$Test.Enable if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedValue) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['ExpectedValue'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedValue } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedCount) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['ExpectedCount'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.ExpectedCount } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.Property) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['Property'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.Property } if ($null -ne $LoadedConfiguration.$Key.Tests.$Test.Parameters.OperationType) { $Script:TestimoConfiguration['ActiveDirectory'][$Key]['Tests'][$Test]['Parameters']['OperationType'] = $LoadedConfiguration.$Key.Tests.$Test.Parameters.OperationType } } } } Out-Informative -OverrideTitle 'Testimo' -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Configuration loaded from $Option") -End } else { Out-Informative -OverrideTitle 'Testimo' -Text "Using configuration defaults" -Level 0 -Status $null -ExtendedValue ("No configuration provided by user") } } function Out-Begin { [CmdletBinding()] param([string] $Text, [int] $Level, [string] $Type = 't', [string] $Domain, [string] $DomainController) if ($Domain -and $DomainController) { if ($Type -eq 't') { [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow } elseif ($Type -eq 'e') { [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow } else { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow } $TestText = "[$Type]", "[$Domain]", "[$($DomainController)] ", $Text } elseif ($Domain) { if ($Type -eq 't') { [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } elseif ($Type -eq 'e') { [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } else { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } $TestText = "[$Type]", "[$Domain] ", $Text } elseif ($DomainController) { Write-Warning "Out-Begin - Shouldn't happen - Fix me." } else { if ($Type -eq 't') { [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } elseif ($Type -eq 'e') { [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } else { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow } $TestText = "[$Type]", "[Forest] ", $Text } Write-Color -Text $TestText -Color $Color -StartSpaces $Level -NoNewLine } function Out-Failure { [CmdletBinding()] param([string] $Text, [int] $Level, [string] $ExtendedValue = 'Input data not provided. Failing test.', [string] $Domain, [string] $DomainController, [string] $ReferenceID, [validateSet('e', 'i', 't')][string] $Type = 't', [System.Collections.IDictionary] $Source, [System.Collections.IDictionary] $Test) Out-Begin -Text $Text -Level $Level -Domain $Domain -DomainController $DomainController -Type $Type Out-Status -Text $Text -Status $false -ExtendedValue $ExtendedValue -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $Source -Test $Test } function Out-Informative { [CmdletBinding()] param([int] $Level = 0, [string] $OverrideTitle, [string] $Domain, [string] $DomainController, [string] $Text, [nullable[bool]] $Status, [string] $ExtendedValue, [switch] $Start, [switch] $End) if ($Start -or (-not $Start -and -not $End)) { $Type = 'i' if ($Domain -and $DomainController) { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow $TestText = "[$Type]", "[$Domain]", "[$($DomainController)] ", $Text } elseif ($Domain) { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow $TestText = "[$Type]", "[$Domain] ", $Text } elseif ($DomainController) { Write-Warning "Out-Begin - Shouldn't happen - Fix me." } else { [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow if ($OverrideTitle) { $TestText = "[$Type]", "[$OverrideTitle] ", $Text } else { $TestText = "[$Type]", "[Forest] ", $Text } } Write-Color -Text $TestText -Color $Color -StartSpaces $Level -NoNewLine } if ($End -or (-not $Start -and -not $End)) { if ($Status -eq $true) { [string] $TextStatus = 'Pass' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::Green, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Green, [ConsoleColor]::Cyan } elseif ($Status -eq $false) { [string] $TextStatus = 'Fail' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::Red, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Red, [ConsoleColor]::Cyan } else { [string] $TextStatus = 'Informative' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Magenta, [ConsoleColor]::Cyan } if ($ExtendedValue) { Write-Color -Text ' [', $TextStatus, ']', " [", $ExtendedValue, "]" -Color $Color } else { Write-Color -Text ' [', $TextStatus, ']' -Color $Color } } } function Out-Skip { [CmdletBinding()] param([PSCustomobject] $TestsSummary, [int] $Level = 0, [string] $Domain, [string] $DomainController, [string] $Test, [string] $Source, [string] $Reason = 'Skipping - unmet dependency') Out-Begin -Type 'i' -Text $Test -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $Test -Status $null -ExtendedValue $Reason -Domain $Domain -DomainController $DomainController -ReferenceID $Source $TestsSummary.Skipped = $TestsSummary.Skipped + 1 $TestsSummary.Total = $TestsSummary.Failed + $TestsSummary.Passed + $TestsSummary.Skipped $TestsSummary } function Out-Status { [CmdletBinding()] param([string] $TestID, [string] $Text, [nullable[bool]] $Status, [string] $Section, [string] $ExtendedValue, [string] $Domain, [string] $DomainController, [System.Collections.IDictionary] $Source, [System.Collections.IDictionary] $Test, [string] $ReferenceID) if ($Status -eq $true) { [string] $TextStatus = 'Pass' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::Green, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Green, [ConsoleColor]::Cyan } elseif ($Status -eq $false) { [string] $TextStatus = 'Fail' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::Red, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Red, [ConsoleColor]::Cyan } else { [string] $TextStatus = 'Informative' [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Cyan, [ConsoleColor]::Cyan, [ConsoleColor]::Magenta, [ConsoleColor]::Cyan } if ($ExtendedValue) { Write-Color -Text ' [', $TextStatus, ']', " [", $ExtendedValue, "]" -Color $Color } else { Write-Color -Text ' [', $TextStatus, ']' -Color $Color } if ($Domain -and $DomainController) { $TestType = 'Domain Controller' $TestText = "Domain Controller - $DomainController | $Text" } elseif ($Domain) { $TestType = 'Domain' $TestText = "Domain - $Domain | $Text" } elseif ($DomainController) { $TestType = 'Should not happen. Find an error.' } else { $TestType = 'Forest' $TestText = "Forest | $Text" } if ($Source) { if ($null -ne $Source.Details.Importance) { $ImportanceInformation = $Script:Importance[$Source.Details.Importance] } else { $ImportanceInformation = 'Not defined' } if ($null -ne $Source.Details.Category) { $Category = $Source.Details.Category } else { $Category = 'Not defined' } if ($null -ne $Source.Details.ActionType) { $Action = $Script:ActionType[$Source.Details.ActionType] } else { $Action = 'Not defined' } if ($null -ne $Source.Details.StatusTrue -and $null -ne $Source.Details.StatusFalse) { if ($Status -eq $true) { $StatusTranslation = $Script:StatusTranslation[$Source.Details.StatusTrue] } elseif ($Status -eq $false) { $StatusTranslation = $Script:StatusTranslation[$Source.Details.StatusFalse] } elseif ($null -eq $Status) { $StatusTranslation = $Script:StatusTranslation[0] } } else { $StatusTranslation = $Status } } else { if ($null -ne $Test.Details.Importance) { $ImportanceInformation = $Script:Importance[$Test.Details.Importance] } else { $ImportanceInformation = 'Not defined' } if ($null -ne $Test.Details.Category) { $Category = $Test.Details.Category } else { $Category = 'Not defined' } if ($null -ne $Test.Details.ActionType) { $Action = $Script:ActionType[$Test.Details.ActionType] } else { $Action = 'Not defined' } if ($null -ne $Test.Details.StatusTrue -and $null -ne $Test.Details.StatusFalse) { if ($Status -eq $true) { $StatusTranslation = $Script:StatusTranslation[$Test.Details.StatusTrue] } elseif ($Status -eq $false) { $StatusTranslation = $Script:StatusTranslation[$Test.Details.StatusFalse] } elseif ($null -eq $Status) { $StatusTranslation = $Script:StatusTranslation[0] } } else { $StatusTranslation = $Status } } $Output = [PSCustomObject]@{Name = $TestText DisplayName = $Text Type = $TestType Category = $Category Assesment = $StatusTranslation Status = $Status Action = $Action Importance = $ImportanceInformation Extended = $ExtendedValue Domain = $Domain DomainController = $DomainController } if (-not $ReferenceID) { $Script:Reporting['Errors'].Add($Output) } else { if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Results'].Add($Output) } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Results'].Add($Output) } else { $Script:Reporting['Forest']['Tests'][$ReferenceID]['Results'].Add($Output) } } $Script:TestResults.Add($Output) } function Out-Summary { [CmdletBinding()] param([System.Diagnostics.Stopwatch] $Time, $Text, [int] $Level, [string] $Domain, [string] $DomainController, [PSCustomobject] $TestsSummary) $EndTime = Stop-TimeLog -Time $Time -Option OneLiner $Type = 'i' if ($Domain -and $DomainController) { if ($Type -eq 't') { [ConsoleColor[]] $Color = @([ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray) } else { [ConsoleColor[]] $Color = @([ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray [ConsoleColor]::Yellow, [ConsoleColor]::White, [ConsoleColor]::Yellow [ConsoleColor]::Green [ConsoleColor]::Yellow [ConsoleColor]::Red [ConsoleColor]::Yellow [ConsoleColor]::Cyan) } $TestText = @("[$Type]", "[$Domain]", "[$($DomainController)] ", $Text, ' [', 'Time to execute tests: ', $EndTime, ']', '[', 'Tests Total: ', ($TestsSummary.Total), ', Passed: ', ($TestsSummary.Passed), ', Failed: ', ($TestsSummary.Failed), ', Skipped: ', ($TestsSummary.Skipped), ']') } elseif ($Domain) { if ($Type -eq 't') { [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray } else { [ConsoleColor[]] $Color = @([ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray [ConsoleColor]::Yellow, [ConsoleColor]::White, [ConsoleColor]::Yellow [ConsoleColor]::Green [ConsoleColor]::Yellow [ConsoleColor]::Red [ConsoleColor]::Yellow [ConsoleColor]::Cyan) } $TestText = @("[$Type]", "[$Domain] ", $Text, ' [', 'Time to execute tests: ', $EndTime, ']', '[', 'Tests Total: ', ($TestsSummary.Total), ', Passed: ', ($TestsSummary.Passed), ', Failed: ', ($TestsSummary.Failed), ', Skipped: ', ($TestsSummary.Skipped), ']') } elseif ($DomainController) { Write-Warning "Out-Begin - Shouldn't happen - Fix me." } else { if ($Type -eq 't') { [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray } else { [ConsoleColor[]] $Color = @([ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray [ConsoleColor]::Yellow, [ConsoleColor]::White, [ConsoleColor]::Yellow [ConsoleColor]::Green [ConsoleColor]::Yellow [ConsoleColor]::Red [ConsoleColor]::Yellow [ConsoleColor]::Cyan) } $TestText = @("[$Type]", "[Forest] ", $Text, ' [', 'Time to execute tests: ', $EndTime, ']', '[', 'Tests Total: ', ($TestsSummary.Total), ', Passed: ', ($TestsSummary.Passed), ', Failed: ', ($TestsSummary.Failed), ', Skipped: ', ($TestsSummary.Skipped), ']') } Write-Color -Text $TestText -Color $Color -StartSpaces $Level } $Script:Importance = @{0 = 'Informational' 1 = 'Negligible' 2 = 'Very low' 3 = 'Low' 4 = 'Minor' 5 = 'Moderate Low' 6 = 'Moderate' 7 = 'High' 8 = 'Very High' 9 = 'Significant' 10 = 'Extreme' } $Script:StatusTranslation = @{-1 = 'Skipped' 0 = 'Informational' 1 = 'Good' 2 = 'Low' 3 = 'Elevated' 4 = 'High' 5 = 'Severe' } $Script:StatusTranslationColors = @{-1 = 'DeepSkyBlue' 0 = 'ElectricBlue' 1 = 'LawnGreen' 2 = 'ParisDaisy' 3 = 'SafetyOrange' 4 = 'InternationalOrange' 5 = 'TorchRed' } $Script:ActionType = @{0 = 'Informational' 1 = 'Recommended' 2 = 'Must Implement' } function Set-TestsStatus { [CmdletBinding()] param([string[]] $Sources, [string[]] $ExcludeSources) foreach ($Source in $Script:TestimoConfiguration.ActiveDirectory.Keys) { $Script:TestimoConfiguration.ActiveDirectory[$Source]['Enable'] = $false } foreach ($Source in $Sources) { $Script:TestimoConfiguration.ActiveDirectory[$Source]['Enable'] = $true } foreach ($Source in $ExcludeSources) { $Script:TestimoConfiguration.ActiveDirectory[$Source]['Enable'] = $false } } function Start-TestimoEmail { [CmdletBinding()] param([string] $From, [string[]] $To, [string[]] $CC, [string[]] $BCC, [string] $Server, [int] $Port, [switch] $SSL, [string] $UserName, [string] $Password, [switch] $PasswordAsSecure, [switch] $PasswordFromFile, [string] $Priority = 'High', [string] $Subject = '[Reporting Evotec] Summary of Active Directory Tests') Email { EmailHeader { EmailFrom -Address $From EmailTo -Addresses $To EmailServer -Server $Server -Username $UserName -Password $PasswordFromFile -PasswordAsSecure:$PasswordAsSecure -PasswordFromFile:$PasswordFromFile -Port 587 -SSL:$SSL EmailOptions -Priority $Priority -DeliveryNotifications Never EmailSubject -Subject $Subject } EmailBody -FontFamily 'Calibri' -Size 15 { EmailTable -DataTable $Results { EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator eq -Value 'True' -BackGroundColor Green -Color White -Inline -Row EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator ne -Value 'True' -BackGroundColor Red -Color White -Inline -Row } -HideFooter } } -AttachSelf -Supress $false } function Start-TestimoReport { [CmdletBinding()] param([System.Collections.IDictionary] $TestResults, [string] $FilePath, [switch] $Online, [switch] $ShowHTML, [switch] $HideSteps, [switch] $AlwaysShowSteps) if ($FilePath -eq '') { $FilePath = Get-FileName -Extension 'html' -Temporary } $ColorPassed = 'LawnGreen' $ColorSkipped = 'DeepSkyBlue' $ColorFailed = 'TorchRed' $ColorPassedText = 'Black' $ColorFailedText = 'Black' $ColorSkippedText = 'Black' $TestResults['Configuration'] = @{Colors = @{ColorPassed = $ColorPassed ColorSkipped = $ColorSkipped ColorFailed = $ColorFailed ColorPassedText = $ColorPassedText ColorFailedText = $ColorFailedText ColorSkippedText = $ColorSkippedText } } $TestResults['Configuration']['ResultConditions'] = { foreach ($Status in $Script:StatusTranslation.Keys) { New-HTMLTableCondition -Name 'Assesment' -Value $Script:StatusTranslation[$Status] -BackgroundColor $Script:StatusTranslationColors[$Status] } New-HTMLTableCondition -Name 'Assesment' -Value $true -BackgroundColor $TestResults['Configuration']['Colors']['ColorPassed'] -Color $TestResults['Configuration']['Colors']['ColorPassedText'] New-HTMLTableCondition -Name 'Assesment' -Value $false -BackgroundColor $TestResults['Configuration']['Colors']['ColorFailed'] -Color $TestResults['Configuration']['Colors']['ColorFailedText'] } $TestResults['Configuration']['ResultConditionsEmail'] = { $Translations = @{-1 = 'Skipped' 0 = 'Informational' 1 = 'Good' 2 = 'Low' 3 = 'Elevated' 4 = 'High' 5 = 'Severe' } $TranslationsColors = @{-1 = 'DeepSkyBlue' 0 = 'ElectricBlue' 1 = 'LawnGreen' 2 = 'ParisDaisy' 3 = 'SafetyOrange' 4 = 'InternationalOrange' 5 = 'TorchRed' } foreach ($Status in $Translations.Keys) { New-HTMLTableCondition -Name 'Assesment' -Value $Translations[$Status] -BackgroundColor $TranslationsColors[$Status] -Inline } New-HTMLTableCondition -Name 'Assesment' -Value $true -BackgroundColor 'LawnGreen' -Inline New-HTMLTableCondition -Name 'Assesment' -Value $false -BackgroundColor 'TorchRed' -Inline } [Array] $PassedTests = $TestResults['Results'] | Where-Object { $_.Status -eq $true } [Array] $FailedTests = $TestResults['Results'] | Where-Object { $_.Status -eq $false } [Array] $SkippedTests = $TestResults['Results'] | Where-Object { $_.Status -ne $true -and $_.Status -ne $false } New-HTML -FilePath $FilePath -Online:$Online { New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoin -ArrayJoinString ', ' -DateTimeFormat 'dd.MM.yyyy HH:mm:ss' New-HTMLTabStyle -BorderRadius 0px -BackgroundColorActive SlateGrey New-HTMLHeader { New-HTMLSection -Invisible { New-HTMLSection { New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue } -JustifyContent flex-start -Invisible New-HTMLSection { New-HTMLText -Text $Script:Reporting['Version'] -Color Blue } -JustifyContent flex-end -Invisible } } $NumberOfSourcesExecuted = 0 $NumberOfSourcesExecuted += $TestResults['Forest']['Tests'].Count foreach ($Domain in $TestResults['Domains'].Keys) { $NumberOfSourcesExecuted += $TestResults['Domains'][$Domain]['Tests'].Count $NumberOfSourcesExecuted += $TestResults['Domains'][$Domain]['DomainControllers'].Count } if ($NumberOfSourcesExecuted -gt 1) { New-HTMLTab -Name 'Summary' -IconBrands galactic-senate { New-HTMLSection -HeaderText "Tests results" -HeaderBackGroundColor DarkGray { New-HTMLContainer { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTests.Count) -Color $ColorPassed New-ChartPie -Name 'Failed' -Value ($FailedTests.Count) -Color $ColorFailed New-ChartPie -Name 'Skipped' -Value ($SkippedTests.Count) -Color $ColorSkipped } New-HTMLTable -DataTable $TestResults['Summary'] -HideFooter -DisableSearch { New-HTMLTableContent -ColumnName 'Passed' -BackgroundColor $TestResults['Configuration']['Colors']['ColorPassed'] -Color $TestResults['Configuration']['Colors']['ColorPassedText'] New-HTMLTableContent -ColumnName 'Failed' -BackgroundColor $TestResults['Configuration']['Colors']['ColorFailed'] -Color $TestResults['Configuration']['Colors']['ColorFailedText'] New-HTMLTableContent -ColumnName 'Skipped' -BackgroundColor $TestResults['Configuration']['Colors']['ColorSkipped'] -Color $TestResults['Configuration']['Colors']['ColorSkippedText'] } -DataStore HTML -Buttons @() -DisablePaging } -Width '35%' New-HTMLContainer { New-HTMLText -Text @("Below you can find overall summary of all tests executed in this Testimo run.") -FontSize 10pt $ResultsDisplay = $TestResults['Results'] | Select-Object -Property DisplayName, Type, Category, Assesment, Importance, Action, Extended, Domain, DomainController New-HTMLTable -DataTable $ResultsDisplay { foreach ($Status in $Script:StatusTranslation.Keys) { New-HTMLTableCondition -Name 'Assesment' -Value $Script:StatusTranslation[$Status] -BackgroundColor $Script:StatusTranslationColors[$Status] -Row } New-HTMLTableCondition -Name 'Assesment' -Value $true -BackgroundColor $TestResults['Configuration']['Colors']['ColorPassed'] -Color $TestResults['Configuration']['Colors']['ColorPassedText'] -Row New-HTMLTableCondition -Name 'Assesment' -Value $false -BackgroundColor $TestResults['Configuration']['Colors']['ColorFailed'] -Color $TestResults['Configuration']['Colors']['ColorFailedText'] -Row } -Filtering } } } } if ($TestResults['Forest']['Tests'].Count -gt 0) { if ($NumberOfSourcesExecuted -eq 1) { foreach ($Source in $TestResults['Forest']['Tests'].Keys) { $Name = $TestResults['Forest']['Tests'][$Source]['Name'] $Data = $TestResults['Forest']['Tests'][$Source]['Data'] $Information = $TestResults['Forest']['Tests'][$Source]['Information'] $SourceCode = $TestResults['Forest']['Tests'][$Source]['SourceCode'] $Results = $TestResults['Forest']['Tests'][$Source]['Results'] $WarningsAndErrors = $TestResults['Forest']['Tests'][$Source]['WarningsAndErrors'] Start-TestimoReportSection -Name $Name -Data $Data -Information $Information -SourceCode $SourceCode -Results $Results -WarningsAndErrors $WarningsAndErrors -TestResults $TestResults -Type 'Forest' } } else { New-HTMLTab -Name 'Forest' -IconBrands first-order { foreach ($Source in $TestResults['Forest']['Tests'].Keys) { $Name = $TestResults['Forest']['Tests'][$Source]['Name'] $Data = $TestResults['Forest']['Tests'][$Source]['Data'] $Information = $TestResults['Forest']['Tests'][$Source]['Information'] $SourceCode = $TestResults['Forest']['Tests'][$Source]['SourceCode'] $Results = $TestResults['Forest']['Tests'][$Source]['Results'] $WarningsAndErrors = $TestResults['Forest']['Tests'][$Source]['WarningsAndErrors'] Start-TestimoReportSection -Name $Name -Data $Data -Information $Information -SourceCode $SourceCode -Results $Results -WarningsAndErrors $WarningsAndErrors -TestResults $TestResults -Type 'Forest' } } } } $DomainsFound = @{} [Array] $ProcessDomains = foreach ($Domain in $TestResults['Domains'].Keys) { if ($TestResults['Domains'][$Domain]['Tests'].Count -gt 0) { $DomainsFound[$Domain] = $true $true } } if ($ProcessDomains -contains $true) { New-HTMLTab -Name 'Domains' -IconBrands wpbeginner { foreach ($Domain in $TestResults['Domains'].Keys) { if ($TestResults['Domains'][$Domain]['Tests'].Count -gt 0 -or $TestResults['Domains'][$Domain]['DomainControllers'].Count -gt 0) { New-HTMLTab -Name "Domain $Domain" -IconBrands deskpro { foreach ($Source in $TestResults['Domains'][$Domain]['Tests'].Keys) { $Information = $TestResults['Domains'][$Domain]['Tests'][$Source]['Information'] $Name = $TestResults['Domains'][$Domain]['Tests'][$Source]['Name'] $Data = $TestResults['Domains'][$Domain]['Tests'][$Source]['Data'] $SourceCode = $TestResults['Domains'][$Domain]['Tests'][$Source]['SourceCode'] $Results = $TestResults['Domains'][$Domain]['Tests'][$Source]['Results'] $WarningsAndErrors = $TestResults['Domains'][$Domain]['Tests'][$Source]['WarningsAndErrors'] Start-TestimoReportSection -Name $Name -Data $Data -Information $Information -SourceCode $SourceCode -Results $Results -WarningsAndErrors $WarningsAndErrors -HideSteps:$HideSteps -TestResults $TestResults -Type 'Domain' -AlwaysShowSteps:$AlwaysShowSteps } } } } } } if ($TestResults['Domains'].Keys.Count -gt 0) { $DomainsFound = @{} [Array] $ProcessDomainControllers = foreach ($Domain in $TestResults['Domains'].Keys) { if ($TestResults['Domains'][$Domain]['DomainControllers'].Count -gt 0) { $DomainsFound[$Domain] = $true $true } } if ($ProcessDomainControllers -contains $true) { New-HTMLTab -Name 'Domain Controllers' -IconRegular snowflake { foreach ($Domain in $TestResults['Domains'].Keys) { if ($TestResults['Domains'][$Domain]['Tests'].Count -gt 0 -or $TestResults['Domains'][$Domain]['DomainControllers'].Count -gt 0) { New-HTMLTab -Name "Domain $Domain" -IconBrands deskpro { if ($TestResults['Domains'][$Domain]['DomainControllers'].Count -gt 0) { foreach ($DC in $TestResults['Domains'][$Domain]['DomainControllers'].Keys) { New-HTMLTab -TabName $DC -TextColor DarkSlateGray { New-HTMLContainer { foreach ($Source in $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'].Keys) { $Information = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Information'] $Name = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Name'] $Data = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Data'] $SourceCode = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['SourceCode'] $Results = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Results'] $WarningsAndErrors = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['WarningsAndErrors'] Start-TestimoReportSection -Name $Name -Data $Data -Information $Information -SourceCode $SourceCode -Results $Results -WarningsAndErrors $WarningsAndErrors -TestResults $TestResults -Type 'DC' } } } } } } } } } } } } -ShowHTML:$ShowHTML } function Start-TestimoReportSection { [cmdletBinding()] param([string] $Name, [Array] $Data, [System.Collections.IDictionary]$Information, [Scriptblock]$SourceCode, [Array] $Results, [Array] $WarningsAndErrors, [switch] $HideSteps, [switch] $AlwaysShowSteps, [System.Collections.IDictionary]$TestResults, [string] $Type) [Array] $PassedTestsSingular = $Results | Where-Object { $_.Status -eq $true } [Array] $FailedTestsSingular = $Results | Where-Object { $_.Status -eq $false } [Array] $SkippedTestsSingular = $Results | Where-Object { $_.Status -ne $true -and $_.Status -ne $false } if ($Type -eq 'Forest') { $ResultsDisplay = $Results | Select-Object -Property DisplayName, Type, Category, Assesment, Importance, Action, Extended } elseif ($Type -eq 'DC') { $ResultsDisplay = $Results | Select-Object -Property DisplayName, Type, Category, Assesment, Importance, Action, Extended, Domain } elseif ($Type -eq 'Domain') { $ResultsDisplay = $Results | Select-Object -Property DisplayName, Type, Category, Assesment, Importance, Action, Extended, Domain, DomainController } $ColorPassed = 'LawnGreen' $ColorSkipped = 'DeepSkyBlue' $ColorFailed = 'TorchRed' $ColorPassedText = 'Black' $ColorFailedText = 'Black' $ColorSkippedText = 'Black' New-HTMLSection -HeaderText $Name -HeaderBackGroundColor CornflowerBlue -Direction column { New-HTMLSection -Invisible -Direction column { New-HTMLSection -HeaderText 'Information' { New-HTMLContainer { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTestsSingular.Count) -Color $ColorPassed New-ChartPie -Name 'Failed' -Value ($FailedTestsSingular.Count) -Color $ColorFailed New-ChartPie -Name 'Skipped' -Value ($SkippedTestsSingular.Count) -Color $ColorSkipped } -Height 250 New-HTMLText -Text @("Below command was used to generate and asses current data that is visible in this report. " "In case there are more information required feel free to confirm problems found yourself. ") -FontSize 10pt New-HTMLCodeBlock -Code $SourceCode -Style 'PowerShell' -Theme enlighter if ($WarningsAndErrors) { New-HTMLSection -HeaderText 'Warnings & Errors' -HeaderBackGroundColor OrangePeel { New-HTMLTable -DataTable $WarningsAndErrors -Filtering -PagingLength 7 } } } New-HTMLContainer { if ($Information.Source.Details) { if ($Information.DataDescription) { & $Information.DataDescription } elseif ($Information.Source.Details.Description) { New-HTMLText -Text $Information.Source.Details.Description -FontSize 10pt } $SummaryOfTests = foreach ($Test in $Information.Tests.Keys) { if ($Information.Tests[$Test].Enable -eq $true -and $Information.Tests[$Test].Details.Description) { New-HTMLListItem -FontSize 10pt -Text $Information.Tests[$Test].Name, " - ", $Information.Tests[$Test].Details.Description if ($Information.Tests[$Test].Details.Resources) { New-HTMLList -FontSize 10pt { foreach ($Resource in $Information.Tests[$Test].Details.Resources) { if ($Resource.StartsWith('[')) { New-HTMLListItem -Text $Resource } else { $Resource = "[$Resource]($Resource)" New-HTMLListItem -Text $Resource } } } } } } if ($SummaryOfTests) { New-HTMLList { $SummaryOfTests } } if ($Information.Source.Details.Resources) { New-HTMLText -Text 'Following resources may be helpful to understand this topic', ', please make sure to read those to understand this topic before following any instructions.' -FontSize 10pt -FontWeight bold, normal New-HTMLList -FontSize 10pt { foreach ($Resource in $Information.Source.Details.Resources) { if ($Resource.StartsWith('[')) { New-HTMLListItem -Text $Resource } else { $Resource = "[$Resource]($Resource)" New-HTMLListItem -Text $Resource } } } } } New-HTMLText -FontSize 10pt -Text @("In the table below you can find summary of tests executed in the " $Name " category. Each test has their " "assesment level ", ', ' "importance level ", ' and ' "action ", "defined. " "Depending on the assesment, importance and action AD Team needs to investigate according to the steps provided including using their internal processes (for example SOP). " "It's important to have an understanding what the test is trying to tell you and what solution is provided. " "If you have doubts, or don't understand some test please consider talking to senior admins for guidance. ") -FontWeight normal, bold, normal, bold, normal, bold, normal, bold, normal, normal, normal, normal New-HTMLTable -DataTable $ResultsDisplay { & $TestResults['Configuration']['ResultConditions'] } -Filtering } } } if ($Data) { New-HTMLSection -HeaderText 'Data' { New-HTMLContainer { if ($Information.DataInformation) { & $Information.DataInformation } New-HTMLTable -DataTable $Data -Filtering { if ($Information.DataHighlights) { & $Information.DataHighlights } } -PagingLength 7 } } } if ($Information.Solution) { if (($HideSteps.IsPresent -eq $false -and $FailedTestsSingular.Count -gt 0) -or $AlwaysShowSteps.IsPresent) { New-HTMLSection -Name 'Solution' { & $Information.Solution } } } } } function Start-Testing { [CmdletBinding()] param([ScriptBlock] $Execute, [string] $Scope, [string] $Domain, [string] $DomainController, [bool] $IsPDC, [Object] $ForestInformation, [Object] $DomainInformation, [System.Collections.IDictionary] $ForestDetails, [switch] $SkipRODC) $GlobalTime = Start-TimeLog if ($Scope -eq 'Forest') { $Level = 3 $LevelTest = 6 $LevelSummary = 3 $LevelTestFailure = 6 } elseif ($Scope -eq 'Domain') { $Level = 6 $LevelTest = 9 $LevelSummary = 6 $LevelTestFailure = 9 } elseif ($Scope -eq 'DomainControllers') { $Level = 9 $LevelTest = 12 $LevelSummary = 9 $LevelTestFailure = 12 } else {} if ($Domain -and $DomainController) { $SummaryText = "Domain $Domain, $DomainController" } elseif ($Domain) { Write-Color $SummaryText = "Domain $Domain" } else { $SummaryText = "Forest" } [bool] $IsDomainRoot = $ForestInformation.Name -eq $Domain Out-Informative -Text $SummaryText -Status $null -ExtendedValue '' -Domain $Domain -DomainController $DomainController -Level ($LevelSummary - 3) $TestsSummaryTogether = @(foreach ($Source in ($($Script:TestimoConfiguration.ActiveDirectory)).Keys) { if ($Scope -ne $Script:TestimoConfiguration.ActiveDirectory[$Source].Scope) { continue } $CurrentSection = $Script:TestimoConfiguration.ActiveDirectory[$Source] if ($null -eq $CurrentSection) { Write-Warning "Source $Source in scope: $Scope is defined improperly. Please verify." continue } if ($CurrentSection['Enable'] -eq $true) { $Time = Start-TimeLog $CurrentSource = $CurrentSection['Source'] [Array] $AllTests = $CurrentSection['Tests'].Keys $ReferenceID = $Source $TestsSummary = [PSCustomobject] @{Passed = 0 Failed = 0 Skipped = 0 Total = 0 } $TestOutput = [ordered] @{Name = $CurrentSource['Name'] SourceCode = $CurrentSource['Data'] Details = $CurrentSource['Details'] Results = [System.Collections.Generic.List[PSCustomObject]]::new() Domain = $Domain DomainController = $DomainController } if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID] = $TestOutput } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID] = $TestOutput } else { $Script:Reporting['Forest']['Tests'][$ReferenceID] = $TestOutput } if (-not $Script:Reporting['BySource'][$Source]) { $Script:Reporting['BySource'][$Source] = [System.Collections.Generic.List[PSCustomObject]]::new() } $Script:Reporting['BySource'][$Source].Add($TestOutput) if (-not $CurrentSection['Source']) { Write-Warning "Source $Source in scope: $Scope is defined improperly. Please verify." continue } if ($CurrentSource['Requirements']) { if ($null -ne $CurrentSource['Requirements']['IsDomainRoot']) { if (-not $CurrentSource['Requirements']['IsDomainRoot'] -eq $IsDomainRoot) { Out-Skip -Test $CurrentSource['Name'] -DomainController $DomainController -Domain $Domain -TestsSummary $TestsSummary -Source $ReferenceID -Level $Level continue } } if ($null -ne $CurrentSource['Requirements']['IsPDC']) { if (-not $CurrentSource['Requirements']['IsPDC'] -eq $IsPDC) { Out-Skip -Test $CurrentSource['Name'] -DomainController $DomainController -Domain $Domain -TestsSummary $TestsSummary -Source $ReferenceID -Level $Level continue } } if ($null -ne $CurrentSource['Requirements']['OperatingSystem']) {} if ($null -ne $CurrentSource['Requirements']['CommandAvailable']) { [Array] $Commands = foreach ($Command in $CurrentSource['Requirements']['CommandAvailable']) { $OutputCommand = Get-Command -Name $Command -ErrorAction SilentlyContinue if (-not $OutputCommand) { $false } } if ($Commands -contains $false) { $CommandsTested = $CurrentSource['Requirements']['CommandAvailable'] -join ', ' Out-Skip -Test $CurrentSource['Name'] -DomainController $DomainController -Domain $Domain -TestsSummary $TestsSummary -Source $ReferenceID -Level $Level -Reason "Skipping - At least one command unavailable ($CommandsTested)" continue } } if ($null -ne $CurrentSource['Requirements']['IsInternalForest']) { if ($CurrentSource['Requirements']['IsInternalForest'] -eq $true) { if ($ForestName) { Out-Skip -Test $CurrentSource['Name'] -DomainController $DomainController -Domain $Domain -TestsSummary $TestsSummary -Source $ReferenceID -Level $Level -Reason "Skipping - External forest requested. Not supported test." continue } } } } Out-Informative -Text $CurrentSource['Name'] -Level $Level -Domain $Domain -DomainController $DomainController -Start if ($CurrentSource['Parameters']) { $SourceParameters = $CurrentSource['Parameters'] } else { $SourceParameters = @{} } $SourceParameters['DomainController'] = $DomainController if ($Scope -eq 'Forest') { $SourceParameters['QueryServer'] = $ForestDetails['QueryServers']['Forest']['HostName'][0] } else { $SourceParameters['QueryServer'] = $ForestDetails['QueryServers'][$Domain]['HostName'][0] } $SourceParameters['Domain'] = $Domain $SourceParameters['ForestDetails'] = $ForestDetails $SourceParameters['ForestName'] = $ForestInformation.Name $SourceParameters['DomainInformation'] = $DomainInformation $SourceParameters['ForestInformation'] = $ForestInformation $SourceParameters['SkipRODC'] = $SkipRODC.IsPresent if ($Script:TestimoConfiguration.Debug.ShowErrors) { & $CurrentSource['Data'] -DomainController $DomainController -Domain $Domain $ErrorMessage = $null } else { $OutputInvoke = Invoke-CommandCustom -ScriptBlock $CurrentSource['Data'] -Parameter $SourceParameters -ReturnVerbose -ReturnError -ReturnWarning -AddParameter if ($OutputInvoke.Error) { $ErrorMessage = $OutputInvoke.Error.Exception.Message -replace "`n", " " -replace "`r", " " } else { $ErrorMessage = $null } } $WarningsAndErrors = @(foreach ($War in $OutputInvoke.Warning) { [PSCustomObject] @{Type = 'Warning' Comment = $War Reason = '' TargetName = '' } } foreach ($Err in $OutputInvoke.Error) { [PSCustomObject] @{Type = 'Error' Comment = $Err Reason = $Err.CategoryInfo.Reason TargetName = $Err.CategoryInfo.TargetName } }) Out-Informative -Text $CurrentSource['Name'] -Status $null -ExtendedValue $null -Domain $Domain -DomainController $DomainController -End $Object = $OutputInvoke.Output if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Data'] = $Object $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Verbose'] = $OutputInvoke.Verbose $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Warning'] = $OutputInvoke.Warning $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Error'] = $OutputInvoke.Error $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['WarningsAndErrors'] = $WarningsAndErrors $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['DetailsTests'] = [ordered]@{} $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['ResultsTests'] = [ordered]@{} $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Information'] = $CurrentSection } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Data'] = $Object $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Verbose'] = $OutputInvoke.Verbose $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Warning'] = $OutputInvoke.Warning $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Error'] = $OutputInvoke.Error $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['WarningsAndErrors'] = $WarningsAndErrors $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['DetailsTests'] = [ordered]@{} $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['ResultsTests'] = [ordered]@{} $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Information'] = $CurrentSection } else { $Script:Reporting['Forest']['Tests'][$ReferenceID]['Data'] = $Object $Script:Reporting['Forest']['Tests'][$ReferenceID]['Verbose'] = $OutputInvoke.Verbose $Script:Reporting['Forest']['Tests'][$ReferenceID]['Warning'] = $OutputInvoke.Warning $Script:Reporting['Forest']['Tests'][$ReferenceID]['Error'] = $OutputInvoke.Error $Script:Reporting['Forest']['Tests'][$ReferenceID]['WarningsAndErrors'] = $WarningsAndErrors $Script:Reporting['Forest']['Tests'][$ReferenceID]['DetailsTests'] = [ordered]@{} $Script:Reporting['Forest']['Tests'][$ReferenceID]['ResultsTests'] = [ordered]@{} $Script:Reporting['Forest']['Tests'][$ReferenceID]['Information'] = $CurrentSection } if ($ErrorMessage) { $FailAllTests = $true $ExtendedValue = $ErrorMessage -join "; " Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue $ExtendedValue -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Failed = $TestsSummary.Failed + 1 } elseif ($Object -and $CurrentSource['ExpectedOutput'] -eq $true) { $FailAllTests = $false Out-Begin -Text $CurrentSource['Name'] -Level $LevelTest -Domain $Domain -DomainController $DomainController Out-Status -Text $CurrentSource['Name'] -Status $true -ExtendedValue 'Data is available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Passed = $TestsSummary.Passed + 1 } elseif ($Object -and $CurrentSource['ExpectedOutput'] -eq $false) { $FailAllTests = $false Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue 'Data is available. This is a bad thing.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Failed = $TestsSummary.Failed + 1 } elseif ($Object -and $null -eq $CurrentSource['ExpectedOutput']) { $FailAllTests = $false Out-Begin -Text $CurrentSource['Name'] -Level $LevelTest -Domain $Domain -DomainController $DomainController Out-Status -Text $CurrentSource['Name'] -Status $null -ExtendedValue 'Data is available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Skipped = $TestsSummary.Skipped + 1 } elseif ($null -eq $Object -and $CurrentSource['ExpectedOutput'] -eq $true) { $FailAllTests = $true Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue 'No data available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Failed = $TestsSummary.Failed + 1 } elseif ($null -eq $Object -and $CurrentSource['ExpectedOutput'] -eq $false) { $FailAllTests = $false Out-Begin -Text $CurrentSource['Name'] -Level $LevelTest -Domain $Domain -DomainController $DomainController Out-Status -Text $CurrentSource['Name'] -Status $true -ExtendedValue 'No data returned, which is a good thing.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Passed = $TestsSummary.Passed + 1 } elseif ($null -eq $Object -and $null -eq $CurrentSource['ExpectedOutput']) { $FailAllTests = $false Out-Begin -Text $CurrentSource['Name'] -Level $LevelTest -Domain $Domain -DomainController $DomainController Out-Status -Text $CurrentSource['Name'] -Status $null -ExtendedValue 'No data returned.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Skipped = $TestsSummary.Skipped + 1 } else { $FailAllTests = $true Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue 'No data available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $CurrentSource $TestsSummary.Failed = $TestsSummary.Failed + 1 } foreach ($Test in $AllTests) { if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['DetailsTests'][$Test] = $CurrentSection['Tests'][$Test]['Details'] } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['DetailsTests'][$Test] = $CurrentSection['Tests'][$Test]['Details'] } else { $Script:Reporting['Forest']['Tests'][$ReferenceID]['DetailsTests'][$Test] = $CurrentSection['Tests'][$Test]['Details'] } $CurrentTest = $CurrentSection['Tests'][$Test] if ($CurrentTest['Enable'] -eq $True) { if ($CurrentTest['Requirements']) { if ($null -ne $CurrentTest['Requirements']['IsDomainRoot']) { if (-not $CurrentTest['Requirements']['IsDomainRoot'] -eq $IsDomainRoot) { $TestsSummary.Skipped = $TestsSummary.Skipped + 1 continue } } if ($null -ne $CurrentTest['Requirements']['IsPDC']) { if (-not $CurrentTest['Requirements']['IsPDC'] -eq $IsPDC) { $TestsSummary.Skipped = $TestsSummary.Skipped + 1 continue } } } if (-not $FailAllTests) { $testStepOneSplat = @{Test = $CurrentTest Object = $Object Domain = $Domain DomainController = $DomainController Level = $LevelTest TestName = $CurrentTest['Name'] ReferenceID = $ReferenceID Requirements = $CurrentTest['Requirements'] } if ($Scope -eq 'Forest') { $testStepOneSplat['QueryServer'] = $ForestDetails['QueryServers']['Forest']['HostName'][0] } else { $testStepOneSplat['QueryServer'] = $ForestDetails['QueryServers'][$Domain]['HostName'][0] } $testStepOneSplat['ForestDetails'] = $ForestDetails $testStepOneSplat['ForestName'] = $ForestInformation.Name $testStepOneSplat['DomainInformation'] = $DomainInformation $testStepOneSplat['ForestInformation'] = $ForestInformation $TestsResults = Test-StepOne @testStepOneSplat $TestsSummary.Passed = $TestsSummary.Passed + ($TestsResults | Where-Object { $_ -eq $true }).Count $TestsSummary.Failed = $TestsSummary.Failed + ($TestsResults | Where-Object { $_ -eq $false }).Count } else { $TestsResults = $null $TestsSummary.Failed = $TestsSummary.Failed + 1 Out-Failure -Text $CurrentTest['Name'] -Level $LevelTestFailure -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -ExtendedValue 'Input data not provided. Failing test.' -Source $CurrentTest } if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['ResultsTests'][$Test] = $TestsResults } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['ResultsTests'][$Test] = $TestsResults } else { $Script:Reporting['Forest']['Tests'][$ReferenceID]['ResultsTests'][$Test] = $TestsResults } } else { $TestsSummary.Skipped = $TestsSummary.Skipped + 1 } } $TestsSummary.Total = $TestsSummary.Failed + $TestsSummary.Passed + $TestsSummary.Skipped $TestsSummary Out-Summary -Text $CurrentSource['Name'] -Time $Time -Level $LevelSummary -Domain $Domain -DomainController $DomainController -TestsSummary $TestsSummary } } if ($Execute) { & $Execute }) $TestsSummaryFinal = [PSCustomObject] @{Passed = ($TestsSummaryTogether.Passed | Measure-Object -Sum).Sum Failed = ($TestsSummaryTogether.Failed | Measure-Object -Sum).Sum Skipped = ($TestsSummaryTogether.Skipped | Measure-Object -Sum).Sum Total = ($TestsSummaryTogether.Total | Measure-Object -Sum).Sum } $TestsSummaryFinal if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Summary'] = $TestsSummaryFinal } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Summary'] = $TestsSummaryFinal } else { $Script:Reporting['Summary'] = $TestsSummaryFinal } Out-Summary -Text $SummaryText -Time $GlobalTime -Level ($LevelSummary - 3) -Domain $Domain -DomainController $DomainController -TestsSummary $TestsSummaryFinal } function Test-StepOne { [CmdletBinding()] param([System.Collections.IDictionary] $Test, [string] $Domain, [string] $DomainController, [Array] $Object, [string] $TestName, [int] $Level, [string] $ReferenceID, [System.Collections.IDictionary] $Requirements, [string] $QueryServer, [System.Collections.IDictionary] $ForestDetails, [object] $DomainInformation, [object] $ForestInformation, [string] $ForestName) [string] $OperationType = $Test.Parameters.OperationType if ($OperationType -eq '') { $OperationType = 'eq' } [string[]] $Property = $Test.Parameters.Property [string[]] $PropertyExtendedValue = $Test.Parameters.PropertyExtendedValue [Object] $ExpectedValue = $Test.Parameters.ExpectedValue [nullable[int]] $ExpectedCount = $Test.Parameters.ExpectedCount [scriptblock] $OverwriteName = $Test.Parameters.OverwriteName [scriptblock] $WhereObject = $Test.Parameters.WhereObject [nullable[bool]] $ExpectedResult = $Test.Parameters.ExpectedResult [nullable[bool]] $ExpectedOutput = $Test.Parameters.ExpectedOutput [string] $OperationResult = $Test.Parameters.OperationResult if ($Object) { if ($WhereObject) { $Object = $Object | Where-Object $WhereObject } if ($null -ne $Requirements) { if ($null -ne $Requirements['ExpectedOutput']) {} } if ($null -eq $ExpectedCount) { if ($null -eq $Object) { if ($ExpectedResult -eq $false) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $true -ExtendedValue "Data is not available. This is expected." -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $true } elseif ($ExpectedResult -eq $true) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $false -ExtendedValue 'Data is not available. This is not expected.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $false } if ($null -eq $ExpectedOutput -or $ExpectedOutput -eq $true) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $false -ExtendedValue 'Data is not available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $false } elseif ($ExpectedOutput -eq $false) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $true -ExtendedValue "Data is not available, but it's not required." -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $true } } else { if ($ExpectedResult -eq $false) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $false -ExtendedValue 'Data is available. This is not expected.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $false } elseif ($ExpectedResult -eq $true) { Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $TestName -Status $true -ExtendedValue "Data is available. This is expected." -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $true } } } if ($null -ne $ExpectedCount) { if ($OverwriteName) { $TestName = & $OverwriteName } Test-StepTwo -Test $Test -Object $Object -ExpectedCount $ExpectedCount -OperationType $OperationType -TestName $TestName -Level $Level -Domain $Domain -DomainController $DomainController -Property $Property -ExpectedValue $ExpectedValue -PropertyExtendedValue $PropertyExtendedValue -OperationResult $OperationResult -ReferenceID $ReferenceID -ExpectedOutput $ExpectedOutput } else { if ($Test.Parameters.Bundle -eq $true) { Test-StepTwo -Test $Test -Object $Object -OperationType $OperationType -TestName $TestName -Level $Level -Domain $Domain -DomainController $DomainController -Property $Property -ExpectedValue $ExpectedValue -PropertyExtendedValue $PropertyExtendedValue -OperationResult $OperationResult -ReferenceID $ReferenceID -ExpectedOutput $ExpectedOutput } else { foreach ($_ in $Object) { if ($OverwriteName) { $TestName = & $OverwriteName } Test-StepTwo -Test $Test -Object $_ -OperationType $OperationType -TestName $TestName -Level $Level -Domain $Domain -DomainController $DomainController -Property $Property -ExpectedValue $ExpectedValue -PropertyExtendedValue $PropertyExtendedValue -OperationResult $OperationResult -ReferenceID $ReferenceID -ExpectedOutput $ExpectedOutput } } } } } function Test-StepTwo { [CmdletBinding()] param([System.Collections.IDictionary] $Test, [string] $Domain, [string] $DomainController, [Array] $Object, [string] $TestName, [string] $OperationType, [int] $Level, [string[]] $Property, [string[]] $PropertyExtendedValue, [Array] $ExpectedValue, [nullable[int]] $ExpectedCount, [string] $OperationResult, [string] $ReferenceID, [nullable[bool]] $ExpectedOutput) Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController $ScriptBlock = { $Operators = @{'lt' = 'Less Than' 'gt' = 'Greater Than' 'le' = 'Less Or Equal' 'ge' = 'Greater Or Equal' 'eq' = 'Equal' 'contains' = 'Contains' 'notcontains' = 'Not contains' 'like' = 'Like' 'match' = 'Match' 'notmatch' = 'Not match' 'notin' = 'Not in' 'in' = 'Either Value' } [Object] $TestedValue = $Object foreach ($V in $Property) { $TestedValue = $TestedValue.$V } if ($null -ne $ExpectedCount) { if ($null -eq $Object) { $TestedValueCount = 0 } else { $TestedValueCount = $TestedValue.Count } if ($OperationType -eq 'lt') { $TestResult = $TestedValueCount -lt $ExpectedCount } elseif ($OperationType -eq 'gt') { $TestResult = $TestedValueCount -gt $ExpectedCount } elseif ($OperationType -eq 'ge') { $TestResult = $TestedValueCount -ge $ExpectedCount } elseif ($OperationType -eq 'le') { $TestResult = $TestedValueCount -le $ExpectedCount } elseif ($OperationType -eq 'like') { $TestResult = $TestedValueCount -like $ExpectedCount } elseif ($OperationType -eq 'contains') { $TestResult = $TestedValueCount -contains $ExpectedCount } elseif ($OperationType -eq 'in') { $TestResult = $ExpectedCount -in $TestedValueCount } elseif ($OperationType -eq 'notin') { $TestResult = $ExpectedCount -notin $TestedValueCount } else { $TestResult = $TestedValueCount -eq $ExpectedCount } $TextTestedValue = $TestedValueCount $TextExpectedValue = $ExpectedCount } elseif ($null -ne $ExpectedValue) { $OutputValues = [System.Collections.Generic.List[Object]]::new() if ($null -eq $TestedValue -and $null -ne $ExpectedValue) { $TestResult = for ($i = 0; $i -lt $ExpectedValue.Count; $i++) { $false if ($ExpectedValue[$i] -is [string] -and $ExpectedValue[$i] -like '*Get-Date*') { [scriptblock] $DateConversion = [scriptblock]::Create($ExpectedValue[$i]) $CompareValue = & $DateConversion } else { $CompareValue = $ExpectedValue[$I] } $OutputValues.Add($CompareValue) } $TextExpectedValue = $OutputValues -join ', ' $TextTestedValue = 'Null' } else { [Array] $TestResult = @(if ($OperationType -eq 'notin') { $ExpectedValue -notin $TestedValue $TextExpectedValue = $ExpectedValue } elseif ($OperationType -eq 'in') { $TestedValue -in $ExpectedValue $TextExpectedValue = $ExpectedValue -join ' or ' } else { for ($i = 0; $i -lt $ExpectedValue.Count; $i++) { if ($ExpectedValue[$i] -is [string] -and $ExpectedValue[$i] -like '*Get-Date*') { [scriptblock] $DateConversion = [scriptblock]::Create($ExpectedValue[$i]) $CompareValue = & $DateConversion } else { $CompareValue = $ExpectedValue[$I] } if ($OperationType -eq 'lt') { $TestedValue -lt $CompareValue } elseif ($OperationType -eq 'gt') { $TestedValue -gt $CompareValue } elseif ($OperationType -eq 'ge') { $TestedValue -ge $CompareValue } elseif ($OperationType -eq 'le') { $TestedValue -le $CompareValue } elseif ($OperationType -eq 'like') { $TestedValue -like $CompareValue } elseif ($OperationType -eq 'contains') { $TestedValue -contains $CompareValue } elseif ($OperationType -eq 'notcontains') { $TestedValue -notcontains $CompareValue } elseif ($OperationType -eq 'match') { $TestedValue -match $CompareValue } elseif ($OperationType -eq 'notmatch') { $TestedValue -notmatch $CompareValue } else { $TestedValue -eq $CompareValue } $OutputValues.Add($CompareValue) } $TextExpectedValue = $OutputValues -join ', ' } $TextTestedValue = $TestedValue) } } else { if ($ExpectedOutput -eq $false) { [Array] $TestResult = @(if ($null -eq $TestedValue) { $true } else { $false }) $TextExpectedValue = 'No output' } else { $TestResult = $null $ExtendedTextValue = "Test provided but no tests required." } } if ($null -eq $TestResult) { $ReportResult = $null $ReportExtended = $ExtendedTextValue } else { if ($OperationResult -eq 'OR') { if ($TestResult -contains $true) { $ReportResult = $true $ReportExtended = "Expected value ($($Operators[$OperationType])): $($TextExpectedValue)" } else { $ReportResult = $false if ($Test.Parameters.DisplayResult -ne $false) { $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue, Found value: $($TextTestedValue)" } else { $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue" } } } else { if ($TestResult -notcontains $false) { $ReportResult = $true $ReportExtended = "Expected value ($($Operators[$OperationType])): $($TextExpectedValue)" } else { $ReportResult = $false if ($Test.Parameters.DisplayResult -ne $false) { $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue, Found value: $($TextTestedValue)" } else { $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue" } } } } if ($PropertyExtendedValue.Count -gt 0) { $ReportExtended = $Object foreach ($V in $PropertyExtendedValue) { $ReportExtended = $ReportExtended.$V } $ReportExtended = $ReportExtended -join ', ' } Out-Status -Text $TestName -Status $ReportResult -ExtendedValue $ReportExtended -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $ReportResult } if ($Script:TestimoConfiguration.Debug.ShowErrors) { & $ScriptBlock } else { try { & $ScriptBlock } catch { Out-Status -Text $TestName -Status $false -ExtendedValue $_.Exception.Message -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Test $Test return $False } } } $Script:TestimoConfiguration = [ordered] @{Exclusions = [ordered] @{Domains = @() DomainControllers = @() } Inclusions = [ordered] @{Domains = @() DomainControllers = @() } ActiveDirectory = [ordered]@{ForestBackup = $Backup ForestReplication = $Replication ForestReplicationStatus = $ReplicationStatus ForestOptionalFeatures = $OptionalFeatures ForestSites = $Sites ForestSiteLinks = $SiteLinks ForestSiteLinksConnections = $SiteLinksConnections ForestRoles = $ForestFSMORoles ForestSubnets = $ForestSubnets ForestOrphanedAdmins = $OrphanedAdmins ForestTombstoneLifetime = $TombstoneLifetime ForestTrusts = $Trusts ForestConfigurationPartitionOwners = $ForestConfigurationPartitionOwners ForestConfigurationPartitionOwnersContainers = $ForestConfigurationPartitionOwnersContainers DomainLDAP = $DomainLDAP DomainDomainControllers = $DomainDomainControllers DomainRoles = $DomainFSMORoles DomainWellKnownFolders = $WellKnownFolders DomainPasswordComplexity = $PasswordComplexity DomainGroupPolicyAssesment = $GroupPolicyAssesment DomainGroupPolicyPermissions = $GroupPolicyPermissions DomainGroupPolicyPermissionConsistency = $GroupPolicyPermissionConsistency DomainGroupPolicyOwner = $GroupPolicyOwner DomainGroupPolicyADM = $GroupPolicyADM DomainGroupPolicySysvol = $GroupPolicySysvol DomainOrphanedForeignSecurityPrincipals = $OrphanedForeignSecurityPrincipals DomainOrganizationalUnitsEmpty = $OrganizationalUnitsEmpty DomainOrganizationalUnitsProtected = $OrganizationalUnitsProtected DomainNetLogonOwner = $NetLogonOwner DomainDNSScavengingForPrimaryDNSServer = $DNSScavengingForPrimaryDNSServer DomainDNSForwaders = $DNSForwaders DomainDnsZonesAging = $DnsZonesAging DomainSecurityGroupsAccountOperators = $SecurityGroupsAccountOperators DomainSecurityGroupsSchemaAdmins = $SecurityGroupsSchemaAdmins DomainSecurityUsers = $SecurityUsers DomainSecurityUsersAcccountAdministrator = $SecurityUsersAcccountAdministrator DomainSecurityKRBGT = $SecurityKRBGT DomainSysVolDFSR = $SysVolDFSR DomainDNSZonesForest0ADEL = $DNSZonesForest0ADEL DomainDNSZonesDomain0ADEL = $DNSZonesDomain0ADEL DomainDHCPAuthorized = $DHCPAuthorized DomainComputersUnsupported = $ComputersUnsupported DomainComputersUnsupportedMainstream = $ComputersUnsupportedMainstream DomainExchangeUsers = $ExchangeUsers DomainDuplicateObjects = $DuplicateObjects DCInformation = $Information DCWindowsRemoteManagement = $WindowsRemoteManagement DCEventLogs = $EventLogs DCOperatingSystem = $OperatingSystem DCServices = $Services DCLDAP = $LDAP DCLDAPInsecureBindings = $LDAPInsecureBindings DCPingable = $Pingable DCPorts = $Ports DCRDPPorts = $RDPPorts DCRDPSecurity = $RDPSecurity DCDiskSpace = $DiskSpace DCTimeSettings = $TimeSettings DCTimeSynchronizationInternal = $TimeSynchronizationInternal DCTimeSynchronizationExternal = $TimeSynchronizationExternal DCNetworkCardSettings = $NetworkCardSettings DCWindowsUpdates = $WindowsUpdates DCWindowsRolesAndFeatures = $WindowsRolesAndFeatures DCDnsResolveInternal = $DNSResolveInternal DCDnsResolveExternal = $DNSResolveExternal DCDnsNameServes = $DNSNameServers DCSMBProtocols = $SMBProtocols DCSMBShares = $SMBShares DCSMBSharesPermissions = $SMBSharesPermissions DCDFS = $DFS DCNTDSParameters = $NTDSParameters DCGroupPolicySYSVOLDC = $GroupPolicySYSVOLDC DCLanManagerSettings = $LanManagerSettings DCDiagnostics = $Diagnostics DCLanManServer = $LanManServer DCMSSLegacy = $MSSLegacy DCFileSystem = $FileSystem DCNetSessionEnumeration = $NetSessionEnumeration DCServiceWINRM = $ServiceWINRM DCUNCHardenedPaths = $UNCHardenedPaths DCDNSForwaders = $DCDNSForwaders } Debug = [ordered] @{ShowErrors = $false } } function Get-TestimoConfiguration { [CmdletBinding()] param([switch] $AsJson, [string] $FilePath) $NewConfig = [ordered] @{} foreach ($Source in ($Script:TestimoConfiguration.ActiveDirectory).Keys) { $NewConfig[$Source] = [ordered] @{} $NewConfig[$Source]['Enable'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Enable'] if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['ExpectedOutput']) { $NewConfig[$Source]['Source'] = [ordered] @{} $NewConfig[$Source]['Source']['ExpectedOutput'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['ExpectedOutput'] } if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['Parameters']) { $NewConfig[$Source]['Source'] = [ordered] @{} $NewConfig[$Source]['Source']['Parameters'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['Parameters'] } $NewConfig[$Source]['Tests'] = [ordered] @{} foreach ($Test in $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'].Keys) { $NewConfig[$Source]['Tests'][$Test] = [ordered] @{} $NewConfig[$Source]['Tests'][$Test]['Enable'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Enable'] $NewConfig[$Source]['Tests'][$Test]['Parameters'] = [ordered] @{} if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']) { if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property']) { if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property']) { $NewConfig[$Source]['Tests'][$Test]['Parameters']['Property'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property'] } if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedValue']) { $NewConfig[$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] } if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedCount']) { $NewConfig[$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] } if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['OperationType']) { $NewConfig[$Source]['Tests'][$Test]['Parameters']['OperationType'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['OperationType'] } } } } } if ($FilePath) { $NewConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $FilePath return } if ($AsJSON) { return $NewConfig | ConvertTo-Json -Depth 10 } return $NewConfig } function Get-TestimoSources { [CmdletBinding()] param([string[]] $Sources, [switch] $SourcesOnly, [switch] $Enabled, [switch] $Advanced) if (-not $Sources) { $Sources = $Script:TestimoConfiguration.ActiveDirectory.Keys } if ($SourcesOnly) { return $Sources } foreach ($S in $Sources) { $Object = [ordered]@{Source = $S Scope = $Script:TestimoConfiguration.ActiveDirectory[$S].Scope Name = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Name Tests = $Script:TestimoConfiguration.ActiveDirectory[$S].Tests.Keys } $Object['Area'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Area $Object['Category'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Category $Object['Tags'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Tags $Object['Severity'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Severity $Object['RiskLevel'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.RiskLevel $Object['Description'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Description $Object['Resolution'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Resolution $Object['Resources'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Resources if ($Advanced) { $Object['Advanced'] = $Script:TestimoConfiguration.ActiveDirectory[$S] } [PSCustomObject] $Object } } function Import-PrivateModule { [cmdletBinding()] param([string] $Name, [switch] $Portable) try { $ADModule = Import-Module -PassThru -Name $Name -ErrorAction Stop } catch { if ($_.Exception.Message -like '*was not loaded because no valid module file was found in any module directory*') { $Module = Get-Module -Name $Name if ($Module) { $ADModule = Import-Module $Module -PassThru } } } $ADModule } function Invoke-Testimo { [alias('Test-ImoAD', 'Test-IMO')] [CmdletBinding()] param([ValidateScript( { $_ -in (& $SourcesAutoCompleter) })] [alias('Type')][string[]] $Sources, [ValidateScript( { $_ -in (& $SourcesAutoCompleter) })] [alias('ExludeType')] [string[]] $ExcludeSources, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [string[]] $IncludeDomains, [string[]] $IncludeDomainControllers, [string] $ForestName, [alias('ReturnResults')][switch] $PassThru, [switch] $ShowErrors, [switch] $ExtendedResults, [Object] $Configuration, [alias('ReportPath')][string] $FilePath, [Parameter(DontShow)][switch] $ShowReport, [switch] $HideHTML, [alias('HideSolution')][switch] $HideSteps, [alias('AlwaysShowSolution')][switch] $AlwaysShowSteps, [switch] $SkipRODC, [switch] $Online, [string[]] $ExternalTests) if (-not $Script:DefaultSources) { $Script:DefaultSources = Get-TestimoSources -Enabled -SourcesOnly } else { Set-TestsStatus -Sources $Script:DefaultSources } if ($ShowReport) { Write-Warning "Invoke-Testimo - Paramter ShowReport is deprecated. By default HTML report will open up after running Testimo. If you want to prevent that, use HideHTML switch instead. This message and parameter will be removed in future releases." } $Script:Reporting = [ordered] @{} $Script:Reporting['Version'] = '' $Script:Reporting['Errors'] = [System.Collections.Generic.List[PSCustomObject]]::new() $Script:Reporting['Results'] = $null $Script:Reporting['Summary'] = [ordered] @{} $Script:Reporting['Forest'] = [ordered] @{} $Script:Reporting['Forest']['Summary'] = $null $Script:Reporting['Forest']['Tests'] = [ordered] @{} $Script:Reporting['Domains'] = [ordered] @{} $Script:Reporting['BySource'] = [ordered] @{} $TestimoVersion = Get-Command -Name 'Invoke-Testimo' -ErrorAction SilentlyContinue $ProgressPreference = 'SilentlyContinue' [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases") $ProgressPreference = 'Continue' $LatestVersion = $GitHubReleases[0] if (-not $LatestVersion.Errors) { if ($TestimoVersion.Version -eq $LatestVersion.Version) { $Script:Reporting['Version'] = "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)" } elseif ($TestimoVersion.Version -lt $LatestVersion.Version) { $Script:Reporting['Version'] = "Current: $($TestimoVersion.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?" } elseif ($TestimoVersion.Version -gt $LatestVersion.Version) { $Script:Reporting['Version'] = "Current: $($TestimoVersion.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!" } } else { $Script:Reporting['Version'] = "Current: $($TestimoVersion.Version)" } Out-Informative -OverrideTitle 'Testimo' -Text 'Version' -Level 0 -Status $null -ExtendedValue $Script:Reporting['Version'] Import-TestimoConfiguration -Configuration $Configuration $global:ProgressPreference = 'SilentlyContinue' $global:ErrorActionPreference = 'Stop' $Script:TestResults = [System.Collections.Generic.List[PSCustomObject]]::new() $Script:TestimoConfiguration.Debug.ShowErrors = $ShowErrors $Script:TestimoConfiguration.Exclusions.Domains = $ExcludeDomains $Script:TestimoConfiguration.Exclusions.DomainControllers = $ExcludeDomainControllers $Script:TestimoConfiguration.Inclusions.Domains = $IncludeDomains $Script:TestimoConfiguration.Inclusions.DomainControllers = $IncludeDomainControllers if (-not $Sources) { $Sources = $Script:DefaultSources } Set-TestsStatus -Sources $Sources -ExcludeSources $ExcludeSources if ($Script:TestimoConfiguration.Inclusions.Domains) { Out-Informative -Text 'Only following Domains will be scanned' -Level 0 -Status $null -ExtendedValue ($Script:TestimoConfiguration.Inclusions.Domains -join ', ') } if ($Script:TestimoConfiguration.Inclusions.DomainControllers) { Out-Informative -Text 'Only following Domain Controllers will be scanned' -Level 0 -Status $null -ExtendedValue ($Script:TestimoConfiguration.Inclusions.DomainControllers -join ', ') } if ($Script:TestimoConfiguration.Exclusions.Domains -and -not $Script:TestimoConfiguration.Inclusions.Domains) { Out-Informative -Text 'Following Domains will be ignored' -Level 0 -Status $null -ExtendedValue ($Script:TestimoConfiguration.Exclusions.Domains -join ', ') } if ($Script:TestimoConfiguration.Exclusions.DomainControllers -and -not $Script:TestimoConfiguration.Inclusions.DomainControllers) { Out-Informative -Text 'Following Domain Controllers will be ignored' -Level 0 -Status $null -ExtendedValue ($Script:TestimoConfiguration.Exclusions.DomainControllers -join ', ') } $ForestDetails = Get-WinADForestDetails -Forest $ForestName -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains -IncludeDomainControllers $IncludeDomainControllers -ExcludeDomainControllers $ExcludeDomainControllers -SkipRODC:$SkipRODC -Extended $null = Start-Testing -Scope 'Forest' -ForestInformation $ForestDetails.Forest -ForestDetails $ForestDetails -SkipRODC:$SkipRODC { foreach ($Domain in $ForestDetails.Domains) { $Script:Reporting['Domains'][$Domain] = [ordered] @{} $Script:Reporting['Domains'][$Domain]['Summary'] = [ordered] @{} $Script:Reporting['Domains'][$Domain]['Tests'] = [ordered] @{} $Script:Reporting['Domains'][$Domain]['DomainControllers'] = [ordered] @{} if ($ForestDetails['DomainsExtended']["$Domain"]) { Start-Testing -Scope 'Domain' -Domain $Domain -DomainInformation $ForestDetails['DomainsExtended']["$Domain"] -ForestInformation $ForestDetails.Forest -ForestDetails $ForestDetails -SkipRODC:$SkipRODC { if (Get-TestimoSourcesStatus -Scope 'DC') { foreach ($DC in $ForestDetails['DomainDomainControllers'][$Domain]) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.HostName] = [ordered] @{} $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.HostName]['Summary'] = [ordered] @{} $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.HostName]['Tests'] = [ordered] @{} Start-Testing -Scope 'DC' -Domain $Domain -DomainController $DC.HostName -IsPDC $DC.IsPDC -DomainInformation $ForestDetails['DomainsExtended']["$Domain"] -ForestInformation $ForestDetails.Forest -ForestDetails $ForestDetails } } } } } } $Script:Reporting['Results'] = $Script:TestResults if ($PassThru -and $ExtendedResults) { $Script:Reporting } else { if ($PassThru) { $Script:TestResults } } if (-not $FilePath) { $FilePath = Get-FileName -Extension 'html' -Temporary } $Time = Start-TimeLog Out-Informative -OverrideTitle 'Testimo' -Text 'HTML Report Generation Started' -Level 0 -Status $null Start-TestimoReport -FilePath $FilePath -Online:$Online -ShowHTML:(-not $HideHTML.IsPresent) -TestResults $Script:Reporting -HideSteps:$HideSteps -AlwaysShowSteps:$AlwaysShowSteps $TimeEnd = Stop-TimeLog -Time $Time Out-Informative -OverrideTitle 'Testimo' -Text "HTML Report Saved to $FilePath" -Level 0 -Status $null -ExtendedValue $TimeEnd } [scriptblock] $SourcesAutoCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $Script:TestimoConfiguration.ActiveDirectory.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" } } Register-ArgumentCompleter -CommandName Invoke-Testimo -ParameterName Sources -ScriptBlock $SourcesAutoCompleter Register-ArgumentCompleter -CommandName Invoke-Testimo -ParameterName ExcludeSources -ScriptBlock $SourcesAutoCompleter Register-ArgumentCompleter -CommandName Get-TestimoSources -ParameterName Sources -ScriptBlock $SourcesAutoCompleter Export-ModuleMember -Function @('Get-TestimoConfiguration', 'Get-TestimoSources', 'Import-PrivateModule', 'Invoke-Testimo') -Alias @('Test-IMO', 'Test-ImoAD') # SIG # Begin signature block # MIIdWQYJKoZIhvcNAQcCoIIdSjCCHUYCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQU5pyFLv/8k3uBXuDD001q/wDm # eTugghhnMIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0B # AQUFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk # IElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg # Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg # +XESpa7cJpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lT # XDGEKvYPmDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5 # a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g # 0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1 # roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf # GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0G # A1UdDgQWBBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLL # gjEtUYunpyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3 # cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmr # EthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+ # fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5Q # Z7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu # 838fYxAe+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw # 8jCCBP4wggPmoAMCAQICEA1CSuC+Ooj/YEAhzhQA8N0wDQYJKoZIhvcNAQELBQAw # cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ # d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk # IElEIFRpbWVzdGFtcGluZyBDQTAeFw0yMTAxMDEwMDAwMDBaFw0zMTAxMDYwMDAw # MDBaMEgxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjEgMB4G # A1UEAxMXRGlnaUNlcnQgVGltZXN0YW1wIDIwMjEwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQDC5mGEZ8WK9Q0IpEXKY2tR1zoRQr0KdXVNlLQMULUmEP4d # yG+RawyW5xpcSO9E5b+bYc0VkWJauP9nC5xj/TZqgfop+N0rcIXeAhjzeG28ffnH # bQk9vmp2h+mKvfiEXR52yeTGdnY6U9HR01o2j8aj4S8bOrdh1nPsTm0zinxdRS1L # sVDmQTo3VobckyON91Al6GTm3dOPL1e1hyDrDo4s1SPa9E14RuMDgzEpSlwMMYpK # jIjF9zBa+RSvFV9sQ0kJ/SYjU/aNY+gaq1uxHTDCm2mCtNv8VlS8H6GHq756Wwog # L0sJyZWnjbL61mOLTqVyHO6fegFz+BnW/g1JhL0BAgMBAAGjggG4MIIBtDAOBgNV # HQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD # CDBBBgNVHSAEOjA4MDYGCWCGSAGG/WwHATApMCcGCCsGAQUFBwIBFhtodHRwOi8v # d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHwYDVR0jBBgwFoAU9LbhIB3+Ka7S5GGlsqIl # ssgXNW4wHQYDVR0OBBYEFDZEho6kurBmvrwoLR1ENt3janq8MHEGA1UdHwRqMGgw # MqAwoC6GLGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtdHMu # Y3JsMDKgMKAuhixodHRwOi8vY3JsNC5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk # LXRzLmNybDCBhQYIKwYBBQUHAQEEeTB3MCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz # cC5kaWdpY2VydC5jb20wTwYIKwYBBQUHMAKGQ2h0dHA6Ly9jYWNlcnRzLmRpZ2lj # ZXJ0LmNvbS9EaWdpQ2VydFNIQTJBc3N1cmVkSURUaW1lc3RhbXBpbmdDQS5jcnQw # DQYJKoZIhvcNAQELBQADggEBAEgc3LXpmiO85xrnIA6OZ0b9QnJRdAojR6OrktIl # xHBZvhSg5SeBpU0UFRkHefDRBMOG2Tu9/kQCZk3taaQP9rhwz2Lo9VFKeHk2eie3 # 8+dSn5On7UOee+e03UEiifuHokYDTvz0/rdkd2NfI1Jpg4L6GlPtkMyNoRdzDfTz # ZTlwS/Oc1np72gy8PTLQG8v1Yfx1CAB2vIEO+MDhXM/EEXLnG2RJ2CKadRVC9S0y # OIHa9GCiurRS+1zgYSQlT7LfySmoc0NR2r1j1h9bm/cuG08THfdKDXF+l7f0P4Tr # weOjSaH6zqe/Vs+6WXZhiV9+p7SOZ3j5NpjhyyjaW4emii8wggUwMIIEGKADAgEC # AhAECRgbX9W7ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVT # MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j # b20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEw # MjIxMjAwMDBaFw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV # BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEi # MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7 # RZmxOttE9X/lqJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p # 0WfTxvspJ8fTeyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj # 6YgsIJWuHEqHCN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grk # V7tKtel05iv+bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHy # DxL0xY4PwaLoLFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMB # AAGjggHNMIIByTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjAT # BgNVHSUEDDAKBggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGG # GGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2Nh # Y2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCB # gQYDVR0fBHoweDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lD # ZXJ0QXNzdXJlZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNl # cnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgG # CmCGSAGG/WwAAgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu # Y29tL0NQUzAKBghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1 # DlgwHwYDVR0jBBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQEL # BQADggEBAD7sDVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q # 3yBVN7Dh9tGSdQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/ # kLEbBw6RFfu6r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dc # IFzZcbEMj7uo+MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6 # dGRrsutmQ9qzsIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT # +hKUGIUukpHqaGxEMrJmoecYpJpkUe8wggUxMIIEGaADAgECAhAKoSXW1jIbfkHk # Bdo2l8IVMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxE # aWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMT # G0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xNjAxMDcxMjAwMDBaFw0z # MTAxMDcxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0 # IFNIQTIgQXNzdXJlZCBJRCBUaW1lc3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB # AQUAA4IBDwAwggEKAoIBAQC90DLuS82Pf92puoKZxTlUKFe2I0rEDgdFM1EQfdD5 # fU1ofue2oPSNs4jkl79jIZCYvxO8V9PD4X4I1moUADj3Lh477sym9jJZ/l9lP+Cb # 6+NGRwYaVX4LJ37AovWg4N4iPw7/fpX786O6Ij4YrBHk8JkDbTuFfAnT7l3ImgtU # 46gJcWvgzyIQD3XPcXJOCq3fQDpct1HhoXkUxk0kIzBdvOw8YGqsLwfM/fDqR9mI # UF79Zm5WYScpiYRR5oLnRlD9lCosp+R1PrqYD4R/nzEU1q3V8mTLex4F0IQZchfx # FwbvPc3WTe8GQv2iUypPhR3EHTyvz9qsEPXdrKzpVv+TAgMBAAGjggHOMIIByjAd # BgNVHQ4EFgQU9LbhIB3+Ka7S5GGlsqIlssgXNW4wHwYDVR0jBBgwFoAUReuir/SS # y4IxLVGLp6chnfNtyA8wEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMC # AYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUF # BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6 # Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5j # cnQwgYEGA1UdHwR6MHgwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwUAYDVR0gBEkw # RzA4BgpghkgBhv1sAAIEMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2lj # ZXJ0LmNvbS9DUFMwCwYJYIZIAYb9bAcBMA0GCSqGSIb3DQEBCwUAA4IBAQBxlRLp # UYdWac3v3dp8qmN6s3jPBjdAhO9LhL/KzwMC/cWnww4gQiyvd/MrHwwhWiq3BTQd # aq6Z+CeiZr8JqmDfdqQ6kw/4stHYfBli6F6CJR7Euhx7LCHi1lssFDVDBGiy23UC # 4HLHmNY8ZOUfSBAYX4k4YU1iRiSHY4yRUiyvKYnleB/WCxSlgNcSR3CzddWThZN+ # tpJn+1Nhiaj1a5bA9FhpDXzIAbG5KHW3mWOFIoxhynmUfln8jA/jb7UBJrZspe6H # USHkWGCbugwtK22ixH67xCUrRwIIfEmuE7bhfEJCKMYYVs9BNLZmXbZ0e/VWMyIv # IjayS6JKldj1po5SMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkq # hkiG9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j # MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT # SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoX # DTIzMDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tp # ZTERMA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlz # IEVWT1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqD # Bqlnr3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfF # jVye3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub # +3tii0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf # 3tZZzO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6 # Ea41zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQAB # o4IBxTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0O # BBYEFBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUE # DDAKBggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdp # Y2VydC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2Ny # bDQuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUw # QzA3BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNl # cnQuY29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNp # Z25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1s # z4lsLARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsR # XPHUF/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHI # NrTCvPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8 # Rj9yG4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6 # o6ESJre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNp # ezQug9ufqExx6lHYDjGCBFwwggRYAgEBMIGGMHIxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAv # BgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EC # EATV3B9I6snYUgC6zZqbKqcwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAI # oAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIB # CzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFKW6yTsuK0ZOwvWXcZ9B # FTV7fqheMA0GCSqGSIb3DQEBAQUABIIBAIB1XKJFan82AXXFJbeISn2ZsgFvIClH # vhBaHOMTBqa9CNEjpLCmbCA2j9mre1JohjplxtdiX0xKGbhcHStrsoZOiiodU/mC # +11/m7c/3MuvkbC0Zgz3pJVvTGDBevz59f/n2PKK0t1trGYqlqBMaYQlg73l/kL+ # Lwr4yAkcIpBRVV/xFRs4V6Kj5SXz8S+RNabdM7kel4F+StFWi12kWnhF+HpGPrwA # DV4RaDOXn9xYxA7x6RZ569KQhQQ0BLI7UHCZWBmNrfttaHDi9/60+9eRRXM//6Fz # jIyfOXBvdbStuM6i810FILrNmWc3XRggUFXR7d4rfUSRzFKe2zQvvBChggIwMIIC # LAYJKoZIhvcNAQkGMYICHTCCAhkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNV # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8G # A1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIFRpbWVzdGFtcGluZyBDQQIQ # DUJK4L46iP9gQCHOFADw3TANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzEL # BgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8XDTIxMDMxNzEyMTkzMVowLwYJKoZI # hvcNAQkEMSIEIOLwxbWR5gYbMYNxCuGrFubto2dceEaxNydOtCrJHE8dMA0GCSqG # SIb3DQEBAQUABIIBADQAphxMzPVwlgOeFM4LtKCagOYjyB2ax7QiTngao4FVKwup # vdwmeiwqllQIeLurKJ3TlI/h1bmKJ0SRO1x9KnyCnFdFE6mLfI4UGRj7H7hwTsCf # KiZGcWar7W+AiCbzB3rSdEeudQcpXXxodPojT3yfr6DN/Rqm5LXr2UN54VX7/5Ls # g3yvITuXcqT+x7rhU6jJb5afUm0Rlv3j4scNuFUWSd1F3uGj94HOaKlj2v6CLJ6U # BvJLtrbzph7caofpNIpKqV0tien4MilCAjLBNnAgEriJdc9ywbtWn4noCSxlg3mG # NPZi98RnxbMnt3QZ7LzvFu5XCNFU/qrAfSqBCO8= # SIG # End signature block |