public/Get-MsecPowerPlatformEnvironment.ps1

function Get-MsecPowerPlatformEnvironment {
    <#
    .SYNOPSIS
        Power Platform environments and whether a connector DLP policy actually covers each
        one - the control that decides what a Power Automate flow is allowed to connect to.

    .DESCRIPTION
        A Power Automate flow runs as the person who built it, needs no approval, and can move
        data between any two connectors it is permitted to use. The only thing constraining
        that is a connector DLP policy, and a policy constrains an environment only if it is
        scoped to include it.

        AN ENVIRONMENT WITH NO DLP POLICY HAS NO CONNECTOR RESTRICTIONS AT ALL. Not weak ones -
        none. SharePoint to a personal Gmail is an ordinary afternoon's work for a maker in an
        uncovered environment, and nothing in Secure Score, DLP for Microsoft 365, or a
        Conditional Access review mentions it.

        RUNS AS THE SIGNED-IN USER, NOT AS THE msec APP. The Power Platform admin APIs return
        403 to the app certificate: app-only access requires the application to be registered
        as a Power Platform MANAGEMENT APPLICATION, which grants administrative - not read-only
        - access to the whole Power Platform estate. Taking that route would break the promise
        that the certificate in Key Vault cannot change the tenant, so this command follows the
        same pattern as Search-MsecAzureResourceGraph and runs on your Az context instead. One
        command, one identity.

        DLP SCOPE IS A FILTER TYPE, NOT A LIST. A policy carries environmentFilterType of
        'none' (every environment), 'include' (only the listed ones) or 'exclude' (all but the
        listed ones). Reading only the environment list would report a tenant-wide policy as
        covering nothing, which is the most dangerous possible way to be wrong here.

        UNREADABLE IS NOT UNCOVERED. If the policy list cannot be read, IsCoveredByDlp is
        $null on every row rather than $false - an environment nobody could check must not
        render as one that is definitely unprotected.

    .PARAMETER UncoveredOnly
        Only environments no DLP policy applies to.

    .EXAMPLE
        Get-MsecPowerPlatformEnvironment

        Every environment, with the policies covering it.

    .EXAMPLE
        Get-MsecPowerPlatformEnvironment -UncoveredOnly

        The environments where a maker may connect anything to anything.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [switch] $UncoveredOnly
    )

    if (-not (Get-AzContext -ErrorAction SilentlyContinue)) {
        throw 'No Azure context. Run Connect-AzAccount first - this command runs as you, not as the msec app.'
    }

    # Get-AzAccessToken returns SecureString on Az.Accounts 5.x+ and a plain string before
    # that; both are handled, same as Get-MsecAzureSecureScore.
    $resource = 'https://api.bap.microsoft.com'
    $tokenResp = Get-AzAccessToken -ResourceUrl $resource -ErrorAction Stop
    $token = if ($tokenResp.Token -is [System.Security.SecureString]) {
        [System.Net.NetworkCredential]::new('', $tokenResp.Token).Password
    }
    else { [string] $tokenResp.Token }
    $headers = @{ Authorization = "Bearer $token" }

    $environments = $null
    try {
        $environments = @((Invoke-RestMethod -Headers $headers -ErrorAction Stop -Uri `
            "$resource/providers/Microsoft.BusinessAppPlatform/scopes/admin/environments?api-version=2020-10-01").value)
    }
    catch {
        $status = if ($_.Exception.Response) { [int] $_.Exception.Response.StatusCode } else { $null }
        if ($status -eq 403) {
            throw 'Power Platform returned 403. This command runs as the signed-in user and needs the Power Platform Administrator, Dynamics 365 Administrator or Global Administrator role.'
        }
        throw "Could not list Power Platform environments: $($_.Exception.Message)"
    }

    $policies = $null
    try {
        $policies = @((Invoke-RestMethod -Headers $headers -ErrorAction Stop -Uri `
            "$resource/providers/PowerPlatform.Governance/v2/policies?api-version=2020-10-01").value)
    }
    catch {
        Write-Warning "Could not read connector DLP policies, so IsCoveredByDlp is null rather than false on every environment - this is NOT the same as finding none. Power Platform said: $($_.Exception.Message)"
    }

    foreach ($environment in $environments) {
        $p = $environment.properties
        $name = [string] $environment.name

        $covering = $null
        if ($null -ne $policies) {
            $covering = @(foreach ($policy in $policies) {
                $filter = [string] $policy.environments.environmentFilterType
                $listed = @($policy.environments.environments | ForEach-Object { [string] $_.name })
                $applies = switch ($filter) {
                    'none'    { $true }                        # every environment in the tenant
                    'include' { $name -in $listed }
                    'exclude' { $name -notin $listed }
                    # An unrecognised filter type is not assumed harmless.
                    default   { $null }
                }
                if ($applies) { [string] $policy.displayName }
            })
        }

        $isCovered = if ($null -eq $policies) { $null } else { [bool] @($covering).Count }
        if ($UncoveredOnly -and $isCovered -ne $false) { continue }

        [PSCustomObject]@{
            PSTypeName      = 'MsecPowerPlatformEnvironment'
            DisplayName     = [string] $p.displayName
            EnvironmentName = $name
            Type            = [string] $p.environmentSku
            IsDefault       = [bool] $p.isDefault
            Region          = [string] $environment.location
            # A Dataverse database is where Power Platform keeps structured business data, so
            # an uncovered environment holding one is a different size of problem to one without.
            HasDataverse    = [bool] $p.linkedEnvironmentMetadata
            IsCoveredByDlp  = $isCovered
            DlpPolicyCount  = if ($null -eq $covering) { $null } else { @($covering).Count }
            DlpPolicies     = if ($null -eq $covering) { $null } else { (@($covering) -join '; ') }
            CreatedBy       = [string] $p.createdBy.displayName
            CreatedTime     = $p.createdTime
        }
    }
}