Skip to content

Commit 225e06b

Browse files
committed
feat: Add Compare-CIPPIntuneAssignments and Get-CIPPIntunePolicyAssignments functions; update Push-CIPPStandardsList and Invoke-CIPPStandardIntuneTemplate for assignment verification; adjust host.json for function concurrency limits
1 parent c785d64 commit 225e06b

5 files changed

Lines changed: 237 additions & 8 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
function Compare-CIPPIntuneAssignments {
2+
<#
3+
.SYNOPSIS
4+
Compares existing Intune policy assignments against expected assignment settings.
5+
.DESCRIPTION
6+
Returns $true if the existing assignments match the expected settings, $false if they differ,
7+
or $null if the comparison could not be completed (e.g. Graph error).
8+
.PARAMETER ExistingAssignments
9+
The current assignments on the policy, as returned by Get-CIPPIntunePolicyAssignments.
10+
.PARAMETER ExpectedAssignTo
11+
The expected assignment target type: allLicensedUsers, AllDevices, AllDevicesAndUsers,
12+
customGroup, or On (no assignment).
13+
.PARAMETER ExpectedCustomGroup
14+
The expected custom group name(s), comma-separated. Used when ExpectedAssignTo is 'customGroup'.
15+
.PARAMETER ExpectedExcludeGroup
16+
The expected exclusion group name(s), comma-separated.
17+
.PARAMETER ExpectedAssignmentFilter
18+
The expected assignment filter display name. Wildcards supported.
19+
.PARAMETER ExpectedAssignmentFilterType
20+
'include' or 'exclude'. Defaults to 'include'.
21+
.PARAMETER TenantFilter
22+
The tenant to query for group/filter resolution.
23+
.FUNCTIONALITY
24+
Internal
25+
#>
26+
param(
27+
[object[]]$ExistingAssignments,
28+
[string]$ExpectedAssignTo,
29+
[string]$ExpectedCustomGroup,
30+
[string]$ExpectedExcludeGroup,
31+
[string]$ExpectedAssignmentFilter,
32+
[string]$ExpectedAssignmentFilterType = 'include',
33+
[Parameter(Mandatory = $true)]
34+
[string]$TenantFilter
35+
)
36+
37+
try {
38+
# Normalize existing targets
39+
$ExistingTargetTypes = @($ExistingAssignments.target.'@odata.type' | Where-Object { $_ })
40+
$ExistingIncludeGroupIds = @(
41+
$ExistingAssignments |
42+
Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' } |
43+
ForEach-Object { $_.target.groupId }
44+
)
45+
$ExistingExcludeGroupIds = @(
46+
$ExistingAssignments |
47+
Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget' } |
48+
ForEach-Object { $_.target.groupId }
49+
)
50+
51+
# Determine expected include target types
52+
$ExpectedIncludeTypes = switch ($ExpectedAssignTo) {
53+
'allLicensedUsers' { @('#microsoft.graph.allLicensedUsersAssignmentTarget') }
54+
'AllDevices' { @('#microsoft.graph.allDevicesAssignmentTarget') }
55+
'AllDevicesAndUsers' { @('#microsoft.graph.allDevicesAssignmentTarget', '#microsoft.graph.allLicensedUsersAssignmentTarget') }
56+
'customGroup' { @('#microsoft.graph.groupAssignmentTarget') }
57+
'On' { @() }
58+
default { @() }
59+
}
60+
61+
# Compare include target types (ignore exclusion targets)
62+
$ExistingIncludeTypes = @($ExistingTargetTypes | Where-Object { $_ -ne '#microsoft.graph.exclusionGroupAssignmentTarget' })
63+
$TargetTypeMatch = $true
64+
foreach ($t in $ExpectedIncludeTypes) {
65+
if ($t -notin $ExistingIncludeTypes) { $TargetTypeMatch = $false; break }
66+
}
67+
if ($TargetTypeMatch) {
68+
foreach ($t in $ExistingIncludeTypes) {
69+
if ($t -notin $ExpectedIncludeTypes) { $TargetTypeMatch = $false; break }
70+
}
71+
}
72+
73+
# Lazy-load groups cache only if needed
74+
$AllGroupsCache = $null
75+
76+
# For custom groups, resolve names to IDs and compare
77+
$IncludeGroupMatch = $true
78+
if ($ExpectedAssignTo -eq 'customGroup' -and $ExpectedCustomGroup) {
79+
$AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter
80+
$ExpectedGroupIds = @(
81+
$ExpectedCustomGroup.Split(',').Trim() | ForEach-Object {
82+
$name = $_
83+
$AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id
84+
} | Where-Object { $_ }
85+
)
86+
$MissingIds = @($ExpectedGroupIds | Where-Object { $_ -notin $ExistingIncludeGroupIds })
87+
$ExtraIds = @($ExistingIncludeGroupIds | Where-Object { $_ -notin $ExpectedGroupIds })
88+
$IncludeGroupMatch = ($MissingIds.Count -eq 0 -and $ExtraIds.Count -eq 0)
89+
}
90+
91+
# Compare exclusion groups
92+
$ExcludeGroupMatch = $true
93+
if ($ExpectedExcludeGroup) {
94+
if (-not $AllGroupsCache) {
95+
$AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter
96+
}
97+
$ExpectedExcludeIds = @(
98+
$ExpectedExcludeGroup.Split(',').Trim() | ForEach-Object {
99+
$name = $_
100+
$AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id
101+
} | Where-Object { $_ }
102+
)
103+
$MissingExcludeIds = @($ExpectedExcludeIds | Where-Object { $_ -notin $ExistingExcludeGroupIds })
104+
$ExtraExcludeIds = @($ExistingExcludeGroupIds | Where-Object { $_ -notin $ExpectedExcludeIds })
105+
$ExcludeGroupMatch = ($MissingExcludeIds.Count -eq 0 -and $ExtraExcludeIds.Count -eq 0)
106+
} elseif ($ExistingExcludeGroupIds.Count -gt 0) {
107+
# No exclusions expected but some exist
108+
$ExcludeGroupMatch = $false
109+
}
110+
111+
# Compare assignment filter
112+
$FilterMatch = $true
113+
if ($ExpectedAssignmentFilter) {
114+
$ExistingFilterIds = @(
115+
$ExistingAssignments |
116+
Where-Object { $_.target.deviceAndAppManagementAssignmentFilterId } |
117+
ForEach-Object { $_.target.deviceAndAppManagementAssignmentFilterId }
118+
)
119+
if ($ExistingFilterIds.Count -eq 0) {
120+
$FilterMatch = $false
121+
} else {
122+
$AllFilters = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $TenantFilter
123+
$ExpectedFilter = $AllFilters | Where-Object { $_.displayName -like $ExpectedAssignmentFilter } | Select-Object -First 1
124+
$FilterMatch = $ExpectedFilter -and ($ExpectedFilter.id -in $ExistingFilterIds)
125+
}
126+
}
127+
128+
return $TargetTypeMatch -and $IncludeGroupMatch -and $ExcludeGroupMatch -and $FilterMatch
129+
130+
} catch {
131+
Write-Warning "Compare-CIPPIntuneAssignments failed for tenant $TenantFilter : $($_.Exception.Message)"
132+
return $null # null = unknown, don't treat as mismatch
133+
}
134+
}

Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ function Push-CIPPStandardsList {
264264
FunctionName = 'CIPPStandard'
265265
}
266266
}
267-
Write-Host "Sending back $($FilteredStandards.Count) standards: $($FilteredStandards | ConvertTo-Json -Depth 5 -Compress)"
267+
Write-Host "Sending back $($FilteredStandards.Count) standards"
268268
return @($FilteredStandards)
269269

270270
} catch {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
function Get-CIPPIntunePolicyAssignments {
2+
<#
3+
.SYNOPSIS
4+
Gets the assignments for an existing Intune policy.
5+
.PARAMETER PolicyId
6+
The Intune policy ID.
7+
.PARAMETER TemplateType
8+
The template type (Device, Catalog, Admin, deviceCompliancePolicies, AppProtection,
9+
windowsDriverUpdateProfiles, windowsFeatureUpdateProfiles, windowsQualityUpdatePolicies,
10+
windowsQualityUpdateProfiles).
11+
.PARAMETER TenantFilter
12+
The tenant to query.
13+
.PARAMETER ExistingPolicy
14+
The existing policy object. Required for AppProtection to determine the odata subtype.
15+
.FUNCTIONALITY
16+
Internal
17+
#>
18+
param(
19+
[Parameter(Mandatory = $true)]
20+
[string]$PolicyId,
21+
[Parameter(Mandatory = $true)]
22+
[string]$TemplateType,
23+
[Parameter(Mandatory = $true)]
24+
[string]$TenantFilter,
25+
$ExistingPolicy
26+
)
27+
28+
switch ($TemplateType) {
29+
'Device' {
30+
$PlatformType = 'deviceManagement'
31+
$TypeUrl = 'deviceConfigurations'
32+
}
33+
'Catalog' {
34+
$PlatformType = 'deviceManagement'
35+
$TypeUrl = 'configurationPolicies'
36+
}
37+
'Admin' {
38+
$PlatformType = 'deviceManagement'
39+
$TypeUrl = 'groupPolicyConfigurations'
40+
}
41+
'deviceCompliancePolicies' {
42+
$PlatformType = 'deviceManagement'
43+
$TypeUrl = 'deviceCompliancePolicies'
44+
}
45+
'AppProtection' {
46+
$PlatformType = 'deviceAppManagement'
47+
$OdataType = if ($ExistingPolicy) { $ExistingPolicy.'@odata.type' -replace '#microsoft.graph.', '' } else { $null }
48+
if (-not $OdataType) { return $null }
49+
$TypeUrl = if ($OdataType -eq 'windowsInformationProtectionPolicy') { 'windowsInformationProtectionPolicies' } else { "${OdataType}s" }
50+
}
51+
'windowsDriverUpdateProfiles' {
52+
$PlatformType = 'deviceManagement'
53+
$TypeUrl = 'windowsDriverUpdateProfiles'
54+
}
55+
'windowsFeatureUpdateProfiles' {
56+
$PlatformType = 'deviceManagement'
57+
$TypeUrl = 'windowsFeatureUpdateProfiles'
58+
}
59+
'windowsQualityUpdatePolicies' {
60+
$PlatformType = 'deviceManagement'
61+
$TypeUrl = 'windowsQualityUpdatePolicies'
62+
}
63+
'windowsQualityUpdateProfiles' {
64+
$PlatformType = 'deviceManagement'
65+
$TypeUrl = 'windowsQualityUpdateProfiles'
66+
}
67+
default { return $null }
68+
}
69+
70+
$Uri = "https://graph.microsoft.com/beta/$PlatformType/$TypeUrl('$PolicyId')/assignments"
71+
return New-GraphGetRequest -uri $Uri -tenantid $TenantFilter
72+
}

Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,16 @@ function Invoke-CIPPStandardIntuneTemplate {
7272
$RawJSON = $rawJsonFromTemplate
7373
$TemplateType = $Template.Type
7474

75+
$AssignmentsMatch = $null
7576
try {
7677
$ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType
78+
if ($ExistingPolicy -and $Settings.verifyAssignments -eq $true) {
79+
Write-Information "Verifying assignments for tenant $Tenant"
80+
$ExistingAssignments = Get-CIPPIntunePolicyAssignments -PolicyId $ExistingPolicy.id -TemplateType $TemplateType -TenantFilter $Tenant -ExistingPolicy $ExistingPolicy
81+
$AssignmentsMatch = Compare-CIPPIntuneAssignments -ExistingAssignments $ExistingAssignments -ExpectedAssignTo $Settings.AssignTo -ExpectedCustomGroup $Settings.customGroup -ExpectedExcludeGroup $Settings.excludeGroup -ExpectedAssignmentFilter $Settings.assignmentFilter -ExpectedAssignmentFilterType $Settings.assignmentFilterType -TenantFilter $Tenant
82+
83+
Write-Information "AssignmentsMatch for tenant $($Tenant): $AssignmentsMatch"
84+
}
7785
} catch {
7886
$ExistingPolicy = $null
7987
}
@@ -113,6 +121,7 @@ function Invoke-CIPPStandardIntuneTemplate {
113121
customGroup = $Settings.customGroup
114122
assignmentFilter = $Settings.assignmentFilter
115123
assignmentFilterType = $Settings.assignmentFilterType
124+
AssignmentsMatch = $AssignmentsMatch
116125
}
117126

118127
if ($Settings.remediate) {
@@ -146,12 +155,21 @@ function Invoke-CIPPStandardIntuneTemplate {
146155
}
147156

148157
if ($Settings.alert) {
149-
$AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId
150-
if ($CompareResult.compare) {
151-
Write-StandardsAlert -message "Template $($CompareResult.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId
152-
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) does not match the expected configuration. We've generated an alert" -sev info
158+
$AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId, AssignmentsMatch
159+
$AssignmentsDiffer = $Settings.verifyAssignments -and ($null -ne $CompareResult.AssignmentsMatch -and -not $CompareResult.AssignmentsMatch)
160+
$HasDifference = $CompareResult.compare -or $AssignmentsDiffer
161+
if ($HasDifference) {
162+
$Message = if ($CompareResult.compare) {
163+
"Template $($CompareResult.displayname) does not match the expected configuration."
164+
} elseif ($AssignmentsDiffer) {
165+
"Template $($CompareResult.displayname) has incorrect assignments."
166+
} else {
167+
"Template $($CompareResult.displayname) does not match the expected configuration."
168+
}
169+
Write-StandardsAlert -message $Message -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId
170+
Write-LogMessage -API 'Standards' -tenant $Tenant -message "$Message We've generated an alert" -sev info
153171
} else {
154-
if ($CompareResult.ExistingPolicyId) {
172+
if ($CompareResult.existingPolicyId) {
155173
Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) has the correct configuration." -sev Info
156174
} else {
157175
Write-StandardsAlert -message "Template $($CompareResult.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId
@@ -173,6 +191,11 @@ function Invoke-CIPPStandardIntuneTemplate {
173191
description = $CompareResult.description
174192
isCompliant = $true
175193
}
194+
195+
if ($Settings.verifyAssignments) {
196+
$CurrentValue['isAssigned'] = if ($null -ne $CompareResult.AssignmentsMatch) { $CompareResult.AssignmentsMatch } else { $false }
197+
$ExpectedValue['isAssigned'] = $true
198+
}
176199
Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant
177200
#Add-CIPPBPAField -FieldName "policy-$id" -FieldValue $Compare -StoreAs bool -Tenant $tenant
178201
}

host.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
"functionTimeout": "00:10:00",
1111
"extensions": {
1212
"durableTask": {
13-
"maxConcurrentActivityFunctions": 20,
14-
"maxConcurrentOrchestratorFunctions": 5,
13+
"maxConcurrentActivityFunctions": 5,
14+
"maxConcurrentOrchestratorFunctions": 1,
1515
"tracing": {
1616
"distributedTracingEnabled": false,
1717
"version": "None"

0 commit comments

Comments
 (0)