From 711d844d6c0b3ecacdbce7998a695320ddd77f95 Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Wed, 6 May 2026 16:52:05 -0400 Subject: [PATCH 1/5] feat: add script to verify licenses in TestSettings against derived signals --- build/Test-MaesterConfigLicenses.ps1 | 394 +++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 build/Test-MaesterConfigLicenses.ps1 diff --git a/build/Test-MaesterConfigLicenses.ps1 b/build/Test-MaesterConfigLicenses.ps1 new file mode 100644 index 000000000..23f55d663 --- /dev/null +++ b/build/Test-MaesterConfigLicenses.ps1 @@ -0,0 +1,394 @@ +<# +.SYNOPSIS + Verifies the Licenses array on every TestSettings entry in tests/maester-config.json + against signals independently re-derived from the Pester test, the underlying PowerShell + function, and the markdown doc. + +.DESCRIPTION + For each TestSettings entry, prints: + Id | Config | Test | Function | Markdown | Verdict + + Verdicts: + OK — config matches the union of independent signals + BASELINE — config is [] and no signals anywhere (intentional baseline) + ORPHAN — no test file backs this Id (config-only entry) + TBD — config says TBD; no signals found + MISMATCH — independent signals contradict the config (config has too few or too many tokens) + + Pass -OnlyMismatches to filter out OK / BASELINE / ORPHAN rows. + +.EXAMPLE + pwsh build/Test-MaesterConfigLicenses.ps1 + pwsh build/Test-MaesterConfigLicenses.ps1 -OnlyMismatches +#> +[CmdletBinding()] +param( + [switch] $OnlyMismatches +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path $PSScriptRoot -Parent +$testsRoot = Join-Path $repoRoot 'tests' +$psPublicRoot = Join-Path $repoRoot 'powershell/public' +$psInternalRoot = Join-Path $repoRoot 'powershell/internal' +$docsRoot = Join-Path $repoRoot 'website/docs/tests' +$configPath = Join-Path $testsRoot 'maester-config.json' + +<# + To Do: Update mapping and actually used tokens to match Microsoft's official license names and SKUs, + rather than the internal shorthand we used during development. The mapping is only used for signal + extraction and display; the config can use any token names we want (e.g. "EntraIDP1" or "AzureADPremiumP1" + or "AADPremiumP1" could all be fine as long as we're consistent in the config and the mapping). Using + official names would just make the output clearer to external audiences and reduce the mental mapping. +#> + +# Token vocabulary mirrors Get-MtLicenseInformation.ps1 + Get-MtSkippedReason.ps1. +$skipReasonMap = @{ + 'NotLicensedEntraIDP1' = 'EntraIDP1' + 'NotLicensedEntraIDP2' = 'EntraIDP2' + 'NotLicensedEntraIDGovernance' = 'EntraIDGovernance' + 'NotLicensedEntraWorkloadID' = 'EntraWorkloadIDP1' + 'NotLicensedEop' = 'Eop' + 'NotLicensedExoDlp' = 'ExoDlp' + 'NotLicensedMdo' = 'MdoP2' + 'NotLicensedMdoP1' = 'MdoP1' + 'NotLicensedMdoP2' = 'MdoP2' + 'NotLicensedAdvAudit' = 'AdvAudit' + 'NotLicensedDefenderXDR' = 'DefenderXDR' + 'NotLicensedIntune' = 'Intune' + 'NotLicensedCustomerLockbox' = 'CustomerLockbox' +} + +$tagMap = @{ + 'entra id p1' = 'EntraIDP1' + 'entra id p2' = 'EntraIDP2' + 'governance' = 'EntraIDGovernance' + 'defenderxdr' = 'DefenderXDR' + 'intune' = 'Intune' + 'mdi' = 'DefenderXDR' + 'customerlockbox' = 'CustomerLockbox' + 'mdop1' = 'MdoP1' + 'mdop2' = 'MdoP2' +} + +# Markdown free-text phrase -> token. Keys are case-insensitive substrings; the verifier +# uses these to detect license claims authors made in remediation/overview prose. +$markdownPhraseMap = [ordered]@{ + 'entra id p2 license' = 'EntraIDP2' + 'entra id p1 license' = 'EntraIDP1' + 'entra id p2' = 'EntraIDP2' + 'entra id p1' = 'EntraIDP1' + 'entra id governance' = 'EntraIDGovernance' + 'aad premium p2' = 'EntraIDP2' + 'aad premium p1' = 'EntraIDP1' + 'azure ad premium p2' = 'EntraIDP2' + 'azure ad premium p1' = 'EntraIDP1' + 'workload identities premium' = 'EntraWorkloadIDP1' + 'defender for office 365 plan 2' = 'MdoP2' + 'defender for office 365 plan 1' = 'MdoP1' + 'defender for office (plan 2)' = 'MdoP2' + 'defender for office (plan 1)' = 'MdoP1' + 'defender for office 365 (plan 2)' = 'MdoP2' + 'defender for office 365 (plan 1)' = 'MdoP1' + 'microsoft defender xdr' = 'DefenderXDR' + 'defender xdr' = 'DefenderXDR' + 'microsoft intune' = 'Intune' + 'customer lockbox' = 'CustomerLockbox' + 'm365 advanced auditing' = 'AdvAudit' + 'microsoft 365 advanced auditing' = 'AdvAudit' + 'purview audit (premium)' = 'AdvAudit' + 'exchange online dlp' = 'ExoDlp' + 'microsoft purview data loss prevention' = 'ExoDlp' + 'exchange online protection' = 'Eop' +} + +$idPattern = '(MT\.\d+|CIS\.[A-Za-z0-9.]+|CISA\.[A-Za-z0-9.]+|EIDSCA\.[A-Z0-9]+|ORCA\.\d+(?:\.\d+)?|AZDO\.\d+)' + +function Get-CodeSignals { + [OutputType([string[]])] + param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text) + $tokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + if ([string]::IsNullOrWhiteSpace($Text)) { return @() } + + foreach ($m in [regex]::Matches($Text, '-SkippedBecause\s+(NotLicensed[A-Za-z0-9]+)')) { + $reason = $m.Groups[1].Value + if ($skipReasonMap.ContainsKey($reason)) { [void] $tokens.Add($skipReasonMap[$reason]) } + } + foreach ($m in [regex]::Matches($Text, "Get-MtLicenseInformation\s+(?:-Product\s+)?['""]?([A-Za-z]+)['""]?\s*\)?\s*(-eq|-ne)\s*['""]([A-Za-z0-9]+)['""]")) { + $product = $m.Groups[1].Value; $op = $m.Groups[2].Value; $val = $m.Groups[3].Value + switch ("$product/$op/$val") { + 'EntraID/-ne/P2' { [void] $tokens.Add('EntraIDP2'); break } + 'EntraID/-eq/P2' { [void] $tokens.Add('EntraIDP2'); break } + 'EntraID/-ne/P1' { [void] $tokens.Add('EntraIDP1'); break } + 'EntraID/-eq/Free' { [void] $tokens.Add('EntraIDP1'); break } + 'EntraID/-eq/Governance' { [void] $tokens.Add('EntraIDGovernance'); break } + default { } + } + } + foreach ($m in [regex]::Matches($Text, "\$EntraIDPlan\s*-eq\s*['""]Free['""]")) { [void] $tokens.Add('EntraIDP1') } + foreach ($m in [regex]::Matches($Text, "\$EntraIDPlan\s*-ne\s*['""]P2['""]")) { [void] $tokens.Add('EntraIDP2') } + foreach ($m in [regex]::Matches($Text, "\$DefenderPlan\s*-ne\s*['""]DefenderXDR['""]")) { [void] $tokens.Add('DefenderXDR') } + foreach ($pair in $tagMap.GetEnumerator()) { + if ($Text -match "[""']\s*$([regex]::Escape($pair.Key))\s*[""']") { [void] $tokens.Add($pair.Value) } + } + foreach ($m in [regex]::Matches($Text, "[""']License:([A-Za-z0-9]+)[""']")) { + [void] $tokens.Add($m.Groups[1].Value) + } + return @($tokens | Sort-Object) +} + +function Get-MarkdownSignals { + [OutputType([string[]])] + param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text) + $tokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + if ([string]::IsNullOrWhiteSpace($Text)) { return @() } + + # Markdown docs are auto-generated from Pester Describe tags. The Category line and + # Tags row both come from the same source — to make markdown signal *independent* + # we exclude the front-matter keywords block and the Test Metadata table, then scan + # the prose for license phrases. + $lines = $Text -split "`r?`n" + $body = [System.Collections.Generic.List[string]]::new() + $inFront = $false + $skipBlock = $false + foreach ($line in $lines) { + if ($line -match '^---\s*$') { $inFront = -not $inFront; continue } + if ($inFront) { continue } + if ($line -match '^## Test Metadata') { $skipBlock = $true; continue } + if ($skipBlock -and $line -match '^## ') { $skipBlock = $false } + if ($skipBlock) { continue } + $body.Add($line) + } + $prose = ($body -join "`n").ToLowerInvariant() + + foreach ($pair in $markdownPhraseMap.GetEnumerator()) { + if ($prose.Contains($pair.Key)) { [void] $tokens.Add($pair.Value) } + } + return @($tokens | Sort-Object) +} + +function Get-FunctionFileForTest { + param([Parameter(Mandatory)] [string] $TestFunctionName) + foreach ($root in @($psPublicRoot, $psInternalRoot)) { + if (-not (Test-Path $root)) { continue } + $hit = Get-ChildItem -Path $root -Recurse -Filter "$TestFunctionName.ps1" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($hit) { return $hit.FullName } + } + return $null +} + +function Get-MarkdownFileForId { + param([Parameter(Mandatory)] [string] $Id) + if (-not (Test-Path $docsRoot)) { return $null } + # Try direct, then parent prefix (MT.1033.0 -> MT.1033.md is unlikely but try). + $candidate = Get-ChildItem -Path $docsRoot -Recurse -Filter "$Id.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + $parent = $Id + while ($parent -match '\.\d+$') { + $parent = $parent -replace '\.\d+$', '' + $candidate = Get-ChildItem -Path $docsRoot -Recurse -Filter "$parent.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + } + return $null +} + +# --- Pass 1: index .Tests.ps1 by testId, capturing per-It text + enclosing header + function refs --- + +$testIndex = @{} + +foreach ($file in Get-ChildItem -Path $testsRoot -Recurse -Filter '*.Tests.ps1' -File) { + $content = Get-Content -Path $file.FullName -Raw + if (-not $content) { continue } + $itMatches = [regex]::Matches($content, '(?ms)\bIt\s+["''](?[^"'']+)["'']') + foreach ($itMatch in $itMatches) { + $itStart = $itMatch.Index + $name = $itMatch.Groups['name'].Value + $idMatch = [regex]::Match($name, "^${idPattern}:") + $id = $null + if ($idMatch.Success) { $id = $idMatch.Groups[1].Value } + + $braceDepth = 0; $started = $false; $itEnd = $content.Length + for ($i = $itStart; $i -lt $content.Length; $i++) { + $ch = $content[$i] + if ($ch -eq '{') { $braceDepth++; $started = $true } + elseif ($ch -eq '}') { $braceDepth--; if ($started -and $braceDepth -eq 0) { $itEnd = $i + 1; break } } + } + $itText = $content.Substring($itStart, [Math]::Min($itEnd - $itStart, $content.Length - $itStart)) + if (-not $id) { + $tagMatch = [regex]::Match($itText, $idPattern, 'IgnoreCase') + if ($tagMatch.Success) { $id = $tagMatch.Groups[1].Value } + } + if (-not $id) { continue } + + $headerText = $content.Substring(0, $itStart) + + $functionNames = [System.Collections.Generic.List[string]]::new() + foreach ($fnMatch in [regex]::Matches($itText, '\b(Test-[A-Za-z0-9_]+)\b')) { + $fn = $fnMatch.Groups[1].Value + if ($fn -eq 'Test-MtEidscaControl') { + $checkIdMatch = [regex]::Match($itText, '-CheckId\s+([A-Z0-9]+)') + if ($checkIdMatch.Success) { $functionNames.Add('Test-MtEidsca' + $checkIdMatch.Groups[1].Value) } + } elseif ($fn -ne 'Test-Path' -and $fn -ne 'Test-MtConnection') { + $functionNames.Add($fn) + } + } + $functionName = if ($functionNames.Count -gt 0) { $functionNames[0] } else { $null } + + if ($testIndex.ContainsKey($id)) { + $testIndex[$id].ItText += "`n$itText" + if (-not $testIndex[$id].EnclosingText.Contains($headerText)) { + $testIndex[$id].EnclosingText += "`n$headerText" + } + if (-not $testIndex[$id].FunctionName -and $functionName) { $testIndex[$id].FunctionName = $functionName } + } else { + $testIndex[$id] = @{ + ItText = $itText; EnclosingText = $headerText; FunctionName = $functionName; File = $file.FullName + } + } + } +} + +# --- Pass 2: per-id verification --- + +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + +$rows = [System.Collections.Generic.List[object]]::new() +$counts = @{ OK = 0; OK_MD = 0; BASELINE = 0; ORPHAN = 0; TBD = 0; MISMATCH = 0 } + +foreach ($setting in $config.TestSettings) { + $id = $setting.Id + $configTokens = @($setting.Licenses | Where-Object { $_ }) + $configIsTBD = ($setting.Licenses -contains 'TBD') + + # Resolve test entry, with parent-prefix fallback. + $entry = $null + if ($testIndex.ContainsKey($id)) { $entry = $testIndex[$id] } + else { + $parent = $id + while ($parent -match '\.\d+$' -and -not $entry) { + $parent = $parent -replace '\.\d+$', '' + if ($testIndex.ContainsKey($parent)) { $entry = $testIndex[$parent] } + } + } + + $testTokens = @() + $functionTokens = @() + $markdownTokens = @() + + if ($entry) { + $itAndHeader = $entry.ItText + "`n" + $entry.EnclosingText + $testTokens = Get-CodeSignals -Text $itAndHeader + + if ($entry.FunctionName) { + $functionFile = Get-FunctionFileForTest -TestFunctionName $entry.FunctionName + if ($functionFile) { + $functionContent = Get-Content -Path $functionFile -Raw + $functionTokens = Get-CodeSignals -Text $functionContent + } + } + } + + $markdownFile = Get-MarkdownFileForId -Id $id + if ($markdownFile) { + $markdownContent = Get-Content -Path $markdownFile -Raw + $markdownTokens = Get-MarkdownSignals -Text $markdownContent + } + + # Also scan the companion .md beside the PowerShell function (e.g. + # powershell/public/maester/entra/Test-MtCaAzureDevOps.md), which often + # contains a "Licensing requirement: Microsoft Entra ID P1 or P2..." line + # that does NOT appear in the website doc. + if ($entry -and $entry.FunctionName) { + $functionFile = Get-FunctionFileForTest -TestFunctionName $entry.FunctionName + if ($functionFile) { + $companionMd = [System.IO.Path]::ChangeExtension($functionFile, '.md') + if (Test-Path $companionMd) { + $companionContent = Get-Content -Path $companionMd -Raw + $companionTokens = Get-MarkdownSignals -Text $companionContent + $existing = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($t in $markdownTokens) { if ($t) { [void] $existing.Add($t) } } + foreach ($t in $companionTokens) { if ($t) { [void] $existing.Add($t) } } + $markdownTokens = @($existing | Sort-Object) + } + } + } + + # Suite default for AZDO is correct without any signal. + $expectAzureDevOps = ($id -like 'AZDO.*' -and $testTokens.Count -eq 0 -and $functionTokens.Count -eq 0 -and $markdownTokens.Count -eq 0) + + $expectedUnion = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($t in $testTokens) { [void] $expectedUnion.Add($t) } + foreach ($t in $functionTokens) { [void] $expectedUnion.Add($t) } + # Markdown is a *cross-check* only — phrases like "Defender for Office 365 Plan 2" + # routinely appear in remediation prose for tests that gate at Plan 1, so we don't + # add markdown tokens to the expected set unless they reinforce code signals. + # We'll still surface them in the row for review. + + if ($expectAzureDevOps) { [void] $expectedUnion.Add('AzureDevOps') } + + $verdict = $null + if (-not $entry -and ($id -notlike 'AZDO.*')) { + $verdict = 'ORPHAN' + } elseif ($configIsTBD) { + $verdict = 'TBD' + } elseif ($expectedUnion.Count -eq 0 -and $configTokens.Count -eq 0) { + $verdict = 'BASELINE' + } else { + $configSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($t in $configTokens) { if ($t) { [void] $configSet.Add($t) } } + $markdownSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + foreach ($t in $markdownTokens) { if ($t) { [void] $markdownSet.Add($t) } } + + # Required: every code-derived token MUST be in the config. + $missingFromConfig = $expectedUnion | Where-Object { -not $configSet.Contains($_) } + + # Extras in config that aren't backed by code — OK only if markdown supports them. + $markdownOnly = @($configSet | Where-Object { -not $expectedUnion.Contains($_) -and $markdownSet.Contains($_) }) + $unsupported = @($configSet | Where-Object { -not $expectedUnion.Contains($_) -and -not $markdownSet.Contains($_) }) + + if ($missingFromConfig -or $unsupported) { + $verdict = 'MISMATCH' + } elseif ($markdownOnly.Count -gt 0) { + $verdict = 'OK_MD' # config matches code + markdown-justified extras + } else { + $verdict = 'OK' + } + } + + $counts[$verdict] += 1 + + $rows.Add([pscustomobject]@{ + Id = $id + Config = ($configTokens -join ', ') + Test = ($testTokens -join ', ') + Function = ($functionTokens -join ', ') + Markdown = ($markdownTokens -join ', ') + Verdict = $verdict + }) +} + +# --- Output --- + +$display = if ($OnlyMismatches) { $rows | Where-Object { $_.Verdict -in @('MISMATCH', 'TBD', 'ORPHAN') } } else { $rows } + +$display | Format-Table -AutoSize | Out-String -Width 240 | Write-Host + +Write-Host '' +Write-Host 'Verdict counts:' -ForegroundColor Cyan +$counts.GetEnumerator() | Sort-Object Name | ForEach-Object { ' {0,-10} {1,4}' -f $_.Key, $_.Value } | Write-Host + +# Also surface markdown-only signals (phrases in docs that were not in code). +Write-Host '' +Write-Host 'Markdown-only signals (in markdown but not in code) — review for missed licenses:' -ForegroundColor Yellow +$mdOnly = $rows | Where-Object { + $cfg = ($_.Config -split ',\s*') | Where-Object { $_ } + $code = (($_.Test -split ',\s*') + ($_.Function -split ',\s*')) | Where-Object { $_ } + $md = ($_.Markdown -split ',\s*') | Where-Object { $_ } + foreach ($t in $md) { if ($t -and ($t -notin $code) -and ($t -notin $cfg)) { return $true } } + $false +} +if ($mdOnly) { + $mdOnly | Format-Table Id, Config, Markdown -AutoSize | Out-String -Width 200 | Write-Host +} else { + Write-Host ' (none)' -ForegroundColor DarkGray +} From 769fbaa6f9e892d930537633c79c769c5f9f14c4 Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Wed, 6 May 2026 16:52:35 -0400 Subject: [PATCH 2/5] feat: add script to populate Licenses array in TestSettings from Pester tests --- build/Update-MaesterConfigLicenses.ps1 | 430 +++++++++++++++++++++++++ 1 file changed, 430 insertions(+) create mode 100644 build/Update-MaesterConfigLicenses.ps1 diff --git a/build/Update-MaesterConfigLicenses.ps1 b/build/Update-MaesterConfigLicenses.ps1 new file mode 100644 index 000000000..11899ad0a --- /dev/null +++ b/build/Update-MaesterConfigLicenses.ps1 @@ -0,0 +1,430 @@ +<# +.SYNOPSIS + Populates the Licenses array on every TestSettings entry in tests/maester-config.json. + +.DESCRIPTION + Scans every Pester test (tests/**/*.Tests.ps1) and the PowerShell function it + invokes (powershell/public|internal/...) for license signals, then writes the + resulting Licenses array onto each matching TestSettings item by Id. + + Signals (first match wins, but multiple matches union): + 1. Add-MtTestResultDetail -SkippedBecause NotLicensed + 2. Get-MtLicenseInformation -Product X comparisons + 3. -Skip:( ... license check ... ) on the It block + 4. Same patterns inside the underlying Test-* function + 5. License-bearing Describe/Context/It tags + 6. Suite default (AZDO -> AzureDevOps; EIDSCA -> []; everything else -> TBD) + + Empty array = baseline (no premium license required). + ['TBD'] = needs human review. + +.PARAMETER DryRun + Print the per-test license map and a tally; do not write to maester-config.json. + +.EXAMPLE + pwsh build/Update-MaesterConfigLicenses.ps1 -DryRun + pwsh build/Update-MaesterConfigLicenses.ps1 +#> +[CmdletBinding()] +param( + [switch] $DryRun +) + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path $PSScriptRoot -Parent +$testsRoot = Join-Path $repoRoot 'tests' +$psPublicRoot = Join-Path $repoRoot 'powershell/public' +$psInternalRoot = Join-Path $repoRoot 'powershell/internal' +$configPath = Join-Path $testsRoot 'maester-config.json' + +# --- Token vocabulary (kept in sync with Get-MtLicenseInformation.ps1 + Get-MtSkippedReason.ps1) --- +$canonicalTokens = @( + 'EntraIDP1', 'EntraIDP2', 'EntraIDGovernance', + 'EntraWorkloadIDP1', 'EntraWorkloadIDP2', + 'Eop', 'MdoP1', 'MdoP2', + 'ExoDlp', 'AdvAudit', 'DefenderXDR', + 'Intune', 'CustomerLockbox', + 'AzureDevOps', 'TBD' +) + +# Map NotLicensed -> token. Mdo (no plan) is treated as MdoP2 because the +# skip text in Get-MtSkippedReason.ps1 references "Defender for Office 365 Plan 2". +$skipReasonMap = @{ + 'NotLicensedEntraIDP1' = 'EntraIDP1' + 'NotLicensedEntraIDP2' = 'EntraIDP2' + 'NotLicensedEntraIDGovernance' = 'EntraIDGovernance' + 'NotLicensedEntraWorkloadID' = 'EntraWorkloadIDP1' + 'NotLicensedEop' = 'Eop' + 'NotLicensedExoDlp' = 'ExoDlp' + 'NotLicensedMdo' = 'MdoP2' + 'NotLicensedMdoP1' = 'MdoP1' + 'NotLicensedMdoP2' = 'MdoP2' + 'NotLicensedAdvAudit' = 'AdvAudit' + 'NotLicensedDefenderXDR' = 'DefenderXDR' + 'NotLicensedIntune' = 'Intune' + 'NotLicensedCustomerLockbox' = 'CustomerLockbox' +} + +# Describe/Context/It tag -> token (case-insensitive match against trimmed tag). +# 'entra id free' is intentionally unmapped: an explicit baseline tag yields the +# same '[]' as the no-signal default, so no entry is needed. +$tagMap = @{ + 'entra id p1' = 'EntraIDP1' + 'entra id p2' = 'EntraIDP2' + 'governance' = 'EntraIDGovernance' + 'defenderxdr' = 'DefenderXDR' + 'intune' = 'Intune' + 'mdi' = 'DefenderXDR' + 'customerlockbox' = 'CustomerLockbox' + 'mdop1' = 'MdoP1' + 'mdop2' = 'MdoP2' +} + +# Test ID regex (matches website/scripts/generate-test-docs.mjs:231) +$idPattern = '(MT\.\d+|CIS\.[A-Za-z0-9.]+|CISA\.[A-Za-z0-9.]+|EIDSCA\.[A-Z0-9]+|ORCA\.\d+(?:\.\d+)?|AZDO\.\d+)' + +function Get-LicenseTokensFromText { + [OutputType([string[]])] + param( + [Parameter(Mandatory)] + [AllowEmptyString()] + [string] $Text + ) + $tokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + + if ([string]::IsNullOrWhiteSpace($Text)) { return @() } + + # Signal 1: Add-MtTestResultDetail -SkippedBecause NotLicensed<...> + foreach ($m in [regex]::Matches($Text, '-SkippedBecause\s+(NotLicensed[A-Za-z0-9]+)')) { + $reason = $m.Groups[1].Value + if ($skipReasonMap.ContainsKey($reason)) { + [void] $tokens.Add($skipReasonMap[$reason]) + } + } + + # Signal 2: Get-MtLicenseInformation -Product X comparisons inside this scope. + # Look at conditions paired with each Get-MtLicenseInformation call. + foreach ($m in [regex]::Matches($Text, "Get-MtLicenseInformation\s+(?:-Product\s+)?['""]?([A-Za-z]+)['""]?\s*\)?\s*(-eq|-ne)\s*['""]([A-Za-z0-9]+)['""]")) { + $product = $m.Groups[1].Value + $operator = $m.Groups[2].Value + $value = $m.Groups[3].Value + + switch ("$product/$operator/$value") { + 'EntraID/-ne/P2' { [void] $tokens.Add('EntraIDP2'); break } + 'EntraID/-eq/P2' { [void] $tokens.Add('EntraIDP2'); break } + 'EntraID/-ne/P1' { [void] $tokens.Add('EntraIDP1'); break } + 'EntraID/-eq/Free' { [void] $tokens.Add('EntraIDP1'); break } + 'EntraID/-ne/Free' { break } # Need P1+, but exact tier unknown; covered by signal 1 if present + 'EntraID/-eq/Governance' { [void] $tokens.Add('EntraIDGovernance'); break } + default { } + } + } + + # Pattern: ($EntraIDPlan -eq 'Free') -> need P1+ + foreach ($m in [regex]::Matches($Text, "\$EntraIDPlan\s*-eq\s*['""](Free)['""]")) { + [void] $tokens.Add('EntraIDP1') + } + # Pattern: ($EntraIDPlan -ne 'P2') -> need P2 + foreach ($m in [regex]::Matches($Text, "\$EntraIDPlan\s*-ne\s*['""]P2['""]")) { + [void] $tokens.Add('EntraIDP2') + } + # Pattern: ($DefenderPlan -ne 'DefenderXDR') -> need DefenderXDR + foreach ($m in [regex]::Matches($Text, "\$DefenderPlan\s*-ne\s*['""]DefenderXDR['""]")) { + [void] $tokens.Add('DefenderXDR') + } + + # Signal 5: License-bearing tags. Tags are quoted strings; we'll look for any of them. + foreach ($pair in $tagMap.GetEnumerator()) { + $tag = $pair.Key + if ($Text -match "[""']\s*$([regex]::Escape($tag))\s*[""']") { + [void] $tokens.Add($pair.Value) + } + } + + # Forward-compatible License: tag form. + foreach ($m in [regex]::Matches($Text, "[""']License:([A-Za-z0-9]+)[""']")) { + $candidate = $m.Groups[1].Value + if ($canonicalTokens -contains $candidate) { + [void] $tokens.Add($candidate) + } + } + + return @($tokens | Sort-Object) +} + +function Get-FunctionFileForTest { + param( + [Parameter(Mandatory)] [string] $TestFunctionName + ) + foreach ($root in @($psPublicRoot, $psInternalRoot)) { + if (-not (Test-Path $root)) { continue } + $hit = Get-ChildItem -Path $root -Recurse -Filter "$TestFunctionName.ps1" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($hit) { return $hit.FullName } + } + return $null +} + +# --- Pass 1: parse every test file into per-test scope text --- + +$testIndex = @{} # testId -> @{ ItText, EnclosingText, FunctionName } + +foreach ($file in Get-ChildItem -Path $testsRoot -Recurse -Filter '*.Tests.ps1' -File) { + $content = Get-Content -Path $file.FullName -Raw + if (-not $content) { continue } + + # Find every It block. We use a brace-counting walker because Pester It blocks contain {...}. + $itMatches = [regex]::Matches($content, '(?ms)\bIt\s+["''](?[^"'']+)["'']') + foreach ($itMatch in $itMatches) { + $itStart = $itMatch.Index + $name = $itMatch.Groups['name'].Value + + # Resolve testId + $idMatch = [regex]::Match($name, "^${idPattern}:") + $id = $null + if ($idMatch.Success) { $id = $idMatch.Groups[1].Value } + + # Walk forward to find the matching closing brace of this It's script block. + $braceDepth = 0 + $started = $false + $itEnd = $content.Length + for ($i = $itStart; $i -lt $content.Length; $i++) { + $ch = $content[$i] + if ($ch -eq '{') { $braceDepth++; $started = $true } + elseif ($ch -eq '}') { + $braceDepth-- + if ($started -and $braceDepth -eq 0) { $itEnd = $i + 1; break } + } + } + $itText = $content.Substring($itStart, [Math]::Min($itEnd - $itStart, $content.Length - $itStart)) + + # Fall back: if no testId in the It name, look for it in the It's tag list (-Tag "MT.1234") + if (-not $id) { + $tagMatch = [regex]::Match($itText, "${idPattern}", 'IgnoreCase') + if ($tagMatch.Success) { $id = $tagMatch.Groups[1].Value } + } + if (-not $id) { continue } + + # Find the enclosing Describe/Context tag arguments (everything before $itStart in this file). + $headerText = $content.Substring(0, $itStart) + # Limit to the most recent Describe/Context block by collecting their tag lists. + # We don't need precise scoping — license tags only appear on Describe/Context anyway. + $enclosingText = $headerText + + # Find function name(s) invoked inside the It. + # Special case: Test-MtEidscaControl -CheckId X -> Test-MtEidscaX + $functionNames = [System.Collections.Generic.List[string]]::new() + foreach ($fnMatch in [regex]::Matches($itText, '\b(Test-[A-Za-z0-9_]+)\b')) { + $fn = $fnMatch.Groups[1].Value + if ($fn -eq 'Test-MtEidscaControl') { + $checkIdMatch = [regex]::Match($itText, '-CheckId\s+([A-Z0-9]+)') + if ($checkIdMatch.Success) { + $functionNames.Add('Test-MtEidsca' + $checkIdMatch.Groups[1].Value) + } + } elseif ($fn -ne 'Test-Path' -and $fn -ne 'Test-MtConnection') { + $functionNames.Add($fn) + } + } + $functionName = if ($functionNames.Count -gt 0) { $functionNames[0] } else { $null } + + # Multiple It blocks with the same Id can exist (CAWhatIf foreach loop). Append. + if ($testIndex.ContainsKey($id)) { + $testIndex[$id].ItText += "`n$itText" + if (-not $testIndex[$id].EnclosingText.Contains($enclosingText)) { + $testIndex[$id].EnclosingText += "`n$enclosingText" + } + if (-not $testIndex[$id].FunctionName -and $functionName) { + $testIndex[$id].FunctionName = $functionName + } + } else { + $testIndex[$id] = @{ + ItText = $itText + EnclosingText = $enclosingText + FunctionName = $functionName + File = $file.FullName + } + } + } +} + +Write-Host "Parsed $($testIndex.Count) unique test IDs from .Tests.ps1 files." -ForegroundColor Cyan + +# --- Pass 2: for each TestSettings entry, derive license tokens --- + +$config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + +$licenseMap = [ordered]@{} +$tally = @{} +$tbdIds = [System.Collections.Generic.List[string]]::new() + +foreach ($setting in $config.TestSettings) { + $id = $setting.Id + $tokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + + # Resolve to testIndex with parent-prefix fallback (config has MT.1033.0 but + # the test only encodes MT.1033 because the .0 is interpolated at runtime). + $entry = $null + if ($testIndex.ContainsKey($id)) { + $entry = $testIndex[$id] + } else { + $parent = $id + while ($parent -match '\.\d+$' -and -not $entry) { + $parent = $parent -replace '\.\d+$', '' + if ($testIndex.ContainsKey($parent)) { $entry = $testIndex[$parent] } + } + } + + if ($entry) { + # Signals 1-3 from the It block. + foreach ($t in (Get-LicenseTokensFromText -Text $entry.ItText)) { [void] $tokens.Add($t) } + + # Signal 5 from the enclosing Describe/Context header. + foreach ($t in (Get-LicenseTokensFromText -Text $entry.EnclosingText)) { [void] $tokens.Add($t) } + + # Signal 4: scan the underlying PowerShell function file. + if ($entry.FunctionName) { + $functionFile = Get-FunctionFileForTest -TestFunctionName $entry.FunctionName + if ($functionFile) { + $functionContent = Get-Content -Path $functionFile -Raw + foreach ($t in (Get-LicenseTokensFromText -Text $functionContent)) { [void] $tokens.Add($t) } + } + } + } + + if ($tokens.Count -eq 0) { + # Signal 6: suite default + if ($id -like 'AZDO.*') { [void] $tokens.Add('AzureDevOps') } + elseif (-not $entry) { [void] $tokens.Add('TBD') } + # else: analyzed but no premium signal -> [] (baseline is adequate) + } + + $licenses = @($tokens | Sort-Object) + $licenseMap[$id] = $licenses + + if ($licenses.Count -eq 0) { + $tally['(baseline [])'] = ($tally['(baseline [])'] ?? 0) + 1 + } else { + foreach ($t in $licenses) { + $tally[$t] = ($tally[$t] ?? 0) + 1 + } + } + + if ($licenses -contains 'TBD') { $tbdIds.Add($id) } +} + +Write-Host "" +Write-Host "License token tally:" -ForegroundColor Cyan +$tally.GetEnumerator() | Sort-Object Name | ForEach-Object { + " {0,-22} {1,4}" -f $_.Key, $_.Value +} +Write-Host "" +Write-Host "$($tbdIds.Count) entries marked TBD:" -ForegroundColor Yellow +$tbdIds | ForEach-Object { " $_" } | Write-Host + +if ($DryRun) { + Write-Host "" + Write-Host "Spot checks:" -ForegroundColor Cyan + foreach ($probeId in @('MT.1029', 'MT.1034.0', 'MT.1033.0', 'CISA.MS.AAD.4.1', 'CISA.MS.AAD.7.7', 'CISA.MS.EXO.15.1', 'CIS.M365.1.3.6', 'AZDO.1000', 'EIDSCA.AP04', 'EIDSCA.AF01')) { + $tokens = $licenseMap[$probeId] + $rendered = if ($tokens.Count -eq 0) { '[]' } else { '[' + ($tokens -join ', ') + ']' } + " {0,-22} {1}" -f $probeId, $rendered + } + Write-Host "" + Write-Host "Dry run — config not modified." -ForegroundColor Green + return +} + +# --- Pass 3: write the Licenses field into maester-config.json (text-level edit) --- + +$json = Get-Content -Path $configPath -Raw +$lines = $json -split "`r?`n" +$out = [System.Collections.Generic.List[string]]::new() + +# State machine: when we see `"Id": "..."` we record the Id; when we close that +# object's body (the line that is exactly " }" or " },") we have already +# emitted "Title" earlier — so we insert the Licenses line just before the close. +$pendingId = $null +$pendingObjectLines = [System.Collections.Generic.List[string]]::new() +$pendingIndent = '' +$inTestSettingsObject = $false + +for ($i = 0; $i -lt $lines.Count; $i++) { + $line = $lines[$i] + if (-not $inTestSettingsObject) { + # Detect entering an item: a line that opens a top-level TestSettings object. + # Pattern: indented "{" inside the TestSettings array. + if ($line -match '^(\s*)\{\s*$') { + $indent = $matches[1] + # Peek ahead a few lines for "Id": + $peekId = $null + for ($j = $i + 1; $j -lt [Math]::Min($i + 10, $lines.Count); $j++) { + if ($lines[$j] -match '"Id":\s*"([^"]+)"') { $peekId = $matches[1]; break } + if ($lines[$j] -match '^\s*\}') { break } + } + if ($peekId -and $licenseMap.Contains($peekId)) { + $inTestSettingsObject = $true + $pendingId = $peekId + $pendingIndent = $indent + $pendingObjectLines.Clear() + $pendingObjectLines.Add($line) + continue + } + } + $out.Add($line) + continue + } + + # Inside a tracked TestSettings object. + $pendingObjectLines.Add($line) + + # Detect close of this object: a line that is exactly indent + "}" or "}," or starts with that. + if ($line -match "^$([regex]::Escape($pendingIndent))\}\,?\s*$") { + # Insert the Licenses line just before the closing brace. + $closingLine = $pendingObjectLines[$pendingObjectLines.Count - 1] + $bodyLines = $pendingObjectLines.GetRange(0, $pendingObjectLines.Count - 1) + + # The previous body line currently has no trailing comma. We need to add one, + # then insert "Licenses": [...]. + $lastBodyIdx = $bodyLines.Count - 1 + $lastBody = $bodyLines[$lastBodyIdx] + if ($lastBody -notmatch ',\s*$') { + $bodyLines[$lastBodyIdx] = $lastBody.TrimEnd() + ',' + } + + $licenses = $licenseMap[$pendingId] + $licenseJson = if ($licenses.Count -eq 0) { + '[]' + } else { + '[' + (($licenses | ForEach-Object { '"' + $_ + '"' }) -join ', ') + ']' + } + # Detect inner indentation by sampling an existing body line (first non-brace line). + $innerIndent = $pendingIndent + ' ' + for ($k = 1; $k -lt $bodyLines.Count; $k++) { + if ($bodyLines[$k] -match '^(\s+)"') { $innerIndent = $matches[1]; break } + } + + foreach ($bl in $bodyLines) { $out.Add($bl) } + $out.Add("$innerIndent`"Licenses`": $licenseJson") + $out.Add($closingLine) + + $inTestSettingsObject = $false + $pendingId = $null + $pendingObjectLines.Clear() + } +} + +# Detect original line ending; default to LF (the repo's convention here). +$lineEnding = if ($json -match "`r`n") { "`r`n" } else { "`n" } +$newJson = $out -join $lineEnding +if ($json.EndsWith("`n") -and -not $newJson.EndsWith("`n")) { $newJson += $lineEnding } + +# Sanity check: still valid JSON. +try { + $null = $newJson | ConvertFrom-Json +} catch { + throw "Generated JSON is invalid: $_" +} + +# Use [IO.File]::WriteAllText to avoid PowerShell's CRLF tendencies. +[System.IO.File]::WriteAllText($configPath, $newJson, [System.Text.UTF8Encoding]::new($false)) +Write-Host "" +Write-Host "Wrote $configPath." -ForegroundColor Green From df4e203777df76c903b34bfb9a9aa8ef6f92231f Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Wed, 6 May 2026 16:53:42 -0400 Subject: [PATCH 3/5] feat: Populate maester-config.json with a new Licenses setting to track the license requirements for each test Id. --- tests/maester-config.json | 1215 ++++++++++++++++++++++++------------- 1 file changed, 810 insertions(+), 405 deletions(-) diff --git a/tests/maester-config.json b/tests/maester-config.json index e055960b7..8683581c8 100644 --- a/tests/maester-config.json +++ b/tests/maester-config.json @@ -7,2027 +7,2432 @@ { "Id": "CIS.M365.1.1.1", "Severity": "High", - "Title": "(L1) Ensure Administrative accounts are cloud-only" + "Title": "(L1) Ensure Administrative accounts are cloud-only", + "Licenses": [] }, { "Id": "CIS.M365.1.1.3", "Severity": "High", - "Title": "(L1) Ensure that between two and four global admins are designated" + "Title": "(L1) Ensure that between two and four global admins are designated", + "Licenses": [] }, { "Id": "CIS.M365.1.2.1", "Severity": "Medium", - "Title": "(L2) Ensure that only organizationally managed/approved public groups exist" + "Title": "(L2) Ensure that only organizationally managed/approved public groups exist", + "Licenses": [] }, { "Id": "CIS.M365.1.2.2", "Severity": "High", - "Title": "(L1) Ensure sign-in to shared mailboxes is blocked" + "Title": "(L1) Ensure sign-in to shared mailboxes is blocked", + "Licenses": [] }, { "Id": "CIS.M365.1.3.1", "Severity": "High", - "Title": "(L1) Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'" + "Title": "(L1) Ensure the 'Password expiration policy' is set to 'Set passwords to never expire (recommended)'", + "Licenses": [] }, { "Id": "CIS.M365.1.3.3", "Severity": "Medium", - "Title": "(L2) Ensure 'External sharing' of calendars is not available" + "Title": "(L2) Ensure 'External sharing' of calendars is not available", + "Licenses": [] }, { "Id": "CIS.M365.1.3.6", "Severity": "High", - "Title": "(L2) Ensure the customer lockbox feature is enabled" + "Title": "(L2) Ensure the customer lockbox feature is enabled", + "Licenses": ["CustomerLockbox"] }, { "Id": "CIS.M365.2.1.1", "Severity": "Medium", - "Title": "(L2) Ensure Safe Links for Office Applications is Enabled (Only Checks Default Policy)" + "Title": "(L2) Ensure Safe Links for Office Applications is Enabled (Only Checks Default Policy)", + "Licenses": ["MdoP1"] }, { "Id": "CIS.M365.2.1.2", "Severity": "Medium", - "Title": "(L1) Ensure the Common Attachment Types Filter is enabled (Only Checks Default Policy)" + "Title": "(L1) Ensure the Common Attachment Types Filter is enabled (Only Checks Default Policy)", + "Licenses": [] }, { "Id": "CIS.M365.2.1.3", "Severity": "Medium", - "Title": "(L1) Ensure notifications for internal users sending malware is Enabled (Only Checks Default Policy)" + "Title": "(L1) Ensure notifications for internal users sending malware is Enabled (Only Checks Default Policy)", + "Licenses": [] }, { "Id": "CIS.M365.2.1.4", "Severity": "High", - "Title": "(L2) Ensure Safe Attachments policy is enabled (Only Checks Default Policy)" + "Title": "(L2) Ensure Safe Attachments policy is enabled (Only Checks Default Policy)", + "Licenses": ["MdoP1"] }, { "Id": "CIS.M365.2.1.5", "Severity": "High", - "Title": "(L2) Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled" + "Title": "(L2) Ensure Safe Attachments for SharePoint, OneDrive, and Microsoft Teams is Enabled", + "Licenses": ["MdoP1"] }, { "Id": "CIS.M365.2.1.6", "Severity": "Medium", - "Title": "(L1) Ensure Exchange Online Spam Policies are set to notify administrators (Only Checks Default Policy)" + "Title": "(L1) Ensure Exchange Online Spam Policies are set to notify administrators (Only Checks Default Policy)", + "Licenses": [] }, { "Id": "CIS.M365.2.1.7", "Severity": "Medium", - "Title": "(L1) Ensure that an anti-phishing policy has been created (Only Checks Default Policy)" + "Title": "(L1) Ensure that an anti-phishing policy has been created (Only Checks Default Policy)", + "Licenses": ["MdoP1"] }, { "Id": "CIS.M365.2.1.9", "Severity": "High", - "Title": "(L1) Ensure that DKIM is enabled for all Exchange Online Domains" + "Title": "(L1) Ensure that DKIM is enabled for all Exchange Online Domains", + "Licenses": [] }, { "Id": "CIS.M365.2.1.11", "Severity": "High", - "Title": "(L2) Ensure comprehensive attachment filtering is applied" + "Title": "(L2) Ensure comprehensive attachment filtering is applied", + "Licenses": [] }, { "Id": "CIS.M365.2.1.12", "Severity": "Medium", - "Title": "(L1) Ensure the connection filter IP allow list is not used (Only Checks Default Policy)" + "Title": "(L1) Ensure the connection filter IP allow list is not used (Only Checks Default Policy)", + "Licenses": [] }, { "Id": "CIS.M365.2.1.13", "Severity": "Medium", - "Title": "(L1) Ensure the connection filter safe list is off (Only Checks Default Policy)" + "Title": "(L1) Ensure the connection filter safe list is off (Only Checks Default Policy)", + "Licenses": [] }, { "Id": "CIS.M365.2.4.4", "Severity": "Medium", - "Title": "(L1) Ensure Zero-hour auto purge for Microsoft Teams is on (Only Checks ZAP is enabled)" + "Title": "(L1) Ensure Zero-hour auto purge for Microsoft Teams is on (Only Checks ZAP is enabled)", + "Licenses": [] }, { "Id": "CIS.M365.3.1.1", "Severity": "High", - "Title": "(L1) Ensure Microsoft 365 audit log search is Enabled" + "Title": "(L1) Ensure Microsoft 365 audit log search is Enabled", + "Licenses": [] }, { "Id": "CIS.M365.8.1.1", "Severity": "Medium", - "Title": "(L2) Ensure external file sharing in Teams is enabled for only approved cloud storage services" + "Title": "(L2) Ensure external file sharing in Teams is enabled for only approved cloud storage services", + "Licenses": [] }, { "Id": "CIS.M365.8.2.2", "Severity": "Medium", - "Title": "(L1) Ensure communication with unmanaged Teams users is disabled" + "Title": "(L1) Ensure communication with unmanaged Teams users is disabled", + "Licenses": [] }, { "Id": "CIS.M365.8.2.4", "Severity": "Medium", - "Title": "(L1) Ensure communication with Skype users is disabled" + "Title": "(L1) Ensure communication with Skype users is disabled", + "Licenses": ["TBD"] }, { "Id": "CIS.M365.8.4.1", "Severity": "High", - "Title": "(L1) Ensure all or a majority of third-party and custom apps are blocked" + "Title": "(L1) Ensure all or a majority of third-party and custom apps are blocked", + "Licenses": [] }, { "Id": "CIS.M365.8.5.3", "Severity": "Medium", - "Title": "(L1) Ensure only people in my org can bypass the lobby" + "Title": "(L1) Ensure only people in my org can bypass the lobby", + "Licenses": [] }, { "Id": "CIS.M365.8.6.1", "Severity": "Medium", - "Title": "(L1) Ensure users can report security concerns in Teams to internal destination" + "Title": "(L1) Ensure users can report security concerns in Teams to internal destination", + "Licenses": [] }, { "Id": "CISA.MS.AAD.1.1", "Severity": "High", - "Title": "Legacy authentication SHALL be blocked." + "Title": "Legacy authentication SHALL be blocked.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.2.1", "Severity": "High", - "Title": "Users detected as high risk SHALL be blocked." + "Title": "Users detected as high risk SHALL be blocked.", + "Licenses": ["EntraIDP2"] }, { "Id": "CISA.MS.AAD.2.2", "Severity": "High", - "Title": "A notification SHOULD be sent to the administrator when high-risk users are detected." + "Title": "A notification SHOULD be sent to the administrator when high-risk users are detected.", + "Licenses": ["EntraIDP2"] }, { "Id": "CISA.MS.AAD.2.3", "Severity": "High", - "Title": "Sign-ins detected as high risk SHALL be blocked." + "Title": "Sign-ins detected as high risk SHALL be blocked.", + "Licenses": ["EntraIDP2"] }, { "Id": "CISA.MS.AAD.3.1", "Severity": "High", - "Title": "Phishing-resistant MFA SHALL be enforced for all users." + "Title": "Phishing-resistant MFA SHALL be enforced for all users.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.2", "Severity": "High", - "Title": "If phishing-resistant MFA has not been enforced, an alternative MFA method SHALL be enforced for all users." + "Title": "If phishing-resistant MFA has not been enforced, an alternative MFA method SHALL be enforced for all users.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.3", "Severity": "Medium", - "Title": "If Microsoft Authenticator is enabled, it SHALL be configured to show login context information." + "Title": "If Microsoft Authenticator is enabled, it SHALL be configured to show login context information.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.4", "Severity": "High", - "Title": "The Authentication Methods Manage Migration feature SHALL be set to Migration Complete." + "Title": "The Authentication Methods Manage Migration feature SHALL be set to Migration Complete.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.5", "Severity": "High", - "Title": "The authentication methods SMS, Voice Call, and Email One-Time Passcode (OTP) SHALL be disabled." + "Title": "The authentication methods SMS, Voice Call, and Email One-Time Passcode (OTP) SHALL be disabled.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.6", "Severity": "High", - "Title": "Phishing-resistant MFA SHALL be required for highly privileged roles." + "Title": "Phishing-resistant MFA SHALL be required for highly privileged roles.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.7", "Severity": "High", - "Title": "Managed devices SHOULD be required for authentication." + "Title": "Managed devices SHOULD be required for authentication.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.3.8", "Severity": "High", - "Title": "Managed Devices SHOULD be required to register MFA." + "Title": "Managed Devices SHOULD be required to register MFA.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.4.1", "Severity": "High", - "Title": "Security logs SHALL be sent to the agency's security operations center for monitoring." + "Title": "Security logs SHALL be sent to the agency's security operations center for monitoring.", + "Licenses": ["EntraIDP1"] }, { "Id": "CISA.MS.AAD.5.1", "Severity": "High", - "Title": "Only administrators SHALL be allowed to register applications." + "Title": "Only administrators SHALL be allowed to register applications.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.5.2", "Severity": "High", - "Title": "Only administrators SHALL be allowed to consent to applications." + "Title": "Only administrators SHALL be allowed to consent to applications.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.5.3", "Severity": "High", - "Title": "An admin consent workflow SHALL be configured for applications." + "Title": "An admin consent workflow SHALL be configured for applications.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.5.4", "Severity": "High", - "Title": "Group owners SHALL NOT be allowed to consent to applications." + "Title": "Group owners SHALL NOT be allowed to consent to applications.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.6.1", "Severity": "High", - "Title": "User passwords SHALL NOT expire." + "Title": "User passwords SHALL NOT expire.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.7.1", "Severity": "High", - "Title": "A minimum of two users and a maximum of eight users SHALL be provisioned with the Global Administrator role." + "Title": "A minimum of two users and a maximum of eight users SHALL be provisioned with the Global Administrator role.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.7.2", "Severity": "High", - "Title": "Privileged users SHALL be provisioned with finer-grained roles instead of Global Administrator." + "Title": "Privileged users SHALL be provisioned with finer-grained roles instead of Global Administrator.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.7.3", "Severity": "High", - "Title": "Privileged users SHALL be provisioned cloud-only accounts separate from an on-premises directory or other federated identity providers." + "Title": "Privileged users SHALL be provisioned cloud-only accounts separate from an on-premises directory or other federated identity providers.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.7.4", "Severity": "High", - "Title": "Permanent active role assignments SHALL NOT be allowed for highly privileged roles." + "Title": "Permanent active role assignments SHALL NOT be allowed for highly privileged roles.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.7.5", "Severity": "High", - "Title": "Provisioning users to highly privileged roles SHALL NOT occur outside of a PAM system." + "Title": "Provisioning users to highly privileged roles SHALL NOT occur outside of a PAM system.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.7.6", "Severity": "High", - "Title": "Activation of the Global Administrator role SHALL require approval." + "Title": "Activation of the Global Administrator role SHALL require approval.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.7.7", "Severity": "High", - "Title": "Eligible and Active highly privileged role assignments SHALL trigger an alert." + "Title": "Eligible and Active highly privileged role assignments SHALL trigger an alert.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.7.8", "Severity": "High", - "Title": "User activation of the Global Administrator role SHALL trigger an alert." + "Title": "User activation of the Global Administrator role SHALL trigger an alert.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.7.9", "Severity": "High", - "Title": "User activation of other highly privileged roles SHOULD trigger an alert." + "Title": "User activation of other highly privileged roles SHOULD trigger an alert.", + "Licenses": ["EntraIDGovernance", "EntraIDP2"] }, { "Id": "CISA.MS.AAD.8.1", "Severity": "Medium", - "Title": "Guest users SHOULD have limited or restricted access to Azure AD directory objects." + "Title": "Guest users SHOULD have limited or restricted access to Azure AD directory objects.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.8.2", "Severity": "High", - "Title": "Only users with the Guest Inviter role SHOULD be able to invite guest users." + "Title": "Only users with the Guest Inviter role SHOULD be able to invite guest users.", + "Licenses": [] }, { "Id": "CISA.MS.AAD.8.3", "Severity": "Medium", - "Title": "Guest invites SHOULD only be allowed to specific external domains that have been authorized by the agency for legitimate business purposes." + "Title": "Guest invites SHOULD only be allowed to specific external domains that have been authorized by the agency for legitimate business purposes.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.1.1", "Severity": "High", - "Title": "Automatic forwarding to external domains SHALL be disabled." + "Title": "Automatic forwarding to external domains SHALL be disabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.2.1", "Severity": "Medium", - "Title": "A list of approved IP addresses for sending mail SHALL be maintained." + "Title": "A list of approved IP addresses for sending mail SHALL be maintained.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.2.2", "Severity": "Medium", - "Title": "An SPF policy SHALL be published for each domain, designating only these addresses as approved senders." + "Title": "An SPF policy SHALL be published for each domain, designating only these addresses as approved senders.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.3.1", "Severity": "Medium", - "Title": "DKIM SHOULD be enabled for all domains." + "Title": "DKIM SHOULD be enabled for all domains.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.4.1", "Severity": "Medium", - "Title": "A DMARC policy SHALL be published for every second-level domain." + "Title": "A DMARC policy SHALL be published for every second-level domain.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.4.2", "Severity": "High", - "Title": "The DMARC message rejection option SHALL be p=reject." + "Title": "The DMARC message rejection option SHALL be p=reject.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.4.3", "Severity": "Medium", - "Title": "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov." + "Title": "The DMARC point of contact for aggregate reports SHALL include reports@dmarc.cyber.dhs.gov.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.5.1", "Severity": "High", - "Title": "SMTP AUTH SHALL be disabled." + "Title": "SMTP AUTH SHALL be disabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.6.1", "Severity": "Medium", - "Title": "Contact folders SHALL NOT be shared with all domains." + "Title": "Contact folders SHALL NOT be shared with all domains.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.6.2", "Severity": "Medium", - "Title": "Calendar details SHALL NOT be shared with all domains." + "Title": "Calendar details SHALL NOT be shared with all domains.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.7.1", "Severity": "Medium", - "Title": "External sender warnings SHALL be implemented." + "Title": "External sender warnings SHALL be implemented.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.8.1", "Severity": "High", - "Title": "A DLP solution SHALL be used." + "Title": "A DLP solution SHALL be used.", + "Licenses": ["ExoDlp"] }, { "Id": "CISA.MS.EXO.8.2", "Severity": "Medium", - "Title": "The DLP solution SHALL protect personally identifiable information (PII) and sensitive information, as defined by the agency." + "Title": "The DLP solution SHALL protect personally identifiable information (PII) and sensitive information, as defined by the agency.", + "Licenses": ["ExoDlp"] }, { "Id": "CISA.MS.EXO.8.3", "Severity": "Medium", - "Title": "The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft." + "Title": "The selected DLP solution SHOULD offer services comparable to the native DLP solution offered by Microsoft.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.8.4", "Severity": "High", - "Title": "At a minimum, the DLP solution SHALL restrict sharing credit card numbers, U.S. Individual Taxpayer Identification Numbers (ITIN), and U.S. Social Security numbers (SSN) via email." + "Title": "At a minimum, the DLP solution SHALL restrict sharing credit card numbers, U.S. Individual Taxpayer Identification Numbers (ITIN), and U.S. Social Security numbers (SSN) via email.", + "Licenses": ["ExoDlp"] }, { "Id": "CISA.MS.EXO.9.1", "Severity": "Medium", - "Title": "Emails SHALL be filtered by attachment file types." + "Title": "Emails SHALL be filtered by attachment file types.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.9.2", "Severity": "Medium", - "Title": "The attachment filter SHOULD attempt to determine the true file type and assess the file extension." + "Title": "The attachment filter SHOULD attempt to determine the true file type and assess the file extension.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.9.3", "Severity": "High", - "Title": "Disallowed file types SHALL be determined and enforced." + "Title": "Disallowed file types SHALL be determined and enforced.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.9.4", "Severity": "Medium", - "Title": "Alternatively chosen filtering solutions SHOULD offer services comparable to Microsoft Defender's Common Attachment Filter." + "Title": "Alternatively chosen filtering solutions SHOULD offer services comparable to Microsoft Defender's Common Attachment Filter.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.9.5", "Severity": "High", - "Title": "At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe)." + "Title": "At a minimum, click-to-run files SHOULD be blocked (e.g., .exe, .cmd, and .vbe).", + "Licenses": [] }, { "Id": "CISA.MS.EXO.10.1", "Severity": "High", - "Title": "Emails SHALL be scanned for malware." + "Title": "Emails SHALL be scanned for malware.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.10.2", "Severity": "High", - "Title": "Emails identified as containing malware SHALL be quarantined or dropped." + "Title": "Emails identified as containing malware SHALL be quarantined or dropped.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.10.3", "Severity": "High", - "Title": "Email scanning SHALL be capable of reviewing emails after delivery." + "Title": "Email scanning SHALL be capable of reviewing emails after delivery.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.11.1", "Severity": "High", - "Title": "Impersonation protection checks SHOULD be used." + "Title": "Impersonation protection checks SHOULD be used.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.11.2", "Severity": "Medium", - "Title": "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed." + "Title": "User warnings, comparable to the user safety tips included with EOP, SHOULD be displayed.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.11.3", "Severity": "Medium", - "Title": "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence." + "Title": "The phishing protection solution SHOULD include an AI-based phishing detection tool comparable to EOP Mailbox Intelligence.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.12.1", "Severity": "Medium", - "Title": "IP allow lists SHOULD NOT be created." + "Title": "IP allow lists SHOULD NOT be created.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.12.2", "Severity": "Medium", - "Title": "Safe lists SHOULD NOT be enabled." + "Title": "Safe lists SHOULD NOT be enabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.13.1", "Severity": "High", - "Title": "Mailbox auditing SHALL be enabled." + "Title": "Mailbox auditing SHALL be enabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.14.1", "Severity": "High", - "Title": "A spam filter SHALL be enabled." + "Title": "A spam filter SHALL be enabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.14.2", "Severity": "Medium", - "Title": "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder." + "Title": "Spam and high confidence spam SHALL be moved to either the junk email folder or the quarantine folder.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.14.3", "Severity": "Medium", - "Title": "Allowed domains SHALL NOT be added to inbound anti-spam protection policies." + "Title": "Allowed domains SHALL NOT be added to inbound anti-spam protection policies.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.14.4", "Severity": "Medium", - "Title": "If a third-party party filtering solution is used, the solution SHOULD offer services comparable to the native spam filtering offered by Microsoft." + "Title": "If a third-party party filtering solution is used, the solution SHOULD offer services comparable to the native spam filtering offered by Microsoft.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.15.1", "Severity": "Medium", - "Title": "URL comparison with a block-list SHOULD be enabled." + "Title": "URL comparison with a block-list SHOULD be enabled.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.15.2", "Severity": "High", - "Title": "Direct download links SHOULD be scanned for malware." + "Title": "Direct download links SHOULD be scanned for malware.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.15.3", "Severity": "Medium", - "Title": "User click tracking SHOULD be enabled." + "Title": "User click tracking SHOULD be enabled.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.16.1", "Severity": "High", - "Title": "Alerts SHALL be enabled." + "Title": "Alerts SHALL be enabled.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.16.2", "Severity": "Medium", - "Title": "Alerts SHOULD be sent to a monitored address or incorporated into a security information and event management (SIEM) system." + "Title": "Alerts SHOULD be sent to a monitored address or incorporated into a security information and event management (SIEM) system.", + "Licenses": ["MdoP1"] }, { "Id": "CISA.MS.EXO.17.1", "Severity": "High", - "Title": "Microsoft Purview Audit (Standard) logging SHALL be enabled." + "Title": "Microsoft Purview Audit (Standard) logging SHALL be enabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.17.2", "Severity": "Medium", - "Title": "Microsoft Purview Audit (Premium) logging SHALL be enabled." + "Title": "Microsoft Purview Audit (Premium) logging SHALL be enabled.", + "Licenses": [] }, { "Id": "CISA.MS.EXO.17.3", "Severity": "Medium", - "Title": "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C)." + "Title": "Audit logs SHALL be maintained for at least the minimum duration dictated by OMB M-21-31 (Appendix C).", + "Licenses": ["AdvAudit"] }, { "Id": "CISA.MS.SHAREPOINT.1.1", "Severity": "Medium", - "Title": "External sharing for SharePoint SHALL be limited to Existing guests or Only People in your organization." + "Title": "External sharing for SharePoint SHALL be limited to Existing guests or Only People in your organization.", + "Licenses": [] }, { "Id": "CISA.MS.SHAREPOINT.1.3", "Severity": "High", - "Title": "External sharing SHALL be restricted to approved external domains and/or users in approved security groups per interagency collaboration needs." + "Title": "External sharing SHALL be restricted to approved external domains and/or users in approved security groups per interagency collaboration needs.", + "Licenses": [] }, { "Id": "EIDSCA.AF01", "Severity": "High", - "Title": "Authentication Method - FIDO2 security key - State." + "Title": "Authentication Method - FIDO2 security key - State.", + "Licenses": [] }, { "Id": "EIDSCA.AF02", "Severity": "Medium", - "Title": "Authentication Method - FIDO2 security key - Allow self-service set up." + "Title": "Authentication Method - FIDO2 security key - Allow self-service set up.", + "Licenses": [] }, { "Id": "EIDSCA.AF03", "Severity": "High", - "Title": "Authentication Method - FIDO2 security key - Enforce attestation." + "Title": "Authentication Method - FIDO2 security key - Enforce attestation.", + "Licenses": [] }, { "Id": "EIDSCA.AF04", "Severity": "High", - "Title": "Authentication Method - FIDO2 security key - Enforce key restrictions." + "Title": "Authentication Method - FIDO2 security key - Enforce key restrictions.", + "Licenses": [] }, { "Id": "EIDSCA.AF05", "Severity": "High", - "Title": "Authentication Method - FIDO2 security key - Restricted." + "Title": "Authentication Method - FIDO2 security key - Restricted.", + "Licenses": [] }, { "Id": "EIDSCA.AF06", "Severity": "Medium", - "Title": "Authentication Method - FIDO2 security key - Restrict specific keys." + "Title": "Authentication Method - FIDO2 security key - Restrict specific keys.", + "Licenses": [] }, { "Id": "EIDSCA.AG01", "Severity": "High", - "Title": "Authentication Method - General Settings - Manage migration." + "Title": "Authentication Method - General Settings - Manage migration.", + "Licenses": [] }, { "Id": "EIDSCA.AG02", "Severity": "Medium", - "Title": "Authentication Method - General Settings - Report suspicious activity - State." + "Title": "Authentication Method - General Settings - Report suspicious activity - State.", + "Licenses": [] }, { "Id": "EIDSCA.AG03", "Severity": "Medium", - "Title": "Authentication Method - General Settings - Report suspicious activity - Included users/groups." + "Title": "Authentication Method - General Settings - Report suspicious activity - Included users/groups.", + "Licenses": [] }, { "Id": "EIDSCA.AM01", "Severity": "High", - "Title": "Authentication Method - Microsoft Authenticator - State." + "Title": "Authentication Method - Microsoft Authenticator - State.", + "Licenses": [] }, { "Id": "EIDSCA.AM02", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Allow use of Microsoft Authenticator OTP." + "Title": "Authentication Method - Microsoft Authenticator - Allow use of Microsoft Authenticator OTP.", + "Licenses": [] }, { "Id": "EIDSCA.AM03", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Require number matching for push notifications." + "Title": "Authentication Method - Microsoft Authenticator - Require number matching for push notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AM04", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Included users/groups of number matching for push notifications." + "Title": "Authentication Method - Microsoft Authenticator - Included users/groups of number matching for push notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AM06", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Show application name in push and passwordless notifications." + "Title": "Authentication Method - Microsoft Authenticator - Show application name in push and passwordless notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AM07", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Included users/groups to show application name in push and passwordless notifications." + "Title": "Authentication Method - Microsoft Authenticator - Included users/groups to show application name in push and passwordless notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AM09", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Show geographic location in push and passwordless notifications." + "Title": "Authentication Method - Microsoft Authenticator - Show geographic location in push and passwordless notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AM10", "Severity": "Medium", - "Title": "Authentication Method - Microsoft Authenticator - Included users/groups to show geographic location in push and passwordless notifications." + "Title": "Authentication Method - Microsoft Authenticator - Included users/groups to show geographic location in push and passwordless notifications.", + "Licenses": [] }, { "Id": "EIDSCA.AP01", "Severity": "High", - "Title": "Default Authorization Settings - Enabled Self service password reset for administrators." + "Title": "Default Authorization Settings - Enabled Self service password reset for administrators.", + "Licenses": [] }, { "Id": "EIDSCA.AP04", "Severity": "Medium", - "Title": "Default Authorization Settings - Guest invite restrictions." + "Title": "Default Authorization Settings - Guest invite restrictions.", + "Licenses": [] }, { "Id": "EIDSCA.AP05", "Severity": "Medium", - "Title": "Default Authorization Settings - Sign-up for email based subscription." + "Title": "Default Authorization Settings - Sign-up for email based subscription.", + "Licenses": [] }, { "Id": "EIDSCA.AP06", "Severity": "Medium", - "Title": "Default Authorization Settings - User can join the tenant by email validation." + "Title": "Default Authorization Settings - User can join the tenant by email validation.", + "Licenses": [] }, { "Id": "EIDSCA.AP07", "Severity": "High", - "Title": "Default Authorization Settings - Guest user access." + "Title": "Default Authorization Settings - Guest user access.", + "Licenses": [] }, { "Id": "EIDSCA.AP08", "Severity": "Medium", - "Title": "Default Authorization Settings - User consent policy assigned for applications." + "Title": "Default Authorization Settings - User consent policy assigned for applications.", + "Licenses": [] }, { "Id": "EIDSCA.AP09", "Severity": "Medium", - "Title": "Default Authorization Settings - Allow user consent on risk-based apps." + "Title": "Default Authorization Settings - Allow user consent on risk-based apps.", + "Licenses": [] }, { "Id": "EIDSCA.AP10", "Severity": "High", - "Title": "Default Authorization Settings - Default User Role Permissions - Allowed to create Apps." + "Title": "Default Authorization Settings - Default User Role Permissions - Allowed to create Apps.", + "Licenses": [] }, { "Id": "EIDSCA.AP14", "Severity": "High", - "Title": "Default Authorization Settings - Default User Role Permissions - Allowed to read other users." + "Title": "Default Authorization Settings - Default User Role Permissions - Allowed to read other users.", + "Licenses": [] }, { "Id": "EIDSCA.AS04", "Severity": "High", - "Title": "Authentication Method - SMS - Use for sign-in." + "Title": "Authentication Method - SMS - Use for sign-in.", + "Licenses": [] }, { "Id": "EIDSCA.AT01", "Severity": "High", - "Title": "Authentication Method - Temporary Access Pass - State." + "Title": "Authentication Method - Temporary Access Pass - State.", + "Licenses": [] }, { "Id": "EIDSCA.AT02", "Severity": "High", - "Title": "Authentication Method - Temporary Access Pass - One-time." + "Title": "Authentication Method - Temporary Access Pass - One-time.", + "Licenses": [] }, { "Id": "EIDSCA.AV01", "Severity": "High", - "Title": "Authentication Method - Voice call - State." + "Title": "Authentication Method - Voice call - State.", + "Licenses": [] }, { "Id": "EIDSCA.CP01", "Severity": "High", - "Title": "Default Settings - Consent Policy Settings - Group owner consent for apps accessing data." + "Title": "Default Settings - Consent Policy Settings - Group owner consent for apps accessing data.", + "Licenses": [] }, { "Id": "EIDSCA.CP03", "Severity": "High", - "Title": "Default Settings - Consent Policy Settings - Block user consent for risky apps." + "Title": "Default Settings - Consent Policy Settings - Block user consent for risky apps.", + "Licenses": [] }, { "Id": "EIDSCA.CP04", "Severity": "Medium", - "Title": "Default Settings - Consent Policy Settings - Users can request admin consent to apps they are unable to consent to." + "Title": "Default Settings - Consent Policy Settings - Users can request admin consent to apps they are unable to consent to.", + "Licenses": [] }, { "Id": "EIDSCA.CR01", "Severity": "High", - "Title": "Consent Framework - Admin Consent Request - Policy to enable or disable admin consent request feature." + "Title": "Consent Framework - Admin Consent Request - Policy to enable or disable admin consent request feature.", + "Licenses": [] }, { "Id": "EIDSCA.CR02", "Severity": "Medium", - "Title": "Consent Framework - Admin Consent Request - Reviewers will receive email notifications for requests." + "Title": "Consent Framework - Admin Consent Request - Reviewers will receive email notifications for requests.", + "Licenses": [] }, { "Id": "EIDSCA.CR03", "Severity": "Medium", - "Title": "Consent Framework - Admin Consent Request - Reviewers will receive email notifications when admin consent requests are about to expire." + "Title": "Consent Framework - Admin Consent Request - Reviewers will receive email notifications when admin consent requests are about to expire.", + "Licenses": [] }, { "Id": "EIDSCA.CR04", "Severity": "High", - "Title": "Consent Framework - Admin Consent Request - Consent request duration (days)." + "Title": "Consent Framework - Admin Consent Request - Consent request duration (days).", + "Licenses": [] }, { "Id": "EIDSCA.PR01", "Severity": "High", - "Title": "Default Settings - Password Rule Settings - Password Protection - Mode." + "Title": "Default Settings - Password Rule Settings - Password Protection - Mode.", + "Licenses": [] }, { "Id": "EIDSCA.PR02", "Severity": "High", - "Title": "Default Settings - Password Rule Settings - Password Protection - Enable password protection on Windows Server Active Directory." + "Title": "Default Settings - Password Rule Settings - Password Protection - Enable password protection on Windows Server Active Directory.", + "Licenses": [] }, { "Id": "EIDSCA.PR03", "Severity": "Medium", - "Title": "Default Settings - Password Rule Settings - Enforce custom list." + "Title": "Default Settings - Password Rule Settings - Enforce custom list.", + "Licenses": [] }, { "Id": "EIDSCA.PR05", "Severity": "Medium", - "Title": "Default Settings - Password Rule Settings - Smart Lockout - Lockout duration in seconds." + "Title": "Default Settings - Password Rule Settings - Smart Lockout - Lockout duration in seconds.", + "Licenses": [] }, { "Id": "EIDSCA.PR06", "Severity": "Medium", - "Title": "Default Settings - Password Rule Settings - Smart Lockout - Lockout threshold." + "Title": "Default Settings - Password Rule Settings - Smart Lockout - Lockout threshold.", + "Licenses": [] }, { "Id": "EIDSCA.ST08", "Severity": "Medium", - "Title": "Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to become Group Owner." + "Title": "Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to become Group Owner.", + "Licenses": [] }, { "Id": "EIDSCA.ST09", "Severity": "Medium", - "Title": "Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to have access to groups content." + "Title": "Default Settings - Classification and M365 Groups - M365 groups - Allow Guests to have access to groups content.", + "Licenses": [] }, { "Id": "MT.1001", "Severity": "Medium", - "Title": "At least one Conditional Access policy is configured with device compliance." + "Title": "At least one Conditional Access policy is configured with device compliance.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1002", "Severity": "High", - "Title": "App management restrictions on applications and service principals is configured and enabled." + "Title": "App management restrictions on applications and service principals is configured and enabled.", + "Licenses": [] }, { "Id": "MT.1003", "Severity": "High", - "Title": "At least one Conditional Access policy is configured with All Apps." + "Title": "At least one Conditional Access policy is configured with All Apps.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1004", "Severity": "High", - "Title": "At least one Conditional Access policy is configured with All Apps and All Users." + "Title": "At least one Conditional Access policy is configured with All Apps and All Users.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1005", "Severity": "High", - "Title": "All Conditional Access policies are configured to exclude at least one emergency/break glass account or group." + "Title": "All Conditional Access policies are configured to exclude at least one emergency/break glass account or group.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1006", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require MFA for admins." + "Title": "At least one Conditional Access policy is configured to require MFA for admins.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1007", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require MFA for all users." + "Title": "At least one Conditional Access policy is configured to require MFA for all users.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1008", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require MFA for Azure management." + "Title": "At least one Conditional Access policy is configured to require MFA for Azure management.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1009", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to block other legacy authentication." + "Title": "At least one Conditional Access policy is configured to block other legacy authentication.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1010", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to block legacy authentication for Exchange ActiveSync." + "Title": "At least one Conditional Access policy is configured to block legacy authentication for Exchange ActiveSync.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1011", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to secure security info registration only from a trusted location." + "Title": "At least one Conditional Access policy is configured to secure security info registration only from a trusted location.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1012", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require MFA for risky sign-ins." + "Title": "At least one Conditional Access policy is configured to require MFA for risky sign-ins.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1013", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require new password when user risk is high." + "Title": "At least one Conditional Access policy is configured to require new password when user risk is high.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1014", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require compliant or Entra hybrid joined devices for admins." + "Title": "At least one Conditional Access policy is configured to require compliant or Entra hybrid joined devices for admins.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1015", "Severity": "Medium", - "Title": "At least one Conditional Access policy is configured to block access for unknown or unsupported device platforms." + "Title": "At least one Conditional Access policy is configured to block access for unknown or unsupported device platforms.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1016", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to require MFA for guest access." + "Title": "At least one Conditional Access policy is configured to require MFA for guest access.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1017", "Severity": "High", - "Title": "At least one Conditional Access policy is configured to enforce non persistent browser session for non-corporate devices." + "Title": "At least one Conditional Access policy is configured to enforce non persistent browser session for non-corporate devices.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1018", "Severity": "Medium", - "Title": "At least one Conditional Access policy is configured to enforce sign-in frequency for non-corporate devices." + "Title": "At least one Conditional Access policy is configured to enforce sign-in frequency for non-corporate devices.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1019", "Severity": "Medium", - "Title": "At least one Conditional Access policy is configured to enable application enforced restrictions." + "Title": "At least one Conditional Access policy is configured to enable application enforced restrictions.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1020", "Severity": "High", - "Title": "All Conditional Access policies are configured to exclude directory synchronization accounts or do not scope them." + "Title": "All Conditional Access policies are configured to exclude directory synchronization accounts or do not scope them.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1021", "Severity": "High", - "Title": "Security Defaults are enabled." + "Title": "Security Defaults are enabled.", + "Licenses": [] }, { "Id": "MT.1022", "Severity": "Medium", - "Title": "All users utilizing a P1 license should be licensed." + "Title": "All users utilizing a P1 license should be licensed.", + "Licenses": ["EntraIDP1", "EntraIDP2"] }, { "Id": "MT.1023", "Severity": "Medium", - "Title": "All users utilizing a P2 license should be licensed." + "Title": "All users utilizing a P2 license should be licensed.", + "Licenses": ["EntraIDP1", "EntraIDP2"] }, { "Id": "MT.1025", "Severity": "High", - "Title": "No external user with permanent role assignment on Control Plane." + "Title": "No external user with permanent role assignment on Control Plane.", + "Licenses": [] }, { "Id": "MT.1026", "Severity": "High", - "Title": "No hybrid user with permanent role assignment on Control Plane." + "Title": "No hybrid user with permanent role assignment on Control Plane.", + "Licenses": [] }, { "Id": "MT.1027", "Severity": "High", - "Title": "No Service Principal with Client Secret and permanent role assignment on Control Plane." + "Title": "No Service Principal with Client Secret and permanent role assignment on Control Plane.", + "Licenses": [] }, { "Id": "MT.1028", "Severity": "High", - "Title": "No user with mailbox and permanent role assignment on Control Plane." + "Title": "No user with mailbox and permanent role assignment on Control Plane.", + "Licenses": [] }, { "Id": "MT.1029", "Severity": "High", - "Title": "Stale accounts are not assigned to privileged roles." + "Title": "Stale accounts are not assigned to privileged roles.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1030", "Severity": "High", - "Title": "Eligible role assignments on Control Plane are in use by administrators." + "Title": "Eligible role assignments on Control Plane are in use by administrators.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1031", "Severity": "High", - "Title": "Privileged role on Control Plane are managed by PIM only." + "Title": "Privileged role on Control Plane are managed by PIM only.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1032", "Severity": "High", - "Title": "Limited number of Global Admins are assigned." + "Title": "Limited number of Global Admins are assigned.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1033.0", "Severity": "High", - "Title": "User should be blocked from using legacy authentication ()" + "Title": "User should be blocked from using legacy authentication ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1033.1", "Severity": "High", - "Title": "User should be blocked from using legacy authentication ()" + "Title": "User should be blocked from using legacy authentication ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1033.2", "Severity": "High", - "Title": "User should be blocked from using legacy authentication ()" + "Title": "User should be blocked from using legacy authentication ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1033.3", "Severity": "High", - "Title": "User should be blocked from using legacy authentication ()" + "Title": "User should be blocked from using legacy authentication ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1033.4", "Severity": "High", - "Title": "User should be blocked from using legacy authentication ()" + "Title": "User should be blocked from using legacy authentication ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1034.0", "Severity": "High", - "Title": "Emergency access users should not be blocked ()" + "Title": "Emergency access users should not be blocked ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1034.1", "Severity": "High", - "Title": "Emergency access users should not be blocked ()" + "Title": "Emergency access users should not be blocked ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1034.2", "Severity": "High", - "Title": "Emergency access users should not be blocked ()" + "Title": "Emergency access users should not be blocked ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1034.3", "Severity": "High", - "Title": "Emergency access users should not be blocked ()" + "Title": "Emergency access users should not be blocked ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1034.4", "Severity": "High", - "Title": "Emergency access users should not be blocked ()" + "Title": "Emergency access users should not be blocked ()", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1035", "Severity": "High", - "Title": "All security groups assigned to Conditional Access Policies should be protected by RMAU." + "Title": "All security groups assigned to Conditional Access Policies should be protected by RMAU.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1036", "Severity": "Medium", - "Title": "All excluded objects should have a fallback include in another policy." + "Title": "All excluded objects should have a fallback include in another policy.", + "Licenses": [] }, { "Id": "MT.1037", "Severity": "High", - "Title": "Only users with Presenter role are allowed to present in Teams meetings" + "Title": "Only users with Presenter role are allowed to present in Teams meetings", + "Licenses": [] }, { "Id": "MT.1038", "Severity": "Medium", - "Title": "Conditional Access policies should not include or exclude deleted groups." + "Title": "Conditional Access policies should not include or exclude deleted groups.", + "Licenses": [] }, { "Id": "MT.1039", "Severity": "Low", - "Title": "Ensure MailTips are enabled for end users" + "Title": "Ensure MailTips are enabled for end users", + "Licenses": [] }, { "Id": "MT.1041", "Severity": "High", - "Title": "Ensure users installing Outlook add-ins is not allowed" + "Title": "Ensure users installing Outlook add-ins is not allowed", + "Licenses": [] }, { "Id": "MT.1042", "Severity": "Medium", - "Title": "Restrict dial-in users from bypassing a meeting lobby" + "Title": "Restrict dial-in users from bypassing a meeting lobby", + "Licenses": [] }, { "Id": "MT.1043", "Severity": "Medium", - "Title": "Ensure Spam confidence level (SCL) is configured in mail transport rules with specific domains" + "Title": "Ensure Spam confidence level (SCL) is configured in mail transport rules with specific domains", + "Licenses": [] }, { "Id": "MT.1044", "Severity": "High", - "Title": "Ensure modern authentication for Exchange Online is enabled" + "Title": "Ensure modern authentication for Exchange Online is enabled", + "Licenses": [] }, { "Id": "MT.1045", "Severity": "Medium", - "Title": "Only invited users should be automatically admitted to Teams meetings" + "Title": "Only invited users should be automatically admitted to Teams meetings", + "Licenses": [] }, { "Id": "MT.1046", "Severity": "Medium", - "Title": "Restrict anonymous users from joining meetings" + "Title": "Restrict anonymous users from joining meetings", + "Licenses": [] }, { "Id": "MT.1047", "Severity": "Medium", - "Title": "Restrict anonymous users from starting Teams meetings" + "Title": "Restrict anonymous users from starting Teams meetings", + "Licenses": [] }, { "Id": "MT.1048", "Severity": "Medium", - "Title": "Limit external participants from having control in a Teams meeting" + "Title": "Limit external participants from having control in a Teams meeting", + "Licenses": [] }, { "Id": "MT.1049", "Severity": "High", - "Title": "Conditional Access policies for User Risk and Sign-in Risk should be configured separately." + "Title": "Conditional Access policies for User Risk and Sign-in Risk should be configured separately.", + "Licenses": ["EntraIDP2"] }, { "Id": "MT.1050", "Severity": "High", - "Title": "Apps with high-risk permissions having a direct path to Global Admin" + "Title": "Apps with high-risk permissions having a direct path to Global Admin", + "Licenses": [] }, { "Id": "MT.1051", "Severity": "High", - "Title": "Apps with high-risk permissions having an indirect path to Global Admin" + "Title": "Apps with high-risk permissions having an indirect path to Global Admin", + "Licenses": [] }, { "Id": "MT.1052", "Severity": "High", - "Title": "At least one Conditional Access policy is targeting the Device Code authentication flow." + "Title": "At least one Conditional Access policy is targeting the Device Code authentication flow.", + "Licenses": [] }, { "Id": "MT.1053", "Severity": "Medium", - "Title": "Ensure intune device clean-up rule is configured" + "Title": "Ensure intune device clean-up rule is configured", + "Licenses": ["Intune"] }, { "Id": "MT.1054", "Severity": "Medium", - "Title": "Ensure built-in Device Compliance Policy marks devices with no compliance policy assigned as 'Not compliant'" + "Title": "Ensure built-in Device Compliance Policy marks devices with no compliance policy assigned as 'Not compliant'", + "Licenses": ["Intune"] }, { "Id": "MT.1055", "Severity": "Medium", - "Title": "Microsoft 365 Group (and Team) creation should be restricted to approved users." + "Title": "Microsoft 365 Group (and Team) creation should be restricted to approved users.", + "Licenses": [] }, { "Id": "MT.1056", "Severity": "High", - "Title": "Ensure that no person has permanent access to all Azure subscriptions at the root scope" + "Title": "Ensure that no person has permanent access to all Azure subscriptions at the root scope", + "Licenses": [] }, { "Id": "MT.1057", "Severity": "Medium", - "Title": "Ensure Microsoft 365 Group (and Team) expiration is configured to notify users." + "Title": "Ensure Microsoft 365 Group (and Team) expiration is configured to notify users.", + "Licenses": [] }, { "Id": "MT.1058", "Severity": "Medium", - "Title": "Ensure Microsoft 365 Group (and Team) expiration is configured to auto-expire groups." + "Title": "Ensure Microsoft 365 Group (and Team) expiration is configured to auto-expire groups.", + "Licenses": [] }, { "Id": "MT.1059", "Severity": "Medium", - "Title": "Microsoft Defender for Identity health issues should be resolved" + "Title": "Microsoft Defender for Identity health issues should be resolved", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1061", "Severity": "Medium", - "Title": "Device registration MFA control conflicts with Conditional Access policies" + "Title": "Device registration MFA control conflicts with Conditional Access policies", + "Licenses": [] }, { "Id": "MT.1062", "Severity": "Medium", - "Title": "Ensure Direct Send is set to be rejected" + "Title": "Ensure Direct Send is set to be rejected", + "Licenses": [] }, { "Id": "MT.1063", "Severity": "High", - "Title": "All app registration owners should have MFA registered" + "Title": "All app registration owners should have MFA registered", + "Licenses": [] }, { "Id": "MT.1064", "Severity": "High", - "Title": "Management group creation should be limited to users with explicit write access" + "Title": "Management group creation should be limited to users with explicit write access", + "Licenses": [] }, { "Id": "MT.1065", "Severity": "High", - "Title": "Soft Delete should be enabled on all Recovery Services Vaults" + "Title": "Soft Delete should be enabled on all Recovery Services Vaults", + "Licenses": [] }, { "Id": "MT.1066", "Severity": "Medium", - "Title": "Conditional Access policies should not include or exclude deleted users, groups, or roles." + "Title": "Conditional Access policies should not include or exclude deleted users, groups, or roles.", + "Licenses": [] }, { "Id": "MT.1067", "Severity": "Medium", - "Title": "Authentication methods policies should not reference deleted groups." + "Title": "Authentication methods policies should not reference deleted groups.", + "Licenses": [] }, { "Id": "MT.1068", "Severity": "Medium", - "Title": "Restrict non-admin users from creating tenants" + "Title": "Restrict non-admin users from creating tenants", + "Licenses": [] }, { "Id": "MT.1069", "Severity": "Low", - "Title": "Restrict non-admin users from creating security groups." + "Title": "Restrict non-admin users from creating security groups.", + "Licenses": [] }, { "Id": "MT.1070", "Severity": "Medium", - "Title": "Restrict device join to selected users/groups or none." + "Title": "Restrict device join to selected users/groups or none.", + "Licenses": [] }, { "Id": "MT.1071", "Severity": "Medium", - "Title": "At least one Conditional Access policy explicitly includes Azure DevOps." + "Title": "At least one Conditional Access policy explicitly includes Azure DevOps.", + "Licenses": ["EntraIDP1"] }, { "Id": "MT.1072", "Severity": "High", - "Title": "Conditional access policies should not use the deprecated Approved Client App grant." + "Title": "Conditional access policies should not use the deprecated Approved Client App grant.", + "Licenses": [] }, { "Id": "MT.1073", "Severity": "Medium", - "Title": "Soft- and hard-matching of synchronized objects should be blocked." + "Title": "Soft- and hard-matching of synchronized objects should be blocked.", + "Licenses": [] }, { "Id": "MT.1074", "Severity": "Medium", - "Title": "Mailboxes should not send outbound mails using the .onmicrosoft.com domain." + "Title": "Mailboxes should not send outbound mails using the .onmicrosoft.com domain.", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1075", "Severity": "Medium", - "Title": "Third Party Entra Apps should only have explicitly assigned users instead of All Users." + "Title": "Third Party Entra Apps should only have explicitly assigned users instead of All Users.", + "Licenses": [] }, { "Id": "MT.1076", "Severity": "High", - "Title": "MOERA SHOULD NOT be used for sent mail." + "Title": "MOERA SHOULD NOT be used for sent mail.", + "Licenses": [] }, { "Id": "MT.1077", "Severity": "Medium", - "Title": "App registrations with privileged API permissions should not have owners" + "Title": "App registrations with privileged API permissions should not have owners", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1078", "Severity": "Medium", - "Title": "App registrations with highly privileged directory roles should not have owners" + "Title": "App registrations with highly privileged directory roles should not have owners", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1079", "Severity": "Medium", - "Title": "Privileged API permissions on service principals should not remain unused" + "Title": "Privileged API permissions on service principals should not remain unused", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1080", "Severity": "Medium", - "Title": "Credentials, tokens, or cookies from highly privileged users should not be exposed on vulnerable endpoints" + "Title": "Credentials, tokens, or cookies from highly privileged users should not be exposed on vulnerable endpoints", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1081", "Severity": "Medium", - "Title": "Hybrid users should not be assigned Entra ID role assignments" + "Title": "Hybrid users should not be assigned Entra ID role assignments", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1083", "Severity": "Low", - "Title": "Ensure Delicensing Resiliency is enabled" + "Title": "Ensure Delicensing Resiliency is enabled", + "Licenses": [] }, { "Id": "MT.1084", "Severity": "High", - "Title": "Seamless Single SignOn should be disabled for all domains in EntraID Connect servers." + "Title": "Seamless Single SignOn should be disabled for all domains in EntraID Connect servers.", + "Licenses": [] }, { "Id": "MT.1085", "Severity": "Medium", - "Title": "Pending approvals for Critical Asset Management should not be present" + "Title": "Pending approvals for Critical Asset Management should not be present", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1086", "Severity": "Low", - "Title": "Devices should not share both critical and non-critical user credentials." + "Title": "Devices should not share both critical and non-critical user credentials.", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1087", "Severity": "High", - "Title": "Devices should not be publicly exposed with remotely exploitable, highly likely to be exploited, high or critical severity CVE's." + "Title": "Devices should not be publicly exposed with remotely exploitable, highly likely to be exploited, high or critical severity CVE's.", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1088", "Severity": "Medium", - "Title": "Devices with critical credentials should be protected by TPM." + "Title": "Devices with critical credentials should be protected by TPM.", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1089", "Severity": "Medium", - "Title": "Devices with critical credentials should be protected by Credential Guard." + "Title": "Devices with critical credentials should be protected by Credential Guard.", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1090", "Severity": "Medium", - "Title": "Global administrator role should not be added as local administrator on the device during Microsoft Entra join" + "Title": "Global administrator role should not be added as local administrator on the device during Microsoft Entra join", + "Licenses": [] }, { "Id": "MT.1091", "Severity": "Medium", - "Title": "Registering user should not be added as local administrator on the device during Microsoft Entra join" + "Title": "Registering user should not be added as local administrator on the device during Microsoft Entra join", + "Licenses": [] }, { "Id": "MT.1092", "Severity": "High", - "Title": "Intune APNS certificate should be valid for more than 30 days" + "Title": "Intune APNS certificate should be valid for more than 30 days", + "Licenses": ["Intune"] }, { "Id": "MT.1093", "Severity": "High", - "Title": "Apple Automated Device Enrollment Tokens should be valid for more than 30 days" + "Title": "Apple Automated Device Enrollment Tokens should be valid for more than 30 days", + "Licenses": ["Intune"] }, { "Id": "MT.1094", "Severity": "High", - "Title": "Apple Volume Purchase Program Tokens should be valid for more than 30 days" + "Title": "Apple Volume Purchase Program Tokens should be valid for more than 30 days", + "Licenses": ["Intune"] }, { "Id": "MT.1095", "Severity": "High", - "Title": "Android Enterprise Account Connection should be healthy" + "Title": "Android Enterprise Account Connection should be healthy", + "Licenses": ["Intune"] }, { "Id": "MT.1096", "Severity": "Medium", - "Title": "Intune Multi Admin approval should be configured" + "Title": "Intune Multi Admin approval should be configured", + "Licenses": ["Intune"] }, { "Id": "MT.1097", "Severity": "High", - "Title": "Certificate Connectors should be healthy and running supported versions" + "Title": "Certificate Connectors should be healthy and running supported versions", + "Licenses": ["Intune"] }, { "Id": "MT.1098", "Severity": "Critical", - "Title": "Mobile Threat Defense Connectors should be healthy" + "Title": "Mobile Threat Defense Connectors should be healthy", + "Licenses": ["Intune"] }, { "Id": "MT.1099", "Severity": "Low", - "Title": "Windows Diagnostic Data Processing should be enabled" + "Title": "Windows Diagnostic Data Processing should be enabled", + "Licenses": ["Intune"] }, { "Id": "MT.1100", "Severity": "High", - "Title": "Intune Audit Logs should be retained" + "Title": "Intune Audit Logs should be retained", + "Licenses": ["Intune"] }, { "Id": "MT.1101", "Severity": "Low", - "Title": "Default Branding Profile should be customized" + "Title": "Default Branding Profile should be customized", + "Licenses": ["Intune"] }, { "Id": "MT.1102", "Severity": "High", - "Title": "Windows Feature Update Policy Settings should not reference end of support builds" + "Title": "Windows Feature Update Policy Settings should not reference end of support builds", + "Licenses": ["Intune"] }, { "Id": "MT.1103", "Severity": "High", - "Title": "Intune RBAC groups should be protected by Restricted Management Administrative Units or Role Assignable groups" + "Title": "Intune RBAC groups should be protected by Restricted Management Administrative Units or Role Assignable groups", + "Licenses": ["EntraIDP1", "Intune"] }, { "Id": "MT.1105", "Severity": "Low", - "Title": "MDM Authority should be set to Microsoft Intune" + "Title": "MDM Authority should be set to Microsoft Intune", + "Licenses": ["Intune"] }, { "Id": "MT.1106", "Severity": "Medium", - "Title": "Catalog resources must have valid roles (no stale app roles or deleted SPNs)" + "Title": "Catalog resources must have valid roles (no stale app roles or deleted SPNs)", + "Licenses": ["EntraIDGovernance"] }, { "Id": "MT.1107", "Severity": "Medium", - "Title": "Access packages and catalogs should not reference deleted groups" + "Title": "Access packages and catalogs should not reference deleted groups", + "Licenses": ["EntraIDGovernance"] }, { "Id": "MT.1108", "Severity": "Medium", - "Title": "Access packages should not have inactive or orphaned assignment policies" + "Title": "Access packages should not have inactive or orphaned assignment policies", + "Licenses": ["EntraIDGovernance"] }, { "Id": "MT.1109", "Severity": "Medium", - "Title": "Access package approval workflows must have valid approvers" + "Title": "Access package approval workflows must have valid approvers", + "Licenses": ["EntraIDGovernance"] }, { "Id": "MT.1110", "Severity": "Medium", - "Title": "No catalog should contain resources without any associated access packages" + "Title": "No catalog should contain resources without any associated access packages", + "Licenses": ["EntraIDGovernance"] }, { "Id": "MT.1111", "Severity": "Low", - "Title": "High privileged user should be linked to an identity" + "Title": "High privileged user should be linked to an identity", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1112", "Severity": "Medium", - "Title": "Privileged user accounts should not remain enabled when the linked primary account is disabled" + "Title": "Privileged user accounts should not remain enabled when the linked primary account is disabled", + "Licenses": ["DefenderXDR"] }, { "Id": "MT.1113", "Severity": "High", - "Title": "AI agents should not be shared with broad access control policies" + "Title": "AI agents should not be shared with broad access control policies", + "Licenses": [] }, { "Id": "MT.1114", "Severity": "High", - "Title": "AI agents should require user authentication" + "Title": "AI agents should require user authentication", + "Licenses": [] }, { "Id": "MT.1115", "Severity": "Medium", - "Title": "AI agents should not have risky HTTP configurations" + "Title": "AI agents should not have risky HTTP configurations", + "Licenses": [] }, { "Id": "MT.1116", "Severity": "High", - "Title": "AI agents should not send email with AI-controlled inputs" + "Title": "AI agents should not send email with AI-controlled inputs", + "Licenses": [] }, { "Id": "MT.1117", "Severity": "Low", - "Title": "Published AI agents should not be dormant" + "Title": "Published AI agents should not be dormant", + "Licenses": [] }, { "Id": "MT.1118", "Severity": "Medium", - "Title": "AI agents should avoid using author (maker) authentication for tools" + "Title": "AI agents should avoid using author (maker) authentication for tools", + "Licenses": [] }, { "Id": "MT.1119", "Severity": "High", - "Title": "AI agents should not have hard-coded credentials in topics" + "Title": "AI agents should not have hard-coded credentials in topics", + "Licenses": [] }, { "Id": "MT.1120", "Severity": "Medium", - "Title": "AI agents should not use MCP server tools without review" + "Title": "AI agents should not use MCP server tools without review", + "Licenses": [] }, { "Id": "MT.1121", "Severity": "Medium", - "Title": "AI agents with generative orchestration should have custom instructions" + "Title": "AI agents with generative orchestration should have custom instructions", + "Licenses": [] }, { "Id": "MT.1122", "Severity": "Medium", - "Title": "AI agents should not have orphaned ownership" + "Title": "AI agents should not have orphaned ownership", + "Licenses": [] }, { "Id": "MT.1123", "Severity": "High", - "Title": "Ensure BitLocker full disk encryption is configured via Intune" + "Title": "Ensure BitLocker full disk encryption is configured via Intune", + "Licenses": ["Intune"] }, { "Id": "MT.1147", "Severity": "High", - "Title": "Do not sync krbtgt_AzureAD to Entra ID" + "Title": "Do not sync krbtgt_AzureAD to Entra ID", + "Licenses": [] }, { "Id": "MT.1148", "Severity": "High", - "Title": "Archive Scanning should be enabled" + "Title": "Archive Scanning should be enabled", + "Licenses": [] }, { "Id": "MT.1149", "Severity": "High", - "Title": "Behavior Monitoring should be enabled" + "Title": "Behavior Monitoring should be enabled", + "Licenses": [] }, { "Id": "MT.1150", "Severity": "High", - "Title": "Cloud Protection should be enabled" + "Title": "Cloud Protection should be enabled", + "Licenses": [] }, { "Id": "MT.1151", "Severity": "High", - "Title": "Email Scanning should be enabled" + "Title": "Email Scanning should be enabled", + "Licenses": [] }, { "Id": "MT.1152", "Severity": "High", - "Title": "Script Scanning should be enabled" + "Title": "Script Scanning should be enabled", + "Licenses": [] }, { "Id": "MT.1153", "Severity": "High", - "Title": "Real-time Monitoring should be enabled" + "Title": "Real-time Monitoring should be enabled", + "Licenses": [] }, { "Id": "MT.1154", "Severity": "High", - "Title": "Full Scan Removable Drives should be enabled" + "Title": "Full Scan Removable Drives should be enabled", + "Licenses": [] }, { "Id": "MT.1155", "Severity": "High", - "Title": "Full Scan Mapped Drives should be disabled for performance" + "Title": "Full Scan Mapped Drives should be disabled for performance", + "Licenses": [] }, { "Id": "MT.1156", "Severity": "High", - "Title": "Scanning Network Files should be enabled" + "Title": "Scanning Network Files should be enabled", + "Licenses": [] }, { "Id": "MT.1157", "Severity": "High", - "Title": "CPU Load Factor should be optimized (20-30%)" + "Title": "CPU Load Factor should be optimized (20-30%)", + "Licenses": [] }, { "Id": "MT.1158", "Severity": "High", - "Title": "Scan should be scheduled" + "Title": "Scan should be scheduled", + "Licenses": [] }, { "Id": "MT.1159", "Severity": "High", - "Title": "Quick Scan Time configuration is not required" + "Title": "Quick Scan Time configuration is not required", + "Licenses": [] }, { "Id": "MT.1160", "Severity": "High", - "Title": "Signatures should be checked before scan" + "Title": "Signatures should be checked before scan", + "Licenses": [] }, { "Id": "MT.1161", "Severity": "High", - "Title": "Cloud Block Level should be High or higher" + "Title": "Cloud Block Level should be High or higher", + "Licenses": [] }, { "Id": "MT.1162", "Severity": "High", - "Title": "Cloud Extended Timeout should be 30-50 seconds" + "Title": "Cloud Extended Timeout should be 30-50 seconds", + "Licenses": [] }, { "Id": "MT.1163", "Severity": "High", - "Title": "Signature Update Interval should be 1-4 hours" + "Title": "Signature Update Interval should be 1-4 hours", + "Licenses": [] }, { "Id": "MT.1164", "Severity": "High", - "Title": "PUA Protection should be enabled" + "Title": "PUA Protection should be enabled", + "Licenses": [] }, { "Id": "MT.1165", "Severity": "High", - "Title": "Network Protection should be enabled" + "Title": "Network Protection should be enabled", + "Licenses": [] }, { "Id": "MT.1166", "Severity": "High", - "Title": "Local Admin Merge should be disabled" + "Title": "Local Admin Merge should be disabled", + "Licenses": [] }, { "Id": "MT.1167", "Severity": "High", - "Title": "Real-Time Scan Direction should cover both directions" + "Title": "Real-Time Scan Direction should cover both directions", + "Licenses": [] }, { "Id": "MT.1168", "Severity": "High", - "Title": "Cleaned Malware should be retained for at least 30 days" + "Title": "Cleaned Malware should be retained for at least 30 days", + "Licenses": [] }, { "Id": "MT.1169", "Severity": "High", - "Title": "Catch-up Full Scan should be disabled" + "Title": "Catch-up Full Scan should be disabled", + "Licenses": [] }, { "Id": "MT.1170", "Severity": "High", - "Title": "Catch-up Quick Scan should be disabled" + "Title": "Catch-up Quick Scan should be disabled", + "Licenses": [] }, { "Id": "MT.1171", "Severity": "High", - "Title": "Sample Submission should send safe samples automatically" + "Title": "Sample Submission should send safe samples automatically", + "Licenses": [] }, { "Id": "ORCA.100", "Severity": "Medium", - "Title": "Bulk Complaint Level threshold is between 4 and 6." + "Title": "Bulk Complaint Level threshold is between 4 and 6.", + "Licenses": [] }, { "Id": "ORCA.101", "Severity": "Medium", - "Title": "Bulk is marked as spam." + "Title": "Bulk is marked as spam.", + "Licenses": [] }, { "Id": "ORCA.102", "Severity": "Medium", - "Title": "Advanced Spam filter options are turned off." + "Title": "Advanced Spam filter options are turned off.", + "Licenses": [] }, { "Id": "ORCA.103", "Severity": "Medium", - "Title": "Outbound spam filter policy settings configured." + "Title": "Outbound spam filter policy settings configured.", + "Licenses": [] }, { "Id": "ORCA.104", "Severity": "High", - "Title": "High Confidence Phish action set to Quarantine message." + "Title": "High Confidence Phish action set to Quarantine message.", + "Licenses": [] }, { "Id": "ORCA.105", "Severity": "Medium", - "Title": "Safe Links Synchronous URL detonation is enabled." + "Title": "Safe Links Synchronous URL detonation is enabled.", + "Licenses": [] }, { "Id": "ORCA.106", "Severity": "Medium", - "Title": "Quarantine retention period is 30 days." + "Title": "Quarantine retention period is 30 days.", + "Licenses": [] }, { "Id": "ORCA.107", "Severity": "Low", - "Title": "End-user spam notification is enabled." + "Title": "End-user spam notification is enabled.", + "Licenses": [] }, { "Id": "ORCA.108", "Severity": "Medium", - "Title": "DKIM signing is set up for all your custom domains." + "Title": "DKIM signing is set up for all your custom domains.", + "Licenses": [] }, { "Id": "ORCA.108.1", "Severity": "Medium", - "Title": "DNS Records have been set up to support DKIM." + "Title": "DNS Records have been set up to support DKIM.", + "Licenses": [] }, { "Id": "ORCA.109", "Severity": "Medium", - "Title": "Senders are not being allow listed in an unsafe manner." + "Title": "Senders are not being allow listed in an unsafe manner.", + "Licenses": [] }, { "Id": "ORCA.110", "Severity": "Medium", - "Title": "Internal Sender notifications are disabled." + "Title": "Internal Sender notifications are disabled.", + "Licenses": [] }, { "Id": "ORCA.111", "Severity": "High", - "Title": "Anti-phishing policy exists and EnableUnauthenticatedSender is true." + "Title": "Anti-phishing policy exists and EnableUnauthenticatedSender is true.", + "Licenses": [] }, { "Id": "ORCA.112", "Severity": "Medium", - "Title": "Anti-spoofing protection action is configured to Move message to the recipients' Junk Email folders in Anti-phishing policy." + "Title": "Anti-spoofing protection action is configured to Move message to the recipients' Junk Email folders in Anti-phishing policy.", + "Licenses": [] }, { "Id": "ORCA.113", "Severity": "Medium", - "Title": "AllowClickThrough is disabled in Safe Links policies." + "Title": "AllowClickThrough is disabled in Safe Links policies.", + "Licenses": [] }, { "Id": "ORCA.114", "Severity": "High", - "Title": "No IP Allow Lists have been configured." + "Title": "No IP Allow Lists have been configured.", + "Licenses": [] }, { "Id": "ORCA.115", "Severity": "Medium", - "Title": "Mailbox intelligence based impersonation protection is enabled in anti-phishing policies." + "Title": "Mailbox intelligence based impersonation protection is enabled in anti-phishing policies.", + "Licenses": [] }, { "Id": "ORCA.116", "Severity": "Medium", - "Title": "Mailbox intelligence based impersonation protection action set to move message to junk mail folder." + "Title": "Mailbox intelligence based impersonation protection action set to move message to junk mail folder.", + "Licenses": [] }, { "Id": "ORCA.118.1", "Severity": "High", - "Title": "Domains are not being allow listed in an unsafe manner in Anti-Spam Policies." + "Title": "Domains are not being allow listed in an unsafe manner in Anti-Spam Policies.", + "Licenses": [] }, { "Id": "ORCA.118.2", "Severity": "High", - "Title": "Domains are not being allow listed in an unsafe manner in Transport Rules." + "Title": "Domains are not being allow listed in an unsafe manner in Transport Rules.", + "Licenses": [] }, { "Id": "ORCA.118.3", "Severity": "Medium", - "Title": "Your own domains are not being allow listed in an unsafe manner in Anti-Spam Policies." + "Title": "Your own domains are not being allow listed in an unsafe manner in Anti-Spam Policies.", + "Licenses": [] }, { "Id": "ORCA.118.4", "Severity": "Medium", - "Title": "Your own domains are not being allow listed in an unsafe manner in Transport Rules." + "Title": "Your own domains are not being allow listed in an unsafe manner in Transport Rules.", + "Licenses": [] }, { "Id": "ORCA.119", "Severity": "Info", - "Title": "Similar Domains Safety Tips is enabled." + "Title": "Similar Domains Safety Tips is enabled.", + "Licenses": [] }, { "Id": "ORCA.120.1", "Severity": "Medium", - "Title": "Zero Hour Autopurge Enabled for Phish." + "Title": "Zero Hour Autopurge Enabled for Phish.", + "Licenses": [] }, { "Id": "ORCA.120.2", "Severity": "Medium", - "Title": "Zero Hour Autopurge Enabled for Malware." + "Title": "Zero Hour Autopurge Enabled for Malware.", + "Licenses": [] }, { "Id": "ORCA.120.3", "Severity": "Medium", - "Title": "Zero Hour Autopurge Enabled for Spam." + "Title": "Zero Hour Autopurge Enabled for Spam.", + "Licenses": [] }, { "Id": "ORCA.121", "Severity": "Low", - "Title": "Supported filter policy action used." + "Title": "Supported filter policy action used.", + "Licenses": [] }, { "Id": "ORCA.123", "Severity": "Info", - "Title": "Unusual Characters Safety Tips is enabled." + "Title": "Unusual Characters Safety Tips is enabled.", + "Licenses": [] }, { "Id": "ORCA.124", "Severity": "High", - "Title": "Safe attachments unknown malware response set to block messages." + "Title": "Safe attachments unknown malware response set to block messages.", + "Licenses": [] }, { "Id": "ORCA.139", "Severity": "Low", - "Title": "Spam action set to move message to junk mail folder or quarantine." + "Title": "Spam action set to move message to junk mail folder or quarantine.", + "Licenses": [] }, { "Id": "ORCA.140", "Severity": "High", - "Title": "High Confidence Spam action set to Quarantine message." + "Title": "High Confidence Spam action set to Quarantine message.", + "Licenses": [] }, { "Id": "ORCA.141", "Severity": "Medium", - "Title": "Bulk action set to Move message to Junk Email Folder." + "Title": "Bulk action set to Move message to Junk Email Folder.", + "Licenses": [] }, { "Id": "ORCA.142", "Severity": "Medium", - "Title": "Phish action set to Quarantine message." + "Title": "Phish action set to Quarantine message.", + "Licenses": [] }, { "Id": "ORCA.143", "Severity": "Info", - "Title": "Safety Tips are enabled." + "Title": "Safety Tips are enabled.", + "Licenses": [] }, { "Id": "ORCA.156", "Severity": "Medium", - "Title": "Safe Links Policies are tracking when user clicks on safe links." + "Title": "Safe Links Policies are tracking when user clicks on safe links.", + "Licenses": [] }, { "Id": "ORCA.158", "Severity": "Medium", - "Title": "Safe Attachments is enabled for SharePoint and Teams." + "Title": "Safe Attachments is enabled for SharePoint and Teams.", + "Licenses": [] }, { "Id": "ORCA.179", "Severity": "Medium", - "Title": "Safe Links is enabled intra-organization." + "Title": "Safe Links is enabled intra-organization.", + "Licenses": [] }, { "Id": "ORCA.180", "Severity": "Medium", - "Title": "Anti-phishing policy exists and EnableSpoofIntelligence is true." + "Title": "Anti-phishing policy exists and EnableSpoofIntelligence is true.", + "Licenses": [] }, { "Id": "ORCA.189", "Severity": "Medium", - "Title": "Safe Attachments is not bypassed." + "Title": "Safe Attachments is not bypassed.", + "Licenses": [] }, { "Id": "ORCA.189.2", "Severity": "High", - "Title": "Safe Links is not bypassed." + "Title": "Safe Links is not bypassed.", + "Licenses": [] }, { "Id": "ORCA.205", "Severity": "Medium", - "Title": "Common attachment type filter is enabled." + "Title": "Common attachment type filter is enabled.", + "Licenses": [] }, { "Id": "ORCA.220", "Severity": "Medium", - "Title": "Advanced Phish filter Threshold level is adequate." + "Title": "Advanced Phish filter Threshold level is adequate.", + "Licenses": [] }, { "Id": "ORCA.221", "Severity": "Medium", - "Title": "Mailbox intelligence is enabled in anti-phishing policies." + "Title": "Mailbox intelligence is enabled in anti-phishing policies.", + "Licenses": [] }, { "Id": "ORCA.222", "Severity": "Medium", - "Title": "Domain Impersonation action is set to move to Quarantine." + "Title": "Domain Impersonation action is set to move to Quarantine.", + "Licenses": [] }, { "Id": "ORCA.223", "Severity": "High", - "Title": "User impersonation action is set to move to Quarantine." + "Title": "User impersonation action is set to move to Quarantine.", + "Licenses": [] }, { "Id": "ORCA.224", "Severity": "Info", - "Title": "Similar Users Safety Tips is enabled." + "Title": "Similar Users Safety Tips is enabled.", + "Licenses": [] }, { "Id": "ORCA.225", "Severity": "Medium", - "Title": "Safe Documents is enabled for Office clients." + "Title": "Safe Documents is enabled for Office clients.", + "Licenses": [] }, { "Id": "ORCA.226", "Severity": "Medium", - "Title": "Each domain has a Safe Link policy applied to it." + "Title": "Each domain has a Safe Link policy applied to it.", + "Licenses": [] }, { "Id": "ORCA.227", "Severity": "Medium", - "Title": "Each domain has a Safe Attachments policy applied to it." + "Title": "Each domain has a Safe Attachments policy applied to it.", + "Licenses": [] }, { "Id": "ORCA.228", "Severity": "High", - "Title": "No trusted senders in Anti-phishing policy." + "Title": "No trusted senders in Anti-phishing policy.", + "Licenses": [] }, { "Id": "ORCA.229", "Severity": "Medium", - "Title": "No trusted domains in Anti-phishing policy." + "Title": "No trusted domains in Anti-phishing policy.", + "Licenses": [] }, { "Id": "ORCA.230", "Severity": "Medium", - "Title": "Each domain has a Anti-phishing policy applied to it, or the default policy is being used." + "Title": "Each domain has a Anti-phishing policy applied to it, or the default policy is being used.", + "Licenses": [] }, { "Id": "ORCA.231", "Severity": "Medium", - "Title": "Each domain has a anti-spam policy applied to it, or the default policy is being used." + "Title": "Each domain has a anti-spam policy applied to it, or the default policy is being used.", + "Licenses": [] }, { "Id": "ORCA.232", "Severity": "High", - "Title": "Each domain has a malware filter policy applied to it, or the default policy is being used." + "Title": "Each domain has a malware filter policy applied to it, or the default policy is being used.", + "Licenses": [] }, { "Id": "ORCA.233", "Severity": "Medium", - "Title": "Domains are pointed directly at EOP or enhanced filtering is used." + "Title": "Domains are pointed directly at EOP or enhanced filtering is used.", + "Licenses": [] }, { "Id": "ORCA.233.1", "Severity": "Medium", - "Title": "Domains are pointed directly at EOP or enhanced filtering is configured on all default connectors." + "Title": "Domains are pointed directly at EOP or enhanced filtering is configured on all default connectors.", + "Licenses": [] }, { "Id": "ORCA.234", "Severity": "Medium", - "Title": "Click through is disabled for Safe Documents." + "Title": "Click through is disabled for Safe Documents.", + "Licenses": [] }, { "Id": "ORCA.235", "Severity": "Medium", - "Title": "SPF records is set up for all your custom domains." + "Title": "SPF records is set up for all your custom domains.", + "Licenses": [] }, { "Id": "ORCA.236", "Severity": "Medium", - "Title": "Safe Links is enabled for emails." + "Title": "Safe Links is enabled for emails.", + "Licenses": [] }, { "Id": "ORCA.237", "Severity": "Medium", - "Title": "Safe Links is enabled for teams messages." + "Title": "Safe Links is enabled for teams messages.", + "Licenses": [] }, { "Id": "ORCA.238", "Severity": "Medium", - "Title": "Safe Links is enabled for office documents." + "Title": "Safe Links is enabled for office documents.", + "Licenses": [] }, { "Id": "ORCA.239", "Severity": "High", - "Title": "No exclusions for the built-in protection policies." + "Title": "No exclusions for the built-in protection policies.", + "Licenses": [] }, { "Id": "ORCA.240", "Severity": "Medium", - "Title": "Outlook is configured to display external tags for external emails." + "Title": "Outlook is configured to display external tags for external emails.", + "Licenses": [] }, { "Id": "ORCA.241", "Severity": "Medium", - "Title": "Anti-phishing policy exists and EnableFirstContactSafetyTips is true." + "Title": "Anti-phishing policy exists and EnableFirstContactSafetyTips is true.", + "Licenses": [] }, { "Id": "ORCA.242", "Severity": "High", - "Title": "Important protection alerts responsible for AIR activities are enabled." + "Title": "Important protection alerts responsible for AIR activities are enabled.", + "Licenses": [] }, { "Id": "ORCA.243", "Severity": "Medium", - "Title": "Authenticated Receive Chain is set up for domains not pointing to EOP/MDO, or all domains point to EOP/MDO." + "Title": "Authenticated Receive Chain is set up for domains not pointing to EOP/MDO, or all domains point to EOP/MDO.", + "Licenses": [] }, { "Id": "ORCA.244", "Severity": "Medium", - "Title": "Policies are configured to honor sending domains DMARC." + "Title": "Policies are configured to honor sending domains DMARC.", + "Licenses": [] }, { "Id": "ORCA.1000", "Severity": "High", - "Title": "Exchange Online Protection (EOP) is enabled." + "Title": "Exchange Online Protection (EOP) is enabled.", + "Licenses": ["TBD"] }, { "Id": "ORCA.1001", "Severity": "High", - "Title": "Spam filter policy settings configured." + "Title": "Spam filter policy settings configured.", + "Licenses": ["TBD"] }, { "Id": "ORCA.1002", "Severity": "High", - "Title": "Malware filter policy settings configured." + "Title": "Malware filter policy settings configured.", + "Licenses": ["TBD"] }, { "Id": "ORCA.1003", "Severity": "High", - "Title": "Phishing filter policy settings configured." + "Title": "Phishing filter policy settings configured.", + "Licenses": ["TBD"] }, { "Id": "ORCA.1004", "Severity": "High", - "Title": " filtering policy settings configured." + "Title": " filtering policy settings configured.", + "Licenses": ["TBD"] }, { "Id": "AZDO.1000", "Severity": "High", - "Title": "Azure DevOps OAuth apps can access resources in your organization through OAuth" + "Title": "Azure DevOps OAuth apps can access resources in your organization through OAuth", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1001", "Severity": "High", - "Title": "Identities can connect to your organization's Git repos through SSH" + "Title": "Identities can connect to your organization's Git repos through SSH", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1002", "Severity": "High", - "Title": "Log Audit Events" + "Title": "Log Audit Events", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1003", "Severity": "High", - "Title": "Restrict public projects" + "Title": "Restrict public projects", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1004", "Severity": "High", - "Title": "Additional protections when using public registries" + "Title": "Additional protections when using public registries", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1005", "Severity": "High", - "Title": "IP Conditional Access policy validation" + "Title": "IP Conditional Access policy validation", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1006", "Severity": "High", - "Title": "External Users access" + "Title": "External Users access", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1007", "Severity": "High", - "Title": "Team and project administrator are allowed to invite new users" + "Title": "Team and project administrator are allowed to invite new users", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1008", "Severity": "Medium", - "Title": "Request access to Azure DevOps by e-mail notifications to administrators" + "Title": "Request access to Azure DevOps by e-mail notifications to administrators", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1009", "Severity": "Info", - "Title": "Feedback Collection" + "Title": "Feedback Collection", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1010", "Severity": "High", - "Title": "Audit streaming" + "Title": "Audit streaming", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1011", "Severity": "Info", - "Title": "Project Resource Limits" + "Title": "Project Resource Limits", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1012", "Severity": "Info", - "Title": "Work Items Tags Limits" + "Title": "Work Items Tags Limits", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1013", "Severity": "High", - "Title": "Organization Owner should not be an individual" + "Title": "Organization Owner should not be an individual", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1014", "Severity": "Medium", - "Title": "Anonymous access to pipeline badges" + "Title": "Anonymous access to pipeline badges", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1015", "Severity": "High", - "Title": "Limit variables that can be set at queue time" + "Title": "Limit variables that can be set at queue time", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1016", "Severity": "High", - "Title": "Limit job authorization scope to current project for non-release pipelines" + "Title": "Limit job authorization scope to current project for non-release pipelines", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1017", "Severity": "High", - "Title": "Limit job authorization scope to current project for classic release pipelines" + "Title": "Limit job authorization scope to current project for classic release pipelines", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1018", "Severity": "High", - "Title": "Protect access to repositories in YAML pipelines" + "Title": "Protect access to repositories in YAML pipelines", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1019", "Severity": "Medium", - "Title": "Stage chooser" + "Title": "Stage chooser", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1020", "Severity": "Medium", - "Title": "Creation of classic build pipelines" + "Title": "Creation of classic build pipelines", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1021", "Severity": "Medium", - "Title": "Creation of classic release pipelines" + "Title": "Creation of classic release pipelines", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1022", "Severity": "High", - "Title": "Limit building pull requests from forked GitHub repositories" + "Title": "Limit building pull requests from forked GitHub repositories", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1023", "Severity": "High", - "Title": "Disable Marketplace tasks" + "Title": "Disable Marketplace tasks", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1024", "Severity": "Medium", - "Title": "Disable Node 6 tasks" + "Title": "Disable Node 6 tasks", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1025", "Severity": "High", - "Title": "Enable shell tasks arguments validation" + "Title": "Enable shell tasks arguments validation", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1026", "Severity": "Medium", - "Title": "Enable automatic enrollment to Advanced Security for Azure DevOps" + "Title": "Enable automatic enrollment to Advanced Security for Azure DevOps", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1027", "Severity": "Medium", - "Title": "Disable showing Gravatar images for users outside of your enterprise" + "Title": "Disable showing Gravatar images for users outside of your enterprise", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1028", "Severity": "Medium", - "Title": "Disable creation of TFVC repositories" + "Title": "Disable creation of TFVC repositories", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1029", "Severity": "Medium", - "Title": "Storage Usage Limit" + "Title": "Storage Usage Limit", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1030", "Severity": "Critical", - "Title": "Project Collection Administrators" + "Title": "Project Collection Administrators", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1031", "Severity": "High", - "Title": "Validate SSH key expiration" + "Title": "Validate SSH key expiration", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1032", "Severity": "High", - "Title": "(Tenant) Restrict creation of global Personal Access Tokens" + "Title": "(Tenant) Restrict creation of global Personal Access Tokens", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1033", "Severity": "Critical", - "Title": "(Tenant) Enable automatic revocation of leaked Personal Access Tokens" + "Title": "(Tenant) Enable automatic revocation of leaked Personal Access Tokens", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1034", "Severity": "High", - "Title": "(Tenant) Restrict creation of new Azure DevOps organizations" + "Title": "(Tenant) Restrict creation of new Azure DevOps organizations", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1035", "Severity": "High", - "Title": "(Tenant) Restrict Personal Access Token lifespan" + "Title": "(Tenant) Restrict Personal Access Token lifespan", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1036", "Severity": "High", - "Title": "(Tenant) Restrict Personal Access Token full scope" + "Title": "(Tenant) Restrict Personal Access Token full scope", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1037", "Severity": "High", - "Title": "(Organization) Restrict Personal Access Token creation" + "Title": "(Organization) Restrict Personal Access Token creation", + "Licenses": ["AzureDevOps"] }, { "Id": "AZDO.1038", "Severity": "Medium", - "Title": "(Organization) Disallow extensions from accessing resources on the local network" + "Title": "(Organization) Disallow extensions from accessing resources on the local network", + "Licenses": ["AzureDevOps"] } ] } From f31d040703301a393038dd21cca3178eb0a1aa6e Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Wed, 6 May 2026 16:54:38 -0400 Subject: [PATCH 4/5] feat: add .claude/settings.local.json to .gitignore to exclude local settings from version control --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 234d9b740..b36ae3483 100644 --- a/.gitignore +++ b/.gitignore @@ -498,6 +498,7 @@ test.json # Claude Code .claude//** !.claude\agents\maester-test-expert.md +.claude/settings.local.json # Don't allow Maester test results in the main branch test-results From 0459a6372bb23de15dd927b44a3ed762d84cd9e9 Mon Sep 17 00:00:00 2001 From: Sam Erde <20478745+SamErde@users.noreply.github.com> Date: Wed, 6 May 2026 17:59:01 -0400 Subject: [PATCH 5/5] fix(build): address Copilot review on license enricher and verifier - Make Update-MaesterConfigLicenses.ps1 idempotent: strip any existing Licenses field from each TestSettings object before re-inserting. Re-running the script no longer produces duplicate "Licenses" keys. - Add markdown-signal extraction to the updater (website doc + companion function .md). MT.1071's "Microsoft Entra ID P1 or P2 is required" language now derives EntraIDP1 deterministically instead of needing a manual override that would be reverted on re-run. - Update the .SYNOPSIS / .DESCRIPTION blocks on both scripts so the documented signal list, defaults, verdicts, and -OnlyMismatches behavior match the actual code paths. - Validate License: tag tokens against the canonical vocabulary in the verifier so it stays consistent with the updater (both reject unknown tokens instead of silently disagreeing). Co-Authored-By: Claude Opus 4.7 (1M context) --- build/Test-MaesterConfigLicenses.ps1 | 23 ++++- build/Update-MaesterConfigLicenses.ps1 | 112 +++++++++++++++++++++++-- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/build/Test-MaesterConfigLicenses.ps1 b/build/Test-MaesterConfigLicenses.ps1 index 23f55d663..f264647be 100644 --- a/build/Test-MaesterConfigLicenses.ps1 +++ b/build/Test-MaesterConfigLicenses.ps1 @@ -9,13 +9,16 @@ Id | Config | Test | Function | Markdown | Verdict Verdicts: - OK — config matches the union of independent signals + OK — config matches the union of code-derived signals + OK_MD — config tokens are not in code, but markdown evidence supports them BASELINE — config is [] and no signals anywhere (intentional baseline) ORPHAN — no test file backs this Id (config-only entry) TBD — config says TBD; no signals found - MISMATCH — independent signals contradict the config (config has too few or too many tokens) + MISMATCH — code signals contradict the config (config is missing or has tokens + that aren't backed by code or markdown) - Pass -OnlyMismatches to filter out OK / BASELINE / ORPHAN rows. + Pass -OnlyMismatches to show only rows that warrant review: MISMATCH, TBD, and ORPHAN. + OK / OK_MD / BASELINE rows are filtered out. .EXAMPLE pwsh build/Test-MaesterConfigLicenses.ps1 @@ -42,6 +45,17 @@ $configPath = Join-Path $testsRoot 'maester-config.json' official names would just make the output clearer to external audiences and reduce the mental mapping. #> +# Canonical token vocabulary. Mirrors $canonicalTokens in Update-MaesterConfigLicenses.ps1 +# so the License: extractor below rejects tokens neither script recognizes. +$canonicalTokens = @( + 'EntraIDP1', 'EntraIDP2', 'EntraIDGovernance', + 'EntraWorkloadIDP1', 'EntraWorkloadIDP2', + 'Eop', 'MdoP1', 'MdoP2', + 'ExoDlp', 'AdvAudit', 'DefenderXDR', + 'Intune', 'CustomerLockbox', + 'AzureDevOps', 'TBD' +) + # Token vocabulary mirrors Get-MtLicenseInformation.ps1 + Get-MtSkippedReason.ps1. $skipReasonMap = @{ 'NotLicensedEntraIDP1' = 'EntraIDP1' @@ -132,7 +146,8 @@ function Get-CodeSignals { if ($Text -match "[""']\s*$([regex]::Escape($pair.Key))\s*[""']") { [void] $tokens.Add($pair.Value) } } foreach ($m in [regex]::Matches($Text, "[""']License:([A-Za-z0-9]+)[""']")) { - [void] $tokens.Add($m.Groups[1].Value) + $candidate = $m.Groups[1].Value + if ($canonicalTokens -contains $candidate) { [void] $tokens.Add($candidate) } } return @($tokens | Sort-Object) } diff --git a/build/Update-MaesterConfigLicenses.ps1 b/build/Update-MaesterConfigLicenses.ps1 index 11899ad0a..dd95b0733 100644 --- a/build/Update-MaesterConfigLicenses.ps1 +++ b/build/Update-MaesterConfigLicenses.ps1 @@ -7,16 +7,22 @@ invokes (powershell/public|internal/...) for license signals, then writes the resulting Licenses array onto each matching TestSettings item by Id. - Signals (first match wins, but multiple matches union): + Signals (multiple matches union): 1. Add-MtTestResultDetail -SkippedBecause NotLicensed 2. Get-MtLicenseInformation -Product X comparisons 3. -Skip:( ... license check ... ) on the It block 4. Same patterns inside the underlying Test-* function 5. License-bearing Describe/Context/It tags - 6. Suite default (AZDO -> AzureDevOps; EIDSCA -> []; everything else -> TBD) + 6. Explicit license-requirement statements in the website doc and in the + function's companion .md (e.g. "Microsoft Entra ID P1 or P2 is required") - Empty array = baseline (no premium license required). - ['TBD'] = needs human review. + Defaults when no signal is found: + - AZDO.* tests -> ["AzureDevOps"] + - Config Id with no backing test -> ["TBD"] (true orphan) + - Test exists, no premium signal -> [] (baseline is adequate) + + The script is idempotent: re-running replaces an existing Licenses field + in-place rather than appending a duplicate key. .PARAMETER DryRun Print the per-test license map and a tally; do not write to maester-config.json. @@ -35,6 +41,7 @@ $repoRoot = Split-Path $PSScriptRoot -Parent $testsRoot = Join-Path $repoRoot 'tests' $psPublicRoot = Join-Path $repoRoot 'powershell/public' $psInternalRoot = Join-Path $repoRoot 'powershell/internal' +$docsRoot = Join-Path $repoRoot 'website/docs/tests' $configPath = Join-Path $testsRoot 'maester-config.json' # --- Token vocabulary (kept in sync with Get-MtLicenseInformation.ps1 + Get-MtSkippedReason.ps1) --- @@ -80,6 +87,24 @@ $tagMap = @{ 'mdop2' = 'MdoP2' } +# Markdown free-text phrase -> token. Scanned in remediation/overview prose of +# website/docs/tests/.md and the companion .md beside the test +# function. Catches license claims that aren't reflected in code (e.g. a doc +# that says "Microsoft Entra ID P1 or P2 is required" but the code doesn't gate). +$markdownPhraseMap = [ordered]@{ + 'entra id p2 license' = 'EntraIDP2' + 'entra id p1 license' = 'EntraIDP1' + 'entra id p1 or p2' = 'EntraIDP1' + 'entra id p2' = 'EntraIDP2' + 'entra id p1' = 'EntraIDP1' + 'entra id governance' = 'EntraIDGovernance' + 'aad premium p2' = 'EntraIDP2' + 'aad premium p1' = 'EntraIDP1' + 'azure ad premium p2' = 'EntraIDP2' + 'azure ad premium p1' = 'EntraIDP1' + 'workload identities premium' = 'EntraWorkloadIDP1' +} + # Test ID regex (matches website/scripts/generate-test-docs.mjs:231) $idPattern = '(MT\.\d+|CIS\.[A-Za-z0-9.]+|CISA\.[A-Za-z0-9.]+|EIDSCA\.[A-Z0-9]+|ORCA\.\d+(?:\.\d+)?|AZDO\.\d+)' @@ -152,6 +177,35 @@ function Get-LicenseTokensFromText { return @($tokens | Sort-Object) } +function Get-MarkdownSignalsFromText { + [OutputType([string[]])] + param([Parameter(Mandatory)] [AllowEmptyString()] [string] $Text) + $tokens = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal) + if ([string]::IsNullOrWhiteSpace($Text)) { return @() } + + # Strip the front-matter and the auto-generated "Test Metadata" section so we + # only inspect prose. Those sections echo Pester Describe tags and would create + # circular signal (markdown reflecting the same tag the code-signal already saw). + $lines = $Text -split "`r?`n" + $body = [System.Collections.Generic.List[string]]::new() + $inFront = $false + $skipBlock = $false + foreach ($line in $lines) { + if ($line -match '^---\s*$') { $inFront = -not $inFront; continue } + if ($inFront) { continue } + if ($line -match '^## Test Metadata') { $skipBlock = $true; continue } + if ($skipBlock -and $line -match '^## ') { $skipBlock = $false } + if ($skipBlock) { continue } + $body.Add($line) + } + $prose = ($body -join "`n").ToLowerInvariant() + + foreach ($pair in $markdownPhraseMap.GetEnumerator()) { + if ($prose.Contains($pair.Key)) { [void] $tokens.Add($pair.Value) } + } + return @($tokens | Sort-Object) +} + function Get-FunctionFileForTest { param( [Parameter(Mandatory)] [string] $TestFunctionName @@ -164,6 +218,21 @@ function Get-FunctionFileForTest { return $null } +function Get-MarkdownFileForId { + param([Parameter(Mandatory)] [string] $Id) + if (-not (Test-Path $docsRoot)) { return $null } + $candidate = Get-ChildItem -Path $docsRoot -Recurse -Filter "$Id.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + # Parent fallback (e.g. MT.1033.0 -> MT.1033.md, if it ever existed). + $parent = $Id + while ($parent -match '\.\d+$') { + $parent = $parent -replace '\.\d+$', '' + $candidate = Get-ChildItem -Path $docsRoot -Recurse -Filter "$parent.md" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + } + return $null +} + # --- Pass 1: parse every test file into per-test scope text --- $testIndex = @{} # testId -> @{ ItText, EnclosingText, FunctionName } @@ -273,6 +342,8 @@ foreach ($setting in $config.TestSettings) { } } + $functionFile = $null + if ($entry) { # Signals 1-3 from the It block. foreach ($t in (Get-LicenseTokensFromText -Text $entry.ItText)) { [void] $tokens.Add($t) } @@ -290,6 +361,22 @@ foreach ($setting in $config.TestSettings) { } } + # Signal 7: explicit license-requirement statements in the website doc and + # in the function's companion .md (e.g. "Microsoft Entra ID P1 or P2 is required"). + # Catches license requirements that the test code doesn't gate on. + $markdownFile = Get-MarkdownFileForId -Id $id + if ($markdownFile) { + $mdContent = Get-Content -Path $markdownFile -Raw + foreach ($t in (Get-MarkdownSignalsFromText -Text $mdContent)) { [void] $tokens.Add($t) } + } + if ($functionFile) { + $companionMd = [System.IO.Path]::ChangeExtension($functionFile, '.md') + if (Test-Path $companionMd) { + $companionContent = Get-Content -Path $companionMd -Raw + foreach ($t in (Get-MarkdownSignalsFromText -Text $companionContent)) { [void] $tokens.Add($t) } + } + } + if ($tokens.Count -eq 0) { # Signal 6: suite default if ($id -like 'AZDO.*') { [void] $tokens.Add('AzureDevOps') } @@ -380,10 +467,21 @@ for ($i = 0; $i -lt $lines.Count; $i++) { if ($line -match "^$([regex]::Escape($pendingIndent))\}\,?\s*$") { # Insert the Licenses line just before the closing brace. $closingLine = $pendingObjectLines[$pendingObjectLines.Count - 1] - $bodyLines = $pendingObjectLines.GetRange(0, $pendingObjectLines.Count - 1) + $bodyLines = [System.Collections.Generic.List[string]]::new() + for ($k = 0; $k -lt $pendingObjectLines.Count - 1; $k++) { + $bodyLines.Add($pendingObjectLines[$k]) + } + + # Idempotency: if a Licenses field already exists in this object, drop it + # so we can rewrite cleanly. Anything else (Id/Severity/Title/etc) is left alone. + for ($k = $bodyLines.Count - 1; $k -ge 0; $k--) { + if ($bodyLines[$k] -match '^\s*"Licenses"\s*:') { $bodyLines.RemoveAt($k) } + } - # The previous body line currently has no trailing comma. We need to add one, - # then insert "Licenses": [...]. + # The previous body line may or may not have a trailing comma (depends on + # whether the object originally ended with Licenses or with another field). + # Normalize: ensure the last remaining body line has a trailing comma so we + # can append the new Licenses line. $lastBodyIdx = $bodyLines.Count - 1 $lastBody = $bodyLines[$lastBodyIdx] if ($lastBody -notmatch ',\s*$') {