├── .github └── FUNDING.yml ├── Build └── Manage-Testimo.ps1 ├── CHANGELOG.MD ├── Example ├── Configuration │ └── TestimoConfiguration.json ├── DSCSample │ └── Azure.ConditionalPolicy.ps1 ├── Example-BuildConfiguration.ps1 ├── Example-CustomTests.ps1 ├── Example-CustomTestsAzure.ps1 ├── Example-DSCComparison.ps1 ├── Example-DSCComparison1.ps1 ├── Example-LoadConfigurationFromFile.ps1 ├── Example-LoadConfigurationFromHash.ps1 ├── Example-LoadSources.ps1 ├── Example-LoadSourcesAdvanced.ps1 ├── Example-QuickTest.ps1 ├── Example-QuickTestExternal01.ps1 ├── Example-QuickTestExternal02.ps1 ├── Example-QuickTestSplitReports.ps1 ├── Example-TestActiveDirectory.ps1 ├── Example-TestActiveDirectoryAdvanced.ps1 ├── Example-TestActiveDirectoryAdvancedDev.ps1 ├── Example-TestActiveDirectoryEmail.ps1 ├── Example-TestActiveDirectoryWithReport.ps1 ├── Example-TestActiveDirectoryWord.ps1 ├── O365 │ ├── Backup.ps1 │ ├── O365.Bookings.ps1 │ ├── O365.Forms.ps1 │ └── O365.MailboxesDisabled.ps1 └── Portable │ └── Portable-Testimo.ps1 ├── Private ├── Add-TestimoBaselines.ps1 ├── Add-TestimoSources.ps1 ├── Get-RequestedSources.ps1 ├── Get-TestimoDomainControllers.ps1 ├── Get-TestimoSourcesStatus.ps1 ├── Import-TestimoConfiguration.ps1 ├── Initialize-TestimoTests.ps1 ├── New-ChartData.ps1 ├── Out-Begin.ps1 ├── Out-Failure.ps1 ├── Out-Informative.ps1 ├── Out-Skip.ps1 ├── Out-Status.ps1 ├── Out-Summary.ps1 ├── RiskLevels.ps1 ├── Set-TestsStatus.ps1 ├── SourcesDomain │ ├── ComputersUnsupported.ps1 │ ├── ComputersUnsupportedMainstream.ps1 │ ├── DHCPAuthorized.ps1 │ ├── DNSForwarders.ps1 │ ├── DNSScavengingForPrimaryDNSServer.ps1 │ ├── DNSZonesAging.ps1 │ ├── DNSZonesDomain0ADEL.ps1 │ ├── DNSZonesForest0ADEL.ps1 │ ├── DomainControllers.ps1 │ ├── DomainFSMORoles.ps1 │ ├── DomainLDAP.ps1 │ ├── DuplicateObjects.ps1 │ ├── ExchangeUsers.ps1 │ ├── GroupPolicy.ps1 │ ├── GroupPolicyADM.ps1 │ ├── GroupPolicyOwners.ps1 │ ├── GroupPolicyPermission.ps1 │ ├── GroupPolicyPermissionConsistency.ps1 │ ├── GroupPolicySysvol.ps1 │ ├── MachineQuota.ps1 │ ├── NetLogonOwner.ps1 │ ├── OrganizationalUnitsEmpty.ps1 │ ├── OrganizationalUnitsProtected.ps1 │ ├── OrphanedSecurityPrincipals.ps1 │ ├── PasswordComplexity.ps1 │ ├── SecurityComputers.ps1 │ ├── SecurityDelegatedObjects.ps1 │ ├── SecurityGroupsAccountOperators.ps1 │ ├── SecurityGroupsSchemaAdmins.ps1 │ ├── SecurityKRBGT.ps1 │ ├── SecurityUsers.ps1 │ ├── SecurityUsersAccountAdministrator.ps1 │ ├── SysVOLDFSR.ps1 │ └── WellKnownFolders.ps1 ├── SourcesDomainControllers │ ├── DCDNSForwaders.ps1 │ ├── DFS.ps1 │ ├── DNSResolveExternal.ps1 │ ├── DNSResolveInternal.ps1 │ ├── Diagnostics.ps1 │ ├── DiskSpace.ps1 │ ├── DnsNameServers.ps1 │ ├── EventLogs.ps1 │ ├── FileSystem.ps1 │ ├── GroupPolicySYSVOLDC.ps1 │ ├── Information.ps1 │ ├── LanManServer.ps1 │ ├── LanManagerSettings.ps1 │ ├── Ldap.ps1 │ ├── LdapInsecureBindings.ps1 │ ├── MSSLegacy.ps1 │ ├── NTDSParameters.ps1 │ ├── NetSessionEnumeration.ps1 │ ├── NetworkCardSettings.ps1 │ ├── OperatingSystem.ps1 │ ├── Pingable.ps1 │ ├── Ports.ps1 │ ├── RDPPorts.ps1 │ ├── RDPSecurity.ps1 │ ├── SMBProtocols.ps1 │ ├── SMBShares.ps1 │ ├── SMBSharesPermissions.ps1 │ ├── ServiceWINRM.ps1 │ ├── Services.ps1 │ ├── TimeSettings.ps1 │ ├── TimeSynchronizationExternal.ps1 │ ├── TimeSynchronizationInternal.ps1 │ ├── UNCHardenedPaths.ps1 │ ├── WindowsFeaturesOptional.ps1 │ ├── WindowsRemoteManagement.ps1 │ ├── WindowsRolesAndFeatures.ps1 │ └── WindowsUpdates.ps1 ├── SourcesForest │ ├── Backup.ps1 │ ├── DuplicateSPN.ps1 │ ├── ForestConfigurationPartitionOwners.ps1 │ ├── ForestConfigurationPartitionOwnersContainer.ps1 │ ├── ForestDHCP.ps1 │ ├── ForestFSMORoles.ps1 │ ├── ForestSubnets.ps1 │ ├── OptionalFeatures.ps1 │ ├── OrphanedAdmins.ps1 │ ├── Replication.ps1 │ ├── ReplicationStatus.ps1 │ ├── RootKDS.ps1 │ ├── SiteLinks.ps1 │ ├── SiteLinksConnections.ps1 │ ├── Sites.ps1 │ ├── TombstoneLifetime.ps1 │ ├── Trusts.ps1 │ └── VurnerableSchemaClass.ps1 ├── Start-TestimoEmail.ps1 ├── Start-TestimoReport.ps1 ├── Start-TestimoReportHTML.ps1 ├── Start-TestimoReportHTMLWithSplit.ps1 ├── Start-TestimoReportSection.ps1 ├── Start-TestimoReportSummary.ps1 ├── Start-TestimoReportSummaryAdvanced.ps1 ├── Start-Testing.ps1 ├── Test-StepOne.ps1 ├── Test-StepTwo.ps1 └── TestimoConfiguration.ps1 ├── Public ├── Compare-Testimo.ps1 ├── Get-TestimoConfiguration.ps1 ├── Get-TestimoSources.ps1 ├── Import-PrivateModule.ps1 └── Invoke-Testimo.ps1 ├── README.MD ├── Testimo.Tests.ps1 ├── Testimo.psd1 ├── Testimo.psm1 └── Tests ├── VerifySources.Tests.ps1 └── VerifyTestimoConfiguration.Tests.ps1 /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: PrzemyslawKlys 4 | custom: https://paypal.me/PrzemyslawKlys -------------------------------------------------------------------------------- /Example/DSCSample/Azure.ConditionalPolicy.ps1: -------------------------------------------------------------------------------- 1 | @{ 2 | Name = 'AzureADConditionalPolicyDenyBasicAuth' 3 | Enable = $true 4 | Scope = 'AzureAD' 5 | Source = @{ 6 | Name = 'Azure AD Conditional Access Deny Basic Auth' 7 | Data = { 8 | Get-AzureADMSConditionalAccessPolicy | Where-Object { $_.DisplayName -eq "All - Deny Basic authentication" } 9 | } 10 | Flatten = $true 11 | Details = [ordered] @{ 12 | Category = 'AzureAD' 13 | Description = '' 14 | Importance = 0 15 | ActionType = 0 16 | Resources = @( 17 | 18 | ) 19 | Tags = 'O365', 'Configuration', 'AzureAD' 20 | StatusTrue = 0 21 | StatusFalse = 5 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | "Conditions.Applications.IncludeApplications.1" = 'All' 27 | 28 | # AADConditionalAccessPolicy = @{ 29 | # Enable = $true 30 | # Name = 'Hidden Mailboxes' 31 | # Parameters = @{ 32 | # ApplicationEnforcedRestrictionsIsEnabled = $False; 33 | # BuiltInControls = @("block"); 34 | # ClientAppTypes = @("exchangeActiveSync", "other"); 35 | # CloudAppSecurityIsEnabled = $False; 36 | # CloudAppSecurityType = ""; 37 | # Credential = $Credscredential; 38 | # DisplayName = "All - Deny Basic authentication"; 39 | # Ensure = "Present"; 40 | # ExcludeApplications = @(); 41 | # ExcludeDevices = @(); 42 | # ExcludeGroups = @(); 43 | # ExcludeLocations = @(); 44 | # ExcludePlatforms = @(); 45 | # ExcludeRoles = @(); 46 | # ExcludeUsers = @("admin@$OrganizationName"); 47 | # GrantControlOperator = "OR"; 48 | # Id = "77000763-8b9e-485a-8cfe-735b2bde5f50"; 49 | # IncludeApplications = @("All"); 50 | # IncludeDevices = @(); 51 | # IncludeGroups = @(); 52 | # IncludeLocations = @(); 53 | # IncludePlatforms = @(); 54 | # IncludeRoles = @(); 55 | # IncludeUserActions = @(); 56 | # IncludeUsers = @("All"); 57 | # PersistentBrowserIsEnabled = $False; 58 | # PersistentBrowserMode = ""; 59 | # SignInFrequencyIsEnabled = $False; 60 | # SignInFrequencyType = ""; 61 | # SignInRiskLevels = @(); 62 | # State = "enabled"; 63 | # UserRiskLevels = @(); 64 | # } 65 | # Details = [ordered] @{ 66 | # Category = 'Configuration' 67 | # Importance = 5 68 | # ActionType = 2 69 | # StatusTrue = 1 70 | # StatusFalse = 5 71 | # } 72 | # } 73 | } 74 | } -------------------------------------------------------------------------------- /Example/Example-BuildConfiguration.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | # There are 3 ways to deal with configuration 4 | 5 | # Straight to FILE/JSON 6 | Get-TestimoConfiguration -FilePath $PSScriptRoot\Configuration\TestimoConfiguration.json 7 | 8 | # Straight to JSON 9 | Get-TestimoConfiguration -AsJson 10 | 11 | # Output to Hashtable so you can edit it freely and keep in ps1 12 | $OutputOrderedDictionary = Get-TestimoConfiguration 13 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.RecycleBinEnabled.Enable = $false 14 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.LapsAvailable.Enable = $true 15 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.LapsAvailable.Parameters.ExpectedValue = $false -------------------------------------------------------------------------------- /Example/Example-CustomTests.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | if (-not $Credentials) { 4 | # $Credentials = Get-Credential 5 | } 6 | # This makes a connection to Office 365 tenant 7 | # since we don't want to save the data we null it out 8 | # keep in mind that if there's an MFA you would be better left without Credentials and just let it prompt you 9 | $Authorization = Connect-O365Admin -Verbose 10 | #$ExchangeConnection = Connect-ExchangeOnline 11 | 12 | Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Sources O365Bookings, O365Forms, Office365Mailboxes -Variables @{ 13 | Authorization = $Authorization 14 | } -IncludeDomainControllers AD1 15 | 16 | 17 | #Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Sources O365Bookings,Office365Mailboxes -Variables @{ 18 | # Authorization = $Authorization1 19 | #} 20 | 21 | #Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Sources ForestBackup, ForestDHCP 22 | 23 | #Invoke-Testimo -Sources DCServices, DCSMBProtocols, DomainLDAP -IncludeDomainControllers AD1 24 | 25 | 26 | # Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Sources O365Bookings, Office365Mailboxes, ForestBackup, ForestDHCP, DCServices, SomeRandomTest -Variables @{ 27 | # Authorization = $Authorization1 28 | # } -AlwaysShowSteps 29 | 30 | 31 | #Invoke-Testimo -Sources ForestBackup, DomainMachineQuota, O365Bookings 32 | #Invoke-Testimo -Sources ForestBackup, DomainMachineQuota, O365Bookings 33 | 34 | #Invoke-Testimo -Sources DCServices, DCSMBProtocols, DomainLDAP -IncludeDomainControllers AD1 -AlwaysShowSteps 35 | #Invoke-Testimo -Sources ForestBackup, DomainMachineQuota, ForestDHCP, ForestSites 36 | 37 | # Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Sources Office365Forms, O365Bookings, Office365Mailboxes -Variables @{ 38 | # Authorization = $Authorization1 39 | # } 40 | 41 | # $Test = Invoke-Testimo -Sources DCServices -PassThru -ExtendedResults 42 | # $Test -------------------------------------------------------------------------------- /Example/Example-CustomTestsAzure.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | 3 | Invoke-Testimo -ExternalTests $PSScriptRoot\DSCSample -Sources AzureADConditionalPolicyDenyBasicAuth -Variables @{ 4 | Authorization = $Authorization1 5 | } -------------------------------------------------------------------------------- /Example/Example-DSCComparison.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | Import-Module "C:\Support\GitHub\DSCParser\DSCParser.psd1" -Force 3 | 4 | $Sources = @( 5 | 'AADConditionalAccessPolicy', 'TeamsChannelsPolicy', 'EXOAtpPolicyForO365' 6 | # 'AADGroupLifecyclePolicy', 'AADGroupsNamingPolicy', 'EXOAntiPhishPolicy', 'EXOAtpPolicyForO365', 7 | # 'EXOHostedContentFilterPolicy', 'EXOHostedOutboundSpamFilterPolicy', 'EXOMalwareFilterPolicy', 'EXOOrganizationConfig', 8 | # 'EXOOwaMailboxPolicy', 'EXORoleAssignmentPolicy', 'EXOSafeAttachmentPolicy', 'EXOSafeAttachmentRule', 'EXOSafeLinksPolicy', 'EXOSafeLinksRule', 9 | # 'EXOSharingPolicy', 'O365AdminAuditLogConfig', 'ODSettings', 'SCLabelPolicy', 'SCSensitivityLabel', 'SPOAccessControlSettings', 'SPOBrowserIdleSignout', 'SPO' 10 | # 'SharingSettings', 'SPOTenantSettings', 'TeamsChannelsPolicy', 'TeamsClientConfiguration', 11 | # 'TeamsGuestMeetingConfiguration', 'TeamsGuestMessagingConfiguration', 'TeamsMeetingBroadcastConfiguration', 'TeamsMeetingBroadcastPolicy', 12 | # 'TeamsMeetingConfiguration', 'TeamsMeetingPolicy', 'TeamsMessagingPolicy' 13 | ) 14 | #$Sources = 'DSC', 'CSEnrollMentRestrictions' 15 | 16 | Invoke-Testimo -Sources $Sources { 17 | Compare-Testimo -Name 'DSC' -Scope 'O365' -BaseLineSourcePath "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" -BaseLineTargetPath "C:\Support\GitHub\Testimo\Ignore\m365\M365_DSC.ps1" 18 | } 19 | return 20 | 21 | 22 | $BaseLineSource = ConvertTo-DSCObject -Path "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" 23 | -------------------------------------------------------------------------------- /Example/Example-DSCComparison1.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | #Import-Module C:\Support\GitHub\PSSharedGoods\PSSharedGoods.psd1 -Force 3 | Import-Module "C:\Support\GitHub\DSCParser\DSCParser.psd1" -Force 4 | 5 | 6 | $Sources = @( 7 | 'AADConditionalAccessPolicy', 'AADGroupLifecyclePolicy', 'AADGroupsNamingPolicy', 'EXOAntiPhishPolicy', 'EXOAtpPolicyForO365' 8 | 'EXOHostedContentFilterPolicy', 'EXOHostedOutboundSpamFilterPolicy', 'EXOMalwareFilterPolicy', 'EXOOrganizationConfig' 9 | 'EXOOwaMailboxPolicy', 'EXORoleAssignmentPolicy', 'EXOSafeAttachmentPolicy', 'EXOSafeAttachmentRule', 'EXOSafeLinksPolicy', 'EXOSafeLinksRule' 10 | 'EXOSharingPolicy', 'O365AdminAuditLogConfig', 'ODSettings', 'SCLabelPolicy', 'SCSensitivityLabel', 'SPOAccessControlSettings', 'SPOBrowserIdleSignout', 'SPO' 11 | 'SharingSettings', 'SPOTenantSettings', 'TeamsChannelsPolicy', 'TeamsClientConfiguration', 12 | 'TeamsGuestMeetingConfiguration', 'TeamsGuestMessagingConfiguration', 'TeamsMeetingBroadcastConfiguration', 'TeamsMeetingBroadcastPolicy', 13 | 'TeamsMeetingConfiguration', 'TeamsMeetingPolicy', 'TeamsMessagingPolicy' 14 | ) 15 | 16 | Invoke-Testimo -Sources $Sources { 17 | Compare-Testimo -Name 'DSC' -Scope 'O365' -BaseLineSourcePath "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" -BaseLineTargetPath "C:\Support\GitHub\Testimo\Ignore\m365\M365_DSC.ps1" -ExcludeProperty 'ApplicationID', 'ApplicationSecret', 'Ensure', 'IsSingleInstance', 'TenantId' 18 | #Compare-Testimo -Name 'DSC' -Scope 'O365' -BaseLineSourcePath "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" -BaseLineTargetPath "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" 19 | } -FilePath $PSScriptRoot\Reports\TestimoDSC.html -ExternalTests $PSScriptRoot\O365 -Sources O365Bookings, O365Forms, Office365Mailboxes -Variables @{ 20 | Authorization = $Authorization 21 | } -------------------------------------------------------------------------------- /Example/Example-LoadConfigurationFromFile.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | $ConfigurationFile = "$PSScriptRoot\Configuration\TestimoConfiguration.json" 4 | 5 | $Sources = @( 6 | #'ForestFSMORoles' 7 | 'ForestOptionalFeatures' 8 | #'ForestBackup' 9 | #'ForestOrphanedAdmins' 10 | #'DomainPasswordComplexity' 11 | #'DomainKerberosAccountAge' 12 | #'DomainDNSScavengingForPrimaryDNSServer' 13 | #'DCWindowsUpdates' 14 | #'DCTimeSynchronizationExternal' 15 | 'DomainDHCPAuthorized' 16 | ) 17 | 18 | $TestResults = Invoke-Testimo -ReturnResults -ExcludeDomains 'ad.evotec.pl' -ExtendedResults -Configuration $ConfigurationFile -Sources $Sources 19 | $TestResults | Format-Table -AutoSize * -------------------------------------------------------------------------------- /Example/Example-LoadConfigurationFromHash.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | $OutputOrderedDictionary = Get-TestimoConfiguration 4 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.RecycleBinEnabled.Enable = $false 5 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.LapsAvailable.Enable = $true 6 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.LapsAvailable.Parameters.ExpectedValue = $false 7 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.PrivAccessManagement.Enable = $false 8 | $OutputOrderedDictionary.ForestOptionalFeatures.Tests.PrivAccessManagement.Parameters.ExpectedValue = $false 9 | $OutputOrderedDictionary.DCTimeSynchronizationExternal.Source.Parameters.TimeSource = '1.pool.ntp.org' 10 | 11 | $Sources = @( 12 | #'ForestFSMORoles' 13 | 'ForestOptionalFeatures' 14 | #'ForestBackup' 15 | #'ForestOrphanedAdmins' 16 | #'DomainPasswordComplexity' 17 | #'DomainKerberosAccountAge' 18 | #'DomainDNSScavengingForPrimaryDNSServer' 19 | #'DCWindowsUpdates' 20 | 'DCTimeSynchronizationExternal' 21 | ) 22 | 23 | $TestResults = Invoke-Testimo -ReturnResults -ExcludeDomains 'ad.evotec.pl' -ExtendedResults -Sources $Sources -Configuration $OutputOrderedDictionary 24 | $TestResults | Format-Table -AutoSize * -------------------------------------------------------------------------------- /Example/Example-LoadSources.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | # this will get you available sources 4 | Get-TestimoSources -SourcesOnly 5 | 6 | <# Currently something like this. You can use this list to ExcludeSources or define Sources for Invoke-Testimo 7 | ForestBackup 8 | ForestReplication 9 | ForestReplicationStatus 10 | ForestOptionalFeatures 11 | ForestSites 12 | ForestSiteLinks 13 | ForestSiteLinksConnections 14 | ForestRoles 15 | ForestOrphanedAdmins 16 | ForestObjectsWithConflict 17 | ForestTombstoneLifetime 18 | DomainRoles 19 | DomainWellKnownFolders 20 | DomainPasswordComplexity 21 | DomainGroupPolicyMissingPermissions 22 | DomainGroupPolicyPermissionConsistency 23 | DomainGroupPolicyOwner 24 | DomainGroupPolicyPermissionUnknown 25 | DomainGroupPolicyADM 26 | DomainTrusts 27 | DomainOrphanedForeignSecurityPrincipals 28 | DomainOrganizationalUnitsEmpty 29 | DomainOrganizationalUnitsProtected 30 | DomainDNSScavengingForPrimaryDNSServer 31 | DomainDNSForwaders 32 | DomainDnsZonesAging 33 | DomainKerberosAccountAge 34 | DomainSecurityGroupsAccountOperators 35 | DomainSecurityGroupsSchemaAdmins 36 | DomainSecurityUsers 37 | DomainSecurityUsersAcccountAdministrator 38 | DomainSecurityKRBGT 39 | DomainSysVolDFSR 40 | DomainDNSZonesForest0ADEL 41 | DomainDNSZonesDomain0ADEL 42 | DomainDHCPAuthorized 43 | DomainComputersUnsupported 44 | DomainComputersUnsupportedMainstream 45 | DomainExchangeUsers 46 | DomainDuplicateObjects 47 | DCInformation 48 | DCWindowsRemoteManagement 49 | DCEventLogs 50 | DCOperatingSystem 51 | DCServices 52 | DCLDAP 53 | DCLDAPInsecureBindings 54 | DCPingable 55 | DCPorts 56 | DCRDPPorts 57 | DCRDPSecurity 58 | DCDiskSpace 59 | DCTimeSettings 60 | DCTimeSynchronizationInternal 61 | DCTimeSynchronizationExternal 62 | DCNetworkCardSettings 63 | DCWindowsUpdates 64 | DCWindowsRolesAndFeatures 65 | DCDnsResolveInternal 66 | DCDnsResolveExternal 67 | DCDnsNameServes 68 | DCSMBProtocols 69 | DCSMBShares 70 | DCSMBSharesPermissions 71 | DCDFS 72 | DCNTDSParameters 73 | DCGroupPolicySYSVOL 74 | DCLanManagerSettings 75 | DCDiagnostics 76 | DCLanManServer 77 | DCMSSLegacy 78 | DCFileSystem 79 | DCNetSessionEnumeration 80 | DCServiceWINRM 81 | DCUNCHardenedPaths 82 | DCDNSForwaders 83 | #> -------------------------------------------------------------------------------- /Example/Example-LoadSourcesAdvanced.ps1: -------------------------------------------------------------------------------- 1 | Import-Module $PSScriptRoot\..\Testimo.psd1 -Force #-Verbose 2 | 3 | # this will get you single source 4 | #Get-TestimoSources -Sources DCDiagnostics 5 | 6 | # 7 | Get-TestimoSources | Format-Table * 8 | #Get-TestimoSources | Out-HtmlView -------------------------------------------------------------------------------- /Example/Example-QuickTest.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | 3 | $Sources = @( 4 | #'ForestTombstoneLifetime' 5 | #'ForestOptionalFeatures' 6 | #'ForestBackup' 7 | #'ForestConfigurationPartitionOwners' 8 | #'ForestConfigurationPartitionOwnersContainers' 9 | #'ForestOptionalFeatures' 10 | #'ForestOrphanedAdmins' 11 | #'DomainDuplicateObjects' 12 | #'DomainDomainControllers' 13 | #'DomainLDAP' 14 | #'ForestSites' 15 | #'ForestSubnets' 16 | #'DomainOrphanedForeignSecurityPrincipals' 17 | #'ForestTrusts' 18 | #'DomainSecurityUsers' 19 | #'DomainSecurityKRBGT' 20 | #'ForestTrusts' 21 | #'ForestRoles' 22 | #'DomainGroupPolicyAssessment' 23 | #'DomainSecurityDelegatedObjects' 24 | #'DomainSecurityComputers' 25 | #'DomainWellKnownFolders' 26 | #'DomainSecurityUsersAcccountAdministrator' 27 | #'DomainSecurityGroupsSchemaAdmins' 28 | #'DomainSecurityGroupsAccountOperators' 29 | #'DomainWellKnownFolders' 30 | #'DomainDHCPAuthorized' 31 | #'ForestDHCP' 32 | #'DCDiskSpace' 33 | #'DomainSecurityDelegatedObjects' 34 | 'DomainWellKnownFolders' 35 | ) 36 | 37 | # Tests one by one 38 | foreach ($_ in $Sources) { 39 | Invoke-Testimo -Source $_ -Online -ReportPath "$PSScriptRoot\Reports\Testimo_$($_).html" -AlwaysShowSteps 40 | } 41 | 42 | # Tests in single report 43 | $T1 = Invoke-Testimo -Source DomainSecurityDelegatedObjects, ForestRoles, ForestDHCP, DomainLDAP, ForestSites, ForestBackup, ForestOptionalFeatures -Online -ReportPath $PSScriptRoot\Reports\TestimoSummary.html -AlwaysShowSteps -PassThru #-HideHTML 44 | $T2 = Invoke-Testimo -Source DomainOrganizationalUnitsEmpty -Online -ReportPath $PSScriptRoot\Reports\TestimoSummary.html -AlwaysShowSteps -PassThru #-HideHTML 45 | 46 | $T1 | Format-Table 47 | $T2 | Format-Table 48 | 49 | Invoke-Testimo -Sources ForestBackup, DomainDNSZonesForest0ADEL, DCInformation, DCDiskSpace, DCServices, DomainDomainControllers, DomainDHCPAuthorized, DomainSecurityComputers, ForestRoles -ExternalTests "C:\Support\GitHub\Testimo\Example\O365" 50 | Invoke-Testimo -Sources DomainFSMORoles, DomainPasswordComplexity -IncludeDomainControllers AD2 51 | Invoke-Testimo -Sources DCLDAP 52 | Invoke-Testimo -Sources DomainSecurityUsersAcccountAdministrator, DomainSecurityAdministrator, ForestBackup, ForestTrusts -------------------------------------------------------------------------------- /Example/Example-QuickTestExternal01.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | 3 | $Object3 = [PSCustomObject] @{ 4 | "Name" = "Przemyslaw Klys" 5 | "Age" = "30" 6 | "Address" = @{ 7 | "Street" = "Kwiatowa" 8 | "City" = "Warszawa" 9 | 10 | "Country" = [ordered] @{ 11 | "Name" = "Poland" 12 | } 13 | List = @( 14 | [PSCustomObject] @{ 15 | "Name" = "Adam Klys" 16 | "Age" = "32" 17 | } 18 | [PSCustomObject] @{ 19 | "Name" = "Justyna Klys" 20 | "Age" = "33" 21 | } 22 | ) 23 | } 24 | ListTest = @( 25 | [PSCustomObject] @{ 26 | "Name" = "Justyna Klys" 27 | "Age" = "33" 28 | } 29 | ) 30 | } 31 | 32 | $Object4 = [PSCustomObject] @{ 33 | "Name" = "Przemyslaw Klys" 34 | "Age" = "30" 35 | "Address" = @{ 36 | "Street" = "Kwiatowa" 37 | "City" = "Warszawa" 38 | "Country" = [ordered] @{ 39 | "Name" = "Gruzja" 40 | } 41 | List = @( 42 | [PSCustomObject] @{ 43 | "Name" = "Adam Klys" 44 | "Age" = "32" 45 | } 46 | [PSCustomObject] @{ 47 | "Name" = "Pankracy Klys" 48 | "Age" = "33" 49 | } 50 | [PSCustomObject] @{ 51 | "Name" = "Justyna Klys" 52 | "Age" = 30 53 | } 54 | [PSCustomObject] @{ 55 | "Name" = "Justyna Klys" 56 | "Age" = $null 57 | } 58 | ) 59 | } 60 | ListTest = @( 61 | [PSCustomObject] @{ 62 | "Name" = "Sława Klys" 63 | "Age" = "33" 64 | } 65 | ) 66 | MoreProperties = $true 67 | } 68 | 69 | Invoke-Testimo -Sources 'DSC','CSEnrollMentRestrictions' { 70 | Compare-Testimo -Name 'CSEnrollMentRestrictions' -Scope 'Intune' -BaseLineSourcePath "C:\Users\przemyslaw.klys\OneDrive - Evotec\Desktop\Comparison of Intune configuration\Enrollment restrictions\CS_Enrollment_Restrictions.json" -BaseLineTargetPath "C:\Users\przemyslaw.klys\OneDrive - Evotec\Desktop\Comparison of Intune configuration\Enrollment restrictions\CS_Enrollment_Restrictions (1).json" 71 | Compare-Testimo -Name 'DSC' -Scope 'O365' -BaseLineSourcePath "C:\Users\przemyslaw.klys\OneDrive - Evotec\Desktop\Comparison of Intune configuration\Enrollment restrictions\CS_Enrollment_Restrictions.json" -BaseLineTargetPath "C:\Support\GitHub\Testimo\Ignore\m365\M365TenantConfig.ps1" 72 | 73 | #Compare-Testimo -Name 'IntuneBaseLine' -Scope 'Intune' -BaseLineSource $Object3 -BaseLineTarget $Object4 74 | } -------------------------------------------------------------------------------- /Example/Example-QuickTestExternal02.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | 3 | Invoke-Testimo -Sources DomainDHCPAuthorized -ExternalTests "C:\Support\GitHub\Testimo\Example\O365" 4 | 5 | $T = Invoke-Testimo -ExternalTests $PSScriptRoot\O365 -Source ForestBackup -Online -ReportPath $PSScriptRoot\Reports\TestimoSummary.html -AlwaysShowSteps -PassThru -ExtendedResults #-HideHTML 6 | $T -------------------------------------------------------------------------------- /Example/Example-QuickTestSplitReports.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force 2 | 3 | # Split reports functionality 4 | Invoke-Testimo -Sources ForestBackup, ForestOptionalFeatures, DomainComputersUnsupported, DomainDuplicateObjects, DCDiskSpace, DCFileSystem -SplitReports -ReportPath "$PSScriptRoot\Reports\Testimo.html" -AlwaysShowSteps 5 | Invoke-Testimo -Sources DCDiskSpace, DCFileSystem -SplitReports -ReportPath "$PSScriptRoot\Reports\Testimo.html" -AlwaysShowSteps 6 | Invoke-Testimo -Sources DomainComputersUnsupported, DomainDuplicateObjects -SplitReports -ReportPath "$PSScriptRoot\Reports\Testimo.html" -AlwaysShowSteps -------------------------------------------------------------------------------- /Example/Example-TestActiveDirectory.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | #$TestResults = Invoke-Testimo -Sources DCDiagnostics, DCLanManagerSettings,DCTimeSettings, DCTimeSynchronizationInternal,DCDnsResolveInternal -ShowReport:$true -ExcludeDomains 'ad.evotec.pl' -ReturnResults #, ForestBackup, DomainDNSZonesDomain0ADEL, DCEventLogs, ForestReplicationStatus #,DomainDNSZonesForest0ADEL,DCEventLogs#-ShowReport # -ExcludeDomains 'ad.evotec.pl' -ExcludeDomainControllers 'ADRODC.ad.evotec.pl' -ReturnResults 4 | #$TestResults | Format-Table -AutoSize * 5 | 6 | #$TestResults = Invoke-Testimo -Sources DCMSSLegacy,DCLanManServer, DCFileSystem -ShowReport:$true -ExcludeDomains 'ad.evotec.pl' -ReturnResults #, ForestBackup, DomainDNSZonesDomain0ADEL, DCEventLogs, ForestReplicationStatus #,DomainDNSZonesForest0ADEL,DCEventLogs#-ShowReport # -ExcludeDomains 'ad.evotec.pl' -ExcludeDomainControllers 'ADRODC.ad.evotec.pl' -ReturnResults 7 | #$TestResults | Format-Table -AutoSize * 8 | 9 | $TestResults = Invoke-Testimo -ShowReport:$true -ReturnResults #, ForestBackup, DomainDNSZonesDomain0ADEL, DCEventLogs, ForestReplicationStatus #,DomainDNSZonesForest0ADEL,DCEventLogs#-ShowReport # -ExcludeDomains 'ad.evotec.pl' -ExcludeDomainControllers 'ADRODC.ad.evotec.pl' -ReturnResults 10 | $TestResults | Format-Table -AutoSize * -------------------------------------------------------------------------------- /Example/Example-TestActiveDirectoryEmail.ps1: -------------------------------------------------------------------------------- 1 | [Array] $Results = Invoke-Testimo -ReturnResults -ExcludeDomains 'ad.evotec.pl' 2 | [Array] $ResultsFailed | Where-Object { $_.Status -eq $false } 3 | 4 | if ($ResultsFailed.Count -gt 0) { 5 | Email { 6 | EmailHeader { 7 | EmailFrom -Address 'myemail@evotec.pl' 8 | EmailTo -Addresses "otheremail@evotec.pl" 9 | EmailServer -Server 'smtp.office365' -UserName 'myemail@evotec.pl' -Password 'C:\Support\Important\Password-Evotec.txt' -PasswordAsSecure -PasswordFromFile -Port 587 -SSL 10 | EmailOptions -Priority High -DeliveryNotifications Never 11 | EmailSubject -Subject '[Reporting Evotec] Summary of Active Directory Tests' 12 | } 13 | EmailBody -FontFamily 'Calibri' -Size 15 { 14 | EmailText -Text "Summary of Active Directory Tests" -Color None, Blue -LineBreak 15 | 16 | EmailTable -DataTable $Results { 17 | EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator eq -Value 'True' -BackgroundColor Green -Color White -Inline -Row 18 | EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator ne -Value 'True' -BackgroundColor Red -Color White -Inline -Row 19 | } -HideFooter 20 | } 21 | } -AttachSelf -Supress $false 22 | } -------------------------------------------------------------------------------- /Example/Example-TestActiveDirectoryWithReport.ps1: -------------------------------------------------------------------------------- 1 | Import-Module .\Testimo.psd1 -Force #-Verbose 2 | 3 | $Sources = @( 4 | 'ForestRoles' 5 | 'ForestOptionalFeatures' 6 | 'ForestOrphanedAdmins' 7 | 'DomainPasswordComplexity' 8 | 'DomainKerberosAccountAge' 9 | 'DomainDNSScavengingForPrimaryDNSServer' 10 | 'DomainSysVolDFSR' 11 | 'DCRDPSecurity' 12 | 'DCSMBShares' 13 | 'DomainGroupPolicyMissingPermissions' 14 | 'DCWindowsRolesAndFeatures' 15 | 'DCNTDSParameters' 16 | 'DCInformation' 17 | 'ForestReplicationStatus' 18 | ) 19 | 20 | Invoke-Testimo -Sources $Sources -ExcludeDomains 'ad.evotec.pl' -ShowReport -------------------------------------------------------------------------------- /Example/O365/Backup.ps1: -------------------------------------------------------------------------------- 1 | @{ 2 | Name = 'ForestBackup' 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Forest Backup' 7 | Data = { 8 | Get-WinADLastBackup -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Description = 'Active Directory is critical system for any company. Having a proper, up to date backup in place is crucial.' 13 | Importance = 0 14 | ActionType = 0 15 | Resources = @( 16 | '[Backing Up and Restoring an Active Directory Server](https://docs.microsoft.com/en-us/windows/win32/ad/backing-up-and-restoring-an-active-directory-server)' 17 | '[Backup Active Directory (Full and Incremental Backup)](https://activedirectorypro.com/backup-active-directory/)' 18 | ) 19 | Tags = 'Backup', 'Configuration' 20 | StatusTrue = 0 21 | StatusFalse = 0 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | #LastBackupDaysAgo = 0 27 | LastBackupTests1 = @{ 28 | Enable = $true 29 | Name = 'Forest Last Backup Time' 30 | Parameters = @{ 31 | ExpectedValue = 5000 32 | OperationType = 'lt' 33 | Property = 'LastBackupDaysAgo' 34 | PropertyExtendedValue = 'LastBackup' 35 | OverwriteName = { "Last Backup $($_.NamingContext)" } 36 | } 37 | Details = [ordered] @{ 38 | Category = 'Configuration' 39 | Importance = 10 40 | ActionType = 2 41 | StatusTrue = 1 42 | StatusFalse = 5 43 | } 44 | } 45 | LastBackupTests2 = @{ 46 | Enable = $true 47 | Name = 'Forest Last Backup Time' 48 | Parameters = @{ 49 | ExpectedValue = 5 50 | OperationType = 'lt' 51 | Property = 'LastBackupDaysAgo' 52 | PropertyExtendedValue = 'LastBackup' 53 | OverwriteName = { "Last Backup $($_.NamingContext)" } 54 | } 55 | Details = [ordered] @{ 56 | Category = 'Configuration' 57 | Importance = 10 58 | ActionType = 2 59 | StatusTrue = 1 60 | StatusFalse = 5 61 | } 62 | } 63 | } 64 | DataDescription = { 65 | New-HTMLSpanStyle -FontSize 10pt { 66 | New-HTMLText -Text @( 67 | 'Active Directory is critical system for any company. Having a proper, up to date backup is crucial. ' 68 | 'Last backup should be maximum few days old, if not less than 24 hours old. ' 69 | "Please keep in mind that this test doesn't verifies the backup, nor provides information if the backup was saved to proper place and will be available for restore operations. " 70 | "This tests merely checks what was reported by Active Directory - that backup did happen. " 71 | "You should make sure that your backup, and more importantly restore process actually works! " 72 | ) 73 | } 74 | } 75 | DataHighlights = { 76 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor PaleGreen -Value 2 -Operator lt 77 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Salmon -Value 2 -Operator ge 78 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Tomato -Value 10 -Operator ge 79 | } 80 | } -------------------------------------------------------------------------------- /Example/O365/O365.Bookings.ps1: -------------------------------------------------------------------------------- 1 | @{ 2 | Name = 'O365Bookings' 3 | Enable = $true 4 | Scope = 'Office365' 5 | Source = @{ 6 | Name = 'O365 Bookings Settings' 7 | Data = { 8 | $Test = Get-O365OrgBookings -Authorization $Authorization 9 | $Test 10 | } 11 | Details = [ordered] @{ 12 | Category = 'Configuration' 13 | Description = "Microsoft Bookings makes scheduling and managing appointments a breeze. Bookings includes a web-based booking calendar and integrates with Outlook to optimize your staff’s calendar and give your customers flexibility to book a time that works best for them. Email and SMS text notifications reduce no-shows and enhances customer satisfaction Your organization saves time with a reduction in repetitive scheduling tasks. With built in flexibility and ability to customize, Bookings can be designed to fit the situation and needs of many different parts of an organization." 14 | Importance = 0 15 | ActionType = 0 16 | Resources = @( 17 | 18 | ) 19 | Tags = 'O365', 'Configuration', 'Bookings' 20 | StatusTrue = 0 21 | StatusFalse = 5 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | BookingsEnabled = @{ 27 | Enable = $true 28 | Name = 'Bookings Status' 29 | Parameters = @{ 30 | ExpectedValue = $true 31 | OperationType = 'eq' 32 | Property = 'Enabled' 33 | } 34 | Details = [ordered] @{ 35 | Category = 'Configuration' 36 | Importance = 5 37 | ActionType = 2 38 | StatusTrue = 1 39 | StatusFalse = 5 40 | } 41 | } 42 | BookingsEnabled2 = @{ 43 | Enable = $true 44 | Name = 'Bookings Status' 45 | Parameters = @{ 46 | ExpectedValue = $true 47 | OperationType = 'eq' 48 | Property = 'Enabled' 49 | } 50 | Details = [ordered] @{ 51 | Category = 'Configuration' 52 | Importance = 5 53 | ActionType = 2 54 | StatusTrue = 1 55 | StatusFalse = 5 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /Example/O365/O365.Forms.ps1: -------------------------------------------------------------------------------- 1 | @{ 2 | Name = 'O365Forms' 3 | Enable = $true 4 | Scope = 'Office365' 5 | Source = @{ 6 | Name = 'Office365 Basic Forms Settings' 7 | Data = { 8 | Get-O365OrgForms -Authorization $Authorization 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Description = 'Office 365 Forms' 13 | Importance = 0 14 | ActionType = 0 15 | Resources = @() 16 | Tags = 'O365', 'Configuration', 'Forms' 17 | StatusTrue = 0 18 | StatusFalse = 5 19 | } 20 | ExpectedOutput = $true 21 | } 22 | Tests = [ordered] @{ 23 | 'ExternalShareCollaborationEnabled' = $true 24 | 25 | Test = @{ 26 | Parameters = @{ 27 | ExpectedValue = $false 28 | OperationType = 'eq' 29 | Property = 'ExternalCollaborationEnabled' 30 | } 31 | } 32 | 33 | ExternalCollaborationEnabled = @{ 34 | Enable = $true 35 | Name = 'ExternalCollaborationEnabled Status' 36 | Parameters = @{ 37 | ExpectedValue = $false 38 | OperationType = 'eq' 39 | Property = 'ExternalCollaborationEnabled' 40 | } 41 | Details = [ordered] @{ 42 | Category = 'Configuration' 43 | Importance = 5 44 | ActionType = 2 45 | StatusTrue = 1 46 | StatusFalse = 5 47 | } 48 | } 49 | ExternalSendFormEnabled = @{ 50 | Enable = $true 51 | Name = 'ExternalSendFormEnabled Status' 52 | Parameters = @{ 53 | ExpectedValue = $false 54 | OperationType = 'eq' 55 | Property = 'ExternalSendFormEnabled' 56 | } 57 | Details = [ordered] @{ 58 | Category = 'Configuration' 59 | Importance = 5 60 | ActionType = 2 61 | StatusTrue = 1 62 | StatusFalse = 5 63 | } 64 | } 65 | } 66 | DataDescription = { } 67 | DataHighlights = { } 68 | } -------------------------------------------------------------------------------- /Example/O365/O365.MailboxesDisabled.ps1: -------------------------------------------------------------------------------- 1 | @{ 2 | Name = 'Office365Mailboxes' 3 | Enable = $true 4 | Scope = ' ' 5 | Source = @{ 6 | Name = 'Office365 Mailboxes' 7 | Data = { 8 | Import-Module (Import-PSSession -Session $Session -Verbose:$false -DisableNameChecking -AllowClobber) -Global -DisableNameChecking 9 | Get-Mailbox -ResultSize Unlimited | Select-Object -Property UserPrincipalName, HiddenFromAddressListsEnabled, PrimarySmtpAddress, WhenChanged, WhenCreated 10 | } 11 | Details = [ordered] @{ 12 | Category = 'Configuration' 13 | Description = 'Office365Mailboxes' 14 | Importance = 0 15 | ActionType = 0 16 | Resources = @( 17 | 18 | ) 19 | Tags = 'O365', 'Configuration' 20 | StatusTrue = 0 21 | StatusFalse = 5 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | HiddenFromAddressListsEnabled = @{ 27 | Enable = $true 28 | Name = 'Hidden Mailboxes' 29 | Parameters = @{ 30 | WhereObject = { $_.HiddenFromAddressListsEnabled -eq $false } 31 | OperationType = 'lt' 32 | ExpectedCount = 5 33 | } 34 | Details = [ordered] @{ 35 | Category = 'Configuration' 36 | Importance = 5 37 | ActionType = 2 38 | StatusTrue = 1 39 | StatusFalse = 5 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Example/Portable/Portable-Testimo.ps1: -------------------------------------------------------------------------------- 1 | # This function is also part of the module PSSharedGoods. If you have it, you don't need to copy paste anything except the command below 2 | # Install-Module PSSharedGoods 3 | 4 | function Initialize-ModulePortable { 5 | [CmdletBinding()] 6 | param( 7 | [alias('ModuleName')][string] $Name, 8 | [string] $Path = $PSScriptRoot, 9 | [switch] $Download, 10 | [switch] $Import 11 | ) 12 | function Get-RequiredModule { 13 | param( 14 | [string] $Path, 15 | [string] $Name 16 | ) 17 | $PrimaryModule = Get-ChildItem -LiteralPath "$Path\$Name" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 18 | if ($PrimaryModule) { 19 | $Module = Get-Module -ListAvailable $PrimaryModule.FullName -ErrorAction SilentlyContinue -Verbose:$false 20 | if ($Module) { 21 | [Array] $RequiredModules = $Module.RequiredModules.Name 22 | $RequiredModules 23 | foreach ($_ in $RequiredModules) { 24 | Get-RequiredModule -Path $Path -Name $_ 25 | } 26 | } 27 | } else { 28 | Write-Warning "Initialize-ModulePortable - Modules to load not found in $Path" 29 | } 30 | } 31 | 32 | if (-not $Name) { 33 | Write-Warning "Initialize-ModulePortable - Module name not given. Terminating." 34 | return 35 | } 36 | if (-not $Download -and -not $Import) { 37 | Write-Warning "Initialize-ModulePortable - Please choose Download/Import switch. Terminating." 38 | return 39 | } 40 | 41 | if ($Download) { 42 | try { 43 | if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { 44 | $null = New-Item -ItemType Directory -Path $Path -Force 45 | } 46 | Save-Module -Name $Name -LiteralPath $Path -WarningVariable WarningData -WarningAction SilentlyContinue -ErrorAction Stop 47 | } catch { 48 | $ErrorMessage = $_.Exception.Message 49 | 50 | if ($WarningData) { 51 | Write-Warning "Initialize-ModulePortable - $WarningData" 52 | } 53 | Write-Warning "Initialize-ModulePortable - Error $ErrorMessage" 54 | return 55 | } 56 | } 57 | 58 | if ($Download -or $Import) { 59 | [Array] $Modules = Get-RequiredModule -Path $Path -Name $Name | Where-Object { $null -ne $_ } 60 | [array]::Reverse($Modules) 61 | 62 | $CleanedModules = [System.Collections.Generic.List[string]]::new() 63 | 64 | foreach ($_ in $Modules) { 65 | if ($CleanedModules -notcontains $_) { 66 | $CleanedModules.Add($_) 67 | } 68 | } 69 | $CleanedModules.Add($Name) 70 | 71 | $Items = foreach ($_ in $CleanedModules) { 72 | Get-ChildItem -LiteralPath "$Path\$_" -Filter '*.psd1' -Recurse -ErrorAction SilentlyContinue -Depth 1 73 | } 74 | [Array] $PSD1Files = $Items.FullName 75 | } 76 | if ($Download) { 77 | $ListFiles = foreach ($PSD1 in $PSD1Files) { 78 | $PSD1.Replace("$Path", '$PSScriptRoot') 79 | } 80 | # Build File 81 | $Content = @( 82 | '$Modules = @(' 83 | foreach ($_ in $ListFiles) { 84 | " `"$_`"" 85 | } 86 | ')' 87 | "foreach (`$_ in `$Modules) {" 88 | " Import-Module `$_ -Verbose:`$false -Force" 89 | "}" 90 | ) 91 | $Content | Set-Content -Path $Path\$Name.ps1 -Force 92 | } 93 | if ($Import) { 94 | $ListFiles = foreach ($PSD1 in $PSD1Files) { 95 | $PSD1 96 | } 97 | foreach ($_ in $ListFiles) { 98 | Import-Module $_ -Verbose:$false -Force 99 | } 100 | } 101 | } 102 | 103 | Initialize-ModulePortable -Name 'Testimo' -Path "$PSScriptRoot\TestimoPortable" -Download -------------------------------------------------------------------------------- /Private/Add-TestimoSources.ps1: -------------------------------------------------------------------------------- 1 | function Add-TestimoSources { 2 | <# 3 | .SYNOPSIS 4 | Adds external sources to the Testimo configuration. 5 | 6 | .DESCRIPTION 7 | This function adds external sources to the Testimo configuration based on the provided folder paths. It processes PowerShell script files within the specified folders to extract configuration data and updates the Testimo configuration accordingly. 8 | 9 | .PARAMETER FolderPath 10 | Specifies the folder paths containing the PowerShell script files to process. 11 | 12 | .EXAMPLE 13 | Add-TestimoSources -FolderPath "C:\Scripts" 14 | 15 | Description: 16 | Adds external sources from the "C:\Scripts" folder to the Testimo configuration. 17 | 18 | .EXAMPLE 19 | Add-TestimoSources -FolderPath "C:\Scripts", "D:\Scripts" 20 | 21 | Description: 22 | Adds external sources from both "C:\Scripts" and "D:\Scripts" folders to the Testimo configuration. 23 | #> 24 | [CmdletBinding()] 25 | param( 26 | [string[]] $FolderPath 27 | ) 28 | $ListNewSources = [System.Collections.Generic.List[string]]::new() 29 | $ListOverwritten = [System.Collections.Generic.List[string]]::new() 30 | foreach ($Folder in $FolderPath) { 31 | $FilesWithCode = @( Get-ChildItem -Path "$Folder\*.ps1" -ErrorAction SilentlyContinue -Recurse ) 32 | 33 | Foreach ($import in $FilesWithCode) { 34 | $Content = Get-Content -LiteralPath $import.fullname -Raw 35 | $Script = [scriptblock]::Create($Content) 36 | $Data = $Script.Invoke() 37 | foreach ($Source in $Data) { 38 | if (-not $Script:TestimoConfiguration[$Source.Scope]) { 39 | $Script:TestimoConfiguration[$Source.Scope] = [ordered] @{} 40 | } 41 | if ($Source.Scope -in 'Forest', 'Domain', 'DC') { 42 | if ($Script:TestimoConfiguration['ActiveDirectory'][$Source.Name]) { 43 | $ListOverwritten.Add($Source.Name) 44 | } else { 45 | $ListNewSources.Add($Source.Name) 46 | } 47 | $Script:TestimoConfiguration['ActiveDirectory'][$Source.Name] = $Source 48 | } else { 49 | $Script:TestimoConfiguration[$Source.Scope][$Source.Name] = $Source 50 | $ListNewSources.Add($Source.Name) 51 | } 52 | } 53 | } 54 | } 55 | if ($ListNewSources.Count -gt 0) { 56 | Out-Informative -Text 'Following external sources were added' -Level 0 -Status $true -ExtendedValue ($ListNewSources -join ', ') -OverrideTextStatus "External Sources" 57 | } 58 | if ($ListOverwritten.Count -gt 0) { 59 | Out-Informative -Text 'Following external sources overwritten' -Level 0 -Status $true -ExtendedValue ($ListOverwritten -join ', ') -OverrideTextStatus "Overwritten Sources" 60 | } 61 | } -------------------------------------------------------------------------------- /Private/Get-RequestedSources.ps1: -------------------------------------------------------------------------------- 1 | function Get-RequestedSources { 2 | <# 3 | .SYNOPSIS 4 | Retrieves requested sources based on specified criteria. 5 | 6 | .DESCRIPTION 7 | This function retrieves requested sources based on the provided sources, exclude sources, include tags, and exclude tags. It filters out sources that do not match the criteria and categorizes them into working and non-working lists. 8 | 9 | .PARAMETER Sources 10 | Specifies an array of sources to be considered. 11 | 12 | .PARAMETER ExcludeSources 13 | Specifies an array of sources to be excluded from consideration. 14 | 15 | .PARAMETER IncludeTags 16 | Specifies an array of tags that sources must include to be considered. 17 | 18 | .PARAMETER ExcludeTags 19 | Specifies an array of tags that sources must exclude to be considered. 20 | 21 | .EXAMPLE 22 | Get-RequestedSources -Sources @('Source1', 'Source2') -ExcludeSources @('Source3') -IncludeTags @('Tag1') 23 | 24 | Description: 25 | Retrieves sources 'Source1' and 'Source2', excludes 'Source3', and includes sources with 'Tag1'. 26 | 27 | .EXAMPLE 28 | Get-RequestedSources -Sources @('SourceA', 'SourceB') -ExcludeTags @('TagX') 29 | 30 | Description: 31 | Retrieves sources 'SourceA' and 'SourceB', excluding sources with 'TagX'. 32 | 33 | #> 34 | [CmdletBinding()] 35 | param( 36 | [string[]] $Sources, 37 | [string[]] $ExcludeSources, 38 | [string[]] $IncludeTags, 39 | [string[]] $ExcludeTags 40 | ) 41 | $NonWorking = [System.Collections.Generic.List[String]]::new() 42 | $Working = [System.Collections.Generic.List[String]]::new() 43 | $NonWorkingExclusions = [System.Collections.Generic.List[String]]::new() 44 | $WorkingExclusions = [System.Collections.Generic.List[String]]::new() 45 | foreach ($Source in $Sources) { 46 | $Found = $false 47 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 48 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 49 | if ($Source -in $Script:TestimoConfiguration[$Key].Keys) { 50 | $Found = $true 51 | break 52 | } 53 | } 54 | } 55 | if ($Found) { 56 | $Working.Add($Source) 57 | } else { 58 | $NonWorking.Add($Source) 59 | } 60 | } 61 | foreach ($Source in $ExcludeSources) { 62 | $Found = $false 63 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 64 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 65 | if ($Source -in $Script:TestimoConfiguration[$Key].Keys) { 66 | $Found = $true 67 | break 68 | } 69 | } 70 | } 71 | if ($Found) { 72 | $WorkingExclusions.Add($Source) 73 | } else { 74 | $NonWorkingExclusions.Add($Source) 75 | } 76 | } 77 | foreach ($Tag in $IncludeTags) { 78 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 79 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 80 | foreach ($Source in $Script:TestimoConfiguration[$Key].Keys) { 81 | if ($Tag -in $Script:TestimoConfiguration[$Key][$Source]['Source']['Details'].Tags) { 82 | $Working.Add($Source) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | if ($IncludeTags.Count -gt 0) { 89 | Out-Informative -Text 'Following tags will be used' -Level 0 -Status $true -ExtendedValue ($IncludeTags -join ', ') -OverrideTextStatus "Tags" 90 | } 91 | if ($ExcludeTags.Count -gt 0) { 92 | Out-Informative -Text 'Following tags will be excluded' -Level 0 -Status $true -ExtendedValue ($ExcludeTags -join ', ') -OverrideTextStatus "Tags" 93 | } 94 | if ($Working.Count -gt 0) { 95 | Out-Informative -Text 'Following sources will be used' -Level 0 -Status $true -ExtendedValue ($Working -join ', ') -OverrideTextStatus "Valid Sources" 96 | } 97 | if ($NonWorking.Count -gt 0) { 98 | Out-Informative -Text 'Following sources were provided incorrectly (skipping)' -Level 0 -Status $false -ExtendedValue ($NonWorking -join ', ') -OverrideTextStatus "Failed Sources" 99 | } 100 | if ($WorkingExclusions.Count -gt 0) { 101 | Out-Informative -Text 'Following sources will be excluded' -Level 0 -Status $true -ExtendedValue ($WorkingExclusions -join ', ') -OverrideTextStatus "Valid Sources" 102 | } 103 | if ($NonWorkingExclusions.Count -gt 0) { 104 | Out-Informative -Text 'Following sources for exclusions were provided incorrectly (skipping)' -Level 0 -Status $false -ExtendedValue ($NonWorkingExclusions -join ', ') -OverrideTextStatus "Failed Sources" 105 | } 106 | } -------------------------------------------------------------------------------- /Private/Get-TestimoDomainControllers.ps1: -------------------------------------------------------------------------------- 1 | function Get-TestimoDomainControllers { 2 | <# 3 | .SYNOPSIS 4 | Retrieves domain controllers in a specified domain, with an option to skip read-only domain controllers. 5 | 6 | .DESCRIPTION 7 | This function retrieves domain controllers in the specified domain. It can skip read-only domain controllers if desired. 8 | 9 | .PARAMETER Domain 10 | Specifies the name of the domain to retrieve domain controllers from. 11 | 12 | .PARAMETER SkipRODC 13 | Indicates whether to skip read-only domain controllers. 14 | 15 | .EXAMPLE 16 | Get-TestimoDomainControllers -Domain "contoso.com" 17 | Retrieves all domain controllers in the "contoso.com" domain. 18 | 19 | .EXAMPLE 20 | Get-TestimoDomainControllers -Domain "contoso.com" -SkipRODC 21 | Retrieves all domain controllers in the "contoso.com" domain, excluding read-only domain controllers. 22 | 23 | #> 24 | [CmdletBinding()] 25 | param( 26 | [string] $Domain, 27 | [switch] $SkipRODC 28 | ) 29 | try { 30 | $DC = Get-ADDomainController -Discover -DomainName $Domain 31 | $DomainControllers = Get-ADDomainController -Server $DC.HostName[0] -Filter * -ErrorAction Stop 32 | if ($SkipRODC) { 33 | $DomainControllers = $DomainControllers | Where-Object { $_.IsReadOnly -eq $false } 34 | } 35 | foreach ($_ in $DomainControllers) { 36 | if ($Script:TestimoConfiguration['Inclusions']['DomainControllers']) { 37 | if ($_ -in $Script:TestimoConfiguration['Inclusions']['DomainControllers']) { 38 | [PSCustomObject] @{ 39 | Name = $($_.HostName).ToLower() 40 | IsPDC = $_.OperationMasterRoles -contains 'PDCEmulator' 41 | } 42 | } 43 | # We skip checking for exclusions 44 | continue 45 | } 46 | if ($_.HostName -notin $Script:TestimoConfiguration['Exclusions']['DomainControllers']) { 47 | [PSCustomObject] @{ 48 | Name = $($_.HostName).ToLower() 49 | IsPDC = $_.OperationMasterRoles -contains 'PDCEmulator' 50 | } 51 | } 52 | } 53 | } catch { 54 | return 55 | } 56 | } -------------------------------------------------------------------------------- /Private/Get-TestimoSourcesStatus.ps1: -------------------------------------------------------------------------------- 1 | function Get-TestimoSourcesStatus { 2 | <# 3 | .SYNOPSIS 4 | Retrieves the status of Testimo sources based on the specified scope. 5 | 6 | .DESCRIPTION 7 | This function retrieves the status of Testimo sources based on the specified scope. It checks if any Testimo source within the specified scope is enabled. 8 | 9 | .PARAMETER Scope 10 | Specifies the scope for which the Testimo sources status should be retrieved. 11 | 12 | .EXAMPLE 13 | Get-TestimoSourcesStatus -Scope "Global" 14 | Retrieves the status of Testimo sources within the Global scope. 15 | 16 | .EXAMPLE 17 | Get-TestimoSourcesStatus -Scope "Local" 18 | Retrieves the status of Testimo sources within the Local scope. 19 | #> 20 | [cmdletbinding()] 21 | param( 22 | [string] $Scope 23 | ) 24 | $AllTests = foreach ($Source in $($Script:TestimoConfiguration.ActiveDirectory.Keys)) { 25 | if ($Scope -ne $Script:TestimoConfiguration.ActiveDirectory[$Source].Scope) { 26 | continue 27 | } 28 | $Script:TestimoConfiguration.ActiveDirectory["$Source"].Enable 29 | } 30 | $AllTests -contains $true 31 | } -------------------------------------------------------------------------------- /Private/Initialize-TestimoTests.ps1: -------------------------------------------------------------------------------- 1 | function Initialize-TestimoTests { 2 | <# 3 | .SYNOPSIS 4 | Simple command that goes thru all the tests and makes sure minimal tests are "improved" to become standard test 5 | 6 | .DESCRIPTION 7 | Simple command that goes thru all the tests and makes sure minimal tests are "improved" to become standard test 8 | 9 | .EXAMPLE 10 | Initialize-TestimoTests 11 | 12 | .NOTES 13 | General notes 14 | #> 15 | [CmdletBinding()] 16 | param() 17 | 18 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 19 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 20 | foreach ($Source in [string[]] $Script:TestimoConfiguration[$Key].Keys) { 21 | foreach ($TestName in [string[]] $Script:TestimoConfiguration[$Key][$Source].Tests.Keys) { 22 | $TestValue = $Script:TestimoConfiguration[$Key][$Source].Tests.$TestName 23 | if ($TestValue -is [System.Collections.IDictionary]) { 24 | if ($TestValue.Details) { 25 | if (-not $TestValue.Details.Category) { 26 | if ($Script:TestimoConfiguration[$Key][$Source].Source.Details -and $Script:TestimoConfiguration[$Key][$Source].Source.Details.Category) { 27 | $TestValue.Details.Category = $Script:TestimoConfiguration[$Key][$Source].Source.Details.Category 28 | } 29 | } 30 | if (-not $TestValue.Details.Category) { 31 | if ($Script:TestimoConfiguration[$Key][$Source].Source.Details -and $Script:TestimoConfiguration[$Key][$Source].Source.Details.ActionType) { 32 | $TestValue.Details.ActionType = $Script:TestimoConfiguration[$Key][$Source].Source.Details.ActionType 33 | } 34 | } 35 | } 36 | } else { 37 | # we use configuration as default category 38 | $DefaultCategory = 'Configuration' 39 | # but we also check if source has something different and we use it 40 | if ($Script:TestimoConfiguration[$Key][$Source].Source.Details -and $Script:TestimoConfiguration[$Key][$Source].Source.Details.Category) { 41 | $DefaultCategory = $Script:TestimoConfiguration[$Key][$Source].Source.Details.Category 42 | } 43 | 44 | # Overwrite the test value with the default settings 45 | # This is to support basic paramters testing in a way that DSC does 46 | $Script:TestimoConfiguration[$Key][$Source].Tests.$TestName = [ordered] @{ 47 | Enable = $true 48 | Name = $TestName 49 | Parameters = @{ 50 | Property = $TestName 51 | OperationType = 'eq' 52 | ExpectedValue = $TestValue 53 | } 54 | Details = [ordered] @{ 55 | Category = $DefaultCategory 56 | Importance = 5 57 | ActionType = 2 58 | StatusTrue = 1 59 | StatusFalse = 5 60 | } 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /Private/New-ChartData.ps1: -------------------------------------------------------------------------------- 1 | function New-ChartData { 2 | <# 3 | .SYNOPSIS 4 | Creates a chart data structure based on the input results. 5 | 6 | .DESCRIPTION 7 | This function takes an array of results and generates a chart data structure that counts the occurrences of each assessment. If an assessment is null, it is categorized as 'Skipped'. 8 | 9 | .PARAMETER Results 10 | The array of results to generate the chart data from. 11 | 12 | .EXAMPLE 13 | $results = @( 14 | [PSCustomObject]@{ Assessment = 'Pass' }, 15 | [PSCustomObject]@{ Assessment = 'Fail' }, 16 | [PSCustomObject]@{ Assessment = 'Pass' }, 17 | [PSCustomObject]@{ Assessment = $null }, 18 | [PSCustomObject]@{ Assessment = 'Pass' } 19 | ) 20 | New-ChartData -Results $results 21 | 22 | This example creates a chart data structure based on the provided results array. 23 | 24 | #> 25 | [cmdletBinding()] 26 | param( 27 | $Results 28 | ) 29 | $ChartData = [ordered] @{} 30 | foreach ($Result in $Results) { 31 | if ($null -ne $Result.Assessment) { 32 | if (-not $ChartData[$Result.Assessment]) { 33 | $ChartData[$Result.Assessment] = [ordered] @{ 34 | Count = 0 35 | Color = $Script:StatusToColors[$Result.Assessment] 36 | } 37 | } 38 | $ChartData[$Result.Assessment].Count++ 39 | } else { 40 | # if for whatever reason result.assesment is null we need to improvise 41 | if (-not $ChartData['Skipped']) { 42 | $ChartData['Skipped'] = [ordered] @{ 43 | Count = 0 44 | Color = $Script:StatusToColors['Skipped'] 45 | } 46 | } 47 | $ChartData['Skipped'].Count++ 48 | } 49 | } 50 | $ChartData 51 | } -------------------------------------------------------------------------------- /Private/Out-Begin.ps1: -------------------------------------------------------------------------------- 1 | function Out-Begin { 2 | <# 3 | .SYNOPSIS 4 | Outputs formatted text based on specified parameters. 5 | 6 | .DESCRIPTION 7 | The Out-Begin function outputs formatted text to the console based on the provided Scope, Text, Level, Type, Domain, and DomainController parameters. 8 | 9 | .PARAMETER Scope 10 | Specifies the scope of the output. Valid values are 'Forest', 'Domain', or 'DC'. 11 | 12 | .PARAMETER Text 13 | Specifies the text to be displayed. 14 | 15 | .PARAMETER Level 16 | Specifies the level of the output. 17 | 18 | .PARAMETER Type 19 | Specifies the type of output. Default value is 't'. 20 | 21 | .PARAMETER Domain 22 | Specifies the domain for the output. 23 | 24 | .PARAMETER DomainController 25 | Specifies the domain controller for the output. 26 | 27 | .EXAMPLE 28 | Out-Begin -Scope 'Forest' -Text 'Sample text' -Level 1 -Type 't' -Domain 'ExampleDomain' -DomainController 'DC1' 29 | Outputs formatted text for the Forest scope with the specified parameters. 30 | 31 | .EXAMPLE 32 | Out-Begin -Scope 'Domain' -Text 'Error message' -Level 2 -Type 'e' -Domain 'AnotherDomain' 33 | Outputs an error message for the Domain scope with the specified parameters. 34 | #> 35 | [CmdletBinding()] 36 | param( 37 | [string] $Scope, 38 | [string] $Text, 39 | [int] $Level, 40 | [string] $Type = 't', 41 | [string] $Domain, 42 | [string] $DomainController 43 | ) 44 | if ($Scope -in 'Forest', 'Domain', 'DC') { 45 | if ($Domain -and $DomainController) { 46 | if ($Type -eq 't') { 47 | [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow 48 | } elseif ($Type -eq 'e') { 49 | [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow 50 | } else { 51 | [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow, [ConsoleColor]::Yellow 52 | } 53 | $TestText = "[$Type]", "[$Domain]", "[$($DomainController)] ", $Text 54 | } elseif ($Domain) { 55 | if ($Type -eq 't') { 56 | [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 57 | } elseif ($Type -eq 'e') { 58 | [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 59 | } else { 60 | [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 61 | } 62 | $TestText = "[$Type]", "[$Domain] ", $Text 63 | } elseif ($DomainController) { 64 | # Shouldn't really happen 65 | Write-Warning "Out-Begin - Shouldn't happen - Fix me." 66 | } else { 67 | if ($Type -eq 't') { 68 | [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 69 | } elseif ($Type -eq 'e') { 70 | [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 71 | } else { 72 | [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 73 | } 74 | $TestText = "[$Type]", "[Forest] ", $Text 75 | } 76 | } else { 77 | if ($Type -eq 't') { 78 | [ConsoleColor[]] $Color = [ConsoleColor]::Cyan, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 79 | } elseif ($Type -eq 'e') { 80 | [ConsoleColor[]] $Color = [ConsoleColor]::Red, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 81 | } else { 82 | [ConsoleColor[]] $Color = [ConsoleColor]::Yellow, [ConsoleColor]::DarkGray, [ConsoleColor]::Yellow 83 | } 84 | $TestText = "[$Type]", "[$Scope] ", $Text 85 | } 86 | Write-Color -Text $TestText -Color $Color -StartSpaces $Level -NoNewLine 87 | } -------------------------------------------------------------------------------- /Private/Out-Failure.ps1: -------------------------------------------------------------------------------- 1 | function Out-Failure { 2 | <# 3 | .SYNOPSIS 4 | Sends a failure status message with detailed information. 5 | 6 | .DESCRIPTION 7 | The Out-Failure function sends a failure status message with detailed information including the scope, text, level, extended value, domain, domain controller, reference ID, type, source, and test. 8 | 9 | .PARAMETER Scope 10 | Specifies the scope of the failure. 11 | 12 | .PARAMETER Text 13 | Specifies the text message associated with the failure. 14 | 15 | .PARAMETER Level 16 | Specifies the level of the failure. 17 | 18 | .PARAMETER ExtendedValue 19 | Specifies additional extended value information for the failure. Default is 'Input data not provided. Failing test.'. 20 | 21 | .PARAMETER Domain 22 | Specifies the domain associated with the failure. 23 | 24 | .PARAMETER DomainController 25 | Specifies the domain controller related to the failure. 26 | 27 | .PARAMETER ReferenceID 28 | Specifies a reference ID for the failure. 29 | 30 | .PARAMETER Type 31 | Specifies the type of failure. Valid values are 'e' (error), 'i' (information), or 't' (test). Default is 't'. 32 | 33 | .PARAMETER Source 34 | Specifies the source of the failure as a dictionary. 35 | 36 | .PARAMETER Test 37 | Specifies the test information related to the failure as a dictionary. 38 | 39 | .EXAMPLE 40 | Out-Failure -Scope "Global" -Text "Connection failed" -Level 3 -Domain "example.com" -DomainController "DC1" -ReferenceID "12345" -Type "e" -Source @{"SourceKey"="SourceValue"} -Test @{"TestKey"="TestValue"} 41 | 42 | Sends a failure status message with the specified parameters. 43 | 44 | #> 45 | [CmdletBinding()] 46 | param( 47 | [string] $Scope, 48 | [string] $Text, 49 | [int] $Level, 50 | [string] $ExtendedValue = 'Input data not provided. Failing test.', 51 | [string] $Domain, 52 | [string] $DomainController, 53 | [string] $ReferenceID, 54 | [validateSet('e', 'i', 't')][string] $Type = 't', 55 | [System.Collections.IDictionary] $Source, 56 | [System.Collections.IDictionary] $Test 57 | ) 58 | Out-Begin -Scope $Scope -Text $Text -Level $Level -Domain $Domain -DomainController $DomainController -Type $Type 59 | Out-Status -Scope $Scope -Text $Text -Status $false -ExtendedValue $ExtendedValue -Domain $Domain -DomainController $DomainController -ReferenceID $ReferenceID -Source $Source -Test $Test 60 | } -------------------------------------------------------------------------------- /Private/Out-Skip.ps1: -------------------------------------------------------------------------------- 1 | function Out-Skip { 2 | <# 3 | .SYNOPSIS 4 | Skips a specific test and updates the test summary. 5 | 6 | .DESCRIPTION 7 | The Out-Skip function is used to skip a specific test and update the test summary with the skipped test count. It also provides a reason for skipping the test. 8 | 9 | .PARAMETER Scope 10 | Specifies the scope of the test. 11 | 12 | .PARAMETER TestsSummary 13 | Specifies the summary of all tests. 14 | 15 | .PARAMETER Level 16 | Specifies the level of the test. 17 | 18 | .PARAMETER Domain 19 | Specifies the domain of the test. 20 | 21 | .PARAMETER DomainController 22 | Specifies the domain controller for the test. 23 | 24 | .PARAMETER Test 25 | Specifies the name of the test being skipped. 26 | 27 | .PARAMETER Source 28 | Specifies the source of the test. 29 | 30 | .PARAMETER Reason 31 | Specifies the reason for skipping the test. Default is 'Skipping - unmet dependency'. 32 | 33 | .EXAMPLE 34 | Out-Skip -Scope 'Integration' -TestsSummary $TestsSummary -Level 1 -Domain 'example.com' -DomainController 'DC1' -Test 'Test1' -Source 'ModuleA' 35 | 36 | Description 37 | ----------- 38 | Skips the test named 'Test1' at level 1 in the 'Integration' scope for the domain 'example.com' using the domain controller 'DC1'. Updates the test summary with the skipped test count. 39 | 40 | #> 41 | [CmdletBinding()] 42 | param( 43 | [string] $Scope, 44 | [PSCustomobject] $TestsSummary, 45 | [int] $Level = 0, 46 | [string] $Domain, 47 | [string] $DomainController, 48 | [string] $Test, 49 | [string] $Source, 50 | [string] $Reason = 'Skipping - unmet dependency' 51 | 52 | ) 53 | Out-Begin -Scope $Scope -Type 'i' -Text $Test -Level $Level -Domain $Domain -DomainController $DomainController 54 | 55 | Out-Status -Scope $Scope -Text $Test -Status $null -ExtendedValue $Reason -Domain $Domain -DomainController $DomainController -ReferenceID $Source 56 | 57 | $TestsSummary.Skipped = $TestsSummary.Skipped + 1 58 | $TestsSummary.Total = $TestsSummary.Failed + $TestsSummary.Passed + $TestsSummary.Skipped 59 | $TestsSummary 60 | } -------------------------------------------------------------------------------- /Private/RiskLevels.ps1: -------------------------------------------------------------------------------- 1 | $Script:Importance = @{ 2 | 0 = 'Informational' 3 | 1 = 'Negligible' 4 | 2 = 'Very low' 5 | 3 = 'Low' 6 | 4 = 'Minor' 7 | 5 = 'Moderate Low' 8 | 6 = 'Moderate' 9 | 7 = 'High' 10 | 8 = 'Very High' 11 | 9 = 'Significant' 12 | 10 = 'Extreme' 13 | } 14 | $Script:StatusTranslation = @{ 15 | -1 = 'Skipped' 16 | 0 = 'Informational' # #4D9F6F # Low risk 17 | 1 = 'Good' 18 | 2 = 'Low' # #507DC6 # General Risk 19 | 3 = 'Elevated' # #998D16 # Significant Risk 20 | 4 = 'High' # #7A5928 High Risk 21 | 5 = 'Severe' # #D65742 Server Risk 22 | } 23 | 24 | $Script:StatusToColors = @{ 25 | 'Skipped' = 'DeepSkyBlue' 26 | 'Informational' = 'CornflowerBlue' 27 | 'Good' = 'LawnGreen' 28 | 'Low' = 'ParisDaisy' # # General Risk 29 | 'Elevated' = 'SafetyOrange' # # Significant Risk 30 | 'High' = 'InternationalOrange' # High Risk 31 | 'Severe' = 'TorchRed' # Server Risk 32 | $true = 'LawnGreen' 33 | $false = 'TorchRed' 34 | } 35 | 36 | $Script:StatusTranslationColors = @{ 37 | -1 = 'DeepSkyBlue' 38 | 0 = 'CornflowerBlue' 39 | 1 = 'LawnGreen' 40 | 2 = 'ParisDaisy' # # General Risk 41 | 3 = 'SafetyOrange' # # Significant Risk 42 | 4 = 'InternationalOrange' # High Risk 43 | 5 = 'TorchRed' # Server Risk 44 | } 45 | $Script:ActionType = @{ 46 | 0 = 'Informational' 47 | 1 = 'Recommended' 48 | 2 = 'Must Implement' 49 | } 50 | $Script:StatusTranslationConsoleColors = @{ 51 | -1 = [System.ConsoleColor]::DarkBlue 52 | 0 = [System.ConsoleColor]::DarkBlue 53 | 1 = [System.ConsoleColor]::Green 54 | 2 = [System.ConsoleColor]::Magenta # # General Risk 55 | 3 = [System.ConsoleColor]::DarkMagenta # # Significant Risk 56 | 4 = [System.ConsoleColor]::Red # High Risk 57 | 5 = [System.ConsoleColor]::DarkRed # Server Risk 58 | } 59 | 60 | <# 61 | $Script:WarningSystem = @{ 62 | 0 = 'All Clear' 63 | 1 = 'Advice' 64 | 2 = 'Watch and Act' 65 | 3 = 'Emergency Warning' 66 | } 67 | $Script:PotentialImpact = @{ 68 | 0 = 'Neglible' 69 | 1 = 'Minor' 70 | 3 = 'Moderate' 71 | 4 = 'Significant' 72 | 5 = 'Severe' 73 | } 74 | $Script:Likelihood = @{ 75 | 0 = 'Very Unlikely' 76 | 1 = 'Unlikely' 77 | 2 = 'Possible' 78 | 3 = 'Likely' 79 | 4 = 'Very Likely' 80 | } 81 | $Script:Consequence = @{ 82 | 0 = 'Small' 83 | 1 = 'Moderate' 84 | 2 = 'Severe' 85 | 3 = 'Catastrophic' 86 | } 87 | #> -------------------------------------------------------------------------------- /Private/Set-TestsStatus.ps1: -------------------------------------------------------------------------------- 1 | function Set-TestsStatus { 2 | <# 3 | .SYNOPSIS 4 | Sets the status of tests based on provided parameters. 5 | 6 | .DESCRIPTION 7 | This function sets the status of tests based on the specified sources, tags, and exclusion criteria. 8 | 9 | .PARAMETER Sources 10 | Specifies an array of sources to include for test status setting. 11 | 12 | .PARAMETER ExcludeSources 13 | Specifies an array of sources to exclude for test status setting. 14 | 15 | .PARAMETER IncludeTags 16 | Specifies an array of tags to include for test status setting. 17 | 18 | .PARAMETER ExcludeTags 19 | Specifies an array of tags to exclude for test status setting. 20 | 21 | .EXAMPLE 22 | Set-TestsStatus -Sources Source1, Source2 -IncludeTags Tag1, Tag2 23 | 24 | Description: 25 | Sets the status of tests for Source1 and Source2 with tags Tag1 and Tag2 enabled. 26 | 27 | .EXAMPLE 28 | Set-TestsStatus -Sources Source3 -ExcludeSources Source4 -ExcludeTags Tag3 29 | 30 | Description: 31 | Sets the status of tests for Source3 with Source4 excluded and Tag3 disabled. 32 | 33 | #> 34 | [CmdletBinding()] 35 | param( 36 | [string[]] $Sources, 37 | [string[]] $ExcludeSources, 38 | [string[]] $IncludeTags, 39 | [string[]] $ExcludeTags 40 | ) 41 | # we first disable all sources to make sure it's a clean start 42 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 43 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 44 | foreach ($Source in $Script:TestimoConfiguration.$Key.Keys) { 45 | if ($Script:TestimoConfiguration[$Key][$Source]) { 46 | $Script:TestimoConfiguration[$Key][$Source]['Enable'] = $false 47 | $Script:TestimoConfiguration.Types[$Key] = $false 48 | } 49 | } 50 | } 51 | } 52 | # then we go thru the sources and enable them 53 | foreach ($Key in $Script:TestimoConfiguration.Keys) { 54 | if ($Key -notin 'Types', 'Exclusions', 'Inclusions', 'Debug') { 55 | foreach ($Tag in $IncludeTags) { 56 | if ($Script:TestimoConfiguration[$Key]) { 57 | foreach ($Source in $Script:TestimoConfiguration[$Key].Keys) { 58 | if ($Tag -in $Script:TestimoConfiguration[$Key][$Source]['Source']['Details'].Tags) { 59 | $Script:TestimoConfiguration[$Key][$Source]['Enable'] = $true 60 | $Script:TestimoConfiguration.Types[$Key] = $true 61 | } 62 | } 63 | } 64 | } 65 | foreach ($Source in $Sources) { 66 | if ($Script:TestimoConfiguration[$Key][$Source]) { 67 | $Script:TestimoConfiguration[$Key][$Source]['Enable'] = $true 68 | $Script:TestimoConfiguration.Types[$Key] = $true 69 | } 70 | } 71 | foreach ($Source in $ExcludeSources) { 72 | if ($Script:TestimoConfiguration[$Key][$Source]) { 73 | $Script:TestimoConfiguration[$Key][$Source]['Enable'] = $false 74 | } 75 | } 76 | foreach ($Tag in $ExcludeTags) { 77 | if ($Script:TestimoConfiguration[$Key]) { 78 | foreach ($Source in $Script:TestimoConfiguration[$Key].Keys) { 79 | if ($Tag -in $Script:TestimoConfiguration[$Key][$Source]['Source']['Details'].Tags) { 80 | $Script:TestimoConfiguration[$Key][$Source]['Enable'] = $false 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/ComputersUnsupported.ps1: -------------------------------------------------------------------------------- 1 | $ComputersUnsupported = @{ 2 | Name = 'DomainComputersUnsupported' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Computers Unsupported" 7 | Data = { 8 | $Computers = Get-ADComputer -Filter { ( operatingsystem -like "*xp*") -or (operatingsystem -like "*vista*") -or ( operatingsystem -like "*Windows NT*") -or ( operatingsystem -like "*2000*") -or ( operatingsystem -like "*2003*") } -Property Name, OperatingSystem, OperatingSystemServicePack, lastlogontimestamp -Server $Domain 9 | $Computers | Select-Object Name, OperatingSystem, OperatingSystemServicePack, @{name = "lastlogontimestamp"; expression = { [datetime]::fromfiletime($_.lastlogontimestamp) } } 10 | } 11 | Details = [ordered] @{ 12 | Area = 'Objects' 13 | Category = 'Cleanup' 14 | Importance = 3 15 | Description = 'Computers running an unsupported operating system.' 16 | Resolution = 'Upgrade or remove computers from Domain.' 17 | Resources = @() 18 | } 19 | ExpectedOutput = $false 20 | } 21 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/ComputersUnsupportedMainstream.ps1: -------------------------------------------------------------------------------- 1 | $ComputersUnsupportedMainstream = @{ 2 | Name = 'DomainComputersUnsupportedMainstream' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Computers Unsupported Mainstream Only" 7 | Data = { 8 | $Computers = Get-ADComputer -Filter { ( operatingsystem -like "*2008*") } -Property Name, OperatingSystem, OperatingSystemServicePack, lastlogontimestamp -Server $Domain 9 | $Computers | Select-Object Name, OperatingSystem, OperatingSystemServicePack, @{name = "lastlogontimestamp"; expression = { [datetime]::fromfiletime($_.lastlogontimestamp) } } 10 | } 11 | Details = [ordered] @{ 12 | Area = 'Objects' 13 | Category = 'Cleanup' 14 | Importance = 3 15 | Description = 'Computers running an unsupported operating system, but with possibly Microsoft support.' 16 | Resolution = 'Consider upgrading computers running Windows Server 2008 or Windows Server 2008 R2 to a version that still offers mainstream support from Microsoft.' 17 | Resources = @() 18 | } 19 | ExpectedOutput = $false 20 | } 21 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/DHCPAuthorized.ps1: -------------------------------------------------------------------------------- 1 | $DHCPAuthorized = @{ 2 | Name = 'DomainDHCPAuthorized' 3 | Enable = $false 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "DHCP authorized in domain" 7 | Data = { 8 | #$DomainInformation = Get-ADDomain -Identity 'ad.evotec.pl' 9 | $SearchBase = 'cn=configuration,{0}' -f $DomainInformation.DistinguishedName 10 | Get-ADObject -SearchBase $searchBase -Filter "objectclass -eq 'dhcpclass' -AND Name -ne 'dhcproot'" #| select name 11 | } 12 | Requirements = @{ 13 | IsDomainRoot = $true 14 | } 15 | Details = [ordered] @{ 16 | Area = 'DHCP' 17 | Category = 'Configuration' 18 | Severity = '' 19 | Importance = 0 20 | Description = "" 21 | Resolution = '' 22 | Resources = @( 23 | 24 | ) 25 | } 26 | ExpectedOutput = $true 27 | } 28 | Tests = [ordered] @{ 29 | DHCPAuthorized = @{ 30 | Enable = $true 31 | Name = 'At least 1 DHCP Server Authorized' 32 | Parameters = @{ 33 | ExpectedCount = '1' 34 | OperationType = 'ge' 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Private/SourcesDomain/DNSForwarders.ps1: -------------------------------------------------------------------------------- 1 | $DNSForwaders = @{ 2 | Name = 'DomainDNSForwaders' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "DNS Forwarders" 7 | Data = { 8 | [Array] $Forwarders = Get-WinADDnsServerForwarder -Forest $ForestName -Domain $Domain -WarningAction SilentlyContinue 9 | if ($Forwarders.Count -gt 1) { 10 | $Comparision = Compare-MultipleObjects -Objects $Forwarders -FormatOutput -CompareSorted:$true -ExcludeProperty GatheredFrom -SkipProperties -Property 'IpAddress' -WarningAction SilentlyContinue 11 | [PSCustomObject] @{ 12 | Source = $Comparision.Source -join ', ' 13 | Status = $Comparision.Status 14 | } 15 | } elseif ($Forwarders.Count -eq 0) { 16 | [PSCustomObject] @{ 17 | # This code takes care of no forwarders 18 | Source = 'No forwarders set' 19 | Status = $false 20 | } 21 | } else { 22 | # This code takes care of only 1 server within a domain. If there is 1 server available (as others may be dead/unavailable at the time it assumes Pass) 23 | [PSCustomObject] @{ 24 | Source = $Forwarders[0].IPAddress -join ', ' 25 | Status = $true 26 | } 27 | } 28 | } 29 | Details = [ordered] @{ 30 | Area = 'DNS' 31 | Category = 'Configuration' 32 | Importance = 3 33 | Description = '' 34 | Resolution = '' 35 | Resources = @( 36 | 37 | ) 38 | } 39 | ExpectedOutput = $true 40 | } 41 | Tests = [ordered] @{ 42 | SameForwarders = @{ 43 | Enable = $true 44 | Name = 'Same DNS Forwarders' 45 | Parameters = @{ 46 | Property = 'Status' 47 | ExpectedValue = $true 48 | OperationType = 'eq' 49 | PropertyExtendedValue = 'Source' 50 | } 51 | Description = 'DNS forwarders within one domain should have identical setup' 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/DNSScavengingForPrimaryDNSServer.ps1: -------------------------------------------------------------------------------- 1 | $DNSScavengingForPrimaryDNSServer = @{ 2 | Name = 'DomainDNSScavengingForPrimaryDNSServer' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "DNS Scavenging - Primary DNS Server" 7 | Data = { 8 | Get-WinADDnsServerScavenging -Forest $ForestName -IncludeDomains $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'DNS' 12 | Category = 'Configuration' 13 | Importance = 3 14 | Description = '' 15 | Resolution = '' 16 | Resources = @( 17 | 18 | ) 19 | } 20 | ExpectedOutput = $true 21 | } 22 | Tests = [ordered] @{ 23 | ScavengingCount = @{ 24 | Enable = $true 25 | Name = 'Scavenging DNS Servers Count' 26 | Parameters = @{ 27 | WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } 28 | ExpectedCount = 1 29 | OperationType = 'eq' 30 | } 31 | 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.' 32 | } 33 | ScavengingInterval = @{ 34 | Enable = $true 35 | Name = 'Scavenging Interval' 36 | Parameters = @{ 37 | WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } 38 | Property = 'ScavengingInterval', 'Days' 39 | ExpectedValue = 7 40 | OperationType = 'le' 41 | } 42 | } 43 | 'Scavenging State' = @{ 44 | Enable = $true 45 | Name = 'Scavenging State' 46 | Parameters = @{ 47 | WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } 48 | Property = 'ScavengingState' 49 | ExpectedValue = $true 50 | OperationType = 'eq' 51 | } 52 | Description = 'Scavenging State is responsible for enablement of scavenging for all new zones created.' 53 | RecommendedValue = $true 54 | DescriptionRecommended = 'It should be enabled so all new zones are subject to scavanging.' 55 | DefaultValue = $false 56 | } 57 | 'Last Scavenge Time' = @{ 58 | Enable = $true 59 | Name = 'Last Scavenge Time' 60 | Parameters = @{ 61 | WhereObject = { $null -ne $_.ScavengingInterval -and $_.ScavengingInterval -ne 0 } 62 | # this date should be the same as in Scavending Interval 63 | Property = 'LastScavengeTime' 64 | # we need to use string which will be converted to ScriptBlock later on due to configuration export to JSON 65 | ExpectedValue = '(Get-Date).AddDays(-7)' 66 | OperationType = 'gt' 67 | } 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/DNSZonesAging.ps1: -------------------------------------------------------------------------------- 1 | $DnsZonesAging = @{ 2 | Name = 'DomainDnsZonesAging' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Aging primary DNS Zone" 7 | Data = { 8 | Get-WinDnsServerZones -Forest $ForestName -ZoneName $Domain -IncludeDomains $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Category = '' 13 | Severity = '' 14 | Importance = 0 15 | Description = '' 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | EnabledAgingEnabledAndIdentical = @{ 25 | Enable = $true 26 | Name = 'Zone DNS aging should be identical on all DCs' 27 | Parameters = @{ 28 | WhereObject = { $_.AgingEnabled -eq $false } 29 | ExpectedCount = 0 30 | } 31 | Description = 'Primary DNS zone should have aging enabled, on all DNS servers.' 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/DNSZonesDomain0ADEL.ps1: -------------------------------------------------------------------------------- 1 | $DNSZonesDomain0ADEL = @{ 2 | Name = 'DomainDNSZonesDomain0ADEL' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "DomainDNSZones should have proper FSMO Owner (0ADEL)" 7 | Data = { 8 | #$DomainController = 'ad.evotec.pl' 9 | #$DomainInformation = Get-ADDomain -Server $DomainController 10 | $IdentityDomain = "CN=Infrastructure,DC=DomainDnsZones,$(($DomainInformation).DistinguishedName)" 11 | $FSMORoleOwner = (Get-ADObject -Identity $IdentityDomain -Properties fSMORoleOwner -Server $Domain) 12 | $FSMORoleOwner 13 | } 14 | Details = [ordered] @{ 15 | Area = 'DNS' 16 | Category = 'Configuration' 17 | Severity = '' 18 | Importance = 0 19 | Description = "" 20 | Resolution = '' 21 | Resources = @( 22 | '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/' 23 | 'https://support.microsoft.com/en-us/help/949257/error-message-when-you-run-the-adprep-rodcprep-command-in-windows-serv' 24 | 'https://social.technet.microsoft.com/Forums/en-US/8b4a7794-13b2-4ef0-90c8-16799e9fd529/orphaned-fsmoroleowner-entry-for-domaindnszones?forum=winserverDS' 25 | ) 26 | } 27 | ExpectedOutput = $true 28 | } 29 | Tests = [ordered] @{ 30 | DNSZonesDomain0ADEL = @{ 31 | Enable = $true 32 | Name = 'DomainDNSZones should have proper FSMO Owner (0ADEL)' 33 | Parameters = @{ 34 | ExpectedValue = '0ADEL:' 35 | Property = 'fSMORoleOwner' 36 | OperationType = 'notmatch' 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/DNSZonesForest0ADEL.ps1: -------------------------------------------------------------------------------- 1 | $DNSZonesForest0ADEL = @{ 2 | Name = 'DomainDNSZonesForest0ADEL' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "ForestDNSZones should have proper FSMO Owner (0ADEL)" 7 | Data = { 8 | #$DomainController = 'ad.evotec.xyz' 9 | #$DomainInformation = Get-ADDomain -Server $DomainController 10 | $IdentityForest = "CN=Infrastructure,DC=ForestDnsZones,$(($DomainInformation).DistinguishedName)" 11 | $FSMORoleOwner = (Get-ADObject -Identity $IdentityForest -Properties fSMORoleOwner -Server $Domain) 12 | $FSMORoleOwner 13 | } 14 | Requirements = @{ 15 | IsDomainRoot = $true 16 | } 17 | Details = [ordered] @{ 18 | Area = 'DNS' 19 | Category = 'Configuration' 20 | Severity = '' 21 | Importance = 0 22 | Description = "" 23 | Resolution = '' 24 | Resources = @( 25 | '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/' 26 | 'https://support.microsoft.com/en-us/help/949257/error-message-when-you-run-the-adprep-rodcprep-command-in-windows-serv' 27 | 'https://social.technet.microsoft.com/Forums/en-US/8b4a7794-13b2-4ef0-90c8-16799e9fd529/orphaned-fsmoroleowner-entry-for-domaindnszones?forum=winserverDS' 28 | ) 29 | 30 | } 31 | ExpectedOutput = $true 32 | } 33 | Tests = [ordered] @{ 34 | DNSZonesForest0ADEL = @{ 35 | Enable = $true 36 | Name = 'ForestDNSZones should have proper FSMO Owner (0ADEL)' 37 | Parameters = @{ 38 | ExpectedValue = '0ADEL:' 39 | Property = 'fSMORoleOwner' 40 | OperationType = 'notmatch' 41 | } 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /Private/SourcesDomain/DomainFSMORoles.ps1: -------------------------------------------------------------------------------- 1 | $DomainFSMORoles = @{ 2 | Name = 'DomainRoles' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = 'Domain Roles Availability' 7 | Data = { 8 | Test-ADRolesAvailability -Domain $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Category = '' 13 | Severity = '' 14 | Importance = 0 15 | Description = '' 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | PDCEmulator = @{ 25 | Enable = $true 26 | Name = 'PDC Emulator Availability' 27 | Parameters = @{ 28 | ExpectedValue = $true 29 | Property = 'PDCEmulatorAvailability' 30 | OperationType = 'eq' 31 | PropertyExtendedValue = 'PDCEmulator' 32 | } 33 | } 34 | RIDMaster = @{ 35 | Enable = $true 36 | Name = 'RID Master Availability' 37 | Parameters = @{ 38 | ExpectedValue = $true 39 | Property = 'RIDMasterAvailability' 40 | OperationType = 'eq' 41 | PropertyExtendedValue = 'RIDMaster' 42 | } 43 | } 44 | InfrastructureMaster = @{ 45 | Enable = $true 46 | Name = 'Infrastructure Master Availability' 47 | Parameters = @{ 48 | ExpectedValue = $true 49 | Property = 'InfrastructureMasterAvailability' 50 | OperationType = 'eq' 51 | PropertyExtendedValue = 'InfrastructureMaster' 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/ExchangeUsers.ps1: -------------------------------------------------------------------------------- 1 | $ExchangeUsers = @{ 2 | Name = 'DomainExchangeUsers' 3 | Enable = $false 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Exchange Users: Missing MailNickName" 7 | Data = { 8 | Get-ADUser -Filter { Mail -like '*' -and MailNickName -notlike '*' } -Properties mailNickName, mail -Server $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Category = '' 13 | Severity = '' 14 | Importance = 0 15 | Description = '' 16 | Resolution = '' 17 | Resources = @( 18 | 'https://evotec.xyz/office-365-msexchhidefromaddresslists-does-not-synchronize-with-office-365/' 19 | ) 20 | 21 | } 22 | ExpectedOutput = $false 23 | } 24 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicy.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicyAssessment = @{ 2 | Name = 'DomainGroupPolicyAssessment' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Group Policy Assessment" 7 | Data = { 8 | Get-GPOZaurr -Forest $ForestName -IncludeDomains $Domain 9 | } 10 | Implementation = { 11 | 12 | } 13 | Details = [ordered] @{ 14 | Area = 'GroupPolicy' 15 | Category = 'Cleanup' 16 | Severity = '' 17 | Importance = 0 18 | Description = "" 19 | Resolution = '' 20 | Resources = @( 21 | 22 | ) 23 | } 24 | ExpectedOutput = $true 25 | } 26 | Tests = [ordered] @{ 27 | Empty = @{ 28 | Enable = $true 29 | Name = 'Group Policy Empty' 30 | Parameters = @{ 31 | #Bundle = $true 32 | WhereObject = { $_.Empty -eq $true } 33 | ExpectedCount = 0 34 | } 35 | } 36 | Linked = @{ 37 | Enable = $true 38 | Name = 'Group Policy Unlinked' 39 | Parameters = @{ 40 | #Bundle = $true 41 | WhereObject = { $_.Linked -eq $false } 42 | ExpectedCount = 0 43 | } 44 | } 45 | Enabled = @{ 46 | Enable = $true 47 | Name = 'Group Policy Disabled' 48 | Parameters = @{ 49 | #Bundle = $true 50 | WhereObject = { $_.Enabled -eq $false } 51 | ExpectedCount = 0 52 | } 53 | } 54 | Problem = @{ 55 | Enable = $true 56 | Name = 'Group Policy with Problem' 57 | Parameters = @{ 58 | #Bundle = $true 59 | WhereObject = { $_.Problem -eq $true } 60 | ExpectedCount = 0 61 | } 62 | } 63 | Optimized = @{ 64 | Enable = $true 65 | Name = 'Group Policy Not Optimized' 66 | Parameters = @{ 67 | #Bundle = $true 68 | WhereObject = { $_.Optimized -eq $false } 69 | ExpectedCount = 0 70 | } 71 | } 72 | ApplyPermission = @{ 73 | Enable = $true 74 | Name = 'Group Policy No Apply Permission' 75 | Parameters = @{ 76 | # Bundle = $true 77 | WhereObject = { $_.ApplyPermissioon -eq $false } 78 | ExpectedCount = 0 79 | } 80 | } 81 | } 82 | DataHighlights = { 83 | New-HTMLTableCondition -Name 'Empty' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon 84 | New-HTMLTableCondition -Name 'Linked' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon 85 | New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon 86 | New-HTMLTableCondition -Name 'Optimized' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon 87 | New-HTMLTableCondition -Name 'Problem' -ComparisonType string -BackgroundColor PaleGreen -Value $false -Operator eq -FailBackgroundColor Salmon 88 | New-HTMLTableCondition -Name 'ApplyPermission' -ComparisonType string -BackgroundColor PaleGreen -Value $true -Operator eq -FailBackgroundColor Salmon 89 | } 90 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicyADM.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicyADM = @{ 2 | Name = 'DomainGroupPolicyADM' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = 'Group Policy Legacy ADM Files' 7 | Data = { 8 | Get-GPOZaurrLegacyFiles -Forest $ForestName -IncludeDomains $Domain 9 | } 10 | Implementation = { 11 | Remove-GPOZaurrLegacyFiles -Verbose -WhatIf 12 | } 13 | Details = [ordered] @{ 14 | Area = 'GroupPolicy' 15 | Category = 'Cleanup' 16 | Severity = '' 17 | Importance = 0 18 | Description = '' 19 | Resolution = '' 20 | Resources = @( 21 | 'https://support.microsoft.com/en-us/help/816662/recommendations-for-managing-group-policy-administrative-template-adm' 22 | 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-vista/cc709647(v=ws.10)?redirectedfrom=MSDN' 23 | 'https://sdmsoftware.com/group-policy-blog/tips-tricks/understanding-the-role-of-admx-and-adm-files-in-group-policy/' 24 | 'https://social.technet.microsoft.com/Forums/en-US/bbbe04f5-218b-4526-ae67-cf82a20d49fc/deleting-adm-templates?forum=winserverGP' 25 | 'https://gallery.technet.microsoft.com/scriptcenter/Removing-ADM-files-from-b532e3b6#content' 26 | ) 27 | } 28 | ExpectedOutput = $false 29 | } 30 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicyOwners.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicyOwner = @{ 2 | Name = 'DomainGroupPolicyOwner' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "GPO: Owner" 7 | Data = { 8 | Get-GPOZaurrOwner -Forest $ForestName -IncludeSysvol -IncludeDomains $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'GroupPolicy' 12 | Category = 'Security' 13 | Severity = '' 14 | Importance = 0 15 | Description = "" 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | OwnerConsistent = @{ 25 | Enable = $true 26 | Name = 'GPO: Owner Consistent' 27 | Parameters = @{ 28 | WhereObject = { $_.IsOwnerConsistent -ne $true } 29 | ExpectedResult = $false # this tests things in bundle rather then per object of array 30 | } 31 | 32 | } 33 | OwnerAdministrative = @{ 34 | Enable = $true 35 | Name = 'GPO: Owner Administrative' 36 | Parameters = @{ 37 | WhereObject = { $_.OwnerType -ne 'Administrative' -or $_.SysvolType -ne 'Administrative' } 38 | ExpectedResult = $false # this tests things in bundle rather then per object of array 39 | } 40 | } 41 | } 42 | } 43 | <# 44 | ExpectedCount = 0,1,2,3 and so on 45 | ExpectedValue = [object] 46 | ExpectedResult = $true # just checks if there is result or there is not 47 | #> -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicyPermission.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicyPermissions = @{ 2 | Name = 'DomainGroupPolicyPermissions' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Group Policy Required Permissions" 7 | Data = { 8 | Get-GPOZaurrPermissionAnalysis -Forest $ForestName -Domain $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'GroupPolicy' 12 | Category = 'Security' 13 | Severity = '' 14 | Importance = 0 15 | Description = "Group Policy permissions should always have Authenticated Users and Domain Computers gropup" 16 | Resolution = 'Do not remove Authenticated Users, Domain Computers from Group Policies.' 17 | Resources = @( 18 | 'https://secureinfra.blog/2018/12/31/most-common-mistakes-in-active-directory-and-domain-services-part-1/' 19 | 'https://support.microsoft.com/en-us/help/3163622/ms16-072-security-update-for-group-policy-june-14-2016' 20 | ) 21 | } 22 | ExpectedOutput = $true 23 | } 24 | Tests = [ordered] @{ 25 | Administrative = @{ 26 | Enable = $true 27 | Name = 'GPO: Administrative Permissions' 28 | Parameters = @{ 29 | ExpectedCount = 0 30 | OperationType = 'eq' 31 | WhereObject = { $_.Administrative -eq $false } 32 | } 33 | 34 | } 35 | AuthenticatedUsers = @{ 36 | Enable = $true 37 | Name = 'GPO: Authenticated Permissions' 38 | Parameters = @{ 39 | ExpectedCount = 0 40 | OperationType = 'eq' 41 | WhereObject = { $_.AuthenticatedUsers -eq $false } 42 | } 43 | } 44 | System = @{ 45 | Enable = $true 46 | Name = 'GPO: System Permissions' 47 | Parameters = @{ 48 | ExpectedCount = 0 49 | OperationType = 'eq' 50 | WhereObject = { $_.System -eq $false } 51 | } 52 | } 53 | Unknown = @{ 54 | Enable = $true 55 | Name = 'GPO: Unknown Permissions' 56 | Parameters = @{ 57 | ExpectedCount = 0 58 | OperationType = 'eq' 59 | WhereObject = { $_.Unknown -eq $true } 60 | } 61 | } 62 | } 63 | <# Another way to do the same thing as above 64 | Tests = [ordered] @{ 65 | Administrative = @{ 66 | Enable = $true 67 | Name = 'GPO: Administrative Permissions' 68 | Parameters = @{ 69 | Bundle = $true 70 | Property = 'Administrative' 71 | ExpectedValue = $false 72 | OperationType = 'notcontains' 73 | DisplayResult = $false 74 | } 75 | 76 | } 77 | AuthenticatedUsers = @{ 78 | Enable = $true 79 | Name = 'GPO: Authenticated Permissions' 80 | Parameters = @{ 81 | Bundle = $true 82 | Property = 'AuthenticatedUsers' 83 | ExpectedValue = $false 84 | OperationType = 'notcontains' 85 | DisplayResult = $false 86 | } 87 | } 88 | System = @{ 89 | Enable = $true 90 | Name = 'GPO: System Permissions' 91 | Parameters = @{ 92 | Bundle = $true 93 | Property = 'System' 94 | ExpectedValue = $false 95 | OperationType = 'notcontains' 96 | DisplayResult = $false 97 | } 98 | } 99 | Unknown = @{ 100 | Enable = $true 101 | Name = 'GPO: Unknown Permissions' 102 | Parameters = @{ 103 | Bundle = $true 104 | Property = 'Unknown' 105 | ExpectedValue = $false 106 | OperationType = 'notcontains' 107 | DisplayResult = $false 108 | } 109 | } 110 | } 111 | #> 112 | } 113 | -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicyPermissionConsistency.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicyPermissionConsistency = @{ 2 | Name = 'DomainGroupPolicyPermissionConsistency' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "GPO: Permission Consistency" 7 | Data = { 8 | Get-GPOZaurrPermissionConsistency -Forest $ForestName -VerifyInheritance -Type Inconsistent -IncludeDomains $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'GroupPolicy' 12 | Category = 'Security' 13 | Severity = '' 14 | Importance = 0 15 | Description = "GPO Permissions are stored in Active Directory and SYSVOL at the same time. Setting up permissions for GPO should replicate itself to SYSVOL and those permissions should be consistent. However, sometimes this doesn't happen or is done on purpose." 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $false 22 | } 23 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/GroupPolicySysvol.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicySysvol = @{ 2 | Name = 'DomainGroupPolicySysvol' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "GPO: Sysvol folder existance" 7 | Data = { 8 | Get-GPOZaurrSysvol -Forest $ForestName -IncludeDomains $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'GroupPolicy' 12 | Category = 'Security' 13 | Severity = '' 14 | Importance = 0 15 | Description = "GPO Permissions are stored in Active Directory and SYSVOL at the same time. Sometimes when deleting GPO or due to replication issues GPO becomes orphaned (no SYSVOL files) or SYSVOL files exists but no GPO." 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | SysvolExists = @{ 25 | Enable = $true 26 | Name = 'GPO: Files on SYSVOL are not Orphaned' 27 | Parameters = @{ 28 | WhereObject = { $_.SysvolStatus -ne 'Exists' -or $_.Status -ne 'Exists' } 29 | ExpectedResult = $false # this tests things in bundle rather then per object of array 30 | } 31 | 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/NetLogonOwner.ps1: -------------------------------------------------------------------------------- 1 | $NetLogonOwner = @{ 2 | Name = 'DomainNetLogonOwner' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "NetLogon Owner" 7 | Data = { 8 | Get-GPOZaurrNetLogon -Forest $ForestName -OwnerOnly -IncludeDomains $Domain 9 | } 10 | Implementation = { 11 | 12 | } 13 | Details = [ordered] @{ 14 | Area = 'FileSystem' 15 | Category = 'Cleanup' 16 | Severity = '' 17 | Importance = 6 18 | Description = "" 19 | Resolution = '' 20 | Resources = @( 21 | 22 | ) 23 | Tags = 'netlogon', 'grouppolicy', 'gpo', 'sysvol' 24 | } 25 | ExpectedOutput = $null 26 | } 27 | Tests = [ordered] @{ 28 | Empty = @{ 29 | Enable = $true 30 | Name = 'Owner should be BUILTIN\Administrators' 31 | Parameters = @{ 32 | #Bundle = $true 33 | WhereObject = { $_.OwnerSid -ne 'S-1-5-32-544' } 34 | ExpectedCount = 0 35 | ExpectedOutput = $true 36 | } 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/OrganizationalUnitsEmpty.ps1: -------------------------------------------------------------------------------- 1 | $OrganizationalUnitsEmpty = @{ 2 | Name = 'DomainOrganizationalUnitsEmpty' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Organizational Units: Orphaned/Empty" 7 | Data = { 8 | <# We should replace it with better alternative 9 | ([adsisearcher]'(objectcategory=organizationalunit)').FindAll() | Where-Object { 10 | -not (-join $_.GetDirectoryEntry().psbase.children) } 11 | #> 12 | $OrganizationalUnits = Get-ADOrganizationalUnit -Filter * -Properties distinguishedname -Server $Domain | Select-Object -ExpandProperty distinguishedname 13 | $WellKnownContainers = Get-ADDomain | Select-Object *Container 14 | 15 | $AllUsedOU = Get-ADObject -Filter "ObjectClass -eq 'user' -or ObjectClass -eq 'computer' -or ObjectClass -eq 'group' -or ObjectClass -eq 'contact'" -Server $Domain | ` 16 | Where-Object { ($_.DistinguishedName -notlike '*LostAndFound*') -and ($_.DistinguishedName -match 'OU=(.*)') } | ` 17 | ForEach-Object { $matches[0] } | ` 18 | Select-Object -Unique 19 | 20 | $OrganizationalUnits | Where-Object { ($AllUsedOU -notcontains $_) -and -not (Get-ADOrganizationalUnit -Filter * -SearchBase $_ -SearchScope 1 -Server $Domain) -and (($_ -notlike $WellKnownContainers.UsersContainer) -or ($_ -notlike $WellKnownContainers.ComputersContainer)) } 21 | } 22 | Details = [ordered] @{ 23 | Category = 'Configuration' 24 | Importance = 3 25 | ActionType = 1 26 | Description = '' 27 | Resolution = '' 28 | Resources = @( 29 | "[Active Directory Friday: Find empty Organizational Unit](https://www.jaapbrasser.com/active-directory-friday-find-empty-organizational-unit/)" 30 | ) 31 | StatusTrue = 1 32 | StatusFalse = 2 33 | } 34 | ExpectedOutput = $false 35 | } 36 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/OrganizationalUnitsProtected.ps1: -------------------------------------------------------------------------------- 1 | $OrganizationalUnitsProtected = @{ 2 | Name = 'DomainOrganizationalUnitsProtected' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Organizational Units: Protected" 7 | Data = { 8 | $OUs = Get-ADOrganizationalUnit -Properties ProtectedFromAccidentalDeletion, CanonicalName -Filter * -Server $Domain 9 | $FilteredOus = foreach ($OU in $OUs) { 10 | if ($OU.ProtectedFromAccidentalDeletion -eq $false) { 11 | $OU 12 | } 13 | } 14 | $FilteredOus | Select-Object -Property Name, CanonicalName, DistinguishedName, ProtectedFromAccidentalDeletion 15 | } 16 | Details = [ordered] @{ 17 | Area = 'Objects' 18 | Category = 'Cleanup' 19 | Severity = '' 20 | Importance = 0 21 | Description = '' 22 | Resolution = '' 23 | Resources = @( 24 | 25 | ) 26 | } 27 | ExpectedOutput = $false 28 | } 29 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/PasswordComplexity.ps1: -------------------------------------------------------------------------------- 1 | $PasswordComplexity = @{ 2 | Name = 'DomainPasswordComplexity' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = 'Password Complexity Requirements' 7 | Data = { 8 | Get-ADDefaultDomainPasswordPolicy -Server $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Objects' 12 | Category = 'Security' 13 | Severity = '' 14 | Importance = 0 15 | Description = '' 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | ComplexityEnabled = @{ 25 | Enable = $true 26 | Name = 'Complexity Enabled' 27 | Details = [ordered] @{ 28 | Area = '' 29 | Category = '' 30 | Severity = '' 31 | Importance = 0 32 | Description = '' 33 | Resolution = '' 34 | Resources = @( 35 | 36 | ) 37 | } 38 | Parameters = @{ 39 | Property = 'ComplexityEnabled' 40 | ExpectedValue = $true 41 | OperationType = 'eq' 42 | } 43 | } 44 | 'LockoutDuration' = @{ 45 | Enable = $true 46 | Name = 'Lockout Duration' 47 | Parameters = @{ 48 | Property = 'LockoutDuration' 49 | ExpectedValue = 30 50 | OperationType = 'ge' 51 | } 52 | } 53 | 'LockoutObservationWindow' = @{ 54 | Enable = $true 55 | Name = 'Lockout Observation Window' 56 | Parameters = @{ 57 | #PropertyExtendedValue = 'LockoutObservationWindow' 58 | Property = 'LockoutObservationWindow', 'TotalMinutes' 59 | ExpectedValue = 30 60 | OperationType = 'ge' 61 | } 62 | } 63 | 'LockoutThreshold' = @{ 64 | Enable = $true 65 | Name = 'Lockout Threshold' 66 | Parameters = @{ 67 | Property = 'LockoutThreshold' 68 | ExpectedValue = 4 69 | OperationType = 'gt' 70 | } 71 | } 72 | 'MaxPasswordAge' = @{ 73 | Enable = $true 74 | Name = 'Maximum Password Age' 75 | Parameters = @{ 76 | Property = 'MaxPasswordAge', 'TotalDays' 77 | ExpectedValue = 60 78 | OperationType = 'le' 79 | } 80 | } 81 | 'MinPasswordLength' = @{ 82 | Enable = $true 83 | Name = 'Minimum Password Length' 84 | Parameters = @{ 85 | Property = 'MinPasswordLength' 86 | ExpectedValue = 8 87 | OperationType = 'gt' 88 | } 89 | } 90 | 'MinPasswordAge' = @{ 91 | Enable = $true 92 | Name = 'Minimum Password Age' 93 | Parameters = @{ 94 | #PropertyExtendedValue = 'MinPasswordAge', 'TotalDays' 95 | Property = 'MinPasswordAge', 'TotalDays' 96 | ExpectedValue = 1 97 | OperationType = 'le' 98 | } 99 | } 100 | 'PasswordHistoryCount' = @{ 101 | Enable = $true 102 | Name = 'Password History Count' 103 | Parameters = @{ 104 | Property = 'PasswordHistoryCount' 105 | ExpectedValue = 10 106 | OperationType = 'ge' 107 | } 108 | } 109 | 'ReversibleEncryptionEnabled' = @{ 110 | Enable = $true 111 | Name = 'Reversible Encryption Enabled' 112 | Parameters = @{ 113 | Property = 'ReversibleEncryptionEnabled' 114 | ExpectedValue = $false 115 | OperationType = 'eq' 116 | } 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/SecurityGroupsAccountOperators.ps1: -------------------------------------------------------------------------------- 1 | $SecurityGroupsAccountOperators = @{ 2 | Name = 'DomainSecurityGroupsAccountOperators' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Groups: Account operators should be empty" 7 | Data = { 8 | Get-ADGroupMember -Identity 'S-1-5-32-548' -Recursive -Server $Domain 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Objects' 12 | Category = 'Cleanup', 'Security' 13 | Severity = '' 14 | Importance = 0 15 | 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!" 16 | Resolution = '' 17 | Resources = @() 18 | } 19 | ExpectedOutput = $false 20 | } 21 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/SecurityGroupsSchemaAdmins.ps1: -------------------------------------------------------------------------------- 1 | $SecurityGroupsSchemaAdmins = @{ 2 | Name = 'DomainSecurityGroupsSchemaAdmins' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Groups: Schema Admins should be empty" 7 | Data = { 8 | $DomainSID = (Get-ADDomain -Server $Domain).DomainSID 9 | Get-ADGroupMember -Recursive -Server $Domain -Identity "$DomainSID-518" 10 | } 11 | Requirements = @{ 12 | IsDomainRoot = $true 13 | } 14 | Details = [ordered] @{ 15 | Area = 'Objects' 16 | Category = 'Cleanup', 'Security' 17 | Severity = '' 18 | Importance = 0 19 | Description = "Schema Admins group should be empty. If you need to manage schema you can always add user for the time of modification." 20 | Resolution = 'Keep Schema group empty.' 21 | Resources = @( 22 | 'https://www.stigviewer.com/stig/active_directory_forest/2016-12-19/finding/V-72835' 23 | ) 24 | 25 | } 26 | ExpectedOutput = $false 27 | } 28 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/SecurityUsersAccountAdministrator.ps1: -------------------------------------------------------------------------------- 1 | $SecurityUsersAcccountAdministrator = @{ 2 | Name = 'DomainSecurityUsersAcccountAdministrator' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "Users: Administrator (SID-500)" 7 | Data = { 8 | # this test is kind of special 9 | # basically when account is disabled it doesn't make sense to check for PasswordLastSet 10 | # therefore i'm adding setting PasswordLastSet to current date to be able to test just that field 11 | # At least until support for multiple checks is added 12 | 13 | $DomainSID = (Get-ADDomain -Server $Domain).DomainSID 14 | $User = Get-ADUser -Identity "$DomainSID-500" -Properties PasswordLastSet, LastLogonDate, servicePrincipalName -Server $Domain 15 | if ($User.Enabled -eq $false) { 16 | [PSCustomObject] @{ 17 | Name = $User.SamAccountName 18 | Enabled = $User.Enabled 19 | PasswordLastSet = Get-Date 20 | ServicePrincipalName = $User.ServicePrincipalName 21 | LastLogonDate = $User.LastLogonDate 22 | DistinguishedName = $User.DistinguishedName 23 | SID = $User.SID 24 | } 25 | } else { 26 | [PSCustomObject] @{ 27 | Name = $User.SamAccountName 28 | Enabled = $User.Enabled 29 | PasswordLastSet = $User.PasswordLastSet 30 | ServicePrincipalName = $User.ServicePrincipalName 31 | LastLogonDate = $User.LastLogonDate 32 | DistinguishedName = $User.DistinguishedName 33 | SID = $User.SID 34 | } 35 | } 36 | } 37 | Details = [ordered] @{ 38 | Category = 'Security' 39 | Importance = 0 40 | ActionType = 0 41 | Description = "Administrator (SID-500) account is critical account in Active Directory. Due to it's role it shouldn't be used as a daily driver, and only as emeregency account." 42 | Resources = @( 43 | 44 | ) 45 | StatusTrue = 0 46 | StatusFalse = 0 47 | } 48 | ExpectedOutput = $true 49 | } 50 | Tests = [ordered] @{ 51 | LastLogonDate = @{ 52 | Enable = $true 53 | Name = 'Last Logon Date should not be recent' 54 | Parameters = @{ 55 | Property = 'LastLogonDate' 56 | ExpectedValue = (Get-Date).AddDays(-60) 57 | OperationType = 'lt' 58 | } 59 | Details = [ordered] @{ 60 | Category = 'Security' 61 | Importance = 9 62 | ActionType = 2 63 | StatusTrue = 1 64 | StatusFalse = 5 65 | } 66 | Description = "" 67 | } 68 | ServicePrincipalName = @{ 69 | Enable = $true 70 | Name = 'Service Principal Name should be empty' 71 | Parameters = @{ 72 | Property = 'servicePrincipalName' 73 | ExpectedValue = $null 74 | OperationType = 'eq' 75 | } 76 | Details = [ordered] @{ 77 | Category = 'Security' 78 | Importance = 10 79 | ActionType = 2 80 | StatusTrue = 1 81 | StatusFalse = 5 82 | } 83 | Description = "" 84 | } 85 | PasswordLastSet = @{ 86 | Enable = $true 87 | Name = 'Administrator Last Password Change Should be less than 360 days ago' 88 | Parameters = @{ 89 | Property = 'PasswordLastSet' 90 | ExpectedValue = '(Get-Date).AddDays(-360)' 91 | OperationType = 'gt' 92 | } 93 | Description = 'Administrator account should be disabled or LastPasswordChange should be less than 1 year ago.' 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /Private/SourcesDomain/SysVOLDFSR.ps1: -------------------------------------------------------------------------------- 1 | $SysVolDFSR = @{ 2 | Name = 'DomainSysVolDFSR' 3 | Enable = $true 4 | Scope = 'Domain' 5 | Source = @{ 6 | Name = "DFSR Flags" 7 | Data = { 8 | $DistinguishedName = (Get-ADDomain -Server $Domain).DistinguishedName 9 | $ADObject = "CN=DFSR-GlobalSettings,CN=System,$DistinguishedName" 10 | $Object = Get-ADObject -Identity $ADObject -Properties * -Server $Domain 11 | if ($Object.'msDFSR-Flags' -gt 47) { 12 | [PSCustomObject] @{ 13 | 'SysvolMode' = 'DFS-R' 14 | 'Flags' = $Object.'msDFSR-Flags' 15 | } 16 | } else { 17 | [PSCustomObject] @{ 18 | 'SysvolMode' = 'Not DFS-R' 19 | 'Flags' = $Object.'msDFSR-Flags' 20 | } 21 | } 22 | } 23 | Details = [ordered] @{ 24 | Category = 'Health' 25 | Area = 'SYSVOL' 26 | Severity = '' 27 | Importance = 0 28 | Description = 'Checks if DFS-R is available.' 29 | Resolution = '' 30 | Resources = @( 31 | 'https://blogs.technet.microsoft.com/askds/2009/01/05/dfsr-sysvol-migration-faq-useful-trivia-that-may-save-your-follicles/' 32 | '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/' 33 | ) 34 | } 35 | ExpectedOutput = $true 36 | } 37 | Tests = [ordered] @{ 38 | DFSRSysvolState = @{ 39 | Enable = $true 40 | Name = 'DFSR Sysvol State' 41 | Parameters = @{ 42 | Property = 'SysvolMode' 43 | ExpectedValue = 'DFS-R' 44 | OperationType = 'eq' 45 | PropertyExtendedValue = 'Flags' 46 | } 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/DCDNSForwaders.ps1: -------------------------------------------------------------------------------- 1 | $DCDNSForwaders = @{ 2 | Name = 'DCDNSForwaders' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "DC DNS Forwarders" 7 | Data = { 8 | $Forwarders = Get-WinADDnsServerForwarder -Forest $ForestName -Domain $Domain -IncludeDomainControllers $DomainController -WarningAction SilentlyContinue -Formatted 9 | $Forwarders 10 | } 11 | Details = [ordered] @{ 12 | Category = 'Configuration' 13 | Area = 'DNS' 14 | Importance = 5 15 | Description = '' 16 | Resolution = '' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | SameForwarders = @{ 25 | Enable = $true 26 | Name = 'Multiple DNS Forwarders' 27 | Parameters = @{ 28 | Property = 'ForwardersCount' 29 | ExpectedValue = 1 30 | OperationType = 'gt' 31 | PropertyExtendedValue = 'IPAddress' 32 | } 33 | Description = 'DNS: More than one forwarding server should be configured' 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/DNSResolveExternal.ps1: -------------------------------------------------------------------------------- 1 | $DNSResolveExternal = @{ 2 | Name = 'DCDnsResolveExternal' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Resolves external DNS queries" 7 | Data = { 8 | $Output = Invoke-Command -ComputerName $DomainController -ErrorAction Stop { 9 | Resolve-DnsName -Name 'testimo-check.evotec.xyz' -ErrorAction SilentlyContinue | Where-Object { $_.Section -eq 'Answer' -and $_.Type -eq 'A' } 10 | } 11 | $Output 12 | } 13 | Details = [ordered] @{ 14 | Area = 'DNS' 15 | Category = 'Health' 16 | Severity = 'High' 17 | Description = '' 18 | Resolution = '' 19 | Importance = 10 20 | Resources = @( 21 | 22 | ) 23 | } 24 | ExpectedOutput = $true 25 | } 26 | Tests = [ordered] @{ 27 | ResolveDNSExternal = @{ 28 | Enable = $true 29 | Name = 'Should resolve External DNS' 30 | Parameters = @{ 31 | Property = 'IPAddress' 32 | ExpectedValue = '1.1.1.1' 33 | OperationType = 'eq' 34 | } 35 | Description = 'DNS should resolve external queries properly.' 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/DNSResolveInternal.ps1: -------------------------------------------------------------------------------- 1 | $DNSResolveInternal = @{ 2 | Name = 'DCDnsResolveInternal' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Resolves internal DNS queries" 7 | Data = { 8 | $Output = Invoke-Command -ComputerName $DomainController -ErrorAction Stop { 9 | param( 10 | [string] $DomainController 11 | ) 12 | $AllDomainControllers = Get-ADDomainController -Identity $DomainController -Server $DomainController 13 | $IPs = $AllDomainControllers.IPv4Address | Sort-Object 14 | $Output = Resolve-DnsName -Name $DomainController -ErrorAction SilentlyContinue 15 | @{ 16 | 'Result' = 'IP Comparison' 17 | 'Status' = if ($null -eq (Compare-Object -ReferenceObject $IPs -DifferenceObject ($Output.IP4Address | Sort-Object))) { $true } else { $false } 18 | 'IPAddresses' = $Output.IP4Address 19 | } 20 | } -ArgumentList $DomainController 21 | $Output 22 | } 23 | Details = [ordered] @{ 24 | Category = 'Health' 25 | Area = 'DNS' 26 | Description = '' 27 | Resolution = '' 28 | Importance = 10 29 | Resources = @( 30 | 31 | ) 32 | } 33 | ExpectedOutput = $true 34 | } 35 | Tests = [ordered] @{ 36 | ResolveDNSInternal = @{ 37 | Enable = $true 38 | Name = 'Should resolve Internal DNS' 39 | Parameters = @{ 40 | Property = 'Status' 41 | ExpectedValue = $true 42 | OperationType = 'eq' 43 | PropertyExtendedValue = 'IPAddresses' 44 | } 45 | Description = 'DNS should resolve internal domains correctly.' 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/DiskSpace.ps1: -------------------------------------------------------------------------------- 1 | $DiskSpace = @{ 2 | Name = 'DCDiskSpace' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'Disk Free' 7 | Data = { 8 | Get-ComputerDiskLogical -ComputerName $DomainController -OnlyLocalDisk -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Area = 'WindowsConfiguration' 12 | Category = 'Health' 13 | Description = '' 14 | Resolution = '' 15 | Importance = 10 16 | Resources = @( 17 | 18 | ) 19 | } 20 | ExpectedOutput = $true 21 | } 22 | Tests = [ordered] @{ 23 | FreeSpace = @{ 24 | Enable = $true 25 | Name = "Free Space in GB" 26 | Parameters = @{ 27 | Property = 'FreeSpace' 28 | PropertyExtendedValue = 'FreeSpace' 29 | ExpectedValue = 10 30 | OperationType = 'gt' 31 | OverwriteName = { "Free Space in GB / $($_.DeviceID)" } 32 | } 33 | } 34 | FreePercent = @{ 35 | Enable = $true 36 | Name = 'Free Space Percent' 37 | Parameters = @{ 38 | Property = 'FreePercent' 39 | PropertyExtendedValue = 'FreePercent' 40 | ExpectedValue = 10 41 | OperationType = 'gt' 42 | OverwriteName = { "Free Space in Percent / $($_.DeviceID)" } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/DnsNameServers.ps1: -------------------------------------------------------------------------------- 1 | $DNSNameServers = @{ 2 | Name = 'DCDnsNameServes' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Name servers for primary domain zone" 7 | Data = { 8 | Test-DNSNameServers -Domain $Domain -DomainController $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Area = 'DNS' 13 | Severity = 'Medium' 14 | Description = '' 15 | Resolution = '' 16 | Importance = 10 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | DnsNameServersIdentical = @{ 25 | Enable = $true 26 | Name = 'DNS Name servers for primary zone are identical' 27 | Parameters = @{ 28 | Property = 'Status' 29 | ExpectedValue = $True 30 | OperationType = 'eq' 31 | PropertyExtendedValue = 'Comment' 32 | } 33 | Description = 'DNS Name servers for primary zone should be equal to Domain Controllers for a Domain.' 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/FileSystem.ps1: -------------------------------------------------------------------------------- 1 | $FileSystem = @{ 2 | Name = 'DCFileSystem' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "FileSystem" 7 | Data = { 8 | Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Control\FileSystem' -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Security' 12 | Area = 'FileSystem' 13 | Description = '' 14 | Resolution = '' 15 | Importance = 10 16 | Resources = @( 17 | '' 18 | ) 19 | } 20 | Requirements = @{ 21 | CommandAvailable = 'Get-WinADLMSettings' 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | NtfsDisable8dot3NameCreation = @{ 27 | Enable = $true 28 | Name = 'NtfsDisable8dot3NameCreation' 29 | Parameters = @{ 30 | Property = 'NtfsDisable8dot3NameCreation' 31 | ExpectedValue = 0 32 | OperationType = 'gt' 33 | } 34 | Details = [ordered] @{ 35 | Area = '' 36 | Description = '' 37 | Resolution = '' 38 | Importance = 10 39 | Resources = @( 40 | 'https://guyrleech.wordpress.com/2014/04/15/ntfs-8-3-short-names-solving-the-issues/' 41 | 'https://blogs.technet.microsoft.com/josebda/2012/11/13/windows-server-2012-file-server-tip-disable-8-3-naming-and-strip-those-short-names-too/' 42 | 'https://support.microsoft.com/en-us/help/121007/how-to-disable-8-3-file-name-creation-on-ntfs-partitions' 43 | ) 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/GroupPolicySYSVOLDC.ps1: -------------------------------------------------------------------------------- 1 | $GroupPolicySYSVOLDC = @{ 2 | Name = 'DCGroupPolicySYSVOLDC' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Group Policy SYSVOL Verification" 7 | Data = { 8 | Get-GPOZaurrSysvol -IncludeDomains $Domain -IncludeDomainControllers $DomainController -VerifyDomainControllers | Where-Object { $_.Status -ne 'Exists' } 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $false 20 | } 21 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/Information.ps1: -------------------------------------------------------------------------------- 1 | $Information = @{ 2 | Name = 'DCInformation' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Domain Controller Information" 7 | Data = { 8 | Get-ADDomainController -Server $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | IsEnabled = @{ 23 | Enable = $true 24 | Name = 'Is Enabled' 25 | Parameters = @{ 26 | Property = 'Enabled' 27 | ExpectedValue = $True 28 | OperationType = 'eq' 29 | } 30 | } 31 | IsGlobalCatalog = @{ 32 | Enable = $true 33 | Name = 'Is Global Catalog' 34 | Parameters = @{ 35 | Property = 'IsGlobalCatalog' 36 | ExpectedValue = $True 37 | OperationType = 'eq' 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/LdapInsecureBindings.ps1: -------------------------------------------------------------------------------- 1 | $LDAPInsecureBindings = @{ 2 | Name = 'DCLDAPInsecureBindings' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'LDAP Insecure Bindings' 7 | Data = { 8 | Get-WinADLDAPBindingsSummary -IncludeDomainControllers $DomainController -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Area = 'LDAP' 12 | Category = 'Security' 13 | Description = 'LDAP channel binding and LDAP signing provide ways to increase the security of network communications between an Active Directory Domain Services (AD DS) or an Active Directory Lightweight Directory Services (AD LDS) and its clients. There is a vulerability in the default configuration for Lightweight Directory Access Protocol (LDAP) channel binding and LDAP signing and may expose Active directory domain controllers to elevation of privilege vulnerabilities. Microsoft Security Advisory ADV190023 address the issue by recommending the administrators enable LDAP channel binding and LDAP signing on Active Directory Domain Controllers. This hardening must be done manually until the release of the security update that will enable these settings by default.' 14 | Resolution = 'Make sure to remove any Clients performing simple or unsigned bindings.' 15 | Importance = 10 16 | Resources = @( 17 | 'https://evotec.xyz/four-commands-to-help-you-track-down-insecure-ldap-bindings-before-march-2020/' 18 | 'https://support.microsoft.com/en-us/topic/2020-ldap-channel-binding-and-ldap-signing-requirements-for-windows-kb4520412-ef185fb8-00f7-167d-744c-f299a66fc00a' 19 | 'https://support.microsoft.com/en-us/help/4520412/2020-ldap-channel-binding-and-ldap-signing-requirement-for-windows' 20 | ) 21 | } 22 | ExpectedOutput = $false 23 | } 24 | Tests = [ordered] @{ 25 | SimpleBinds = @{ 26 | Enable = $true 27 | Name = 'Simple binds performed without SSL/TLS is 0' 28 | Parameters = @{ 29 | Property = 'Number of simple binds performed without SSL/TLS' 30 | ExpectedValue = 0 31 | OperationType = 'eq' 32 | } 33 | } 34 | UnsignedBinds = @{ 35 | Enable = $true 36 | Name = 'Negotiate/Kerberos/NTLM/Digest binds performed without signing is 0' 37 | Parameters = @{ 38 | Property = 'Number of Negotiate/Kerberos/NTLM/Digest binds performed without signing' 39 | ExpectedValue = 0 40 | OperationType = 'eq' 41 | } 42 | } 43 | 44 | } 45 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/MSSLegacy.ps1: -------------------------------------------------------------------------------- 1 | $MSSLegacy = @{ 2 | Name = 'DCMSSLegacy' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "MSS (Legacy)" 7 | Data = { 8 | Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Network' 12 | Category = 'Security' 13 | Description = 'Provides verification of MSS Network Settings on Domain Controllers' 14 | Resolution = '' 15 | Importance = 10 16 | Resources = @( 17 | 'https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/' 18 | ) 19 | } 20 | Requirements = @{ 21 | CommandAvailable = 'Get-PSRegistry' 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | DisableIPSourceRouting = @{ 27 | Enable = $true 28 | Name = 'DisableIPSourceRouting' 29 | Parameters = @{ 30 | Property = 'DisableIPSourceRouting' 31 | ExpectedValue = 2 32 | OperationType = 'eq' 33 | } 34 | Details = [ordered] @{ 35 | Description = 'Highest protection, source routing is completely disabled' 36 | Resolution = '' 37 | Importance = 10 38 | Resources = @( 39 | 'https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/' 40 | ) 41 | } 42 | } 43 | EnableICMPRedirect = @{ 44 | Enable = $true 45 | Name = 'EnableICMPRedirect' 46 | Parameters = @{ 47 | Property = 'EnableICMPRedirect' 48 | ExpectedValue = 0 49 | OperationType = 'eq' 50 | } 51 | Details = [ordered] @{ 52 | Description = '' 53 | Resolution = '' 54 | Importance = 10 55 | Resources = @( 56 | 'https://blogs.technet.microsoft.com/secguide/2016/10/02/the-mss-settings/' 57 | ) 58 | } 59 | } 60 | } 61 | } 62 | 63 | 64 | #Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters' -ComputerName AD1 65 | #Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters' -ComputerName AD1 -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/NTDSParameters.ps1: -------------------------------------------------------------------------------- 1 | $NTDSParameters = @{ 2 | Name = 'DCNTDSParameters' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "NTDS Parameters" 7 | Data = { 8 | Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | DsaNotWritable = @{ 23 | Enable = $true 24 | Name = 'Domain Controller should be writeable' 25 | Parameters = @{ 26 | Property = 'Dsa Not Writable' 27 | ExpectedOutput = $false 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/NetSessionEnumeration.ps1: -------------------------------------------------------------------------------- 1 | $NetSessionEnumeration = @{ 2 | Name = 'DCNetSessionEnumeration' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Net Session Enumeration" 7 | Data = { 8 | $Registry = Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\LanmanServer\DefaultSecurity" -ComputerName $DomainController 9 | $CSD = [System.Security.AccessControl.CommonSecurityDescriptor]::new($true, $false, $Registry.SrvsvcSessionInfo, 0) 10 | $CSD.DiscretionaryAcl.SecurityIdentifier | Where-Object { $_ -eq 'S-1-5-11' } 11 | # ConvertFrom-SID -sid $CSD.DiscretionaryAcl.SecurityIdentifier | Where-Object { $_.Name -eq 'Authenticated Users' } 12 | } 13 | Details = [ordered] @{ 14 | Category = 'Security' 15 | Description = 'Net Session Enumeration is a method used to retrieve information about established sessions on a server. Any domain user can query a server for its established sessions.' 16 | Resolution = 'Hardening Net Session Enumeration' 17 | Importance = 10 18 | Resources = @( 19 | 'https://gallery.technet.microsoft.com/Net-Cease-Blocking-Net-1e8dcb5b' 20 | ) 21 | } 22 | Requirements = @{ 23 | CommandAvailable = 'Get-PSRegistry' 24 | } 25 | ExpectedOutput = $false 26 | } 27 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/NetworkCardSettings.ps1: -------------------------------------------------------------------------------- 1 | $NetworkCardSettings = @{ 2 | Name = 'DCNetworkCardSettings' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Get all network interfaces and firewall status" 7 | Data = { 8 | Get-ComputerNetwork -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Network' 12 | Category = 'Configuration' 13 | Importance = 0 14 | Description = '' 15 | Resolution = '' 16 | Resources = @( 17 | 18 | ) 19 | } 20 | ExpectedOutput = $true 21 | } 22 | Tests = [ordered] @{ 23 | NETBIOSOverTCPIP = @{ 24 | Enable = $true 25 | Name = 'NetBIOS over TCPIP should be disabled.' 26 | Parameters = @{ 27 | Property = 'NetBIOSOverTCPIP' 28 | ExpectedValue = 'Disabled' 29 | OperationType = 'eq' 30 | } 31 | Details = @{ 32 | Area = 'Network' 33 | Category = 'Configuration' 34 | Importance = 9 # 10 is top 35 | Description = @' 36 | 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. 37 | Enabling NetBios might help an attackers access shared directories, files and also gain sensitive information such as computer name, domain, or workgroup. 38 | '@ 39 | Resolution = 'Disable NetBIOS over TCPIP' 40 | Resources = @( 41 | 'http://woshub.com/how-to-disable-netbios-over-tcpip-and-llmnr-using-gpo/' 42 | ) 43 | } 44 | } 45 | Loopbackpresent = @{ 46 | Enable = $true 47 | Name = 'Loopback IP address should be list in DNS servers on network card' 48 | Parameters = @{ 49 | Property = 'DNSServerSearchOrder' 50 | ExpectedValue = '127.0.0.1' 51 | OperationType = 'Contains' 52 | } 53 | } 54 | WindowsFirewall = @{ 55 | Enable = $true 56 | Name = 'Windows Firewall should be enabled on network card' 57 | Parameters = @{ 58 | Property = 'FirewallStatus' 59 | ExpectedValue = $true 60 | OperationType = 'eq' 61 | } 62 | } 63 | WindowsFirewallProfile = @{ 64 | Enable = $true 65 | Name = 'Windows Firewall should be set on domain network profile' 66 | Parameters = @{ 67 | Property = 'FirewallProfile' 68 | ExpectedValue = 'DomainAuthenticated' 69 | OperationType = 'eq' 70 | } 71 | } 72 | DHCPDisabled = @{ 73 | Enable = $false 74 | Name = 'DHCP should be disabled on network card' 75 | Parameters = @{ 76 | Property = 'DHCPEnabled' 77 | ExpectedValue = $false 78 | OperationType = 'eq' 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/OperatingSystem.ps1: -------------------------------------------------------------------------------- 1 | $OperatingSystem = @{ 2 | Name = 'DCOperatingSystem' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'Operating System' 7 | Data = { 8 | Get-ComputerOperatingSystem -ComputerName $DomainController -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | OperatingSystem = @{ 23 | Enable = $true 24 | Name = 'Operating system Windows Server 2012 and up' 25 | Parameters = @{ 26 | Property = 'OperatingSystem' 27 | ExpectedValue = @('Microsoft Windows Server 2019*', 'Microsoft Windows Server 2016*', 'Microsoft Windows Server 2012*', 'Microsoft Windows Server 2022*') 28 | OperationType = 'like' 29 | # this means Expected Value will require at least one $true comparison 30 | # anything else will require all values to match $true 31 | OperationResult = 'OR' 32 | # This overwrites value, normally it shows results of comparison 33 | PropertyExtendedValue = 'OperatingSystem' 34 | } 35 | 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/Pingable.ps1: -------------------------------------------------------------------------------- 1 | $Pingable = @{ 2 | Name = 'DCPingable' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'Ping Connectivity' 7 | Data = { 8 | Test-NetConnection -ComputerName $DomainController -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | Ping = @{ 23 | Enable = $true 24 | Name = 'Responding to PING' 25 | Parameters = @{ 26 | Property = 'PingSucceeded' 27 | PropertyExtendedValue = 'PingReplyDetails', 'RoundtripTime' 28 | ExpectedValue = $true 29 | OperationType = 'eq' 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/RDPPorts.ps1: -------------------------------------------------------------------------------- 1 | # This is already done in RDPSecurity as well, stays disabled by default. 2 | 3 | $RDPPorts = [ordered] @{ 4 | Name = 'DCRDPPorts' 5 | Enable = $false 6 | Scope = 'DC' 7 | Source = [ordered] @{ 8 | Name = 'RDP Port is open' 9 | Data = { 10 | Test-ComputerPort -ComputerName $DomainController -PortTCP 3389 -WarningAction SilentlyContinue 11 | } 12 | Details = [ordered] @{ 13 | Area = '' 14 | Description = '' 15 | Resolution = '' 16 | Importance = 10 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | PortOpen = [ordered] @{ 25 | Enable = $false 26 | Name = 'Port is OPEN' 27 | Parameters = @{ 28 | Property = 'Status' 29 | ExpectedValue = $true 30 | OperationType = 'eq' 31 | PropertyExtendedValue = 'Summary' 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/RDPSecurity.ps1: -------------------------------------------------------------------------------- 1 | $RDPSecurity = [ordered] @{ 2 | Name = 'DCRDPSecurity' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = [ordered] @{ 6 | Name = 'RDP Security' 7 | Data = { 8 | Get-ComputerRDP -ComputerName $DomainController -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Network' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | PortOpen = [ordered] @{ 23 | Enable = $true 24 | Name = 'Port is OPEN' 25 | Parameters = @{ 26 | Property = 'Network' 27 | ExpectedValue = $true 28 | OperationType = 'eq' 29 | PropertyExtendedValue = 'ConnectivitySummary' 30 | } 31 | Details = [ordered] @{ 32 | Area = 'Network' 33 | Description = '' 34 | Resolution = '' 35 | Importance = 10 36 | Resources = @( 37 | 'https://lazywinadmin.com/2014/04/powershell-getset-network-level.html' 38 | 'https://devblogs.microsoft.com/scripting/weekend-scripter-report-on-network-level-authentication/' 39 | ) 40 | } 41 | } 42 | NLAAuthenticationEnabled = [ordered] @{ 43 | Enable = $true 44 | Name = 'NLA Authentication is Enabled' 45 | Parameters = @{ 46 | Property = 'UserAuthenticationRequired' 47 | ExpectedValue = $true 48 | OperationType = 'eq' 49 | } 50 | Details = [ordered] @{ 51 | Area = 'Network' 52 | Description = '' 53 | Resolution = '' 54 | Importance = 10 55 | Resources = @( 56 | 'https://lazywinadmin.com/2014/04/powershell-getset-network-level.html' 57 | 'https://devblogs.microsoft.com/scripting/weekend-scripter-report-on-network-level-authentication/' 58 | ) 59 | } 60 | } 61 | MinimalEncryptionLevel = [ordered] @{ 62 | Enable = $true 63 | Name = 'Minimal Encryption Level is set to at least High' 64 | Parameters = @{ 65 | Property = 'MinimalEncryptionLevelValue' 66 | ExpectedValue = 3 67 | OperationType = 'ge' 68 | PropertyExtendedValue = 'MinimalEncryptionLevel' 69 | } 70 | Details = [ordered] @{ 71 | Area = 'Network' 72 | Description = 'Remote connections must be encrypted to prevent interception of data or sensitive information. Selecting "High Level" will ensure encryption of Remote Desktop Services sessions in both directions.' 73 | Resolution = '' 74 | Importance = 10 75 | Resources = @( 76 | 'https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-3454' 77 | ) 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/SMBShares.ps1: -------------------------------------------------------------------------------- 1 | $SMBShares = @{ 2 | Name = 'DCSMBShares' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'Default SMB Shares' 7 | Data = { 8 | Get-ComputerSMBShare -ComputerName $DomainController -Translated 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | Requirements = @{ 20 | CommandAvailable = 'Get-ComputerSMBShare' 21 | } 22 | ExpectedOutput = $true 23 | } 24 | Tests = [ordered] @{ 25 | AdminShare = @{ 26 | Enable = $true 27 | Name = 'Remote Admin Share is available' 28 | Parameters = @{ 29 | WhereObject = { $_.Name -eq 'ADMIN$' } 30 | ExpectedCount = 1 31 | PropertyExtendedValue = 'Path' 32 | } 33 | } 34 | DefaultShare = @{ 35 | Enable = $true 36 | Name = 'Default Share is available' 37 | Parameters = @{ 38 | WhereObject = { $_.Name -eq 'C$' } 39 | ExpectedCount = 1 40 | PropertyExtendedValue = 'Path' 41 | } 42 | } 43 | RemoteIPC = @{ 44 | Enable = $true 45 | Name = 'Remote IPC Share is available' 46 | Parameters = @{ 47 | WhereObject = { $_.Name -eq 'IPC$' } 48 | ExpectedCount = 1 49 | PropertyExtendedValue = 'Path' 50 | } 51 | } 52 | NETLOGON = @{ 53 | Enable = $true 54 | Name = 'NETLOGON Share is available' 55 | Parameters = @{ 56 | WhereObject = { $_.Name -eq 'NETLOGON' } 57 | ExpectedCount = 1 58 | PropertyExtendedValue = 'Path' 59 | } 60 | } 61 | SYSVOL = @{ 62 | Enable = $true 63 | Name = 'SYSVOL Share is available' 64 | Parameters = @{ 65 | WhereObject = { $_.Name -eq 'SYSVOL' } 66 | ExpectedCount = 1 67 | PropertyExtendedValue = 'Path' 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/ServiceWINRM.ps1: -------------------------------------------------------------------------------- 1 | $ServiceWINRM = @{ 2 | Name = 'DCServiceWINRM' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Service WINRM" 7 | Data = { 8 | Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\WinRM\Service' -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Security' 12 | Area = '' 13 | Description = 'Storage of administrative credentials could allow unauthorized access. Disallowing the storage of RunAs credentials for Windows Remote Management will prevent them from being used with plug-ins. The Windows Remote Management (WinRM) service must not store RunAs credentials.' 14 | Resolution = '' 15 | Importance = 10 16 | Resources = @( 17 | 18 | ) 19 | } 20 | Requirements = @{ 21 | CommandAvailable = 'Get-PSRegistry' 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | DisableRunAs = @{ 27 | Enable = $true 28 | Name = 'DisableRunAs' 29 | Parameters = @{ 30 | Property = 'DisableRunAs' 31 | ExpectedValue = 1 32 | OperationType = 'eq' 33 | } 34 | Details = [ordered] @{ 35 | Area = '' 36 | Description = 'Storage of administrative credentials could allow unauthorized access. Disallowing the storage of RunAs credentials for Windows Remote Management will prevent them from being used with plug-ins. The Windows Remote Management (WinRM) service must not store RunAs credentials.' 37 | Resolution = '' 38 | Importance = 10 39 | Resources = @( 40 | 'https://www.stigviewer.com/stig/windows_server_2016/2018-03-07/finding/V-73603' 41 | ) 42 | } 43 | } 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/TimeSettings.ps1: -------------------------------------------------------------------------------- 1 | $TimeSettings = [ordered] @{ 2 | Name = 'DCTimeSettings' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Time Settings" 7 | Data = { 8 | Get-TimeSettings -ComputerName $DomainController -Domain $Domain 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 2 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | NTPServerEnabled = @{ 23 | Enable = $true 24 | Name = 'NtpServer must be enabled.' 25 | Parameters = @{ 26 | WhereObject = { $_.ComputerName -eq $DomainController } 27 | Property = 'NtpServerEnabled' 28 | ExpectedValue = $true 29 | OperationType = 'eq' 30 | } 31 | } 32 | NTPServerIntervalMissing = @{ 33 | Enable = $true 34 | Name = 'Ntp Server Interval should be set' 35 | Parameters = @{ 36 | WhereObject = { $_.ComputerName -eq $DomainController } 37 | Property = 'NtpServerIntervals' 38 | ExpectedValue = 'Missing' 39 | OperationType = 'notcontains' 40 | } 41 | } 42 | NTPServerIntervalIncorrect = @{ 43 | Enable = $true 44 | Name = 'Ntp Server Interval should be within known settings' 45 | Parameters = @{ 46 | WhereObject = { $_.ComputerName -eq $DomainController } 47 | Property = 'NtpServerIntervals' 48 | ExpectedValue = 'Incorrect' 49 | OperationType = 'notcontains' 50 | } 51 | } 52 | VMTimeProvider = @{ 53 | Enable = $true 54 | Name = 'Virtual Machine Time Provider should be disabled.' 55 | Parameters = @{ 56 | WhereObject = { $_.ComputerName -eq $DomainController } 57 | Property = 'VMTimeProvider' 58 | ExpectedValue = $false 59 | OperationType = 'eq' 60 | } 61 | } 62 | NtpTypeNonPDC = [ordered] @{ 63 | Enable = $true 64 | Name = 'NTP Server should be set to Domain Hierarchy' 65 | Requirements = @{ 66 | IsPDC = $false 67 | } 68 | Parameters = @{ 69 | WhereObject = { $_.ComputerName -eq $DomainController } 70 | Property = 'NtpType' 71 | ExpectedValue = 'NT5DS' 72 | OperationType = 'eq' 73 | 74 | } 75 | } 76 | NtpTypePDC = [ordered] @{ 77 | Enable = $true 78 | Name = 'NTP Server should be set to NTP' 79 | Requirements = @{ 80 | IsPDC = $true 81 | } 82 | Parameters = @{ 83 | WhereObject = { $_.ComputerName -eq $DomainController } 84 | Property = 'NtpType' 85 | ExpectedValue = 'NTP' 86 | OperationType = 'eq' 87 | 88 | } 89 | } 90 | WindowsSecureTimeSeeding = [ordered] @{ 91 | Enable = $true 92 | Name = 'Windows Secure Time Seeding should be disabled.' 93 | Parameters = @{ 94 | WhereObject = { $_.ComputerName -eq $DomainController } 95 | Property = 'WindowsSecureTimeSeeding' 96 | ExpectedValue = $false 97 | OperationType = 'eq' 98 | } 99 | Details = @{ 100 | Importance = 10 101 | ActionType = 2 102 | StatusTrue = 1 103 | StatusFalse = 5 104 | Resources = @( 105 | '[Windows Secure Time Seeding, should you disable it?](https://www.askwoody.com/forums/topic/windows-secure-time-seeding-should-you-disable-it/)' 106 | '[Wrong system time and insecure Secure Time Seeding](https://www.kaspersky.com/blog/windows-system-time-sudden-changes/48956/)' 107 | '[How to enable or disable Secure Time Seeding in Windows computers](https://www.thewindowsclub.com/secure-time-seeding-windows-10)' 108 | '[Windows feature that resets system clocks based on random data is wreaking havoc](https://arstechnica.com/security/2023/08/windows-feature-that-resets-system-clocks-based-on-random-data-is-wreaking-havoc/3/)' 109 | ) 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/TimeSynchronizationExternal.ps1: -------------------------------------------------------------------------------- 1 | $TimeSynchronizationExternal = @{ 2 | Name = 'DCTimeSynchronizationExternal' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Time Synchronization External" 7 | Data = { 8 | Get-ComputerTime -TimeTarget $DomainController -WarningAction SilentlyContinue -TimeSource $TimeSource 9 | } 10 | Parameters = @{ 11 | TimeSource = 'pool.ntp.org' 12 | } 13 | Details = [ordered] @{ 14 | Area = '' 15 | Category = 'Configuration' 16 | Description = '' 17 | Resolution = '' 18 | Importance = 2 19 | Resources = @( 20 | '[How to: Fix Time Sync in your Domain](https://community.spiceworks.com/how_to/166215-fix-time-sync-in-your-domain-use-w32time)' 21 | '[Windows Time Settings in a Domain](https://www.concurrency.com/blog/october-2018/windows-time-settings-in-a-domain)' 22 | ) 23 | } 24 | ExpectedOutput = $true 25 | } 26 | Tests = [ordered] @{ 27 | TimeSynchronizationTest = @{ 28 | Enable = $true 29 | Name = 'Time Difference' 30 | Details = [ordered] @{ 31 | Area = '' 32 | Category = 'Configuration' 33 | Description = '' 34 | Importance = 2 35 | Resources = @( 36 | 37 | ) 38 | } 39 | Parameters = @{ 40 | Property = 'TimeDifferenceSeconds' 41 | ExpectedValue = 1 42 | OperationType = 'le' 43 | PropertyExtendedValue = 'TimeDifferenceSeconds' 44 | } 45 | } 46 | } 47 | MicrosoftMaterials = 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc773263(v=ws.10)#w2k3tr_times_tools_uhlp' 48 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/TimeSynchronizationInternal.ps1: -------------------------------------------------------------------------------- 1 | $TimeSynchronizationInternal = @{ 2 | Name = 'DCTimeSynchronizationInternal' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Time Synchronization Internal" 7 | Data = { 8 | Get-ComputerTime -TimeTarget $DomainController -WarningAction SilentlyContinue 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Area = '' 13 | Description = '' 14 | Resolution = '' 15 | Importance = 2 16 | Resources = @( 17 | 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc773263(v=ws.10)#w2k3tr_times_tools_uhlp' 18 | ) 19 | } 20 | ExpectedOutput = $true 21 | } 22 | Tests = [ordered] @{ 23 | LastBootUpTime = @{ 24 | Enable = $true 25 | Name = 'Last Boot Up time should be less than X days' 26 | Parameters = @{ 27 | Property = 'LastBootUpTime' 28 | ExpectedValue = '(Get-Date).AddDays(-60)' 29 | OperationType = 'gt' 30 | } 31 | } 32 | TimeSynchronizationTest = @{ 33 | Enable = $true 34 | Name = 'Time Difference' 35 | Parameters = @{ 36 | Property = 'TimeDifferenceSeconds' 37 | ExpectedValue = 1 38 | OperationType = 'le' 39 | PropertyExtendedValue = 'TimeDifferenceSeconds' 40 | } 41 | } 42 | } 43 | } 44 | 45 | <# 46 | Name LocalDateTime RemoteDateTime InstallTime LastBootUpTime TimeDifferenceMinutes TimeDifferenceSeconds TimeDifferenceMilliseconds TimeSourceName Status 47 | ---- ------------- -------------- ----------- -------------- --------------------- --------------------- -------------------------- -------------- ------ 48 | AD2 17.09.2019 07:38:57 17.09.2019 07:38:57 30.05.2018 18:30:48 13.09.2019 07:54:10 0,0417166666666667 2,503 2503 AD1.ad.evotec.xyz 49 | AD3 17.09.2019 07:38:56 17.09.2019 02:38:57 26.05.2019 17:30:17 13.09.2019 07:54:09 0,02175 1,305 1305 AD1.ad.evotec.xyz 50 | EVOWin 17.09.2019 07:38:57 17.09.2019 07:38:57 24.05.2019 22:46:45 13.09.2019 07:53:44 0,0415 2,49 2490 AD1.ad.evotec.xyz 51 | #> -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/UNCHardenedPaths.ps1: -------------------------------------------------------------------------------- 1 | $UNCHardenedPaths = @{ 2 | Name = 'DCUNCHardenedPaths' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Hardened UNC Paths" 7 | Data = { 8 | Get-PSRegistry -RegistryPath "HKLM\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths" -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Security' 12 | Area = '' 13 | Description = 'Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares.' 14 | Resolution = 'Harden UNC Paths for SYSVOL and NETLOGON' 15 | Importance = 10 16 | Resources = @( 17 | 'https://docs.microsoft.com/en-us/archive/blogs/leesteve/demystifying-the-unc-hardening-dilemma' 18 | 'https://www.stigviewer.com/stig/windows_10/2016-06-24/finding/V-63577' 19 | 'https://support.microsoft.com/en-us/help/3000483/ms15-011-vulnerability-in-group-policy-could-allow-remote-code-executi' 20 | ) 21 | } 22 | Requirements = @{ 23 | CommandAvailable = 'Get-PSRegistry' 24 | } 25 | Implementation = { 26 | 27 | } 28 | Rollback = { 29 | Remove-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows\NetworkProvider\HardenedPaths" -Name "*" 30 | } 31 | ExpectedOutput = $true 32 | } 33 | Tests = [ordered] @{ 34 | NetLogonUNCPath = @{ 35 | Enable = $true 36 | Name = 'Netlogon UNC Hardening' 37 | Parameters = @{ 38 | Property = '\\*\NETLOGON' 39 | ExpectedValue = 'RequireMutualAuthentication=1, RequireIntegrity=1', 'RequireMutualAuthentication=1,RequireIntegrity=1' 40 | OperationType = 'in' 41 | } 42 | Description = "Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares." 43 | } 44 | SysVolUNCPath = @{ 45 | Enable = $true 46 | Name = 'SysVol UNC Hardening' 47 | Parameters = @{ 48 | Property = '\\*\SYSVOL' 49 | ExpectedValue = 'RequireMutualAuthentication=1, RequireIntegrity=1', 'RequireMutualAuthentication=1,RequireIntegrity=1' 50 | OperationType = 'in' 51 | } 52 | Description = "Hardened UNC Paths must be defined to require mutual authentication and integrity for at least the \\*\SYSVOL and \\*\NETLOGON shares." 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/WindowsRemoteManagement.ps1: -------------------------------------------------------------------------------- 1 | $WindowsRemoteManagement = @{ 2 | Name = 'DCWindowsRemoteManagement' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = 'Windows Remote Management' 7 | Data = { 8 | Test-WinRM -ComputerName $DomainController 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | WindowsRemoteManagement = @{ 23 | Enable = $true 24 | Name = 'Test submits an identification request that determines whether the WinRM service is running.' 25 | Parameters = @{ 26 | Property = 'Status' 27 | ExpectedValue = $true 28 | OperationType = 'eq' 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/WindowsRolesAndFeatures.ps1: -------------------------------------------------------------------------------- 1 | $WindowsRolesAndFeatures = @{ 2 | Name = 'DCWindowsRolesAndFeatures' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Windows Roles and Features" 7 | Data = { 8 | Get-WindowsFeature -ComputerName $DomainController #| Where-Object { $_.'InstallState' -eq 'Installed' } 9 | } 10 | ExpectedOutput = $true 11 | } 12 | Tests = [ordered] @{ 13 | ActiveDirectoryDomainServices = @{ 14 | Enable = $true 15 | Name = 'Active Directory Domain Services is installed' 16 | Parameters = @{ 17 | WhereObject = { $_.Name -eq 'AD-Domain-Services' } 18 | Property = 'Installed' 19 | ExpectedValue = $true 20 | OperationType = 'eq' 21 | } 22 | } 23 | DNSServer = @{ 24 | Enable = $true 25 | Name = 'DNS Server is installed' 26 | Parameters = @{ 27 | WhereObject = { $_.Name -eq 'DNS' } 28 | Property = 'Installed' 29 | ExpectedValue = $true 30 | OperationType = 'eq' 31 | } 32 | } 33 | FileandStorageServices = @{ 34 | Enable = $true 35 | Name = 'File and Storage Services is installed' 36 | Parameters = @{ 37 | WhereObject = { $_.Name -eq 'FileAndStorage-Services' } 38 | Property = 'Installed' 39 | ExpectedValue = $true 40 | OperationType = 'eq' 41 | } 42 | } 43 | FileandiSCSIServices = @{ 44 | Enable = $true 45 | Name = 'File and iSCSI Services is installed' 46 | Parameters = @{ 47 | WhereObject = { $_.Name -eq 'File-Services' } 48 | Property = 'Installed' 49 | ExpectedValue = $true 50 | OperationType = 'eq' 51 | } 52 | } 53 | FileServer = @{ 54 | Enable = $true 55 | Name = 'File Server is installed' 56 | Parameters = @{ 57 | WhereObject = { $_.Name -eq 'FS-FileServer' } 58 | Property = 'Installed' 59 | ExpectedValue = $true 60 | OperationType = 'eq' 61 | } 62 | } 63 | StorageServices = @{ 64 | Enable = $true 65 | Name = 'Storage Services is installed' 66 | Parameters = @{ 67 | WhereObject = { $_.Name -eq 'Storage-Services' } 68 | Property = 'Installed' 69 | ExpectedValue = $true 70 | OperationType = 'eq' 71 | } 72 | } 73 | WindowsPowerShell51 = @{ 74 | Enable = $true 75 | Name = 'Windows PowerShell 5.1 is installed' 76 | Parameters = @{ 77 | WhereObject = { $_.Name -eq 'PowerShell' } 78 | Property = 'Installed' 79 | ExpectedValue = $true 80 | OperationType = 'eq' 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /Private/SourcesDomainControllers/WindowsUpdates.ps1: -------------------------------------------------------------------------------- 1 | $WindowsUpdates = @{ 2 | Name = 'DCWindowsUpdates' 3 | Enable = $true 4 | Scope = 'DC' 5 | Source = @{ 6 | Name = "Windows Updates" 7 | Data = { 8 | Get-HotFix -ComputerName $DomainController | Sort-Object -Property InstalledOn -Descending | Select-Object -First 1 -Property HotFixID, InstalledOn, Description, InstalledBy 9 | } 10 | Details = [ordered] @{ 11 | Area = '' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 10 15 | Resources = @( 16 | 17 | ) 18 | } 19 | ExpectedOutput = $true 20 | } 21 | Tests = [ordered] @{ 22 | WindowsUpdates = @{ 23 | Enable = $true 24 | Name = 'Last Windows Updates should be less than X days ago' 25 | Parameters = @{ 26 | Property = 'InstalledOn' 27 | ExpectedValue = '(Get-Date).AddDays(-60)' 28 | OperationType = 'gt' 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Private/SourcesForest/Backup.ps1: -------------------------------------------------------------------------------- 1 | $Backup = @{ 2 | Name = "ForestBackup" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Forest Backup' 7 | Data = { 8 | Get-WinADLastBackup -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Description = 'Active Directory is critical system for any company. Having a proper, up to date backup in place is crucial.' 13 | Importance = 0 14 | ActionType = 0 15 | Resources = @( 16 | '[Backing Up and Restoring an Active Directory Server](https://docs.microsoft.com/en-us/windows/win32/ad/backing-up-and-restoring-an-active-directory-server)' 17 | '[Backup Active Directory (Full and Incremental Backup)](https://activedirectorypro.com/backup-active-directory/)' 18 | ) 19 | Tags = 'Backup', 'Configuration' 20 | StatusTrue = 0 21 | StatusFalse = 0 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | LastBackupTests = @{ 27 | Enable = $true 28 | Name = 'Forest Last Backup Time' 29 | Parameters = @{ 30 | ExpectedValue = 2 31 | OperationType = 'lt' 32 | Property = 'LastBackupDaysAgo' 33 | PropertyExtendedValue = 'LastBackup' 34 | OverwriteName = { "Last Backup $($_.NamingContext)" } 35 | } 36 | Details = [ordered] @{ 37 | Category = 'Configuration' 38 | Importance = 10 39 | ActionType = 2 40 | StatusTrue = 1 41 | StatusFalse = 5 42 | } 43 | } 44 | } 45 | DataDescription = { 46 | New-HTMLSpanStyle -FontSize 10pt { 47 | New-HTMLText -Text @( 48 | 'Active Directory is critical system for any company. Having a proper, up to date backup is crucial. ' 49 | 'Last backup should be maximum few days old, if not less than 24 hours old. ' 50 | "Please keep in mind that this test doesn't verifies the backup, nor provides information if the backup was saved to proper place and will be available for restore operations. " 51 | "This tests merely checks what was reported by Active Directory - that backup did happen. " 52 | "You should make sure that your backup, and more importantly restore process actually works! " 53 | ) 54 | } 55 | } 56 | DataHighlights = { 57 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor PaleGreen -Value 2 -Operator lt 58 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Salmon -Value 2 -Operator ge 59 | New-HTMLTableCondition -Name 'LastBackupDaysAgo' -ComparisonType number -BackgroundColor Tomato -Value 10 -Operator ge 60 | } 61 | } -------------------------------------------------------------------------------- /Private/SourcesForest/DuplicateSPN.ps1: -------------------------------------------------------------------------------- 1 | $DuplicateSPN = @{ 2 | Name = 'ForestDuplicateSPN' 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Duplicate SPN' 7 | Data = { 8 | Get-WinADDuplicateSPN -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Security' 12 | Description = "SPNs must be unique, so if an SPN already exists for a service on a server then you must delete the SPN that is is already registered to one account and recreate the SPN registered to the correct account." 13 | Importance = 5 14 | ActionType = 1 15 | Resources = @( 16 | "[Duplicate SPN found - Troubleshooting Duplicate SPNs](https://support.squaredup.com/hc/en-us/articles/4406616176657-Duplicate-SPN-found-Troubleshooting-Duplicate-SPNs)" 17 | "[SPN and UPN uniqueness](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/spn-and-upn-uniqueness)" 18 | "[Name Formats for Unique SPNs](https://docs.microsoft.com/en-us/windows/win32/ad/name-formats-for-unique-spns)" 19 | "[Kerberos - duplicate SPNs](https://itworldjd.wordpress.com/2017/02/15/kerberos-duplicate-spns/)" 20 | ) 21 | StatusTrue = 0 22 | StatusFalse = 5 23 | } 24 | ExpectedOutput = $false 25 | } 26 | DataDescription = { 27 | New-HTMLSpanStyle -FontSize 10pt { 28 | New-HTMLText -Text @( 29 | 'Services use service publication in the Active Directory directory service to provide information about themselves in the directory for easy discovery by client applications and other services. ' 30 | 'Service publication occurs when the installation program for a service publishes information about the service, including binding and keyword data, to the directory. ' 31 | 'Service publication happens by creating service objects (also called connection point objects) in Active Directory. ' 32 | ) 33 | New-HTMLText -Text @( 34 | 'In addition, Active Directory supports service principal names (SPNs) as a means by which client applications can identify ' 35 | 'and authenticate the services that they use. Service authentication happens through Kerberos authentication of SPNs. ' 36 | 'Kerberos uses SPNs extensively. When a Kerberos client uses its TGT to request a service ticket for a specific service, the service uses SPN to identify it. ' 37 | 'The KDC will grant the client a service ticket that is encrypted in part with a shared secret ' 38 | 'that the service account identified by the AD account matches the SPN has (basically the account password). ' 39 | ) 40 | New-HTMLText -Text @( 41 | 'In the case of a duplicate SPN, what can happen is that the KDC will generate a service ticket that may base its shared secret on the wrong account. ' 42 | 'Then, when the client provides that ticket to the service during authentication, the service itself cannot decrypt it, and the authentication fails. ' 43 | 'The server will typically log an "AP Modified" error, and the client will see a "wrong principal" error code.' 44 | ) 45 | } 46 | } 47 | DataInformation = { 48 | New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt 49 | New-HTMLList { 50 | New-HTMLListItem -FontWeight bold, normal -Text "IsOrphaned", " - means object contains AdminCount set to 1, while not being a critical object or direct or indirect member of any critical system groups. " 51 | New-HTMLListItem -FontWeight bold, normal -Text "IsMember", " - means object is memberof (direct or indirect) of critical system groups. " 52 | New-HTMLListItem -FontWeight bold, normal -Text "IsCriticalSystemObject", " - means object is critical system object. " 53 | } -FontSize 10pt 54 | } 55 | DataHighlights = { 56 | # New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor Salmon -Value $true 57 | # New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor PaleGreen -Value $false 58 | # New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor PaleGreen -Value $true 59 | # New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor TangerineYellow -Value $false 60 | } 61 | } -------------------------------------------------------------------------------- /Private/SourcesForest/ForestFSMORoles.ps1: -------------------------------------------------------------------------------- 1 | $ForestFSMORoles = @{ 2 | Name = 'ForestRoles' 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Roles availability' 7 | Data = { 8 | Test-ADRolesAvailability -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Health' 12 | Description = '' 13 | Resolution = '' 14 | Importance = 0 15 | ActionType = 0 16 | Severity = 'High' 17 | Resources = @( 18 | 19 | ) 20 | StatusTrue = 0 21 | StatusFalse = 2 22 | } 23 | ExpectedOutput = $true 24 | } 25 | Tests = [ordered] @{ 26 | SchemaMasterAvailability = @{ 27 | Enable = $true 28 | Name = 'Schema Master Availability' 29 | Parameters = @{ 30 | ExpectedValue = $true 31 | Property = 'SchemaMasterAvailability' 32 | OperationType = 'eq' 33 | PropertyExtendedValue = 'SchemaMaster' 34 | } 35 | Details = [ordered] @{ 36 | Category = 'Health' 37 | Importance = 10 38 | ActionType = 2 39 | StatusTrue = 1 40 | StatusFalse = 10 41 | } 42 | } 43 | DomainNamingMasterAvailability = @{ 44 | Enable = $true 45 | Name = 'Domain Master Availability' 46 | Parameters = @{ 47 | ExpectedValue = $true 48 | Property = 'DomainNamingMasterAvailability' 49 | OperationType = 'eq' 50 | PropertyExtendedValue = 'DomainNamingMaster' 51 | } 52 | Details = [ordered] @{ 53 | Category = 'Health' 54 | Importance = 10 55 | ActionType = 2 56 | StatusTrue = 1 57 | StatusFalse = 10 58 | } 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Private/SourcesForest/OrphanedAdmins.ps1: -------------------------------------------------------------------------------- 1 | $OrphanedAdmins = @{ 2 | Name = "ForestOrphanedAdmins" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Orphaned Administrative Objects (AdminCount)' 7 | Data = { 8 | Get-WinADPrivilegedObjects -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Security' 12 | Description = "Active Directory user, group, and computer objects possess an AdminCount attribute. The AdminCount attribute’s value defaults to NOT SET. Its utility comes from the fact when a user, group, or computer is added, either directly or transitively, to any of a specific set of protected groups its value is updated to 1. This can provide a relatively simple method by which objects with inherited administrative privileges may be identified. Consider this: a user is stamped with an AdminCount of 1, as a result of being added to Domain Admins; the user is removed from Domain Admins; the AdminCount value persists. In this instance the user is considered as orphaned. The ramifications? The AdminSDHolder ACL will be stamped upon this user every hour to protect against tampering. In turn, this can cause unexpected issues with delegation and application permissions." 13 | Importance = 4 14 | ActionType = 1 15 | Resources = @( 16 | '[Security Focus: Orphaned AdminCount -eq 1 AD Users](https://blogs.technet.microsoft.com/poshchap/2016/07/29/security-focus-orphaned-admincount-eq-1-ad-users/)' 17 | "[Fun with Active Directory's AdminCount Attiribute](https://stealthbits.com/blog/fun-with-active-directorys-admincount-attribute/)" 18 | '[AdminSDHolder, Protected Groups and SDPROP](https://technet.microsoft.com/en-us/magazine/2009.09.sdadminholder.aspx)' 19 | '[Scanning for Active Directory Privileges & Privileged Accounts](https://adsecurity.org/?p=3658)' 20 | 'https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-r2-and-2012/dn535495(v=ws.11)' 21 | ) 22 | StatusTrue = 0 23 | StatusFalse = 0 24 | } 25 | ExpectedOutput = $true 26 | } 27 | Tests = [ordered] @{ 28 | Enabled = @{ 29 | Enable = $true 30 | Name = 'No orphaned AdminCount' 31 | Parameters = @{ 32 | ExpectedCount = 0 33 | OperationType = 'eq' 34 | WhereObject = { $_.IsOrphaned -ne $false } 35 | } 36 | Details = [ordered] @{ 37 | Category = 'Security' 38 | Importance = 4 39 | StatusTrue = 1 40 | StatusFalse = 2 41 | } 42 | } 43 | } 44 | DataInformation = { 45 | New-HTMLText -Text 'Explanation to table columns:' -FontSize 10pt 46 | New-HTMLList { 47 | New-HTMLListItem -FontWeight bold, normal -Text "IsOrphaned", " - means object contains AdminCount set to 1, while not being a critical object or direct or indirect member of any critical system groups. " 48 | New-HTMLListItem -FontWeight bold, normal -Text "IsMember", " - means object is memberof (direct or indirect) of critical system groups. " 49 | New-HTMLListItem -FontWeight bold, normal -Text "IsCriticalSystemObject", " - means object is critical system object. " 50 | } -FontSize 10pt 51 | } 52 | DataHighlights = { 53 | New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor Salmon -Value $true 54 | New-HTMLTableCondition -Name 'IsOrphaned' -ComparisonType string -BackgroundColor PaleGreen -Value $false 55 | New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor PaleGreen -Value $true 56 | New-HTMLTableCondition -Name 'IsCriticalSystemObject' -ComparisonType string -BackgroundColor TangerineYellow -Value $false 57 | } 58 | } -------------------------------------------------------------------------------- /Private/SourcesForest/Replication.ps1: -------------------------------------------------------------------------------- 1 | $Replication = @{ 2 | Name = "ForestReplication" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Forest Replication' 7 | Data = { 8 | Get-WinADForestReplication -WarningAction SilentlyContinue -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Health' 12 | Description = '' 13 | Importance = 10 14 | ActionType = 2 15 | Severity = 'High' 16 | Resources = @( 17 | "[Active Directory Replication](https://blog.netwrix.com/2017/02/20/active-directory-replication/)" 18 | "[Active Directory Replication Concepts](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/get-started/replication/active-directory-replication-concepts)" 19 | "[Repadmin: How to Check Active Directory Replication](https://activedirectorypro.com/repadmin-how-to-check-active-directory-replication/)" 20 | ) 21 | StatusTrue = 1 22 | StatusFalse = 5 23 | } 24 | ExpectedOutput = $true 25 | } 26 | Tests = [ordered] @{ 27 | ReplicationTests = @{ 28 | Enable = $true 29 | Name = 'Replication Test' 30 | Parameters = @{ 31 | ExpectedValue = $true 32 | Property = 'Status' 33 | OperationType = 'eq' 34 | PropertyExtendedValue = 'StatusMessage' 35 | OverwriteName = { "Replication from $($_.Server) to $($_.ServerPartner)" } 36 | } 37 | Details = @{ 38 | Category = 'Health' 39 | Importance = 10 40 | ActionType = 2 41 | StatusTrue = 1 42 | StatusFalse = 5 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /Private/SourcesForest/ReplicationStatus.ps1: -------------------------------------------------------------------------------- 1 |  2 | $ReplicationStatus = @{ 3 | Name = "ForestReplicationStatus" 4 | Enable = $true 5 | Scope = 'Forest' 6 | Source = @{ 7 | Name = 'Forest Replication using RepAdmin' 8 | Data = { 9 | $Header = '"showrepl_COLUMNS","Destination DSA Site","Destination DSA","Naming Context","Source DSA Site","Source DSA","Transport Type","Number of Failures","Last Failure Time","Last Success Time","Last Failure Status"' 10 | $data = repadmin /showrepl * /csv 11 | $data[0] = $Header 12 | $data | ConvertFrom-Csv 13 | } 14 | Details = [ordered] @{ 15 | Category = 'Health' 16 | Description = '' 17 | Importance = 10 18 | ActionType = 2 19 | Severity = 'High' 20 | Resources = @( 21 | "[Active Directory Replication](https://blog.netwrix.com/2017/02/20/active-directory-replication/)" 22 | "[Active Directory Replication Concepts](https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/get-started/replication/active-directory-replication-concepts)" 23 | "[Repadmin: How to Check Active Directory Replication](https://activedirectorypro.com/repadmin-how-to-check-active-directory-replication/)" 24 | ) 25 | StatusTrue = 1 26 | StatusFalse = 5 27 | } 28 | Requirements = @{ 29 | CommandAvailable = 'repadmin' 30 | IsInternalForest = $true 31 | } 32 | ExpectedOutput = $true 33 | } 34 | Tests = [ordered] @{ 35 | ReplicationTests = @{ 36 | Enable = $true 37 | Name = 'Replication Test' 38 | Parameters = @{ 39 | ExpectedValue = 0 40 | Property = 'Number of Failures' 41 | OperationType = 'eq' 42 | PropertyExtendedValue = 'Last Success Time' 43 | OverwriteName = { "Replication from $($_.'Source DSA') to $($_.'Destination DSA'), Naming Context: $($_.'Naming Context')" } 44 | } 45 | Details = @{ 46 | Category = 'Health' 47 | Importance = 10 48 | ActionType = 2 49 | StatusTrue = 1 50 | StatusFalse = 5 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Private/SourcesForest/RootKDS.ps1: -------------------------------------------------------------------------------- 1 | $RootKDS = @{ 2 | Name = "ForestRootKDS" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Forest Root KDS Key' 7 | Data = { 8 | Get-KdsRootKey 9 | } 10 | Details = [ordered] @{ 11 | Category = 'Configuration' 12 | Description = 'Active Directory KDS Root Key is required to create GMSA accounts' 13 | Importance = 6 14 | ActionType = 1 15 | Resources = @( 16 | '[ConfigMgr – SQL and Active Directory gMSA](https://configmgr.com/tag/root-key/)' 17 | '[Create the Key Distribution Services KDS Root Key](https://docs.microsoft.com/en-us/windows-server/security/group-managed-service-accounts/create-the-key-distribution-services-kds-root-key)' 18 | ) 19 | Tags = 'Configuration', 'GMSA' 20 | StatusTrue = 1 21 | StatusFalse = 3 22 | } 23 | ExpectedOutput = $true 24 | } 25 | DataDescription = { 26 | New-HTMLSpanStyle -FontSize 10pt { 27 | New-HTMLText -Text @( 28 | "Domain Controllers (DC) require a root key to begin generating gMSA passwords. " 29 | "The domain controllers will wait up to 10 hours from time of creation to allow all domain controllers to converge their AD replication before allowing the creation of a gMSA. " 30 | "The 10 hours is a safety measure to prevent password generation from occurring before all DCs in the environment are capable of answering gMSA requests. " 31 | "If you try to use a gMSA too soon the key might not have been replicated to all domain controllers and therefore password retrieval might fail when the gMSA host attempts to retrieve the password. " 32 | "gMSA password retrieval failures can also occur when using DCs with limited replication schedules or if there is a replication issue." 33 | ) 34 | } 35 | } 36 | DataHighlights = { 37 | 38 | } 39 | Solution = { 40 | New-HTMLContainer { 41 | New-HTMLSpanStyle -FontSize 10pt { 42 | New-HTMLWizard { 43 | New-HTMLWizardStep -Name 'Setting up KDS Root Key' { 44 | New-HTMLText -Text @( 45 | "On the Windows Server 2012 or later domain controller, run the Windows PowerShell from the Taskbar. " 46 | "To create the KDS root key using the Add-KdsRootKey cmdlet, run the following command: " 47 | ) 48 | New-HTMLCodeBlock { 49 | Add-KdsRootKey -EffectiveImmediately 50 | } 51 | New-HTMLText -Text "The 10 hours is a safety measure to prevent password generation from occurring before all DCs in the environment are capable of answering gMSA requests." 52 | New-HTMLText -LineBreak 53 | New-HTMLText -Text @( 54 | "The Effective time parameter can be used to give time for keys to be propagated to all DCs before use. " 55 | "Using Add-KdsRootKey -EffectiveImmediately will add a root key to the target DC which will be used by the KDS service immediately. " 56 | "However, other domain controllers will not be able to use the root key until replication is successful." 57 | "For test environments with only one DC, you can create a KDS root key and set the start time in the past to avoid the interval wait for key generation by using the following procedure. " 58 | "Validate that a 4004 event has been logged in the kds event log." 59 | ) 60 | New-HTMLText -Text "To create the ", "KDS root key ", "using the ", "Add-KdsRootKey", " cmdlet" -Color None, Tangerine, None, Tangerine, None 61 | New-HTMLCodeBlock { 62 | Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10)) 63 | } 64 | } 65 | } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Private/SourcesForest/SiteLinks.ps1: -------------------------------------------------------------------------------- 1 | $SiteLinks = @{ 2 | Name = "ForestSiteLinks" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Site Links' 7 | Data = { 8 | Get-WinADSiteLinks -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Sites' 12 | Category = 'Configuration' 13 | Description = '' 14 | Resolution = '' 15 | Importance = 10 16 | Severity = 'Informational' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | MinimalReplicationFrequency = @{ 25 | Enable = $true 26 | Name = 'Replication Frequency should be set to maximum 60 minutes' 27 | Parameters = @{ 28 | Property = 'ReplicationFrequencyInMinutes' 29 | ExpectedValue = 60 30 | OperationType = 'le' 31 | } 32 | } 33 | UseNotificationsForLinks = @{ 34 | Enable = $true 35 | Name = 'Automatic site links should use notifications' 36 | Parameters = @{ 37 | Property = 'Options' 38 | ExpectedValue = 'UseNotify' 39 | OperationType = 'contains' 40 | PropertyExtendedValue = 'Options' 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Private/SourcesForest/SiteLinksConnections.ps1: -------------------------------------------------------------------------------- 1 | $SiteLinksConnections = @{ 2 | Name = "ForestSiteLinksConnections" 3 | Enable = $true 4 | Scope = 'Forest' 5 | Source = @{ 6 | Name = 'Site Links Connections' 7 | Data = { 8 | Test-ADSiteLinks -Splitter ', ' -Forest $ForestName 9 | } 10 | Details = [ordered] @{ 11 | Area = 'Sites' 12 | Category = 'Configuration' 13 | Description = '' 14 | Resolution = '' 15 | Importance = 10 16 | Severity = 'Informational' 17 | Resources = @( 18 | 19 | ) 20 | } 21 | ExpectedOutput = $true 22 | } 23 | Tests = [ordered] @{ 24 | AutomaticSiteLinks = @{ 25 | Enable = $true 26 | Name = 'All site links are automatic' 27 | Description = 'Verify there are no manually configured sitelinks' 28 | Parameters = @{ 29 | Property = 'SiteLinksManualCount' 30 | ExpectedValue = 0 31 | OperationType = 'eq' 32 | } 33 | } 34 | SiteLinksCrossSiteNotifications = @{ 35 | Enable = $true 36 | Name = 'All cross-site links use notifications' 37 | Parameters = @{ 38 | Property = 'SiteLinksCrossSiteNotUseNotifyCount' 39 | ExpectedValue = 0 40 | OperationType = 'eq' 41 | } 42 | } 43 | SiteLinksSameSiteNotifications = @{ 44 | Enable = $true 45 | Name = 'All same-site links have no notifications' 46 | Parameters = @{ 47 | Property = 'SiteLinksSameSiteUseNotifyCount' 48 | ExpectedValue = 0 49 | OperationType = 'eq' 50 | } 51 | } 52 | NoDisabledLinks = @{ 53 | Enable = $true 54 | Name = 'All links are enabled' 55 | Parameters = @{ 56 | Property = 'SiteLinksDisabledCount' 57 | ExpectedValue = 0 58 | OperationType = 'eq' 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Private/Start-TestimoEmail.ps1: -------------------------------------------------------------------------------- 1 | function Start-TestimoEmail { 2 | <# 3 | .SYNOPSIS 4 | Sends an email with a summary of Active Directory tests. 5 | 6 | .DESCRIPTION 7 | This function sends an email with a summary of Active Directory tests. It allows customization of email settings such as From, To, CC, BCC, Server, Port, SSL, UserName, Password, Priority, and Subject. 8 | 9 | .PARAMETER From 10 | The email address from which the email will be sent. 11 | 12 | .PARAMETER To 13 | An array of email addresses to which the email will be sent. 14 | 15 | .PARAMETER CC 16 | An array of email addresses to be CC'd on the email. 17 | 18 | .PARAMETER BCC 19 | An array of email addresses to be BCC'd on the email. 20 | 21 | .PARAMETER Server 22 | The SMTP server used to send the email. 23 | 24 | .PARAMETER Port 25 | The port number of the SMTP server. 26 | 27 | .PARAMETER SSL 28 | Indicates whether SSL should be used for the email connection. 29 | 30 | .PARAMETER UserName 31 | The username for authentication with the SMTP server. 32 | 33 | .PARAMETER Password 34 | The password for authentication with the SMTP server. 35 | 36 | .PARAMETER PasswordAsSecure 37 | Indicates whether the password should be treated as a secure string. 38 | 39 | .PARAMETER PasswordFromFile 40 | Indicates whether the password should be read from a file. 41 | 42 | .PARAMETER Priority 43 | The priority of the email. Default is 'High'. 44 | 45 | .PARAMETER Subject 46 | The subject of the email. Default is '[Reporting Evotec] Summary of Active Directory Tests'. 47 | 48 | .EXAMPLE 49 | Start-TestimoEmail -From "sender@example.com" -To "recipient@example.com" -Server "mail.example.com" -Port 587 -SSL -UserName "username" -Password "password" -Priority "High" -Subject "Summary of Active Directory Tests" 50 | 51 | Sends an email with the specified parameters. 52 | 53 | #> 54 | [CmdletBinding()] 55 | param( 56 | [string] $From, 57 | [string[]] $To, 58 | [string[]] $CC, 59 | [string[]] $BCC, 60 | [string] $Server, 61 | [int] $Port, 62 | [switch] $SSL, 63 | [string] $UserName, 64 | [string] $Password, 65 | [switch] $PasswordAsSecure, 66 | [switch] $PasswordFromFile, 67 | [string] $Priority = 'High', 68 | [string] $Subject = '[Reporting Evotec] Summary of Active Directory Tests' 69 | ) 70 | Email { 71 | EmailHeader { 72 | EmailFrom -Address $From 73 | EmailTo -Addresses $To 74 | EmailServer -Server $Server -UserName $UserName -Password $PasswordFromFile -PasswordAsSecure:$PasswordAsSecure -PasswordFromFile:$PasswordFromFile -Port 587 -SSL:$SSL 75 | EmailOptions -Priority $Priority -DeliveryNotifications Never 76 | EmailSubject -Subject $Subject 77 | } 78 | EmailBody -FontFamily 'Calibri' -Size 15 { 79 | #EmailText -Text "Summary of Active Directory Tests" -Color None, Blue -LineBreak 80 | 81 | EmailTable -DataTable $Results { 82 | EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator eq -Value 'True' -BackgroundColor Green -Color White -Inline -Row 83 | EmailTableCondition -ComparisonType 'string' -Name 'Status' -Operator ne -Value 'True' -BackgroundColor Red -Color White -Inline -Row 84 | } -HideFooter 85 | } 86 | } -AttachSelf -Supress $false 87 | } -------------------------------------------------------------------------------- /Public/Get-TestimoConfiguration.ps1: -------------------------------------------------------------------------------- 1 | function Get-TestimoConfiguration { 2 | <# 3 | .SYNOPSIS 4 | Retrieves Testimo configuration details for Active Directory sources. 5 | 6 | .DESCRIPTION 7 | This function retrieves the Testimo configuration details for Active Directory sources. It organizes the configuration into a structured format for better readability and management. 8 | 9 | .PARAMETER AsJson 10 | Indicates whether to output the configuration as JSON format. 11 | 12 | .PARAMETER FilePath 13 | Specifies the file path to save the configuration. 14 | 15 | .EXAMPLE 16 | Example 1 17 | ---------------- 18 | Get-TestimoConfiguration 19 | 20 | .EXAMPLE 21 | Example 2 22 | ---------------- 23 | Get-TestimoConfiguration -AsJson -FilePath "C:\TestimoConfiguration.json" 24 | #> 25 | [CmdletBinding()] 26 | param( 27 | [switch] $AsJson, 28 | [string] $FilePath 29 | ) 30 | $NewConfig = [ordered] @{ } 31 | foreach ($Source in ($Script:TestimoConfiguration.ActiveDirectory).Keys) { 32 | if (-not $Script:TestimoConfiguration['ActiveDirectory'][$Source]) { 33 | Out-Informative -Text "Configuration for $Source is not available. Skipping source $Source" -Level 0 -Status $null -ExtendedValue $null 34 | continue 35 | } 36 | $NewConfig[$Source] = [ordered] @{ } 37 | $NewConfig[$Source]['Enable'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Enable'] 38 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['ExpectedOutput']) { 39 | $NewConfig[$Source]['Source'] = [ordered] @{ } 40 | $NewConfig[$Source]['Source']['ExpectedOutput'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['ExpectedOutput'] 41 | } 42 | 43 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['Parameters']) { 44 | $NewConfig[$Source]['Source'] = [ordered] @{ } 45 | $NewConfig[$Source]['Source']['Parameters'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Source']['Parameters'] 46 | } 47 | $NewConfig[$Source]['Tests'] = [ordered] @{ } 48 | foreach ($Test in $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'].Keys) { 49 | $NewConfig[$Source]['Tests'][$Test] = [ordered] @{ } 50 | $NewConfig[$Source]['Tests'][$Test]['Enable'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Enable'] 51 | $NewConfig[$Source]['Tests'][$Test]['Parameters'] = [ordered] @{ } 52 | 53 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']) { 54 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property']) { 55 | 56 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property']) { 57 | $NewConfig[$Source]['Tests'][$Test]['Parameters']['Property'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['Property'] 58 | } 59 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedValue']) { 60 | $NewConfig[$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedValue'] 61 | } 62 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedCount']) { 63 | $NewConfig[$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['ExpectedCount'] 64 | } 65 | if ($null -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['OperationType']) { 66 | $NewConfig[$Source]['Tests'][$Test]['Parameters']['OperationType'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['OperationType'] 67 | } 68 | #if ($nulle -ne $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['PropertyExtendedValue']) { 69 | # $NewConfig[$Source]['Tests'][$Test]['Parameters']['PropertyExtendedValue'] = $Script:TestimoConfiguration['ActiveDirectory'][$Source]['Tests'][$Test]['Parameters']['PropertyExtendedValue'] 70 | #} 71 | } 72 | } 73 | } 74 | } 75 | if ($FilePath) { 76 | $NewConfig | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath $FilePath 77 | return 78 | } 79 | if ($AsJSON) { 80 | return $NewConfig | ConvertTo-Json -Depth 10 81 | } 82 | return $NewConfig 83 | } -------------------------------------------------------------------------------- /Public/Get-TestimoSources.ps1: -------------------------------------------------------------------------------- 1 | function Get-TestimoSources { 2 | <# 3 | .SYNOPSIS 4 | Retrieves information about Testimo data sources. 5 | 6 | .DESCRIPTION 7 | This function retrieves detailed information about Testimo data sources based on the provided source names. It returns information such as source name, scope, tests available, area, category, tags, severity, risk level, description, resolution, and resources. 8 | 9 | .PARAMETER Sources 10 | Specifies an array of source names to retrieve information for. 11 | 12 | .PARAMETER SourcesOnly 13 | Indicates whether to return only the list of source names without additional details. 14 | 15 | .PARAMETER Enabled 16 | Indicates whether to retrieve information only for enabled sources. 17 | 18 | .PARAMETER Advanced 19 | Indicates whether to include advanced details for each source. 20 | 21 | .EXAMPLE 22 | Example 1 23 | ---------------- 24 | Get-TestimoSources -Sources "DomainComputersUnsupported", "DomainDHCPAuthorized" 25 | 26 | .EXAMPLE 27 | Example 2 28 | ---------------- 29 | Get-TestimoSources -Sources "DomainComputersUnsupported", "DomainDHCPAuthorized" -Advanced 30 | 31 | #> 32 | [CmdletBinding()] 33 | param( 34 | [string[]] $Sources, 35 | [switch] $SourcesOnly, 36 | [switch] $Enabled, 37 | [switch] $Advanced 38 | ) 39 | if (-not $Sources) { 40 | $Sources = $Script:TestimoConfiguration.ActiveDirectory.Keys 41 | } 42 | if ($SourcesOnly) { 43 | return $Sources 44 | } 45 | foreach ($S in $Sources) { 46 | $Object = [ordered]@{ 47 | Source = $S 48 | Scope = $Script:TestimoConfiguration.ActiveDirectory[$S].Scope 49 | Name = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Name 50 | Tests = $Script:TestimoConfiguration.ActiveDirectory[$S].Tests.Keys 51 | } 52 | $Object['Area'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Area 53 | $Object['Category'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Category 54 | $Object['Tags'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Tags 55 | $Object['Severity'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Severity 56 | $Object['RiskLevel'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.RiskLevel 57 | $Object['Description'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Description 58 | $Object['Resolution'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Resolution 59 | $Object['Resources'] = $Script:TestimoConfiguration.ActiveDirectory[$S].Source.Details.Resources 60 | if ($Advanced) { 61 | $Object['Advanced'] = $Script:TestimoConfiguration.ActiveDirectory[$S] 62 | } 63 | [PSCustomObject] $Object 64 | } 65 | } -------------------------------------------------------------------------------- /Public/Import-PrivateModule.ps1: -------------------------------------------------------------------------------- 1 | function Import-PrivateModule { 2 | <# 3 | .SYNOPSIS 4 | Imports a private module by name. 5 | 6 | .DESCRIPTION 7 | This function imports a private module by name. It attempts to import the module specified by the given name. If the module is not found in the standard module directories, it looks for the module in the loaded modules. 8 | 9 | .PARAMETER Name 10 | Specifies the name of the private module to import. 11 | 12 | .PARAMETER Portable 13 | Indicates whether the module should be imported as portable. 14 | 15 | .EXAMPLE 16 | Example 1 17 | ---------------- 18 | Import-PrivateModule -Name "MyPrivateModule" 19 | 20 | .EXAMPLE 21 | Example 2 22 | ---------------- 23 | Import-PrivateModule -Name "MyPrivateModule" -Portable 24 | 25 | #> 26 | [cmdletBinding()] 27 | param( 28 | [string] $Name, 29 | [switch] $Portable 30 | ) 31 | try { 32 | $ADModule = Import-Module -PassThru -Name $Name -ErrorAction Stop 33 | } catch { 34 | if ($_.Exception.Message -like '*was not loaded because no valid module file was found in any module directory*') { 35 | $Module = Get-Module -Name $Name 36 | #$PSD1 = -join ($Name, ".psd1") 37 | #$Module = [io.path]::Combine($Module.ModuleBase, $PSD1) 38 | if ($Module) { 39 | $ADModule = Import-Module $Module -PassThru 40 | } 41 | } 42 | } 43 | $ADModule 44 | } -------------------------------------------------------------------------------- /Testimo.Tests.ps1: -------------------------------------------------------------------------------- 1 | $PSVersionTable.PSVersion 2 | 3 | $ModuleName = (Get-ChildItem $PSScriptRoot\*.psd1).BaseName 4 | $RequiredModules = @( 5 | 'PSSharedGoods' 6 | 'Pester' 7 | ) 8 | foreach ($_ in $RequiredModules) { 9 | if ($null -eq (Get-Module -ListAvailable $_)) { 10 | Write-Warning "$ModuleName - Downloading $_ from PSGallery" 11 | Install-Module -Name $_ -Repository PSGallery -Force -SkipPublisherCheck 12 | } 13 | Import-Module $_ -Force 14 | } 15 | Import-Module $PSScriptRoot\Testimo.psd1 -Force 16 | 17 | $result = Invoke-Pester -Path $PSScriptRoot\Tests #-Output Detailed 18 | 19 | if ($result.FailedCount -gt 0) { 20 | throw "$($result.FailedCount) tests failed." 21 | } -------------------------------------------------------------------------------- /Testimo.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | AliasesToExport = @('Test-ImoAD', 'Test-IMO', 'Testimo') 3 | Author = 'Przemyslaw Klys' 4 | CmdletsToExport = @() 5 | CompanyName = 'Evotec' 6 | CompatiblePSEditions = @('Desktop') 7 | Copyright = '(c) 2011 - 2025 Przemyslaw Klys @ Evotec. All rights reserved.' 8 | Description = 'Testimo is Powershell module that tests Active Directory against specific set of tests.' 9 | FunctionsToExport = @('Compare-Testimo', 'Get-TestimoConfiguration', 'Get-TestimoSources', 'Import-PrivateModule', 'Invoke-Testimo') 10 | GUID = '0c1b99de-55ac-4410-8cb5-e689ff3be39b' 11 | ModuleVersion = '0.0.91' 12 | PowerShellVersion = '5.1' 13 | PrivateData = @{ 14 | PSData = @{ 15 | IconUri = 'https://evotec.xyz/wp-content/uploads/2019/08/Testimo.png' 16 | ProjectUri = 'https://github.com/EvotecIT/Testimo' 17 | Tags = @('Windows', 'ActiveDirectory', 'AD', 'Infrastructure', 'Testing', 'Checks', 'Audits', 'Checklist', 'Validation') 18 | } 19 | } 20 | RequiredModules = @(@{ 21 | Guid = 'ee272aa8-baaa-4edf-9f45-b6d6f7d844fe' 22 | ModuleName = 'PSSharedGoods' 23 | ModuleVersion = '0.0.303' 24 | }, @{ 25 | Guid = '5df72a79-cdf6-4add-b38d-bcacf26fb7bc' 26 | ModuleName = 'PSEventViewer' 27 | ModuleVersion = '2.4.3' 28 | }, @{ 29 | Guid = 'a7bdf640-f5cb-4acf-9de0-365b322d245c' 30 | ModuleName = 'PSWriteHTML' 31 | ModuleVersion = '1.28.0' 32 | }, @{ 33 | Guid = 'f7d4c9e4-0298-4f51-ad77-e8e3febebbde' 34 | ModuleName = 'GPOZaurr' 35 | ModuleVersion = '1.1.9' 36 | }, @{ 37 | Guid = '0b0ba5c5-ec85-4c2b-a718-874e55a8bc3f' 38 | ModuleName = 'PSWriteColor' 39 | ModuleVersion = '1.0.1' 40 | }, @{ 41 | Guid = '9fc9fd61-7f11-4f4b-a527-084086f1905f' 42 | ModuleName = 'ADEssentials' 43 | ModuleVersion = '0.0.230' 44 | }) 45 | RootModule = 'Testimo.psm1' 46 | } -------------------------------------------------------------------------------- /Testimo.psm1: -------------------------------------------------------------------------------- 1 | #Get public and private function definition files. 2 | $Public = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 3 | $Private = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction SilentlyContinue -Recurse ) 4 | 5 | #Dot source the files 6 | Foreach ($import in @($Public + $Private)) { 7 | Try { 8 | . $import.fullname 9 | # Write-Warning $import.fullname 10 | } Catch { 11 | Write-Error -Message "Failed to import function $($import.fullname): $_" 12 | } 13 | } 14 | 15 | Export-ModuleMember -Function * -Alias '*' -------------------------------------------------------------------------------- /Tests/VerifySources.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Testimo Sources' { 2 | $TestimoSources = Get-TestimoSources -Advanced 3 | foreach ($Source in $TestimoSources) { 4 | It "Source $($Source.Source) should contain Enable key and be enabled or disabled" -TestCases @{ Source = $Source } { 5 | $Source.Advanced.Enable | Should -BeIn @($True, $False) 6 | } 7 | It "Source $($Source.Source) should contain Source Key" -TestCases @{ Source = $Source } { 8 | $Source.Advanced.Keys | Should -Contain 'Source' 9 | } 10 | It "Source $($Source.Source) should contain 6 in Source Details" -TestCases @{ Source = $Source } { 11 | $Keys = @( 12 | 'Area' 13 | 'Category' 14 | 'Severity' 15 | 'RiskLevel' 16 | 'Description' 17 | 'Resolution' 18 | 'Resources' 19 | ) 20 | $Source.Advanced.Source.Details.Keys | Sort-Object | Should -Be ($Keys | Sort-Object) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Tests/VerifyTestimoConfiguration.Tests.ps1: -------------------------------------------------------------------------------- 1 | Describe 'Testimo Configuration for Forest' { 2 | # Preparations 3 | $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru 4 | $TestimoConfiguration = & $ImportedModule { 5 | $Script:TestimoConfiguration 6 | } 7 | 8 | foreach ($Key in $TestimoConfiguration['Forest'].Keys) { 9 | $PSDefaultParameterValues = @{ 10 | "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration } 11 | } 12 | It -Name "Test Source $Key should not be NULL" { 13 | $TestimoConfiguration['Forest'].$Key | Should -Not -Be $Null 14 | } 15 | It -Name "Test Source $Key should contain Enable" { 16 | $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Enable' 17 | } 18 | It -Name "Test Source $Key should contain Source" { 19 | $TestimoConfiguration['Forest'].$Key.Keys | Should -Contain 'Source' 20 | } 21 | } 22 | } 23 | 24 | Describe 'Testimo Configuration for Domains' { 25 | # Preparations 26 | $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru 27 | $TestimoConfiguration = & $ImportedModule { 28 | $Script:TestimoConfiguration 29 | } 30 | 31 | foreach ($Key in $TestimoConfiguration['Domain'].Keys) { 32 | $PSDefaultParameterValues = @{ 33 | "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration } 34 | } 35 | It -Name "Test Source $Key should not be NULL" { 36 | $TestimoConfiguration['Domain'].$Key | Should -Not -Be $Null 37 | } 38 | It -Name "Test Source $Key should contain Enable" { 39 | $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Enable' 40 | } 41 | It -Name "Test Source $Key should contain Source" { 42 | $TestimoConfiguration['Domain'].$Key.Keys | Should -Contain 'Source' 43 | } 44 | } 45 | } 46 | 47 | Describe 'Testimo Configuration for DomainControllers' { 48 | # Preparations 49 | $ImportedModule = Import-Module $PSScriptRoot\..\Testimo.psd1 -Force -PassThru 50 | $TestimoConfiguration = & $ImportedModule { 51 | $Script:TestimoConfiguration 52 | } 53 | 54 | foreach ($Key in $TestimoConfiguration['DomainControllers'].Keys) { 55 | $PSDefaultParameterValues = @{ 56 | "It:TestCases" = @{ Key = $Key; TestimoConfiguration = $TestimoConfiguration } 57 | } 58 | It -Name "Test Source $Key should not be NULL" { 59 | $TestimoConfiguration['DomainControllers'].$Key | Should -Not -Be $Null 60 | } 61 | It -Name "Test Source $Key should contain Enable" { 62 | $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Enable' 63 | } 64 | It -Name "Test Source $Key should contain Source" { 65 | $TestimoConfiguration['DomainControllers'].$Key.Keys | Should -Contain 'Source' 66 | } 67 | } 68 | } --------------------------------------------------------------------------------