Collaborate, Innovate, Automate

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

Scope

Prerequisites

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

Related