Network - 25398 - Domain controller RDP access is protected by phishing-resistant authentication through Global Secure Access#873
Network - 25398 - Domain controller RDP access is protected by phishing-resistant authentication through Global Secure Access#873Manoj-Kesana wants to merge 2 commits intomainfrom
Conversation
… into Feature-25398
There was a problem hiding this comment.
Pull request overview
Adds a new Network assessment (25398) to evaluate whether Entra Private Access (Global Secure Access) RDP access to domain controllers is protected by Conditional Access requiring phishing-resistant authentication.
Changes:
- Introduces a new PowerShell test (25398) that discovers Private Access apps/segments with RDP (3389) and correlates them with CA policies requiring “Phishing-resistant MFA”.
- Generates markdown reporting tables for identified DC-like hosts, RDP apps, and relevant CA policies.
- Adds the corresponding remediation documentation markdown page for assessment 25398.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
| src/powershell/tests/Test-Assessment.25398.ps1 | New assessment logic to discover Private Access RDP exposure and evaluate CA phishing-resistant auth coverage, plus report generation. |
| src/powershell/tests/Test-Assessment.25398.md | New assessment documentation describing risk and remediation steps with a results placeholder. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| foreach ($segment in $segments) { | ||
| $ports = $segment.port | ||
|
|
||
| # Check if port 88 is explicitly configured (not in a range) | ||
| if ($ports -contains '88') { | ||
| $has88 = $true | ||
| $hostsWith88 += $segment.destinationHost | ||
| } | ||
|
|
||
| # Check if port 389 is explicitly configured (not in a range) | ||
| if ($ports -contains '389') { | ||
| $has389 = $true |
There was a problem hiding this comment.
applicationSegments returned by the Private Access segmentsConfiguration API appears to use a ports property (see similar parsing in Test-Assessment.25395.ps1). This test uses $segment.port, which will always be $null and causes DC/RDP detection to fail. Update the code to read from $segment.ports and normalize to an array before doing membership/range checks so both single and multi-value segments work reliably.
| $privateAccessApps = Invoke-ZtGraphRequest -RelativeUri 'applications' -QueryParameters @{ | ||
| '$filter' = "tags/any(t:t eq 'PrivateAccessNonWebApplication')" | ||
| '$count' = 'true' | ||
| '$select' = 'id,appId,displayName,tags' | ||
| } -ConsistencyLevel 'eventual' |
There was a problem hiding this comment.
The initial Private Access application query doesn’t specify -ApiVersion beta. Other Private Access tests in this repo query applications with -ApiVersion beta when filtering on Private Access tags, likely because these tags/objects are exposed via beta. Consider adding -ApiVersion beta here for consistency and to avoid tenants where the filter isn’t supported on v1.0.
| # Remove duplicates | ||
| $rdpApps = $rdpApps | Sort-Object AppId -Unique |
There was a problem hiding this comment.
$rdpApps = $rdpApps | Sort-Object AppId -Unique drops entries when the same app exposes RDP to multiple destination hosts (only one host will be kept), which can hide affected targets in the report and skew investigation. Dedupe on a compound key (e.g., AppId + DestinationHost), or keep all rows and only aggregate at the final pass/fail decision if needed.
| # Remove duplicates | |
| $rdpApps = $rdpApps | Sort-Object AppId -Unique | |
| # Remove duplicates (per AppId and DestinationHost) | |
| $rdpApps = $rdpApps | Sort-Object AppId, DestinationHost -Unique |
| $caPolicies = Invoke-ZtGraphRequest -RelativeUri "policies/authenticationStrengthPolicies/$authStrengthId/usage" -ApiVersion 'beta' | ||
|
|
||
| # Filter for enabled policies only | ||
| $enabledPolicies = $caPolicies | Where-Object { $_.state -eq 'enabled' } | ||
|
|
There was a problem hiding this comment.
The code treats the response from policies/authenticationStrengthPolicies/{id}/usage as if it were a list of Conditional Access policy objects (accessing .state, .displayName, .conditions...). In this repo, usage is used to retrieve CA policy IDs (e.g., Test-Assessment.21783.ps1 reads .none.id) and then joins against a full CA policy list. Update this test to fetch CA policies via Get-ZtConditionalAccessPolicy (or identity/conditionalAccess/policies) and filter by grantControls.authenticationStrength.id (or join by IDs from usage) before evaluating targeting/conditions.
| Status = $status | ||
| TargetingMethod = $targetingMethod | ||
| PolicyId = if ($protected) { ($enabledPolicies | Where-Object { $_.displayName -eq $protectedBy }).id } else { $null } | ||
| } |
There was a problem hiding this comment.
PolicyId is derived by re-searching $enabledPolicies by displayName. Display names are not guaranteed unique, so this can attach the wrong policyId/link. Capture the matching $policy.id at the time you decide the app is protected (inside the loop) and store that directly in the result object.
No description provided.