Skip to content

PowerShell Setup: Control 1.17 — Endpoint Data Loss Prevention

Read the FSI PowerShell baseline first

Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. Snippets below show abbreviated patterns; the baseline is authoritative.

Last Updated: April 2026 Modules Required: ExchangeOnlineManagement (for Security & Compliance PowerShell — Connect-IPPSSession and the *-DlpCompliance* cmdlet surface)


Scope and Limitations

Capability PowerShell Coverage Notes
Create / read / update / delete DLP policies and rules ✅ Full New-DlpCompliancePolicy, Set-DlpComplianceRule
Set policy mode (Test / Enable / Disable) ✅ Full Set-DlpCompliancePolicy -Mode <value>
Configure rule actions (block, audit, override) ✅ Full Via New-/Set-DlpComplianceRule parameters
Manage Endpoint DLP global settings (restricted apps, USB groups, service domains) ⚠️ Partial / portal-preferred Cmdlet support is limited and changing; the Purview portal is the supported path
Onboard / inventory devices ❌ None (DLP cmdlets) Use Defender for Endpoint Graph API or Intune endpoint security policy
Edge for Business inline AI DLP toggles ❌ None (DLP cmdlets) Portal-only for the location toggle; rule actions configurable via cmdlets once location is enabled
Global Secure Access security profiles ❌ None (DLP cmdlets) Use Microsoft Graph (Microsoft.Graph.NetworkAccess)

PowerShell is best used to create policies idempotently, export evidence, and validate state. UI configuration remains required for global Endpoint DLP settings.


Prerequisites

# REQUIRED: Replace <version> with the version approved by your CAB.
# See the PowerShell baseline for the canonical pinning pattern.
Install-Module -Name ExchangeOnlineManagement `
    -RequiredVersion '<version>' `
    -Repository PSGallery `
    -Scope CurrentUser `
    -AllowClobber `
    -AcceptLicense

Required role: Purview Compliance Admin (canonical) — assigned via Microsoft Purview role groups (Compliance Administrator or Compliance Data Administrator).


Connection Helper (Sovereign-Cloud Aware)

function Connect-FsiCompliance {
    <#
    .SYNOPSIS
        Connects to Security & Compliance PowerShell with sovereign-cloud awareness.
    #>
    [CmdletBinding()]
    param(
        [ValidateSet('Commercial','GCC','GCCHigh','DoD')]
        [string]$Cloud = 'Commercial',

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

    $connectionUri = switch ($Cloud) {
        'Commercial' { 'https://ps.compliance.protection.outlook.com/powershell-liveid/' }
        'GCC'        { 'https://ps.compliance.protection.outlook.com/powershell-liveid/' }
        'GCCHigh'    { 'https://ps.compliance.protection.office365.us/powershell-liveid/' }
        'DoD'        { 'https://l5.ps.compliance.protection.office365.us/powershell-liveid/' }
    }

    try {
        Connect-IPPSSession -UserPrincipalName $UserPrincipalName -ConnectionUri $connectionUri -ErrorAction Stop
        Write-Host "[OK] Connected to Security & Compliance ($Cloud)" -ForegroundColor Green
    }
    catch {
        throw "Failed to connect to Security & Compliance ($Cloud): $($_.Exception.Message)"
    }
}

Script 1 — Create or Update the Endpoint DLP Policy (Idempotent)

<#
.SYNOPSIS
    Creates or updates an Endpoint DLP policy for FSI financial-data protection.

.DESCRIPTION
    Idempotent: detects existing policy by name and updates rules in place.
    Defaults to TestWithNotifications for safe pilot rollout.
    Supports -WhatIf and -Confirm.

.PARAMETER PolicyName
    Zone-prefixed policy name, e.g. FSI-Z3-Endpoint-FinancialData.

.PARAMETER Zone
    Governance zone: 1 (Personal), 2 (Team), or 3 (Enterprise). Drives action defaults.

.PARAMETER Mode
    Policy mode: TestWithNotifications | TestWithoutNotifications | Enable | Disable.
    Begin with TestWithNotifications; promote to Enable only after pilot validation.

.EXAMPLE
    .\Set-Control117Policy.ps1 -PolicyName 'FSI-Z3-Endpoint-FinancialData' -Zone 3 -Mode 'TestWithNotifications' -WhatIf
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
    [Parameter(Mandatory)][string]$PolicyName,
    [Parameter(Mandatory)][ValidateSet(1,2,3)][int]$Zone,
    [ValidateSet('TestWithNotifications','TestWithoutNotifications','Enable','Disable')]
    [string]$Mode = 'TestWithNotifications',
    [string]$ContactEmail = 'compliance@contoso.com',
    [string]$EvidencePath = ".\Control-1.17_PolicyState_$(Get-Date -Format 'yyyyMMdd-HHmmss').json"
)

$ErrorActionPreference = 'Stop'

# Zone-driven action defaults
$blockAccess         = $Zone -ge 2
$allowOverride       = $Zone -eq 2
$endpointActions     = switch ($Zone) {
    1 { 'Audit' }
    2 { 'Block' }
    3 { 'Block' }
}

try {
    Write-Host "[Step 1/4] Locating existing policy '$PolicyName'..." -ForegroundColor Cyan
    $existing = Get-DlpCompliancePolicy -Identity $PolicyName -ErrorAction SilentlyContinue

    if ($existing) {
        Write-Host "  Found existing policy (current mode: $($existing.Mode))." -ForegroundColor Yellow
        if ($PSCmdlet.ShouldProcess($PolicyName, "Update DLP policy mode to $Mode")) {
            Set-DlpCompliancePolicy -Identity $PolicyName -Mode $Mode | Out-Null
            Write-Host "  [OK] Policy mode set to $Mode" -ForegroundColor Green
        }
    }
    else {
        if ($PSCmdlet.ShouldProcess($PolicyName, "Create new Endpoint DLP policy (Zone $Zone, mode $Mode)")) {
            New-DlpCompliancePolicy -Name $PolicyName `
                -Mode $Mode `
                -EndpointDlpLocation 'All' `
                -Comment "FSI Endpoint DLP — Zone $Zone — created $(Get-Date -Format 'u')" | Out-Null
            Write-Host "  [OK] Policy created" -ForegroundColor Green
        }
    }

    $ruleName = "$PolicyName-FinancialData"
    Write-Host "[Step 2/4] Locating rule '$ruleName'..." -ForegroundColor Cyan
    $existingRule = Get-DlpComplianceRule -Identity $ruleName -ErrorAction SilentlyContinue

    $sits = @(
        @{Name='U.S. Social Security Number (SSN)';   minCount=1; confidencelevel='High'}
        @{Name='Credit Card Number';                  minCount=1; confidencelevel='High'}
        @{Name='U.S. Bank Account Number';            minCount=1; confidencelevel='High'}
        @{Name='ABA Routing Number';                  minCount=1; confidencelevel='High'}
    )

    $policyTip = "This content contains sensitive financial information. " +
                 "Per firm policy, this action is restricted. " +
                 "Contact $ContactEmail for assistance."

    $ruleParams = @{
        ContentContainsSensitiveInformation = $sits
        BlockAccess                         = $blockAccess
        AllowOverride                       = $allowOverride
        NotifyUser                          = @('Owner','LastModifier','SiteAdmin')
        NotifyPolicyTipCustomText           = $policyTip
        GenerateAlert                       = 'SiteAdmin'
        ReportSeverityLevel                 = 'High'
    }

    if ($existingRule) {
        if ($PSCmdlet.ShouldProcess($ruleName, "Update DLP rule (Zone $Zone)")) {
            Set-DlpComplianceRule -Identity $ruleName @ruleParams | Out-Null
            Write-Host "  [OK] Rule updated" -ForegroundColor Green
        }
    }
    else {
        if ($PSCmdlet.ShouldProcess($ruleName, "Create DLP rule (Zone $Zone)")) {
            New-DlpComplianceRule -Name $ruleName -Policy $PolicyName @ruleParams | Out-Null
            Write-Host "  [OK] Rule created" -ForegroundColor Green
        }
    }

    Write-Host "[Step 3/4] Re-reading policy state..." -ForegroundColor Cyan
    $finalPolicy = Get-DlpCompliancePolicy -Identity $PolicyName
    $finalRule   = Get-DlpComplianceRule   -Identity $ruleName

    Write-Host "[Step 4/4] Writing evidence artefact..." -ForegroundColor Cyan
    $evidence = [PSCustomObject]@{
        Control          = '1.17'
        PolicyName       = $PolicyName
        Zone             = $Zone
        Mode             = $finalPolicy.Mode
        EndpointLocation = $finalPolicy.EndpointDlpLocation -join ','
        RuleName         = $finalRule.Name
        BlockAccess      = $finalRule.BlockAccess
        AllowOverride    = $finalRule.AllowOverride
        NotifyUser       = $finalRule.NotifyUser -join ','
        AlertSeverity    = $finalRule.ReportSeverityLevel
        Sits             = ($sits | ForEach-Object { $_.Name }) -join ','
        CapturedUtc      = (Get-Date).ToUniversalTime().ToString('o')
        CapturedBy       = $env:USERNAME
        TenantUpn        = (Get-ConnectionInformation | Select-Object -ExpandProperty UserPrincipalName -First 1)
    }

    $evidence | ConvertTo-Json -Depth 4 | Out-File -FilePath $EvidencePath -Encoding utf8
    $hash = (Get-FileHash -Path $EvidencePath -Algorithm SHA256).Hash
    "$hash  $(Split-Path $EvidencePath -Leaf)" | Out-File -FilePath "$EvidencePath.sha256" -Encoding ascii

    Write-Host "[DONE] Evidence: $EvidencePath" -ForegroundColor Green
    Write-Host "[DONE] SHA-256:  $hash" -ForegroundColor Green
}
catch {
    Write-Error "Control 1.17 policy configuration failed: $($_.Exception.Message)"
    Write-Host $_.ScriptStackTrace -ForegroundColor DarkGray
    throw
}

Script 2 — Inventory and Export Endpoint DLP Posture (Read-Only Evidence)

<#
.SYNOPSIS
    Read-only export of all Endpoint DLP policies and rules for compliance evidence.

.EXAMPLE
    .\Get-Control117Posture.ps1 -OutputDir 'C:\Audit\1.17'
#>
[CmdletBinding()]
param(
    [string]$OutputDir = ".\Control-1.17-Posture_$(Get-Date -Format 'yyyyMMdd')"
)

$ErrorActionPreference = 'Stop'

if (-not (Test-Path $OutputDir)) {
    New-Item -ItemType Directory -Path $OutputDir | Out-Null
}

try {
    Write-Host "Enumerating Endpoint DLP policies..." -ForegroundColor Cyan
    $allPolicies = Get-DlpCompliancePolicy
    $endpointPolicies = $allPolicies | Where-Object {
        $_.EndpointDlpLocation -and $_.EndpointDlpLocation.Count -gt 0
    }

    if (-not $endpointPolicies) {
        Write-Warning "No Endpoint DLP policies found. Control 1.17 is NOT configured."
    }

    $rows = foreach ($p in $endpointPolicies) {
        $rules = Get-DlpComplianceRule -Policy $p.Name
        foreach ($r in $rules) {
            [PSCustomObject]@{
                PolicyName        = $p.Name
                PolicyMode        = $p.Mode
                PolicyEnabled     = $p.Enabled
                EndpointLocation  = $p.EndpointDlpLocation -join ','
                RuleName          = $r.Name
                Priority          = $r.Priority
                BlockAccess       = $r.BlockAccess
                AllowOverride     = $r.AllowOverride
                NotifyUser        = ($r.NotifyUser -join ',')
                ReportSeverity    = $r.ReportSeverityLevel
                ContentContains   = ($r.ContentContainsSensitiveInformation | ForEach-Object { $_.Name }) -join ';'
                CapturedUtc       = (Get-Date).ToUniversalTime().ToString('o')
            }
        }
    }

    $csvPath = Join-Path $OutputDir 'Control-1.17_Posture.csv'
    $rows | Export-Csv -Path $csvPath -NoTypeInformation -Encoding utf8

    $jsonPath = Join-Path $OutputDir 'Control-1.17_Posture.json'
    $rows | ConvertTo-Json -Depth 5 | Out-File -FilePath $jsonPath -Encoding utf8

    foreach ($file in @($csvPath, $jsonPath)) {
        $hash = (Get-FileHash -Path $file -Algorithm SHA256).Hash
        "$hash  $(Split-Path $file -Leaf)" | Out-File -FilePath "$file.sha256" -Encoding ascii
    }

    Write-Host "[OK] Posture exported: $OutputDir" -ForegroundColor Green
    Write-Host "  Policies discovered: $($endpointPolicies.Count)" -ForegroundColor Green
    Write-Host "  Rules captured:      $($rows.Count)" -ForegroundColor Green
}
catch {
    Write-Error "Posture export failed: $($_.Exception.Message)"
    throw
}

Script 3 — Validation Script (Pass/Fail Gates for Pre-Audit)

<#
.SYNOPSIS
    Validates Control 1.17 posture against zone-based pass/fail criteria.

.PARAMETER MinPolicies
    Minimum number of Endpoint DLP policies expected (default 1).

.PARAMETER RequireEnforcement
    If specified, fails any policy not in 'Enable' mode.

.EXAMPLE
    .\Test-Control117.ps1 -RequireEnforcement
#>
[CmdletBinding()]
param(
    [int]$MinPolicies = 1,
    [switch]$RequireEnforcement
)

$ErrorActionPreference = 'Stop'
$results = [System.Collections.Generic.List[object]]::new()

function Add-Check {
    param($Name, $Status, $Detail)
    $results.Add([PSCustomObject]@{ Check = $Name; Status = $Status; Detail = $Detail })
}

try {
    $endpointPolicies = Get-DlpCompliancePolicy | Where-Object {
        $_.EndpointDlpLocation -and $_.EndpointDlpLocation.Count -gt 0
    }

    # Check 1: At least one Endpoint DLP policy
    if ($endpointPolicies.Count -ge $MinPolicies) {
        Add-Check 'Endpoint DLP policy exists' 'PASS' "$($endpointPolicies.Count) policy(ies) found"
    } else {
        Add-Check 'Endpoint DLP policy exists' 'FAIL' "Found $($endpointPolicies.Count); required >= $MinPolicies"
    }

    # Check 2: Enforcement mode (optional gate)
    $enabled = $endpointPolicies | Where-Object { $_.Mode -eq 'Enable' }
    if ($RequireEnforcement) {
        if ($enabled.Count -eq $endpointPolicies.Count -and $enabled.Count -gt 0) {
            Add-Check 'All policies enforced' 'PASS' "$($enabled.Count)/$($endpointPolicies.Count) in Enable mode"
        } else {
            Add-Check 'All policies enforced' 'FAIL' "$($enabled.Count)/$($endpointPolicies.Count) in Enable mode"
        }
    } else {
        Add-Check 'All policies enforced' 'INFO' "$($enabled.Count)/$($endpointPolicies.Count) in Enable mode (gate not required)"
    }

    # Check 3: Each policy has a rule with BlockAccess for Z2/Z3 naming convention
    foreach ($p in $endpointPolicies) {
        $rules = Get-DlpComplianceRule -Policy $p.Name
        if ($rules.Count -eq 0) {
            Add-Check "Policy '$($p.Name)' has rules" 'FAIL' 'No rules attached'
            continue
        }
        $blockingRules = $rules | Where-Object { $_.BlockAccess -eq $true }
        if ($p.Name -match 'Z[23]' -and $blockingRules.Count -eq 0) {
            Add-Check "Policy '$($p.Name)' enforces blocking" 'FAIL' 'Z2/Z3 naming convention but no blocking rule'
        } else {
            Add-Check "Policy '$($p.Name)' enforces blocking" 'PASS' "$($blockingRules.Count) blocking rule(s)"
        }
    }

    # Output
    Write-Host "`n=== Control 1.17 Validation ===" -ForegroundColor Cyan
    $results | Format-Table -AutoSize

    $failures = ($results | Where-Object { $_.Status -eq 'FAIL' }).Count
    if ($failures -gt 0) {
        Write-Host "[OVERALL] FAIL ($failures failures)" -ForegroundColor Red
        exit 1
    }
    Write-Host "[OVERALL] PASS" -ForegroundColor Green
}
catch {
    Write-Error "Validation failed: $($_.Exception.Message)"
    throw
}

Manual Steps That PowerShell Cannot Cover

These configuration items must be performed in the portal (see Portal Walkthrough):

  1. Restricted apps and app groupsPurview > Endpoint DLP settings > Restricted apps
  2. Removable storage device groups (USB allowlist by Vendor/Product ID) — Purview > Endpoint DLP settings > Removable storage device groups
  3. Service domain groups (allow/block lists for cloud and AI services) — Purview > Endpoint DLP settings > Service domains
  4. Browser and domain restrictions (unallowed browsers) — Purview > Endpoint DLP settings > Browser and domain restrictions to sensitive data
  5. Just-in-time protection toggle and fallback action — Purview > Endpoint DLP settings > Just-in-time protection
  6. Edge for Business location toggle on each DLP policy — Purview > DLP > Policies > <policy> > Locations
  7. Global Secure Access security profilesMicrosoft Entra > Global Secure Access > Secure > Security profiles
  8. Defender for Endpoint device onboardingMicrosoft Intune > Endpoint security > Endpoint detection and response

Capture screenshots of each portal-only setting and store under maintainers-local/tenant-evidence/1.17/ for audit evidence.


Cleanup

Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue

Back to Control 1.17 | Portal Walkthrough | Verification Testing | Troubleshooting