diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index d00448cf9..8d3e20a93 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -184,7 +184,17 @@ 'Test-MtXspmCriticalCredsOnDevicesWithNonCriticalAccounts', 'Test-MtXspmPublicRemotelyExploitableHighExposureDevices', 'Test-MtXspmCriticalCredentialsOnNonTpmProtectedDevices', - 'Test-MtXspmCriticalCredentialsOnNonCredGuardProtectedDevices' + '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-MtAdForestDomain','Test-MtAdForestExternalLdap','Test-MtAdForestFsmoStatus', + '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/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..06092e019 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', '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..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 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..947cf8f57 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerContainer.ps1 @@ -0,0 +1,148 @@ +<# +.SYNOPSIS + Checks AD Computer Containers + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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.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..a6734f891 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerCreatorSid.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + Checks for computers with Creator SID + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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.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..4b7c2de33 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDns.ps1 @@ -0,0 +1,166 @@ +<# +.SYNOPSIS + Checks computer DNS + +.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 + + Returns true if AD Computer DNS state is clean + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerDns +#> +function Test-MtAdComputerDns { + [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.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.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..b89daaf51 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerDomainController.ps1 @@ -0,0 +1,108 @@ +<# +.SYNOPSIS + Checks domain controllers + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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 $__MtSession.AdCache.DomainControllerPgids + } + $AdObjects.Data.DomainControllersCount = ($AdObjects.Data.DomainControllers | Measure-Object).Count + #endregion + + $__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..9f552a4e0 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerKerberos.ps1 @@ -0,0 +1,186 @@ +<# +.SYNOPSIS + Checks computer Kerberos configuration + +.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 + + Returns true if AD Computer Kerberos is adequate + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerKerberos +#> +function Test-MtAdComputerKerberos { + [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.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.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..992c1d0df --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerOperatingSystem.ps1 @@ -0,0 +1,151 @@ +<# +.SYNOPSIS + Checks computer operating systems + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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.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..ee061dbc5 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerPrimaryGroup.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + Checks the primary group IDs of computers + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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 $__MtSession.AdCache.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.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..b25f78e4f --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerService.ps1 @@ -0,0 +1,430 @@ +<# +.SYNOPSIS + Checks Computer SPNs + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + Add-MtTestResultDetail -SkippedBecause NotConnectedActiveDirectory + return $null + } + + if (-not $__MtSession.AdCache.AdComputers.SetFlag){ + 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 + } + + #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.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 = { + $_.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.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 = { + $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.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 + } + 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 + 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 + } + 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 + 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..7d54b07e0 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerSidHistory.ps1 @@ -0,0 +1,111 @@ +<# +.SYNOPSIS + Checks for computer SID History + +.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 + + 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 ) { + Write-Verbose "ActiveDirectory not set as connection" + 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.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..8157f43c0 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdComputerStatus.ps1 @@ -0,0 +1,165 @@ +<# +.SYNOPSIS + Checks the computer objects for activity + +.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 + + Returns true if AD Computer status is within thresholds + +.LINK + https://maester.dev/docs/commands/Test-MtAdComputerStatus +#> +function Test-MtAdComputerStatus { + [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.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.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/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/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 new file mode 100644 index 000000000..577bede24 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestDomain.ps1 @@ -0,0 +1,106 @@ +<# +.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.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..39778ea53 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestExternalLdap.ps1 @@ -0,0 +1,107 @@ +<# +.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.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..998852ee6 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestFsmoStatus.ps1 @@ -0,0 +1,108 @@ +<# +.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.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..56fb6e2dd --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestFunctionalLevel.ps1 @@ -0,0 +1,94 @@ +<# +.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 + } + + #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..ffb1dfb19 --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestSite.ps1 @@ -0,0 +1,115 @@ +<# +.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.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..9ba6355ab --- /dev/null +++ b/powershell/public/maester/ad/Test-MtAdForestSuffix.ps1 @@ -0,0 +1,117 @@ +<# +.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.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 new file mode 100644 index 000000000..235f1ea6a --- /dev/null +++ b/tests/Maester/ad/Test-MtAd.Tests.ps1 @@ -0,0 +1,145 @@ +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" + } + } + 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" + } + } + 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