tests/Purview.Tests.ps1
|
#Requires -Module Pester # # Tests for the Get-MsecPurview* commands. # # The traps these pin are all of one family - a value that means "no answer" must never render # as a measurement, and a setting that exists must never be read off a property that does not: # # Get-Label HAS NO EncryptionEnabled PROPERTY. Asking for one returns empty on every label, # which reads as "nothing encrypts anything". The real settings are JSON in LabelActions. # # THE disabled FLAG IS THE STRING 'true'/'false', and [bool]'false' is $true in PowerShell. # A truthiness test marks every configured action as switched off. # # 'All' IS AN ORDINARY MEMBER of a DLP location list, not a flag, so an estate-wide policy and # a single site called All are indistinguishable without looking. # # A POLICY'S Mode IS NOT ITS Enabled FLAG. Only 'Enable' stops anything. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'msec.psm1') -Force -ErrorAction Stop # The Security & Compliance cmdlets do not exist without a live session, so they are stubbed # globally here purely so Pester has something to mock. function global:Get-ConnectionInformation { } function global:Get-DlpCompliancePolicy { } function global:Get-DlpComplianceRule { } function global:Get-Label { } function global:Get-LabelPolicy { } function global:Get-ComplianceTag { } function global:Get-RetentionCompliancePolicy { } function global:Disconnect-ExchangeOnline { param($ConnectionId, $Confirm) } function global:Get-AutoSensitivityLabelPolicy { } function global:Get-AutoSensitivityLabelRule { } function global:Get-InformationBarrierPolicy { } function global:Get-ProtectionAlert { } function global:New-Connected { param([string] $TenantId = 't', [string] $AppId = 'c') # Regional prefix on purpose: the real URI is eur01b.ps.compliance... [PSCustomObject]@{ ConnectionUri = 'https://eur01b.ps.compliance.protection.outlook.com' TenantID = $TenantId; AppId = $AppId; ConnectionId = 'conn-1' UserPrincipalName = 'OAuthUser@contoso.com' } } } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue foreach ($f in 'Get-ConnectionInformation','Get-DlpCompliancePolicy','Get-DlpComplianceRule', 'Get-Label','Get-LabelPolicy','Get-ComplianceTag','Get-RetentionCompliancePolicy','New-Connected','Disconnect-ExchangeOnline','Get-AutoSensitivityLabelPolicy','Get-AutoSensitivityLabelRule','Get-InformationBarrierPolicy','Get-ProtectionAlert') { Remove-Item "function:global:$f" -ErrorAction SilentlyContinue } } Describe 'Initialize-MsecExoSession' { It 'connects by itself when no compliance session is open' { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { @() } Mock Connect-MsecPurview -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy Should -Invoke Connect-MsecPurview -Times 1 -Scope It } } It 'does NOT reconnect when a session is already open' { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Connect-MsecPurview -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy # The handshake costs seconds and imports hundreds of cmdlets - paying it per call # would be worse than the manual connect this replaced. Should -Invoke Connect-MsecPurview -Times 0 -Scope It } } It 'is not fooled by an Exchange Online connection into skipping the connect' { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { [PSCustomObject]@{ ConnectionUri = 'https://outlook.office365.com/PowerShell-LiveId' } } Mock Connect-MsecPurview -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy Should -Invoke Connect-MsecPurview -Times 1 -Scope It } } It 'points at Connect-Msec, not Connect-MsecPurview, when there is no app session to build on' { InModuleScope msec { $script:MsecSession = $null Mock Get-ConnectionInformation -MockWith { @() } { Get-MsecPurviewDlpPolicy } | Should -Throw '*Connect-Msec first*' } } } Describe 'Get-MsecPurviewDlpPolicy' { It 'reports Mode separately from IsEnforcing' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @( [PSCustomObject]@{ Name = 'Enforcing'; Mode = 'Enable'; Enabled = $true } [PSCustomObject]@{ Name = 'Testing'; Mode = 'TestWithNotifications'; Enabled = $true } [PSCustomObject]@{ Name = 'Off'; Mode = 'Disable'; Enabled = $false } ) } @(Get-MsecPurviewDlpPolicy) } ($rows | Where-Object Name -eq 'Enforcing').IsEnforcing | Should -BeTrue # Enabled AND reporting, but it stops nothing - the distinction the whole column exists for. ($rows | Where-Object Name -eq 'Testing').Enabled | Should -BeTrue ($rows | Where-Object Name -eq 'Testing').IsEnforcing | Should -BeFalse ($rows | Where-Object Name -eq 'Off').IsEnforcing | Should -BeFalse } It 'tells an estate-wide location apart from a site called All' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Mode = 'Enable'; Enabled = $true SharePointLocation = @([PSCustomObject]@{ DisplayName = 'All' }) OneDriveLocation = @([PSCustomObject]@{ DisplayName = 'Site A' }, [PSCustomObject]@{ DisplayName = 'Site B' }) ExchangeLocation = @() }) } @(Get-MsecPurviewDlpPolicy) } $rows[0].SharePointScope | Should -Be 'All' # Count is 0 for All: there is no list to count, and reading it as coverage would be wrong. $rows[0].SharePointCount | Should -Be 0 $rows[0].OneDriveScope | Should -Be 'Named' $rows[0].OneDriveCount | Should -Be 2 $rows[0].ExchangeScope | Should -Be 'None' } It 'nulls the rule columns when the rules cannot be read, rather than reporting zero' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Mode = 'Enable'; Enabled = $true }) } Mock Get-DlpComplianceRule -MockWith { throw 'access denied' } @(Get-MsecPurviewDlpPolicy -WarningAction SilentlyContinue) } # "no blocking rule" and "could not tell" must not look alike on a control question. $rows[0].RuleCount | Should -BeNullOrEmpty $rows[0].BlockingRuleCount | Should -BeNullOrEmpty } It 'counts blocking rules and takes the highest severity' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Mode = 'Enable'; Enabled = $true }) } Mock Get-DlpComplianceRule -MockWith { @( [PSCustomObject]@{ Name = 'r1'; ParentPolicyName = 'P'; BlockAccess = $true; ReportSeverityLevel = 'Low' } [PSCustomObject]@{ Name = 'r2'; ParentPolicyName = 'P'; BlockAccess = $false; ReportSeverityLevel = 'High' } ) } @(Get-MsecPurviewDlpPolicy) } $rows[0].RuleCount | Should -Be 2 $rows[0].BlockingRuleCount | Should -Be 1 $rows[0].MaxRuleSeverity | Should -Be 'High' } } Describe 'Get-MsecPurviewSensitivityLabel' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-LabelPolicy -MockWith { @([PSCustomObject]@{ Name = 'Information Classification'; Labels = @('Confidential') }) } } } It 'reads protection out of LabelActions, which is the only place it exists' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-LabelPolicy -MockWith { @() } Mock Get-Label -MockWith { @([PSCustomObject]@{ Name = 'Confidential'; DisplayName = 'Confidential'; Priority = 2; Disabled = $false LabelActions = @('{"Type":"encrypt","Settings":[{"Key":"protectiontype","Value":"userdefined"},{"Key":"disabled","Value":"false"}]}') }) } @(Get-MsecPurviewSensitivityLabel) } $rows[0].EncryptionConfigured | Should -BeTrue $rows[0].EncryptionEnabled | Should -BeTrue $rows[0].EncryptionType | Should -Be 'userdefined' $rows[0].ActionTypes | Should -Contain 'encrypt' } It "treats the STRING 'true' as disabled - [bool]'false' is truthy and would invert this" { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-LabelPolicy -MockWith { @() } Mock Get-Label -MockWith { @( [PSCustomObject]@{ Name = 'On'; DisplayName = 'On'; Priority = 0; Disabled = $false LabelActions = @('{"Type":"encrypt","Settings":[{"Key":"disabled","Value":"false"}]}') } [PSCustomObject]@{ Name = 'Off'; DisplayName = 'Off'; Priority = 1; Disabled = $false LabelActions = @('{"Type":"encrypt","Settings":[{"Key":"disabled","Value":"true"}]}') } ) } @(Get-MsecPurviewSensitivityLabel) } # Both have encryption CONFIGURED; only one has it on. ($rows | Where-Object DisplayName -eq 'On').EncryptionConfigured | Should -BeTrue ($rows | Where-Object DisplayName -eq 'On').EncryptionEnabled | Should -BeTrue ($rows | Where-Object DisplayName -eq 'Off').EncryptionConfigured | Should -BeTrue ($rows | Where-Object DisplayName -eq 'Off').EncryptionEnabled | Should -BeFalse } It 'marks a label no policy publishes as unpublished' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-LabelPolicy -MockWith { @([PSCustomObject]@{ Name = 'Information Classification'; Labels = @('Confidential') }) } Mock Get-Label -MockWith { @( [PSCustomObject]@{ Name = 'Confidential'; DisplayName = 'Confidential'; Priority = 0; Disabled = $false; LabelActions = @() } [PSCustomObject]@{ Name = 'Orphan'; DisplayName = 'Orphan'; Priority = 1; Disabled = $false; LabelActions = @() } ) } @(Get-MsecPurviewSensitivityLabel) } ($rows | Where-Object DisplayName -eq 'Confidential').IsPublished | Should -BeTrue ($rows | Where-Object DisplayName -eq 'Confidential').PublishedBy | Should -Contain 'Information Classification' ($rows | Where-Object DisplayName -eq 'Orphan').IsPublished | Should -BeFalse } It 'nulls IsPublished when the label policies could not be read' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-LabelPolicy -MockWith { throw 'denied' } Mock Get-Label -MockWith { @([PSCustomObject]@{ Name = 'L'; DisplayName = 'L'; Priority = 0; Disabled = $false; LabelActions = @() }) } @(Get-MsecPurviewSensitivityLabel -WarningAction SilentlyContinue) } # Not $false, which would claim we checked and it reaches nobody. $rows[0].IsPublished | Should -BeNullOrEmpty $rows[0].PublishedBy | Should -BeNullOrEmpty } } Describe 'Get-MsecPurviewRetention' { It 'reports an unpublished label as not in force' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-RetentionCompliancePolicy -MockWith { @() } Mock Get-ComplianceTag -MockWith { @([PSCustomObject]@{ Name = 'Retain indefinitely'; RetentionAction = 'Keep'; RetentionDuration = 'Unlimited' IsRecordLabel = $false; Published = $false }) } @(Get-MsecPurviewRetention) } $rows[0].Kind | Should -Be 'Label' $rows[0].IsInForce | Should -BeFalse } It 'returns labels and policies in one stream, tagged by Kind' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-ComplianceTag -MockWith { @([PSCustomObject]@{ Name = 'L'; Published = $true }) } Mock Get-RetentionCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Enabled = $true }) } @(Get-MsecPurviewRetention) } @($rows | Where-Object Kind -eq 'Label').Count | Should -Be 1 @($rows | Where-Object Kind -eq 'Policy').Count | Should -Be 1 } It 'takes a side with -Kind' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-ComplianceTag -MockWith { @([PSCustomObject]@{ Name = 'L'; Published = $true }) } Mock Get-RetentionCompliancePolicy -MockWith { throw 'should not be called' } @(Get-MsecPurviewRetention -Kind Label) } $rows.Count | Should -Be 1 } It 'returns nothing, without throwing, when no retention is configured' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-ComplianceTag -MockWith { @() } Mock Get-RetentionCompliancePolicy -MockWith { @() } @(Get-MsecPurviewRetention) } # An empty result is a real answer here, and a report must say so rather than omit it. $rows.Count | Should -Be 0 } } Describe 'Session handling is consistent across the workloads that need one' { It 'reconnects when the session belongs to a DIFFERENT tenant' { # A workload session outlives the Connect-Msec that prompted it. Matching only on "is # something connected" reuses the previous tenant's session and reports its data under # the new tenant's name, which looks like nothing at all. InModuleScope msec { $script:MsecSession = @{ TenantId = 'tenant-B'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected -TenantId 'tenant-A' } Mock Disconnect-ExchangeOnline -MockWith { } Mock Connect-MsecPurview -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy -WarningAction SilentlyContinue Should -Invoke Connect-MsecPurview -Times 1 -Scope It # And the stale one is CLOSED, not left alongside - two live sessions would make the # wrong-tenant read intermittent instead of consistent. Should -Invoke Disconnect-ExchangeOnline -Times 1 -Scope It } } It 'says which tenant it closed and why' { $warnings = InModuleScope msec { $script:MsecSession = @{ TenantId = 'tenant-B'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected -TenantId 'tenant-A' } Mock Disconnect-ExchangeOnline -MockWith { } Mock Connect-MsecPurview -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy -WarningVariable w -WarningAction SilentlyContinue @($w) } ($warnings -join ' ') | Should -Match 'tenant-A' ($warnings -join ' ') | Should -Match 'tenant-B' } It 'does NOT reconnect a session that merely signed in as someone else on the right tenant' { # Identity is the caller''s business; reconnecting as the app would quietly remove rights # they deliberately signed in to use. Tenant is the correctness question, not identity. InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'msec-app'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected -TenantId 't' -AppId 'some-other-app' } Mock Connect-MsecPurview -MockWith { } Mock Disconnect-ExchangeOnline -MockWith { } Mock Get-DlpCompliancePolicy -MockWith { @() } Mock Get-DlpComplianceRule -MockWith { @() } $null = Get-MsecPurviewDlpPolicy Should -Invoke Connect-MsecPurview -Times 0 -Scope It Should -Invoke Disconnect-ExchangeOnline -Times 0 -Scope It } } It 'tells an Exchange session apart from a Compliance one, in both directions' { # One module, two services, distinguishable only by URI. A loose match lets either # satisfy a requirement for the other, and the failure then lands later as a # missing-cmdlet error naming nothing. InModuleScope msec { Mock Get-ConnectionInformation -MockWith { [PSCustomObject]@{ ConnectionUri = 'https://outlook.office365.com/PowerShell-LiveId' } } @(Get-MsecExoConnection -Endpoint Exchange).Count | Should -Be 1 @(Get-MsecExoConnection -Endpoint Compliance).Count | Should -Be 0 Mock Get-ConnectionInformation -MockWith { New-Connected } @(Get-MsecExoConnection -Endpoint Exchange).Count | Should -Be 0 @(Get-MsecExoConnection -Endpoint Compliance).Count | Should -Be 1 } } It 'reports not-connected rather than throwing when Get-ConnectionInformation is unavailable' { InModuleScope msec { Mock Get-ConnectionInformation -MockWith { throw 'module not loaded' } @(Get-MsecExoConnection -Endpoint Exchange).Count | Should -Be 0 } } It 'leaves no workload demanding a manual connect step' { # The house rule, pinned: every command needing a workload session opens one itself. # Get-MsecTeamsPolicy and Get-MsecSharePointSiteUser already did; Exchange and Purview # were brought into line. $offenders = foreach ($name in 'Get-MsecExchangeMailboxPermission', 'Get-MsecTeamsPolicy', 'Get-MsecPurviewDlpPolicy', 'Get-MsecPurviewSensitivityLabel', 'Get-MsecPurviewRetention') { $body = (Get-Command $name).Definition if ($body -notmatch 'Initialize-MsecExoSession|Connect-Msec\w+') { $name } } $offenders | Should -BeNullOrEmpty } } Describe 'A tenant without the feature' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } } } It 'says the tenant cannot be asked, rather than returning no policies' { # The compliance endpoint imports only the cmdlets a tenant is licensed for, so on a # tenant without DLP the cmdlet is simply absent. Returning an empty result would claim # "no DLP policies are configured" - the opposite conclusion on a compliance report. InModuleScope msec { Mock Get-Command -ParameterFilter { $Name -eq 'Get-DlpCompliancePolicy' } -MockWith { $null } { Get-MsecPurviewDlpPolicy } | Should -Throw '*not measurable from here*' } } It 'names the cmdlet and says it is a capability limit, not a permission problem' { InModuleScope msec { Mock Get-Command -ParameterFilter { $Name -eq 'Get-Label' } -MockWith { $null } $message = $null try { Get-MsecPurviewSensitivityLabel } catch { $message = $_.Exception.Message } $message | Should -Match 'Get-Label' $message | Should -Match 'sensitivity labels' $message | Should -Match 'ROLE GROUPS' # Sending someone to grant an API permission would waste their time - it cannot help. $message | Should -Match 'not an API permission' } } It 'points at the half that still works when only one retention cmdlet is missing' { InModuleScope msec { Mock Get-Command -ParameterFilter { $Name -eq 'Get-RetentionCompliancePolicy' } -MockWith { $null } { Get-MsecPurviewRetention } | Should -Throw '*-Kind Label*' } } It 'still answers -Kind Label when only the policy cmdlet is missing' { $rows = InModuleScope msec { Mock Get-Command -ParameterFilter { $Name -eq 'Get-RetentionCompliancePolicy' } -MockWith { $null } Mock Get-ComplianceTag -MockWith { @([PSCustomObject]@{ Name = 'L'; Published = $true }) } @(Get-MsecPurviewRetention -Kind Label) } # The guard is per half and only fires for the half being read. $rows.Count | Should -Be 1 } } Describe 'Get-MsecPurviewAutoLabelingPolicy' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } } } It 'returns nothing, without throwing, when no auto-labeling exists' { # Zero here is a FINDING - labels are being applied by users only - so the command must # answer cleanly rather than error, and a report must print the section. $rows = InModuleScope msec { Mock Get-AutoSensitivityLabelPolicy -MockWith { @() } Mock Get-AutoSensitivityLabelRule -MockWith { @() } @(Get-MsecPurviewAutoLabelingPolicy) } $rows.Count | Should -Be 0 } It 'separates simulation from enforcement' { $rows = InModuleScope msec { Mock Get-AutoSensitivityLabelRule -MockWith { @() } Mock Get-AutoSensitivityLabelPolicy -MockWith { @( [PSCustomObject]@{ Name = 'Live'; Mode = 'Enable'; Enabled = $true; ApplySensitivityLabel = 'Confidential' } [PSCustomObject]@{ Name = 'Sim'; Mode = 'TestWithoutNotifications'; Enabled = $true; ApplySensitivityLabel = 'Confidential' } ) } @(Get-MsecPurviewAutoLabelingPolicy) } ($rows | Where-Object Name -eq 'Live').IsEnforcing | Should -BeTrue # Enabled and running, but it labels nothing. ($rows | Where-Object Name -eq 'Sim').Enabled | Should -BeTrue ($rows | Where-Object Name -eq 'Sim').IsEnforcing | Should -BeFalse } It 'keeps Raw, because the projection is unverified against a tenant that has one' { $rows = InModuleScope msec { Mock Get-AutoSensitivityLabelRule -MockWith { @() } Mock Get-AutoSensitivityLabelPolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Mode = 'Enable'; Enabled = $true SomeUndocumentedField = 'still reachable' }) } @(Get-MsecPurviewAutoLabelingPolicy) } $rows[0].Raw.SomeUndocumentedField | Should -Be 'still reachable' } It 'nulls the rule columns when the rule cmdlet is not exposed' { $rows = InModuleScope msec { Mock Get-AutoSensitivityLabelPolicy -MockWith { @([PSCustomObject]@{ Name = 'P'; Mode = 'Enable'; Enabled = $true }) } Mock Get-Command -ParameterFilter { $Name -eq 'Get-AutoSensitivityLabelRule' } -MockWith { $null } @(Get-MsecPurviewAutoLabelingPolicy -WarningAction SilentlyContinue) } # Not 0, which would say "this policy has no conditions". $rows[0].RuleCount | Should -BeNullOrEmpty } } Describe 'Get-MsecPurviewInformationBarrier' { It 'distinguishes an authored policy from an applied one' { $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-InformationBarrierPolicy -MockWith { @( [PSCustomObject]@{ Name = 'Live'; State = 'Active'; AssignedSegment = 'Traders' } [PSCustomObject]@{ Name = 'Draft'; State = 'Inactive'; AssignedSegment = 'Research' } ) } @(Get-MsecPurviewInformationBarrier) } ($rows | Where-Object Name -eq 'Live').IsActive | Should -BeTrue # Counted as a policy, protecting nobody. ($rows | Where-Object Name -eq 'Draft').IsActive | Should -BeFalse } } Describe 'A renamed policy has two names' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } } } It 'reports the DISPLAY name, which is what the portal shows' { # Renaming a DLP policy changes DisplayName and leaves Name as created. Reporting Name # means the page says one thing and the portal another, and nobody can find the policy. $rows = InModuleScope msec { Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'TEST - Label-based DLP (pilot)' DisplayName = 'DLP - Confidential document shared' Mode = 'Enable'; Enabled = $true }) } @(Get-MsecPurviewDlpPolicy) } $rows[0].Name | Should -Be 'DLP - Confidential document shared' $rows[0].InternalName | Should -Be 'TEST - Label-based DLP (pilot)' $rows[0].Renamed | Should -BeTrue } It 'still joins rules on the INTERNAL name, which is what ParentPolicyName tracks' { # Measured live: ParentPolicyName matched Name on 10 of 10 rules and DisplayName only # where the two happened to be equal. Joining on the display name silently loses the # rules of every renamed policy - and RuleCount 0 reads as "no conditions". $rows = InModuleScope msec { Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'TEST - Label-based DLP (pilot)' DisplayName = 'DLP - Confidential document shared' Mode = 'Enable'; Enabled = $true }) } Mock Get-DlpComplianceRule -MockWith { @([PSCustomObject]@{ Name = 'r1' ParentPolicyName = 'TEST - Label-based DLP (pilot)' BlockAccess = $false; ReportSeverityLevel = 'Low' }) } @(Get-MsecPurviewDlpPolicy) } $rows[0].RuleCount | Should -Be 1 $rows[0].RuleNames | Should -Contain 'r1' } It 'is findable by either name' { $byDisplay, $byInternal = InModuleScope msec { Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'Old name'; DisplayName = 'New name'; Mode = 'Enable'; Enabled = $true }) } ,@(Get-MsecPurviewDlpPolicy -Name 'New*') ,@(Get-MsecPurviewDlpPolicy -Name 'Old*') } $byDisplay.Count | Should -Be 1 $byInternal.Count | Should -Be 1 } It 'falls back to Name when there is no DisplayName' { $rows = InModuleScope msec { Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @([PSCustomObject]@{ Name = 'Only one name'; Mode = 'Enable'; Enabled = $true }) } @(Get-MsecPurviewDlpPolicy) } $rows[0].Name | Should -Be 'Only one name' $rows[0].Renamed | Should -BeFalse } } Describe 'The Workload property contradicts the scopes' { It 'flags a policy that claims Exchange but targets no mailboxes' { # Measured live: all 8 policies on one tenant listed Exchange in Workload while every # Exchange targeting property was empty. Microsoft's reference is explicit that an unset # ExchangeLocation means email is not included - so Workload is declarative, not derived, # and reading it is how a careful admin concludes email is covered when it is not. $rows = InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-DlpComplianceRule -MockWith { @() } Mock Get-DlpCompliancePolicy -MockWith { @( [PSCustomObject]@{ Name = 'Claims email'; Mode = 'Enable'; Enabled = $true Workload = 'Exchange, SharePoint, OneDriveForBusiness' ExchangeLocation = @() SharePointLocation = @([PSCustomObject]@{ DisplayName = 'All' }) } [PSCustomObject]@{ Name = 'Actually covers email'; Mode = 'Enable'; Enabled = $true Workload = 'Exchange' ExchangeLocation = @([PSCustomObject]@{ DisplayName = 'All' }) } ) } @(Get-MsecPurviewDlpPolicy) } $claims = $rows | Where-Object Name -eq 'Claims email' $claims.ExchangeScope | Should -Be 'None' $claims.WorkloadClaims | Should -Contain 'Exchange' $claims.ClaimsEmailWithoutTarget | Should -BeTrue $real = $rows | Where-Object Name -eq 'Actually covers email' $real.ExchangeScope | Should -Be 'All' $real.ClaimsEmailWithoutTarget | Should -BeFalse } } Describe 'Get-MsecPurviewAlertPolicy' { BeforeEach { InModuleScope msec { $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} } Mock Get-ConnectionInformation -MockWith { New-Connected } Mock Get-ProtectionAlert -MockWith { @( [PSCustomObject]@{ Name = 'Shared files externally'; Category = 'DataGovernance' Severity = 'Medium'; Disabled = $true; IsSystemRule = $true NotificationEnabled = $true } [PSCustomObject]@{ Name = 'DLP-Custom rule'; Category = 'DataLossPrevention' Severity = 'Low'; Disabled = $false; IsSystemRule = $false NotificationEnabled = $false } [PSCustomObject]@{ Name = 'Malware campaign'; Category = 'ThreatManagement' Severity = 'High'; Disabled = $false; IsSystemRule = $true NotificationEnabled = $true } ) } } } It 'inverts Disabled into IsEnabled, keeping both' { # The service stores Disabled, so a filter written against it reads backwards and # Where-Object Disabled silently returns the healthy policies. $rows = InModuleScope msec { @(Get-MsecPurviewAlertPolicy) } ($rows | Where-Object Name -eq 'Shared files externally').IsEnabled | Should -BeFalse ($rows | Where-Object Name -eq 'Shared files externally').Disabled | Should -BeTrue ($rows | Where-Object Name -eq 'Malware campaign').IsEnabled | Should -BeTrue } It 'separates Microsoft built-ins from what this organisation configured' { $rows = InModuleScope msec { @(Get-MsecPurviewAlertPolicy -CustomOnly) } # A count mixing the two says nothing about how much alerting anyone here set up. $rows.Count | Should -Be 1 $rows[0].Name | Should -Be 'DLP-Custom rule' } It 'reports an enabled policy that emails nobody' { # NotificationEnabled is not whether the alert fires - it is whether anyone is told. $rows = InModuleScope msec { @(Get-MsecPurviewAlertPolicy | Where-Object { $_.IsEnabled -and -not $_.NotificationEnabled }) } $rows.Count | Should -Be 1 $rows[0].Name | Should -Be 'DLP-Custom rule' } It 'filters by category' { $rows = InModuleScope msec { @(Get-MsecPurviewAlertPolicy -Category ThreatManagement) } $rows.Count | Should -Be 1 } } |