PowerShell Setup: Control 3.12 - Agent Governance Exception and Override Management
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.
Last Updated: April 2026 Prerequisites: Microsoft.PowerApps.Administration.PowerShell module, Microsoft Graph PowerShell SDK Estimated Time: 45-60 minutes
Overview
This playbook provides PowerShell scripts for automating exception management tasks including:
- Exporting exception register data for audit reviews
- Detecting and alerting on expired or expiring exceptions
- Generating exception compliance reports by zone
- Monitoring renewal counts and excessive exceptions
- Creating audit-ready evidence exports with SHA-256 integrity hashes
Prerequisites
Required PowerShell Modules
Install the following modules with versions pinned per the PowerShell Authoring Baseline §1. Replace <version> with the build approved by your Change Advisory Board (CAB). The example pins below are illustrative; verify the current GA build for your tenant before each change window.
# Microsoft.Xrm.Data.PowerShell — community module for FetchXML / Dataverse on Windows PowerShell 5.1
Install-Module -Name Microsoft.Xrm.Data.PowerShell `
-RequiredVersion '<version>' `
-Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
# Microsoft Graph SDK — only needed when validating approver identities or sending Graph mail
Install-Module -Name Microsoft.Graph `
-RequiredVersion '<version>' `
-Repository PSGallery -Scope CurrentUser -AllowClobber -AcceptLicense
Edition requirement
Microsoft.Xrm.Data.PowerShell runs on Windows PowerShell 5.1 (Desktop). Add the edition guard from baseline §2 at the top of every script. Microsoft Graph SDK requires PowerShell 7.2+. If you mix the two, run them in separate processes — do not import both into the same session.
Sovereign clouds (GCC / GCC High / DoD)
Every connection example below assumes the commercial Dataverse endpoint. For US Government tenants, set the environment URL to your sovereign endpoint (e.g., https://orgname.crm.appsplatform.us for GCC High, https://orgname.crm.microsoftdynamics.us for DoD) and pass the matching -Environment value to Connect-MgGraph per baseline §3. Running these scripts against the wrong endpoint authenticates against commercial and returns false-clean results.
Required Permissions
- Power Platform Admin role (to query Dataverse)
- Dataverse system administrator or custom role with read access to Governance Exceptions table
- Microsoft Graph User.Read.All (to validate approver identities)
Script 1: Export Exception Register
Purpose
Export all exception records from Dataverse with complete audit trail for compliance reviews.
Script: Get-ExceptionRegister.ps1
<#
.SYNOPSIS
Exports agent governance exception register from Dataverse.
.DESCRIPTION
Retrieves all exception records including approval history, expiration dates,
and audit trail. Exports to CSV for compliance review and regulatory examination.
.PARAMETER EnvironmentUrl
The URL of the Dataverse environment (e.g., https://org.crm.dynamics.com)
.PARAMETER OutputPath
Directory path for output CSV file
.PARAMETER FilterStatus
Optional. Filter by approval status (Pending, Fully Approved, Expired, Closed)
.EXAMPLE
.\Get-ExceptionRegister.ps1 -EnvironmentUrl "https://contoso.crm.dynamics.com" -OutputPath "C:\Reports"
.EXAMPLE
.\Get-ExceptionRegister.ps1 -EnvironmentUrl "https://contoso.crm.dynamics.com" -OutputPath "C:\Reports" -FilterStatus "Fully Approved"
#>
param(
[Parameter(Mandatory=$true)]
[string]$EnvironmentUrl,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[Parameter(Mandatory=$false)]
[ValidateSet("All", "Pending", "Fully Approved", "Expired", "Closed", "Denied")]
[string]$FilterStatus = "All"
)
# Import required modules
Import-Module Microsoft.Xrm.Data.PowerShell
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Exception Register Export" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
# Connect to Dataverse
Write-Host "Connecting to Dataverse environment: $EnvironmentUrl..." -ForegroundColor Cyan
$conn = Connect-CrmOnline -ServerUrl $EnvironmentUrl
if ($conn.IsReady) {
Write-Host "✓ Connected successfully" -ForegroundColor Green
} else {
Write-Host "✗ Failed to connect to Dataverse" -ForegroundColor Red
exit 1
}
# Build FetchXML query
Write-Host "Querying Governance Exceptions table..." -ForegroundColor Cyan
$fetchXml = @"
<fetch>
<entity name="fsi_governanceexception">
<attribute name="fsi_governanceexceptionid" />
<attribute name="fsi_name" />
<attribute name="fsi_exceptionrequestdate" />
<attribute name="fsi_requestor" />
<attribute name="fsi_agentname" />
<attribute name="fsi_governancezone" />
<attribute name="fsi_exceptiontype" />
<attribute name="fsi_businessjustification" />
<attribute name="fsi_riskassessment" />
<attribute name="fsi_compensatingcontrols" />
<attribute name="fsi_approvalstatus" />
<attribute name="fsi_approver1" />
<attribute name="fsi_approvaldate1" />
<attribute name="fsi_approver2" />
<attribute name="fsi_approvaldate2" />
<attribute name="fsi_approver3" />
<attribute name="fsi_approvaldate3" />
<attribute name="fsi_expirationdate" />
<attribute name="fsi_renewalcount" />
<attribute name="fsi_closuredate" />
<attribute name="fsi_closurereason" />
<attribute name="createdon" />
<attribute name="modifiedon" />
"@
# Add status filter if specified
if ($FilterStatus -ne "All") {
$fetchXml += @"
<filter type="and">
<condition attribute="fsi_approvalstatus" operator="eq" value="$FilterStatus" />
</filter>
"@
}
$fetchXml += @"
<order attribute="fsi_exceptionrequestdate" descending="true" />
</entity>
</fetch>
"@
# Execute query
$results = Get-CrmRecords -conn $conn -Fetch $fetchXml
Write-Host "Found $($results.CrmRecords.Count) exception records" -ForegroundColor White
# Transform results to CSV-friendly format
$exportData = @()
foreach ($record in $results.CrmRecords) {
$exportData += [PSCustomObject]@{
ExceptionID = $record.fsi_governanceexceptionid
ExceptionName = $record.fsi_name
RequestDate = $record.fsi_exceptionrequestdate
Requestor = $record.fsi_requestor_Property.Value.Name
AgentName = $record.fsi_agentname
GovernanceZone = $record.fsi_governancezone_Property.Value.Name
ExceptionType = $record.fsi_exceptiontype_Property.Value.Name
BusinessJustification = $record.fsi_businessjustification
RiskAssessment = $record.fsi_riskassessment
CompensatingControls = $record.fsi_compensatingcontrols
ApprovalStatus = $record.fsi_approvalstatus_Property.Value.Name
Approver1 = if ($record.fsi_approver1_Property.Value) { $record.fsi_approver1_Property.Value.Name } else { "" }
ApprovalDate1 = $record.fsi_approvaldate1
Approver2 = if ($record.fsi_approver2_Property.Value) { $record.fsi_approver2_Property.Value.Name } else { "" }
ApprovalDate2 = $record.fsi_approvaldate2
Approver3 = if ($record.fsi_approver3_Property.Value) { $record.fsi_approver3_Property.Value.Name } else { "" }
ApprovalDate3 = $record.fsi_approvaldate3
ExpirationDate = $record.fsi_expirationdate
RenewalCount = $record.fsi_renewalcount
ClosureDate = $record.fsi_closuredate
ClosureReason = $record.fsi_closurereason
CreatedOn = $record.createdon
ModifiedOn = $record.modifiedon
}
}
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$outputFile = Join-Path $OutputPath "ExceptionRegister_$timestamp.csv"
$exportData | Export-Csv -Path $outputFile -NoTypeInformation -Encoding UTF8
Write-Host "✓ Exception register exported to: $outputFile" -ForegroundColor Green
# Calculate statistics
$stats = @{
TotalExceptions = $exportData.Count
Pending = ($exportData | Where-Object { $_.ApprovalStatus -eq "Pending" }).Count
FullyApproved = ($exportData | Where-Object { $_.ApprovalStatus -eq "Fully Approved" }).Count
Expired = ($exportData | Where-Object { $_.ApprovalStatus -eq "Expired" }).Count
Closed = ($exportData | Where-Object { $_.ApprovalStatus -eq "Closed" }).Count
Denied = ($exportData | Where-Object { $_.ApprovalStatus -eq "Denied" }).Count
}
Write-Host ""
Write-Host "--- Exception Register Summary ---" -ForegroundColor Cyan
Write-Host "Total Exceptions: $($stats.TotalExceptions)" -ForegroundColor White
Write-Host " Pending: $($stats.Pending)" -ForegroundColor Yellow
Write-Host " Fully Approved: $($stats.FullyApproved)" -ForegroundColor Green
Write-Host " Expired: $($stats.Expired)" -ForegroundColor Red
Write-Host " Closed: $($stats.Closed)" -ForegroundColor Gray
Write-Host " Denied: $($stats.Denied)" -ForegroundColor Red
Write-Host ""
Write-Host "Script completed successfully." -ForegroundColor Cyan
Script 2: Detect Expiring and Expired Exceptions
Purpose
Identify exceptions nearing expiration or already expired requiring remediation.
Script: Find-ExpiringExceptions.ps1
<#
.SYNOPSIS
Detects agent governance exceptions expiring soon or already expired.
.DESCRIPTION
Queries Dataverse for approved exceptions expiring within specified days.
Sends alerts via email and generates remediation report.
.PARAMETER EnvironmentUrl
The URL of the Dataverse environment
.PARAMETER OutputPath
Directory path for output CSV file
.PARAMETER ExpirationWindowDays
Number of days to look ahead for expiring exceptions (default: 7)
.PARAMETER SendEmailAlerts
If specified, sends email alerts to requestors and approvers
.PARAMETER SmtpServer
SMTP server for email alerts (required if SendEmailAlerts is specified)
.PARAMETER FromEmail
From email address (required if SendEmailAlerts is specified)
.EXAMPLE
.\Find-ExpiringExceptions.ps1 -EnvironmentUrl "https://contoso.crm.dynamics.com" -OutputPath "C:\Reports" -ExpirationWindowDays 7
#>
param(
[Parameter(Mandatory=$true)]
[string]$EnvironmentUrl,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[Parameter(Mandatory=$false)]
[int]$ExpirationWindowDays = 7,
[Parameter(Mandatory=$false)]
[switch]$SendEmailAlerts,
[Parameter(Mandatory=$false)]
[string]$SmtpServer,
[Parameter(Mandatory=$false)]
[string]$FromEmail
)
# Import required modules
Import-Module Microsoft.Xrm.Data.PowerShell
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Exception Expiration Monitor" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
# Connect to Dataverse
Write-Host "Connecting to Dataverse environment: $EnvironmentUrl..." -ForegroundColor Cyan
$conn = Connect-CrmOnline -ServerUrl $EnvironmentUrl
if (-not $conn.IsReady) {
Write-Host "✗ Failed to connect to Dataverse" -ForegroundColor Red
exit 1
}
Write-Host "✓ Connected successfully" -ForegroundColor Green
# Calculate date thresholds
$today = Get-Date -Format "yyyy-MM-dd"
$futureDate = (Get-Date).AddDays($ExpirationWindowDays).ToString("yyyy-MM-dd")
Write-Host "Searching for exceptions expiring between $today and $futureDate..." -ForegroundColor Cyan
# Build FetchXML query for expiring exceptions
$fetchXml = @"
<fetch>
<entity name="fsi_governanceexception">
<attribute name="fsi_governanceexceptionid" />
<attribute name="fsi_name" />
<attribute name="fsi_agentname" />
<attribute name="fsi_requestor" />
<attribute name="fsi_governancezone" />
<attribute name="fsi_exceptiontype" />
<attribute name="fsi_expirationdate" />
<attribute name="fsi_renewalcount" />
<attribute name="fsi_approver1" />
<attribute name="fsi_approver2" />
<attribute name="fsi_approver3" />
<filter type="and">
<condition attribute="fsi_approvalstatus" operator="eq" value="Fully Approved" />
<condition attribute="fsi_expirationdate" operator="on-or-before" value="$futureDate" />
<condition attribute="fsi_expirationdate" operator="on-or-after" value="$today" />
</filter>
<order attribute="fsi_expirationdate" descending="false" />
</entity>
</fetch>
"@
$results = Get-CrmRecords -conn $conn -Fetch $fetchXml
Write-Host "Found $($results.CrmRecords.Count) exceptions expiring within $ExpirationWindowDays days" -ForegroundColor Yellow
# Also query for already expired exceptions
$fetchXmlExpired = @"
<fetch>
<entity name="fsi_governanceexception">
<attribute name="fsi_governanceexceptionid" />
<attribute name="fsi_name" />
<attribute name="fsi_agentname" />
<attribute name="fsi_requestor" />
<attribute name="fsi_governancezone" />
<attribute name="fsi_exceptiontype" />
<attribute name="fsi_expirationdate" />
<attribute name="fsi_renewalcount" />
<filter type="and">
<condition attribute="fsi_approvalstatus" operator="eq" value="Fully Approved" />
<condition attribute="fsi_expirationdate" operator="last-x-days" value="0" />
</filter>
<order attribute="fsi_expirationdate" descending="false" />
</entity>
</fetch>
"@
$resultsExpired = Get-CrmRecords -conn $conn -Fetch $fetchXmlExpired
Write-Host "Found $($resultsExpired.CrmRecords.Count) exceptions already expired" -ForegroundColor Red
# Combine results
$allResults = @()
$allResults += $results.CrmRecords
$allResults += $resultsExpired.CrmRecords
# Transform to export format
$exportData = @()
foreach ($record in $allResults) {
$expirationDate = [DateTime]::Parse($record.fsi_expirationdate)
$daysUntilExpiration = ($expirationDate - (Get-Date)).Days
$status = if ($daysUntilExpiration -lt 0) { "EXPIRED" }
elseif ($daysUntilExpiration -le 3) { "CRITICAL (<=3 days)" }
elseif ($daysUntilExpiration -le 7) { "WARNING (<=7 days)" }
else { "OK" }
$exportData += [PSCustomObject]@{
ExceptionID = $record.fsi_governanceexceptionid
AgentName = $record.fsi_agentname
Requestor = $record.fsi_requestor_Property.Value.Name
GovernanceZone = $record.fsi_governancezone_Property.Value.Name
ExceptionType = $record.fsi_exceptiontype_Property.Value.Name
ExpirationDate = $expirationDate.ToString("yyyy-MM-dd")
DaysUntilExpiration = $daysUntilExpiration
Status = $status
RenewalCount = $record.fsi_renewalcount
MaxRenewalsReached = if ($record.fsi_renewalcount -ge 2) { "YES" } else { "NO" }
Approver1 = if ($record.fsi_approver1_Property.Value) { $record.fsi_approver1_Property.Value.Name } else { "" }
Approver2 = if ($record.fsi_approver2_Property.Value) { $record.fsi_approver2_Property.Value.Name } else { "" }
Approver3 = if ($record.fsi_approver3_Property.Value) { $record.fsi_approver3_Property.Value.Name } else { "" }
}
}
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$outputFile = Join-Path $OutputPath "ExpiringExceptions_$timestamp.csv"
$exportData | Export-Csv -Path $outputFile -NoTypeInformation -Encoding UTF8
Write-Host "✓ Expiring exceptions report saved to: $outputFile" -ForegroundColor Green
# Display summary
Write-Host ""
Write-Host "--- Expiration Summary ---" -ForegroundColor Cyan
Write-Host "Total Expiring/Expired: $($exportData.Count)" -ForegroundColor White
Write-Host " EXPIRED: $(($exportData | Where-Object { $_.Status -eq 'EXPIRED' }).Count)" -ForegroundColor Red
Write-Host " CRITICAL (<=3 days): $(($exportData | Where-Object { $_.Status -eq 'CRITICAL (<=3 days)' }).Count)" -ForegroundColor Red
Write-Host " WARNING (<=7 days): $(($exportData | Where-Object { $_.Status -eq 'WARNING (<=7 days)' }).Count)" -ForegroundColor Yellow
Write-Host " Max Renewals Reached: $(($exportData | Where-Object { $_.MaxRenewalsReached -eq 'YES' }).Count)" -ForegroundColor Red
Write-Host ""
Write-Host "Script completed successfully." -ForegroundColor Cyan
Script 3: Generate Exception Compliance Report
Purpose
Create zone-specific compliance report showing exception adherence to governance policies.
Script: Get-ExceptionComplianceReport.ps1
<#
.SYNOPSIS
Generates compliance report for agent governance exceptions by zone.
.DESCRIPTION
Analyzes exception data against governance policies including maximum durations,
renewal limits, and compensating control requirements.
.PARAMETER EnvironmentUrl
The URL of the Dataverse environment
.PARAMETER OutputPath
Directory path for output CSV file
.EXAMPLE
.\Get-ExceptionComplianceReport.ps1 -EnvironmentUrl "https://contoso.crm.dynamics.com" -OutputPath "C:\Reports"
#>
param(
[Parameter(Mandatory=$true)]
[string]$EnvironmentUrl,
[Parameter(Mandatory=$true)]
[string]$OutputPath
)
# Import required modules
Import-Module Microsoft.Xrm.Data.PowerShell
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Exception Compliance Report" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
# Connect to Dataverse
Write-Host "Connecting to Dataverse environment: $EnvironmentUrl..." -ForegroundColor Cyan
$conn = Connect-CrmOnline -ServerUrl $EnvironmentUrl
if (-not $conn.IsReady) {
Write-Host "✗ Failed to connect to Dataverse" -ForegroundColor Red
exit 1
}
Write-Host "✓ Connected successfully" -ForegroundColor Green
# Query all active exceptions
Write-Host "Querying all active exceptions..." -ForegroundColor Cyan
$fetchXml = @"
<fetch>
<entity name="fsi_governanceexception">
<attribute name="fsi_governanceexceptionid" />
<attribute name="fsi_name" />
<attribute name="fsi_agentname" />
<attribute name="fsi_governancezone" />
<attribute name="fsi_exceptiontype" />
<attribute name="fsi_exceptionrequestdate" />
<attribute name="fsi_expirationdate" />
<attribute name="fsi_renewalcount" />
<attribute name="fsi_compensatingcontrols" />
<attribute name="fsi_approvalstatus" />
<filter type="and">
<condition attribute="fsi_approvalstatus" operator="eq" value="Fully Approved" />
</filter>
</entity>
</fetch>
"@
$results = Get-CrmRecords -conn $conn -Fetch $fetchXml
Write-Host "Analyzing $($results.CrmRecords.Count) active exceptions for compliance..." -ForegroundColor Cyan
# Define policy limits by zone
$zoneLimits = @{
"Zone 1 - Personal" = 90
"Zone 2 - Team" = 60
"Zone 3 - Enterprise" = 30
}
# Analyze compliance
$complianceData = @()
foreach ($record in $results.CrmRecords) {
$zone = $record.fsi_governancezone_Property.Value.Name
$requestDate = [DateTime]::Parse($record.fsi_exceptionrequestdate)
$expirationDate = [DateTime]::Parse($record.fsi_expirationdate)
$duration = ($expirationDate - $requestDate).Days
$maxDuration = $zoneLimits[$zone]
# Check compliance
$issues = @()
# Duration compliance
if ($duration -gt $maxDuration) {
$issues += "Duration exceeds maximum ($duration days > $maxDuration days)"
}
# Renewal count compliance
if ($record.fsi_renewalcount -gt 2) {
$issues += "Renewal count exceeds limit ($($record.fsi_renewalcount) > 2)"
}
# Compensating controls requirement
if ($zone -in @("Zone 2 - Team", "Zone 3 - Enterprise") -and
([string]::IsNullOrWhiteSpace($record.fsi_compensatingcontrols) -or
$record.fsi_compensatingcontrols.Length -lt 50)) {
$issues += "Insufficient compensating controls documentation"
}
$complianceStatus = if ($issues.Count -eq 0) { "Compliant" } else { "Non-Compliant" }
$complianceData += [PSCustomObject]@{
ExceptionID = $record.fsi_governanceexceptionid
AgentName = $record.fsi_agentname
GovernanceZone = $zone
ExceptionType = $record.fsi_exceptiontype_Property.Value.Name
DurationDays = $duration
MaxAllowedDays = $maxDuration
RenewalCount = $record.fsi_renewalcount
ComplianceStatus = $complianceStatus
Issues = if ($issues.Count -gt 0) { $issues -join "; " } else { "None" }
}
}
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$outputFile = Join-Path $OutputPath "ExceptionComplianceReport_$timestamp.csv"
$complianceData | Export-Csv -Path $outputFile -NoTypeInformation -Encoding UTF8
Write-Host "✓ Compliance report saved to: $outputFile" -ForegroundColor Green
# Calculate summary statistics
$totalExceptions = $complianceData.Count
$compliantCount = ($complianceData | Where-Object { $_.ComplianceStatus -eq "Compliant" }).Count
$nonCompliantCount = $totalExceptions - $compliantCount
$complianceRate = if ($totalExceptions -gt 0) {
[math]::Round(($compliantCount / $totalExceptions) * 100, 2)
} else {
0
}
Write-Host ""
Write-Host "--- Compliance Summary ---" -ForegroundColor Cyan
Write-Host "Total Active Exceptions: $totalExceptions" -ForegroundColor White
Write-Host " Compliant: $compliantCount" -ForegroundColor Green
Write-Host " Non-Compliant: $nonCompliantCount" -ForegroundColor Red
Write-Host "Compliance Rate: $complianceRate%" -ForegroundColor White
# Zone-specific breakdown
Write-Host ""
Write-Host "--- Compliance by Zone ---" -ForegroundColor Cyan
foreach ($zone in $zoneLimits.Keys | Sort-Object) {
$zoneTotal = ($complianceData | Where-Object { $_.GovernanceZone -eq $zone }).Count
$zoneCompliant = ($complianceData | Where-Object { $_.GovernanceZone -eq $zone -and $_.ComplianceStatus -eq "Compliant" }).Count
$zoneRate = if ($zoneTotal -gt 0) { [math]::Round(($zoneCompliant / $zoneTotal) * 100, 2) } else { 0 }
Write-Host "$zone : $zoneCompliant / $zoneTotal ($zoneRate%)" -ForegroundColor White
}
if ($nonCompliantCount -gt 0) {
Write-Host ""
Write-Host "⚠ WARNING: $nonCompliantCount non-compliant exceptions require immediate review" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Script completed successfully." -ForegroundColor Cyan
Script 4: Create Audit-Ready Evidence Export
Purpose
Generate a SHA-256 hashed evidence package with manifest.json for regulatory examination, aligned to the PowerShell Authoring Baseline §5. The manifest records {file, sha256, bytes, generated_utc, script_version} for every artifact and supports chain-of-custody for SEC 17a-4(f) WORM evidence and FINRA Rule 4511 record-keeping.
Land artifacts in WORM storage
The script writes to a local directory for review, but audit-defensible evidence must land in storage configured for write-once-read-many — Microsoft Purview Data Lifecycle Management retention lock, SharePoint records center with retention label, or Azure Storage immutability policy. Local disk is not sufficient under SEC 17a-4(f) or FINRA Rule 4511.
Script: Export-ExceptionAuditEvidence.ps1
<#
.SYNOPSIS
Creates audit-ready evidence export of exception register with integrity hash.
.DESCRIPTION
Exports exception data with SHA-256 hash for regulatory examination.
Includes metadata about export process for chain of custody.
.PARAMETER EnvironmentUrl
The URL of the Dataverse environment
.PARAMETER OutputPath
Directory path for output files
.PARAMETER ExaminerName
Name of person requesting evidence
.PARAMETER ExaminationPurpose
Purpose of evidence collection
.EXAMPLE
.\Export-ExceptionAuditEvidence.ps1 -EnvironmentUrl "https://contoso.crm.dynamics.com" -OutputPath "C:\Evidence" -ExaminerName "Jane Auditor" -ExaminationPurpose "Annual SOX Audit"
#>
param(
[Parameter(Mandatory=$true)]
[string]$EnvironmentUrl,
[Parameter(Mandatory=$true)]
[string]$OutputPath,
[Parameter(Mandatory=$true)]
[string]$ExaminerName,
[Parameter(Mandatory=$true)]
[string]$ExaminationPurpose
)
# Import required modules
Import-Module Microsoft.Xrm.Data.PowerShell
# Edition guard (baseline §2) — Microsoft.Xrm.Data.PowerShell requires Windows PowerShell 5.1
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "This script requires Windows PowerShell 5.1 (Desktop). Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)."
}
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Exception Audit Evidence Export" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
# Create evidence directory + start transcript for full session capture
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$evidenceDir = Join-Path $OutputPath "ExceptionEvidence_$timestamp"
New-Item -ItemType Directory -Path $evidenceDir -Force | Out-Null
Start-Transcript -Path (Join-Path $evidenceDir "transcript-$timestamp.log") -IncludeInvocationHeader | Out-Null
Write-Host "Evidence directory: $evidenceDir" -ForegroundColor Cyan
# Connect to Dataverse
Write-Host "Connecting to Dataverse environment: $EnvironmentUrl..." -ForegroundColor Cyan
$conn = Connect-CrmOnline -ServerUrl $EnvironmentUrl
if (-not $conn.IsReady) {
Write-Host "✗ Failed to connect to Dataverse" -ForegroundColor Red
exit 1
}
Write-Host "✓ Connected successfully" -ForegroundColor Green
# Export all exceptions (same query as Script 1)
Write-Host "Exporting exception register..." -ForegroundColor Cyan
# Query all exceptions (same FetchXML as Script 1)
$fetchXml = @"
<fetch>
<entity name="fsi_governanceexception">
<attribute name="fsi_governanceexceptionid" />
<attribute name="fsi_name" />
<attribute name="fsi_exceptionrequestdate" />
<attribute name="fsi_requestor" />
<attribute name="fsi_agentname" />
<attribute name="fsi_governancezone" />
<attribute name="fsi_exceptiontype" />
<attribute name="fsi_businessjustification" />
<attribute name="fsi_riskassessment" />
<attribute name="fsi_compensatingcontrols" />
<attribute name="fsi_approvalstatus" />
<attribute name="fsi_approver1" />
<attribute name="fsi_approvaldate1" />
<attribute name="fsi_approver2" />
<attribute name="fsi_approvaldate2" />
<attribute name="fsi_approver3" />
<attribute name="fsi_approvaldate3" />
<attribute name="fsi_expirationdate" />
<attribute name="fsi_renewalcount" />
<attribute name="fsi_closuredate" />
<attribute name="fsi_closurereason" />
<attribute name="createdon" />
<attribute name="modifiedon" />
<order attribute="fsi_exceptionrequestdate" descending="true" />
</entity>
</fetch>
"@
$results = Get-CrmRecords -conn $conn -Fetch $fetchXml
Write-Host "Found $($results.CrmRecords.Count) exception records" -ForegroundColor White
$exportData = @()
foreach ($record in $results.CrmRecords) {
$exportData += [PSCustomObject]@{
ExceptionID = $record.fsi_governanceexceptionid
ExceptionName = $record.fsi_name
RequestDate = $record.fsi_exceptionrequestdate
Requestor = $record.fsi_requestor_Property.Value.Name
AgentName = $record.fsi_agentname
GovernanceZone = $record.fsi_governancezone_Property.Value.Name
ExceptionType = $record.fsi_exceptiontype_Property.Value.Name
BusinessJustification = $record.fsi_businessjustification
RiskAssessment = $record.fsi_riskassessment
CompensatingControls = $record.fsi_compensatingcontrols
ApprovalStatus = $record.fsi_approvalstatus_Property.Value.Name
Approver1 = if ($record.fsi_approver1_Property.Value) { $record.fsi_approver1_Property.Value.Name } else { "" }
ApprovalDate1 = $record.fsi_approvaldate1
Approver2 = if ($record.fsi_approver2_Property.Value) { $record.fsi_approver2_Property.Value.Name } else { "" }
ApprovalDate2 = $record.fsi_approvaldate2
Approver3 = if ($record.fsi_approver3_Property.Value) { $record.fsi_approver3_Property.Value.Name } else { "" }
ApprovalDate3 = $record.fsi_approvaldate3
ExpirationDate = $record.fsi_expirationdate
RenewalCount = $record.fsi_renewalcount
ClosureDate = $record.fsi_closuredate
ClosureReason = $record.fsi_closurereason
CreatedOn = $record.createdon
ModifiedOn = $record.modifiedon
}
}
$exportFile = Join-Path $evidenceDir "ExceptionRegister.csv"
$exportData | Export-Csv -Path $exportFile -NoTypeInformation -Encoding UTF8
Write-Host "✓ Exception register exported" -ForegroundColor Green
# Calculate SHA-256 hash
Write-Host "Calculating SHA-256 integrity hash..." -ForegroundColor Cyan
$hashAlgorithm = [System.Security.Cryptography.SHA256]::Create()
$fileStream = [System.IO.File]::OpenRead($exportFile)
$hashBytes = $hashAlgorithm.ComputeHash($fileStream)
$fileStream.Close()
$hash = [System.BitConverter]::ToString($hashBytes).Replace("-", "").ToLower()
Write-Host "✓ SHA-256 hash: $hash" -ForegroundColor Green
# Create chain of custody metadata
$metadata = @"
===========================================
EXCEPTION REGISTER AUDIT EVIDENCE
===========================================
Export Timestamp: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC")
Environment URL: $EnvironmentUrl
Examiner Name: $ExaminerName
Examination Purpose: $ExaminationPurpose
Exported By: $env:USERNAME
Export Host: $env:COMPUTERNAME
--- FILE INTEGRITY ---
File Name: ExceptionRegister.csv
File Size: $((Get-Item $exportFile).Length) bytes
SHA-256 Hash: $hash
--- DATA SUMMARY ---
Total Records: [COUNT]
Date Range: [FIRST] to [LAST]
===========================================
"@
$metadataFile = Join-Path $evidenceDir "EVIDENCE_METADATA.txt"
$metadata | Out-File -FilePath $metadataFile -Encoding UTF8
Write-Host "✓ Evidence metadata created" -ForegroundColor Green
# Create hash verification file
$hashFile = Join-Path $evidenceDir "SHA256_HASH.txt"
"$hash ExceptionRegister.csv" | Out-File -FilePath $hashFile -Encoding UTF8
Write-Host "✓ Hash verification file created" -ForegroundColor Green
# Emit canonical manifest.json (baseline §5) for chain-of-custody and downstream verifiers
$manifestPath = Join-Path $evidenceDir "manifest.json"
$scriptVersion = "1.3.3"
$manifestEntries = @()
foreach ($artifact in @($exportFile, $metadataFile, $hashFile)) {
$artifactHash = (Get-FileHash -Path $artifact -Algorithm SHA256).Hash.ToLower()
$manifestEntries += [PSCustomObject]@{
file = (Split-Path $artifact -Leaf)
sha256 = $artifactHash
bytes = (Get-Item $artifact).Length
generated_utc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
script_version = $scriptVersion
}
}
$manifestEntries | ConvertTo-Json -Depth 5 | Set-Content -Path $manifestPath -Encoding UTF8
Write-Host "✓ manifest.json written with SHA-256 entries for all artifacts" -ForegroundColor Green
Stop-Transcript | Out-Null
Write-Host ""
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Evidence Package Complete" -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Location: $evidenceDir" -ForegroundColor White
Write-Host ""
Write-Host "Files included:" -ForegroundColor Cyan
Write-Host " - ExceptionRegister.csv (exception data)" -ForegroundColor White
Write-Host " - EVIDENCE_METADATA.txt (chain of custody)" -ForegroundColor White
Write-Host " - SHA256_HASH.txt (integrity verification)" -ForegroundColor White
Write-Host " - manifest.json (canonical evidence manifest per FSI baseline §5)" -ForegroundColor White
Write-Host " - transcript-$timestamp.log (full PowerShell session transcript)" -ForegroundColor White
Write-Host ""
Write-Host "To verify file integrity later, run:" -ForegroundColor Cyan
Write-Host " certutil -hashfile ExceptionRegister.csv SHA256" -ForegroundColor Yellow
Write-Host " Compare output to SHA256_HASH.txt" -ForegroundColor Yellow
Write-Host ""
Write-Host "Script completed successfully." -ForegroundColor Cyan
Usage Examples
Daily Exception Monitoring
Run this command daily to export expiring exceptions:
.\Find-ExpiringExceptions.ps1 `
-EnvironmentUrl "https://contoso.crm.dynamics.com" `
-OutputPath "C:\Reports\Exceptions" `
-ExpirationWindowDays 7
Quarterly Compliance Audit
Generate quarterly compliance report:
.\Get-ExceptionComplianceReport.ps1 `
-EnvironmentUrl "https://contoso.crm.dynamics.com" `
-OutputPath "C:\Reports\Quarterly"
Regulatory Examination Evidence
Create audit-ready evidence package:
.\Export-ExceptionAuditEvidence.ps1 `
-EnvironmentUrl "https://contoso.crm.dynamics.com" `
-OutputPath "C:\Evidence" `
-ExaminerName "SEC Examiner" `
-ExaminationPurpose "Cybersecurity Rule Examination"
Automation with Task Scheduler
Schedule daily expiration monitoring:
$action = New-ScheduledTaskAction `
-Execute "PowerShell.exe" `
-Argument "-File C:\Scripts\Find-ExpiringExceptions.ps1 -EnvironmentUrl 'https://contoso.crm.dynamics.com' -OutputPath 'C:\Reports'"
$trigger = New-ScheduledTaskTrigger -Daily -At "8:00AM"
Register-ScheduledTask `
-TaskName "Exception Expiration Monitor" `
-Action $action `
-Trigger $trigger `
-Description "Daily monitoring of expiring agent governance exceptions"
Best Practices
- Credential Management: Use service accounts with minimal required permissions for scheduled scripts
- Error Handling: Implement try-catch blocks and logging for production deployments
- Output Retention: Retain exception reports for regulatory retention periods (typically 7 years for FSI)
- Automation: Schedule Script 2 (expiration monitoring) to run daily
- Review Cadence: Review compliance reports (Script 3) quarterly with governance committee
- Evidence Chain: Use Script 4 for all regulatory examinations to maintain integrity verification
Next Steps
- Proceed to Verification & Testing for test cases
- Review Troubleshooting for common issues
- Schedule automated monitoring with Task Scheduler or Azure Automation
Updated: April 2026 | Version: v1.4.0 | UI Verification Status: Current