Get-FlowConnectionOwnershipAudit
Get-AdminFlow only tells you who created or owns a flow record — it says nothing about the connections that flow actually depends on to run. This PowerShell script closes that gap for tenant-wide governance: it enumerates every flow and every connection across one or all environments, joins each flow to the connections its definition actually references, and flags any flow or connection owner that isn't a nominated service account. Where Get-FlowsByPersonConnection targets a single departing person for an offboarding check, this script audits the whole tenant at once against your automation account baseline.
Purpose
- Enumerates environments in scope — all of them, or a single one via
-EnvironmentName - Pulls every flow via
Get-AdminFlowand every connection viaGet-AdminPowerAppConnectionin scope - Extracts each flow's connection references (
Get-AdminFlow -ReturnConnectionReferences) and joins them back to the owning connection - Flags any flow owner or connection owner that doesn't match the nominated service account UPN
- Exports two CSVs: a full audit with one row per flow-connection pair, and a flagged-only subset for triage
Scope
- Matches ownership against whatever value
Get-AdminFlowandGet-AdminPowerAppConnectionreturn inCreatedBy.userIdfor your tenant — pass-ServiceAccountUpnin that same form, not necessarily a literal email address - Flows whose connection references fail to resolve (child flows, certain trigger types) are recorded with "N/A - no resolvable connection reference" rather than skipped, so the flow-ownership flag still applies to them
- Only flags owners where
CreatedBy.typeis "User" — connections or flows owned by service principals or other non-user identities aren't flagged, since the goal is catching individual staff ownership, not validating every owner type - This is a whole-tenant sweep against one service-account baseline, not a single-person check — for a departing employee's offboarding audit, use Get-FlowsByPersonConnection instead
Prerequisites
- Microsoft.PowerApps.Administration.PowerShell and Microsoft.PowerApps.PowerShell modules (auto-installed if missing)
- Power Platform Administrator or Global Administrator role
- The nominated service account's UPN, e.g.
svc-automation@tenantName.onmicrosoft.com
PowerShell Script
<#
.SYNOPSIS
Audits Power Automate flows across one or all environments in a tenant,
joining each flow to the connections it actually references, and flags
any flow/connection combination
.DESCRIPTION
Get-AdminFlow only tells you who created/owns a flow record. I
This script:
1. Enumerates environments (all, or a single one via -EnvironmentName)
2. Pulls every flow in scope via Get-AdminFlow
3. Pulls every connection in scope via Get-AdminPowerAppConnection
4. Extracts the connectionReferences from each flow's definition
(Get-AdminFlow -EnvironmentName ... -FlowName ... -ReturnConnectionReferences)
and matchs each reference back to its connection's owner
5. Flags any flow that references a connection NOT owned by the
service account you specify
Two CSVs are produced:
- FlowConnectionAudit_<timestamp>.csv (one row per flow-connection pair)
- FlowConnectionAudit_FlaggedOnly_<timestamp>.csv (just the risky rows)
.PARAMETER EnvironmentName
Optional. The GUID (EnvironmentName property from Get-AdminPowerAppEnvironment)
of a single environment to audit. Omit to audit every environment in the tenant.
.PARAMETER ServiceAccountUpn
The UPN (email) of your nominated account, e.g.
svc-automation@tenantName.onmicrosoft.com. Any connection or flow owner
that does NOT match this UPN is flagged.
.PARAMETER OutputFolder
Where to write the CSVs. Defaults to current directory.
.EXAMPLE
# Just the dev environment
.\Get-FlowConnectionOwnershipAudit.ps1 -EnvironmentName "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" -ServiceAccountUpn "svc-automation@tenantName.onmicrosoft.com"
.NOTES
Requires: Microsoft.PowerApps.Administration.PowerShell, Microsoft.PowerApps.PowerShell
Requires: Power Platform Administrator or Global Administrator role
Author: Cameron Griffiths
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$EnvironmentName,
[Parameter(Mandatory = $true)]
[string]$ServiceAccountUpn,
[Parameter(Mandatory = $false)]
[string]$OutputFolder = "."
)
# --- Modules ---
$requiredModules = @("Microsoft.PowerApps.Administration.PowerShell", "Microsoft.PowerApps.PowerShell")
foreach ($mod in $requiredModules) {
if (-not (Get-Module -ListAvailable -Name $mod)) {
Write-Host "Installing $mod ..." -ForegroundColor Cyan
Install-Module -Name $mod -Scope CurrentUser -Force -AllowClobber
}
}
# --- Auth ---
Write-Host "Signing in to Power Platform admin APIs ..." -ForegroundColor Cyan
Add-PowerAppsAccount
# --- Resolve environments in scope ---
if ($EnvironmentName) {
$envs = Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentName
if (-not $envs) {
Write-Error "Environment '$EnvironmentName' not found or not accessible."
return
}
}
else {
Write-Host "Enumerating all environments in the tenant ..." -ForegroundColor Cyan
$envs = Get-AdminPowerAppEnvironment
}
Write-Host "Auditing $($envs.Count) environment(s)." -ForegroundColor Cyan
$results = New-Object System.Collections.Generic.List[Object]
$envCounter = 0
foreach ($env in $envs) {
$envCounter++
Write-Host "`n[$envCounter/$($envs.Count)] Environment: $($env.DisplayName)" -ForegroundColor Yellow
# --- Connections in this environment, keyed by connection ID for fast lookup ---
$connLookup = @{}
try {
$conns = Get-AdminPowerAppConnection -EnvironmentName $env.EnvironmentName -ErrorAction Stop
foreach ($c in $conns) {
$connLookup[$c.ConnectionName] = $c
}
Write-Host " Found $($conns.Count) connection(s)." -ForegroundColor Gray
}
catch {
Write-Host " Could not enumerate connections: $($_.Exception.Message)" -ForegroundColor Red
continue
}
# --- Flows in this environment ---
try {
$flows = Get-AdminFlow -EnvironmentName $env.EnvironmentName -ErrorAction Stop
Write-Host " Found $($flows.Count) flow(s)." -ForegroundColor Gray
}
catch {
Write-Host " Could not enumerate flows: $($_.Exception.Message)" -ForegroundColor Red
continue
}
$flowCounter = 0
foreach ($flow in $flows) {
$flowCounter++
Write-Progress -Activity "Auditing flows in $($env.DisplayName)" `
-Status $flow.DisplayName `
-PercentComplete (($flowCounter / [Math]::Max($flows.Count,1)) * 100)
$flowOwnerFlagged = $flow.CreatedBy.userId -and
($flow.CreatedBy.userId -ne $ServiceAccountUpn) -and
($flow.CreatedBy.type -eq "User")
# --- Pull this flow's connection references ---
$connectionRefs = $null
try {
$connectionRefs = Get-AdminFlow -EnvironmentName $env.EnvironmentName `
-FlowName $flow.FlowName `
-ReturnConnectionReferences -ErrorAction Stop
}
catch {
# Some flows (child flows, certain trigger types) can fail here; log and continue
}
$refs = $connectionRefs.Internal.properties.connectionReferences
if (-not $refs -or ($refs.PSObject.Properties.Count -eq 0)) {
# Flow has no resolvable connection references (e.g. HTTP-only, or API failed)
$results.Add([PSCustomObject]@{
EnvironmentName = $env.DisplayName
FlowName = $flow.DisplayName
FlowId = $flow.FlowName
FlowOwner = $flow.CreatedBy.userId
FlowOwnerFlagged = $flowOwnerFlagged
ConnectorName = "N/A - no resolvable connection reference"
ConnectionId = ""
ConnectionOwner = ""
ConnectionFlagged = $false
FlowEnabled = $flow.Enabled
})
continue
}
foreach ($refProp in $refs.PSObject.Properties) {
$ref = $refProp.Value
$connId = $ref.connectionName
$connectorName = $ref.id -replace ".*/providers/Microsoft.PowerApps/apis/", ""
$matchedConn = $connLookup[$connId]
$connOwner = if ($matchedConn) { $matchedConn.CreatedBy.userId } else { "UNKNOWN - not found in environment connection list" }
$connOwnerType = if ($matchedConn) { $matchedConn.CreatedBy.type } else { "" }
$connFlagged = $matchedConn -and
($connOwner -ne $ServiceAccountUpn) -and
($connOwnerType -eq "User")
$results.Add([PSCustomObject]@{
EnvironmentName = $env.DisplayName
FlowName = $flow.DisplayName
FlowId = $flow.FlowName
FlowOwner = $flow.CreatedBy.userId
FlowOwnerFlagged = $flowOwnerFlagged
ConnectorName = $connectorName
ConnectionId = $connId
ConnectionOwner = $connOwner
ConnectionFlagged = $connFlagged
FlowEnabled = $flow.Enabled
})
}
}
Write-Progress -Activity "Auditing flows in $($env.DisplayName)" -Completed
}
# --- Output ---
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$fullPath = Join-Path $OutputFolder "FlowConnectionAudit_$timestamp.csv"
$flaggedPath = Join-Path $OutputFolder "FlowConnectionAudit_FlaggedOnly_$timestamp.csv"
$results | Sort-Object EnvironmentName, FlowName | Export-Csv -Path $fullPath -NoTypeInformation -Encoding UTF8
$flagged = $results | Where-Object { $_.FlowOwnerFlagged -or $_.ConnectionFlagged }
$flagged | Sort-Object EnvironmentName, FlowName | Export-Csv -Path $flaggedPath -NoTypeInformation -Encoding UTF8
Write-Host "`n============================================" -ForegroundColor Green
Write-Host "Audit complete." -ForegroundColor Green
Write-Host "Full report: $fullPath" -ForegroundColor Green
Write-Host "Flagged only: $flaggedPath ($($flagged.Count) row(s))" -ForegroundColor Green
Write-Host "============================================`n" -ForegroundColor Green
if ($flagged.Count -gt 0) {
Write-Host "Flows/connections NOT owned by '$ServiceAccountUpn':" -ForegroundColor Yellow
$flagged | Select-Object EnvironmentName, FlowName, ConnectorName, ConnectionOwner, FlowOwner |
Format-Table -AutoSize
}
else {
Write-Host "No flagged flows or connections found against '$ServiceAccountUpn'." -ForegroundColor Green
}
Usage Notes
-EnvironmentNameis optional and scopes the audit to a single environment's GUID; omit it to audit every environment in the tenant-ServiceAccountUpnis required — any flow or connection owner that doesn't match this value is flagged-OutputFolderdefaults to the current directory; two CSVs are written:FlowConnectionAudit_<timestamp>.csv(full) andFlowConnectionAudit_FlaggedOnly_<timestamp>.csv(flagged rows only)- Console output includes a formatted table of flagged flows and connections after both CSVs are written
- Auditing every environment in a large tenant is slow —
Get-AdminFlow -ReturnConnectionReferencesis called once per flow, so tenants with thousands of flows will take a while
Related
- Get-FlowsByPersonConnection — reuses the same flow-to-connection join logic, but scoped to a single departing person for offboarding instead of a tenant-wide service-account baseline