Control 3.7: PPAC Security Posture Assessment — PowerShell Setup
Read the FSI PowerShell baseline first
Before running anything 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 assume those patterns and reference them by section number.
Automation scripts for Control 3.7. All cmdlets in this playbook are read-only collectors — they do not change tenant state. Even so, scripts emit SHA-256-hashed evidence so the posture review itself is audit-defensible under SEC 17a-4(f) and FINRA 4511.
Prerequisites
- Windows PowerShell 5.1 (Desktop edition) — required for
Microsoft.PowerApps.Administration.PowerShell(baseline §2). PowerShell 7 will silently fail on Power Apps admin cmdlets and produce false-clean evidence. - Power Platform Admin role for the executing identity (or service principal with the equivalent application user in each Dataverse environment).
- Microsoft Graph scopes for the optional Defender cross-reference:
SecurityEvents.Read.All,Policy.Read.All. - Tenant-level analytics enabled (the security score and recommendations are populated only after enablement; allow up to 24 hours).
# REQUIRED EDITION GUARD — see baseline §2
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Microsoft.PowerApps.Administration.PowerShell requires Windows PowerShell 5.1 (Desktop). Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)."
}
# REQUIRED PINNED INSTALL — see baseline §1
# Replace <version> with the version approved by your CAB.
Install-Module -Name Microsoft.PowerApps.Administration.PowerShell `
-RequiredVersion '<version>' `
-Repository PSGallery `
-Scope CurrentUser `
-AllowClobber `
-AcceptLicense
Install-Module -Name Microsoft.Graph `
-RequiredVersion '<version>' `
-Repository PSGallery `
-Scope CurrentUser `
-AllowClobber `
-AcceptLicense
Sovereign-aware authentication (baseline §3)
param(
[ValidateSet('prod','usgov','usgovhigh','dod')]
[string]$Endpoint = 'prod'
)
$graphEnv = @{ prod='Global'; usgov='USGov'; usgovhigh='USGovDoD'; dod='USGovDoD' }[$Endpoint]
Add-PowerAppsAccount -Endpoint $Endpoint
Connect-MgGraph -Environment $graphEnv -Scopes 'SecurityEvents.Read.All','Policy.Read.All'
Failure to pass
-Endpointin a sovereign tenant authenticates against commercial endpoints, returns zero environments, and produces false-clean assessment results.
Evidence emission helper (baseline §5)
The posture collector uses the canonical Write-FsiEvidence helper from powershell-baseline.md §5. Copy the function from the baseline into your script library or dot-source it.
Collector 1 — Per-environment security configuration
function Get-Control37EnvironmentPosture {
[CmdletBinding()]
param(
[string]$EnvironmentName
)
$envs = if ($EnvironmentName) {
@(Get-AdminPowerAppEnvironment -EnvironmentName $EnvironmentName)
} else {
Get-AdminPowerAppEnvironment
}
$results = foreach ($env in $envs) {
$detail = Get-AdminPowerAppEnvironment -EnvironmentName $env.EnvironmentName
$props = $detail.Internal.properties
[PSCustomObject]@{
EnvironmentId = $env.EnvironmentName
DisplayName = $env.DisplayName
EnvironmentType = $env.EnvironmentType
HasDataverse = $detail.CommonDataServiceDatabaseProvisioningState -eq 'Succeeded'
IsManagedEnvironment = [bool]$props.governanceConfiguration.enableManagedEnvironment
SecurityGroupAssigned = [bool]$props.linkedEnvironmentMetadata.securityGroupId
CreatedTimeUtc = $env.CreatedTime
CollectedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
}
return $results
}
The cmdlet does not return Privacy + Security settings (blocked attachments, inactivity timeout, CSP, etc.) — those live in Dataverse
organizationtable and require Dataverse Web API orGet-CrmOrganizationaccess. Capture them via the per-environment manual review in portal walkthrough Step 7, or extend this collector with Dataverse Web API calls if you have an authorized service principal.
Collector 2 — DLP policy coverage
function Get-Control37DlpCoverage {
[CmdletBinding()] param()
$envs = Get-AdminPowerAppEnvironment
$policies = Get-DlpPolicy
foreach ($env in $envs) {
$tenantWide = $policies | Where-Object {
$_.environmentType -in @('AllEnvironments','OnlyEnvironments') -or
($_.environments -and $_.environments.name -contains $env.EnvironmentName)
}
[PSCustomObject]@{
EnvironmentId = $env.EnvironmentName
DisplayName = $env.DisplayName
PolicyCount = ($tenantWide | Measure-Object).Count
PolicyNames = ($tenantWide.DisplayName -join '; ')
HasCoverage = ($tenantWide | Measure-Object).Count -gt 0
CollectedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
}
}
Get-DlpPolicyshape varies between module versions; the snippet handles both the legacyenvironmentsarray and the newerenvironmentTypediscriminator. Verify against your pinned module version (baseline §1).
Collector 3 — Tenant settings snapshot
function Get-Control37TenantSettings {
[CmdletBinding()] param()
$tenant = Get-TenantSettings
[PSCustomObject]@{
TenantIsolationEnabled = $tenant.powerPlatform.tenantIsolation.isDisabled -eq $false
DisableDeveloperEnvironmentCreationByNonAdminUsers = $tenant.disableDeveloperEnvironmentCreationByNonAdminUsers
DisableEnvironmentCreationByNonAdminUsers = $tenant.disableEnvironmentCreationByNonAdminUsers
WalkMeOptOut = $tenant.walkMeOptOut
CollectedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
}
Orchestrator — produce posture report with SHA-256 evidence
<#
.SYNOPSIS
Control 3.7 posture collector and report generator (read-only).
.DESCRIPTION
Collects per-environment security posture, DLP coverage, and tenant settings.
Emits JSON evidence with SHA-256 hashes and a manifest, plus a human-readable HTML report.
DOES NOT mutate tenant state.
.PARAMETER Endpoint
Sovereign cloud endpoint: prod | usgov | usgovhigh | dod (baseline §3).
.PARAMETER EvidencePath
Output directory for evidence and report. Defaults to .\evidence-3.7.
.EXAMPLE
.\New-Control37PostureReport.ps1 -Endpoint prod -EvidencePath .\evidence-3.7
#>
[CmdletBinding()]
param(
[ValidateSet('prod','usgov','usgovhigh','dod')]
[string]$Endpoint = 'prod',
[string]$EvidencePath = ".\evidence-3.7"
)
$ErrorActionPreference = 'Stop'
# Edition guard (baseline §2)
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Run under Windows PowerShell 5.1 (Desktop) — see baseline §2."
}
New-Item -ItemType Directory -Force -Path $EvidencePath | Out-Null
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
Start-Transcript -Path "$EvidencePath\transcript-$ts.log" -IncludeInvocationHeader
try {
# Sovereign-aware sign-in (baseline §3)
$graphEnv = @{ prod='Global'; usgov='USGov'; usgovhigh='USGovDoD'; dod='USGovDoD' }[$Endpoint]
Add-PowerAppsAccount -Endpoint $Endpoint | Out-Null
Connect-MgGraph -Environment $graphEnv -Scopes 'SecurityEvents.Read.All','Policy.Read.All' -NoWelcome | Out-Null
Write-Host "Collecting environment posture..." -ForegroundColor Cyan
$envPosture = Get-Control37EnvironmentPosture
Write-Host "Collecting DLP coverage..." -ForegroundColor Cyan
$dlpCoverage = Get-Control37DlpCoverage
Write-Host "Collecting tenant settings..." -ForegroundColor Cyan
$tenantSettings = Get-Control37TenantSettings
# Compose summary (FSI-defensible: report inputs, not just headline numbers)
$managedCount = ($envPosture | Where-Object IsManagedEnvironment).Count
$sgCount = ($envPosture | Where-Object SecurityGroupAssigned).Count
$dlpCovered = ($dlpCoverage | Where-Object HasCoverage).Count
$summary = [PSCustomObject]@{
ControlId = '3.7'
FrameworkVersion = 'v1.3.3'
Endpoint = $Endpoint
GeneratedUtc = (Get-Date).ToUniversalTime().ToString('o')
EnvironmentCount = $envPosture.Count
ManagedEnvironmentCount = $managedCount
SecurityGroupCoverageCount = $sgCount
DlpCoverageCount = $dlpCovered
TenantIsolationEnabled = $tenantSettings.TenantIsolationEnabled
Notes = 'Security score (Preview) NOT collected via PowerShell — capture from PPAC Security > Overview manually and archive screenshot.'
}
# Emit SHA-256-hashed evidence (baseline §5)
Write-FsiEvidence -Object $envPosture -Name 'environment-posture' -EvidencePath $EvidencePath | Out-Null
Write-FsiEvidence -Object $dlpCoverage -Name 'dlp-coverage' -EvidencePath $EvidencePath | Out-Null
Write-FsiEvidence -Object $tenantSettings -Name 'tenant-settings' -EvidencePath $EvidencePath | Out-Null
Write-FsiEvidence -Object $summary -Name 'control-3.7-summary' -EvidencePath $EvidencePath | Out-Null
Write-Host "[PASS] Control 3.7 posture collected. Evidence at $EvidencePath" -ForegroundColor Green
}
catch {
Write-Host "[FAIL] $($_.Exception.Message)" -ForegroundColor Red
Write-Host $_.ScriptStackTrace -ForegroundColor Yellow
exit 1
}
finally {
Stop-Transcript | Out-Null
}
What this script does NOT cover
The collector is intentionally bounded. The following items must be evidenced via other means:
| Item | How to evidence |
|---|---|
| Security score (Preview) | Manual screenshot of PPAC > Security > Overview — not exposed via supported PowerShell or Graph API |
| Recommendations list, severity, refresh date | Manual review of PPAC > Actions page or use the Power Platform for Admin v2 connector in a Power Automate flow |
| Dismissed / snoozed recommendations | Manual export from PPAC > Actions > Dismissed recommendations and Snoozed recommendations |
| Per-environment Privacy + Security (blocked attachments, MIME, timeout, CSP) | Dataverse Web API against organization entity, or manual review per environment |
| Defender for Cloud Apps AI agent inventory | Microsoft Defender portal; Graph Security API where available |
Document each manual evidence item in the same evidence package and add its file to the SHA-256 manifest using
Write-FsiEvidence.
Scheduling
For Zone 3 environments run weekly via a service principal in Azure Automation or a hardened build agent. Pin module versions (baseline §1), enable verbose logging, and write evidence to a WORM-protected store (Purview Data Lifecycle retention lock or Azure Storage immutability policy) per SEC 17a-4(f).
Back to Control 3.7 | Portal Walkthrough | Verification & Testing | Troubleshooting
Updated: April 2026 | Version: v1.4.0 | UI Verification Status: Current