diff --git a/CIPPHttpTrigger/function.json b/CIPPHttpTrigger/function.json index bfd835e6a6a8..643fa9b6a3ad 100644 --- a/CIPPHttpTrigger/function.json +++ b/CIPPHttpTrigger/function.json @@ -26,6 +26,12 @@ "name": "starter", "type": "durableClient", "direction": "in" + }, + { + "type": "queue", + "direction": "out", + "name": "QueueItem", + "queueName": "cippqueue" } ] } diff --git a/CIPPQueueTrigger/function.json b/CIPPQueueTrigger/function.json new file mode 100644 index 000000000000..82b530006d41 --- /dev/null +++ b/CIPPQueueTrigger/function.json @@ -0,0 +1,17 @@ +{ + "scriptFile": "../Modules/CippEntrypoints/CippEntrypoints.psm1", + "entryPoint": "Receive-CippQueueTrigger", + "bindings": [ + { + "name": "QueueItem", + "type": "queueTrigger", + "direction": "in", + "queueName": "cippqueue" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ] +} diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 index 8cea32a93caa..1ee7decbbe2b 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMFAAdmins.ps1 @@ -18,31 +18,63 @@ function Get-CIPPAlertMFAAdmins { } } if (!$DuoActive) { - $Users = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true | - Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' } + $MFAReport = try { Get-CIPPMFAStateReport -TenantFilter $TenantFilter } catch { $null } + $IncludeDisabled = [System.Convert]::ToBoolean($InputValue) - # Filter out JIT admins if any users were found - if ($Users) { + # Check 1: Admins with no MFA registered — prefer cache, fall back to live Graph + $Users = if ($MFAReport) { + $MFAReport | Where-Object { $_.IsAdmin -eq $true -and $_.MFARegistration -eq $false -and ($IncludeDisabled -or $_.AccountEnabled -eq $true) } + } else { + New-GraphGETRequest -uri "https://graph.microsoft.com/beta/reports/authenticationMethods/userRegistrationDetails?`$top=999&filter=IsAdmin eq true and isMfaRegistered eq false and userType eq 'member'&`$select=id,userDisplayName,userPrincipalName,lastUpdatedDateTime,isMfaRegistered,IsAdmin" -tenantid $($TenantFilter) -AsApp $true | + Where-Object { $_.userDisplayName -ne 'On-Premises Directory Synchronization Service Account' } | + Select-Object @{n = 'ID'; e = { $_.id } }, @{n = 'UPN'; e = { $_.userPrincipalName } }, @{n = 'DisplayName'; e = { $_.userDisplayName } } + } + + # Check 2: Admins with MFA registered but no enforcement. + # I hate how this ended up looking, but I couldn't think of a better way to do it ¯\_(ツ)_/¯ + $UnenforcedAdmins = $MFAReport | Where-Object { + $_.IsAdmin -eq $true -and + $_.MFARegistration -eq $true -and + ($IncludeDisabled -or $_.AccountEnabled -eq $true) -and + $_.PerUser -notin @('Enforced', 'Enabled') -and + $null -ne $_.CoveredBySD -and + $_.CoveredBySD -ne $true -and + $_.CoveredByCA -notlike 'Enforced*' + } + + # Filter out JIT admins + if ($Users -or $UnenforcedAdmins) { $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } | Select-Object -First 1 $JITAdmins = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/users?`$select=id,$($Schema.id)&`$filter=$($Schema.id)/jitAdminEnabled eq true" -tenantid $TenantFilter -ComplexFilter $JITAdminIds = $JITAdmins.id - $Users = $Users | Where-Object { $_.id -notin $JITAdminIds } + $Users = $Users | Where-Object { $_.ID -notin $JITAdminIds } + $UnenforcedAdmins = $UnenforcedAdmins | Where-Object { $_.ID -notin $JITAdminIds } + } + + $AlertData = [System.Collections.Generic.List[PSCustomObject]]::new() + + foreach ($user in $Users) { + $AlertData.Add([PSCustomObject]@{ + Message = "Admin user $($user.DisplayName) ($($user.UPN)) does not have MFA registered." + UserPrincipalName = $user.UPN + DisplayName = $user.DisplayName + Id = $user.ID + Tenant = $TenantFilter + }) } - if ($Users.UserPrincipalName) { - $AlertData = foreach ($user in $Users) { - [PSCustomObject]@{ - Message = "Admin user $($user.userDisplayName) ($($user.userPrincipalName)) does not have MFA registered." - UserPrincipalName = $user.userPrincipalName - DisplayName = $user.userDisplayName - Id = $user.id - LastUpdated = $user.lastUpdatedDateTime + foreach ($user in $UnenforcedAdmins) { + $AlertData.Add([PSCustomObject]@{ + Message = "Admin user $($user.DisplayName) ($($user.UPN)) has MFA registered but no enforcement method (Per-User MFA, Security Defaults, or Conditional Access) is active." + UserPrincipalName = $user.UPN + DisplayName = $user.DisplayName + Id = $user.ID Tenant = $TenantFilter - } - } + }) + } + if ($AlertData.Count -gt 0) { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $AlertData - } } else { Write-LogMessage -message 'Potentially using Duo for MFA, could not check MFA status for Admins with 100% accuracy' -API 'MFA Alerts - Informational' -tenant $TenantFilter -sev Info diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 index 8abf8bb43747..9d86abcb8b35 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertMXRecordChanged.ps1 @@ -16,6 +16,10 @@ function Get-CIPPAlertMXRecordChanged { $CacheTable = Get-CippTable -tablename 'CacheMxRecords' $PreviousResults = Get-CIPPAzDataTableEntity @CacheTable -Filter "PartitionKey eq '$TenantFilter'" + if (!$DomainData) { + return + } + $ChangedDomains = foreach ($Domain in $DomainData) { try { $PreviousDomain = $PreviousResults | Where-Object { $_.Domain -eq $Domain.Domain } @@ -60,8 +64,6 @@ function Get-CIPPAlertMXRecordChanged { if ($ChangedDomains) { Write-AlertTrace -cmdletName $MyInvocation.MyCommand -tenantFilter $TenantFilter -data $ChangedDomains } - return $true - } catch { Write-LogMessage -message "Failed to check MX record changes: $($_.Exception.Message)" -API 'MX Record Alert' -tenant $TenantFilter -sev Error } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 index 9208d700d4dc..99b52d38193d 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertNewMFADevice.ps1 @@ -20,13 +20,30 @@ function Get-CIPPAlertNewMFADevice { $User = $Log.targetResources[0].userPrincipalName if (-not $User) { $User = $Log.initiatedBy.user.userPrincipalName } + $IPAddress = $Log.initiatedBy.user.ipAddress + $LocationData = $null + if (-not [string]::IsNullOrEmpty($IPAddress) -and $IPAddress -notmatch '[X]+') { + try { + $LocationData = Get-CIPPGeoIPLocation -IP $IPAddress + } catch { + Write-Information "Could not enrich MFA audit IP ${$IPAddress}: $($_.Exception.Message)" + } + } + [PSCustomObject]@{ - Message = "New MFA method registered: $User" - User = $User - DisplayName = $Log.targetResources[0].displayName - Activity = $Log.activityDisplayName - ActivityTime = $Log.activityDateTime - Tenant = $TenantFilter + Message = "New MFA method registered: $User" + User = $User + DisplayName = $Log.targetResources[0].displayName + Activity = $Log.activityDisplayName + ActivityTime = $Log.activityDateTime + Tenant = $TenantFilter + IpAddress = $IPAddress + CountryOrRegion = if ($LocationData) { $LocationData.countryCode } else { $null } + City = if ($LocationData) { $LocationData.city } else { $null } + Proxy = if ($LocationData) { $LocationData.proxy } else { $null } + Hosting = if ($LocationData) { $LocationData.hosting } else { $null } + ASN = if ($LocationData) { $LocationData.asname } else { $null } + GeoLocationInfo = if ($LocationData) { ($LocationData | ConvertTo-Json -Depth 10 -Compress) } else { $null } } } } diff --git a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 index f8e4b0f9b809..b27517b85438 100644 --- a/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 +++ b/Modules/CIPPCore/Public/Alerts/Get-CIPPAlertSmtpAuthSuccess.ps1 @@ -13,7 +13,7 @@ function Get-CIPPAlertSmtpAuthSuccess { try { # Graph API endpoint for sign-ins - $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=clientAppUsed eq 'Authenticated SMTP' and status/errorCode eq 0" + $uri = "https://graph.microsoft.com/v1.0/auditLogs/signIns?`$filter=(clientAppUsed eq 'Authenticated SMTP' or clientAppUsed eq 'SMTP') and status/errorCode eq 0" # Call Graph API for the given tenant $SignIns = New-GraphGetRequest -uri $uri -tenantid $TenantFilter diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneAssignments.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneAssignments.ps1 new file mode 100644 index 000000000000..a123691d9d8c --- /dev/null +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneAssignments.ps1 @@ -0,0 +1,134 @@ +function Compare-CIPPIntuneAssignments { + <# + .SYNOPSIS + Compares existing Intune policy assignments against expected assignment settings. + .DESCRIPTION + Returns $true if the existing assignments match the expected settings, $false if they differ, + or $null if the comparison could not be completed (e.g. Graph error). + .PARAMETER ExistingAssignments + The current assignments on the policy, as returned by Get-CIPPIntunePolicyAssignments. + .PARAMETER ExpectedAssignTo + The expected assignment target type: allLicensedUsers, AllDevices, AllDevicesAndUsers, + customGroup, or On (no assignment). + .PARAMETER ExpectedCustomGroup + The expected custom group name(s), comma-separated. Used when ExpectedAssignTo is 'customGroup'. + .PARAMETER ExpectedExcludeGroup + The expected exclusion group name(s), comma-separated. + .PARAMETER ExpectedAssignmentFilter + The expected assignment filter display name. Wildcards supported. + .PARAMETER ExpectedAssignmentFilterType + 'include' or 'exclude'. Defaults to 'include'. + .PARAMETER TenantFilter + The tenant to query for group/filter resolution. + .FUNCTIONALITY + Internal + #> + param( + [object[]]$ExistingAssignments, + [string]$ExpectedAssignTo, + [string]$ExpectedCustomGroup, + [string]$ExpectedExcludeGroup, + [string]$ExpectedAssignmentFilter, + [string]$ExpectedAssignmentFilterType = 'include', + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + # Normalize existing targets + $ExistingTargetTypes = @($ExistingAssignments.target.'@odata.type' | Where-Object { $_ }) + $ExistingIncludeGroupIds = @( + $ExistingAssignments | + Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.groupAssignmentTarget' } | + ForEach-Object { $_.target.groupId } + ) + $ExistingExcludeGroupIds = @( + $ExistingAssignments | + Where-Object { $_.target.'@odata.type' -eq '#microsoft.graph.exclusionGroupAssignmentTarget' } | + ForEach-Object { $_.target.groupId } + ) + + # Determine expected include target types + $ExpectedIncludeTypes = switch ($ExpectedAssignTo) { + 'allLicensedUsers' { @('#microsoft.graph.allLicensedUsersAssignmentTarget') } + 'AllDevices' { @('#microsoft.graph.allDevicesAssignmentTarget') } + 'AllDevicesAndUsers' { @('#microsoft.graph.allDevicesAssignmentTarget', '#microsoft.graph.allLicensedUsersAssignmentTarget') } + 'customGroup' { @('#microsoft.graph.groupAssignmentTarget') } + 'On' { @() } + default { @() } + } + + # Compare include target types (ignore exclusion targets) + $ExistingIncludeTypes = @($ExistingTargetTypes | Where-Object { $_ -ne '#microsoft.graph.exclusionGroupAssignmentTarget' }) + $TargetTypeMatch = $true + foreach ($t in $ExpectedIncludeTypes) { + if ($t -notin $ExistingIncludeTypes) { $TargetTypeMatch = $false; break } + } + if ($TargetTypeMatch) { + foreach ($t in $ExistingIncludeTypes) { + if ($t -notin $ExpectedIncludeTypes) { $TargetTypeMatch = $false; break } + } + } + + # Lazy-load groups cache only if needed + $AllGroupsCache = $null + + # For custom groups, resolve names to IDs and compare + $IncludeGroupMatch = $true + if ($ExpectedAssignTo -eq 'customGroup' -and $ExpectedCustomGroup) { + $AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter + $ExpectedGroupIds = @( + $ExpectedCustomGroup.Split(',').Trim() | ForEach-Object { + $name = $_ + $AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id + } | Where-Object { $_ } + ) + $MissingIds = @($ExpectedGroupIds | Where-Object { $_ -notin $ExistingIncludeGroupIds }) + $ExtraIds = @($ExistingIncludeGroupIds | Where-Object { $_ -notin $ExpectedGroupIds }) + $IncludeGroupMatch = ($MissingIds.Count -eq 0 -and $ExtraIds.Count -eq 0) + } + + # Compare exclusion groups + $ExcludeGroupMatch = $true + if ($ExpectedExcludeGroup) { + if (-not $AllGroupsCache) { + $AllGroupsCache = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$select=id,displayName&$top=999' -tenantid $TenantFilter + } + $ExpectedExcludeIds = @( + $ExpectedExcludeGroup.Split(',').Trim() | ForEach-Object { + $name = $_ + $AllGroupsCache | Where-Object { $_.displayName -like $name } | Select-Object -ExpandProperty id + } | Where-Object { $_ } + ) + $MissingExcludeIds = @($ExpectedExcludeIds | Where-Object { $_ -notin $ExistingExcludeGroupIds }) + $ExtraExcludeIds = @($ExistingExcludeGroupIds | Where-Object { $_ -notin $ExpectedExcludeIds }) + $ExcludeGroupMatch = ($MissingExcludeIds.Count -eq 0 -and $ExtraExcludeIds.Count -eq 0) + } elseif ($ExistingExcludeGroupIds.Count -gt 0) { + # No exclusions expected but some exist + $ExcludeGroupMatch = $false + } + + # Compare assignment filter + $FilterMatch = $true + if ($ExpectedAssignmentFilter) { + $ExistingFilterIds = @( + $ExistingAssignments | + Where-Object { $_.target.deviceAndAppManagementAssignmentFilterId } | + ForEach-Object { $_.target.deviceAndAppManagementAssignmentFilterId } + ) + if ($ExistingFilterIds.Count -eq 0) { + $FilterMatch = $false + } else { + $AllFilters = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $TenantFilter + $ExpectedFilter = $AllFilters | Where-Object { $_.displayName -like $ExpectedAssignmentFilter } | Select-Object -First 1 + $FilterMatch = $ExpectedFilter -and ($ExpectedFilter.id -in $ExistingFilterIds) + } + } + + return $TargetTypeMatch -and $IncludeGroupMatch -and $ExcludeGroupMatch -and $FilterMatch + + } catch { + Write-Warning "Compare-CIPPIntuneAssignments failed for tenant $TenantFilter : $($_.Exception.Message)" + return $null # null = unknown, don't treat as mismatch + } +} diff --git a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 index f8d591457071..1b90516a6e30 100644 --- a/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 +++ b/Modules/CIPPCore/Public/Compare-CIPPIntuneObject.ps1 @@ -340,6 +340,11 @@ function Compare-CIPPIntuneObject { } } else { $intuneCollection = Get-Content .\intuneCollection.json | ConvertFrom-Json -ErrorAction SilentlyContinue + # Build a hashtable index for O(1) lookups instead of O(n) Where-Object scans + $intuneCollectionIndex = @{} + foreach ($item in $intuneCollection) { + if ($item.id) { $intuneCollectionIndex[$item.id] = $item } + } # Recursive function to process group setting collections at any depth function Process-GroupSettingChildren { @@ -349,13 +354,13 @@ function Compare-CIPPIntuneObject { [Parameter(Mandatory = $true)] [string]$Source, [Parameter(Mandatory = $true)] - $IntuneCollection + $IntuneCollectionIndex ) $results = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($child in $Children) { - $childIntuneObj = $IntuneCollection | Where-Object { $_.id -eq $child.settingDefinitionId } + $childIntuneObj = $IntuneCollectionIndex[$child.settingDefinitionId] $childLabel = if ($childIntuneObj?.displayName) { $childIntuneObj.displayName } else { @@ -367,7 +372,7 @@ function Compare-CIPPIntuneObject { if ($child.groupSettingCollectionValue) { foreach ($groupValue in $child.groupSettingCollectionValue) { if ($groupValue.children) { - $nestedResults = Process-GroupSettingChildren -Children $groupValue.children -Source $Source -IntuneCollection $IntuneCollection + $nestedResults = Process-GroupSettingChildren -Children $groupValue.children -Source $Source -IntuneCollectionIndex $IntuneCollectionIndex foreach ($nr in $nestedResults) { $results.Add($nr) } } } @@ -453,7 +458,7 @@ function Compare-CIPPIntuneObject { # Also process any children within choice setting values if ($child.choiceSettingValue?.children) { - $nestedResults = Process-GroupSettingChildren -Children $child.choiceSettingValue.children -Source $Source -IntuneCollection $IntuneCollection + $nestedResults = Process-GroupSettingChildren -Children $child.choiceSettingValue.children -Source $Source -IntuneCollectionIndex $IntuneCollectionIndex foreach ($nr in $nestedResults) { $results.Add($nr) } } } @@ -464,14 +469,14 @@ function Compare-CIPPIntuneObject { # Process reference object settings $referenceItems = $ReferenceObject.settings | ForEach-Object { $settingInstance = $_.settingInstance - $intuneObj = $intuneCollection | Where-Object { $_.id -eq $settingInstance.settingDefinitionId } + $intuneObj = $intuneCollectionIndex[$settingInstance.settingDefinitionId] $tempOutput = switch ($settingInstance.'@odata.type') { '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance' { if ($null -ne $settingInstance.groupSettingCollectionValue) { $groupResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($groupValue in $settingInstance.groupSettingCollectionValue) { if ($groupValue.children -is [System.Array]) { - $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Reference' -IntuneCollection $intuneCollection + $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Reference' -IntuneCollectionIndex $intuneCollectionIndex foreach ($cr in $childResults) { $groupResults.Add($cr) } } } @@ -536,14 +541,14 @@ function Compare-CIPPIntuneObject { # Process difference object settings $differenceItems = $DifferenceObject.settings | ForEach-Object { $settingInstance = $_.settingInstance - $intuneObj = $intuneCollection | Where-Object { $_.id -eq $settingInstance.settingDefinitionId } + $intuneObj = $intuneCollectionIndex[$settingInstance.settingDefinitionId] $tempOutput = switch ($settingInstance.'@odata.type') { '#microsoft.graph.deviceManagementConfigurationGroupSettingCollectionInstance' { if ($null -ne $settingInstance.groupSettingCollectionValue) { $groupResults = [System.Collections.Generic.List[PSCustomObject]]::new() foreach ($groupValue in $settingInstance.groupSettingCollectionValue) { if ($groupValue.children -is [System.Array]) { - $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Difference' -IntuneCollection $intuneCollection + $childResults = Process-GroupSettingChildren -Children $groupValue.children -Source 'Difference' -IntuneCollectionIndex $intuneCollectionIndex foreach ($cr in $childResults) { $groupResults.Add($cr) } } } @@ -624,7 +629,7 @@ function Compare-CIPPIntuneObject { $settingId = $key.Substring(8) } - $settingDefinition = $intuneCollection | Where-Object { $_.id -eq $settingId } + $settingDefinition = $intuneCollectionIndex[$settingId] $refRawValue = if ($refItem) { $refItem.Value } else { $null } $diffRawValue = if ($diffItem) { $diffItem.Value } else { $null } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 index f3ac78cf719a..3111bbe2461f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-DomainAnalyserTenant.ps1 @@ -5,7 +5,7 @@ function Push-DomainAnalyserTenant { #> param($Item) - $Tenant = Get-Tenants -TenantFilter $Item.customerId + $Tenant = Get-Tenants -TenantFilter $Item.customerId -IncludeAll $DomainTable = Get-CippTable -tablename 'Domains' if ($Tenant.Excluded -eq $true) { diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetDomainAnalyserResults.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetDomainAnalyserResults.ps1 index 9a98b49ceb06..826c4c702016 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetDomainAnalyserResults.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Domain Analyser/Push-GetDomainAnalyserResults.ps1 @@ -5,6 +5,22 @@ function Push-GetDomainAnalyserResults { ) $Tenant = $Item.Parameters.Tenant - Write-LogMessage -API 'DomainAnalyser' -Tenant $Tenant.defaultDomainName -TenantId $Tenant.customerId -message "Domain Analyser completed for tenant $($Tenant.defaultDomainName)" -sev Info -LogData ($Item.Results | Select-Object Domain, @{Name = 'Score'; Expression = { "$($_.Score)/$($_.MaximumScore)" } }) + $Results = if ($Item.Results -is [array]) { $Item.Results } else { @($Item.Results) } + $DomainCount = $Results | Measure-Object | Select-Object -ExpandProperty Count + + # Create summary for logging + $Summary = [system.collections.generic.list[object]]::new() + $Results | ForEach-Object { + $Summary.Add([PSCustomObject]@{ + Domain = $_.Domain + Score = "$($_.Score)/$($_.MaximumScore)" + Percentage = "$($_.ScorePercentage)%" + } + ) + } + + $Message = "Domain Analyser completed for $DomainCount domain(s) in tenant $($Tenant.defaultDomainName)" + Write-LogMessage -API 'DomainAnalyser' -Tenant $Tenant.defaultDomainName -TenantId $Tenant.customerId -message $Message -sev Info -LogData $Summary + return } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 index d86c51cd10a6..6da7d9618edd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-GetMailboxPermissionsBatch.ps1 @@ -13,6 +13,7 @@ function Push-GetMailboxPermissionsBatch { $TenantFilter = $Item.TenantFilter $Mailboxes = $Item.Mailboxes + $MailboxData = @($Item.MailboxData) $BatchNumber = $Item.BatchNumber $TotalBatches = $Item.TotalBatches @@ -85,10 +86,31 @@ function Push-GetMailboxPermissionsBatch { $MailboxPermissions['Get-RecipientPermission'] = $NormalizedRecipientPerms } + $MailboxIdentityLookup = @{} + foreach ($MappedMailbox in ($MailboxData | Where-Object { $_.Id -and $_.UPN })) { + $MailboxIdentityLookup[[string]$MappedMailbox.Id] = [string]$MappedMailbox.UPN + } + + # Normalize SendOnBehalf permissions from passed mailbox metadata + $NormalizedSendOnBehalfPerms = foreach ($Mailbox in ($MailboxData | Where-Object { $_.GrantSendOnBehalfTo -and ($Mailboxes -contains $_.UPN) })) { + foreach ($Delegate in (@($Mailbox.GrantSendOnBehalfTo) | Where-Object { $_ -and $MailboxIdentityLookup.ContainsKey([string]$_) })) { + [PSCustomObject]@{ + id = [guid]::NewGuid().ToString() + Identity = $Mailbox.UPN + User = $MailboxIdentityLookup[[string]$Delegate] + AccessRights = @('SendOnBehalf') + IsInherited = $false + Deny = $false + } + } + } + $MailboxPermissions['Get-Mailbox'] = @($NormalizedSendOnBehalfPerms) + $MailboxPermCount = if ($MailboxPermissions['Get-MailboxPermission']) { $MailboxPermissions['Get-MailboxPermission'].Count } else { 0 } $RecipientPermCount = if ($MailboxPermissions['Get-RecipientPermission']) { $MailboxPermissions['Get-RecipientPermission'].Count } else { 0 } + $SendOnBehalfPermCount = if ($MailboxPermissions['Get-Mailbox']) { $MailboxPermissions['Get-Mailbox'].Count } else { 0 } - Write-Information "Completed batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $MailboxPermCount mailbox permissions, $RecipientPermCount recipient permissions" + Write-Information "Completed batch $BatchNumber of $TotalBatches - processed $($Mailboxes.Count) mailboxes: $MailboxPermCount mailbox permissions, $RecipientPermCount recipient permissions, $SendOnBehalfPermCount send-on-behalf permissions" # Return results to be aggregated by post-execution function return $MailboxPermissions diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 index 42a4f93d60dc..65a163761d8c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Mailbox Permissions/Push-StoreMailboxPermissions.ps1 @@ -28,6 +28,7 @@ function Push-StoreMailboxPermissions { # Aggregate results by command type from all batches $AllMailboxPermissions = [System.Collections.Generic.List[object]]::new() $AllRecipientPermissions = [System.Collections.Generic.List[object]]::new() + $AllSendOnBehalfPermissions = [System.Collections.Generic.List[object]]::new() $AllCalendarPermissions = [System.Collections.Generic.List[object]]::new() foreach ($BatchResult in $Results) { @@ -50,6 +51,11 @@ function Push-StoreMailboxPermissions { Write-Information "Adding $($ActualResult['Get-RecipientPermission'].Count) recipient permissions" $AllRecipientPermissions.AddRange($ActualResult['Get-RecipientPermission']) } + if ($ActualResult['Get-Mailbox']) { + $SendOnBehalfRows = @($ActualResult['Get-Mailbox']) + Write-Information "Adding $($SendOnBehalfRows.Count) send-on-behalf permissions" + $AllSendOnBehalfPermissions.AddRange($SendOnBehalfRows) + } if ($ActualResult['Get-MailboxFolderPermission']) { Write-Information "Adding $($ActualResult['Get-MailboxFolderPermission'].Count) calendar permissions" $AllCalendarPermissions.AddRange($ActualResult['Get-MailboxFolderPermission']) @@ -63,8 +69,9 @@ function Push-StoreMailboxPermissions { $AllPermissions = [System.Collections.Generic.List[object]]::new() $AllPermissions.AddRange($AllMailboxPermissions) $AllPermissions.AddRange($AllRecipientPermissions) + $AllPermissions.AddRange($AllSendOnBehalfPermissions) - Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient)" + Write-Information "Aggregated $($AllPermissions.Count) total permissions ($($AllMailboxPermissions.Count) mailbox + $($AllRecipientPermissions.Count) recipient + $($AllSendOnBehalfPermissions.Count) send-on-behalf)" Write-Information "Aggregated $($AllCalendarPermissions.Count) calendar permissions" # Store all permissions together as MailboxPermissions diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 index 907f5ad3de9d..8663bd09066e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-CIPPDBCacheData.ps1 @@ -51,14 +51,12 @@ function Push-CIPPDBCacheData { 'PIMSettings' 'Domains' 'B2BManagementPolicy' - 'AuthenticationFlowsPolicy' 'DeviceRegistrationPolicy' - 'CredentialUserRegistrationDetails' - 'UserRegistrationDetails' 'OAuth2PermissionGrants' 'AppRoleAssignments' 'LicenseOverview' 'MFAState' + 'BitlockerKeys' ) foreach ($CacheFunction in $BasicCacheFunctions) { @@ -116,12 +114,20 @@ function Push-CIPPDBCacheData { #region Conditional Access Licensed - Azure AD Premium features if ($ConditionalAccessCapable) { - $Batch.Add(@{ - FunctionName = 'ExecCIPPDBCache' - Name = 'ConditionalAccessPolicies' - TenantFilter = $TenantFilter - QueueId = $QueueId - }) + $ConditionalAccessCacheFunctions = @( + 'ConditionalAccessPolicies' + 'AuthenticationFlowsPolicy' + 'CredentialUserRegistrationDetails' + 'UserRegistrationDetails' + ) + foreach ($CacheFunction in $ConditionalAccessCacheFunctions) { + $Batch.Add(@{ + FunctionName = 'ExecCIPPDBCache' + Name = $CacheFunction + TenantFilter = $TenantFilter + QueueId = $QueueId + }) + } } else { Write-Host 'Skipping Conditional Access data collection - tenant does not have required license' } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index a7744b0a1af0..53cf446760c4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -26,6 +26,10 @@ function Push-ExecScheduledCommand { # Detect if this is a multi-tenant task that should store results per-tenant $IsMultiTenantTask = ($task.Tenant -eq 'AllTenants' -or $task.TenantGroup) + # Detect if this is an individual tenant execution within a multi-tenant task + # In this case, skip parent task state updates - the PostExecution function will handle it + $IsMultiTenantExecution = $IsMultiTenantTask -and ($Tenant -ne $task.Tenant) + # For tenant group tasks, the tenant will be the expanded tenant from the orchestrator # We don't need to expand groups here as that's handled in the orchestrator $TenantInfo = Get-Tenants -TenantFilter $Tenant @@ -45,7 +49,8 @@ function Push-ExecScheduledCommand { # We accept both to handle edge cases # Check for rerun protection - prevent duplicate executions within the recurrence interval - if ($task.Recurrence -and $task.Recurrence -ne '0') { + # Do this BEFORE updating state to 'Running' to avoid getting stuck + if ($task.Recurrence -and $task.Recurrence -ne '0' -and !$IsMultiTenantExecution) { # Calculate interval in seconds from recurrence string $IntervalSeconds = switch -Regex ($task.Recurrence) { '^(\d+)$' { [int64]$matches[1] * 86400 } # Plain number = days @@ -84,6 +89,19 @@ function Push-ExecScheduledCommand { } } + # Also check for one-time task rerun protection based on ExecutedTime + if ((!$task.Recurrence -or $task.Recurrence -eq '0') -and $task.ExecutedTime -and !$IsMultiTenantExecution) { + $currentUnixTime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + $timeSinceExecution = $currentUnixTime - [int64]$task.ExecutedTime + + # If executed within last 15 minutes, skip (likely a duplicate pickup) + if ($timeSinceExecution -lt 900) { + Write-Information "One-time task $($task.Name) for tenant $Tenant was recently executed ($timeSinceExecution seconds ago). Skipping to prevent duplicate execution." + Remove-Variable -Name ScheduledTaskId -Scope Script -ErrorAction SilentlyContinue + return + } + } + if ($task.Trigger) { # Extract trigger data from the task and process $Trigger = if (Test-Json -Json $task.Trigger) { $task.Trigger | ConvertFrom-Json } else { $task.Trigger } @@ -133,21 +151,27 @@ function Push-ExecScheduledCommand { $IsTriggerTask = $false } - $null = Update-AzDataTableEntity -Force @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - TaskState = 'Running' + # Only update parent task state if this is NOT a multi-tenant execution + # Multi-tenant executions have their parent state managed by PostExecution + if (!$IsMultiTenantExecution) { + $null = Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + TaskState = 'Running' + } } $Function = Get-Command -Name $Item.Command if ($null -eq $Function) { $Results = "Task Failed: The command $($Item.Command) does not exist." $State = 'Failed' - Update-AzDataTableEntity -Force @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - Results = "$Results" - TaskState = $State + if (!$IsMultiTenantExecution) { + Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$Results" + TaskState = $State + } } Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Failed to execute task $($task.Name): The command $($Item.Command) does not exist." -sev Error @@ -308,18 +332,20 @@ function Push-ExecScheduledCommand { $nextRunUnixTime = [int64]$task.ScheduledTime + [int64]$secondsToAdd if ($task.Recurrence -ne 0) { $State = 'Failed - Planned' } else { $State = 'Failed' } Write-Information "The job is recurring, but failed. It was scheduled for $($task.ScheduledTime). The next runtime should be $nextRunUnixTime" - Update-AzDataTableEntity -Force @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - Results = "$errorMessage" - ScheduledTime = "$nextRunUnixTime" - TaskState = $State + if (!$IsMultiTenantExecution) { + Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = "$errorMessage" + ScheduledTime = "$nextRunUnixTime" + TaskState = $State + } } Write-LogMessage -API 'Scheduler_UserTasks' -tenant $Tenant -tenantid $TenantInfo.customerId -message "Failed to execute task $($task.Name): $errorMessage" -sev Error -LogData (Get-CippExceptionData -Exception $_.Exception) } # For orchestrator-based commands, skip post-execution alerts as they will be handled by the orchestrator's post-execution function - if ($Results -and $Item.Command -notin $OrchestratorBasedCommands) { + if ($Results -and $Item.Command -notin $OrchestratorBasedCommands -and -not [string]::IsNullOrWhiteSpace($Task.PostExecution)) { Write-Information "Sending task results to post execution target(s): $($Task.PostExecution -join ', ')." Send-CIPPScheduledTaskAlert -Results $Results -TaskInfo $task -TenantFilter $Tenant -TaskType $TaskType } @@ -328,13 +354,19 @@ function Push-ExecScheduledCommand { # For orchestrator-based commands, skip task state update as it will be handled by post-execution if ($Item.Command -in $OrchestratorBasedCommands) { Write-Information "Command $($Item.Command) is orchestrator-based. Skipping task state update - will be handled by post-execution." - # Update task state to 'Running' to indicate orchestration is in progress - Update-AzDataTableEntity -Force @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - Results = 'Orchestration in progress' - TaskState = 'Processing' + if (!$IsMultiTenantExecution) { + # Update task state to 'Running' to indicate orchestration is in progress + Update-AzDataTableEntity -Force @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + Results = 'Orchestration in progress' + TaskState = 'Processing' + } } + } elseif ($IsMultiTenantExecution) { + # For multi-tenant executions, skip parent task state updates + # The PostExecution function will aggregate all results and update the parent task + Write-Information "Multi-tenant execution for tenant $Tenant - parent task state will be updated by PostExecution" } elseif ($task.Recurrence -eq '0' -or [string]::IsNullOrEmpty($task.Recurrence) -or $Trigger.ExecutionMode.value -eq 'once' -or $Trigger.ExecutionMode -eq 'once') { Write-Information 'Recurrence empty or 0. Task is not recurring. Setting task state to completed.' Update-AzDataTableEntity -Force @Table -Entity @{ diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListBasicAuthAllTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListBasicAuthAllTenants.ps1 index 2f2a5218c8a1..faeb21039fc6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListBasicAuthAllTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ListBasicAuthAllTenants.ps1 @@ -1,4 +1,4 @@ -Function Push-ListBasicAuthAllTenants { +function Push-ListBasicAuthAllTenants { <# .FUNCTIONALITY Entrypoint @@ -6,7 +6,9 @@ Function Push-ListBasicAuthAllTenants { [CmdletBinding()] param($Item) - $domainName = $Item.defaultDomainName + + $Tenant = Get-Tenants -TenantFilter $Item.customerId + $domainName = $Tenant.defaultDomainName # XXX; This function seems to be unused in the frontend. -Bobby diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ScheduledTaskPostExecution.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ScheduledTaskPostExecution.ps1 new file mode 100644 index 000000000000..949cceed7d60 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ScheduledTaskPostExecution.ps1 @@ -0,0 +1,156 @@ +function Push-ScheduledTaskPostExecution { + <# + .SYNOPSIS + Post-execution aggregation function for multi-tenant scheduled tasks + + .DESCRIPTION + Called by orchestrator after all tenant-specific scheduled task executions complete. + Aggregates results, updates parent task state, and handles recurrence scheduling. + + .FUNCTIONALITY + Entrypoint + #> + param($Item) + + # Extract parameters and results from the item + $Parameters = $Item.Parameters + $Results = $Item.Results + + Write-Information "Post-execution started for scheduled task: $($Parameters.TaskRowKey)" + Write-Information "Received $($Results.Count) tenant execution results" + + $Table = Get-CippTable -tablename 'ScheduledTasks' + + # Get the parent task + $ParentTask = Get-AzDataTableEntity @Table -Filter "RowKey eq '$($Parameters.TaskRowKey)'" + if (!$ParentTask) { + Write-Warning "Parent task $($Parameters.TaskRowKey) not found in ScheduledTasks table" + return + } + + Write-Information "Parent task found: $($ParentTask.Name) - Current state: $($ParentTask.TaskState)" + + # Aggregate results + $SuccessCount = 0 + $FailureCount = 0 + $TotalTenants = $Results.Count + + foreach ($Result in $Results) { + if ($Result -and $Result -notlike '*Failed*' -and $Result -notlike '*Error*') { + $SuccessCount++ + } else { + $FailureCount++ + } + } + + Write-Information "Aggregated results: $SuccessCount successful, $FailureCount failed out of $TotalTenants tenants" + + # Determine if this was a recurring task + $IsRecurring = $ParentTask.Recurrence -and $ParentTask.Recurrence -ne '0' + + # Check trigger execution mode + $IsTriggerOnce = $false + if ($ParentTask.Trigger) { + $Trigger = if (Test-Json -Json $ParentTask.Trigger) { + $ParentTask.Trigger | ConvertFrom-Json + } else { + $ParentTask.Trigger + } + $TriggerExecutionMode = $Trigger.ExecutionMode.value ?? $Trigger.ExecutionMode + if ($TriggerExecutionMode -eq 'once') { + $IsTriggerOnce = $true + } + } + + # Prepare aggregated results message + $AggregatedMessage = "Multi-tenant task completed: $SuccessCount successful, $FailureCount failed (Total: $TotalTenants tenants)" + + # Calculate next run time for recurring tasks + if ($IsRecurring -and !$IsTriggerOnce) { + # Convert recurrence to seconds + if ($ParentTask.Recurrence -match '^\d+$') { + $ParentTask.Recurrence = $ParentTask.Recurrence + 'd' + } + + $secondsToAdd = switch -Regex ($ParentTask.Recurrence) { + '(\d+)m$' { [int64]$matches[1] * 60 } + '(\d+)h$' { [int64]$matches[1] * 3600 } + '(\d+)d$' { [int64]$matches[1] * 86400 } + default { 0 } + } + + if ($secondsToAdd -gt 0) { + $unixtimeNow = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + if ([int64]$ParentTask.ScheduledTime -lt ($unixtimeNow - $secondsToAdd)) { + $ParentTask.ScheduledTime = $unixtimeNow + } + $nextRunUnixTime = [int64]$ParentTask.ScheduledTime + [int64]$secondsToAdd + + Write-Information "Recurring task: next run scheduled for $nextRunUnixTime" + + # Update parent task to 'Planned' state for next execution + $UpdateEntity = @{ + PartitionKey = $ParentTask.PartitionKey + RowKey = $ParentTask.RowKey + Results = $AggregatedMessage + TaskState = if ($FailureCount -gt 0 -and $FailureCount -eq $TotalTenants) { 'Failed - Planned' } else { 'Planned' } + ScheduledTime = "$nextRunUnixTime" + } + } else { + # Invalid recurrence, mark as completed + Write-Warning "Invalid recurrence value: $($ParentTask.Recurrence). Marking task as completed." + $UpdateEntity = @{ + PartitionKey = $ParentTask.PartitionKey + RowKey = $ParentTask.RowKey + Results = "$AggregatedMessage - Warning: Invalid recurrence, task will not repeat" + TaskState = 'Completed' + } + } + } else { + # One-time task or trigger with 'once' mode - mark as completed + Write-Information 'Non-recurring task: marking as completed' + $UpdateEntity = @{ + PartitionKey = $ParentTask.PartitionKey + RowKey = $ParentTask.RowKey + Results = $AggregatedMessage + TaskState = if ($FailureCount -gt 0 -and $FailureCount -eq $TotalTenants) { 'Failed' } else { 'Completed' } + } + } + + # Update the parent task + try { + $null = Update-AzDataTableEntity -Force @Table -Entity $UpdateEntity + Write-Information "Parent task updated successfully to state: $($UpdateEntity.TaskState)" + } catch { + Write-Warning "Failed to update parent task: $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + } + + # Send consolidated alert/notification if results exist + # Individual tenant alerts are already sent by Push-ExecScheduledCommand + # This is just for overall task completion notification if needed + try { + if ($Parameters.SendCompletionAlert) { + Write-Information 'Completion notification available for multi-tenant task' + Write-Information "Task: $($ParentTask.Name)" + Write-Information "Results: $SuccessCount successful, $FailureCount failed out of $TotalTenants tenants" + if ($IsRecurring -and $nextRunUnixTime) { + $nextRunDate = (Get-Date '1970-01-01').AddSeconds($nextRunUnixTime).ToUniversalTime() + Write-Information "Next run scheduled for: $($nextRunDate.ToString('yyyy-MM-dd HH:mm:ss UTC'))" + } + # Note: Individual tenant results are already in ScheduledTaskResults table + } + } catch { + Write-Warning "Failed to log completion info: $($_.Exception.Message)" + } + + Write-Information "Post-execution completed for task: $($ParentTask.Name)" + return @{ + Status = 'Success' + TaskState = $UpdateEntity.TaskState + SuccessCount = $SuccessCount + FailureCount = $FailureCount + TotalTenants = $TotalTenants + AggregatedMessage = $AggregatedMessage + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 index c0af2f9c5467..1b744d532315 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Standards/Push-CIPPStandardsList.ps1 @@ -64,12 +64,16 @@ function Push-CIPPStandardsList { } else { # License valid - check policy timestamps to filter unchanged templates $TypeMap = @{ - Device = 'deviceManagement/deviceConfigurations' - Catalog = 'deviceManagement/configurationPolicies' - Admin = 'deviceManagement/groupPolicyConfigurations' - deviceCompliancePolicies = 'deviceManagement/deviceCompliancePolicies' - AppProtection_Android = 'deviceAppManagement/androidManagedAppProtections' - AppProtection_iOS = 'deviceAppManagement/iosManagedAppProtections' + Device = 'deviceManagement/deviceConfigurations' + Catalog = 'deviceManagement/configurationPolicies' + Admin = 'deviceManagement/groupPolicyConfigurations' + deviceCompliancePolicies = 'deviceManagement/deviceCompliancePolicies' + AppProtection_Android = 'deviceAppManagement/androidManagedAppProtections' + AppProtection_iOS = 'deviceAppManagement/iosManagedAppProtections' + windowsDriverUpdateProfiles = 'deviceManagement/windowsDriverUpdateProfiles' + windowsFeatureUpdateProfiles = 'deviceManagement/windowsFeatureUpdateProfiles' + windowsQualityUpdatePolicies = 'deviceManagement/windowsQualityUpdatePolicies' + windowsQualityUpdateProfiles = 'deviceManagement/windowsQualityUpdateProfiles' } $BulkRequests = $TypeMap.GetEnumerator() | ForEach-Object { @@ -86,8 +90,9 @@ function Push-CIPPStandardsList { $PolicyTimestamps = @{} foreach ($Result in $BulkResults) { - $GraphTime = $Result.body.value[0].lastModifiedDateTime - $GraphId = $Result.body.value[0].id + $FirstPolicy = if ($Result.body.value) { $Result.body.value[0] } else { $null } + $GraphTime = $FirstPolicy.lastModifiedDateTime + $GraphId = $FirstPolicy.id $GraphCount = ($Result.body.value | Measure-Object).Count $Cached = Get-CIPPAzDataTableEntity @TrackingTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$($Result.id)'" @@ -117,26 +122,65 @@ function Push-CIPPStandardsList { LatestPolicyId = $GraphId PolicyCount = $GraphCount } -Force | Out-Null + } elseif ($Cached -and $Cached.PolicyCount -ne $null) { + # No timestamp available - fall back to count-based detection + $Changed = $CountChanged -or $IdChanged + Add-CIPPAzDataTableEntity @TrackingTable -Entity @{ + PartitionKey = $TenantFilter + RowKey = $Result.id + LatestPolicyId = $GraphId + PolicyCount = $GraphCount + } -Force | Out-Null } else { + # No timestamp and no prior cache entry - treat as changed and seed the cache $Changed = $true + Add-CIPPAzDataTableEntity @TrackingTable -Entity @{ + PartitionKey = $TenantFilter + RowKey = $Result.id + LatestPolicyId = $GraphId + PolicyCount = $GraphCount + } -Force | Out-Null } $PolicyTimestamps[$Result.id] = $Changed + Write-Host "POLICY TYPE CHANGE CHECK: $($Result.id) -> Changed=$Changed (GraphCount=$GraphCount, CachedCount=$($Cached.PolicyCount), IdChanged=$IdChanged)" } # Filter unchanged templates $TemplateTable = Get-CippTable -tablename 'templates' - $StandardTemplateTable = Get-CippTable -tablename 'templates' $IntuneKeys = @($ComputedStandards.Keys | Where-Object { $_ -like '*IntuneTemplate*' }) + Write-Host "INTUNE FILTER: Processing $($IntuneKeys.Count) IntuneTemplate standards for $TenantFilter" + + # Build compliance lookup - keyed by "standards.IntuneTemplate." + $IntuneComplianceLookup = @{} + try { + $AlignmentResults = Get-CIPPTenantAlignment -TenantFilter $TenantFilter + foreach ($AlignmentResult in $AlignmentResults) { + foreach ($Detail in $AlignmentResult.ComparisonDetails) { + if ($Detail.StandardName -like 'standards.IntuneTemplate.*') { + $IntuneComplianceLookup[$Detail.StandardName] = $Detail.Compliant + } + } + } + } catch { + Write-Warning "Failed to get tenant alignment data for $TenantFilter : $($_.Exception.Message)" + } + Write-Host "COMPLIANCE LOOKUP: Found $($IntuneComplianceLookup.Count) IntuneTemplate entries in alignment data" foreach ($Key in $IntuneKeys) { $Template = $ComputedStandards[$Key] $TemplateEntity = Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq 'IntuneTemplate' and RowKey eq '$($Template.Settings.TemplateList.value)'" - if (-not $TemplateEntity) { continue } + if (-not $TemplateEntity) { + Write-Host "SKIP: $Key - no IntuneTemplate entity found for RowKey '$($Template.Settings.TemplateList.value)'" + continue + } $ParsedTemplate = $TemplateEntity.JSON | ConvertFrom-Json - if (-not $ParsedTemplate.Type) { continue } + if (-not $ParsedTemplate.Type) { + Write-Host "SKIP: $Key - template has no Type property" + continue + } $PolicyType = $ParsedTemplate.Type $PolicyChanged = if ($PolicyType -eq 'AppProtection') { @@ -144,9 +188,10 @@ function Push-CIPPStandardsList { } else { [bool]$PolicyTimestamps[$PolicyType] } + Write-Host "TEMPLATE CHECK: $Key | PolicyType=$PolicyType | PolicyChanged=$PolicyChanged" # Check StandardTemplate changes - $StandardTemplate = Get-CIPPAzDataTableEntity @StandardTemplateTable -Filter "PartitionKey eq 'StandardsTemplateV2' and RowKey eq '$($Template.TemplateId)'" + $StandardTemplate = Get-CIPPAzDataTableEntity @TemplateTable -Filter "PartitionKey eq 'StandardsTemplateV2' and RowKey eq '$($Template.TemplateId)'" $StandardTemplateChanged = $false if ($StandardTemplate) { @@ -157,8 +202,10 @@ function Push-CIPPStandardsList { $CachedStandardTimeUtc = ([DateTimeOffset]$CachedStandardTemplate.CachedTimestamp).UtcDateTime $TimeDiff = [Math]::Abs(($StandardTimeUtc - $CachedStandardTimeUtc).TotalSeconds) $StandardTemplateChanged = ($TimeDiff -gt 60) + Write-Host "STDTEMPLATE CHECK: TemplateId=$($Template.TemplateId) | TimeDiff=${TimeDiff}s | Changed=$StandardTemplateChanged" } else { $StandardTemplateChanged = $true + Write-Host "STDTEMPLATE CHECK: TemplateId=$($Template.TemplateId) | No cached timestamp - treating as changed" } Add-CIPPAzDataTableEntity @TrackingTable -Entity @{ @@ -168,10 +215,21 @@ function Push-CIPPStandardsList { } -Force | Out-Null } - # Remove if both unchanged if (-not $PolicyChanged -and -not $StandardTemplateChanged) { - Write-Host "NO INTUNE CHANGE: Filtering out $key for $($TenantFilter)" - [void]$ComputedStandards.Remove($Key) + $AlignmentKey = "standards.IntuneTemplate.$($Template.Settings.TemplateList.value)" + $IsDeployed = $IntuneComplianceLookup.ContainsKey($AlignmentKey) + $IsCompliant = $IsDeployed -and ($IntuneComplianceLookup[$AlignmentKey] -eq $true) + Write-Host "COMPLIANCE CHECK: $AlignmentKey | InLookup=$IsDeployed | Compliant=$IsCompliant | LookupValue=$($IntuneComplianceLookup[$AlignmentKey])" + + if ($IsCompliant) { + # Policy unchanged and compliant - no action needed + Write-Host "NO INTUNE CHANGE: Filtering out $Key for $TenantFilter (compliant)" + [void]$ComputedStandards.Remove($Key) + } else { + Write-Host "KEEPING: $Key - not compliant or not in lookup (InLookup=$IsDeployed, Compliant=$IsCompliant)" + } + } else { + Write-Host "KEEPING: $Key - changed (PolicyChanged=$PolicyChanged, StdTemplateChanged=$StandardTemplateChanged)" } } } catch { @@ -223,9 +281,8 @@ function Push-CIPPStandardsList { FunctionName = 'CIPPStandard' } } - Write-Host "Sending back $($FilteredStandards.Count) standards: $($FilteredStandards | ConvertTo-Json -Depth 5 -Compress)" - return $FilteredStandards - + Write-Host "Sending back $($FilteredStandards.Count) standards" + return @($FilteredStandards) } catch { Write-Warning "Error listing standards for $TenantFilter : $($_.Exception.Message)" return @() diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 index 34ef1b3e1029..257721c1471e 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecCIPPDBCache.ps1 @@ -89,7 +89,7 @@ function Invoke-ExecCIPPDBCache { Write-LogMessage -Headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Starting CIPP DB cache for $Name on tenant $TenantFilter" -sev Info } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject $ResultsMessage = if ($TenantFilter -eq 'AllTenants') { "Successfully started cache operation for $Name for all tenants" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 index a492cba9d733..21335f2b64f1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 @@ -10,12 +10,10 @@ function Invoke-ExecSetCIPPAutoBackup { $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds if ($Request.Body.Enabled -eq $true) { $Table = Get-CIPPTable -TableName 'ScheduledTasks' - $AutomatedCIPPBackupTask = Get-AzDataTableEntity @table -Filter "Name eq 'Automated CIPP Backup'" - $task = @{ - RowKey = $AutomatedCIPPBackupTask.RowKey - PartitionKey = 'ScheduledTask' + $AutomatedCIPPBackupTask = Get-AzDataTableEntity @table -Filter "Name eq 'Automated CIPP Backup'" -Property RowKey, PartitionKey, ETag + if ($AutomatedCIPPBackupTask) { + Remove-AzDataTableEntity -Force @Table -Entity $AutomatedCIPPBackupTask | Out-Null } - Remove-AzDataTableEntity -Force @Table -Entity $task | Out-Null $TaskBody = [pscustomobject]@{ TenantFilter = 'PartnerTenant' @@ -28,8 +26,18 @@ function Invoke-ExecSetCIPPAutoBackup { ScheduledTime = $unixtime Recurrence = '1d' } - Add-CIPPScheduledTask -Task $TaskBody -hidden $false + Add-CIPPScheduledTask -Task $TaskBody -hidden $false -DisallowDuplicateName $true $Result = @{ 'Results' = 'Scheduled Task Successfully created' } + } elseif ($Request.Body.Enabled -eq $false) { + $Table = Get-CIPPTable -TableName 'ScheduledTasks' + $AutomatedCIPPBackupTask = Get-AzDataTableEntity @table -Filter "Name eq 'Automated CIPP Backup'" -Property RowKey, PartitionKey, ETag + if ($AutomatedCIPPBackupTask) { + Remove-AzDataTableEntity -Force @Table -Entity $AutomatedCIPPBackupTask | Out-Null + $Result = @{ 'Results' = 'Scheduled Task Successfully removed' } + } else { + $Result = @{ 'Results' = 'No existing scheduled task found to remove' } + } + } Write-LogMessage -headers $Request.Headers -API $Request.Params.CIPPEndpoint -message 'Scheduled automatic CIPP backups' -Sev 'Info' return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 index a086b28cd4ae..066dda75515a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/invoke-ListEmptyResults.ps1 @@ -1,13 +1,13 @@ using namespace System.Net -Function invoke-ListEmptyResults { +function invoke-ListEmptyResults { <# .SYNOPSIS - Purposely lists an empty result .FUNCTIONALITY Entrypoint,AnyTenant .ROLE - CIPP.Core + CIPP.Core.Read #> [CmdletBinding()] param($Request, $TriggerMetadata) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 index f9b0927b2841..8cdaf3000eb9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionMapping.ps1 @@ -100,7 +100,7 @@ Function Invoke-ExecExtensionMapping { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" $Result = 'AutoMapping Request has been queued. Exact name matches will appear first and matches on device names and serials will take longer. Please check the CIPP Logbook and refresh the page once complete.' } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionSync.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionSync.ps1 index a268c3075aae..797bf04ec555 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionSync.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Extensions/Invoke-ExecExtensionSync.ps1 @@ -18,13 +18,8 @@ Function Invoke-ExecExtensionSync { switch ($ConfigItem) { 'Gradient' { If ($Configuration.Gradient.enabled -and $Configuration.Gradient.BillingEnabled) { - $ProcessorQueue = Get-CIPPTable -TableName 'ProcessorQueue' - $ProcessorFunction = [PSCustomObject]@{ - PartitionKey = 'Function' - RowKey = 'New-GradientServiceSyncRun' - FunctionName = 'New-GradientServiceSyncRun' - } - Add-AzDataTableEntity @ProcessorQueue -Entity $ProcessorFunction -Force + # Queue the sync function for immediate execution + Add-CippQueueMessage -Cmdlet 'New-GradientServiceSyncRun' -Parameters @{} $Results = [pscustomobject]@{'Results' = 'Successfully queued Gradient Sync' } } } @@ -58,7 +53,7 @@ Function Invoke-ExecExtensionSync { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queued for $($Tenant.IntegrationName)" } } else { @@ -75,7 +70,7 @@ Function Invoke-ExecExtensionSync { Batch = @($Batch) } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queuing $(($TenantsToProcess | Measure-Object).count) Tenants" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 index 37ae04aa3a21..2062a5e8f64b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-ListScheduledItems.ps1 @@ -63,7 +63,7 @@ function Invoke-ListScheduledItems { Write-Information "Found $($Tasks.Count) scheduled tasks after filtering and access check." $ScheduledTasks = foreach ($Task in $Tasks) { - if (!$Task.Tenant -or !$Task.Command) { + if (!$Task.Command) { Write-Information "Skipping invalid scheduled task entry: $($Task.RowKey)" continue } @@ -105,10 +105,14 @@ function Invoke-ListScheduledItems { # Fall back to keeping original tenant value } } else { - $Task.Tenant = [PSCustomObject]@{ - label = $Task.Tenant - value = $Task.Tenant - type = 'Tenant' + if (!$Task.Tenant) { + $Task | Add-Member -NotePropertyName Tenant -NotePropertyValue 'None' -Force + } else { + $Task.Tenant = [PSCustomObject]@{ + label = $Task.Tenant + value = $Task.Tenant + type = 'Tenant' + } } } if ($Task.Trigger) { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 index eb7dc7273680..a9b5561dce5d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecApiClient.ps1 @@ -99,12 +99,16 @@ function Invoke-ExecApiClient { } } 'GetAzureConfiguration' { - $Owner = $env:WEBSITE_OWNER_NAME - Write-Information "Owner: $Owner" - if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { - $RGName = $Matches.RGName - } else { + if ($env:WEBSITE_RESOURCE_GROUP) { $RGName = $env:WEBSITE_RESOURCE_GROUP + } else { + $Owner = $env:WEBSITE_OWNER_NAME + if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $RGName = $Matches.RGName + } else { + Write-Information "Could not determine resource group from environment variables. Owner: $Owner" + $RGName = $null + } } $FunctionAppName = $env:WEBSITE_SITE_NAME try { @@ -122,11 +126,16 @@ function Invoke-ExecApiClient { } 'SaveToAzure' { $TenantId = $env:TenantID - $Owner = $env:WEBSITE_OWNER_NAME - if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { - $RGName = $Matches.RGName - } else { + if ($env:WEBSITE_RESOURCE_GROUP) { $RGName = $env:WEBSITE_RESOURCE_GROUP + } else { + $Owner = $env:WEBSITE_OWNER_NAME + if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $RGName = $Matches.RGName + } else { + Write-Information "Could not determine resource group from environment variables. Owner: $Owner" + $RGName = $null + } } $FunctionAppName = $env:WEBSITE_SITE_NAME $AllClients = Get-CIPPAzDataTableEntity @Table -Filter 'Enabled eq true' | Where-Object { ![string]::IsNullOrEmpty($_.RowKey) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 index 699550f4cf69..d5860fe89c78 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecBackendURLs.ps1 @@ -13,11 +13,16 @@ function Invoke-ExecBackendURLs { # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' - $Owner = $env:WEBSITE_OWNER_NAME - if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { - $RGName = $Matches.RGName - } else { + if ($env:WEBSITE_RESOURCE_GROUP) { $RGName = $env:WEBSITE_RESOURCE_GROUP + } else { + $Owner = $env:WEBSITE_OWNER_NAME + if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $RGName = $Matches.RGName + } else { + Write-Information "Could not determine resource group from environment variables. Owner: $Owner" + $RGName = $null + } } $results = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecPartnerMode.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecPartnerMode.ps1 index 3703b4c278c3..9c2b9ea49dc5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecPartnerMode.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecPartnerMode.ps1 @@ -36,7 +36,7 @@ function Invoke-ExecPartnerMode { OrchestratorName = 'UpdateTenants' SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $InputObject } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 index 61a133046e41..4623b0c4d7fd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecRestoreBackup.ps1 @@ -29,16 +29,30 @@ function Invoke-ExecRestoreBackup { throw "Failed to parse backup JSON: $($_.Exception.Message)" } + $SelectedTypes = $Request.Body.SelectedTypes + if ($SelectedTypes -and $SelectedTypes.Count -gt 0) { + $BackupData = $BackupData | Where-Object { + $item = $_ + if ($item.table -eq 'templates') { + $typeKey = "templates:$($item.PartitionKey)" + } else { + $typeKey = $item.table + } + $SelectedTypes -contains $typeKey + } + } + $RestoredCount = 0 $BackupData | ForEach-Object { $Table = Get-CippTable -tablename $_.table $ht2 = @{} $_.psobject.properties | ForEach-Object { $ht2[$_.Name] = [string]$_.Value } $Table.Entity = $ht2 - Add-CIPPAzDataTableEntity @Table -Force + Add-AzDataTableEntity @Table -Force + $RestoredCount++ } - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Restored backup $($Request.Body.BackupName)" -Sev 'Info' + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Restored backup $($Request.Body.BackupName) - $RestoredCount rows restored" -Sev 'Info' $body = [pscustomobject]@{ - 'Results' = 'Successfully restored backup.' + 'Results' = "Successfully restored $RestoredCount rows from backup." } } else { $body = [pscustomobject]@{ @@ -46,17 +60,19 @@ function Invoke-ExecRestoreBackup { } } } else { + $RestoredCount = 0 foreach ($line in ($Request.body | Select-Object * -ExcludeProperty ETag, Timestamp)) { $Table = Get-CippTable -tablename $line.table $ht2 = @{} $line.psobject.properties | ForEach-Object { $ht2[$_.Name] = [string]$_.Value } $Table.Entity = $ht2 Add-AzDataTableEntity @Table -Force + $RestoredCount++ } - Write-LogMessage -headers $Request.Headers -API $APINAME -message "Restored backup $($Request.Body.BackupName)" -Sev 'Info' + Write-LogMessage -headers $Request.Headers -API $APINAME -message "Restored backup - $RestoredCount rows restored" -Sev 'Info' $body = [pscustomobject]@{ - 'Results' = 'Successfully restored backup.' + 'Results' = "Successfully restored $RestoredCount rows from backup." } } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 index ef058b8a94b2..819e8e32f1fb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecTimeSettings.ps1 @@ -10,12 +10,16 @@ function Invoke-ExecTimeSettings { try { $Subscription = Get-CIPPAzFunctionAppSubId - $Owner = $env:WEBSITE_OWNER_NAME - - if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { - $RGName = $Matches.RGName - } else { + if ($env:WEBSITE_RESOURCE_GROUP) { $RGName = $env:WEBSITE_RESOURCE_GROUP + } else { + $Owner = $env:WEBSITE_OWNER_NAME + if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $RGName = $Matches.RGName + } else { + Write-Information "Could not determine resource group from environment variables. Owner: $Owner" + $RGName = $null + } } $FunctionName = $env:WEBSITE_SITE_NAME diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 index f1345a069ea6..b3e7dd951d00 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecAddTenant.ps1 @@ -103,7 +103,7 @@ function Invoke-ExecAddTenant { OrchestratorName = 'UpdatePermissionsOrchestrator' Batch = @($TenantBatch) } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started permissions update orchestrator for $displayName" } catch { Write-Warning "Failed to start permissions orchestrator: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 index 8725c2c87de1..e8602931b9b0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecUpdateRefreshToken.ps1 @@ -51,7 +51,7 @@ function Invoke-ExecUpdateRefreshToken { OrchestratorName = 'UpdatePermissionsOrchestrator' Batch = @($TenantBatch) } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject Write-Information 'Started permissions update orchestrator for Partner Tenant' } catch { Write-Warning "Failed to start permissions orchestrator: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 index 00d696892b7c..aa7996657fee 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecEditMailboxPermissions.ps1 @@ -8,6 +8,9 @@ function Invoke-ExecEditMailboxPermissions { [CmdletBinding()] param($Request, $TriggerMetadata) + + # This endpoint is not called in the frontend at all. This can only be called manually via the scheduler, via the API, or via the CIPPAPIModule -Bobby + $APIName = $Request.Params.CIPPEndpoint $Headers = $Request.Headers Write-LogMessage -headers $Headers -API $APINAME-message 'Accessed this API' -Sev 'Debug' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleMailboxVacation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleMailboxVacation.ps1 new file mode 100644 index 000000000000..04eac0b8393e --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleMailboxVacation.ps1 @@ -0,0 +1,139 @@ +function Invoke-ExecScheduleMailboxVacation { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $TenantFilter = $Request.Body.tenantFilter + $MailboxOwners = @($Request.Body.mailboxOwners) + $Delegates = @($Request.Body.delegates) + $PermissionTypes = @($Request.Body.permissionTypes) + $AutoMap = if ($null -ne $Request.Body.autoMap) { [bool]$Request.Body.autoMap } else { $true } + $IncludeCalendar = [bool]$Request.Body.includeCalendar + $CalendarPermission = $Request.Body.calendarPermission.value ?? $Request.Body.calendarPermission + $CanViewPrivateItems = [bool]$Request.Body.canViewPrivateItems + $StartDate = $Request.Body.startDate + $EndDate = $Request.Body.endDate + + # Extract UPNs from addedFields + $OwnerUPNs = @($MailboxOwners | ForEach-Object { $_.addedFields.userPrincipalName ?? $_.value }) + $DelegateUPNs = @($Delegates | ForEach-Object { $_.addedFields.userPrincipalName ?? $_.value }) + + if ($OwnerUPNs.Count -eq 0 -or $DelegateUPNs.Count -eq 0 -or $PermissionTypes.Count -eq 0) { + throw 'Mailbox owners, delegates, and permission types are required.' + } + + # Build mailbox permissions array: Cartesian product of owners x delegates x permissionTypes + $MailboxPermissions = @(foreach ($owner in $OwnerUPNs) { + foreach ($delegate in $DelegateUPNs) { + foreach ($permType in $PermissionTypes) { + $level = $permType.value ?? $permType + [PSCustomObject]@{ + UserId = $owner + AccessUser = $delegate + PermissionLevel = $level + AutoMap = $AutoMap + } + } + } + }) + + # Build calendar permissions array if requested + $CalendarPermissions = @() + if ($IncludeCalendar -and $CalendarPermission) { + $CalendarPermissions = @(foreach ($owner in $OwnerUPNs) { + # Resolve the calendar folder name for this owner's mailbox locale at schedule time. + $FolderStats = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -cmdParams @{ + Identity = $owner + FolderScope = 'Calendar' + } -Anchor $owner | Where-Object { $_.FolderType -eq 'Calendar' } + $CalFolderName = if ($FolderStats) { $FolderStats.Name } else { 'Calendar' } + + foreach ($delegate in $DelegateUPNs) { + [PSCustomObject]@{ + UserID = $owner + UserToGetPermissions = $delegate + FolderName = $CalFolderName + Permissions = $CalendarPermission + CanViewPrivateItems = $CanViewPrivateItems + } + } + }) + } + + # Build display names for task naming + $OwnerDisplay = ($OwnerUPNs | Select-Object -First 3) -join ', ' + if ($OwnerUPNs.Count -gt 3) { $OwnerDisplay += " (+$($OwnerUPNs.Count - 3) more)" } + $DelegateDisplay = ($DelegateUPNs | Select-Object -First 3) -join ', ' + if ($DelegateUPNs.Count -gt 3) { $DelegateDisplay += " (+$($DelegateUPNs.Count - 3) more)" } + + # Create Add task + $AddParameters = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Action = 'Add' + MailboxPermissions = $MailboxPermissions + CalendarPermissions = $CalendarPermissions + APIName = $APIName + } + + $AddTaskBody = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Name = "Add Mailbox Vacation Mode: $DelegateDisplay -> $OwnerDisplay" + Command = @{ + value = 'Set-CIPPMailboxVacation' + label = 'Set-CIPPMailboxVacation' + } + Parameters = $AddParameters + ScheduledTime = [int64]$StartDate + PostExecution = $Request.Body.postExecution + Reference = $Request.Body.reference + } + + Add-CIPPScheduledTask -Task $AddTaskBody -hidden $false + + # Create Remove task (separate Parameters object to avoid reference mutation) + $RemoveParameters = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Action = 'Remove' + MailboxPermissions = $MailboxPermissions + CalendarPermissions = $CalendarPermissions + APIName = $APIName + } + + $RemoveTaskBody = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Name = "Remove Mailbox Vacation Mode: $DelegateDisplay -> $OwnerDisplay" + Command = @{ + value = 'Set-CIPPMailboxVacation' + label = 'Set-CIPPMailboxVacation' + } + Parameters = $RemoveParameters + ScheduledTime = [int64]$EndDate + PostExecution = $Request.Body.postExecution + Reference = $Request.Body.reference + } + + Add-CIPPScheduledTask -Task $RemoveTaskBody -hidden $false + + $Result = "Successfully scheduled mailbox vacation mode for $DelegateDisplay -> $OwnerDisplay." + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to schedule mailbox vacation mode: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev 'Error' -tenant $TenantFilter -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 new file mode 100644 index 000000000000..22582d8ac03f --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ExecScheduleOOOVacation.ps1 @@ -0,0 +1,77 @@ +function Invoke-ExecScheduleOOOVacation { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Exchange.Mailbox.ReadWrite + #> + param($Request, $TriggerMetadata) + + $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers + + try { + $TenantFilter = $Request.Body.tenantFilter + $Users = @($Request.Body.Users) + $InternalMessage = $Request.Body.internalMessage + $ExternalMessage = $Request.Body.externalMessage + $StartDate = $Request.Body.startDate + $EndDate = $Request.Body.endDate + + # Extract UPNs — Users arrive as autocomplete option objects with addedFields + $UserUPNs = @($Users | ForEach-Object { $_.addedFields.userPrincipalName ?? $_.value }) + + if ($UserUPNs.Count -eq 0) { throw 'At least one user is required.' } + + $UserDisplay = ($UserUPNs | Select-Object -First 3) -join ', ' + if ($UserUPNs.Count -gt 3) { $UserDisplay += " (+$($UserUPNs.Count - 3) more)" } + + $SharedParams = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Users = $UserUPNs + InternalMessage = $InternalMessage + ExternalMessage = $ExternalMessage + APIName = $APIName + } + + # Add task — enables OOO with messages at start date + Add-CIPPScheduledTask -Task ([PSCustomObject]@{ + TenantFilter = $TenantFilter + Name = "Add OOO Vacation Mode: $UserDisplay" + Command = @{ value = 'Set-CIPPVacationOOO'; label = 'Set-CIPPVacationOOO' } + Parameters = ($SharedParams | Select-Object *, @{ n = 'Action'; e = { 'Add' } }) + ScheduledTime = [int64]$StartDate + PostExecution = $Request.Body.postExecution + Reference = $Request.Body.reference + }) -hidden $false + + # Remove task — disables OOO at end date (no messages — preserve user's own updates) + Add-CIPPScheduledTask -Task ([PSCustomObject]@{ + TenantFilter = $TenantFilter + Name = "Remove OOO Vacation Mode: $UserDisplay" + Command = @{ value = 'Set-CIPPVacationOOO'; label = 'Set-CIPPVacationOOO' } + Parameters = [PSCustomObject]@{ + TenantFilter = $TenantFilter + Users = $UserUPNs + Action = 'Remove' + APIName = $APIName + } + ScheduledTime = [int64]$EndDate + PostExecution = $Request.Body.postExecution + Reference = $Request.Body.reference + }) -hidden $false + + $Result = "Successfully scheduled OOO vacation mode for $UserDisplay." + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Failed to schedule OOO vacation mode: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Error -tenant $TenantFilter -LogData $ErrorMessage + $StatusCode = [HttpStatusCode]::InternalServerError + } + + return ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = @{ Results = $Result } + }) +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 index c20d575be6bb..007f8ad84865 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Administration/Invoke-ListMailboxRules.ps1 @@ -75,7 +75,7 @@ function Invoke-ListMailboxRules { SkipLog = $true } #Write-Host ($InputObject | ConvertTo-Json) - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started mailbox rules orchestration with ID = '$InstanceId'" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 index 7780f90588eb..f34c71a1cc66 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Spamfilter/Invoke-ListMailQuarantine.ps1 @@ -47,7 +47,7 @@ function Invoke-ListMailQuarantine { } SkipLog = $true } - $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $null = Start-CIPPOrchestrator -InputObject $InputObject } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 index 0783685af1da..1a9129360357 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-AddEditTransportRule.ps1 @@ -54,7 +54,13 @@ function Invoke-AddEditTransportRule { $FromAddressMatchesPatterns = $Request.Body.FromAddressMatchesPatterns $AttachmentContainsWords = $Request.Body.AttachmentContainsWords $AttachmentMatchesPatterns = $Request.Body.AttachmentMatchesPatterns + $AttachmentNameMatchesPatterns = $Request.Body.AttachmentNameMatchesPatterns + $AttachmentPropertyContainsWords = $Request.Body.AttachmentPropertyContainsWords $AttachmentExtensionMatchesWords = $Request.Body.AttachmentExtensionMatchesWords + $AttachmentHasExecutableContent = $Request.Body.AttachmentHasExecutableContent + $AttachmentIsPasswordProtected = $Request.Body.AttachmentIsPasswordProtected + $AttachmentIsUnsupported = $Request.Body.AttachmentIsUnsupported + $AttachmentProcessingLimitExceeded = $Request.Body.AttachmentProcessingLimitExceeded $AttachmentSizeOver = $Request.Body.AttachmentSizeOver $MessageSizeOver = $Request.Body.MessageSizeOver $SCLOver = $Request.Body.SCLOver @@ -99,6 +105,7 @@ function Invoke-AddEditTransportRule { $ApplyHtmlDisclaimerLocation = $Request.Body.ApplyHtmlDisclaimerLocation $ApplyHtmlDisclaimerFallbackAction = $Request.Body.ApplyHtmlDisclaimerFallbackAction $GenerateIncidentReport = $Request.Body.GenerateIncidentReport + $IncidentReportContent = $Request.Body.IncidentReportContent $GenerateNotification = $Request.Body.GenerateNotification $ApplyOME = $Request.Body.ApplyOME @@ -117,7 +124,13 @@ function Invoke-AddEditTransportRule { $ExceptIfFromAddressMatchesPatterns = $Request.Body.ExceptIfFromAddressMatchesPatterns $ExceptIfAttachmentContainsWords = $Request.Body.ExceptIfAttachmentContainsWords $ExceptIfAttachmentMatchesPatterns = $Request.Body.ExceptIfAttachmentMatchesPatterns + $ExceptIfAttachmentNameMatchesPatterns = $Request.Body.ExceptIfAttachmentNameMatchesPatterns + $ExceptIfAttachmentPropertyContainsWords = $Request.Body.ExceptIfAttachmentPropertyContainsWords $ExceptIfAttachmentExtensionMatchesWords = $Request.Body.ExceptIfAttachmentExtensionMatchesWords + $ExceptIfAttachmentHasExecutableContent = $Request.Body.ExceptIfAttachmentHasExecutableContent + $ExceptIfAttachmentIsPasswordProtected = $Request.Body.ExceptIfAttachmentIsPasswordProtected + $ExceptIfAttachmentIsUnsupported = $Request.Body.ExceptIfAttachmentIsUnsupported + $ExceptIfAttachmentProcessingLimitExceeded = $Request.Body.ExceptIfAttachmentProcessingLimitExceeded $ExceptIfAttachmentSizeOver = $Request.Body.ExceptIfAttachmentSizeOver $ExceptIfMessageSizeOver = $Request.Body.ExceptIfMessageSizeOver $ExceptIfSCLOver = $Request.Body.ExceptIfSCLOver @@ -266,6 +279,8 @@ function Invoke-AddEditTransportRule { $FromAddressMatchesPatterns = Process-TextArrayField -Field $FromAddressMatchesPatterns $AttachmentContainsWords = Process-TextArrayField -Field $AttachmentContainsWords $AttachmentMatchesPatterns = Process-TextArrayField -Field $AttachmentMatchesPatterns + $AttachmentNameMatchesPatterns = Process-TextArrayField -Field $AttachmentNameMatchesPatterns + $AttachmentPropertyContainsWords = Process-TextArrayField -Field $AttachmentPropertyContainsWords $AttachmentExtensionMatchesWords = Process-TextArrayField -Field $AttachmentExtensionMatchesWords $RecipientAddressContainsWords = Process-TextArrayField -Field $RecipientAddressContainsWords $RecipientAddressMatchesPatterns = Process-TextArrayField -Field $RecipientAddressMatchesPatterns @@ -273,6 +288,7 @@ function Invoke-AddEditTransportRule { $AnyOfRecipientAddressMatchesPatterns = Process-TextArrayField -Field $AnyOfRecipientAddressMatchesPatterns $HeaderContainsWords = Process-TextArrayField -Field $HeaderContainsWords $HeaderMatchesPatterns = Process-TextArrayField -Field $HeaderMatchesPatterns + $IncidentReportContent = Process-TextArrayField -Field $IncidentReportContent # Process exception text array fields $ExceptIfSubjectContainsWords = Process-TextArrayField -Field $ExceptIfSubjectContainsWords @@ -283,6 +299,8 @@ function Invoke-AddEditTransportRule { $ExceptIfFromAddressMatchesPatterns = Process-TextArrayField -Field $ExceptIfFromAddressMatchesPatterns $ExceptIfAttachmentContainsWords = Process-TextArrayField -Field $ExceptIfAttachmentContainsWords $ExceptIfAttachmentMatchesPatterns = Process-TextArrayField -Field $ExceptIfAttachmentMatchesPatterns + $ExceptIfAttachmentNameMatchesPatterns = Process-TextArrayField -Field $ExceptIfAttachmentNameMatchesPatterns + $ExceptIfAttachmentPropertyContainsWords = Process-TextArrayField -Field $ExceptIfAttachmentPropertyContainsWords $ExceptIfAttachmentExtensionMatchesWords = Process-TextArrayField -Field $ExceptIfAttachmentExtensionMatchesWords $ExceptIfRecipientAddressContainsWords = Process-TextArrayField -Field $ExceptIfRecipientAddressContainsWords $ExceptIfRecipientAddressMatchesPatterns = Process-TextArrayField -Field $ExceptIfRecipientAddressMatchesPatterns @@ -366,9 +384,27 @@ function Invoke-AddEditTransportRule { if ($null -ne $AttachmentMatchesPatterns -and $AttachmentMatchesPatterns.Count -gt 0) { $ruleParams.Add('AttachmentMatchesPatterns', $AttachmentMatchesPatterns) } + if ($null -ne $AttachmentNameMatchesPatterns -and $AttachmentNameMatchesPatterns.Count -gt 0) { + $ruleParams.Add('AttachmentNameMatchesPatterns', $AttachmentNameMatchesPatterns) + } + if ($null -ne $AttachmentPropertyContainsWords -and $AttachmentPropertyContainsWords.Count -gt 0) { + $ruleParams.Add('AttachmentPropertyContainsWords', $AttachmentPropertyContainsWords) + } if ($null -ne $AttachmentExtensionMatchesWords -and $AttachmentExtensionMatchesWords.Count -gt 0) { $ruleParams.Add('AttachmentExtensionMatchesWords', $AttachmentExtensionMatchesWords) } + if ($null -ne $AttachmentHasExecutableContent) { + $ruleParams.Add('AttachmentHasExecutableContent', $AttachmentHasExecutableContent) + } + if ($null -ne $AttachmentIsPasswordProtected) { + $ruleParams.Add('AttachmentIsPasswordProtected', $AttachmentIsPasswordProtected) + } + if ($null -ne $AttachmentIsUnsupported) { + $ruleParams.Add('AttachmentIsUnsupported', $AttachmentIsUnsupported) + } + if ($null -ne $AttachmentProcessingLimitExceeded) { + $ruleParams.Add('AttachmentProcessingLimitExceeded', $AttachmentProcessingLimitExceeded) + } if ($null -ne $AttachmentSizeOver) { $ruleParams.Add('AttachmentSizeOver', $AttachmentSizeOver) } if ($null -ne $MessageSizeOver) { $ruleParams.Add('MessageSizeOver', $MessageSizeOver) } if ($null -ne $SCLOver) { @@ -476,6 +512,9 @@ function Invoke-AddEditTransportRule { } if ($null -ne $GenerateIncidentReport -and $GenerateIncidentReport.Count -gt 0) { $ruleParams.Add('GenerateIncidentReport', $GenerateIncidentReport) + if ($null -ne $IncidentReportContent -and $IncidentReportContent.Count -gt 0) { + $ruleParams.Add('IncidentReportContent', $IncidentReportContent) + } } if ($null -ne $GenerateNotification -and $GenerateNotification -ne '') { $ruleParams.Add('GenerateNotification', $GenerateNotification) @@ -519,9 +558,27 @@ function Invoke-AddEditTransportRule { if ($null -ne $ExceptIfAttachmentMatchesPatterns -and $ExceptIfAttachmentMatchesPatterns.Count -gt 0) { $ruleParams.Add('ExceptIfAttachmentMatchesPatterns', $ExceptIfAttachmentMatchesPatterns) } + if ($null -ne $ExceptIfAttachmentNameMatchesPatterns -and $ExceptIfAttachmentNameMatchesPatterns.Count -gt 0) { + $ruleParams.Add('ExceptIfAttachmentNameMatchesPatterns', $ExceptIfAttachmentNameMatchesPatterns) + } + if ($null -ne $ExceptIfAttachmentPropertyContainsWords -and $ExceptIfAttachmentPropertyContainsWords.Count -gt 0) { + $ruleParams.Add('ExceptIfAttachmentPropertyContainsWords', $ExceptIfAttachmentPropertyContainsWords) + } if ($null -ne $ExceptIfAttachmentExtensionMatchesWords -and $ExceptIfAttachmentExtensionMatchesWords.Count -gt 0) { $ruleParams.Add('ExceptIfAttachmentExtensionMatchesWords', $ExceptIfAttachmentExtensionMatchesWords) } + if ($null -ne $ExceptIfAttachmentHasExecutableContent) { + $ruleParams.Add('ExceptIfAttachmentHasExecutableContent', $ExceptIfAttachmentHasExecutableContent) + } + if ($null -ne $ExceptIfAttachmentIsPasswordProtected) { + $ruleParams.Add('ExceptIfAttachmentIsPasswordProtected', $ExceptIfAttachmentIsPasswordProtected) + } + if ($null -ne $ExceptIfAttachmentIsUnsupported) { + $ruleParams.Add('ExceptIfAttachmentIsUnsupported', $ExceptIfAttachmentIsUnsupported) + } + if ($null -ne $ExceptIfAttachmentProcessingLimitExceeded) { + $ruleParams.Add('ExceptIfAttachmentProcessingLimitExceeded', $ExceptIfAttachmentProcessingLimitExceeded) + } if ($null -ne $ExceptIfAttachmentSizeOver) { $ruleParams.Add('ExceptIfAttachmentSizeOver', $ExceptIfAttachmentSizeOver) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 index ce444bd46cf2..82d324069783 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Email-Exchange/Transport/Invoke-ListTransportRules.ps1 @@ -55,7 +55,7 @@ function Invoke-ListTransportRules { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + Start-CIPPOrchestrator -InputObject $InputObject | Out-Null } else { # Return cached data $Metadata = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 index a1dcb247e382..3e555a136954 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAppUpload.ps1 @@ -8,27 +8,12 @@ function Invoke-ExecAppUpload { [CmdletBinding()] param($Request, $TriggerMetadata) - $ConfigTable = Get-CIPPTable -tablename Config - $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" - - if ($Config -and $Config.state -eq $true) { - if ($env:CIPP_PROCESSOR -ne 'true') { - $ProcessorFunction = [PSCustomObject]@{ - PartitionKey = 'Function' - RowKey = 'Start-ApplicationOrchestrator' - FunctionName = 'Start-ApplicationOrchestrator' - } - $ProcessorQueue = Get-CIPPTable -TableName 'ProcessorQueue' - Add-AzDataTableEntity @ProcessorQueue -Entity $ProcessorFunction -Force - $Results = [pscustomobject]@{'Results' = 'Application upload job has started. Please check back in 15 minutes or track the logbook for results.' } - } - } else { - try { - Start-ApplicationOrchestrator - $Results = [pscustomobject]@{'Results' = 'Started application upload' } - } catch { - $Results = [pscustomobject]@{'Results' = "Failed to start application upload. Error: $($_.Exception.Message)" } - } + try { + # Start the orchestrator directly - it handles queuing internally + Start-ApplicationOrchestrator + $Results = [pscustomobject]@{'Results' = 'Application upload job has started. Track the logbook for results.' } + } catch { + $Results = [pscustomobject]@{'Results' = "Failed to start application upload. Error: $($_.Exception.Message)" } } return ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 index 6733e150366f..5639605b1ffb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-ExecAssignApp.ps1 @@ -21,6 +21,8 @@ function Invoke-ExecAssignApp { $GroupNamesRaw = $Request.Query.GroupNames ?? $Request.Body.GroupNames $GroupIdsRaw = $Request.Query.GroupIds ?? $Request.Body.GroupIds $AssignmentMode = $Request.Body.assignmentMode + $AssignmentFilterName = $Request.Body.AssignmentFilterName + $AssignmentFilterType = $Request.Body.AssignmentFilterType $Intent = if ([string]::IsNullOrWhiteSpace($Intent)) { 'Required' } else { $Intent } @@ -96,6 +98,13 @@ function Invoke-ExecAssignApp { $setParams.GroupIds = $GroupIds } + if (-not [string]::IsNullOrWhiteSpace($AssignmentFilterName)) { + $setParams.AssignmentFilterName = $AssignmentFilterName + } + if (-not [string]::IsNullOrWhiteSpace($AssignmentFilterType)) { + $setParams.AssignmentFilterType = $AssignmentFilterType + } + try { $Result = Set-CIPPAssignedApplication @setParams $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 index 74d66cea6d1a..8861c30455dd 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 @@ -48,11 +48,13 @@ function Invoke-AddIntuneTemplate { $reusableResult = Get-CIPPReusableSettingsFromPolicy -PolicyJson $Template.TemplateJson -Tenant $TenantFilter -Headers $Headers -APIName $APIName $reusableTemplateRefs = $reusableResult.ReusableSettings + # Intune templates store payload in RAWJson; only the content is rewritten to use reusable template GUID placeholders. + $templateJson = if ($reusableResult.RawJSON) { $reusableResult.RawJSON } else { $Template.TemplateJson } $object = [PSCustomObject]@{ Displayname = $Template.DisplayName Description = $Template.Description - RAWJson = $Template.TemplateJson + RAWJson = $templateJson Type = $Template.Type GUID = $GUID ReusableSettings = $reusableTemplateRefs diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 index 1c633f205e49..38bb26ed2cd4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 @@ -55,6 +55,7 @@ function Invoke-AddPolicy { # Discover referenced reusable settings from the policy JSON when none were supplied $reusableResult = Get-CIPPReusableSettingsFromPolicy -PolicyJson $RawJSON -Tenant $Tenant -Headers $Headers -APIName $APIName if ($reusableResult.ReusableSettings) { $reusableSettings = $reusableResult.ReusableSettings } + if ($reusableResult.RawJSON) { $RawJSON = $reusableResult.RawJSON } } catch {} } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 index 93dd3b986ce3..429c09387c3a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ListIntuneReusableSettingTemplates.ps1 @@ -17,6 +17,9 @@ function Invoke-ListIntuneReusableSettingTemplates { } $RawTemplates = Get-CIPPAzDataTableEntity @Table -Filter $Filter + if ($Request.query.ID) { + $RawTemplates = @($RawTemplates) | Where-Object -Property RowKey -EQ $Request.query.ID + } $Templates = foreach ($Item in $RawTemplates) { $Parsed = $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 index 60ca9abf1fa0..10f00534abae 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-CIPPOffboardingJob.ps1 @@ -320,7 +320,7 @@ function Invoke-CIPPOffboardingJob { } } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started offboarding job for $Username with ID = '$InstanceId'" Write-LogMessage -API $APIName -tenant $TenantFilter -message "Started offboarding job for $Username with $($Batch.Count) tasks. Instance ID: $InstanceId" -sev Info diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 index fc6c0b457cc7..cdba0aa98322 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecBECCheck.ps1 @@ -40,7 +40,7 @@ Function Invoke-ExecBECCheck { SkipLog = $true } #Write-Host ($InputObject | ConvertTo-Json) - $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ( ConvertTo-Json -InputObject $InputObject -Depth 5 -Compress ) + $null = Start-CIPPOrchestrator -InputObject $InputObject @{ GUID = $Request.Query.userid } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index 4eca5bd0cbe8..15728f58e429 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -25,15 +25,15 @@ function Invoke-ExecJITAdmin { $ConfigTable = Get-CIPPTable -TableName Config $Filter = "PartitionKey eq 'JITAdminSettings' and RowKey eq 'JITAdminSettings'" $JITAdminConfig = Get-CIPPAzDataTableEntity @ConfigTable -Filter $Filter - + if ($JITAdminConfig -and ![string]::IsNullOrWhiteSpace($JITAdminConfig.MaxDuration)) { # Calculate the duration between start and expiration $RequestedDuration = $Expiration - $Start - + # Parse the max duration from ISO 8601 format try { $MaxDurationTimeSpan = [System.Xml.XmlConvert]::ToTimeSpan($JITAdminConfig.MaxDuration) - + if ($RequestedDuration -gt $MaxDurationTimeSpan) { $RequestedDays = $RequestedDuration.TotalDays.ToString('0.00') $MaxDays = $MaxDurationTimeSpan.TotalDays.ToString('0.00') @@ -194,6 +194,7 @@ function Invoke-ExecJITAdmin { $TaskBody = @{ TenantFilter = $TenantFilter Name = "JIT Admin (enable): $Username" + AlertComment = if (![string]::IsNullOrWhiteSpace($Request.Body.Reason)) { "JIT Reason: $($Request.Body.Reason)" } else { $null } Command = @{ value = 'Set-CIPPUserJITAdmin' label = 'Set-CIPPUserJITAdmin' @@ -226,6 +227,7 @@ function Invoke-ExecJITAdmin { $DisableTaskBody = [pscustomobject]@{ TenantFilter = $TenantFilter Name = "JIT Admin ($($Request.Body.ExpireAction.value)): $Username" + AlertComment = if (![string]::IsNullOrWhiteSpace($Request.Body.Reason)) { "JIT Reason: $($Request.Body.Reason)" } else { $null } Command = @{ value = 'Set-CIPPUserJITAdmin' label = 'Set-CIPPUserJITAdmin' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 index 954760a7a464..713f40769a01 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListJITAdmin.ps1 @@ -93,7 +93,7 @@ } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 index 91ae1a2c4d8d..06b90ed29bb4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListBasicAuth.ps1 @@ -58,7 +58,7 @@ Function Invoke-ListBasicAuth { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Start-CIPPOrchestrator -InputObject $InputObject $GraphRequest = [PSCustomObject]@{ MetaData = 'Loading data for all tenants. Please check back in 10 minutes' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 index 8dd0274fe7f9..9446c664eef3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Reports/Invoke-ListMFAUsers.ps1 @@ -55,20 +55,22 @@ function Invoke-ListMFAUsers { SkipLog = $true } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) - Write-Host "Started permissions orchestration with ID = '$InstanceId'" + Start-CIPPOrchestrator -InputObject $InputObject + } } else { + Write-Information 'Getting cached MFA state for all tenants' + Write-Information "Found $($Rows.Count) rows in cache" $Rows = foreach ($Row in $Rows) { if ($Row.CAPolicies -and $Row.CAPolicies -is [string]) { - $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json } catch { @() } + $Row.CAPolicies = try { $Row.CAPolicies | ConvertFrom-Json -ErrorAction Stop } catch { @() } } elseif (-not $Row.CAPolicies) { - $Row.CAPolicies = @() + $Row | Add-Member -NotePropertyName CAPolicies -NotePropertyValue @() -Force } if ($Row.MFAMethods -and $Row.MFAMethods -is [string]) { - $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json } catch { @() } + $Row.MFAMethods = try { $Row.MFAMethods | ConvertFrom-Json -ErrorAction Stop } catch { @() } } elseif (-not $Row.MFAMethods) { - $Row.MFAMethods = @() + $Row | Add-Member -NotePropertyName MFAMethods -NotePropertyValue @() -Force } $Row } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 index 1a228cff5dc3..a4a0892d0eb3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Invoke-ExecTestRun.ps1 @@ -28,7 +28,7 @@ function Invoke-ExecTestRun { SkipLog = $false } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject $StatusCode = [HttpStatusCode]::OK $Body = [PSCustomObject]@{ Results = "Successfully started data collection and test run for $TenantFilter" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 index 5e87cdc60e00..05a6e2e67e14 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecAlertsList.ps1 @@ -81,8 +81,8 @@ function Invoke-ExecAlertsList { DurableName = 'ExecAlertsListAllTenants' } SkipLog = $true - } | ConvertTo-Json -Depth 10 - Start-NewOrchestration -FunctionName CIPPOrchestrator -InputObject $InputObject + } + Start-CIPPOrchestrator -InputObject $InputObject } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 index 77dbdb060b08..e0a91cfe8dbb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecIncidentsList.ps1 @@ -67,7 +67,7 @@ function Invoke-ExecIncidentsList { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + Start-CIPPOrchestrator -InputObject $InputObject | Out-Null } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 index 241f91ca8c85..5e3fe87688f7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Security/Invoke-ExecMdoAlertsList.ps1 @@ -48,7 +48,7 @@ function Invoke-ExecMDOAlertsList { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + Start-CIPPOrchestrator -InputObject $InputObject | Out-Null } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 index 01fcd2b59e6e..e53ce5c3924f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Application Approval/Invoke-ExecAddMultiTenantApp.ps1 @@ -43,7 +43,7 @@ function Invoke-ExecAddMultiTenantApp { Batch = @($Batch) SkipLog = $true } - $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $null = Start-CIPPOrchestrator -InputObject $InputObject $Results = 'Deploying {0} to {1}, see the logbook for details' -f $Request.Body.AppId, ($Request.Body.tenantFilter.label -join ', ') } catch { $ErrorMsg = Get-NormalizedError -message $($_.Exception.Message) @@ -81,7 +81,7 @@ function Invoke-ExecAddMultiTenantApp { Batch = @($Batch) SkipLog = $true } - $null = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $null = Start-CIPPOrchestrator -InputObject $InputObject $Results = 'Deploying {0} to {1}, see the logbook for details' -f $Request.Body.selectedTemplate.label, ($Request.Body.tenantFilter.label -join ', ') } catch { $Results = "Error queuing application - $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-AddDomain.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-AddDomain.ps1 index 4e6f40b4b01b..d3d287b70886 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-AddDomain.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-AddDomain.ps1 @@ -9,6 +9,7 @@ function Invoke-AddDomain { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers $TenantFilter = $Request.Body.tenantFilter $DomainName = $Request.Body.domain @@ -29,15 +30,15 @@ function Invoke-AddDomain { id = $DomainName } | ConvertTo-Json -Compress - $GraphRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter -type POST -body $Body -AsApp $true + $null = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/domains' -tenantid $TenantFilter -type POST -body $Body -AsApp $true $Result = "Successfully added domain $DomainName to tenant $TenantFilter. Please verify the domain to complete setup." - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Added domain $DomainName" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Added domain $DomainName" -Sev 'Info' $StatusCode = [HttpStatusCode]::OK } catch { $ErrorMessage = Get-CippException -Exception $_ $Result = "Failed to add domain $DomainName`: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Failed to add domain $DomainName`: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to add domain $DomainName`: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage $StatusCode = [HttpStatusCode]::Forbidden } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-ExecDomainAction.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-ExecDomainAction.ps1 index 91380834dae8..f7c57225f4b6 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-ExecDomainAction.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Domains/Invoke-ExecDomainAction.ps1 @@ -9,6 +9,7 @@ function Invoke-ExecDomainAction { param($Request, $TriggerMetadata) $APIName = $Request.Params.CIPPEndpoint + $Headers = $Request.Headers $TenantFilter = $Request.Body.tenantFilter $DomainName = $Request.Body.domain $Action = $Request.Body.Action @@ -37,8 +38,7 @@ function Invoke-ExecDomainAction { state = 'success' } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Verified domain $DomainName" -Sev 'Info' - $StatusCode = [HttpStatusCode]::OK + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Verified domain $DomainName" -Sev 'Info' } 'delete' { Write-Information "Deleting domain $DomainName from tenant $TenantFilter" @@ -50,8 +50,7 @@ function Invoke-ExecDomainAction { state = 'success' } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Deleted domain $DomainName" -Sev 'Info' - $StatusCode = [HttpStatusCode]::OK + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Deleted domain $DomainName" -Sev 'Info' } 'setDefault' { Write-Information "Setting domain $DomainName as default for tenant $TenantFilter" @@ -60,15 +59,14 @@ function Invoke-ExecDomainAction { isDefault = $true } | ConvertTo-Json -Compress - $GraphRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/domains/$DomainName" -tenantid $TenantFilter -type PATCH -body $Body -AsApp $true + $null = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/domains/$DomainName" -tenantid $TenantFilter -type PATCH -body $Body -AsApp $true $Result = @{ resultText = "Domain $DomainName has been set as the default domain successfully." state = 'success' } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Set domain $DomainName as default" -Sev 'Info' - $StatusCode = [HttpStatusCode]::OK + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Set domain $DomainName as default" -Sev 'Info' } default { throw "Invalid action: $Action" @@ -80,12 +78,12 @@ function Invoke-ExecDomainAction { resultText = "Failed to perform action on domain $DomainName`: $($ErrorMessage.NormalizedError)" state = 'error' } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $TenantFilter -message "Failed to perform action on domain $DomainName`: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed to perform action on domain $DomainName`: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage $StatusCode = [HttpStatusCode]::Forbidden } return ([HttpResponseContext]@{ - StatusCode = $StatusCode + StatusCode = ($StatusCode ?? [HttpStatusCode]::OK) Body = @{'Results' = $Result } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 index fc182fe05dac..ccf9d21dced3 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOnboardTenant.ps1 @@ -84,7 +84,7 @@ function Invoke-ExecOnboardTenant { OrchestratorName = 'OnboardingOrchestrator' Batch = @($Item) } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-LogMessage -headers $Headers -API $APIName -message "Onboarding job $Id started" -Sev 'Info' -LogData @{ 'InstanceId' = $InstanceId } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 index 43ee69370193..0247c0ef78a5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Tenant/Invoke-ListTenants.ps1 @@ -33,7 +33,7 @@ function Invoke-ListTenants { OrchestratorName = 'UpdateTenants' SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $InputObject $GraphRequest = [pscustomobject]@{'Results' = 'Cache has been cleared and a tenant refresh is queued.' } return ([HttpResponseContext]@{ @@ -61,7 +61,7 @@ function Invoke-ListTenants { OrchestratorName = 'UpdateTenants' SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + Start-CIPPOrchestrator -InputObject $InputObject } } try { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCACheck.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCACheck.ps1 index 9d0cc1b1f683..51f90871cacb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCACheck.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ExecCACheck.ps1 @@ -14,7 +14,7 @@ function Invoke-ExecCaCheck { } else { $IncludeApplications = '67ad5377-2d78-4ac2-a867-6300cda00e85' } - $results = try { + $Results = try { $CAContext = @{ '@odata.type' = '#microsoft.graph.applicationContext' 'includeApplications' = @($IncludeApplications) @@ -22,7 +22,7 @@ function Invoke-ExecCaCheck { $ConditionalAccessWhatIfDefinition = @{ 'signInIdentity' = @{ '@odata.type' = '#microsoft.graph.userSignIn' - 'userId' = "$userId" + 'userId' = "$UserID" } 'signInContext' = $CAContext 'signInConditions' = @{} @@ -33,21 +33,22 @@ function Invoke-ExecCaCheck { if ($Request.body.ClientAppType) { $whatIfConditions.clientAppType = $Request.body.ClientAppType.value } if ($Request.body.DevicePlatform) { $whatIfConditions.devicePlatform = $Request.body.DevicePlatform.value } if ($Request.body.Country) { $whatIfConditions.country = $Request.body.Country.value } - if ($Request.body.IpAddress) { $whatIfConditions.ipAddress = $Request.body.IpAddress.value } + if ($Request.body.IpAddress) { $whatIfConditions.ipAddress = $Request.body.IpAddress } + if ($Request.body.authenticationFlow) { $whatIfConditions.authenticationFlow = @{ transferMethod = $Request.body.authenticationFlow.value } } $JSONBody = $ConditionalAccessWhatIfDefinition | ConvertTo-Json -Depth 10 Write-Host $JSONBody $Request = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/evaluate' -tenantid $tenant -type POST -body $JsonBody -AsApp $true $Request + $StatusCode = [HttpStatusCode]::OK } catch { "Failed to execute check: $($_.Exception.Message)" + $StatusCode = [HttpStatusCode]::InternalServerError } - $body = [pscustomobject]@{'Results' = $results } - return ([HttpResponseContext]@{ - StatusCode = [HttpStatusCode]::OK - Body = $body + StatusCode = $StatusCode + Body = @{'Results' = $Results } }) } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 index 301f27f70abb..39c90b46fa75 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Conditional/Invoke-ListConditionalAccessPolicies.ps1 @@ -228,7 +228,7 @@ function Invoke-ListConditionalAccessPolicies { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + Start-CIPPOrchestrator -InputObject $InputObject | Out-Null } else { $Metadata = [PSCustomObject]@{ QueueId = $RunningQueue.RowKey ?? $null diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 index b33334241408..c241e26c0736 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Reports/Invoke-ListLicenses.ps1 @@ -32,7 +32,7 @@ function Invoke-ListLicenses { SkipLog = $true } #Write-Host ($InputObject | ConvertTo-Json) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Host "Started permissions orchestration with ID = '$InstanceId'" } } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-CIPPStandardsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-CIPPStandardsRun.ps1 index 81810ce00d00..faab875fa01f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-CIPPStandardsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-CIPPStandardsRun.ps1 @@ -41,7 +41,7 @@ function Invoke-CIPPStandardsRun { SkipLog = $true } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started orchestration with ID = '$InstanceId' for drift standards run" #$Orchestrator = New-OrchestrationCheckStatusResponse -Request $Request -InstanceId $InstanceId return @@ -89,7 +89,7 @@ function Invoke-CIPPStandardsRun { } Write-Information "InputObject: $($InputObject | ConvertTo-Json -Depth 5 -Compress)" - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject Write-Information "Started standards list orchestration with ID = '$InstanceId'" } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecBPA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecBPA.ps1 index 398ef51a6fbc..7f3e7495f478 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecBPA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecBPA.ps1 @@ -8,35 +8,12 @@ function Invoke-ExecBPA { [CmdletBinding()] param($Request, $TriggerMetadata) - $ConfigTable = Get-CIPPTable -tablename Config - $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" - $TenantFilter = $Request.Query.tenantFilter ? $Request.Query.tenantFilter.value : $Request.Body.tenantfilter.value - if ($Config -and $Config.state -eq $true) { - if ($env:CIPP_PROCESSOR -ne 'true') { - $Parameters = @{Force = $true } - if ($TenantFilter -and $TenantFilter -ne 'AllTenants') { - $Parameters.TenantFilter = $TenantFilter - $RowKey = "Start-BPAOrchestrator-$($TenantFilter)" - } else { - $RowKey = 'Start-BPAOrchestrator' - } + # Start the orchestrator - it will handle queuing internally + Start-BPAOrchestrator -TenantFilter $TenantFilter -Force - $ProcessorQueue = Get-CIPPTable -TableName 'ProcessorQueue' - $ProcessorFunction = [PSCustomObject]@{ - PartitionKey = 'Function' - RowKey = $RowKey - FunctionName = 'Start-BPAOrchestrator' - Parameters = [string](ConvertTo-Json -Compress -InputObject $Parameters) - } - Add-AzDataTableEntity @ProcessorQueue -Entity $ProcessorFunction -Force - $Results = [pscustomobject]@{'Results' = 'BPA queued for execution' } - } - } else { - Start-BPAOrchestrator -TenantFilter $TenantFilter - $Results = [pscustomobject]@{'Results' = 'BPA started' } - } + $Results = [pscustomobject]@{'Results' = 'BPA queued for execution' } return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDomainAnalyser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDomainAnalyser.ps1 index 573498b0c8f6..b48d0ca647a7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDomainAnalyser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecDomainAnalyser.ps1 @@ -8,29 +8,18 @@ function Invoke-ExecDomainAnalyser { [CmdletBinding()] param($Request, $TriggerMetadata) - $ConfigTable = Get-CIPPTable -tablename Config - $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" - - if ($Config -and $Config.state -eq $true) { - if ($env:CIPP_PROCESSOR -ne 'true') { - $ProcessorFunction = [PSCustomObject]@{ - PartitionKey = 'Function' - RowKey = 'Start-DomainOrchestrator' - FunctionName = 'Start-DomainOrchestrator' - } - $ProcessorQueue = Get-CIPPTable -TableName 'ProcessorQueue' - Add-AzDataTableEntity @ProcessorQueue -Entity $ProcessorFunction -Force - $Results = [pscustomobject]@{'Results' = 'Queueing Domain Analyser' } - } + # Call the wrapper - it handles queuing internally via Start-CIPPOrchestrator + $Params = @{} + if ($Request.Body.tenantFilter) { + $Params.TenantFilter = $Request.Body.tenantFilter.value ?? $Request.Body.tenantFilter + } + $OrchStatus = Start-DomainOrchestrator @Params + if ($OrchStatus) { + $Message = 'Domain Analyser started' } else { - $OrchStatus = Start-DomainOrchestrator - if ($OrchStatus) { - $Message = 'Domain Analyser started' - } else { - $Message = 'Domain Analyser error: check logs' - } - $Results = [pscustomobject]@{'Results' = $Message } + $Message = 'Domain Analyser error: check logs' } + $Results = [pscustomobject]@{'Results' = $Message } return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 index 0e2f940f7948..6b86ce99cfd1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ExecStandardsRun.ps1 @@ -26,39 +26,15 @@ function Invoke-ExecStandardsRun { $_.guid -like $TemplateId } - - - $ConfigTable = Get-CIPPTable -tablename Config - $Config = Get-CIPPAzDataTableEntity @ConfigTable -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" - - if ($Config -and $Config.state -eq $true) { - if ($env:CIPP_PROCESSOR -ne 'true') { - - $ProcessorFunction = [PSCustomObject]@{ - PartitionKey = 'Function' - RowKey = "Invoke-CIPPStandardsRun-$TenantFilter-$TemplateId" - FunctionName = 'Invoke-CIPPStandardsRun' - Parameters = [string](ConvertTo-Json -Compress -InputObject @{ - TenantFilter = $TenantFilter - TemplateId = $TemplateId - runManually = [bool]$Templates.runManually - Force = $true - }) - } - $ProcessorQueue = Get-CIPPTable -TableName 'ProcessorQueue' - Add-AzDataTableEntity @ProcessorQueue -Entity $ProcessorFunction -Force - $Results = "Successfully Queued Standards Run for Tenant $TenantFilter" - } - } else { - try { - $null = Invoke-CIPPStandardsRun -TenantFilter $TenantFilter -TemplateID $TemplateId -runManually ([bool]$Templates.runManually) -Force - $Results = "Successfully started Standards Run for tenant: $TenantFilter" - Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Results -Sev 'Info' - } catch { - $ErrorMessage = Get-CippException -Exception $_ - $Results = "Failed to start standards run for tenant: $TenantFilter. Error: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage - } + # Call the wrapper - it handles queuing internally via Start-CIPPOrchestrator + try { + $null = Invoke-CIPPStandardsRun -TenantFilter $TenantFilter -TemplateID $TemplateId -runManually ([bool]$Templates.runManually) -Force + $Results = "Successfully started Standards Run for tenant: $TenantFilter" + Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Results -Sev 'Info' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Results = "Failed to start standards run for tenant: $TenantFilter. Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -tenant $TenantFilter -API $APIName -message $Results -Sev 'Error' -LogData $ErrorMessage } $Results = [pscustomobject]@{'Results' = "$Results" } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 index d9d3f9e2dfc0..283913c6702c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-RemoveStandardTemplate.ps1 @@ -16,8 +16,8 @@ function Invoke-RemoveStandardTemplate { $ID = $Request.Body.ID ?? $Request.Query.ID try { $Table = Get-CippTable -tablename 'templates' - $Filter = "PartitionKey eq 'StandardsTemplateV2' and GUID eq '$ID'" - $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, JSON + $Filter = "PartitionKey eq 'StandardsTemplateV2' and (GUID eq '$ID' or RowKey eq '$ID' or OriginalEntityId eq '$ID')" + $ClearRow = Get-CIPPAzDataTableEntity @Table -Filter $Filter -Property PartitionKey, RowKey, ETag, JSON if ($ClearRow.JSON) { $TemplateName = (ConvertFrom-Json -InputObject $ClearRow.JSON -ErrorAction SilentlyContinue).templateName } else { diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecBitlockerSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecBitlockerSearch.ps1 new file mode 100644 index 000000000000..697d467b3b7f --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecBitlockerSearch.ps1 @@ -0,0 +1,109 @@ +function Invoke-ExecBitlockerSearch { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + Endpoint.Device.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + # Get search parameters from query string or POST body + $KeyId = $Request.Query.keyId ?? $Request.Body.keyId + $DeviceId = $Request.Query.deviceId ?? $Request.Body.deviceId + $Limit = $Request.Query.limit ?? $Request.Body.limit ?? 0 + + # Handle tenant filtering with access control + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + + $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter + if ($AllowedTenants -notcontains 'AllTenants') { + if ($TenantFilter) { + # Verify user has access to requested tenant + $TenantList = Get-Tenants | Select-Object -ExpandProperty defaultDomainName + if ($TenantList -notcontains $TenantFilter) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::Forbidden + Body = @{ + error = "Access denied to tenant: $TenantFilter" + } + } + } + } else { + $TenantFilter = Get-Tenants | Select-Object -ExpandProperty defaultDomainName + } + } elseif (-not $TenantFilter) { + $TenantFilter = 'allTenants' + } + + # Build parameters for Search-CIPPBitlockerKeys + $SearchParams = @{} + + if ($TenantFilter) { + $SearchParams.TenantFilter = $TenantFilter + Write-Information "Filtering by tenant: $TenantFilter" + } + + if ($KeyId) { + $SearchParams.KeyId = $KeyId + Write-Information "Searching for key ID: $KeyId" + } elseif ($DeviceId) { + $SearchParams.DeviceId = $DeviceId + Write-Information "Searching for device ID: $DeviceId" + } else { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ + error = 'No search criteria provided. Please provide keyId or deviceId.' + } + } + } + + if ($Limit -gt 0) { + $SearchParams.Limit = [int]$Limit + } + + # Execute the search + $Results = Search-CIPPBitlockerKeys @SearchParams + + Write-Information "Found $($Results.Count) BitLocker key record(s)" + + # Format results for output + $OutputResults = @($Results | ForEach-Object { + [PSCustomObject]@{ + tenant = $_.Tenant + keyId = $_.Data.id + createdDateTime = $_.Data.createdDateTime + volumeType = $_.Data.volumeType + deviceId = $_.Data.deviceId + deviceName = $_.Data.deviceName + operatingSystem = $_.Data.operatingSystem + osVersion = $_.Data.osVersion + lastSignIn = $_.Data.lastSignIn + accountEnabled = $_.Data.accountEnabled + trustType = $_.Data.trustType + deviceFound = $_.Data.deviceFound + timestamp = $_.Timestamp + } + }) + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = @{ + Results = $OutputResults + Count = $OutputResults.Count + } + } + + } catch { + Write-Information "Error occurred during BitLocker key search: $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ + error = "Failed to search for BitLocker keys: $($_.Exception.Message)" + } + } + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecLicenseSearch.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecLicenseSearch.ps1 new file mode 100644 index 000000000000..6cf5bbd4f2bd --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecLicenseSearch.ps1 @@ -0,0 +1,70 @@ +function Invoke-ExecLicenseSearch { + <# + .FUNCTIONALITY + Entrypoint,AnyTenant + .ROLE + CIPP.Core.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + try { + # Get skuIds from POST body + $SkuIds = $Request.Body.skuIds + + if (-not $SkuIds -or $SkuIds.Count -eq 0) { + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::BadRequest + Body = @{ + error = 'No skuIds provided. Please provide an array of skuIds in the request body.' + } + } + } + + Write-Information "Searching for licenses with skuIds: $($SkuIds -join ', ')" + + # Search for licenses using the skuIds as search terms + # This searches across all tenants for matching licenses + $Results = Search-CIPPDbData -SearchTerms $SkuIds -Types 'LicenseOverview' -Properties 'License', 'skuId' + + Write-Information "Found $($Results.Count) license records matching skuIds" + + # Initialize hashtable to store unique licenses by skuId + $UniqueLicenses = @{} + + # Process each result and extract unique skuId/displayName pairs + foreach ($Result in $Results) { + if ($Result.Data -and $Result.Data.skuId) { + $SkuIdKey = $Result.Data.skuId + + # Only add if we haven't seen this skuId yet + if (-not $UniqueLicenses.ContainsKey($SkuIdKey)) { + $UniqueLicenses[$SkuIdKey] = [PSCustomObject]@{ + skuId = $Result.Data.skuId + displayName = $Result.Data.License + } + } + } + } + + # Convert hashtable to array for output + $OutputResults = @($UniqueLicenses.Values) + + Write-Information "Returning $($OutputResults.Count) unique licenses" + + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $OutputResults + } + + } catch { + Write-Information "Error occurred during license search: $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + return [HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::InternalServerError + Body = @{ + error = "Failed to search for licenses: $($_.Exception.Message)" + } + } + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 index df21429f9605..6bcb3befbb49 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ExecUniversalSearchV2.ps1 @@ -12,16 +12,24 @@ function Invoke-ExecUniversalSearchV2 { $Limit = if ($Request.Query.limit) { [int]$Request.Query.limit } else { 10 } $Type = if ($Request.Query.type) { $Request.Query.type } else { 'Users' } + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList + + if ($AllowedTenants -notcontains 'AllTenants') { + $TenantFilter = Get-Tenants | Select-Object -ExpandProperty defaultDomainName + } else { + $TenantFilter = 'allTenants' + } + # Always search all tenants - do not pass TenantFilter parameter switch ($Type) { 'Users' { - $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' -TenantFilter $TenantFilter } 'Groups' { - $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Groups' -Limit $Limit -Properties 'id', 'displayName', 'mail', 'mailEnabled', 'securityEnabled', 'groupTypes', 'description' + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Groups' -Limit $Limit -Properties 'id', 'displayName', 'mail', 'mailEnabled', 'securityEnabled', 'groupTypes', 'description' -TenantFilter $TenantFilter } default { - $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' + $Results = Search-CIPPDbData -SearchTerms $SearchTerms -Types 'Users' -Limit $Limit -Properties 'id', 'userPrincipalName', 'displayName' -TenantFilter $TenantFilter } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 index 5aa10ac22f33..d3019125ca8f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListTenantAllowBlockList.ps1 @@ -52,7 +52,7 @@ function Invoke-ListTenantAllowBlockList { } SkipLog = $true } - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) | Out-Null + Start-CIPPOrchestrator -InputObject $InputObject | Out-Null $Results = @() } else { $TenantList = Get-Tenants -IncludeErrors diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 index eb4d4386aeac..3fe5e97095c0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-ApplicationOrchestrator.ps1 @@ -17,6 +17,6 @@ function Start-ApplicationOrchestrator { } if ($PSCmdlet.ShouldProcess('Upload Applications')) { - return Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + return Start-CIPPOrchestrator -InputObject $InputObject } } diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 index 41dd759b0c36..7900d8399f3c 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-BPAOrchestrator.ps1 @@ -95,7 +95,7 @@ function Start-BPAOrchestrator { OrchestratorName = 'BPAOrchestrator' SkipLog = $true } - return Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + return Start-CIPPOrchestrator -InputObject $InputObject } } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 new file mode 100644 index 000000000000..53ecbe6247cb --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-CIPPOrchestrator.ps1 @@ -0,0 +1,111 @@ +function Start-CIPPOrchestrator { + <# + .SYNOPSIS + Start a CIPP orchestrator with automatic queue routing + .DESCRIPTION + Wrapper around Start-NewOrchestration that stores input objects in table storage + and routes orchestration execution through the queue to avoid size limits and enable offloading. + + When called from HTTP functions: Stores input object, queues message with GUID + When called from queue trigger with GUID: Retrieves input object, starts orchestration + When called from queue trigger with -CallerIsQueueTrigger: Starts orchestration directly (no re-queuing) + .PARAMETER InputObjectGuid + GUID reference to retrieve stored input object from table (used internally by queue trigger) + .PARAMETER InputObject + The orchestrator input object (same structure as Start-NewOrchestration) + .PARAMETER CallerIsQueueTrigger + Indicates the caller is already running in a queue trigger context. + Skips queuing and starts orchestration directly to avoid double-queuing. + .EXAMPLE + Start-CIPPOrchestrator -InputObject @{OrchestratorName='BPA'; Batch=@($Tenants)} + .EXAMPLE + Start-CIPPOrchestrator -InputObject $InputObject -CallerIsQueueTrigger + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$InputObjectGuid, + + [Parameter(Mandatory = $false)] + [object]$InputObject, + + [switch]$CallerIsQueueTrigger + ) + $OrchestratorTable = Get-CippTable -TableName 'CippOrchestratorInput' + + # If already running in processor context (e.g., timer trigger) and we have an InputObject, + # start orchestration directly without queuing + if ($InputObject -and ($env:CIPP_PROCESSOR -eq 'true' -or $CallerIsQueueTrigger.IsPresent)) { + Write-Information 'Running in processor context - starting orchestration directly' + try { + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + Write-Information "Orchestration started with instance ID: $InstanceId" + return $InstanceId + } catch { + Write-Error "Failed to start orchestration in processor context: $_" + throw + } + } + + # If we have a GUID, we're being called from the queue trigger - retrieve and execute + if ($InputObjectGuid) { + Write-Information "Retrieving orchestrator input object: $InputObjectGuid" + try { + $StoredInput = Get-CIPPAzDataTableEntity @OrchestratorTable -Filter "PartitionKey eq 'Input' and RowKey eq '$InputObjectGuid'" + + if (-not $StoredInput) { + throw "Input object not found for GUID: $InputObjectGuid" + } + + # Start the orchestration with the compressed JSON string from storage + # Note: StoredInput.InputObject is already a compressed JSON string + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject $StoredInput.InputObject + + Write-Information "Orchestration started with instance ID: $InstanceId" + + # Clean up the stored input object after starting the orchestration + try { + $Entities = Get-AzDataTableEntity @OrchestratorTable -Filter "PartitionKey eq 'Input' and (RowKey eq '$InputObjectGuid' or OriginalEntityId eq '$InputObjectGuid')" -Property 'PartitionKey', 'RowKey', 'ETag' + Remove-AzDataTableEntity @OrchestratorTable -Entity $Entities + Write-Information "Cleaned up stored input object: $InputObjectGuid" + } catch { + Write-Warning "Failed to clean up stored input object $InputObjectGuid : $_" + } + + return $InstanceId + + } catch { + Write-Error "Failed to start orchestration from stored input: $_" + throw + } + } elseif ($InputObject) { + try { + # Store the input object in table storage + $Guid = (New-Guid).Guid.ToString() + + $StoredInput = @{ + PartitionKey = 'Input' + RowKey = $Guid + InputObject = [string]($InputObject | ConvertTo-Json -Depth 10 -Compress) + } + + Add-CIPPAzDataTableEntity @OrchestratorTable -Entity $StoredInput -Force + Write-Information "Stored orchestrator input with GUID: $Guid" + + # Queue the orchestration execution with just the GUID + Add-CippQueueMessage -Cmdlet 'Start-CIPPOrchestrator' -Parameters @{ + InputObjectGuid = $Guid + } + + Write-Information "Queued orchestration execution for GUID: $Guid" + + } catch { + Write-Error "Failed to queue orchestration: $_" + throw + } + } else { + Write-Warning 'No input object or GUID provided to Start-CIPPOrchestrator. Nothing to execute.' + } +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DomainOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DomainOrchestrator.ps1 index 60c1d20600b7..db26487381b7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DomainOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-DomainOrchestrator.ps1 @@ -8,12 +8,23 @@ function Start-DomainOrchestrator { Entrypoint #> [CmdletBinding(SupportsShouldProcess = $true)] - param() + param($TenantFilter) try { - $TenantList = Get-Tenants -IncludeAll - if (($TenantList | Measure-Object).Count -eq 0) { - Write-Information 'No tenants found' - return 0 + + if ($TenantFilter -and $TenantFilter -ne 'allTenants') { + $TenantList = @($TenantFilter) + $TenantParams = @{ + TenantFilter = $TenantFilter + } + } else { + $TenantList = Get-Tenants -IncludeAll + if (($TenantList | Measure-Object).Count -eq 0) { + Write-Information 'No tenants found' + return 0 + } + $TenantParams = @{ + IncludeAll = $true + } } $Queue = New-CippQueueEntry -Name 'Domain Analyser' -TotalTasks ($TenantList | Measure-Object).Count @@ -22,16 +33,14 @@ function Start-DomainOrchestrator { FunctionName = 'GetTenants' DurableName = 'DomainAnalyserTenant' QueueId = $Queue.RowKey - TenantParams = @{ - IncludeAll = $true - } + TenantParams = $TenantParams } OrchestratorName = 'DomainAnalyser_Tenants' SkipLog = $true } if ($PSCmdlet.ShouldProcess('Domain Analyser', 'Starting Orchestrator')) { Write-LogMessage -API 'DomainAnalyser' -message 'Starting Domain Analyser' -sev Info - return Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Compress -Depth 5) + return Start-CIPPOrchestrator -InputObject $InputObject } } catch { $ErrorMessage = Get-CippException -Exception $_ diff --git a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 index a9a2f4ab3da5..364b821f4dc1 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Orchestrator Functions/Start-UserTasksOrchestrator.ps1 @@ -10,11 +10,11 @@ function Start-UserTasksOrchestrator { param() $Table = Get-CippTable -tablename 'ScheduledTasks' - $30MinutesAgo = (Get-Date).AddMinutes(-30).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') $4HoursAgo = (Get-Date).AddHours(-4).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') + $24HoursAgo = (Get-Date).AddHours(-24).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') # Pending = orchestrator queued, Running = actively executing - # Pick up: Planned, Failed-Planned, stuck Pending (>30min), or stuck Running (>4hr for large AllTenants tasks) - $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$30MinutesAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))" + # Pick up: Planned, Failed-Planned, stuck Pending (>24hr), or stuck Running (>4hr for large AllTenants tasks) + $Filter = "PartitionKey eq 'ScheduledTask' and (TaskState eq 'Planned' or TaskState eq 'Failed - Planned' or (TaskState eq 'Pending' and Timestamp lt datetime'$24HoursAgo') or (TaskState eq 'Running' and Timestamp lt datetime'$4HoursAgo'))" $tasks = Get-CIPPAzDataTableEntity @Table -Filter $Filter $Batch = [System.Collections.Generic.List[object]]::new() @@ -28,11 +28,19 @@ function Start-UserTasksOrchestrator { # Update task state to 'Pending' immediately to prevent concurrent orchestrator runs from picking it up # 'Pending' = orchestrator has picked it up and is queuing commands # 'Running' = actual execution is happening (set by Push-ExecScheduledCommand) - $null = Update-AzDataTableEntity -Force @Table -Entity @{ - PartitionKey = $task.PartitionKey - RowKey = $task.RowKey - ExecutedTime = "$currentUnixTime" - TaskState = 'Pending' + # Use ETag for optimistic concurrency to prevent race conditions + try { + $null = Update-AzDataTableEntity @Table -Entity @{ + PartitionKey = $task.PartitionKey + RowKey = $task.RowKey + ExecutedTime = "$currentUnixTime" + TaskState = 'Pending' + ETag = $task.ETag + } + } catch { + # Task was already picked up by another orchestrator instance - skip it + Write-Information "Task $($task.Name) already being processed by another orchestrator instance. Skipping." + continue } $task.Parameters = $task.Parameters | ConvertFrom-Json -AsHashtable if (!$task.Parameters) { $task.Parameters = @{} } @@ -136,35 +144,110 @@ function Start-UserTasksOrchestrator { Write-Information "Total tasks to process: $($Batch.Count)" if ($Batch.Count -gt 0) { - # Group tasks by tenant instead of command type - $TenantGroups = $Batch | Group-Object -Property { $_.Parameters.TenantFilter } - $ProcessedBatches = [System.Collections.Generic.List[object]]::new() + # Separate multi-tenant tasks from single-tenant tasks + $MultiTenantTasks = [System.Collections.Generic.List[object]]::new() + $SingleTenantTasks = [System.Collections.Generic.List[object]]::new() + + foreach ($Task in $Batch) { + $IsMultiTenant = ($Task.TaskInfo.Tenant -eq 'AllTenants' -or $Task.TaskInfo.TenantGroup) + if ($IsMultiTenant) { + $MultiTenantTasks.Add($Task) + } else { + $SingleTenantTasks.Add($Task) + } + } - foreach ($TenantGroup in $TenantGroups) { - $TenantName = $TenantGroup.Name - $TenantCommands = [System.Collections.Generic.List[object]]::new($TenantGroup.Group) + Write-Information "Multi-tenant tasks: $($MultiTenantTasks.Count), Single-tenant tasks: $($SingleTenantTasks.Count)" - Write-Information "Creating batch for tenant: $TenantName with $($TenantCommands.Count) tasks" - $ProcessedBatches.Add($TenantCommands) - } + # Process single-tenant tasks: Group by tenant for efficiency + if ($SingleTenantTasks.Count -gt 0) { + $TenantGroups = $SingleTenantTasks | Group-Object -Property { $_.Parameters.TenantFilter } + + foreach ($TenantGroup in $TenantGroups) { + $TenantName = $TenantGroup.Name + $TenantCommands = @($TenantGroup.Group) - # Process each tenant batch separately - foreach ($ProcessedBatch in $ProcessedBatches) { - $TenantName = $ProcessedBatch[0].Parameters.TenantFilter + Write-Information "Creating orchestrator for single-tenant tasks: $TenantName with $($TenantCommands.Count) tasks" - # Create queue entry for each tenant batch - $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName" - $QueueId = $Queue.RowKey - $BatchWithQueue = $ProcessedBatch | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, $TenantName } } + # Create queue entry for this tenant's tasks + $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName" + $QueueId = $Queue.RowKey + $BatchWithQueue = @($TenantCommands | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $_.TaskInfo.Name, $TenantName } }) - $InputObject = [PSCustomObject]@{ - OrchestratorName = "UserTaskOrchestrator_$TenantName" - Batch = @($BatchWithQueue) - SkipLog = $true + $InputObject = [PSCustomObject]@{ + OrchestratorName = "UserTaskOrchestrator_$TenantName" + Batch = $BatchWithQueue + SkipLog = $true + } + + if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting Single-Tenant Tasks Orchestrator')) { + try { + $OrchestratorId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + Write-Information "Single-tenant orchestrator started for $TenantName with ID: $OrchestratorId" + } catch { + Write-Warning "Failed to start single-tenant orchestrator for $TenantName : $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + } + } } + } + + # Process multi-tenant tasks: Each gets its own orchestrator with PostExecution + if ($MultiTenantTasks.Count -gt 0) { + # Group by parent task (RowKey) to handle each multi-tenant task separately + $ParentTaskGroups = $MultiTenantTasks | Group-Object -Property { $_.TaskInfo.RowKey } + + foreach ($ParentTaskGroup in $ParentTaskGroups) { + $ParentTask = $ParentTaskGroup.Group[0].TaskInfo + $TaskCommands = @($ParentTaskGroup.Group) - if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting User Tasks Orchestrator')) { - Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + Write-Information "Creating orchestrator for multi-tenant task: $($ParentTask.Name) with $($TaskCommands.Count) tenant executions" + + # Combine all tenant executions for this parent task + $AllBatchItems = [System.Collections.Generic.List[object]]::new() + + # Group by tenant within this parent task for queue organization + $TenantSubGroups = $TaskCommands | Group-Object -Property { $_.Parameters.TenantFilter } + + foreach ($TenantSubGroup in $TenantSubGroups) { + $TenantName = $TenantSubGroup.Name + $TenantItems = @($TenantSubGroup.Group) + + Write-Information " Including tenant: $TenantName with $($TenantItems.Count) items" + + # Create queue entry for each tenant within this multi-tenant task + $Queue = New-CippQueueEntry -Name "Scheduled Tasks - $TenantName" + $QueueId = $Queue.RowKey + $BatchWithQueue = @($TenantItems | Select-Object *, @{Name = 'QueueId'; Expression = { $QueueId } }, @{Name = 'QueueName'; Expression = { '{0} - {1}' -f $ParentTask.Name, $TenantName } }) + + $AllBatchItems.AddRange($BatchWithQueue) + } + + $InputObject = [PSCustomObject]@{ + OrchestratorName = "UserTaskOrchestrator_$($ParentTask.Name)" + Batch = @($AllBatchItems) + SkipLog = $true + PostExecution = @{ + FunctionName = 'ScheduledTaskPostExecution' + Parameters = @{ + TaskRowKey = $ParentTask.RowKey + TaskName = $ParentTask.Name + SendCompletionAlert = $true + } + } + } + + Write-Information "Starting multi-tenant orchestrator for task: $($ParentTask.Name) with $($AllBatchItems.Count) total executions" + + if ($PSCmdlet.ShouldProcess('Start-UserTasksOrchestrator', 'Starting Multi-Tenant Task Orchestrator')) { + try { + $OrchestratorId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 10 -Compress) + Write-Information "Multi-tenant orchestrator started for $($ParentTask.Name) with ID: $OrchestratorId" + } catch { + Write-Warning "Failed to start multi-tenant orchestrator for $($ParentTask.Name): $($_.Exception.Message)" + Write-Information $_.InvocationInfo.PositionMessage + } + } } } } diff --git a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 index b0eac304e54c..5ffcaf908254 100644 --- a/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 +++ b/Modules/CIPPCore/Public/Functions/Get-CIPPTenantAlignment.ps1 @@ -34,6 +34,7 @@ function Get-CIPPTenantAlignment { $JSON = $_.JSON try { $RowKey = $_.RowKey + if ([string]::IsNullOrWhiteSpace($JSON)) { return } $Data = $JSON | ConvertFrom-Json -Depth 100 -ErrorAction Stop } catch { Write-Warning "$($RowKey) standard could not be loaded: $($_.Exception.Message)" diff --git a/Modules/CIPPCore/Public/Get-CIPPIntunePolicyAssignments.ps1 b/Modules/CIPPCore/Public/Get-CIPPIntunePolicyAssignments.ps1 new file mode 100644 index 000000000000..591797c17ec3 --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPIntunePolicyAssignments.ps1 @@ -0,0 +1,72 @@ +function Get-CIPPIntunePolicyAssignments { + <# + .SYNOPSIS + Gets the assignments for an existing Intune policy. + .PARAMETER PolicyId + The Intune policy ID. + .PARAMETER TemplateType + The template type (Device, Catalog, Admin, deviceCompliancePolicies, AppProtection, + windowsDriverUpdateProfiles, windowsFeatureUpdateProfiles, windowsQualityUpdatePolicies, + windowsQualityUpdateProfiles). + .PARAMETER TenantFilter + The tenant to query. + .PARAMETER ExistingPolicy + The existing policy object. Required for AppProtection to determine the odata subtype. + .FUNCTIONALITY + Internal + #> + param( + [Parameter(Mandatory = $true)] + [string]$PolicyId, + [Parameter(Mandatory = $true)] + [string]$TemplateType, + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + $ExistingPolicy + ) + + switch ($TemplateType) { + 'Device' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'deviceConfigurations' + } + 'Catalog' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'configurationPolicies' + } + 'Admin' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'groupPolicyConfigurations' + } + 'deviceCompliancePolicies' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'deviceCompliancePolicies' + } + 'AppProtection' { + $PlatformType = 'deviceAppManagement' + $OdataType = if ($ExistingPolicy) { $ExistingPolicy.'@odata.type' -replace '#microsoft.graph.', '' } else { $null } + if (-not $OdataType) { return $null } + $TypeUrl = if ($OdataType -eq 'windowsInformationProtectionPolicy') { 'windowsInformationProtectionPolicies' } else { "${OdataType}s" } + } + 'windowsDriverUpdateProfiles' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'windowsDriverUpdateProfiles' + } + 'windowsFeatureUpdateProfiles' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'windowsFeatureUpdateProfiles' + } + 'windowsQualityUpdatePolicies' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'windowsQualityUpdatePolicies' + } + 'windowsQualityUpdateProfiles' { + $PlatformType = 'deviceManagement' + $TypeUrl = 'windowsQualityUpdateProfiles' + } + default { return $null } + } + + $Uri = "https://graph.microsoft.com/beta/$PlatformType/$TypeUrl('$PolicyId')/assignments" + return New-GraphGetRequest -uri $Uri -tenantid $TenantFilter +} diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 752cf71f8cc7..a0fafd94fa9a 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -150,11 +150,13 @@ function Get-CIPPLicenseOverview { skuId = [string]$sku.skuId skuPartNumber = [string]$PrettyName availableUnits = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits - TermInfo = $TermInfo + TermInfo = @($TermInfo) AssignedUsers = ($UsersBySku.ContainsKey($SkuKey) ? @(($UsersBySku[$SkuKey])) : $null) AssignedGroups = ($GroupsBySku.ContainsKey($SkuKey) ? @(($GroupsBySku[$SkuKey])) : $null) + ServicePlans = $sku.servicePlans } } } return ($GraphRequest | Sort-Object -Property License) } + diff --git a/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 index f7a8e8c32ce2..d4c7f8a94dc0 100644 --- a/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPReusableSettingsFromPolicy.ps1 @@ -8,6 +8,8 @@ function Get-CIPPReusableSettingsFromPolicy { $result = [pscustomobject]@{ ReusableSettings = [System.Collections.Generic.List[psobject]]::new() + RawJSON = $PolicyJson + Map = @{} } if (-not $PolicyJson) { return $result } @@ -196,6 +198,10 @@ function Get-CIPPReusableSettingsFromPolicy { } } + if ($templateGuid) { + $result.Map[$settingId] = $templateGuid + } + $result.ReusableSettings.Add([pscustomobject]@{ displayName = $settingDisplayName templateId = $templateGuid @@ -206,6 +212,14 @@ function Get-CIPPReusableSettingsFromPolicy { } } + if ($result.Map.Count -gt 0) { + $updatedJson = $PolicyJson + foreach ($pair in $result.Map.GetEnumerator()) { + $updatedJson = $updatedJson -replace [regex]::Escape($pair.Key), $pair.Value + } + $result.RawJSON = $updatedJson + } + Write-LogMessage -headers $Headers -API $APIName -message "Reusable settings mapped: $($result.ReusableSettings.Count) -> $($result.ReusableSettings.displayName -join ', ')" -Sev 'Info' return $result } diff --git a/Modules/CIPPCore/Public/GraphHelper/Add-CippQueueMessage.ps1 b/Modules/CIPPCore/Public/GraphHelper/Add-CippQueueMessage.ps1 new file mode 100644 index 000000000000..0252e0b88043 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Add-CippQueueMessage.ps1 @@ -0,0 +1,39 @@ +function Add-CippQueueMessage { + <# + .SYNOPSIS + Push a message to the Azure Storage Queue for background processing + .DESCRIPTION + Wraps Push-OutputBinding to send messages to the cippqueue for processing by CippQueueTrigger. + This offloads orchestration execution to the processor function app. + .PARAMETER Cmdlet + The name of the function to execute (must exist in CIPPCore module) + .PARAMETER Parameters + Hashtable of parameters to pass to the function + .EXAMPLE + Add-CippQueueMessage -Cmdlet 'Start-BPAOrchestrator' -Parameters @{ TenantFilter = 'AllTenants'; Force = $true } + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Cmdlet, + + [Parameter(Mandatory = $false)] + [hashtable]$Parameters = @{} + ) + + $QueueMessage = @{ + Cmdlet = $Cmdlet + Parameters = $Parameters + } + + try { + Push-OutputBinding -Name QueueItem -Value $QueueMessage + Write-Information "Queued $Cmdlet for execution" + return $true + } catch { + Write-Error "Failed to queue message: $_" + return $false + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Get-CIPPOmaSettingDecryptedValue.ps1 b/Modules/CIPPCore/Public/GraphHelper/Get-CIPPOmaSettingDecryptedValue.ps1 new file mode 100644 index 000000000000..206f37feffcd --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Get-CIPPOmaSettingDecryptedValue.ps1 @@ -0,0 +1,120 @@ +function Get-CIPPOmaSettingDecryptedValue { + <# + .SYNOPSIS + Decrypts encrypted OMA setting values from Intune device configurations + + .DESCRIPTION + When Intune policies contain encrypted OMA settings (e.g., using Custom policy templates), + the Graph API returns a placeholder value (PGEvPg==) instead of the actual value. + This function detects encrypted OMA settings and retrieves their plaintext values + using the getOmaSettingPlainTextValue Graph API endpoint. + + .PARAMETER DeviceConfiguration + The device configuration object retrieved from Graph API that may contain encrypted OMA settings + + .PARAMETER DeviceConfigurationId + The ID of the device configuration policy + + .PARAMETER TenantFilter + The tenant ID to query + + .EXAMPLE + $policy = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations/$id" -tenantid $tenant + $decryptedPolicy = Get-CIPPOmaSettingDecryptedValue -DeviceConfiguration $policy -DeviceConfigurationId $id -TenantFilter $tenant + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [object]$DeviceConfiguration, + + [Parameter(Mandatory = $true)] + [string]$DeviceConfigurationId, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + + try { + Write-Host "Checking for encrypted OMA settings in device configuration: $($DeviceConfiguration.displayName)" + # Check if the device configuration has OMA settings + if (-not $DeviceConfiguration.omaSettings -or $DeviceConfiguration.omaSettings.Count -eq 0) { + Write-Verbose 'No OMA settings found in device configuration' + return $DeviceConfiguration + } + + $hasEncryptedSettings = $false + + # Iterate through each OMA setting to find encrypted values + for ($i = 0; $i -lt $DeviceConfiguration.omaSettings.Count; $i++) { + $omaSetting = $DeviceConfiguration.omaSettings[$i] + + # Check if this OMA setting has a secretReferenceValueId (indicates encryption) + if ($omaSetting.secretReferenceValueId) { + $hasEncryptedSettings = $true + Write-Verbose "Found encrypted OMA setting: $($omaSetting.displayName) with secretReferenceValueId: $($omaSetting.secretReferenceValueId)" + + try { + # Call the Graph API to get the plaintext value + $plaintextUri = "https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations('$DeviceConfigurationId')/getOmaSettingPlainTextValue(secretReferenceValueId='$($omaSetting.secretReferenceValueId)')" + Write-Verbose "Calling Graph API: $plaintextUri" + + $plaintextResponse = New-GraphGetRequest -uri $plaintextUri -tenantid $TenantFilter + + # The API returns the plaintext value in the 'value' property + if ($plaintextResponse) { + Write-Verbose "Successfully decrypted OMA setting: $($omaSetting.displayName)" + + # Check the OMA setting type to determine if we need to base64 encode the value + # omaSettingStringXml requires base64 encoded values (Edm.Binary) + # omaSettingString uses plaintext values + $omaType = $DeviceConfiguration.omaSettings[$i].'@odata.type' + + if ($omaType -eq '#microsoft.graph.omaSettingStringXml') { + # For StringXml type, the value must be base64 encoded + Write-Verbose "OMA setting type is StringXml, encoding value to base64" + $bytes = [System.Text.Encoding]::UTF8.GetBytes($plaintextResponse) + $DeviceConfiguration.omaSettings[$i].value = [System.Convert]::ToBase64String($bytes) + } else { + # For other types (String, Integer, Boolean, etc.), use the value as-is + Write-Verbose "OMA setting type is $omaType, using plaintext value" + $DeviceConfiguration.omaSettings[$i].value = $plaintextResponse + } + + # Remove encryption-related properties as we now have the plaintext value + # This is important for when the policy is re-applied to another tenant + # Check if properties exist before attempting to remove them to avoid errors + if ($DeviceConfiguration.omaSettings[$i].PSObject.Properties['secretReferenceValueId']) { + $DeviceConfiguration.omaSettings[$i].PSObject.Properties.Remove('secretReferenceValueId') + } + if ($DeviceConfiguration.omaSettings[$i].PSObject.Properties['isEncrypted']) { + $DeviceConfiguration.omaSettings[$i].PSObject.Properties.Remove('isEncrypted') + } + } else { + Write-Warning "Failed to decrypt OMA setting: $($omaSetting.displayName) - No value returned from API" + } + } catch { + Write-Warning "Error decrypting OMA setting '$($omaSetting.displayName)': $($_.Exception.Message)" + # Continue with other settings even if one fails + } + } + # Also check for the placeholder value PGEvPg== (base64 encoded '') + elseif ($omaSetting.value -eq 'PGEvPg==') { + Write-Warning "Found placeholder value (PGEvPg==) for OMA setting '$($omaSetting.displayName)' but no secretReferenceValueId. This setting may not be decryptable." + } + } + + if (-not $hasEncryptedSettings) { + Write-Verbose 'No encrypted OMA settings found in device configuration' + } + + return $DeviceConfiguration + + } catch { + Write-Error "Error processing OMA settings for device configuration: $($_.Exception.Message)" + # Return the original configuration if there's an error + return $DeviceConfiguration + } +} diff --git a/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 new file mode 100644 index 000000000000..e3f5ed469340 --- /dev/null +++ b/Modules/CIPPCore/Public/GraphHelper/Set-CIPPOffloadFunctionTriggers.ps1 @@ -0,0 +1,109 @@ +function Set-CIPPOffloadFunctionTriggers { + <# + .SYNOPSIS + Manages non-HTTP triggers on function apps based on offloading configuration. + .DESCRIPTION + Automatically detects if running on an offloaded function app (contains hyphen in name). + If this is the main function app (no hyphen), checks the offloading state from Config table + and disables/enables timer, activity, orchestrator, and queue triggers accordingly. + Offloaded function apps (with hyphen) are skipped as they should have triggers enabled. + .EXAMPLE + Set-CIPPOffloadFunctionTriggers + Automatically manages triggers based on current function app context and offloading state. + #> + [CmdletBinding(SupportsShouldProcess)] + param() + + # Get current function app name + $FunctionAppName = $env:WEBSITE_SITE_NAME + + # Check if this is an offloaded function app (contains hyphen) + if ($FunctionAppName -match '-') { + return $true + } + + # Get offloading state from Config table + $Table = Get-CippTable -tablename 'Config' + $OffloadConfig = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'OffloadFunctions' and RowKey eq 'OffloadFunctions'" + $OffloadEnabled = [bool]$OffloadConfig.state + + # Determine resource group + if ($env:WEBSITE_RESOURCE_GROUP) { + $ResourceGroupName = $env:WEBSITE_RESOURCE_GROUP + } else { + $Owner = $env:WEBSITE_OWNER_NAME + if ($env:WEBSITE_SKU -ne 'FlexConsumption' -and $Owner -match '^(?[^+]+)\+(?[^-]+(?:-[^-]+)*?)(?:-[^-]+webspace(?:-Linux)?)?$') { + $ResourceGroupName = $Matches.RGName + } else { + throw 'Could not determine resource group. Please provide ResourceGroupName parameter.' + } + } + + # Define the triggers to disable when offloading is enabled + $TargetedTriggers = @( + 'CIPPTimer' + 'CIPPActivityFunction' + 'CIPPOrchestrator' + 'CIPPQueueTrigger' + ) + + try { + if ($OffloadEnabled -and $env:WEBSITE_SKU -ne 'FlexConsumption') { + $AppSettings = @{} + $SkippedTriggers = [System.Collections.Generic.List[string]]::new() + foreach ($Trigger in $TargetedTriggers) { + $SettingKey = "AzureWebJobs.$Trigger.Disabled" + # Convert setting key to environment variable format (dots become underscores) + $EnvVarName = $SettingKey -replace '\.', '_' + $CurrentValue = [System.Environment]::GetEnvironmentVariable($EnvVarName) + + if ($CurrentValue -eq '1') { + Write-Verbose "Skipping $SettingKey - already set to 1" + $SkippedTriggers.Add($Trigger) + } else { + $AppSettings[$SettingKey] = '1' + Write-Verbose "Setting $SettingKey = 1" + } + } + + # Update app settings only if there are changes to make + if ($AppSettings.Count -gt 0) { + if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Disable non-HTTP triggers')) { + Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting $AppSettings | Out-Null + Write-Information "Successfully disabled $($AppSettings.Count) non-HTTP trigger(s) on $FunctionAppName" + } + } + } else { + $RemoveKeys = [System.Collections.Generic.List[string]]::new() + $SkippedTriggers = [System.Collections.Generic.List[string]]::new() + foreach ($Trigger in $TargetedTriggers) { + $SettingKey = "AzureWebJobs.$Trigger.Disabled" + # Convert setting key to environment variable format (dots become underscores) + $EnvVarName = $SettingKey -replace '\.', '_' + $CurrentValue = [System.Environment]::GetEnvironmentVariable($EnvVarName) + + if ([string]::IsNullOrEmpty($CurrentValue) -or $CurrentValue -ne '1') { + Write-Verbose "Skipping $SettingKey - already enabled or not set" + $SkippedTriggers.Add($Trigger) + } else { + $RemoveKeys.Add($SettingKey) + Write-Verbose "Removing $SettingKey" + } + } + + # Update app settings with removal of keys only if there are changes to make + if ($RemoveKeys.Count -gt 0) { + if ($PSCmdlet.ShouldProcess($FunctionAppName, 'Re-enable non-HTTP triggers')) { + Update-CIPPAzFunctionAppSetting -Name $FunctionAppName -ResourceGroupName $ResourceGroupName -AppSetting @{} -RemoveKeys $RemoveKeys | Out-Null + Write-Information "Successfully re-enabled $($RemoveKeys.Count) non-HTTP trigger(s) on $FunctionAppName" + } + } + } + + return $true + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-Warning "Failed to update trigger settings: $($ErrorMessage.NormalizedError)" + return $false + } +} diff --git a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 index 7ca83d89a119..b1ec9b5d6c80 100644 --- a/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 +++ b/Modules/CIPPCore/Public/GraphRequests/Get-GraphRequestList.ps1 @@ -279,7 +279,7 @@ function Get-GraphRequestList { Batch = @($Batch) } #Write-Information ($InputObject | ConvertTo-Json -Depth 5) - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject } catch { Write-Information "QUEUE ERROR: $($_.Exception.Message)" } @@ -321,7 +321,7 @@ function Get-GraphRequestList { OrchestratorName = 'GraphRequestOrchestrator' Batch = @($QueueTenant) } - $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + $InstanceId = Start-CIPPOrchestrator -InputObject $InputObject [PSCustomObject]@{ QueueMessage = ('Loading {0} rows for {1}. Please check back after the job completes' -f $Count, $TenantFilter) diff --git a/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 index 8360058a0a29..fbce430ac03c 100644 --- a/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 @@ -64,6 +64,13 @@ function New-CIPPIntuneTemplate { 'deviceConfigurations' { $Type = 'Device' $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $TenantFilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' + + # Check for and decrypt encrypted OMA settings + if ($Template.omaSettings) { + Write-Information "Checking for encrypted OMA settings in policy: $($Template.displayName)" + $Template = Get-CIPPOmaSettingDecryptedValue -DeviceConfiguration $Template -DeviceConfigurationId $ID -TenantFilter $TenantFilter + } + $DisplayName = $Template.displayName $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress } diff --git a/Modules/CIPPCore/Public/Search-CIPPBitlockerKeys.ps1 b/Modules/CIPPCore/Public/Search-CIPPBitlockerKeys.ps1 new file mode 100644 index 000000000000..d018b678b1e6 --- /dev/null +++ b/Modules/CIPPCore/Public/Search-CIPPBitlockerKeys.ps1 @@ -0,0 +1,162 @@ +function Search-CIPPBitlockerKeys { + <# + .SYNOPSIS + Search for BitLocker recovery keys and merge with device information + + .DESCRIPTION + Searches cached BitLocker recovery keys and automatically enriches results with device information + by cross-referencing the deviceId with Devices or ManagedDevices data. + + .PARAMETER TenantFilter + Tenant domain or GUID to search. If not specified, searches all tenants. + + .PARAMETER KeyId + Optional BitLocker recovery key ID to search for. If not specified, returns all keys. + + .PARAMETER DeviceId + Optional device ID to filter BitLocker keys by device. + + .PARAMETER SearchTerms + Optional search terms to filter results (searches across all BitLocker key fields). + + .PARAMETER Limit + Maximum number of results to return. Default is unlimited (0). + + .EXAMPLE + Search-CIPPBitlockerKeys -TenantFilter 'contoso.onmicrosoft.com' -KeyId '8911a878-b631-47e8-b5e8-bcb00e586c74' + + .EXAMPLE + Search-CIPPBitlockerKeys -DeviceId '1b418b08-a0c6-4db1-95cd-08a9b943b70e' + + .EXAMPLE + Search-CIPPBitlockerKeys -SearchTerms 'device-name' + + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)] + [string]$TenantFilter, + + [Parameter(Mandatory = $false)] + [string]$KeyId, + + [Parameter(Mandatory = $false)] + [string]$DeviceId, + + [Parameter(Mandatory = $false)] + [string[]]$SearchTerms, + + [Parameter(Mandatory = $false)] + [int]$Limit = 0 + ) + + try { + # Build search parameters + $SearchParams = @{ + Types = @('BitlockerKeys') + } + + if ($TenantFilter) { + $SearchParams.TenantFilter = @($TenantFilter) + } + + # Determine what to search for + if ($KeyId) { + $SearchParams.SearchTerms = @($KeyId) + } elseif ($DeviceId) { + $SearchParams.SearchTerms = @($DeviceId) + } elseif ($SearchTerms) { + $SearchParams.SearchTerms = $SearchTerms + } else { + # If no search criteria, search for a pattern that matches any GUID or just get all + $SearchParams.SearchTerms = @('[a-f0-9]{8}-') + } + + if ($Limit -gt 0) { + $SearchParams.Limit = $Limit + } + + Write-Verbose "Searching for BitLocker keys with params: $($SearchParams | ConvertTo-Json -Compress)" + + # Search for BitLocker keys + $BitlockerResults = Search-CIPPDbData @SearchParams + + if (-not $BitlockerResults -or $BitlockerResults.Count -eq 0) { + Write-Verbose 'No BitLocker keys found' + return @() + } + + Write-Verbose "Found $($BitlockerResults.Count) BitLocker key(s)" + + # Enrich each result with device information + $EnrichedResults = foreach ($Result in $BitlockerResults) { + $BitlockerData = $Result.Data + $DeviceInfo = $null + + if ($BitlockerData.deviceId) { + Write-Verbose "Looking up device info for deviceId: $($BitlockerData.deviceId)" + + # Try to find device in Devices collection first + try { + $DeviceSearch = Search-CIPPDbData -TenantFilter $Result.Tenant -Types 'Devices' -SearchTerms $BitlockerData.deviceId -Limit 1 + if ($DeviceSearch -and $DeviceSearch.Count -gt 0) { + $DeviceInfo = $DeviceSearch[0].Data + Write-Verbose "Found device in Devices collection: $($DeviceInfo.displayName)" + } + } catch { + Write-Verbose "Error searching Devices: $($_.Exception.Message)" + } + + # If not found in Devices, try ManagedDevices + if (-not $DeviceInfo) { + try { + $DeviceSearch = Search-CIPPDbData -TenantFilter $Result.Tenant -Types 'ManagedDevices' -SearchTerms $BitlockerData.deviceId -Limit 1 + if ($DeviceSearch -and $DeviceSearch.Count -gt 0) { + $DeviceInfo = $DeviceSearch[0].Data + Write-Verbose "Found device in ManagedDevices collection: $($DeviceInfo.deviceName)" + } + } catch { + Write-Verbose "Error searching ManagedDevices: $($_.Exception.Message)" + } + } + } + + # Create enriched result + $EnrichedData = [PSCustomObject]@{ + # BitLocker key information + id = $BitlockerData.id + createdDateTime = $BitlockerData.createdDateTime + volumeType = $BitlockerData.volumeType + deviceId = $BitlockerData.deviceId + + # Device information (if found) + deviceName = if ($DeviceInfo) { $DeviceInfo.displayName ?? $DeviceInfo.deviceName } else { $null } + operatingSystem = if ($DeviceInfo) { $DeviceInfo.operatingSystem } else { $null } + osVersion = if ($DeviceInfo) { $DeviceInfo.operatingSystemVersion ?? $DeviceInfo.osVersion } else { $null } + lastSignIn = if ($DeviceInfo) { $DeviceInfo.approximateLastSignInDateTime ?? $DeviceInfo.lastSyncDateTime } else { $null } + accountEnabled = if ($DeviceInfo) { $DeviceInfo.accountEnabled ?? $DeviceInfo.isCompliant } else { $null } + trustType = if ($DeviceInfo) { $DeviceInfo.trustType ?? $DeviceInfo.joinType } else { $null } + + # Metadata + deviceFound = $null -ne $DeviceInfo + } + + [PSCustomObject]@{ + Tenant = $Result.Tenant + Type = $Result.Type + RowKey = $Result.RowKey + Data = $EnrichedData + Timestamp = $Result.Timestamp + } + } + + Write-Verbose "Returning $($EnrichedResults.Count) enriched result(s)" + return $EnrichedResults + + } catch { + Write-LogMessage -API 'SearchBitlockerKeys' -tenant $TenantFilter -message "Failed to search BitLocker keys: $($_.Exception.Message)" -sev Error + throw + } +} diff --git a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 index 1f60a59a4f2c..cf09305a8dd6 100644 --- a/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 +++ b/Modules/CIPPCore/Public/Search-CIPPDbData.ps1 @@ -58,7 +58,7 @@ function Search-CIPPDbData { [CmdletBinding()] param( [Parameter(Mandatory = $false)] - [string]$TenantFilter, + [string[]]$TenantFilter, [Parameter(Mandatory = $true)] [string[]]$SearchTerms, @@ -68,7 +68,8 @@ function Search-CIPPDbData { 'Users', 'Domains', 'ConditionalAccessPolicies', 'ManagedDevices', 'Organization', 'Groups', 'Roles', 'LicenseOverview', 'IntuneDeviceCompliancePolicies', 'SecureScore', 'SecureScoreControlProfiles', 'Mailboxes', 'CASMailbox', 'MailboxPermissions', - 'OneDriveUsage', 'MailboxUsage', 'Devices', 'AllRoles', 'Licenses', 'DeviceCompliancePolicies' + 'OneDriveUsage', 'MailboxUsage', 'Devices', 'AllRoles', 'Licenses', 'DeviceCompliancePolicies', + 'BitlockerKeys' )] [string[]]$Types, diff --git a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 index 771c6f4fc6fe..b555edf48b75 100644 --- a/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPAssignedApplication.ps1 @@ -9,10 +9,29 @@ function Set-CIPPAssignedApplication { $GroupIds, $AssignmentMode = 'replace', $APIName = 'Assign Application', - $Headers + $Headers, + $AssignmentFilterName, + $AssignmentFilterType = 'include' ) Write-Host "GroupName: $GroupName Intent: $Intent AppType: $AppType ApplicationId: $ApplicationId TenantFilter: $TenantFilter APIName: $APIName" try { + # Resolve assignment filter name to ID if provided + $ResolvedFilterId = $null + if ($AssignmentFilterName) { + Write-Host "Looking up assignment filter by name: $AssignmentFilterName" + $AllFilters = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/assignmentFilters' -tenantid $TenantFilter + $MatchingFilter = $AllFilters | Where-Object { $_.displayName -like $AssignmentFilterName } | Select-Object -First 1 + + if ($MatchingFilter) { + $ResolvedFilterId = $MatchingFilter.id + Write-Host "Found assignment filter: $($MatchingFilter.displayName) with ID: $ResolvedFilterId" + } else { + $ErrorMessage = "No assignment filter found matching the name: $AssignmentFilterName. Application assigned without filter." + Write-LogMessage -headers $Headers -API $APIName -message $ErrorMessage -sev 'Warn' -tenant $TenantFilter + Write-Host $ErrorMessage + } + } + $assignmentSettings = $null if ($AppType) { $assignmentSettings = @{ @@ -118,6 +137,15 @@ function Set-CIPPAssignedApplication { } } + # Add assignment filter to each assignment if specified + if ($ResolvedFilterId) { + Write-Host "Adding assignment filter $ResolvedFilterId with type $AssignmentFilterType to assignments" + foreach ($assignment in $MobileAppAssignment) { + $assignment.target.deviceAndAppManagementAssignmentFilterId = $ResolvedFilterId + $assignment.target.deviceAndAppManagementAssignmentFilterType = $AssignmentFilterType + } + } + # If we're appending, we need to get existing assignments if ($AssignmentMode -eq 'append') { try { diff --git a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 index adba643d3f2b..8435ecf7dbdd 100644 --- a/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPCalendarPermission.ps1 @@ -11,7 +11,8 @@ function Set-CIPPCalendarPermission { $LoggingName, $Permissions, [bool]$CanViewPrivateItems, - [bool]$SendNotificationToUser = $false + [bool]$SendNotificationToUser = $false, + [switch]$AutoResolveFolderName ) try { @@ -22,8 +23,21 @@ function Set-CIPPCalendarPermission { $LoggingName = $UserToGetPermissions } + # When -AutoResolveFolderName is set, look up the locale-independent FolderId. + # FolderType -eq 'Calendar' is an internal Exchange enum, always English regardless of mailbox language. + # Callers that already supply the correct localized FolderName should NOT pass this switch. + if ($AutoResolveFolderName) { + $CalFolderStats = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-MailboxFolderStatistics' -cmdParams @{ + Identity = $UserID + FolderScope = 'Calendar' + } -Anchor $UserID | Where-Object { $_.FolderType -eq 'Calendar' } + $FolderIdentity = if ($CalFolderStats) { "$($UserID):$($CalFolderStats.FolderId)" } else { "$($UserID):\$FolderName" } + } else { + $FolderIdentity = "$($UserID):\$FolderName" + } + $CalParam = [PSCustomObject]@{ - Identity = "$($UserID):\$FolderName" + Identity = $FolderIdentity AccessRights = @($Permissions) User = $UserToGetPermissions SendNotificationToUser = $SendNotificationToUser @@ -35,10 +49,10 @@ function Set-CIPPCalendarPermission { if ($RemoveAccess) { if ($PSCmdlet.ShouldProcess("$UserID\$FolderName", "Remove permissions for $LoggingName")) { - $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = "$($UserID):\$FolderName"; User = $RemoveAccess } + $null = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Remove-MailboxFolderPermission' -cmdParams @{Identity = $FolderIdentity; User = $RemoveAccess } $Result = "Successfully removed access for $LoggingName from calendar $($CalParam.Identity)" Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info - + # Sync cache Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $UserID -FolderName $FolderName -User $RemoveAccess -Action 'Remove' } @@ -57,7 +71,7 @@ function Set-CIPPCalendarPermission { $Result += ' A notification has been sent to the user.' } Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -sev Info - + # Sync cache Sync-CIPPCalendarPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $UserID -FolderName $FolderName -User $UserToGetPermissions -Permissions $Permissions -Action 'Add' } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 index da9738675345..de2780b0497c 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheAuthenticationFlowsPolicy.ps1 @@ -27,9 +27,6 @@ function Set-CIPPDBCacheAuthenticationFlowsPolicy { } } catch { - Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter ` - -message "Failed to cache authentication flows policy: $($_.Exception.Message)" ` - -sev Warning ` - -LogData (Get-CippException -Exception $_) + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache authentication flows policy: $($_.Exception.Message)" -sev Warning -LogData (Get-CippException -Exception $_) } } diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheBitlockerKeys.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheBitlockerKeys.ps1 new file mode 100644 index 000000000000..8bb2bf9971fd --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheBitlockerKeys.ps1 @@ -0,0 +1,33 @@ +function Set-CIPPDBCacheBitlockerKeys { + <# + .SYNOPSIS + Caches all BitLocker recovery keys for a tenant + + .PARAMETER TenantFilter + The tenant to cache BitLocker keys for + + .PARAMETER QueueId + The queue ID to update with total tasks (optional) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$QueueId + ) + + try { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching BitLocker recovery keys' -sev Debug + + $BitlockerKeys = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/informationProtection/bitlocker/recoveryKeys' -tenantid $TenantFilter + if (!$BitlockerKeys) { $BitlockerKeys = @() } + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'BitlockerKeys' -Data $BitlockerKeys + Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'BitlockerKeys' -Data $BitlockerKeys -Count + $BitlockerKeys = $null + + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Cached BitLocker recovery keys successfully' -sev Debug + + } catch { + Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message "Failed to cache BitLocker recovery keys: $($_.Exception.Message)" -sev Error + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 index d42279de2ec3..d8a10e8e703b 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDBCacheMailboxes.ps1 @@ -18,7 +18,7 @@ function Set-CIPPDBCacheMailboxes { [Parameter(Mandatory = $true)] [string]$TenantFilter, [string]$QueueId, - [ValidateSet('All', 'Permissions', 'CalendarPermissions', 'Rules')] + [ValidateSet('All', 'None', 'Permissions', 'CalendarPermissions', 'Rules')] [string[]]$Types = @('All') ) @@ -26,7 +26,7 @@ function Set-CIPPDBCacheMailboxes { Write-LogMessage -API 'CIPPDBCache' -tenant $TenantFilter -message 'Caching mailboxes' -sev Debug # Get mailboxes with select properties - $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled' + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox,ForwardingSmtpAddress,DeliverToMailboxAndForward,ForwardingAddress,HiddenFromAddressListsEnabled,ExternalDirectoryObjectId,MessageCopyForSendOnBehalfEnabled,MessageCopyForSentAsEnabled,GrantSendOnBehalfTo' $ExoRequest = @{ tenantid = $TenantFilter cmdlet = 'Get-Mailbox' @@ -51,7 +51,8 @@ function Set-CIPPDBCacheMailboxes { HiddenFromAddressListsEnabled, ExternalDirectoryObjectId, MessageCopyForSendOnBehalfEnabled, - MessageCopyForSentAsEnabled)) + MessageCopyForSentAsEnabled, + GrantSendOnBehalfTo)) } $Mailboxes | Add-CIPPDbItem -TenantFilter $TenantFilter -Type 'Mailboxes' -AddCount @@ -61,6 +62,8 @@ function Set-CIPPDBCacheMailboxes { # Expand 'All' to all available types if ($Types -contains 'All') { $Types = @('Permissions', 'CalendarPermissions', 'Rules') + } elseif ($Types -contains 'None') { + $Types = @() } # Process additional types if specified @@ -77,6 +80,7 @@ function Set-CIPPDBCacheMailboxes { # Separate batches for permissions and rules $PermissionBatches = [System.Collections.Generic.List[object]]::new() $RuleBatches = [System.Collections.Generic.List[object]]::new() + $AllMailboxData = @($Mailboxes | Select-Object id, UPN, GrantSendOnBehalfTo) for ($i = 0; $i -lt $Mailboxes.Count; $i += $BatchSize) { $BatchMailboxes = $Mailboxes[$i..[Math]::Min($i + $BatchSize - 1, $Mailboxes.Count - 1)] @@ -90,6 +94,7 @@ function Set-CIPPDBCacheMailboxes { QueueName = "Mailbox Permissions Batch $BatchNumber/$TotalBatches - $TenantFilter" TenantFilter = $TenantFilter Mailboxes = $BatchMailboxUPNs + MailboxData = $AllMailboxData BatchNumber = $BatchNumber TotalBatches = $TotalBatches }) diff --git a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 index 4a46e344d5e9..8d10d3137e03 100644 --- a/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPDefaultAPDeploymentProfile.ps1 @@ -16,7 +16,7 @@ function Set-CIPPDefaultAPDeploymentProfile { $AutoKeyboard, $Headers, $Language = 'os-default', - $APIName = 'Add Default Enrollment Status Page' + $APIName = 'Add Default Autopilot Deployment Profile' ) $User = $Request.Headers diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxPermission.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxPermission.ps1 new file mode 100644 index 000000000000..7f07def04cff --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxPermission.ps1 @@ -0,0 +1,150 @@ +function Set-CIPPMailboxPermission { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$UserId, + + [Parameter(Mandatory = $true)] + [string]$AccessUser, + + [Parameter(Mandatory = $true)] + [ValidateSet('FullAccess', 'SendAs', 'SendOnBehalf', 'ReadPermission', + 'ExternalAccount', 'DeleteItem', 'ChangePermission', 'ChangeOwner')] + [string]$PermissionLevel, + + [Parameter(Mandatory = $true)] + [ValidateSet('Add', 'Remove')] + [string]$Action, + + [bool]$AutoMap = $true, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [string]$APIName = 'Set Mailbox Permission', + + $Headers, + + [switch]$AsCmdletObject + ) + + $CmdletName = '' + $CmdletParams = @{} + $ExpectedResult = '' + + switch ($PermissionLevel) { + 'FullAccess' { + if ($Action -eq 'Add') { + $CmdletName = 'Add-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId + user = $AccessUser + accessRights = @('FullAccess') + automapping = $AutoMap + InheritanceType = 'all' + Confirm = $false + } + $ExpectedResult = "Granted $AccessUser FullAccess to $UserId with automapping $AutoMap" + } else { + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId + user = $AccessUser + accessRights = @('FullAccess') + Confirm = $false + } + $ExpectedResult = "Removed $AccessUser FullAccess from $UserId" + } + } + 'SendAs' { + if ($Action -eq 'Add') { + $CmdletName = 'Add-RecipientPermission' + $CmdletParams = @{ + Identity = $UserId + Trustee = $AccessUser + accessRights = @('SendAs') + Confirm = $false + } + $ExpectedResult = "Granted $AccessUser SendAs permissions to $UserId" + } else { + $CmdletName = 'Remove-RecipientPermission' + $CmdletParams = @{ + Identity = $UserId + Trustee = $AccessUser + accessRights = @('SendAs') + Confirm = $false + } + $ExpectedResult = "Removed $AccessUser SendAs permissions from $UserId" + } + } + 'SendOnBehalf' { + $CmdletName = 'Set-Mailbox' + if ($Action -eq 'Add') { + $CmdletParams = @{ + Identity = $UserId + GrantSendonBehalfTo = @{ + '@odata.type' = '#Exchange.GenericHashTable' + add = $AccessUser + } + Confirm = $false + } + $ExpectedResult = "Granted $AccessUser SendOnBehalf permissions to $UserId" + } else { + $CmdletParams = @{ + Identity = $UserId + GrantSendonBehalfTo = @{ + '@odata.type' = '#Exchange.GenericHashTable' + remove = $AccessUser + } + Confirm = $false + } + $ExpectedResult = "Removed $AccessUser SendOnBehalf permissions from $UserId" + } + } + default { + # ReadPermission, ExternalAccount, DeleteItem, ChangePermission, ChangeOwner — Remove only + if ($Action -eq 'Remove') { + $CmdletName = 'Remove-MailboxPermission' + $CmdletParams = @{ + Identity = $UserId + user = $AccessUser + accessRights = @($PermissionLevel) + Confirm = $false + } + $ExpectedResult = "Removed $AccessUser $PermissionLevel from $UserId" + } else { + return "Add action is not supported for $PermissionLevel" + } + } + } + + if ($AsCmdletObject) { + return @{ + CmdletName = $CmdletName + Parameters = $CmdletParams + ExpectedResult = $ExpectedResult + } + } + + # Execute mode + try { + $null = New-ExoRequest -Anchor $UserId -tenantid $TenantFilter -cmdlet $CmdletName -cmdParams $CmdletParams + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $ExpectedResult -Sev 'Info' + + # Sync cache for permission types that have cache entries + if ($PermissionLevel -in @('FullAccess', 'SendAs', 'SendOnBehalf')) { + try { + Sync-CIPPMailboxPermissionCache -TenantFilter $TenantFilter -MailboxIdentity $UserId -User $AccessUser -PermissionType $PermissionLevel -Action $Action + } catch { + Write-Information "Cache sync warning: $($_.Exception.Message)" + } + } + + return $ExpectedResult + } catch { + $ErrorMessage = (Get-CippException -Exception $_).NormalizedError + $Result = "Failed to $Action $PermissionLevel for $AccessUser on ${UserId}: $ErrorMessage" + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message $Result -Sev 'Error' -LogData (Get-CippException -Exception $_) + return $Result + } +} diff --git a/Modules/CIPPCore/Public/Set-CIPPMailboxVacation.ps1 b/Modules/CIPPCore/Public/Set-CIPPMailboxVacation.ps1 new file mode 100644 index 000000000000..e190c6b3a0d6 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPMailboxVacation.ps1 @@ -0,0 +1,95 @@ +function Set-CIPPMailboxVacation { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + + [Parameter(Mandatory = $true)] + [ValidateSet('Add', 'Remove')] + [string]$Action, + + [object[]]$MailboxPermissions, + + [object[]]$CalendarPermissions, + + [string]$APIName = 'Mailbox Vacation Mode', + + $Headers + ) + + $Results = [System.Collections.Generic.List[string]]::new() + + # Normalize single-element arrays (JSON deserialization quirk) + $MailboxPermissions = @($MailboxPermissions) + $CalendarPermissions = @($CalendarPermissions) + + # Process mailbox permissions + foreach ($perm in $MailboxPermissions) { + if ($null -eq $perm) { continue } + + # Handle both hashtable and PSCustomObject property access + $permUserId = $perm.UserId ?? $perm['UserId'] + $permAccessUser = $perm.AccessUser ?? $perm['AccessUser'] + $permLevel = $perm.PermissionLevel ?? $perm['PermissionLevel'] + $permAutoMap = $perm.AutoMap ?? $perm['AutoMap'] ?? $true + + if (-not $permUserId -or -not $permAccessUser -or -not $permLevel) { + $Results.Add('Skipped mailbox permission with missing fields') + continue + } + + $PermSplat = @{ + UserId = $permUserId + AccessUser = $permAccessUser + PermissionLevel = $permLevel + Action = $Action + AutoMap = [bool]$permAutoMap + TenantFilter = $TenantFilter + APIName = $APIName + Headers = $Headers + } + $result = Set-CIPPMailboxPermission @PermSplat + + $Results.Add($result) + } + + # Process calendar permissions + foreach ($calPerm in $CalendarPermissions) { + if ($null -eq $calPerm) { continue } + + $calUserId = $calPerm.UserID ?? $calPerm['UserID'] + $calDelegate = $calPerm.UserToGetPermissions ?? $calPerm['UserToGetPermissions'] + $calFolder = $calPerm.FolderName ?? $calPerm['FolderName'] ?? 'Calendar' + $calPermissions = $calPerm.Permissions ?? $calPerm['Permissions'] + $calPrivateItems = $calPerm.CanViewPrivateItems ?? $calPerm['CanViewPrivateItems'] ?? $false + + if (-not $calUserId -or -not $calDelegate) { + $Results.Add('Skipped calendar permission with missing fields') + continue + } + + try { + $CalSplat = @{ + TenantFilter = $TenantFilter + UserID = $calUserId + FolderName = $calFolder + APIName = $APIName + Headers = $Headers + } + if ($Action -eq 'Remove') { + $CalSplat.RemoveAccess = $calDelegate + } else { + $CalSplat.UserToGetPermissions = $calDelegate + $CalSplat.Permissions = $calPermissions + $CalSplat.CanViewPrivateItems = [bool]$calPrivateItems + } + $result = Set-CIPPCalendarPermission @CalSplat + $Results.Add($result) + } catch { + $ErrorMessage = (Get-CippException -Exception $_).NormalizedError + $Results.Add("Failed calendar permission for $calDelegate on ${calUserId}: $ErrorMessage") + } + } + + return $Results +} diff --git a/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 b/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 new file mode 100644 index 000000000000..9ae967996977 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPVacationOOO.ps1 @@ -0,0 +1,43 @@ +function Set-CIPPVacationOOO { + param( + [Parameter(Mandatory)] [string]$TenantFilter, + [Parameter(Mandatory)] [ValidateSet('Add', 'Remove')] [string]$Action, + [object[]]$Users, + [string]$InternalMessage, + [string]$ExternalMessage, + [string]$APIName = 'OOO Vacation Mode', + $Headers + ) + + $Results = [System.Collections.Generic.List[string]]::new() + + foreach ($upn in $Users) { + if ([string]::IsNullOrWhiteSpace($upn)) { continue } + try { + $SplatParams = @{ + UserID = $upn + TenantFilter = $TenantFilter + State = if ($Action -eq 'Add') { 'Enabled' } else { 'Disabled' } + APIName = $APIName + Headers = $Headers + } + # Only pass messages on Add — Remove only disables, preserving any messages + # the user may have updated themselves during vacation + if ($Action -eq 'Add') { + if (-not [string]::IsNullOrWhiteSpace($InternalMessage)) { + $SplatParams.InternalMessage = $InternalMessage + } + if (-not [string]::IsNullOrWhiteSpace($ExternalMessage)) { + $SplatParams.ExternalMessage = $ExternalMessage + } + } + $result = Set-CIPPOutOfOffice @SplatParams + $Results.Add($result) + } catch { + $err = (Get-CippException -Exception $_).NormalizedError + $Results.Add("Failed to set OOO for ${upn}: $err") + Write-LogMessage -headers $Headers -API $APIName -tenant $TenantFilter -message "Failed OOO for ${upn}: $err" -Sev Error + } + } + return $Results +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 index e4831bb8f50e..4a2ff5224389 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAddDKIM.ps1 @@ -113,10 +113,10 @@ function Invoke-CIPPStandardAddDKIM { $MissingDKIM = [System.Collections.Generic.List[string]]::new() if ($null -ne $NewDomains) { - $MissingDKIM.AddRange($NewDomains) + $MissingDKIM.AddRange(@($NewDomains)) } if ($null -ne $SetDomains) { - $MissingDKIM.AddRange($SetDomains.Domain) + $MissingDKIM.AddRange(@($SetDomains.Domain)) } $CurrentValue = if ($MissingDKIM.Count -eq 0) { [PSCustomObject]@{'state' = 'Configured correctly' } } else { [PSCustomObject]@{'MissingDKIM' = $MissingDKIM } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 index cb3591f04121..90a9730dfd42 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDelegateSentItems.ps1 @@ -53,7 +53,7 @@ function Invoke-CIPPStandardDelegateSentItems { $CurrentValue = if (!$Mailboxes) { [PSCustomObject]@{ state = 'Configured correctly' } } else { - [PSCustomObject]@{ NonCompliantMailboxes = $Mailboxes | Select-Object -Property UserPrincipalName, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled } + [PSCustomObject]@{ NonCompliantMailboxes = $Mailboxes | Select-Object -Property UPN, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled } } $ExpectedValue = [PSCustomObject]@{ state = 'Configured correctly' @@ -66,7 +66,7 @@ function Invoke-CIPPStandardDelegateSentItems { @{ CmdletInput = @{ CmdletName = 'Set-Mailbox' - Parameters = @{Identity = $Mailbox.UserPrincipalName ; MessageCopyForSendOnBehalfEnabled = $true; MessageCopyForSentAsEnabled = $true } + Parameters = @{Identity = $Mailbox.UPN ; MessageCopyForSendOnBehalfEnabled = $true; MessageCopyForSentAsEnabled = $true } } } } @@ -97,7 +97,7 @@ function Invoke-CIPPStandardDelegateSentItems { } if ($Settings.report -eq $true) { - $Filtered = $Mailboxes | Select-Object -Property UserPrincipalName, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled + $Filtered = $Mailboxes | Select-Object -Property UPN, MessageCopyForSendOnBehalfEnabled, MessageCopyForSentAsEnabled Set-CIPPStandardsCompareField -FieldName 'standards.DelegateSentItems' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant Add-CIPPBPAField -FieldName 'DelegateSentItems' -FieldValue $Filtered -StoreAs json -Tenant $Tenant } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index c08f688dace3..f9f4236205cb 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -38,46 +38,66 @@ function Invoke-CIPPStandardIntuneTemplate { param($Tenant, $Settings) Write-Host 'INTUNETEMPLATERUN' + $sw = [System.Diagnostics.Stopwatch]::StartNew() + $lap = $sw.Elapsed + $Table = Get-CippTable -tablename 'templates' $Filter = "PartitionKey eq 'IntuneTemplate'" $Template = (Get-CIPPAzDataTableEntity @Table -Filter $Filter | Where-Object -Property RowKey -Like "$($Settings.TemplateList.value)*").JSON | ConvertFrom-Json -ErrorAction SilentlyContinue + Write-Information "[IntuneTemplate][$Tenant] TableLoad: $([int]($sw.Elapsed - $lap).TotalMilliseconds)ms" + $lap = $sw.Elapsed + if ($null -eq $Template) { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to find template $($Settings.TemplateList.value). Has this Intune Template been deleted?" -sev 'Error' return $true } + $rawJsonFromTemplate = $Template.RAWJson try { $reusableSync = Sync-CIPPReusablePolicySettings -TemplateInfo $Template -Tenant $Tenant -ErrorAction Stop if ($null -ne $reusableSync -and $reusableSync.PSObject.Properties.Name -contains 'RawJSON' -and $reusableSync.RawJSON) { - $Template.RawJSON = $reusableSync.RawJSON + $rawJsonFromTemplate = $reusableSync.RawJSON } } catch { Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to sync reusable policy settings for template $($Settings.TemplateList.value): $($_.Exception.Message)" -sev 'Error' Write-Host "IntuneTemplate: $($Settings.TemplateList.value) - Failed to sync reusable policy settings. Skipping this template." return $true } + Write-Information "[IntuneTemplate][$Tenant] ReusableSync: $([int]($sw.Elapsed - $lap).TotalMilliseconds)ms" + $lap = $sw.Elapsed $displayname = $Template.Displayname $description = $Template.Description - $RawJSON = $Template.RawJSON + $RawJSON = $rawJsonFromTemplate $TemplateType = $Template.Type + $AssignmentsMatch = $null try { $ExistingPolicy = Get-CIPPIntunePolicy -tenantFilter $Tenant -DisplayName $displayname -TemplateType $TemplateType + if ($ExistingPolicy -and $Settings.verifyAssignments -eq $true) { + Write-Information "Verifying assignments for tenant $Tenant" + $ExistingAssignments = Get-CIPPIntunePolicyAssignments -PolicyId $ExistingPolicy.id -TemplateType $TemplateType -TenantFilter $Tenant -ExistingPolicy $ExistingPolicy + $AssignmentsMatch = Compare-CIPPIntuneAssignments -ExistingAssignments $ExistingAssignments -ExpectedAssignTo $Settings.AssignTo -ExpectedCustomGroup $Settings.customGroup -ExpectedExcludeGroup $Settings.excludeGroup -ExpectedAssignmentFilter $Settings.assignmentFilter -ExpectedAssignmentFilterType $Settings.assignmentFilterType -TenantFilter $Tenant + + Write-Information "AssignmentsMatch for tenant $($Tenant): $AssignmentsMatch" + } } catch { $ExistingPolicy = $null } + Write-Information "[IntuneTemplate][$Tenant] GetPolicy '$displayname' ($TemplateType): $([int]($sw.Elapsed - $lap).TotalMilliseconds)ms" + $lap = $sw.Elapsed if ($ExistingPolicy) { try { $RawJSON = Get-CIPPTextReplacement -Text $RawJSON -TenantFilter $Tenant $JSONExistingPolicy = $ExistingPolicy.cippconfiguration | ConvertFrom-Json $JSONTemplate = $RawJSON | ConvertFrom-Json - #This might be a slow one. $Compare = Compare-CIPPIntuneObject -ReferenceObject $JSONTemplate -DifferenceObject $JSONExistingPolicy -compareType $TemplateType -ErrorAction SilentlyContinue } catch { } + Write-Information "[IntuneTemplate][$Tenant] Compare '$displayname': $([int]($sw.Elapsed - $lap).TotalMilliseconds)ms" + $lap = $sw.Elapsed } else { $compare = [pscustomobject]@{ MatchFailed = $true @@ -101,6 +121,7 @@ function Invoke-CIPPStandardIntuneTemplate { customGroup = $Settings.customGroup assignmentFilter = $Settings.assignmentFilter assignmentFilterType = $Settings.assignmentFilterType + AssignmentsMatch = $AssignmentsMatch } if ($Settings.remediate) { @@ -127,16 +148,28 @@ function Invoke-CIPPStandardIntuneTemplate { } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to create or update Intune Template $($CompareResult.displayname), Error: $ErrorMessage" -sev 'Error' + Write-Information "IntuneTemplate: $($CompareResult.displayname) - Failed to remediate. Error: $ErrorMessage" } + Write-Information "[IntuneTemplate][$Tenant] Remediate '$displayname': $([int]($sw.Elapsed - $lap).TotalMilliseconds)ms" + $lap = $sw.Elapsed } if ($Settings.alert) { - $AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId - if ($CompareResult.compare) { - Write-StandardsAlert -message "Template $($CompareResult.displayname) does not match the expected configuration." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId - Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) does not match the expected configuration. We've generated an alert" -sev info + $AlertObj = $CompareResult | Select-Object -Property displayname, description, compare, assignTo, excludeGroup, existingPolicyId, AssignmentsMatch + $AssignmentsDiffer = $Settings.verifyAssignments -and ($null -ne $CompareResult.AssignmentsMatch -and -not $CompareResult.AssignmentsMatch) + $HasDifference = $CompareResult.compare -or $AssignmentsDiffer + if ($HasDifference) { + $Message = if ($CompareResult.compare) { + "Template $($CompareResult.displayname) does not match the expected configuration." + } elseif ($AssignmentsDiffer) { + "Template $($CompareResult.displayname) has incorrect assignments." + } else { + "Template $($CompareResult.displayname) does not match the expected configuration." + } + Write-StandardsAlert -message $Message -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "$Message We've generated an alert" -sev info } else { - if ($CompareResult.ExistingPolicyId) { + if ($CompareResult.existingPolicyId) { Write-LogMessage -API 'Standards' -tenant $Tenant -message "Template $($CompareResult.displayname) has the correct configuration." -sev Info } else { Write-StandardsAlert -message "Template $($CompareResult.displayname) is missing." -object $AlertObj -tenant $Tenant -standardName 'IntuneTemplate' -standardId $Settings.templateId @@ -158,7 +191,15 @@ function Invoke-CIPPStandardIntuneTemplate { description = $CompareResult.description isCompliant = $true } + + if ($Settings.verifyAssignments) { + $CurrentValue['isAssigned'] = if ($null -ne $CompareResult.AssignmentsMatch) { $CompareResult.AssignmentsMatch } else { $false } + $ExpectedValue['isAssigned'] = $true + } Set-CIPPStandardsCompareField -FieldName "standards.IntuneTemplate.$id" -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant #Add-CIPPBPAField -FieldName "policy-$id" -FieldValue $Compare -StoreAs bool -Tenant $tenant } + + $sw.Stop() + Write-Information "[IntuneTemplate][$Tenant] TOTAL '$displayname': $([int]$sw.Elapsed.TotalMilliseconds)ms" } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 index b06b0567df97..48ba7c77b964 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardMailContacts.ps1 @@ -59,8 +59,7 @@ function Invoke-CIPPStandardMailContacts { $Body = [pscustomobject]@{} switch ($Contacts) { { $Contacts.MarketingContact } { $body | Add-Member -NotePropertyName marketingNotificationEmails -NotePropertyValue @($Contacts.MarketingContact) } - { $Contacts.SecurityContact } { $body | Add-Member -NotePropertyName technicalNotificationMails -NotePropertyValue @($Contacts.SecurityContact) } - { $Contacts.TechContact } { $body | Add-Member -NotePropertyName technicalNotificationMails -NotePropertyValue @($Contacts.TechContact) -ErrorAction SilentlyContinue } + { $Contacts.SecurityContact -or $Contacts.TechContact } { $body | Add-Member -NotePropertyName technicalNotificationMails -NotePropertyValue @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ } | Select-Object -Unique) } { $Contacts.GeneralContact } { $body | Add-Member -NotePropertyName privacyProfile -NotePropertyValue @{contactEmail = $Contacts.GeneralContact } } } New-GraphPostRequest -tenantid $tenant -Uri "https://graph.microsoft.com/v1.0/organization/$($TenantID.id)" -asApp $true -Type patch -Body (ConvertTo-Json -InputObject $body) -ContentType 'application/json' @@ -112,7 +111,7 @@ function Invoke-CIPPStandardMailContacts { } $ExpectedValue = @{ marketingNotificationEmails = @($Contacts.MarketingContact) - technicalNotificationMails = @($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } + technicalNotificationMails = @(@($Contacts.SecurityContact, $Contacts.TechContact) | Where-Object { $_ -ne $null } | Select-Object -Unique) contactEmail = $Contacts.GeneralContact } Set-CIPPStandardsCompareField -FieldName 'standards.MailContacts' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -Tenant $tenant diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 new file mode 100644 index 000000000000..946e0c3d555d --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardintuneRestrictUserDeviceRegistration.ps1 @@ -0,0 +1,92 @@ +function Invoke-CIPPStandardintuneRestrictUserDeviceRegistration { + <# + .FUNCTIONALITY + Internal + .COMPONENT + (APIName) intuneRestrictUserDeviceRegistration + .SYNOPSIS + (Label) Configure user restriction for Entra device registration + .DESCRIPTION + (Helptext) Controls whether users can register devices with Entra. + (DocsDescription) Configures whether users can register devices with Entra. When disabled, users are unable to register devices with Entra. + .NOTES + CAT + Entra (AAD) Standards + TAG + EXECUTIVETEXT + Controls whether employees can register their devices for corporate access. Disabling user device registration prevents unauthorized or unmanaged devices from connecting to company resources, enhancing overall security posture. + ADDEDCOMPONENT + {"type":"switch","name":"standards.intuneRestrictUserDeviceRegistration.disableUserDeviceRegistration","label":"Disable users from registering devices","defaultValue":true} + IMPACT + High Impact + ADDEDDATE + 2026-03-05 + POWERSHELLEQUIVALENT + Update-MgBetaPolicyDeviceRegistrationPolicy + RECOMMENDEDBY + UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + .LINK + https://docs.cipp.app/user-documentation/tenant/standards/list-standards + #> + + param($Tenant, $Settings) + + try { + $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the intuneRestrictUserDeviceRegistration state for $Tenant. Error: $($ErrorMessage.NormalizedError)" -Sev Error -LogData $ErrorMessage + return + } + # Current M365 Config + $CurrentOdataType = $PreviousSetting.azureADJoin.allowedToJoin.'@odata.type' + + # Standards Config + $DisableUserDeviceRegistration = [bool]$Settings.disableUserDeviceRegistration + + # State comparison + $DesiredOdataType = if ($DisableUserDeviceRegistration) { '#microsoft.graph.noDeviceRegistrationMembership' } else { '#microsoft.graph.allDeviceRegistrationMembership' } + $CurrentDisableUserDeviceRegistration = ($CurrentOdataType -eq '#microsoft.graph.noDeviceRegistrationMembership') + $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) + $DesiredStateText = if ($DisableUserDeviceRegistration) { 'disabled' } else { 'enabled' } + + if ($Settings.remediate -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is already configured (registering users allowed to join: $DesiredStateText)." -sev Info + } else { + try { + $PreviousSetting.azureADJoin.allowedToJoin = @{ '@odata.type' = $DesiredOdataType; users = $null; groups = $null } + $NewBody = ConvertTo-Json -Compress -InputObject $PreviousSetting -Depth 10 + New-GraphPostRequest -tenantid $Tenant -Uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -Type PUT -Body $NewBody -ContentType 'application/json' + $CurrentOdataType = $DesiredOdataType + $CurrentDisableUserDeviceRegistration = ($CurrentOdataType -eq '#microsoft.graph.noDeviceRegistrationMembership') + $StateIsCorrect = ($CurrentOdataType -eq $DesiredOdataType) + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Set device registration restriction (registering users allowed to join: $DesiredStateText)." -sev Info + } catch { + $ErrorMessage = Get-CippException -Exception $_ + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Failed to set device registration restriction (registering users allowed to join: $DesiredStateText). Error: $($ErrorMessage.NormalizedError)" -sev Error -LogData $ErrorMessage + } + } + } + + if ($Settings.alert -eq $true) { + if ($StateIsCorrect -eq $true) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is configured as expected (registering users allowed to join: $DesiredStateText)." -sev Info + } else { + Write-StandardsAlert -message "Device registration restriction is not configured as expected (registering users allowed to join: $DesiredStateText)" -object @{ current = @{ disableUserDeviceRegistration = $CurrentDisableUserDeviceRegistration }; desired = @{ disableUserDeviceRegistration = $DisableUserDeviceRegistration } } -tenant $Tenant -standardName 'intuneRestrictUserDeviceRegistration' -standardId $Settings.standardId + Write-LogMessage -API 'Standards' -tenant $Tenant -message "Device registration restriction is not configured as expected (registering users allowed to join: $DesiredStateText)." -sev Info + } + } + + if ($Settings.report -eq $true) { + $CurrentValue = @{ + disableUserDeviceRegistration = $CurrentDisableUserDeviceRegistration + } + $ExpectedValue = @{ + disableUserDeviceRegistration = $DisableUserDeviceRegistration + } + Set-CIPPStandardsCompareField -FieldName 'standards.intuneRestrictUserDeviceRegistration' -CurrentValue $CurrentValue -ExpectedValue $ExpectedValue -TenantFilter $Tenant + Add-CIPPBPAField -FieldName 'intuneRestrictUserDeviceRegistration' -FieldValue $StateIsCorrect -StoreAs bool -Tenant $Tenant + } +} diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 index b7fa8f3e7544..eb3c1f911201 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardlaps.ps1 @@ -33,7 +33,7 @@ function Invoke-CIPPStandardlaps { param($Tenant, $Settings) try { - $PreviousSetting = New-CippDbRequest -TenantFilter $Tenant -Type 'DeviceRegistrationPolicy' + $PreviousSetting = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/policies/deviceRegistrationPolicy' -tenantid $Tenant } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message Write-LogMessage -API 'Standards' -Tenant $Tenant -Message "Could not get the DeviceRegistrationPolicy state for $Tenant. Error: $ErrorMessage" -Sev Error diff --git a/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 b/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 index 6d1313157f03..3a87963cba8c 100644 --- a/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 +++ b/Modules/CIPPCore/Public/Sync-CIPPReusablePolicySettings.ps1 @@ -18,7 +18,7 @@ function Sync-CIPPReusablePolicySettings { foreach ($ref in $reusableRefs) { $templateId = $ref.templateId ?? $ref.templateID ?? $ref.GUID ?? $ref.RowKey - $sourceId = $ref.sourceId ?? $ref.sourceReusableSettingId ?? $ref.sourceGuid ?? $ref.id + $sourceId = $ref.sourceId ?? $ref.sourceGuid ?? $ref.id $displayName = $ref.displayName ?? $ref.DisplayName if (-not $templateId -or -not $displayName) { continue } @@ -65,7 +65,14 @@ function Sync-CIPPReusablePolicySettings { } } - if ($sourceId -and $targetId) { $result.Map[$sourceId] = $targetId } + if ($targetId) { + $replacementKey = $sourceId + if ($TemplateInfo.RawJSON -and $templateId -and $TemplateInfo.RawJSON -match [regex]::Escape($templateId)) { + $replacementKey = $templateId + } + + if ($replacementKey) { $result.Map[$replacementKey] = $targetId } + } } $updatedJson = $result.RawJSON diff --git a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 index f83f7e649a23..6c4dece1f8ba 100644 --- a/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 +++ b/Modules/CIPPCore/Public/Test-CIPPRerun.ps1 @@ -37,7 +37,7 @@ function Test-CIPPRerun { if ($TenantFilter -ne 'AllTenants') { $Filters.Add("PartitionKey eq '$TenantFilter'") } - $Filters.Add("RowKey ge '$($Type)_$($API)' and RowKey le '$($Type)_$($API)~'") # ~ is the highest ascii character, this ensures we only get entries for this API. + $Filters.Add("RowKey ge '$($Type)_$($API)' and RowKey le '$($Type)_$($API)~'") # ~ is the highest ASCII character, this ensures we only get entries for this API. $FilterString = [string]::Join(' and ', $Filters) $RerunData = Get-CIPPAzDataTableEntity @RerunTable -filter $FilterString @@ -69,7 +69,7 @@ function Test-CIPPRerun { } } if ($RerunData.EstimatedNextRun -gt $CurrentUnixTime) { - Write-LogMessage -API $API -message "Standard rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info' + Write-LogMessage -API $API -message "$Type rerun detected for $($API). Prevented from running again." -tenant $TenantFilter -headers $Headers -Sev 'Info' return $true } else { $RerunData.EstimatedNextRun = $EstimatedNextRun @@ -91,7 +91,7 @@ function Test-CIPPRerun { } catch { $ErrorMessage = Get-CippException -Exception $_ Write-Host "Could not detect if this is a rerun: $($ErrorMessage.NormalizedError)" - Write-LogMessage -headers $Headers -API $API -message "Could not detect if this is a rerun: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData (Get-CippException -Exception $_) + Write-LogMessage -headers $Headers -API $API -message "Could not detect if this is a rerun: $($ErrorMessage.NormalizedError)" -Sev 'Error' -LogData $ErrorMessage return $false } } diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 index 96536f481a0f..94b7c876fef7 100644 --- a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -576,7 +576,7 @@ function Test-CIPPAuditLogRules { $ReturnedData = foreach ($item in $ReturnedData) { $item.CIPPAction = $clause.expectedAction $item.CIPPClause = $clause.CIPPClause -join ' and ' - $item.CIPPAlertComment = $clause.AlertComment + $item | Add-Member -NotePropertyName 'CIPPAlertComment' -NotePropertyValue $clause.AlertComment -Force -ErrorAction SilentlyContinue $MatchedRules.Add($clause.CIPPClause -join ' and ') $item } diff --git a/Modules/CippEntrypoints/CippEntrypoints.psm1 b/Modules/CippEntrypoints/CippEntrypoints.psm1 index 08614116fba9..361383745460 100644 --- a/Modules/CippEntrypoints/CippEntrypoints.psm1 +++ b/Modules/CippEntrypoints/CippEntrypoints.psm1 @@ -160,6 +160,48 @@ function Receive-CippHttpTrigger { return } +function Receive-CippQueueTrigger { + <# + .SYNOPSIS + Execute a queue trigger function + .DESCRIPTION + Execute a queue trigger function from an Azure Function App. + .PARAMETER QueueItem + The item from the queue that triggered the function + .PARAMETER TriggerMetadata + Metadata about the trigger, such as function name and other context + .FUNCTIONALITY + Entrypoint for the queue trigger function + #> + param($QueueItem, $TriggerMetadata) + + Write-Information '####### Starting CIPP Queue Trigger' + Write-Information "QueueItem: $($QueueItem | ConvertTo-Json -Depth 10 -Compress)" + Set-Location (Get-Item $PSScriptRoot).Parent.Parent.FullName + + if (Get-Command -Name $QueueItem.Cmdlet -Module CIPPCore -ErrorAction SilentlyContinue) { + Write-Information "Executing command: $($QueueItem.Cmdlet) with parameters: $($QueueItem.Parameters | ConvertTo-Json -Depth 10 -Compress)" + } else { + Write-Warning "Command not found: $($QueueItem.Cmdlet). Skipping execution." + return + } + + $Cmdlet = $QueueItem.Cmdlet + $Parameters = $QueueItem.Parameters + if ($Parameters) { + $Parameters = $Parameters | ConvertTo-Json -Depth 10 | ConvertFrom-Json -AsHashtable + } else { + $Parameters = @{} + } + + try { + & $Cmdlet @Parameters + } catch { + $ErrorMsg = $_.Exception.Message + Write-Warning "Error in $($Cmdlet): $($ErrorMsg)" + } +} + function Receive-CippOrchestrationTrigger { <# .SYNOPSIS @@ -188,6 +230,10 @@ function Receive-CippOrchestrationTrigger { BackoffCoefficient = 2 } + if ($env:WEBSITE_SKU -match '^Premium') { + $OrchestratorInput | Add-Member -MemberType NoteProperty -Name DurableMode -Value 'FanOut' -Force + } + switch ($OrchestratorInput.DurableMode) { 'FanOut' { $DurableMode = 'FanOut' @@ -209,10 +255,13 @@ function Receive-CippOrchestrationTrigger { Write-Information "Durable Mode: $DurableMode" $RetryOptions = New-DurableRetryOptions @DurableRetryOptions - if (!$OrchestratorInput.Batch -or ($OrchestratorInput.Batch | Measure-Object).Count -eq 0) { + if (!$OrchestratorInput.Batch -or ($OrchestratorInput.Batch | Measure-Object).Count -eq 0 -and $OrchestratorInput.QueueFunction) { $Batch = (Invoke-ActivityFunction -FunctionName 'CIPPActivityFunction' -Input $OrchestratorInput.QueueFunction -ErrorAction Stop) | Where-Object { $null -ne $_.FunctionName } - } else { + } elseif ($OrchestratorInput.Batch) { $Batch = $OrchestratorInput.Batch | Where-Object { $null -ne $_.FunctionName } + } else { + Write-Information 'No batch or queue function provided to orchestrator input' + $Batch = @() } if (($Batch | Measure-Object).Count -gt 0) { @@ -258,8 +307,18 @@ function Receive-CippOrchestrationTrigger { Write-Information "Running post execution function $($OrchestratorInput.PostExecution.FunctionName)" $PostExecParams = @{ FunctionName = $OrchestratorInput.PostExecution.FunctionName - Parameters = $OrchestratorInput.PostExecution.Parameters - Results = @($Results) + } + + if ($Results) { + $ResultsList = [System.Collections.Generic.List[object]]::new() + foreach ($Result in $Results) { + $ResultsList.Add($Result) + } + $PostExecParams['Results'] = $ResultsList + } + + if ($OrchestratorInput.PostExecution.Parameters) { + $PostExecParams['Parameters'] = $OrchestratorInput.PostExecution.Parameters } if ($null -ne $PostExecParams.FunctionName) { $null = Invoke-ActivityFunction -FunctionName CIPPActivityFunction -Input $PostExecParams @@ -271,6 +330,7 @@ function Receive-CippOrchestrationTrigger { } } catch { Write-Information "Orchestrator error $($_.Exception.Message) line $($_.InvocationInfo.ScriptLineNumber)" + Write-Information $_.InvocationInfo.PositionMessage } return $true } @@ -504,5 +564,6 @@ function Receive-CIPPTimerTrigger { return $true } -Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger') +Export-ModuleMember -Function @('Receive-CippHttpTrigger', 'Receive-CippQueueTrigger', 'Receive-CippOrchestrationTrigger', 'Receive-CippActivityTrigger', 'Receive-CIPPTimerTrigger') + diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 index 894bb7a4469b..1d2fe899da25 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Get-CippExtensionReportingData.ps1 @@ -66,7 +66,9 @@ function Get-CippExtensionReportingData { $Return.Licenses = $ParsedLicenseData | Select-Object @{N = 'skuId'; E = { $_.skuId } }, @{N = 'skuPartNumber'; E = { $_.skuPartNumber } }, @{N = 'consumedUnits'; E = { $_.CountUsed } }, - @{N = 'prepaidUnits'; E = { @{enabled = $_.TotalLicenses } } } + @{N = 'prepaidUnits'; E = { @{enabled = $_.TotalLicenses } } }, + @{N = 'TermInfo'; E = { @($_.TermInfo) } }, + @{N = 'servicePlans'; E = { $_.ServicePlans } } } else { $Return.Licenses = @() } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 941c5883775e..9282e7b80c08 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -301,7 +301,6 @@ function Invoke-NinjaOneTenantSync { $MailboxStatsFull = $ExtensionCache.MailboxUsage $Permissions = $ExtensionCache.MailboxPermissions $SecureScore = $ExtensionCache.SecureScore - $Subscriptions = if ($ExtensionCache.Licenses) { $ExtensionCache.Licenses.TermInfo | Where-Object { $null -ne $_ } } else { @() } $SecureScoreProfiles = $ExtensionCache.SecureScoreControlProfiles $TenantDetails = $ExtensionCache.Organization $RawDomains = $ExtensionCache.Domains @@ -1027,13 +1026,16 @@ function Invoke-NinjaOneTenantSync { } } - - # Format Conditional Access Polcies - $UserPoliciesFormatted = '
    ' - foreach ($Policy in $UserPolicies) { - $UserPoliciesFormatted = $UserPoliciesFormatted + "
  • $($Policy.displayName)
  • " + if ($UserPolicies) { + # Format Conditional Access Policies + $UserPoliciesFormatted = '
      ' + foreach ($Policy in $UserPolicies) { + $UserPoliciesFormatted = $UserPoliciesFormatted + "
    • $($Policy.displayName)
    • " + } + $UserPoliciesFormatted = $UserPoliciesFormatted + '
    ' + } else { + $UserPoliciesFormatted = 'No Conditional Access Policies Assigned' } - $UserPoliciesFormatted = $UserPoliciesFormatted + '
' $UserOverviewCard = [PSCustomObject]@{ @@ -1384,7 +1386,7 @@ function Invoke-NinjaOneTenantSync { if ($Configuration.LicenseDocumentsEnabled -eq $True) { $LicenseDetails = foreach ($License in $Licenses) { - $MatchedSubscriptions = $Subscriptions | Where-Object -Property skuid -EQ $License.skuId + $MatchedSubscriptions = $License.TermInfo Write-Information "License info: $($License | ConvertTo-Json -Depth 100)" $FriendlyLicenseName = $License.skuPartNumber @@ -1446,7 +1448,7 @@ function Invoke-NinjaOneTenantSync { $LicenseFields = @{ cippLicenseSummary = @{'html' = $LicenseSummaryHTML } cippLicenseUsers = @{'html' = $LicenseUsersHTML } - cippLicenseID = $License.id + cippLicenseID = $License.skuId } diff --git a/Modules/CippExtensions/Public/PwPush/Get-PwPushAccount.ps1 b/Modules/CippExtensions/Public/PwPush/Get-PwPushAccount.ps1 index c0f429863035..22c92dde601b 100644 --- a/Modules/CippExtensions/Public/PwPush/Get-PwPushAccount.ps1 +++ b/Modules/CippExtensions/Public/PwPush/Get-PwPushAccount.ps1 @@ -1,8 +1,9 @@ function Get-PwPushAccount { $Table = Get-CIPPTable -TableName Extensionsconfig - $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json).PWPush + $ParsedConfig = (Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ErrorAction SilentlyContinue + $Configuration = $ParsedConfig.PWPush if ($Configuration.Enabled -eq $true -and $Configuration.UseBearerAuth -eq $true) { - Set-PwPushConfig -Configuration $Configuration + Set-PwPushConfig -Configuration $Configuration -FullConfiguration $ParsedConfig Get-PushAccount } else { return @(@{ diff --git a/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 b/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 index 1991b90598ae..b3f3c8f448b6 100644 --- a/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 +++ b/Modules/CippExtensions/Public/PwPush/New-PwPushLink.ps1 @@ -33,7 +33,7 @@ function New-PwPushLink { # Proceed with creating the PwPush link try { - Set-PwPushConfig -Configuration $Configuration + Set-PwPushConfig -Configuration $Configuration -FullConfiguration $ParsedConfig $PushParams = @{ Payload = $Payload } @@ -41,6 +41,7 @@ function New-PwPushLink { if ($Configuration.ExpireAfterViews) { $PushParams.ExpireAfterViews = $Configuration.ExpireAfterViews } if ($Configuration.DeletableByViewer) { $PushParams.DeletableByViewer = $Configuration.DeletableByViewer } if ($Configuration.AccountId) { $PushParams.AccountId = $Configuration.AccountId.value } + if (![string]::IsNullOrEmpty($Configuration.DefaultPassphrase)) { $PushParams.Passphrase = $Configuration.DefaultPassphrase } if ($PSCmdlet.ShouldProcess('Create a new PwPush link')) { $Link = New-Push @PushParams diff --git a/Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 b/Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 index 845b8e3caf49..4d403f0d9a95 100644 --- a/Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 +++ b/Modules/CippExtensions/Public/PwPush/Set-PwPushConfig.ps1 @@ -8,10 +8,14 @@ function Set-PwPushConfig { .PARAMETER Configuration Configuration object + + .PARAMETER FullConfiguration + Full parsed configuration object including CFZTNA settings #> [CmdletBinding(SupportsShouldProcess = $true)] param( - $Configuration + $Configuration, + $FullConfiguration ) $InitParams = @{} if ($Configuration.BaseUrl) { @@ -35,5 +39,18 @@ function Set-PwPushConfig { Write-Information ($InitParams | ConvertTo-Json) Initialize-PassPushPosh @InitParams } + + if ($Configuration.CFEnabled -eq $true -and $FullConfiguration.CFZTNA.Enabled -eq $true) { + $CFAPIKey = Get-ExtensionAPIKey -Extension 'CFZTNA' + $PPPModule = Get-Module PassPushPosh + & $PPPModule { + if (-not $Script:PPPHeaders) { + $Script:PPPHeaders = @{} + } + $Script:PPPHeaders['CF-Access-Client-Id'] = $args[0] + $Script:PPPHeaders['CF-Access-Client-Secret'] = $args[1] + } $FullConfiguration.CFZTNA.ClientId "$CFAPIKey" + Write-Information 'CF-Access-Client-Id and CF-Access-Client-Secret headers added to PWPush API request' + } } diff --git a/host.json b/host.json index 5462a094374b..1246bb822fed 100644 --- a/host.json +++ b/host.json @@ -10,13 +10,13 @@ "functionTimeout": "00:10:00", "extensions": { "durableTask": { - "maxConcurrentActivityFunctions": 1, - "maxConcurrentOrchestratorFunctions": 5, + "maxConcurrentActivityFunctions": 5, + "maxConcurrentOrchestratorFunctions": 1, "tracing": { "distributedTracingEnabled": false, "version": "None" }, - "defaultVersion": "10.1.2", + "defaultVersion": "10.2.0", "versionMatchStrategy": "Strict", "versionFailureStrategy": "Fail" } diff --git a/profile.ps1 b/profile.ps1 index a1ec3269f8d5..9d52861225d7 100644 --- a/profile.ps1 +++ b/profile.ps1 @@ -127,6 +127,10 @@ if (!$LastStartup -or $CurrentVersion -ne $LastStartup.Version) { $SwVersion.Stop() $Timings['VersionCheck'] = $SwVersion.Elapsed.TotalMilliseconds +if ($env:AzureWebJobsStorage -ne 'UseDevelopmentStorage=true' -and $env:NonLocalHostAzurite -ne 'true') { + Set-CIPPOffloadFunctionTriggers +} + $TotalStopwatch.Stop() $Timings['Total'] = $TotalStopwatch.Elapsed.TotalMilliseconds diff --git a/version_latest.txt b/version_latest.txt index b6132546fce8..2bd6f7e39277 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -10.1.2 +10.2.0