Skip to content

Commit 3d99250

Browse files
committed
Sankey - 27021
1 parent 79e32d6 commit 3d99250

17 files changed

Lines changed: 381 additions & 12 deletions

src/powershell/assets/ReportTemplate.classic.html

Lines changed: 6 additions & 6 deletions
Large diffs are not rendered by default.

src/powershell/assets/ReportTemplate.html

Lines changed: 6 additions & 6 deletions
Large diffs are not rendered by default.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<#
2+
.SYNOPSIS
3+
Publish structured data from a check so that composite dashboards can consume it.
4+
5+
.DESCRIPTION
6+
Roll-up test results only carry a Pass/Fail verdict. Dashboards that aggregate several
7+
checks (for example the Private Access funnel) need the per-item rows a check already
8+
computed. This stores those rows in threadsafe module state so they survive the parallel
9+
test runspaces and can be read during the tenant information stage.
10+
11+
.EXAMPLE
12+
Add-ZtTestData -Name 'PrivateAccessSegmentation' -Value $appResults
13+
14+
Publishes the per-application segmentation rows for later aggregation.
15+
#>
16+
17+
function Add-ZtTestData {
18+
[CmdletBinding()]
19+
param(
20+
# The unique name for this data set.
21+
[Parameter(Mandatory = $true)]
22+
[string] $Name,
23+
24+
# The value to publish.
25+
$Value
26+
)
27+
28+
$script:__ZtSession.TestData.Value[$Name] = $Value
29+
}

src/powershell/private/core/Clear-ZtModuleVariable.ps1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ function Clear-ZtModuleVariable {
1818
$script:__ZtSession.AzureCache.Value.Clear()
1919
$script:__ZtSession.GraphBaseUri = $null
2020
$script:__ZtSession.TestResultDetail.Value.Clear()
21+
$script:__ZtSession.TestData.Value.Clear()
2122
$script:__ZtSession.TestStatistics.Value.Clear()
2223
$script:__ZtSession.TenantInfo.Value.Clear()
2324
$script:__ZtSession.ProgressState.Value.Clear()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<#
2+
.SYNOPSIS
3+
Read structured data published by a check via Add-ZtTestData.
4+
5+
.DESCRIPTION
6+
Returns $null when the check did not run, was skipped, or published nothing.
7+
8+
.EXAMPLE
9+
Get-ZtTestData -Name 'PrivateAccessSegmentation'
10+
#>
11+
12+
function Get-ZtTestData {
13+
[CmdletBinding()]
14+
param(
15+
# The unique name of the data set to read.
16+
[Parameter(Mandatory = $true)]
17+
[string] $Name
18+
)
19+
20+
$script:__ZtSession.TestData.Value[$Name]
21+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<#
2+
.SYNOPSIS
3+
Builds the Private Access Zero Trust posture funnel (spec 27021).
4+
5+
.DESCRIPTION
6+
Aggregates the per-item rows published by the segmentation (25395), authentication (25396)
7+
and administration (25384) checks into a Sankey funnel.
8+
9+
Every Private Access application enters at the source node and flows through two sequential
10+
gates. Segmentation and authentication share the application denominator and are joined on
11+
App ID, so only apps that clear segmentation are partitioned by the authentication gate.
12+
Administration is denominated in role assignments, not applications, so it is rendered as a
13+
separate band.
14+
#>
15+
16+
function Add-ZtOverviewPrivateAccess {
17+
[CmdletBinding()]
18+
param()
19+
20+
$tenantInfoName = 'OverviewPrivateAccess'
21+
22+
$activity = 'Building Private Access Zero Trust posture'
23+
Write-ZtProgress -Activity $activity -Status 'Processing'
24+
25+
$segmentation = @(Get-ZtTestData -Name 'PrivateAccessSegmentation')
26+
$authentication = @(Get-ZtTestData -Name 'PrivateAccessAuthentication')
27+
$administration = Get-ZtTestData -Name 'PrivateAccessAdministration'
28+
29+
if ($segmentation.Count -eq 0 -and $authentication.Count -eq 0 -and $null -eq $administration) {
30+
Write-PSFMessage '🟦 Skipping: No Private Access check results available' -Tag Test -Level VeryVerbose
31+
Add-ZtTenantInfo -Name $tenantInfoName -Value $null
32+
return
33+
}
34+
35+
# Gate 1 - partition every Private Access app by its segmentation status
36+
$broadSegments = @($segmentation | Where-Object { $_.Status -eq 'Fail' }).Count
37+
$segmentationReview = @($segmentation | Where-Object { $_.Status -eq 'ManualReview' }).Count
38+
$leastPrivilegeApps = @($segmentation | Where-Object { $_.Status -eq 'Pass' })
39+
40+
# Gate 2 - partition only the segmentation-clean apps, joined to the auth gate on App ID
41+
$authByAppId = @{}
42+
foreach ($row in $authentication) {
43+
if ($row.AppId) { $authByAppId[[string]$row.AppId] = $row.Status }
44+
}
45+
46+
$passwordOnly = 0
47+
$authenticationReview = 0
48+
$strongAuth = 0
49+
foreach ($app in $leastPrivilegeApps) {
50+
switch ($authByAppId[[string]$app.AppId]) {
51+
'Pass' { $strongAuth++ }
52+
'Fail' { $passwordOnly++ }
53+
# An app with no matching authentication row was not evaluated by 25396
54+
default { $authenticationReview++ }
55+
}
56+
}
57+
58+
# Gate 3 - separate band denominated in Application Administrator assignments
59+
$tenantWideAdmin = ($administration.TenantWide -as [int]) ?? 0
60+
$scopedAdmin = ($administration.Scoped -as [int]) ?? 0
61+
62+
$nodes = @(
63+
@{ source = 'Private Access apps'; target = 'Broad segments - at-risk'; value = $broadSegments }
64+
@{ source = 'Private Access apps'; target = 'Segmentation manual review'; value = $segmentationReview }
65+
@{ source = 'Private Access apps'; target = 'Least-privilege segments'; value = $leastPrivilegeApps.Count }
66+
@{ source = 'Least-privilege segments'; target = 'Password-only - at-risk'; value = $passwordOnly }
67+
@{ source = 'Least-privilege segments'; target = 'Authentication manual review'; value = $authenticationReview }
68+
@{ source = 'Least-privilege segments'; target = 'Strong auth - Zero Trust'; value = $strongAuth }
69+
@{ source = 'Application Administrator assignments'; target = 'Tenant-wide admin - at-risk'; value = $tenantWideAdmin }
70+
@{ source = 'Application Administrator assignments'; target = 'App-scoped admin - Zero Trust'; value = $scopedAdmin }
71+
)
72+
73+
$summary = @{
74+
description = "$($segmentation.Count) Private Access application(s) evaluated. $strongAuth reached the Zero Trust set by clearing both least-privilege segmentation and strong authentication."
75+
nodes = $nodes
76+
# A tenant-wide Application Administrator can rewrite segments and authentication for every app
77+
adminAtRisk = $tenantWideAdmin -gt 0
78+
# The two gates disagree on the app population, so unmatched apps need review
79+
populationMismatch = ($segmentation.Count -ne $authentication.Count)
80+
}
81+
82+
Add-ZtTenantInfo -Name $tenantInfoName -Value $summary
83+
}

src/powershell/private/tenantinfo/Invoke-ZtTenantInfo.ps1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ function Invoke-ZtTenantInfo {
2626
Add-ZtOverviewAuthMethodsPrivilegedUsers -Database $Database
2727
}
2828

29+
if ($Pillar -in ('All', 'Network')) {
30+
Add-ZtOverviewPrivateAccess
31+
}
32+
2933
if ($Pillar -in ('All', 'Devices')) {
3034
$IntunePlan = Get-ZtLicenseInformation -Product Intune
3135
Add-ZtDeviceOverview -Database $Database

src/powershell/scripts/variables.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ $script:__ZtSession = @{
1515
TestMeta = @()
1616
# A DCO dictionary is the same threadsafe dictionary across all runspaces, allowing parallelized checks to write results to the same store safely
1717
TestResultDetail = Set-PSFDynamicContentObject -Name "ZtAssessment.TestResultDetails" -Dictionary -PassThru
18+
# Structured data published by checks for composite dashboards (see Add-ZtTestData)
19+
TestData = Set-PSFDynamicContentObject -Name "ZtAssessment.TestData" -Dictionary -PassThru
1820
TestStatistics = Set-PSFDynamicContentObject -Name "ZtAssessment.TestStatistics" -Dictionary -PassThru
1921
TenantInfo = Set-PSFDynamicContentObject -Name "ZtAssessment.TenantInfo" -Dictionary -PassThru
2022
ProgressState = Set-PSFDynamicContentObject -Name "ZtAssessment.ProgressState" -Dictionary -PassThru

src/powershell/tests/Test-Assessment.25384.ps1

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,12 @@ function Test-Assessment-25384 {
385385
$testResultMarkdown = $mdInfo
386386
#endregion Report Generation
387387

388+
# Publish the administration band for the Private Access overview funnel (27021)
389+
Add-ZtTestData -Name 'PrivateAccessAdministration' -Value ([PSCustomObject]@{
390+
TenantWide = $tenantWideAssignments.Count
391+
Scoped = $scopedAssignments.Count
392+
})
393+
388394
$params = @{
389395
TestId = '25384'
390396
Title = 'Application admin rights are constrained to specific Private Access apps, not tenant-wide'

src/powershell/tests/Test-Assessment.25395.ps1

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,21 @@ WHERE list_contains(tags, 'PrivateAccessNonWebApplication')
476476
# Replace the placeholder with detailed information
477477
$testResultMarkdown = $testResultMarkdown -replace '%TestResult%', $mdInfo
478478
#endregion Report Generation
479+
480+
# Publish the per-app segmentation gate for the Private Access overview funnel (27021)
481+
Add-ZtTestData -Name 'PrivateAccessSegmentation' -Value @(
482+
foreach ($r in $appResults) {
483+
[PSCustomObject]@{
484+
AppId = $r.AppId
485+
Status = switch -Wildcard ($r.Status) {
486+
'Fail*' { 'Fail' }
487+
'Pass' { 'Pass' }
488+
default { 'ManualReview' }
489+
}
490+
}
491+
}
492+
)
493+
479494
$params = @{
480495
TestId = '25395'
481496
Title = 'Entra Private Access Application segments are defined to enforce least-privilege access'

0 commit comments

Comments
 (0)