From c94eab2e6d282887ea6507ba1b8942030d625887 Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Tue, 6 Jan 2026 12:47:19 +0530 Subject: [PATCH 01/16] Adding test for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.md | 23 ++ .../tests/Test-Assessment.25395.ps1 | 365 ++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 src/powershell/tests/Test-Assessment.25395.md create mode 100644 src/powershell/tests/Test-Assessment.25395.ps1 diff --git a/src/powershell/tests/Test-Assessment.25395.md b/src/powershell/tests/Test-Assessment.25395.md new file mode 100644 index 000000000..0749e8589 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25395.md @@ -0,0 +1,23 @@ +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). + + + +%TestResult% diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 new file mode 100644 index 000000000..141b44607 --- /dev/null +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -0,0 +1,365 @@ +<# +.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 + } + } + } + + + # Initialize evaluation containers + $passed = $false + $customStatus = $null + $testResultMarkdown = '' + $broadAccessApps = @() + $appsWithoutCSA = @() + $segmentFindings = @() + $appResults = @() + + #endregion Data Collection + + #region Assessment Logic + + # Step 1: Check if any per-app Private Access applications exist + if ($apps) { + + 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 + } + + $testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo + + $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 + + #endregion Report Generation +} From 56b763719f4a21e037c9d56801b913a0498ae80f Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Tue, 6 Jan 2026 14:27:02 +0530 Subject: [PATCH 02/16] Updated region markers and app conditional check Refactor assessment script by updating region markers and improving condition checks. --- src/powershell/tests/Test-Assessment.25395.ps1 | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 141b44607..53fc74db8 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -137,6 +137,9 @@ function Test-Assessment-25395 { } } + #endregion Data Collection + + #region Assessment Logic # Initialize evaluation containers $passed = $false @@ -146,13 +149,9 @@ function Test-Assessment-25395 { $appsWithoutCSA = @() $segmentFindings = @() $appResults = @() - - #endregion Data Collection - - #region Assessment Logic - + # Step 1: Check if any per-app Private Access applications exist - if ($apps) { + if ($null -ne $apps -and $apps.Count -gt 0) { Write-ZtProgress -Activity $activity -Status 'Evaluating application segments' @@ -344,8 +343,10 @@ function Test-Assessment-25395 { $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' @@ -360,6 +361,4 @@ function Test-Assessment-25395 { # Add test result details Add-ZtTestResultDetail @params - - #endregion Report Generation } From b1f861f4256a6301b47f901f0abad02687d23232 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Tue, 6 Jan 2026 14:27:58 +0530 Subject: [PATCH 03/16] removed extra lines removed extra lines --- src/powershell/tests/Test-Assessment.25395.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.md b/src/powershell/tests/Test-Assessment.25395.md index 0749e8589..95f150ef0 100644 --- a/src/powershell/tests/Test-Assessment.25395.md +++ b/src/powershell/tests/Test-Assessment.25395.md @@ -1,12 +1,9 @@ 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. From 0a648d944e0ecab056d7a447fbfbdb35bd7e999c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:02:12 +0000 Subject: [PATCH 04/16] Initial plan From 6a0065da4585253ffe84a520546048e53d4e3e99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:05:34 +0000 Subject: [PATCH 05/16] Extract AD ports to constant $AD_WELL_KNOWN_PORTS Co-authored-by: aahmed-spec <250927798+aahmed-spec@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 53fc74db8..6503b79bf 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -31,6 +31,9 @@ function Test-Assessment-25395 { [CmdletBinding()] param() + # Active Directory well-known ports + $AD_WELL_KNOWN_PORTS = @('53','88','135','389','445','464','636','3268','3269') + #region Helper Functions function Test-IsBroadCidr { @@ -98,11 +101,10 @@ function Test-Assessment-25395 { 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 ($matches[1] -eq $matches[2] -and $AD_WELL_KNOWN_PORTS -contains $matches[1]) } - return ($valid -contains $Port) + return ($AD_WELL_KNOWN_PORTS -contains $Port) } #endregion Helper Functions From 1d168edc3a5c1a4af7399c8928aafd8172bde123 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:12:13 +0530 Subject: [PATCH 06/16] Update src/powershell/tests/Test-Assessment.25395.ps1 Inconsistent indentation detected. Lines 249-256 use tabs while the rest of the file uses spaces. PowerShell style guidelines typically recommend using spaces consistently throughout the file for better readability across different editors. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 6503b79bf..046cc5cf8 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -248,14 +248,14 @@ function Test-Assessment-25395 { } $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 - } + 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 + } } From 003cabcec0c6a9c1bd7007ec9d15c58acdfeafb8 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:13:00 +0530 Subject: [PATCH 07/16] Update src/powershell/tests/Test-Assessment.25395.ps1 Inconsistent indentation detected. Line 346 uses tabs while the rest of the file uses spaces. PowerShell style guidelines typically recommend using spaces consistently throughout the file for better readability across different editors. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 046cc5cf8..5cfd3e29f 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -345,10 +345,10 @@ function Test-Assessment-25395 { $mdInfo += $formatTemplate -f $tableRows } - # Replace the placeholder with detailed information + # 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' From b0e888d6c9945ac9f4165e32f1a9783a72a63caa Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Wed, 7 Jan 2026 18:13:45 +0530 Subject: [PATCH 08/16] Update src/powershell/tests/Test-Assessment.25395.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 5cfd3e29f..8410f2363 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -151,7 +151,6 @@ function Test-Assessment-25395 { $appsWithoutCSA = @() $segmentFindings = @() $appResults = @() - # Step 1: Check if any per-app Private Access applications exist if ($null -ne $apps -and $apps.Count -gt 0) { From da4d1fd6044c0e57cd2ea36e65c1d8cb590beda7 Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:12:14 +0530 Subject: [PATCH 09/16] Added comments for Test-IsBroadCidr function Clarified output descriptions in Test-IsBroadCidr function. --- src/powershell/tests/Test-Assessment.25395.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 8410f2363..43c416fdc 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -40,8 +40,13 @@ function Test-Assessment-25395 { <# .SYNOPSIS Checks if a CIDR range is overly permissive (/16 or broader). + .DESCRIPTION + CIDR ranges with prefix length <= 16 are treated as overly permissive. + This includes /16 itself (65,536 IPs) and any broader ranges such as /15, /14, etc. .OUTPUTS - System.Boolean - True if CIDR is /16 or broader, false otherwise. + System.Boolean + True - CIDR prefix length <= 16 + False - CIDR prefix length > 16 or invalid format #> param([string]$Cidr) if ($Cidr -match '/(\d+)$') { return ([int]$matches[1] -le 16) } From cc80027d542f8b6b7f41498c75f97d7e8c878d8a Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:15:37 +0530 Subject: [PATCH 10/16] Added BroadPortRangeThreshold variable for src/powershell/tests/Test-Assessment.25395.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 43c416fdc..65cc072f9 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -79,8 +79,12 @@ function Test-Assessment-25395 { System.Boolean - True if port range is considered too broad, false otherwise. #> param([string]$Port) + + # Maximum number of ports allowed in a range before it is considered "broad". + $BroadPortRangeThreshold = 10 + if ($Port -eq '1-65535') { return $true } - if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt 10)) { return $true } + if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt $BroadPortRangeThreshold)) { return $true } return $false } From 6244d6ba2d5e41cef700ebc87f31222dd272937a Mon Sep 17 00:00:00 2001 From: aahmed-spec Date: Thu, 8 Jan 2026 10:55:44 +0530 Subject: [PATCH 11/16] Update line no: 69 src/powershell/tests/Test-Assessment.25395.ps1 updated 255 instead of 256 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 65cc072f9..8c3210a08 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -66,7 +66,7 @@ function Test-Assessment-25395 { $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 (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 255) } return $false } From 0349d5aac13449e669de4e1d88feff28d2264027 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 05:34:05 +0000 Subject: [PATCH 12/16] Initial plan From 6b15741379a71e3acc7d921a1824c251426a2066 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 8 Jan 2026 05:36:37 +0000 Subject: [PATCH 13/16] Fix off-by-one errors in IP and port range calculations Co-authored-by: aahmed-spec <250927798+aahmed-spec@users.noreply.github.com> --- src/powershell/tests/Test-Assessment.25395.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 8c3210a08..cd5575438 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -66,7 +66,7 @@ function Test-Assessment-25395 { $end = [System.Net.IPAddress]::Parse($matches[2]).GetAddressBytes() [array]::Reverse($start) [array]::Reverse($end) - return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0)) -gt 255) + return (([BitConverter]::ToUInt32($end,0) - [BitConverter]::ToUInt32($start,0) + 1) -gt 256) } return $false } @@ -84,7 +84,7 @@ function Test-Assessment-25395 { $BroadPortRangeThreshold = 10 if ($Port -eq '1-65535') { return $true } - if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1]) -gt $BroadPortRangeThreshold)) { return $true } + if ($Port -match '^(\d+)-(\d+)$' -and (([int]$matches[2] - [int]$matches[1] + 1) -gt $BroadPortRangeThreshold)) { return $true } return $false } From 184a79c5f6472b6cfb53cede4490230d9bbbcbfe Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 12:43:23 +0530 Subject: [PATCH 14/16] Update test logic for assessment 25395 --- .../tests/Test-Assessment.25395.ps1 | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index cd5575438..7f59076af 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -116,6 +116,29 @@ function Test-Assessment-25395 { return ($AD_WELL_KNOWN_PORTS -contains $Port) } + function Test-ContainsAdWellKnownPort { + <# + .SYNOPSIS + Checks if a port range contains any well-known Active Directory ports. + .DESCRIPTION + Evaluates whether a port range (e.g., '50-500') includes any of the + well-known AD ports (53, 88, 135, 389, 445, 464, 636, 3268, 3269). + .OUTPUTS + System.Boolean - True if range contains AD ports, false otherwise. + #> + param([string]$Port) + if ($Port -match '^(\d+)-(\d+)$') { + $start = [int]$matches[1] + $end = [int]$matches[2] + foreach ($adPort in $AD_WELL_KNOWN_PORTS) { + if ([int]$adPort -ge $start -and [int]$adPort -le $end) { + return $true + } + } + } + return $false + } + #endregion Helper Functions #region Data Collection @@ -208,10 +231,16 @@ function Test-Assessment-25395 { # Step 3: Evaluate port breadth with AD RPC exceptions foreach ($port in $segment.ports) { if (Test-IsBroadPortRange $port) { + # Check if this is a valid AD RPC exception or exact AD well-known port if (-not (Test-IsAdRpcException -AppName $app.displayName -Port $port) ` -and -not (Test-IsAdWellKnownPort $port)) { $hasBroadPorts = $true $issues += 'Broad port range' + + # Additionally flag if the broad range contains AD well-known ports + if (Test-ContainsAdWellKnownPort $port) { + $issues += 'Broad range overlaps AD ports' + } } } } @@ -356,7 +385,6 @@ function Test-Assessment-25395 { # 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' From c1fdf84fa0e6cfedc948052b8a78d0e69483544b Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 13:35:30 +0530 Subject: [PATCH 15/16] Refine segment evaluation logic for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.ps1 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 7f59076af..9753b8e2b 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -254,6 +254,7 @@ function Test-Assessment-25395 { if ($issues.Count -gt 0) { $segmentFindings += [PSCustomObject]@{ AppName = $app.displayName + AppId = $app.appId SegmentId = $segment.id Issue = ($issues -join ', ') Destination = $segment.destinationHost @@ -369,7 +370,7 @@ function Test-Assessment-25395 { if ($segmentFindings.Count -gt 0) { $tableRows = "" $formatTemplate = @' -## Segment Findings +## Segment findings | App name | Segment id | Issue | Destination | Ports | Recommendation | |---|---|---|---|---|---| @@ -377,7 +378,9 @@ function Test-Assessment-25395 { '@ foreach ($f in $segmentFindings) { - $tableRows += "| $(Get-SafeMarkdown $f.AppName) | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" + $appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($f.AppId)" + $linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $f.AppName), $appLink + $tableRows += "| $linkedAppName | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" } $mdInfo += $formatTemplate -f $tableRows } From 20d428a346fa3b367446e35c025249408c438c6d Mon Sep 17 00:00:00 2001 From: Afif Ahmed Patel Date: Thu, 8 Jan 2026 16:54:41 +0530 Subject: [PATCH 16/16] Refine segment scope column logic for assessment 25395 --- src/powershell/tests/Test-Assessment.25395.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/powershell/tests/Test-Assessment.25395.ps1 b/src/powershell/tests/Test-Assessment.25395.ps1 index 9753b8e2b..31ca66f74 100644 --- a/src/powershell/tests/Test-Assessment.25395.ps1 +++ b/src/powershell/tests/Test-Assessment.25395.ps1 @@ -207,7 +207,7 @@ function Test-Assessment-25395 { # Step 2: Evaluate segment destination granularity $issues = @() - $segmentSummary += "$($segment.destinationType):$($segment.destinationHost):$($segment.ports -join ',')" + $segmentSummary += "$($segment.destinationHost):$($segment.ports -join ',')" switch ($segment.destinationType) { 'dnsSuffix' { @@ -372,15 +372,15 @@ function Test-Assessment-25395 { $formatTemplate = @' ## Segment findings -| App name | Segment id | Issue | Destination | Ports | Recommendation | -|---|---|---|---|---|---| +| App name | Issue | Destination | Ports | Recommendation | +|---|---|---|---|---| {0} '@ foreach ($f in $segmentFindings) { $appLink = "https://entra.microsoft.com/#view/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/~/overview/appId/$($f.AppId)" $linkedAppName = "[{0}]({1})" -f (Get-SafeMarkdown $f.AppName), $appLink - $tableRows += "| $linkedAppName | $($f.SegmentId) | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" + $tableRows += "| $linkedAppName | $($f.Issue) | $($f.Destination) | $($f.Ports) | Narrow destination and ports |`n" } $mdInfo += $formatTemplate -f $tableRows }