Network - 27021 - Private Access applications are governed by least-privilege segmentation, strong authentication, and constrained administration - #1476
Conversation
Aleksandar Nikolić (alexandair)
left a comment
There was a problem hiding this comment.
Manoj Kesana (@Manoj-Kesana) Please, address my feedback.
The implementation introduces a shared cross-runspace data store and a Private Access Sankey overview that aggregates checks 25384, 25395, and 25396. The PowerShell parses successfully and the patch is structurally clean, but the aggregation currently loses child-check verdicts and availability state.
That loss of state can cause failed, skipped, timed-out, partially executed, or population-mismatched checks to appear green, yellow, or absent in the overview. In particular:
- Failing scoped administrator assignments can be labelled App-scoped admin — Zero Trust.
- Unprotected Quick Access applications can disappear from the funnel.
- Equal-size but different application populations are not detected.
- Missing child output is treated as manual review instead of skipped/unavailable.
- Partial test execution can produce a misleading card claiming zero applications were evaluated.
These issues conflict with spec 27021, which states that child roll-up verdicts determine gate status and that skipped children must be represented as unavailable/gray rather than converted into pass, fail, or investigate.
Finding 1 — Blocking: failing administration assignments are rendered as Zero Trust
Relevant code
Publisher: src/powershell/tests/Test-Assessment.25384.ps1, lines 389–392
Add-ZtTestData -Name 'PrivateAccessAdministration' -Value ([PSCustomObject]@{
TenantWide = $tenantWideAssignments.Count
Scoped = $scopedAssignments.Count
})Consumer: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 57–70
$tenantWideAdmin = ($administration.TenantWide -as [int]) ?? 0
$scopedAdmin = ($administration.Scoped -as [int]) ?? 0
$nodes = @(
# ...
@{ source = 'Application Administrator assignments'; target = 'Tenant-wide admin - at-risk'; value = $tenantWideAdmin }
@{ source = 'Application Administrator assignments'; target = 'App-scoped admin - Zero Trust'; value = $scopedAdmin }
)Risk flag: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, line 77
adminAtRisk = $tenantWideAdmin -gt 0Problem
Check 25384 fails for more than tenant-wide scope. It also sets $passed = $false when an assignment principal is a group, service principal, or guest user. The published dashboard payload discards that verdict and reduces the result to only two counts: TenantWide and Scoped.
Every non-tenant-wide assignment is then sent to the green App-scoped admin - Zero Trust node. As a result, a scoped assignment held by a guest, group, or service principal can make check 25384 fail while the dashboard:
- renders the assignment as green;
- leaves
adminAtRiskfalse; and - does not tint the terminal Zero Trust node.
This contradicts spec 27021, where the child roll-up verdict determines the gate status.
Required correction
Publish or retrieve the child roll-up status and preserve it in the overview. Separate scoped assignments that satisfy all 25384 pass criteria from scoped assignments that fail principal or applicability checks. Do not label all scoped assignments as Zero Trust.
Suggested PR review comment
Scopedincludes every non-tenant-wide assignment, but check 25384 also fails scoped assignments held by groups, guests, or service principals. Those failing assignments are consequently rendered asApp-scoped admin - Zero Trust, whileadminAtRiskremains false. Publish the child verdict and/or separate compliant scoped assignments from problematic scoped assignments before assigning this green flow.
Finding 2 — Blocking: Quick Access and authentication-only applications disappear
Relevant code
Source population: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 35–39
$broadSegments = @($segmentation | Where-Object { $_.Status -eq 'Fail' }).Count
$segmentationReview = @($segmentation | Where-Object { $_.Status -eq 'ManualReview' }).Count
$leastPrivilegeApps = @($segmentation | Where-Object { $_.Status -eq 'Pass' })All Private Access apps source flows are derived exclusively from these segmentation rows.
Authentication publisher: src/powershell/tests/Test-Assessment.25396.ps1, lines 329–340
Add-ZtTestData -Name 'PrivateAccessAuthentication' -Value @(
foreach ($detail in $allAppDetails) {
[PSCustomObject]@{
AppId = $detail.AppId
Status = switch ($detail.Status) {
'Protected' { 'Pass' }
'Unprotected' { 'Fail' }
default { 'ManualReview' }
}
}
}
)Displayed denominator: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, line 74
description = "$($segmentation.Count) Private Access application(s) evaluated. $strongAuth reached the Zero Trust set by clearing both least-privilege segmentation and strong authentication."Problem
The child checks do not use the same population:
- Check 25395 publishes only
PrivateAccessNonWebApplicationapplications. - Check 25396 publishes both
PrivateAccessNonWebApplicationandNetworkAccessQuickAccessApplicationapplications.
The overview nevertheless defines the source population entirely from 25395. Authentication-only applications therefore disappear from the funnel. This can hide an unprotected Quick Access application even though it causes 25396 to fail.
The description also reports $segmentation.Count, not the larger or unified application population required by spec 27021.
Required correction
Either align both child checks to the same application population or construct the parent population from the union of normalized, distinct App IDs. Authentication-only applications need an explicit unavailable/manual-review segmentation state rather than being omitted. The child verdict must remain failed even when a failing authentication row is outside the segmentation-clean subset.
Suggested PR review comment
The source population is built exclusively from 25395 rows, but 25396 also evaluates Quick Access applications. Authentication-only apps therefore disappear from the funnel, including potentially unprotected Quick Access apps. Spec 27021 requires the larger population when children disagree. Build the denominator from distinct App IDs across both datasets, or align the child populations explicitly.
Finding 3 — Important: population mismatch detection compares counts, not App ID sets
Relevant code
src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, line 79
populationMismatch = ($segmentation.Count -ne $authentication.Count)Problem
Equal counts do not prove that the two children evaluated the same applications. For example:
- Segmentation:
{A, B} - Authentication:
{A, C}
Both collections have a count of two, so populationMismatch is false. Nevertheless:
- B has no authentication result and becomes manual review.
- C has no segmentation result and disappears entirely.
Raw counts may also include duplicate App IDs, while spec 27021 defines the population in terms of distinct applications.
Required correction
Normalize App IDs, remove duplicates, and compare sets in both directions. Derive the source denominator from the intended distinct set or union rather than raw row counts.
Suggested PR review comment
Comparing only counts does not establish that the children evaluated the same applications.
{A,B}and{A,C}both have count 2 but disagree on one app in each direction. Compare normalized distinct App ID sets, and derive the source denominator from their union.
Finding 4 — Important: missing child output is incorrectly classified as manual review
Relevant code
Data retrieval: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 25–27
$segmentation = @(Get-ZtTestData -Name 'PrivateAccessSegmentation')
$authentication = @(Get-ZtTestData -Name 'PrivateAccessAuthentication')
$administration = Get-ZtTestData -Name 'PrivateAccessAdministration'Default classification: src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 48–55
foreach ($app in $leastPrivilegeApps) {
switch ($authByAppId[[string]$app.AppId]) {
'Pass' { $strongAuth++ }
'Fail' { $passwordOnly++ }
# An app with no matching authentication row was not evaluated by 25396
default { $authenticationReview++ }
}
}Problem
The shared payload contains no child execution status. A missing authentication row can mean:
- 25396 was skipped because of licensing or service connectivity;
- 25396 timed out;
- 25396 threw before publishing;
- selective execution through
-Testsexcluded 25396; - the children genuinely evaluated different populations; or
- an individual data retrieval omitted the app.
All these cases currently increment authenticationReview. This converts unavailable or failed collection into an application-level Investigate result. Spec 27021 explicitly requires skipped children to render gray rather than being treated as pass, fail, or manual review.
The same loss of availability state affects segmentation and administration.
Required correction
Consume each child’s TestStatus/TestSkipped state or publish an explicit structured envelope containing execution status, verdict, and rows. Only classify an unmatched App ID as manual review when both child datasets completed successfully and the mismatch is genuine.
Suggested PR review comment
A missing authentication row is not necessarily
ManualReview: 25396 may have been skipped, timed out, failed before publishing, or excluded through-Tests. The spec requires skipped children to render gray. Consume the child execution/result status and incrementauthenticationReviewonly when both datasets completed successfully and the App ID is genuinely unmatched.
Finding 5 — Important: partial child data creates a misleading overview
Relevant code
src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 29–34
if ($segmentation.Count -eq 0 -and $authentication.Count -eq 0 -and $null -eq $administration) {
Write-PSFMessage '🟦 Skipping: No Private Access check results available' -Tag Test -Level VeryVerbose
Add-ZtTenantInfo -Name $tenantInfoName -Value $null
return
}Problem
The function skips only when all three payloads are absent. If only one child publishes data, the function still builds a summary as though the funnel were valid.
Examples:
- If only 25396 ran, the description claims
0 Private Access application(s) evaluatedeven though authentication rows exist. - If 25396 is absent but 25395 ran, every segmentation-clean app becomes authentication manual review.
- If 25384 is absent, its band silently collapses to zero instead of being marked unavailable.
- If only 25384 ran, the card can show an administration band while implying a complete Private Access posture overview.
Required correction
Handle each child’s availability independently. A complete overview should distinguish completed, skipped, failed, unavailable, and partially executed gates. Do not generate a complete-looking funnel from partial row data without an explicit degraded-state indication.
Suggested PR review comment
This guard skips only when all three payloads are absent. If only 25396 ran, the overview is still created with zero segmentation flows and a description claiming
0 Private Access application(s) evaluated, despite authentication rows being available. Handle each child’s availability independently and represent unavailable gates explicitly instead of constructing a partially misleading funnel.
Finding 6 — Important: the parent never implements the required overall determination
Relevant code
src/powershell/private/tenantinfo/Add-ZtOverviewPrivateAccess.ps1, lines 73–80
$summary = @{
description = "$($segmentation.Count) Private Access application(s) evaluated. $strongAuth reached the Zero Trust set by clearing both least-privilege segmentation and strong authentication."
nodes = $nodes
adminAtRisk = $tenantWideAdmin -gt 0
populationMismatch = ($segmentation.Count -ne $authentication.Count)
}Problem
Spec 27021 defines an overall determination:
- Pass when all three children pass.
- Investigate when none fails and one or more investigates.
- Fail when any child fails.
The summary contains no overall result and no per-gate child verdicts. It derives visual state only from selected row counts. This is the underlying reason a child can fail while its corresponding visual flow appears green or absent.
Required correction
Retrieve the roll-up result for checks 25384, 25395, and 25396 and include explicit gate and overall statuses in the summary. Row widths should quantify the gap; they must not replace the child verdicts.
Suggested PR review comment
Spec 27021 says the child roll-up verdict determines each gate and defines an overall Pass/Investigate/Fail result. This summary contains only row-derived counts and two warning flags, so a child can fail while its gate appears green or absent. Preserve the three child verdicts and compute the required overall status independently of Sankey widths.
Finding 7 — Important coverage gap: no PowerShell tests were added
Relevant code
New execution path: src/powershell/private/tenantinfo/Invoke-ZtTenantInfo.ps1, lines 29–31
if ($Pillar -in ('All', 'Network')) {
Add-ZtOverviewPrivateAccess
}Existing test file: code-tests/commands/Invoke-ZtTenantInfo.Tests.ps1
Problem
The PR introduces:
- a new cross-runspace
TestDatadictionary; Add-ZtTestDataandGet-ZtTestData;- a new multi-gate aggregation function;
- a new
Network/Allbranch inInvoke-ZtTenantInfo; and - three child publishers.
No corresponding PowerShell tests were added. The existing Invoke-ZtTenantInfo test invokes only the Devices branch and does not mock or assert Add-ZtOverviewPrivateAccess.
Required tests
At minimum, add tests for:
- Normal pass/fail/manual-review partitioning.
- App ID joins with matching populations.
- Different populations with different counts.
- Different populations with equal counts.
- Duplicate and case-variant App IDs.
- Quick Access rows present only in authentication.
- Scoped problematic administrator assignments.
- Child pass/fail/investigate propagation.
- Skipped, timed-out, errored, and missing children.
- Selective execution where only one child runs.
- Clearing and retrieving the new shared state.
Invoke-ZtTenantInfobehavior forNetwork,All, and unrelated pillars.
Suggested PR review comment
Please add coverage for the new
NetworkandAllbranches. The existing test only invokes theDevicesbranch, and there are no tests for the new aggregation function, App ID join, missing-child behavior, child verdict propagation, or sharedTestDatastate.
Aleksandar Nikolić (alexandair)
left a comment
There was a problem hiding this comment.
Manoj Kesana (@Manoj-Kesana) Please, address my feedback (in 3 additional comments).
Suggestions
- Add a direct test for
Get-ZtTestResultStatus()against the shared dynamic dictionary. Current aggregation tests mock the helper, so its real dictionary behavior is not covered. - Add component coverage verifying that failed, investigate, and unavailable gate verdicts are visible independently of link widths.
- Regenerate both report templates after correcting the source component.
Previous-report disposition
| Previous finding | Commit result |
|---|---|
| Failing scoped administrators shown as Zero Trust | Resolved |
| Authentication-only apps disappear | Partially resolved — retained in the population, but their authentication failure can remain visually hidden |
| Population comparison uses counts | Partially resolved — sets are compared, but mismatch does not affect overall status |
| Missing child classified as manual review | Resolved for segmentation/authentication joins |
| Partial data creates complete-looking overview | Partially resolved — degraded state added, but administration has no gray band |
| Parent lacks child and overall verdicts | Partially resolved — fields are produced but not rendered |
| No PowerShell tests | Resolved, with the gaps identified above |
|
Copilot resolve the merge conflicts in this pull request |
Aleksandar Nikolić (alexandair)
left a comment
There was a problem hiding this comment.
LGTM
43d04c5
into
astaykov/preview-report
No description provided.