Skip to content

Control 2.6 — PowerShell Setup: MRM Evidence Inventories and Reports (OCC 2011-12 / SR 11-7)

Non-Substitution — This PowerShell Produces Evidence; Validation Judgments Remain with People

The helpers in this playbook are inventory, change-detection, and evidence-export utilities. They do not perform model validation, do not assign model tiers, do not issue effective challenge, and do not approve, reject, or retire any AI agent. Those judgments are reserved — under OCC Bulletin 2011-12 and Federal Reserve SR 11-7 §V — to:

  • The firm's Model Risk Management Committee (model-tiering, validation acceptance, model retirement).
  • The firm's independent model validation function — personnel organizationally and functionally separate from model owners and developers. "Independent" is the SR 11-7 §V test; third-party engagement is one way to achieve it but is not required and is not equivalent.
  • The firm's effective challenge process — qualified personnel not involved in model development conducting critical, objective review.
  • Three lines of defense — first line (model owner / developer), second line (independent MRM / validation / compliance), third line (Internal Audit).
  • Registered-principal supervisory review under FINRA Rule 3110 (see Control 2.12).

The Microsoft Purview DSPM for AI inventory, Azure AI Foundry evaluation harness, Copilot Studio analytics, Agent 365 Admin Center, and Microsoft Entra Agent ID surfaces this playbook reads from are evidence-collection surfaces. They do not constitute, and must not be presented in your firm's MRM policy, WSPs, or examiner submissions as, a substitute for the human governance above. Treat every artifact this playbook emits as input to an MRM Committee or independent validation review — never as the conclusion of one.

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, Dataverse compatibility, and SHA-256 evidence emission. Sovereign-cloud endpoints are documented in §3 — Sovereign Cloud Endpoints (GCC, GCC High, DoD).

Scope. This playbook automates read-only evidence inventories for Control 2.6 — Model Risk Management (OCC 2011-12 / SR 11-7). Outputs feed your existing MRM program; they do not replace it.

Namespace. All functions use the FsiMRM prefix to prevent collision with peer-control automation (Agt36, Agt225, Agt226).

Hedged-language reminder. The artifacts produced here support compliance with OCC 2011-12, SR 11-7, FDIC FIL-22-2017, FFIEC IT Handbook, FINRA 3110/4511, SEC 17a-3/17a-4, SOX §§302/404, GLBA 501(b), and NYDFS 23 NYCRR 500. They do not — and cannot — ensure, guarantee, prevent, or eliminate anything on their own.


§1 — Pre-flight: Modules, licenses, roles, and connection verification

1.1 — Module version matrix

Pin to versions approved by your Change Advisory Board. Floating versions break reproducibility and weaken SOX 404 / OCC 2023-17 evidence.

# PowerShell 7.2+ required for Microsoft.Graph; Power Apps Administration cmdlets
# require Windows PowerShell 5.1 (Desktop). See baseline §2 for the wrong-shell guard
# pattern. Run inventory and change-feed steps from PS 7; run Power Apps maker/env
# enumeration from a PS 5.1 session that imports the produced JSON.

$FsiMRMModuleMatrix = @(
    @{ Name = 'Microsoft.Graph.Authentication';            Min = '2.19.0' },
    @{ Name = 'Microsoft.Graph.Identity.Governance';       Min = '2.19.0' },
    @{ Name = 'Microsoft.Graph.Applications';              Min = '2.19.0' },
    @{ Name = 'Microsoft.Graph.Users';                     Min = '2.19.0' },
    @{ Name = 'ExchangeOnlineManagement';                  Min = '3.4.0'  },  # Purview / unified audit log
    @{ Name = 'Microsoft.PowerApps.Administration.PowerShell'; Min = '2.0.175' },
    @{ Name = 'Az.Accounts';                               Min = '2.15.0' },  # Foundry workspace access
    @{ Name = 'Az.MachineLearningServices';                Min = '1.0.0'  }   # Foundry evaluator runs (TODO: verify exact module surface for Foundry GA)
)

function Test-FsiMRMModuleMatrix {
    [CmdletBinding()]
    param([switch]$InstallMissing)
    $results = foreach ($m in $FsiMRMModuleMatrix) {
        $installed = Get-Module -ListAvailable -Name $m.Name |
            Sort-Object Version -Descending | Select-Object -First 1
        $status = if (-not $installed) { 'Anomaly' }
                  elseif ($installed.Version -lt [version]$m.Min) { 'Anomaly' }
                  else { 'Clean' }
        if ($status -eq 'Anomaly' -and $InstallMissing) {
            try {
                Install-Module $m.Name -MinimumVersion $m.Min -Scope CurrentUser -AllowClobber -Force
                $status = 'Pending'   # re-import required
            } catch { $status = 'Error' }
        }
        [pscustomobject]@{
            Module    = $m.Name
            Required  = $m.Min
            Installed = $installed.Version
            Status    = $status
        }
    }
    $results
}

1.2 — Licenses and role prerequisites

Capability Minimum license / SKU Canonical role(s)
Power Platform CoE inventory Power Apps per-user OR per-app for the CoE owner; CoE Starter Kit installed Power Platform Admin
DSPM for AI inventory of Copilot / Copilot Studio / Foundry / Entra-registered AI apps Microsoft Purview (E5 Compliance or equivalent add-on); DSPM for AI enabled Purview Data Security AI Admin + Purview Compliance Admin
Agent 365 Admin Center inventory Agent 365 GA SKU in the tenant region (verify per Microsoft Learn) AI Administrator
Entra Agent ID enumeration Entra ID P1 minimum; P2 recommended for Identity Governance lifecycle workflows Entra Global Reader (read-only); Entra Identity Governance Admin for lifecycle exports
Purview Unified Audit Log queries E5 Compliance; audit log search enabled Purview Audit Admin
Azure AI Foundry evaluator-run export Contributor on the Foundry workspace; Storage Blob Data Reader on the linked storage AI Administrator + Foundry-workspace RBAC
Evidence label application & WORM routing Purview retention labels published; Records Management feature enabled Purview Records Manager

All role activations for evidence collection should be performed via PIM with a ChangeTicketId in the activation justification — see Control 1.9 for retention routing and Control 1.7 for audit-log / 17a-4(f) handoff.

1.3 — Connection verification (commercial vs sovereign)

function Test-FsiMRMConnection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Commercial','USGov','USGovHigh','USGovDoD')]
        [string]$Cloud
    )
    $envMap = @{
        Commercial = @{ Graph='Global';     PP='prod';      Az='AzureCloud'        }
        USGov      = @{ Graph='USGov';      PP='usgov';     Az='AzureUSGovernment' }
        USGovHigh  = @{ Graph='USGovDoD';   PP='usgovhigh'; Az='AzureUSGovernment' }
        USGovDoD   = @{ Graph='USGovDoD';   PP='dod';       Az='AzureUSGovernment' }
    }
    $tgt = $envMap[$Cloud]

    $checks = @()
    # Graph
    try {
        $ctx = Get-MgContext
        $checks += [pscustomobject]@{
            Surface = 'MicrosoftGraph'
            Expected = $tgt.Graph
            Actual   = $ctx.Environment
            Status   = if ($ctx.Environment -eq $tgt.Graph) { 'Clean' } else { 'Anomaly' }
            Note     = if (-not $ctx) { 'Not connected — call Connect-MgGraph -Environment ' + $tgt.Graph } else { '' }
        }
    } catch { $checks += [pscustomobject]@{ Surface='MicrosoftGraph'; Status='Error'; Note=$_.Exception.Message } }

    # Power Platform — no native session-introspection cmdlet; probe via env list
    try {
        $envs = Get-AdminPowerAppEnvironment -ErrorAction Stop
        $checks += [pscustomobject]@{
            Surface = 'PowerPlatform'
            Expected = $tgt.PP
            Actual   = "$($envs.Count) environment(s) returned"
            Status   = if ($envs.Count -gt 0) { 'Clean' } else { 'Anomaly' }
            Note     = if ($envs.Count -eq 0) { 'Empty list — likely wrong sovereign endpoint; see baseline §3' } else { '' }
        }
    } catch { $checks += [pscustomobject]@{ Surface='PowerPlatform'; Status='Error'; Note=$_.Exception.Message } }

    # Azure (Foundry)
    try {
        $azCtx = Get-AzContext
        $checks += [pscustomobject]@{
            Surface = 'Azure'
            Expected = $tgt.Az
            Actual   = $azCtx.Environment.Name
            Status   = if ($azCtx.Environment.Name -eq $tgt.Az) { 'Clean' } else { 'Anomaly' }
            Note     = if (-not $azCtx) { "Run Connect-AzAccount -Environment $($tgt.Az)" } else { '' }
        }
    } catch { $checks += [pscustomobject]@{ Surface='Azure'; Status='Error'; Note=$_.Exception.Message } }

    # Exchange / Purview
    try {
        $eo = Get-ConnectionInformation -ErrorAction Stop | Where-Object { $_.State -eq 'Connected' }
        $checks += [pscustomobject]@{
            Surface = 'ExchangeOnline'
            Expected = $Cloud
            Actual   = $eo.ConnectionUri
            Status   = if ($eo) { 'Clean' } else { 'Anomaly' }
            Note     = if (-not $eo) { 'Run Connect-ExchangeOnline with -ExchangeEnvironmentName for sovereign clouds' } else { '' }
        }
    } catch { $checks += [pscustomobject]@{ Surface='ExchangeOnline'; Status='Error'; Note=$_.Exception.Message } }

    [pscustomobject]@{
        Cloud  = $Cloud
        RunUtc = (Get-Date).ToUniversalTime()
        Status = if ($checks.Status -contains 'Error')   { 'Error' }
                 elseif ($checks.Status -contains 'Anomaly') { 'Anomaly' }
                 else { 'Clean' }
        Checks = $checks
    }
}

§2 — Sovereign-cloud guard (call this first)

Test-FsiMRMSovereignReadiness must run before any inventory or evidence export. It does not block execution in commercial cloud; in sovereign clouds it surfaces parity gaps and prints the compensating-control reminder so MRM Committee material is not falsely produced from incomplete surfaces.

function Test-FsiMRMSovereignReadiness {
<#
.SYNOPSIS
    Probes sovereign-cloud parity for the Microsoft surfaces Control 2.6 reads from
    and returns a Status object the caller can gate downstream collection on.

.DESCRIPTION
    Returns one row per surface with Status ∈ {Clean, NotApplicable, Anomaly, Error}
    plus a CompensatingControl hint. NotApplicable means the surface is not
    available in the requested cloud and the compensating manual control must be
    used — it does NOT mean "skip silently".

.PARAMETER Cloud
    Commercial | USGov | USGovHigh | USGovDoD

.OUTPUTS
    [pscustomobject] with Cloud, RunUtc, Status, Surfaces[]
#>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Commercial','USGov','USGovHigh','USGovDoD')]
        [string]$Cloud
    )

    # Parity matrix is a STATIC snapshot — re-verify quarterly via the
    # Microsoft 365 Government roadmap (https://aka.ms/m365gov-roadmap).
    # TODO: replace with a live Microsoft Learn / roadmap probe when an API exists.
    $parity = @{
        'DSPMforAI'        = @{ Commercial='Clean'; USGov='Anomaly';      USGovHigh='Anomaly';      USGovDoD='NotApplicable' }
        'FoundryEvaluators'= @{ Commercial='Clean'; USGov='Anomaly';      USGovHigh='Anomaly';      USGovDoD='NotApplicable' }
        'Agent365Admin'    = @{ Commercial='Clean'; USGov='NotApplicable'; USGovHigh='NotApplicable'; USGovDoD='NotApplicable' }
        'EntraAgentID'     = @{ Commercial='Clean'; USGov='Anomaly';      USGovHigh='Anomaly';      USGovDoD='Anomaly' }
        'CopilotStudio3PModels' = @{ Commercial='Clean'; USGov='NotApplicable'; USGovHigh='NotApplicable'; USGovDoD='NotApplicable' }
    }

    $compensating = @{
        'DSPMforAI'             = 'Maintain a manual SharePoint-list agent inventory under Purview retention; export via Get-FsiMRMManualInventory (§4.5).'
        'FoundryEvaluators'     = 'Run open-source evaluators (e.g. Azure ML Responsible AI dashboard in sovereign region, or third-party harness) and route results to MRM Committee.'
        'Agent365Admin'         = 'Use Power Platform admin enumeration (§3.1) plus Copilot Studio export-to-zip review for agent inventory until Agent 365 GA in sovereign cloud.'
        'EntraAgentID'          = 'Use service-principal tags + manual sponsor-mapping CSV per Control 3.6 §8 worksheet.'
        'CopilotStudio3PModels' = 'Restrict approved underlying models to first-party OpenAI GPT family available in your sovereign region; document in MRM model card.'
    }

    $rows = foreach ($surface in $parity.Keys) {
        $status = $parity[$surface][$Cloud]
        [pscustomobject]@{
            Surface             = $surface
            Cloud               = $Cloud
            Status              = $status
            CompensatingControl = if ($status -ne 'Clean') { $compensating[$surface] } else { '' }
            VerifiedUtc         = (Get-Date).ToUniversalTime()
        }
    }

    $aggregate = if ($rows.Status -contains 'Error')          { 'Error' }
                 elseif ($rows.Status -contains 'Anomaly')    { 'Anomaly' }
                 elseif ($rows.Status -contains 'NotApplicable') { 'NotApplicable' }
                 else { 'Clean' }

    if ($aggregate -ne 'Clean') {
        Write-Warning @"
Sovereign-cloud parity gaps detected for cloud '$Cloud'.
DO NOT treat the inventory or evidence package as authoritative for MRM Committee
review without applying the compensating controls listed in the Surfaces output.
Re-verify the parity matrix quarterly via https://aka.ms/m365gov-roadmap.
"@
    }

    [pscustomobject]@{
        Cloud    = $Cloud
        RunUtc   = (Get-Date).ToUniversalTime()
        Status   = $aggregate
        Surfaces = $rows
    }
}

Required gating pattern at the top of every scheduled job:

$ready = Test-FsiMRMSovereignReadiness -Cloud $Cloud
if ($ready.Status -eq 'Error') {
    throw "Sovereign readiness probe errored: see Surfaces."
}
# NotApplicable / Anomaly do NOT halt execution — they propagate into the
# Status field of each affected artifact so the MRM Committee sees the gap
# rather than a silent omission.

§3 — Helper definitions

All four helpers below return [pscustomobject] with a top-level Status{Clean, Anomaly, Pending, NotApplicable, Error} and a Findings collection. Errors are caught and reported, never thrown to the caller — a thrown exception in a scheduled MRM job produces no evidence at all, which is worse than an Error artifact for an examiner.

3.1 — Get-FsiAgentInventoryForMRM

function Get-FsiAgentInventoryForMRM {
<#
.SYNOPSIS
    Produces a unified AI-agent inventory for MRM Committee review by joining
    Power Platform CoE (Copilot Studio agents), Purview DSPM for AI (Copilot,
    Foundry, Entra-registered AI apps), and the Agent 365 Admin Center.

.DESCRIPTION
    Read-only. Does NOT classify model tier — Tier is populated from the firm's
    1.2 agent registry if supplied, otherwise emitted as 'Unassigned' with
    Status='Pending' so the MRM Committee assigns it.

    Each row is a [pscustomobject] with:
      AgentId, DisplayName, Owner, Environment, UnderlyingModel, ModelVersion,
      KnowledgeSources, LastValidationDate, NextValidationDate, Tier,
      Source ∈ {PowerPlatformCoE, DSPMforAI, Agent365}, Status

    Status values:
      Clean         — all required MRM fields populated, validation in-window
      Anomaly       — validation overdue, missing model card, or untiered with
                      production usage signals
      Pending       — newly discovered agent awaiting MRM Committee tiering
      NotApplicable — agent surface unavailable in sovereign cloud
      Error         — surface call failed; row preserved for audit

.PARAMETER Cloud
    Commercial | USGov | USGovHigh | USGovDoD

.PARAMETER RegistryPath
    Optional path to the firm's 1.2 AI agent registry (CSV or JSON) used to
    enrich rows with Tier, LastValidationDate, NextValidationDate.

.PARAMETER ValidationWindowDays
    Days inside which a validation is considered current. Default 365 per
    common MRM annual-revalidation cycles; Tier-1 agents may require shorter.

.OUTPUTS
    [pscustomobject] { RunUtc, Cloud, Status, Findings[] }
#>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Commercial','USGov','USGovHigh','USGovDoD')]
        [string]$Cloud,
        [string]$RegistryPath,
        [ValidateRange(30,1095)][int]$ValidationWindowDays = 365
    )

    $ready = Test-FsiMRMSovereignReadiness -Cloud $Cloud
    $registry = @{}
    if ($RegistryPath -and (Test-Path $RegistryPath)) {
        try {
            $entries = if ($RegistryPath -match '\.json$') { Get-Content $RegistryPath -Raw | ConvertFrom-Json }
                       else { Import-Csv $RegistryPath }
            foreach ($e in $entries) { $registry[$e.AgentId] = $e }
        } catch {
            Write-Warning "Registry load failed: $($_.Exception.Message). Tier/validation fields will be Unassigned."
        }
    }

    $findings = @()

    # --- Source 1: Power Platform CoE (Copilot Studio agents) -------------
    try {
        $envs = Get-AdminPowerAppEnvironment -ErrorAction Stop
        foreach ($env in $envs) {
            # Copilot Studio agents surface as bots within the environment;
            # use Get-AdminPowerAppEnvironment + Dataverse query in production.
            # TODO: confirm the GA cmdlet name for Copilot Studio bot enumeration
            # (currently Get-AdminBot in the BAP API; verify against your pinned
            # Microsoft.PowerApps.Administration.PowerShell version).
            try {
                $bots = Get-AdminBot -EnvironmentName $env.EnvironmentName -ErrorAction Stop
            } catch { $bots = @() }

            foreach ($b in $bots) {
                $reg = $registry[$b.BotName]
                $row = [pscustomobject]@{
                    AgentId            = $b.BotName
                    DisplayName        = $b.DisplayName
                    Owner              = $b.Owner.userPrincipalName
                    Environment        = $env.DisplayName
                    UnderlyingModel    = $reg.UnderlyingModel    # CoE does not always expose model assignment
                    ModelVersion       = $reg.ModelVersion
                    KnowledgeSources   = $reg.KnowledgeSources
                    LastValidationDate = $reg.LastValidationDate
                    NextValidationDate = $reg.NextValidationDate
                    Tier               = if ($reg.Tier) { $reg.Tier } else { 'Unassigned' }
                    Source             = 'PowerPlatformCoE'
                    Status             = 'Pending'   # set below
                }
                $findings += (Resolve-FsiMRMRowStatus -Row $row -ValidationWindowDays $ValidationWindowDays)
            }
        }
    } catch {
        $findings += [pscustomobject]@{
            AgentId='(power-platform-error)'; Source='PowerPlatformCoE';
            Status='Error'; DisplayName=$_.Exception.Message
        }
    }

    # --- Source 2: Purview DSPM for AI ------------------------------------
    $dspmStatus = ($ready.Surfaces | Where-Object Surface -eq 'DSPMforAI').Status
    if ($dspmStatus -in @('NotApplicable','Anomaly')) {
        $findings += [pscustomobject]@{
            AgentId='(dspm-not-available)'; Source='DSPMforAI';
            Status=$dspmStatus
            DisplayName="DSPM for AI parity gap in $Cloud — apply compensating control"
        }
    } else {
        try {
            # DSPM for AI inventory is exposed via Microsoft Graph
            # /security/dataSecurityAndGovernance/* surface (preview at time of writing).
            # TODO: verify the GA endpoint path before production scheduling.
            $dspmAgents = Invoke-MgGraphRequest -Method GET `
                -Uri '/v1.0/security/dataSecurityAndGovernance/aiInteractionHistory?$top=999' `
                -ErrorAction Stop
            foreach ($a in $dspmAgents.value) {
                $reg = $registry[$a.appId]
                $row = [pscustomobject]@{
                    AgentId            = $a.appId
                    DisplayName        = $a.appDisplayName
                    Owner              = $a.ownerUpn
                    Environment        = $a.platform   # Copilot | CopilotStudio | Foundry | EntraRegistered
                    UnderlyingModel    = $a.modelId
                    ModelVersion       = $a.modelVersion
                    KnowledgeSources   = ($a.dataSources -join ';')
                    LastValidationDate = $reg.LastValidationDate
                    NextValidationDate = $reg.NextValidationDate
                    Tier               = if ($reg.Tier) { $reg.Tier } else { 'Unassigned' }
                    Source             = 'DSPMforAI'
                    Status             = 'Pending'
                }
                $findings += (Resolve-FsiMRMRowStatus -Row $row -ValidationWindowDays $ValidationWindowDays)
            }
        } catch {
            $findings += [pscustomobject]@{
                AgentId='(dspm-error)'; Source='DSPMforAI';
                Status='Error'; DisplayName=$_.Exception.Message
            }
        }
    }

    # --- Source 3: Agent 365 Admin Center ---------------------------------
    $a365Status = ($ready.Surfaces | Where-Object Surface -eq 'Agent365Admin').Status
    if ($a365Status -ne 'Clean') {
        $findings += [pscustomobject]@{
            AgentId='(agent365-not-available)'; Source='Agent365';
            Status=$a365Status
            DisplayName="Agent 365 Admin Center not available in $Cloud — see Control 2.25 compensating control"
        }
    } else {
        try {
            # Agent 365 inventory is exposed via Graph /agents (preview).
            # TODO: pin to GA endpoint when announced.
            $a365 = Invoke-MgGraphRequest -Method GET -Uri '/beta/agents?$top=999' -ErrorAction Stop
            foreach ($a in $a365.value) {
                $reg = $registry[$a.id]
                $row = [pscustomobject]@{
                    AgentId            = $a.id
                    DisplayName        = $a.displayName
                    Owner              = $a.owner.userPrincipalName
                    Environment        = 'Agent365'
                    UnderlyingModel    = $a.modelBinding.modelId
                    ModelVersion       = $a.modelBinding.modelVersion
                    KnowledgeSources   = ($a.knowledge.sources -join ';')
                    LastValidationDate = $reg.LastValidationDate
                    NextValidationDate = $reg.NextValidationDate
                    Tier               = if ($reg.Tier) { $reg.Tier } else { 'Unassigned' }
                    Source             = 'Agent365'
                    Status             = 'Pending'
                }
                $findings += (Resolve-FsiMRMRowStatus -Row $row -ValidationWindowDays $ValidationWindowDays)
            }
        } catch {
            $findings += [pscustomobject]@{
                AgentId='(agent365-error)'; Source='Agent365';
                Status='Error'; DisplayName=$_.Exception.Message
            }
        }
    }

    $aggregate = if ($findings.Status -contains 'Error') { 'Error' }
                 elseif ($findings.Status -contains 'Anomaly') { 'Anomaly' }
                 elseif ($findings.Status -contains 'Pending') { 'Pending' }
                 elseif ($findings.Status -contains 'NotApplicable') { 'NotApplicable' }
                 else { 'Clean' }

    [pscustomobject]@{
        RunUtc   = (Get-Date).ToUniversalTime()
        Cloud    = $Cloud
        Source   = 'Get-FsiAgentInventoryForMRM'
        Status   = $aggregate
        Findings = $findings
    }
}

function Resolve-FsiMRMRowStatus {
    param($Row, [int]$ValidationWindowDays)
    $now = Get-Date
    if ($Row.Tier -eq 'Unassigned') { $Row.Status = 'Pending'; return $Row }
    if (-not $Row.LastValidationDate) { $Row.Status = 'Anomaly'; return $Row }
    try {
        $last = [datetime]$Row.LastValidationDate
        if (($now - $last).TotalDays -gt $ValidationWindowDays) { $Row.Status = 'Anomaly' }
        else { $Row.Status = 'Clean' }
    } catch { $Row.Status = 'Anomaly' }
    $Row
}

3.2 — Get-FsiAgentModelChangeFeed

Per SR 11-7 §V (vendor-model governance), the firm must monitor and disposition vendor-driven changes to underlying models — including default-model migrations in Copilot Studio and model deprecations in Azure AI Foundry. This helper aggregates two signal sources into a disposition-ready feed.

function Get-FsiAgentModelChangeFeed {
<#
.SYNOPSIS
    Surfaces vendor-driven model-change signals (Power Platform release plan
    items + M365 Message Center notices) the MRM Committee must disposition
    under SR 11-7 §V vendor-model governance.

.DESCRIPTION
    Read-only. Pulls candidate change events from:
      1. Microsoft Graph /admin/serviceAnnouncement/messages (Message Center)
         filtered to Copilot / Copilot Studio / Foundry / Azure OpenAI services.
      2. Power Platform Release Plan public RSS feed (commercial cloud only;
         sovereign tenants must verify availability and may need a manual
         monthly review against the published release plan PDF).

    Each row is dispositioned as:
      Anomaly       — pending default-model migration or Foundry deprecation
                      affecting an in-scope agent
      Pending       — change announced but not yet effective; MRM tracking only
      Clean         — informational notice (e.g. UI changes) not affecting model
      NotApplicable — sovereign cloud where the surface is gated
      Error         — feed call failed

.PARAMETER Cloud
    Commercial | USGov | USGovHigh | USGovDoD

.PARAMETER LookbackDays
    Days of message history to pull. Default 90.

.PARAMETER InventoryFindings
    Optional output of Get-FsiAgentInventoryForMRM .Findings to enrich change
    rows with affected-agent counts.
#>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Commercial','USGov','USGovHigh','USGovDoD')]
        [string]$Cloud,
        [ValidateRange(7,365)][int]$LookbackDays = 90,
        [object[]]$InventoryFindings
    )

    $cutoff = (Get-Date).AddDays(-$LookbackDays).ToUniversalTime()
    $rows = @()

    # --- Signal 1: Message Center via Graph -------------------------------
    try {
        $msgs = Invoke-MgGraphRequest -Method GET `
            -Uri "/v1.0/admin/serviceAnnouncement/messages?`$top=200" `
            -ErrorAction Stop
        foreach ($m in $msgs.value) {
            $title = "$($m.title)"
            $body  = "$($m.body.content)"
            $isModelChange = $title -match '(?i)copilot|foundry|azure openai|gpt|claude|model.*deprecat|default model'
            if (-not $isModelChange) { continue }
            $effective = if ($m.actionRequiredByDateTime) { [datetime]$m.actionRequiredByDateTime } else { $null }
            $affected  = if ($InventoryFindings) {
                @($InventoryFindings | Where-Object { $body -match [regex]::Escape("$($_.UnderlyingModel)") }).Count
            } else { 0 }
            $status = if ($effective -and $effective -gt (Get-Date) -and $affected -gt 0) { 'Anomaly' }
                      elseif ($affected -gt 0) { 'Pending' }
                      else { 'Clean' }
            $rows += [pscustomobject]@{
                Source           = 'MessageCenter'
                Id               = $m.id
                Title            = $title
                Category         = $m.category
                EffectiveUtc     = $effective
                PublishedUtc     = $m.startDateTime
                AffectedAgentCount = $affected
                DispositionDue   = if ($effective) { $effective.AddDays(-30) } else { $null }
                Status           = $status
            }
        }
    } catch {
        $rows += [pscustomobject]@{
            Source='MessageCenter'; Status='Error'; Title=$_.Exception.Message
        }
    }

    # --- Signal 2: Power Platform Release Plan ----------------------------
    if ($Cloud -ne 'Commercial') {
        $rows += [pscustomobject]@{
            Source='PowerPlatformReleasePlan'; Status='NotApplicable'
            Title="Release plan RSS not relied upon in $Cloud — review the published release-plan PDF manually each wave."
        }
    } else {
        try {
            # Public release plan RSS — verify the canonical URL with Microsoft Learn
            # before scheduling. TODO: confirm endpoint stability.
            $rss = Invoke-RestMethod -Uri 'https://learn.microsoft.com/en-us/power-platform/release-plan/feed' -ErrorAction Stop
            foreach ($item in $rss) {
                if ([datetime]$item.pubDate -lt $cutoff) { continue }
                $isModel = "$($item.title) $($item.description)" -match '(?i)copilot studio.*model|default.*model|foundry'
                if (-not $isModel) { continue }
                $rows += [pscustomobject]@{
                    Source           = 'PowerPlatformReleasePlan'
                    Id               = $item.guid
                    Title            = $item.title
                    Category         = 'ReleasePlan'
                    EffectiveUtc     = [datetime]$item.pubDate
                    PublishedUtc     = [datetime]$item.pubDate
                    AffectedAgentCount = 0
                    DispositionDue   = ([datetime]$item.pubDate).AddDays(30)
                    Status           = 'Pending'
                }
            }
        } catch {
            $rows += [pscustomobject]@{ Source='PowerPlatformReleasePlan'; Status='Error'; Title=$_.Exception.Message }
        }
    }

    $aggregate = if ($rows.Status -contains 'Error') { 'Error' }
                 elseif ($rows.Status -contains 'Anomaly') { 'Anomaly' }
                 elseif ($rows.Status -contains 'Pending') { 'Pending' }
                 else { 'Clean' }

    [pscustomobject]@{
        RunUtc   = (Get-Date).ToUniversalTime()
        Cloud    = $Cloud
        Source   = 'Get-FsiAgentModelChangeFeed'
        Status   = $aggregate
        Findings = $rows
    }
}

3.3 — Export-FsiMRMEvidencePackage

Bundles the inventory, change feed, and (optionally) Foundry evaluator runs into a structured folder ready for MRM Committee review and 17a-4(f)-vendor ingestion. Mutation safety is not required — this helper writes only to the local evidence path supplied by the caller; downstream WORM routing is performed by Control 1.9 retention labels and the 17a-4(f) vendor pipeline.

function Export-FsiMRMEvidencePackage {
<#
.SYNOPSIS
    Exports an MRM Committee evidence package: inventory snapshot, vendor-model
    change feed, optional Foundry evaluator-run index, and a SHA-256 manifest.

.DESCRIPTION
    Writes a folder structured as:
      <EvidencePath>\mrm-<yyyyMMddTHHmmssZ>\
        ├─ inventory.json
        ├─ inventory-summary.csv
        ├─ change-feed.json
        ├─ foundry-evaluator-runs.json   (only if -FoundryWorkspaceId supplied)
        ├─ sovereign-readiness.json
        ├─ manifest.json                 (SHA-256 per artifact + script version)
        └─ README.md                     (human-readable cover for committee)

    Each artifact in manifest.json carries Status ∈ {Clean, Anomaly, Pending,
    NotApplicable, Error} so the MRM Committee chair can triage at a glance.

.PARAMETER Cloud
    Commercial | USGov | USGovHigh | USGovDoD

.PARAMETER EvidencePath
    Local folder to write into. Should be Purview-labeled (see Control 1.7) or
    pre-mounted to your 17a-4(f) vendor staging area (see Control 1.9).

.PARAMETER RegistryPath
    Optional 1.2 agent registry CSV/JSON; passed through to the inventory helper.

.PARAMETER FoundryWorkspaceId
    Optional Azure AI Foundry workspace resource ID. When supplied, the helper
    enumerates evaluator runs and includes a structured index. Omit if Foundry
    is not in scope for this MRM cycle.

.PARAMETER ScriptVersion
    Tag used in the manifest. Default 'v1.4'.
#>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][ValidateSet('Commercial','USGov','USGovHigh','USGovDoD')]
        [string]$Cloud,
        [Parameter(Mandatory)][string]$EvidencePath,
        [string]$RegistryPath,
        [string]$FoundryWorkspaceId,
        [string]$ScriptVersion = 'v1.4'
    )

    $ts        = Get-Date -Format 'yyyyMMddTHHmmssZ'
    $pkgRoot   = Join-Path $EvidencePath "mrm-$ts"
    New-Item -ItemType Directory -Force -Path $pkgRoot | Out-Null

    $artifacts = @()

    function _emit {
        param($Name, $Object)
        $path = Join-Path $pkgRoot $Name
        try {
            $Object | ConvertTo-Json -Depth 25 | Set-Content -Path $path -Encoding UTF8
            $hash = (Get-FileHash -Path $path -Algorithm SHA256).Hash
            $artifacts += [pscustomobject]@{
                File          = $Name
                Sha256        = $hash
                Bytes         = (Get-Item $path).Length
                GeneratedUtc  = $ts
                ScriptVersion = $ScriptVersion
                Status        = $Object.Status
            }
        } catch {
            $artifacts += [pscustomobject]@{
                File=$Name; Status='Error'; ScriptVersion=$ScriptVersion;
                GeneratedUtc=$ts; Note=$_.Exception.Message
            }
        }
        # Return the updated artifacts collection back to the parent scope.
        Set-Variable -Scope 1 -Name artifacts -Value $artifacts
    }

    # 1) Sovereign readiness (always first so the cover sheet shows gaps)
    $ready = Test-FsiMRMSovereignReadiness -Cloud $Cloud
    _emit -Name 'sovereign-readiness.json' -Object $ready

    # 2) Inventory
    $inv = Get-FsiAgentInventoryForMRM -Cloud $Cloud -RegistryPath $RegistryPath
    _emit -Name 'inventory.json' -Object $inv
    try {
        $invCsv = $inv.Findings | Select-Object AgentId,DisplayName,Owner,Environment,UnderlyingModel,ModelVersion,Tier,LastValidationDate,NextValidationDate,Source,Status
        $csvPath = Join-Path $pkgRoot 'inventory-summary.csv'
        $invCsv | Export-Csv -NoTypeInformation -Path $csvPath -Encoding UTF8
        $artifacts += [pscustomobject]@{
            File='inventory-summary.csv'
            Sha256=(Get-FileHash $csvPath -Algorithm SHA256).Hash
            Bytes=(Get-Item $csvPath).Length; GeneratedUtc=$ts; ScriptVersion=$ScriptVersion
            Status=$inv.Status
        }
    } catch {
        $artifacts += [pscustomobject]@{
            File='inventory-summary.csv'; Status='Error'
            ScriptVersion=$ScriptVersion; GeneratedUtc=$ts; Note=$_.Exception.Message
        }
    }

    # 3) Change feed
    $feed = Get-FsiAgentModelChangeFeed -Cloud $Cloud -InventoryFindings $inv.Findings
    _emit -Name 'change-feed.json' -Object $feed

    # 4) Foundry evaluator runs (optional — caller provides workspace ID)
    if ($FoundryWorkspaceId) {
        try {
            # TODO: confirm the GA cmdlet for Foundry evaluator-run enumeration
            # in your pinned Az.MachineLearningServices version. The Foundry
            # surface is moving; in the absence of a stable cmdlet, fall back
            # to the REST API at /providers/Microsoft.MachineLearningServices/workspaces/{ws}/jobs.
            $runs = Get-AzMLWorkspaceJob -ResourceId $FoundryWorkspaceId -ErrorAction Stop |
                Where-Object { $_.Properties.jobType -eq 'evaluation' }
            $runIndex = [pscustomobject]@{
                RunUtc   = (Get-Date).ToUniversalTime()
                WorkspaceId = $FoundryWorkspaceId
                Status   = if ($runs.Count -eq 0) { 'Pending' } else { 'Clean' }
                Findings = @($runs | Select-Object Id, Name, Status, CreatedAt, ModelVersion, EvaluatorIds)
            }
            _emit -Name 'foundry-evaluator-runs.json' -Object $runIndex
        } catch {
            _emit -Name 'foundry-evaluator-runs.json' -Object ([pscustomobject]@{
                RunUtc=(Get-Date).ToUniversalTime(); WorkspaceId=$FoundryWorkspaceId
                Status='Error'; Findings=@(); Note=$_.Exception.Message
            })
        }
    }

    # 5) Manifest + README
    $manifest = [pscustomobject]@{
        PackageId     = "mrm-$ts"
        Cloud         = $Cloud
        ScriptVersion = $ScriptVersion
        GeneratedUtc  = $ts
        Status        = if ($artifacts.Status -contains 'Error') { 'Error' }
                        elseif ($artifacts.Status -contains 'Anomaly') { 'Anomaly' }
                        elseif ($artifacts.Status -contains 'Pending') { 'Pending' }
                        elseif ($artifacts.Status -contains 'NotApplicable') { 'NotApplicable' }
                        else { 'Clean' }
        Artifacts     = $artifacts
    }
    $manifest | ConvertTo-Json -Depth 10 | Set-Content -Path (Join-Path $pkgRoot 'manifest.json') -Encoding UTF8

    $readme = @"
# MRM Evidence Package $($manifest.PackageId)

**Generated (UTC):** $ts
**Cloud:** $Cloud
**Script version:** $ScriptVersion
**Aggregate Status:** $($manifest.Status)

This package is **input to** an MRM Committee or independent validation review.
It is not a validation, a tier assignment, or an effective-challenge record. All
MRM judgments under OCC 2011-12 / SR 11-7 §V remain with the firm's MRM
Committee and independent validators.

## Contents
$($artifacts | ForEach-Object { "- ``$($_.File)`` — Status: $($_.Status)" } | Out-String)

## Next steps
1. Apply Purview retention label per Control 1.9 (Data Retention and Deletion Policies).
2. Route to 17a-4(f) audit/vendor staging per Control 1.7 (Comprehensive Audit Logging and Compliance).
3. Add to the next MRM Committee agenda; assign disposition owners for any
   row with Status = Anomaly or Pending.
"@
    $readme | Set-Content -Path (Join-Path $pkgRoot 'README.md') -Encoding UTF8

    [pscustomobject]@{
        RunUtc        = (Get-Date).ToUniversalTime()
        PackagePath   = $pkgRoot
        Status        = $manifest.Status
        ArtifactCount = $artifacts.Count
        Manifest      = $manifest
    }
}

§4 — Reporting recipes

4.1 — Quarterly inventory snapshot for MRM Committee

$pkg = Export-FsiMRMEvidencePackage `
    -Cloud Commercial `
    -EvidencePath 'D:\evidence\mrm\Q2-2026' `
    -RegistryPath 'D:\registry\agent-registry-1.2.json' `
    -FoundryWorkspaceId '/subscriptions/.../workspaces/mrm-foundry-prod'

$pkg | Format-List PackagePath, Status, ArtifactCount
# Hand PackagePath to MRM Committee chair via Purview-labeled email or
# SharePoint location with retention label applied (see Control 1.7).

4.2 — Validation-due-soon list (next 30 / 60 / 90 days)

$inv = Get-FsiAgentInventoryForMRM -Cloud Commercial -RegistryPath '.\registry.json'
$now = Get-Date

$dueSoon = $inv.Findings | Where-Object {
    $_.NextValidationDate -and ([datetime]$_.NextValidationDate - $now).TotalDays -le 90
} | Select-Object AgentId, DisplayName, Tier, NextValidationDate,
    @{n='DaysUntilDue';e={[int]([datetime]$_.NextValidationDate - $now).TotalDays}},
    @{n='Bucket';e={
        $d = ([datetime]$_.NextValidationDate - $now).TotalDays
        if ($d -le 30) {'30-day'} elseif ($d -le 60) {'60-day'} else {'90-day'}
    }} | Sort-Object DaysUntilDue

$dueSoon | Export-Csv -Path '.\validation-due-soon.csv' -NoTypeInformation
$dueSoon | Group-Object Bucket | Format-Table Name, Count

4.3 — Vendor-model-change disposition log

$inv  = Get-FsiAgentInventoryForMRM -Cloud Commercial -RegistryPath '.\registry.json'
$feed = Get-FsiAgentModelChangeFeed -Cloud Commercial -InventoryFindings $inv.Findings -LookbackDays 180

$dispositionLog = $feed.Findings | Where-Object Status -in @('Anomaly','Pending') |
    Select-Object Source, Id, Title, EffectiveUtc, AffectedAgentCount, DispositionDue, Status,
        @{n='OwnerToAssign';e={'MRM Committee chair'}},
        @{n='DispositionDecision';e={''}},
        @{n='DecisionDateUtc';e={''}}

$dispositionLog | Export-Csv -Path '.\sr11-7-vendor-model-disposition.csv' -NoTypeInformation
# DispositionDecision and DecisionDateUtc are filled in by the MRM Committee,
# NOT by this script. The script produces the queue; people produce the decisions.

4.4 — Outcomes-analysis evidence index

Outcomes analysis (SR 11-7 §V) requires that model output be compared against actual outcomes on an ongoing basis. This recipe indexes the evidence inputs the MRM analyst will join — it does not perform the analysis.

$pkg = Export-FsiMRMEvidencePackage -Cloud Commercial `
    -EvidencePath 'D:\evidence\mrm\outcomes-Q2-2026' `
    -RegistryPath '.\registry.json' `
    -FoundryWorkspaceId '/subscriptions/.../workspaces/mrm-foundry-prod'

$index = [pscustomobject]@{
    PackagePath       = $pkg.PackagePath
    InventoryArtifact = 'inventory.json'
    EvaluatorArtifact = 'foundry-evaluator-runs.json'
    ChangeFeedArtifact= 'change-feed.json'
    Status            = $pkg.Status
    AnalystAction     = 'Join evaluator run output to production telemetry from the firm''s downstream business systems; perform outcomes analysis per SR 11-7 §V; record conclusion in MRM Committee minutes.'
}
$index | ConvertTo-Json -Depth 5 |
    Set-Content -Path (Join-Path $pkg.PackagePath 'outcomes-analysis-index.json')

§5 — Scheduling and retention

5.1 — Azure Automation pattern (commercial cloud)

Run the quarterly snapshot as an Azure Automation runbook with a managed identity granted the read-only roles in §1.2. Do not grant remediation roles to the runbook identity — this control's automation is read-only.

# Runbook entry point (PowerShell 7.2 Runbook, hybrid worker recommended for
# Power Platform cmdlet support which still requires Windows PowerShell 5.1).
param([string]$Cloud='Commercial')

Connect-AzAccount -Identity -Environment AzureCloud | Out-Null
Connect-MgGraph -Identity -Environment Global -NoWelcome
# Power Platform cmdlets cannot use managed identity directly; use a CAB-approved
# certificate-based service principal stored in Automation credentials.
$pp = Get-AutomationPSCredential -Name 'pp-coe-readonly-spn'
Add-PowerAppsAccount -ApplicationId $pp.UserName -ClientSecret $pp.GetNetworkCredential().Password -TenantID $env:AZURE_TENANT_ID

$evidencePath = Join-Path $env:TEMP "mrm-$(Get-Date -Format yyyyMMdd)"
$pkg = Export-FsiMRMEvidencePackage -Cloud $Cloud -EvidencePath $evidencePath `
    -RegistryPath $env:MRM_REGISTRY_BLOB_LOCAL_PATH

# Upload to Purview-labeled SharePoint or to 17a-4(f) vendor staging blob
Set-AzStorageBlobContent -Container 'mrm-evidence' `
    -File (Join-Path $pkg.PackagePath 'manifest.json') `
    -Blob "$($pkg.Manifest.PackageId)/manifest.json" `
    -Context (New-AzStorageContext -StorageAccountName $env:WORM_STORAGE_ACCOUNT -UseConnectedAccount)
# Apply Purview retention label downstream — see Control 1.9.

5.2 — Sovereign / on-prem scheduled task pattern

For sovereign tenants or where Automation is not approved, use a Windows Task Scheduler job on a hardened admin workstation. The task must:

  • Run under a least-privilege managed service account.
  • Write artifacts to a folder that is inside a Purview retention scope OR mounted to your 17a-4(f) vendor's secure ingestion share.
  • Produce Start-Transcript / Stop-Transcript logs for the audit trail (see baseline §4).

5.3 — Retention routing

Artifact Retention requirement Routing
Inventory snapshot (CSV + JSON) 7 years (SOX 802) minimum; 6 years (SEC 17a-4) for broker-dealer scope Purview retention label (Control 1.7) → 17a-4(f) vendor (Control 1.9)
Vendor-model-change disposition log Life of model + 7 years (MRM policy norm) Same as above
Foundry evaluator-run index 7 years; underlying run artifacts retained per Foundry workspace policy Same as above
MRM Committee minutes referencing the package Permanent (MRM policy) Records Management retention label
manifest.json (SHA-256 integrity) Permanent — integrity proof must outlive the artifact Records Management retention label

Cross-link: Control 1.9 — Data Retention and Deletion Policies, Control 1.7 — Comprehensive Audit Logging and Compliance.


§6 — Verification checks

Run these after every scheduled execution. Each check returns a [pscustomobject] with the canonical Status enum so a downstream SIEM / Sentinel rule can alert on Anomaly or Error without parsing free text.

function Test-FsiMRMPackageIntegrity {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$PackagePath)

    $checks = @()
    $manifestPath = Join-Path $PackagePath 'manifest.json'

    # Check 1: package exists
    $checks += [pscustomobject]@{
        Check  = 'PackageExists'
        Status = if (Test-Path $PackagePath) { 'Clean' } else { 'Error' }
        Note   = $PackagePath
    }

    # Check 2: manifest exists
    $checks += [pscustomobject]@{
        Check  = 'ManifestExists'
        Status = if (Test-Path $manifestPath) { 'Clean' } else { 'Error' }
        Note   = $manifestPath
    }
    if (-not (Test-Path $manifestPath)) {
        return [pscustomobject]@{ Status='Error'; Checks=$checks }
    }

    $manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json

    # Check 3: every artifact in manifest exists on disk
    foreach ($a in $manifest.Artifacts) {
        $p = Join-Path $PackagePath $a.File
        $checks += [pscustomobject]@{
            Check  = "ArtifactPresent:$($a.File)"
            Status = if (Test-Path $p) { 'Clean' } else { 'Anomaly' }
            Note   = $p
        }
    }

    # Check 4: SHA-256 matches recorded hash
    foreach ($a in $manifest.Artifacts) {
        $p = Join-Path $PackagePath $a.File
        if (-not (Test-Path $p)) { continue }
        $live = (Get-FileHash -Path $p -Algorithm SHA256).Hash
        $checks += [pscustomobject]@{
            Check  = "ShaMatch:$($a.File)"
            Status = if ($live -eq $a.Sha256) { 'Clean' } else { 'Anomaly' }
            Note   = "expected=$($a.Sha256) actual=$live"
        }
    }

    # Check 5: package age (alert if > 100 days for quarterly cadence)
    $ageDays = ((Get-Date).ToUniversalTime() - [datetime]::ParseExact($manifest.GeneratedUtc,'yyyyMMddTHHmmssZ',$null)).TotalDays
    $checks += [pscustomobject]@{
        Check  = 'PackageFreshness'
        Status = if ($ageDays -le 100) { 'Clean' } elseif ($ageDays -le 130) { 'Anomaly' } else { 'Error' }
        Note   = "$([int]$ageDays) days old"
    }

    # Check 6: inventory contains at least one row (else likely auth failure)
    $invPath = Join-Path $PackagePath 'inventory.json'
    if (Test-Path $invPath) {
        $inv = Get-Content $invPath -Raw | ConvertFrom-Json
        $checks += [pscustomobject]@{
            Check  = 'InventoryNonEmpty'
            Status = if ($inv.Findings.Count -gt 0) { 'Clean' } else { 'Anomaly' }
            Note   = "$($inv.Findings.Count) row(s)"
        }
    }

    [pscustomobject]@{
        PackagePath = $PackagePath
        RunUtc      = (Get-Date).ToUniversalTime()
        Status      = if ($checks.Status -contains 'Error') { 'Error' }
                      elseif ($checks.Status -contains 'Anomaly') { 'Anomaly' }
                      else { 'Clean' }
        Checks      = $checks
    }
}

§7 — Troubleshooting hooks

Symptom Likely cause First check Cross-link
Get-FsiAgentInventoryForMRM returns 0 rows from DSPM for AI in commercial cloud DSPM for AI not provisioned, or Purview Data Security AI Admin role not active Run Test-FsiMRMSovereignReadiness -Cloud Commercial; verify role activation in PIM Troubleshooting playbook
Get-FsiAgentInventoryForMRM returns 0 rows from Agent 365 Admin Center Agent 365 not GA in your region, or licensing not enabled Confirm Agent 365 SKU per Microsoft Learn; check ($ready.Surfaces \| ? Surface -eq 'Agent365Admin').Status Control 2.25
Export-FsiMRMEvidencePackage writes Foundry artifact with Status='Error' and Note=ResourceNotFound Foundry workspace ID malformed or RBAC missing Validate full /subscriptions/.../resourceGroups/.../providers/Microsoft.MachineLearningServices/workspaces/<name> path; verify Contributor + Storage Blob Data Reader Troubleshooting playbook
Power Platform cmdlets return empty environment list in sovereign cloud Wrong -Endpoint value on Add-PowerAppsAccount Re-run with the correct sovereign endpoint per baseline §3 Baseline §3
Graph throttling (HTTP 429) on aiInteractionHistory or messages DSPM for AI / Message Center pages aggressively at tenant scale Implement Retry-After honoring; reduce $top; split runs by week Troubleshooting playbook
Test-FsiMRMPackageIntegrity reports ShaMatch:Anomaly Artifact mutated after manifest write (possible WORM violation) Treat as incident; preserve package and the divergent artifact; notify Purview Records Manager and Internal Audit Control 1.7
Get-AdminBot cmdlet not found Module surface for Copilot Studio bot enumeration changed; module pinning drifted Check pinned Microsoft.PowerApps.Administration.PowerShell version; review module release notes; substitute Dataverse query or BAP REST call as fallback Baseline §1
Sovereign cloud package shows aggregate Status='NotApplicable' for multiple surfaces Expected behavior — apply compensating controls Review sovereign-readiness.json CompensatingControl field; document fallback in MRM Committee minutes Control 2.6 sovereign caveat

For full diagnostic flows, see the companion Troubleshooting playbook.


Cross-references


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