Skip to content

Control 3.11: Record Keeping and Books-and-Records Compliance — PowerShell Setup

Automation scripts for implementing and managing records management for Copilot-generated content, including Preservation Lock configuration for the audit-trail alternative path under SEC Rule 17a-4(f)(2)(ii)(A) and retention label deployment for regulatory records.

Prerequisites

  • Modules: ExchangeOnlineManagement
  • Permissions: Records Management Administrator, Compliance Administrator
  • PowerShell: Version 7.x recommended

Connect to Required Services

Import-Module ExchangeOnlineManagement
Connect-IPPSSession -UserPrincipalName admin@contoso.com

Scripts

Script 1: Create Regulatory Record Retention Labels

# Create retention labels for books-and-records compliance
# These labels, when applied, mark items as regulatory records (immutable for SEC 17a-4 audit-trail alternative)
$labels = @(
    @{Name="SEC-17a4-Business-Communication-6yr"; Duration=2190; Description="SEC 17a-4 business communication records — 6 year retention"},
    @{Name="FINRA-4511-Client-Record-7yr"; Duration=2555; Description="FINRA 4511 client correspondence — 7 year retention"},
    @{Name="Investment-Recommendation-Record-7yr"; Duration=2555; Description="Investment recommendation records — 7 year retention"},
    @{Name="Marketing-Material-Record-3yr"; Duration=1095; Description="Marketing material records — 3 year retention"}
)

foreach ($label in $labels) {
    New-ComplianceTag `
        -Name $label.Name `
        -Comment $label.Description `
        -RetentionAction KeepAndDelete `
        -RetentionDuration $label.Duration `
        -RetentionType CreationAgeInDays `
        -IsRecordLabel $true `
        -Regulatory $true

    Write-Host "Created regulatory record label: $($label.Name)" -ForegroundColor Green
}

Script 2: Publish Retention Labels to All Locations

# Publish all regulatory record labels to relevant locations
$labelNames = @(
    "SEC-17a4-Business-Communication-6yr",
    "FINRA-4511-Client-Record-7yr",
    "Investment-Recommendation-Record-7yr",
    "Marketing-Material-Record-3yr"
)

New-RetentionCompliancePolicy `
    -Name "FSI-Regulatory-Record-Labels" `
    -RetentionComplianceTag $labelNames `
    -ExchangeLocation "All" `
    -SharePointLocation "All" `
    -OneDriveLocation "All" `
    -ModernGroupLocation "All" `
    -Enabled $true

Write-Host "Regulatory record labels published to all locations" -ForegroundColor Green

Script 3: Enable Preservation Lock on Critical Policies

# Enable Preservation Lock for SEC 17a-4(f) WORM compliance or audit-trail alternative (Rule 17a-4(f)(2)(ii)(A))
# Preservation Lock makes the retention policy irreversible — required for both compliance paths
#
# IMPORTANT: Preservation Lock is IRREVERSIBLE.
# After locking:
# - The retention period cannot be shortened
# - The policy cannot be disabled or deleted
# - Additional locations can be added, but existing settings cannot be reduced
# - Even Global Administrators cannot reverse this action
#
# Only apply after the policy has been fully validated and approved.
$policyName = "FSI-Regulatory-Record-Labels"

Write-Warning "Preservation Lock is IRREVERSIBLE. The policy cannot be shortened or disabled after locking."
Write-Warning "This lock is required for the SEC Rule 17a-4(f)(2)(ii)(A) audit-trail alternative compliance path."
Write-Host "Policy to lock: $policyName" -ForegroundColor Yellow
Write-Host ""
Write-Host "Prerequisites before locking:"
Write-Host "  1. Verify all label configurations are correct"
Write-Host "  2. Verify retention durations are accurate"
Write-Host "  3. Obtain written approval from Records Management lead and Legal"
Write-Host "  4. Document the lock in the compliance record"
Write-Host ""
$confirm = Read-Host "Type 'CONFIRM-LOCK' to enable Preservation Lock (this is irreversible)"

if ($confirm -eq "CONFIRM-LOCK") {
    Set-RetentionCompliancePolicy `
        -Identity $policyName `
        -RestrictiveRetention $true

    $lockDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC"
    Write-Host "Preservation Lock ENABLED on '$policyName' at $lockDate" -ForegroundColor Red
    Write-Host "Document this action: Policy '$policyName' locked on $lockDate" -ForegroundColor Yellow
    Write-Host "This lock supports the SEC Rule 17a-4(f)(2)(ii)(A) audit-trail alternative compliance path" -ForegroundColor Cyan
} else {
    Write-Host "Preservation Lock NOT enabled — operation cancelled" -ForegroundColor Yellow
}

Script 4: Verify Preservation Lock Status

# Verify Preservation Lock status on all FSI retention policies
# Run regularly to confirm WORM / audit-trail alternative posture
$policies = Get-RetentionCompliancePolicy | Where-Object { $_.Name -like "*FSI*" -or $_.Name -like "*Record*" -or $_.Name -like "*Regulatory*" }

$report = foreach ($policy in $policies) {
    $rules = Get-RetentionComplianceRule -Policy $policy.Name -ErrorAction SilentlyContinue
    [PSCustomObject]@{
        PolicyName           = $policy.Name
        Enabled              = $policy.Enabled
        RestrictiveRetention = $policy.RestrictiveRetention  # TRUE = Preservation Lock enabled
        RetentionDays        = $rules.RetentionDuration
        IsRegulatory         = $rules.Regulatory
        DistributionStatus   = $policy.DistributionStatus
        LockStatus           = if ($policy.RestrictiveRetention) { "LOCKED - Audit-Trail Alt. Active" } else { "Unlocked" }
    }
}

Write-Host "=== Retention Policy Preservation Lock Status ==="
$report | Format-Table PolicyName, LockStatus, Enabled, DistributionStatus -AutoSize
Write-Host ""
Write-Host "Policies with Preservation Lock (RestrictiveRetention = True) support the"
Write-Host "SEC Rule 17a-4(f)(2)(ii)(A) audit-trail alternative compliance path."

$report | Export-Csv "RetentionPolicy_LockStatus_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host "Lock status report exported" -ForegroundColor Green

Script 5: Verify Audit Trail Coverage for Regulatory Records

# Verify that the audit trail captures required events for the Rule 17a-4(f)(2)(ii)(A) audit-trail alternative
# The audit trail must log all modifications, deletions, and access events for regulatory records
param(
    [int]$DaysBack = 7
)

$startDate = (Get-Date).AddDays(-$DaysBack)
$endDate = Get-Date

Write-Host "Searching audit log for regulatory record events (last $DaysBack days)..." -ForegroundColor Cyan

# Check for record status changes (items declared as regulatory records)
$recordDeclarations = Search-UnifiedAuditLog `
    -StartDate $startDate -EndDate $endDate `
    -Operations "RecordStatusChanged" `
    -ResultSize 100

# Check for blocked deletion/modification attempts on regulatory records
$blockedAttempts = Search-UnifiedAuditLog `
    -StartDate $startDate -EndDate $endDate `
    -Operations "ComplianceRecordChanged", "FileSensitivityLabelChanged" `
    -ResultSize 100

Write-Host ""
Write-Host "=== Audit Trail Coverage Report ==="
Write-Host "Regulatory record declarations (last $DaysBack days): $($recordDeclarations.Count)"
Write-Host "Compliance record change events (last $DaysBack days): $($blockedAttempts.Count)"
Write-Host ""

if ($recordDeclarations.Count -gt 0) {
    Write-Host "Audit trail IS capturing regulatory record events." -ForegroundColor Green
    Write-Host "This supports the SEC Rule 17a-4(f)(2)(ii)(A) audit-trail alternative compliance path."
} else {
    Write-Host "WARNING: No regulatory record events found in last $DaysBack days." -ForegroundColor Yellow
    Write-Host "Verify that regulatory record labels are being applied to Copilot content."
    Write-Host "If labels are applied but not appearing in audit, check audit log policy retention settings."
}

# Export audit trail evidence
$auditEvidence = @(
    $recordDeclarations | Select-Object CreationDate, UserIds, Operations, AuditData
    $blockedAttempts | Select-Object CreationDate, UserIds, Operations, AuditData
)
$auditEvidence | Export-Csv "AuditTrailEvidence_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
Write-Host "Audit trail evidence exported for compliance documentation" -ForegroundColor Green

Script 6: Records Management Compliance Report

# Generate a report of records management status across the tenant
$policies = Get-RetentionCompliancePolicy | Where-Object { $_.Name -like "*FSI*" -or $_.Name -like "*Record*" }

$report = foreach ($policy in $policies) {
    $rules = Get-RetentionComplianceRule -Policy $policy.Name
    [PSCustomObject]@{
        PolicyName           = $policy.Name
        Enabled              = $policy.Enabled
        RestrictiveRetention = $policy.RestrictiveRetention
        RetentionDays        = $rules.RetentionDuration
        IsRegulatory         = $rules.Regulatory
        DistributionStatus   = $policy.DistributionStatus
    }
}

Write-Host "Records Management Policy Report:"
$report | Format-Table -AutoSize
$report | Export-Csv "RecordsManagement_$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation

Scheduled Tasks

Task Frequency Script
Label deployment verification Monthly Script 6
New label creation As needed Script 1
Preservation Lock review Monthly Script 4
Audit trail coverage check Monthly Script 5
Policy compliance report Quarterly Script 6

Next Steps