Control 1.9 — PowerShell Setup: Data Retention and Deletion Policies
Control: 1.9 — Data Retention and Deletion Policies Pillar: Pillar 1 — Security Audience: Purview Records Manager, Purview Compliance Admin, Power Platform Admin Companion playbooks: Portal walkthrough · Verification & testing · Troubleshooting
Read the FSI PowerShell baseline first
Before running anything below, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for ExchangeOnlineManagement version pinning, mutation safety (-WhatIf / SupportsShouldProcess), and SHA-256 evidence emission. Snippets below assume that baseline is in effect.
This playbook automates Control 1.9 via Security & Compliance PowerShell (the IPPS endpoint exposed by
ExchangeOnlineManagement). Every mutating command below is wrapped in safety checks, supports-WhatIf, and writes evidence artifacts intended for examiner production.Hedging note. This automation helps meet, and is recommended to support compliance with, SEC 17 CFR 240.17a-4, FINRA 4511, SOX §404 / §802, GLBA 501(b), CFTC 1.31, and IRS recordkeeping rules. It does not by itself satisfy 17a-4(f) — see the parent control's "SEC 17a-4(f) caveat."
§1. Pre-flight
1.1 Module and edition
| Module | Required version | Edition |
|---|---|---|
ExchangeOnlineManagement |
Pin to a CAB-approved stable production release | Desktop (5.1) or Core (7.2+) |
# Pin to a CAB-approved version. Substitute <version>.
Install-Module -Name ExchangeOnlineManagement `
-RequiredVersion '<version>' `
-Repository PSGallery `
-Scope CurrentUser `
-AllowClobber `
-AcceptLicense
1.2 Required role
The signed-in account must hold Purview Records Manager and Purview Compliance Admin to author labels, policies, and apply Preservation Lock. For Zone 3 lock application, require a second-person review (named in the change ticket) before the lock cmdlet runs.
1.3 Connect to Security & Compliance
Verify the session:
Get-ConnectionInformation | Where-Object { $_.ConnectionUri -like '*compliance*' } |
Select-Object UserPrincipalName, ConnectionUri, State
1.4 Common parameters used below
$EvidenceRoot = 'C:\fsi-evidence\1.9'
$null = New-Item -ItemType Directory -Path $EvidenceRoot -Force
$Stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
$ReviewerStage1 = 'records-mgmt-stage1@contoso.com'
$ReviewerStage2 = 'compliance-stage2@contoso.com'
$ReviewerStage3 = 'legal-stage3@contoso.com'
§2. Idempotent label creation
New-ComplianceTag fails if the tag exists. Wrap creates in an idempotency check and use Set-ComplianceTag for updates. Set-ComplianceTag cannot reduce retention on a record/regulatory label — see §6.
2.1 Helper: ensure a label exists
function Ensure-ComplianceTag {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)] [string] $Name,
[Parameter(Mandatory)] [string] $Comment,
[Parameter(Mandatory)] [int] $RetentionDurationDays,
[Parameter(Mandatory)] [ValidateSet('Keep','Delete','KeepAndDelete')] [string] $RetentionAction,
[bool] $IsRecordLabel = $false,
[bool] $Regulatory = $false,
[string] $ReviewerEmail
)
$existing = Get-ComplianceTag -Identity $Name -ErrorAction SilentlyContinue
if ($existing) {
Write-Host "[SKIP] Label '$Name' already exists." -ForegroundColor Yellow
return $existing
}
if ($PSCmdlet.ShouldProcess($Name, "Create retention label")) {
$params = @{
Name = $Name
Comment = $Comment
RetentionDuration = $RetentionDurationDays
RetentionAction = $RetentionAction
RetentionType = 'CreationAgeInDays'
}
if ($IsRecordLabel) { $params.IsRecordLabel = $true }
if ($Regulatory) { $params.Regulatory = $true }
if ($ReviewerEmail) { $params.ReviewerEmail = $ReviewerEmail }
New-ComplianceTag @params -ErrorAction Stop |
Tee-Object -FilePath (Join-Path $EvidenceRoot "label-$Name-$Stamp.json")
Write-Host "[CREATED] Label '$Name'." -ForegroundColor Green
}
}
2.2 Create the FSI label set
# Communications classification (3 years)
Ensure-ComplianceTag -Name 'FSI-Agent-Communications-3Year' `
-Comment 'Agent transcript classified as a communication under SEC 17a-4(b)(4). 3-year retention.' `
-RetentionDurationDays 1095 `
-RetentionAction KeepAndDelete `
-IsRecordLabel $true `
-ReviewerEmail $ReviewerStage2
# Books-and-records (6 years)
Ensure-ComplianceTag -Name 'FSI-Agent-BooksRecords-6Year' `
-Comment 'Agent transcript that evidences or generates a 17a-3 record. 6-year retention.' `
-RetentionDurationDays 2190 `
-RetentionAction KeepAndDelete `
-IsRecordLabel $true `
-ReviewerEmail $ReviewerStage2
# Regulatory record (10 years, immutable)
Ensure-ComplianceTag -Name 'FSI-Agent-RegRecord-10Year' `
-Comment 'Agent audit metadata and regulated artifacts. 10-year retention; immutable.' `
-RetentionDurationDays 3650 `
-RetentionAction KeepAndDelete `
-IsRecordLabel $true `
-Regulatory $true `
-ReviewerEmail $ReviewerStage3
# Agent configuration (6 years, automatic deletion)
Ensure-ComplianceTag -Name 'FSI-Agent-Configuration-6Year' `
-Comment 'Agent definitions, version history, exports. 6-year retention.' `
-RetentionDurationDays 2190 `
-RetentionAction Delete
Regulatory record is one-way
Once Regulatory = $true is set on a label, the label cannot be unmarked or deleted, items it has been applied to cannot have the label removed, and retention can only be extended. Treat creation of a regulatory label as a SEV-2 change.
§3. Idempotent retention-policy creation for AI surfaces
The exact location parameter names for Microsoft 365 Copilot and AI experiences, Enterprise AI Apps, and Other AI Apps evolve as Microsoft adds capabilities. Always cross-check against New-RetentionCompliancePolicy immediately before a production change.
3.1 Helper: ensure a policy + rule exist
function Ensure-RetentionPolicy {
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)] [string] $PolicyName,
[Parameter(Mandatory)] [string] $RuleName,
[Parameter(Mandatory)] [int] $RetentionDurationDays,
[Parameter(Mandatory)] [hashtable] $LocationParams,
[string] $ContentMatchQuery
)
$existing = Get-RetentionCompliancePolicy -Identity $PolicyName -ErrorAction SilentlyContinue
if (-not $existing) {
if ($PSCmdlet.ShouldProcess($PolicyName, "Create retention policy")) {
New-RetentionCompliancePolicy -Name $PolicyName @LocationParams -ErrorAction Stop |
Out-Null
Write-Host "[CREATED] Policy '$PolicyName'." -ForegroundColor Green
}
} else {
Write-Host "[SKIP] Policy '$PolicyName' already exists." -ForegroundColor Yellow
}
$rule = Get-RetentionComplianceRule -Policy $PolicyName -ErrorAction SilentlyContinue |
Where-Object Name -eq $RuleName
if (-not $rule) {
if ($PSCmdlet.ShouldProcess($RuleName, "Create retention rule")) {
$ruleParams = @{
Name = $RuleName
Policy = $PolicyName
RetentionDuration = $RetentionDurationDays
RetentionComplianceAction = 'KeepAndDelete'
}
if ($ContentMatchQuery) { $ruleParams.ContentMatchQuery = $ContentMatchQuery }
New-RetentionComplianceRule @ruleParams -ErrorAction Stop | Out-Null
Write-Host "[CREATED] Rule '$RuleName'." -ForegroundColor Green
}
} else {
Write-Host "[SKIP] Rule '$RuleName' already exists." -ForegroundColor Yellow
}
}
3.2 Microsoft 365 Copilot and AI experiences
# BLOCKED: The PowerShell location parameter for "Microsoft Copilot experiences"
# retention has not been confirmed against the published New-RetentionCompliancePolicy
# syntax. ModernGroupLocation targets Microsoft 365 Group mailboxes — NOT Copilot AI
# interaction content — and must not be used here.
#
# ACTION REQUIRED before un-commenting:
# Run: Get-Help New-RetentionCompliancePolicy -Full | Out-String |
# Select-String 'Copilot','AIApp','Location'
# Confirm the correct -Location parameter for your pinned ExchangeOnlineManagement
# module version.
#
# RECOMMENDED: Create FSI-Copilot-AIExperiences-Retention via the Microsoft Purview
# portal (Purview → Data lifecycle management → Retention policies → New retention
# policy → select "Microsoft Copilot experiences" as the location) until the correct
# PowerShell parameter is confirmed for your module version.
Write-Error ("ACTION REQUIRED: Confirm the Copilot/AI experiences retention location " +
"parameter name from 'Get-Help New-RetentionCompliancePolicy -Full' before " +
"running this section. ModernGroupLocation targets M365 Group mailboxes, NOT " +
"Copilot AI interaction content. Use the Purview portal to create the " +
"FSI-Copilot-AIExperiences-Retention policy until the correct parameter is " +
"confirmed.") -ErrorAction Stop
3.3 Agent-related Exchange email retention (content-match safety net)
Ensure-RetentionPolicy -PolicyName 'FSI-AgentEmail-Retention' `
-RuleName 'FSI-AgentEmail-6Year' `
-RetentionDurationDays 2190 `
-LocationParams @{ ExchangeLocation = 'All' } `
-ContentMatchQuery '(Copilot OR "agent" OR "AI assistant" OR chatbot)'
3.4 SharePoint / OneDrive container retention
Ensure-RetentionPolicy -PolicyName 'FSI-AgentSharePoint-Retention' `
-RuleName 'FSI-AgentSharePoint-6Year' `
-RetentionDurationDays 2190 `
-LocationParams @{ SharePointLocation = 'All'; OneDriveLocation = 'All' }
§4. Publish the FSI agent labels
$LabelPolicyName = 'FSI-Agent-Labels-Publish-Zone3'
$existing = Get-RetentionCompliancePolicy -Identity $LabelPolicyName -ErrorAction SilentlyContinue
if (-not $existing) {
New-RetentionCompliancePolicy -Name $LabelPolicyName `
-ExchangeLocation 'All' `
-SharePointLocation 'All' `
-OneDriveLocation 'All' `
-ModernGroupLocation 'All' `
-Comment 'Publishes FSI Control 1.9 retention labels to Zone 3 in-scope locations.' |
Out-Null
}
$AgentLabels = Get-ComplianceTag | Where-Object { $_.Name -like 'FSI-Agent-*' }
$RuleName = 'FSI-Agent-Labels-Rule'
$rule = Get-RetentionComplianceRule -Policy $LabelPolicyName -ErrorAction SilentlyContinue |
Where-Object Name -eq $RuleName
if (-not $rule) {
New-RetentionComplianceRule -Name $RuleName `
-Policy $LabelPolicyName `
-PublishComplianceTag $AgentLabels.Name | Out-Null
}
Verify distribution:
Get-RetentionCompliancePolicy -Identity $LabelPolicyName |
Select-Object Name, Mode, Enabled, DistributionStatus
§5. Audit-log retention for deletion events
$AuditPolicyName = 'FSI-DeletionAudit-10Year'
$existing = Get-UnifiedAuditLogRetentionPolicy -Identity $AuditPolicyName -ErrorAction SilentlyContinue
if (-not $existing) {
New-UnifiedAuditLogRetentionPolicy -Name $AuditPolicyName `
-Description 'Extended retention for retention/disposition/deletion events (Control 1.9).' `
-Operations FileDeleted, FileVersionRecycled, HardDelete, SoftDelete, MoveToDeletedItems, `
ApplyRetentionLabel, RemoveRetentionLabel, `
NewRetentionComplianceRule, SetRetentionCompliancePolicy `
-RetentionDuration TenYears `
-Priority 100
}
Audit-log retention beyond Microsoft 365 Audit (Standard) requires Microsoft 365 Audit (Premium) and the appropriate per-user license. See Control 1.7.
§6. Preservation Lock — irreversible
STOP. Read this entire section before running the lock cmdlet. After this completes, the policy cannot be disabled, deleted, or shortened by anyone — including Microsoft Support.
6.1 Pre-lock validation
function Test-PolicyReadyForLock {
param([Parameter(Mandatory)] [string] $PolicyName)
$p = Get-RetentionCompliancePolicy -Identity $PolicyName -DistributionDetail
$r = Get-RetentionComplianceRule -Policy $PolicyName
$issues = @()
if (-not $p) { $issues += 'Policy not found.' }
if ($p.Enabled -ne $true) { $issues += 'Policy is not Enabled.' }
if ($p.Mode -ne 'Enforce') { $issues += "Policy Mode is '$($p.Mode)' (not 'Enforce')." }
if ($p.DistributionStatus -ne 'Success') { $issues += "DistributionStatus is '$($p.DistributionStatus)'." }
if ($p.RetentionComplianceLockType -eq 'Lock') { $issues += 'Policy is already Locked.' }
if (-not $r) { $issues += 'No rules attached to policy.' }
foreach ($rule in $r) {
if ($rule.RetentionDuration -lt 1095) {
$issues += "Rule '$($rule.Name)' retention is $($rule.RetentionDuration) days (< 3 years floor)."
}
}
if ($issues) {
Write-Host "[BLOCK] '$PolicyName' is NOT ready for lock:" -ForegroundColor Red
$issues | ForEach-Object { Write-Host " - $_" -ForegroundColor Red }
return $false
}
Write-Host "[OK] '$PolicyName' passed pre-lock validation." -ForegroundColor Green
return $true
}
6.2 Apply Preservation Lock (with explicit confirmation)
function Lock-RetentionPolicy {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $PolicyName,
[Parameter(Mandatory)] [string] $ChangeTicket,
[Parameter(Mandatory)] [string] $SecondReviewerUpn
)
if (-not (Test-PolicyReadyForLock -PolicyName $PolicyName)) { return }
$confirmation = Read-Host "Type the policy name '$PolicyName' to confirm IRREVERSIBLE Preservation Lock"
if ($confirmation -ne $PolicyName) {
Write-Host "[ABORT] Confirmation did not match. No action taken." -ForegroundColor Yellow
return
}
if ($PSCmdlet.ShouldProcess($PolicyName,
"Apply Preservation Lock (IRREVERSIBLE) under ticket $ChangeTicket, reviewed by $SecondReviewerUpn")) {
Set-RetentionCompliancePolicy -Identity $PolicyName `
-RetentionComplianceLockType Lock -ErrorAction Stop
$evidence = Get-RetentionCompliancePolicy -Identity $PolicyName |
Select-Object Name, Mode, Enabled, RetentionComplianceLockType, WhenChanged
$evidence | ConvertTo-Json |
Out-File (Join-Path $EvidenceRoot "lock-$PolicyName-$Stamp.json")
Write-Host "[LOCKED] '$PolicyName'. Evidence saved." -ForegroundColor Green
}
}
# Example (Zone 3 only):
# Lock-RetentionPolicy -PolicyName 'FSI-Copilot-AIExperiences-Retention' `
# -ChangeTicket 'CHG-0123456' -SecondReviewerUpn 'records.mgr@contoso.com'
6.3 Locked-policy gotchas
| Gotcha | Reality | Mitigation |
|---|---|---|
| "Locked" prevents all changes | False — you can still add locations and extend retention | Document permitted change paths in the change ticket |
| Lock can be removed by Global Admin | False — no role can unlock | Treat lock as a CAB SEV-2 change with second-person review |
| Lock applies to label policies too | True — Set-RetentionCompliancePolicy -RetentionComplianceLockType Lock works on the policy that publishes labels |
Lock the publish-policy after labels stabilize |
| Lock affects existing rules retroactively | True — but new rules added after lock are also locked | Add all rules before locking |
| Lock survives tenant-to-tenant migration | Microsoft does not support migrating locked policies in tenant moves | Plan the records strategy with the tenant lifecycle in mind |
§7. Evidence export
$labels = Get-ComplianceTag | Where-Object { $_.Name -like 'FSI-Agent-*' } |
Select-Object Name, RetentionDuration, RetentionAction, IsRecordLabel, Regulatory, ReviewerEmail, WhenCreated, WhenChanged
$labels | Export-Csv (Join-Path $EvidenceRoot "labels-$Stamp.csv") -NoTypeInformation
$policies = Get-RetentionCompliancePolicy | Where-Object { $_.Name -like 'FSI-*' } |
Select-Object Name, Mode, Enabled, DistributionStatus, RetentionComplianceLockType, WhenCreated, WhenChanged
$policies | Export-Csv (Join-Path $EvidenceRoot "policies-$Stamp.csv") -NoTypeInformation
$rules = foreach ($p in $policies) {
Get-RetentionComplianceRule -Policy $p.Name -ErrorAction SilentlyContinue |
Select-Object @{n='PolicyName';e={$p.Name}}, Name, RetentionDuration, RetentionComplianceAction, ContentMatchQuery
}
$rules | Export-Csv (Join-Path $EvidenceRoot "rules-$Stamp.csv") -NoTypeInformation
# SHA-256 evidence stamp (per FSI baseline)
Get-ChildItem -Path $EvidenceRoot -Filter "*-$Stamp.*" | ForEach-Object {
[PSCustomObject]@{
File = $_.Name
SHA256 = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash
}
} | Export-Csv (Join-Path $EvidenceRoot "evidence-manifest-$Stamp.csv") -NoTypeInformation
§8. Cleanup
Cross-references
- Control 1.7 — Comprehensive Audit Logging and Compliance
- Microsoft Learn:
New-ComplianceTag - Microsoft Learn:
New-RetentionCompliancePolicy - Microsoft Learn: Use Preservation Lock
Back to Control 1.9 · Portal walkthrough · Verification & testing · Troubleshooting
Updated: May 2026 | Version: v1.6.2 | UI Verification Status: Current