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..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 @@ -18,9 +18,11 @@ 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 { - + 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('$($_)')" @{ 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 }) diff --git a/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 new file mode 100644 index 000000000000..effeb5d27866 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Convert-SingleStandardObject.ps1 @@ -0,0 +1,41 @@ +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 + $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 + } + $Obj.PSObject.Properties.Remove('action') | Out-Null + } + + # 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 + + # 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 + } + } + } + $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 ee3e5b680072..e2cfe2735653 100644 --- a/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 +++ b/Modules/CIPPCore/Public/Standards/ConvertTo-CippStandardObject.ps1 @@ -1,59 +1,18 @@ function ConvertTo-CippStandardObject { + param( [Parameter(Mandatory = $true)] $StandardObject ) - - # If $StandardObject is an array (like for ConditionalAccessTemplate or IntuneTemplate), - # we need to process each item individually. + # 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 scenario + # Single object 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..53417bacbeab 100644 --- a/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Get-CIPPStandards.ps1 @@ -2,26 +2,33 @@ 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 { + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Sort-Object TimeStamp).JSON | + ForEach-Object { try { - $JSON = ($_).replace('"Action":', '"action":') #fix cap mistake of antique standards + # Fix old "Action" => "action" + $JSON = $_ -replace '"Action":', '"action":' ConvertFrom-Json -InputObject $JSON -ErrorAction SilentlyContinue - } catch { - } - } | Where-Object { + } 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 { @@ -29,6 +36,7 @@ function Get-CIPPStandards { } } + # 3. If -ListAllTenants, build standards for "AllTenants" only if ($ListAllTenants.IsPresent) { $AllTenantsTemplates = $Templates | Where-Object { $_.tenantFilter.value -contains 'AllTenants' @@ -38,26 +46,57 @@ function Get-CIPPStandards { 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 + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) - $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 + 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() - $TempCopy.TemplateId ? $TempCopy.PSObject.Properties.Remove('TemplateId') : $null + + # 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 @@ -65,26 +104,37 @@ function Get-CIPPStandards { Tenant = 'AllTenants' Standard = $Standard Settings = $Normalized - TemplateId = $ComputedStandards[$Standard].TemplateId + 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) { - $excludedTenantValues = $template.excludedTenants | ForEach-Object { $_.value } + 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))) { + if ($tenantFilterValues -contains 'AllTenants' -and -not ($excludedTenantValues -contains $TenantName)) { $AllTenantsApplicable = $true } if ($tenantFilterValues -contains $TenantName) { @@ -96,6 +146,7 @@ function Get-CIPPStandards { } } + # Separate them into AllTenant vs. TenantSpecific sets $AllTenantTemplatesSet = $ApplicableTemplates | Where-Object { $_.tenantFilter.value -contains 'AllTenants' } @@ -105,47 +156,98 @@ function Get-CIPPStandards { $ComputedStandards = [ordered]@{} + # 4a. Merge the AllTenantTemplatesSet 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 + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) - $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 + 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) { - $CurrentStandard = $Standards.$StandardName.PSObject.Copy() - $CurrentStandard | Add-Member -NotePropertyName 'TemplateId' -NotePropertyValue $Template.GUID -Force + $Value = $Standards.$StandardName + $IsArray = $Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) - $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 + 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() - $TempCopy.PSObject.Properties.Remove('TemplateId') + # 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 @@ -159,3 +261,4 @@ function Get-CIPPStandards { } } } + diff --git a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 index abd8f21ab319..dcea014c0def 100644 --- a/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 +++ b/Modules/CIPPCore/Public/Standards/Merge-CippStandards.ps1 @@ -1,34 +1,26 @@ - function Merge-CippStandards { param( - [Parameter(Mandatory = $true)] $Existing, - [Parameter(Mandatory = $true)] $CurrentStandard + [Parameter(Mandatory = $true)][object]$Existing, + [Parameter(Mandatory = $true)][object]$New, + [Parameter(Mandatory = $true)][string]$StandardName ) - $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') - } - $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 - } - if ($AllActionValues.Count -gt 0) { - $Existing | Add-Member -NotePropertyName 'combinedActions' -NotePropertyValue $AllActionValues -Force - } + # If $Existing or $New is $null/empty, just return the other. + if (-not $Existing) { return $New } + if (-not $New) { return $Existing } + + # 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]) - return $Existing + # 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 + } }