From c39b7beb69a67fabe55bf7bd087d2c14500a7a9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 6 Nov 2025 18:21:05 +0100 Subject: [PATCH 01/10] Fix: Refactor group type determination logic in Invoke-ListGroups and Invoke-ListUserGroups Fixes bug of M365 groups that are security enabled, reporting being both Mail-Enabled Security and Microsoft 365, causing add/remove functions to fail --- .../Administration/Groups/Invoke-ListGroups.ps1 | 13 ++++++------- .../Administration/Users/Invoke-ListUserGroups.ps1 | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 index 73d260965f23..aeb79dddc16a 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroups.ps1 @@ -88,10 +88,10 @@ function Invoke-ListGroups { groupInfo = ($RawGraphRequest | Where-Object { $_.id -eq 1 }).body | Select-Object *, @{ Name = 'primDomain'; Expression = { $_.mail -split '@' | Select-Object -Last 1 } }, @{Name = 'teamsEnabled'; Expression = { if ($_.resourceProvisioningOptions -like '*Team*') { $true } else { $false } } }, @{Name = 'calculatedGroupType'; Expression = { - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } members = ($RawGraphRequest | Where-Object { $_.id -eq 2 }).body.value @@ -105,11 +105,10 @@ function Invoke-ListGroups { @{Name = 'membersCsv'; Expression = { $_.members.userPrincipalName -join ',' } }, @{Name = 'teamsEnabled'; Expression = { if ($_.resourceProvisioningOptions -like '*Team*') { $true }else { $false } } }, @{Name = 'calculatedGroupType'; Expression = { - - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } }, @{Name = 'dynamicGroupBool'; Expression = { if ($_.groupTypes -contains 'DynamicMembership') { $true } else { $false } } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 index d0c533708320..a223aef2c597 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ListUserGroups.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListUserGroups { +function Invoke-ListUserGroups { <# .FUNCTIONALITY Entrypoint @@ -22,10 +22,10 @@ Function Invoke-ListUserGroups { @{ Name = 'OnPremisesSync'; Expression = { $_.onPremisesSyncEnabled } }, @{ Name = 'IsAssignableToRole'; Expression = { $_.isAssignableToRole } }, @{ Name = 'calculatedGroupType'; Expression = { - if ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } - if (!$_.mailEnabled -and $_.securityEnabled) { 'Security' } if ($_.groupTypes -contains 'Unified') { 'Microsoft 365' } - if (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (!$_.securityEnabled)) { 'Distribution List' } + elseif ($_.mailEnabled -and $_.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $_.mailEnabled -and $_.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($_.groupTypes)) -and ($_.mailEnabled) -and (-not $_.securityEnabled)) { 'Distribution List' } } } From 04be4209ab4654c67e25ff084ae219bfc9dd6cab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 6 Nov 2025 20:36:06 +0100 Subject: [PATCH 02/10] Chore: move file to correct folder --- .../Endpoint/Applications}/Invoke-RemoveQueuedApp.ps1 | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Modules/CIPPCore/Public/{ => Entrypoints/HTTP Functions/Endpoint/Applications}/Invoke-RemoveQueuedApp.ps1 (100%) diff --git a/Modules/CIPPCore/Public/Invoke-RemoveQueuedApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-RemoveQueuedApp.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-RemoveQueuedApp.ps1 rename to Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-RemoveQueuedApp.ps1 From f8c0b511fbd50546974a4195b83ab90697dd5a78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Thu, 6 Nov 2025 22:50:36 +0100 Subject: [PATCH 03/10] Fix: Refactor ExecGetRecoveryKey to get FileVault key too and add comment based help Fixes https://github.com/KelvinTegelaar/CIPP/issues/4880 --- .../MEM/Invoke-ExecGetRecoveryKey.ps1 | 7 ++- .../CIPPCore/Public/Get-CIPPBitlockerKey.ps1 | 28 +++++++++ .../CIPPCore/Public/Get-CIPPFileVaultKey.ps1 | 63 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 index cc9e747b9787..a87941bfd8e4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-ExecGetRecoveryKey.ps1 @@ -15,9 +15,14 @@ function Invoke-ExecGetRecoveryKey { # Interact with query parameters or the body of the request. $TenantFilter = $Request.Query.tenantFilter ?? $Request.Body.tenantFilter $GUID = $Request.Query.GUID ?? $Request.Body.GUID + $RecoveryKeyType = $Request.Body.RecoveryKeyType ?? 'BitLocker' try { - $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers + switch ($RecoveryKeyType) { + 'BitLocker' { $Result = Get-CIPPBitLockerKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers } + 'FileVault' { $Result = Get-CIPPFileVaultKey -Device $GUID -TenantFilter $TenantFilter -APIName $APIName -Headers $Headers } + default { throw "Invalid RecoveryKeyType specified: $RecoveryKeyType." } + } $StatusCode = [HttpStatusCode]::OK } catch { $Result = $_.Exception.Message diff --git a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 index 6035975ad1d2..0b159e9807be 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBitlockerKey.ps1 @@ -1,3 +1,31 @@ +<# +.SYNOPSIS + Retrieves BitLocker recovery keys for a managed device from Microsoft Graph API. + +.DESCRIPTION + This function queries the Microsoft Graph API to retrieve all BitLocker recovery keys + associated with a specified device. It handles cases where no key is found and provides appropriate + logging and error handling. +.PARAMETER Device + The ID of the device for which to retrieve BitLocker recovery keys. + +.PARAMETER TenantFilter + The tenant ID to filter the request to the appropriate tenant. + +.PARAMETER APIName + The name of the API operation for logging purposes. Defaults to 'Get BitLocker key'. + +.PARAMETER Headers + The headers to include in the request, typically used for authentication and logging. + +.OUTPUTS + Array of PSCustomObject with properties: + - resultText: Formatted string containing the key ID and key value + - copyField: The raw key value + - state: Status of the operation ('success') + + Or a string message if no keys are found. +#> function Get-CIPPBitLockerKey { [CmdletBinding()] diff --git a/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 b/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 new file mode 100644 index 000000000000..bc1933d3bc2f --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPFileVaultKey.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Retrieves the FileVault recovery key for a managed device from Microsoft Graph API. + +.DESCRIPTION + This function makes a request to the Microsoft Graph API to retrieve the FileVault recovery key + for a specified managed device. It handles cases where no key is found and provides appropriate + logging and error handling. + +.PARAMETER Device + The GUID of the managed device for which to retrieve the FileVault key. + +.PARAMETER TenantFilter + The tenant ID to filter the request to the appropriate tenant. + +.PARAMETER APIName + The name of the API operation for logging purposes. Defaults to 'Get FileVault key'. + +.PARAMETER Headers + The headers to include in the request, typically used for authentication and logging. + +.OUTPUTS + PSCustomObject with properties: + - resultText: Formatted string containing the key + - copyField: The raw key value + - state: Status of the operation ('success') + + Or a string message if no key is found. + +#> + +function Get-CIPPFileVaultKey { + [CmdletBinding()] + param ( + $Device, + $TenantFilter, + $APIName = 'Get FileVault key', + $Headers + ) + + try { + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices/$Device/getFileVaultKey" -tenantid $TenantFilter + + if ([string]::IsNullOrEmpty($GraphRequest)) { + $Result = "No FileVault recovery key found for $($Device)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Info -tenant $TenantFilter + return $Result + } + + Write-LogMessage -headers $Headers -API $APIName -message "Retrieved FileVault recovery key for $($Device)" -Sev Info -tenant $TenantFilter + return [PSCustomObject]@{ + resultText = "Key: $($GraphRequest)" + copyField = $GraphRequest + state = 'success' + } + } catch { + $ErrorMessage = Get-CippException -Exception $_ + $Result = "Could not retrieve FileVault recovery key for $($Device). Error: $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -message $Result -Sev Error -tenant $TenantFilter -LogData $ErrorMessage + throw $Result + } + +} From df0a123565261d277fa08506e06cbc2272bb4a17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 8 Nov 2025 16:17:54 +0100 Subject: [PATCH 04/10] fix: remove some unneeded returns capitalization --- .../Teams-Sharepoint/Invoke-ListSites.ps1 | 8 +++----- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 10 +++++----- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 index 4fd95ff2dba2..dd112c3f16ec 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Teams-Sharepoint/Invoke-ListSites.ps1 @@ -1,4 +1,4 @@ -Function Invoke-ListSites { +function Invoke-ListSites { <# .FUNCTIONALITY Entrypoint @@ -11,15 +11,14 @@ Function Invoke-ListSites { $TenantFilter = $Request.Query.TenantFilter - $Type = $request.query.Type - $UserUPN = $request.query.UserUPN + $Type = $Request.Query.Type + $UserUPN = $Request.Query.UserUPN if (!$TenantFilter) { return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::BadRequest Body = 'TenantFilter is required' }) - return } if (!$Type) { @@ -27,7 +26,6 @@ Function Invoke-ListSites { StatusCode = [HttpStatusCode]::BadRequest Body = 'Type is required' }) - return } $Tenant = Get-Tenants -TenantFilter $TenantFilter diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 8a5cddf7a400..5ca780b96c1d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -38,9 +38,9 @@ function Get-CIPPLicenseOverview { $ConvertTable = Import-Csv ConversionTable.csv $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable - $GraphRequest = foreach ($singlereq in $RawGraphRequest) { - $skuid = $singlereq.Licenses - foreach ($sku in $skuid) { + $GraphRequest = foreach ($singleReq in $RawGraphRequest) { + $skuId = $singleReq.Licenses + foreach ($sku in $skuId) { if ($sku.skuId -in $ExcludedSkuList.GUID) { continue } $PrettyNameAdmin = $AdminPortalLicenses | Where-Object { $_.SkuId -eq $sku.skuId } | Select-Object -ExpandProperty Name $PrettyNameCSV = ($ConvertTable | Where-Object { $_.guid -eq $sku.skuid }).'Product_Display_Name' | Select-Object -Last 1 @@ -72,7 +72,7 @@ function Get-CIPPLicenseOverview { } } [pscustomobject]@{ - Tenant = [string]$singlereq.Tenant + Tenant = [string]$singleReq.Tenant License = [string]$PrettyName CountUsed = [string]"$($sku.consumedUnits)" CountAvailable = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits @@ -82,7 +82,7 @@ function Get-CIPPLicenseOverview { availableUnits = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits TermInfo = [string]($TermInfo | ConvertTo-Json -Depth 10 -Compress) 'PartitionKey' = 'License' - 'RowKey' = "$($singlereq.Tenant) - $($sku.skuid)" + 'RowKey' = "$($singleReq.Tenant) - $($sku.skuid)" } } } From dd2ce0f318afdee577f195da1f2d9d05c84c0507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sat, 8 Nov 2025 18:34:16 +0100 Subject: [PATCH 05/10] feat: add property to show what users has the license assigned feat: add support for licensed groups in license overview --- .../Public/Get-CIPPLicenseOverview.ps1 | 65 ++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 5ca780b96c1d..73a49b8d0de2 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -18,6 +18,22 @@ function Get-CIPPLicenseOverview { url = 'directory/subscriptions' method = 'GET' } + @{ + id = 'licensedUsers' + url = "users?`$select=id,displayName,userPrincipalName,assignedLicenses&`$filter=assignedLicenses/`$count ne 0&`$count=true" + method = 'GET' + headers = @{ + 'ConsistencyLevel' = 'eventual' + } + } + @{ + id = 'licensedGroups' + url = "groups?`$select=id,displayName,assignedLicenses,mailEnabled,securityEnabled,groupTypes,onPremisesSyncEnabled&`$filter=assignedLicenses/`$count ne 0&`$count=true" + method = 'GET' + headers = @{ + 'ConsistencyLevel' = 'eventual' + } + } ) try { @@ -38,6 +54,51 @@ function Get-CIPPLicenseOverview { $ConvertTable = Import-Csv ConversionTable.csv $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable + + $AllLicensedUsers = @(($Results | Where-Object { $_.id -eq 'licensedUsers' }).body.value) + $UsersBySku = @{} + foreach ($User in $AllLicensedUsers) { + if (-not $User.assignedLicenses) { continue } # Skip users with no assigned licenses. Should not happens as the filter is applied, but just in case + $UserInfo = [PSCustomObject]@{ + displayName = [string]$User.displayName + userPrincipalName = [string]$User.userPrincipalName + id = [string]$User.id + } + + foreach ($AssignedLicense in $User.assignedLicenses) { + $LicenseSkuId = ([string]$AssignedLicense.skuId).ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($LicenseSkuId)) { continue } # Skip if SKU ID is null or whitespace. Should not happen but just in case + if (-not $UsersBySku.ContainsKey($LicenseSkuId)) { + $UsersBySku[$LicenseSkuId] = [System.Collections.Generic.List[object]]::new() + } + $UsersBySku[$LicenseSkuId].Add($UserInfo) + } + + } + + $AllLicensedGroups = @(($Results | Where-Object { $_.id -eq 'licensedGroups' }).body.value) + $GroupsBySku = @{} + foreach ($Group in $AllLicensedGroups) { + if (-not $Group.assignedLicenses) { continue } + $GroupInfo = [PSCustomObject]@{ + displayName = [string]$Group.displayName + calculatedGroupType = if ($Group.groupTypes -contains 'Unified') { 'Microsoft 365' } + elseif ($Group.mailEnabled -and $Group.securityEnabled) { 'Mail-Enabled Security' } + elseif (-not $Group.mailEnabled -and $Group.securityEnabled) { 'Security' } + elseif (([string]::isNullOrEmpty($Group.groupTypes)) -and ($Group.mailEnabled) -and (-not $Group.securityEnabled)) { 'Distribution List' } + id = [string]$Group.id + onPremisesSyncEnabled = [bool]$Group.onPremisesSyncEnabled + + } + foreach ($AssignedLicense in $Group.assignedLicenses) { + $LicenseSkuId = ([string]$AssignedLicense.skuId).ToLowerInvariant() + if ([string]::IsNullOrWhiteSpace($LicenseSkuId)) { continue } + if (-not $GroupsBySku.ContainsKey($LicenseSkuId)) { + $GroupsBySku[$LicenseSkuId] = [System.Collections.Generic.List[object]]::new() + } + $GroupsBySku[$LicenseSkuId].Add($GroupInfo) + } + } $GraphRequest = foreach ($singleReq in $RawGraphRequest) { $skuId = $singleReq.Licenses foreach ($sku in $skuId) { @@ -71,6 +132,7 @@ function Get-CIPPLicenseOverview { OCPSubscriptionId = $SubInfo.ocpSubscriptionId } } + $SkuKey = ([string]$sku.skuId).ToLowerInvariant() [pscustomobject]@{ Tenant = [string]$singleReq.Tenant License = [string]$PrettyName @@ -81,6 +143,8 @@ function Get-CIPPLicenseOverview { skuPartNumber = [string]$PrettyName availableUnits = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits TermInfo = [string]($TermInfo | ConvertTo-Json -Depth 10 -Compress) + AssignedUsers = [string]($UsersBySku.ContainsKey($SkuKey) ? (ConvertTo-Json -InputObject ($UsersBySku[$SkuKey].ToArray()) -Depth 5 -Compress) : '[]') + AssignedGroups = [string]($GroupsBySku.ContainsKey($SkuKey) ? (ConvertTo-Json -InputObject ($GroupsBySku[$SkuKey].ToArray()) -Depth 5 -Compress) : '[]') 'PartitionKey' = 'License' 'RowKey' = "$($singleReq.Tenant) - $($sku.skuid)" } @@ -88,4 +152,3 @@ function Get-CIPPLicenseOverview { } return $GraphRequest } - From 1a28683cd2d8bd167ce3ac9f2f8dd65158b81271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 9 Nov 2025 00:46:02 +0100 Subject: [PATCH 06/10] fix: filter out excluded licenses in Sync-CippExtensionData function Fixes https://github.com/KelvinTegelaar/CIPP/issues/4878 --- .../Extension Functions/Sync-CippExtensionData.ps1 | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 index d08a46c09027..3ddeb244de00 100644 --- a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -228,6 +228,15 @@ function Sync-CippExtensionData { $Data = $Data.Value } + # Filter out excluded licenses to respect the ExcludedLicenses table + if ($_.id -eq 'Licenses') { + $LicenseTable = Get-CIPPTable -TableName ExcludedLicenses + $ExcludedSkuList = Get-CIPPAzDataTableEntity @LicenseTable + if ($ExcludedSkuList) { + $Data = $Data | Where-Object { $_.skuId -notin $ExcludedSkuList.GUID } + } + } + $Entity = @{ PartitionKey = $TenantFilter RowKey = $_.id From 80d4a41bb58a319989add2b07243049bce81b219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 9 Nov 2025 01:25:44 +0100 Subject: [PATCH 07/10] fix: ensure previously loaded modules are removed for reloading new code changes --- Tools/Initialize-DevEnvironment.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Tools/Initialize-DevEnvironment.ps1 b/Tools/Initialize-DevEnvironment.ps1 index bf9641461a77..bc8fd6193f91 100644 --- a/Tools/Initialize-DevEnvironment.ps1 +++ b/Tools/Initialize-DevEnvironment.ps1 @@ -15,6 +15,14 @@ if ((Test-Path $PowerShellWorkerRoot) -and !('Microsoft.Azure.Functions.PowerShe Add-Type -Path $PowerShellWorkerRoot } +# Remove previously loaded modules to force reloading if new code changes were made +$LoadedModules = Get-Module | Select-Object -ExpandProperty Name +switch ($LoadedModules) { + 'CIPPCore' { Remove-Module CIPPCore -Force } + 'CippExtensions' { Remove-Module CippExtensions -Force } + 'MicrosoftTeams' { Remove-Module MicrosoftTeams -Force } +} + Import-Module ( Join-Path $CippRoot 'Modules\AzBobbyTables' ) Import-Module ( Join-Path $CippRoot 'Modules\DNSHealth' ) Import-Module ( Join-Path $CippRoot 'Modules\CIPPCore' ) From 0ae59e9cd9a6c3c30fe099708db3da264ee74738 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 9 Nov 2025 16:11:19 +0100 Subject: [PATCH 08/10] chore: general cleanup and capitalization --- .../Applications/Invoke-AddOfficeApp.ps1 | 54 +++++++++---------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index 56e01c9f21cb..06701d5a3fcf 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -1,4 +1,4 @@ -Function Invoke-AddOfficeApp { +function Invoke-AddOfficeApp { <# .FUNCTIONALITY Entrypoint @@ -10,15 +10,17 @@ Function Invoke-AddOfficeApp { # Input bindings are passed in via param block. $Tenants = $Request.body.selectedTenants.defaultDomainName + $Headers = $Request.Headers + $APIName = $Request.Params.CIPPEndpoint if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants).defaultDomainName } - $AssignTo = if ($request.body.Assignto -ne 'on') { $request.body.Assignto } + $AssignTo = if ($Request.Body.AssignTo -ne 'on') { $Request.Body.AssignTo } - $results = foreach ($Tenant in $tenants) { + $Results = foreach ($Tenant in $Tenants) { try { - $ExistingO365 = New-graphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $tenant | Where-Object { $_.displayname -eq 'Microsoft 365 Apps for Windows 10 and later' } + $ExistingO365 = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $Tenant | Where-Object { $_.displayName -eq 'Microsoft 365 Apps for Windows 10 and later' } if (!$ExistingO365) { # Check if custom XML is provided - if ($request.body.useCustomXml -and $request.body.customXml) { + if ($Request.Body.useCustomXml -and $Request.Body.customXml) { # Use custom XML configuration $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' @@ -35,10 +37,9 @@ Function Invoke-AddOfficeApp { 'value' = 'iVBORw0KGgoAAAANSUhEUgAAAF0AAAAeCAMAAAEOZNKlAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJhUExURf////7z7/i9qfF1S/KCW/i+qv3q5P/9/PrQwfOMae1RG+s8AOxGDfBtQPWhhPvUx/759/zg1vWgg+9fLu5WIvKFX/rSxP728/nCr/FyR+tBBvOMaO1UH+1RHOs+AvSScP3u6f/+/v3s5vzg1+xFDO9kNPOOa/i7pvzj2/vWyes9Af76+Pzh2PrTxf/6+f7y7vOGYexHDv3t5+1SHfi8qPOIZPvb0O1NFuxDCe9hMPSVdPnFs/3q4/vaz/STcu5VIe5YJPWcfv718v/9/e1MFfF4T/F4TvF2TP3o4exECvF0SexIEPONavzn3/vZze1QGvF3Te5dK+5cKvrPwPrQwvKAWe1OGPexmexKEveulfezm/BxRfamiuxLE/apj/zf1e5YJfSXd/OHYv3r5feznPakiPze1P7x7f739f3w6+xJEfnEsvWdf/Wfge1LFPe1nu9iMvnDsfBqPOs/BPOIY/WZevJ/V/zl3fnIt/vTxuxHD+xEC+9mN+5ZJv749vBpO/KBWvBwRP/8+/SUc/etlPjArP/7+vOLZ/F7UvWae/708e1OF/aihvSWdvi8p+tABfSZefvVyPWihfSVde9lNvami+9jM/zi2fKEXvBuQvOKZvalifF5UPJ/WPSPbe9eLfrKuvvd0uxBB/7w7Pzj2vrRw/rOv+1PGfi/q/eymu5bKf3n4PnJuPBrPf3t6PWfgvWegOxCCO9nOO9oOfaskvSYePi5pPi2oPnGtO5eLPevlvKDXfrNvv739Pzd0/708O9gL+9lNfJ9VfrLu/OPbPnDsPBrPus+A/nArfarkQAAAGr5HKgAAADLdFJOU/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AvuakogAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAz5JREFUOE+tVTtu4zAQHQjppmWzwIJbEVCzpTpjbxD3grQHSOXKRXgCAT6EC7UBVAmp3KwBnmvfzNCyZTmxgeTZJsXx43B+HBHRE34ZkXgkerXFTheeiCkRrbB4UXmp4wSWz5raaQEMTM5TZwuiXoaKgV+6FsmkZQcSy0kA71yMTMGHanX+AzMMGLAQCxU1F/ZwjULPugazl82GM0NEKm/U8EqFwEkO3/EAT4grgl0nucwlk9pcpTTJ4VPA4g/Rb3yIRhhp507e9nTQmZ1OS5RO4sS7nIRPEeHXCHdkw9ZEW2yVE5oIS7peD58Avs7CN+PVCmHh21oOqBdjDzIs+FldPJ74TFESUSJEfVzy9U/dhu+AuOT6eBp6gGKyXEx8euO450ZE4CMfstMFT44broWw/itkYErWXRx+fFArt9Ca9os78TFed0LVIUsmIHrwbwaw3BEOnOk94qVpQ6Ka2HjxewJnfyd6jUtGDQLdWlzmYNYLeKbbGOucJsNabCq1Yub0o92rtR+i30V2dapxYVEePXcOjeCKPnYyit7BtKeNlZqHbr+gt7i+AChWA9RsRs03pxTQc67ouWpxyESvjK5Vs3DVSy3IpkxPm5X+wZoBi+MFHWW69/w8FRhc7VBe6HAhMB2b8Q0XqDzTNZtXUMnKMjwKVaCrB/CSUL7WSx/HsdJC86lFGXwnioTeOMPjV+szlFvrZLA5VMVK4y+41l4e1xfx7Z88o4hkilRUH/qKqwNVlgDgpvYCpH3XwAy5eMCRnezIUxffVXoDql2rTHFDO+pjWnTWzAfrYXn6BFECblUpWGrvPZvBipETjS5ydM7tdXpH41ZCEbBNy/+wFZu71QO2t9pgT+iZEf657Q1vpN94PQNDxUHeKR103LV9nPVOtDikcNKO+2naCw7yKBhOe9Hm79pe8C4/CfC2wDjXnqC94kEeBU3WwN7dt/2UScXas7zDl5GpkY+M8WKv2J7fd4Ib2rGTk+jsC2cleEM7jI9veF7B0MBJrsZqfKd/81q9pR2NZfwJK2JzsmIT1Ns8jUH0UusQBpU8d2JzsHiXg1zXGLqxfitUNTDT/nUUeqDBp2HZVr+Ocqi/Ty3Rf4Jn82xxfSNtAAAAAElFTkSuQmCC' } } - } - else { + } else { # Use standard configuration - $Arch = if ($request.body.arch) { 'x64' } else { 'x86' } + $Arch = if ($Request.Body.arch) { 'x64' } else { 'x86' } $products = @('o365ProPlusRetail') $ExcludedApps = [pscustomobject]@{ infoPath = $true @@ -54,8 +55,8 @@ Function Invoke-AddOfficeApp { access = $false bing = $false } - foreach ($ExcludedApp in $request.body.excludedApps.value) { - $ExcludedApps.$excludedapp = $true + foreach ($ExcludedApp in $Request.Body.excludedApps.value) { + $ExcludedApps.$ExcludedApp = $true } $ObjBody = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.officeSuiteApp' @@ -66,14 +67,14 @@ Function Invoke-AddOfficeApp { 'publisher' = 'Microsoft' 'notes' = '' 'owner' = 'Microsoft' - 'autoAcceptEula' = [bool]$request.body.AcceptLicense + 'autoAcceptEula' = [bool]$Request.Body.AcceptLicense 'excludedApps' = $ExcludedApps 'officePlatformArchitecture' = $Arch 'officeSuiteAppDefaultFileFormat' = 'OfficeOpenXMLFormat' - 'localesToInstall' = @($request.body.languages.value) - 'shouldUninstallOlderVersionsOfOffice' = [bool]$request.body.RemoveVersions - 'updateChannel' = $request.body.updateChannel.value - 'useSharedComputerActivation' = [bool]$request.body.SharedComputerActivation + 'localesToInstall' = @($Request.Body.languages.value) + 'shouldUninstallOlderVersionsOfOffice' = [bool]$Request.Body.RemoveVersions + 'updateChannel' = $Request.Body.updateChannel.value + 'useSharedComputerActivation' = [bool]$Request.Body.SharedComputerActivation 'productIds' = $products 'largeIcon' = @{ 'type' = 'image/png' @@ -83,31 +84,28 @@ Function Invoke-AddOfficeApp { } Write-Host ($ObjBody | ConvertTo-Json -Compress) $OfficeAppID = New-graphPostRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $tenant -Body (ConvertTo-Json -InputObject $ObjBody -Depth 10) -type POST - } - else { + } else { "Office deployment already exists for $($Tenant)" - Continue + continue } - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Added Office profile to $($tenant)" -Sev 'Info' + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Added Office profile to $($Tenant)" -Sev 'Info' if ($AssignTo) { $AssignO365 = if ($AssignTo -ne 'AllDevicesAndUsers') { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.' + $($AssignTo) + 'AssignmentTarget"},"intent":"Required"}]}' } else { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"},"intent":"Required"},{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"},"intent":"Required"}]}' } Write-Host ($AssignO365) - New-graphPostRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $tenant -Body $AssignO365 -type POST - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Assigned Office to $AssignTo" -Sev 'Info' + New-graphPostRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $Tenant -Body $AssignO365 -type POST + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Assigned Office to $AssignTo" -Sev 'Info' } "Successfully added Office App for $($Tenant)" - } - catch { - "Failed to add Office App for $($Tenant): $($_.Exception.Message)" - Write-LogMessage -headers $Request.Headers -API $APIName -tenant $($tenant) -message "Failed to add Office App. Error: $($_.Exception.Message)" -Sev 'Error' + } catch { + $ErrorMessage = Get-CippException -Exception $_ + "Failed to add Office App for $($Tenant): $($ErrorMessage.NormalizedError)" + Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Failed to add Office App. Error: $($ErrorMessage.NormalizedError)" -Sev 'Error' -Logdata $ErrorMessage continue } } - $body = [pscustomobject]@{'Results' = $results } - return ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = $body + Body = @{'Results' = $Results } }) } From b80465a78e208325a6b2bf12383d7021ef85e5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Sun, 9 Nov 2025 16:29:22 +0100 Subject: [PATCH 09/10] fix: add privacy information URL to Office App deployment and fix largeIcon value Fixes https://github.com/KelvinTegelaar/CIPP/issues/4846 --- .../Endpoint/Applications/Invoke-AddOfficeApp.ps1 | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 index 06701d5a3fcf..036d1afaae85 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/Applications/Invoke-AddOfficeApp.ps1 @@ -9,7 +9,7 @@ function Invoke-AddOfficeApp { param($Request, $TriggerMetadata) # Input bindings are passed in via param block. - $Tenants = $Request.body.selectedTenants.defaultDomainName + $Tenants = $Request.Body.selectedTenants.defaultDomainName $Headers = $Request.Headers $APIName = $Request.Params.CIPPEndpoint if ('AllTenants' -in $Tenants) { $Tenants = (Get-Tenants).defaultDomainName } @@ -27,6 +27,7 @@ function Invoke-AddOfficeApp { 'displayName' = 'Microsoft 365 Apps for Windows 10 and later' 'description' = 'Microsoft 365 Apps for Windows 10 and later' 'informationUrl' = 'https://products.office.com/en-us/explore-office-for-home' + 'privacyInformationUrl' = 'https://privacy.microsoft.com/en-us/privacystatement' 'isFeatured' = $true 'publisher' = 'Microsoft' 'notes' = '' @@ -63,6 +64,7 @@ function Invoke-AddOfficeApp { 'displayName' = 'Microsoft 365 Apps for Windows 10 and later' 'description' = 'Microsoft 365 Apps for Windows 10 and later' 'informationUrl' = 'https://products.office.com/en-us/explore-office-for-home' + 'privacyInformationUrl' = 'https://privacy.microsoft.com/en-us/privacystatement' 'isFeatured' = $true 'publisher' = 'Microsoft' 'notes' = '' @@ -78,12 +80,12 @@ function Invoke-AddOfficeApp { 'productIds' = $products 'largeIcon' = @{ 'type' = 'image/png' - 'value' = 'iVBORw0KGgoAAAANSUhEUgAAAF0AAAAeCAMAAAEOZNKlAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJhUExURf////7z7/i9qfF1S/KCW/i+qv3q5P/9/PrQwfOMae1RG+s8AOxGDfBtQPWhhPvUx/759/zg1vWgg+9fLu5WIvKFX/rSxP728/nCr/FyR+tBBvOMaO1UH+1RHOs+AvSScP3u6f/+/v3s5vzg1+xFDO9kNPOOa/i7pvzj2/vWyes9Af76+Pzh2PrTxf/6+f7y7vOGYexHDv3t5+1SHfi8qPOIZPvb0O1NFuxDCe9hMPSVdPnFs/3q4/vaz/STcu5VIe5YJPWcfv718v/9/e1MFfF4T/F4TvF2TP3o4exECvF0SexIEPONavzn3/vZze1QGvF3Te5dK+5cKvrPwPrQwvKAWe1OGPexmexKEveulfezm/BxRfamiuxLE/apj/zf1e5YJfSXd/OHYv3r5feznPakiPze1P7x7f739f3w6+xJEfnEsvWdf/Wfge1LFPe1nu9iMvnDsfBqPOs/BPOIY/WZevJ/V/zl3fnIt/vTxuxHD+xEC+9mN+5ZJv749vBpO/KBWvBwRP/8+/SUc/etlPjArP/7+vOLZ/F7UvWae/708e1OF/aihvSWdvi8p+tABfSZefvVyPWihfSVde9lNvami+9jM/zi2fKEXvBuQvOKZvalifF5UPJ/WPSPbe9eLfrKuvvd0uxBB/7w7Pzj2vrRw/rOv+1PGfi/q/eymu5bKf3n4PnJuPBrPf3t6PWfgvWegOxCCO9nOO9oOfaskvSYePi5pPi2oPnGtO5eLPevlvKDXfrNvv739Pzd0/708O9gL+9lNfJ9VfrLu/OPbPnDsPBrPus+A/nArfarkQAAAGr5HKgAAADLdFJOU/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AvuakogAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAz5JREFUOE+tVTtu4zAQHQjppmWzwIJbEVCzpTpjbxD3grQHSOXKRXgCAT6EC7UBVAmp3KwBnmvfzNCyZTmxgeTZJsXx43B+HBHRE34ZkXgkerXFTheeiCkRrbB4UXmp4wSWz5raaQEMTM5TZwuiXoaKgV+6FsmkZQcSy0kA71yMTMGHanX+AzMMGLAQCxU1F/ZwjULPugazl82GM0NEKm/U8EqFwEkO3/EAT4grgl0nucwlk9pcpTTJ4VPA4g/Rb3yIRhhp507e9nTQmZ1OS5RO4sS7nIRPEeHXCHdkw9ZEW2yVE5oIS7peD58Avs7CN+PVCmHh21oOqBdjDzIs+FldPJ74TFESUSJEfVzy9U/dhu+AuOT6eBp6gGKyXEx8euO450ZE4CMfstMFT44broWw/itkYErWXRx+fFArt9Ca9os78TFed0LVIUsmIHrwbwaw3BEOnOk94qVpQ6Ka2HjxewJnfyd6jUtGDQLdWlzmYNYLeKbbGOucJsNabCq1Yub0o92rtR+i30V2dapxYVEePXcOjeCKPnYyit7BtKeNlZqHbr+gt7i+AChWA9RsRs03pxTQc67ouWpxyESvjK5Vs3DVSy3IpkxPm5X+wZoBi+MFHWW69/w8FRhc7VBe6HAhMB2b8Q0XqDzTNZtXUMnKMjwKVaCrB/CSUL7WSx/HsdJC86lFGXwnioTeOMPjV+szlFvrZLA5VMVK4y+41l4e1xfx7Z88oXLhMo/hbYyqDV3FGjfD+Atbk7YjJAy9h/F4fWNbC6lwKUrhVUBPnPCDDsL0A/mLWxujCLvx4hE/VhOTf8j/C3kf3PWRqJKUAAAAASUVORK5CYII=' + 'value' = 'iVBORw0KGgoAAAANSUhEUgAAAF0AAAAeCAMAAAEOZNKlAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAJhUExURf////7z7/i9qfF1S/KCW/i+qv3q5P/9/PrQwfOMae1RG+s8AOxGDfBtQPWhhPvUx/759/zg1vWgg+9fLu5WIvKFX/rSxP728/nCr/FyR+tBBvOMaO1UH+1RHOs+AvSScP3u6f/+/v3s5vzg1+xFDO9kNPOOa/i7pvzj2/vWyes9Af76+Pzh2PrTxf/6+f7y7vOGYexHDv3t5+1SHfi8qPOIZPvb0O1NFuxDCe9hMPSVdPnFs/3q4/vaz/STcu5VIe5YJPWcfv718v/9/e1MFfF4T/F4TvF2TP3o4exECvF0SexIEPONavzn3/vZze1QGvF3Te5dK+5cKvrPwPrQwvKAWe1OGPexmexKEveulfezm/BxRfamiuxLE/apj/zf1e5YJfSXd/OHYv3r5feznPakiPze1P7x7f739f3w6+xJEfnEsvWdf/Wfge1LFPe1nu9iMvnDsfBqPOs/BPOIY/WZevJ/V/zl3fnIt/vTxuxHD+xEC+9mN+5ZJv749vBpO/KBWvBwRP/8+/SUc/etlPjArP/7+vOLZ/F7UvWae/708e1OF/aihvSWdvi8p+tABfSZefvVyPWihfSVde9lNvami+9jM/zi2fKEXvBuQvOKZvalifF5UPJ/WPSPbe9eLfrKuvvd0uxBB/7w7Pzj2vrRw/rOv+1PGfi/q/eymu5bKf3n4PnJuPBrPf3t6PWfgvWegOxCCO9nOO9oOfaskvSYePi5pPi2oPnGtO5eLPevlvKDXfrNvv739Pzd0/708O9gL+9lNfJ9VfrLu/OPbPnDsPBrPus+A/nArfarkQAAAGr5HKgAAADLdFJOU/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8AvuakogAAAAlwSFlzAAAOwwAADsMBx2+oZAAAAz5JREFUOE+tVTtu4zAQHQjppmWzwIJbEVCzpTpjbxD3grQHSOXKRXgCAT6EC7UBVAmp3KwBnmvfzNCyZTmxgeTZJsXx43B+HBHRE34ZkXgkerXFTheeiCkRrbB4UXmp4wSWz5raaQEMTM5TZwuiXoaKgV+6FsmkZQcSy0kA71yMTMGHanX+AzMMGLAQCxU1F/ZwjULPugazl82GM0NEKm/U8EqFwEkO3/EAT4grgl0nucwlk9pcpTTJ4VPA4g/Rb3yIRhhp507e9nTQmZ1OS5RO4sS7nIRPEeHXCHdkw9ZEW2yVE5oIS7peD58Avs7CN+PVCmHh21oOqBdjDzIs+FldPJ74TFESUSJEfVzy9U/dhu+AuOT6eBp6gGKyXEx8euO450ZE4CMfstMFT44broWw/itkYErWXRx+fFArt9Ca9os78TFed0LVIUsmIHrwbwaw3BEOnOk94qVpQ6Ka2HjxewJnfyd6jUtGDQLdWlzmYNYLeKbbGOucJsNabCq1Yub0o92rtR+i30V2dapxYVEePXcOjeCKPnYyit7BtKeNlZqHbr+gt7i+AChWA9RsRs03pxTQc67ouWpxyESvjK5Vs3DVSy3IpkxPm5X+wZoBi+MFHWW69/w8FRhc7VBe6HAhMB2b8Q0XqDzTNZtXUMnKMjwKVaCrB/CSUL7WSx/HsdJC86lFGXwnioTeOMPjV+szlFvrZLA5VMVK4y+41l4e1xfx7Z88o4hkilRUH/qKqwNVlgDgpvYCpH3XwAy5eMCRnezIUxffVXoDql2rTHFDO+pjWnTWzAfrYXn6BFECblUpWGrvPZvBipETjS5ydM7tdXpH41ZCEbBNy/+wFZu71QO2t9pgT+iZEf657Q1vpN94PQNDxUHeKR103LV9nPVOtDikcNKO+2naCw7yKBhOe9Hm79pe8C4/CfC2wDjXnqC94kEeBU3WwN7dt/2UScXas7zDl5GpkY+M8WKv2J7fd4Ib2rGTk+jsC2cleEM7jI9veF7B0MBJrsZqfKd/81q9pR2NZfwJK2JzsmIT1Ns8jUH0UusQBpU8d2JzsHiXg1zXGLqxfitUNTDT/nUUeqDBp2HZVr+Ocqi/Ty3Rf4Jn82xxfSNtAAAAAElFTkSuQmCC' } } } - Write-Host ($ObjBody | ConvertTo-Json -Compress) - $OfficeAppID = New-graphPostRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $tenant -Body (ConvertTo-Json -InputObject $ObjBody -Depth 10) -type POST + # Write-Host ($ObjBody | ConvertTo-Json -Compress) + $OfficeAppID = New-GraphPOSTRequest -Uri 'https://graph.microsoft.com/beta/deviceAppManagement/mobileApps' -tenantid $tenant -Body (ConvertTo-Json -InputObject $ObjBody -Depth 10) -type POST } else { "Office deployment already exists for $($Tenant)" continue @@ -91,7 +93,7 @@ function Invoke-AddOfficeApp { Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Added Office profile to $($Tenant)" -Sev 'Info' if ($AssignTo) { $AssignO365 = if ($AssignTo -ne 'AllDevicesAndUsers') { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.' + $($AssignTo) + 'AssignmentTarget"},"intent":"Required"}]}' } else { '{"mobileAppAssignments":[{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"},"intent":"Required"},{"@odata.type":"#microsoft.graph.mobileAppAssignment","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"},"intent":"Required"}]}' } Write-Host ($AssignO365) - New-graphPostRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $Tenant -Body $AssignO365 -type POST + New-GraphPOSTRequest -Uri "https://graph.microsoft.com/beta/deviceAppManagement/mobileApps/$($OfficeAppID.id)/assign" -tenantid $Tenant -Body $AssignO365 -type POST Write-LogMessage -headers $Headers -API $APIName -tenant $($Tenant) -message "Assigned Office to $AssignTo" -Sev 'Info' } "Successfully added Office App for $($Tenant)" From 77a174059d93a71597dca4fc0a5303a4bd2ef500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristian=20Kj=C3=A6rg=C3=A5rd?= Date: Mon, 10 Nov 2025 14:19:51 +0100 Subject: [PATCH 10/10] why did i make it json, we use psobjects in this town dawg --- Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 index 73a49b8d0de2..9ef67b5e673f 100644 --- a/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPLicenseOverview.ps1 @@ -143,8 +143,8 @@ function Get-CIPPLicenseOverview { skuPartNumber = [string]$PrettyName availableUnits = [string]$sku.prepaidUnits.enabled - $sku.consumedUnits TermInfo = [string]($TermInfo | ConvertTo-Json -Depth 10 -Compress) - AssignedUsers = [string]($UsersBySku.ContainsKey($SkuKey) ? (ConvertTo-Json -InputObject ($UsersBySku[$SkuKey].ToArray()) -Depth 5 -Compress) : '[]') - AssignedGroups = [string]($GroupsBySku.ContainsKey($SkuKey) ? (ConvertTo-Json -InputObject ($GroupsBySku[$SkuKey].ToArray()) -Depth 5 -Compress) : '[]') + AssignedUsers = ($UsersBySku.ContainsKey($SkuKey) ? @(($UsersBySku[$SkuKey])) : $null) + AssignedGroups = ($GroupsBySku.ContainsKey($SkuKey) ? @(($GroupsBySku[$SkuKey])) : $null) 'PartitionKey' = 'License' 'RowKey' = "$($singleReq.Tenant) - $($sku.skuid)" }