Skip to content

Commit 52f857d

Browse files
committed
Expanding ZTNA tests to cover more or update stubs to actually test data
1 parent 32bc382 commit 52f857d

19 files changed

Lines changed: 753 additions & 6 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
function Invoke-CippTestZTNA21775 {
2+
<#
3+
.SYNOPSIS
4+
Tenant app management policy is configured
5+
#>
6+
param($Tenant)
7+
8+
try {
9+
$PolicyData = Get-CIPPTestData -TenantFilter $Tenant -Type 'DefaultAppManagementPolicy'
10+
11+
if (-not $PolicyData) {
12+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21775' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Tenant app management policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management'
13+
return
14+
}
15+
16+
$Policy = if ($PolicyData -is [System.Collections.IList]) { $PolicyData[0] } else { $PolicyData }
17+
18+
$Enabled = $Policy.isEnabled -eq $true
19+
$AppRestrictions = $Policy.applicationRestrictions
20+
$SpRestrictions = $Policy.servicePrincipalRestrictions
21+
22+
$HasActiveRule = {
23+
param($Restrictions)
24+
if (-not $Restrictions) { return $false }
25+
foreach ($Section in 'passwordCredentials', 'keyCredentials') {
26+
$Rules = $Restrictions.$Section
27+
if ($Rules -and ($Rules.Where({ $_.state -eq 'enabled' })).Count -gt 0) {
28+
return $true
29+
}
30+
}
31+
return $false
32+
}
33+
34+
$AppHasRule = & $HasActiveRule $AppRestrictions
35+
$SpHasRule = & $HasActiveRule $SpRestrictions
36+
$Passed = $Enabled -and ($AppHasRule -or $SpHasRule)
37+
38+
$Lines = [System.Collections.Generic.List[string]]::new()
39+
if ($Passed) {
40+
$Status = 'Passed'
41+
$Lines.Add('Tenant default app management policy is enabled with active credential restrictions.')
42+
} else {
43+
$Status = 'Failed'
44+
$Lines.Add('Tenant default app management policy is not properly configured.')
45+
$Lines.Add('')
46+
$Lines.Add("- **isEnabled:** $Enabled")
47+
$Lines.Add("- **applicationRestrictions has active rule:** $AppHasRule")
48+
$Lines.Add("- **servicePrincipalRestrictions has active rule:** $SpHasRule")
49+
$Lines.Add('')
50+
$Lines.Add('**Remediation:** Enable the default app management policy and configure credential restrictions to control how applications can use password and key credentials.')
51+
}
52+
53+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21775' -TestType 'Identity' -Status $Status -ResultMarkdown ($Lines -join "`n") -Risk 'Medium' -Name 'Tenant app management policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management'
54+
55+
} catch {
56+
$ErrorMessage = Get-CippException -Exception $_
57+
Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
58+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21775' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Tenant app management policy is configured' -UserImpact 'Low' -ImplementationEffort 'Medium' -Category 'Application Management'
59+
}
60+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
function Invoke-CippTestZTNA21777 {
2+
<#
3+
.SYNOPSIS
4+
App instance property lock is configured for all multitenant applications
5+
#>
6+
param($Tenant)
7+
8+
try {
9+
$Apps = Get-CIPPTestData -TenantFilter $Tenant -Type 'Apps'
10+
11+
if (-not $Apps) {
12+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21777' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'App instance property lock is configured for all multitenant applications' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
13+
return
14+
}
15+
16+
$MultitenantAudiences = 'AzureADMultipleOrgs', 'AzureADandPersonalMicrosoftAccount', 'PersonalMicrosoftAccount'
17+
$MultitenantApps = $Apps.Where({ $_.signInAudience -in $MultitenantAudiences })
18+
19+
if ($MultitenantApps.Count -eq 0) {
20+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21777' -TestType 'Identity' -Status 'Passed' -ResultMarkdown 'No multitenant applications found in the tenant.' -Risk 'High' -Name 'App instance property lock is configured for all multitenant applications' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
21+
return
22+
}
23+
24+
$NonCompliantApps = [System.Collections.Generic.List[object]]::new()
25+
foreach ($App in $MultitenantApps) {
26+
$Lock = $App.servicePrincipalLockConfiguration
27+
$LockEnabled = $Lock.isEnabled -eq $true -and $Lock.allProperties -eq $true
28+
if (-not $LockEnabled) {
29+
$NonCompliantApps.Add($App)
30+
}
31+
}
32+
33+
$Lines = [System.Collections.Generic.List[string]]::new()
34+
if ($NonCompliantApps.Count -eq 0) {
35+
$Status = 'Passed'
36+
$Lines.Add("All $($MultitenantApps.Count) multitenant application(s) have property lock configured.")
37+
} else {
38+
$Status = 'Failed'
39+
$Lines.Add("$($NonCompliantApps.Count) of $($MultitenantApps.Count) multitenant application(s) are missing property lock configuration.")
40+
$Lines.Add('')
41+
$Lines.Add('| Display Name | App ID | Sign-In Audience |')
42+
$Lines.Add('| :----------- | :----- | :--------------- |')
43+
foreach ($App in ($NonCompliantApps | Select-Object -First 25)) {
44+
$Lines.Add("| $($App.displayName) | $($App.appId) | $($App.signInAudience) |")
45+
}
46+
if ($NonCompliantApps.Count -gt 25) {
47+
$Lines.Add('')
48+
$Lines.Add("...and $($NonCompliantApps.Count - 25) more.")
49+
}
50+
$Lines.Add('')
51+
$Lines.Add('**Remediation:** Configure `servicePrincipalLockConfiguration` with `isEnabled = true` and `allProperties = true` on each multitenant app to prevent unauthorized property modifications.')
52+
}
53+
54+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21777' -TestType 'Identity' -Status $Status -ResultMarkdown ($Lines -join "`n") -Risk 'High' -Name 'App instance property lock is configured for all multitenant applications' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
55+
56+
} catch {
57+
$ErrorMessage = Get-CippException -Exception $_
58+
Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
59+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21777' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'App instance property lock is configured for all multitenant applications' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
60+
}
61+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
...
1+
Microsoft Entra ID Protection generates risk detections for sign-in and user-level anomalies, including unfamiliar locations, anonymous IP usage, leaked credentials, and impossible travel. When these detections remain in an untriaged state for an extended time, an organization loses the operational signal that an account may be compromised. Threat actors then have a longer window to extend persistence, move laterally, or stage further attacks before defenders are aware of the activity.
2+
3+
Triaging each detection — by dismissing, marking the user as compromised, or confirming safe — closes the feedback loop with ID Protection's machine-learning models and ensures the security team has actioned every risk indicator the platform has surfaced.
24

35
**Remediation action**
46

7+
- [Investigate risk in Microsoft Entra ID Protection](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-investigate-risk?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
8+
- [Remediate risks and unblock users](https://learn.microsoft.com/entra/id-protection/howto-identity-protection-remediate-unblock?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
59
<!--- Results --->
610
%TestResult%
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
function Invoke-CippTestZTNA21864 {
2+
<#
3+
.SYNOPSIS
4+
All risk detections are triaged
5+
#>
6+
param($Tenant)
7+
8+
try {
9+
$RiskDetections = Get-CIPPTestData -TenantFilter $Tenant -Type 'RiskDetections'
10+
11+
if (-not $RiskDetections) {
12+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21864' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'High' -Name 'All risk detections are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Access Control'
13+
return
14+
}
15+
16+
# Risk detections that haven't been actioned. Anything still in atRisk/unknownFutureValue
17+
# older than 30 days is untriaged.
18+
$TriagedStates = 'remediated', 'dismissed', 'confirmedSafe', 'none'
19+
$Threshold = (Get-Date).AddDays(-30)
20+
21+
$Untriaged = [System.Collections.Generic.List[object]]::new()
22+
foreach ($Detection in $RiskDetections) {
23+
if ($Detection.riskState -in $TriagedStates) { continue }
24+
$When = $Detection.detectedDateTime ?? $Detection.activityDateTime
25+
if (-not $When) { continue }
26+
try {
27+
if (([DateTime]$When) -lt $Threshold) { $Untriaged.Add($Detection) }
28+
} catch { }
29+
}
30+
31+
$Lines = [System.Collections.Generic.List[string]]::new()
32+
if ($Untriaged.Count -eq 0) {
33+
$Status = 'Passed'
34+
$Lines.Add("All $($RiskDetections.Count) risk detection(s) have been triaged or are recent (within 30 days).")
35+
} else {
36+
$Status = 'Failed'
37+
$Lines.Add("$($Untriaged.Count) risk detection(s) older than 30 days remain in an untriaged state.")
38+
$Lines.Add('')
39+
$Lines.Add("**Total detections:** $($RiskDetections.Count)")
40+
$Lines.Add("**Untriaged (>30 days):** $($Untriaged.Count)")
41+
$Lines.Add('')
42+
$Lines.Add('| User | Risk Event | Risk Level | Risk State | Detected |')
43+
$Lines.Add('| :--- | :--------- | :--------- | :--------- | :------- |')
44+
$Top = $Untriaged | Sort-Object { [DateTime]($_.detectedDateTime ?? $_.activityDateTime) } | Select-Object -First 25
45+
foreach ($D in $Top) {
46+
$When = ($D.detectedDateTime ?? $D.activityDateTime)
47+
$Lines.Add("| $($D.userDisplayName ?? '-') | $($D.riskEventType ?? '-') | $($D.riskLevel ?? '-') | $($D.riskState ?? '-') | $When |")
48+
}
49+
if ($Untriaged.Count -gt 25) {
50+
$Lines.Add('')
51+
$Lines.Add("...and $($Untriaged.Count - 25) more.")
52+
}
53+
$Lines.Add('')
54+
$Lines.Add('**Remediation:** Investigate and triage the listed risk detections through the Microsoft Entra ID Protection portal. Resolve each by marking the user as compromised, dismissing, or confirming safe.')
55+
}
56+
57+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21864' -TestType 'Identity' -Status $Status -ResultMarkdown ($Lines -join "`n") -Risk 'High' -Name 'All risk detections are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Access Control'
58+
59+
} catch {
60+
$ErrorMessage = Get-CippException -Exception $_
61+
Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
62+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21864' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'High' -Name 'All risk detections are triaged' -UserImpact 'Low' -ImplementationEffort 'High' -Category 'Access Control'
63+
}
64+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
...
1+
Standing (permanent) assignments to privileged Microsoft Entra roles such as Global Administrator, Privileged Role Administrator, or Security Administrator expand the blast radius of a single account compromise. A threat actor who acquires credentials for an account with a permanent privileged assignment immediately inherits the full role, with no MFA challenge, approval workflow, or time bound on the access.
2+
3+
Privileged Identity Management (PIM) replaces permanent assignments with just-in-time eligibility. Users must request and activate the role for a bounded duration, typically with MFA and optionally with approval. This shrinks the window of opportunity for an attacker and produces audit trails on every elevation.
24

35
**Remediation action**
46

7+
- [Convert standing privileged role assignments to eligible PIM assignments](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/pim-how-to-add-role-to-user?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
8+
- [Configure PIM role settings](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/pim-how-to-change-default-settings?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
59
<!--- Results --->
610
%TestResult%
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
function Invoke-CippTestZTNA21876 {
2+
<#
3+
.SYNOPSIS
4+
Use PIM for Microsoft Entra privileged roles
5+
#>
6+
param($Tenant)
7+
8+
try {
9+
$RoleAssignments = Get-CIPPTestData -TenantFilter $Tenant -Type 'RoleAssignmentScheduleInstances'
10+
11+
if (-not $RoleAssignments) {
12+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21876' -TestType 'Identity' -Status 'Skipped' -ResultMarkdown 'No data found in database. This may be due to missing required licenses or data collection not yet completed.' -Risk 'Medium' -Name 'Use PIM for Microsoft Entra privileged roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
13+
return
14+
}
15+
16+
# Well-known privileged role template IDs.
17+
$PrivilegedRoleTemplateIds = @(
18+
'62e90394-69f5-4237-9190-012177145e10' # Global Administrator
19+
'e8611ab8-c189-46e8-94e1-60213ab1f814' # Privileged Role Administrator
20+
'194ae4cb-b126-40b2-bd5b-6091b380977d' # Security Administrator
21+
'fe930be7-5e62-47db-91af-98c3a49a38b1' # User Administrator
22+
'729827e3-9c14-49f7-bb1b-9608f156bbb8' # Helpdesk Administrator
23+
'f28a1f50-f6e7-4571-818b-6a12f2af6b6c' # SharePoint Administrator
24+
'29232cdf-9323-42fd-ade2-1d097af3e4de' # Exchange Administrator
25+
'69091246-20e8-4a56-aa4d-066075b2a7a8' # Teams Administrator
26+
'158c047a-c907-4556-b7ef-446551a6b5f7' # Cloud Application Administrator
27+
'9b895d92-2cd3-44c7-9d02-a6ac2d5ea5c3' # Application Administrator
28+
'b0f54661-2d74-4c50-afa3-1ec803f12efe' # Billing Administrator
29+
'b1be1c3e-b65d-4f19-8427-f6fa0d97feb9' # Conditional Access Administrator
30+
'966707d0-3269-4727-9be2-8c3a10f19b9d' # Password Administrator
31+
'e3973bdf-4987-49ae-837a-ba8e231c7286' # Azure DevOps Administrator
32+
'7be44c8a-adaf-4e2a-84d6-ab2649e08a13' # Privileged Authentication Administrator
33+
)
34+
35+
$PermanentToPrivileged = [System.Collections.Generic.List[object]]::new()
36+
foreach ($A in $RoleAssignments) {
37+
if ($A.roleDefinitionId -notin $PrivilegedRoleTemplateIds) { continue }
38+
if ($A.assignmentType -eq 'Assigned' -and $A.memberType -in 'Direct', 'Group') {
39+
$PermanentToPrivileged.Add($A)
40+
}
41+
}
42+
43+
$Lines = [System.Collections.Generic.List[string]]::new()
44+
if ($PermanentToPrivileged.Count -eq 0) {
45+
$Status = 'Passed'
46+
$Lines.Add('No permanent (non-PIM) assignments found for privileged Microsoft Entra roles.')
47+
} else {
48+
$Status = 'Failed'
49+
$Lines.Add("$($PermanentToPrivileged.Count) permanent assignment(s) found for privileged Microsoft Entra roles. These should be managed via PIM eligibility instead.")
50+
$Lines.Add('')
51+
$Lines.Add('| Principal | Role Definition ID | Assignment Type | Member Type |')
52+
$Lines.Add('| :-------- | :----------------- | :-------------- | :---------- |')
53+
foreach ($A in ($PermanentToPrivileged | Select-Object -First 25)) {
54+
$Lines.Add("| $($A.principalId) | $($A.roleDefinitionId) | $($A.assignmentType) | $($A.memberType) |")
55+
}
56+
if ($PermanentToPrivileged.Count -gt 25) {
57+
$Lines.Add('')
58+
$Lines.Add("...and $($PermanentToPrivileged.Count - 25) more.")
59+
}
60+
$Lines.Add('')
61+
$Lines.Add('**Remediation:** Move standing privileged role assignments into PIM as eligible assignments so users must activate the role just-in-time with MFA and approval.')
62+
}
63+
64+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21876' -TestType 'Identity' -Status $Status -ResultMarkdown ($Lines -join "`n") -Risk 'Medium' -Name 'Use PIM for Microsoft Entra privileged roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
65+
66+
} catch {
67+
$ErrorMessage = Get-CippException -Exception $_
68+
Write-LogMessage -API 'Tests' -tenant $Tenant -message "Failed to run test: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage
69+
Add-CippTestResult -TenantFilter $Tenant -TestId 'ZTNA21876' -TestType 'Identity' -Status 'Failed' -ResultMarkdown "Test failed: $($ErrorMessage.NormalizedError)" -Risk 'Medium' -Name 'Use PIM for Microsoft Entra privileged roles' -UserImpact 'Low' -ImplementationEffort 'Low' -Category 'Access Control'
70+
}
71+
}
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
...
1+
PIM for Groups extends just-in-time elevation to group membership, so users only become members of a role-assignable group for a bounded duration. When such groups contain other groups as members instead of direct user assignments, the PIM activation flow is bypassed for everyone in the nested group — they inherit membership at all times rather than going through PIM activation.
2+
3+
Nested groups also obscure the effective access picture. Auditors can no longer determine, from the role-assignable group alone, which users actually hold the role at any given moment.
24

35
**Remediation action**
46

7+
- [Replace nested group memberships with direct user assignments on role-assignable groups](https://learn.microsoft.com/entra/id-governance/privileged-identity-management/concept-pim-for-groups?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
8+
- [Best practices for role-assignable groups](https://learn.microsoft.com/entra/identity/role-based-access-control/groups-concept?wt.mc_id=zerotrustrecommendations_automation_content_cnl_csasci)
59
<!--- Results --->
610
%TestResult%

0 commit comments

Comments
 (0)