Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/powershell/tests/Test-Assessment.25395.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
When organizations configure Microsoft Entra Private Access with broad application segments—such as wide IP ranges, multiple protocols, or Quick Access configurations—they effectively replicate the over-permissive access model of traditional VPNs. This approach contradicts the Zero Trust principle of least-privilege access, where users should only reach the specific resources required for their role. Threat actors who compromise a user's credentials or device can leverage these broad network permissions to perform reconnaissance, identifying additional systems and services within the permitted range.

With visibility into the network topology, they can escalate privileges by targeting vulnerable systems, move laterally to access sensitive data stores or administrative interfaces, and establish persistence by deploying backdoors across multiple accessible systems. The lack of granular segmentation also complicates incident response, as security teams cannot quickly determine which specific resources a compromised identity could access. By contrast, per-application segmentation with tightly scoped destination hosts, specific ports, and Custom Security Attributes enables dynamic, attribute-driven Conditional Access enforcement—requiring stronger authentication or device compliance for high-risk applications while streamlining access to lower-risk resources.

This approach aligns with the Zero Trust "verify explicitly" principle by ensuring each access request is evaluated against the specific security requirements of the target application rather than applying uniform policies to broad network segments.

**Remediation action**
- [Transition from Quick Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-configure-per-app-access) to per-app Private Access by creating individual Global Secure Access enterprise applications with specific FQDNs, IP addresses, and ports for each private resource.
- [Use Application Discovery](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-application-discovery) to identify which resources users access through Quick Access, then create targeted Private Access apps for those resources.
- [Create Custom Security Attribute sets](https://learn.microsoft.com/en-us/entra/fundamentals/custom-security-attributes-add) and definitions to categorize Private Access applications by risk level, department, or compliance requirements.
- [Assign Custom Security Attributes](https://learn.microsoft.com/en-us/entra/identity/enterprise-apps/custom-security-attributes-apps) to Private Access application service principals to enable attribute-based access control.
- [Create Conditional Access policies using application filters](https://learn.microsoft.com/en-us/entra/identity/conditional-access/concept-filter-for-applications) to target Private Access apps based on their Custom Security Attributes, enforcing granular controls like MFA or device compliance.
- [Apply Conditional Access policies to Private Access](https://learn.microsoft.com/en-us/entra/global-secure-access/how-to-target-resource-private-access-apps) apps from within Global Secure Access for streamlined configuration.

Review
- [Zero Trust network segmentation guidance for software-defined perimeters](https://learn.microsoft.com/en-us/security/zero-trust/deploy/networks#1-network-segmentation-and-software-defined-perimeters).


<!--- Results --->
%TestResult%
364 changes: 364 additions & 0 deletions src/powershell/tests/Test-Assessment.25395.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
<#
.SYNOPSIS
Validates that Entra Private Access applications enforce least-privilege
using granular network segments and Custom Security Attributes (CSA).

.DESCRIPTION
This test evaluates Private Access applications to ensure segmentation
follows least-privilege principles and supports attribute-based
Conditional Access targeting.

.NOTES
Test ID: 25395
Category: Global Secure Access
Required APIs: applications (beta), servicePrincipals (beta), conditionalAccess/policies (beta)
#>

function Test-Assessment-25395 {

[ZtTest(
Category = 'Global Secure Access',
ImplementationCost = 'High',
MinimumLicense = 'Entra_Premium_Private_Access',
Pillar = 'Network',
RiskLevel = 'High',
SfiPillar = 'Protect networks',
TenantType = 'Workforce',
TestId = 25395,
Title = 'Private Access application segments enforce least-privilege access',
UserImpact = 'Medium'
)]
[CmdletBinding()]
param()

#region Helper Functions

function Test-IsBroadCidr {
<#
.SYNOPSIS
Checks if a CIDR range is overly permissive (/16 or broader).
.OUTPUTS
System.Boolean - True if CIDR is /16 or broader, false otherwise.
#>
param([string]$Cidr)
if ($Cidr -match '/(\d+)$') { return ([int]$matches[1] -le 16) }
return $false
}

function Test-IsBroadIpRange {
<#
.SYNOPSIS
Checks if an IP range spans more than 256 addresses.
.OUTPUTS
System.Boolean - True if range exceeds 256 addresses, false otherwise.
#>
param([string]$Range)
if ($Range -match '^([\d\.]+)-([\d\.]+)$') {
$start = [System.Net.IPAddress]::Parse($matches[1]).GetAddressBytes()
$end = [System.Net.IPAddress]::Parse($matches[2]).GetAddressBytes()
[array]::Reverse($start)
[array]::Reverse($end)
return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 256)
}
return $false
}

function Test-IsBroadPortRange {
<#
.SYNOPSIS
Checks if a port range is overly broad (>10 ports or fully open).
.OUTPUTS
System.Boolean - True if port range is considered too broad, false otherwise.
#>
param([string]$Port)
if ($Port -eq '1-65535') { return $true }
if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt 10)) { return $true }
return $false
}

function Test-IsAdRpcException {
<#
.SYNOPSIS
Checks if a port range is a valid Active Directory RPC ephemeral port exception.
.OUTPUTS
System.Boolean - True if port is a valid AD RPC exception, false otherwise.
#>
param([string]$AppName, [string]$Port)
if ($AppName -match 'Active Directory|Domain Controller|AD DS') {
if ($Port -in @('49152-65535','1025-5000')) { return $true }
}
return $false
}

function Test-IsAdWellKnownPort {
<#
.SYNOPSIS
Checks if a port is a well-known Active Directory port.
.OUTPUTS
System.Boolean - True if port is a valid AD well-known port, false otherwise.
#>
param([string]$Port)
$valid = @('53','88','135','389','445','464','636','3268','3269')
if ($Port -match '^(\d+)-(\d+)$') {
return ($matches[1] -eq $matches[2] -and $valid -contains $matches[1])
}
return ($valid -contains $Port)
}

#endregion Helper Functions

#region Data Collection

Write-PSFMessage '🟦 Start' -Tag Test -Level VeryVerbose
$activity = 'Evaluating Private Access application segmentation'
Write-ZtProgress -Activity $activity -Status 'Querying applications'

# Query Q1: List all Private Access enterprise applications
$apps = Invoke-ZtGraphRequest -RelativeUri "applications?`$filter=(tags/any(t:t eq 'PrivateAccessNonWebApplication') or tags/any(t:t eq 'NetworkAccessQuickAccessApplication'))&`$select=id,displayName,appId,tags" -ApiVersion beta

# Query Q2: Retrieve service principals with Custom Security Attributes
$servicePrincipals = Invoke-ZtGraphRequest -RelativeUri "servicePrincipals?`$filter=(tags/any(t:t eq 'PrivateAccessNonWebApplication') or tags/any(t:t eq 'NetworkAccessQuickAccessApplication'))&`$select=id,appId,displayName,customSecurityAttributes&`$count=true" -ApiVersion beta -ConsistencyLevel eventual

# Query Q3: Retrieve enabled Conditional Access policies
$caPolicies = $null
$filterPolicies = @()

if ($null -ne $apps -and $apps.Count -gt 0) {

Write-ZtProgress -Activity $activity -Status 'Checking Conditional Access policies'

$allCAPolicies = Get-ZtConditionalAccessPolicy
$caPolicies = $allCAPolicies | Where-Object { $_.state -eq 'enabled' }

if ($caPolicies) {
$filterPolicies = $caPolicies | Where-Object {
$_.conditions.applications.applicationFilter
}
}
}

#endregion Data Collection

#region Assessment Logic

# Initialize evaluation containers
$passed = $false
$customStatus = $null
$testResultMarkdown = ''
$broadAccessApps = @()
$appsWithoutCSA = @()
$segmentFindings = @()
$appResults = @()

# Step 1: Check if any per-app Private Access applications exist
if ($null -ne $apps -and $apps.Count -gt 0) {

Write-ZtProgress -Activity $activity -Status 'Evaluating application segments'

foreach ($app in $apps) {

# Query Q4: Retrieve application segments for the current app
$segments = Invoke-ZtGraphRequest -RelativeUri "applications/$($app.id)/onPremisesPublishing/segmentsConfiguration/microsoft.graph.ipSegmentConfiguration/applicationSegments" -ApiVersion beta

$hasBroadSegment = $false
$hasWildcardDns = $false
$hasBroadPorts = $false
$segmentSummary = @()

if (-not $segments -or $segments.Count -eq 0) {
$segmentSummary = @('No segments configured')
}

foreach ($segment in $segments) {

# Step 2: Evaluate segment destination granularity
$issues = @()

$segmentSummary += "$($segment.destinationType):$($segment.destinationHost):$($segment.ports -join ',')"

switch ($segment.destinationType) {
'dnsSuffix' {
$hasWildcardDns = $true
$issues += 'Wildcard DNS'
}
'ipRangeCidr' {
if (Test-IsBroadCidr $segment.destinationHost) {
$hasBroadSegment = $true
$issues += 'Broad CIDR'
}
}
'ipRange' {
if (Test-IsBroadIpRange $segment.destinationHost) {
$hasBroadSegment = $true
$issues += 'Broad IP range'
}
}
}

# Step 3: Evaluate port breadth with AD RPC exceptions
foreach ($port in $segment.ports) {
if (Test-IsBroadPortRange $port) {
if (-not (Test-IsAdRpcException -AppName $app.displayName -Port $port) `
-and -not (Test-IsAdWellKnownPort $port)) {
$hasBroadPorts = $true
$issues += 'Broad port range'
}
}
}

# Step 4: Flag dual-protocol usage combined with broad scope
if ($segment.protocol -eq 'tcp,udp' -and $issues.Count -gt 0) {
$hasBroadPorts = $true
$issues += 'Dual protocol with broad scope'
}

if ($issues.Count -gt 0) {
$segmentFindings += [PSCustomObject]@{
AppName = $app.displayName
SegmentId = $segment.id
Issue = ($issues -join ', ')
Destination = $segment.destinationHost
Ports = ($segment.ports -join ', ')
}
}
}

# Step 5: Identify apps with overly broad access
if ($hasBroadSegment -or $hasWildcardDns -or $hasBroadPorts) {
$broadAccessApps += $app
}

# Step 6: Check CSA presence for the app
$sp = $servicePrincipals | Where-Object { $_.appId -eq $app.appId }
if (-not $sp.customSecurityAttributes) {
$appsWithoutCSA += $app
}

# Determine per-app status including Manual Review when filterPolicies exist
$appStatus = if (-not $sp.customSecurityAttributes) {
'Fail – Missing CSA'
} elseif ($hasBroadSegment -or $hasWildcardDns -or $hasBroadPorts) {
'Fail – Broad segment'
} elseif ($filterPolicies.Count -gt 0) {
'Manual Review'
} else {
'Pass'
}

$appResults += [PSCustomObject]@{
AppName = $app.displayName
AppObjectId = $app.id
AppId = $app.appId
SegmentType = if ($segments) { ($segments.destinationType | Select-Object -Unique) -join ', ' } else { 'None' }
SegmentScope = ($segmentSummary -join ' | ')
HasCSA = [bool]$sp.customSecurityAttributes
Status = $appStatus
}


}
}

# Step 7: Determine overall test result (Pass / Fail / Investigate)

if (-not $apps -or $apps.Count -eq 0) {

$customStatus = 'Investigate'
$testResultMarkdown =
"⚠️ No per-app Private Access applications configured. Please review the documentation on how to configure Private Access applications with granular network segments.`n`n%TestResult%"

}
elseif ($broadAccessApps.Count -eq 0 -and $appsWithoutCSA.Count -eq 0) {

if ($filterPolicies.Count -gt 0) {

# Pass conditions met but filterPolicies exist - requires manual review
$customStatus = 'Investigate'
$testResultMarkdown =
"⚠️ Private Access applications exist with appropriate segmentation and CSAs assigned. CA policies use applicationFilter targeting. Manual review required to verify CA policy coverage for these apps.`n`n%TestResult%"

}
else {

$passed = $true
$testResultMarkdown =
"✅ All Private Access applications are configured with granular network segments and are protected by Conditional Access policies using Custom Security Attributes, enforcing least-privilege access.`n`n%TestResult%"

}

}
else {

$passed = $false
$testResultMarkdown =
"❌ One or more Private Access applications have overly broad network segments or lack Custom Security Attribute-based Conditional Access policies, potentially allowing excessive network access.`n`n%TestResult%"

}

#endregion Assessment Logic

#region Report Generation

$mdInfo = "`n## Summary`n`n"
$mdInfo += "| Metric | Value |`n|---|---|`n"
$mdInfo += "| Total Private Access apps | $($apps.Count) |`n"
$mdInfo += "| Apps with broad segments | $($broadAccessApps.Count) |`n"
$mdInfo += "| Apps with CSA assigned | $($apps.Count - $appsWithoutCSA.Count) |`n"
$mdInfo += "| Apps without CSA | $($appsWithoutCSA.Count) |`n"
$mdInfo += "| CA policies using applicationFilter | $($filterPolicies.Count) |`n`n"

if ($appResults.Count -gt 0) {
$tableRows = ""
$formatTemplate = @'
## [Application details](https://entra.microsoft.com/#view/Microsoft_AAD_IAM/EnterpriseApplicationListBladeV3/fromNav/globalSecureAccess/applicationType/GlobalSecureAccessApplication)

| App name | Segment type | Segment scope | Has CSAs | Status |
|---|---|---|---|---|
{0}

'@
foreach ($r in $appResults) {
$appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($r.AppId)"
$linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $r.AppName), $appLink
$hasCSAText = if ($r.HasCSA) {'Yes'} else {'No'}
$tableRows += "| $linkedAppName | $($r.SegmentType) | $($r.SegmentScope) | $hasCSAText | $($r.Status) |`n"
}
$mdInfo += $formatTemplate -f $tableRows
}


if ($segmentFindings.Count -gt 0) {
$tableRows = ""
$formatTemplate = @'
## Segment Findings

| App name | Segment id | Issue | Destination | Ports | Recommendation |
|---|---|---|---|---|---|
{0}

'@
foreach ($f in $segmentFindings) {
$tableRows += "| $(Get-SafeMarkdown $f.AppName) | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n"
}
$mdInfo += $formatTemplate -f $tableRows
}

# Replace the placeholder with detailed information
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
#endregion Report Generation

$params = @{
TestId = '25395'
Title = 'Private Access application segments enforce least-privilege access'
Status = $passed
Result = $testResultMarkdown
}

# Add CustomStatus if status is 'Investigate'
if ($null -ne $customStatus) {
$params.CustomStatus = $customStatus
}

# Add test result details
Add-ZtTestResultDetail @params
}