Skip to content

Control 1.5 — PowerShell Setup: DLP and Sensitivity Labels

Scope. This playbook automates inventory, drift detection, and bounded mutation for Microsoft Purview DLP, sensitivity labels, sensitive-information types (SITs), exact-data-match (EDM) classifiers, keyword dictionaries, Power Platform DLP, and the cross-surface settings that make Control 1.5 enforceable for Microsoft 365 Copilot and agentic workloads in US financial services tenants.

What this is. A baseline-conformant authoring guide. Every helper returns [pscustomobject] with a Status field of Clean | Anomaly | Pending | NotApplicable | Error, mutating cmdlets honour -WhatIf, and all evidence is hashed via Write-FsiEvidence.

What this isn't. A substitute for the portal walkthrough, the verification & testing playbook, or troubleshooting. It also does not replace tenant-specific risk acceptance for preview features.

Hedged-language reminder. PowerShell coverage of Purview DLP supports compliance with FINRA 3110 / SEC 17a-4 / GLBA / SOX recordkeeping and supervision obligations. It does not, by itself, guarantee compliance — implementation requires policy targeting, label deployment, end-user training, and exception governance.


Metadata

Field Value
Control ID 1.5
Pillar 1 — Security
Playbook PowerShell setup (companion to portal walkthrough, verification, troubleshooting)
PowerShell editions PS 7.4+ for Graph + Exchange; PS 5.1 Desktop required for Microsoft.PowerApps.Administration.PowerShell mutation cmdlets
Sovereign clouds Commercial · GCC · GCC High · DoD (Adaptive Protection = NotApplicable in any US Gov cloud — see §10)
Companion playbooks portal-walkthrough · verification-testing · troubleshooting
Related controls 1.6 DSPM for AI · 1.10 Communication Compliance · 1.12 IRM · 1.13 SITs/EDM · 1.15 DKE/encryption · 2.12 Supervision · 3.4 Incident reporting · 3.9 Sentinel forwarding
Baseline _shared/powershell-baseline.md

§0 — READ FIRST: Wrong-shell trap & DLP false-clean defects

Three failure modes silently produce false-clean evidence for Control 1.5. Reviewers who do not read this section will sign off on policies that were never actually inspected.

# Trap False-clean symptom Remediation
1 Wrong DLP plane. Get-DlpCompliancePolicy (Purview) and Get-DlpPolicy (Power Platform) are different cmdlets in different connections. Running only the first returns "0 anomalies" while every connector policy is missing. M365 DLP looks complete; connector classification gap is invisible. Always run both planes. The §13 wrapper enforces this; ad-hoc scripts must too.
2 Power Platform UI/API label inversion. API value Confidential renders in the Maker portal as Business; API General renders as Non-Business; Blocked is Blocked. A reviewer comparing a script that prints General against a screenshot showing Non-Business will (correctly) think they match — and miss that the intent was Business. Connector evidence reads as "expected" but the API label is opposite of the documented business decision. Always emit both labels via ConvertTo-FsiUiLabel (§9).
3 Audit RecordType = DLP is invalid. Search-UnifiedAuditLog -RecordType DLP returns zero rows on every tenant. Real record types are ComplianceDLPSharePoint, ComplianceDLPSharePointClassification, ComplianceDLPExchange, ComplianceDLPExchangeClassification, DLPEndpoint. For Copilot, query -Operations 'DLPRuleMatch' and filter the payload Workload -eq 'Applications'. "No DLP events in 30 days" — looks like a clean tenant; actually a broken query. Use the helper Get-FsiDlpAuditEvents in §11.

A fourth trap — shell mismatch — is enforced as a guard:

# Shell guard — paste at the top of every Control 1.5 script
if ($PSVersionTable.PSEdition -ne 'Core' -and $PSVersionTable.PSVersion.Major -lt 7) {
    # Allowed: PS 5.1 Desktop is required for Power Apps Admin mutation cmdlets (see §9)
    Write-Warning 'Running on PS 5.1 Desktop. Graph + Exchange helpers will work; Power Apps Admin mutations are supported here. For Graph performance, prefer PS 7.4+ in a separate session.'
}

§1 — Module / CLI / permission matrix

Pin every module via Install-Module -RequiredVersion per the baseline. Do not use -Force to silently upgrade in production.

Module Min version Edition Used for Connect cmdlet Least-privilege role
ExchangeOnlineManagement 3.4.0 PS 7.4+ Purview DLP, sensitivity labels, label policies, auto-label rules, EDM, dictionaries Connect-IPPSSession Purview Compliance Admin (or scoped Information Protection Admin + DLP Compliance Mgmt)
Microsoft.Graph.Authentication 2.20.0 PS 7.4+ Tokens for downstream Graph calls Connect-MgGraph n/a (delegated by app role)
Microsoft.Graph.Beta.Security 2.20.0 PS 7.4+ Get-MgBetaSecurityInformationProtectionSensitivityLabel (label tenant inventory) n/a InformationProtectionPolicy.Read.All
Microsoft.Graph.Beta.Compliance 2.20.0 PS 7.4+ DSPM-for-AI cross-link (Control 1.6) n/a InformationProtectionPolicy.Read.All
Microsoft.Online.SharePoint.PowerShell or PnP.PowerShell 16.0.25410 / 2.4.0 PS 7.4+ SharePoint/OneDrive label-default + DLP scoping Connect-SPOService / Connect-PnPOnline SharePoint Admin
Microsoft.PowerApps.Administration.PowerShell 2.0.190 PS 5.1 Desktop only Get-DlpPolicy, New-DlpPolicy, Add-ConnectorToPolicy, Set-ConnectorGroup Add-PowerAppsAccount Power Platform Admin
Microsoft.PowerApps.PowerShell 1.0.34 PS 5.1 Desktop Maker-side enumeration Add-PowerAppsAccount Power Platform Admin

Edition guard. Any helper that calls a *-DlpPolicy (Power Platform) cmdlet must be invoked from PS 5.1 Desktop:

function Assert-FsiDesktopPowerShell {
    if ($PSVersionTable.PSEdition -ne 'Desktop') {
        throw 'Power Platform DLP cmdlets require PowerShell 5.1 Desktop. Open Windows PowerShell (not pwsh.exe) and re-run.'
    }
}

§2 — Sovereign-aware bootstrap (Initialize-FsiDlpSession)

Endpoints follow the baseline §3 sovereign cloud table. The Connect-IPPSSession URI differs between GCC, GCC High, and DoD and must be passed explicitly — there is no auto-discovery.

function Initialize-FsiDlpSession {
    [CmdletBinding()]
    param(
        [ValidateSet('Commercial','GCC','GCCHigh','DoD')]
        [string]$Cloud = 'Commercial',

        [Parameter(Mandatory)] [string]$TenantId,
        [Parameter(Mandatory)] [string]$UserPrincipalName,

        # Optional service-principal-with-certificate flow for unattended runs
        [string]$AppId,
        [string]$CertificateThumbprint
    )

    $map = @{
        Commercial = @{ Ipps = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'; Graph = 'Global';      PowerApps = 'prod'    }
        GCC        = @{ Ipps = 'https://ps.compliance.protection.outlook.com/powershell-liveid/'; Graph = 'USGov';       PowerApps = 'usgov'   }
        GCCHigh    = @{ Ipps = 'https://ps.compliance.protection.office365.us/powershell-liveid/'; Graph = 'USGovDoD';   PowerApps = 'usgovhigh' }
        DoD        = @{ Ipps = 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/'; Graph = 'USGovDoD'; PowerApps = 'dod'    }
    }
    $cfg = $map[$Cloud]

    # Purview / Exchange (IPPS)
    if ($AppId -and $CertificateThumbprint) {
        Connect-IPPSSession -ConnectionUri $cfg.Ipps -AppId $AppId -CertificateThumbprint $CertificateThumbprint -Organization "$TenantId" -ShowBanner:$false
    } else {
        Connect-IPPSSession -ConnectionUri $cfg.Ipps -UserPrincipalName $UserPrincipalName -ShowBanner:$false
    }

    # Graph
    Connect-MgGraph -TenantId $TenantId -Environment $cfg.Graph -Scopes 'InformationProtectionPolicy.Read.All','Policy.Read.All' -NoWelcome

    # Power Platform (only on PS 5.1 Desktop)
    if ($PSVersionTable.PSEdition -eq 'Desktop') {
        Add-PowerAppsAccount -Endpoint $cfg.PowerApps | Out-Null
    } else {
        Write-Warning 'Skipping Add-PowerAppsAccount: not on PS 5.1 Desktop. Power Platform DLP helpers will return Status=Pending.'
    }

    [pscustomobject]@{
        Status      = 'Clean'
        Cloud       = $Cloud
        TenantId    = $TenantId
        IppsUri     = $cfg.Ipps
        GraphEnv    = $cfg.Graph
        PowerAppsEp = $cfg.PowerApps
        Timestamp   = (Get-Date).ToUniversalTime()
    }
}

§3 — Helper library overview

# Helper Purpose Returns Status
1 Get-FsiDlpCoverage (§4) Map every Purview DLP policy across the 13 surfaces Clean if all surfaces covered; else Anomaly
2 Get-FsiCopilotDlpPolicies (§5) Validate Copilot DLP rule shape (Custom template, EnforcementPlanes, MicrosoftCopilotEnabledLocation) Clean / Anomaly / Pending
3 Test-FsiPolicyTipOverrideSettings (§6) Verify NotifyAllowOverride='WithJustification' and override-justification capture Clean / Anomaly
4 Get-FsiSensitivityLabelInventory (§7) Reconcile labels via Graph beta + IPPS Clean / Anomaly
5 Get-FsiAdaptiveProtectionStatus (§10) Detect Adaptive Protection rollout state; always NotApplicable in US Gov clouds Clean / NotApplicable / Pending

Auxiliary (non-status-bearing) utilities introduced inline: ConvertTo-FsiUiLabel (§9), Get-FsiDlpAuditEvents (§11), Invoke-FsiControl15Audit (§13).


§4 — DLP cmdlet inventory + Get-FsiDlpCoverage

The 13 enforcement surfaces tracked by Control 1.5 (the on-prem AIP scanner is recognised in the control doc but is portal-only and out of PS scope):

  1. SharePoint sites · 2. OneDrive accounts · 3. Exchange email · 4. Teams chat & channel · 5. Devices (Endpoint DLP) · 6. Copilot — block by sensitivity label (GA) · 7. Copilot — block by SIT (preview) · 8. Power Platform connector classification · 9. Power Platform HTTP endpoint filtering (preview) · 10. Edge for Business unmanaged AI (preview) · 11. Network DLP for unmanaged AI (preview, portal-only) · 12. Defender for Cloud Apps · 13. Power BI / Fabric workspaces.

Cmdlet inventory (Purview plane). Get-DlpCompliancePolicy, Get-DlpComplianceRule, New-DlpCompliancePolicy, Set-DlpCompliancePolicy, New-DlpComplianceRule, Set-DlpComplianceRule, Remove-DlpCompliancePolicy. Note: DLP policies use the Mode parameter (Enable | TestWithNotifications | TestWithoutNotifications | Disable | PendingDeletion) — there is no Enabled Boolean.

function Get-FsiDlpCoverage {
    [CmdletBinding()]
    param()

    $surfaces = @(
        'SharePoint','OneDriveForBusiness','ExchangeLocation','TeamsLocation',
        'EndpointDevices','MicrosoftCopilotEnabledLocation','PowerPlatform',
        'EdgeBrowser','NetworkUnmanagedAi','DefenderForCloudApps','PowerBIWorkspaces','OnPremScanner'
    )

    $policies = Get-DlpCompliancePolicy -ErrorAction Stop
    $covered  = @{}
    foreach ($s in $surfaces) { $covered[$s] = $false }

    foreach ($p in $policies) {
        if ($p.Mode -eq 'Disable' -or $p.Mode -eq 'PendingDeletion') { continue }
        if ($p.SharePointLocation)      { $covered.SharePoint = $true }
        if ($p.OneDriveLocation)        { $covered.OneDriveForBusiness = $true }
        if ($p.ExchangeLocation)        { $covered.ExchangeLocation = $true }
        if ($p.TeamsLocation)           { $covered.TeamsLocation = $true }
        if ($p.EndpointDlpLocation)     { $covered.EndpointDevices = $true }
        if ($p.MicrosoftCopilotEnabledLocation) { $covered.MicrosoftCopilotEnabledLocation = $true }
        if ($p.PowerBIDlpLocation)      { $covered.PowerBIWorkspaces = $true }
    }

    $gaps = $covered.GetEnumerator() | Where-Object { -not $_.Value } | Select-Object -ExpandProperty Key

    [pscustomobject]@{
        Status        = if ($gaps.Count -eq 0) { 'Clean' } else { 'Anomaly' }
        PolicyCount   = $policies.Count
        SurfacesCovered = ($covered.GetEnumerator() | Where-Object { $_.Value } | Select-Object -ExpandProperty Key)
        SurfacesMissing = $gaps
        Note          = 'NetworkUnmanagedAi and OnPremScanner are not addressable via PowerShell; verify via portal evidence.'
        Timestamp     = (Get-Date).ToUniversalTime()
    }
}

§5 — Copilot DLP policy creation & Get-FsiCopilotDlpPolicies

Copilot DLP requires the Purview UI Custom template. Two facts trip up most teams:

  1. You cannot combine SIT and sensitivity-label conditions in the same rule. Use two rules in the same policy.
  2. The location object is a JSON blob shaped [{ "Workload":"Applications" }] and the policy parameter is MicrosoftCopilotEnabledLocation. The rule must declare EnforcementPlanes=@('CopilotExperiences').
$copilotLocations = ConvertTo-Json -Compress -InputObject @(@{ Workload = 'Applications' })

New-DlpCompliancePolicy `
    -Name 'FSI-Copilot-MNPI-Block' `
    -MicrosoftCopilotEnabledLocation $copilotLocations `
    -Mode 'TestWithNotifications' `
    -WhatIf

# Rule A — block by sensitivity label (GA)
New-DlpComplianceRule `
    -Name 'BlockHighlyConfidentialFromCopilot' `
    -Policy 'FSI-Copilot-MNPI-Block' `
    -EnforcementPlanes @('CopilotExperiences') `
    -ContentContainsSensitivityLabel @(@{ labels = @('<labelGuidHighlyConfidential>') }) `
    -BlockAccess $true `
    -NotifyUser @('SiteAdmin','LastModifier') `
    -NotifyAllowOverride 'WithJustification' `
    -NotifyEmailCustomText 'This content is restricted from Copilot grounding under FINRA 3110 / SEC 17a-4 controls.' `
    -WhatIf

# Rule B — block by SIT (preview). Separate rule, same policy.
New-DlpComplianceRule `
    -Name 'BlockMnpiSitFromCopilot' `
    -Policy 'FSI-Copilot-MNPI-Block' `
    -EnforcementPlanes @('CopilotExperiences') `
    -ContentContainsSensitiveInformation @(@{ name = '<sitGuidMnpi>'; mincount = 1 }) `
    -BlockAccess $true `
    -WhatIf
function Get-FsiCopilotDlpPolicies {
    [CmdletBinding()]
    param()

    $policies = Get-DlpCompliancePolicy | Where-Object { $_.MicrosoftCopilotEnabledLocation }
    $findings = foreach ($p in $policies) {
        $rules = Get-DlpComplianceRule -Policy $p.Name
        $copilotRules = $rules | Where-Object { $_.EnforcementPlanes -contains 'CopilotExperiences' }
        $hasLabelRule = $copilotRules | Where-Object { $_.ContentContainsSensitivityLabel }
        $hasSitRule   = $copilotRules | Where-Object { $_.ContentContainsSensitiveInformation }
        $combined     = $copilotRules | Where-Object { $_.ContentContainsSensitivityLabel -and $_.ContentContainsSensitiveInformation }

        $status = if ($combined) { 'Anomaly' }
                  elseif (-not ($hasLabelRule -or $hasSitRule)) { 'Anomaly' }
                  elseif ($p.Mode -ne 'Enable') { 'Pending' }
                  else { 'Clean' }

        [pscustomobject]@{
            Status        = $status
            Policy        = $p.Name
            Mode          = $p.Mode
            LabelRule     = [bool]$hasLabelRule
            SitRule       = [bool]$hasSitRule
            CombinedRule  = [bool]$combined
            RuleCount     = $copilotRules.Count
            Note          = if ($combined) { 'Combined SIT+Label rule detected — split into two rules.' } else { '' }
        }
    }
    if (-not $findings) {
        return [pscustomobject]@{ Status = 'Anomaly'; Policy = '<none>'; Note = 'No Copilot-scoped DLP policies found.' }
    }
    $findings
}

§6 — Rules: notifications, overrides, policy tips

Test-FsiPolicyTipOverrideSettings validates that overrides require justification (a supervision-evidence requirement that supports compliance with FINRA Rule 3110).

function Test-FsiPolicyTipOverrideSettings {
    [CmdletBinding()]
    param([string[]]$PolicyNames)

    $rules = if ($PolicyNames) { $PolicyNames | ForEach-Object { Get-DlpComplianceRule -Policy $_ } }
             else              { Get-DlpComplianceRule }

    $rules | ForEach-Object {
        $needsJustification = ($_.NotifyAllowOverride -contains 'WithJustification')
        $hasNotifyText = -not [string]::IsNullOrWhiteSpace($_.NotifyEmailCustomText)
        $status = if ($needsJustification -and $hasNotifyText) { 'Clean' } else { 'Anomaly' }
        [pscustomobject]@{
            Status                 = $status
            Rule                   = $_.Name
            Policy                 = $_.ParentPolicyName
            NotifyAllowOverride    = ($_.NotifyAllowOverride -join ',')
            NotifyUserType         = ($_.NotifyUser -join ',')
            HasCustomEmailText     = $hasNotifyText
        }
    }
}

§7 — Sensitivity labels & label policies

Cmdlet correction. The real Graph beta cmdlet is Get-MgBetaSecurityInformationProtectionSensitivityLabel. The frequently-cited Get-MgBetaInformationProtectionSensitivityPolicyLabel does not exist.

Inventory: Get-Label, Get-LabelPolicy, Get-AutoSensitivityLabelPolicy, Get-AutoSensitivityLabelRule. Mutation: New-Label, Set-Label, New-LabelPolicy, Set-LabelPolicy, New-AutoSensitivityLabelPolicy, New-AutoSensitivityLabelRule.

function Get-FsiSensitivityLabelInventory {
    [CmdletBinding()]
    param()

    $ippsLabels  = Get-Label | Select-Object Guid, DisplayName, ParentId, Priority, ContentType, Disabled
    $ippsPolicies = Get-LabelPolicy | Select-Object Guid, Name, Labels, ScopedLabels, Mode
    try {
        $graphLabels = Get-MgBetaSecurityInformationProtectionSensitivityLabel -All -ErrorAction Stop |
                       Select-Object Id, Name, Description
    } catch {
        $graphLabels = $null
    }

    $ippsIds  = ($ippsLabels  | Select-Object -ExpandProperty Guid) -as [string[]]
    $graphIds = if ($graphLabels) { ($graphLabels | Select-Object -ExpandProperty Id) -as [string[]] } else { @() }
    $delta    = if ($graphLabels) { Compare-Object $ippsIds $graphIds } else { $null }

    [pscustomobject]@{
        Status             = if (-not $graphLabels) { 'Pending' } elseif ($delta) { 'Anomaly' } else { 'Clean' }
        IppsLabelCount     = $ippsLabels.Count
        GraphLabelCount    = if ($graphLabels) { $graphLabels.Count } else { 0 }
        PolicyCount        = $ippsPolicies.Count
        DeltaItems         = $delta
        Note               = if (-not $graphLabels) { 'Graph beta call failed; verify InformationProtectionPolicy.Read.All consent.' } else { '' }
    }
}

Read-side: Get-DataClassification, Get-DlpSensitiveInformationType, Get-DlpEdmSchema, Get-DlpKeywordDictionary. Mutation: New-DlpSensitiveInformationType, Set-DlpSensitiveInformationType, New-DlpEdmSchema, Edit-DlpEdmSchema, New-DlpKeywordDictionary, Set-DlpKeywordDictionary.

EDM is the canonical mechanism for MNPI watchlists (deal-codename rotations, restricted-list tickers). The schema and the upload pipeline live in Control 1.13; from Control 1.5 the consumption pattern is:

# Surface MNPI EDM and dictionary classifiers for use in DLP rules
$edm   = Get-DlpEdmSchema
$dicts = Get-DlpKeywordDictionary

$edm | Select-Object Name, IsValid, RowCount, LastUpdatedTime
$dicts | Select-Object Name, Identity, FileData.Length

# In a rule definition
# -ContentContainsSensitiveInformation @(@{ name = '<edmSitGuidMnpiTickers>'; mincount = 1 })

Authoring an EDM-backed SIT and a dictionary (non-mutating preview):

New-DlpEdmSchema -FileData ([Byte[]](Get-Content -Path .\mnpi-schema.xml -Encoding Byte -Raw)) -WhatIf
New-DlpKeywordDictionary -Name 'FSI-MNPI-Codenames' -Description 'Quarterly rotation' `
    -FileData ([Byte[]](Get-Content -Path .\mnpi-codenames.csv -Encoding Byte -Raw)) -WhatIf

§9 — Power Platform DLP + UI/API label inversion

Edition reminder. Run from PS 5.1 Desktop (Assert-FsiDesktopPowerShell from §1). Cmdlets: Get-DlpPolicy, New-DlpPolicy, Set-DlpPolicy, Remove-DlpPolicy, Add-PowerAppsPolicyToTenant, Add-ConnectorToPolicy, Set-ConnectorGroup, Remove-ConnectorFromPolicy.

function ConvertTo-FsiUiLabel {
    [CmdletBinding()]
    param([Parameter(Mandatory)][ValidateSet('Confidential','General','Blocked')] [string]$ApiLabel)
    switch ($ApiLabel) {
        'Confidential' { 'Business' }
        'General'      { 'Non-Business' }
        'Blocked'      { 'Blocked' }
    }
}

function Get-FsiPowerPlatformDlpInventory {
    [CmdletBinding()]
    param()
    Assert-FsiDesktopPowerShell

    $policies = Get-DlpPolicy
    $rows = foreach ($p in $policies.value) {
        foreach ($g in $p.connectorGroups) {
            foreach ($c in $g.connectors) {
                [pscustomobject]@{
                    Status         = 'Clean'
                    Policy         = $p.displayName
                    Environment    = ($p.environments | Select-Object -ExpandProperty name) -join ','
                    Connector      = $c.name
                    ConnectorId    = $c.id
                    ApiLabel       = $g.classification
                    UiLabel        = ConvertTo-FsiUiLabel -ApiLabel $g.classification
                }
            }
        }
    }
    $rows
}

Evidence requirement. Always emit both ApiLabel and UiLabel. A reviewer comparing a script that prints General against a Maker-portal screenshot showing Non-Business will conclude they match — and miss that the documented business decision was Business (Confidential).


§10 — Adaptive Protection (Get-FsiAdaptiveProtectionStatus)

Adaptive Protection / Insider Risk-driven DLP is not available in any US Gov cloud (GCC, GCC High, DoD). The helper must return Status='NotApplicable' and surface the sovereign rationale link rather than appearing as a remediation gap on US Gov tenants.

function Get-FsiAdaptiveProtectionStatus {
    [CmdletBinding()]
    param([Parameter(Mandatory)][ValidateSet('Commercial','GCC','GCCHigh','DoD')][string]$Cloud)

    if ($Cloud -in 'GCC','GCCHigh','DoD') {
        return [pscustomobject]@{
            Status        = 'NotApplicable'
            Cloud         = $Cloud
            Reason        = 'Adaptive Protection / Insider Risk Management is not available in US Gov clouds.'
            Reference     = '../../../controls/pillar-1-security/1.5-data-loss-prevention-dlp-and-sensitivity-labels.md#sovereign-cloud-parity'
        }
    }

    try {
        $ap = Get-DlpAdaptiveProtectionConfiguration -ErrorAction Stop
        [pscustomobject]@{
            Status   = if ($ap.Enabled) { 'Clean' } else { 'Pending' }
            Cloud    = $Cloud
            Enabled  = $ap.Enabled
            Profiles = ($ap.RiskLevels -join ',')
        }
    } catch {
        [pscustomobject]@{ Status = 'Pending'; Cloud = $Cloud; Note = "Cmdlet unavailable: $($_.Exception.Message)" }
    }
}

§11 — Audit log queries + Sentinel forwarding

The trap is real. -RecordType DLP is invalid and returns zero rows on every tenant. Use the explicit record types below; for Copilot, query -Operations 'DLPRuleMatch' and filter the payload Workload -eq 'Applications'.

function Get-FsiDlpAuditEvents {
    [CmdletBinding()]
    param(
        [datetime]$StartDate = (Get-Date).AddDays(-7),
        [datetime]$EndDate   = (Get-Date)
    )

    $recordTypes = @(
        'ComplianceDLPSharePoint',
        'ComplianceDLPSharePointClassification',
        'ComplianceDLPExchange',
        'ComplianceDLPExchangeClassification',
        'DLPEndpoint'
    )

    $rows = foreach ($rt in $recordTypes) {
        Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -RecordType $rt -ResultSize 5000 |
            Select-Object CreationDate, UserIds, Operations, RecordType, AuditData
    }

    $copilot = Search-UnifiedAuditLog -StartDate $StartDate -EndDate $EndDate -Operations 'DLPRuleMatch' -ResultSize 5000 |
        Where-Object { ($_.AuditData | ConvertFrom-Json).Workload -eq 'Applications' }

    [pscustomobject]@{
        Status         = if (($rows.Count + $copilot.Count) -gt 0) { 'Clean' } else { 'Pending' }
        ClassicEvents  = $rows.Count
        CopilotEvents  = $copilot.Count
        WindowStartUtc = $StartDate.ToUniversalTime()
        WindowEndUtc   = $EndDate.ToUniversalTime()
        Note           = 'Forwarding to Sentinel is owned by Control 3.9.'
    }
}

For the Sentinel forwarding pipeline (M365 connector, custom log table, KQL detection rules), see Control 3.9.


§12 — 13-surface walkthrough

# Surface PowerShell coverage Helper / cmdlet Skip rationale
1 SharePoint sites Full Get-DlpCompliancePolicy .SharePointLocation
2 OneDrive accounts Full .OneDriveLocation
3 Exchange email Full .ExchangeLocation
4 Teams chat / channel Full .TeamsLocation
5 Endpoint devices Full .EndpointDlpLocation
6 Copilot — block by label (GA) Full Get-FsiCopilotDlpPolicies
7 Copilot — block by SIT (preview) Full Get-FsiCopilotDlpPolicies (Rule B) Preview; verify portal availability
8 Power Platform connector classification Full (PS 5.1) Get-FsiPowerPlatformDlpInventory
9 Power Platform HTTP endpoint filtering (preview) Full (PS 5.1) Get-DlpPolicy .endpointConfigurations Preview
10 Edge for Business unmanaged AI (preview) Partial Get-DlpCompliancePolicy (when surfaced) Preview; portal evidence required
11 Network DLP for unmanaged AI (preview) None n/a Portal-only; capture screenshot evidence
12 Defender for Cloud Apps Indirect Graph Security API Owned by MDA team; cross-reference
13 Power BI / Fabric workspaces Full .PowerBIDlpLocation

§13 — Wrapper orchestration with transcript & evidence manifest

function Invoke-FsiControl15Audit {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
    param(
        [Parameter(Mandatory)] [ValidateSet('Commercial','GCC','GCCHigh','DoD')] [string]$Cloud,
        [Parameter(Mandatory)] [string]$TenantId,
        [Parameter(Mandatory)] [string]$UserPrincipalName,
        [string]$EvidencePath = (Join-Path $PWD ('control-15-' + (Get-Date -Format 'yyyyMMdd-HHmmss')))
    )

    New-Item -ItemType Directory -Path $EvidencePath -Force | Out-Null
    Start-Transcript -Path (Join-Path $EvidencePath 'transcript.log') -Append | Out-Null

    try {
        $session = Initialize-FsiDlpSession -Cloud $Cloud -TenantId $TenantId -UserPrincipalName $UserPrincipalName

        $results = [ordered]@{
            session             = $session
            dlpCoverage         = Get-FsiDlpCoverage
            copilotPolicies     = Get-FsiCopilotDlpPolicies
            policyTipOverrides  = Test-FsiPolicyTipOverrideSettings
            labelInventory      = Get-FsiSensitivityLabelInventory
            adaptiveProtection  = Get-FsiAdaptiveProtectionStatus -Cloud $Cloud
            dlpAuditEvents      = Get-FsiDlpAuditEvents
        }

        if ($PSVersionTable.PSEdition -eq 'Desktop') {
            $results['powerPlatformDlp'] = Get-FsiPowerPlatformDlpInventory
        } else {
            $results['powerPlatformDlp'] = [pscustomobject]@{ Status = 'Pending'; Note = 'Run from PS 5.1 Desktop.' }
        }

        foreach ($k in $results.Keys) {
            $results[$k] | ConvertTo-Json -Depth 8 | Out-File (Join-Path $EvidencePath "$k.json") -Encoding UTF8
        }

        # Hash + manifest (baseline-conformant)
        Write-FsiEvidence -Path $EvidencePath -ControlId '1.5'

        $rollup = $results.GetEnumerator() | ForEach-Object {
            [pscustomobject]@{ Section = $_.Key; Status = ($_.Value | Select-Object -First 1 -ExpandProperty Status -ErrorAction SilentlyContinue) }
        }
        $rollup
    } finally {
        Stop-Transcript | Out-Null
    }
}

§14 — Anti-patterns & cross-references

Anti-pattern Why it fails Correct approach
Running only Get-DlpCompliancePolicy and declaring the tenant clean Misses every Power Platform connector policy Run both planes (§4 + §9)
Search-UnifiedAuditLog -RecordType DLP Invalid record type — returns zero rows Use the five record types in §11
Combining SIT + sensitivity label conditions in one Copilot rule Rule will not save, or will save with silent precedence Two rules, same policy (§5)
Reporting Power Platform DLP labels as Confidential / General only Inverted vs the Maker-portal UI; reviewers misread evidence Emit both via ConvertTo-FsiUiLabel (§9)
Calling Get-MgBetaInformationProtectionSensitivityPolicyLabel Cmdlet does not exist Use Get-MgBetaSecurityInformationProtectionSensitivityLabel (§7)
Treating Adaptive Protection gap as a US Gov anomaly Feature not available in GCC / GCC High / DoD Helper returns NotApplicable (§10)
Using -Force on Install-Module in production Silent module upgrade breaks reproducibility Install-Module -RequiredVersion per baseline §1
Treating Mode = 'Disable' policies as active They emit no enforcement Filter on Mode -eq 'Enable' (§4)
Running Power Apps Admin cmdlets from pwsh 7.x Cmdlets are Desktop-only; silently no-op or error Assert-FsiDesktopPowerShell (§1)

Cross-references. 1.6 DSPM for AI consumes label + SIT signals from this control. 1.10 Communication Compliance and 2.12 Supervision consume DLP-rule-match signals to drive reviewer queues. 1.12 IRM and 1.15 DKE/encryption provide the rights-management substrate for label-based blocking. 1.13 SITs/EDM owns classifier authoring. 3.4 Incident reporting and 3.9 Sentinel forwarding own downstream telemetry.


Updated: April 2026 | Version: v1.4.0 | UI Verification Status: Current