From 7c9ab4c8ed466be5714c38ce1c6ef8251f4bc545 Mon Sep 17 00:00:00 2001 From: Snozz Date: Sun, 7 Dec 2025 17:45:10 -0700 Subject: [PATCH 01/11] Initial build of AD DS tests. --- powershell/Maester.psd1 | 12 +- powershell/Maester.psm1 | 3 + powershell/internal/Get-MtSkippedReason.ps1 | 1 + powershell/public/Add-MtTestResultDetail.ps1 | 2 +- powershell/public/Clear-MtAdCache.ps1 | 26 +++ powershell/public/Connect-Maester.ps1 | 55 ++++- powershell/public/Set-MtAdCache.ps1 | 184 ++++++++++++++++ .../maester/ad/Test-MtAdComputerContainer.md | 2 + .../maester/ad/Test-MtAdComputerContainer.ps1 | 142 ++++++++++++ .../maester/ad/Test-MtAdComputerCreatorSid.md | 2 + .../ad/Test-MtAdComputerCreatorSid.ps1 | 105 +++++++++ .../public/maester/ad/Test-MtAdComputerDns.md | 2 + .../maester/ad/Test-MtAdComputerDns.ps1 | 159 ++++++++++++++ .../ad/Test-MtAdComputerDomainController.md | 2 + .../ad/Test-MtAdComputerDomainController.ps1 | 102 +++++++++ .../maester/ad/Test-MtAdComputerKerberos.md | 2 + .../maester/ad/Test-MtAdComputerKerberos.ps1 | 179 +++++++++++++++ .../ad/Test-MtAdComputerOperatingSystem.md | 2 + .../ad/Test-MtAdComputerOperatingSystem.ps1 | 145 ++++++++++++ .../ad/Test-MtAdComputerPrimaryGroup.md | 2 + .../ad/Test-MtAdComputerPrimaryGroup.ps1 | 105 +++++++++ .../maester/ad/Test-MtAdComputerService.md | 2 + .../maester/ad/Test-MtAdComputerService.ps1 | 207 ++++++++++++++++++ .../maester/ad/Test-MtAdComputerSidHistory.md | 2 + .../ad/Test-MtAdComputerSidHistory.ps1 | 105 +++++++++ .../maester/ad/Test-MtAdComputerStatus.md | 2 + .../maester/ad/Test-MtAdComputerStatus.ps1 | 158 +++++++++++++ tests/Maester/ad/Test-MtAd.Tests.ps1 | 62 ++++++ 28 files changed, 1762 insertions(+), 10 deletions(-) create mode 100644 powershell/public/Clear-MtAdCache.ps1 create mode 100644 powershell/public/Set-MtAdCache.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerContainer.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerCreatorSid.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerDns.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerDns.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerDomainController.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerKerberos.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerService.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerService.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerSidHistory.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdComputerStatus.md create mode 100644 powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 create mode 100644 tests/Maester/ad/Test-MtAd.Tests.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index d00448cf9..a99f9f75a 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -184,7 +184,17 @@ 'Test-MtXspmCriticalCredsOnDevicesWithNonCriticalAccounts', 'Test-MtXspmPublicRemotelyExploitableHighExposureDevices', 'Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices', - 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices' + 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices', + 'Test-MtAdComputerContainer.ps1', + 'Test-MtAdComputerCreatorSid.ps1', + 'Test-MtAdComputerDns.ps1', + 'Test-MtAdComputerDomainController.ps1', + 'Test-MtAdComputerKerberos.ps1', + 'Test-MtAdComputerOperatingSystem.ps1', + 'Test-MtAdComputerPrimaryGroup.ps1', + 'Test-MtAdComputerService.ps1', + 'Test-MtAdComputerSidHistory.ps1', + 'Test-MtAdComputerStatus.ps1' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/powershell/Maester.psm1 b/powershell/Maester.psm1 index 2852c10f4..c3fc75810 100644 --- a/powershell/Maester.psm1 +++ b/powershell/Maester.psm1 @@ -14,6 +14,9 @@ ## Initialize Module Variables ## Update Clear-ModuleVariable function in internal/Clear-ModuleVariable.ps1 if you add new variables here $__MtSession = @{ + AdCredential = $null + AdServer = $null + AdCache = @{} GraphCache = @{} GraphBaseUri = $null TestResultDetail = @{} diff --git a/powershell/internal/Get-MtSkippedReason.ps1 b/powershell/internal/Get-MtSkippedReason.ps1 index d4185835a..1126a8a10 100644 --- a/powershell/internal/Get-MtSkippedReason.ps1 +++ b/powershell/internal/Get-MtSkippedReason.ps1 @@ -9,6 +9,7 @@ function Get-MtSkippedReason { ) switch($SkippedBecause){ + "NotConnectedActiveDirectory" { "Not connected to Active Directory. See [Connecting to Active Directory](https://maester.dev/docs/connect-maester/#todo)"; break} "NotConnectedAzure" { "Not connected to Azure. See [Connecting to Azure](https://maester.dev/docs/connect-maester/#connect-to-azure-exchange-online-and-teams)"; break} "NotConnectedExchange" { "Not connected to Exchange Online. See [Connecting to Exchange Online](https://maester.dev/docs/connect-maester/#connect-to-azure-exchange-online-and-teams)"; break} "NotConnectedSecurityCompliance" { "Not connected to Security & Compliance. See [Connecting to Security & Compliance](https://maester.dev/docs/connect-maester/#connect-to-azure-exchange-online-and-teams)"; break} diff --git a/powershell/public/Add-MtTestResultDetail.ps1 b/powershell/public/Add-MtTestResultDetail.ps1 index a9f7e5835..dfb5f08ff 100644 --- a/powershell/public/Add-MtTestResultDetail.ps1 +++ b/powershell/public/Add-MtTestResultDetail.ps1 @@ -74,7 +74,7 @@ function Add-MtTestResultDetail { # Common reasons for why the test was skipped. [Parameter(Mandatory = $false)] - [ValidateSet('NotConnectedAzure', 'NotConnectedExchange', 'NotConnectedGraph', 'NotDotGovDomain', 'NotLicensedEntraIDP1', 'NotConnectedSecurityCompliance', 'NotConnectedTeams', + [ValidateSet('NotConnectedActiveDirectory', 'NotConnectedAzure', 'NotConnectedExchange', 'NotConnectedGraph', 'NotDotGovDomain', 'NotLicensedEntraIDP1', 'NotConnectedSecurityCompliance', 'NotConnectedTeams', 'NotLicensedEntraIDP2', 'NotLicensedEntraIDGovernance', 'NotLicensedEntraWorkloadID', 'NotLicensedExoDlp', "LicensedEntraIDPremium", 'NotSupported', 'Custom', 'NotLicensedMdo', 'NotLicensedMdoP2', 'NotLicensedMdoP1', 'NotLicensedAdvAudit', 'NotLicensedEop', 'Error', 'NotSupportedAppPermission', 'LimitedPermissions', 'NotLicensedDefenderXDR', 'NotLicensedCustomerLockbox','NotAuthorized', 'NotLicensedIntune' diff --git a/powershell/public/Clear-MtAdCache.ps1 b/powershell/public/Clear-MtAdCache.ps1 new file mode 100644 index 000000000..cab4ab830 --- /dev/null +++ b/powershell/public/Clear-MtAdCache.ps1 @@ -0,0 +1,26 @@ +<# +.SYNOPSIS + Resets the local cache of AD lookups. Use this if you need to force a refresh of the cache in the current session. + +.DESCRIPTION + By default all AD queries are cached and re-used for the duration of the session. + + Use this function to clear the cache and force a refresh of the data. + +.EXAMPLE + Clear-MtAdCache + + This example clears the cache of all AD queries. + +.LINK + https://maester.dev/docs/commands/Clear-MtAdCache +#> +function Clear-MtAdCache { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification='Setting module level variable')] + [CmdletBinding()] + param() + + Write-Verbose -Message "Clearing the results cached from DNS lookups in this session" + + $__MtSession.AdCache = @{} +} \ No newline at end of file diff --git a/powershell/public/Connect-Maester.ps1 b/powershell/public/Connect-Maester.ps1 index 16ed5aa49..2ce58c37f 100644 --- a/powershell/public/Connect-Maester.ps1 +++ b/powershell/public/Connect-Maester.ps1 @@ -103,8 +103,14 @@ [ValidateSet('TeamsChina', 'TeamsGCCH', 'TeamsDOD')] [string]$TeamsEnvironmentName = $null, #ToValidate: Don't use this parameter, this is the default. + # The AD Server to connect to. + [string]$AdServer = $null, + + # The AD Credential object. + [pscredential]$AdCredential = $null, + # The services to connect to such as Azure and EXO. Default is Graph. - [ValidateSet('All', 'Azure', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'Teams')] + [ValidateSet('ActiveDirectory', 'All', 'Azure', 'Cloud', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'Teams')] [string[]]$Service = 'Graph', # The Tenant ID to connect to, if not specified the sign-in user's default tenant is used. @@ -116,11 +122,44 @@ $__MtSession.Connections = $Service - $OrderedImport = Get-ModuleImportOrder -Name @('Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'MicrosoftTeams') + $OrderedImport = Get-ModuleImportOrder -Name @('ActiveDirectory', 'Az.Accounts', 'ExchangeOnlineManagement', 'Microsoft.Graph.Authentication', 'MicrosoftTeams') switch ($OrderedImport.Name) { + 'ActiveDirectory' { + if ($Service -contains 'ActiveDirectory' -or $Service -contains 'All') { + Write-Verbose 'Connecting to Active Directory' + $adWarning = @() + try { + if($AdServer -and $AdCredential){ + $rootDse = Get-ADRootDSE -Server $AdServer -Credential $AdCredential -WarningAction SilentlyContinue -WarningVariable adWarning + $__MtSession.AdServer = $AdServer + $__MtSession.AdCredential = $AdCredential + }elseif($AdServer){ + $rootDse = Get-ADRootDSE -Server $AdServer -WarningAction SilentlyContinue -WarningVariable adWarning + $__MtSession.AdServer = $AdServer + $__MtSession.AdCredential = $null + }elseif($AdCredential){ + $rootDse = Get-ADRootDSE -Credential $AdCredential -WarningAction SilentlyContinue -WarningVariable adWarning + $__MtSession.AdServer = $rootDse.dnsHostName + $__MtSession.AdCredential = $AdCredential + }else{ + $rootDse = Get-ADRootDSE -WarningAction SilentlyContinue -WarningVariable adWarning + $__MtSession.AdServer = $rootDse.dnsHostName + $__MtSession.AdCredential = $null + } + if ($adWarning.Count -gt 0) { + foreach ($warning in $adWarning) { + Write-Verbose $warning.Message + } + } + } catch [Management.Automation.CommandNotFoundException] { + Write-Host "`nThe ActiveDirectory PowerShell module is not installed. Please install the module using the following information https://learn.microsoft.com/en-us/troubleshoot/windows-server/system-management-components/remote-server-administration-tools#rsat-for-windows-10-version-1809-or-later-versions" -ForegroundColor Red + } + } + } + 'Az.Accounts' { - if ($Service -contains 'Azure' -or $Service -contains 'All') { + if ($Service -contains 'Azure' -or $Service -contains 'All' -or $Service -contains 'Cloud') { Write-Verbose 'Connecting to Microsoft Azure' try { $azWarning = @() @@ -143,7 +182,7 @@ 'ExchangeOnlineManagement' { $ExchangeModuleNotInstalledWarningShown = $false - if ($Service -contains 'ExchangeOnline' -or $Service -contains 'All') { + if ($Service -contains 'ExchangeOnline' -or $Service -contains 'All' -or $Service -contains 'Cloud') { Write-Verbose 'Connecting to Microsoft Exchange Online' try { if ($UseDeviceCode -and $PSVersionTable.PSEdition -eq 'Desktop') { @@ -161,7 +200,7 @@ } } - if ($Service -contains 'SecurityCompliance' -or $Service -contains 'All') { + if ($Service -contains 'SecurityCompliance' -or $Service -contains 'All' -or $Service -contains 'Cloud') { $Environments = @{ 'O365China' = @{ ConnectionUri = 'https://ps.compliance.protection.partner.outlook.cn/powershell-liveid' @@ -189,7 +228,7 @@ } } Write-Verbose 'Connecting to Microsoft Security & Compliance PowerShell' - if ($Service -notcontains 'ExchangeOnline' -and $Service -notcontains 'All') { + if ($Service -notcontains 'ExchangeOnline' -and $Service -notcontains 'All' -or $Service -notcontains 'Cloud') { Write-Host "`nThe Security & Compliance module is dependent on the Exchange Online module. Please include ExchangeOnline when specifying the services.`nFor more information see https://learn.microsoft.com/en-us/powershell/exchange/connect-to-scc-powershell" -ForegroundColor Red } else { if ($UseDeviceCode) { @@ -236,7 +275,7 @@ } 'Microsoft.Graph.Authentication' { - if ($Service -contains 'Graph' -or $Service -contains 'All') { + if ($Service -contains 'Graph' -or $Service -contains 'All' -or $Service -contains 'Cloud') { Write-Verbose 'Connecting to Microsoft Graph' try { @@ -273,7 +312,7 @@ } 'MicrosoftTeams' { - if ($Service -contains 'Teams' -or $Service -contains 'All') { + if ($Service -contains 'Teams' -or $Service -contains 'All' -or $Service -contains 'Cloud') { Write-Verbose 'Connecting to Microsoft Teams' try { if ($UseDeviceCode) { diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 new file mode 100644 index 000000000..ff55a4ee8 --- /dev/null +++ b/powershell/public/Set-MtAdCache.ps1 @@ -0,0 +1,184 @@ +<# +.SYNOPSIS + Sets the local cache of AD lookups. + +.DESCRIPTION + By default all AD queries are cached and re-used for the duration of the session. + + Use this function to set the cache. + +.EXAMPLE + Set-MtAdCache + + This example sets the cache of AD queries. + +.LINK + https://maester.dev/docs/commands/Set-MtAdCache +#> +function Set-MtAdCache { + [CmdletBinding()] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential, + [ValidateSet('All', 'Computers')] + [string[]]$Objects = 'All' + ) + + if($Server){ + $PSDefaultParameterValues.Add("Get-Ad*:Server",$Server) + } + + if($Credential){ + $PSDefaultParameterValues.Add("Get-Ad*:Credential",$Credential) + } + + $primaryGroupIds = @(515,516,521) + $dcPrimaryGroupIds = @(516,521) + + $rootDse = try{ + Get-AdRootDSE + }catch{ + Write-Error $_ + return $null + } + + $configurationNamingContext = $rootDse.configurationNamingContext + $ADObjectDirectoryService = @{ + Identity = "CN=Directory Service,CN=Windows NT,CN=Services,$configurationNamingContext" + Properties = "*" + } + $directoryService = try{ + Get-ADObject @ADObjectDirectoryService + }catch{ + Write-Error $_ + return $null + } + + $HostSpnAlias = (($directoryService).spnmappings | Where-Object { + $_ -like "host=*" + }) -replace "host=" -split "," + + $Thresholds = @{ + DormantThresholdInDays = 90 + DormantDate = $null + ExpiredThresholdInDays = 180 + ExpiredDate = $null + StaleThresholdInDays = 30 + StaleDate = $null + } + $Thresholds.DormantDate = ((Get-Date).AddDays(-$Thresholds.DormantThresholdInDays)).Date + $Thresholds.ExpiredDate = ((Get-Date).AddDays(-$Thresholds.ExpiredThresholdInDays)).Date + $Thresholds.StaleDate = ((Get-Date).AddDays(-$Thresholds.StaleThresholdInDays)).Date + + $__MtSession.AdCache = @{ + RootDSE = $rootDse + ConfigurationNamingContext = $configurationNamingContext + DirectoryServiceConfigObj = $directoryService + Thresholds = $Thresholds + HostSpnAlias = @($HostSpnAlias) + PrimaryGroupIds = $primaryGroupIds + DomainControllerPgids = $dcPrimaryGroupIds + AdComputers = $__MtSession.AdCache.AdComputers + } + + if($Objects -contains "Computers" -or $Objects -contains "All"){ + $computers = try{ + Get-ADComputer -Filter * -Properties * + }catch{ + Write-Error $_ + return $null + } + + $__MtSession.AdCache.AdComputers = @{ + SetFlag = $true + Computers = @($computers) + Data = @{ + ComputersCount = $(($computers | Measure-Object).Count) + EnabledComputers = @() + EnabledComputersCount = $null + DisabledComputers = @() + DisabledComputersCount = $null + DormantComputers = @() + DormantComputersCount = $null + ExpiredComputers = @() + ExpiredComputersCount = $null + StaleComputers = @() + StaleComputersCount = $null + StaleComputersRatio = $null + NonPgIdCoumputers = @() + NonPgIdCoumputersCount = $null + SidHistoryComputers = @() + SidHistoryComputersCount = $null + ContainerComputers = @() + ContainerComputersCount = $null + BaseDns = @() + BaseDnCount = $null + BaseDnAvg = $null + LowBaseDns = @() + LowBaseDnsCount = $null + CreatorSidComputers = @() + CreatorSidComputersCount = $null + DomainControllers = @() + DomainControllersCount = $null + ServiceClasses = @() + ServiceClassesCount = $null + ServiceClassesComputers = @() + ServiceClassesComputersCount = $null + HostBypassComputers = @() + HostBypassComputersCount = $null + ServiceHosts = @() + ServiceHostsCount = $null + ServiceHostsComputers = @() + ServiceHostsComputersCount = $null + ServiceDnsBypassComputers = @() + ServiceDnsBypassComputersCount = $null + DnsZones = @() + DnsZonesCount = $null + DnsZoneAvg = $null + LowDnsZones = @() + LowDnsZonesCount = $null + NoDnsComputers = @() + NoDnsComputersCount = $null + DnsOverlapComputers = @() + DnsOverlapComputersCount = $null + UnconstrainedComputers = @() + UnconstrainedComputersCount = $null + KcdComputers = @() + KcdComputersCount = $null + S4U2SelfComputers = @() + S4U2SelfComputersCount = $null + RbcdComputers = @() + RbcdComputersCount = $null + MissingSpnsComputers = @() + MissingSpnsComputersCount = $null + OperatingSystems = @() + OperatingSystemsCount = $null + NoOperatingSystem = $() + NoOperatingSystemCount = $null + OperatingSystemAvg = $null + LowOperatingSystem = $() + LowOperatingSystemCount = $null + DisabledComputersRatio = $null + DormantComputersRatio = $null + ExpiredComputersRatio = $null + NonPgIdComputersRatio = $null + SidHistoryComputersRatio = $null + ContainerComputersRatio = $null + CreatorSidComputersRatio = $null + ServiceClassesComputersRatio = $null + HostBypassComputersRatio = $null + ServiceHostsRatio = $null + ServiceHostsComputersRatio = $null + ServiceDnsBypassComputersRatio = $null + NoDnsComputersRatio = $null + DnsOverlapComputersRatio = $null + UnconstrainedComputersRatio = $null + KcdComputersRatio = $null + S4U2SelfComputersRatio = $null + RbcdComputersRatio = $null + MissingSpnsComputersRatio = $null + NoOperatingSystemRatio = $null + } + } + } +} \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerContainer.md b/powershell/public/maester/ad/Test-MtAdComputerContainer.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerContainer.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 new file mode 100644 index 000000000..92cbab3ce --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 @@ -0,0 +1,142 @@ +<# +.SYNOPSIS + Checks AD Computer Containers + +.DESCRIPTION + Identifies if computer containers are misused + +.EXAMPLE + Test-MtAdComputerContainer + + Returns true if AD Computer containers are clean + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerContainer +#> +function Test-MtAdComputerContainer { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.ContainerComputers = $AdObjects.Computers | Where-Object { + $_.DistinguishedName -like "*CN=Computers*" + } + $AdObjects.Data.ContainerComputersCount = ($AdObjects.Data.ContainerComputers | Measure-Object).Count + $AdObjects.Data.ContainerComputersRatio = try{ + $AdObjects.Data.ContainerComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.BaseDns = $AdObjects.Computers | Select-Object @{ + Name = "BaseDN" + Expression = {($_.DistinguishedName).Substring(($_.DistinguishedName).IndexOf(",")+1)} + } | Group-Object BaseDN + $AdObjects.Data.BaseDnCount = ($AdObjects.Data.BaseDns | Measure-Object).Count + $AdObjects.Data.BaseDnAvg = try{ + [Math]::Round($AdObjects.Data.ComputersCount / $AdObjects.Data.BaseDnCount,2) + }catch{0} + $AdObjects.Data.LowBaseDns = $AdObjects.Data.BaseDns | Where-Object { + $_.Count -lt $AdObjects.Data.BaseDnAvg + } + $AdObjects.Data.LowBaseDnsCount = ($AdObjects.Data.LowBaseDns | Measure-Object).Count + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + ContainerComputers = @{ + Name = "Computers within the Computers Container" + Value = $AdObjects.Data.ContainerComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects within the Computers Container" + Status = $null + } + BaseDns = @{ + Name = "Distinct Containers with Computers" + Value = $AdObjects.Data.BaseDnCount + Threshold = 5 + Indicator = "<" + Description = "Number of containers that hold computer objects" + Status = $null + } + ComputerCnDensity = @{ + Name = "Density of Computers in Containers" + Value = $AdObjects.Data.BaseDnAvg + Threshold = 1 + Indicator = ">" + Description = "Average number of computers per container" + Status = $null + } + LowBaseDns = @{ + Name = "Containers with below average number of computers" + Value = $AdObjects.Data.LowBaseDnsCount + Threshold = 1 + Indicator = "<=" + Description = "Number of containers that hold fewer than the average number of computers in containers" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.md b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 new file mode 100644 index 000000000..051f7b18e --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + Checks for computers with Creator SID + +.DESCRIPTION + Identifies if computers are joining through non-admin users + +.EXAMPLE + Test-MtAdComputerCreatorSid + + Returns true if AD Computer creator SID is not in use + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerCreatorSid +#> +function Test-MtAdComputerCreatorSid { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.CreatorSidComputers = $AdObjects.Computers | Where-Object { + $_.'ms-ds-CreatorSid' -ne "" + } + $AdObjects.Data.CreatorSidComputers = ($AdObjects.Data.CreatorSidComputers | Measure-Object).Count + $AdObjects.Data.CreatorSidComputersRatio = try{ + $AdObjects.Data.CreatorSidComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + CreatorSidComputers = @{ + Name = "Computers with a Creator SID" + Value = $AdObjects.Data.CreatorSidComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects with a Creator SID set" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerDns.md b/powershell/public/maester/ad/Test-MtAdComputerDns.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 new file mode 100644 index 000000000..aee6b92fc --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 @@ -0,0 +1,159 @@ +<# +.SYNOPSIS + Checks computer DNS + +.DESCRIPTION + Identifies issues with computer DNS registration + +.EXAMPLE + Test-MtAdComputerDns + + Returns true if AD Computer DNS state is clean + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerDns +#> +function Test-MtAdComputerDns { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.DnsZones = ($AdObjects.Computers | Select-Object @{ + Name = "DnsZone" + Expression = {($_.DNSHostName) -replace "^[^.]*\."} + }).DnsZone | Group-Object | Where-Object {$_.Name -ne ""} + $AdObjects.Data.DnsZonesCount = ($AdObjects.Data.DnsZones | Measure-Object).Count + $AdObjects.Data.DnsZoneAvg = try{ + [Math]::Round($AdObjects.Data.ComputersCount / $AdObjects.Data.DnsZonesCount,2) + }catch{0} + $AdObjects.Data.LowDnsZones = $AdObjects.Data.DnsZones | Where-Object { + $_.Count -lt $AdObjects.Data.DnsZoneAvg + } + $AdObjects.Data.LowDnsZonesCount = ($AdObjects.Data.LowDnsZones | Measure-Object).Count + + $AdObjects.Data.NoDnsComputers = $AdObjects.Computers | Where-Object { + $null -eq $_.DNSHostName + } + $AdObjects.Data.NoDnsComputersCount = ($AdObjects.Data.NoDnsComputers | Measure-Object).Count + $AdObjects.Data.NoDnsComputersRatio = try{ + $AdObjects.Data.NoDnsComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.DnsOverlapComputers = $AdObjects.Computers | Group-Object DNSHostName | Where-Object { + $_.Count -gt 1 -and + $_.Name -ne "" + } + $AdObjects.Data.DnsOverlapComputersCount = ($AdObjects.Data.DnsOverlapComputers | Measure-Object).Count + $AdObjects.Data.DnsOverlapComputersRatio = try{ + $AdObjects.Data.DnsOverlapComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + DnsZones = @{ + Name = "DNS Zones observed" + Value = $AdObjects.Data.DnsZonesCount + Threshold = 0.00 + Indicator = ">" + Description = "Discrete number of DNS Zones observed in use" + Status = $null + } + NoDnsComputers = @{ + Name = "Computers without a DNS Hostname set" + Value = $AdObjects.Data.NoDnsComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects without a DNS Hostname set" + Status = $null + } + DnsOverlapComputers = @{ + Name = "Computers with overlapping DNS Hostnames" + Value = $AdObjects.Data.DnsOverlapComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects where a DNS Hostname is on more than 1 object" + Status = $null + } + ComputerDnsDensity = @{ + Name = "Density of Computers in DNS Zones" + Value = $AdObjects.Data.DnsZoneAvg + Threshold = 1 + Indicator = ">" + Description = "Average number of computers per container" + Status = $null + } + LowDnsZones = @{ + Name = "DNS Zones with below average number of computers" + Value = $AdObjects.Data.LowDnsZonesCount + Threshold = 1 + Indicator = "<=" + Description = "Number of DNS Zones that hold fewer than the average number of computers in Zones" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerDomainController.md b/powershell/public/maester/ad/Test-MtAdComputerDomainController.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 new file mode 100644 index 000000000..658a89a84 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 @@ -0,0 +1,102 @@ +<# +.SYNOPSIS + Checks domain controllers + +.DESCRIPTION + Identifies number of domain controllers + +.EXAMPLE + Test-MtAdComputerDomainController + + Returns true if AD Computer Domain Controller status + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerDomainController +#> +function Test-MtAdComputerDomainController { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.DomainControllers = $AdObjects.Computers | Where-Object { + $_.primaryGroupId -notin $AdObjects.Data.DomainControllerPgids + } + $AdObjects.Data.DomainControllersCount = ($AdObjects.Data.DomainControllers | Measure-Object).Count + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + DomainControllers = @{ + Name = "Domain controller computers" + Value = $AdObjects.Data.DomainControllersCount + Threshold = 1 + Indicator = ">" + Description = "Number of Domain Controllers" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerKerberos.md b/powershell/public/maester/ad/Test-MtAdComputerKerberos.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 new file mode 100644 index 000000000..e7f346396 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 @@ -0,0 +1,179 @@ +<# +.SYNOPSIS + Checks computer Kerberos configuration + +.DESCRIPTION + Identifies misconfigurations with computer objects and Kerberos + +.EXAMPLE + Test-MtAdComputerKerberos + + Returns true if AD Computer Kerberos is adequate + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerKerberos +#> +function Test-MtAdComputerKerberos { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.UnconstrainedComputers = $AdObjects.Computers | Where-Object { + $_.TrustedForDelegation -and + $_.primaryGroupId -notin $AdObjects.Data.DomainControllerPgids + } + $AdObjects.Data.UnconstrainedComputersCount = ($AdObjects.Data.UnconstrainedComputers | Measure-Object).Count + $AdObjects.Data.UnconstrainedComputersRatio = try{ + $AdObjects.Data.UnconstrainedComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.KcdComputers = $AdObjects.Computers | Where-Object { + -not $_.TrustedToAuthForDelegation -and + $_.'msDS-AllowedToDelegateTo'.Count -ne 0 + } + $AdObjects.Data.KcdComputersCount = ($AdObjects.Data.KcdComputers | Measure-Object).Count + $AdObjects.Data.KcdComputersRatio = try{ + $AdObjects.Data.KcdComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.S4U2SelfComputers = $AdObjects.Computers | Where-Object { + $_.TrustedToAuthForDelegation -and + $_.'msDS-AllowedToDelegateTo'.Count -ne 0 + } + $AdObjects.Data.S4U2SelfComputersCount = ($AdObjects.Data.S4U2SelfComputers | Measure-Object).Count + $AdObjects.Data.S4U2SelfComputersRatio = try{ + $AdObjects.Data.S4U2SelfComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.RbcdComputers = $AdObjects.Computers | Where-Object { + $_.PrincipalsAllowedToDelegateToAccount.Count -ne 0 + } + $AdObjects.Data.RbcdComputersCount = ($AdObjects.Data.RbcdComputers | Measure-Object).Count + $AdObjects.Data.RbcdComputersRatio = try{ + $AdObjects.Data.UnconstrainedComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.MissingSpnsComputers = $AdObjects.Computers | Where-Object { + ($_.TrustedForDelegation -or + $_.TrustedToAuthForDelegation -or + $_.'msDS-AllowedToDelegateTo'.Count -ne 0) -and + $_.servicePrincipalName.Count -eq 0 + } + $AdObjects.Data.MissingSpnsComputersCount = ($AdObjects.Data.MissingSpnsComputers | Measure-Object).Count + $AdObjects.Data.MissingSpnsComputersRatio = try{ + $AdObjects.Data.UnconstrainedComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + UnconstrainedComputers = @{ + Name = "Computers allowing unconstrained delegation" + Value = $AdObjects.Data.UnconstrainedComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects allowing unconstrained Kerberos delegation (Excluding Domain Controllers)" + Status = $null + } + KcdComputers = @{ + Name = "Computers allowing Kerberos Constrained Delegation" + Value = $AdObjects.Data.KcdComputersRatio + Threshold = 0.00 + Indicator = ">=" + Description = "Percent of computer objects allowing Kerberos Constrained Delegation" + Status = $null + } + S4U2SelfComputers = @{ + Name = "Computers allowing S4U2Self Kerberos Constrained Delegation" + Value = $AdObjects.Data.S4U2SelfComputersRatio + Threshold = 0.00 + Indicator = ">=" + Description = "Percent of computer objects allowing Kerberos Constrained Delegation with Protocol Transition (i.e. S4U2Self)" + Status = $null + } + RbcdComputers = @{ + Name = "Computers allowing Kerberos Resource Based Constrained Delegation" + Value = $AdObjects.Data.RbcdComputersRatio + Threshold = 0.00 + Indicator = ">=" + Description = "Percent of computer objects allowing Resource-based Constrained Delegation (i.e., S4U2proxy) Kerberos delegation" + Status = $null + } + MissingSpnsComputers = @{ + Name = "Computers allowing delegation with no SPNs" + Value = $AdObjects.Data.MissingSpnsComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects allowing Kerberos delegation but without any SPNs" + Status = $null + } + } + #endregion + + # TODO + # Check DACLs for restrictions to modify property msDS-AllowedToDelegateTo + # Check DACLs for restrictions to modify property PrincipalsAllowedToDelegateToAccount + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.md b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 new file mode 100644 index 000000000..dfdd0f54b --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 @@ -0,0 +1,145 @@ +<# +.SYNOPSIS + Checks computer operating systems + +.DESCRIPTION + Identifies issues with computer object operating systems + +.EXAMPLE + Test-MtAdComputerOperatingSystem + + Returns true if AD Computer operating systems are clean + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerOperatingSystem +#> +function Test-MtAdComputerOperatingSystem { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.OperatingSystems = $AdObjects.Computers | Group-Object OperatingSystem + $AdObjects.Data.OperatingSystemsCount = ($AdObjects.Data.OperatingSystems | Measure-Object).Count + + $AdObjects.Data.NoOperatingSystem = $AdObjects.Data.OperatingSystems | Where-Object { + $_.Name -eq "" + } + $AdObjects.Data.NoOperatingSystemCount = $AdObjects.Data.NoOperatingSystem.Count + $AdObjects.Data.NoOperatingSystemRatio = try{ + $AdObjects.Data.NoOperatingSystemCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.OperatingSystemAvg = [Math]::Round($AdObjects.Data.ComputersCount / $AdObjects.Data.OperatingSystemsCount,2) + $AdObjects.Data.LowOperatingSystem = $AdObjects.Data.OperatingSystems | Where-Object { + $_.Count -lt $AdObjects.Data.OperatingSystemAvg + } + $AdObjects.Data.LowOperatingSystemCount = ($AdObjects.Data.LowOperatingSystem | Measure-Object).Count + $AdObjects.Data.OperatingSystemAvg = try{ + [Math]::Round($AdObjects.Data.ComputersCount / $AdObjects.Data.OperatingSystemsCount,2) + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + OperatingSystems = @{ + Name = "Operating systems observed" + Value = $AdObjects.Data.OperatingSystemsCount + Threshold = 0.00 + Indicator = ">" + Description = "Discrete number of operating systems observed in use" + Status = $null + } + NoOperatingSystem = @{ + Name = "Computers without an operating system set" + Value = $AdObjects.Data.NoOperatingSystemRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects without an operating system set" + Status = $null + } + OperatingSystemAvg = @{ + Name = "Density of Computer operating systems" + Value = $AdObjects.Data.OperatingSystemAvg + Threshold = 1 + Indicator = ">=" + Description = "Average number of computers per operating system" + Status = $null + } + LowOperatingSystem = @{ + Name = "Operating systems with below average number of computers" + Value = $AdObjects.Data.LowOperatingSystemCount + Threshold = 1 + Indicator = "<=" + Description = "Number of operating systems that with fewer than the average number of computers" + Status = $null + } + } + #endregion + + # TODO + # Check DACLs for restrictions to modify property msDS-AllowedToDelegateTo + # Check DACLs for restrictions to modify property PrincipalsAllowedToDelegateToAccount + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.md b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 new file mode 100644 index 000000000..14933d955 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + Checks the primary group IDs of computers + +.DESCRIPTION + Identifies if any computer objects do not have a normal primary group ID + +.EXAMPLE + Test-MtAdComputerPrimaryGroup + + Returns true if AD Computer primary groups are all default + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerPrimaryGroup +#> +function Test-MtAdComputerPrimaryGroup { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.NonPgIdCoumputers = $AdObjects.Computers | Where-Object { + $_.primaryGroupId -notin $AdObjects.Data.PrimaryGroupIds + } + $AdObjects.Data.NonPgIdCoumputersCount = ($AdObjects.Data.NonPgIdCoumputers | Measure-Object).Count + $AdObjects.Data.NonPgIdComputersRatio = try{ + $AdObjects.Data.NonPgIdCoumputersCount / $AdObjects.Data.ComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + NonPgIdCoumputers = @{ + Name = "Computers with non-default Primary Group ID" + Value = $AdObjects.Data.NonPgIdCoumputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects with a Primary Group ID other than " + ($__MtSession.AdCache.PrimaryGroupIds -join ", ").ToString() + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerService.md b/powershell/public/maester/ad/Test-MtAdComputerService.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerService.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 new file mode 100644 index 000000000..ee9d35eca --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 @@ -0,0 +1,207 @@ +<# +.SYNOPSIS + Checks Computer SPNs + +.DESCRIPTION + Identifies potential misconfiguration of computer SPNs + +.EXAMPLE + Test-MtAdComputerService + + Returns true if AD Computer SPNs are proper + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerService +#> +function Test-MtAdComputerService { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.ServiceClasses = ($AdObjects.Computers | Select-Object @{ + Name = "ServiceClasses" + Expression = {($_.servicePrincipalName) -replace "\/.*$"} + }).ServiceClasses | Group-Object + $AdObjects.Data.ServiceClassesCount = ($AdObjects.Data.ServiceClasses | Measure-Object).Count + + $AdObjects.Data.ServiceClassesComputers = $AdObjects.Computers | Select-Object ObjectGUID,@{ + Name = "ServiceClasses" + Expression = { + (($_.servicePrincipalName) -replace "\/.*$") | Sort-Object -Unique + } + } | Where-Object {$null -ne $_.ServiceClasses} + $AdObjects.Data.ServiceClassesComputersCount = ($AdObjects.Data.ServiceClassesComputers | Measure-Object).Count + $AdObjects.Data.ServiceClassesComputersRatio = try{ + $AdObjects.Data.ServiceClassesComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.HostBypassComputers = $AdObjects.Data.ServiceClassesComputers | Select-Object ObjectGUID,@{ + Name = "ServiceClassesBypassingHost" + Expression = { + $_.serviceClasses | ForEach-Object { + $_ | Where-Object { + $_ -in $AdObjects.Data.HostSpnAlias -and + $_ -ne "host" + } + } + } + } + $AdObjects.Data.HostBypassComputersCount = ($AdObjects.Data.HostBypassComputers | Measure-Object).Count + $AdObjects.Data.HostBypassComputersRatio = try{ + $AdObjects.Data.UnconstrainedComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.ServiceHosts = ($AdObjects.Computers | Select-Object @{ + Name = "ServiceHosts" + Expression = {($_.servicePrincipalName) -replace "^[^\/]*\/" -replace "\/[^\/]*$"} + }).ServiceHosts | Group-Object + $AdObjects.Data.ServiceHostsCount = ($AdObjects.Data.ServiceHosts | Measure-Object).Count + $AdObjects.Data.ServiceHostsRatio = try{ + $AdObjects.Data.ServiceHostsCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.ServiceHostsComputers = $AdObjects.Computers | Select-Object ObjectGUID,@{ + Name = "ServiceHosts" + Expression = { + (($_.servicePrincipalName) -replace "^[^\/]*\/" -replace "\/[^\/]*$") | Sort-Object -Unique + } + } | Where-Object {$null -ne $_.ServiceHosts} + $AdObjects.Data.ServiceHostsComputersCount = ($AdObjects.Data.ServiceHostsComputers | Measure-Object).Count + $AdObjects.Data.ServiceHostsComputersRatio = try{ + $AdObjects.Data.ServiceHostsComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.ServiceDnsBypassComputers = $AdObjects.Computers | Select-Object ObjectGUID,DNSHostName,@{ + Name = "ServiceDnsBypass" + Expression = { + $dnsHostName = $_.DNSHostName + (($_.servicePrincipalName) -replace "^[^\/]*\/" -replace "\/[^\/]*$") | Sort-Object -Unique | ForEach-Object { + $_ | Where-Object { + $_ -ne $dnsHostName -and + $_ -ne ($dnsHostName -replace "\..*$") + } + } + } + } | Where-Object {$null -ne $_.ServiceDnsBypass} + $AdObjects.Data.ServiceDnsBypassComputersCount = ($AdObjects.Data.ServiceDnsBypassComputers | Measure-Object).Count + $AdObjects.Data.ServiceDnsBypassComputersRatio = try{ + $AdObjects.Data.ServiceDnsBypassComputersCount / $AdObjects.Data.ServiceHostsComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + ServiceClasses = @{ + Name = "SPN Service Classes Found" + Value = $AdObjects.Data.ServiceClassesCount + Threshold = 10 + Indicator = "<" + Description = "Discrete number of Service Principal Name (SPN) Service Classes observed" + Status = $null + } + ServiceClassesComputers = @{ + Name = "Computers with SPN configured" + Value = $AdObjects.Data.ServiceClassesComputersRatio + Threshold = 0.3 + Indicator = "<" + Description = "Percent of computer objects with Service Principal Names (SPN) configured" + Status = $null + } + HostBypassComputers = @{ + Name = "Computers with SPN configured that overlaps with HOST alias" + Value = $AdObjects.Data.HostBypassComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects with Service Principal Names (SPN) configured that overlap with the HOST SPN Service Class alias" + Status = $null + } + ServiceHosts = @{ + Name = "Hostnames found in SPNs of computer objects" + Value = $AdObjects.Data.ServiceHostsRatio + Threshold = 2 + Indicator = "<" + Description = "Percent of unique Service Principal Names (SPN) hostnames configured relative to the total number of computer objects" + Status = $null + } + ServiceHostsComputers = @{ + Name = "Computers with SPN configured that overlaps with HOST alias" + Value = $AdObjects.Data.ServiceHostsComputersRatio + Threshold = 1 + Indicator = "<" + Description = "Percent of computer objects with Service Principal Names (SPN) configured relative to the total number of computer objects" + Status = $null + } + ServiceDnsBypassComputers = @{ + Name = "Computers with SPN configured that overlaps with HOST alias" + Value = $AdObjects.Data.ServiceDnsBypassComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects with Service Principal Names (SPN) configured with a service hostname that does not match their DNS hostname" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.md b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 new file mode 100644 index 000000000..1fd550dad --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 @@ -0,0 +1,105 @@ +<# +.SYNOPSIS + Checks for computer SID History + +.DESCRIPTION + Identifies any computer objects with values set in the SID History attribute + +.EXAMPLE + Test-MtAdComputerSidHistory + + Returns true if AD Computer SID History is not in use + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerSidHistory +#> +function Test-MtAdComputerSidHistory { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.SidHistoryComputers = $AdObjects.Computers | Where-Object { + $_.SIDHistory.Count -ne 0 + } + $AdObjects.Data.SidHistoryComputersCount = ($AdObjects.Data.SidHistoryComputers | Measure-Object).Count + $AdObjects.Data.SidHistoryComputersRatio = try{ + $AdObjects.Data.SidHistoryComputers / $AdObjects.Data.ComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + SidHistoryComputers = @{ + Name = "Computers with a SID History" + Value = $AdObjects.Data.SidHistoryComputersRatio + Threshold = 0.00 + Indicator = "=" + Description = "Percent of computer objects with a SID History" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdComputerStatus.md b/powershell/public/maester/ad/Test-MtAdComputerStatus.md new file mode 100644 index 000000000..591bc6882 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.md @@ -0,0 +1,2 @@ + +%TestResult% \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 new file mode 100644 index 000000000..17f11776b --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 @@ -0,0 +1,158 @@ +<# +.SYNOPSIS + Checks the computer objects for activity + +.DESCRIPTION + Checks the last logon for computer objects against multiple time periods + +.EXAMPLE + Test-MtAdComputerStatus + + Returns true if AD Computer status is within thresholds + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerStatus +#> +function Test-MtAdComputerStatus { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Computers = $__MtSession.AdCache.AdComputers.Computers + Data = $__MtSession.AdCache.AdComputers.Data + } + + #region Collect + $AdObjects.Data.EnabledComputers = $AdObjects.Computers | Where-Object { + $_.Enabled + } + $AdObjects.Data.EnabledComputersCount = ($AdObjects.Data.EnabledComputers | Measure-Object).Count + + $AdObjects.Data.DisabledComputers = $AdObjects.Computers | Where-Object { + -not $_.Enabled + } + $AdObjects.Data.DisabledComputersCount = ($AdObjects.Data.DisabledComputers | Measure-Object).Count + $AdObjects.Data.DisabledComputersRatio = try{ + $AdObjects.Data.DisabledComputersCount / $AdObjects.Data.ComputersCount + }catch{0} + + $AdObjects.Data.DormantComputers = $AdObjects.Data.EnabledComputers | Where-Object { + $_.LastLogonDate -lt $Thresholds.DormantDate + } + $AdObjects.Data.DormantComputersCount = ($AdObjects.Data.DormantComputers | Measure-Object).Count + $AdObjects.Data.DormantComputersRatio = try{ + $AdObjects.Data.DormantComputersCount / $AdObjects.Data.EnabledComputersCount + }catch{0} + + $AdObjects.Data.ExpiredComputers = $AdObjects.Data.EnabledComputers | Where-Object { + $_.LastLogonDate -lt $Thresholds.ExpiredDate + } + $AdObjects.Data.ExpiredComputersCount = ($AdObjects.Data.ExpiredComputers | Measure-Object).Count + $AdObjects.Data.ExpiredComputersRatio = try{ + $AdObjects.Data.ExpiredComputersCount / $AdObjects.Data.EnabledComputersCount + }catch{0} + + $AdObjects.Data.StaleComputers = $AdObjects.Data.DisabledComputers | Where-Object { + $_.Modified -lt $Thresholds.StaleDate + } + $AdObjects.Data.StaleComputersCount = ($AdObjects.Data.StaleComputers | Measure-Object).Count + $AdObjects.Data.StaleComputersRatio = try{ + $AdObjects.Data.StaleComputersCount / $AdObjects.Data.DisabledComputersCount + }catch{0} + #endregion + + $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers + $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + DisabledComputers = @{ + Name = "Disabled Computers" + Value = $AdObjects.Data.DisabledComputersRatio + Threshold = 0.05 + Indicator = "<" + Description = "Percent of all computer objects that are disabled" + Status = $null + } + StaleComputers = @{ + Name = "Stale Computers" + Value = $AdObjects.Data.StaleComputersRatio + Threshold = 0.05 + Indicator = "<" + Description = "Percent of disabled computer objects with Modified before " + ($__MtSession.AdCache.Thresholds.StaleDate).ToString() + Status = $null + } + DormantComputers = @{ + Name = "Dormant Computers" + Value = $AdObjects.Data.DormantComputersRatio + Threshold = 0.05 + Indicator = "<" + Description = "Percent of enabled computer objects with LastLogonDate before " + ($__MtSession.AdCache.Thresholds.DormantDate).ToString() + Status = $null + } + ExpiredComputers = @{ + Name = "Expired Computers" + Value = $AdObjects.Data.ExpiredComputersRatio + Threshold = 0.05 + Indicator = "<" + Description = "Percent of enabled computer objects with LastLogonDate before " + ($__MtSession.AdCache.Thresholds.ExpiredDate).ToString() + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/tests/Maester/ad/Test-MtAd.Tests.ps1 b/tests/Maester/ad/Test-MtAd.Tests.ps1 new file mode 100644 index 000000000..e4fd6bc4b --- /dev/null +++ b/tests/Maester/ad/Test-MtAd.Tests.ps1 @@ -0,0 +1,62 @@ +Describe "Maester/Active Directory" -Tag "Maester", "Active Directory", "MT.AD" { + It "MT.AD.0001: AD Computer Containers" -Tag "MT.AD.0000","MT.AD.0001","AD Computer" { + $result = Test-MtAdComputerContainer + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer Containers are properly used" + } + } + It "MT.AD.0002: AD Computer Creator SID" -Tag "MT.AD.0000","MT.AD.0002","AD Computer" { + $result = Test-MtAdComputerCreatorSid + if ($null -ne $result){ + $result | Should -Be $true -Because "Computers do not use creator SID" + } + } + It "MT.AD.0003: AD Computer DNS" -Tag "MT.AD.0000","MT.AD.0003","AD Computer" { + $result = Test-MtAdComputerDns + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer DNS hostnames are properly used" + } + } + It "MT.AD.0004: AD Computer Domain Controllers" -Tag "MT.AD.0000","MT.AD.0004","AD Computer" { + $result = Test-MtAdComputerDomainController + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Controllers are properly used" + } + } + It "MT.AD.0005: AD Computer Kerberos" -Tag "MT.AD.0000","MT.AD.0005","AD Computer" { + $result = Test-MtAdComputerKerberos + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer Kerberos configurations are properly used" + } + } + It "MT.AD.0006: AD Computer Operating Systems" -Tag "MT.AD.0000","MT.AD.0006","AD Computer" { + $result = Test-MtAdComputerOperatingSystem + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer operating systems are properly used" + } + } + It "MT.AD.0007: AD Computer Primary Group" -Tag "MT.AD.0000","MT.AD.0007","AD Computer" { + $result = Test-MtAdComputerPrimaryGroup + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer primary groups are properly used" + } + } + It "MT.AD.0008: AD Computer Services" -Tag "MT.AD.0000","MT.AD.0008","AD Computer" { + $result = Test-MtAdComputerService + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer services configurations are properly used" + } + } + It "MT.AD.0009: AD Computer SID History" -Tag "MT.AD.0000","MT.AD.0009","AD Computer" { + $result = Test-MtAdComputerSidHistory + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer SID History is properly used" + } + } + It "MT.AD.0010: AD Computer Status" -Tag "MT.AD.0000","MT.AD.0010","AD Computer" { + $result = Test-MtAdComputerStatus + if ($null -ne $result){ + $result | Should -Be $true -Because "Computer state is proper" + } + } +} \ No newline at end of file From 7e588348e379f9b16d5c1c653457e3d63706b656 Mon Sep 17 00:00:00 2001 From: Snozz Date: Sun, 7 Dec 2025 17:59:06 -0700 Subject: [PATCH 02/11] Fixing pssingular --- powershell/public/maester/ad/Test-MtAdComputerDns.ps1 | 1 + powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 | 1 + powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 | 1 + 3 files changed, 3 insertions(+) diff --git a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 index aee6b92fc..382cd2023 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 @@ -15,6 +15,7 @@ #> function Test-MtAdComputerDns { [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Proper name')] [OutputType([bool])] param( [string]$Server = $__MtSession.AdServer, diff --git a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 index e7f346396..f848fe435 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 @@ -15,6 +15,7 @@ #> function Test-MtAdComputerKerberos { [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Proper name')] [OutputType([bool])] param( [string]$Server = $__MtSession.AdServer, diff --git a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 index 17f11776b..702bcd4af 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 @@ -15,6 +15,7 @@ #> function Test-MtAdComputerStatus { [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Proper name')] [OutputType([bool])] param( [string]$Server = $__MtSession.AdServer, From bd0ea97a19043497f5aab41248f1c1566ae53605 Mon Sep 17 00:00:00 2001 From: Snozz Date: Sun, 7 Dec 2025 18:03:41 -0700 Subject: [PATCH 03/11] Fixes --- powershell/Maester.psd1 | 1 + powershell/public/Connect-Maester.ps1 | 2 +- powershell/public/Set-MtAdCache.ps1 | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index a99f9f75a..8277ce806 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -185,6 +185,7 @@ 'Test-MtXspmPublicRemotelyExploitableHighExposureDevices', 'Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices', 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices', + 'Set-MtAdCache','Clear-MtAdCache', 'Test-MtAdComputerContainer.ps1', 'Test-MtAdComputerCreatorSid.ps1', 'Test-MtAdComputerDns.ps1', diff --git a/powershell/public/Connect-Maester.ps1 b/powershell/public/Connect-Maester.ps1 index 2ce58c37f..06092e019 100644 --- a/powershell/public/Connect-Maester.ps1 +++ b/powershell/public/Connect-Maester.ps1 @@ -110,7 +110,7 @@ [pscredential]$AdCredential = $null, # The services to connect to such as Azure and EXO. Default is Graph. - [ValidateSet('ActiveDirectory', 'All', 'Azure', 'Cloud', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'Teams')] + [ValidateSet('ActiveDirectory', 'All', 'Azure', 'ExchangeOnline', 'Graph', 'SecurityCompliance', 'Teams')] [string[]]$Service = 'Graph', # The Tenant ID to connect to, if not specified the sign-in user's default tenant is used. diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 index ff55a4ee8..b6d644a7d 100644 --- a/powershell/public/Set-MtAdCache.ps1 +++ b/powershell/public/Set-MtAdCache.ps1 @@ -17,6 +17,7 @@ #> function Set-MtAdCache { [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal function')] param( [string]$Server = $__MtSession.AdServer, [pscredential]$Credential = $__MtSession.AdCredential, From aa849408f5daf43761e61492eff5021ac223e4e1 Mon Sep 17 00:00:00 2001 From: Snozz Date: Sun, 7 Dec 2025 18:10:15 -0700 Subject: [PATCH 04/11] Fixes --- powershell/Maester.psd1 | 20 ++++++++++---------- powershell/public/Set-MtAdCache.ps1 | 12 ++++++++++++ 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 8277ce806..53a16a5c0 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -186,16 +186,16 @@ 'Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices', 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices', 'Set-MtAdCache','Clear-MtAdCache', - 'Test-MtAdComputerContainer.ps1', - 'Test-MtAdComputerCreatorSid.ps1', - 'Test-MtAdComputerDns.ps1', - 'Test-MtAdComputerDomainController.ps1', - 'Test-MtAdComputerKerberos.ps1', - 'Test-MtAdComputerOperatingSystem.ps1', - 'Test-MtAdComputerPrimaryGroup.ps1', - 'Test-MtAdComputerService.ps1', - 'Test-MtAdComputerSidHistory.ps1', - 'Test-MtAdComputerStatus.ps1' + 'Test-MtAdComputerContainer', + 'Test-MtAdComputerCreatorSid', + 'Test-MtAdComputerDns', + 'Test-MtAdComputerDomainController', + 'Test-MtAdComputerKerberos', + 'Test-MtAdComputerOperatingSystem', + 'Test-MtAdComputerPrimaryGroup', + 'Test-MtAdComputerService', + 'Test-MtAdComputerSidHistory', + 'Test-MtAdComputerStatus' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 index b6d644a7d..5d4a16acd 100644 --- a/powershell/public/Set-MtAdCache.ps1 +++ b/powershell/public/Set-MtAdCache.ps1 @@ -7,6 +7,15 @@ Use this function to set the cache. +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.PARAMETER Objects + Specific type of AD objects to query. + .EXAMPLE Set-MtAdCache @@ -37,6 +46,7 @@ function Set-MtAdCache { $dcPrimaryGroupIds = @(516,521) $rootDse = try{ + Write-Verbose "Attempting Get-AdRootDSE" Get-AdRootDSE }catch{ Write-Error $_ @@ -49,6 +59,7 @@ function Set-MtAdCache { Properties = "*" } $directoryService = try{ + Write-Verbose "Attempting query for Directory Service object" Get-ADObject @ADObjectDirectoryService }catch{ Write-Error $_ @@ -84,6 +95,7 @@ function Set-MtAdCache { if($Objects -contains "Computers" -or $Objects -contains "All"){ $computers = try{ + Write-Verbose "Attempting AD query for Computers" Get-ADComputer -Filter * -Properties * }catch{ Write-Error $_ From 6fd9f896b06a1932bb32ccf6cc04eb9ba7ba374e Mon Sep 17 00:00:00 2001 From: Snozz Date: Sun, 7 Dec 2025 18:19:54 -0700 Subject: [PATCH 05/11] Verbose and descriptions --- .../public/maester/ad/Test-MtAdComputerContainer.ps1 | 7 +++++++ .../public/maester/ad/Test-MtAdComputerCreatorSid.ps1 | 7 +++++++ powershell/public/maester/ad/Test-MtAdComputerDns.ps1 | 7 +++++++ .../maester/ad/Test-MtAdComputerDomainController.ps1 | 7 +++++++ powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 | 7 +++++++ .../public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 | 7 +++++++ .../public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 | 7 +++++++ powershell/public/maester/ad/Test-MtAdComputerService.ps1 | 7 +++++++ .../public/maester/ad/Test-MtAdComputerSidHistory.ps1 | 7 +++++++ powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 | 7 +++++++ 10 files changed, 70 insertions(+) diff --git a/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 index 92cbab3ce..0b8205d5c 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies if computer containers are misused +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerContainer @@ -22,6 +28,7 @@ function Test-MtAdComputerContainer { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 index 051f7b18e..00de4fed2 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies if computers are joining through non-admin users +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerCreatorSid @@ -22,6 +28,7 @@ function Test-MtAdComputerCreatorSid { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 index 382cd2023..dea4e376d 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies issues with computer DNS registration +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerDns @@ -23,6 +29,7 @@ function Test-MtAdComputerDns { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 index 658a89a84..4987cbc58 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies number of domain controllers +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerDomainController @@ -22,6 +28,7 @@ function Test-MtAdComputerDomainController { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 index f848fe435..5b43be718 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies misconfigurations with computer objects and Kerberos +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerKerberos @@ -23,6 +29,7 @@ function Test-MtAdComputerKerberos { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 index dfdd0f54b..c2ceccb05 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies issues with computer object operating systems +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerOperatingSystem @@ -22,6 +28,7 @@ function Test-MtAdComputerOperatingSystem { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 index 14933d955..e3ed49f20 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies if any computer objects do not have a normal primary group ID +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerPrimaryGroup @@ -22,6 +28,7 @@ function Test-MtAdComputerPrimaryGroup { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 index ee9d35eca..8fdf893cf 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies potential misconfiguration of computer SPNs +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerService @@ -22,6 +28,7 @@ function Test-MtAdComputerService { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 index 1fd550dad..3cb1ccbb0 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Identifies any computer objects with values set in the SID History attribute +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerSidHistory @@ -22,6 +28,7 @@ function Test-MtAdComputerSidHistory { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } diff --git a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 index 702bcd4af..bdd7029f5 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 @@ -5,6 +5,12 @@ .DESCRIPTION Checks the last logon for computer objects against multiple time periods +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + .EXAMPLE Test-MtAdComputerStatus @@ -23,6 +29,7 @@ function Test-MtAdComputerStatus { ) if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory return $null } From 5dfe77faae353344c3ebfa6cd6d6f0658d8b25a9 Mon Sep 17 00:00:00 2001 From: Snozz Date: Mon, 8 Dec 2025 06:57:10 -0700 Subject: [PATCH 06/11] Adding two new checks within the SPN test --- powershell/public/Set-MtAdCache.ps1 | 4 + .../maester/ad/Test-MtAdComputerService.ps1 | 217 ++++++++++++++++++ 2 files changed, 221 insertions(+) diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 index 5d4a16acd..aa919ca7a 100644 --- a/powershell/public/Set-MtAdCache.ps1 +++ b/powershell/public/Set-MtAdCache.ps1 @@ -137,12 +137,16 @@ function Set-MtAdCache { ServiceClassesCount = $null ServiceClassesComputers = @() ServiceClassesComputersCount = $null + UnknownServiceClasses = @() + UnknownServiceClassesCount = $null HostBypassComputers = @() HostBypassComputersCount = $null ServiceHosts = @() ServiceHostsCount = $null ServiceHostsComputers = @() ServiceHostsComputersCount = $null + ServiceNoFqdnComputers = @() + ServiceNoFqdnComputersCount = $null ServiceDnsBypassComputers = @() ServiceDnsBypassComputersCount = $null DnsZones = @() diff --git a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 index 8fdf893cf..8979010d9 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 @@ -37,6 +37,189 @@ function Test-MtAdComputerService { Set-MtAdCache -Objects "Computers" -Server $Server -Credential $Credential } + #Bulk of list from https://adsecurity.org/?page_id=183 as of 12/2025 + #Thank you Sean Metcalf + $knownSpns=@" +SPN,Application +{14E52635-0A95-4a5c-BDB1-E0D0C703B6C8}, +{54094C05-F977-4987-BFC9-E8B90E088973},Graphon +AcronisAgent,Acronis backup/data recovery software +AdtServer,Microsoft System Center Operations Manager (2007/2012) Management Server with ACS +afpserver,Apple Filing Protocol +AFServer,Pi AF Server +Agent VProRecovery Norton Ghost 12.0,VProRecovery Norton Ghost 12.0 +AgpmServer,Microsoft Advanced Group Policy Management (AGPM) +aradminsvc,Quest Active Roles Server +Backup Exec System Recovery Agent 6.x,Backup Exec System Recovery Agent 6.x +BICMS,SAP Business Objects +BO3SSO,Business Objects? +BOBJCentralMS,SAL +BOCMS,SAP Business Objects +BOSSO,Business Objects +CAXOsoftEngine,CA XOsoft Exchange Replication +CAARCserveRHAEngine,CA ArcServe +CESREMOTE,seems to be related to a Citrix VDI solution on VMWare. Many VDI workstations have this SPN. +CIFS,Common Internet File System +ckp_pdp,Checkpoint Identity +CmRcService,Microsoft System Center Configuration Manager (SCCM) Remote Control +Cognos,IBM Cognos +CUSESSIONKEYSVR,Cisco Unity VOIP System +cvs,CVS Repository +Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04,Distributed File System Replication +DNS,Domain Name Server +DynamicsNAV,Microsoft Dynamics? +E3514235-4B06-11D1-AB04-00C04FC2DCD2,NTDS DC RPC Replication +E3514235-4B06-11D1-AB04-00C04FC2DCD2-ADAM,Microsoft ADAM Instance +exchangeAB,Exchange Address Book service (typically a Domain Controller supporting NSPI, which is usually all GCs) +exchangeMDB,RPC client access - Client Access Server role +exchangeRFR,Exchange Address Book service +EDVR,ExacqVision +fcsvr,Apple Final Cut Server +FIMService,Microsoft Forefront Identity Manager (FIM) +FileRepService,WSFileRepService.exe ? +ftp,File Transfer Protocol +flume,Clodera Flume +gateway,Hadoop Knox +GC,Domain Controller Global Catalog services +hbase,Cloudera Hbase +HBase,Hadoop MasterServer +hdb,Hana DB +hdfs,Hadoop +hive,Hadoop Metastore +host,The HOST service represents the host computer. The HOST SPN is used to access the host computer account whose long term key is used by the Kerberos protocol when it creates a service ticket. +HTTP,SPN for http web services that support Kerberos authentication +httpfs,Hadoop HDFS over HTTP +https,SPN for http web services that support Kerberos authentication +Hue,Hadoop Hue Interface +Hyper-V Replica Service,Microsoft Hyper-V's Replica Service +iem,IBM BigFix +IMAP,Internet Message Access Protocol +IMAP4,Internet Message Access Protocol version 4 +impala,Cloudera Impala +ImDmsSvc,Worksite (Imanage) Server +ipp,Internet Printing Protocol +iSCSITarget,iSCSI Configuration +jboss,RedHat Jboss +JournalNode Server,Hadoop JournalNode +kadmin,Kerberos +Kafka,Hadoop KafkaServer +kafka,Apache Kafka +kudu,Apache Kudu +kafka_mirror_maker,Apache Kafka +krbsvr400,IBM OS/400 +ldap,LDAP service such as on a Domain Controller or ADAM instance. +LiveState Recovery Agent 6.x,Symantec LiveState Recovery +magfs,Maginatics MagFS +mapred,Cloudera Map reduce +M-Files,M-Files? +Microsoft Virtual Console Service,HyperV Host +Microsoft Virtual System Migration Service,P2V Support (Hyper-V) +mongod,MongoDB Enterprise +mongos,MongoDB Enterprise +mr2,Hadoop History Server +MSClusterVirtualServer,Windows Cluster Server +MSCRMAsyncService,Microsoft Dynamics 365 +MSCRMSandboxService,Microsoft Dynamics 365 +MSOLAPDisco.3,SQL Server Analysis Services +msolapdisco3,SQL Server Analysis Services +MSOLAPSvc,SQL Server Analysis Services +MSOLAPSvc.3,SQL Server Analysis Services +MSOMHSvc,Micrsoft SCOM 2012 +MSOMSdkSvc,Micrsoft SCOM 2012 +MSServerCluster,Windows Cluster Server +MSServerClusterMgmtAPI,This SPN is needed for cluster APIs to authenticate to the server by using Kerberos +MSSQL,Microsoft SQL Server +MSSQL`$ADOBECONNECT,Microsoft SQL Server supporting Adobe Connect +MSSQL`$BIZTALK,Microsoft SQL Server supporting Microsoft Biztalk Server +MSSQL`$BUSINESSOBJECTS,Microsoft SQL Server supporting Business Objects +MSSQL`$DB01NETIQ,Microsoft SQL Server supporting NetIQ +MSSQLSvc,Microsoft SQL Server +NAV2016,Microsoft Dynamics NAV +nfs,Network File System +Norskale,Citrix Infrastructure +NPPolicyEvaluator,Quest Change Auditor +NPRepository4(DEFAULT),Quest Change Auditor +NPRepository4(*),Quest Change Auditor +NtFrs-88f5d2bd-b646-11d2-a6d3-00c04fc9b232,NT File Replication Service +oozie,Hadoop Oozie Server +OA60,OpenAccess (sometimes) +oracle,Oracle Kerberos auth +pcast,Apple Podcast Producer +PCNSCLNT,Automated Password Synchronization Solution (MIIS 2003 & FIM) +PIServer,Pi AF Server +PowerBIReportServer,Power BI Report Server +POP,Post Office Protocol +POP3,Post Office Protocol version 3 +PVSSoap,Citrix Provisioning Services (7.1) +postgres,Postgres database server +RestrictedKrbHost,The class of services that use SPNs with the serviceclass string equal to “RestrictedKrbHost”, whose service tickets use the computer account’s key and share a session key. +RPC,Remote Procedure Call +SAP,SAP/SAPService +SAPService,SAP/SAPService +SAS,SAS 9.3 Intelligence Platform +SCVMM,Micrsoft System Center Virtual Machine Manager (SCVMM) +SQLAgent`$DB01NETIQ,SQL service for NetIQ +secshd,IBM InfoSphere +SeapineLicenseSvr,Helix ALM +sentry,Cloudera Enterprise 5.2.x +sip,Session Initiation Protocol +SMTP,Simple Mail Transfer Protocol +SMTPSVC,Simple Mail Transfer Protocol +SoftGrid,Microsoft Application Virtualization (App-V) formerly “SoftGrid” +solr,Apache Solr +spark,Apache Spark Server +*informatica*,Informatica +Storm,Hadoop Nimbus server +STS,VMWare SSO service +tapinego,Associated with routing applications such as Microsoft firewalls (ISA, TMG, etc) +TERMSERV,Microsoft Remote Desktop Protocol Services, aka Terminal Services. +TERMSRV,Microsoft Remote Desktop Protocol Services, aka Terminal Services. +tnetdgines,Juniper Kerberos auth? “Tnetd is a daemon used for internal communication between different components like Routing Engine and Packet Forwarding En +VCSClusterVirtualServer,Microsoft Cluster Server +VMMSvc,Micrsoft System Center Virtual Machine Manager (SCVMM) +vmrc,Microsoft Virtual Server 2005 +vnc,VNC Server +vpn,Virtual Private Network +VProRecovery Backup Exec System Recovery Agent 7.0, +VProRecovery Backup Exec System Recovery Agent 8.0, +VProRecovery Backup Exec System Recovery Agent 8.5, +VProRecovery Backup Exec System Recovery Agent 9.0, +VProRecovery Norton Ghost Agent 12.0, +VProRecovery Norton Ghost Agent 14.0, +VProRecovery Norton Ghost Agent 15.0, +VProRecovery Symantec System Recovery Agent 10.0, +VProRecovery Symantec System Recovery Agent 11.0, +VProRecovery Symantec System Recovery Agent 11.1, +VProRecovery Symantec System Recovery Agent 14.0, +vssrvc,Microsoft Virtual Server (2005) +WSMAN,Windows Remote Management (based on WS-Management standard) service +xgrid,Apple's distributed (grid) computing / Mac OS X 10.6 Server Admin +xmpp,Extensible Messaging and Presence Protocol (Jabber) +yarn,Hadoop NodeManager +yarn,Cloudera MapReduce +Zeppelin,Hadoop Zeppelin Server +ZooKeeper,Hadoop ZooKeeper +zookeeper,Cloudera Zookeeper +boostfs,Data Domain +UPM_SPN_7DC3CE86,Citrix UPM +http,Web Server +https,Web Server +DNS,DNS, +host,alias +SCVMM,System Center Virtual Machine Manager +BOBJCentralMS,SAL +MSSQL`$ADOBECONNECT,Microsoft SQL Server supporting Adobe Connect +MSSQL`$BIZTALK,Microsoft SQL Server supporting Microsoft Biztalk Server +MSSQL`$BUSINESSOBJECTS,Microsoft SQL Server supporting Business Objects +MSSQL`$DB01NETIQ,Microsoft SQL Server supporting NetIQ +PowerBIReportServer,Power BI Report Server +SQLAgent`$DB01NETIQ,SQL service for NetIQ +boostfs,Data Domain +UPM_SPN_7DC3CE86,Citrix UPM +XicNotifier,Honeywell Notifier +"@ + $knownSpns=$knownSpns|ConvertFrom-Csv + $AdObjects = @{ Computers = $__MtSession.AdCache.AdComputers.Computers Data = $__MtSession.AdCache.AdComputers.Data @@ -60,6 +243,12 @@ function Test-MtAdComputerService { $AdObjects.Data.ServiceClassesComputersCount / $AdObjects.Data.ComputersCount }catch{0} + $AdObjects.Data.UnknownServiceClasses = ($AdObjects.Computers | Select-Object @{ + Name = "ServiceClasses" + Expression = {($_.servicePrincipalName) -replace "\/.*$"} + }).ServiceClasses | Group-Object | Where-Object {$_.Name -notin $knownSpns.SPN} + $AdObjects.Data.UnknownServiceClassesCount = ($AdObjects.Data.UnknownServiceClasses | Measure-Object).Count + $AdObjects.Data.HostBypassComputers = $AdObjects.Data.ServiceClassesComputers | Select-Object ObjectGUID,@{ Name = "ServiceClassesBypassingHost" Expression = { @@ -96,6 +285,18 @@ function Test-MtAdComputerService { $AdObjects.Data.ServiceHostsComputersCount / $AdObjects.Data.ComputersCount }catch{0} + $AdObjects.Data.ServiceNoFqdnComputers = $AdObjects.Computers | Select-Object ObjectGUID,DNSHostName,@{ + Name = "FqdnCheck" + Expression = { + $dnsHostName = $_.DNSHostName + $null -ne $dnsHostName -and + $dnsHostName -eq ((($_.servicePrincipalName) -replace "^[^\/]*\/" -replace "\/[^\/]*$") | Sort-Object -Unique | Where-Object { + $dnsHostName -eq $_ + }) + } + } | Where-Object {-not $_.FqdnCheck} + $AdObjects.Data.ServiceNoFqdnComputersCount = ($AdObjects.Data.ServiceNoFqdnComputers | Measure-Object).Count + $AdObjects.Data.ServiceDnsBypassComputers = $AdObjects.Computers | Select-Object ObjectGUID,DNSHostName,@{ Name = "ServiceDnsBypass" Expression = { @@ -135,6 +336,14 @@ function Test-MtAdComputerService { Description = "Percent of computer objects with Service Principal Names (SPN) configured" Status = $null } + UnknonwServiceClasses = @{ + Name = "Unknown SPN Service Classes Found" + Value = $AdObjects.Data.UnknonwServiceClassesCount + Threshold = 0 + Indicator = "=" + Description = "Discrete number of Service Principal Name (SPN) Service Classes observed where the purpose is not known" + Status = $null + } HostBypassComputers = @{ Name = "Computers with SPN configured that overlaps with HOST alias" Value = $AdObjects.Data.HostBypassComputersRatio @@ -159,6 +368,14 @@ function Test-MtAdComputerService { Description = "Percent of computer objects with Service Principal Names (SPN) configured relative to the total number of computer objects" Status = $null } + ServiceNoFqdnComputers = @{ + Name = "Computers without SPN matching DNS Hostname" + Value = $AdObjects.Data.ServiceHostsComputersRatio + Threshold = 0 + Indicator = "=" + Description = "Discrete number of computers without Fully Qualified Domain Name (FQDN) Service Principal Names (SPN)" + Status = $null + } ServiceDnsBypassComputers = @{ Name = "Computers with SPN configured that overlaps with HOST alias" Value = $AdObjects.Data.ServiceDnsBypassComputersRatio From 6d2054092333c76503972fd4bb11182d42586d15 Mon Sep 17 00:00:00 2001 From: Snozz Date: Mon, 8 Dec 2025 07:38:13 -0700 Subject: [PATCH 07/11] Fix variable reference --- .../public/maester/ad/Test-MtAdComputerDomainController.ps1 | 2 +- powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 index 4987cbc58..5bfb90f2b 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 @@ -44,7 +44,7 @@ function Test-MtAdComputerDomainController { #region Collect $AdObjects.Data.DomainControllers = $AdObjects.Computers | Where-Object { - $_.primaryGroupId -notin $AdObjects.Data.DomainControllerPgids + $_.primaryGroupId -notin $__MtSession.AdCache.DomainControllerPgids } $AdObjects.Data.DomainControllersCount = ($AdObjects.Data.DomainControllers | Measure-Object).Count #endregion diff --git a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 index e3ed49f20..fb53b9e77 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 @@ -44,7 +44,7 @@ function Test-MtAdComputerPrimaryGroup { #region Collect $AdObjects.Data.NonPgIdCoumputers = $AdObjects.Computers | Where-Object { - $_.primaryGroupId -notin $AdObjects.Data.PrimaryGroupIds + $_.primaryGroupId -notin $__MtSession.AdCache.PrimaryGroupIds } $AdObjects.Data.NonPgIdCoumputersCount = ($AdObjects.Data.NonPgIdCoumputers | Measure-Object).Count $AdObjects.Data.NonPgIdComputersRatio = try{ From c8a56ee3be50c87169c9c49f9d87f72b6e61d118 Mon Sep 17 00:00:00 2001 From: Snozz Date: Wed, 10 Dec 2025 05:41:30 -0700 Subject: [PATCH 08/11] Init of Forest checks --- powershell/Maester.psd1 | 16 +-- powershell/public/Set-MtAdCache.ps1 | 35 +++++- .../maester/ad/Test-MtAdForestDomain.ps1 | 107 ++++++++++++++++ .../ad/Test-MtAdForestExternalLdap.ps1 | 108 ++++++++++++++++ .../maester/ad/Test-MtAdForestFsmoStatus.ps1 | 109 ++++++++++++++++ .../ad/Test-MtAdForestFunctionalLevel.ps1 | 106 ++++++++++++++++ .../public/maester/ad/Test-MtAdForestSite.ps1 | 116 +++++++++++++++++ .../maester/ad/Test-MtAdForestSuffix.ps1 | 118 ++++++++++++++++++ tests/Maester/ad/Test-MtAd.Tests.ps1 | 36 ++++++ 9 files changed, 740 insertions(+), 11 deletions(-) create mode 100644 powershell/public/maester/ad/Test-MtAdForestDomain.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdForestSite.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 53a16a5c0..e89802af1 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -186,16 +186,12 @@ 'Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices', 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices', 'Set-MtAdCache','Clear-MtAdCache', - 'Test-MtAdComputerContainer', - 'Test-MtAdComputerCreatorSid', - 'Test-MtAdComputerDns', - 'Test-MtAdComputerDomainController', - 'Test-MtAdComputerKerberos', - 'Test-MtAdComputerOperatingSystem', - 'Test-MtAdComputerPrimaryGroup', - 'Test-MtAdComputerService', - 'Test-MtAdComputerSidHistory', - 'Test-MtAdComputerStatus' + 'Test-MtAdComputerContainer','Test-MtAdComputerCreatorSid','Test-MtAdComputerDns', + 'Test-MtAdComputerDomainController','Test-MtAdComputerKerberos', + 'Test-MtAdComputerOperatingSystem','Test-MtAdComputerPrimaryGroup', + 'Test-MtAdComputerService','Test-MtAdComputerSidHistory','Test-MtAdComputerStatus', + 'Test-MtAdForestDomain','Test-MtAdForestExternalLdap','Test-MtAdForestFsmoStatus', + 'Test-MtAdForestFunctionalLevel','Test-MtAdForestSite','Test-MtAdForestSuffix' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 index aa919ca7a..499986242 100644 --- a/powershell/public/Set-MtAdCache.ps1 +++ b/powershell/public/Set-MtAdCache.ps1 @@ -30,7 +30,7 @@ function Set-MtAdCache { param( [string]$Server = $__MtSession.AdServer, [pscredential]$Credential = $__MtSession.AdCredential, - [ValidateSet('All', 'Computers')] + [ValidateSet('All', 'Computers', 'Forest')] [string[]]$Objects = 'All' ) @@ -91,6 +91,39 @@ function Set-MtAdCache { PrimaryGroupIds = $primaryGroupIds DomainControllerPgids = $dcPrimaryGroupIds AdComputers = $__MtSession.AdCache.AdComputers + AdForest = $__MtSession.AdCache.AdForest + } + + if($Objects -contains "Forest" -or $Objects -contains "All"){ + $forest = try{ + Write-Verbose "Attempting AD query for Forest" + Get-ADForest + }catch{ + Write-Error $_ + return $null + } + + $__MtSession.AdCache.AdForest = @{ + SetFlag = $true + Forest = @($forest) + Data = @{ + FunctionalLevel = $rootDse.forestFunctionality + CrossForestReferences = @() + CrossForestReferencesCount = $null + DomainNamingMaster = $null + SchemaMaster = $null + CommonFsmo = $null + Sites = @() + SitesCount = $null + DefaultSite = $null + Domains = @() + DomainsCount = $null + UpnSuffixes = @() + UpnSuffixesCount = $null + SpnSuffixes = @() + SpnSuffixesCount = $null + } + } } if($Objects -contains "Computers" -or $Objects -contains "All"){ diff --git a/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 new file mode 100644 index 000000000..8d15a9ea3 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 @@ -0,0 +1,107 @@ +<# +.SYNOPSIS + Checks AD Forest Domains + +.DESCRIPTION + Identifies if forest has domains + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestDomain + + Returns true if AD Forest has domains + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestDomain +#> +function Test-MtAdForestDomain { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + $AdObjects.Data.Domains = @($AdObjects.Forest.Domains) + $AdObjects.Data.DomainsCount = ($AdObjects.Data.Domains | Measure-Object).Count + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + Domains = @{ + Name = "Domains within forest" + Value = $AdObjects.Data.DomainsCount + Threshold = 1 + Indicator = ">=" + Description = "Discrete number of domains within the forest" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 b/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 new file mode 100644 index 000000000..b6dd506a7 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 @@ -0,0 +1,108 @@ +<# +.SYNOPSIS + Checks AD Forest LDAP Referrals + +.DESCRIPTION + Identifies if forest has external LDAP referrals + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestExternalLdap + + Returns true if AD Forest does not use external LDAP referrals + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestExternalLdap +#> +function Test-MtAdForestExternalLdap { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + $AdObjects.Data.CrossForestReferences = @($AdObjects.Forest.CrossForestReferences) + $AdObjects.Data.CrossForestReferencesCount = ($AdObjects.Data.CrossForestReferences | Measure-Object).Count + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + CrossForestReferences = @{ + #https://learn.microsoft.com/en-us/windows/win32/ad/referrals + Name = "External Cross References for LDAP" + Value = $AdObjects.Data.CrossForestReferencesCount + Threshold = 0 + Indicator = "=" + Description = "Discrete number of external LDAP referrals available" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 b/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 new file mode 100644 index 000000000..79e9b8509 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 @@ -0,0 +1,109 @@ +<# +.SYNOPSIS + Checks AD Forest-level FSMO roles + +.DESCRIPTION + Identifies if forest-level FSMO roles are on the same domain controller + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestFsmoStatus + + Returns true if AD Forest-level FSMO roles are on the same DC + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestFsmoStatus +#> +function Test-MtAdForestFsmoStatus { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Proper name')] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + $AdObjects.Data.DomainNamingMaster = $AdObjects.Forest.DomainNamingMaster + $AdObjects.Data.SchemaMaster = $AdObjects.Forest.SchemaMaster + $AdObjects.Data.CommonFsmo = ($AdObjects.Data.DomainNamingMaster -eq $AdObjects.Data.SchemaMaster) + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + CommonFsmo = @{ + Name = "Forest FSMO roles are on common domain controller" + Value = $AdObjects.Data.CommonFsmo + Threshold = $true + Indicator = "=" + Description = "Validates the Domain Naming Master and Schema Master FSMO roles are on the same domain controller" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 b/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 new file mode 100644 index 000000000..b256cfd3a --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 @@ -0,0 +1,106 @@ +<# +.SYNOPSIS + Checks AD Forest Functional Level + +.DESCRIPTION + Identifies if forest functional level is high enough + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestFunctionalLevel + + Returns true if AD Forest Functional Level is high enough + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestFunctionalLevel +#> +function Test-MtAdForestFunctionalLevel { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + FunctionalLevel = @{ + Name = "Forest Functional Level" + Value = [int]$AdObjects.Data.FunctionalLevel + Threshold = 7 + Indicator = ">=" + Description = "Validates the Forest Functional Level is Windows 2016 or higher" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdForestSite.ps1 b/powershell/public/maester/ad/Test-MtAdForestSite.ps1 new file mode 100644 index 000000000..f1d735434 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestSite.ps1 @@ -0,0 +1,116 @@ +<# +.SYNOPSIS + Checks AD Forest Sites + +.DESCRIPTION + Identifies if forest has Sites + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestSite + + Returns true if AD Forest has sites + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestSite +#> +function Test-MtAdForestSite { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + $AdObjects.Data.Sites = @($AdObjects.Forest.Sites) + $AdObjects.Data.SitesCount = ($AdObjects.Data.Sites | Measure-Object).Count + $AdObjects.Data.DefaultSite = ($AdObjects.Data.Sites -contains "Default-First-Site-Name") + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + Sites = @{ + Name = "Sites configured" + Value = $AdObjects.Data.Sites + Threshold = 2 + Indicator = ">=" + Description = "Discrete number of sites within the forest" + Status = $null + } + DefaultSite = @{ + Name = "Default site exists" + Value = $AdObjects.Data.Sites + Threshold = $true + Indicator = "=" + Description = "Validates the Default-First-Site-Name exists" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 b/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 new file mode 100644 index 000000000..473195132 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 @@ -0,0 +1,118 @@ +<# +.SYNOPSIS + Checks AD Forest Suffixes + +.DESCRIPTION + Identifies if forest has suffixes + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdForestSuffix + + Returns true if AD Forest is using suffixes + +.LINK + https://maester.dev/docs/commands/Test-MtAdForestSuffix +#> +function Test-MtAdForestSuffix { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdForest.SetFlag){ + Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential + } + + $AdObjects = @{ + Forest = $__MtSession.AdCache.AdForest.Forest + Data = $__MtSession.AdCache.AdForest.Data + } + + #region Collect + $AdObjects.Data.UpnSuffixes = @($AdObjects.Forest.UpnSuffixes) + $AdObjects.Data.UpnSuffixesCount = ($AdObjects.Data.UpnSuffixes | Measure-Object).Count + + $AdObjects.Data.SpnSuffixes = @($AdObjects.Forest.SpnSuffixes) + $AdObjects.Data.SpnSuffixesCount = ($AdObjects.Data.SpnSuffixes | Measure-Object).Count + #endregion + + $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest + $__MtSession.AdCache.AdForest.Data = $AdObjects.Data + + #region Analysis + $Tests = @{ + UpnSuffixes = @{ + Name = "UPN Suffixes configured" + Value = $AdObjects.Data.UpnSuffixesCount + Threshold = 1 + Indicator = ">=" + Description = "Discrete number of UPN Suffixes within the forest" + Status = $null + } + SpnSuffixes = @{ + Name = "SPN Suffixes configured" + Value = $AdObjects.Data.SpnSuffixesCount + Threshold = 0 + Indicator = ">=" + Description = "Discrete number of SPN Suffixes within the forest" + Status = $null + } + } + #endregion + + #region Processing + foreach($test in $Tests.GetEnumerator()){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($test in $Tests.GetEnumerator()){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/tests/Maester/ad/Test-MtAd.Tests.ps1 b/tests/Maester/ad/Test-MtAd.Tests.ps1 index e4fd6bc4b..1f85f9862 100644 --- a/tests/Maester/ad/Test-MtAd.Tests.ps1 +++ b/tests/Maester/ad/Test-MtAd.Tests.ps1 @@ -59,4 +59,40 @@ Describe "Maester/Active Directory" -Tag "Maester", "Active Directory", "MT.AD" $result | Should -Be $true -Because "Computer state is proper" } } + It "MT.AD.0101: AD Forest Domains" -Tag "MT.AD.0100","MT.AD.0101","AD Forest" { + $result = Test-MtAdForestDomain + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest has appropriate domains" + } + } + It "MT.AD.0102: AD Forest LDAP Referrals" -Tag "MT.AD.0100","MT.AD.0102","AD Forest" { + $result = Test-MtAdForestExternalLdap + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest does not use external LDAP referrals" + } + } + It "MT.AD.0103: AD Forest FSMO Roles" -Tag "MT.AD.0100","MT.AD.0103","AD Forest" { + $result = Test-MtAdForestFsmoStatus + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest-level FSMO roles are on single DC" + } + } + It "MT.AD.0104: AD Forest Functional Level" -Tag "MT.AD.0100","MT.AD.0104","AD Forest" { + $result = Test-MtAdForestFunctionalLevel + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest Functional Level is within n-1 of highest" + } + } + It "MT.AD.0105: AD Forest Sites" -Tag "MT.AD.0100","MT.AD.0105","AD Forest" { + $result = Test-MtAdForestSite + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest has appropriate sites" + } + } + It "MT.AD.0106: AD Forest Suffixes" -Tag "MT.AD.0100","MT.AD.0106","AD Forest" { + $result = Test-MtAdForestSuffix + if ($null -ne $result){ + $result | Should -Be $true -Because "Forest uses appropriate UPN and SPN suffixes" + } + } } \ No newline at end of file From 3ce081b80c971158fda4250411f645cedb90f9a0 Mon Sep 17 00:00:00 2001 From: Snozz Date: Fri, 19 Dec 2025 05:46:27 -0700 Subject: [PATCH 09/11] AD Domain tests --- powershell/Maester.psd1 | 5 +- powershell/public/Set-MtAdCache.ps1 | 137 +++++++++++++- .../maester/ad/Test-MtAdDomainContainer.ps1 | 138 ++++++++++++++ .../maester/ad/Test-MtAdDomainFsmoStatus.ps1 | 119 +++++++++++++ .../ad/Test-MtAdDomainFunctionalLevel.ps1 | 115 ++++++++++++ .../ad/Test-MtAdDomainMachineAccountQuota.ps1 | 124 +++++++++++++ .../maester/ad/Test-MtAdDomainManagedBy.ps1 | 116 ++++++++++++ .../maester/ad/Test-MtAdDomainNaming.ps1 | 139 +++++++++++++++ .../ad/Test-MtAdDomainPasswordPolicy.ps1 | 168 ++++++++++++++++++ .../maester/ad/Test-MtAdDomainStructure.ps1 | 133 ++++++++++++++ tests/Maester/ad/Test-MtAd.Tests.ps1 | 47 +++++ 11 files changed, 1239 insertions(+), 2 deletions(-) create mode 100644 powershell/public/maester/ad/Test-MtAdDomainContainer.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainFsmoStatus.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainFunctionalLevel.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainMachineAccountQuota.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainManagedBy.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainNaming.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainPasswordPolicy.ps1 create mode 100644 powershell/public/maester/ad/Test-MtAdDomainStructure.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index e89802af1..8d3e20a93 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -191,7 +191,10 @@ 'Test-MtAdComputerOperatingSystem','Test-MtAdComputerPrimaryGroup', 'Test-MtAdComputerService','Test-MtAdComputerSidHistory','Test-MtAdComputerStatus', 'Test-MtAdForestDomain','Test-MtAdForestExternalLdap','Test-MtAdForestFsmoStatus', - 'Test-MtAdForestFunctionalLevel','Test-MtAdForestSite','Test-MtAdForestSuffix' + 'Test-MtAdForestFunctionalLevel','Test-MtAdForestSite','Test-MtAdForestSuffix', + 'Test-MtAdDomainContainer','Test-MtAdDomainFsmoStatus','Test-MtAdDomainFunctionalLevel', + 'Test-MtAdDomainMachineAccountQuota','Test-MtAdDomainManagedBy','Test-MtAdDomainNaming', + 'Test-MtAdDomainPasswordPolicy','Test-MtAdDomainStructure' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. CmdletsToExport = @() diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 index 499986242..226d8fa6d 100644 --- a/powershell/public/Set-MtAdCache.ps1 +++ b/powershell/public/Set-MtAdCache.ps1 @@ -30,7 +30,7 @@ function Set-MtAdCache { param( [string]$Server = $__MtSession.AdServer, [pscredential]$Credential = $__MtSession.AdCredential, - [ValidateSet('All', 'Computers', 'Forest')] + [ValidateSet('All', 'Computers', 'Domains', 'Forest')] [string[]]$Objects = 'All' ) @@ -91,9 +91,144 @@ function Set-MtAdCache { PrimaryGroupIds = $primaryGroupIds DomainControllerPgids = $dcPrimaryGroupIds AdComputers = $__MtSession.AdCache.AdComputers + AdDomains = $__MtSession.AdCache.AdDomains AdForest = $__MtSession.AdCache.AdForest } + + <# + if($Objects -contains "Domains" -or $Objects -contains "All"){ + $domainControllers = @() + $domainControllersSplat = @{ + SearchBase = $d.DomainControllersContainer + Server = $domain + LDAPFilter = "objectClass=Computer" + Properties = "*" + } + $dcs = try{ + Write-Verbose "Attempting AD query for DCs in $domain" + Get-ADObject @domainControllersSplat + }catch{ + Write-Error $_ + return $null + } + $domainControllers += @{ + Domain = $d.Name + DomainControllers = $dcs + } + } + $__MtSession.AdCache.AdDomainControllers = @{ + SetFlag = $true + DomainControllers = @($domainControllers) # TODO Process this + Data = @{} + } + foreach ($domain in $__MtSession.AdCache.AdDomainControllers.Domains){ + $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) + } + } + + #DeletedObjectsContainer = $null + #ForeignSecurityPrincipalsContainer = $null + #LinkedGroupPolicyObjects = $null + #LostAndFoundContainer = $null + #QuotasContainer = $null + #SystemsContainer = $null + #> + + if($Objects -contains "Domains" -or $Objects -contains "All"){ + $forest = try{ + Write-Verbose "Attempting AD query for Forest" + Get-ADForest + }catch{ + Write-Error $_ + return $null + } + + $domains = @() + $domainObjects = @() + foreach ($domain in $forest.Domains){ + $d = try{ + Write-Verbose "Attempting AD query for $domain" + Get-ADDomain -Server $domain + }catch{ + Write-Error $_ + return $null + } + + $do = try{ + Write-Verbose "Attempting AD query for $domain object" + Get-ADObject -Identity $domain.DistinguishedName -Properties * + }catch{ + Write-Error $_ + return $null + } + + $domains += $d + $domainObjects += $do + } + + $__MtSession.AdCache.AdDomains = @{ + SetFlag = $true + Domains = @($domains) + DomainObjects = @($domainObjects) + Data = @{ + Name = $null + NetBIOSName = $null + IsNetBIOSNameCompliant = $null + DistinguishedName = $null + DNSRoot = $null + IsDNSRootCompliant = $null + DomainFunctionalLevel = $null + AllowedDNSSuffixes = @() + AllowedDNSSuffixesCount = $null + InfrastructureMaster = $null + PDCEmulator = $null + RIDMaster = $null + CommonFsmo = $null + ChildDomains = @() + ChildDomainsCount = $null + ComputersContainer = $null + DefaultComputersContainer = $null + DomainControllersContainer = $null + DefaultDomainControllersContainer = $null + UsersContainer = $null + DefaultUsersContainer = $null + ReadOnlyDomainControllers = @() + ReadOnlyDomainControllersCount = $null + DomainControllers = @() + DomainControllersCount = $null + PublicKeyRequiredPasswordRolling = $null + ManagedBy = $null + IsSetManagedBy = $null + ParentDomain = $null + IsRootDomain = $null + MachineAccountQuota = $null + IsDefaultMachineAccountQuota = $null + ForceLogoff = $null + LockoutDuration = $null + LockOutObservationWindow = $null + LockoutThreshold = $null + MaxPwdAge = $null + MinPwdAge = $null + MinPwdLength = $null + PwdHistoryLength = $null + IsDefaultForceLogoff = $null + IsDefaultLockoutDuration = $null + IsDefaultLockOutObservationWindow = $null + IsDefaultLockoutThreshold = $null + IsDefaultMaxPwdAge = $null + IsDefaultMinPwdAge = $null + IsDefaultMinPwdLength = $null + IsDefaultPwdHistoryLength = $null + IsDefaultPasswordPolicy = $null + } + } + + foreach ($domain in $__MtSession.AdCache.AdDomains.Domains){ + $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) + } + } + if($Objects -contains "Forest" -or $Objects -contains "All"){ $forest = try{ Write-Verbose "Attempting AD query for Forest" diff --git a/powershell/public/maester/ad/Test-MtAdDomainContainer.ps1 b/powershell/public/maester/ad/Test-MtAdDomainContainer.ps1 new file mode 100644 index 000000000..0782cc67a --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainContainer.ps1 @@ -0,0 +1,138 @@ +<# +.SYNOPSIS + Checks AD Domain default containers + +.DESCRIPTION + Identifies if default containers are still set + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainContainer + + Returns true if default containers have been updated + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainContainer +#> +function Test-MtAdDomainContainer { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".ComputersContainer = $domain.ComputersContainer + $AdObjects."Data-$($domain.Name)".DefaultComputersContainer = $domain.ComputersContainer -like "CN=Computers,$($domain.DistinguishedName)" + + $AdObjects."Data-$($domain.Name)".UsersContainer = $domain.UsersContainer + $AdObjects."Data-$($domain.Name)".DefaultUsersContainerContainer = $domain.UsersContainer -like "CN=Users,$($domain.DistinguishedName)" + + $AdObjects."Data-$($domain.Name)".DomainControllersContainer = $domain.DomainControllersContainer + $AdObjects."Data-$($domain.Name)".DefaultDomainControllersContainer = $domain.DomainControllersContainer -like "OU=Domain Controllers,$($domain.DistinguishedName)" + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + DefaultComputersContainer = @{ + Name = "Checks configuration of default computers container" + Value = $AdObjects."Data-$($domain.Name)".DefaultComputersContainer + Threshold = $false + Indicator = "=" + Description = "Checks that the default computers container is not in use" + Status = $null + } + DefaultUsersContainerContainer = @{ + Name = "Checks configuration of default users container" + Value = $AdObjects."Data-$($domain.Name)".DefaultUsersContainerContainer + Threshold = $false + Indicator = "=" + Description = "Checks that the default users container is not in use" + Status = $null + } + DefaultDomainControllersContainer = @{ + Name = "Checks configuration of default domain controllers container" + Value = $AdObjects."Data-$($domain.Name)".DefaultDomainControllersContainer + Threshold = $false + Indicator = "=" + Description = "Checks that the default domain controllers container is not in use" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainFsmoStatus.ps1 b/powershell/public/maester/ad/Test-MtAdDomainFsmoStatus.ps1 new file mode 100644 index 000000000..0c47ed523 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainFsmoStatus.ps1 @@ -0,0 +1,119 @@ +<# +.SYNOPSIS + Checks AD Domain FSMO Status + +.DESCRIPTION + Identifies if FSMO roles are on a single DC + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainFsmoStatus + + Returns true if AD domain FSMO roles are on single DC + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainFsmoStatus +#> +function Test-MtAdDomainFsmoStatus { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'Proper name')] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".InfrastructureMaster = $domain.InfrastructureMaster + $AdObjects."Data-$($domain.Name)".PDCEmulator = $domain.PDCEmulator + $AdObjects."Data-$($domain.Name)".RIDMaster = $domain.RIDMaster + $AdObjects."Data-$($domain.Name)".CommonFsmo = $domain.InfrastructureMaster -eq $domain.PDCEmulator -eq $domain.RIDMaster + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + CommonFsmo = @{ + Name = "All domain-level FSMO roles are on a single DC" + Value = $AdObjects."Data-$($domain.Name)".CommonFsmo + Threshold = $true + Indicator = "=" + Description = "Checks that all domain-level FSMO roles are on a single DC" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainFunctionalLevel.ps1 b/powershell/public/maester/ad/Test-MtAdDomainFunctionalLevel.ps1 new file mode 100644 index 000000000..b0560c344 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainFunctionalLevel.ps1 @@ -0,0 +1,115 @@ +<# +.SYNOPSIS + Checks AD Domain Functional Level + +.DESCRIPTION + Identifies if DFL is adequate + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainFunctionalLevel + + Returns true if AD DFL is 7 or higher + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainFunctionalLevel +#> +function Test-MtAdDomainFunctionalLevel { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".DomainFunctionalLevel = $domain.DomainMode + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + DomainFunctionalLevel = @{ + Name = "" + Value = $AdObjects."Data-$($domain.Name)".DomainFunctionalLevel + Threshold = 7 + Indicator = ">=" + Description = "Checks the DNS Root alignment with RFCs" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainMachineAccountQuota.ps1 b/powershell/public/maester/ad/Test-MtAdDomainMachineAccountQuota.ps1 new file mode 100644 index 000000000..3d2971bcf --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainMachineAccountQuota.ps1 @@ -0,0 +1,124 @@ +<# +.SYNOPSIS + Checks AD Domain machine account quota + +.DESCRIPTION + Identifies if domain machine account quota is set + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainMachineAccountQuota + + Returns true if AD machine account quota is 0 + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainMachineAccountQuota +#> +function Test-MtAdDomainMachineAccountQuota { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".MachineAccountQuota = $domain.MachineAccountQuota + $AdObjects."Data-$($domain.Name)".IsDefaultMachineAccountQuota = (10 -eq $domain.MachineAccountQuota) + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + MachineAccountQuota = @{ + Name = "Machine Account Quota" + Value = $AdObjects."Data-$($domain.Name)".MachineAccountQuota + Threshold = 0 + Indicator = "=" + Description = "Checks that the default machine account quota is disabled" + Status = $null + } + IsDefaultMachineAccountQuota = @{ + Name = "Machine Account Quota Default" + Value = $AdObjects."Data-$($domain.Name)".IsDefaultMachineAccountQuota + Threshold = $false + Indicator = "=" + Description = "Checks that the default machine account quota is not the default" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainManagedBy.ps1 b/powershell/public/maester/ad/Test-MtAdDomainManagedBy.ps1 new file mode 100644 index 000000000..d07990b02 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainManagedBy.ps1 @@ -0,0 +1,116 @@ +<# +.SYNOPSIS + Checks AD Domain Managed By + +.DESCRIPTION + Identifies if domain managed by attribute is set + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainManagedBy + + Returns true if AD Domain managed by is not empty + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainManagedBy +#> +function Test-MtAdDomainManagedBy { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".ManagedBy = $domain.ManagedBy + $AdObjects."Data-$($domain.Name)".IsSetManagedBy = ($null -ne $domain.ManagedBy) + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + IsSetManagedBy = @{ + Name = "Managed By is set" + Value = $AdObjects."Data-$($domain.Name)".IsSetManagedBy + Threshold = $true + Indicator = "=" + Description = "Checks that the domain has a managed by contact" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainNaming.ps1 b/powershell/public/maester/ad/Test-MtAdDomainNaming.ps1 new file mode 100644 index 000000000..34e5a02de --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainNaming.ps1 @@ -0,0 +1,139 @@ +<# +.SYNOPSIS + Checks AD Domain names + +.DESCRIPTION + Identifies if domain names meet requirements + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainNaming + + Returns true if AD Domain names meet requirements + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainNaming +#> +function Test-MtAdDomainNaming { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".NetBIOSName = $domain.NetBIOSName + #Standards for internet domain names (RFCs 952, 1035, 1123) + $AdObjects."Data-$($domain.Name)".IsNetBIOSNameCompliant = ($domain.NetBIOSName -match "^([a-zA-Z0-9]{0,15})?$") + + $AdObjects."Data-$($domain.Name)".DNSRoot = $domain.DNSRoot + $AdObjects."Data-$($domain.Name)".IsDNSRootCompliant = ($domain.DNSRoot -match "^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?$") + + $AdObjects."Data-$($domain.Name)".AllowedDNSSuffixes = $domain.AllowedDNSSuffixes + $AdObjects."Data-$($domain.Name)".AllowedDNSSuffixesCount = ($AdObjects."Data-$($domain.Name)".AllowedDNSSuffixes | Measure-Object).Count + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + IsNetBIOSNameCompliant = @{ + Name = "NetBIOS Name aligns with standards" + Value = $AdObjects."Data-$($domain.Name)".IsNetBIOSNameCompliant + Threshold = $true + Indicator = "=" + Description = "Checks the NetBIOS Name alignment with RFCs" + Status = $null + } + IsDNSRootCompliant = @{ + Name = "DNS Root aligns with standards" + Value = $AdObjects."Data-$($domain.Name)".IsDNSRootCompliant + Threshold = $true + Indicator = "=" + Description = "Checks the DNS Root alignment with RFCs" + Status = $null + } + AllowedDNSSuffixesCount = @{ + Name = "DNS Suffixes allowed" + Value = $AdObjects."Data-$($domain.Name)".AllowedDNSSuffixesCount + Threshold = 0 + Indicator = ">=" + Description = "Checks the number of DNS Suffixes configured" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainPasswordPolicy.ps1 b/powershell/public/maester/ad/Test-MtAdDomainPasswordPolicy.ps1 new file mode 100644 index 000000000..ec2fcd239 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainPasswordPolicy.ps1 @@ -0,0 +1,168 @@ +<# +.SYNOPSIS + Checks AD Domain Password Policies + +.DESCRIPTION + Identifies if Domain password policies are set + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainPasswordPolicy + + Returns true if default domain password policy has been modified + and passwords are auto-rotated for passwordless users + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainPasswordPolicy +#> +function Test-MtAdDomainPasswordPolicy { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".PublicKeyRequiredPasswordRolling = $domain.PublicKeyRequiredPasswordRolling + } + + foreach($domainObject in $AdObjects.DomainObjects){ + $AdObjects."Data-$($domainObject.Name)".ForceLogoff = $domainObject.ForceLogoff + #-9223372036854775808 is the maximum value for a 64-bit signed integer + $AdObjects."Data-$($domainObject.Name)".IsDefaultForceLogoff = (-9223372036854775808 -eq $domainObject.ForceLogoff) + + $AdObjects."Data-$($domainObject.Name)".LockoutDuration = $domainObject.LockoutDuration + #-6000000000 is nanosecond representation for minutes + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockoutDuration = (-6000000000 -eq $domainObject.LockoutDuration) + + $AdObjects."Data-$($domainObject.Name)".LockOutObservationWindow = $domainObject.LockOutObservationWindow + #-6000000000 is nanosecond representation for minutes + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockOutObservationWindow = (-6000000000 -eq $domainObject.LockOutObservationWindow) + + $AdObjects."Data-$($domainObject.Name)".LockoutThreshold = $domainObject.LockoutThreshold + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockoutThreshold = (0 -eq $domainObject.LockoutThreshold) + + $AdObjects."Data-$($domainObject.Name)".MaxPwdAge = $domainObject.MaxPwdAge + #-36288000000000 is the number of 100-nanosecond intervals + #-36288000000000*100 = -3628800000000000 nanoseconds / 8.64e+13 = -42 days + $AdObjects."Data-$($domainObject.Name)".IsDefaultMaxPwdAge = (-36288000000000 -eq $domainObject.MaxPwdAge) + + $AdObjects."Data-$($domainObject.Name)".MinPwdAge = $domainObject.MinPwdAge + #-864000000000 is the number of 100-nanosecond intervals + #-864000000000*100 = -86400000000000 nanoseconds / 8.64e+13 = -1 days + $AdObjects."Data-$($domainObject.Name)".IsDefaultMinPwdAge = (-864000000000 -eq $domainObject.MinPwdAge) + + $AdObjects."Data-$($domainObject.Name)".MinPwdLength = $domainObject.MinPwdLength + $AdObjects."Data-$($domainObject.Name)".IsDefaultMinPwdLength = (7 -eq $domainObject.MinPwdLength) + + $AdObjects."Data-$($domainObject.Name)".PwdHistoryLength = $domainObject.PwdHistoryLength + $AdObjects."Data-$($domainObject.Name)".IsDefaultPwdHistoryLength = (24 -eq $domainObject.PwdHistoryLength) + + $AdObjects."Data-$($domainObject.Name)".DefaultPasswordPolicy = $false -notin @( + $AdObjects."Data-$($domainObject.Name)".IsDefaultForceLogoff, + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockoutDuration, + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockOutObservationWindow, + $AdObjects."Data-$($domainObject.Name)".IsDefaultLockoutThreshold, + $AdObjects."Data-$($domainObject.Name)".IsDefaultMaxPwdAge, + $AdObjects."Data-$($domainObject.Name)".IsDefaultMinPwdAge, + $AdObjects."Data-$($domainObject.Name)".IsDefaultMinPwdLength, + $AdObjects."Data-$($domainObject.Name)".IsDefaultPwdHistoryLength + ) + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + PublicKeyRequiredPasswordRolling = @{ + Name = "Require password rotation for passwordless accounts" + Value = $AdObjects."Data-$($domain.Name)".PublicKeyRequiredPasswordRolling + Threshold = $true + Indicator = "=" + Description = "Checks that passwords on accounts configured for passwordless are automatically rotated" + Status = $null + } + DefaultPasswordPolicy = @{ + Name = "Checks state of domain password policy" + Value = $AdObjects."Data-$($domain.Name)".DefaultPasswordPolicy + Threshold = $false + Indicator = "=" + Description = "Checks that the domain password policy has been updated" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/powershell/public/maester/ad/Test-MtAdDomainStructure.ps1 b/powershell/public/maester/ad/Test-MtAdDomainStructure.ps1 new file mode 100644 index 000000000..78a577dd3 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdDomainStructure.ps1 @@ -0,0 +1,133 @@ +<# +.SYNOPSIS + Checks AD Domain structure + +.DESCRIPTION + Identifies if DCs, RODCs, and child domains are adequate + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.EXAMPLE + Test-MtAdDomainStructure + + Returns true if no RODCs and >= 2 DCs + +.LINK + https://maester.dev/docs/commands/Test-MtAdDomainStructure +#> +function Test-MtAdDomainStructure { + [CmdletBinding()] + [OutputType([bool])] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential + ) + + if ('ActiveDirectory' -notin $__MtSession.Connections -and 'All' -notin $__MtSession.Connections ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdDomains.SetFlag){ + Set-MtAdCache -Objects "Domains" -Server $Server -Credential $Credential + } + + $AdObjects = $__MtSession.AdCache.AdDomains + + #region Collect + foreach($domain in $AdObjects.Domains){ + $AdObjects."Data-$($domain.Name)".Name = $domain.Name + + $AdObjects."Data-$($domain.Name)".ChildDomains = $domain.ChildDomains + $AdObjects."Data-$($domain.Name)".ChildDomainsCount = ($AdObjects."Data-$($domain.Name)".ChildDomains | Measure-Object).Count + + $AdObjects."Data-$($domain.Name)".ReadOnlyDomainControllers = $domain.ReadOnlyReplicaDirectoryServers + $AdObjects."Data-$($domain.Name)".ReadOnlyDomainControllersCount = ($AdObjects."Data-$($domain.Name)".ReadOnlyDomainControllers | Measure-Object).Count + + $AdObjects."Data-$($domain.Name)".DomainControllers = $domain.ReplicaDirectoryServers + $AdObjects."Data-$($domain.Name)".DomainControllersCount = ($AdObjects."Data-$($domain.Name)".DomainControllers | Measure-Object).Count + + $AdObjects."Data-$($domain.Name)".ParentDomain = $domain.ParentDomain + $AdObjects."Data-$($domain.Name)".IsRootDomain = ($null -eq $domain.ParentDomain) + } + #endregion + + $__MtSession.AdCache.AdDomains = $AdObjects + + #region Analysis + $DomainTests = @() + foreach($domain in $AdObjects.Domains){ + $Tests = @{ + Domain = $domain.Name + ReadOnlyDomainControllersCount = @{ + Name = "Number of RODCs" + Value = $AdObjects."Data-$($domain.Name)".ReadOnlyDomainControllersCount + Threshold = 0 + Indicator = "=" + Description = "Checks that Read Only Domain Controllers (RODC) are not in use" + Status = $null + } + DomainControllersCount = @{ + Name = "Number of writeable DCs" + Value = $AdObjects."Data-$($domain.Name)".DomainControllersCount + Threshold = 2 + Indicator = ">=" + Description = "Checks that at least 2 writeable domain controllers are in use" + Status = $null + } + } + $DomainTests += $Tests + } + #endregion + + #region Processing + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + switch($test.Value.Indicator){ + "=" { + $test.Value.Status = $test.Value.Value -eq $test.Value.Threshold + } + "<" { + $test.Value.Status = $test.Value.Value -lt $test.Value.Threshold + } + "<=" { + $test.Value.Status = $test.Value.Value -le $test.Value.Threshold + } + ">" { + $test.Value.Status = $test.Value.Value -gt $test.Value.Threshold + } + ">=" { + $test.Value.Status = $test.Value.Value -ge $test.Value.Threshold + } + } + } + } + + $result = $true + $testResultMarkdown = $null + foreach($domain in $DomainTests){ + foreach($test in $domain.GetEnumerator()|Where-Object{$_.Name -ne "Domain"}){ + [int]$result *= [int]$test.Value.Status + + $testResultMarkdown += "#### $($test.Value.Name)`n`n" + $testResultMarkdown += "$($test.Value.Description)`n`n" + $testResultMarkdown += "| Current State Value | Comparison | Threshold |`n" + $testResultMarkdown += "| - | - | - |`n" + $testResultMarkdown += "| $($test.Value.Value) | $($test.Value.Indicator) | $($test.Value.threshold) |`n`n" + if($test.Value.Status){ + $testResultMarkdown += "Well done. Your current state is in alignment with the threshold.`n`n" + }else{ + $testResultMarkdown += "Your current state is **NOT** in alignment with the threshold.`n`n" + } + } + } + + Add-MtTestResultDetail -Result $testResultMarkdown + return [bool]$result + #endregion +} diff --git a/tests/Maester/ad/Test-MtAd.Tests.ps1 b/tests/Maester/ad/Test-MtAd.Tests.ps1 index 1f85f9862..235f1ea6a 100644 --- a/tests/Maester/ad/Test-MtAd.Tests.ps1 +++ b/tests/Maester/ad/Test-MtAd.Tests.ps1 @@ -95,4 +95,51 @@ Describe "Maester/Active Directory" -Tag "Maester", "Active Directory", "MT.AD" $result | Should -Be $true -Because "Forest uses appropriate UPN and SPN suffixes" } } + It "MT.AD.0201: AD Domain Containers" -Tag "MT.AD.0200","MT.AD.0201","AD Domain" { + $result = Test-MtAdDomainContainer + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain containers are used properly" + } + } + It "MT.AD.0202: AD Domain FSMO Status" -Tag "MT.AD.0200","MT.AD.0202","AD Domain" { + $result = Test-MtAdDomainFsmoStatus + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain FSMO roles are on common DC" + } + } + It "MT.AD.0203: AD Domain Functional Level" -Tag "MT.AD.0200","MT.AD.0203","AD Domain" { + $result = Test-MtAdDomainFunctionalLevel + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Functional Level is modern" + } + } + It "MT.AD.0204: AD Domain Machine Account Quota" -Tag "MT.AD.0200","MT.AD.0204","AD Domain" { + $result = Test-MtAdDomainMachineAccountQuota + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Machine Account Quota is disabled" + } + } + It "MT.AD.0205: AD Domain Managed By" -Tag "MT.AD.0200","MT.AD.0205","AD Domain" { + $result = Test-MtAdDomainManagedBy + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Managed By is set" + } + } + It "MT.AD.0206: AD Domain Naming" -Tag "MT.AD.0200","MT.AD.0206","AD Domain" { + $result = Test-MtAdDomainNaming + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Naming conforms to standards" + } + } + It "MT.AD.0207: AD Domain Password Policy" -Tag "MT.AD.0200","MT.AD.0207","AD Domain" { + $result = Test-MtAdDomainPasswordPolicy + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Password Policy is configured" + } + It "MT.AD.0208: AD Domain Structure" -Tag "MT.AD.0200","MT.AD.0208","AD Domain" { + $result = Test-MtAdDomainStructure + if ($null -ne $result){ + $result | Should -Be $true -Because "Domain Structure is adequate" + } + } } \ No newline at end of file From 2566ac583fa21c28a07a7b02e3fce156c02786c8 Mon Sep 17 00:00:00 2001 From: Snozz Date: Sat, 20 Dec 2025 06:18:08 -0700 Subject: [PATCH 10/11] Cleanup --- powershell/public/Set-MtAdCache.ps1 | 369 ------------------ .../maester/ad/Test-MtAdComputerContainer.ps1 | 1 - .../ad/Test-MtAdComputerCreatorSid.ps1 | 1 - .../maester/ad/Test-MtAdComputerDns.ps1 | 1 - .../ad/Test-MtAdComputerDomainController.ps1 | 1 - .../maester/ad/Test-MtAdComputerKerberos.ps1 | 1 - .../ad/Test-MtAdComputerOperatingSystem.ps1 | 1 - .../ad/Test-MtAdComputerPrimaryGroup.ps1 | 1 - .../maester/ad/Test-MtAdComputerService.ps1 | 1 - .../ad/Test-MtAdComputerSidHistory.ps1 | 1 - .../maester/ad/Test-MtAdComputerStatus.ps1 | 1 - .../maester/ad/Test-MtAdForestDomain.ps1 | 1 - .../ad/Test-MtAdForestExternalLdap.ps1 | 1 - .../maester/ad/Test-MtAdForestFsmoStatus.ps1 | 1 - .../ad/Test-MtAdForestFunctionalLevel.ps1 | 12 - .../public/maester/ad/Test-MtAdForestSite.ps1 | 1 - .../maester/ad/Test-MtAdForestSuffix.ps1 | 1 - 17 files changed, 396 deletions(-) delete mode 100644 powershell/public/Set-MtAdCache.ps1 diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 deleted file mode 100644 index 226d8fa6d..000000000 --- a/powershell/public/Set-MtAdCache.ps1 +++ /dev/null @@ -1,369 +0,0 @@ -<# -.SYNOPSIS - Sets the local cache of AD lookups. - -.DESCRIPTION - By default all AD queries are cached and re-used for the duration of the session. - - Use this function to set the cache. - -.PARAMETER Server - Server name to pass through to the AD Cmdlets - -.PARAMETER Credential - Credential object to pass through to the AD Cmdlets - -.PARAMETER Objects - Specific type of AD objects to query. - -.EXAMPLE - Set-MtAdCache - - This example sets the cache of AD queries. - -.LINK - https://maester.dev/docs/commands/Set-MtAdCache -#> -function Set-MtAdCache { - [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal function')] - param( - [string]$Server = $__MtSession.AdServer, - [pscredential]$Credential = $__MtSession.AdCredential, - [ValidateSet('All', 'Computers', 'Domains', 'Forest')] - [string[]]$Objects = 'All' - ) - - if($Server){ - $PSDefaultParameterValues.Add("Get-Ad*:Server",$Server) - } - - if($Credential){ - $PSDefaultParameterValues.Add("Get-Ad*:Credential",$Credential) - } - - $primaryGroupIds = @(515,516,521) - $dcPrimaryGroupIds = @(516,521) - - $rootDse = try{ - Write-Verbose "Attempting Get-AdRootDSE" - Get-AdRootDSE - }catch{ - Write-Error $_ - return $null - } - - $configurationNamingContext = $rootDse.configurationNamingContext - $ADObjectDirectoryService = @{ - Identity = "CN=Directory Service,CN=Windows NT,CN=Services,$configurationNamingContext" - Properties = "*" - } - $directoryService = try{ - Write-Verbose "Attempting query for Directory Service object" - Get-ADObject @ADObjectDirectoryService - }catch{ - Write-Error $_ - return $null - } - - $HostSpnAlias = (($directoryService).spnmappings | Where-Object { - $_ -like "host=*" - }) -replace "host=" -split "," - - $Thresholds = @{ - DormantThresholdInDays = 90 - DormantDate = $null - ExpiredThresholdInDays = 180 - ExpiredDate = $null - StaleThresholdInDays = 30 - StaleDate = $null - } - $Thresholds.DormantDate = ((Get-Date).AddDays(-$Thresholds.DormantThresholdInDays)).Date - $Thresholds.ExpiredDate = ((Get-Date).AddDays(-$Thresholds.ExpiredThresholdInDays)).Date - $Thresholds.StaleDate = ((Get-Date).AddDays(-$Thresholds.StaleThresholdInDays)).Date - - $__MtSession.AdCache = @{ - RootDSE = $rootDse - ConfigurationNamingContext = $configurationNamingContext - DirectoryServiceConfigObj = $directoryService - Thresholds = $Thresholds - HostSpnAlias = @($HostSpnAlias) - PrimaryGroupIds = $primaryGroupIds - DomainControllerPgids = $dcPrimaryGroupIds - AdComputers = $__MtSession.AdCache.AdComputers - AdDomains = $__MtSession.AdCache.AdDomains - AdForest = $__MtSession.AdCache.AdForest - } - - - <# - if($Objects -contains "Domains" -or $Objects -contains "All"){ - $domainControllers = @() - $domainControllersSplat = @{ - SearchBase = $d.DomainControllersContainer - Server = $domain - LDAPFilter = "objectClass=Computer" - Properties = "*" - } - $dcs = try{ - Write-Verbose "Attempting AD query for DCs in $domain" - Get-ADObject @domainControllersSplat - }catch{ - Write-Error $_ - return $null - } - $domainControllers += @{ - Domain = $d.Name - DomainControllers = $dcs - } - } - $__MtSession.AdCache.AdDomainControllers = @{ - SetFlag = $true - DomainControllers = @($domainControllers) # TODO Process this - Data = @{} - } - foreach ($domain in $__MtSession.AdCache.AdDomainControllers.Domains){ - $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) - } - } - - #DeletedObjectsContainer = $null - #ForeignSecurityPrincipalsContainer = $null - #LinkedGroupPolicyObjects = $null - #LostAndFoundContainer = $null - #QuotasContainer = $null - #SystemsContainer = $null - #> - - if($Objects -contains "Domains" -or $Objects -contains "All"){ - $forest = try{ - Write-Verbose "Attempting AD query for Forest" - Get-ADForest - }catch{ - Write-Error $_ - return $null - } - - $domains = @() - $domainObjects = @() - foreach ($domain in $forest.Domains){ - $d = try{ - Write-Verbose "Attempting AD query for $domain" - Get-ADDomain -Server $domain - }catch{ - Write-Error $_ - return $null - } - - $do = try{ - Write-Verbose "Attempting AD query for $domain object" - Get-ADObject -Identity $domain.DistinguishedName -Properties * - }catch{ - Write-Error $_ - return $null - } - - $domains += $d - $domainObjects += $do - } - - $__MtSession.AdCache.AdDomains = @{ - SetFlag = $true - Domains = @($domains) - DomainObjects = @($domainObjects) - Data = @{ - Name = $null - NetBIOSName = $null - IsNetBIOSNameCompliant = $null - DistinguishedName = $null - DNSRoot = $null - IsDNSRootCompliant = $null - DomainFunctionalLevel = $null - AllowedDNSSuffixes = @() - AllowedDNSSuffixesCount = $null - InfrastructureMaster = $null - PDCEmulator = $null - RIDMaster = $null - CommonFsmo = $null - ChildDomains = @() - ChildDomainsCount = $null - ComputersContainer = $null - DefaultComputersContainer = $null - DomainControllersContainer = $null - DefaultDomainControllersContainer = $null - UsersContainer = $null - DefaultUsersContainer = $null - ReadOnlyDomainControllers = @() - ReadOnlyDomainControllersCount = $null - DomainControllers = @() - DomainControllersCount = $null - PublicKeyRequiredPasswordRolling = $null - ManagedBy = $null - IsSetManagedBy = $null - ParentDomain = $null - IsRootDomain = $null - MachineAccountQuota = $null - IsDefaultMachineAccountQuota = $null - ForceLogoff = $null - LockoutDuration = $null - LockOutObservationWindow = $null - LockoutThreshold = $null - MaxPwdAge = $null - MinPwdAge = $null - MinPwdLength = $null - PwdHistoryLength = $null - IsDefaultForceLogoff = $null - IsDefaultLockoutDuration = $null - IsDefaultLockOutObservationWindow = $null - IsDefaultLockoutThreshold = $null - IsDefaultMaxPwdAge = $null - IsDefaultMinPwdAge = $null - IsDefaultMinPwdLength = $null - IsDefaultPwdHistoryLength = $null - IsDefaultPasswordPolicy = $null - } - } - - foreach ($domain in $__MtSession.AdCache.AdDomains.Domains){ - $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) - } - } - - if($Objects -contains "Forest" -or $Objects -contains "All"){ - $forest = try{ - Write-Verbose "Attempting AD query for Forest" - Get-ADForest - }catch{ - Write-Error $_ - return $null - } - - $__MtSession.AdCache.AdForest = @{ - SetFlag = $true - Forest = @($forest) - Data = @{ - FunctionalLevel = $rootDse.forestFunctionality - CrossForestReferences = @() - CrossForestReferencesCount = $null - DomainNamingMaster = $null - SchemaMaster = $null - CommonFsmo = $null - Sites = @() - SitesCount = $null - DefaultSite = $null - Domains = @() - DomainsCount = $null - UpnSuffixes = @() - UpnSuffixesCount = $null - SpnSuffixes = @() - SpnSuffixesCount = $null - } - } - } - - if($Objects -contains "Computers" -or $Objects -contains "All"){ - $computers = try{ - Write-Verbose "Attempting AD query for Computers" - Get-ADComputer -Filter * -Properties * - }catch{ - Write-Error $_ - return $null - } - - $__MtSession.AdCache.AdComputers = @{ - SetFlag = $true - Computers = @($computers) - Data = @{ - ComputersCount = $(($computers | Measure-Object).Count) - EnabledComputers = @() - EnabledComputersCount = $null - DisabledComputers = @() - DisabledComputersCount = $null - DormantComputers = @() - DormantComputersCount = $null - ExpiredComputers = @() - ExpiredComputersCount = $null - StaleComputers = @() - StaleComputersCount = $null - StaleComputersRatio = $null - NonPgIdCoumputers = @() - NonPgIdCoumputersCount = $null - SidHistoryComputers = @() - SidHistoryComputersCount = $null - ContainerComputers = @() - ContainerComputersCount = $null - BaseDns = @() - BaseDnCount = $null - BaseDnAvg = $null - LowBaseDns = @() - LowBaseDnsCount = $null - CreatorSidComputers = @() - CreatorSidComputersCount = $null - DomainControllers = @() - DomainControllersCount = $null - ServiceClasses = @() - ServiceClassesCount = $null - ServiceClassesComputers = @() - ServiceClassesComputersCount = $null - UnknownServiceClasses = @() - UnknownServiceClassesCount = $null - HostBypassComputers = @() - HostBypassComputersCount = $null - ServiceHosts = @() - ServiceHostsCount = $null - ServiceHostsComputers = @() - ServiceHostsComputersCount = $null - ServiceNoFqdnComputers = @() - ServiceNoFqdnComputersCount = $null - ServiceDnsBypassComputers = @() - ServiceDnsBypassComputersCount = $null - DnsZones = @() - DnsZonesCount = $null - DnsZoneAvg = $null - LowDnsZones = @() - LowDnsZonesCount = $null - NoDnsComputers = @() - NoDnsComputersCount = $null - DnsOverlapComputers = @() - DnsOverlapComputersCount = $null - UnconstrainedComputers = @() - UnconstrainedComputersCount = $null - KcdComputers = @() - KcdComputersCount = $null - S4U2SelfComputers = @() - S4U2SelfComputersCount = $null - RbcdComputers = @() - RbcdComputersCount = $null - MissingSpnsComputers = @() - MissingSpnsComputersCount = $null - OperatingSystems = @() - OperatingSystemsCount = $null - NoOperatingSystem = $() - NoOperatingSystemCount = $null - OperatingSystemAvg = $null - LowOperatingSystem = $() - LowOperatingSystemCount = $null - DisabledComputersRatio = $null - DormantComputersRatio = $null - ExpiredComputersRatio = $null - NonPgIdComputersRatio = $null - SidHistoryComputersRatio = $null - ContainerComputersRatio = $null - CreatorSidComputersRatio = $null - ServiceClassesComputersRatio = $null - HostBypassComputersRatio = $null - ServiceHostsRatio = $null - ServiceHostsComputersRatio = $null - ServiceDnsBypassComputersRatio = $null - NoDnsComputersRatio = $null - DnsOverlapComputersRatio = $null - UnconstrainedComputersRatio = $null - KcdComputersRatio = $null - S4U2SelfComputersRatio = $null - RbcdComputersRatio = $null - MissingSpnsComputersRatio = $null - NoOperatingSystemRatio = $null - } - } - } -} \ No newline at end of file diff --git a/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 index 0b8205d5c..947cf8f57 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 @@ -65,7 +65,6 @@ function Test-MtAdComputerContainer { $AdObjects.Data.LowBaseDnsCount = ($AdObjects.Data.LowBaseDns | Measure-Object).Count #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 index 00de4fed2..a6734f891 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 @@ -52,7 +52,6 @@ function Test-MtAdComputerCreatorSid { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 index dea4e376d..4b7c2de33 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 @@ -75,7 +75,6 @@ function Test-MtAdComputerDns { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 index 5bfb90f2b..b89daaf51 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 @@ -49,7 +49,6 @@ function Test-MtAdComputerDomainController { $AdObjects.Data.DomainControllersCount = ($AdObjects.Data.DomainControllers | Measure-Object).Count #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 index 5b43be718..9f552a4e0 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 @@ -91,7 +91,6 @@ function Test-MtAdComputerKerberos { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 index c2ceccb05..992c1d0df 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 @@ -64,7 +64,6 @@ function Test-MtAdComputerOperatingSystem { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 index fb53b9e77..ee061dbc5 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 @@ -52,7 +52,6 @@ function Test-MtAdComputerPrimaryGroup { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 index 8979010d9..b25f78e4f 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerService.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 @@ -315,7 +315,6 @@ XicNotifier,Honeywell Notifier }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 index 3cb1ccbb0..7d54b07e0 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 @@ -52,7 +52,6 @@ function Test-MtAdComputerSidHistory { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 index bdd7029f5..8157f43c0 100644 --- a/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 @@ -82,7 +82,6 @@ function Test-MtAdComputerStatus { }catch{0} #endregion - $__MtSession.AdCache.AdComputers.Computers = $AdObjects.Computers $__MtSession.AdCache.AdComputers.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 index 8d15a9ea3..577bede24 100644 --- a/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 @@ -47,7 +47,6 @@ function Test-MtAdForestDomain { $AdObjects.Data.DomainsCount = ($AdObjects.Data.Domains | Measure-Object).Count #endregion - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest $__MtSession.AdCache.AdForest.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 b/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 index b6dd506a7..39778ea53 100644 --- a/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 @@ -47,7 +47,6 @@ function Test-MtAdForestExternalLdap { $AdObjects.Data.CrossForestReferencesCount = ($AdObjects.Data.CrossForestReferences | Measure-Object).Count #endregion - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest $__MtSession.AdCache.AdForest.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 b/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 index 79e9b8509..998852ee6 100644 --- a/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 @@ -49,7 +49,6 @@ function Test-MtAdForestFsmoStatus { $AdObjects.Data.CommonFsmo = ($AdObjects.Data.DomainNamingMaster -eq $AdObjects.Data.SchemaMaster) #endregion - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest $__MtSession.AdCache.AdForest.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 b/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 index b256cfd3a..56fb6e2dd 100644 --- a/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 @@ -37,18 +37,6 @@ function Test-MtAdForestFunctionalLevel { Set-MtAdCache -Objects "Forest" -Server $Server -Credential $Credential } - $AdObjects = @{ - Forest = $__MtSession.AdCache.AdForest.Forest - Data = $__MtSession.AdCache.AdForest.Data - } - - #region Collect - - #endregion - - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest - $__MtSession.AdCache.AdForest.Data = $AdObjects.Data - #region Analysis $Tests = @{ FunctionalLevel = @{ diff --git a/powershell/public/maester/ad/Test-MtAdForestSite.ps1 b/powershell/public/maester/ad/Test-MtAdForestSite.ps1 index f1d735434..ffb1dfb19 100644 --- a/powershell/public/maester/ad/Test-MtAdForestSite.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestSite.ps1 @@ -48,7 +48,6 @@ function Test-MtAdForestSite { $AdObjects.Data.DefaultSite = ($AdObjects.Data.Sites -contains "Default-First-Site-Name") #endregion - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest $__MtSession.AdCache.AdForest.Data = $AdObjects.Data #region Analysis diff --git a/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 b/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 index 473195132..9ba6355ab 100644 --- a/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 +++ b/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 @@ -50,7 +50,6 @@ function Test-MtAdForestSuffix { $AdObjects.Data.SpnSuffixesCount = ($AdObjects.Data.SpnSuffixes | Measure-Object).Count #endregion - $__MtSession.AdCache.AdForest.Forest = $AdObjects.Forest $__MtSession.AdCache.AdForest.Data = $AdObjects.Data #region Analysis From aa7374e4ffb7ec41e8aad0b1d30a2c5b0023f15f Mon Sep 17 00:00:00 2001 From: Snozz Date: Sat, 20 Dec 2025 06:48:03 -0700 Subject: [PATCH 11/11] Missing file --- powershell/public/Set-MtAdCache.ps1 | 439 ++++++++++++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 powershell/public/Set-MtAdCache.ps1 diff --git a/powershell/public/Set-MtAdCache.ps1 b/powershell/public/Set-MtAdCache.ps1 new file mode 100644 index 000000000..71d3e2fca --- /dev/null +++ b/powershell/public/Set-MtAdCache.ps1 @@ -0,0 +1,439 @@ +<# +.SYNOPSIS + Sets the local cache of AD lookups. + +.DESCRIPTION + By default all AD queries are cached and re-used for the duration of the session. + + Use this function to set the cache. + +.PARAMETER Server + Server name to pass through to the AD Cmdlets + +.PARAMETER Credential + Credential object to pass through to the AD Cmdlets + +.PARAMETER Objects + Specific type of AD objects to query. + +.EXAMPLE + Set-MtAdCache + + This example sets the cache of AD queries. + +.LINK + https://maester.dev/docs/commands/Set-MtAdCache +#> +function Set-MtAdCache { + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Internal function')] + param( + [string]$Server = $__MtSession.AdServer, + [pscredential]$Credential = $__MtSession.AdCredential, + [ValidateSet('All', 'Computers', 'Configuration', 'Domains', 'Forest')] + [string[]]$Objects = 'All' + ) + + if($Server){ + $PSDefaultParameterValues.Add("Get-Ad*:Server",$Server) + } + + if($Credential){ + $PSDefaultParameterValues.Add("Get-Ad*:Credential",$Credential) + } + + $primaryGroupIds = @(515,516,521) + $dcPrimaryGroupIds = @(516,521) + + $rootDse = try{ + Write-Verbose "Attempting Get-AdRootDSE" + Get-AdRootDSE + }catch{ + Write-Error $_ + return $null + } + + $configurationNamingContext = $rootDse.configurationNamingContext + $ADObjectDirectoryService = @{ + Identity = "CN=Directory Service,CN=Windows NT,CN=Services,$configurationNamingContext" + Properties = "*" + } + $directoryService = try{ + Write-Verbose "Attempting query for Directory Service object" + Get-ADObject @ADObjectDirectoryService + }catch{ + Write-Error $_ + return $null + } + + $HostSpnAlias = (($directoryService).spnmappings | Where-Object { + $_ -like "host=*" + }) -replace "host=" -split "," + + $Thresholds = @{ + DormantThresholdInDays = 90 + DormantDate = $null + ExpiredThresholdInDays = 180 + ExpiredDate = $null + StaleThresholdInDays = 30 + StaleDate = $null + } + $Thresholds.DormantDate = ((Get-Date).AddDays(-$Thresholds.DormantThresholdInDays)).Date + $Thresholds.ExpiredDate = ((Get-Date).AddDays(-$Thresholds.ExpiredThresholdInDays)).Date + $Thresholds.StaleDate = ((Get-Date).AddDays(-$Thresholds.StaleThresholdInDays)).Date + + $__MtSession.AdCache = @{ + RootDSE = $rootDse + ConfigurationNamingContext = $configurationNamingContext + DirectoryServiceConfigObj = $directoryService + Thresholds = $Thresholds + HostSpnAlias = @($HostSpnAlias) + PrimaryGroupIds = $primaryGroupIds + DomainControllerPgids = $dcPrimaryGroupIds + AdComputers = $__MtSession.AdCache.AdComputers + AdConfiguration = $__MtSession.AdCache.AdConfiguration + AdDomains = $__MtSession.AdCache.AdDomains + AdForest = $__MtSession.AdCache.AdForest + } + + + <# + if($Objects -contains "Domains" -or $Objects -contains "All"){ + $domainControllers = @() + $domainControllersSplat = @{ + SearchBase = $d.DomainControllersContainer + Server = $domain + LDAPFilter = "objectClass=Computer" + Properties = "*" + } + $dcs = try{ + Write-Verbose "Attempting AD query for DCs in $domain" + Get-ADObject @domainControllersSplat + }catch{ + Write-Error $_ + return $null + } + $domainControllers += @{ + Domain = $d.Name + DomainControllers = $dcs + } + } + $__MtSession.AdCache.AdDomainControllers = @{ + SetFlag = $true + DomainControllers = @($domainControllers) # TODO Process this + Data = @{} + } + foreach ($domain in $__MtSession.AdCache.AdDomainControllers.Domains){ + $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) + } + } + + #DeletedObjectsContainer = $null + #ForeignSecurityPrincipalsContainer = $null + #LinkedGroupPolicyObjects = $null + #LostAndFoundContainer = $null + #QuotasContainer = $null + #SystemsContainer = $null + #> + + if($Objects -contains "Domains" -or $Objects -contains "All"){ + $forest = try{ + Write-Verbose "Attempting AD query for Forest" + Get-ADForest + }catch{ + Write-Error $_ + return $null + } + + $domains = @() + $domainObjects = @() + foreach ($domain in $forest.Domains){ + $d = try{ + Write-Verbose "Attempting AD query for $domain" + Get-ADDomain -Server $domain + }catch{ + Write-Error $_ + return $null + } + + $do = try{ + Write-Verbose "Attempting AD query for $domain object" + Get-ADObject -Identity $domain.DistinguishedName -Properties * + }catch{ + Write-Error $_ + return $null + } + + $domains += $d + $domainObjects += $do + } + + $__MtSession.AdCache.AdDomains = @{ + SetFlag = $true + Domains = @($domains) + DomainObjects = @($domainObjects) + Data = @{ + Name = $null + NetBIOSName = $null + IsNetBIOSNameCompliant = $null + DistinguishedName = $null + DNSRoot = $null + IsDNSRootCompliant = $null + DomainFunctionalLevel = $null + AllowedDNSSuffixes = @() + AllowedDNSSuffixesCount = $null + InfrastructureMaster = $null + PDCEmulator = $null + RIDMaster = $null + CommonFsmo = $null + ChildDomains = @() + ChildDomainsCount = $null + ComputersContainer = $null + DefaultComputersContainer = $null + DomainControllersContainer = $null + DefaultDomainControllersContainer = $null + UsersContainer = $null + DefaultUsersContainer = $null + ReadOnlyDomainControllers = @() + ReadOnlyDomainControllersCount = $null + DomainControllers = @() + DomainControllersCount = $null + PublicKeyRequiredPasswordRolling = $null + ManagedBy = $null + IsSetManagedBy = $null + ParentDomain = $null + IsRootDomain = $null + MachineAccountQuota = $null + IsDefaultMachineAccountQuota = $null + ForceLogoff = $null + LockoutDuration = $null + LockOutObservationWindow = $null + LockoutThreshold = $null + MaxPwdAge = $null + MinPwdAge = $null + MinPwdLength = $null + PwdHistoryLength = $null + IsDefaultForceLogoff = $null + IsDefaultLockoutDuration = $null + IsDefaultLockOutObservationWindow = $null + IsDefaultLockoutThreshold = $null + IsDefaultMaxPwdAge = $null + IsDefaultMinPwdAge = $null + IsDefaultMinPwdLength = $null + IsDefaultPwdHistoryLength = $null + IsDefaultPasswordPolicy = $null + } + } + + foreach ($domain in $__MtSession.AdCache.AdDomains.Domains){ + $__MtSession.AdCache.AdDomains.Add("Data-$($domain.Name)",$__MtSession.AdCache.AdDomains.Data) + } + } + + if($Objects -contains "Forest" -or $Objects -contains "All"){ + $forest = try{ + Write-Verbose "Attempting AD query for Forest" + Get-ADForest + }catch{ + Write-Error $_ + return $null + } + + $__MtSession.AdCache.AdForest = @{ + SetFlag = $true + Forest = @($forest) + Data = @{ + FunctionalLevel = $rootDse.forestFunctionality + CrossForestReferences = @() + CrossForestReferencesCount = $null + DomainNamingMaster = $null + SchemaMaster = $null + CommonFsmo = $null + Sites = @() + SitesCount = $null + DefaultSite = $null + Domains = @() + DomainsCount = $null + UpnSuffixes = @() + UpnSuffixesCount = $null + SpnSuffixes = @() + SpnSuffixesCount = $null + } + } + } + + if($Objects -contains "Computers" -or $Objects -contains "All"){ + $computers = try{ + Write-Verbose "Attempting AD query for Computers" + Get-ADComputer -Filter * -Properties * + }catch{ + Write-Error $_ + return $null + } + + $__MtSession.AdCache.AdComputers = @{ + SetFlag = $true + Computers = @($computers) + Data = @{ + ComputersCount = $(($computers | Measure-Object).Count) + EnabledComputers = @() + EnabledComputersCount = $null + DisabledComputers = @() + DisabledComputersCount = $null + DormantComputers = @() + DormantComputersCount = $null + ExpiredComputers = @() + ExpiredComputersCount = $null + StaleComputers = @() + StaleComputersCount = $null + StaleComputersRatio = $null + NonPgIdCoumputers = @() + NonPgIdCoumputersCount = $null + SidHistoryComputers = @() + SidHistoryComputersCount = $null + ContainerComputers = @() + ContainerComputersCount = $null + BaseDns = @() + BaseDnCount = $null + BaseDnAvg = $null + LowBaseDns = @() + LowBaseDnsCount = $null + CreatorSidComputers = @() + CreatorSidComputersCount = $null + DomainControllers = @() + DomainControllersCount = $null + ServiceClasses = @() + ServiceClassesCount = $null + ServiceClassesComputers = @() + ServiceClassesComputersCount = $null + UnknownServiceClasses = @() + UnknownServiceClassesCount = $null + HostBypassComputers = @() + HostBypassComputersCount = $null + ServiceHosts = @() + ServiceHostsCount = $null + ServiceHostsComputers = @() + ServiceHostsComputersCount = $null + ServiceNoFqdnComputers = @() + ServiceNoFqdnComputersCount = $null + ServiceDnsBypassComputers = @() + ServiceDnsBypassComputersCount = $null + DnsZones = @() + DnsZonesCount = $null + DnsZoneAvg = $null + LowDnsZones = @() + LowDnsZonesCount = $null + NoDnsComputers = @() + NoDnsComputersCount = $null + DnsOverlapComputers = @() + DnsOverlapComputersCount = $null + UnconstrainedComputers = @() + UnconstrainedComputersCount = $null + KcdComputers = @() + KcdComputersCount = $null + S4U2SelfComputers = @() + S4U2SelfComputersCount = $null + RbcdComputers = @() + RbcdComputersCount = $null + MissingSpnsComputers = @() + MissingSpnsComputersCount = $null + OperatingSystems = @() + OperatingSystemsCount = $null + NoOperatingSystem = $() + NoOperatingSystemCount = $null + OperatingSystemAvg = $null + LowOperatingSystem = $() + LowOperatingSystemCount = $null + DisabledComputersRatio = $null + DormantComputersRatio = $null + ExpiredComputersRatio = $null + NonPgIdComputersRatio = $null + SidHistoryComputersRatio = $null + ContainerComputersRatio = $null + CreatorSidComputersRatio = $null + ServiceClassesComputersRatio = $null + HostBypassComputersRatio = $null + ServiceHostsRatio = $null + ServiceHostsComputersRatio = $null + ServiceDnsBypassComputersRatio = $null + NoDnsComputersRatio = $null + DnsOverlapComputersRatio = $null + UnconstrainedComputersRatio = $null + KcdComputersRatio = $null + S4U2SelfComputersRatio = $null + RbcdComputersRatio = $null + MissingSpnsComputersRatio = $null + NoOperatingSystemRatio = $null + } + } + } + + if($Objects -contains "Configuration" -or $Objects -contains "All"){ + $configuration = try{ + Write-Verbose "Attempting AD query for Configuration" + Get-ADObject -Filter * -Properties * -SearchBase $__MtSession.AdCache.ConfigurationNamingContext + }catch{ + Write-Error $_ + return $null + } + + $__MtSession.AdCache.AdConfiguration = @{ + SetFlag = $true + Configuration = @($configuration) + Data = @{ + TombstoneQuotaFactor = $null + BehaviorVersion = $null + LingeringObjects = @()#LostAndFoundConfig + QuotaControls = @() + Partitions = @()#SystemFlags, Domain + PhysicalLocations = @() + AuthenticationPolicies = @() + AuthenticationSilos = @() + AccessPolicies = @() + AccessRules = @() + ClaimTypes = @() + ClaimsTransformations = @() + ResourceProperties = @() + ResourcePropertyLists = @() + ValueTypes = @() + RootKeys = @() + KdsConfig = $null + ### KMS + #ActivationObjects = @() + ### MSMQ + #MessageQueuingObjects = @() + ### DHCP + #NetServicesObjects = @() + #DhcpAuthorizations = @() + ### Certificates + #AiaObjects = @() + #CdpObjects = @() + #CertificateTemplates = @() + #CertificationAuthorities = @() + #EnrollmentServices = @() + #Kra = @() + #Oid = @() + ### Routing and Remote Access + #Rras = @() + ### Shadow Principals + #ShadowObjects = @() + DirectoryService = $__MtSession.AdCache.DirectoryServiceConfigObj + OtherSettings = @() + TombstoneLifetime = $null + SpnMappings = $null + OptionalFeatures = @() + QueryPolicies = @() + DefaultQueryPolicy = @()#LDAPAdminLimits + Sites = @()#Class -eq Site + SiteSchedules = @()#Site>NTDS>Schedule + SiteServers = @()#Site>Servers + KeymasterZones = @()#Site>Servers>DNS>Keymaster + ServerSettings = @()#Site>Servers>NTDS>hasmasterncs,options,msDS-* + IpSiteLinks = @()#Inter-Site>IP>cost,replint>sitelist + SmtpSiteLinks = @()#Should be 0 + Subnets = @()#Should be >0 + WellKnownPrincipals = @()#CN,ObjectSid + } + } + } +} \ No newline at end of file