Testimo.psm1
$DNSForwaders = @{Enable = $true Source = @{Name = "DNS Forwarders" Data = { $PSWinDocumentationDNS = Import-Module PSWinDocumentation.DNS -PassThru & $PSWinDocumentationDNS { param($Domain) $Forwarders = Get-WinDnsServerForwarder -Domain $Domain -WarningAction SilentlyContinue Compare-MultipleObjects -Objects $Forwarders -FormatOutput -CompareSorted:$true -ExcludeProperty GatheredFrom -SkipProperties -Property 'IpAddress' } $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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 Source = @{Name = "DNS Scavenging - Primary DNS Server" Data = { $PSWinDocumentationDNS = Import-Module PSWinDocumentation.DNS -PassThru & $PSWinDocumentationDNS { param($Domain) $Object = Get-WinDnsServerScavenging -Domain $Domain $Object | Where-Object { $_.ScavengingInterval -ne 0 -and $null -ne $_.ScavengingInterval } } $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } Tests = [ordered] @{ScavengingCount = @{Enable = $true Name = 'Scavenging DNS Servers Count' Parameters = @{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 = @{Property = 'ScavengingInterval', 'Days' ExpectedValue = 7 OperationType = 'le' } } 'Scavenging State' = @{Enable = $true Name = 'Scavenging State' Parameters = @{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 = @{Property = 'LastScavengeTime' ExpectedValue = '(Get-Date).AddDays(-7)' OperationType = 'gt' } } } } $DnsZonesAging = @{Enable = $true Source = @{Name = "Aging primary DNS Zone" Data = { $PSWinDocumentationDNS = Import-Module PSWinDocumentation.DNS -PassThru & $PSWinDocumentationDNS { param($Domain) $Zones = Get-WinDnsServerZones -ZoneName $Domain -Domain $Domain Compare-MultipleObjects -Objects $Zones -FormatOutput -CompareSorted:$true -ExcludeProperty GatheredFrom -SkipProperties -Property 'AgingEnabled' } $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } Tests = [ordered] @{EnabledAgingEnabled = @{Enable = $true Name = 'Zone DNS aging should be enabled' Parameters = @{Property = 'Source' ExpectedValue = $true OperationType = 'eq' } Description = 'Primary DNS zone should have aging enabled.' } EnabledAgingIdentical = @{Enable = $true Name = 'Zone DNS aging should be identical on all DCs' Parameters = @{Property = 'Status' ExpectedValue = $true OperationType = 'eq' } Description = 'Primary DNS zone should have aging enabled, on all DNS servers.' } } } $DNSZonesDomain0ADEL = @{Enable = $true 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 = '' RiskLevel = 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') } } Tests = [ordered] @{DNSZonesDomain0ADEL = @{Enable = $true Name = 'DomainDNSZones should have proper FSMO Owner (0ADEL)' Parameters = @{ExpectedValue = '0ADEL:' Property = 'fSMORoleOwner' OperationType = 'notmatch' } } } } $DNSZonesForest0ADEL = @{Enable = $true 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 = '' RiskLevel = 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') } } Tests = [ordered] @{DNSZonesForest0ADEL = @{Enable = $true Name = 'ForestDNSZones should have proper FSMO Owner (0ADEL)' Parameters = @{ExpectedValue = '0ADEL:' Property = 'fSMORoleOwner' OperationType = 'notmatch' } } } } $DomainFSMORoles = @{Enable = $true Source = @{Name = 'Roles availability' Data = { Test-ADRolesAvailability -Domain $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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' } } } } $EmptyOrganizationalUnits = @{Enable = $true Source = @{Name = "Orphaned/Empty Organizational Units" Data = { $OrganizationalUnits = Get-ADOrganizationalUnit -Filter * -Properties distinguishedname -Server $Domain | Select-Object -ExpandProperty distinguishedname $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) } } ExpectedOutput = $false Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } } $GroupPolicyMissingPermissions = @{Enable = $true Source = @{Name = "Group Policy Missing Permissions" Data = { Get-WinADGPOMissingPermissions -Domain $Domain } ExpectedOutput = $false Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 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') } } } $KerberosAccountAge = @{Enable = $true Source = @{Name = "Kerberos Account Age" Data = { Get-ADUser -Identity krbtgt -Properties Created, PasswordLastSet, msDS-KeyVersionNumber -Server $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } Tests = [ordered] @{EnabledAgingEnabled = @{Enable = $true Name = 'Kerberos Last Password Change Should be less than 180 days' Parameters = @{Property = 'PasswordLastSet' ExpectedValue = '(Get-Date).AddDays(-180)' OperationType = 'gt' } } } } $OrphanedForeignSecurityPrincipals = @{Enable = $true Source = @{Name = "Orphaned Foreign Security Principals" Data = { $AllFSP = Get-WinADUsersForeignSecurityPrincipalList -Domain $Domain $OrphanedObjects = $AllFSP | Where-Object { $_.TranslatedName -eq $null } $OrphanedObjects } ExpectedOutput = $false Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } } $PasswordComplexity = @{Enable = $true Source = @{Name = 'Password Complexity Requirements' Data = { $ADModule = Import-Module PSWinDocumentation.AD -PassThru & $ADModule { param($Domain); Get-WinADDomainDefaultPasswordPolicy -Domain $Domain } $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } Tests = [ordered] @{ComplexityEnabled = @{Enable = $true Name = 'Complexity Enabled' Parameters = @{Property = 'Complexity Enabled' ExpectedValue = $true OperationType = 'eq' } } 'LockoutDuration' = @{Enable = $true Name = 'Lockout Duration' Parameters = @{Property = 'Lockout Duration' ExpectedValue = 30 OperationType = 'ge' } } 'LockoutObservationWindow' = @{Enable = $true Name = 'Lockout Observation Window' Parameters = @{Property = 'Lockout Observation Window' ExpectedValue = 30 OperationType = 'ge' } } 'LockoutThreshold' = @{Enable = $true Name = 'Lockout Threshold' Parameters = @{Property = 'Lockout Threshold' ExpectedValue = 5 OperationType = 'gt' } } 'MaxPasswordAge' = @{Enable = $true Name = 'Max Password Age' Parameters = @{Property = 'Max Password Age' ExpectedValue = 60 OperationType = 'le' } } 'MinPasswordLength' = @{Enable = $true Name = 'Min Password Length' Parameters = @{Property = 'Min Password Length' ExpectedValue = 8 OperationType = 'gt' } } 'MinPasswordAge' = @{Enable = $true Name = 'Min Password Age' Parameters = @{Property = 'Min Password Age' ExpectedValue = 1 OperationType = 'le' } } 'PasswordHistoryCount' = @{Enable = $true Name = 'Password History Count' Parameters = @{Property = 'Password History Count' ExpectedValue = 10 OperationType = 'ge' } } 'ReversibleEncryptionEnabled' = @{Enable = $true Name = 'Reversible Encryption Enabled' Parameters = @{Property = 'Reversible Encryption Enabled' ExpectedValue = $false OperationType = 'eq' } } } } $SecurityGroupsAccountOperators = @{Enable = $true Source = @{Name = "Groups: Account operators should be empty" Data = { Get-ADGroupMember -Identity 'S-1-5-32-548' -Recursive -Server $Domain } ExpectedOutput = $false Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 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 = @() } } } $SecurityGroupsSchemaAdmins = @{Enable = $true 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 } ExpectedOutput = $false Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 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') } } } $SecurityUsersAcccountAdministrator = @{Enable = $true 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 = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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 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 = 'Configuration' Category = '' Severity = '' RiskLevel = 0 Description = 'DFS-R should be 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/') } } Tests = [ordered] @{DFSRSysvolState = @{Enable = $true Name = 'DFSR Sysvol State' Parameters = @{Property = 'SysvolMode' ExpectedValue = 'DFS-R' OperationType = 'eq' PropertyExtendedValue = 'Flags' } } } } $Trusts = @{Enable = $true Source = @{Name = "Trust Availability" Data = { $ADModule = Import-Module PSWinDocumentation.AD -PassThru & $ADModule { param($Domain) Get-WinADDomainTrusts -Domain $Domain } -Domain $Domain } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @('https://blogs.technet.microsoft.com/askpfeplat/2019/04/11/changes-to-ticket-granting-ticket-tgt-delegation-across-trusts-in-windows-server-askpfeplat-edition/') } } Tests = [ordered] @{TrustsConnectivity = @{Enable = $true Name = 'Trust status verification' Parameters = @{OverwriteName = { "Trust status verification | Source $Domain, Target $($_.'Trust Target'), Direction $($_.'Trust Direction')" } Property = 'Trust Status' ExpectedValue = 'OK' OperationType = 'eq' } } TrustsUnconstrainedDelegation = @{Enable = $true Name = 'Trust unconstrained TGTDelegation' Parameters = @{OverwriteName = { "Trust unconstrained TGTDelegation | Source $Domain, Target $($_.'Trust Target'), Direction $($_.'Trust Direction')" } WhereObject = { $($_.'Trust Direction' -eq 'BiDirectional' -or $_.'Trust Direction' -eq 'InBound') } Property = 'TGTDelegation' ExpectedValue = $True OperationType = 'eq' } } } } $WellKnownFolders = @{Enable = $true 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.$_ $CurrentWellKnownFolders[$_] = $DomainInformation.$_ } Compare-MultipleObjects -Object @($DefaultWellKnownFolders, $CurrentWellKnownFolders) -SkipProperties } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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 shouldn 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' } } } } $DFSAutoRecovery = @{Enable = $true Source = @{Name = 'DFSR AutoRecovery' Data = { Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\DFSR\Parameters" -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 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/') } } Tests = [ordered] @{EnableSMB1Protocol = @{Enable = $true Name = 'DFSR AutoRecovery should be enabled' Parameters = @{Property = 'StopReplicationOnAutoRecovery' ExpectedValue = 0 OperationType = 'eq' } } } } $DiskSpace = @{Enable = $true Source = @{Name = 'Disk Free' Data = { Get-ComputerDiskLogical -ComputerName $DomainController -OnlyLocalDisk -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = @{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 Source = @{Name = "Name servers for primary domain zone" Data = { Test-DNSNameServers -Domain $Domain -DomainController $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 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 = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 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 = '' RiskLevel = 10 Resources = @() } } 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.' } } } $Information = @{Enable = $true Source = @{Name = "Domain Controller Information" Data = { Get-ADDomainController -Server $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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' } } } } $LDAP = @{Enable = $true Source = @{Name = 'LDAP Connectivity' Data = { Test-LDAP -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{PortLDAP = @{Enable = $true Name = 'LDAP Port is Available' Parameters = @{Property = 'LDAP' ExpectedValue = $true OperationType = 'eq' } } PortLDAPS = @{Enable = $true Name = 'LDAP SSL Port is Available' Parameters = @{Property = 'LDAPS' ExpectedValue = $true OperationType = 'eq' } } PortLDAP_GC = @{Enable = $true Name = 'LDAP GC Port is Available' Parameters = @{Property = 'GlobalCatalogLDAP' ExpectedValue = $true OperationType = 'eq' } } PortLDAPS_GC = @{Enable = $true Name = 'LDAP SSL GC Port is Available' Parameters = @{Property = 'GlobalCatalogLDAPS' ExpectedValue = $true OperationType = 'eq' } } } } $NetworkCardSettings = @{Enable = $true Source = @{Name = "Get all network interfaces and firewall status" Data = { Get-ComputerNetwork -ComputerName $DomainController } Details = [ordered] @{Area = 'Connectivity' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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' RiskLevel = 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/') } } WindowsFirewall = @{Enable = $true Name = 'Windows Firewall should be enabled on network card' Parameters = @{Property = 'FirewallStatus' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'FirewallProfile' } } } } $NTDSParameters = @{Enable = $true Source = @{Name = "NTDS Parameters" Data = { Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{DsaNotWritable = @{Enable = $true Name = 'Domain Controller should be writeable' Parameters = @{Property = 'Dsa Not Writable' ExpectedOutput = $false } } } } $OperatingSystem = @{Enable = $true Source = @{Name = 'Operating System' Data = { Get-ComputerOperatingSystem -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 Source = @{Name = 'Ping Connectivity' Data = { Test-NetConnection -ComputerName $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = @{Ping = @{Enable = $true Name = 'Responding to PING' Parameters = @{Property = 'PingSucceeded' PropertyExtendedValue = 'PingReplyDetails', 'RoundtripTime' ExpectedValue = $true OperationType = 'eq' } } } } $Ports = [ordered] @{Enable = $true 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 } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 0 Description = '' Resolution = '' Resources = @() } } 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 OPEN' Parameters = @{WhereObject = { $_.Port -eq '139' } Property = 'Status' ExpectedValue = $false OperationType = 'eq' PropertyExtendedValue = 'Summary' } Details = [ordered] @{Area = '' Category = '' Severity = '' RiskLevel = 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 Source = [ordered] @{Name = 'RDP Port is open' Data = { Test-ComputerPort -ComputerName $DomainController -PortTCP 3389 -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{PortOpen = [ordered] @{Enable = $false Name = 'Port is OPEN' Parameters = @{Property = 'Status' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'Summary' } } } } $RDPSecurity = [ordered] @{Enable = $true Source = [ordered] @{Name = 'RDP Security' Data = { Get-ComputerRDP -ComputerName $DomainController } Details = [ordered] @{Area = 'Connectivity' Description = '' Resolution = '' RiskLevel = 10 Resources = @('https://lazywinadmin.com/2014/04/powershell-getset-network-level.html' 'https://devblogs.microsoft.com/scripting/weekend-scripter-report-on-network-level-authentication/') } } Tests = [ordered] @{PortOpen = [ordered] @{Enable = $true Name = 'Port is OPEN' Parameters = @{Property = 'Connectivity' ExpectedValue = $true OperationType = 'eq' PropertyExtendedValue = 'ConnectivitySummary' } } NLAAuthenticationEnabled = [ordered] @{Enable = $true Name = 'NLA Authentication is Enabled' Parameters = @{Property = 'UserAuthenticationRequired' ExpectedValue = $true OperationType = 'eq' } } } } $Services = [ordered] @{Enable = $true Source = @{Name = 'Service Status' Data = { $Services = @('ADWS', 'DNS', 'DFS', 'DFSR', 'Eventlog', 'EventSystem', 'KDC', 'LanManWorkstation', 'LanManServer', 'NetLogon', 'NTDS', 'RPCSS', 'SAMSS', 'Spooler', 'W32Time') Get-PSService -Computers $DomainController -Services $Services } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' 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 = 'Automatic' OperationType = 'eq' } } SpoolerServiceStatus = @{Enable = $true Name = 'Spooler Service is STOPPED' Parameters = @{WhereObject = { $_.Name -eq 'Spooler' } Property = 'Status' ExpectedValue = 'Stopped' OperationType = 'eq' } } SpoolerServiceStartType = @{Enable = $true Name = 'Spooler Service START TYPE is DISABLED' Parameters = @{WhereObject = { $_.Name -eq 'Spooler' } Property = 'StartType' ExpectedValue = 'Disabled' OperationType = 'eq' } } 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 = 'Automatic' OperationType = 'eq' } } } } $SMBProtocols = @{Enable = $true Source = @{Name = 'SMB Protocols' Data = { Get-ComputerSMB -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{EnableSMB1Protocol = @{Enable = $true Name = 'SMB v1 Protocol should be disabled' Parameters = @{Property = 'EnableSMB1Protocol' ExpectedValue = $false OperationType = 'eq' } } EnableSMB2Protocol = @{Enable = $true Name = 'SMB v2 Protocol should be enabled' Parameters = @{Property = 'EnableSMB2Protocol' ExpectedValue = $true OperationType = 'eq' } } } } $SMBShares = @{Enable = $true Source = @{Name = 'Default SMB Shares' Data = { Get-ComputerSMBShare -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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' } } } } $TimeSettings = [ordered] @{Enable = $true Source = @{Name = "Time Settings" Data = { Get-TimeSetttings -ComputerName $DomainController -Domain $Domain } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{NTPServerEnabled = @{Enable = $true Name = 'NtpServer must be enabled.' Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpServerEnabled' ExpectedValue = $true OperationType = 'eq' } } 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 AllSync' Requirements = @{IsPDC = $true } Parameters = @{WhereObject = { $_.ComputerName -eq $DomainController } Property = 'NtpType' ExpectedValue = 'AllSync' OperationType = 'eq' } } } } $TimeSynchronizationExternal = @{Enable = $true Source = @{Name = "Time Synchronization External" Data = { Get-ComputerTime -TimeTarget $DomainController -TimeSource 'pool.ntp.org' -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{TimeSynchronizationTest = @{Enable = $true Name = 'Time Difference' 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 Source = @{Name = "Time Synchronization Internal" Data = { Get-ComputerTime -TimeTarget $DomainController -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @('https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc773263(v=ws.10)#w2k3tr_times_tools_uhlp') } } Tests = [ordered] @{TimeSynchronizationTest = @{Enable = $true Name = 'Time Difference' Parameters = @{Property = 'TimeDifferenceSeconds' ExpectedValue = 1 OperationType = 'le' PropertyExtendedValue = 'TimeDifferenceSeconds' } } } } $WindowsRemoteManagement = @{Enable = $true Source = @{Name = 'Windows Remote Management' Data = { Test-WinRM -ComputerName $DomainController } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 Source = @{Name = "Windows Roles and Features" Data = { Get-WindowsFeature -ComputerName $DomainController } } Tests = [ordered] @{ActiveDirectoryDomainServices = @{Enable = $true Name = 'Active Directory Domain Services is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'Active Directory Domain Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } DNSServer = @{Enable = $true Name = 'DNS Server is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'DNS Server' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileandStorageServices = @{Enable = $true Name = 'File and Storage Services is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'File and Storage Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileandiSCSIServices = @{Enable = $true Name = 'File and iSCSI Services is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'File and iSCSI Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } FileServer = @{Enable = $true Name = 'File Server is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'File Server' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } StorageServices = @{Enable = $true Name = 'Storage Services is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'Storage Services' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } WindowsPowerShell51 = @{Enable = $true Name = 'Windows PowerShell 5.1 is installed' Parameters = @{WhereObject = { $_.DisplayName -eq 'Windows PowerShell 5.1' } Property = 'Installed' ExpectedValue = $true OperationType = 'eq' } } } } $WindowsUpdates = @{Enable = $true Source = @{Name = "Windows Updates" Data = { Get-HotFix -ComputerName $DomainController | Sort-Object -Property InstalledOn -Descending | Select-Object -First 1 } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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' } } } } $ForestFSMORoles = @{Enable = $true Source = @{Name = 'Roles availability' Data = { Test-ADRolesAvailability } Details = [ordered] @{Area = 'Features' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{SchemaMasterAvailability = @{Enable = $true Name = 'Schema Master Availability' Parameters = @{ExpectedValue = $true Property = 'SchemaMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'SchemaMaster' } } DomainNamingMasterAvailability = @{Enable = $true Name = 'Domain Master Availability' Parameters = @{ExpectedValue = $true Property = 'DomainNamingMasterAvailability' OperationType = 'eq' PropertyExtendedValue = 'DomainNamingMaster' } } } } $ForestBackup = @{Enable = $true Source = @{Name = 'Forest Backup' Data = { Get-WinADLastBackup } Details = [ordered] @{Area = 'Backup' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{LastBackupTests = @{Enable = $true Name = 'Forest Last Backup Time - Context' Parameters = @{ExpectedValue = 2 OperationType = 'lt' Property = 'LastBackupDaysAgo' PropertyExtendedValue = 'LastBackup' OverwriteName = { "Last Backup $($_.NamingContext)" } } } } } $OptionalFeatures = [ordered] @{Enable = $true Source = [ordered] @{Name = 'Optional Features' Data = { $ADModule = Import-Module PSWinDocumentation.AD -PassThru & $ADModule { Get-WinADForestOptionalFeatures -WarningAction SilentlyContinue } } Details = [ordered] @{Area = 'Features' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{RecycleBinEnabled = @{Enable = $true Name = 'Recycle Bin Enabled' Parameters = @{Property = 'Recycle Bin Enabled' ExpectedValue = $true OperationType = 'eq' } } LapsAvailable = @{Enable = $true Name = 'LAPS Schema Extended' Parameters = @{Property = 'Laps Enabled' ExpectedValue = $true OperationType = 'eq' } } PrivAccessManagement = @{Enable = $true Name = 'Privileged Access Management Enabled' Parameters = @{Property = 'Privileged Access Management Feature Enabled' ExpectedValue = $true OperationType = 'eq' } } } } $OrphanedAdmins = @{Enable = $true Source = @{Name = 'Orphaned Administrative Objects (AdminCount)' Data = { Get-WinADPriviligedObjects -OrphanedOnly } ExpectedOutput = $false Details = [ordered] @{Area = 'Features' Description = "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." Resolution = '' RiskLevel = 10 Resources = @('https://blogs.technet.microsoft.com/poshchap/2016/07/29/security-focus-orphaned-admincount-eq-1-ad-users/') } } } $Replication = @{Enable = $true Source = @{Name = 'Forest Replication' Data = { Get-WinADForestReplication -WarningAction SilentlyContinue } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 Source = @{Name = 'Forest Replication using RepAdmin' Data = { repadmin /showrepl * /csv | ConvertFrom-Csv } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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 Source = @{Name = 'Site Links' Data = { Get-WinADSiteLinks } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{MinimalReplicationFrequency = @{Enable = $true Name = 'Replication Frequency should be set to maximum 60 minutes' Parameters = @{Property = 'ReplicationFrequencyInMinutes' ExpectedValue = 60 OperationType = 'lt' } } UseNotificationsForLinks = @{Enable = $true Name = 'Automatic site links should use notifications' Parameters = @{Property = 'Options' ExpectedValue = 'UseNotify' OperationType = 'contains' PropertyExtendedValue = 'Options' } } } } $SiteLinksConnections = @{Enable = $true Source = @{Name = 'Site Links Connections' Data = { Test-ADSiteLinks -Splitter ', ' } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } 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' PropertyExtendedValue = 'SiteLinksManual' } } SiteLinksNotifications = @{Enable = $true Name = 'All site links use notifications' Parameters = @{Property = 'SiteLinksNotUsingNotifyCount' ExpectedValue = 0 OperationType = 'eq' } } SiteLinksDoNotUseNotifications = @{Enable = $false Name = 'All site links are not using notifications' Parameters = @{Property = 'SiteLinksUseNotifyCount' ExpectedValue = 0 OperationType = 'eq' } } } } $Sites = @{Enable = $true Source = [ordered] @{Name = 'Sites Verification' Data = { $ADModule = Import-Module PSWinDocumentation.AD -PassThru $Sites = & $ADModule { Get-WinADForestSites } [Array] $SitesWithoutDC = $Sites | Where-Object { $_.DomainControllersCount -eq 0 } [Array] $SitesWithoutSubnets = $Sites | Where-Object { $_.SubnetsCount -eq 0 } [PSCustomObject] @{SitesWithoutDC = $SitesWithoutDC.Count SitesWithoutSubnets = $SitesWithoutSubnets.Count SitesWithoutDCName = $SitesWithoutDC.Name -join ', ' SitesWithoutSubnetsName = $SitesWithoutSubnets.Name -join ', ' } } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{SitesWithoutDC = @{Enable = $true Name = 'Sites without Domain Controllers' Description = 'Verify each `site has at least [one subnet configured]`' Parameters = @{Property = 'SitesWithoutDC' ExpectedValue = 0 OperationType = 'eq' } } SitesWithoutSubnets = @{Enable = $true Name = 'Sites without Subnets' Parameters = @{Property = 'SitesWithoutSubnets' ExpectedValue = 0 OperationType = 'eq' } } } } $TombstoneLifetime = @{Enable = $true Source = [ordered]@{Name = 'Tombstone Lifetime' Data = { $Output = (Get-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$((Get-ADRootDSE).configurationNamingContext)" -Properties tombstoneLifetime).tombstoneLifetime if ($null -eq $Output) { [PSCustomObject] @{TombstoneLifeTime = 60 } } else { [PSCustomObject] @{TombstoneLifeTime = $Output } } } Details = [ordered] @{Area = '' Description = '' Resolution = '' RiskLevel = 10 Resources = @() } } Tests = [ordered] @{TombstoneLifetime = @{Enable = $true Name = 'TombstoneLifetime should be set to minimum of 180 days' Parameters = @{ExpectedValue = 180 Property = 'TombstoneLifeTime' OperationType = 'ge' } } } Resources = @('https://helpcenter.netwrix.com/Configure_IT_Infrastructure/AD/AD_Tombstone.html') } function ConvertTo-Source { [CmdletBinding()] param([string] $Source) if ($Source.StartsWith('Forest')) { $ProperSource = [ordered] @{Scope = 'Forest' Name = $Source -replace '^Forest' } } elseif ($Source.StartsWith('Domain')) { $ProperSource = [ordered] @{Scope = 'Domain' Name = $Source -replace '^Domain' } } elseif ($Source.StartsWith('DC')) { $ProperSource = [ordered] @{Scope = 'DomainControllers' Name = $Source -replace '^DC' } } return $ProperSource } function Get-TestimoDomain { [CmdletBinding()] param([string] $Domain) $Output = Get-ADDomain -Server $Domain -ErrorAction Stop $Output } function Get-TestimoDomainControllers { [CmdletBinding()] param([string] $Domain) try { $DomainControllers = Get-ADDomainController -Server $Domain -Filter * -ErrorAction Stop foreach ($_ in $DomainControllers) { if ($_.HostName -notin $Script:TestimoConfiguration['Exclusions']['DomainControllers']) { [PSCustomObject] @{Name = $($_.HostName).ToLower() IsPDC = $_.OperationMasterRoles -contains 'PDCEmulator' } } } } catch { return } } function Get-TestimoForest { [CmdletBinding()] param() try { $Forest = Get-ADForest -ErrorAction Stop $Domains = foreach ($_ in $Forest.Domains) { if ($_ -notin $Script:TestimoConfiguration['Exclusions']['Domains']) { $_.ToLower() } } [ordered] @{Name = $Forest.Name ForestMode = $Forest.ForestMode Domains = $Domains PartitionsContainer = $Forest.PartitionsContainer DomainNamingMaster = $Forest.DomainNamingMaster SchemaMaster = $Forest.SchemaMaster GlobalCatalogs = $Forest.GlobalCatalogs Sites = $Forest.Sites SPNSuffixes = $Forest.SPNSuffixes UPNSuffixes = $Forest.UPNSuffixes ApplicationPartitions = $Forest.ApplicationPartitions CrossForestReferences = $Forest.CrossForestReferences } } catch { return } } 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-Begin -Text "Loading configuration from JSON failed. Skipping." -Level 0 Out-Status -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Not JSON or syntax is incorrect.") return } } else { Out-Begin -Text "Loading configuratio failed. Skipping." -Level 0 Out-Status -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Not JSON/Hashtable or syntax is incorrect.") } Out-Begin -Text "Using configuration provided by user" -Level 0 $Scopes = 'Forest', 'Domain', 'DomainControllers' foreach ($Scope in $Scopes) { if ($LoadedConfiguration -is [System.Collections.IDictionary]) { foreach ($Key in ($LoadedConfiguration.$Scope).Keys) { $Script:TestimoConfiguration[$Scope][$Key]['Enable'] = $LoadedConfiguration.$Scope.$Key.Enable foreach ($Test in $LoadedConfiguration.$Scope.$Key.Tests.Keys) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Enable'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Enable if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedValue) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['ExpectedValue'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedValue } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedCount) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['ExpectedCount'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedCount } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.Property) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['Property'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.Property } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.OperationType) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['OperationType'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.OperationType } } } } else { foreach ($Key in ($LoadedConfiguration.$Scope).PSObject.Properties.Name) { $Script:TestimoConfiguration[$Scope][$Key]['Enable'] = $LoadedConfiguration.$Scope.$Key.Enable foreach ($Test in $LoadedConfiguration.$Scope.$Key.Tests.PSObject.Properties.Name) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Enable'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Enable if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedValue) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['ExpectedValue'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedValue } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedCount) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['ExpectedCount'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.ExpectedCount } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.Property) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['Property'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.Property } if ($null -ne $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.OperationType) { $Script:TestimoConfiguration[$Scope][$Key]['Tests'][$Test]['Parameters']['OperationType'] = $LoadedConfiguration.$Scope.$Key.Tests.$Test.Parameters.OperationType } } } } } Out-Status -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ("Configuration loaded from $Option") } else { Out-Begin -Text "Using configuration defaults" -Level 0 Out-Status -Status $null -Domain $Domain -DomainController $DomainController -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 } 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 } 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 } 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) Out-Begin -Text $Text -Level $Level -Domain $Domain -DomainController $DomainController Out-Status -Text $Text -Status $false -ExtendedValue $ExtendedValue -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID } function Out-Status { [CmdletBinding()] param([string] $TestID, [string] $Text, [nullable[bool]] $Status, [string] $Section, [string] $ExtendedValue, [string] $Domain, [string] $DomainController, [System.Collections.IDictionary] $SourceDetails, [System.Collections.IDictionary] $TestDetails, [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 ($null -ne $Status) { $Output = [PSCustomObject]@{Name = $TestText Type = $TestType Domain = $Domain DomainController = $DomainController Status = $Status Extended = $ExtendedValue } 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 } function Set-TestsStatus { [CmdletBinding()] param([string[]] $Sources, [string[]] $ExcludeSources) if ($Sources) { $Scopes = @('Forest', 'Domain', 'DomainControllers') foreach ($Scope in $Scopes) { foreach ($Test in $Script:TestimoConfiguration.$Scope.Keys) { $Script:TestimoConfiguration.$Scope[$Test]['Enable'] = $false } } foreach ($Source in $Sources) { if ($Source.StartsWith('Forest')) { $ProperSource = $Source -replace '^Forest' $Script:TestimoConfiguration['Forest'][$ProperSource]['Enable'] = $true } elseif ($Source.StartsWith('Domain')) { $ProperSource = $Source -replace '^Domain' $Script:TestimoConfiguration['Domain'][$ProperSource]['Enable'] = $true } elseif ($Source.StartsWith('DC')) { $ProperSource = $Source -replace '^DC' $Script:TestimoConfiguration['DomainControllers'][$ProperSource]['Enable'] = $true } } } foreach ($Source in $ExcludeSources) { if ($Source.StartsWith('Forest')) { $ProperSource = $Source -replace '^Forest' $Script:TestimoConfiguration['Forest'][$ProperSource]['Enable'] = $false } elseif ($Source.StartsWith('Domain')) { $ProperSource = $Source -replace '^Domain' $Script:TestimoConfiguration['Domain'][$ProperSource]['Enable'] = $false } elseif ($Source.StartsWith('DC')) { $ProperSource = $Source -replace '^DC' $Script:TestimoConfiguration['DomainControllers'][$ProperSource]['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] $UseCssLinks, [switch] $UseJavaScriptLinks, [switch] $ShowHTML) if ($FilePath -eq '') { $FilePath = Get-FileName -Extension 'html' -Temporary } New-HTML -FilePath $FilePath -UseCssLinks:$UseCssLinks -UseJavaScriptLinks:$UseJavaScriptLinks { [Array] $PassedTests = $TestResults['Results'] | Where-Object { $_.Status -eq $true } [Array] $FailedTests = $TestResults['Results'] | Where-Object { $_.Status -ne $true } New-HTMLTab -Name 'Summary' -IconBrands galactic-senate { New-HTMLSection -HeaderText "Tests results" -HeaderBackGroundColor DarkGray { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTests.Count) -Color ForestGreen New-ChartPie -Name 'Failed' -Value ($FailedTests.Count) -Color OrangeRed } New-HTMLTable -DataTable $TestResults['Summary'] -HideFooter -DisableSearch { New-HTMLTableContent -ColumnName 'Passed' -BackGroundColor ForestGreen -Color White New-HTMLTableContent -ColumnName 'Failed' -BackGroundColor OrangeRed -Color White } } New-HTMLPanel { New-HTMLTable -DataTable $TestResults['Results'] { New-HTMLTableCondition -Name 'Status' -Value $true -Color Green -Row New-HTMLTableCondition -Name 'Status' -Value $false -Color Red -Row } } } } 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'] $SourceCode = $TestResults['Forest']['Tests'][$Source]['SourceCode'] $Results = $TestResults['Forest']['Tests'][$Source]['Results'] [Array] $PassedTestsSingular = $TestResults['Forest']['Tests'][$Source]['Results'] | Where-Object { $_.Status -eq $true } [Array] $FailedTestsSingular = $TestResults['Forest']['Tests'][$Source]['Results'] | Where-Object { $_.Status -ne $true } New-HTMLSection -HeaderText $Name -HeaderBackGroundColor DarkGray { New-HTMLContainer { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTestsSingular.Count) -Color ForestGreen New-ChartPie -Name 'Failed' -Value ($FailedTestsSingular.Count) -Color OrangeRed } New-HTMLCodeBlock -Code $SourceCode -Style 'PowerShell' -Theme enlighter } } New-HTMLContainer { New-HTMLPanel { New-HTMLTable -DataTable $Data New-HTMLTable -DataTable $Results { New-HTMLTableCondition -Name 'Status' -Value $true -Color Green -Row New-HTMLTableCondition -Name 'Status' -Value $false -Color Red -Row } } } } } } foreach ($Domain in $TestResults['Domains'].Keys) { New-HTMLTab -Name "Domain $Domain" -IconBrands deskpro { foreach ($Source in $TestResults['Domains'][$Domain]['Tests'].Keys) { $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'] [Array] $PassedTestsSingular = $TestResults['Domains'][$Domain]['Tests'][$Source]['Results'] | Where-Object { $_.Status -eq $true } [Array] $FailedTestsSingular = $TestResults['Domains'][$Domain]['Tests'][$Source]['Results'] | Where-Object { $_.Status -ne $true } New-HTMLSection -HeaderText $Name -HeaderBackGroundColor DarkGray { New-HTMLContainer { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTestsSingular.Count) -Color ForestGreen New-ChartPie -Name 'Failed' -Value ($FailedTestsSingular.Count) -Color OrangeRed } New-HTMLCodeBlock -Code $SourceCode -Style 'PowerShell' -Theme enlighter } } New-HTMLContainer { New-HTMLPanel { New-HTMLTable -DataTable $Data New-HTMLTable -DataTable $Results { New-HTMLTableCondition -Name 'Status' -Value $true -Color Green -Row New-HTMLTableCondition -Name 'Status' -Value $false -Color Red -Row } } } } } foreach ($DC in $TestResults['Domains'][$Domain]['DomainControllers'].Keys) { New-HTMLSection -HeaderText "Domain Controller - $DC" -HeaderBackGroundColor DarkSlateGray { New-HTMLContainer { foreach ($Source in $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'].Keys) { $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'] [Array] $PassedTestsSingular = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Results'] | Where-Object { $_.Status -eq $true } [Array] $FailedTestsSingular = $TestResults['Domains'][$Domain]['DomainControllers'][$DC]['Tests'][$Source]['Results'] | Where-Object { $_.Status -ne $true } New-HTMLSection -HeaderText $Name -HeaderBackGroundColor DarkGray { New-HTMLContainer { New-HTMLPanel { New-HTMLChart { New-ChartPie -Name 'Passed' -Value ($PassedTestsSingular.Count) -Color ForestGreen New-ChartPie -Name 'Failed' -Value ($FailedTestsSingular.Count) -Color OrangeRed } New-HTMLCodeBlock -Code $SourceCode -Style 'PowerShell' -Theme enlighter } } New-HTMLContainer { New-HTMLPanel { New-HTMLTable -DataTable $Data New-HTMLTable -DataTable $Results { New-HTMLTableCondition -Name 'Status' -Value $true -Color Green -Row New-HTMLTableCondition -Name 'Status' -Value $false -Color Red -Row } } } } } } } } } } } -ShowHTML:$ShowHTML } function Start-Testing { [CmdletBinding()] param([ScriptBlock] $Execute, [string] $Scope, [string] $Domain, [string] $DomainController, [bool] $IsPDC, [Object] $ForestInformation, [Object] $DomainInformation) $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-Begin -Type 'i' -Text $SummaryText -Level ($LevelSummary - 3) -Domain $Domain -DomainController $DomainController Out-Status -Text $SummaryText -Status $null -ExtendedValue '' -Domain $Domain -DomainController $DomainController $TestsSummaryTogether = @(foreach ($Source in $($Script:TestimoConfiguration.$Scope.Keys)) { $CurrentSection = $Script:TestimoConfiguration.$Scope[$Source] if ($null -eq $CurrentSection) { Write-Warning "Source $Source in scope: $Scope is defined improperly. Please verify." continue } if ($CurrentSection['Enable'] -eq $true) { $ReferenceID = $Source $TestsSummary = [PSCustomobject] @{Passed = 0 Failed = 0 Skipped = 0 Total = 0 } if (-not $CurrentSection['Source']) { Write-Warning "Source $Source in scope: $Scope is defined improperly. Please verify." continue } $CurrentSource = $CurrentSection['Source'] [Array] $AllTests = $CurrentSection['Tests'].Keys $Time = Start-TimeLog if ($CurrentSource['Requirements']) { if ($null -ne $CurrentSource['Requirements']['IsDomainRoot']) { if (-not $CurrentSource['Requirements']['IsDomainRoot'] -eq $IsDomainRoot) { continue } } if ($null -ne $CurrentSource['Requirements']['IsPDC']) { if (-not $CurrentSource['Requirements']['IsPDC'] -eq $IsPDC) { continue } } } if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID] = [ordered] @{Name = $CurrentSource['Name'] SourceCode = $CurrentSource['Data'] Details = $CurrentSource['Details'] Results = [System.Collections.Generic.List[PSCustomObject]]::new() Domain = $Domain DomainController = $DomainController } } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID] = [ordered] @{Name = $CurrentSource['Name'] SourceCode = $CurrentSource['Data'] Details = $CurrentSource['Details'] Results = [System.Collections.Generic.List[PSCustomObject]]::new() Domain = $Domain DomainController = $DomainController } } else { $Script:Reporting['Forest']['Tests'][$ReferenceID] = [ordered] @{Name = $CurrentSource['Name'] SourceCode = $CurrentSource['Data'] Details = $CurrentSource['Details'] Results = [System.Collections.Generic.List[PSCustomObject]]::new() Domain = $Domain DomainController = $DomainController } } if ($CurrentSource['Parameters']) { $SourceParameters = $CurrentSource['Parameters'] $Object = Start-TestProcessing -Test $CurrentSource['Name'] -Level $Level -OutputRequired -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID { & $CurrentSource['Data'] @SourceParameters -DomainController $DomainController -Domain $Domain } } else { $Object = Start-TestProcessing -Test $CurrentSource['Name'] -Level $Level -OutputRequired -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID { & $CurrentSource['Data'] -DomainController $DomainController -Domain $Domain } } if ($Domain -and $DomainController) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DomainController]['Tests'][$ReferenceID]['Data'] = $Object } elseif ($Domain) { $Script:Reporting['Domains'][$Domain]['Tests'][$ReferenceID]['Data'] = $Object } else { $Script:Reporting['Forest']['Tests'][$ReferenceID]['Data'] = $Object } if ($Object -and ($null -eq $CurrentSource['ExpectedOutput'] -or $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 $TestsSummary.Passed = $TestsSummary.Passed + 1 } elseif ($Object -and $CurrentSource['ExpectedOutput'] -eq $false) { $FailAllTests = $true Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue 'Data is available. This is a bad thing.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID $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 $TestsSummary.Passed = $TestsSummary.Passed + 1 } else { $FailAllTests = $true Out-Failure -Text $CurrentSource['Name'] -Level $LevelTest -ExtendedValue 'No data available.' -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID $TestsSummary.Failed = $TestsSummary.Failed + 1 } foreach ($Test in $AllTests) { $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) { if ($CurrentTest['Parameters']) { $Parameters = $CurrentTest['Parameters'] } else { $Parameters = $null } $TestsResults = Start-TestingTest -Test $CurrentTest['Name'] -Level $LevelTest -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID { if ($CurrentTest['Data'] -is [ScriptBlock]) { & $CurrentTest['Data'] -Object $Object -Domain $Domain -DomainController $DomainController @Parameters -Level $LevelTest } else { Test-Value -Object $Object -Domain $Domain -DomainController $DomainController @Parameters -Level $LevelTest -TestName $CurrentTest['Name'] -ReferenceID $ReferenceID } } $TestsSummary.Passed = $TestsSummary.Passed + ($TestsResults | Where-Object { $_ -eq $true }).Count $TestsSummary.Failed = $TestsSummary.Failed + ($TestsResults | Where-Object { $_ -eq $false }).Count } else { $TestsSummary.Failed = $TestsSummary.Failed + 1 Out-Failure -Text $CurrentTest['Name'] -Level $LevelTestFailure -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID } } 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 Start-TestingTest { [CmdletBinding()] param([ScriptBlock] $Execute, $Test, [int] $Level, [string] $Domain, [string] $DomainController, [string] $ReferenceID) if ($Execute) { if ($Script:TestimoConfiguration.Debug.ShowErrors) { [Array] $Output = & $Execute $Output } else { try { [Array] $Output = & $Execute $Output } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " } if (-not $ErrorMessage) { } else { Out-Failure -Text $CurrentTest['TestName'] -Level $Level -ExtendedValue $ErrorMessage -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID } } } } function Start-TestProcessing { [CmdletBinding()] param([ScriptBlock] $Execute, [string] $Test, [switch] $OutputRequired, [nullable[bool]] $ExpectedStatus, [int] $Level = 0, [switch] $IsTest, [switch] $Simple, [string] $Domain, [string] $DomainController, [string] $ReferenceID) if ($Execute) { if ($IsTest) { Out-Begin -Type 't' -Text $Test -Level $Level -Domain $Domain -DomainController $DomainController } else { Out-Begin -Type 'i' -Text $Test -Level $Level -Domain $Domain -DomainController $DomainController } if ($Script:TestimoConfiguration.Debug.ShowErrors) { [Array] $Output = & $Execute $ErrorMessage = $null } else { try { [Array] $Output = & $Execute } catch { $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " " } } if (-not $ErrorMessage) { foreach ($O in $Output) { if ($OutputRequired.IsPresent) { if ($O['Output']) { foreach ($_ in $O['Output']) { $_ } } else { foreach ($_ in $O) { $_ } } } } if ($null -eq $ExpectedStatus) { $TestResult = $null } else { $TestResult = $ExpectedStatus -eq $Output.Status } Out-Status -Text $Test -Status $TestResult -ExtendedValue $O.Extended -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID } else { Out-Status -Text $Test -Status $false -ExtendedValue $ErrorMessage -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID } } } function Test-Me { [CmdletBinding()] param([string] $OperationType, [string] $TestName, [int] $Level, [string] $Domain, [string] $DomainController, [string[]] $Property, [Object] $TestedValue, [Array] $Object, [Array] $ExpectedValue, [string[]] $PropertyExtendedValue, [string] $OperationResult, [int] $ExpectedCount = -1, [string] $ReferenceID, [nullable[bool]] $ExpectedOutput) Out-Begin -Text $TestName -Level $Level -Domain $Domain -DomainController $DomainController $TestedValue = $Object foreach ($V in $Property) { $TestedValue = $TestedValue.$V } if ($OperationType -eq '') { $OperationType = 'eq' } $ScriptBlock = { $Operators = @{'lt' = 'LessThan' 'gt' = 'GreaterThan' 'le' = 'LessOrEqual' 'ge' = 'GreaterOrEqual' 'eq' = 'Equal' 'contains' = 'Contains' 'notcontains' = 'Not contains' 'like' = 'Like' 'match' = 'Match' 'notmatch' = 'Not match' } if ($ExpectedCount -ne -1) { if ($OperationType -eq 'lt') { $TestResult = $Object.Count -lt $ExpectedCount } elseif ($OperationType -eq 'gt') { $TestResult = $Object.Count -lt $ExpectedCount } elseif ($OperationType -eq 'ge') { $TestResult = $Object.Count -lt $ExpectedCount } elseif ($OperationType -eq 'le') { $TestResult = $Object.Count -lt $ExpectedCount } elseif ($OperationType -eq 'like') { $TestResult = $Object.Count -like $ExpectedCount } elseif ($OperationType -eq 'contains') { $TestResult = $Object.Count -like $ExpectedCount } elseif ($OperationType -eq 'in') { $TestResult = $ExpectedCount -in $Object.Count } elseif ($OperationType -eq 'notin') { $TestResult = $ExpectedCount -notin $Object.Count } else { $TestResult = $Object.Count -eq $ExpectedCount } $TextTestedValue = $Object.Count $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') { $ExpectedValue -in $TestedValue $TextExpectedValue = $ExpectedValue } 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 $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue, Found value: $($TextTestedValue)" } } else { if ($TestResult -notcontains $false) { $ReportResult = $true $ReportExtended = "Expected value ($($Operators[$OperationType])): $($TextExpectedValue)" } else { $ReportResult = $false $ReportExtended = "Expected value ($($Operators[$OperationType])): $TextExpectedValue, Found value: $($TextTestedValue)" } } } if ($PropertyExtendedValue.Count -gt 0) { $ReportExtended = $Object foreach ($V in $PropertyExtendedValue) { $ReportExtended = $ReportExtended.$V } } Out-Status -Text $TestName -Status $ReportResult -ExtendedValue $ReportExtended -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID 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 return $False } } } function Test-Value { [CmdletBinding()] param([Array] $Object, [string] $TestName, [string[]] $Property, [Object] $ExpectedValue, [string[]] $PropertyExtendedValue, [string] $OperationType, [int] $Level, [string] $Domain, [Object] $DomainController, [int] $ExpectedCount, [string] $OperationResult, [scriptblock] $WhereObject, [string] $ReferenceID, [scriptblock] $OverwriteName, [nullable[bool]] $ExpectedOutput) if ($Object) { if ($WhereObject) { $Object = $Object | Where-Object $WhereObject } if ($ExpectedCount) { if ($OverwriteName) { $TestName = & $OverwriteName } Test-Me -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 { foreach ($_ in $Object) { if ($OverwriteName) { $TestName = & $OverwriteName } Test-Me -Object $_ -OperationType $OperationType -TestName $TestName -Level $Level -Domain $Domain -DomainController $DomainController -Property $Property -ExpectedValue $ExpectedValue -PropertyExtendedValue $PropertyExtendedValue -OperationResult $OperationResult -ReferenceID $ReferenceID -ExpectedOutput $ExpectedOutput } } } else { Write-Warning 'Objected not passed to Test-VALUE.' } } $Script:TestimoConfiguration = [ordered] @{Exclusions = [ordered] @{Domains = @() DomainControllers = @() } Forest = [ordered]@{Backup = $ForestBackup Replication = $Replication ReplicationStatus = $ReplicationStatus OptionalFeatures = $OptionalFeatures Sites = $Sites SiteLinks = $SiteLinks SiteLinksConnections = $SiteLinksConnections Roles = $ForestFSMORoles OrphanedAdmins = $OrphanedAdmins TombstoneLifetime = $TombstoneLifetime } Domain = [ordered] @{Roles = $DomainFSMORoles WellKnownFolders = $WellKnownFolders PasswordComplexity = $PasswordComplexity GroupPolicyMissingPermissions = $GroupPolicyMissingPermissions Trusts = $Trusts OrphanedForeignSecurityPrincipals = $OrphanedForeignSecurityPrincipals EmptyOrganizationalUnits = $EmptyOrganizationalUnits DNSScavengingForPrimaryDNSServer = $DNSScavengingForPrimaryDNSServer DNSForwaders = $DNSForwaders DnsZonesAging = $DnsZonesAging KerberosAccountAge = $KerberosAccountAge SecurityGroupsAccountOperators = $SecurityGroupsAccountOperators SecurityGroupsSchemaAdmins = $SecurityGroupsSchemaAdmins SecurityUsersAcccountAdministrator = $SecurityUsersAcccountAdministrator SysVolDFSR = $SysVolDFSR 'DNSZonesForest0ADEL' = $DNSZonesForest0ADEL 'DNSZonesDomain0ADEL' = $DNSZonesDomain0ADEL } DomainControllers = [ordered] @{Information = $Information WindowsRemoteManagement = $WindowsRemoteManagement OperatingSystem = $OperatingSystem Services = $Services LDAP = $LDAP Pingable = $Pingable Ports = $Ports RDPPorts = $RDPPorts RDPSecurity = $RDPSecurity DiskSpace = $DiskSpace TimeSettings = $TimeSettings TimeSynchronizationInternal = $TimeSynchronizationInternal TimeSynchronizationExternal = $TimeSynchronizationExternal NetworkCardSettings = $NetworkCardSettings WindowsUpdates = $WindowsUpdates WindowsRolesAndFeatures = $WindowsRolesAndFeatures DnsResolveInternal = $DNSResolveInternal DnsResolveExternal = $DNSResolveExternal DnsNameServes = $DNSNameServers SMBProtocols = $SMBProtocols SMBShares = $SMBShares DFSRAutoRecovery = $DFSAutoRecovery NTDSParameters = $NTDSParameters } Debug = [ordered] @{ShowErrors = $false } } function Get-TestimoConfiguration { [CmdletBinding()] param([switch] $AsJson, [string] $FilePath) $NewConfig = [ordered] @{ } $Scopes = 'Forest', 'Domain', 'DomainControllers' foreach ($Scope in $Scopes) { $NewConfig[$Scope] = [ordered] @{ } foreach ($Source in ($Script:TestimoConfiguration[$Scope]).Keys) { $NewConfig[$Scope][$Source] = [ordered] @{ } $NewConfig[$Scope][$Source]['Enable'] = $Script:TestimoConfiguration[$Scope][$Source]['Enable'] if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Source']['ExpectedOutput']) { $NewConfig[$Scope][$Source]['Source'] = [ordered] @{ } $NewConfig[$Scope][$Source]['Source']['ExpectedOutput'] = $Script:TestimoConfiguration[$Scope][$Source]['Source']['ExpectedOutput'] } $NewConfig[$Scope][$Source]['Tests'] = [ordered] @{ } foreach ($Test in $Script:TestimoConfiguration[$Scope][$Source]['Tests'].Keys) { $NewConfig[$Scope][$Source]['Tests'][$Test] = [ordered] @{ } $NewConfig[$Scope][$Source]['Tests'][$Test]['Enable'] = $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Enable'] $NewConfig[$Scope][$Source]['Tests'][$Test]['Parameters'] = [ordered] @{ } if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['Property']) { if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['Property']) { $NewConfig[$Scope][$Source]['Tests'][$Test]['Parameters']['Property'] = $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['Property'] } if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedValue']) { $NewConfig[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] = $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] } if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedCount']) { $NewConfig[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] = $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] } if ($null -ne $Script:TestimoConfiguration[$Scope][$Source]['Tests'][$Test]['Parameters']['OperationType']) { $NewConfig[$Scope][$Source]['Tests'][$Test]['Parameters']['OperationType'] = $Script:TestimoConfiguration[$Scope][$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[]] $Source) if ($Source) { $DetectedSource = ConvertTo-Source -Source $Source $Scope = $DetectedSource.Scope $Name = $DetectedSource.Name $Script:TestimoConfiguration.$Scope[$Name].Tests.Keys } else { $ForestKeys = $TestimoConfiguration.Forest.Keys $DomainKeys = $TestimoConfiguration.Domain.Keys $DomainControllerKeys = $TestimoConfiguration.DomainControllers.Keys $TestSources = @(foreach ($Key in $ForestKeys) { "Forest$Key" } foreach ($Key in $DomainKeys) { "Domain$Key" } foreach ($Key in $DomainControllerKeys) { "DC$Key" }) $TestSources | Sort-Object } } function Invoke-Testimo { [alias('Test-ImoAD', 'Test-IMO')] [CmdletBinding()] param([ValidateScript( { $_ -in (& $SourcesAutoCompleter) })] [string[]] $Sources, [ValidateScript( { $_ -in (& $SourcesAutoCompleter) })] [string[]] $ExcludeSources, [string[]] $ExcludeDomains, [string[]] $ExcludeDomainControllers, [switch] $ReturnResults, [switch] $ShowErrors, [switch] $ExtendedResults, [Object] $Configuration, [string] $ReportPath, [switch] $ShowReport) $Script:Reporting = [ordered] @{ } $Script:Reporting['Results'] = $null $Script:Reporting['Summary'] = $null $Script:Reporting['Forest'] = [ordered] @{ } $Script:Reporting['Forest']['Summary'] = $null $Script:Reporting['Forest']['Tests'] = [ordered] @{ } $Script:Reporting['Domains'] = [ordered] @{ } 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 Set-TestsStatus -Sources $Sources -ExcludeSources $ExcludeSources if ($Script:TestimoConfiguration.Exclusions.Domains) { Out-Begin -Text 'Following Domains will be ignored' -Level 0 Out-Status -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ($Script:TestimoConfiguration.Exclusions.Domains -join ', ') } if ($Script:TestimoConfiguration.Exclusions.DomainControllers) { Out-Begin -Text 'Following Domain Controllers will be ignored' -Level 0 Out-Status -Status $null -Domain $Domain -DomainController $DomainController -ExtendedValue ($Script:TestimoConfiguration.Exclusions.DomainControllers -join ', ') } $ForestInformation = Get-TestimoForest $null = Start-Testing -Scope 'Forest' -ForestInformation $ForestInformation { foreach ($Domain in $ForestInformation.Domains) { $Script:Reporting['Domains'][$Domain] = [ordered] @{ } $Script:Reporting['Domains'][$Domain]['Summary'] = $null $Script:Reporting['Domains'][$Domain]['Tests'] = [ordered] @{ } $Script:Reporting['Domains'][$Domain]['DomainControllers'] = [ordered] @{ } $DomainInformation = Get-TestimoDomain -Domain $Domain Start-Testing -Scope 'Domain' -Domain $Domain -DomainInformation $DomainInformation -ForestInformation $ForestInformation { $DomainControllers = Get-TestimoDomainControllers -Domain $Domain foreach ($DC in $DomainControllers) { $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.Name] = [ordered] @{ } $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.Name]['Summary'] = $null $Script:Reporting['Domains'][$Domain]['DomainControllers'][$DC.Name]['Tests'] = [ordered] @{ } Start-Testing -Scope 'DomainControllers' -Domain $Domain -DomainController $DC.Name -IsPDC $DC.IsPDC -DomainInformation $DomainInformation -ForestInformation $ForestInformation } } } } $Script:Reporting['Results'] = $Script:TestResults if ($ReturnResults -and $ExtendedResults) { $Script:Reporting } else { if ($ReturnResults) { $Script:TestResults } } if ($ReportPath -or $ShowReport) { Start-TestimoReport -FilePath $ReportPath -UseCssLinks:$true -UseJavaScriptLinks:$true -ShowHTML:$ShowReport -TestResults $Script:Reporting } } [scriptblock] $SourcesAutoCompleter = { param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $ForestKeys = $Script:TestimoConfiguration.Forest.Keys $DomainKeys = $Script:TestimoConfiguration.Domain.Keys $DomainControllerKeys = $Script:TestimoConfiguration.DomainControllers.Keys $TestSources = @(foreach ($Key in $ForestKeys) { "Forest$Key" } foreach ($Key in $DomainKeys) { "Domain$Key" } foreach ($Key in $DomainControllerKeys) { "DC$Key" }) $TestSources | Sort-Object } Register-ArgumentCompleter -CommandName Invoke-Testimo -ParameterName Sources -ScriptBlock $SourcesAutoCompleter Register-ArgumentCompleter -CommandName Invoke-Testimo -ParameterName ExcludeSources -ScriptBlock $SourcesAutoCompleter Export-ModuleMember -Function @('Get-TestimoConfiguration', 'Get-TestimoSources', 'Invoke-Testimo') -Alias @('Test-IMO', 'Test-ImoAD') |