From b8e1db9968da551586d90fda7527455c730ded16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Wed, 29 Jan 2025 20:43:56 +0100 Subject: [PATCH 1/5] Refactor Invoke-ExecSyncAPDevices to improve error handling and logging --- .../Entrypoints/Invoke-ExecSyncAPDevices.ps1 | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSyncAPDevices.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSyncAPDevices.ps1 index 98dae4f0f308..40b0a18263e2 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSyncAPDevices.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecSyncAPDevices.ps1 @@ -10,20 +10,27 @@ Function Invoke-ExecSyncAPDevices { [CmdletBinding()] param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' - $tenantfilter = $Request.Query.TenantFilter + $ExecutingUser = $request.headers.'x-ms-client-principal' + $TenantFilter = $Request.Body.tenantFilter ?? $Request.Query.tenantFilter + Write-LogMessage -user $ExecutingUser -API $APINAME -message 'Accessed this API' -Sev Debug + try { - New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotSettings/sync' -tenantid $TenantFilter + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/windowsAutopilotSettings/sync' -tenantid $TenantFilter $Results = "Successfully Started Sync for $($TenantFilter)" + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant $TenantFilter -message 'Successfully started Autopilot sync' -Sev Info + $StatusCode = [HttpStatusCode]::OK } catch { - $Results = "Failed to start sync for $tenantfilter. Did you try syncing in the last 10 minutes?" + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to start sync for $TenantFilter. Did you try syncing in the last 10 minutes?" + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant $TenantFilter -message 'Failed to start Autopilot sync. Did you try syncing in the last 10 minutes?' -Sev Error -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::Forbidden } - $Results = [pscustomobject]@{'Results' = "$results" } + $Results = [pscustomobject]@{'Results' = "$Results" } # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK + StatusCode = $StatusCode Body = $Results }) From 236962530c363ff66342b7d58b6ec9a249ddb4ac Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 30 Jan 2025 11:33:35 +0100 Subject: [PATCH 2/5] fix: fixed Add Team --- .../HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 index 94647e739f8b..952f051c681e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 @@ -18,7 +18,7 @@ Function Invoke-AddTeam { # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' - $Owners = ($userobj.owner).value + $Owners = ($userobj.owner) try { $Owners = $Owners | ForEach-Object { From 7b72786ca116f4147d60b5f2bf6d498a8a42f1b6 Mon Sep 17 00:00:00 2001 From: Esco Date: Thu, 30 Jan 2025 11:38:58 +0100 Subject: [PATCH 3/5] added throw message if no owner is defined --- .../HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 index 952f051c681e..ffeb0da53459 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-AddTeam.ps1 @@ -20,7 +20,9 @@ Function Invoke-AddTeam { $Owners = ($userobj.owner) try { - + if ($null -eq $Owners) { + throw "You have to add at least one owner to the team" + } $Owners = $Owners | ForEach-Object { $OwnerID = "https://graph.microsoft.com/beta/users('$($_)')" @{ From bdd3794cb5ab85dee7dfd540e4f569eb91dab65b Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:04:06 +0100 Subject: [PATCH 4/5] fix sstandards v1 --- .../Convert-SingleStandardObject.ps1 | 38 +++++ .../ConvertTo-CippStandardObject.ps1 | 44 ----- .../Public/Standards/Get-CIPPStandards.ps1 | 161 ------------------ .../Public/Standards/Merge-CippStandards.ps1 | 40 ++--- 4 files changed, 52 insertions(+), 231 deletions(-) create mode 100644 Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 diff --git a/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 new file mode 100644 index 000000000000..3845d51b51b8 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 @@ -0,0 +1,38 @@ +function Convert-SingleStandardObject { + + param( + [Parameter(Mandatory = $true)] + $Obj + ) + $Obj = [pscustomobject]$Obj + $AllActionValues = @() + if ($Obj.PSObject.Properties.Name -contains 'combinedActions') { + $AllActionValues = $Obj.combinedActions + $null = $Obj.PSObject.Properties.Remove('combinedActions') + } elseif ($Obj.PSObject.Properties.Name -contains 'action') { + if ($Obj.action -and $Obj.action.value) { + $AllActionValues = $Obj.action.value + } + $null = $Obj.PSObject.Properties.Remove('action') + } + + # Convert actions to booleans + $Obj | Add-Member -NotePropertyName 'remediate' -NotePropertyValue ($AllActionValues -contains 'Remediate') -Force + $Obj | Add-Member -NotePropertyName 'alert' -NotePropertyValue ($AllActionValues -contains 'warn') -Force + $Obj | Add-Member -NotePropertyName 'report' -NotePropertyValue ($AllActionValues -contains 'Report') -Force + + # Flatten "standards" if present + if ($Obj.PSObject.Properties.Name -contains 'standards' -and $Obj.standards) { + foreach ($standardKey in $Obj.standards.PSObject.Properties.Name) { + $NestedStandard = $Obj.standards.$standardKey + if ($NestedStandard) { + foreach ($nsProp in $NestedStandard.PSObject.Properties) { + $Obj | Add-Member -NotePropertyName $nsProp.Name -NotePropertyValue $nsProp.Value -Force + } + } + } + $null = $Obj.PSObject.Properties.Remove('standards') + } + + return $Obj +} diff --git a/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 b/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 index ee3e5b680072..852b99691287 100644 --- a/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 +++ b/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 @@ -3,9 +3,6 @@ function ConvertTo-CippStandardObject { [Parameter(Mandatory = $true)] $StandardObject ) - - # If $StandardObject is an array (like for ConditionalAccessTemplate or IntuneTemplate), - # we need to process each item individually. if ($StandardObject -is [System.Collections.IEnumerable] -and -not ($StandardObject -is [string])) { $ProcessedItems = New-Object System.Collections.ArrayList foreach ($Item in $StandardObject) { @@ -13,47 +10,6 @@ function ConvertTo-CippStandardObject { } return [System.Collections.ArrayList]$ProcessedItems } else { - # Single object scenario return Convert-SingleStandardObject $StandardObject } } - -function Convert-SingleStandardObject { - param( - [Parameter(Mandatory = $true)] - $Obj - ) - - $Obj = [pscustomobject]$Obj - - $AllActionValues = @() - if ($Obj.PSObject.Properties.Name -contains 'combinedActions') { - $AllActionValues = $Obj.combinedActions - $null = $Obj.PSObject.Properties.Remove('combinedActions') - } elseif ($Obj.PSObject.Properties.Name -contains 'action') { - if ($Obj.action -and $Obj.action.value) { - $AllActionValues = $Obj.action.value - } - $null = $Obj.PSObject.Properties.Remove('action') - } - - # Convert actions to booleans - $Obj | Add-Member -NotePropertyName 'remediate' -NotePropertyValue ($AllActionValues -contains 'Remediate') -Force - $Obj | Add-Member -NotePropertyName 'alert' -NotePropertyValue ($AllActionValues -contains 'warn') -Force - $Obj | Add-Member -NotePropertyName 'report' -NotePropertyValue ($AllActionValues -contains 'Report') -Force - - # Flatten standards if present - if ($Obj.PSObject.Properties.Name -contains 'standards' -and $Obj.standards) { - foreach ($standardKey in $Obj.standards.PSObject.Properties.Name) { - $NestedStandard = $Obj.standards.$standardKey - if ($NestedStandard) { - foreach ($nsProp in $NestedStandard.PSObject.Properties) { - $Obj | Add-Member -NotePropertyName $nsProp.Name -NotePropertyValue $nsProp.Value -Force - } - } - } - $null = $Obj.PSObject.Properties.Remove('standards') - } - - return $Obj -} diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index c78f71172ba5..e69de29bb2d1 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -1,161 +0,0 @@ -function Get-CIPPStandards { - param( - [Parameter(Mandatory = $false)] - [string]$TenantFilter = 'allTenants', - [Parameter(Mandatory = $false)] - [switch]$ListAllTenants, - [Parameter(Mandatory = $false)] - $TemplateId = '*', - [Parameter(Mandatory = $false)] - $runManually = $false - ) - - $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'StandardsTemplateV2'" - $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON | ForEach-Object { - try { - $JSON = ($_).replace('"Action":', '"action":') #fix cap mistake of antique standards - ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue - } catch { - } - } | Where-Object { - $_.GUID -like $TemplateId -and $_.runManually -eq $runManually - } - - $AllTenantsList = Get-Tenants - if ($TenantFilter -ne 'allTenants') { - $AllTenantsList = $AllTenantsList | Where-Object { - $_.defaultDomainName -eq $TenantFilter -or $_.customerId -eq $TenantFilter - } - } - - if ($ListAllTenants.IsPresent) { - $AllTenantsTemplates = $Templates | Where-Object { - $_.tenantFilter.value -contains 'AllTenants' - } - - $ComputedStandards = [ordered]@{} - - foreach ($Template in $AllTenantsTemplates) { - $Standards = $Template.standards - foreach ($StandardName in $Standards.PSObject.Properties.Name) { - $CurrentStandard = $Standards.$StandardName.PSObject.Copy() - $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force - - $Actions = $CurrentStandard.action.value - if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { - if (-not $ComputedStandards.Contains($StandardName)) { - $ComputedStandards[$StandardName] = $CurrentStandard - } else { - $MergedStandard = Merge-CippStandards $ComputedStandards[$StandardName] $CurrentStandard - $MergedStandard.TemplateId = $CurrentStandard.TemplateId - $ComputedStandards[$StandardName] = $MergedStandard - } - } - } - } - - foreach ($Standard in $ComputedStandards.Keys) { - $TempCopy = $ComputedStandards[$Standard].PSObject.Copy() - $TempCopy.TemplateId ? $TempCopy.PSObject.Properties.Remove('TemplateId') : $null - - $Normalized = ConvertTo-CippStandardObject $TempCopy - - [pscustomobject]@{ - Tenant = 'AllTenants' - Standard = $Standard - Settings = $Normalized - TemplateId = $ComputedStandards[$Standard].TemplateId - } - } - - } else { - foreach ($Tenant in $AllTenantsList) { - $TenantName = $Tenant.defaultDomainName - - $ApplicableTemplates = $Templates | ForEach-Object { - $template = $_ - $tenantFilterValues = $template.tenantFilter | ForEach-Object { $_.value } - $excludedTenantValues = @() - if ($template.excludedTenants) { - $excludedTenantValues = $template.excludedTenants | ForEach-Object { $_.value } - } - - $AllTenantsApplicable = $false - $TenantSpecificApplicable = $false - - if ($tenantFilterValues -contains 'AllTenants' -and (-not ($excludedTenantValues -contains $TenantName))) { - $AllTenantsApplicable = $true - } - if ($tenantFilterValues -contains $TenantName) { - $TenantSpecificApplicable = $true - } - - if ($AllTenantsApplicable -or $TenantSpecificApplicable) { - $template - } - } - - $AllTenantTemplatesSet = $ApplicableTemplates | Where-Object { - $_.tenantFilter.value -contains 'AllTenants' - } - $TenantSpecificTemplatesSet = $ApplicableTemplates | Where-Object { - $_.tenantFilter.value -notcontains 'AllTenants' - } - - $ComputedStandards = [ordered]@{} - - foreach ($Template in $AllTenantTemplatesSet) { - $Standards = $Template.standards - foreach ($StandardName in $Standards.PSObject.Properties.Name) { - $CurrentStandard = $Standards.$StandardName.PSObject.Copy() - $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force - - $Actions = $CurrentStandard.action.value - if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { - if (-not $ComputedStandards.Contains($StandardName)) { - $ComputedStandards[$StandardName] = $CurrentStandard - } else { - $MergedStandard = Merge-CippStandards $ComputedStandards[$StandardName] $CurrentStandard - $MergedStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $CurrentStandard.TemplateId -Force - $ComputedStandards[$StandardName] = $MergedStandard - } - } - } - } - - foreach ($Template in $TenantSpecificTemplatesSet) { - $Standards = $Template.standards - foreach ($StandardName in $Standards.PSObject.Properties.Name) { - $CurrentStandard = $Standards.$StandardName.PSObject.Copy() - $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force - - $Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'report' } - if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { - if (-not $ComputedStandards.Contains($StandardName)) { - $ComputedStandards[$StandardName] = $CurrentStandard - } else { - $MergedStandard = Merge-CippStandards $ComputedStandards[$StandardName] $CurrentStandard - $MergedStandard.TemplateId = $CurrentStandard.TemplateId - $ComputedStandards[$StandardName] = $MergedStandard - } - } - } - } - - foreach ($Standard in $ComputedStandards.Keys) { - $TempCopy = $ComputedStandards[$Standard].PSObject.Copy() - $TempCopy.PSObject.Properties.Remove('TemplateId') - - $Normalized = ConvertTo-CippStandardObject $TempCopy - - [pscustomobject]@{ - Tenant = $TenantName - Standard = $Standard - Settings = $Normalized - TemplateId = $ComputedStandards[$Standard].TemplateId - } - } - } - } -} diff --git a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 index abd8f21ab319..2478cf91fd4a 100644 --- a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 @@ -1,34 +1,22 @@ - function Merge-CippStandards { param( - [Parameter(Mandatory = $true)] $Existing, - [Parameter(Mandatory = $true)] $CurrentStandard + [Parameter(Mandatory = $true)] + [object]$Existing, + [Parameter(Mandatory = $true)] + [object]$New ) - $Existing = [pscustomobject]$Existing - $CurrentStandard = [pscustomobject]$CurrentStandard - $ExistingActionValues = @() - if ($Existing.PSObject.Properties.Name -contains 'action') { - if ($Existing.action -and $Existing.action.value) { - $ExistingActionValues = @($Existing.action.value) - } - $null = $Existing.PSObject.Properties.Remove('action') - } - $CurrentActionValues = @() - if ($CurrentStandard.PSObject.Properties.Name -contains 'action') { - if ($CurrentStandard.action -and $CurrentStandard.action.value) { - $CurrentActionValues = @($CurrentStandard.action.value) - } - $null = $CurrentStandard.PSObject.Properties.Remove('action') + if (-not $Existing) { + return $New } - $AllActionValues = ($ExistingActionValues + $CurrentActionValues) | Select-Object -Unique - foreach ($prop in $CurrentStandard.PSObject.Properties) { - if ($prop.Name -eq 'action') { continue } - $Existing | Add-Member -NotePropertyName $prop.Name -NotePropertyValue $prop.Value -Force + $ExistingIsArray = ($Existing -is [System.Collections.IEnumerable] -and -not ($Existing -is [string])) + $NewIsArray = ($New -is [System.Collections.IEnumerable] -and -not ($New -is [string])) + + if (-not $ExistingIsArray) { + $Existing = @($Existing) } - if ($AllActionValues.Count -gt 0) { - $Existing | Add-Member -NotePropertyName 'combinedActions' -NotePropertyValue $AllActionValues -Force + if (-not $NewIsArray) { + $New = @($New) } - - return $Existing + return $Existing + $New } From 2b4e7c4b81be870789efa55c5d88e335af4653e1 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar <49186168+KelvinTegelaar@users.noreply.github.com> Date: Fri, 31 Jan 2025 18:39:05 +0100 Subject: [PATCH 5/5] changes to standards --- .../Convert-SingleStandardObject.ps1 | 13 +- .../ConvertTo-CippStandardObject.ps1 | 5 +- .../Public/Standards/Get-CIPPStandards.ps1 | 264 ++++++++++++++++++ .../Public/Standards/Merge-CippStandards.ps1 | 34 ++- 4 files changed, 295 insertions(+), 21 deletions(-) diff --git a/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 index 3845d51b51b8..effeb5d27866 100644 --- a/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 +++ b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 @@ -1,22 +1,25 @@ function Convert-SingleStandardObject { - param( [Parameter(Mandatory = $true)] $Obj ) + + # Ensure we have a PSCustomObject we can modify $Obj = [pscustomobject]$Obj + + # Extract action arrays $AllActionValues = @() if ($Obj.PSObject.Properties.Name -contains 'combinedActions') { $AllActionValues = $Obj.combinedActions - $null = $Obj.PSObject.Properties.Remove('combinedActions') + $Obj.PSObject.Properties.Remove('combinedActions') | Out-Null } elseif ($Obj.PSObject.Properties.Name -contains 'action') { if ($Obj.action -and $Obj.action.value) { $AllActionValues = $Obj.action.value } - $null = $Obj.PSObject.Properties.Remove('action') + $Obj.PSObject.Properties.Remove('action') | Out-Null } - # Convert actions to booleans + # Convert to booleans $Obj | Add-Member -NotePropertyName 'remediate' -NotePropertyValue ($AllActionValues -contains 'Remediate') -Force $Obj | Add-Member -NotePropertyName 'alert' -NotePropertyValue ($AllActionValues -contains 'warn') -Force $Obj | Add-Member -NotePropertyName 'report' -NotePropertyValue ($AllActionValues -contains 'Report') -Force @@ -31,7 +34,7 @@ function Convert-SingleStandardObject { } } } - $null = $Obj.PSObject.Properties.Remove('standards') + $Obj.PSObject.Properties.Remove('standards') | Out-Null } return $Obj diff --git a/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 b/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 index 852b99691287..e2cfe2735653 100644 --- a/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 +++ b/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 @@ -1,15 +1,18 @@ function ConvertTo-CippStandardObject { + param( [Parameter(Mandatory = $true)] $StandardObject ) + # If it's an array of items, process each item if ($StandardObject -is [System.Collections.IEnumerable] -and -not ($StandardObject -is [string])) { $ProcessedItems = New-Object System.Collections.ArrayList foreach ($Item in $StandardObject) { $ProcessedItems.Add((Convert-SingleStandardObject $Item)) | Out-Null } - return [System.Collections.ArrayList]$ProcessedItems + return $ProcessedItems } else { + # Single object return Convert-SingleStandardObject $StandardObject } } diff --git a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 index e69de29bb2d1..53417bacbeab 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -0,0 +1,264 @@ +function Get-CIPPStandards { + param( + [Parameter(Mandatory = $false)] + [string]$TenantFilter = 'allTenants', + + [Parameter(Mandatory = $false)] + [switch]$ListAllTenants, + + [Parameter(Mandatory = $false)] + $TemplateId = '*', + + [Parameter(Mandatory = $false)] + $runManually = $false + ) + + # 1. Get all JSON-based templates from the "templates" table + $Table = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'StandardsTemplateV2'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON | + ForEach-Object { + try { + # Fix old "Action" => "action" + $JSON = $_ -replace '"Action":', '"action":' + ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue + } catch {} + } | + Where-Object { + $_.GUID -like $TemplateId -and $_.runManually -eq $runManually + } + + # 2. Get tenant list, filter if needed + $AllTenantsList = Get-Tenants + if ($TenantFilter -ne 'allTenants') { + $AllTenantsList = $AllTenantsList | Where-Object { + $_.defaultDomainName -eq $TenantFilter -or $_.customerId -eq $TenantFilter + } + } + + # 3. If -ListAllTenants, build standards for "AllTenants" only + if ($ListAllTenants.IsPresent) { + $AllTenantsTemplates = $Templates | Where-Object { + $_.tenantFilter.value -contains 'AllTenants' + } + + $ComputedStandards = [ordered]@{} + + foreach ($Template in $AllTenantsTemplates) { + $Standards = $Template.standards + + foreach ($StandardName in $Standards.PSObject.Properties.Name) { + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) + + if ($IsArray) { + # e.g. IntuneTemplate with 2 items + foreach ($Item in $Value) { + $CurrentStandard = $Item.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + $Actions = $CurrentStandard.action.value + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } else { + # single object + $CurrentStandard = $Value.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + $Actions = $CurrentStandard.action.value + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } + } + + # Output result for 'AllTenants' + foreach ($Standard in $ComputedStandards.Keys) { + $TempCopy = $ComputedStandards[$Standard].PSObject.Copy() + + # Remove 'TemplateId' from final output + if ($TempCopy -is [System.Collections.IEnumerable] -and -not ($TempCopy -is [string])) { + foreach ($subItem in $TempCopy) { + $subItem.PSObject.Properties.Remove('TemplateId') | Out-Null + } + } else { + $TempCopy.PSObject.Properties.Remove('TemplateId') | Out-Null + } + + $Normalized = ConvertTo-CippStandardObject $TempCopy + + [pscustomobject]@{ + Tenant = 'AllTenants' + Standard = $Standard + Settings = $Normalized + TemplateId = if ($ComputedStandards[$Standard] -is [System.Collections.IEnumerable] -and -not ($ComputedStandards[$Standard] -is [string])) { + # If multiple items from multiple templates, you may have multiple TemplateIds + $ComputedStandards[$Standard] | ForEach-Object { $_.TemplateId } + } else { + $ComputedStandards[$Standard].TemplateId + } + } + } + } else { + # 4. For each tenant, figure out which templates apply, merge them, and output. + foreach ($Tenant in $AllTenantsList) { + $TenantName = $Tenant.defaultDomainName + + # Determine which templates apply to this tenant + $ApplicableTemplates = $Templates | ForEach-Object { + $template = $_ + $tenantFilterValues = $template.tenantFilter | ForEach-Object { $_.value } + $excludedTenantValues = @() + + if ($template.excludedTenants) { + if ($template.excludedTenants -is [System.Collections.IEnumerable] -and -not ($template.excludedTenants -is [string])) { + $excludedTenantValues = $template.excludedTenants | ForEach-Object { $_.value } + } else { + $excludedTenantValues = @($template.excludedTenants) + } + } + + $AllTenantsApplicable = $false + $TenantSpecificApplicable = $false + + if ($tenantFilterValues -contains 'AllTenants' -and -not ($excludedTenantValues -contains $TenantName)) { + $AllTenantsApplicable = $true + } + if ($tenantFilterValues -contains $TenantName) { + $TenantSpecificApplicable = $true + } + + if ($AllTenantsApplicable -or $TenantSpecificApplicable) { + $template + } + } + + # Separate them into AllTenant vs. TenantSpecific sets + $AllTenantTemplatesSet = $ApplicableTemplates | Where-Object { + $_.tenantFilter.value -contains 'AllTenants' + } + $TenantSpecificTemplatesSet = $ApplicableTemplates | Where-Object { + $_.tenantFilter.value -notcontains 'AllTenants' + } + + $ComputedStandards = [ordered]@{} + + # 4a. Merge the AllTenantTemplatesSet + foreach ($Template in $AllTenantTemplatesSet) { + $Standards = $Template.standards + + foreach ($StandardName in $Standards.PSObject.Properties.Name) { + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) + + if ($IsArray) { + foreach ($Item in $Value) { + $CurrentStandard = $Item.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + $Actions = $CurrentStandard.action.value + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } else { + $CurrentStandard = $Value.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + $Actions = $CurrentStandard.action.value + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } + } + + # 4b. Merge the TenantSpecificTemplatesSet + foreach ($Template in $TenantSpecificTemplatesSet) { + $Standards = $Template.standards + + foreach ($StandardName in $Standards.PSObject.Properties.Name) { + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) + + if ($IsArray) { + foreach ($Item in $Value) { + $CurrentStandard = $Item.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + # Filter actions only 'Remediate','warn','Report' + $Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' } + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } else { + $CurrentStandard = $Value.PSObject.Copy() + $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + + $Actions = $CurrentStandard.action.value | Where-Object { $_ -in 'Remediate', 'warn', 'Report' } + if ($Actions -contains 'Remediate' -or $Actions -contains 'warn' -or $Actions -contains 'Report') { + if (-not $ComputedStandards.Contains($StandardName)) { + $ComputedStandards[$StandardName] = $CurrentStandard + } else { + $MergedStandard = Merge-CippStandards -Existing $ComputedStandards[$StandardName] -New $CurrentStandard -StandardName $StandardName + $ComputedStandards[$StandardName] = $MergedStandard + } + } + } + } + } + + # 4c. Output each final standard for this tenant + foreach ($Standard in $ComputedStandards.Keys) { + $TempCopy = $ComputedStandards[$Standard].PSObject.Copy() + # Remove local 'TemplateId' from final object(s) + if ($TempCopy -is [System.Collections.IEnumerable] -and -not ($TempCopy -is [string])) { + foreach ($subItem in $TempCopy) { + $subItem.PSObject.Properties.Remove('TemplateId') | Out-Null + } + } else { + $TempCopy.PSObject.Properties.Remove('TemplateId') | Out-Null + } + + $Normalized = ConvertTo-CippStandardObject $TempCopy + + [pscustomobject]@{ + Tenant = $TenantName + Standard = $Standard + Settings = $Normalized + TemplateId = $ComputedStandards[$Standard].TemplateId + } + } + } + } +} + diff --git a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 index 2478cf91fd4a..dcea014c0def 100644 --- a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 @@ -1,22 +1,26 @@ function Merge-CippStandards { param( - [Parameter(Mandatory = $true)] - [object]$Existing, - [Parameter(Mandatory = $true)] - [object]$New + [Parameter(Mandatory = $true)][object]$Existing, + [Parameter(Mandatory = $true)][object]$New, + [Parameter(Mandatory = $true)][string]$StandardName ) - if (-not $Existing) { - return $New - } - $ExistingIsArray = ($Existing -is [System.Collections.IEnumerable] -and -not ($Existing -is [string])) - $NewIsArray = ($New -is [System.Collections.IEnumerable] -and -not ($New -is [string])) + # If $Existing or $New is $null/empty, just return the other. + if (-not $Existing) { return $New } + if (-not $New) { return $Existing } - if (-not $ExistingIsArray) { - $Existing = @($Existing) - } - if (-not $NewIsArray) { - $New = @($New) + # If the standard name ends with 'Template', we treat them as arrays to merge. + if ($StandardName -like '*Template') { + $ExistingIsArray = $Existing -is [System.Collections.IEnumerable] -and -not ($Existing -is [string]) + $NewIsArray = $New -is [System.Collections.IEnumerable] -and -not ($New -is [string]) + + # Make sure both are arrays + if (-not $ExistingIsArray) { $Existing = @($Existing) } + if (-not $NewIsArray) { $New = @($New) } + + return $Existing + $New + } else { + # Single‐value standard: override the old with the new + return $New } - return $Existing + $New }