diff --git a/src/powershell/tests/Test-Assessment.35012.md b/src/powershell/tests/Test-Assessment.35012.md new file mode 100644 index 000000000..1e8564305 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35012.md @@ -0,0 +1,16 @@ +Container labels extend sensitivity classification beyond individual files to entire collaboration workspaces including Microsoft Teams, Microsoft 365 Groups, and SharePoint sites. These labels control container-level settings such as external sharing policies, guest access permissions, device access restrictions, and privacy (public vs private). Without container labels, organizations cannot enforce consistent security policies at the workspace level, allowing users to create Teams with external guest access enabled even when handling confidential information. This gap creates data exfiltration risks where properly labeled documents exist within improperly secured collaboration spaces. Container labels ensure that workspace security posture matches the sensitivity of content stored within, preventing scenarios where "Highly Confidential" documents reside in Teams that permit external sharing. Organizations using Microsoft Teams or Microsoft 365 Groups for collaboration require container labels to maintain governance over workspace creation and access controls. + +**Remediation action** +- Navigate to Microsoft Purview portal → Information protection → Labels +- Create a new label or edit existing label +- Under "Define the scope for this label", enable "Groups & sites". +- Configure container protection settings: + - Privacy (Public/Private) + - External user access + - External sharing + - Unmanaged device access + - Conditional Access policy +- Publish label through label policy. +- [Use sensitivity labels with Microsoft Teams, Microsoft 365 Groups, and SharePoint sites](https://learn.microsoft.com/en-us/purview/sensitivity-labels-teams-groups-sites) + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35012.ps1 b/src/powershell/tests/Test-Assessment.35012.ps1 new file mode 100644 index 000000000..44a970302 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35012.ps1 @@ -0,0 +1,276 @@ +<# +.SYNOPSIS + Validates that container labels are configured for Teams, Groups, and Sites. + +.DESCRIPTION + This test evaluates sensitivity label configuration to ensure container labels + are enabled for Microsoft Teams, Microsoft 365 Groups, and SharePoint sites. + Container labels enforce consistent security policies at the workspace level, + controlling external sharing, guest access, and device restrictions. + +.NOTES + Test ID: 35012 + Category: Sensitivity Labels Configuration + Required APIs: Get-Label (Exchange PowerShell) +#> + +function Test-Assessment-35012 { + + [ZtTest( + Category = 'Sensitivity Labels Configuration', + ImplementationCost = 'Medium', + MinimumLicense = 'Microsoft_365_E5', + Pillar = 'Data', + RiskLevel = 'Medium', + SfiPillar = 'Protect tenants and production systems', + TenantType = 'Workforce', + TestId = 35012, + Title = 'Container labels are configured for Teams, Groups, and Sites', + UserImpact = 'High' + )] + [CmdletBinding()] + param() + + #region Helper Functions + + function Get-ContainerLabelSummary { + <# + .SYNOPSIS + Extracts container protection settings from a sensitivity label's LabelActions JSON. + .OUTPUTS + PSCustomObject with container protection details. + #> + param( + [object]$Label, + [object]$ProtectGroupAction, + [object]$ProtectSiteAction + ) + + # Extract content types from label + $contentType = if ($Label.ContentType) { $Label.ContentType -join ', ' } else { 'Not specified' } + + # Extract Group Privacy Setting from protectgroup action + $groupPrivacy = 'Not configured' + if ($ProtectGroupAction -and $ProtectGroupAction.Settings) { + $privacySetting = $ProtectGroupAction.Settings | Where-Object { $_.Key -eq 'privacy' } + if ($privacySetting) { + $groupPrivacy = switch ($privacySetting.Value) { + '1' { 'Public' } + '2' { 'Private' } + default { $privacySetting.Value } + } + } + } + + # Extract Site External Sharing from protectsite action + $siteExternalSharing = 'Not configured' + $siteGuestAccess = 'Not configured' + if ($ProtectSiteAction -and $ProtectSiteAction.Settings) { + # External sharing setting + $sharingSetting = $ProtectSiteAction.Settings | Where-Object { $_.Key -eq 'externalsharingcontrol' } + if ($sharingSetting) { + $siteExternalSharing = switch ($sharingSetting.Value) { + '0' { 'Full Access' } + '1' { 'Limited Access' } + '2' { 'Block Access' } + default { $sharingSetting.Value } + } + } + + # Guest access setting + $guestSetting = $ProtectSiteAction.Settings | Where-Object { $_.Key -eq 'allowaccesstoguestusers' } + if ($guestSetting) { + $siteGuestAccess = switch ($guestSetting.Value) { + 'true' { 'Allowed' } + 'false' { 'Blocked' } + default { $guestSetting.Value } + } + } + } + + return [PSCustomObject]@{ + LabelName = $Label.DisplayName + LabelId = $Label.Guid + ContentType = $contentType + GroupPrivacySetting = $groupPrivacy + SiteExternalSharing = $siteExternalSharing + SiteGuestAccess = $siteGuestAccess + } + } + + function Test-ContainerLabel { + <# + .SYNOPSIS + Tests if a label has both protectgroup and protectsite actions in LabelActions. + .OUTPUTS + Hashtable with IsContainer boolean and parsed actions, or $null if parsing fails. + #> + param([object]$Label) + + try { + if ([string]::IsNullOrWhiteSpace($Label.LabelActions)) { + return @{ IsContainer = $false; ProtectGroup = $null; ProtectSite = $null } + } + + $actions = $Label.LabelActions | ConvertFrom-Json -ErrorAction Stop + $protectGroup = $actions | Where-Object { $_.Type -eq 'protectgroup' } + $protectSite = $actions | Where-Object { $_.Type -eq 'protectsite' } + + return @{ + IsContainer = ($null -ne $protectGroup -and $null -ne $protectSite) + ProtectGroup = $protectGroup + ProtectSite = $protectSite + } + } + catch { + # Return null to indicate parsing failure + return $null + } + } + + #endregion Helper Functions + + #region Data Collection + + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + $activity = 'Evaluating container label configuration' + Write-ZtProgress -Activity $activity -Status 'Retrieving sensitivity labels' + + # Query Q1: Retrieve all sensitivity labels + $allLabels = $null + $containerLabels = @() + $queryError = $false + + try { + $allLabels = Get-Label -ErrorAction Stop + } + catch { + Write-PSFMessage -Level Warning -Message "Failed to retrieve sensitivity labels: $_" + $queryError = $true + } + + # Query Q2: Filter for container-enabled labels (both protectgroup and protectsite actions) + $parseError = $false + $containerLabelData = @() + + if ($null -ne $allLabels -and $allLabels.Count -gt 0) { + + Write-ZtProgress -Activity $activity -Status 'Filtering container-enabled labels' + + foreach ($label in $allLabels) { + $result = Test-ContainerLabel -Label $label + if ($null -eq $result) { + # JSON parsing failed for at least one label + $parseError = $true + } + elseif ($result.IsContainer) { + $containerLabelData += @{ + Label = $label + ProtectGroup = $result.ProtectGroup + ProtectSite = $result.ProtectSite + } + } + } + + $containerLabels = $containerLabelData + } + + #endregion Data Collection + + #region Assessment Logic + + # Initialize evaluation containers + $passed = $false + $customStatus = $null + $testResultMarkdown = '' + $labelResults = @() + + # Step 1: Check if query execution failed + if ($queryError) { + + $customStatus = 'Investigate' + $testResultMarkdown = + "⚠️ Query fails or LabelActions JSON cannot be parsed due to permissions issues or service connection failure. Ensure the Security & Compliance PowerShell module is connected and the account has appropriate permissions to retrieve label properties.`n`n%TestResult%" + + } + # Step 2: Check if LabelActions JSON parsing failed for any label + elseif ($parseError) { + + $customStatus = 'Investigate' + $testResultMarkdown = + "⚠️ Query fails or LabelActions JSON cannot be parsed due to permissions issues or service connection failure. Some labels could not be evaluated.`n`n%TestResult%" + + } + # Step 3: Check if container labels exist (count >= 1) - Pass + elseif ($containerLabels.Count -ge 1) { + + # Container labels are configured - Pass + $passed = $true + $testResultMarkdown = + "✅ Container labels are configured for Teams, Groups, and SharePoint sites.`n`n%TestResult%" + + # Build label results for reporting + foreach ($data in $containerLabels) { + $labelResults += Get-ContainerLabelSummary -Label $data.Label -ProtectGroupAction $data.ProtectGroup -ProtectSiteAction $data.ProtectSite + } + + } + # Step 4: Count = 0 - Fail + else { + + # No container labels configured + # Per spec: "Fail: No container labels are configured (acceptable if Teams/Groups not used; may be a gap if collaboration workspaces exist)" + $passed = $false + $testResultMarkdown = + "❌ No container labels are configured (acceptable if Teams/Groups not used; may be a gap if collaboration workspaces exist).`n`n%TestResult%" + + } + + #endregion Assessment Logic + + #region Report Generation + + $mdInfo = "`n## Summary`n`n" + $mdInfo += "| Metric | Value |`n|---|---|`n" + $mdInfo += "| Total sensitivity labels | $(if ($allLabels) { $allLabels.Count } else { 0 }) |`n" + $mdInfo += "| Container-protected labels | $($containerLabels.Count) |`n`n" + + if ($labelResults.Count -gt 0) { + $tableRows = "" + $formatTemplate = @' +## [Container label details](https://purview.microsoft.com/informationprotection/informationprotectionlabels/sensitivitylabels) + +| Label name | Content type | Group privacy setting | Site external sharing | Site guest access | +|---|---|---|---|---| +{0} + +'@ + foreach ($r in $labelResults) { + $labelLink = "https://purview.microsoft.com/informationprotection/informationprotectionlabels/sensitivitylabels" + $linkedLabelName = "[{0}]({1})" -f (Get-SafeMarkdown $r.LabelName), $labelLink + + $tableRows += "| $linkedLabelName | $($r.ContentType) | $($r.GroupPrivacySetting) | $($r.SiteExternalSharing) | $($r.SiteGuestAccess) |`n" + } + $mdInfo += $formatTemplate -f $tableRows + } + + # Replace the placeholder with detailed information + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + #endregion Report Generation + + $params = @{ + TestId = '35012' + Title = 'Container labels are configured for Teams, Groups, and Sites' + Status = $passed + Result = $testResultMarkdown + } + + # Add CustomStatus if status is 'Investigate' + if ($null -ne $customStatus) { + $params.CustomStatus = $customStatus + } + + # Add test result details + Add-ZtTestResultDetail @params +} diff --git a/src/powershell/tests/Test-Assessment.35035.md b/src/powershell/tests/Test-Assessment.35035.md new file mode 100644 index 000000000..06ef8ceab --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35035.md @@ -0,0 +1,46 @@ +Named Entity Sensitive Information Types (SITs) are pre-built, Microsoft-managed classifiers designed to detect common sensitive entities like people's names, physical addresses, and medical terminology. Unlike custom SITs that organizations create for specific business needs, Named Entity SITs are provided by Microsoft and enable organizations to implement sophisticated data protection without requiring custom development. By configuring Named Entity SITs in auto-labeling policies and DLP rules, organizations can automatically classify and protect content containing sensitive personal information, employee data, and domain-specific terminology. This transforms data protection from purely technical pattern-matching (like detecting credit card numbers or social security numbers) into intelligent, semantically-aware classification systems that understand context. Organizations handling content with sensitive entity information—executive communications, customer data, medical records, or other high-sensitivity content—should deploy at least one Named Entity SIT in their protection policies. Demonstrating Named Entity SIT deployment shows sophisticated, context-aware information protection beyond basic generic SIT detection. + +**Remediation action** + +To deploy Named Entity SITs in your policies: + +**Option 1: Deploy via DLP Policy** +1. Sign in as Global Administrator or Compliance Administrator to the [Microsoft Purview portal](https://purview.microsoft.com) +2. Navigate to [DLP Policies](https://purview.microsoft.com/datalossprevention/policies) +3. Create a new DLP policy or edit an existing one +4. Add a rule with condition: "Content contains sensitive information" +5. Select Named Entity SITs from the dropdown: + - **All Full Names** - Detects common and uncommon full names worldwide + - **All Physical Addresses** - Detects addresses in various formats + - **All Medical Terms and Conditions** - Detects medical terminology and conditions + - **Country/Region-Specific Variants** - e.g., "Austria Physical Addresses", "Canada Physical Addresses" +6. Configure the action (notify user, restrict access, send alert, etc.) +7. Specify the workload scope (Exchange, SharePoint, OneDrive, Teams, Power BI) +8. Enable and deploy the policy + +**Option 2: Deploy via Auto-Labeling Policy** +1. Navigate to [Auto-Labeling Policies](https://purview.microsoft.com/informationprotection/autolabeling) +2. Create a new auto-labeling policy or edit an existing one +3. In the rule configuration, add a condition: "Content contains sensitive information" +4. From the sensitive information types list, select Named Entity SITs (e.g., "All Full Names") +5. Configure the sensitivity label to apply when content matches +6. Set the policy scope (Exchange, SharePoint, OneDrive, Teams, Power BI, or All) +7. Enable and deploy the policy + +**View Available Named Entity SITs:** +- Navigate to [Sensitive Information Types](https://purview.microsoft.com/informationprotection/dataclassification/multicloudsensitiveinfotypes) +- Named Entity SITs have `Classifier: EntityMatch` in their properties + +**Query via PowerShell:** +```powershell +Connect-IPPSSession +Get-DlpSensitiveInformationType | Where-Object { $_.Classifier -eq "EntityMatch" } | Select-Object Name, Classifier, Capability +``` + +**Example Scenarios:** +- **Protect Executive Communications**: Auto-label emails containing "All Full Names" with "Executive Communications" label +- **Protect Healthcare Records**: DLP rule blocking external sharing of content with "All Medical Terms and Conditions" +- **Address Data Protection**: DLP rule restricting content with "All Physical Addresses" to internal sharing only + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.35035.ps1 b/src/powershell/tests/Test-Assessment.35035.ps1 new file mode 100644 index 000000000..874435d10 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.35035.ps1 @@ -0,0 +1,380 @@ +<# +.SYNOPSIS + Validates that Named Entity SITs are used in auto-labeling and DLP policies. + +.DESCRIPTION + This test evaluates whether the organization has deployed Named Entity Sensitive + Information Types (SITs) in auto-labeling policies or DLP rules. Named Entity SITs + are pre-built, Microsoft-managed classifiers designed to detect common sensitive + entities like people's names, physical addresses, and medical terminology. + +.NOTES + Test ID: 35035 + Category: Advanced Classification + Pillar: Data + Risk Level: High +#> + +function Test-Assessment-35035 { + [ZtTest( + Category = 'Advanced Classification', + ImplementationCost = 'Low', + MinimumLicense = ('Microsoft 365 E3'), + Pillar = 'Data', + RiskLevel = 'High', + SfiPillar = 'Protect tenants and production systems', + TenantType = ('Workforce'), + TestId = 35035, + Title = 'Named Entity SITs usage in Auto-Labeling and DLP policies', + UserImpact = 'Medium' + )] + [CmdletBinding()] + param() + + #region Helper Functions + + function Get-NamedEntitySitsFromRule { + <# + .SYNOPSIS + Extracts Named Entity SITs from an AdvancedRule JSON property using ID-based matching. + .DESCRIPTION + Parses the AdvancedRule JSON and checks SIT IDs against the Named Entity SIT catalog + (Classifier -eq "EntityMatch"). This approach is future-proof as new Named Entity SITs + are automatically detected. + .OUTPUTS + Array of PSCustomObjects with Name and Id of Named Entity SITs found in the rule. + #> + param( + [Parameter(Mandatory = $false)] + [AllowNull()] + [AllowEmptyString()] + [string]$AdvancedRuleJson, + + [Parameter(Mandatory = $true)] + [AllowEmptyCollection()] + [array]$NamedEntitySitIds, + + [Parameter(Mandatory = $false)] + [string]$RuleName = 'Unknown', + + [Parameter(Mandatory = $false)] + [ValidateSet('AutoLabeling', 'DLP')] + [string]$RuleType = 'AutoLabeling' + ) + + $namedEntitySits = @() + + if ([string]::IsNullOrWhiteSpace($AdvancedRuleJson)) { + return $namedEntitySits + } + + if ($NamedEntitySitIds.Count -eq 0) { + return $namedEntitySits + } + + try { + $advancedRule = $AdvancedRuleJson | ConvertFrom-Json -ErrorAction Stop + + # Navigate to SubConditions + $subConditions = $advancedRule.Condition.SubConditions + if (-not $subConditions) { + return $namedEntitySits + } + + foreach ($subCondition in $subConditions) { + # Only process ContentContainsSensitiveInformation conditions + if ($subCondition.ConditionName -ne 'ContentContainsSensitiveInformation') { + continue + } + + $values = $subCondition.Value + if (-not $values) { + continue + } + + if ($RuleType -eq 'AutoLabeling') { + # Auto-labeling: Grouped structure - Value[].Groups[].Sensitivetypes[] + foreach ($value in $values) { + if ($value.Groups) { + foreach ($group in $value.Groups) { + if ($group.Sensitivetypes) { + foreach ($sit in $group.Sensitivetypes) { + if ($sit.id -and $sit.id -in $NamedEntitySitIds) { + $namedEntitySits += [PSCustomObject]@{ + Name = $sit.name + Id = $sit.id + } + } + } + } + } + } + } + } + else { + # DLP: Nested structure - Value[0].groups[].sensitivetypes[] + if ($values -and $values[0].groups) { + foreach ($group in $values[0].groups) { + if ($group.sensitivetypes) { + foreach ($sit in $group.sensitivetypes) { + if ($sit.id -and $sit.id -in $NamedEntitySitIds) { + $namedEntitySits += [PSCustomObject]@{ + Name = $sit.name + Id = $sit.id + } + } + } + } + } + } + } + } + } + catch { + Write-PSFMessage "Error parsing AdvancedRule JSON for rule '$RuleName': $_" -Level Warning + throw + } + + # Return unique SITs by Id + return $namedEntitySits | Sort-Object -Property Id -Unique + } + + #endregion Helper Functions + + #region Data Collection + + Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose + $activity = 'Evaluating Named Entity SIT usage in policies' + Write-ZtProgress -Activity $activity -Status 'Building Named Entity SIT catalog lookup' + + $namedEntitySitIds = @() + $autoLabelRules = @() + $dlpRules = @() + $queryError = $null + $catalogError = $null + + # Build lookup of Named Entity SIT IDs from catalog (Classifier -eq "EntityMatch") + try { + $namedEntitySits = Get-DlpSensitiveInformationType -ErrorAction Stop | Where-Object { $_.Classifier -eq 'EntityMatch' } + $namedEntitySitIds = @($namedEntitySits.Id) + Write-PSFMessage "Built Named Entity SIT catalog with $($namedEntitySitIds.Count) SITs" -Level Verbose + } + catch { + Write-PSFMessage "Error building Named Entity SIT catalog: $_" -Level Warning + $catalogError = $_ + } + + # Q1: Get all auto-sensitivity label rules + Write-ZtProgress -Activity $activity -Status 'Retrieving auto-labeling rules' + try { + $autoLabelRules = Get-AutoSensitivityLabelRule -ErrorAction Stop + Write-PSFMessage "Retrieved $($autoLabelRules.Count) auto-labeling rules" -Level Verbose + } + catch { + Write-PSFMessage "Error retrieving auto-labeling rules: $_" -Level Warning + $queryError = $_ + } + + # Q2: Get all DLP compliance rules + Write-ZtProgress -Activity $activity -Status 'Retrieving DLP compliance rules' + try { + $dlpRules = Get-DlpComplianceRule -ErrorAction Stop + Write-PSFMessage "Retrieved $($dlpRules.Count) DLP rules" -Level Verbose + } + catch { + Write-PSFMessage "Error retrieving DLP rules: $_" -Level Warning + if (-not $queryError) { + $queryError = $_ + } + } + + #endregion Data Collection + + #region Assessment Logic + + $passed = $false + $customStatus = $null + $testResultMarkdown = '' + + $autoLabelRulesWithNamedEntity = @() + $dlpRulesWithNamedEntity = @() + $parseErrors = @() + + # Check if catalog lookup failed + if ($catalogError) { + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Unable to determine Named Entity SIT usage. Failed to build SIT catalog lookup: $catalogError`n`n%TestResult%" + } + # Check if both queries failed + elseif ($queryError -and $autoLabelRules.Count -eq 0 -and $dlpRules.Count -eq 0) { + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Unable to determine Named Entity SIT usage due to query error: $queryError`n`n%TestResult%" + } + # Check if catalog is empty (no Named Entity SITs found - unusual) + elseif ($namedEntitySitIds.Count -eq 0) { + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Unable to determine Named Entity SIT usage. No Named Entity SITs found in the SIT catalog (Classifier = 'EntityMatch'). This is unexpected - please verify tenant access.`n`n%TestResult%" + } + else { + # Process auto-labeling rules + Write-ZtProgress -Activity $activity -Status 'Analyzing auto-labeling rules for Named Entity SITs' + foreach ($rule in $autoLabelRules) { + try { + $foundSits = Get-NamedEntitySitsFromRule -AdvancedRuleJson $rule.AdvancedRule -NamedEntitySitIds $namedEntitySitIds -RuleName $rule.Name -RuleType 'AutoLabeling' + + if ($foundSits.Count -gt 0) { + $autoLabelRulesWithNamedEntity += [PSCustomObject]@{ + RuleName = $rule.Name + PolicyName = $rule.ParentPolicyName + NamedEntitySits = ($foundSits | ForEach-Object { $_.Name }) -join ', ' + SitIds = ($foundSits | ForEach-Object { $_.Id }) -join ', ' + Workload = $rule.Workload + CreatedDate = $rule.WhenCreatedUTC + RuleType = 'Auto-Labeling' + Count = $foundSits.Count + } + } + } + catch { + $parseErrors += [PSCustomObject]@{ + RuleName = $rule.Name + RuleType = 'Auto-Labeling' + Error = $_.Exception.Message + } + } + } + + # Process DLP rules + Write-ZtProgress -Activity $activity -Status 'Analyzing DLP rules for Named Entity SITs' + foreach ($rule in $dlpRules) { + try { + $foundSits = Get-NamedEntitySitsFromRule -AdvancedRuleJson $rule.AdvancedRule -NamedEntitySitIds $namedEntitySitIds -RuleName $rule.Name -RuleType 'DLP' + + if ($foundSits.Count -gt 0) { + $dlpRulesWithNamedEntity += [PSCustomObject]@{ + RuleName = $rule.Name + PolicyName = $rule.ParentPolicyName + NamedEntitySits = ($foundSits | ForEach-Object { $_.Name }) -join ', ' + SitIds = ($foundSits | ForEach-Object { $_.Id }) -join ', ' + Workload = $rule.Workload + CreatedDate = $rule.WhenCreatedUTC + RuleType = 'DLP' + Count = $foundSits.Count + } + } + } + catch { + $parseErrors += [PSCustomObject]@{ + RuleName = $rule.Name + RuleType = 'DLP' + Error = $_.Exception.Message + } + } + } + + # Determine pass/fail status + $totalRulesWithNamedEntity = $autoLabelRulesWithNamedEntity.Count + $dlpRulesWithNamedEntity.Count + + if ($totalRulesWithNamedEntity -gt 0) { + $passed = $true + $testResultMarkdown = "✅ At least one auto-labeling or DLP policy rule uses a Named Entity SIT (such as 'All Full Names', 'All Physical Addresses', 'All Medical Terms and Conditions', or similar pre-built classifiers).`n`n%TestResult%" + } + else { + $passed = $false + + if ($autoLabelRules.Count -eq 0 -and $dlpRules.Count -eq 0) { + $testResultMarkdown = "❌ No auto-labeling or DLP rules were found in your tenant.`n`n%TestResult%" + } + else { + $testResultMarkdown = "❌ No auto-labeling or DLP policy rules contain any Named Entity SITs. All policies use only standard SITs (credit card numbers, social security numbers, etc.) or are not configured.`n`n%TestResult%" + } + } + + # Check for excessive parse errors which might indicate Investigate status + if ($parseErrors.Count -gt 0 -and $totalRulesWithNamedEntity -eq 0) { + $totalRules = $autoLabelRules.Count + $dlpRules.Count + if ($parseErrors.Count -eq $totalRules -and $totalRules -gt 0) { + $customStatus = 'Investigate' + $testResultMarkdown = "⚠️ Unable to determine Named Entity SIT usage due to JSON parsing errors in all rules.`n`n%TestResult%" + } + } + } + + #endregion Assessment Logic + + #region Report Generation + + $mdInfo = '' + + # Combine all rules with Named Entity SITs for display + $allRulesWithNamedEntity = @() + $allRulesWithNamedEntity += $autoLabelRulesWithNamedEntity + $allRulesWithNamedEntity += $dlpRulesWithNamedEntity + + if ($allRulesWithNamedEntity.Count -gt 0) { + $mdInfo += "`n`n### [Rules using named entity SITs](https://purview.microsoft.com/informationprotection/dataclassification/multicloudsensitiveinfotypes)`n" + $mdInfo += "| Rule name | Policy name | Named Entity SITs | Count | Workload | Type |`n" + $mdInfo += "| :--- | :--- | :--- | :--- | :--- | :--- |`n" + + foreach ($rule in $allRulesWithNamedEntity) { + $ruleName = Get-SafeMarkdown -Text $rule.RuleName + $safePolicyName = Get-SafeMarkdown -Text $rule.PolicyName + $sits = Get-SafeMarkdown -Text $rule.NamedEntitySits + $workload = Get-SafeMarkdown -Text ($rule.Workload -join ', ') + + # Build policy URL based on rule type + if ($rule.RuleType -eq 'Auto-Labeling') { + $policyUrl = 'https://purview.microsoft.com/informationprotection/autolabeling' + } + else { + $policyUrl = 'https://purview.microsoft.com/datalossprevention/policies' + } + $policyLink = "[$safePolicyName]($policyUrl)" + + $mdInfo += "| $ruleName | $policyLink | $sits | $($rule.Count) | $workload | $($rule.RuleType) |`n" + } + } + + # Summary section + $mdInfo += "`n`n### Summary`n" + $mdInfo += "| Metric | Count |`n" + $mdInfo += "| :--- | :--- |`n" + $mdInfo += "| Named entity SITs in catalog | $($namedEntitySitIds.Count) |`n" + $mdInfo += "| Total auto-labeling rules | $($autoLabelRules.Count) |`n" + $mdInfo += "| Total DLP rules | $($dlpRules.Count) |`n" + $mdInfo += "| Auto-labeling rules using named entity SITs | $($autoLabelRulesWithNamedEntity.Count) |`n" + $mdInfo += "| DLP rules using named entity SITs | $($dlpRulesWithNamedEntity.Count) |" + + # Report parsing errors if any occurred + if ($parseErrors.Count -gt 0) { + $mdInfo += "`n`n### ⚠️ Parsing Errors`n" + $mdInfo += "The following rules could not be fully parsed:`n`n" + $mdInfo += "| Rule name | Type | Error |`n" + $mdInfo += "| :--- | :--- | :--- |`n" + foreach ($parseError in $parseErrors) { + $ruleName = Get-SafeMarkdown -Text $parseError.RuleName + $errorMsg = Get-SafeMarkdown -Text $parseError.Error + $mdInfo += "| $ruleName | $($parseError.RuleType) | $errorMsg |`n" + } + $mdInfo += "`n**Note**: These rules were excluded from the named entity SIT analysis.`n" + } + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + #endregion Report Generation + + $params = @{ + TestId = '35035' + Title = 'Named Entity SITs Usage in Auto-Labeling and DLP Policies' + Status = $passed + Result = $testResultMarkdown + } + + # Add CustomStatus if status is 'Investigate' + if ($null -ne $customStatus) { + $params.CustomStatus = $customStatus + } + + # Add test result details + Add-ZtTestResultDetail @params +}