PowerShell Setup: Control 3.11 - Centralized Agent Inventory Enforcement
Last Updated: February 2026 PowerShell Version: 7.0 or higher recommended Required Modules: - Microsoft.PowerApps.Administration.PowerShell - Microsoft.Graph (for Entra ID user validation) - ExchangeOnlineManagement (for email notifications)
Prerequisites
Before running these scripts, ensure:
- PowerShell 7.0 or higher installed
- Required PowerShell modules installed (see Installation section below)
- Power Platform Admin role assigned to your account
- Microsoft Graph API permissions:
User.Read.All,Group.Read.All - Access to Power Platform Admin Center and Entra ID
- Change management system or ticketing system for automated ticket creation (optional)
- Teams webhook URL for notifications (optional but recommended)
Module Installation
Run the following commands to install required PowerShell modules:
# Install Power Apps Administration module
Install-Module -Name Microsoft.PowerApps.Administration.PowerShell -Scope CurrentUser -Force
# Install Microsoft Graph module for Entra ID queries
Install-Module -Name Microsoft.Graph -Scope CurrentUser -Force
# Install Exchange Online Management module for email notifications
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force
# Verify installations
Get-Module -ListAvailable Microsoft.PowerApps.Administration.PowerShell
Get-Module -ListAvailable Microsoft.Graph
Get-Module -ListAvailable ExchangeOnlineManagement
Script 1: Get-AgentInventoryReport.ps1
Exports comprehensive agent inventory from Power Platform with metadata completeness analysis.
<#
.SYNOPSIS
Exports complete agent inventory from Power Platform environments with metadata completeness analysis.
.DESCRIPTION
This script queries all Power Platform environments, retrieves Copilot Studio agents, and generates
a comprehensive inventory report with completeness metrics. Supports zone-based analysis and
identifies agents with missing or incomplete metadata.
.PARAMETER OutputPath
Directory where inventory report CSV will be saved. Default: Current directory.
.PARAMETER ZoneMappingFile
Path to CSV file containing environment-to-zone mappings (columns: EnvironmentId, ZoneName).
Optional. If not provided, zone classification will be marked as "Unknown".
.PARAMETER IncludeDecommissioned
Switch to include decommissioned agents in the report. Default: False (exclude decommissioned).
.EXAMPLE
.\Get-AgentInventoryReport.ps1 -OutputPath "C:\Reports" -ZoneMappingFile "C:\Config\zone-mappings.csv"
.NOTES
Author: FSI-AgentGov Framework
Version: 1.0
Last Updated: February 2026
#>
param(
[Parameter(Mandatory=$false)]
[string]$OutputPath = ".",
[Parameter(Mandatory=$false)]
[string]$ZoneMappingFile = "",
[Parameter(Mandatory=$false)]
[switch]$IncludeDecommissioned = $false
)
# Import required modules
Import-Module Microsoft.PowerApps.Administration.PowerShell
Import-Module Microsoft.Graph.Users
# Connect to Power Platform
Write-Host "Connecting to Power Platform..." -ForegroundColor Cyan
Add-PowerAppsAccount
# Connect to Microsoft Graph for user validation
Write-Host "Connecting to Microsoft Graph..." -ForegroundColor Cyan
Connect-MgGraph -Scopes "User.Read.All"
# Load zone mappings if provided
$zoneMappings = @{}
if ($ZoneMappingFile -and (Test-Path $ZoneMappingFile)) {
Write-Host "Loading zone mappings from $ZoneMappingFile..." -ForegroundColor Cyan
$zoneMappings = Import-Csv $ZoneMappingFile | ForEach-Object { @{$_.EnvironmentId = $_.ZoneName} }
} else {
Write-Host "No zone mapping file provided. Zone classification will be marked as 'Unknown'." -ForegroundColor Yellow
}
# Get all environments
Write-Host "Retrieving all Power Platform environments..." -ForegroundColor Cyan
$environments = Get-AdminPowerAppEnvironment
$agentInventory = @()
$envCount = 0
foreach ($env in $environments) {
$envCount++
Write-Host "[$envCount/$($environments.Count)] Processing environment: $($env.DisplayName)" -ForegroundColor Green
# Get Copilot Studio agents (bots) in this environment
try {
# NOTE: Get-AdminPowerAppCopilotStudioAgent is unverified and may not be available
# in all environments. As a fallback, use the Power Platform Admin Center inventory
# or the Dataverse API: GET /api/data/v9.2/bots?$select=name,botid,statecode
$agents = Get-AdminPowerAppCopilotStudioAgent -EnvironmentName $env.EnvironmentName -ErrorAction SilentlyContinue
foreach ($agent in $agents) {
# Skip decommissioned agents unless explicitly included
if (-not $IncludeDecommissioned -and $agent.Internal.properties.statecode -eq 1) {
continue
}
# Extract metadata
$owner = $agent.Owner.email
$createdTime = $agent.CreatedTime
$lastModifiedTime = $agent.LastModifiedTime
$displayName = $agent.DisplayName
# Validate owner (check if user still exists in Entra ID)
$ownerStatus = "Unknown"
$ownerValid = $false
if ($owner) {
try {
$user = Get-MgUser -UserId $owner -ErrorAction SilentlyContinue
if ($user) {
$ownerValid = $true
$ownerStatus = "Active"
} else {
$ownerStatus = "Departed"
}
} catch {
$ownerStatus = "Invalid"
}
} else {
$ownerStatus = "Missing"
}
# Determine zone classification
$zone = if ($zoneMappings.ContainsKey($env.EnvironmentName)) {
$zoneMappings[$env.EnvironmentName]
} else {
"Unknown"
}
# Calculate metadata completeness
$metadataScore = 0
$maxScore = 7
if ($owner) { $metadataScore++ }
if ($displayName) { $metadataScore++ }
if ($env.DisplayName) { $metadataScore++ }
if ($createdTime) { $metadataScore++ }
if ($lastModifiedTime) { $metadataScore++ }
if ($zone -ne "Unknown") { $metadataScore++ }
if ($ownerValid) { $metadataScore++ }
$completenessPercent = [math]::Round(($metadataScore / $maxScore) * 100, 2)
# Identify missing fields
$missingFields = @()
if (-not $owner) { $missingFields += "Owner" }
if (-not $displayName) { $missingFields += "DisplayName" }
if ($zone -eq "Unknown") { $missingFields += "ZoneClassification" }
if (-not $ownerValid) { $missingFields += "ValidOwner" }
$missingFieldsString = $missingFields -join "; "
# Calculate staleness (days since last modification)
$daysSinceModified = if ($lastModifiedTime) {
[math]::Round((Get-Date).Subtract([datetime]$lastModifiedTime).TotalDays, 0)
} else {
"Unknown"
}
# Build inventory record
$inventoryRecord = [PSCustomObject]@{
AgentName = $displayName
AgentId = $agent.AgentName
Owner = $owner
OwnerStatus = $ownerStatus
Environment = $env.DisplayName
EnvironmentId = $env.EnvironmentName
ZoneClassification = $zone
CreatedDate = $createdTime
LastModifiedDate = $lastModifiedTime
DaysSinceModified = $daysSinceModified
State = if ($agent.Internal.properties.statecode -eq 1) { "Decommissioned" } else { "Active" }
MetadataCompletenessPercent = $completenessPercent
MissingFields = $missingFieldsString
}
$agentInventory += $inventoryRecord
}
} catch {
Write-Host " Error retrieving agents from environment $($env.DisplayName): $_" -ForegroundColor Red
}
}
# Generate summary statistics
Write-Host "`n--- Agent Inventory Summary ---" -ForegroundColor Cyan
Write-Host "Total Agents: $($agentInventory.Count)" -ForegroundColor White
Write-Host "Active Agents: $(($agentInventory | Where-Object { $_.State -eq 'Active' }).Count)" -ForegroundColor White
Write-Host "Decommissioned Agents: $(($agentInventory | Where-Object { $_.State -eq 'Decommissioned' }).Count)" -ForegroundColor White
Write-Host "Agents with Valid Owner: $(($agentInventory | Where-Object { $_.OwnerStatus -eq 'Active' }).Count)" -ForegroundColor Green
Write-Host "Agents with Invalid/Missing Owner: $(($agentInventory | Where-Object { $_.OwnerStatus -ne 'Active' }).Count)" -ForegroundColor Red
Write-Host "Agents with Unknown Zone: $(($agentInventory | Where-Object { $_.ZoneClassification -eq 'Unknown' }).Count)" -ForegroundColor Yellow
Write-Host "Average Metadata Completeness: $([math]::Round(($agentInventory | Measure-Object -Property MetadataCompletenessPercent -Average).Average, 2))%" -ForegroundColor White
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportPath = Join-Path $OutputPath "AgentInventoryReport_$timestamp.csv"
$agentInventory | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-Host "`n✓ Inventory report saved to: $reportPath" -ForegroundColor Green
# Disconnect
Disconnect-MgGraph | Out-Null
Write-Host "`nScript completed successfully." -ForegroundColor Cyan
Script 2: Detect-OrphanedAgents.ps1
Identifies agents with departed owners, stale agents, and unmanaged agents requiring remediation.
<#
.SYNOPSIS
Detects orphaned agents requiring ownership transfer or decommissioning.
.DESCRIPTION
This script identifies agents with departed owners (no longer in Entra ID), stale agents
(not modified in >12 months), and unmanaged agents (missing critical metadata). Generates
a prioritized remediation report and optionally sends Teams notifications.
.PARAMETER InventoryReportPath
Path to agent inventory CSV file generated by Get-AgentInventoryReport.ps1.
If not provided, script will generate a fresh inventory.
.PARAMETER StalenessThresholdDays
Number of days since last modification to consider an agent "stale". Default: 365 days.
.PARAMETER OutputPath
Directory where orphaned agent report CSV will be saved. Default: Current directory.
.PARAMETER TeamsWebhookUrl
Optional Teams webhook URL for sending notifications. If provided, script will post
orphaned agent alert to Teams channel.
.EXAMPLE
.\Detect-OrphanedAgents.ps1 -InventoryReportPath "C:\Reports\AgentInventory.csv" -StalenessThresholdDays 365 -TeamsWebhookUrl "https://outlook.office.com/webhook/..."
.NOTES
Author: FSI-AgentGov Framework
Version: 1.0
Last Updated: February 2026
#>
param(
[Parameter(Mandatory=$false)]
[string]$InventoryReportPath = "",
[Parameter(Mandatory=$false)]
[int]$StalenessThresholdDays = 365,
[Parameter(Mandatory=$false)]
[string]$OutputPath = ".",
[Parameter(Mandatory=$false)]
[string]$TeamsWebhookUrl = ""
)
# Load inventory report
if ($InventoryReportPath -and (Test-Path $InventoryReportPath)) {
Write-Host "Loading inventory report from $InventoryReportPath..." -ForegroundColor Cyan
$inventory = Import-Csv $InventoryReportPath
} else {
Write-Host "No inventory report provided. Generating fresh inventory..." -ForegroundColor Yellow
# Call Get-AgentInventoryReport.ps1 to generate fresh inventory
& "$PSScriptRoot\Get-AgentInventoryReport.ps1" -OutputPath $OutputPath
# Find the most recent inventory report
$latestReport = Get-ChildItem $OutputPath -Filter "AgentInventoryReport_*.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latestReport) {
$inventory = Import-Csv $latestReport.FullName
} else {
Write-Host "ERROR: Could not generate or find inventory report." -ForegroundColor Red
exit 1
}
}
Write-Host "Analyzing $($inventory.Count) agents for orphaned status..." -ForegroundColor Cyan
# Identify orphaned agents
$orphanedAgents = @()
foreach ($agent in $inventory) {
$orphanReason = @()
$priority = "Low"
# Check 1: Owner is departed or invalid
if ($agent.OwnerStatus -in @("Departed", "Invalid", "Missing")) {
$orphanReason += "Owner status: $($agent.OwnerStatus)"
$priority = "High"
}
# Check 2: Agent is stale (not modified in >$StalenessThresholdDays days)
if ($agent.DaysSinceModified -ne "Unknown" -and [int]$agent.DaysSinceModified -gt $StalenessThresholdDays) {
$orphanReason += "Stale (not modified in $($agent.DaysSinceModified) days)"
if ($priority -eq "Low") { $priority = "Medium" }
}
# Check 3: Missing critical metadata
if ($agent.ZoneClassification -eq "Unknown") {
$orphanReason += "Zone classification missing"
if ($priority -eq "Low") { $priority = "Medium" }
}
# If any orphan conditions met, add to report
if ($orphanReason.Count -gt 0) {
$orphanedAgents += [PSCustomObject]@{
AgentName = $agent.AgentName
Owner = $agent.Owner
OwnerStatus = $agent.OwnerStatus
Environment = $agent.Environment
ZoneClassification = $agent.ZoneClassification
DaysSinceModified = $agent.DaysSinceModified
OrphanReason = $orphanReason -join "; "
Priority = $priority
RecommendedAction = if ($agent.OwnerStatus -in @("Departed", "Invalid")) {
"Transfer ownership or decommission"
} elseif ([int]$agent.DaysSinceModified -gt $StalenessThresholdDays) {
"Verify usage and decommission if unused"
} else {
"Complete metadata"
}
}
}
}
# Sort by priority
$orphanedAgents = $orphanedAgents | Sort-Object @{Expression={
switch ($_.Priority) {
"High" { 1 }
"Medium" { 2 }
"Low" { 3 }
}
}}
# Generate summary
Write-Host "`n--- Orphaned Agent Detection Summary ---" -ForegroundColor Cyan
Write-Host "Total Orphaned Agents: $($orphanedAgents.Count)" -ForegroundColor White
Write-Host " High Priority: $(($orphanedAgents | Where-Object { $_.Priority -eq 'High' }).Count)" -ForegroundColor Red
Write-Host " Medium Priority: $(($orphanedAgents | Where-Object { $_.Priority -eq 'Medium' }).Count)" -ForegroundColor Yellow
Write-Host " Low Priority: $(($orphanedAgents | Where-Object { $_.Priority -eq 'Low' }).Count)" -ForegroundColor Green
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportPath = Join-Path $OutputPath "OrphanedAgentsReport_$timestamp.csv"
$orphanedAgents | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-Host "`n✓ Orphaned agents report saved to: $reportPath" -ForegroundColor Green
# Send Teams notification if webhook URL provided
if ($TeamsWebhookUrl -and $orphanedAgents.Count -gt 0) {
Write-Host "`nSending Teams notification..." -ForegroundColor Cyan
$highPriorityCount = ($orphanedAgents | Where-Object { $_.Priority -eq 'High' }).Count
$mediumPriorityCount = ($orphanedAgents | Where-Object { $_.Priority -eq 'Medium' }).Count
# Build adaptive card
$adaptiveCard = @{
type = "message"
attachments = @(
@{
contentType = "application/vnd.microsoft.card.adaptive"
content = @{
type = "AdaptiveCard"
body = @(
@{
type = "TextBlock"
size = "Large"
weight = "Bolder"
text = "⚠️ Orphaned Agent Detection Alert"
}
@{
type = "TextBlock"
text = "The following agents require remediation due to invalid ownership or inactivity:"
wrap = $true
}
@{
type = "FactSet"
facts = @(
@{
title = "Total Orphaned Agents:"
value = "$($orphanedAgents.Count)"
}
@{
title = "High Priority:"
value = "$highPriorityCount"
}
@{
title = "Medium Priority:"
value = "$mediumPriorityCount"
}
)
}
@{
type = "TextBlock"
text = "📊 View full report: $reportPath"
wrap = $true
}
@{
type = "TextBlock"
text = "Please review and remediate high-priority agents within 14 days (Zone 2/3)."
wrap = $true
}
)
"`$schema" = "http://adaptivecards.io/schemas/adaptive-card.json"
version = "1.2"
}
}
)
}
try {
$body = $adaptiveCard | ConvertTo-Json -Depth 20
Invoke-RestMethod -Uri $TeamsWebhookUrl -Method Post -Body $body -ContentType 'application/json'
Write-Host "✓ Teams notification sent successfully." -ForegroundColor Green
} catch {
Write-Host "✗ Failed to send Teams notification: $_" -ForegroundColor Red
}
}
Write-Host "`nScript completed successfully." -ForegroundColor Cyan
Script 3: Test-InventoryCompleteness.ps1
Validates agent inventory against mandatory metadata requirements and generates compliance report.
<#
.SYNOPSIS
Validates agent inventory completeness against mandatory metadata requirements.
.DESCRIPTION
This script validates that all agents meet mandatory metadata requirements defined by governance
policies. Generates a compliance report showing completeness rates by zone and identifies
agents requiring metadata updates.
.PARAMETER InventoryReportPath
Path to agent inventory CSV file generated by Get-AgentInventoryReport.ps1.
.PARAMETER OutputPath
Directory where compliance report CSV will be saved. Default: Current directory.
.PARAMETER MandatoryFieldsConfig
Path to JSON file defining mandatory fields by zone. If not provided, uses default requirements.
.EXAMPLE
.\Test-InventoryCompleteness.ps1 -InventoryReportPath "C:\Reports\AgentInventory.csv" -OutputPath "C:\Reports"
.NOTES
Author: FSI-AgentGov Framework
Version: 1.0
Last Updated: February 2026
#>
param(
[Parameter(Mandatory=$true)]
[string]$InventoryReportPath,
[Parameter(Mandatory=$false)]
[string]$OutputPath = ".",
[Parameter(Mandatory=$false)]
[string]$MandatoryFieldsConfig = ""
)
# Default mandatory fields by zone (if config file not provided)
$defaultMandatoryFields = @{
"Zone 1" = @("AgentName", "Owner", "Environment", "ZoneClassification")
"Zone 2" = @("AgentName", "Owner", "Environment", "ZoneClassification", "OwnerStatus")
"Zone 3" = @("AgentName", "Owner", "Environment", "ZoneClassification", "OwnerStatus")
"Unknown" = @("AgentName", "Owner", "Environment")
}
# Load mandatory fields config
$mandatoryFields = $defaultMandatoryFields
if ($MandatoryFieldsConfig -and (Test-Path $MandatoryFieldsConfig)) {
Write-Host "Loading mandatory fields config from $MandatoryFieldsConfig..." -ForegroundColor Cyan
$mandatoryFields = Get-Content $MandatoryFieldsConfig | ConvertFrom-Json -AsHashtable
}
# Load inventory report
Write-Host "Loading inventory report from $InventoryReportPath..." -ForegroundColor Cyan
$inventory = Import-Csv $InventoryReportPath
Write-Host "Validating $($inventory.Count) agents against mandatory metadata requirements..." -ForegroundColor Cyan
# Validate completeness
$complianceReport = @()
foreach ($agent in $inventory) {
$zone = $agent.ZoneClassification
$requiredFields = $mandatoryFields[$zone]
if (-not $requiredFields) {
Write-Host "Warning: No mandatory fields defined for zone '$zone'. Skipping agent $($agent.AgentName)." -ForegroundColor Yellow
continue
}
$missingFields = @()
$fieldsComplete = $true
foreach ($field in $requiredFields) {
$value = $agent.$field
if (-not $value -or $value -eq "Unknown" -or $value -eq "Missing") {
$missingFields += $field
$fieldsComplete = $false
}
}
$complianceStatus = if ($fieldsComplete) { "Compliant" } else { "Non-Compliant" }
$complianceReport += [PSCustomObject]@{
AgentName = $agent.AgentName
Environment = $agent.Environment
ZoneClassification = $zone
ComplianceStatus = $complianceStatus
MissingFields = if ($missingFields.Count -gt 0) { $missingFields -join "; " } else { "None" }
MetadataCompletenessPercent = $agent.MetadataCompletenessPercent
}
}
# Calculate summary metrics
$totalAgents = $complianceReport.Count
$compliantAgents = ($complianceReport | Where-Object { $_.ComplianceStatus -eq 'Compliant' }).Count
$nonCompliantAgents = $totalAgents - $compliantAgents
$complianceRate = if ($totalAgents -gt 0) { [math]::Round(($compliantAgents / $totalAgents) * 100, 2) } else { 0 }
Write-Host "`n--- Inventory Completeness Summary ---" -ForegroundColor Cyan
Write-Host "Total Agents: $totalAgents" -ForegroundColor White
Write-Host "Compliant: $compliantAgents" -ForegroundColor Green
Write-Host "Non-Compliant: $nonCompliantAgents" -ForegroundColor Red
Write-Host "Compliance Rate: $complianceRate%" -ForegroundColor White
# Zone-specific compliance
Write-Host "`n--- Compliance by Zone ---" -ForegroundColor Cyan
foreach ($zone in ($complianceReport | Select-Object -ExpandProperty ZoneClassification -Unique)) {
$zoneAgents = $complianceReport | Where-Object { $_.ZoneClassification -eq $zone }
$zoneCompliant = ($zoneAgents | Where-Object { $_.ComplianceStatus -eq 'Compliant' }).Count
$zoneRate = if ($zoneAgents.Count -gt 0) { [math]::Round(($zoneCompliant / $zoneAgents.Count) * 100, 2) } else { 0 }
Write-Host "$zone`: $zoneCompliant / $($zoneAgents.Count) ($zoneRate%)" -ForegroundColor White
}
# Export to CSV
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$reportPath = Join-Path $OutputPath "InventoryComplianceReport_$timestamp.csv"
$complianceReport | Export-Csv -Path $reportPath -NoTypeInformation -Encoding UTF8
Write-Host "`n✓ Compliance report saved to: $reportPath" -ForegroundColor Green
# Alert if compliance rate is below target
$targetComplianceRate = 95
if ($complianceRate -lt $targetComplianceRate) {
Write-Host "`n⚠ WARNING: Compliance rate ($complianceRate%) is below target ($targetComplianceRate%)!" -ForegroundColor Red
Write-Host "Review non-compliant agents and initiate remediation." -ForegroundColor Yellow
}
Write-Host "`nScript completed successfully." -ForegroundColor Cyan
Script 4: Invoke-InventoryEnforcementSuite.ps1
Master orchestration script that runs all inventory enforcement scripts in sequence.
<#
.SYNOPSIS
Master script to run complete agent inventory enforcement suite.
.DESCRIPTION
This orchestration script executes all inventory enforcement scripts in sequence:
1. Generate fresh agent inventory report
2. Detect orphaned agents
3. Validate inventory completeness
4. Send consolidated Teams notification with all findings
.PARAMETER OutputPath
Directory where all reports will be saved. Default: .\Reports
.PARAMETER ZoneMappingFile
Path to CSV file containing environment-to-zone mappings.
.PARAMETER TeamsWebhookUrl
Teams webhook URL for notifications.
.PARAMETER StalenessThresholdDays
Number of days since last modification to consider an agent "stale". Default: 365.
.EXAMPLE
.\Invoke-InventoryEnforcementSuite.ps1 -OutputPath "C:\Reports" -ZoneMappingFile "C:\Config\zone-mappings.csv" -TeamsWebhookUrl "https://outlook.office.com/webhook/..."
.NOTES
Author: FSI-AgentGov Framework
Version: 1.0
Last Updated: February 2026
#>
param(
[Parameter(Mandatory=$false)]
[string]$OutputPath = ".\Reports",
[Parameter(Mandatory=$false)]
[string]$ZoneMappingFile = "",
[Parameter(Mandatory=$false)]
[string]$TeamsWebhookUrl = "",
[Parameter(Mandatory=$false)]
[int]$StalenessThresholdDays = 365
)
# Ensure output directory exists
if (-not (Test-Path $OutputPath)) {
New-Item -Path $OutputPath -ItemType Directory -Force | Out-Null
}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Agent Inventory Enforcement Suite" -ForegroundColor Cyan
Write-Host "========================================`n" -ForegroundColor Cyan
# Step 1: Generate agent inventory report
Write-Host "[1/3] Generating agent inventory report..." -ForegroundColor Green
$inventoryParams = @{
OutputPath = $OutputPath
}
if ($ZoneMappingFile) {
$inventoryParams.Add("ZoneMappingFile", $ZoneMappingFile)
}
& "$PSScriptRoot\Get-AgentInventoryReport.ps1" @inventoryParams
# Find the most recent inventory report
$latestInventory = Get-ChildItem $OutputPath -Filter "AgentInventoryReport_*.csv" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if (-not $latestInventory) {
Write-Host "ERROR: Inventory report not generated. Aborting." -ForegroundColor Red
exit 1
}
Write-Host "`n✓ Inventory report generated: $($latestInventory.FullName)`n" -ForegroundColor Green
# Step 2: Detect orphaned agents
Write-Host "[2/3] Detecting orphaned agents..." -ForegroundColor Green
$orphanedParams = @{
InventoryReportPath = $latestInventory.FullName
StalenessThresholdDays = $StalenessThresholdDays
OutputPath = $OutputPath
}
if ($TeamsWebhookUrl) {
$orphanedParams.Add("TeamsWebhookUrl", $TeamsWebhookUrl)
}
& "$PSScriptRoot\Detect-OrphanedAgents.ps1" @orphanedParams
Write-Host "`n✓ Orphaned agent detection completed`n" -ForegroundColor Green
# Step 3: Validate inventory completeness
Write-Host "[3/3] Validating inventory completeness..." -ForegroundColor Green
& "$PSScriptRoot\Test-InventoryCompleteness.ps1" -InventoryReportPath $latestInventory.FullName -OutputPath $OutputPath
Write-Host "`n✓ Completeness validation completed`n" -ForegroundColor Green
# Summary
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Enforcement Suite Completed" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "All reports saved to: $OutputPath" -ForegroundColor White
Write-Host "`nNext Steps:" -ForegroundColor Yellow
Write-Host "1. Review orphaned agent report and initiate remediation" -ForegroundColor White
Write-Host "2. Review completeness report and contact agent authors for metadata updates" -ForegroundColor White
Write-Host "3. Update zone mappings if any agents have 'Unknown' classification" -ForegroundColor White
Write-Host "4. Schedule this suite to run daily for Zone 2/3 environments" -ForegroundColor White
Scheduled Execution with Windows Task Scheduler
To run the enforcement suite on a schedule:
# Create scheduled task (run as administrator)
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-File C:\Scripts\Invoke-InventoryEnforcementSuite.ps1 -OutputPath C:\Reports -ZoneMappingFile C:\Config\zone-mappings.csv -TeamsWebhookUrl 'https://outlook.office.com/webhook/...'"
# Run daily at 4:00 AM
$trigger = New-ScheduledTaskTrigger -Daily -At 4:00AM
# Run with highest privileges
$principal = New-ScheduledTaskPrincipal -UserId "DOMAIN\ServiceAccount" -LogonType Password -RunLevel Highest
# Register the task
Register-ScheduledTask -TaskName "Agent Inventory Enforcement Suite" -Action $action -Trigger $trigger -Principal $principal -Description "Daily agent inventory enforcement and orphaned agent detection"
Zone Mapping CSV Template
Create a CSV file with environment-to-zone mappings:
EnvironmentId,ZoneName
00000000-0000-0000-0000-000000000001,Zone 1
00000000-0000-0000-0000-000000000002,Zone 2
00000000-0000-0000-0000-000000000003,Zone 3
Get environment IDs by running:
Get-AdminPowerAppEnvironment | Select-Object EnvironmentName, DisplayName
Additional Resources
- Microsoft.PowerApps.Administration.PowerShell Documentation
- Microsoft Graph PowerShell SDK
- Teams Incoming Webhooks
Back to Control 3.11 | Portal Walkthrough | Verification Testing | Troubleshooting
Updated: February 2026 | Version: v1.0