Skip to content

Control 2.13 — PowerShell Setup: Documentation and Record Keeping

Control: 2.13 — Documentation and Record Keeping Pillar: Pillar 2 — Management Audience: SharePoint Admin, Purview Records Manager, Purview Compliance Admin, Power Platform Admin Companion playbooks: Portal Walkthrough · Verification & Testing · Troubleshooting

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 may show abbreviated patterns; the baseline is authoritative.

This playbook automates Control 2.13 via PnP.PowerShell (SharePoint site and library provisioning) and ExchangeOnlineManagement (the IPPS endpoint for Purview retention labels and policies). Every mutating command is wrapped in idempotency checks, supports -WhatIf, emits SHA-256 evidence hashes, and writes to a timestamped transcript.

Hedging note. This automation helps support, and is recommended for compliance with, FINRA 4511 (books and records), FINRA 3110 (supervision documentation), SEC 17a-3/4 (record creation and preservation), SOX §§302/404 (internal controls), GLBA 501(b) (safeguards), OCC 2011-12 / Fed SR 11-7 (model risk documentation), and CFTC 1.31 (regulatory records). It does not by itself satisfy SEC 17a-4(f) preservation requirements — see the parent control's SEC 17a-4 guidance.


§1. Pre-flight

1.1 Module and edition requirements

Module Required Edition Minimum Version Purpose
PnP.PowerShell v2+ Core only (PS 7.2+) 2.x (CAB-approved) SharePoint site, library, column, and content type management
ExchangeOnlineManagement Desktop (5.1) or Core (7.2+) CAB-approved stable release Purview retention labels, policies, auto-labeling
Microsoft.Graph Core (PS 7+) CAB-approved Optional: agent metadata retrieval
# REQUIRED: Replace <version> with the version approved by your CAB.
Install-Module -Name PnP.PowerShell `
    -RequiredVersion '<version>' `
    -Repository PSGallery `
    -Scope CurrentUser `
    -AllowClobber `
    -AcceptLicense

Install-Module -Name ExchangeOnlineManagement `
    -RequiredVersion '<version>' `
    -Repository PSGallery `
    -Scope CurrentUser `
    -AllowClobber `
    -AcceptLicense

1.2 Required roles

Task Required Role
SharePoint site and library provisioning SharePoint Admin
Retention label and policy creation Purview Records Manager + Purview Compliance Admin
Auto-labeling policy creation Purview Compliance Admin
Copilot Studio agent metadata export Power Platform Admin

1.3 Initialize evidence directory and transcript

$EvidenceRoot = 'C:\fsi-evidence\2.13'
$null = New-Item -ItemType Directory -Path $EvidenceRoot -Force
$Stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
$TranscriptPath = Join-Path $EvidenceRoot "transcript-2.13-$Stamp.log"
Start-Transcript -Path $TranscriptPath -Append

Write-Host "=== Control 2.13: Documentation and Record Keeping ===" -ForegroundColor Cyan
Write-Host "Evidence Root : $EvidenceRoot" -ForegroundColor Cyan
Write-Host "Timestamp     : $Stamp" -ForegroundColor Cyan
Write-Host "Transcript    : $TranscriptPath" -ForegroundColor Cyan

1.4 Common parameters

# SharePoint configuration
$TenantAdminUrl  = 'https://contoso-admin.sharepoint.com'        # Replace with your tenant admin URL
$SiteAlias       = 'AI-Governance'
$SiteUrl         = 'https://contoso.sharepoint.com/sites/AI-Governance'  # Replace

# Purview configuration
$ReviewerStage1  = 'records-mgmt@contoso.com'     # Replace with your org's records manager
$ReviewerStage2  = 'compliance@contoso.com'         # Replace with your org's compliance reviewer

# Evidence manifest
$ManifestPath    = Join-Path $EvidenceRoot "manifest-2.13-$Stamp.csv"
$ManifestEntries = [System.Collections.ArrayList]::new()

function Add-EvidenceToManifest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $FilePath,
        [Parameter(Mandatory)] [string] $Description
    )
    if (Test-Path $FilePath) {
        $hash = (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash
        $null = $ManifestEntries.Add([PSCustomObject]@{
            File        = (Split-Path $FilePath -Leaf)
            FullPath    = $FilePath
            SHA256      = $hash
            Description = $Description
            Timestamp   = (Get-Date).ToString('yyyy-MM-ddTHH:mm:ssZ')
        })
        Write-Host "[EVIDENCE] SHA-256: $hash — $Description" -ForegroundColor Green
    } else {
        Write-Host "[WARN] Evidence file not found: $FilePath" -ForegroundColor Yellow
    }
}

1.5 Connect to SharePoint and Purview

# Connect to SharePoint admin
Connect-PnPOnline -Url $TenantAdminUrl -Interactive

# Connect to Security & Compliance (Purview)
Connect-IPPSSession -ShowBanner:$false

# GCC High alternative:
# Connect-IPPSSession -ConnectionUri https://ps.compliance.protection.office365.us/powershell-liveid/ `
#     -AzureADAuthorizationEndpointUri https://login.microsoftonline.us/common -ShowBanner:$false

§2. Idempotent SharePoint site provisioning

2.1 Helper: ensure AI Governance site exists

function Ensure-AIGovernanceSite {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [string] $AdminUrl,
        [Parameter(Mandatory)] [string] $SiteAlias,
        [Parameter(Mandatory)] [string] $SiteUrl
    )

    $existing = Get-PnPTenantSite -Url $SiteUrl -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "[SKIP] AI Governance site already exists: $SiteUrl" -ForegroundColor Yellow
        return $existing
    }

    if ($PSCmdlet.ShouldProcess($SiteUrl, "Create AI Governance SharePoint site")) {
        $site = New-PnPSite -Type TeamSite -Title "AI Governance" -Alias $SiteAlias
        Write-Host "[CREATED] AI Governance site: $SiteUrl" -ForegroundColor Green
        return $site
    }
}

Ensure-AIGovernanceSite -AdminUrl $TenantAdminUrl -SiteAlias $SiteAlias -SiteUrl $SiteUrl

2.2 Helper: ensure document library exists

function Ensure-DocumentLibrary {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [string] $SiteUrl,
        [Parameter(Mandatory)] [string] $LibraryName,
        [string] $Description = ''
    )

    Connect-PnPOnline -Url $SiteUrl -Interactive
    $existing = Get-PnPList -Identity $LibraryName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "[SKIP] Library '$LibraryName' exists ($($existing.ItemCount) items)" -ForegroundColor Yellow
        return $existing
    }

    if ($PSCmdlet.ShouldProcess($LibraryName, "Create document library")) {
        $lib = New-PnPList -Title $LibraryName -Template DocumentLibrary -Description $Description
        Write-Host "[CREATED] Library: $LibraryName" -ForegroundColor Green
        return $lib
    }
}

2.3 Provision all required libraries

Connect-PnPOnline -Url $SiteUrl -Interactive

$RequiredLibraries = @(
    @{ Name = 'AgentConfigurations';  Desc = 'Agent manifest exports, prompt versions, system instructions' }
    @{ Name = 'InteractionLogs';      Desc = 'Conversation transcripts, session logs' }
    @{ Name = 'ApprovalRecords';      Desc = 'Deployment approvals, change requests, WSP addenda' }
    @{ Name = 'IncidentReports';      Desc = 'Security incidents, compliance findings, remediation evidence' }
    @{ Name = 'GovernanceDecisions';  Desc = 'Policy decisions, risk acceptances, governance meeting minutes' }
    @{ Name = 'SupervisionRecords';   Desc = 'FINRA 3110 supervision logs, sampling evidence, review outcomes' }
)

$libraryReport = @()
foreach ($lib in $RequiredLibraries) {
    $result = Ensure-DocumentLibrary -SiteUrl $SiteUrl -LibraryName $lib.Name -Description $lib.Desc
    $status = if ($result -and $result.ItemCount -ge 0) { 'Exists' } else { 'Created' }
    $libraryReport += [PSCustomObject]@{
        Library   = $lib.Name
        Status    = $status
        ItemCount = if ($result) { $result.ItemCount } else { 0 }
    }
}

# Export library inventory
$libReportPath = Join-Path $EvidenceRoot "library-inventory-$Stamp.csv"
$libraryReport | Export-Csv -Path $libReportPath -NoTypeInformation
Add-EvidenceToManifest -FilePath $libReportPath -Description 'SharePoint library inventory for AI Governance site'

§3. Idempotent site column and metadata schema provisioning

3.1 Helper: ensure site column exists

function Ensure-SiteColumn {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [string] $DisplayName,
        [Parameter(Mandatory)] [string] $InternalName,
        [Parameter(Mandatory)] [string] $Type,
        [string[]] $Choices,
        [string] $Group = 'AI Governance'
    )

    $existing = Get-PnPField -Identity $InternalName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "[SKIP] Site column '$DisplayName' already exists" -ForegroundColor Yellow
        return $existing
    }

    if ($PSCmdlet.ShouldProcess($DisplayName, "Create site column")) {
        $params = @{
            DisplayName  = $DisplayName
            InternalName = $InternalName
            Type         = $Type
            Group        = $Group
        }
        if ($Choices -and $Type -eq 'Choice') {
            $params.Choices = $Choices
        }
        $field = New-PnPField @params
        Write-Host "[CREATED] Site column: $DisplayName" -ForegroundColor Green
        return $field
    }
}

3.2 Create all metadata columns

# Core columns (Zone 1+)
Ensure-SiteColumn -DisplayName 'Agent ID'           -InternalName 'AgentID'           -Type Text
Ensure-SiteColumn -DisplayName 'Document Category'  -InternalName 'DocCategory'       -Type Choice `
    -Choices @('Configuration','Log','Approval','Incident','Decision','Supervision')
Ensure-SiteColumn -DisplayName 'Classification Date'-InternalName 'ClassificationDate'-Type DateTime

# Extended columns (Zone 2+)
Ensure-SiteColumn -DisplayName 'Regulatory Reference' -InternalName 'RegReference'    -Type Choice `
    -Choices @('FINRA 4511','FINRA 3110','SEC 17a-3','SEC 17a-4','SOX 302','SOX 404','GLBA 501(b)','OCC 2011-12','Fed SR 11-7','CFTC 1.31')
Ensure-SiteColumn -DisplayName 'Retention Period'    -InternalName 'RetentionPeriod'  -Type Choice `
    -Choices @('3 years','5 years','6 years','7 years','10 years','Permanent')
Ensure-SiteColumn -DisplayName 'Governance Zone'     -InternalName 'GovZone'          -Type Choice `
    -Choices @('Zone 1','Zone 2','Zone 3')
Ensure-SiteColumn -DisplayName 'Record Owner'        -InternalName 'RecordOwner'      -Type User

# Export column configuration as evidence
$columns = Get-PnPField | Where-Object { $_.Group -eq 'AI Governance' } |
    Select-Object InternalName, Title, TypeDisplayName, Required
$colReportPath = Join-Path $EvidenceRoot "site-columns-$Stamp.csv"
$columns | Export-Csv -Path $colReportPath -NoTypeInformation
Add-EvidenceToManifest -FilePath $colReportPath -Description 'AI Governance site columns configuration'

§4. Idempotent retention label creation

4.1 Helper: ensure a retention label exists

function Ensure-RetentionLabel {
    [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] Retention label '$Name' already exists (Duration: $($existing.RetentionDuration) days)" -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 }

        $label = New-ComplianceTag @params -ErrorAction Stop
        $labelPath = Join-Path $EvidenceRoot "label-$Name-$Stamp.json"
        $label | ConvertTo-Json -Depth 5 | Out-File -FilePath $labelPath -Encoding UTF8
        Add-EvidenceToManifest -FilePath $labelPath -Description "Retention label created: $Name"
        Write-Host "[CREATED] Retention label: $Name ($RetentionDurationDays days)" -ForegroundColor Green
        return $label
    }
}

Regulatory record labels are 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 requiring second-person approval. Organizations should verify this behavior meets their requirements.

4.2 Create the FSI retention label set

# --- Zone 2 labels (standard record) ---

# Communications classification (3 years per SEC 17a-4(b)(4))
Ensure-RetentionLabel -Name 'FSI-Agent-Communications-3Year' `
    -Comment 'Agent conversation logs classified as communications under SEC 17a-4(b)(4). 3-year retention.' `
    -RetentionDurationDays 1095 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -ReviewerEmail $ReviewerStage1

# Books and records (6 years per SEC 17a-4(a))
Ensure-RetentionLabel -Name 'FSI-Agent-BooksRecords-6Year' `
    -Comment 'Agent records that evidence or generate a 17a-3 record. 6-year retention per SEC 17a-4(a).' `
    -RetentionDurationDays 2190 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -ReviewerEmail $ReviewerStage1

# Governance records (6 years per FINRA 4511)
Ensure-RetentionLabel -Name 'FSI-Agent-Governance-6Year' `
    -Comment 'AI governance decisions, policy records per FINRA 4511. 6-year retention.' `
    -RetentionDurationDays 2190 `
    -RetentionAction KeepAndDelete `
    -ReviewerEmail $ReviewerStage1

# Supervision records (6 years per FINRA 3110)
Ensure-RetentionLabel -Name 'FSI-Agent-Supervision-6Year' `
    -Comment 'FINRA 3110 supervision records including review logs and sampling evidence. 6-year retention.' `
    -RetentionDurationDays 2190 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -ReviewerEmail $ReviewerStage1

# Agent configuration (6 years)
Ensure-RetentionLabel -Name 'FSI-Agent-Configuration-6Year' `
    -Comment 'Agent definitions, version history, manifest exports. 6-year retention.' `
    -RetentionDurationDays 2190 `
    -RetentionAction Delete

# --- Zone 3 labels (regulatory record — IRREVERSIBLE) ---

# SEC 17a-4 regulatory record (7 years = 6-year requirement + 1-year buffer)
Ensure-RetentionLabel -Name 'FSI-Agent-RegRecord-7Year' `
    -Comment 'SEC 17a-4 regulatory record. 7-year retention (6-year requirement + buffer). IRREVERSIBLE.' `
    -RetentionDurationDays 2555 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -Regulatory $true `
    -ReviewerEmail $ReviewerStage2

# CFTC 1.31 (5 years for derivatives records)
Ensure-RetentionLabel -Name 'FSI-Agent-CFTC-5Year' `
    -Comment 'CFTC 1.31 regulatory record for FCMs, swap dealers, CPOs. 5-year retention. IRREVERSIBLE.' `
    -RetentionDurationDays 1825 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -Regulatory $true `
    -ReviewerEmail $ReviewerStage2

# Model risk documentation (6 years per OCC 2011-12 / Fed SR 11-7)
Ensure-RetentionLabel -Name 'FSI-Agent-ModelRisk-6Year' `
    -Comment 'OCC 2011-12 / Fed SR 11-7 model risk documentation. 6-year retention.' `
    -RetentionDurationDays 2190 `
    -RetentionAction KeepAndDelete `
    -IsRecordLabel $true `
    -ReviewerEmail $ReviewerStage2

§5. Idempotent retention policy creation

5.1 Helper: ensure a retention policy exists

function Ensure-RetentionPolicy {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)] [string]   $PolicyName,
        [Parameter(Mandatory)] [string[]] $LabelNames,
        [Parameter(Mandatory)] [string]   $SharePointLocation,
        [string] $Comment = ''
    )

    $existing = Get-RetentionCompliancePolicy -Identity $PolicyName -ErrorAction SilentlyContinue
    if ($existing) {
        Write-Host "[SKIP] Retention policy '$PolicyName' already exists (Mode: $($existing.Mode))" -ForegroundColor Yellow
        return $existing
    }

    if ($PSCmdlet.ShouldProcess($PolicyName, "Publish retention labels via policy")) {
        $policy = New-RetentionCompliancePolicy -Name $PolicyName `
            -Comment $Comment `
            -SharePointLocation $SharePointLocation `
            -Enabled $true `
            -ErrorAction Stop

        foreach ($labelName in $LabelNames) {
            New-RetentionComplianceRule -Policy $PolicyName `
                -PublishComplianceTag $labelName `
                -ErrorAction Stop
            Write-Host "  [PUBLISHED] Label '$labelName' via policy '$PolicyName'" -ForegroundColor Green
        }

        $policyPath = Join-Path $EvidenceRoot "policy-$PolicyName-$Stamp.json"
        $policy | ConvertTo-Json -Depth 5 | Out-File -FilePath $policyPath -Encoding UTF8
        Add-EvidenceToManifest -FilePath $policyPath -Description "Retention policy created: $PolicyName"
        Write-Host "[CREATED] Retention policy: $PolicyName" -ForegroundColor Green
        return $policy
    }
}

5.2 Publish labels to the AI Governance site

$Zone2Labels = @(
    'FSI-Agent-Communications-3Year',
    'FSI-Agent-BooksRecords-6Year',
    'FSI-Agent-Governance-6Year',
    'FSI-Agent-Supervision-6Year',
    'FSI-Agent-Configuration-6Year'
)

Ensure-RetentionPolicy -PolicyName 'FSI-AI-Governance-Retention-Zone2' `
    -LabelNames $Zone2Labels `
    -SharePointLocation $SiteUrl `
    -Comment 'Zone 2 retention labels for AI governance documentation per FINRA 4511 / SEC 17a-4'

# Zone 3 regulatory labels (separate policy for audit segregation)
$Zone3Labels = @(
    'FSI-Agent-RegRecord-7Year',
    'FSI-Agent-CFTC-5Year',
    'FSI-Agent-ModelRisk-6Year'
)

Ensure-RetentionPolicy -PolicyName 'FSI-AI-Governance-Retention-Zone3' `
    -LabelNames $Zone3Labels `
    -SharePointLocation $SiteUrl `
    -Comment 'Zone 3 regulatory record labels per SEC 17a-4(f) / CFTC 1.31 / OCC 2011-12'

Propagation delay

Published labels may take up to 7 days to appear in SharePoint libraries. Schedule policy creation at least 1 week before planned label application.


§6. Documentation completeness audit

6.1 Audit required documents

function Invoke-DocumentationAudit {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $SiteUrl
    )

    Connect-PnPOnline -Url $SiteUrl -Interactive

    $requiredDocs = @(
        @{ Library = 'GovernanceDecisions'; DocPattern = 'AI-Governance-Policy';            Zone = 'Zone 1' }
        @{ Library = 'GovernanceDecisions'; DocPattern = 'Annual-Documentation-Review';     Zone = 'Zone 1' }
        @{ Library = 'GovernanceDecisions'; DocPattern = 'Examination-Response-Procedure';  Zone = 'Zone 3' }
        @{ Library = 'GovernanceDecisions'; DocPattern = 'Quarterly-Audit-Schedule';        Zone = 'Zone 3' }
        @{ Library = 'GovernanceDecisions'; DocPattern = 'WSP-Addendum';                    Zone = 'Zone 2' }
        @{ Library = 'SupervisionRecords';  DocPattern = 'Supervision-Sampling-Report';     Zone = 'Zone 2' }
    )

    $auditResults = @()
    foreach ($req in $requiredDocs) {
        $items = Get-PnPListItem -List $req.Library -Query @"
<View><Query><Where>
    <Contains><FieldRef Name='FileLeafRef'/><Value Type='Text'>$($req.DocPattern)</Value></Contains>
</Where></Query></View>
"@ -ErrorAction SilentlyContinue

        $status = if ($items -and $items.Count -gt 0) { 'PASS' } else { 'FAIL' }
        $color  = if ($status -eq 'PASS') { 'Green' } else { 'Red' }
        Write-Host "[$status] $($req.DocPattern) in $($req.Library) ($($req.Zone))" -ForegroundColor $color

        $auditResults += [PSCustomObject]@{
            Library     = $req.Library
            Document    = $req.DocPattern
            Zone        = $req.Zone
            Status      = $status
            ItemCount   = if ($items) { $items.Count } else { 0 }
            AuditDate   = (Get-Date).ToString('yyyy-MM-dd')
        }
    }

    return $auditResults
}

$auditResults = Invoke-DocumentationAudit -SiteUrl $SiteUrl
$auditPath = Join-Path $EvidenceRoot "doc-completeness-audit-$Stamp.csv"
$auditResults | Export-Csv -Path $auditPath -NoTypeInformation
Add-EvidenceToManifest -FilePath $auditPath -Description 'Documentation completeness audit results'

§7. Retention policy status export

function Export-RetentionStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $EvidenceRoot,
        [Parameter(Mandatory)] [string] $Stamp
    )

    Write-Host "`n=== Retention Label and Policy Status ===" -ForegroundColor Cyan

    # Export labels
    $labels = Get-ComplianceTag | Where-Object { $_.Name -like 'FSI-Agent*' } |
        Select-Object Name, Comment, RetentionDuration, RetentionAction, IsRecordLabel,
                      @{N='IsRegulatory';E={$_.Regulatory}}, Disabled, WhenCreatedUTC
    $labelPath = Join-Path $EvidenceRoot "retention-labels-$Stamp.csv"
    $labels | Export-Csv -Path $labelPath -NoTypeInformation
    Add-EvidenceToManifest -FilePath $labelPath -Description 'Purview retention label inventory'

    Write-Host "Labels found: $($labels.Count)" -ForegroundColor Cyan
    $labels | Format-Table Name, RetentionDuration, RetentionAction, IsRecordLabel -AutoSize

    # Export policies
    $policies = Get-RetentionCompliancePolicy | Where-Object {
        $_.Name -like '*AI*' -or $_.Name -like '*Agent*' -or $_.Name -like '*FSI*'
    } | Select-Object Name, Mode, Enabled, SharePointLocation, WhenCreatedUTC

    $policyPath = Join-Path $EvidenceRoot "retention-policies-$Stamp.csv"
    $policies | Export-Csv -Path $policyPath -NoTypeInformation
    Add-EvidenceToManifest -FilePath $policyPath -Description 'Purview retention policy inventory'

    Write-Host "Policies found: $($policies.Count)" -ForegroundColor Cyan
    $policies | Format-Table Name, Mode, Enabled -AutoSize
}

Export-RetentionStatus -EvidenceRoot $EvidenceRoot -Stamp $Stamp

§8. Copilot Studio agent documentation export

function Export-AgentDocumentation {
    <#
    .SYNOPSIS
        Exports Copilot Studio agent metadata for governance documentation.
    .DESCRIPTION
        Retrieves agent metadata from Power Platform Admin Center and exports
        to the evidence directory for record-keeping per FINRA 4511 / OCC 2011-12.
    .PARAMETER EnvironmentId
        The Power Platform environment ID containing the agents.
    .PARAMETER EvidenceRoot
        Path to the evidence output directory.
    .EXAMPLE
        Export-AgentDocumentation -EnvironmentId 'abc-123' -EvidenceRoot 'C:\fsi-evidence\2.13'
    #>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $EnvironmentId,
        [Parameter(Mandatory)] [string] $EvidenceRoot
    )

    # Guard: requires Windows PowerShell 5.1 for PPAC module
    if ($PSVersionTable.PSEdition -ne 'Desktop') {
        Write-Host "[WARN] Microsoft.PowerApps.Administration.PowerShell requires Windows PowerShell 5.1." -ForegroundColor Yellow
        Write-Host "[INFO] Run this section in Windows PowerShell (Desktop edition)." -ForegroundColor Cyan
        return
    }

    Import-Module Microsoft.PowerApps.Administration.PowerShell -ErrorAction Stop
    Add-PowerAppsAccount  # Interactive login; use -Endpoint 'usgov' for GCC

    $localStamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
    $agents = Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentId |
              ForEach-Object { Get-AdminPowerApp -EnvironmentName $EnvironmentId }

    if (-not $agents -or $agents.Count -eq 0) {
        Write-Host "[INFO] No agents found in environment $EnvironmentId" -ForegroundColor Yellow
        return
    }

    $agentReport = $agents | Select-Object @{N='AgentName';E={$_.DisplayName}},
        @{N='AgentId';E={$_.AppName}},
        @{N='Owner';E={$_.Owner.displayName}},
        @{N='CreatedTime';E={$_.CreatedTime}},
        @{N='LastModified';E={$_.LastModifiedTime}},
        @{N='EnvironmentId';E={$EnvironmentId}}

    $agentPath = Join-Path $EvidenceRoot "agent-inventory-$EnvironmentId-$localStamp.csv"
    $agentReport | Export-Csv -Path $agentPath -NoTypeInformation
    Add-EvidenceToManifest -FilePath $agentPath -Description "Agent inventory for environment $EnvironmentId"

    Write-Host "[PASS] Exported $($agents.Count) agents from environment $EnvironmentId" -ForegroundColor Green
}

# Usage (uncomment and set your environment ID):
# Export-AgentDocumentation -EnvironmentId '<your-environment-id>' -EvidenceRoot $EvidenceRoot

§9. Evidence manifest finalization

# Write the manifest CSV
$ManifestEntries | Export-Csv -Path $ManifestPath -NoTypeInformation
Write-Host "`n=== Evidence Manifest ===" -ForegroundColor Cyan
Write-Host "Manifest: $ManifestPath" -ForegroundColor Cyan
Write-Host "Entries:  $($ManifestEntries.Count)" -ForegroundColor Cyan
$ManifestEntries | Format-Table File, SHA256, Description -AutoSize

# Hash the manifest itself
$manifestHash = (Get-FileHash -Path $ManifestPath -Algorithm SHA256).Hash
Write-Host "`n[MANIFEST SHA-256] $manifestHash" -ForegroundColor Green

# Stop the transcript
Stop-Transcript
Write-Host "[COMPLETE] Transcript saved to: $TranscriptPath" -ForegroundColor Green

# Hash the transcript
$transcriptHash = (Get-FileHash -Path $TranscriptPath -Algorithm SHA256).Hash
Write-Host "[TRANSCRIPT SHA-256] $transcriptHash" -ForegroundColor Green

§10. Complete validation script

<#
.SYNOPSIS
    Validates Control 2.13 — Documentation and Record Keeping configuration.
.DESCRIPTION
    Performs a read-only validation of SharePoint site structure, retention labels,
    retention policies, and documentation completeness for Control 2.13.
    This script does not mutate any configuration.
.PARAMETER SiteUrl
    URL of the AI Governance SharePoint site.
.PARAMETER EvidenceRoot
    Path to write validation evidence.
.EXAMPLE
    .\Validate-Control-2.13.ps1 -SiteUrl 'https://contoso.sharepoint.com/sites/AI-Governance'
.NOTES
    Supports compliance with FINRA 4511, SEC 17a-3/4, SOX 302/404.
    Does not guarantee regulatory compliance.
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory)] [string] $SiteUrl,
    [string] $EvidenceRoot = 'C:\fsi-evidence\2.13'
)

$null = New-Item -ItemType Directory -Path $EvidenceRoot -Force
$Stamp = (Get-Date).ToString('yyyyMMdd-HHmmss')
$results = @()

Write-Host "=== Control 2.13 Validation ===" -ForegroundColor Cyan

# Check 1: Site structure
Write-Host "`n[Check 1] SharePoint Site Structure" -ForegroundColor Cyan
try {
    Connect-PnPOnline -Url $SiteUrl -Interactive
    $lists = Get-PnPList | Where-Object { $_.BaseTemplate -eq 101 } |
        Select-Object Title, ItemCount, Created
    $requiredLibs = @('AgentConfigurations','InteractionLogs','ApprovalRecords',
                      'IncidentReports','GovernanceDecisions','SupervisionRecords')
    foreach ($lib in $requiredLibs) {
        $found = $lists | Where-Object { $_.Title -eq $lib }
        $status = if ($found) { 'PASS' } else { 'FAIL' }
        $color  = if ($status -eq 'PASS') { 'Green' } else { 'Red' }
        Write-Host "  [$status] Library: $lib" -ForegroundColor $color
        $results += [PSCustomObject]@{ Check = "Library-$lib"; Status = $status }
    }
    Disconnect-PnPOnline
} catch {
    Write-Host "  [FAIL] Cannot connect to site: $($_.Exception.Message)" -ForegroundColor Red
    $results += [PSCustomObject]@{ Check = 'SiteConnect'; Status = 'FAIL' }
}

# Check 2: Retention labels
Write-Host "`n[Check 2] Retention Labels" -ForegroundColor Cyan
try {
    Connect-IPPSSession -ShowBanner:$false
    $labels = Get-ComplianceTag | Where-Object { $_.Name -like 'FSI-Agent*' }
    if ($labels -and $labels.Count -ge 4) {
        Write-Host "  [PASS] Found $($labels.Count) FSI-Agent retention labels" -ForegroundColor Green
        $results += [PSCustomObject]@{ Check = 'RetentionLabels'; Status = 'PASS' }
    } else {
        Write-Host "  [WARN] Found $($labels.Count) labels (expected 4+)" -ForegroundColor Yellow
        $results += [PSCustomObject]@{ Check = 'RetentionLabels'; Status = 'WARN' }
    }

    # Check 3: Retention policies
    Write-Host "`n[Check 3] Retention Policies" -ForegroundColor Cyan
    $policies = Get-RetentionCompliancePolicy | Where-Object {
        $_.Name -like '*AI*' -or $_.Name -like '*Agent*' -or $_.Name -like '*FSI*'
    }
    if ($policies -and $policies.Count -ge 1) {
        Write-Host "  [PASS] Found $($policies.Count) retention policies" -ForegroundColor Green
        $results += [PSCustomObject]@{ Check = 'RetentionPolicies'; Status = 'PASS' }
    } else {
        Write-Host "  [WARN] No AI-specific retention policies found" -ForegroundColor Yellow
        $results += [PSCustomObject]@{ Check = 'RetentionPolicies'; Status = 'WARN' }
    }

    Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
} catch {
    Write-Host "  [FAIL] Cannot connect to Purview: $($_.Exception.Message)" -ForegroundColor Red
    $results += [PSCustomObject]@{ Check = 'PurviewConnect'; Status = 'FAIL' }
}

# Check 4: SEC 17a-4 compliant storage (manual verification)
Write-Host "`n[Check 4] SEC 17a-4 Compliant Storage (Zone 3)" -ForegroundColor Cyan
Write-Host "  [INFO] Manual verification required: confirm WORM storage or audit-trail" -ForegroundColor Yellow
Write-Host "  [INFO] alternative is configured per October 2022 amendments" -ForegroundColor Yellow
$results += [PSCustomObject]@{ Check = 'SEC17a4Storage'; Status = 'MANUAL' }

# Check 5: Examination procedures (manual verification)
Write-Host "`n[Check 5] Examination Response Procedures" -ForegroundColor Cyan
Write-Host "  [INFO] Verify examination response procedure is documented with" -ForegroundColor Yellow
Write-Host "  [INFO] designated custodians and response SLAs" -ForegroundColor Yellow
$results += [PSCustomObject]@{ Check = 'ExamProcedures'; Status = 'MANUAL' }

# Export results
$resultPath = Join-Path $EvidenceRoot "validation-results-$Stamp.csv"
$results | Export-Csv -Path $resultPath -NoTypeInformation

$passCount = ($results | Where-Object { $_.Status -eq 'PASS' }).Count
$failCount = ($results | Where-Object { $_.Status -eq 'FAIL' }).Count
$warnCount = ($results | Where-Object { $_.Status -eq 'WARN' }).Count

Write-Host "`n=== Validation Summary ===" -ForegroundColor Cyan
Write-Host "PASS: $passCount | FAIL: $failCount | WARN: $warnCount | MANUAL: $(($results | Where-Object { $_.Status -eq 'MANUAL' }).Count)" -ForegroundColor Cyan
Write-Host "Results: $resultPath" -ForegroundColor Cyan

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


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