Skip to content

Commit b6c5c74

Browse files
committed
Refactor standards orchestration to batch activities
Introduces Push-CIPPStandardsApplyBatch and Push-CIPPStandardsList entrypoints to support distributed, per-tenant standards listing and aggregation. Refactors Invoke-CIPPStandardsRun to build and orchestrate tenant batches, and moves license and policy timestamp filtering logic from Get-CIPPStandards to Push-CIPPStandardsList for improved scalability and separation of concerns.
1 parent 2766531 commit b6c5c74

File tree

4 files changed

+275
-166
lines changed

4 files changed

+275
-166
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
function Push-CIPPStandardsApplyBatch {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
param($Item)
7+
8+
try {
9+
# Aggregate all standards from all tenants
10+
$AllStandards = @($Item.Results | Where-Object { $_ -and $_.FunctionName -eq 'CIPPStandard' })
11+
12+
if ($AllStandards.Count -eq 0) {
13+
Write-Information 'No standards to apply across all tenants'
14+
return
15+
}
16+
17+
Write-Information "Aggregated $($AllStandards.Count) standards from all tenants"
18+
19+
# Start orchestrator to apply standards
20+
$InputObject = [PSCustomObject]@{
21+
OrchestratorName = 'StandardsApply'
22+
Batch = @($AllStandards)
23+
SkipLog = $true
24+
}
25+
26+
$InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
27+
Write-Information "Started standards apply orchestrator with ID = '$InstanceId'"
28+
29+
} catch {
30+
Write-Warning "Error in standards apply batch aggregation: $($_.Exception.Message)"
31+
}
32+
return @{
33+
Success = $true
34+
}
35+
}
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
function Push-CIPPStandardsList {
2+
<#
3+
.FUNCTIONALITY
4+
Entrypoint
5+
#>
6+
param($Item)
7+
8+
$TenantFilter = $Item.TenantFilter
9+
$runManually = $Item.runManually
10+
$TemplateId = $Item.TemplateId
11+
12+
try {
13+
# Get standards for this tenant
14+
$GetStandardParams = @{
15+
TenantFilter = $TenantFilter
16+
runManually = $runManually
17+
}
18+
if ($TemplateId) {
19+
$GetStandardParams['TemplateId'] = $TemplateId
20+
}
21+
22+
$AllStandards = Get-CIPPStandards @GetStandardParams
23+
24+
if ($AllStandards.Count -eq 0) {
25+
Write-Information "No standards found for tenant $TenantFilter"
26+
return @()
27+
}
28+
29+
# Build hashtable for efficient lookup
30+
$ComputedStandards = @{}
31+
foreach ($Standard in $AllStandards) {
32+
$Key = "$($Standard.Standard)|$($Standard.Settings.TemplateList.value)"
33+
$ComputedStandards[$Key] = $Standard
34+
}
35+
36+
# Check if IntuneTemplate standards are present
37+
$IntuneTemplateFound = ($ComputedStandards.Keys.Where({ $_ -like '*IntuneTemplate*' }, 'First').Count -gt 0)
38+
39+
if ($IntuneTemplateFound) {
40+
# Perform license check
41+
$TestResult = Test-CIPPStandardLicense -StandardName 'IntuneTemplate_general' -TenantFilter $TenantFilter -RequiredCapabilities @('INTUNE_A', 'MDM_Services', 'EMS', 'SCCM', 'MICROSOFTINTUNEPLAN1')
42+
43+
if (-not $TestResult) {
44+
# Remove IntuneTemplate standards and set compare fields
45+
$IntuneKeys = @($ComputedStandards.Keys | Where-Object { $_ -like '*IntuneTemplate*' })
46+
$BulkFields = [System.Collections.Generic.List[object]]::new()
47+
48+
foreach ($Key in $IntuneKeys) {
49+
$TemplateKey = ($Key -split '\|', 2)[1]
50+
if ($TemplateKey) {
51+
$BulkFields.Add([PSCustomObject]@{
52+
FieldName = "standards.IntuneTemplate.$TemplateKey"
53+
FieldValue = 'This tenant does not have the required license for this standard.'
54+
})
55+
}
56+
[void]$ComputedStandards.Remove($Key)
57+
}
58+
59+
if ($BulkFields.Count -gt 0) {
60+
Set-CIPPStandardsCompareField -TenantFilter $TenantFilter -BulkFields $BulkFields
61+
}
62+
63+
Write-Information "Removed IntuneTemplate standards for $TenantFilter - missing required license"
64+
} else {
65+
# License valid - check policy timestamps to filter unchanged templates
66+
$TypeMap = @{
67+
Device = 'deviceManagement/deviceConfigurations'
68+
Catalog = 'deviceManagement/configurationPolicies'
69+
Admin = 'deviceManagement/groupPolicyConfigurations'
70+
deviceCompliancePolicies = 'deviceManagement/deviceCompliancePolicies'
71+
AppProtection_Android = 'deviceAppManagement/androidManagedAppProtections'
72+
AppProtection_iOS = 'deviceAppManagement/iosManagedAppProtections'
73+
}
74+
75+
$BulkRequests = $TypeMap.GetEnumerator() | ForEach-Object {
76+
@{
77+
id = $_.Key
78+
url = "$($_.Value)?`$orderby=lastModifiedDateTime desc&`$select=id,lastModifiedDateTime&`$top=999"
79+
method = 'GET'
80+
}
81+
}
82+
83+
try {
84+
$TrackingTable = Get-CippTable -tablename 'IntunePolicyTypeTracking'
85+
$BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter -NoPaginateIds @($BulkRequests.id)
86+
$PolicyTimestamps = @{}
87+
88+
foreach ($Result in $BulkResults) {
89+
$GraphTime = $Result.body.value[0].lastModifiedDateTime
90+
$GraphId = $Result.body.value[0].id
91+
$GraphCount = ($Result.body.value | Measure-Object).Count
92+
$Cached = Get-CIPPAzDataTableEntity @TrackingTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$($Result.id)'"
93+
94+
$CountChanged = $false
95+
if ($Cached -and $Cached.PolicyCount -ne $null) {
96+
$CountChanged = ($GraphCount -ne $Cached.PolicyCount)
97+
}
98+
99+
$IdChanged = $false
100+
if ($GraphId -and $Cached -and $Cached.LatestPolicyId) {
101+
$IdChanged = ($GraphId -ne $Cached.LatestPolicyId)
102+
}
103+
104+
if ($GraphTime) {
105+
$GraphTimeUtc = ([DateTime]$GraphTime).ToUniversalTime()
106+
if ($Cached -and $Cached.LatestPolicyModified -and -not $IdChanged -and -not $CountChanged) {
107+
$CachedTimeUtc = ([DateTimeOffset]$Cached.LatestPolicyModified).UtcDateTime
108+
$TimeDiff = [Math]::Abs(($GraphTimeUtc - $CachedTimeUtc).TotalSeconds)
109+
$Changed = ($TimeDiff -gt 60)
110+
} else {
111+
$Changed = $true
112+
}
113+
Add-CIPPAzDataTableEntity @TrackingTable -Entity @{
114+
PartitionKey = $TenantFilter
115+
RowKey = $Result.id
116+
LatestPolicyModified = $GraphTime
117+
LatestPolicyId = $GraphId
118+
PolicyCount = $GraphCount
119+
} -Force | Out-Null
120+
} else {
121+
$Changed = $true
122+
}
123+
124+
$PolicyTimestamps[$Result.id] = $Changed
125+
}
126+
127+
# Filter unchanged templates
128+
$TemplateTable = Get-CippTable -tablename 'templates'
129+
$StandardTemplateTable = Get-CippTable -tablename 'templates'
130+
$IntuneKeys = @($ComputedStandards.Keys | Where-Object { $_ -like '*IntuneTemplate*' })
131+
132+
foreach ($Key in $IntuneKeys) {
133+
$Template = $ComputedStandards[$Key]
134+
$TemplateEntity = Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq 'IntuneTemplate' and RowKey eq '$($Template.Settings.TemplateList.value)'"
135+
136+
if (-not $TemplateEntity) { continue }
137+
138+
$ParsedTemplate = $TemplateEntity.JSON | ConvertFrom-Json
139+
if (-not $ParsedTemplate.Type) { continue }
140+
141+
$PolicyType = $ParsedTemplate.Type
142+
$PolicyChanged = if ($PolicyType -eq 'AppProtection') {
143+
[bool]($PolicyTimestamps['AppProtection_Android'] -or $PolicyTimestamps['AppProtection_iOS'])
144+
} else {
145+
[bool]$PolicyTimestamps[$PolicyType]
146+
}
147+
148+
# Check StandardTemplate changes
149+
$StandardTemplate = Get-CIPPAzDataTableEntity @StandardTemplateTable -Filter "PartitionKey eq 'StandardsTemplateV2' and RowKey eq '$($Template.TemplateId)'"
150+
$StandardTemplateChanged = $false
151+
152+
if ($StandardTemplate) {
153+
$StandardTimeUtc = ([DateTimeOffset]$StandardTemplate.Timestamp).UtcDateTime
154+
$CachedStandardTemplate = Get-CIPPAzDataTableEntity @TrackingTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq 'StandardTemplate_$($Template.TemplateId)'"
155+
156+
if ($CachedStandardTemplate -and $CachedStandardTemplate.CachedTimestamp) {
157+
$CachedStandardTimeUtc = ([DateTimeOffset]$CachedStandardTemplate.CachedTimestamp).UtcDateTime
158+
$TimeDiff = [Math]::Abs(($StandardTimeUtc - $CachedStandardTimeUtc).TotalSeconds)
159+
$StandardTemplateChanged = ($TimeDiff -gt 60)
160+
} else {
161+
$StandardTemplateChanged = $true
162+
}
163+
164+
Add-CIPPAzDataTableEntity @TrackingTable -Entity @{
165+
PartitionKey = $TenantFilter
166+
RowKey = "StandardTemplate_$($Template.TemplateId)"
167+
CachedTimestamp = $StandardTemplate.Timestamp
168+
} -Force | Out-Null
169+
}
170+
171+
# Remove if both unchanged
172+
if (-not $PolicyChanged -and -not $StandardTemplateChanged) {
173+
[void]$ComputedStandards.Remove($Key)
174+
}
175+
}
176+
} catch {
177+
Write-Warning "Timestamp check failed for $TenantFilter : $($_.Exception.Message)"
178+
}
179+
}
180+
}
181+
182+
# Return filtered standards
183+
$ComputedStandards.Values | ForEach-Object {
184+
[PSCustomObject]@{
185+
Tenant = $_.Tenant
186+
Standard = $_.Standard
187+
Settings = $_.Settings
188+
TemplateId = $_.TemplateId
189+
FunctionName = 'CIPPStandard'
190+
}
191+
}
192+
193+
} catch {
194+
Write-Warning "Error listing standards for $TenantFilter : $($_.Exception.Message)"
195+
return @()
196+
}
197+
}

Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-CIPPStandardsRun.ps1

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,49 +48,52 @@ function Invoke-CIPPStandardsRun {
4848
} else {
4949
Write-Information 'Classic Standards Run'
5050

51-
$GetStandardParams = @{
52-
TenantFilter = $TenantFilter
53-
runManually = $runManually
54-
LicenseChecks = $true
55-
}
56-
57-
if ($TemplateID) {
58-
$GetStandardParams['TemplateId'] = $TemplateID
59-
}
60-
61-
$AllTasks = Get-CIPPStandards @GetStandardParams
62-
6351
if ($Force.IsPresent) {
6452
Write-Information 'Clearing Rerun Cache'
6553
Test-CIPPRerun -ClearAll -TenantFilter $TenantFilter -Type 'Standard'
6654
}
6755

68-
if ($AllTasks.Count -eq 0) {
69-
Write-Information "No standards found for tenant $($TenantFilter)."
56+
# Get tenant list for batch processing
57+
$AllTenantsList = if ($TenantFilter -eq 'allTenants') {
58+
Get-Tenants
59+
} else {
60+
Get-Tenants | Where-Object {
61+
$_.defaultDomainName -eq $TenantFilter -or $_.customerId -eq $TenantFilter
62+
}
63+
}
64+
65+
if ($AllTenantsList.Count -eq 0) {
66+
Write-Information "No tenants found for filter $TenantFilter"
7067
return
7168
}
7269

73-
#For each item in our object, run the queue.
74-
$Queue = New-CippQueueEntry -Name "Applying Standards ($TenantFilter)" -TotalTasks ($AllTasks | Measure-Object).Count
70+
# Build batch of per-tenant list activities
71+
$Batch = foreach ($Tenant in $AllTenantsList) {
72+
$BatchItem = @{
73+
FunctionName = 'CIPPStandardsList'
74+
TenantFilter = $Tenant.defaultDomainName
75+
runManually = $runManually
76+
}
77+
if ($TemplateID) {
78+
$BatchItem['TemplateId'] = $TemplateID
79+
}
80+
$BatchItem
81+
}
82+
83+
Write-Information "Built batch of $($Batch.Count) tenant standards list activities"
7584

85+
# Start orchestrator with distributed batch and post-exec aggregation
7686
$InputObject = [PSCustomObject]@{
77-
OrchestratorName = 'StandardsOrchestrator'
78-
QueueFunction = @{
79-
FunctionName = 'GetStandards'
80-
QueueId = $Queue.RowKey
81-
StandardParams = @{
82-
TenantFilter = $TenantFilter
83-
runManually = $runManually
84-
}
87+
OrchestratorName = 'StandardsList'
88+
Batch = @($Batch)
89+
PostExecution = @{
90+
FunctionName = 'CIPPStandardsApplyBatch'
8591
}
8692
SkipLog = $true
8793
}
88-
if ($TemplateID) {
89-
$InputObject.QueueFunction.StandardParams['TemplateId'] = $TemplateID
90-
}
94+
9195
Write-Information "InputObject: $($InputObject | ConvertTo-Json -Depth 5 -Compress)"
9296
$InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress)
93-
Write-Information "Started orchestration with ID = '$InstanceId'"
94-
#$Orchestrator = New-OrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId
97+
Write-Information "Started standards list orchestration with ID = '$InstanceId'"
9598
}
9699
}

0 commit comments

Comments
 (0)