Skip to content

Commit 34a3605

Browse files
authored
Merge pull request #1019 from KelvinTegelaar/dev
[pull] dev from KelvinTegelaar:dev
2 parents e85828e + 7f78c70 commit 34a3605

21 files changed

Lines changed: 757 additions & 7 deletions

Modules/CIPPHTTP/Public/Entrypoints/HTTP Functions/Tools/GitHub/Invoke-ExecCommunityRepo.ps1

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ function Invoke-ExecCommunityRepo {
160160
$Path = $Request.Body.Path
161161
$FullName = $Request.Body.FullName
162162
$Branch = $Request.Body.Branch
163+
$Force = [bool]$Request.Body.Force
163164
try {
164165
$Template = Get-GitHubFileContents -FullName $FullName -Path $Path -Branch $Branch
165166

@@ -178,7 +179,7 @@ function Invoke-ExecCommunityRepo {
178179
(Get-GitHubFileContents -FullName $FullName -Branch $Branch -Path $Location.path).content | ConvertFrom-Json
179180
}
180181
}
181-
$ImportResult = Import-CommunityTemplate -Template $Content -SHA $Template.sha -MigrationTable $MigrationTable -LocationData $LocationData -Source $FullName
182+
$ImportResult = Import-CommunityTemplate -Template $Content -SHA $Template.sha -MigrationTable $MigrationTable -LocationData $LocationData -Source $FullName -Force:$Force
182183

183184
$Results = @{
184185
resultText = $ImportResult ?? 'Template imported'

Modules/CIPPTests/Public/Tests/GenericTests/Identity/Invoke-CippTestGenericTest011.ps1

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ function Invoke-CippTestGenericTest011 {
5858
$ResolveDisplayName = {
5959
param($StandardName, $TemplateSettings)
6060

61+
if ([string]::IsNullOrWhiteSpace($StandardName)) { return $null }
62+
6163
# 1. Regular standards — look up in standards.json
6264
if ($StandardsLabelMap.ContainsKey($StandardName)) {
6365
return $StandardsLabelMap[$StandardName]
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+
}

0 commit comments

Comments
 (0)