From ac677f9a9ce0c6a395ffef23a2fee8fbb1ff14f2 Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 14:58:45 +0200 Subject: [PATCH 01/10] feat: add session license cache and Get-MtSessionLicenses Add $__MtSession.Licenses hashtable populated once by Initialize-MtSession at startup. Get-MtLicenseInformation is now cache-first and lazy-loads SKU data only on a cache miss. Add Get-MtSessionLicenses for use in Pester BeforeDiscovery blocks to read the cache without additional Graph API calls. Clear-ModuleVariable resets the cache on each Invoke-Maester run. Co-authored-by: Copilot --- powershell/Maester.psd1 | 2 +- powershell/Maester.psm1 | 1 + powershell/internal/Clear-ModuleVariable.ps1 | 1 + powershell/internal/Initialize-MtSession.ps1 | 15 +++++++ .../public/Get-MtLicenseInformation.ps1 | 43 +++++++++++-------- powershell/public/Get-MtSessionLicenses.ps1 | 42 ++++++++++++++++++ 6 files changed, 85 insertions(+), 19 deletions(-) create mode 100644 powershell/public/Get-MtSessionLicenses.ps1 diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index a8142ff5c..de39012d2 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -63,7 +63,7 @@ 'Get-MailAuthenticationRecord', 'Get-MtAdminPortalUrl', 'Get-MtAuthenticationMethodPolicyConfig', 'Get-MtAzureManagementGroup', 'Get-MtConditionalAccessPolicy', 'Get-MtExo', 'Get-MtExoThreatPolicyMalware', 'Get-MtGraphScope', 'Get-MtGroupMember', 'Get-MtHtmlReport', 'Get-MtLicenseInformation', 'Get-MtMaesterApp', 'Get-MtRole', - 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtTestInventory', 'Get-MtUser', + 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSessionLicenses', 'Get-MtTestInventory', 'Get-MtUser', 'Get-MtUserAuthenticationMethod', 'Get-MtUserAuthenticationMethodInfoByType', 'Import-MtMaesterResult', 'Install-MaesterTests', 'Invoke-Maester', 'Invoke-MtAzureRequest', 'Invoke-MtAzureResourceGraphRequest', 'Invoke-MtGraphRequest', 'Invoke-MtGraphSecurityQuery', 'Merge-MtMaesterResult', 'New-MtMaesterApp', 'Resolve-SPFRecord', diff --git a/powershell/Maester.psm1 b/powershell/Maester.psm1 index 1cb989e9f..2da05c9f7 100644 --- a/powershell/Maester.psm1 +++ b/powershell/Maester.psm1 @@ -25,6 +25,7 @@ $__MtSession = @{ DataverseApiBase = $null # Resolved Dataverse OData API base URL (e.g. https://org123.api.crm.dynamics.com/api/data/v9.2) DataverseResourceUrl = $null # Dataverse resource URL for token acquisition (e.g. https://org123.crm.dynamics.com) DataverseEnvironmentId = $null # Environment identifier for display (e.g. org123.crm.dynamics.com) + Licenses = @{} # Pre-fetched license map keyed by product name, populated by Initialize-MtSession } New-Variable -Name __MtSession -Value $__MtSession -Scope Script -Force diff --git a/powershell/internal/Clear-ModuleVariable.ps1 b/powershell/internal/Clear-ModuleVariable.ps1 index 17879e660..c383e753a 100644 --- a/powershell/internal/Clear-ModuleVariable.ps1 +++ b/powershell/internal/Clear-ModuleVariable.ps1 @@ -17,6 +17,7 @@ $__MtSession.TestResultDetail = @{} $__MtSession.MaesterConfig = $null $__MtSession.AdminPortalUrl = @{} + $__MtSession.Licenses = @{} Clear-MtDnsCache Clear-MtExoCache $__MtSession.AIAgentInfo = $null diff --git a/powershell/internal/Initialize-MtSession.ps1 b/powershell/internal/Initialize-MtSession.ps1 index 2a75a8736..dfd2fa082 100644 --- a/powershell/internal/Initialize-MtSession.ps1 +++ b/powershell/internal/Initialize-MtSession.ps1 @@ -19,4 +19,19 @@ } $__MtSession.AdminPortalUrl = Get-MtAdminPortalUrl -Environment $environment + + # Pre-fetch all license products into the session cache so that BeforeDiscovery blocks + # in test files pay zero Graph API cost when calling Get-MtLicenseInformation. + # ExoLicenseCount is intentionally excluded here as it returns a number (seat count) + # rather than a product string and is not used for skip-gating. + $licenseProducts = @('EntraID', 'EntraWorkloadID', 'Eop', 'ExoDlp', 'Mdo', 'MdoV2', + 'AdvAudit', 'DefenderXDR', 'CustomerLockbox', 'Intune') + try { + foreach ($product in $licenseProducts) { + $__MtSession.Licenses[$product] = Get-MtLicenseInformation -Product $product + } + Write-Verbose "License information pre-fetched for $($licenseProducts.Count) products." + } catch { + Write-Verbose "License pre-fetch skipped (tenant not reachable or not connected to Graph): $_" + } } diff --git a/powershell/public/Get-MtLicenseInformation.ps1 b/powershell/public/Get-MtLicenseInformation.ps1 index fc618c58b..750465fc2 100644 --- a/powershell/public/Get-MtLicenseInformation.ps1 +++ b/powershell/public/Get-MtLicenseInformation.ps1 @@ -27,11 +27,26 @@ function Get-MtLicenseInformation { begin { # Get all subscribed SKUs once to optimize performance. - $SKUs = Invoke-MtGraphRequest -RelativeUri 'subscribedSkus' | Where-Object { $_.capabilityStatus -eq 'Enabled' } - $ServicePlans = $SKUs | Select-Object -ExpandProperty servicePlans | Select-Object -ExpandProperty servicePlanId | Sort-Object -Unique + # Skip the Graph call entirely if the session cache already has this product. + # (Initialize-MtSession pre-populates $__MtSession.Licenses for all products.) + $SKUs = $null + $ServicePlans = $null } process { + # Return from session cache when available (populated by Initialize-MtSession). + if ($__MtSession.Licenses.ContainsKey($Product)) { + Write-Verbose "Returning cached license information for $Product" + return $__MtSession.Licenses[$Product] + } + + # Lazy-load SKU data only when a cache miss requires it. + if ($null -eq $SKUs) { + $SKUs = Invoke-MtGraphRequest -RelativeUri 'subscribedSkus' | Where-Object { $_.capabilityStatus -eq 'Enabled' } + $ServicePlans = $SKUs | Select-Object -ExpandProperty servicePlans | Select-Object -ExpandProperty servicePlanId | Sort-Object -Unique + } + + $LicenseType = $null switch ($Product) { 'EntraID' { Write-Verbose 'Retrieving license information for Entra ID' @@ -45,7 +60,6 @@ function Get-MtLicenseInformation { $LicenseType = 'Free' } Write-Information "The license type for Entra ID is $LicenseType" - return $LicenseType break } 'EntraWorkloadID' { @@ -58,7 +72,6 @@ function Get-MtLicenseInformation { $LicenseType = $null } Write-Information "The license type for Entra ID is $LicenseType" - return $LicenseType break } 'Eop' { @@ -76,7 +89,6 @@ function Get-MtLicenseInformation { } } Write-Information "The license type for Exchange Online Protection is $LicenseType" - return $LicenseType break } 'ExoDlp' { @@ -99,7 +111,6 @@ function Get-MtLicenseInformation { } } Write-Information "The license type for Exchange Online DLP is $LicenseType" - return $LicenseType break } 'Mdo' { @@ -118,7 +129,6 @@ function Get-MtLicenseInformation { } } Write-Information "The license type for Defender for Office is $LicenseType" - return $LicenseType break } 'MdoV2' { @@ -135,7 +145,6 @@ function Get-MtLicenseInformation { $LicenseType = 'EOP' # Exchange Online Protection / EOP_ENTERPRISE (326e2b78-9d27-42c9-8509-46c827743a17) } Write-Information "The license type for Defender for Office is $LicenseType" - return $LicenseType break } 'AdvAudit' { @@ -153,7 +162,6 @@ function Get-MtLicenseInformation { } } Write-Information "The license type for Advanced Audit is $LicenseType" - return $LicenseType break } 'ExoLicenseCount' { @@ -189,7 +197,7 @@ function Get-MtLicenseInformation { } Write-Information "Total Exchange Online licenses: $TotalLicenses" - return $TotalLicenses + $LicenseType = $TotalLicenses break } 'DefenderXDR' { @@ -208,7 +216,6 @@ function Get-MtLicenseInformation { } } Write-Information 'The tenant is licensed for Defender XDR' - return $LicenseType break } 'CustomerLockbox' { @@ -224,13 +231,10 @@ function Get-MtLicenseInformation { $LicenseType = $null } Write-Information "The license type for Customer Lockbox is $LicenseType" - return $LicenseType break } "Intune" { Write-Verbose "Retrieving license SKUs for Intune" - $subscribedSkus = Invoke-MtGraphRequest -RelativeUri "subscribedSkus" | Where-Object {$_.capabilityStatus -eq 'Enabled'} - $uniqueServicePlans = $subscribedSkus.servicePlans.servicePlanId | Sort-Object -Unique $requiredServicePlans = @( # https://learn.microsoft.com/en-us/entra/identity/users/licensing-service-plan-reference "c1ec4a95-1f05-45b3-a911-aa3fa01094f5", # INTUNE_A (Microsoft Intune Plan 1) @@ -244,16 +248,19 @@ function Get-MtLicenseInformation { ) $LicenseType = $null - foreach($servicePlan in $requiredServicePlans){ - if($servicePlan -in $uniqueServicePlans){ + foreach ($servicePlan in $requiredServicePlans) { + if ($servicePlan -in $ServicePlans) { $LicenseType = "Intune" } } Write-Information "The tenant is licensed for Intune" - return $LicenseType - Break + break } Default {} } + + # Store in session cache so subsequent calls (including BeforeDiscovery blocks) pay no API cost. + $__MtSession.Licenses[$Product] = $LicenseType + return $LicenseType } } diff --git a/powershell/public/Get-MtSessionLicenses.ps1 b/powershell/public/Get-MtSessionLicenses.ps1 new file mode 100644 index 000000000..6dba5c6c1 --- /dev/null +++ b/powershell/public/Get-MtSessionLicenses.ps1 @@ -0,0 +1,42 @@ +function Get-MtSessionLicenses { + <# + .SYNOPSIS + Returns the pre-fetched license map for the current tenant session. + + .DESCRIPTION + Returns a hashtable of all license products evaluated for the current tenant. + The map is populated once by Initialize-MtSession (called at the start of Invoke-Maester) + so that BeforeDiscovery blocks in test files can gate tests on license availability + without making additional Graph API calls. + + Keys match the -Product parameter of Get-MtLicenseInformation. + Use this function in BeforeDiscovery blocks instead of calling Get-MtLicenseInformation + per-product. + + .EXAMPLE + BeforeDiscovery { + $Licenses = Get-MtSessionLicenses + } + + Describe "Maester/Entra" -Tag "Maester", "Entra" { + It "MT.XXXX: ..." -Tag "MT.XXXX" -Skip:($Licenses.EntraID -eq "Free") { + Test-MtSomeP1Feature | Should -Be $true + } + It "MT.XXXY: ..." -Tag "MT.XXXY" -Skip:($Licenses.EntraID -ne "P2") { + Test-MtSomeP2Feature | Should -Be $true + } + It "MT.XXXZ: ..." -Tag "MT.XXXZ" -Skip:($null -eq $Licenses.Intune) { + Test-MtSomeIntuneFeature | Should -Be $true + } + } + + .LINK + https://maester.dev/docs/commands/Get-MtSessionLicenses + #> + [OutputType([hashtable])] + [CmdletBinding()] + param() + + Write-Verbose "Returning session license cache with $($__MtSession.Licenses.Count) products." + return $__MtSession.Licenses +} From 1d625bdd234d4ed9090ace64ed4584bbabc9d569 Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 14:59:00 +0200 Subject: [PATCH 02/10] feat: add -AutoFilterLicenses switch to Invoke-Maester When -AutoFilterLicenses is set, Maester reads the pre-fetched session license cache and appends the appropriate License-* tags to ExcludeTag so tests requiring absent licenses are skipped cleanly at discovery time. Co-authored-by: Copilot --- powershell/public/Invoke-Maester.ps1 | 60 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/powershell/public/Invoke-Maester.ps1 b/powershell/public/Invoke-Maester.ps1 index 59d866025..4794e3fa2 100644 --- a/powershell/public/Invoke-Maester.ps1 +++ b/powershell/public/Invoke-Maester.ps1 @@ -97,6 +97,13 @@ Connect to all tested services and run all tests, including the long-running and preview tests. + .EXAMPLE + Invoke-Maester -AutoFilterLicenses + + Runs tests and automatically skips any test that requires a license the tenant does not have. + For example, on a tenant with only a Business Premium license, tests requiring Entra ID P2, + Defender XDR, or Advanced Audit will be excluded from the run. + .LINK https://maester.dev/docs/commands/Invoke-Maester #> @@ -206,7 +213,14 @@ # The root directory for configuration drift tracking. [Parameter(HelpMessage = 'Specify drift root directory, see https://maester.dev/docs/tests/MT.1060')] - [string] $DriftRoot + [string] $DriftRoot, + + # Automatically exclude tests that require licenses not present in the tenant. + # When set, Maester queries the tenant license information at startup and adds the + # appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly. + # This requires a Graph connection and is silently ignored when not connected. + [Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')] + [switch] $AutoFilterLicenses ) function GetDefaultFileName() { @@ -324,9 +338,51 @@ Test-MtContext -SendMail:$isMail -SendTeamsMessage:$isTeamsChannelMessage | Out-Null } - # Initialize MtSession after Graph connected. + # Initialize MtSession after Graph connected (also pre-fetches license information). Initialize-MtSession + # If -AutoFilterLicenses is set, exclude tests whose required license is absent in the tenant. + if ($AutoFilterLicenses.IsPresent) { + $tenantLicenses = Get-MtSessionLicenses + if ($tenantLicenses.Count -gt 0) { + $licenseExclusions = [System.Collections.Generic.List[string]]::new() + + # Entra ID tiers: Free excludes P1 + P2 + Governance; P1 excludes P2 + Governance + switch ($tenantLicenses['EntraID']) { + 'Free' { + $licenseExclusions.AddRange([string[]]@('License-EntraP1', 'License-EntraP2', 'License-EntraGovernance')) + } + 'P1' { + $licenseExclusions.AddRange([string[]]@('License-EntraP2', 'License-EntraGovernance')) + } + } + + # Binary (licensed / not licensed) products + $binaryProducts = @{ + 'EntraWorkloadID' = 'License-EntraWorkloadID' + 'Eop' = 'License-Eop' + 'ExoDlp' = 'License-ExoDlp' + 'Mdo' = 'License-Mdo' + 'AdvAudit' = 'License-AdvAudit' + 'DefenderXDR' = 'License-DefenderXDR' + 'CustomerLockbox' = 'License-CustomerLockbox' + 'Intune' = 'License-Intune' + } + foreach ($entry in $binaryProducts.GetEnumerator()) { + if ($null -eq $tenantLicenses[$entry.Key]) { + $licenseExclusions.Add($entry.Value) + } + } + + if ($licenseExclusions.Count -gt 0) { + $ExcludeTag = @($ExcludeTag | Where-Object { $_ }) + $licenseExclusions + Write-Verbose "AutoFilterLicenses: excluding tags $($licenseExclusions -join ', ')" + } + } else { + Write-Verbose 'AutoFilterLicenses: license data not available, skipping auto-filter.' + } + } + if ($isWebUri) { # Check if TeamChannelWebhookUri is a valid URL. $urlPattern = '^(https)://[^\s/$.?#].[^\s]*$' From adaa4f95626265a782ea2c7711f26e420dc8f97c Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 14:59:57 +0200 Subject: [PATCH 03/10] fix: correct CA helper error reporting and skip logic Replace ambiguous -Error parameter with -SkippedBecause Error -SkippedError in six CA helper catch blocks (was causing InvalidResult in Pester). Move the $hasRiskCAPolicy skip in Test-MtCaMisconfiguredIDProtection outside the try-catch to prevent Pester's internal flow-control exception from being caught and re-thrown as a double Set-ItResult error (MT.1049). Replace MtRoleDefinition object casts with .ToString() plus stable GUID fallbacks in Test-MtCaExclusionForDirectorySyncAccount, and add a null guard to Get-MtRoleInfo for when the module role cache is unavailable (MT.1020). Co-authored-by: Copilot --- powershell/internal/Get-MtRoleInfo.ps1 | 277 +++++++++--------- ...t-MtCaExclusionForDirectorySyncAccount.ps1 | 15 +- .../maester/entra/Test-MtCaMfaForAllUsers.ps1 | 2 +- .../maester/entra/Test-MtCaMfaForGuest.ps1 | 2 +- .../Test-MtCaMisconfiguredIDProtection.ps1 | 29 +- .../entra/Test-MtCaReferencedGroupsExist.ps1 | 4 +- ...CaRequirePasswordChangeForHighUserRisk.ps1 | 2 +- .../Test-MtCaWIFBlockLegacyAuthentication.ps1 | 2 +- 8 files changed, 173 insertions(+), 160 deletions(-) diff --git a/powershell/internal/Get-MtRoleInfo.ps1 b/powershell/internal/Get-MtRoleInfo.ps1 index ea26054da..f9340a08e 100644 --- a/powershell/internal/Get-MtRoleInfo.ps1 +++ b/powershell/internal/Get-MtRoleInfo.ps1 @@ -18,144 +18,144 @@ # To update, run: build/Update-MtRoleDefinitions.ps1 $script:MtRoles = @{ # BEGIN AUTO-GENERATED ROLE DEFINITIONS - 'AgentIDAdministrator' = [MtRoleDefinition]::new('db506228-d27e-4b7d-95e5-295956d6615f', $true) - 'AgentIDDeveloper' = [MtRoleDefinition]::new('adb2368d-a9be-41b5-8667-d96778e081b0', $false) - 'AgentRegistryAdministrator' = [MtRoleDefinition]::new('6b942400-691f-4bf0-9d12-d8a254a2baf5', $false) - 'AIAdministrator' = [MtRoleDefinition]::new('d2562ede-74db-457e-a7b6-544e236ebb61', $true) - 'ApplicationAdministrator' = [MtRoleDefinition]::new('9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3', $true) - 'ApplicationDeveloper' = [MtRoleDefinition]::new('cf1c38e5-3621-4004-a7cb-879624dced7c', $true) - 'AttackPayloadAuthor' = [MtRoleDefinition]::new('9c6df0f2-1e7c-4dc3-b195-66dfbd24aa8f', $false) - 'AttackSimulationAdministrator' = [MtRoleDefinition]::new('c430b396-e693-46cc-96f3-db01bf8bb62a', $false) - 'AttributeAssignmentAdministrator' = [MtRoleDefinition]::new('58a13ea3-c632-46ae-9ee0-9c0d43cd7f3d', $false) - 'AttributeAssignmentReader' = [MtRoleDefinition]::new('ffd52fa5-98dc-465c-991d-fc073eb59f8f', $false) - 'AttributeDefinitionAdministrator' = [MtRoleDefinition]::new('8424c6f0-a189-499e-bbd0-26c1753c96d4', $false) - 'AttributeDefinitionReader' = [MtRoleDefinition]::new('1d336d2c-4ae8-42ef-9711-b3604ce3fc2c', $false) - 'AttributeLogAdministrator' = [MtRoleDefinition]::new('5b784334-f94b-471a-a387-e7219fc49ca2', $false) - 'AttributeLogReader' = [MtRoleDefinition]::new('9c99539d-8186-4804-835f-fd51ef9e2dcd', $false) - 'AttributeProvisioningAdministrator' = [MtRoleDefinition]::new('ecb2c6bf-0ab6-418e-bd87-7986f8d63bbe', $true) - 'AttributeProvisioningReader' = [MtRoleDefinition]::new('422218e4-db15-4ef9-bbe0-8afb41546d79', $true) - 'AuthenticationAdministrator' = [MtRoleDefinition]::new('c4e39bd9-1100-46d3-8c65-fb160da0071f', $true) - 'AuthenticationExtensibilityAdministrator' = [MtRoleDefinition]::new('25a516ed-2fa0-40ea-a2d0-12923a21473a', $true) + 'AgentIDAdministrator' = [MtRoleDefinition]::new('db506228-d27e-4b7d-95e5-295956d6615f', $true) + 'AgentIDDeveloper' = [MtRoleDefinition]::new('adb2368d-a9be-41b5-8667-d96778e081b0', $false) + 'AgentRegistryAdministrator' = [MtRoleDefinition]::new('6b942400-691f-4bf0-9d12-d8a254a2baf5', $false) + 'AIAdministrator' = [MtRoleDefinition]::new('d2562ede-74db-457e-a7b6-544e236ebb61', $true) + 'ApplicationAdministrator' = [MtRoleDefinition]::new('9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3', $true) + 'ApplicationDeveloper' = [MtRoleDefinition]::new('cf1c38e5-3621-4004-a7cb-879624dced7c', $true) + 'AttackPayloadAuthor' = [MtRoleDefinition]::new('9c6df0f2-1e7c-4dc3-b195-66dfbd24aa8f', $false) + 'AttackSimulationAdministrator' = [MtRoleDefinition]::new('c430b396-e693-46cc-96f3-db01bf8bb62a', $false) + 'AttributeAssignmentAdministrator' = [MtRoleDefinition]::new('58a13ea3-c632-46ae-9ee0-9c0d43cd7f3d', $false) + 'AttributeAssignmentReader' = [MtRoleDefinition]::new('ffd52fa5-98dc-465c-991d-fc073eb59f8f', $false) + 'AttributeDefinitionAdministrator' = [MtRoleDefinition]::new('8424c6f0-a189-499e-bbd0-26c1753c96d4', $false) + 'AttributeDefinitionReader' = [MtRoleDefinition]::new('1d336d2c-4ae8-42ef-9711-b3604ce3fc2c', $false) + 'AttributeLogAdministrator' = [MtRoleDefinition]::new('5b784334-f94b-471a-a387-e7219fc49ca2', $false) + 'AttributeLogReader' = [MtRoleDefinition]::new('9c99539d-8186-4804-835f-fd51ef9e2dcd', $false) + 'AttributeProvisioningAdministrator' = [MtRoleDefinition]::new('ecb2c6bf-0ab6-418e-bd87-7986f8d63bbe', $true) + 'AttributeProvisioningReader' = [MtRoleDefinition]::new('422218e4-db15-4ef9-bbe0-8afb41546d79', $true) + 'AuthenticationAdministrator' = [MtRoleDefinition]::new('c4e39bd9-1100-46d3-8c65-fb160da0071f', $true) + 'AuthenticationExtensibilityAdministrator' = [MtRoleDefinition]::new('25a516ed-2fa0-40ea-a2d0-12923a21473a', $true) 'AuthenticationExtensibilityPasswordAdministrator' = [MtRoleDefinition]::new('0b00bede-4072-4d22-b441-e7df02a1ef63', $true) - 'AuthenticationPolicyAdministrator' = [MtRoleDefinition]::new('0526716b-113d-4c15-b2c8-68e3c22b9f80', $false) - 'AzureDevOpsAdministrator' = [MtRoleDefinition]::new('e3973bdf-4987-49ae-837a-ba8e231c7286', $false) - 'AzureInformationProtectionAdministrator' = [MtRoleDefinition]::new('7495fdc4-34c4-4d15-a289-98788ce399fd', $false) - 'B2CIEFKeysetAdministrator' = [MtRoleDefinition]::new('aaf43236-0c0d-4d5f-883a-6955382ac081', $true) - 'B2CIEFPolicyAdministrator' = [MtRoleDefinition]::new('3edaf663-341e-4475-9f94-5c398ef6c070', $false) - 'BillingAdministrator' = [MtRoleDefinition]::new('b0f54661-2d74-4c50-afa3-1ec803f12efe', $false) - 'CloudApplicationAdministrator' = [MtRoleDefinition]::new('158c047a-c907-4556-b7ef-446551a6b5f7', $true) - 'CloudAppSecurityAdministrator' = [MtRoleDefinition]::new('892c5842-a9a6-463a-8041-72aa08ca3cf6', $false) - 'CloudDeviceAdministrator' = [MtRoleDefinition]::new('7698a772-787b-4ac8-901f-60d6b08affd2', $true) - 'ComplianceAdministrator' = [MtRoleDefinition]::new('17315797-102d-40b4-93e0-432062caca18', $false) - 'ComplianceDataAdministrator' = [MtRoleDefinition]::new('e6d1a23a-da11-4be4-9570-befc86d067a7', $false) - 'ConditionalAccessAdministrator' = [MtRoleDefinition]::new('b1be1c3e-b65d-4f19-8427-f6fa0d97feb9', $true) - 'CustomerLockboxAccessApprover' = [MtRoleDefinition]::new('5c4f9dcd-47dc-4cf7-8c9a-9e4207cbfc91', $false) - 'DesktopAnalyticsAdministrator' = [MtRoleDefinition]::new('38a96431-2bdf-4b4c-8b6e-5d3d8abac1a4', $false) - 'DeviceJoin' = [MtRoleDefinition]::new('9c094953-4995-41c8-84c8-3ebb9b32c93f', $false) - 'DeviceManagers' = [MtRoleDefinition]::new('2b499bcd-da44-4968-8aec-78e1674fa64d', $false) - 'DeviceUsers' = [MtRoleDefinition]::new('d405c6df-0af8-4e3b-95e4-4d06e542189e', $false) - 'DirectoryReaders' = [MtRoleDefinition]::new('88d8e3e3-8f55-4a1e-953a-9b9898b8876b', $false) - 'DirectorySynchronizationAccounts' = [MtRoleDefinition]::new('d29b2b05-8046-44ba-8758-1e26182fcf32', $false) - 'DirectoryWriters' = [MtRoleDefinition]::new('9360feb5-f418-4baa-8175-e2a00bac4301', $true) - 'DomainNameAdministrator' = [MtRoleDefinition]::new('8329153b-31d0-4727-b945-745eb3bc5f31', $true) - 'DragonAdministrator' = [MtRoleDefinition]::new('e93e3737-fa85-474a-aee4-7d3fb86510f3', $false) - 'Dynamics365Administrator' = [MtRoleDefinition]::new('44367163-eba1-44c3-98af-f5787879f96a', $false) - 'Dynamics365BusinessCentralAdministrator' = [MtRoleDefinition]::new('963797fb-eb3b-4cde-8ce3-5878b3f32a3f', $false) - 'EdgeAdministrator' = [MtRoleDefinition]::new('3f1acade-1e04-4fbc-9b69-f0302cd84aef', $false) - 'EntraBackupAdministrator' = [MtRoleDefinition]::new('b6a27b2b-f905-4b2e-81b5-0d90e0ef1fdb', $false) - 'EntraBackupReader' = [MtRoleDefinition]::new('f42252d9-5400-4d7b-b9ef-cc582dbb8577', $false) - 'ExchangeAdministrator' = [MtRoleDefinition]::new('29232cdf-9323-42fd-ade2-1d097af3e4de', $false) - 'ExchangeBackupAdministrator' = [MtRoleDefinition]::new('49eb8f75-97e9-4e37-9b2b-6c3ebfcffa31', $false) - 'ExchangeRecipientAdministrator' = [MtRoleDefinition]::new('31392ffb-586c-42d1-9346-e59415a2cc4e', $false) - 'ExtendedDirectoryUserAdministrator' = [MtRoleDefinition]::new('dd13091a-6207-4fc0-82ba-3641e056ab95', $false) - 'ExternalIdentityProviderAdministrator' = [MtRoleDefinition]::new('be2f45a1-457d-42af-a067-6ec1fa63bc45', $true) - 'ExternalIDUserFlowAdministrator' = [MtRoleDefinition]::new('6e591065-9bad-43ed-90f3-e9424366d2f0', $false) - 'ExternalIDUserFlowAttributeAdministrator' = [MtRoleDefinition]::new('0f971eea-41eb-4569-a71e-57bb8a3eff1e', $false) - 'FabricAdministrator' = [MtRoleDefinition]::new('a9ea8996-122f-4c74-9520-8edcd192826c', $false) - 'GlobalAdministrator' = [MtRoleDefinition]::new('62e90394-69f5-4237-9190-012177145e10', $true) - 'GlobalReader' = [MtRoleDefinition]::new('f2ef992c-3afb-46b9-b7cf-a126ee74c451', $true) - 'GlobalSecureAccessAdministrator' = [MtRoleDefinition]::new('ac434307-12b9-4fa1-a708-88bf58caabc1', $false) - 'GlobalSecureAccessLogReader' = [MtRoleDefinition]::new('843318fb-79a6-4168-9e6f-aa9a07481cc4', $false) - 'GroupsAdministrator' = [MtRoleDefinition]::new('fdd7a751-b60b-444a-984c-02652fe8fa1c', $false) - 'GuestInviter' = [MtRoleDefinition]::new('95e79109-95c0-4d8e-aee3-d01accf2d47b', $false) - 'GuestUser' = [MtRoleDefinition]::new('10dae51f-b6af-4016-8d66-8c2a99b929b3', $false) - 'HelpdeskAdministrator' = [MtRoleDefinition]::new('729827e3-9c14-49f7-bb1b-9608f156bbb8', $true) - 'HybridIdentityAdministrator' = [MtRoleDefinition]::new('8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2', $true) - 'IdentityGovernanceAdministrator' = [MtRoleDefinition]::new('45d8d3c5-c802-45c6-b32a-1d70b5e1e86e', $false) - 'InsightsAdministrator' = [MtRoleDefinition]::new('eb1f4a8d-243a-41f0-9fbd-c7cdf6c5ef7c', $false) - 'InsightsAnalyst' = [MtRoleDefinition]::new('25df335f-86eb-4119-b717-0ff02de207e9', $false) - 'InsightsBusinessLeader' = [MtRoleDefinition]::new('31e939ad-9672-4796-9c2e-873181342d2d', $false) - 'IntuneAdministrator' = [MtRoleDefinition]::new('3a2c62db-5318-420d-8d74-23affee5d9d5', $true) - 'IoTDeviceAdministrator' = [MtRoleDefinition]::new('2ea5ce4c-b2d8-4668-bd81-3680bd2d227a', $false) - 'KaizalaAdministrator' = [MtRoleDefinition]::new('74ef975b-6605-40af-a5d2-b9539d836353', $false) - 'KnowledgeAdministrator' = [MtRoleDefinition]::new('b5a8dcf3-09d5-43a9-a639-8e29ef291470', $false) - 'KnowledgeManager' = [MtRoleDefinition]::new('744ec460-397e-42ad-a462-8b3f9747a02c', $false) - 'LicenseAdministrator' = [MtRoleDefinition]::new('4d6ac14f-3453-41d0-bef9-a3e0c569773a', $false) - 'LifecycleWorkflowsAdministrator' = [MtRoleDefinition]::new('59d46f88-662b-457b-bceb-5c3809e5908f', $true) - 'MessageCenterPrivacyReader' = [MtRoleDefinition]::new('ac16e43d-7b2d-40e0-ac05-243ff356ab5b', $false) - 'MessageCenterReader' = [MtRoleDefinition]::new('790c1fb9-7f7d-4f88-86a1-ef1f95c05c1b', $false) - 'Microsoft365BackupAdministrator' = [MtRoleDefinition]::new('1707125e-0aa2-4d4d-8655-a7c786c76a25', $false) - 'Microsoft365MigrationAdministrator' = [MtRoleDefinition]::new('8c8b803f-96e1-4129-9349-20738d9f9652', $false) - 'MicrosoftEntraJoinedDeviceLocalAdministrator' = [MtRoleDefinition]::new('9f06204d-73c1-4d4c-880a-6edb90606fd8', $false) - 'MicrosoftGraphDataConnectAdministrator' = [MtRoleDefinition]::new('ee67aa9c-e510-4759-b906-227085a7fd4d', $false) - 'MicrosoftHardwareWarrantyAdministrator' = [MtRoleDefinition]::new('1501b917-7653-4ff9-a4b5-203eaf33784f', $false) - 'MicrosoftHardwareWarrantySpecialist' = [MtRoleDefinition]::new('281fe777-fb20-4fbb-b7a3-ccebce5b0d96', $false) - 'NetworkAdministrator' = [MtRoleDefinition]::new('d37c8bed-0711-4417-ba38-b4abe66ce4c2', $false) - 'OfficeAppsAdministrator' = [MtRoleDefinition]::new('2b745bdf-0803-4d80-aa65-822c4493daac', $false) - 'OnPremisesDirectorySyncAccount' = [MtRoleDefinition]::new('a92aed5d-d78a-4d16-b381-09adb37eb3b0', $false) - 'OrganizationalBrandingAdministrator' = [MtRoleDefinition]::new('92ed04bf-c94a-4b82-9729-b799a7a4c178', $false) - 'OrganizationalDataSourceAdministrator' = [MtRoleDefinition]::new('9d70768a-0cbc-4b4c-aea3-2e124b2477f4', $false) - 'OrganizationalMessagesApprover' = [MtRoleDefinition]::new('e48398e2-f4bb-4074-8f31-4586725e205b', $false) - 'OrganizationalMessagesWriter' = [MtRoleDefinition]::new('507f53e4-4e52-4077-abd3-d2e1558b6ea2', $false) - 'PartnerTier1Support' = [MtRoleDefinition]::new('4ba39ca4-527c-499a-b93d-d9b492c50246', $true) - 'PartnerTier2Support' = [MtRoleDefinition]::new('e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8', $true) - 'PasswordAdministrator' = [MtRoleDefinition]::new('966707d0-3269-4727-9be2-8c3a10f19b9d', $true) - 'PeopleAdministrator' = [MtRoleDefinition]::new('024906de-61e5-49c8-8572-40335f1e0e10', $false) - 'PermissionsManagementAdministrator' = [MtRoleDefinition]::new('af78dc32-cf4d-46f9-ba4e-4428526346b5', $false) - 'PlacesAdministrator' = [MtRoleDefinition]::new('78b0ccd1-afc2-4f92-9116-b41aedd09592', $false) - 'PowerPlatformAdministrator' = [MtRoleDefinition]::new('11648597-926c-4cf3-9c36-bcebb0ba8dcc', $false) - 'PrinterAdministrator' = [MtRoleDefinition]::new('644ef478-e28f-4e28-b9dc-3fdde9aa0b1f', $false) - 'PrinterTechnician' = [MtRoleDefinition]::new('e8cef6f1-e4bd-4ea8-bc07-4b8d950f4477', $false) - 'PrivilegedAuthenticationAdministrator' = [MtRoleDefinition]::new('7be44c8a-adaf-4e2a-84d6-ab2649e08a13', $true) - 'PrivilegedRoleAdministrator' = [MtRoleDefinition]::new('e8611ab8-c189-46e8-94e1-60213ab1f814', $true) - 'ReportsReader' = [MtRoleDefinition]::new('4a5d8f65-41da-4de4-8968-e035b65339cf', $false) - 'RestrictedGuestUser' = [MtRoleDefinition]::new('2af84b1e-32c8-42b7-82bc-daa82404023b', $false) - 'SearchAdministrator' = [MtRoleDefinition]::new('0964bb5e-9bdb-4d7b-ac29-58e794862a40', $false) - 'SearchEditor' = [MtRoleDefinition]::new('8835291a-918c-4fd7-a9ce-faa49f0cf7d9', $false) - 'SecurityAdministrator' = [MtRoleDefinition]::new('194ae4cb-b126-40b2-bd5b-6091b380977d', $true) - 'SecurityOperator' = [MtRoleDefinition]::new('5f2222b1-57c3-48ba-8ad5-d4759f1fde6f', $true) - 'SecurityReader' = [MtRoleDefinition]::new('5d6b6bb7-de71-4623-b4af-96380a352509', $true) - 'ServiceSupportAdministrator' = [MtRoleDefinition]::new('f023fd81-a637-4b56-95fd-791ac0226033', $false) - 'SharePointAdministrator' = [MtRoleDefinition]::new('f28a1f50-f6e7-4571-818b-6a12f2af6b6c', $false) - 'SharePointAdvancedManagementAdministrator' = [MtRoleDefinition]::new('99009c4a-3b3f-4957-82a9-9d35e12db77e', $false) - 'SharePointBackupAdministrator' = [MtRoleDefinition]::new('9d3e04ba-3ee4-4d1b-a3a7-9aef423a09be', $false) - 'SharePointEmbeddedAdministrator' = [MtRoleDefinition]::new('1a7d78b6-429f-476b-b8eb-35fb715fffd4', $false) - 'SkypeForBusinessAdministrator' = [MtRoleDefinition]::new('75941009-915a-4869-abe7-691bff18279e', $false) - 'TeamsAdministrator' = [MtRoleDefinition]::new('69091246-20e8-4a56-aa4d-066075b2a7a8', $false) - 'TeamsCommunicationsAdministrator' = [MtRoleDefinition]::new('baf37b3a-610e-45da-9e62-d9d1e5e8914b', $false) - 'TeamsCommunicationsSupportEngineer' = [MtRoleDefinition]::new('f70938a0-fc10-4177-9e90-2178f8765737', $false) - 'TeamsCommunicationsSupportSpecialist' = [MtRoleDefinition]::new('fcf91098-03e3-41a9-b5ba-6f0ec8188a12', $false) - 'TeamsDevicesAdministrator' = [MtRoleDefinition]::new('3d762c5a-1b6c-493f-843e-55a3b42923d4', $false) - 'TeamsExternalCollaborationAdministrator' = [MtRoleDefinition]::new('2fe872fb-daa8-4afc-8f6c-53c4565cfef4', $false) - 'TeamsReader' = [MtRoleDefinition]::new('1076ac91-f3d9-41a7-a339-dcdf5f480acc', $false) - 'TeamsTelephonyAdministrator' = [MtRoleDefinition]::new('aa38014f-0993-46e9-9b45-30501a20909d', $false) - 'TenantCreator' = [MtRoleDefinition]::new('112ca1a2-15ad-4102-995e-45b0bc479a6a', $false) - 'TenantGovernanceAdministrator' = [MtRoleDefinition]::new('1981f584-96e9-4a6f-95b0-f522373f8fae', $false) - 'TenantGovernanceReader' = [MtRoleDefinition]::new('e0a4caa6-fe82-443f-b92f-d87341d17b2e', $false) - 'TenantGovernanceRelationshipAdministrator' = [MtRoleDefinition]::new('b8e31d83-1534-480f-9b10-0338ded51b7e', $false) - 'TenantGovernanceRelationshipReader' = [MtRoleDefinition]::new('124577f8-48ed-456a-839f-13b419002e33', $false) - 'UsageSummaryReportsReader' = [MtRoleDefinition]::new('75934031-6c7e-415a-99d7-48dbd49e875e', $false) - 'User' = [MtRoleDefinition]::new('a0b1b346-4d3e-4e8b-98f8-753987be4970', $false) - 'UserAdministrator' = [MtRoleDefinition]::new('fe930be7-5e62-47db-91af-98c3a49a38b1', $true) - 'UserExperienceSuccessManager' = [MtRoleDefinition]::new('27460883-1df1-4691-b032-3b79643e5e63', $false) - 'VirtualVisitsAdministrator' = [MtRoleDefinition]::new('e300d9e7-4a2b-4295-9eff-f1c78b36cc98', $false) - 'VivaGlintTenantAdministrator' = [MtRoleDefinition]::new('0ec3f692-38d6-4d14-9e69-0377ca7797ad', $false) - 'VivaGoalsAdministrator' = [MtRoleDefinition]::new('92b086b3-e367-4ef2-b869-1de128fb986e', $false) - 'VivaPulseAdministrator' = [MtRoleDefinition]::new('87761b17-1ed2-4af3-9acd-92a150038160', $false) - 'Windows365Administrator' = [MtRoleDefinition]::new('11451d60-acb2-45eb-a7d6-43d0f0125c13', $false) - 'WindowsUpdateDeploymentAdministrator' = [MtRoleDefinition]::new('32696413-001a-46ae-978c-ce0f6b3620d2', $false) - 'WorkplaceDeviceJoin' = [MtRoleDefinition]::new('c34f683f-4d5a-4403-affd-6615e00e3a7f', $false) - 'YammerAdministrator' = [MtRoleDefinition]::new('810a2642-a034-447f-a5e8-41beaa378541', $false) + 'AuthenticationPolicyAdministrator' = [MtRoleDefinition]::new('0526716b-113d-4c15-b2c8-68e3c22b9f80', $false) + 'AzureDevOpsAdministrator' = [MtRoleDefinition]::new('e3973bdf-4987-49ae-837a-ba8e231c7286', $false) + 'AzureInformationProtectionAdministrator' = [MtRoleDefinition]::new('7495fdc4-34c4-4d15-a289-98788ce399fd', $false) + 'B2CIEFKeysetAdministrator' = [MtRoleDefinition]::new('aaf43236-0c0d-4d5f-883a-6955382ac081', $true) + 'B2CIEFPolicyAdministrator' = [MtRoleDefinition]::new('3edaf663-341e-4475-9f94-5c398ef6c070', $false) + 'BillingAdministrator' = [MtRoleDefinition]::new('b0f54661-2d74-4c50-afa3-1ec803f12efe', $false) + 'CloudApplicationAdministrator' = [MtRoleDefinition]::new('158c047a-c907-4556-b7ef-446551a6b5f7', $true) + 'CloudAppSecurityAdministrator' = [MtRoleDefinition]::new('892c5842-a9a6-463a-8041-72aa08ca3cf6', $false) + 'CloudDeviceAdministrator' = [MtRoleDefinition]::new('7698a772-787b-4ac8-901f-60d6b08affd2', $true) + 'ComplianceAdministrator' = [MtRoleDefinition]::new('17315797-102d-40b4-93e0-432062caca18', $false) + 'ComplianceDataAdministrator' = [MtRoleDefinition]::new('e6d1a23a-da11-4be4-9570-befc86d067a7', $false) + 'ConditionalAccessAdministrator' = [MtRoleDefinition]::new('b1be1c3e-b65d-4f19-8427-f6fa0d97feb9', $true) + 'CustomerLockboxAccessApprover' = [MtRoleDefinition]::new('5c4f9dcd-47dc-4cf7-8c9a-9e4207cbfc91', $false) + 'DesktopAnalyticsAdministrator' = [MtRoleDefinition]::new('38a96431-2bdf-4b4c-8b6e-5d3d8abac1a4', $false) + 'DeviceJoin' = [MtRoleDefinition]::new('9c094953-4995-41c8-84c8-3ebb9b32c93f', $false) + 'DeviceManagers' = [MtRoleDefinition]::new('2b499bcd-da44-4968-8aec-78e1674fa64d', $false) + 'DeviceUsers' = [MtRoleDefinition]::new('d405c6df-0af8-4e3b-95e4-4d06e542189e', $false) + 'DirectoryReaders' = [MtRoleDefinition]::new('88d8e3e3-8f55-4a1e-953a-9b9898b8876b', $false) + 'DirectorySynchronizationAccounts' = [MtRoleDefinition]::new('d29b2b05-8046-44ba-8758-1e26182fcf32', $false) + 'DirectoryWriters' = [MtRoleDefinition]::new('9360feb5-f418-4baa-8175-e2a00bac4301', $true) + 'DomainNameAdministrator' = [MtRoleDefinition]::new('8329153b-31d0-4727-b945-745eb3bc5f31', $true) + 'DragonAdministrator' = [MtRoleDefinition]::new('e93e3737-fa85-474a-aee4-7d3fb86510f3', $false) + 'Dynamics365Administrator' = [MtRoleDefinition]::new('44367163-eba1-44c3-98af-f5787879f96a', $false) + 'Dynamics365BusinessCentralAdministrator' = [MtRoleDefinition]::new('963797fb-eb3b-4cde-8ce3-5878b3f32a3f', $false) + 'EdgeAdministrator' = [MtRoleDefinition]::new('3f1acade-1e04-4fbc-9b69-f0302cd84aef', $false) + 'EntraBackupAdministrator' = [MtRoleDefinition]::new('b6a27b2b-f905-4b2e-81b5-0d90e0ef1fdb', $false) + 'EntraBackupReader' = [MtRoleDefinition]::new('f42252d9-5400-4d7b-b9ef-cc582dbb8577', $false) + 'ExchangeAdministrator' = [MtRoleDefinition]::new('29232cdf-9323-42fd-ade2-1d097af3e4de', $false) + 'ExchangeBackupAdministrator' = [MtRoleDefinition]::new('49eb8f75-97e9-4e37-9b2b-6c3ebfcffa31', $false) + 'ExchangeRecipientAdministrator' = [MtRoleDefinition]::new('31392ffb-586c-42d1-9346-e59415a2cc4e', $false) + 'ExtendedDirectoryUserAdministrator' = [MtRoleDefinition]::new('dd13091a-6207-4fc0-82ba-3641e056ab95', $false) + 'ExternalIdentityProviderAdministrator' = [MtRoleDefinition]::new('be2f45a1-457d-42af-a067-6ec1fa63bc45', $true) + 'ExternalIDUserFlowAdministrator' = [MtRoleDefinition]::new('6e591065-9bad-43ed-90f3-e9424366d2f0', $false) + 'ExternalIDUserFlowAttributeAdministrator' = [MtRoleDefinition]::new('0f971eea-41eb-4569-a71e-57bb8a3eff1e', $false) + 'FabricAdministrator' = [MtRoleDefinition]::new('a9ea8996-122f-4c74-9520-8edcd192826c', $false) + 'GlobalAdministrator' = [MtRoleDefinition]::new('62e90394-69f5-4237-9190-012177145e10', $true) + 'GlobalReader' = [MtRoleDefinition]::new('f2ef992c-3afb-46b9-b7cf-a126ee74c451', $true) + 'GlobalSecureAccessAdministrator' = [MtRoleDefinition]::new('ac434307-12b9-4fa1-a708-88bf58caabc1', $false) + 'GlobalSecureAccessLogReader' = [MtRoleDefinition]::new('843318fb-79a6-4168-9e6f-aa9a07481cc4', $false) + 'GroupsAdministrator' = [MtRoleDefinition]::new('fdd7a751-b60b-444a-984c-02652fe8fa1c', $false) + 'GuestInviter' = [MtRoleDefinition]::new('95e79109-95c0-4d8e-aee3-d01accf2d47b', $false) + 'GuestUser' = [MtRoleDefinition]::new('10dae51f-b6af-4016-8d66-8c2a99b929b3', $false) + 'HelpdeskAdministrator' = [MtRoleDefinition]::new('729827e3-9c14-49f7-bb1b-9608f156bbb8', $true) + 'HybridIdentityAdministrator' = [MtRoleDefinition]::new('8ac3fc64-6eca-42ea-9e69-59f4c7b60eb2', $true) + 'IdentityGovernanceAdministrator' = [MtRoleDefinition]::new('45d8d3c5-c802-45c6-b32a-1d70b5e1e86e', $false) + 'InsightsAdministrator' = [MtRoleDefinition]::new('eb1f4a8d-243a-41f0-9fbd-c7cdf6c5ef7c', $false) + 'InsightsAnalyst' = [MtRoleDefinition]::new('25df335f-86eb-4119-b717-0ff02de207e9', $false) + 'InsightsBusinessLeader' = [MtRoleDefinition]::new('31e939ad-9672-4796-9c2e-873181342d2d', $false) + 'IntuneAdministrator' = [MtRoleDefinition]::new('3a2c62db-5318-420d-8d74-23affee5d9d5', $true) + 'IoTDeviceAdministrator' = [MtRoleDefinition]::new('2ea5ce4c-b2d8-4668-bd81-3680bd2d227a', $false) + 'KaizalaAdministrator' = [MtRoleDefinition]::new('74ef975b-6605-40af-a5d2-b9539d836353', $false) + 'KnowledgeAdministrator' = [MtRoleDefinition]::new('b5a8dcf3-09d5-43a9-a639-8e29ef291470', $false) + 'KnowledgeManager' = [MtRoleDefinition]::new('744ec460-397e-42ad-a462-8b3f9747a02c', $false) + 'LicenseAdministrator' = [MtRoleDefinition]::new('4d6ac14f-3453-41d0-bef9-a3e0c569773a', $false) + 'LifecycleWorkflowsAdministrator' = [MtRoleDefinition]::new('59d46f88-662b-457b-bceb-5c3809e5908f', $true) + 'MessageCenterPrivacyReader' = [MtRoleDefinition]::new('ac16e43d-7b2d-40e0-ac05-243ff356ab5b', $false) + 'MessageCenterReader' = [MtRoleDefinition]::new('790c1fb9-7f7d-4f88-86a1-ef1f95c05c1b', $false) + 'Microsoft365BackupAdministrator' = [MtRoleDefinition]::new('1707125e-0aa2-4d4d-8655-a7c786c76a25', $false) + 'Microsoft365MigrationAdministrator' = [MtRoleDefinition]::new('8c8b803f-96e1-4129-9349-20738d9f9652', $false) + 'MicrosoftEntraJoinedDeviceLocalAdministrator' = [MtRoleDefinition]::new('9f06204d-73c1-4d4c-880a-6edb90606fd8', $false) + 'MicrosoftGraphDataConnectAdministrator' = [MtRoleDefinition]::new('ee67aa9c-e510-4759-b906-227085a7fd4d', $false) + 'MicrosoftHardwareWarrantyAdministrator' = [MtRoleDefinition]::new('1501b917-7653-4ff9-a4b5-203eaf33784f', $false) + 'MicrosoftHardwareWarrantySpecialist' = [MtRoleDefinition]::new('281fe777-fb20-4fbb-b7a3-ccebce5b0d96', $false) + 'NetworkAdministrator' = [MtRoleDefinition]::new('d37c8bed-0711-4417-ba38-b4abe66ce4c2', $false) + 'OfficeAppsAdministrator' = [MtRoleDefinition]::new('2b745bdf-0803-4d80-aa65-822c4493daac', $false) + 'OnPremisesDirectorySyncAccount' = [MtRoleDefinition]::new('a92aed5d-d78a-4d16-b381-09adb37eb3b0', $false) + 'OrganizationalBrandingAdministrator' = [MtRoleDefinition]::new('92ed04bf-c94a-4b82-9729-b799a7a4c178', $false) + 'OrganizationalDataSourceAdministrator' = [MtRoleDefinition]::new('9d70768a-0cbc-4b4c-aea3-2e124b2477f4', $false) + 'OrganizationalMessagesApprover' = [MtRoleDefinition]::new('e48398e2-f4bb-4074-8f31-4586725e205b', $false) + 'OrganizationalMessagesWriter' = [MtRoleDefinition]::new('507f53e4-4e52-4077-abd3-d2e1558b6ea2', $false) + 'PartnerTier1Support' = [MtRoleDefinition]::new('4ba39ca4-527c-499a-b93d-d9b492c50246', $true) + 'PartnerTier2Support' = [MtRoleDefinition]::new('e00e864a-17c5-4a4b-9c06-f5b95a8d5bd8', $true) + 'PasswordAdministrator' = [MtRoleDefinition]::new('966707d0-3269-4727-9be2-8c3a10f19b9d', $true) + 'PeopleAdministrator' = [MtRoleDefinition]::new('024906de-61e5-49c8-8572-40335f1e0e10', $false) + 'PermissionsManagementAdministrator' = [MtRoleDefinition]::new('af78dc32-cf4d-46f9-ba4e-4428526346b5', $false) + 'PlacesAdministrator' = [MtRoleDefinition]::new('78b0ccd1-afc2-4f92-9116-b41aedd09592', $false) + 'PowerPlatformAdministrator' = [MtRoleDefinition]::new('11648597-926c-4cf3-9c36-bcebb0ba8dcc', $false) + 'PrinterAdministrator' = [MtRoleDefinition]::new('644ef478-e28f-4e28-b9dc-3fdde9aa0b1f', $false) + 'PrinterTechnician' = [MtRoleDefinition]::new('e8cef6f1-e4bd-4ea8-bc07-4b8d950f4477', $false) + 'PrivilegedAuthenticationAdministrator' = [MtRoleDefinition]::new('7be44c8a-adaf-4e2a-84d6-ab2649e08a13', $true) + 'PrivilegedRoleAdministrator' = [MtRoleDefinition]::new('e8611ab8-c189-46e8-94e1-60213ab1f814', $true) + 'ReportsReader' = [MtRoleDefinition]::new('4a5d8f65-41da-4de4-8968-e035b65339cf', $false) + 'RestrictedGuestUser' = [MtRoleDefinition]::new('2af84b1e-32c8-42b7-82bc-daa82404023b', $false) + 'SearchAdministrator' = [MtRoleDefinition]::new('0964bb5e-9bdb-4d7b-ac29-58e794862a40', $false) + 'SearchEditor' = [MtRoleDefinition]::new('8835291a-918c-4fd7-a9ce-faa49f0cf7d9', $false) + 'SecurityAdministrator' = [MtRoleDefinition]::new('194ae4cb-b126-40b2-bd5b-6091b380977d', $true) + 'SecurityOperator' = [MtRoleDefinition]::new('5f2222b1-57c3-48ba-8ad5-d4759f1fde6f', $true) + 'SecurityReader' = [MtRoleDefinition]::new('5d6b6bb7-de71-4623-b4af-96380a352509', $true) + 'ServiceSupportAdministrator' = [MtRoleDefinition]::new('f023fd81-a637-4b56-95fd-791ac0226033', $false) + 'SharePointAdministrator' = [MtRoleDefinition]::new('f28a1f50-f6e7-4571-818b-6a12f2af6b6c', $false) + 'SharePointAdvancedManagementAdministrator' = [MtRoleDefinition]::new('99009c4a-3b3f-4957-82a9-9d35e12db77e', $false) + 'SharePointBackupAdministrator' = [MtRoleDefinition]::new('9d3e04ba-3ee4-4d1b-a3a7-9aef423a09be', $false) + 'SharePointEmbeddedAdministrator' = [MtRoleDefinition]::new('1a7d78b6-429f-476b-b8eb-35fb715fffd4', $false) + 'SkypeForBusinessAdministrator' = [MtRoleDefinition]::new('75941009-915a-4869-abe7-691bff18279e', $false) + 'TeamsAdministrator' = [MtRoleDefinition]::new('69091246-20e8-4a56-aa4d-066075b2a7a8', $false) + 'TeamsCommunicationsAdministrator' = [MtRoleDefinition]::new('baf37b3a-610e-45da-9e62-d9d1e5e8914b', $false) + 'TeamsCommunicationsSupportEngineer' = [MtRoleDefinition]::new('f70938a0-fc10-4177-9e90-2178f8765737', $false) + 'TeamsCommunicationsSupportSpecialist' = [MtRoleDefinition]::new('fcf91098-03e3-41a9-b5ba-6f0ec8188a12', $false) + 'TeamsDevicesAdministrator' = [MtRoleDefinition]::new('3d762c5a-1b6c-493f-843e-55a3b42923d4', $false) + 'TeamsExternalCollaborationAdministrator' = [MtRoleDefinition]::new('2fe872fb-daa8-4afc-8f6c-53c4565cfef4', $false) + 'TeamsReader' = [MtRoleDefinition]::new('1076ac91-f3d9-41a7-a339-dcdf5f480acc', $false) + 'TeamsTelephonyAdministrator' = [MtRoleDefinition]::new('aa38014f-0993-46e9-9b45-30501a20909d', $false) + 'TenantCreator' = [MtRoleDefinition]::new('112ca1a2-15ad-4102-995e-45b0bc479a6a', $false) + 'TenantGovernanceAdministrator' = [MtRoleDefinition]::new('1981f584-96e9-4a6f-95b0-f522373f8fae', $false) + 'TenantGovernanceReader' = [MtRoleDefinition]::new('e0a4caa6-fe82-443f-b92f-d87341d17b2e', $false) + 'TenantGovernanceRelationshipAdministrator' = [MtRoleDefinition]::new('b8e31d83-1534-480f-9b10-0338ded51b7e', $false) + 'TenantGovernanceRelationshipReader' = [MtRoleDefinition]::new('124577f8-48ed-456a-839f-13b419002e33', $false) + 'UsageSummaryReportsReader' = [MtRoleDefinition]::new('75934031-6c7e-415a-99d7-48dbd49e875e', $false) + 'User' = [MtRoleDefinition]::new('a0b1b346-4d3e-4e8b-98f8-753987be4970', $false) + 'UserAdministrator' = [MtRoleDefinition]::new('fe930be7-5e62-47db-91af-98c3a49a38b1', $true) + 'UserExperienceSuccessManager' = [MtRoleDefinition]::new('27460883-1df1-4691-b032-3b79643e5e63', $false) + 'VirtualVisitsAdministrator' = [MtRoleDefinition]::new('e300d9e7-4a2b-4295-9eff-f1c78b36cc98', $false) + 'VivaGlintTenantAdministrator' = [MtRoleDefinition]::new('0ec3f692-38d6-4d14-9e69-0377ca7797ad', $false) + 'VivaGoalsAdministrator' = [MtRoleDefinition]::new('92b086b3-e367-4ef2-b869-1de128fb986e', $false) + 'VivaPulseAdministrator' = [MtRoleDefinition]::new('87761b17-1ed2-4af3-9acd-92a150038160', $false) + 'Windows365Administrator' = [MtRoleDefinition]::new('11451d60-acb2-45eb-a7d6-43d0f0125c13', $false) + 'WindowsUpdateDeploymentAdministrator' = [MtRoleDefinition]::new('32696413-001a-46ae-978c-ce0f6b3620d2', $false) + 'WorkplaceDeviceJoin' = [MtRoleDefinition]::new('c34f683f-4d5a-4403-affd-6615e00e3a7f', $false) + 'YammerAdministrator' = [MtRoleDefinition]::new('810a2642-a034-447f-a5e8-41beaa378541', $false) # END AUTO-GENERATED ROLE DEFINITIONS } @@ -191,6 +191,9 @@ function Get-MtRoleInfo { if ([string]::IsNullOrWhiteSpace($RoleName)) { return $null } + # Guard against the role definitions not being loaded (e.g., if module scope was lost). + if ($null -eq $script:MtRoles) { return $null } + if ($script:MtRoles.ContainsKey($RoleName)) { return $script:MtRoles[$RoleName] } diff --git a/powershell/public/maester/entra/Test-MtCaExclusionForDirectorySyncAccount.ps1 b/powershell/public/maester/entra/Test-MtCaExclusionForDirectorySyncAccount.ps1 index cf9a5e577..b0b3ffacf 100644 --- a/powershell/public/maester/entra/Test-MtCaExclusionForDirectorySyncAccount.ps1 +++ b/powershell/public/maester/entra/Test-MtCaExclusionForDirectorySyncAccount.ps1 @@ -28,12 +28,19 @@ $testResult = "The following conditional access policies are scoped to all users but don't exclude the directory/OnPremises synchronization accounts:`n`n" try { - $DirectorySynchronizationAccountsRole = Get-MtRoleInfo -RoleName 'DirectorySynchronizationAccounts' - $OnPremisesDirectorySyncAccountRole = Get-MtRoleInfo -RoleName 'OnPremisesDirectorySyncAccount' + # Role template IDs are stable Microsoft-defined GUIDs; use them as fallback + # when Get-MtRoleInfo cannot resolve via the module role definitions cache. + # Variables kept as strings so they work correctly in -in comparisons against + # CA policy conditions (includeRoles/excludeRoles contain GUID strings). + $dirSyncInfo = Get-MtRoleInfo -RoleName 'DirectorySynchronizationAccounts' + $DirectorySynchronizationAccountsRole = if ($null -ne $dirSyncInfo) { $dirSyncInfo.ToString() } else { 'd29b2b05-8046-44ba-8758-1e26182fcf32' } + + $onPremInfo = Get-MtRoleInfo -RoleName 'OnPremisesDirectorySyncAccount' + $OnPremisesDirectorySyncAccountRole = if ($null -ne $onPremInfo) { $onPremInfo.ToString() } else { 'a92aed5d-d78a-4d16-b381-09adb37eb3b0' } $Members = @() - $Members += Get-MtRoleMember -RoleId $DirectorySynchronizationAccountsRole - $Members += Get-MtRoleMember -RoleId $OnPremisesDirectorySyncAccountRole + $Members += Get-MtRoleMember -RoleId ([guid]$DirectorySynchronizationAccountsRole) + $Members += Get-MtRoleMember -RoleId ([guid]$OnPremisesDirectorySyncAccountRole) $Members = @($Members | Where-Object { $null -ne $_ }) if ( $Members.Count -eq 0 ) { diff --git a/powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1 b/powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1 index b466d698b..b2ef19043 100644 --- a/powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1 +++ b/powershell/public/maester/entra/Test-MtCaMfaForAllUsers.ps1 @@ -61,7 +61,7 @@ return $result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess return $false } } diff --git a/powershell/public/maester/entra/Test-MtCaMfaForGuest.ps1 b/powershell/public/maester/entra/Test-MtCaMfaForGuest.ps1 index 41938ebf7..70661e42a 100644 --- a/powershell/public/maester/entra/Test-MtCaMfaForGuest.ps1 +++ b/powershell/public/maester/entra/Test-MtCaMfaForGuest.ps1 @@ -69,7 +69,7 @@ Add-MtTestResultDetail -Result $testResult -GraphObjects $policiesResult -GraphObjectType ConditionalAccess return $result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess return $false } } diff --git a/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 b/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 index 2b64a5ae8..a6a0fb4e2 100644 --- a/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 +++ b/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 @@ -24,12 +24,13 @@ return $null } + $result = $false + $hasRiskCAPolicy = $false # flag to check if there is any policy with risk controls, we skip the test if there is none + $policiesResult = New-Object System.Collections.ArrayList + $testResult = $null + try { $policies = Get-MtConditionalAccessPolicy | Where-Object { $_.state -eq 'enabled' } - $policiesResult = New-Object System.Collections.ArrayList - - $result = $false - $hasRiskCAPolicy = $false # flag to check if there is any policy with risk controls, we skip the test if there is none foreach ($policy in $policies) { if ($policy.conditions.userRiskLevels -or $policy.conditions.signInRiskLevels) { @@ -45,21 +46,23 @@ Write-Verbose "$($policy.displayName) - $CurrentResult" } - if ( -not $hasRiskCAPolicy ) { - Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason 'There are no Conditional Access policies with risk controls configured.' - return $null - } - if ( $result ) { $testResult = "The following conditional access policies have both sign-in risk and user risk controls configured:`n`n%TestResult%" } else { $testResult = 'Well done! No conditional access policies detected where sign-in risk and user risk are combined.' } - - Add-MtTestResultDetail -Result $testResult -GraphObjects $policiesResult -GraphObjectType ConditionalAccess - return $result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess return $false } + + # Skip check must happen outside the try-catch block to prevent Pester's internal + # flow-control exception from being caught and re-thrown as an error result. + if ( -not $hasRiskCAPolicy ) { + Add-MtTestResultDetail -SkippedBecause Custom -SkippedCustomReason 'There are no Conditional Access policies with risk controls configured.' + return $null + } + + Add-MtTestResultDetail -Result $testResult -GraphObjects $policiesResult -GraphObjectType ConditionalAccess + return $result } diff --git a/powershell/public/maester/entra/Test-MtCaReferencedGroupsExist.ps1 b/powershell/public/maester/entra/Test-MtCaReferencedGroupsExist.ps1 index f19f00ef5..a54cdcab4 100644 --- a/powershell/public/maester/entra/Test-MtCaReferencedGroupsExist.ps1 +++ b/powershell/public/maester/entra/Test-MtCaReferencedGroupsExist.ps1 @@ -1,5 +1,5 @@ function Test-MtCaReferencedGroupsExist { - <# + <# .Synopsis Checks if any conditional access policies include or exclude groups that have been deleted. @@ -79,7 +79,7 @@ return $result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess return $null } } diff --git a/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 b/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 index 20a781f37..94d00c3b7 100644 --- a/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 +++ b/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 @@ -56,7 +56,7 @@ return $result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess return $false } } diff --git a/powershell/public/maester/entra/Test-MtCaWIFBlockLegacyAuthentication.ps1 b/powershell/public/maester/entra/Test-MtCaWIFBlockLegacyAuthentication.ps1 index 64ee1c145..24c32f602 100644 --- a/powershell/public/maester/entra/Test-MtCaWIFBlockLegacyAuthentication.ps1 +++ b/powershell/public/maester/entra/Test-MtCaWIFBlockLegacyAuthentication.ps1 @@ -42,7 +42,7 @@ Write-Verbose "Checking if the user $UserId is blocked from using legacy authentication" return $Result } catch { - Add-MtTestResultDetail -Error $_ -GraphObjectType ConditionalAccess + Add-MtTestResultDetail -SkippedBecause Error -SkippedError $_ -GraphObjectType ConditionalAccess Write-Verbose "An error occurred while checking if the user $UserId is blocked from using legacy authentication" return $false } From 64b3671a263682d5429ac21e8a571e95465f94cc Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 15:00:45 +0200 Subject: [PATCH 04/10] feat: gate tests on license availability using BeforeDiscovery skip pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add BeforeDiscovery { $Licenses = Get-MtSessionLicenses } and Describe-level -Skip expressions to tests that require specific licenses: - License-EntraP2: CA risk sign-in/user risk tests (MT.1012, 1013, 1049), PIM alerts tests (MT.1029–1032) - License-EntraGovernance: Entitlement Management tests (MT.1106–1110) - License-Intune: MDE antivirus and Intune platform tests Also fix MT.1033/1034 test numbering (array IndexOf on single-object variables) and convert MT.1034 inline license check to -Skip expression. Co-authored-by: Copilot --- .../Test-MtMdeAntivirusPolicy.Tests.ps1 | 6 ++- .../Test-ConditionalAccessBaseline.Tests.ps1 | 12 ++++-- .../Test-ConditionalAccessWhatIf.Tests.ps1 | 13 +++--- ...titlementManagementDeletedGroups.Tests.ps1 | 5 ++- ...lementManagementInactivePolicies.Tests.ps1 | 5 ++- ...ementManagementOrphanedResources.Tests.ps1 | 5 ++- ...itlementManagementValidApprovers.Tests.ps1 | 5 ++- ...mentManagementValidResourceRoles.Tests.ps1 | 5 ++- .../Test-PrivilegedAssignments.Tests.ps1 | 40 +++++++------------ .../Intune/Test-MtIntunePlatform.Tests.ps1 | 6 ++- 10 files changed, 57 insertions(+), 45 deletions(-) diff --git a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 index ae63cbb5f..f2a4af74d 100644 --- a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 +++ b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 @@ -1,4 +1,8 @@ -Describe "Maester/Defender" -Tag "Maester", "Defender" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} + +Describe "Maester/Defender" -Tag "Maester", "Defender", "License-Intune" -Skip:($null -eq $Licenses.Intune) { It "MT.1148: Archive Scanning should be enabled. See https://maester.dev/docs/tests/MT.1148" -Tag "MT.1148" { $result = Test-MtMdeArchiveScanning if ($null -ne $result) { diff --git a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 index 98b253af1..b495a0e1b 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 @@ -1,4 +1,8 @@ -Describe "Maester/Entra" -Tag "Maester", "CA" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses + $EntraIDPlan = $Licenses.EntraID +} +Describe "Maester/Entra" -Tag "Maester", "CA" { It "MT.1001: At least one Conditional Access policy is configured with device compliance. See https://maester.dev/docs/tests/MT.1001" -Tag "MT.1001" { Test-MtCaDeviceComplianceExists | Should -Be $true -Because "there is no policy which requires device compliances" } @@ -29,10 +33,10 @@ It "MT.1011: At least one Conditional Access policy is configured to secure security info registration only from a trusted location. See https://maester.dev/docs/tests/MT.1011" -Tag "MT.1011" { Test-MtCaSecureSecurityInfoRegistration | Should -Be $true -Because "there is no policy that secures security info registration" } - It "MT.1012: At least one Conditional Access policy is configured to require MFA for risky sign-ins. See https://maester.dev/docs/tests/MT.1012" -Skip:( $EntraIDPlan -eq "P1" ) -Tag "MT.1012" { + It "MT.1012: At least one Conditional Access policy is configured to require MFA for risky sign-ins. See https://maester.dev/docs/tests/MT.1012" -Skip:($EntraIDPlan -notin 'P2', 'Governance') -Tag "MT.1012", "License-EntraP2" { Test-MtCaMfaForRiskySignIn | Should -Be $true -Because "there is no policy that requires MFA for risky sign-ins" } - It "MT.1013: At least one Conditional Access policy is configured to require new password when user risk is high. See https://maester.dev/docs/tests/MT.1013" -Skip:( $EntraIDPlan -eq "P1" ) -Tag "MT.1013" { + It "MT.1013: At least one Conditional Access policy is configured to require new password when user risk is high. See https://maester.dev/docs/tests/MT.1013" -Skip:($EntraIDPlan -notin 'P2', 'Governance') -Tag "MT.1013", "License-EntraP2" { Test-MtCaRequirePasswordChangeForHighUserRisk | Should -Be $true -Because "there is no policy that requires new password when user risk is high" } It "MT.1014: At least one Conditional Access policy is configured to require compliant or Entra hybrid joined devices for admins. See https://maester.dev/docs/tests/MT.1014" -Tag "MT.1014" { @@ -65,7 +69,7 @@ It "MT.1038: Conditional Access policies should not include or exclude deleted groups. See https://maester.dev/docs/tests/MT.1038" -Tag "MT.1038" { Test-MtCaReferencedGroupsExist | Should -Be $true -Because "there are one or more policies relying on deleted groups." } - It "MT.1049: Conditional Access policies for User Risk and Sign-in Risk should be configured separately. See https://maester.dev/docs/tests/MT.1049" -Tag "MT.1049" { + It "MT.1049: Conditional Access policies for User Risk and Sign-in Risk should be configured separately. See https://maester.dev/docs/tests/MT.1049" -Tag "MT.1049", "License-EntraP2" -Skip:($EntraIDPlan -notin 'P2', 'Governance') { Test-MtCaMisconfiguredIDProtection | Should -Be $false -Because "there is one or more policy with common misconfiguration for ID Protection " } It "MT.1052: At least one Conditional Access policy is targeting the Device Code authentication flow. See https://maester.dev/docs/tests/MT.1052" -Tag "MT.1052" { diff --git a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 index 8cf14863b..cc0270fa9 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 @@ -1,6 +1,7 @@ BeforeDiscovery { try { - $EntraIDPlan = Get-MtLicenseInformation -Product 'EntraID' + $Licenses = Get-MtSessionLicenses + $EntraIDPlan = $Licenses.EntraID $RegularUsers = Get-MtUser -Count 5 -UserType 'Member' $AdminUsers = Get-MtUser -Count 5 -UserType 'Admin' $EmergencyAccessUsers = Get-MtUser -Count 5 -UserType 'EmergencyAccess' @@ -21,7 +22,7 @@ Describe 'Maester/Entra' -Tag 'CA', 'CAWhatIf', 'LongRunning', 'Maester' { Context 'Maester/Entra' -ForEach @( $RegularUsers ) { # Regular users - It "MT.1033.$($RegularUsers.IndexOf($_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) { + It "MT.1033.$([array]::IndexOf(@($RegularUsers), $_)): User should be blocked from using legacy authentication ($($_.userPrincipalName))" -Tag 'MT.1033', 'CA', 'CAWhatIf', 'LongRunning', 'Maester' -Skip:( $EntraIDPlan -eq 'Free' ) { Test-MtCaWIFBlockLegacyAuthentication -UserId $id | Should -Be $true } @@ -29,12 +30,8 @@ Describe 'Maester/Entra' -Tag 'CA', 'CAWhatIf', 'LongRunning', 'Maester' { Context 'Maester/Entra' -ForEach @( $EmergencyAccessUsers ) { # Emergency access users - It "MT.1034.$($EmergencyAccessUsers.IndexOf($_)): Emergency access users should not be blocked ($($_.userPrincipalName))" -Tag 'MT.1034' { - if ( ( Get-MtLicenseInformation EntraID ) -eq 'Free' ) { - Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP1 - } else { - Test-MtConditionalAccessWhatIf -UserId $id -IncludeApplications '00000002-0000-0ff1-ce00-000000000000' -ClientAppType exchangeActiveSync | Should -BeNullOrEmpty - } + It "MT.1034.$([array]::IndexOf(@($EmergencyAccessUsers), $_)): Emergency access users should not be blocked ($($_.userPrincipalName))" -Tag 'MT.1034' -Skip:($EntraIDPlan -eq 'Free') { + Test-MtConditionalAccessWhatIf -UserId $id -IncludeApplications '00000002-0000-0ff1-ce00-000000000000' -ClientAppType exchangeActiveSync | Should -BeNullOrEmpty } } diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 index e4038d1d8..e8c4f955d 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 @@ -1,4 +1,7 @@ -Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} +Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1107: Access packages and catalogs should not reference deleted groups. See https://maester.dev/docs/tests/MT.1107" -Tag "MT.1107" { $result = Test-MtEntitlementManagementDeletedGroups $result | Should -Be $true -Because "Access packages and catalogs should not reference deleted groups to prevent access provisioning failures and configuration inconsistencies." diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 index 6982320d5..710a93cde 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 @@ -1,4 +1,7 @@ -Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} +Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1108: Access packages should not reference inactive or orphaned assignment policies. See https://maester.dev/docs/tests/MT.1108" -Tag "MT.1108" { $result = Test-MtEntitlementManagementInactivePolicies $result | Should -Be $true -Because "Access packages should not have inactive or misconfigured assignment policies that block access requests or break approval workflows." diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 index e06ad4f27..c68be8ee5 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 @@ -1,4 +1,7 @@ -Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} +Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1110: No catalog should contain resources without any associated access packages. See https://maester.dev/docs/tests/MT.1110" -Tag "MT.1110" { $result = Test-MtEntitlementManagementOrphanedResources $result | Should -Be $true -Because "Catalog resources without associated access packages indicate configuration drift and should be removed to maintain clean governance setup." diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 index c52cd8209..fa235a389 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 @@ -1,4 +1,7 @@ -Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} +Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1109: Access package approval workflows must have valid approvers. See https://maester.dev/docs/tests/MT.1109" -Tag "MT.1109" { $result = Test-MtEntitlementManagementValidApprovers $result | Should -Be $true -Because "Access package approval workflows must have valid approvers to prevent workflow failures and blocked access requests." diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 index 6f0bfeb0e..82693d084 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 @@ -1,4 +1,7 @@ -Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} +Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1106: Catalog resources must have valid roles (no stale / removed app roles or SPNs). See https://maester.dev/docs/tests/MT.1106" -Tag "MT.1106" { $result = Test-MtEntitlementManagementValidResourceRoles $result | Should -Be $true -Because "Catalog resources must have valid roles to ensure proper access provisioning. Stale or removed app roles and service principals can cause assignment failures." diff --git a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 index 9a45d24d6..3d52d77ef 100644 --- a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 +++ b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 @@ -1,4 +1,8 @@ -Describe "Maester/Entra" -Tag "Maester", "Privileged" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} + +Describe "Maester/Entra" -Tag "Maester", "Privileged" { It "MT.1025: No external user with permanent role assignment on Control Plane. See https://maester.dev/docs/tests/MT.1025" -Tag "MT.1025" { $Check = Test-MtPrivPermanentDirectoryRole -FilteredAccessLevel "ControlPlane" -FilterPrincipal "ExternalUser" $Check | Should -Be $false -Because "External user shouldn't have high-privileged roles" @@ -17,37 +21,21 @@ } } -Describe "Maester/Entra" -Tag "Privileged", "PIM" { +Describe "Maester/Entra" -Tag "Privileged", "PIM", "License-EntraP2" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1029: Stale accounts are not assigned to privileged roles. See https://maester.dev/docs/tests/MT.1029" -Tag "MT.1029" { - if ( ( Get-MtLicenseInformation EntraID ) -ne "P2" ) { - Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 - } else { - $Check = Test-MtPimAlertsExists -AlertId "StaleSignInAlert" - $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact - } + $Check = Test-MtPimAlertsExists -AlertId "StaleSignInAlert" + $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact } It "MT.1030: Eligible role assignments on Control Plane are in use by administrators. See https://maester.dev/docs/tests/MT.1030" -Tag "MT.1030" { - if ( ( Get-MtLicenseInformation EntraID ) -ne "P2" ) { - Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 - } else { - $Check = Test-MtPimAlertsExists -AlertId "RedundantAssignmentAlert" -FilteredAccessLevel "ControlPlane" - $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact - } + $Check = Test-MtPimAlertsExists -AlertId "RedundantAssignmentAlert" -FilteredAccessLevel "ControlPlane" + $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact } It "MT.1031: Privileged role on Control Plane are managed by PIM only. See https://maester.dev/docs/tests/MT.1031" -Tag "MT.1031" { - if ( ( Get-MtLicenseInformation EntraID ) -ne "P2" ) { - Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 - } else { - $Check = Test-MtPimAlertsExists -AlertId "RolesAssignedOutsidePimAlert" -FilteredAccessLevel "ControlPlane" - $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact - } + $Check = Test-MtPimAlertsExists -AlertId "RolesAssignedOutsidePimAlert" -FilteredAccessLevel "ControlPlane" + $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact } It "MT.1032: Limited number of Global Admins are assigned. See https://maester.dev/docs/tests/MT.1032" -Tag "MT.1032" { - if ( ( Get-MtLicenseInformation EntraID ) -ne "P2" ) { - Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 - } else { - $Check = Test-MtPimAlertsExists -AlertId "TooManyGlobalAdminsAssignedToTenantAlert" - $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact - } + $Check = Test-MtPimAlertsExists -AlertId "TooManyGlobalAdminsAssignedToTenantAlert" + $check.isActive -eq $false -or $check.numberOfAffectedItems -eq "0" | Should -Be $true -Because $check.securityImpact } } diff --git a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 index 0fa9fb966..e1b9d7d28 100644 --- a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 +++ b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 @@ -1,4 +1,8 @@ -Describe "Maester/Intune" -Tag "Maester", "Intune" { +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} + +Describe "Maester/Intune" -Tag "Maester", "Intune", "License-Intune" -Skip:($null -eq $Licenses.Intune) { It "MT.1053: Ensure intune device clean-up rule is configured" -Tag "MT.1053" { $result = Test-MtManagedDeviceCleanupSettings if ($null -ne $result) { From e4c7b05034c97d007df4360e8b7de0d7a6955990 Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 15:09:42 +0200 Subject: [PATCH 05/10] feat: add guidance for gating tests on license availability using Get-MtSessionLicenses Co-authored-by: Copilot --- .../docs/writing-tests/advanced-concepts.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/website/docs/writing-tests/advanced-concepts.md b/website/docs/writing-tests/advanced-concepts.md index a02ac6901..67aa015cb 100644 --- a/website/docs/writing-tests/advanced-concepts.md +++ b/website/docs/writing-tests/advanced-concepts.md @@ -70,6 +70,69 @@ $policy = Invoke-MtGraphRequest @policySplat To learn more see [Invoke-MtGraphRequest](https://github.com/maester365/maester/blob/main/powershell/public/Invoke-MtGraphRequest.ps1). +## Gating tests on license availability + +Some tests only make sense when the tenant has a specific license. Rather than letting those tests fail or produce misleading results on unlicensed tenants, you can skip them cleanly at Pester discovery time using `Get-MtSessionLicenses` and a `BeforeDiscovery` block. + +### Get-MtSessionLicenses + +`Get-MtSessionLicenses` returns a hashtable of all license products evaluated for the current tenant. The map is populated once by `Initialize-MtSession` when `Invoke-Maester` starts, so calling it inside a `BeforeDiscovery` block costs zero additional Graph API calls. + +The keys match the `-Product` parameter of `Get-MtLicenseInformation`: + +| Key | Possible values | +| ----------------- | ------------------------------------------ | +| `EntraID` | `'Free'`, `'P1'`, `'P2'`, `'Governance'` | +| `EntraWorkloadID` | `'P1'`, `'P2'`, or `$null` if not licensed | +| `Eop` | `'Eop'` or `$null` | +| `Mdo` | plan string or `$null` | +| `Intune` | `'Intune'` or `$null` | +| `DefenderXDR` | plan string or `$null` | +| `CustomerLockbox` | plan string or `$null` | +| *(others)* | plan string or `$null` | + +### BeforeDiscovery skip pattern + +Place `Get-MtSessionLicenses` inside a `BeforeDiscovery` block at the top of the test file. Variables set there are available to `-Skip:()` expressions on `Describe` and `It` blocks. + +```powershell +BeforeDiscovery { + $Licenses = Get-MtSessionLicenses +} + +Describe "Contoso" -Tag "Entra", "License-EntraP2" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { + It "CTS.2001: Some P2-only check" -Tag "CTS.2001" { + Test-ContosoSomeP2Feature | Should -Be $true + } +} +``` + +Use this pattern for each license tier: + +| Requirement | `-Skip:()` expression | +| ------------------------- | ----------------------------------------------------- | +| Entra ID P1 or above | `-Skip:($Licenses.EntraID -eq 'Free')` | +| Entra ID P2 or Governance | `-Skip:($Licenses.EntraID -notin 'P2', 'Governance')` | +| Intune | `-Skip:($null -eq $Licenses.Intune)` | +| Defender XDR | `-Skip:($null -eq $Licenses.DefenderXDR)` | + +### License tags + +Add the corresponding `License-*` tag to every `Describe` (or `It`) block that uses a license skip. This enables `Invoke-Maester -AutoFilterLicenses` to exclude the block entirely via tag filtering before discovery — a faster path on unlicensed tenants. + +| License requirement | Tag to add | +| ----------------------------- | ------------------------- | +| Entra ID P1 | `License-EntraP1` | +| Entra ID P2 | `License-EntraP2` | +| Entra ID Governance | `License-EntraGovernance` | +| Entra Workload ID | `License-EntraWorkloadID` | +| Exchange Online Protection | `License-Eop` | +| Microsoft Defender for Office | `License-Mdo` | +| Advanced Audit | `License-AdvAudit` | +| Defender XDR | `License-DefenderXDR` | +| Customer Lockbox | `License-CustomerLockbox` | +| Intune | `License-Intune` | + ## Splitting tests into multiple files As you write more tests you might find it helpful to split out the markdown part of the tests into a separate file. This helps reduce clutter in the test code and also allows content writers to independently edit the markdown files. Almost all the out of the box Maester tests use this approach of splitting out the markdown content for the test. From aa1e4fb3096c589ed9d9ba473dfe23e899ba9654 Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 19:44:27 +0200 Subject: [PATCH 06/10] fix: build errors 1. Pester test singular noun for Get-MTSessionLicenses -> Get-MTSessionLicens + all references 2. website build error with incomplete explicit mapping pair; a key node is missed; or followed by a non-tabulated empty line due to Helper: not being encapsulated --- powershell/Maester.psd1 | 2 +- ...SessionLicenses.ps1 => Get-MtSessionLicens.ps1} | 6 +++--- powershell/public/Invoke-Maester.ps1 | 14 +++++++------- .../Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 | 2 +- .../Entra/Test-ConditionalAccessBaseline.Tests.ps1 | 2 +- .../Entra/Test-ConditionalAccessWhatIf.Tests.ps1 | 2 +- ...-MtEntitlementManagementDeletedGroups.Tests.ps1 | 2 +- ...EntitlementManagementInactivePolicies.Tests.ps1 | 2 +- ...ntitlementManagementOrphanedResources.Tests.ps1 | 2 +- ...MtEntitlementManagementValidApprovers.Tests.ps1 | 2 +- ...titlementManagementValidResourceRoles.Tests.ps1 | 2 +- .../Entra/Test-PrivilegedAssignments.Tests.ps1 | 2 +- .../Maester/Intune/Test-MtIntunePlatform.Tests.ps1 | 2 +- website/docs/commands/Import-SingleResultFile.mdx | 2 +- website/docs/commands/Test-MaesterResultValid.mdx | 2 +- 15 files changed, 23 insertions(+), 23 deletions(-) rename powershell/public/{Get-MtSessionLicenses.ps1 => Get-MtSessionLicens.ps1} (90%) diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index de39012d2..3dd18f741 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -63,7 +63,7 @@ 'Get-MailAuthenticationRecord', 'Get-MtAdminPortalUrl', 'Get-MtAuthenticationMethodPolicyConfig', 'Get-MtAzureManagementGroup', 'Get-MtConditionalAccessPolicy', 'Get-MtExo', 'Get-MtExoThreatPolicyMalware', 'Get-MtGraphScope', 'Get-MtGroupMember', 'Get-MtHtmlReport', 'Get-MtLicenseInformation', 'Get-MtMaesterApp', 'Get-MtRole', - 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSessionLicenses', 'Get-MtTestInventory', 'Get-MtUser', + 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSessionLicens', 'Get-MtTestInventory', 'Get-MtUser', 'Get-MtUserAuthenticationMethod', 'Get-MtUserAuthenticationMethodInfoByType', 'Import-MtMaesterResult', 'Install-MaesterTests', 'Invoke-Maester', 'Invoke-MtAzureRequest', 'Invoke-MtAzureResourceGraphRequest', 'Invoke-MtGraphRequest', 'Invoke-MtGraphSecurityQuery', 'Merge-MtMaesterResult', 'New-MtMaesterApp', 'Resolve-SPFRecord', diff --git a/powershell/public/Get-MtSessionLicenses.ps1 b/powershell/public/Get-MtSessionLicens.ps1 similarity index 90% rename from powershell/public/Get-MtSessionLicenses.ps1 rename to powershell/public/Get-MtSessionLicens.ps1 index 6dba5c6c1..74d32b429 100644 --- a/powershell/public/Get-MtSessionLicenses.ps1 +++ b/powershell/public/Get-MtSessionLicens.ps1 @@ -1,4 +1,4 @@ -function Get-MtSessionLicenses { +function Get-MtSessionLicens { <# .SYNOPSIS Returns the pre-fetched license map for the current tenant session. @@ -15,7 +15,7 @@ .EXAMPLE BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Maester", "Entra" { @@ -31,7 +31,7 @@ } .LINK - https://maester.dev/docs/commands/Get-MtSessionLicenses + https://maester.dev/docs/commands/Get-MtSessionLicens #> [OutputType([hashtable])] [CmdletBinding()] diff --git a/powershell/public/Invoke-Maester.ps1 b/powershell/public/Invoke-Maester.ps1 index 4794e3fa2..a9339c389 100644 --- a/powershell/public/Invoke-Maester.ps1 +++ b/powershell/public/Invoke-Maester.ps1 @@ -98,7 +98,7 @@ Connect to all tested services and run all tests, including the long-running and preview tests. .EXAMPLE - Invoke-Maester -AutoFilterLicenses + Invoke-Maester -AutoFilterLicens Runs tests and automatically skips any test that requires a license the tenant does not have. For example, on a tenant with only a Business Premium license, tests requiring Entra ID P2, @@ -220,7 +220,7 @@ # appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly. # This requires a Graph connection and is silently ignored when not connected. [Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')] - [switch] $AutoFilterLicenses + [switch] $AutoFilterLicens ) function GetDefaultFileName() { @@ -341,9 +341,9 @@ # Initialize MtSession after Graph connected (also pre-fetches license information). Initialize-MtSession - # If -AutoFilterLicenses is set, exclude tests whose required license is absent in the tenant. - if ($AutoFilterLicenses.IsPresent) { - $tenantLicenses = Get-MtSessionLicenses + # If -AutoFilterLicens is set, exclude tests whose required license is absent in the tenant. + if ($AutoFilterLicens.IsPresent) { + $tenantLicenses = Get-MtSessionLicens if ($tenantLicenses.Count -gt 0) { $licenseExclusions = [System.Collections.Generic.List[string]]::new() @@ -376,10 +376,10 @@ if ($licenseExclusions.Count -gt 0) { $ExcludeTag = @($ExcludeTag | Where-Object { $_ }) + $licenseExclusions - Write-Verbose "AutoFilterLicenses: excluding tags $($licenseExclusions -join ', ')" + Write-Verbose "AutoFilterLicens: excluding tags $($licenseExclusions -join ', ')" } } else { - Write-Verbose 'AutoFilterLicenses: license data not available, skipping auto-filter.' + Write-Verbose 'AutoFilterLicens: license data not available, skipping auto-filter.' } } diff --git a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 index f2a4af74d..4b134e4a2 100644 --- a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 +++ b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Defender" -Tag "Maester", "Defender", "License-Intune" -Skip:($null -eq $Licenses.Intune) { diff --git a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 index b495a0e1b..ba27c022c 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens $EntraIDPlan = $Licenses.EntraID } Describe "Maester/Entra" -Tag "Maester", "CA" { diff --git a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 index cc0270fa9..26532085d 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 @@ -1,6 +1,6 @@ BeforeDiscovery { try { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens $EntraIDPlan = $Licenses.EntraID $RegularUsers = Get-MtUser -Count 5 -UserType 'Member' $AdminUsers = Get-MtUser -Count 5 -UserType 'Admin' diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 index e8c4f955d..17e0ae115 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1107: Access packages and catalogs should not reference deleted groups. See https://maester.dev/docs/tests/MT.1107" -Tag "MT.1107" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 index 710a93cde..9d6ed3a67 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1108: Access packages should not reference inactive or orphaned assignment policies. See https://maester.dev/docs/tests/MT.1108" -Tag "MT.1108" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 index c68be8ee5..e8ba44015 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1110: No catalog should contain resources without any associated access packages. See https://maester.dev/docs/tests/MT.1110" -Tag "MT.1110" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 index fa235a389..d8517084e 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1109: Access package approval workflows must have valid approvers. See https://maester.dev/docs/tests/MT.1109" -Tag "MT.1109" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 index 82693d084..1ba3e2123 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1106: Catalog resources must have valid roles (no stale / removed app roles or SPNs). See https://maester.dev/docs/tests/MT.1106" -Tag "MT.1106" { diff --git a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 index 3d52d77ef..86647c241 100644 --- a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 +++ b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Entra" -Tag "Maester", "Privileged" { diff --git a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 index e1b9d7d28..0fc6edcc2 100644 --- a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 +++ b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicens } Describe "Maester/Intune" -Tag "Maester", "Intune", "License-Intune" -Skip:($null -eq $Licenses.Intune) { diff --git a/website/docs/commands/Import-SingleResultFile.mdx b/website/docs/commands/Import-SingleResultFile.mdx index 2ca2ab17f..1681f96dd 100644 --- a/website/docs/commands/Import-SingleResultFile.mdx +++ b/website/docs/commands/Import-SingleResultFile.mdx @@ -1,6 +1,6 @@ --- sidebar_class_name: hidden -description: Helper: loads a single JSON file and adds valid results to the list. +description: "Helper: loads a single JSON file and adds valid results to the list." id: Import-SingleResultFile title: Import-SingleResultFile hide_title: false diff --git a/website/docs/commands/Test-MaesterResultValid.mdx b/website/docs/commands/Test-MaesterResultValid.mdx index b407f8539..807a34914 100644 --- a/website/docs/commands/Test-MaesterResultValid.mdx +++ b/website/docs/commands/Test-MaesterResultValid.mdx @@ -1,6 +1,6 @@ --- sidebar_class_name: hidden -description: Helper: validates that a result object has the required properties. +description: "Helper: validates that a result object has the required properties." id: Test-MaesterResultValid title: Test-MaesterResultValid hide_title: false From bbbb81bc0d403060fa33620f4f17e53d20b86c7b Mon Sep 17 00:00:00 2001 From: -Mynster Date: Sun, 3 May 2026 19:51:02 +0200 Subject: [PATCH 07/10] exclusion/ignore singular nouns on the command as it is already using it --- powershell/public/Get-MtSessionLicens.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/powershell/public/Get-MtSessionLicens.ps1 b/powershell/public/Get-MtSessionLicens.ps1 index 74d32b429..ce7db7636 100644 --- a/powershell/public/Get-MtSessionLicens.ps1 +++ b/powershell/public/Get-MtSessionLicens.ps1 @@ -33,6 +33,7 @@ .LINK https://maester.dev/docs/commands/Get-MtSessionLicens #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This command is already using singular nouns and is appropriately named for its purpose.')] [OutputType([hashtable])] [CmdletBinding()] param() From c734011972bcc3267b3679103135110d4faf2434 Mon Sep 17 00:00:00 2001 From: -Mynster <66535357+Mynster9361@users.noreply.github.com> Date: Mon, 4 May 2026 10:38:54 +0200 Subject: [PATCH 08/10] corrected singular noun name --- powershell/Maester.psd1 | 2 +- powershell/public/Get-MtSessionLicens.ps1 | 7 +++---- powershell/public/Invoke-Maester.ps1 | 2 +- .../Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 | 2 +- .../Entra/Test-ConditionalAccessBaseline.Tests.ps1 | 2 +- .../Entra/Test-ConditionalAccessWhatIf.Tests.ps1 | 2 +- ...st-MtEntitlementManagementDeletedGroups.Tests.ps1 | 2 +- ...MtEntitlementManagementInactivePolicies.Tests.ps1 | 2 +- ...tEntitlementManagementOrphanedResources.Tests.ps1 | 2 +- ...t-MtEntitlementManagementValidApprovers.Tests.ps1 | 2 +- ...EntitlementManagementValidResourceRoles.Tests.ps1 | 2 +- .../Entra/Test-PrivilegedAssignments.Tests.ps1 | 2 +- tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 | 2 +- website/docs/writing-tests/advanced-concepts.md | 12 ++++++------ 14 files changed, 21 insertions(+), 22 deletions(-) diff --git a/powershell/Maester.psd1 b/powershell/Maester.psd1 index 3dd18f741..5243a068f 100644 --- a/powershell/Maester.psd1 +++ b/powershell/Maester.psd1 @@ -63,7 +63,7 @@ 'Get-MailAuthenticationRecord', 'Get-MtAdminPortalUrl', 'Get-MtAuthenticationMethodPolicyConfig', 'Get-MtAzureManagementGroup', 'Get-MtConditionalAccessPolicy', 'Get-MtExo', 'Get-MtExoThreatPolicyMalware', 'Get-MtGraphScope', 'Get-MtGroupMember', 'Get-MtHtmlReport', 'Get-MtLicenseInformation', 'Get-MtMaesterApp', 'Get-MtRole', - 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSessionLicens', 'Get-MtTestInventory', 'Get-MtUser', + 'Get-MtRoleMember', 'Get-MtSafeMarkdown', 'Get-MtSession', 'Get-MtSessionLicense', 'Get-MtTestInventory', 'Get-MtUser', 'Get-MtUserAuthenticationMethod', 'Get-MtUserAuthenticationMethodInfoByType', 'Import-MtMaesterResult', 'Install-MaesterTests', 'Invoke-Maester', 'Invoke-MtAzureRequest', 'Invoke-MtAzureResourceGraphRequest', 'Invoke-MtGraphRequest', 'Invoke-MtGraphSecurityQuery', 'Merge-MtMaesterResult', 'New-MtMaesterApp', 'Resolve-SPFRecord', diff --git a/powershell/public/Get-MtSessionLicens.ps1 b/powershell/public/Get-MtSessionLicens.ps1 index ce7db7636..9d198069b 100644 --- a/powershell/public/Get-MtSessionLicens.ps1 +++ b/powershell/public/Get-MtSessionLicens.ps1 @@ -1,4 +1,4 @@ -function Get-MtSessionLicens { +function Get-MtSessionLicense { <# .SYNOPSIS Returns the pre-fetched license map for the current tenant session. @@ -15,7 +15,7 @@ .EXAMPLE BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Maester", "Entra" { @@ -31,9 +31,8 @@ } .LINK - https://maester.dev/docs/commands/Get-MtSessionLicens + https://maester.dev/docs/commands/Get-MtSessionLicense #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'This command is already using singular nouns and is appropriately named for its purpose.')] [OutputType([hashtable])] [CmdletBinding()] param() diff --git a/powershell/public/Invoke-Maester.ps1 b/powershell/public/Invoke-Maester.ps1 index a9339c389..d79fa414f 100644 --- a/powershell/public/Invoke-Maester.ps1 +++ b/powershell/public/Invoke-Maester.ps1 @@ -343,7 +343,7 @@ # If -AutoFilterLicens is set, exclude tests whose required license is absent in the tenant. if ($AutoFilterLicens.IsPresent) { - $tenantLicenses = Get-MtSessionLicens + $tenantLicenses = Get-MtSessionLicense if ($tenantLicenses.Count -gt 0) { $licenseExclusions = [System.Collections.Generic.List[string]]::new() diff --git a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 index 4b134e4a2..9cea61452 100644 --- a/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 +++ b/tests/Maester/Defender/Test-MtMdeAntivirusPolicy.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Defender" -Tag "Maester", "Defender", "License-Intune" -Skip:($null -eq $Licenses.Intune) { diff --git a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 index ba27c022c..1440aad02 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessBaseline.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense $EntraIDPlan = $Licenses.EntraID } Describe "Maester/Entra" -Tag "Maester", "CA" { diff --git a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 index 26532085d..a2ed4274d 100644 --- a/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 +++ b/tests/Maester/Entra/Test-ConditionalAccessWhatIf.Tests.ps1 @@ -1,6 +1,6 @@ BeforeDiscovery { try { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense $EntraIDPlan = $Licenses.EntraID $RegularUsers = Get-MtUser -Count 5 -UserType 'Member' $AdminUsers = Get-MtUser -Count 5 -UserType 'Admin' diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 index 17e0ae115..1e18daeaa 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementDeletedGroups.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1107: Access packages and catalogs should not reference deleted groups. See https://maester.dev/docs/tests/MT.1107" -Tag "MT.1107" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 index 9d6ed3a67..c8439acae 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementInactivePolicies.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1108: Access packages should not reference inactive or orphaned assignment policies. See https://maester.dev/docs/tests/MT.1108" -Tag "MT.1108" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 index e8ba44015..4872771f9 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementOrphanedResources.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1110: No catalog should contain resources without any associated access packages. See https://maester.dev/docs/tests/MT.1110" -Tag "MT.1110" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 index d8517084e..8ff25dca7 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidApprovers.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1109: Access package approval workflows must have valid approvers. See https://maester.dev/docs/tests/MT.1109" -Tag "MT.1109" { diff --git a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 index 1ba3e2123..989478540 100644 --- a/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 +++ b/tests/Maester/Entra/Test-MtEntitlementManagementValidResourceRoles.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Governance", "Entra", "AccessPackages", "License-EntraGovernance" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { It "MT.1106: Catalog resources must have valid roles (no stale / removed app roles or SPNs). See https://maester.dev/docs/tests/MT.1106" -Tag "MT.1106" { diff --git a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 index 86647c241..4bed3b251 100644 --- a/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 +++ b/tests/Maester/Entra/Test-PrivilegedAssignments.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Entra" -Tag "Maester", "Privileged" { diff --git a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 index 0fc6edcc2..4d74eaa8f 100644 --- a/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 +++ b/tests/Maester/Intune/Test-MtIntunePlatform.Tests.ps1 @@ -1,5 +1,5 @@ BeforeDiscovery { - $Licenses = Get-MtSessionLicens + $Licenses = Get-MtSessionLicense } Describe "Maester/Intune" -Tag "Maester", "Intune", "License-Intune" -Skip:($null -eq $Licenses.Intune) { diff --git a/website/docs/writing-tests/advanced-concepts.md b/website/docs/writing-tests/advanced-concepts.md index 67aa015cb..5b45d4cb6 100644 --- a/website/docs/writing-tests/advanced-concepts.md +++ b/website/docs/writing-tests/advanced-concepts.md @@ -27,7 +27,7 @@ The cache is reset when you run Invoke-Maester to ensure you always have the lat If your tests use Graph cmdlets like `Get-MgUser`, they will not benefit from this caching mechanism and will make a call to the Graph API every time they are run. -### Other key features of `Invoke-MtGraphRequest`: +### Other key features of `Invoke-MtGraphRequest` In addition to caching, `Invoke-MtGraphRequest` has other key features that make it very easy to write tests that query data. @@ -72,11 +72,11 @@ To learn more see [Invoke-MtGraphRequest](https://github.com/maester365/maester/ ## Gating tests on license availability -Some tests only make sense when the tenant has a specific license. Rather than letting those tests fail or produce misleading results on unlicensed tenants, you can skip them cleanly at Pester discovery time using `Get-MtSessionLicenses` and a `BeforeDiscovery` block. +Some tests only make sense when the tenant has a specific license. Rather than letting those tests fail or produce misleading results on unlicensed tenants, you can skip them cleanly at Pester discovery time using `Get-MtSessionLicense` and a `BeforeDiscovery` block. -### Get-MtSessionLicenses +### Get-MtSessionLicense -`Get-MtSessionLicenses` returns a hashtable of all license products evaluated for the current tenant. The map is populated once by `Initialize-MtSession` when `Invoke-Maester` starts, so calling it inside a `BeforeDiscovery` block costs zero additional Graph API calls. +`Get-MtSessionLicense` returns a hashtable of all license products evaluated for the current tenant. The map is populated once by `Initialize-MtSession` when `Invoke-Maester` starts, so calling it inside a `BeforeDiscovery` block costs zero additional Graph API calls. The keys match the `-Product` parameter of `Get-MtLicenseInformation`: @@ -93,11 +93,11 @@ The keys match the `-Product` parameter of `Get-MtLicenseInformation`: ### BeforeDiscovery skip pattern -Place `Get-MtSessionLicenses` inside a `BeforeDiscovery` block at the top of the test file. Variables set there are available to `-Skip:()` expressions on `Describe` and `It` blocks. +Place `Get-MtSessionLicense` inside a `BeforeDiscovery` block at the top of the test file. Variables set there are available to `-Skip:()` expressions on `Describe` and `It` blocks. ```powershell BeforeDiscovery { - $Licenses = Get-MtSessionLicenses + $Licenses = Get-MtSessionLicense } Describe "Contoso" -Tag "Entra", "License-EntraP2" -Skip:($Licenses.EntraID -notin 'P2', 'Governance') { From 49ccd2d1b4edf5f937dd5c86c782548169ea8592 Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Mon, 4 May 2026 09:46:45 -0400 Subject: [PATCH 09/10] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- powershell/public/Invoke-Maester.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powershell/public/Invoke-Maester.ps1 b/powershell/public/Invoke-Maester.ps1 index d79fa414f..fc505e5af 100644 --- a/powershell/public/Invoke-Maester.ps1 +++ b/powershell/public/Invoke-Maester.ps1 @@ -220,7 +220,7 @@ # appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly. # This requires a Graph connection and is silently ignored when not connected. [Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')] - [switch] $AutoFilterLicens + [switch] $AutoFilterLicenses ) function GetDefaultFileName() { From ccec8049ef26f46a4a88ff4e3446bf1dc9a1c7ff Mon Sep 17 00:00:00 2001 From: -Mynster <66535357+Mynster9361@users.noreply.github.com> Date: Mon, 4 May 2026 22:46:05 +0200 Subject: [PATCH 10/10] fixed most of copilots suggestions still missing suggestion for unit test. Will take a look at that wednessday unless you do it before me @SamErde :) --- ...-MtSessionLicens.ps1 => Get-MtSessionLicense.ps1} | 0 powershell/public/Invoke-Maester.ps1 | 12 ++++++------ .../maester/entra/Test-MtCaMfaForRiskySignIn.ps1 | 2 +- .../entra/Test-MtCaMisconfiguredIDProtection.ps1 | 2 +- ...Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 | 2 +- website/docs/writing-tests/advanced-concepts.md | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) rename powershell/public/{Get-MtSessionLicens.ps1 => Get-MtSessionLicense.ps1} (100%) diff --git a/powershell/public/Get-MtSessionLicens.ps1 b/powershell/public/Get-MtSessionLicense.ps1 similarity index 100% rename from powershell/public/Get-MtSessionLicens.ps1 rename to powershell/public/Get-MtSessionLicense.ps1 diff --git a/powershell/public/Invoke-Maester.ps1 b/powershell/public/Invoke-Maester.ps1 index fc505e5af..321225258 100644 --- a/powershell/public/Invoke-Maester.ps1 +++ b/powershell/public/Invoke-Maester.ps1 @@ -98,7 +98,7 @@ Connect to all tested services and run all tests, including the long-running and preview tests. .EXAMPLE - Invoke-Maester -AutoFilterLicens + Invoke-Maester -AutoFilterLicense Runs tests and automatically skips any test that requires a license the tenant does not have. For example, on a tenant with only a Business Premium license, tests requiring Entra ID P2, @@ -220,7 +220,7 @@ # appropriate License-* tags to ExcludeTag so unlicensed tests are skipped cleanly. # This requires a Graph connection and is silently ignored when not connected. [Parameter(HelpMessage = 'Skip tests that require licenses the tenant does not have.')] - [switch] $AutoFilterLicenses + [switch] $AutoFilterLicense ) function GetDefaultFileName() { @@ -341,8 +341,8 @@ # Initialize MtSession after Graph connected (also pre-fetches license information). Initialize-MtSession - # If -AutoFilterLicens is set, exclude tests whose required license is absent in the tenant. - if ($AutoFilterLicens.IsPresent) { + # If -AutoFilterLicense is set, exclude tests whose required license is absent in the tenant. + if ($AutoFilterLicense.IsPresent) { $tenantLicenses = Get-MtSessionLicense if ($tenantLicenses.Count -gt 0) { $licenseExclusions = [System.Collections.Generic.List[string]]::new() @@ -376,10 +376,10 @@ if ($licenseExclusions.Count -gt 0) { $ExcludeTag = @($ExcludeTag | Where-Object { $_ }) + $licenseExclusions - Write-Verbose "AutoFilterLicens: excluding tags $($licenseExclusions -join ', ')" + Write-Verbose "AutoFilterLicense: excluding tags $($licenseExclusions -join ', ')" } } else { - Write-Verbose 'AutoFilterLicens: license data not available, skipping auto-filter.' + Write-Verbose 'AutoFilterLicense: license data not available, skipping auto-filter.' } } diff --git a/powershell/public/maester/entra/Test-MtCaMfaForRiskySignIn.ps1 b/powershell/public/maester/entra/Test-MtCaMfaForRiskySignIn.ps1 index 9a0847f72..e811d0a33 100644 --- a/powershell/public/maester/entra/Test-MtCaMfaForRiskySignIn.ps1 +++ b/powershell/public/maester/entra/Test-MtCaMfaForRiskySignIn.ps1 @@ -19,7 +19,7 @@ [OutputType([bool])] param () - if ( ( Get-MtLicenseInformation EntraID ) -ne "P2" ) { + if ( ( Get-MtLicenseInformation EntraID ) -notin 'P2', 'Governance') { Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 return $null } diff --git a/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 b/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 index a6a0fb4e2..1ab4b318f 100644 --- a/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 +++ b/powershell/public/maester/entra/Test-MtCaMisconfiguredIDProtection.ps1 @@ -19,7 +19,7 @@ [OutputType([bool])] param () - if ( ( Get-MtLicenseInformation EntraID ) -ne 'P2' ) { + if ( ( Get-MtLicenseInformation EntraID ) -notin 'P2', 'Governance') { Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 return $null } diff --git a/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 b/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 index 94d00c3b7..304f020e7 100644 --- a/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 +++ b/powershell/public/maester/entra/Test-MtCaRequirePasswordChangeForHighUserRisk.ps1 @@ -19,7 +19,7 @@ [OutputType([bool])] param () - if ( ( Get-MtLicenseInformation EntraID ) -ne 'P2' ) { + if ( ( Get-MtLicenseInformation EntraID ) -notin 'P2', 'Governance') { Add-MtTestResultDetail -SkippedBecause NotLicensedEntraIDP2 return $null } diff --git a/website/docs/writing-tests/advanced-concepts.md b/website/docs/writing-tests/advanced-concepts.md index 5b45d4cb6..77eefb607 100644 --- a/website/docs/writing-tests/advanced-concepts.md +++ b/website/docs/writing-tests/advanced-concepts.md @@ -118,7 +118,7 @@ Use this pattern for each license tier: ### License tags -Add the corresponding `License-*` tag to every `Describe` (or `It`) block that uses a license skip. This enables `Invoke-Maester -AutoFilterLicenses` to exclude the block entirely via tag filtering before discovery — a faster path on unlicensed tenants. +Add the corresponding `License-*` tag to every `Describe` (or `It`) block that uses a license skip. This enables `Invoke-Maester -AutoFilterLicense` to exclude the block entirely via tag filtering before discovery — a faster path on unlicensed tenants. | License requirement | Tag to add | | ----------------------------- | ------------------------- |