From afa25ec6602f05c1c38e0feed64b5c8b1457323a Mon Sep 17 00:00:00 2001 From: BNWEIN Date: Sun, 23 Jun 2024 22:20:35 +0100 Subject: [PATCH 01/33] Create Invoke-ListGroupSenderAuthentication.ps1 --- .../Invoke-ListGroupSenderAuthentication.ps1 | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 new file mode 100644 index 000000000000..1eaaf99e0555 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -0,0 +1,41 @@ +using namespace System.Net + +Function Invoke-ListGroupSenderAuthentication { + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + + # Write to the Azure Functions log stream. + Write-Host 'PowerShell HTTP trigger function processed a request.' + + # Interact with query parameters or the body of the request. + + $TenantFilter = $Request.Query.TenantFilter + $groupid = $Request.query.groupid + + $params = @{ + Identity = $groupid + } + + try { + $Request = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DistributionGroup' -cmdParams $params -UseSystemMailbox $true + $StatusCode = [HttpStatusCode]::OK + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $StatusCode = [HttpStatusCode]::Forbidden + $Request = $ErrorMessage + } + + write-host "Group ID is: $($groupid)" + write-host "Tenant Filter is: $($TenantFilter)" + write-host "Search This New: $($Request.RequireSenderAuthenticationEnabled)" + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = $StatusCode + Body = $($Request.RequireSenderAuthenticationEnabled) + }) +} \ No newline at end of file From d1a315506145698f95da655a8abd82b8be88d1d5 Mon Sep 17 00:00:00 2001 From: BNWEIN Date: Sun, 23 Jun 2024 22:22:17 +0100 Subject: [PATCH 02/33] Update Invoke-ListGroupSenderAuthentication.ps1 --- .../Groups/Invoke-ListGroupSenderAuthentication.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 index 1eaaf99e0555..305c8b927564 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -29,10 +29,6 @@ Function Invoke-ListGroupSenderAuthentication { $Request = $ErrorMessage } - write-host "Group ID is: $($groupid)" - write-host "Tenant Filter is: $($TenantFilter)" - write-host "Search This New: $($Request.RequireSenderAuthenticationEnabled)" - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode From 46f9dad45d7a4e52259d8526f0a5dae5a9eb35d2 Mon Sep 17 00:00:00 2001 From: BNWEIN Date: Wed, 26 Jun 2024 14:05:35 +0100 Subject: [PATCH 03/33] Update Invoke-ListGroupSenderAuthentication.ps1 --- .../Groups/Invoke-ListGroupSenderAuthentication.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 index 305c8b927564..189dd39468b0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -20,6 +20,11 @@ Function Invoke-ListGroupSenderAuthentication { Identity = $groupid } + Write-Host = "This is the group id $groupid" + Write-Host = "This is the tenant filter $TenantFilter" + $GroupType = Invoke-ListGroups -tenantFilter $TenantFilter -GroupID $groupid + Write-Host = "This is the group type $($GroupType.calculatedGroupType)" + try { $Request = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DistributionGroup' -cmdParams $params -UseSystemMailbox $true $StatusCode = [HttpStatusCode]::OK @@ -32,6 +37,6 @@ Function Invoke-ListGroupSenderAuthentication { # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = $StatusCode - Body = $($Request.RequireSenderAuthenticationEnabled) + Body = @{ enabled = $Request.RequireSenderAuthenticationEnabled } }) } \ No newline at end of file From f229fd42ae7d3558839c1abb49cb5a3a8c6d2b9a Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Jul 2024 17:05:02 -0400 Subject: [PATCH 04/33] Extension fix - CIPP-API Remove ResetPassword property from saved settings to prevent rotating secret every save --- .../Settings/Invoke-ExecExtensionsConfig.ps1 | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 index f40536cebcb4..60eed5ba5165 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 @@ -11,21 +11,21 @@ Function Invoke-ExecExtensionsConfig { param($Request, $TriggerMetadata) $APIName = $TriggerMetadata.FunctionName - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + Write-LogMessage -user $Request.Headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' #Connect-AzAccount -UseDeviceAuthentication # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' $results = try { - if ($Request.body.CIPPAPI.Enabled) { - $APIConfig = New-CIPPAPIConfig -ExecutingUser $request.headers.'x-ms-client-principal' -resetpassword $request.body.CIPPAPI.ResetPassword + if ($Request.Body.CIPPAPI.Enabled) { + $APIConfig = New-CIPPAPIConfig -ExecutingUser $Request.Headers.'x-ms-client-principal' -resetpassword $Request.Body.CIPPAPI.ResetPassword $AddedText = $APIConfig.Results } # Check if NinjaOne URL is set correctly and the instance has at least version 5.6 - if ($request.body.NinjaOne) { + if ($Request.Body.NinjaOne) { try { - [version]$Version = (Invoke-WebRequest -Method GET -Uri "https://$(($request.body.NinjaOne.Instance -replace '/ws','') -replace 'https://','')/app-version.txt" -ea stop).content + [version]$Version = (Invoke-WebRequest -Method GET -Uri "https://$(($Request.Body.NinjaOne.Instance -replace '/ws','') -replace 'https://','')/app-version.txt" -ea stop).content } catch { throw "Failed to connect to NinjaOne check your Instance is set correctly eg 'app.ninjarmmm.com'" } @@ -35,30 +35,31 @@ Function Invoke-ExecExtensionsConfig { } $Table = Get-CIPPTable -TableName Extensionsconfig - foreach ($APIKey in ([pscustomobject]$request.body).psobject.properties.name) { + foreach ($APIKey in ([pscustomobject]$Request.Body).psobject.properties.name) { Write-Host "Working on $apikey" - if ($request.body.$APIKey.APIKey -eq 'SentToKeyVault' -or $request.body.$APIKey.APIKey -eq '') { + if ($Request.Body.$APIKey.APIKey -eq 'SentToKeyVault' -or $Request.Body.$APIKey.APIKey -eq '') { Write-Host 'Not sending to keyvault. Key previously set or left blank.' } else { Write-Host 'writing API Key to keyvault, and clearing.' Write-Host "$ENV:WEBSITE_DEPLOYMENT_ID" - if ($request.body.$APIKey.APIKey) { + if ($Request.Body.$APIKey.APIKey) { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' $Secret = [PSCustomObject]@{ 'PartitionKey' = $APIKey 'RowKey' = $APIKey - 'APIKey' = $request.body.$APIKey.APIKey + 'APIKey' = $Request.Body.$APIKey.APIKey } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { - $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -String $request.body.$APIKey.APIKey -AsPlainText -Force) + $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -String $Request.Body.$APIKey.APIKey -AsPlainText -Force) } } - $request.body.$APIKey.APIKey = 'SentToKeyVault' + $Request.Body.$APIKey.APIKey = 'SentToKeyVault' } + $Request.Body.$APIKey = $Request.Body.$APIKey | Select-Object * -ExcludeProperty ResetPassword } - $body = $request.body | Select-Object * -ExcludeProperty APIKey, Enabled | ConvertTo-Json -Depth 10 -Compress + $body = $Request.Body | Select-Object * -ExcludeProperty APIKey, Enabled | ConvertTo-Json -Depth 10 -Compress $Config = @{ 'PartitionKey' = 'CippExtensions' 'RowKey' = 'Config' From 414e7add7b4c235aca32597292fc14e5296e48d4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Jul 2024 17:09:38 -0400 Subject: [PATCH 05/33] Update Invoke-ExecExtensionsConfig.ps1 --- .../CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 index 60eed5ba5165..0dbd6f5bd95d 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 @@ -7,6 +7,7 @@ Function Invoke-ExecExtensionsConfig { .ROLE CIPP.Extension.ReadWrite #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function')] [CmdletBinding()] param($Request, $TriggerMetadata) @@ -52,7 +53,7 @@ Function Invoke-ExecExtensionsConfig { } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { - $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -String $Request.Body.$APIKey.APIKey -AsPlainText -Force) + $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name $APIKey -SecretValue (ConvertTo-SecureString -AsPlainText -Force -String $Request.Body.$APIKey.APIKey) } } $Request.Body.$APIKey.APIKey = 'SentToKeyVault' From dc588a28ffcf7382c174e10b4d3a43f89b7edce0 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Tue, 2 Jul 2024 17:11:56 -0400 Subject: [PATCH 06/33] Update Invoke-ExecExtensionsConfig.ps1 --- .../CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 index 0dbd6f5bd95d..ebbf5ca9dba4 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 @@ -16,7 +16,7 @@ Function Invoke-ExecExtensionsConfig { #Connect-AzAccount -UseDeviceAuthentication # Write to the Azure Functions log stream. - Write-Host 'PowerShell HTTP trigger function processed a request.' + Write-Information 'PowerShell HTTP trigger function processed a request.' $results = try { if ($Request.Body.CIPPAPI.Enabled) { $APIConfig = New-CIPPAPIConfig -ExecutingUser $Request.Headers.'x-ms-client-principal' -resetpassword $Request.Body.CIPPAPI.ResetPassword @@ -37,12 +37,12 @@ Function Invoke-ExecExtensionsConfig { $Table = Get-CIPPTable -TableName Extensionsconfig foreach ($APIKey in ([pscustomobject]$Request.Body).psobject.properties.name) { - Write-Host "Working on $apikey" + Write-Information "Working on $apikey" if ($Request.Body.$APIKey.APIKey -eq 'SentToKeyVault' -or $Request.Body.$APIKey.APIKey -eq '') { - Write-Host 'Not sending to keyvault. Key previously set or left blank.' + Write-Information 'Not sending to keyvault. Key previously set or left blank.' } else { - Write-Host 'writing API Key to keyvault, and clearing.' - Write-Host "$ENV:WEBSITE_DEPLOYMENT_ID" + Write-Information 'writing API Key to keyvault, and clearing.' + Write-Information "$ENV:WEBSITE_DEPLOYMENT_ID" if ($Request.Body.$APIKey.APIKey) { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $DevSecretsTable = Get-CIPPTable -tablename 'DevSecrets' From a5f0476e0feefd21935b94159b87ade35945abb3 Mon Sep 17 00:00:00 2001 From: rvdwegen Date: Wed, 3 Jul 2024 22:27:38 +0200 Subject: [PATCH 07/33] Adding self-service license management Standard --- Cache_SAMSetup/SAMManifest.json | 6 ++ Modules/CIPPCore/Public/SAMManifest.json | 6 ++ ...CIPPStandardDisableSelfServiceLicenses.ps1 | 69 +++++++++++++++++-- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index 6b1f6429af88..e2a457b734fb 100644 --- a/Cache_SAMSetup/SAMManifest.json +++ b/Cache_SAMSetup/SAMManifest.json @@ -11,6 +11,12 @@ ] }, "requiredResourceAccess": [ + { + "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "resourceAccess": [ + { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } + ] + }, { "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", "resourceAccess": [ diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index d545a87d25a5..7316e34c2246 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -11,6 +11,12 @@ ] }, "requiredResourceAccess": [ + { + "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "resourceAccess": [ + { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } + ] + }, { "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", "resourceAccess": [ diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 index 147aa0fefe89..fa8b5cb537e2 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardDisableSelfServiceLicenses.ps1 @@ -9,28 +9,87 @@ function Invoke-CIPPStandardDisableSelfServiceLicenses { .TAG "mediumimpact" .HELPTEXT - This standard currently does not function and can be safely disabled + This standard disables all self service licenses and enables all exclusions .ADDEDCOMPONENT .LABEL Disable Self Service Licensing .IMPACT Medium Impact .POWERSHELLEQUIVALENT - Set-MsolCompanySettings -AllowAdHocSubscriptions $false + Update-MSCommerceProductPolicy -PolicyId AllowSelfServicePurchase -ProductId {productId} -Value "Disabled" .RECOMMENDEDBY .DOCSDESCRIPTION - This standard currently does not function and can be safely disabled + This standard disables all self service licenses and enables all exclusions .UPDATECOMMENTBLOCK Run the Tools\Update-StandardsComments.ps1 script to update this comment block #> + param($Tenant, $Settings) + #Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error + try { + $selfServiceItems = (New-GraphGETRequest -scope "aeb86249-8ea3-49e2-900b-54cc8e308f85/.default" -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products" -tenantid $Tenant).items + #$selfServiceItems = (Invoke-RestMethod -Method GET -Uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products" -Headers $header).items + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to retrieve self service products: $($_.Exception.Message)" -sev Error + throw "Failed to retrieve self service products: $($_.Exception.Message)" + } + if ($settings.remediate) { + if ($settings.exclusions -like "*;*") { + $exclusions = $settings.Exclusions -split(';') + } else { + $exclusions = $settings.Exclusions -split(',') + } - param($Tenant, $Settings) + $selfServiceItems | ForEach-Object { + $body = $null + + if ($_.policyValue -eq "Enabled" -AND ($_.productId -in $exclusions)) { + # Self service is enabled on product and productId is in exclusions, skip + } + if ($_.policyValue -eq "Disabled" -AND ($_.productId -in $exclusions)) { + # Self service is disabled on product and productId is in exclusions, enable + $body = '{ "policyValue": "Enabled" }' + } + if ($_.policyValue -eq "Enabled" -AND ($_.productId -notin $exclusions)) { + # Self service is enabled on product and productId is NOT in exclusions, disable + $body = '{ "policyValue": "Disabled" }' + } + if ($_.policyValue -eq "Disabled" -AND ($_.productId -notin $exclusions)) { + # Self service is disabled on product and productId is NOT in exclusions, skip + } + + try { + if ($body) { + $product = $_ + New-GraphPOSTRequest -scope "aeb86249-8ea3-49e2-900b-54cc8e308f85/.default" -uri "https://licensing.m365.microsoft.com/v1.0/policies/AllowSelfServicePurchase/products/$($product.productId)" -tenantid $Tenant -body $body -type PUT + } + } catch { + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to set product status for $($product.productId) with body $($body) for reason: $($_.Exception.Message)" -sev Error + #Write-Error "Failed to disable product $($product.productName):$($_.Exception.Message)" + } + } + + if (!$exclusions) { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'No exclusions set for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $Tenant -message 'Exclusions present for self-service licenses, disabled all not excluded licenses for self-service.' -sev Info + } + } - Write-LogMessage -API 'Standards' -tenant $tenant -message 'Self Service Licenses cannot be disabled' -sev Error + if ($Settings.alert) { + $selfServiceItemsToAlert = $selfServiceItems | Where-Object { $_.policyValue -eq "Enabled"} + if (!$selfServiceItemsToAlert) { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'All self-service licenses are disabled' -sev Info + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'One or more self-service licenses are enabled' -sev Alert + } + } + if ($Settings.report -eq $true) { + #Add-CIPPBPAField -FieldName '????' -FieldValue "????" -StoreAs bool -Tenant $tenant + } } From 79b05a49d1e2801be23b780a40fbb98ecf05c3ef Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:40:11 -0500 Subject: [PATCH 08/33] Update Invoke-CIPPStandardEnableLitigationHold.ps1 --- .../Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 1de0a2315a13..ba3120814bc8 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -5,7 +5,7 @@ function Invoke-CIPPStandardEnableLitigationHold { #> param($Tenant, $Settings) - $MailboxesNoLitHold = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdparams @{ MailboxPlan = 'ExchangeOnlineEnterprise'; Filter = 'LitigationHoldEnabled -eq "False"'} + $MailboxesNoLitHold = New-ExoRequest -tenantid $Tenant -cmdlet 'Get-Mailbox' -cmdparams @{ Filter = 'LitigationHoldEnabled -eq "False"'} | Where-Object {$_.PersistedCapabilities -contains "BPOS_S_DlpAddOn" -or $_.PersistedCapabilities -contains "BPOS_S_Enterprise"} If ($Settings.remediate -eq $true) { From 2b721d4ac3c95245ce8848848fa866b01e6674bb Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jul 2024 17:11:53 -0400 Subject: [PATCH 09/33] Standardize Extension Output and Mappings --- .../Settings/Invoke-ExecExtensionMapping.ps1 | 18 +- .../Settings/Invoke-ExecExtensionSync.ps1 | 8 +- .../Settings/Invoke-ExecExtensionTest.ps1 | 8 +- .../Get-ExtensionMapping.ps1 | 15 + .../Set-ExtensionFieldMapping.ps1 | 24 ++ .../Public/Halo/Get-HaloMapping.ps1 | 26 +- .../Public/Halo/Set-HaloMapping.ps1 | 12 +- .../Public/Hudu/Get-HuduFieldMapping.ps1 | 68 ++++ .../Public/Hudu/Get-HuduMapping.ps1 | 14 +- .../Public/Hudu/Set-HuduMapping.ps1 | 6 +- .../Public/New-CippExtAlert.ps1 | 8 +- .../NinjaOne/Get-NinjaOneFieldMapping.ps1 | 136 ++++---- .../NinjaOne/Get-NinjaOneOrgMapping.ps1 | 21 +- .../NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 | 24 +- .../Invoke-NinjaOneExtensionScheduler.ps1 | 4 +- .../NinjaOne/Invoke-NinjaOneOrgMapping.ps1 | 12 +- .../Invoke-NinjaOneOrgMappingTenant.ps1 | 26 +- .../Public/NinjaOne/Invoke-NinjaOneSync.ps1 | 4 +- .../NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 312 +++++++++--------- .../NinjaOne/Set-NinjaOneFieldMapping.ps1 | 14 +- .../NinjaOne/Set-NinjaOneOrgMapping.ps1 | 12 +- 21 files changed, 460 insertions(+), 312 deletions(-) create mode 100644 Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 create mode 100644 Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 create mode 100644 Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 index 595bf587ef19..d8a5bfba10dc 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionMapping.ps1 @@ -20,36 +20,42 @@ Function Invoke-ExecExtensionMapping { if ($Request.Query.List) { switch ($Request.Query.List) { - 'Halo' { + 'HaloPSA' { $body = Get-HaloMapping -CIPPMapping $Table } - 'NinjaOrgs' { + 'NinjaOne' { $Body = Get-NinjaOneOrgMapping -CIPPMapping $Table } - 'NinjaFields' { + 'NinjaOneFields' { $Body = Get-NinjaOneFieldMapping -CIPPMapping $Table } 'Hudu' { $Body = Get-HuduMapping -CIPPMapping $Table } + 'HuduFields' { + $Body = Get-HuduFieldMapping -CIPPMapping $Table + } } } try { if ($Request.Query.AddMapping) { switch ($Request.Query.AddMapping) { - 'Halo' { + 'HaloPSA' { $body = Set-HaloMapping -CIPPMapping $Table -APIName $APIName -Request $Request } - 'NinjaOrgs' { + 'NinjaOne' { $Body = Set-NinjaOneOrgMapping -CIPPMapping $Table -APIName $APIName -Request $Request } - 'NinjaFields' { + 'NinjaOneFields' { $Body = Set-NinjaOneFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -TriggerMetadata $TriggerMetadata } 'Hudu' { $Body = Set-HuduMapping -CIPPMapping $Table -APIName $APIName -Request $Request } + 'HuduFields' { + $Body = Set-ExtensionFieldMapping -CIPPMapping $Table -APIName $APIName -Request $Request -Extension 'Hudu' + } } } } catch { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 index 2f069988996a..9c090c91f094 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionSync.ps1 @@ -23,7 +23,7 @@ Function Invoke-ExecExtensionSync { 'Gradient' { If ($Configuration.Gradient.enabled -and $Configuration.Gradient.BillingEnabled) { Push-OutputBinding -Name gradientqueue -Value 'LetsGo' - $Results = [pscustomobject]@{'Results' = 'Succesfully started Gradient Sync' } + $Results = [pscustomobject]@{'Results' = 'Successfully started Gradient Sync' } } } } @@ -40,8 +40,8 @@ Function Invoke-ExecExtensionSync { $Table = Get-CIPPTable -TableName NinjaOneSettings $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } if ($Request.Query.TenantID) { $Tenant = $TenantsToProcess | Where-Object { $_.RowKey -eq $Request.Query.TenantID } @@ -59,7 +59,7 @@ Function Invoke-ExecExtensionSync { $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) Write-Host "Started permissions orchestration with ID = '$InstanceId'" - $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queued for $($Tenant.NinjaOneName)" } + $Results = [pscustomobject]@{'Results' = "NinjaOne Synchronization Queued for $($Tenant.IntegrationName)" } } else { $Results = [pscustomobject]@{'Results' = 'Tenant was not found.' } } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 index 74b188742a97..e9a6465c4ff0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionTest.ps1 @@ -27,7 +27,7 @@ Function Invoke-ExecExtensionTest { if ($ExistingIntegrations.Status -ne 'active') { $ActivateRequest = Invoke-RestMethod -Uri 'https://app.usegradient.com/api/vendor-api/organization/status/active' -Method PATCH -Headers $GradientToken } - $Results = [pscustomobject]@{'Results' = 'Succesfully Connected to Gradient' } + $Results = [pscustomobject]@{'Results' = 'Successfully Connected to Gradient' } } 'CIPP-API' { @@ -35,13 +35,13 @@ Function Invoke-ExecExtensionTest { } 'NinjaOne' { $token = Get-NinjaOneToken -configuration $Configuration.NinjaOne - $Results = [pscustomobject]@{'Results' = 'Succesfully Connected to NinjaOne' } + $Results = [pscustomobject]@{'Results' = 'Successfully Connected to NinjaOne' } } 'PWPush' { $Payload = 'This is a test from CIPP' $PasswordLink = New-PwPushLink -Payload $Payload if ($PasswordLink) { - $Results = [pscustomobject]@{'Results' = 'Succesfully generated PWPush'; 'Link' = $PasswordLink } + $Results = [pscustomobject]@{'Results' = 'Successfully generated PWPush'; 'Link' = $PasswordLink } } else { $Results = [pscustomobject]@{'Results' = 'PWPush is not enabled' } } @@ -50,7 +50,7 @@ Function Invoke-ExecExtensionTest { Connect-HuduAPI -configuration $Configuration.Hudu $Version = Get-HuduAppInfo Write-Host ($Version | ConvertTo-Json) - $Results = [pscustomobject]@{'Results' = ('Succesfully Connected to Hudu, version: {0}' -f $Version.version) } + $Results = [pscustomobject]@{'Results' = ('Successfully Connected to Hudu, version: {0}' -f $Version.version) } } } } catch { diff --git a/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 new file mode 100644 index 000000000000..6a0ac35728c6 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Get-ExtensionMapping.ps1 @@ -0,0 +1,15 @@ +function Get-ExtensionMapping { + param( + $Extension + ) + + $Table = Get-CIPPTable -TableName CippMapping + $Mapping = @{} + Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($Extension)Mapping'" | ForEach-Object { + $Mapping[$_.RowKey] = @{ + label = "$($_.IntegrationName)" + value = "$($_.IntegrationId)" + } + } + return [PSCustomObject]$Mapping +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 b/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 new file mode 100644 index 000000000000..52d59ab12d77 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Set-ExtensionFieldMapping.ps1 @@ -0,0 +1,24 @@ +function Set-ExtensionFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping, + $Extension, + $APIName, + $Request, + $TriggerMetadata + ) + + foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { + $AddObject = @{ + PartitionKey = "$($Extension)FieldMapping" + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" + } + Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + } + $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } + + Return $Result +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 b/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 index fcae99cfd5d1..2a8aae7646ef 100644 --- a/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 +++ b/Modules/CippExtensions/Public/Halo/Get-HaloMapping.ps1 @@ -6,16 +6,28 @@ function Get-HaloMapping { #Get available mappings $Mappings = [pscustomobject]@{} + # Migrate legacy mappings $Filter = "PartitionKey eq 'Mapping'" - Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.HaloPSAName)"; value = "$($_.HaloPSA)" } + $MigrateRows = Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { + [PSCustomObject]@{ + PartitionKey = 'HaloMapping' + RowKey = $_.RowKey + IntegrationId = $_.HaloPSA + IntegrationName = $_.HaloPSAName + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ | Out-Null + } + if (($MigrateRows | Measure-Object).Count -gt 0) { + Add-CIPPAzDataTableEntity @CIPPMapping -Entity $MigrateRows -Force } + + $Mappings = Get-ExtensionMapping -Extension 'Halo' + $Tenants = Get-Tenants -IncludeErrors $Table = Get-CIPPTable -TableName Extensionsconfig try { $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).HaloPSA - $Token = Get-HaloToken -configuration $Configuration $i = 1 $RawHaloClients = do { @@ -32,7 +44,7 @@ function Get-HaloMapping { } Write-LogMessage -Message "Could not get HaloPSA Clients, error: $Message " -Level Error -tenant 'CIPP' -API 'HaloMapping' - $RawHaloClients = @(@{name = "Could not get HaloPSA Clients, error: $Message"; value = '-1' }) + $RawHaloClients = @(@{name = "Could not get HaloPSA Clients, error: $Message"; id = '-1' }) } $HaloClients = $RawHaloClients | ForEach-Object { [PSCustomObject]@{ @@ -41,9 +53,9 @@ function Get-HaloMapping { } } $MappingObj = [PSCustomObject]@{ - Tenants = @($Tenants) - HaloClients = @($HaloClients) - Mappings = $Mappings + Tenants = @($Tenants) + Companies = @($HaloClients) + Mappings = $Mappings } return $MappingObj diff --git a/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 b/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 index 527bbc94fd22..129b1578ad59 100644 --- a/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 +++ b/Modules/CippExtensions/Public/Halo/Set-HaloMapping.ps1 @@ -5,20 +5,20 @@ function Set-HaloMapping { $APIName, $Request ) - Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'Mapping'" | ForEach-Object { + Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'HaloMapping'" | ForEach-Object { Remove-AzDataTableEntity @CIPPMapping -Entity $_ } foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'Mapping' - RowKey = "$($mapping.name)" - 'HaloPSA' = "$($mapping.value.value)" - 'HaloPSAName' = "$($mapping.value.label)" + PartitionKey = 'HaloMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-CIPPAzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 new file mode 100644 index 000000000000..64b4700af62f --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 @@ -0,0 +1,68 @@ +function Get-HuduFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + + $Mappings = Get-ExtensionMapping -Extension 'HuduFields' + + $CIPPFieldHeaders = @( + [PSCustomObject]@{ + Title = 'Hudu Asset Layouts' + FieldType = 'Layouts' + Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Field' + } + ) + $CIPPFields = @( + [PSCustomObject]@{ + FieldName = 'Users' + FieldLabel = 'Asset Layout for M365 Users' + FieldType = 'Layouts' + } + [PSCustomObject]@{ + FieldName = 'Devices' + FieldLabel = 'Asset Layout for M365 Devices' + FieldType = 'Layouts' + } + [PSCustomObject]@{ + FieldName = 'Licenses' + FieldLabel = 'Asset Layout for M365 Licenses' + FieldType = 'Layouts' + } + ) + + + $Tenants = Get-Tenants -IncludeErrors + $Table = Get-CIPPTable -TableName Extensionsconfig + try { + $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).Hudu + Connect-HuduAPI -configuration $Configuration + + $AssetLayouts = Get-HuduAssetLayouts | Select-Object @{Name = 'FieldType' ; Expression = { 'Layouts' } }, @{Name = 'value'; Expression = { $_.id } }, name, fields + } catch { + $Message = if ($_.ErrorDetails.Message) { + Get-NormalizedError -Message $_.ErrorDetails.Message + } else { + $_.Exception.message + } + + Write-LogMessage -Message "Could not get Hudu Companies, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $HuduCompanies = @(@{name = "Could not get Hudu Companies, error: $Message"; value = '-1' }) + } + + $Unset = [PSCustomObject]@{ + name = '--- Do not synchronize ---' + value = $null + type = 'unset' + } + + $MappingObj = [PSCustomObject]@{ + CIPPFields = $CIPPFields + CIPPFieldHeaders = $CIPPFieldHeaders + IntegrationFields = @($Unset) + @($AssetLayouts) + Mappings = $Mappings + } + + return $MappingObj + +} \ No newline at end of file diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 index cff35483ca7a..a8d775168cf7 100644 --- a/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduMapping.ps1 @@ -3,13 +3,9 @@ function Get-HuduMapping { param ( $CIPPMapping ) - #Get available mappings - $Mappings = [pscustomobject]@{} - $Filter = "PartitionKey eq 'HuduMapping'" - Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.HuduCompany)"; value = "$($_.HuduCompanyId)" } - } + $Mappings = Get-ExtensionMapping -Extension 'Hudu' + $Tenants = Get-Tenants -IncludeErrors $Table = Get-CIPPTable -TableName Extensionsconfig try { @@ -35,9 +31,9 @@ function Get-HuduMapping { } } $MappingObj = [PSCustomObject]@{ - Tenants = @($Tenants) - HuduCompanies = @($HuduCompanies) - Mappings = $Mappings + Tenants = @($Tenants) + Companies = @($HuduCompanies) + Mappings = $Mappings } return $MappingObj diff --git a/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 index a7ad9f8172b3..03c6dddb8fb3 100644 --- a/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Set-HuduMapping.ps1 @@ -10,10 +10,10 @@ function Set-HuduMapping { } foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'Mapping' + PartitionKey = 'HuduMapping' RowKey = "$($mapping.name)" - 'HuduCompanyId' = "$($mapping.value.value)" - 'HuduCompany' = "$($mapping.value.label)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-CIPPAzDataTableEntity @CIPPMapping -Entity $AddObject -Force diff --git a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 index 827347a613d2..21f5acf1923e 100644 --- a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 +++ b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 @@ -11,18 +11,18 @@ function New-CippExtAlert { $MappingFile = (Get-CIPPAzDataTableEntity @MappingTable) foreach ($ConfigItem in $Configuration.psobject.properties.name) { switch ($ConfigItem) { - "HaloPSA" { + 'HaloPSA' { If ($Configuration.HaloPSA.enabled) { $TenantId = (Get-Tenants | Where-Object defaultDomainName -EQ $Alert.TenantId).customerId Write-Host "TenantId: $TenantId" - $MappedId = ($MappingFile | Where-Object RowKey -EQ $TenantId).HaloPSA + $MappedId = ($MappingFile | Where-Object { $_.PartitionKey -eq 'HaloMapping' -and $_.RowKey -eq $TenantId }).IntegrationId Write-Host "MappedId: $MappedId" if (!$mappedId) { $MappedId = 1 } Write-Host "MappedId: $MappedId" - New-HaloPSATicket -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $mappedId + New-HaloPSATicket -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $mappedId } } - "Gradient" { + 'Gradient' { If ($Configuration.Gradient.enabled) { New-GradientAlert -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $Alert.TenantId } diff --git a/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 index 8be773c2d030..e88f53ceba9c 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneFieldMapping.ps1 @@ -7,93 +7,109 @@ function Get-NinjaOneFieldMapping { #Get available mappings $Mappings = [pscustomobject]@{} - [System.Collections.Generic.List[PSCustomObject]]$CIPPFields = @( + [System.Collections.Generic.List[object]]$CIPPFieldHeaders = @( [PSCustomObject]@{ - InternalName = 'TenantLinks' - Description = 'Microsoft 365 Tenant Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' - Scope = 'Organization' - Type = 'WYSIWYG' + Title = 'NinjaOne Organization Global Custom Field Mapping' + FieldType = 'Organization' + Description = 'Use the table below to map your Organization Field to the correct NinjaOne Field' + } + [PSCustomObject]@{ + Title = 'NinjaOne Device Custom Field Mapping' + FieldType = 'Device' + Description = 'Use the table below to map your Device Field to the correct NinjaOne Field' + } + ) + + [System.Collections.Generic.List[object]]$CIPPFields = @( + [PSCustomObject]@{ + FieldName = 'TenantLinks' + FieldLabel = 'Microsoft 365 Tenant Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' + FieldType = 'Organization' + Type = 'WYSIWYG' }, [PSCustomObject]@{ - InternalName = 'TenantSummary' - Description = 'Microsoft 365 Tenant Summary - Field Used to Display Tenant Summary Information' - Scope = 'Organization' - Type = 'WYSIWYG' + FieldName = 'TenantSummary' + FieldLabel = 'Microsoft 365 Tenant Summary - Field Used to Display Tenant Summary Information' + FieldType = 'Organization' + Type = 'WYSIWYG' }, [PSCustomObject]@{ - InternalName = 'UsersSummary' - Description = 'Microsoft 365 Users Summary - Field Used to Display User Summary Information' - Scope = 'Organization' - Type = 'WYSIWYG' + FieldName = 'UsersSummary' + FieldLabel = 'Microsoft 365 Users Summary - Field Used to Display User Summary Information' + FieldType = 'Organization' + Type = 'WYSIWYG' }, [PSCustomObject]@{ - InternalName = 'DeviceLinks' - Description = 'Microsoft 365 Device Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' - Scope = 'Device' - Type = 'WYSIWYG' + FieldName = 'DeviceLinks' + FieldLabel = 'Microsoft 365 Device Links - Field Used to Display Links to Microsoft 365 Portals and CIPP' + FieldType = 'Device' + Type = 'WYSIWYG' }, [PSCustomObject]@{ - InternalName = 'DeviceSummary' - Description = 'Microsoft 365 Device Summary - Field Used to Display Device Summary Information' - Scope = 'Device' - Type = 'WYSIWYG' + FieldName = 'DeviceSummary' + FieldLabel = 'Microsoft 365 Device Summary - Field Used to Display Device Summary Information' + FieldType = 'Device' + Type = 'WYSIWYG' }, [PSCustomObject]@{ - InternalName = 'DeviceCompliance' - Description = 'Intune Device Compliance Status - Field Used to Monitor Device Compliance' - Scope = 'Device' - Type = 'TEXT' + FieldName = 'DeviceCompliance' + FieldLabel = 'Intune Device Compliance Status - Field Used to Monitor Device Compliance' + FieldType = 'Device' + Type = 'TEXT' } ) - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + $MappingFieldMigrate = Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaFieldMapping'" | ForEach-Object { + [PSCustomObject]@{ + PartitionKey = 'NinjaOneFieldMapping' + RowKey = $_.RowKey + IntegrationId = $_.NinjaOne + IntegrationName = $_.NinjaOneName + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ + } + if (($MappingFieldMigrate | Measure-Object).count -gt 0) { + Add-CIPPAzDataTableEntity @CIPPMapping -Entity $MappingFieldMigrate -Force } + $Mappings = Get-ExtensionMapping -Extension 'NinjaOneField' $Table = Get-CIPPTable -TableName Extensionsconfig $Configuration = ((Get-AzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).NinjaOne - - $Token = Get-NinjaOneToken -configuration $Configuration - - $NinjaCustomFieldsNodeRaw = (Invoke-WebRequest -uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=node" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -depth 100 - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = $NinjaCustomFieldsNodeRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type - - $NinjaCustomFieldsOrgRaw = (Invoke-WebRequest -uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=organization" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -depth 100 - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = $NinjaCustomFieldsOrgRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type - - if ($Null -eq $NinjaCustomFieldsNode){ - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = @() + + $NinjaCustomFieldsNodeRaw = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=node" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 + + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = $NinjaCustomFieldsNodeRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type, @{n = 'FieldType'; e = { 'Device' } } + + $NinjaCustomFieldsOrgRaw = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device-custom-fields?scopes=organization" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 + + [System.Collections.Generic.List[object]]$NinjaCustomFieldsOrg = $NinjaCustomFieldsOrgRaw | Where-Object { $_.apiPermission -eq 'READ_WRITE' -and $_.type -in $CIPPFields.Type } | Select-Object @{n = 'name'; e = { $_.label } }, @{n = 'value'; e = { $_.name } }, type, @{n = 'FieldType'; e = { 'Organization' } } + + if ($Null -eq $NinjaCustomFieldsNode) { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = @() } - - if ($Null -eq $NinjaCustomFieldsOrg){ - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = @() + + if ($Null -eq $NinjaCustomFieldsOrg) { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsOrg = @() + } + $Unset = [PSCustomObject]@{ + name = '--- Do not synchronize ---' + value = $null + type = 'unset' } - - } catch { - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsNode = @() - [System.Collections.Generic.List[PSCustomObject]]$NinjaCustomFieldsOrg = @() - } - $DoNotSync = [PSCustomObject]@{ - name = '--- Do not synchronize ---' - value = $null - type = 'unset' + } catch { + [System.Collections.Generic.List[object]]$NinjaCustomFieldsNode = @() + [System.Collections.Generic.List[objecgt]]$NinjaCustomFieldsOrg = @() } - $NinjaCustomFieldsOrg.Insert(0, $DoNotSync) - $NinjaCustomFieldsNode.Insert(0, $DoNotSync) - - $MappingObj = [PSCustomObject]@{ - CIPPOrgFields = $CIPPFields | Where-Object { $_.Scope -eq 'Organization' } - CIPPNodeFields = @($CIPPFields | Where-Object { $_.Scope -eq 'Device' }) - NinjaOrgFields = @($NinjaCustomFieldsOrg) - NinjaNodeFields = @($NinjaCustomFieldsNode) - Mappings = $Mappings + CIPPFields = $CIPPFields + CIPPFieldHeaders = $CIPPFieldHeaders + IntegrationFields = @($Unset) + @($NinjaCustomFieldsOrg) + @($NinjaCustomFieldsNode) + Mappings = $Mappings } return $MappingObj diff --git a/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 index d2d914c89589..24c7e6405560 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Get-NinjaOneOrgMapping.ps1 @@ -4,14 +4,25 @@ function Get-NinjaOneOrgMapping { $CIPPMapping ) try { - #Get available mappings - $Mappings = [pscustomobject]@{} $Tenants = Get-Tenants -IncludeErrors $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + $MigrateRows = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { + #$Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + [PSCustomObject]@{ + RowKey = $_.RowKey + IntegrationName = $_.NinjaOneName + IntegrationId = $_.NinjaOne + PartitionKey = 'NinjaOneMapping' + } + Remove-AzDataTableEntity @CIPPMapping -Entity $_ } + + if (($MigrateRows | Measure-Object).Count -gt 0) { + Add-AzDataTableEntity @CIPPMapping -Entity $MigrateRows -Force + } + + $Mappings = Get-ExtensionMapping -Extension 'NinjaOne' #Get Available Tenants #Get available Ninja clients @@ -43,7 +54,7 @@ function Get-NinjaOneOrgMapping { $MappingObj = [PSCustomObject]@{ Tenants = @($Tenants) - NinjaOrgs = @($NinjaOrgs | Sort-Object name) + Companies = @($NinjaOrgs | Sort-Object name) Mappings = $Mappings } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 index a8363917efcc..9213a7015b1a 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneDeviceWebhook.ps1 @@ -8,9 +8,9 @@ function Invoke-NinjaOneDeviceWebhook { Write-LogMessage -user $ExecutingUser -API $APIName -message "Webhook Recieved - Updating NinjaOne Device compliance for $($Data.resourceData.id) in $($Data.tenantId)" -Sev 'Info' -tenant $TenantFilter $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } | ForEach-Object { - $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.NinjaOne) + $Filter = "PartitionKey eq 'NinjaOneFieldMapping'" + Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } | ForEach-Object { + $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.IntegrationId) } if ($MappedFields.DeviceCompliance) { @@ -18,14 +18,14 @@ function Invoke-NinjaOneDeviceWebhook { $M365DeviceID = $Data.resourceData.id $DeviceM365 = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/devices/$($M365DeviceID)" -Tenantid $tenantfilter - + $DeviceFilter = "PartitionKey eq '$($tenantfilter)' and RowKey eq '$($DeviceM365.deviceID)'" $DeviceMapTable = Get-CippTable -tablename 'NinjaOneDeviceMap' $Device = Get-CIPPAzDataTableEntity @DeviceMapTable -Filter $DeviceFilter - + if (($Device | Measure-Object).count -eq 1) { - $Token = Get-NinjaOneToken -configuration $Configuration - + $Token = Get-NinjaOneToken -configuration $Configuration + if ($DeviceM365.isCompliant -eq $True) { $Compliant = 'Compliant' } else { @@ -37,16 +37,16 @@ function Invoke-NinjaOneDeviceWebhook { } | ConvertTo-Json $Null = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/device/$($Device.NinjaOneID)/custom-fields" -Method PATCH -Body $ComplianceBody -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json' - + Write-Host 'Updated NinjaOne Device Compliance' - + } else { Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "$($DeviceM365.displayName) ($($M365DeviceID)) was not matched in Ninja for $($tenantfilter)" -Sev 'Info' } } - + } catch { $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message @@ -56,7 +56,7 @@ function Invoke-NinjaOneDeviceWebhook { Write-Error "Failed NinjaOne Device Webhook for: $($Data | ConvertTo-Json -Depth 100) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" Write-LogMessage -API 'NinjaOneSync' -user 'CIPP' -message "Failed NinjaOne Device Webhook Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" -Sev 'Error' } - - + + } \ No newline at end of file diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 index 1faf7d92c833..ca69e5b10935 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneExtensionScheduler.ps1 @@ -26,8 +26,8 @@ function Invoke-NinjaOneExtensionScheduler { Write-Host "Current Interval: $CurrentInterval" $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } if ($Null -eq $LastRunTime -or $LastRunTime -le (Get-Date).addhours(-25) -or $TimeSetting -eq $CurrentInterval) { Write-Host 'Executing' diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 index 6ea239b73e36..6b5687d6059f 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMapping.ps1 @@ -9,9 +9,9 @@ function Invoke-NinjaOneOrgMapping { #Get available mappings $Mappings = [pscustomobject]@{} - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" + $Filter = "PartitionKey eq 'NinjaOneMapping'" Get-AzDataTableEntity @CIPPMapping -Filter $Filter | ForEach-Object { - $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.NinjaOneName)"; value = "$($_.NinjaOne)" } + $Mappings | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue @{ label = "$($_.IntegrationName)"; value = "$($_.IntegrationId)" } } #Get Available Tenants @@ -81,10 +81,10 @@ function Invoke-NinjaOneOrgMapping { $MatchedM365Tenants.add($Tenant) $MatchedNinjaOrgs.add($MatchedOrg) $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($Tenant.customerId)" - 'NinjaOne' = "$($MatchedOrg.id)" - 'NinjaOneName' = "$($MatchedOrg.name)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($Tenant.customerId)" + IntegrationId = "$($MatchedOrg.id)" + IntegrationName = "$($MatchedOrg.name)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Organization name match for $($Tenant.customerId). to $($($MatchedOrg.name))" -Sev 'Info' diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 index 317770c3bb78..c3f05acf1cc3 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneOrgMappingTenant.ps1 @@ -14,7 +14,7 @@ function Invoke-NinjaOneOrgMappingTenant { $TenantFilter = $Tenant.customerId - $M365DevicesRaw = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices" -Tenantid $tenantfilter + $M365DevicesRaw = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/managedDevices' -Tenantid $tenantfilter $M365Devices = foreach ($Device in $M365DevicesRaw) { [pscustomobject]@{ @@ -28,10 +28,10 @@ function Invoke-NinjaOneOrgMappingTenant { [System.Collections.Generic.List[PSCustomObject]]$MatchedDevices = @() # Match devices on serial - $DevicesToMatchSerial = $M365Devices | where-object { $null -ne $_.DeviceSerial } + $DevicesToMatchSerial = $M365Devices | Where-Object { $null -ne $_.DeviceSerial } foreach ($SerialMatchDevice in $DevicesToMatchSerial) { - $MatchedDevice = $NinjaDevices | where-object { $_.Serial -eq $SerialMatchDevice.DeviceSerial -or $_.BiosSerialNumber -eq $SerialMatchDevice.DeviceSerial } - if (($MatchedDevice | measure-object).count -eq 1) { + $MatchedDevice = $NinjaDevices | Where-Object { $_.Serial -eq $SerialMatchDevice.DeviceSerial -or $_.BiosSerialNumber -eq $SerialMatchDevice.DeviceSerial } + if (($MatchedDevice | Measure-Object).count -eq 1) { $Match = [pscustomobject]@{ M365 = $SerialMatchDevice Ninja = $MatchedDevice @@ -41,10 +41,10 @@ function Invoke-NinjaOneOrgMappingTenant { } # Try to match on Name - $DevicesToMatchName = $M365Devices | where-object { $_ -notin $MatchedDevices.M365 } + $DevicesToMatchName = $M365Devices | Where-Object { $_ -notin $MatchedDevices.M365 } foreach ($NameMatchDevice in $DevicesToMatchName) { - $MatchedDevice = $NinjaDevices | where-object { $_.SystemName -eq $NameMatchDevice.DeviceName -or $_.DNSName -eq $NameMatchDevice.DeviceName } - if (($MatchedDevice | measure-object).count -eq 1) { + $MatchedDevice = $NinjaDevices | Where-Object { $_.SystemName -eq $NameMatchDevice.DeviceName -or $_.DNSName -eq $NameMatchDevice.DeviceName } + if (($MatchedDevice | Measure-Object).count -eq 1) { $Match = [pscustomobject]@{ M365 = $NameMatchDevice Ninja = $MatchedDevice @@ -56,17 +56,17 @@ function Invoke-NinjaOneOrgMappingTenant { # Match on the Org with the most devices that match if (($MatchedDevices.Ninja.ID | Measure-Object).Count -eq 1) { - $MatchedOrgID = ($MatchedDevices.Ninja | group-object OrgID | sort-object Count -desc)[0].name + $MatchedOrgID = ($MatchedDevices.Ninja | Group-Object OrgID | Sort-Object Count -desc)[0].name $MatchedOrg = $NinjaOrgs | Where-Object { $_.id -eq $MatchedOrgID } $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($Tenant.customerId)" - 'NinjaOne' = "$($MatchedOrg.id)" - 'NinjaOneName' = "$($MatchedOrg.name)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($Tenant.customerId)" + IntegrationId = "$($MatchedOrg.id)" + IntegrationName = "$($MatchedOrg.name)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Device match for $($Tenant.displayName) to $($($MatchedOrg.name))" -Sev 'Info' + Write-LogMessage -API 'NinjaOneAutoMap_Queue' -user 'CIPP' -message "Added mapping from Device match for $($Tenant.displayName) to $($($MatchedOrg.name))" -Sev 'Info' } diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 index 5567ddb7c1b8..c6fb732eb30a 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneSync.ps1 @@ -3,8 +3,8 @@ function Invoke-NinjaOneSync { $Table = Get-CIPPTable -TableName NinjaOneSettings $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaOrgsMapping'" - $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } + $Filter = "PartitionKey eq 'NinjaOneMapping'" + $TenantsToProcess = Get-AzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } $Batch = foreach ($Tenant in $TenantsToProcess) { diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index 9828682c6348..f58cec70b4aa 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -6,13 +6,13 @@ function Invoke-NinjaOneTenantSync { try { $StartQueueTime = Get-Date Write-Host "$(Get-Date) - Starting NinjaOne Sync" - - # Stagger start + + # Stagger start # Check Global Rate Limiting - $CurrentMap = Get-ExtensionRateLimit -ExtensionName 'NinjaOne' -ExtensionPartitionKey 'NinjaOrgsMapping' -RateLimit 5 -WaitTime 10 + $CurrentMap = Get-ExtensionRateLimit -ExtensionName 'NinjaOne' -ExtensionPartitionKey 'NinjaOneMapping' -RateLimit 5 -WaitTime 10 $StartTime = Get-Date - + # Parse out the Tenant we are processing $MappedTenant = $QueueItem.MappedTenant @@ -21,7 +21,7 @@ function Invoke-NinjaOneTenantSync { $StartDate = try { Get-Date($CurrentItem.lastStartTime) } catch { $Null } $EndDate = try { Get-Date($CurrentItem.lastEndTime) } catch { $Null } - + if (($null -ne $CurrentItem.lastStartTime) -and ($StartDate -gt (Get-Date).AddMinutes(-10)) -and ( $Null -eq $CurrentItem.lastEndTime -or ($StartDate -gt $EndDate))) { Throw "NinjaOne Sync for Tenant $($MappedTenant.RowKey) is still running, please wait 10 minutes and try again." } @@ -40,19 +40,19 @@ function Invoke-NinjaOneTenantSync { $Table = Get-CIPPTable -TableName NinjaOneSettings $NinjaSettings = (Get-CIPPAzDataTableEntity @Table) $CIPPUrl = ($NinjaSettings | Where-Object { $_.RowKey -eq 'CIPPURL' }).SettingValue - - + + $Customer = Get-Tenants | Where-Object { $_.customerId -eq $MappedTenant.RowKey } Write-Host "Processing: $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" - Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' + Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' if (($Customer | Measure-Object).count -ne 1) { Throw "Unable to match the recieved ID to a tenant QueueItem: $($QueueItem | ConvertTo-Json -Depth 100 | Out-String) Matched Customer: $($Customer| ConvertTo-Json -Depth 100 | Out-String)" } $TenantFilter = $Customer.defaultDomainName - $NinjaOneOrg = $MappedTenant.NinjaOne + $NinjaOneOrg = $MappedTenant.IntegrationId # Get the NinjaOne general extension settings. @@ -62,9 +62,9 @@ function Invoke-NinjaOneTenantSync { # Pull the list of field Mappings so we know which fields to render. $MappedFields = [pscustomobject]@{} $CIPPMapping = Get-CIPPTable -TableName CippMapping - $Filter = "PartitionKey eq 'NinjaFieldMapping'" - Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.NinjaOne -and $_.NinjaOne -ne '' } | ForEach-Object { - $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.NinjaOne) + $Filter = "PartitionKey eq 'NinjaOneFieldMapping'" + Get-CIPPAzDataTableEntity @CIPPMapping -Filter $Filter | Where-Object { $Null -ne $_.IntegrationId -and $_.IntegrationId -ne '' } | ForEach-Object { + $MappedFields | Add-Member -NotePropertyName $_.RowKey -NotePropertyValue $($_.IntegrationId) } # Get NinjaOne Devices @@ -76,14 +76,14 @@ function Invoke-NinjaOneTenantSync { $Result $ResultCount = ($Result.id | Measure-Object -Maximum) $After = $ResultCount.maximum - + } while ($ResultCount.count -eq $PageSize) Write-Host 'Fetched NinjaOne Devices' - + [System.Collections.Generic.List[PSCustomObject]]$NinjaOneUserDocs = @() - if ($Configuration.UserDocumentsEnabled -eq $True) { + if ($Configuration.UserDocumentsEnabled -eq $True) { # Get NinjaOne User Documents $UserDocTemplate = [PSCustomObject]@{ name = 'CIPP - Microsoft 365 Users' @@ -169,7 +169,7 @@ function Invoke-NinjaOneTenantSync { # Get NinjaOne Users [System.Collections.Generic.List[PSCustomObject]]$NinjaOneUserDocs = ((Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents?organizationIds=$($NinjaOneOrg)&templateIds=$($NinjaOneUsersTemplate.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100) - + foreach ($NinjaDoc in $NinjaOneUserDocs) { $ParsedFields = [pscustomobject]@{} foreach ($Field in $NinjaDoc.Fields) { @@ -185,7 +185,7 @@ function Invoke-NinjaOneTenantSync { Write-Host 'Fetched NinjaOne User Docs' } - + [System.Collections.Generic.List[PSCustomObject]]$NinjaOneLicenseDocs = @() if ($Configuration.LicenseDocumentsEnabled) { # NinjaOne License Documents @@ -236,10 +236,10 @@ function Invoke-NinjaOneTenantSync { } $NinjaOneLicenseTemplate = Invoke-NinjaOneDocumentTemplate -Template $LicenseDocTemplate -Token $Token - + # Get NinjaOne Licenses [System.Collections.Generic.List[PSCustomObject]]$NinjaOneLicenseDocs = ((Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents?organizationIds=$($NinjaOneOrg)&templateIds=$($NinjaOneLicenseTemplate.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100) - + foreach ($NinjaLic in $NinjaOneLicenseDocs) { $ParsedFields = [pscustomobject]@{} foreach ($Field in $NinjaLic.Fields) { @@ -328,8 +328,8 @@ function Invoke-NinjaOneTenantSync { id = 'Subscriptions' method = 'GET' url = '/directory/subscriptions' - } - + } + ) Write-Verbose "$(Get-Date) - Fetching Bulk Data" @@ -346,14 +346,14 @@ function Invoke-NinjaOneTenantSync { $SecureScore = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'SecureScore' $Subscriptions = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Subscriptions' - + [System.Collections.Generic.List[PSCustomObject]]$SecureScoreProfiles = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'SecureScoreControlProfiles' $CurrentSecureScore = ($SecureScore | Sort-Object createDateTime -Descending | Select-Object -First 1) $MaxSecureScoreRank = ($SecureScoreProfiles.rank | Measure-Object -Maximum).maximum $MaxSecureScore = $CurrentSecureScore.maxScore - + [System.Collections.Generic.List[PSCustomObject]]$SecureScoreParsed = Foreach ($Score in $CurrentSecureScore.controlScores) { $MatchedProfile = $SecureScoreProfiles | Where-Object { $_.id -eq $Score.controlName } [PSCustomObject]@{ @@ -368,20 +368,20 @@ function Invoke-NinjaOneTenantSync { maxScore = $MatchedProfile.maxScore rank = $MatchedProfile.rank adjustedRank = $MaxSecureScoreRank - $MatchedProfile.rank - + } } $TenantDetails = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'TenantDetails' Write-Verbose "$(Get-Date) - Parsing Users" - # Grab licensed users - $licensedUsers = $Users | Where-Object { $null -ne $_.AssignedLicenses.SkuId } | Sort-Object UserPrincipalName - - Write-Verbose "$(Get-Date) - Parsing Roles" + # Grab licensed users + $licensedUsers = $Users | Where-Object { $null -ne $_.AssignedLicenses.SkuId } | Sort-Object UserPrincipalName + + Write-Verbose "$(Get-Date) - Parsing Roles" # Get All Roles $AllRoles = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'AllRoles' - + $SelectList = 'id', 'displayName', 'userPrincipalName' [System.Collections.Generic.List[PSCustomObject]]$RolesRequestArray = @() @@ -410,11 +410,11 @@ function Invoke-NinjaOneTenantSync { ParsedMembers = $Result.body.value.Displayname -join ', ' } } - + $AdminUsers = (($Roles | Where-Object { $_.Displayname -match 'Administrator' }).Members | Where-Object { $null -ne $_.displayName }) - + Write-Verbose "$(Get-Date) - Fetching Domains" try { $RawDomains = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'RawDomains' @@ -422,8 +422,8 @@ function Invoke-NinjaOneTenantSync { $RawDomains = $null } $customerDomains = ($RawDomains | Where-Object { $_.IsVerified -eq $True }).id -join ', ' | Out-String - - + + Write-Verbose "$(Get-Date) - Parsing Licenses" # Get Licenses $Licenses = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Licenses' @@ -432,7 +432,7 @@ function Invoke-NinjaOneTenantSync { if ($Licenses) { $LicensesParsed = $Licenses | Where-Object { $_.PrepaidUnits.Enabled -gt 0 } | Select-Object @{N = 'License Name'; E = { (Get-Culture).TextInfo.ToTitleCase((convert-skuname -skuname $_.SkuPartNumber).Tolower()) } }, @{N = 'Active'; E = { $_.PrepaidUnits.Enabled } }, @{N = 'Consumed'; E = { $_.ConsumedUnits } }, @{N = 'Unused'; E = { $_.PrepaidUnits.Enabled - $_.ConsumedUnits } } } - + Write-Verbose "$(Get-Date) - Parsing Devices" # Get all devices from Intune $devices = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Devices' @@ -440,7 +440,7 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Parsing Device Compliance Polcies" # Fetch Compliance Policy Status $DeviceCompliancePolicies = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'DeviceCompliancePolicies' - + # Get the status of each device for each policy [System.Collections.Generic.List[PSCustomObject]]$PolicyRequestArray = @() foreach ($CompliancePolicy in $DeviceCompliancePolicies) { @@ -466,9 +466,9 @@ function Invoke-NinjaOneTenantSync { DeviceStatuses = $Result.body.value } } - + Write-Verbose "$(Get-Date) - Parsing Groups" - # Fetch Groups + # Fetch Groups $AllGroups = Get-GraphBulkResultByID -value -Results $TenantResults -ID 'Groups' # Fetch the App status for each device @@ -492,7 +492,7 @@ function Invoke-NinjaOneTenantSync { $Groups = foreach ($Result in $GroupMembersReturn) { [pscustomobject]@{ ID = $Result.id - DisplayName = ($AllGroups | Where-Object { $_.id -eq $Result.id }).DisplayName + DisplayName = ($AllGroups | Where-Object { $_.id -eq $Result.id }).DisplayName Members = $result.body.value } } @@ -555,10 +555,10 @@ function Invoke-NinjaOneTenantSync { Members = $CAMembers } } - + Write-Verbose "$(Get-Date) - Fetching One Drive Details" try { - $OneDriveDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + $OneDriveDetails = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/reports/getOneDriveUsageAccountDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv } catch { Write-Error "Failed to fetch Onedrive Details: $_" $OneDriveDetails = $null @@ -571,7 +571,7 @@ function Invoke-NinjaOneTenantSync { Write-Error "Failed to fetch CAS Details: $_" $CASFull = $null } - + Write-Verbose "$(Get-Date) - Fetching Mailbox Details" try { $MailboxDetailedFull = New-ExoRequest -TenantID $Customer.defaultDomainName -cmdlet 'Get-Mailbox' @@ -590,12 +590,12 @@ function Invoke-NinjaOneTenantSync { Write-Verbose "$(Get-Date) - Fetching Mailbox Stats" try { - $MailboxStatsFull = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv + $MailboxStatsFull = New-GraphGetRequest -uri "https://graph.microsoft.com/v1.0/reports/getMailboxUsageDetail(period='D7')" -tenantid $TenantFilter | ConvertFrom-Csv } catch { Write-Error "Failed to fetch Mailbox Stats: $_" $MailboxStatsFull = $null } - + Write-Host 'Fetched M365 Additional Data' @@ -604,7 +604,7 @@ function Invoke-NinjaOneTenantSync { ############################ Format and Synchronize to NinjaOne ############################ $DeviceTable = Get-CippTable -tablename 'CacheNinjaOneParsedDevices' $DeviceMapTable = Get-CippTable -tablename 'NinjaOneDeviceMap' - + $DeviceFilter = "PartitionKey eq '$($Customer.CustomerId)'" [System.Collections.Generic.List[PSCustomObject]]$RawParsedDevices = Get-CIPPAzDataTableEntity @DeviceTable -Filter $DeviceFilter @@ -621,13 +621,13 @@ function Invoke-NinjaOneTenantSync { # Parse Devices Foreach ($Device in $Devices | Where-Object { $_.id -notin $ParsedDevices.id }) { - + # First lets match on serial $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.system.biosSerialNumber -eq $Device.SerialNumber -or $_.system.serialNumber -eq $Device.SerialNumber } # See if we found just one device, if not match on name if (($MatchedNinjaDevice | Measure-Object).count -ne 1) { - $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.Name -or $_.dnsName -eq $Device.Name } + $MatchedNinjaDevice = $NinjaDevices | Where-Object { $_.systemName -eq $Device.Name -or $_.dnsName -eq $Device.Name } } # Check on a match again and set name @@ -658,8 +658,8 @@ function Invoke-NinjaOneTenantSync { Add-CIPPAzDataTableEntity @DeviceMapTable -Entity $MappedDevice -Force } - - + + Foreach ($DeviceUser in $Device.usersloggedon) { $FoundUser = ($Users | Where-Object { $_.id -eq $DeviceUser.userid }) @@ -690,7 +690,7 @@ function Invoke-NinjaOneTenantSync { }) } } - + } } @@ -743,7 +743,7 @@ function Invoke-NinjaOneTenantSync { } -Force $ParsedDevices.add($ParsedDevice) - + ### Update NinjaOne Device Fields if ($MatchedNinjaDevice) { $NinjaDeviceUpdate = [PSCustomObject]@{} @@ -767,7 +767,7 @@ function Invoke-NinjaOneTenantSync { ) - + $DeviceLinksHTML = Get-NinjaOneLinks -Data $DeviceLinksData -SmallCols 2 -MedCols 3 -LargeCols 3 -XLCols 3 $DeviceLinksHtml = '
' + $DeviceLinksHTML + '
' @@ -778,7 +778,7 @@ function Invoke-NinjaOneTenantSync { } if ($MappedFields.DeviceSummary) { - + # Set Compliance Status if ($Device.complianceState -eq 'compliant') { $Compliance = '   Compliant' @@ -795,9 +795,9 @@ function Invoke-NinjaOneTenantSync { 'Enrolled' = $Device.enrolledDateTime 'Last Checkin' = $Device.lastSyncDateTime 'Compliant' = $Compliance - 'Management Type' = $Device.managementAgent + 'Management Type' = $Device.managementAgent } - + $DeviceDetailsCard = Get-NinjaOneInfoCard -Title 'Device Details' -Data $DeviceDetailsData -Icon 'fas fa-laptop' # Device Hardware @@ -808,8 +808,8 @@ function Invoke-NinjaOneTenantSync { 'Chassis' = $Device.chassisType 'Model' = $Device.model 'Manufacturer' = $Device.manufacturer - } - + } + $DeviceHardwareCard = Get-NinjaOneInfoCard -Title 'Device Details' -Data $DeviceHardwareData -Icon 'fas fa-microchip' # Device Enrollment @@ -821,8 +821,8 @@ function Invoke-NinjaOneTenantSync { 'Device Guard Requirements' = $Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityHardwareRequirementState 'Virtualistation Based Security' = $Device.hardwareinformation.deviceGuardVirtualizationBasedSecurityState 'Credential Guard' = $Device.hardwareinformation.deviceGuardLocalSystemAuthorityCredentialGuardState - } - + } + $DeviceEnrollmentCard = Get-NinjaOneInfoCard -Title 'Device Enrollment' -Data $DeviceEnrollmentData -Icon 'fas fa-table-list' @@ -831,7 +831,7 @@ function Invoke-NinjaOneTenantSync { $DevicePoliciesHTML = ([System.Web.HttpUtility]::HtmlDecode($DevicePoliciesFormatted) -replace '', '') -replace '', '' $TitleLink = "https://intune.microsoft.com/$($Customer.defaultDomainName)/#view/Microsoft_Intune_Devices/DeviceSettingsMenuBlade/~/compliance/mdmDeviceId/$($Device.id)/primaryUserId/" $DeviceCompliancePoliciesCard = Get-NinjaOneCard -Title 'Device Compliance Policies' -Body $DevicePoliciesHTML -Icon 'fas fa-list-check' -TitleLink $TitleLink - + # Device Groups $DeviceGroupsTable = foreach ($Group in $Groups) { if ($device.azureADDeviceId -in $Group.members.deviceId) { @@ -844,16 +844,16 @@ function Invoke-NinjaOneTenantSync { $DeviceGroupsHTML = ([System.Web.HttpUtility]::HtmlDecode($DeviceGroupsFormatted) -replace '', '') -replace '', '' $DeviceGroupsCard = Get-NinjaOneCard -Title 'Device Groups' -Body $DeviceGroupsHTML -Icon 'fas fa-layer-group' - $DeviceSummaryHTML = '
' + - '
' + $DeviceDetailsCard + + $DeviceSummaryHTML = '
' + + '
' + $DeviceDetailsCard + '
' + $DeviceHardwareCard + - '
' + $DeviceEnrollmentCard + + '
' + $DeviceEnrollmentCard + '
' + $DeviceCompliancePoliciesCard + '
' + $DeviceGroupsCard + '
' - - $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceSummary -NotePropertyValue @{'html' = $DeviceSummaryHTML } - } + + $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceSummary -NotePropertyValue @{'html' = $DeviceSummaryHTML } + } } if ($MappedFields.DeviceCompliance) { @@ -863,7 +863,7 @@ function Invoke-NinjaOneTenantSync { $Compliant = 'Non-Compliant' } $NinjaDeviceUpdate | Add-Member -NotePropertyName $MappedFields.DeviceCompliance -NotePropertyValue $Compliant - + } # Update Device @@ -888,11 +888,11 @@ function Invoke-NinjaOneTenantSync { $SyncUsers = $Users } - + $UsersTable = Get-CippTable -tablename 'CacheNinjaOneParsedUsers' $UsersUpdateTable = Get-CippTable -tablename 'CacheNinjaOneUsersUpdate' $UsersMapTable = Get-CippTable -tablename 'NinjaOneUserMap' - + $UsersFilter = "PartitionKey eq '$($Customer.CustomerId)'" [System.Collections.Generic.List[PSCustomObject]]$ParsedUsers = Get-CIPPAzDataTableEntity @UsersTable -Filter $UsersFilter @@ -923,7 +923,7 @@ function Invoke-NinjaOneTenantSync { foreach ($user in $SyncUsers | Where-Object { $_.id -notin $ParsedUsers.RowKey }) { try { - + $NinjaOneUser = $NinjaOneUserDocs | Where-Object { $_.ParsedFields.cippUserID -eq $User.ID } if (($NinjaOneUser | Measure-Object).count -gt 1) { Throw 'Multiple Users with the same ID found' @@ -1010,7 +1010,7 @@ function Invoke-NinjaOneTenantSync { $MatchedNinjaDevice = $UserDevice.NinjaDevice $ParsedDeviceName = $UserDevice.DeviceLink - + # Set Last Login Time $LastLoginTime = ($UserDevice.UserDetails | Where-Object { $_.id -eq $User.id }).lastLogin if (!$LastLoginTime) { @@ -1033,7 +1033,7 @@ function Invoke-NinjaOneTenantSync { } '
  • ' + "$ComplianceIcon $OSIcon $($ParsedDeviceName) ($LastLoginTime)
  • " - + } @@ -1048,7 +1048,7 @@ function Invoke-NinjaOneTenantSync { } catch {} }) -join '' - + $UserOneDriveStats = $OneDriveDetails | Where-Object { $_.'Owner Principal Name' -eq $User.userPrincipalName } | Select-Object -First 1 $UserOneDriveUse = $UserOneDriveStats.'Storage Used (Byte)' / 1GB @@ -1083,7 +1083,7 @@ function Invoke-NinjaOneTenantSync { $OneDriveParsed = 'Not Enabled' } - + if ($UserOneDriveStats) { $OneDriveCardData = [PSCustomObject]@{ 'One Drive URL' = '' + ($UserOneDriveStats.'Site URL') + '' @@ -1100,9 +1100,9 @@ function Invoke-NinjaOneTenantSync { $OneDriveCardData = [PSCustomObject]@{ 'One Drive' = 'Disabled' } - } + } + - $UserMailboxStats = $MailboxStatsFull | Where-Object { $_.'User Principal Name' -eq $User.userPrincipalName } | Select-Object -First 1 $UserMailUse = $UserMailboxStats.'Storage Used (Byte)' / 1GB $UserMailTotal = $UserMailboxStats.'Prohibit Send/Receive Quota (Byte)' / 1GB @@ -1243,7 +1243,7 @@ function Invoke-NinjaOneTenantSync {     "@ - + # Return Data for Users Summary Table $ParsedUser = [PSCustomObject]@{ @@ -1264,8 +1264,8 @@ function Invoke-NinjaOneTenantSync { Add-CIPPAzDataTableEntity @UsersTable -Entity $ParsedUser -Force $ParsedUsers.add($ParsedUser) - - + + if ($Configuration.UserDocumentsEnabled -eq $True) { # Format into Ninja HTML @@ -1283,13 +1283,13 @@ function Invoke-NinjaOneTenantSync { $UserPolciesCard = Get-NinjaOneCard -Title 'Assigned Conditional Access Policies' -Body $UserPoliciesFormatted - $UserSummaryHTML = '
    ' + - '
    ' + $UserOverviewCardHTML + + $UserSummaryHTML = '
    ' + + '
    ' + $UserOverviewCardHTML + '
    ' + $MailboxDetailsCardHTML + '
    ' + $MailboxSettingsCardHTML + - '
    ' + $OneDriveCardHTML + - '
    ' + $UserPolciesCard + - '
    ' + $DeviceSummaryCardHTML + + '
    ' + $OneDriveCardHTML + + '
    ' + $UserPolciesCard + + '
    ' + $DeviceSummaryCardHTML + '
    ' @@ -1301,10 +1301,10 @@ function Invoke-NinjaOneTenantSync { @{n = 'State'; e = { $_.Compliance } }, @{n = 'Model'; e = { $_.Model } }, @{n = 'Manufacturer'; e = { $_.Make } } - + $UserDeviceDetailHTML = $UserDeviceDetailsTable | ConvertTo-Html -As Table -Fragment $UserDeviceDetailHTML = ([System.Web.HttpUtility]::HtmlDecode($UserDeviceDetailHTML) -replace '', '') -replace '', '' - + $UserFields = @{ cippUserLinks = @{'html' = $UserLinksHTML } @@ -1361,7 +1361,7 @@ function Invoke-NinjaOneTenantSync { } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 100) { @@ -1385,7 +1385,7 @@ function Invoke-NinjaOneTenantSync { } else { $Field = $UserDoc.fields | Where-Object { $_.name -eq 'cippUserID' } } - + if ($Null -ne $Field.value -and $Field.value -ne '') { $MappedUser = ($UsersMap | Where-Object { $_.M365ID -eq $Field.value }) @@ -1411,15 +1411,15 @@ function Invoke-NinjaOneTenantSync { } - + } } catch { Write-Error "User $($User.UserPrincipalName): A fatal error occured while processing user $_" } - + } - + $CreatedUsers = $Null $UpdatedUsers = $Null @@ -1431,12 +1431,12 @@ function Invoke-NinjaOneTenantSync { Write-Host 'Creating NinjaOne Users' [System.Collections.Generic.List[PSCustomObject]]$CreatedUsers = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/documents" -Method POST -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ("[$($NinjaUserCreation.body -join ',')]") -EA Stop).content | ConvertFrom-Json -Depth 100 Remove-AzDataTableEntity @UsersUpdateTable -Entity $NinjaUserCreation - + } } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Users if (($NinjaUserUpdates | Measure-Object).count -ge 1) { @@ -1450,8 +1450,8 @@ function Invoke-NinjaOneTenantSync { ### Relationship Mapping # Parse out the NinjaOne ID to MS ID - - + + [System.Collections.Generic.List[PSCustomObject]]$UserDocResults = $UpdatedUsers + $CreatedUsers if (($UserDocResults | Where-Object { $Null -ne $_ -and $_ -ne '' } | Measure-Object).count -ge 1) { @@ -1462,7 +1462,7 @@ function Invoke-NinjaOneTenantSync { } else { $Field = $UserDoc.fields | Where-Object { $_.name -eq 'cippUserID' } } - + if ($Null -ne $Field.value -and $Field.value -ne '') { $MappedUser = ($UsersMap | Where-Object { $_.M365ID -eq $Field.value }) @@ -1486,8 +1486,8 @@ function Invoke-NinjaOneTenantSync { } } - - + + # Relate Users to Devices Foreach ($LinkDevice in $ParsedDevices | Where-Object { $null -ne $_.NinjaDevice }) { $RelatedItems = (Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/related-items/with-entity/NODE/$($LinkDevice.NinjaDevice.id)" -Method GET -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json').content | ConvertFrom-Json -Depth 100 @@ -1507,7 +1507,7 @@ function Invoke-NinjaOneTenantSync { } } - + try { # Update Relations @@ -1534,7 +1534,7 @@ function Invoke-NinjaOneTenantSync { $FriendlyLicenseName = $License.SkuPartNumber } - + $LicenseUsers = foreach ($SubUser in $Users) { $MatchedLicense = $SubUser.assignedLicenses | Where-Object { $License.skuId -in $_.skuId } $MatchedPlans = $SubUser.AssignedPlans | Where-Object { $_.servicePlanId -in $License.servicePlans.servicePlanID } @@ -1551,7 +1551,7 @@ function Invoke-NinjaOneTenantSync { 'License Assigned' = $(try { $(Get-Date(($MatchedPlans | Group-Object assignedDateTime | Sort-Object Count -Desc | Select-Object -First 1).name) -Format u) } catch { 'Unknown' }) NinjaUserDocID = $SubRelUserID } - } + } } $LicenseUsersHTML = $LicenseUsers | Select-Object -ExcludeProperty NinjaUserDocID | ConvertTo-Html -As Table -Fragment @@ -1578,12 +1578,12 @@ function Invoke-NinjaOneTenantSync { $LicenseItemsTable = $License.servicePlans | Select-Object @{n = 'Plan Name'; e = { convert-skuname -skuname $_.servicePlanName } }, @{n = 'Applies To'; e = { $_.appliesTo } }, @{n = 'Provisioning Status'; e = { $_.provisioningStatus } } $LicenseItemsHTML = $LicenseItemsTable | ConvertTo-Html -As Table -Fragment $LicenseItemsHTML = ([System.Web.HttpUtility]::HtmlDecode($LicenseItemsHTML) -replace '', '') -replace '', '' - + $LicenseItemsCardHTML = Get-NinjaOneCard -Title 'License Items' -Body $LicenseItemsHTML -Icon 'fas fa-chart-bar' - $LicenseSummaryHTML = '
    ' + - '
    ' + $LicenseOverviewCardHTML + + $LicenseSummaryHTML = '
    ' + + '
    ' + $LicenseOverviewCardHTML + '
    ' + $SubscriptionCardHTML + '
    ' + $LicenseItemsCardHTML + '
    ' @@ -1630,7 +1630,7 @@ function Invoke-NinjaOneTenantSync { } Catch { Write-Host "Bulk Creation Error, but may have been successful as only 1 record with an issue could have been the cause: $_" } - + try { # Update Subscriptions if (($NinjaLicenseUpdates | Measure-Object).count -ge 1) { @@ -1663,7 +1663,7 @@ function Invoke-NinjaOneTenantSync { ) } } - + try { # Update Relations @@ -1750,7 +1750,7 @@ function Invoke-NinjaOneTenantSync { $M365LinksHTML = Get-NinjaOneLinks -Data $ManagementLinksData -Title 'Portals' -SmallCols 2 -MedCols 3 -LargeCols 3 -XLCols 3 $CIPPLinksData = @( - + @{ Name = 'CIPP Tenant Dashboard' Link = "https://$CIPPUrl/home?customerId=$($Customer.CustomerId)" @@ -1802,8 +1802,8 @@ function Invoke-NinjaOneTenantSync { ### Tenant Overview Card $ParsedAdmins = [PSCustomObject]@{} - - $AdminUsers | Select-Object displayname, userPrincipalName -Unique | ForEach-Object { + + $AdminUsers | Select-Object displayname, userPrincipalName -Unique | ForEach-Object { $ParsedAdmins | Add-Member -NotePropertyName $_.displayname -NotePropertyValue $_.userPrincipalName } @@ -1814,7 +1814,7 @@ function Invoke-NinjaOneTenantSync { 'Creation Date' = $TenantDetails.createdDateTime 'Domains' = $customerDomains 'Admin Users' = ($AdminUsers | ForEach-Object { "$($_.DisplayName)" }) -join ', ' - + } $TenantSummaryCard = Get-NinjaOneInfoCard -Title 'Tenant Details' -Data $TenantDetailsItems -Icon 'fas fa-building' @@ -1826,8 +1826,8 @@ function Invoke-NinjaOneTenantSync { $LicensedUsersCount = ($licensedUsers | Measure-Object).count $UnlicensedUsersCount = $TotalUsersCount - $GuestUsersCount - $LicensedUsersCount $UsersEnabledCount = ($Users | Where-Object { $_.accountEnabled -eq $True } | Measure-Object).count - - # Enabled Users + + # Enabled Users $Data = @( @{ @@ -1841,10 +1841,10 @@ function Invoke-NinjaOneTenantSync { Colour = '#D53948' } ) - - + + $UsersEnabledChartHTML = Get-NinjaInLineBarGraph -Title 'User Status' -Data $Data -KeyInLine - + # User Types $Data = @( @@ -1863,8 +1863,8 @@ function Invoke-NinjaOneTenantSync { Amount = $GuestUsersCount Colour = '#8063BF' } - ) - + ) + $UsersTypesChartHTML = Get-NinjaInLineBarGraph -Title 'User Types' -Data $Data -KeyInLine # Create the Users Card @@ -1901,8 +1901,8 @@ function Invoke-NinjaOneTenantSync { Colour = '#D53948' } ) - - + + $DeviceComplianceChartHTML = Get-NinjaInLineBarGraph -Title 'Device Compliance' -Data $Data -KeyInLine # Device OS Types @@ -1928,8 +1928,8 @@ function Invoke-NinjaOneTenantSync { Amount = $IOSCount Colour = '#007AFF' } - ) - + ) + $DeviceOsChartHTML = Get-NinjaInLineBarGraph -Title 'Device Operating Systems' -Data $Data -KeyInLine # Last online time @@ -1946,7 +1946,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' } ) - + $DeviceOnlineChartHTML = Get-NinjaInLineBarGraph -Title 'Devices Online in the last 30 days' -Data $Data -KeyInLine # Create the Devices Card @@ -1974,7 +1974,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' } ) - + $SecureScoreHTML = Get-NinjaInLineBarGraph -Title "Secure Score - $([System.Math]::Round((($CurrentSecureScore.currentScore / $MaxSecureScore) * 100),2))%" -Data $Data -KeyInLine -NoCount -NoSort # Recommended Actions HTML @@ -1995,7 +1995,7 @@ function Invoke-NinjaOneTenantSync { $Table = Get-CippTable -tablename 'standards' - $Filter = "PartitionKey eq 'standards'" + $Filter = "PartitionKey eq 'standards'" $AllStandards = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json -Depth 100 @@ -2025,11 +2025,11 @@ function Invoke-NinjaOneTenantSync { Write-Host 'License Details' $LicenseTableHTML = $LicensesParsed | Sort-Object 'License Name' | ConvertTo-Html -As Table -Fragment $LicenseTableHTML = '
    ' + (([System.Web.HttpUtility]::HtmlDecode($LicenseTableHTML) -replace '', '') -replace '', '') + '
    ' - + $TitleLink = "https://$CIPPUrl/tenant/administration/list-licenses?customerId=$($Customer.customerId)" $LicensesSummaryCardHTML = Get-NinjaOneCard -Title 'Licenses' -Body $LicenseTableHTML -Icon 'fas fa-chart-bar' -TitleLink $TitleLink - + ### Summary Stats Write-Host 'Widget Details' @@ -2056,7 +2056,7 @@ function Invoke-NinjaOneTenantSync { # Colour = $ResultColour # Link = "https://$CIPPUrl/tenant/standards/bpa-report?SearchNow=true&Report=CIPP+Best+Practices+v1.0+-+Tenant+view&tenantFilter=$($Customer.customerId)" # }) - + # Unused Licenses $WidgetData.add([PSCustomObject]@{ Value = $( @@ -2076,15 +2076,15 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://$CIPPUrl/tenant/standards/bpa-report?SearchNow=true&Report=CIPP+Best+Practices+v1.5+-+Tenant+view&tenantFilter=$($Customer.customerId)" }) - - + + # Unified Audit Log $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.UnifiedAuditLog -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2092,14 +2092,14 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://security.microsoft.com/auditlogsearch?viewid=Async%20Search&tid=$($Customer.customerId)" }) - + # Passwords Never Expire $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.PasswordNeverExpires -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2111,10 +2111,10 @@ function Invoke-NinjaOneTenantSync { # oAuth App Consent $WidgetData.add([PSCustomObject]@{ Value = $(if ($BPAData.OAuthAppConsent -eq $True) { - $ResultColour = '#26A644' + $ResultColour = '#26A644' '' } else { - $ResultColour = '#D53948' + $ResultColour = '#D53948' '' } ) @@ -2122,7 +2122,7 @@ function Invoke-NinjaOneTenantSync { Colour = $ResultColour Link = "https://entra.microsoft.com/$($Customer.customerId)/#view/Microsoft_AAD_IAM/ConsentPoliciesMenuBlade/~/UserSettings" }) - + } # Blocked Senders @@ -2146,7 +2146,7 @@ function Invoke-NinjaOneTenantSync { Colour = '#CCCCCC' Link = "https://$CIPPUrl/identity/administration/users?customerId=$($Customer.customerId)" }) - + # Devices $WidgetData.add([PSCustomObject]@{ Value = ($Devices | Measure-Object).count @@ -2211,11 +2211,11 @@ function Invoke-NinjaOneTenantSync { Link = "https://entra.microsoft.com/$($Customer.customerId)/#view/Microsoft_AAD_IAM/DirectoriesADConnectBlade" }) - - + + Write-Host 'Summary Details' $SummaryDetailsCardHTML = Get-NinjaOneWidgetCard -Data $WidgetData -Icon 'fas fa-building' -SmallCols 2 -MedCols 3 -LargeCols 4 -XLCols 6 -NoCard @@ -2223,15 +2223,15 @@ function Invoke-NinjaOneTenantSync { # Create the Tenant Summary Field Write-Host 'Complete Tenant Summary' $TenantSummaryHTML = '
    ' + $SummaryDetailsCardHTML + '
    ' + - '
    ' + - '
    ' + $TenantSummaryCard + + '
    ' + + '
    ' + $TenantSummaryCard + '
    ' + $LicensesSummaryCardHTML + - '
    ' + $DeviceSummaryCardHTML + + '
    ' + $DeviceSummaryCardHTML + '
    ' + $CIPPStandardsSummaryCardHTML + - '
    ' + $SecureScoreSummaryCardHTML + - '
    ' + $UserSummaryCardHTML + + '
    ' + $SecureScoreSummaryCardHTML + + '
    ' + $UserSummaryCardHTML + '
    ' - + $NinjaOrgUpdate | Add-Member -NotePropertyName $MappedFields.TenantSummary -NotePropertyValue @{'html' = $TenantSummaryHTML } @@ -2241,7 +2241,7 @@ function Invoke-NinjaOneTenantSync { if ($MappedFields.UsersSummary) { Write-Host 'User Details Section' - $UsersTableFornatted = $ParsedUsers | Sort-Object name | Select-Object -First 100 Name, + $UsersTableFornatted = $ParsedUsers | Sort-Object name | Select-Object -First 100 Name, @{n = 'User Principal Name'; e = { $_.UPN } }, #Aliases, Licenses, @@ -2250,7 +2250,7 @@ function Invoke-NinjaOneTenantSync { @{n = 'Devices (Last Login)'; e = { $_.Devices } }, Actions - + $UsersTableHTML = $UsersTableFornatted | ConvertTo-Html -As Table -Fragment $UsersTableHTML = ([System.Web.HttpUtility]::HtmlDecode($UsersTableHTML) -replace '', '') -replace '', '' @@ -2270,47 +2270,47 @@ function Invoke-NinjaOneTenantSync { } else { $Overflow = '' } - + $NinjaOrgUpdate | Add-Member -NotePropertyName $MappedFields.UsersSummary -NotePropertyValue @{'html' = $Overflow + $UsersTableHTML } } - + Write-Host 'Posting Details' - + $Token = Get-NinjaOneToken -configuration $Configuration Write-Host "Ninja Body: $($NinjaOrgUpdate | ConvertTo-Json -Depth 100)" - $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.NinjaOne)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) + $Result = Invoke-WebRequest -Uri "https://$($Configuration.Instance)/api/v2/organization/$($MappedTenant.IntegrationId)/custom-fields" -Method PATCH -Headers @{Authorization = "Bearer $($token.access_token)" } -ContentType 'application/json; charset=utf-8' -Body ($NinjaOrgUpdate | ConvertTo-Json -Depth 100) Write-Host 'Cleaning Users Cache' if (($ParsedUsers | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @UsersTable -Entity ($ParsedUsers | Select-Object PartitionKey, RowKey) } - + Write-Host 'Cleaning Device Cache' if (($ParsedDevices | Measure-Object).count -gt 0) { Remove-AzDataTableEntity @DeviceTable -Entity ($ParsedDevices | Select-Object PartitionKey, RowKey) } - + Write-Host "Total Fetch Time: $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds)" - Write-Host "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" + Write-Host "Completed Total Time: $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds)" # Set Last End Time $CurrentItem | Add-Member -NotePropertyName lastEndTime -NotePropertyValue ([string]$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))) -Force $CurrentItem | Add-Member -NotePropertyName lastStatus -NotePropertyValue 'Completed' -Force Add-CIPPAzDataTableEntity @MappingTable -Entity $CurrentItem -Force - Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Completed NinjaOne Sync for $($Customer.displayName). Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds) seconds. Data fetched in $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds) seconds. Total processing time $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds) seconds" -Sev 'info' + Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Completed NinjaOne Sync for $($Customer.displayName). Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds) seconds. Data fetched in $((New-TimeSpan -Start $StartTime -End $FetchEnd).TotalSeconds) seconds. Total processing time $((New-TimeSpan -Start $StartTime -End (Get-Date)).TotalSeconds) seconds" -Sev 'info' } catch { $Message = if ($_.ErrorDetails.Message) { Get-NormalizedError -Message $_.ErrorDetails.Message } else { $_.Exception.message - } + } Write-Error "Failed NinjaOne Processing for $($Customer.displayName) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Failed NinjaOne Processing for $($Customer.displayName) Linenumber: $($_.InvocationInfo.ScriptLineNumber) Error: $Message" -Sev 'Error' $CurrentItem | Add-Member -NotePropertyName lastEndTime -NotePropertyValue ([string]$((Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ'))) -Force diff --git a/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 index 4653d51ed13a..87d243b8cda1 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneFieldMapping.ps1 @@ -6,7 +6,7 @@ function Set-NinjaOneFieldMapping { $Request, $TriggerMetadata ) - + $SettingsTable = Get-CIPPTable -TableName NinjaOneSettings $AddObject = @{ PartitionKey = 'NinjaConfig' @@ -17,15 +17,15 @@ function Set-NinjaOneFieldMapping { foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'NinjaFieldMapping' - RowKey = "$($mapping.name)" - 'NinjaOne' = "$($mapping.value.value)" - 'NinjaOneName' = "$($mapping.value.label)" + PartitionKey = 'NinjaOneFieldMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } - $Result = [pscustomobject]@{'Results' = "Successfully edited mapping table." } + $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } Return $Result } \ No newline at end of file diff --git a/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 index ee09580b94bf..43b1c597e3b0 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Set-NinjaOneOrgMapping.ps1 @@ -6,18 +6,18 @@ function Set-NinjaOneOrgMapping { $Request ) - Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaOrgsMapping'" | ForEach-Object { + Get-CIPPAzDataTableEntity @CIPPMapping -Filter "PartitionKey eq 'NinjaOneMapping'" | ForEach-Object { Remove-AzDataTableEntity @CIPPMapping -Entity $_ } foreach ($Mapping in ([pscustomobject]$Request.body.mappings).psobject.properties) { $AddObject = @{ - PartitionKey = 'NinjaOrgsMapping' - RowKey = "$($mapping.name)" - 'NinjaOne' = "$($mapping.value.value)" - 'NinjaOneName' = "$($mapping.value.label)" + PartitionKey = 'NinjaOneMapping' + RowKey = "$($mapping.name)" + IntegrationId = "$($mapping.value.value)" + IntegrationName = "$($mapping.value.label)" } Add-AzDataTableEntity @CIPPMapping -Entity $AddObject -Force - Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' + Write-LogMessage -API $APINAME -user $request.headers.'x-ms-client-principal' -message "Added mapping for $($mapping.name)." -Sev 'Info' } $Result = [pscustomobject]@{'Results' = 'Successfully edited mapping table.' } From 9681e66e8e6188953eb77c016cf808cf237dd537 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jul 2024 17:12:29 -0400 Subject: [PATCH 10/33] Typos --- Durable_BECRun/run.ps1 | 6 +++--- .../CIPP/Settings/Invoke-ExecRestoreBackup.ps1 | 2 +- .../Administration/Invoke-ExecOffboardTenant.ps1 | 12 ++++++------ .../Administration/Invoke-ExecUpdateSecureScore.ps1 | 2 +- Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Durable_BECRun/run.ps1 b/Durable_BECRun/run.ps1 index 377ca2c5533b..44eecff33d2f 100644 --- a/Durable_BECRun/run.ps1 +++ b/Durable_BECRun/run.ps1 @@ -10,7 +10,7 @@ Write-Host "Working on $UserName" try { $startDate = (Get-Date).AddDays(-7) $endDate = (Get-Date) - $auditLog = (New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AdminAuditLogConfig').UnifiedAuditLogIngestionEnabled + $auditLog = (New-ExoRequest -tenantid $Tenantfilter -cmdlet 'Get-AdminAuditLogConfig').UnifiedAuditLogIngestionEnabled $7dayslog = if ($auditLog -eq $false) { $ExtractResult = 'AuditLog is disabled. Cannot perform full analysis' } else { @@ -40,10 +40,10 @@ try { Write-Host "Retrieved $($logsTenant.count) logs" -ForegroundColor Yellow $logsTenant } while ($LogsTenant.count % 5000 -eq 0 -and $LogsTenant.count -ne 0) - $ExtractResult = 'Succesfully extracted logs from auditlog' + $ExtractResult = 'Successfully extracted logs from auditlog' } Try { - $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" + $URI = "https://graph.microsoft.com/beta/auditLogs/signIns?`$filter=(userId eq '$SuspectUser')&`$top=1&`$orderby=createdDateTime desc" $LastSignIn = New-GraphGetRequest -uri $URI -tenantid $TenantFilter -noPagination $true -verbose | Select-Object @{ Name = 'CreatedDateTime'; Expression = { $(($_.createdDateTime | Out-String) -replace '\r\n') } }, id, @{ Name = 'AppDisplayName'; Expression = { $_.resourceDisplayName } }, 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 b9820352bfdd..476fffa02389 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 @@ -25,7 +25,7 @@ Function Invoke-ExecRestoreBackup { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' $body = [pscustomobject]@{ - 'Results' = 'Succesfully restored backup.' + 'Results' = 'Successfully restored backup.' } } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup: $($_.Exception.Message)" -Sev 'Error' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 index adcb580e45cf..230816f2f58f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecOffboardTenant.ps1 @@ -41,7 +41,7 @@ Function Invoke-ExecOffboardTenant { $BulkResults = New-GraphBulkRequest -Requests $BulkRequests -tenantid $TenantFilter - $results.Add('Succesfully removed guest users') + $results.Add('Successfully removed guest users') Write-LogMessage -user $ExecutingUser -API $APIName -message "CSP Guest users were removed" -Sev "Info" -tenant $TenantFilter } else { $results.Add('No guest users found to remove') @@ -83,7 +83,7 @@ Function Invoke-ExecOffboardTenant { try { New-GraphPostRequest -type PATCH -body $patchContactBody -Uri "https://graph.microsoft.com/v1.0/organization/$($orgContacts.id)" -tenantid $Tenantfilter -ContentType "application/json" - $results.Add("Succesfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split("@")[1] }))") + $results.Add("Successfully removed notification contacts from $($property): $(($propertyContacts | Where-Object { $domains -contains $_.Split("@")[1] }))") Write-LogMessage -user $ExecutingUser -API $APIName -message "Contacts were removed from $($property)" -Sev "Info" -tenant $TenantFilter } catch { $errors.Add("Failed to update property $($property): $($_.Exception.message)") @@ -100,7 +100,7 @@ Function Invoke-ExecOffboardTenant { $request.body.RemoveVendorApps | ForEach-Object { try { $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.value)" -tenantid $Tenantfilter) - $results.Add("Succesfully removed app $($_.label)") + $results.Add("Successfully removed app $($_.label)") Write-LogMessage -user $ExecutingUser -API $APIName -message "App $($_.label) was removed" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to removed app $($_.displayName)") @@ -118,7 +118,7 @@ Function Invoke-ExecOffboardTenant { $sortedArray | ForEach-Object { try { $delete = (New-GraphPostRequest -type 'DELETE' -Uri "https://graph.microsoft.com/v1.0/serviceprincipals/$($_.id)" -tenantid $Tenantfilter) - $results.Add("Succesfully removed app $($_.displayName)") + $results.Add("Successfully removed app $($_.displayName)") Write-LogMessage -user $ExecutingUser -API $APIName -message "App $($_.displayName) was removed" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to removed app $($_.displayName)") @@ -141,7 +141,7 @@ Function Invoke-ExecOffboardTenant { $delegatedAdminRelationships | ForEach-Object { try { $terminate = (New-GraphPostRequest -type 'POST' -Uri "https://graph.microsoft.com/v1.0/tenantRelationships/delegatedAdminRelationships/$($_.id)/requests" -body '{"action":"terminate"}' -ContentType 'application/json' -tenantid $env:TenantID) - $results.Add("Succesfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") + $results.Add("Successfully terminated GDAP relationship $($_.displayName) from tenant $TenantFilter") Write-LogMessage -user $ExecutingUser -API $APIName -message "GDAP Relationship $($_.displayName) has been terminated" -Sev "Info" -tenant $TenantFilter } catch { $($_.Exception.message) @@ -160,7 +160,7 @@ Function Invoke-ExecOffboardTenant { # Terminate contract relationship try { $terminate = (New-GraphPostRequest -type 'PATCH' -body '{ "relationshipToPartner": "none" }' -Uri "https://api.partnercenter.microsoft.com/v1/customers/$TenantFilter" -ContentType 'application/json' -scope 'https://api.partnercenter.microsoft.com/user_impersonation' -tenantid $env:TenantID) - $results.Add('Succesfully terminated contract relationship') + $results.Add('Successfully terminated contract relationship') Write-LogMessage -user $ExecutingUser -API $APIName -message "Contract relationship terminated" -Sev "Info" -tenant $TenantFilter } catch { #$results.Add("Failed to terminate contract relationship: $($_.Exception.message)") diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 index 26bc7332a928..237b5f3415e5 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Invoke-ExecUpdateSecureScore.ps1 @@ -21,7 +21,7 @@ Function Invoke-ExecUpdateSecureScore { } try { $GraphRequest = New-GraphPostRequest -uri "https://graph.microsoft.com/beta/security/secureScoreControlProfiles/$($Request.body.ControlName)" -tenantid $Request.body.TenantFilter -type PATCH -Body $($Body | ConvertTo-Json -Compress) - $Results = [pscustomobject]@{'Results' = "Succesfully set control to $($body.state) " } + $Results = [pscustomobject]@{'Results' = "Successfully set control to $($body.state) " } } catch { $Results = [pscustomobject]@{'Results' = "Failed to set Control to $($body.state) $($_.Exception.Message)" } } diff --git a/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 b/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 index 58231273000e..47d111209e0f 100644 --- a/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPAPIConfig.ps1 @@ -51,7 +51,7 @@ function New-CIPPAPIConfig { Write-Host "writing to Azure" $SetAPIAuth = New-GraphPOSTRequest -type "PUT" -uri "https://management.azure.com/subscriptions/$($subscription)/resourceGroups/$ENV:WEBSITE_RESOURCE_GROUP/providers/Microsoft.Web/sites/$ENV:WEBSITE_SITE_NAME/Config/authsettingsV2?api-version=2018-11-01" -scope "https://management.azure.com/.default" -NoAuthCheck $true -body $currentBody $null = Set-AzKeyVaultSecret -VaultName $ENV:WEBSITE_DEPLOYMENT_ID -Name 'CIPPAPIAPP' -SecretValue (ConvertTo-SecureString -String $APIApp.AppID -AsPlainText -Force) - Write-LogMessage -user $ExecutingUser -API $APINAME -tenant 'None '-message "Succesfully setup CIPP-API Access." -Sev "info" + Write-LogMessage -user $ExecutingUser -API $APINAME -tenant 'None '-message "Successfully setup CIPP-API Access." -Sev "info" } return @{ ApplicationID = $APIApp.AppId From 6921c77a026cfdacc5d3730038c2f5981088d2d7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Wed, 3 Jul 2024 18:52:58 -0400 Subject: [PATCH 11/33] Reset graph error count on successful query --- Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 | 1 + .../Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index 7c4eb8927b35..e5fe77f2e484 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -88,6 +88,7 @@ function New-GraphGetRequest { } } until ([string]::IsNullOrEmpty($NextURL) -or $NextURL -is [object[]] -or ' ' -eq $NextURL) $Tenant.LastGraphError = '' + $Tenant.GraphErrorCount = 0 Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData } else { diff --git a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index f58cec70b4aa..9a3b6949b056 100644 --- a/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/Public/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -42,7 +42,7 @@ function Invoke-NinjaOneTenantSync { $CIPPUrl = ($NinjaSettings | Where-Object { $_.RowKey -eq 'CIPPURL' }).SettingValue - $Customer = Get-Tenants | Where-Object { $_.customerId -eq $MappedTenant.RowKey } + $Customer = Get-Tenants -IncludeErrors | Where-Object { $_.customerId -eq $MappedTenant.RowKey } Write-Host "Processing: $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" Write-LogMessage -API 'NinjaOneSync' -user 'NinjaOneSync' -message "Processing NinjaOne Synchronization for $($Customer.displayName) - Queued for $((New-TimeSpan -Start $StartQueueTime -End $StartTime).TotalSeconds)" -Sev 'Info' From d633ad7517b3ea2487d1778b9a8ba7931c6e5115 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Thu, 4 Jul 2024 23:44:09 +0200 Subject: [PATCH 12/33] added backups --- .../Scheduler/Invoke-ListScheduledItems.ps1 | 5 ++ .../Alerts/Invoke-ListAlertsQueue.ps1 | 2 +- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 52 ++++++++----------- .../CIPPCore/Public/New-CIPPBackupTask.ps1 | 48 +++++++++++++++++ 4 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 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 81231ca9df96..4a3869b56176 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 @@ -19,6 +19,11 @@ Function Invoke-ListScheduledItems { $HiddenTasks = $true } $Tasks = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq 'ScheduledTask'" | Where-Object { $_.Hidden -ne $HiddenTasks } + if ($Request.Query.Type) { + $tasks.Command + $Tasks = $Tasks | Where-Object { $_.command -eq $Request.Query.Type } + } + $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList if ($AllowedTenants -notcontains 'AllTenants') { $Tasks = $Tasks | Where-Object -Property TenantId -In $AllowedTenants diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 index 9b0e5aab48bb..0b90937f4feb 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAlertsQueue.ps1 @@ -20,7 +20,7 @@ Function Invoke-ListAlertsQueue { $WebhookRules = Get-CIPPAzDataTableEntity @WebhookTable $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' - $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true } + $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' } $AllowedTenants = Test-CIPPAccess -Request $Request -TenantList $TenantList = Get-Tenants -IncludeErrors diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 477ea7c2e690..27a2fc9fe629 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -4,6 +4,7 @@ function New-CIPPBackup { $backupType, $StorageOutput = 'default', $TenantFilter, + $ScheduledBackupValues, $APIName = 'CIPP Backup', $ExecutingUser ) @@ -50,36 +51,27 @@ function New-CIPPBackup { } #If Backup type is ConditionalAccess, create Conditional Access backup. - 'ConditionalAccess' { - $ConditionalAccessPolicyOutput = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/policies' -tenantid $tenantfilter - $AllNamedLocations = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/identity/conditionalAccess/namedLocations' -tenantid $tenantfilter - switch ($StorageOutput) { - 'default' { - [PSCustomObject]@{ - ConditionalAccessPolicies = $ConditionalAccessPolicyOutput - NamedLocations = $AllNamedLocations - } - } - 'table' { - #Store output in tablestorage for Recovery - $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') - $entity = [PSCustomObject]@{ - PartitionKey = 'ConditionalAccessBackup' - RowKey = $RowKey - TenantFilter = $TenantFilter - Policies = [string]($ConditionalAccessPolicyOutput | ConvertTo-Json -Compress -Depth 10) - NamedLocations = [string]($AllNamedLocations | ConvertTo-Json -Compress -Depth 10) - } - $Table = Get-CippTable -tablename 'ConditionalAccessBackup' - try { - $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup for Conditional Access Policies' -Sev 'Debug' - $Result - } catch { - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for Conditional Access Policies: $($_.Exception.Message)" -Sev 'Error' - [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } - } - } + 'Scheduled' { + #Do a sub switch here based on the ScheduledBackupValues? + #Store output in tablestorage for Recovery + $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') + $entity = @{ + PartitionKey = 'ScheduledBackup' + RowKey = $RowKey + TenantFilter = $TenantFilter + } + foreach ($ScheduledBackup in $ScheduledBackupValues.psobject.Properties.Name) { + $entity[$ScheduledBackup] = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter + } + + $Table = Get-CippTable -tablename 'ScheduledBackup' + try { + $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup for Conditional Access Policies' -Sev 'Debug' + $Result + } catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for Conditional Access Policies: $($_.Exception.Message)" -Sev 'Error' + [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } } } diff --git a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 new file mode 100644 index 000000000000..d159c76d9bee --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -0,0 +1,48 @@ +function New-CIPPBackupTask { + [CmdletBinding()] + param ( + $ScheduledBackup, + $TenantFilter + ) + + $BackupData = switch ($ScheduledBackup) { + 'users' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + } + 'groups' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + } + 'ca' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + } + 'namedlocations' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + } + 'authstrengths' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter + } + 'intuneconfig' { + #alert + } + 'intunecompliance' {} + + 'intuneprotection' {} + + 'CippWebhookAlerts' { + $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' + Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } + } + 'CippScriptedAlerts' { + $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' + Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } + } + 'CippStandards' { + $Table = Get-CippTable -tablename 'standards' + $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" + (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + } + + } + return $BackupData +} + From 169cf664421c19f0439424daf653fb9b99083cc9 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 00:11:30 +0200 Subject: [PATCH 13/33] improvements createbackup --- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 3 ++- .../CIPPCore/Public/New-CIPPBackupTask.ps1 | 20 +++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 27a2fc9fe629..21e25cb7813a 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -60,10 +60,11 @@ function New-CIPPBackup { RowKey = $RowKey TenantFilter = $TenantFilter } + Write-Host "ScheduledBackupValues: $($ScheduledBackupValues | ConvertTo-Json -Compress -Depth 100)" + Write-Host "Scheduled backup value psproperties: $($ScheduledBackupValues.psobject.Properties.Name)" foreach ($ScheduledBackup in $ScheduledBackupValues.psobject.Properties.Name) { $entity[$ScheduledBackup] = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter } - $Table = Get-CippTable -tablename 'ScheduledBackup' try { $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force diff --git a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 index d159c76d9bee..3bf2ff778156 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -1,25 +1,25 @@ function New-CIPPBackupTask { [CmdletBinding()] param ( - $ScheduledBackup, + $Task, $TenantFilter ) - $BackupData = switch ($ScheduledBackup) { + $BackupData = switch ($Task) { 'users' { - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter } 'groups' { - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter } 'ca' { - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter } 'namedlocations' { - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter } 'authstrengths' { - New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter + $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter } 'intuneconfig' { #alert @@ -30,16 +30,16 @@ function New-CIPPBackupTask { 'CippWebhookAlerts' { $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' - Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } + $BackupData = Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } } 'CippScriptedAlerts' { $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' - Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } + $BackupData = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } } 'CippStandards' { $Table = Get-CippTable -tablename 'standards' $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" - (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + $BackupData = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) } } From 6a5a37de6ad929c48e3417f76f1c85b33970ce5f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 12:58:45 +0200 Subject: [PATCH 14/33] fixes external sender --- .../Invoke-ListGroupSenderAuthentication.ps1 | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 index 189dd39468b0..97ca6fe52147 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -6,8 +6,6 @@ Function Invoke-ListGroupSenderAuthentication { $APIName = $TriggerMetadata.FunctionName Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' - - # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' @@ -15,28 +13,34 @@ Function Invoke-ListGroupSenderAuthentication { $TenantFilter = $Request.Query.TenantFilter $groupid = $Request.query.groupid + $GroupType = $Request.query.Type $params = @{ Identity = $groupid } - Write-Host = "This is the group id $groupid" - Write-Host = "This is the tenant filter $TenantFilter" - $GroupType = Invoke-ListGroups -tenantFilter $TenantFilter -GroupID $groupid - Write-Host = "This is the group type $($GroupType.calculatedGroupType)" - + try { - $Request = New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DistributionGroup' -cmdParams $params -UseSystemMailbox $true - $StatusCode = [HttpStatusCode]::OK + switch ($GroupType) { + 'Distribution List' { + Write-Host 'Checking DL' + $State = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'Get-DistributionGroup' -cmdParams $params -UseSystemMailbox $true).RequireSenderAuthenticationEnabled + } + 'Microsoft 365' { + Write-Host 'Checking M365 Group' + $State = (New-ExoRequest -tenantid $TenantFilter -cmdlet 'get-unifiedgroup' -cmdParams $params -UseSystemMailbox $true).RequireSenderAuthenticationEnabled + + } + default { $state = $true } + } + } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $StatusCode = [HttpStatusCode]::Forbidden - $Request = $ErrorMessage + $state = $true } - # Associate values to output bindings by calling 'Push-OutputBinding'. + # We flip the value because the API is asking if the group is allowed to receive external mail Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ - StatusCode = $StatusCode - Body = @{ enabled = $Request.RequireSenderAuthenticationEnabled } + StatusCode = [HttpStatusCode]::OK + Body = @{ allowedToReceiveExternal = !$state } }) } \ No newline at end of file From 988c08cf139ae929af5a09511b4eea043ec47d70 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 13:11:43 +0200 Subject: [PATCH 15/33] fixes issue if there are no known locations or apps. --- .../Invoke-ListConditionalAccessPolicies.ps1 | 10 ---------- 1 file changed, 10 deletions(-) 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 9c46ceae1ec4..39dd529a4ceb 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 @@ -19,8 +19,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Locations ) if ($id -eq 'All') { @@ -39,8 +37,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $RoleDefinitions ) if ($id -eq 'All') { @@ -59,8 +55,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Users ) if ($id -eq 'All') { @@ -78,8 +72,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Groups ) if ($id -eq 'All') { @@ -98,8 +90,6 @@ Function Invoke-ListConditionalAccessPolicies { param ( [Parameter()] $ID, - - [Parameter(Mandatory = $true)] $Applications ) if ($id -eq 'All') { From b323911fc4b4c03a6ce68f6e601aa8ad624b01e7 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 08:46:31 -0400 Subject: [PATCH 16/33] Sharepoint functions --- Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 | 27 ++++ .../Public/New-CIPPSharepointSite.ps1 | 145 ++++++++++++++++++ Modules/CIPPCore/Public/SAMManifest.json | 13 +- Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 88 +++++++++++ 4 files changed, 267 insertions(+), 6 deletions(-) create mode 100644 Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 create mode 100644 Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 create mode 100644 Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 diff --git a/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 new file mode 100644 index 000000000000..80f6e83453aa --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPSPOTenant.ps1 @@ -0,0 +1,27 @@ +function Get-CIPPSPOTenant { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [string]$SharepointPrefix + ) + + if (!$SharepointPrefix) { + # get sharepoint admin site + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + } else { + $tenantName = $SharepointPrefix + } + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + + # Query tenant settings + $XML = @' + +'@ + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + $Results = New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + + $Results | Select-Object -Last 1 *, @{n = 'SharepointPrefix'; e = { $tenantName } }, @{n = 'TenantFilter'; e = { $TenantFilter } } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 new file mode 100644 index 000000000000..ccf2e8b81b22 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPSharepointSite.ps1 @@ -0,0 +1,145 @@ +function New-CIPPSharepointSite { + <# + .SYNOPSIS + Create a new SharePoint site + + .DESCRIPTION + Create a new SharePoint site using the Modern REST API + + .PARAMETER SiteName + The name of the site + + .PARAMETER SiteDescription + The description of the site + + .PARAMETER SiteOwner + The username of the site owner + + .PARAMETER TemplateName + The template to use for the site. Default is Communication + + .PARAMETER SiteDesign + The design to use for the site. Default is Topic + + .PARAMETER WebTemplateExtensionId + The web template extension ID to use + + .PARAMETER SensitivityLabel + The Purview sensitivity label to apply to the site + + .PARAMETER TenantFilter + The tenant associated with the site + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [string]$SiteName, + + [Parameter(Mandatory = $true)] + [string]$SiteDescription, + + [Parameter(Mandatory = $true)] + [string]$SiteOwner, + + [Parameter(Mandatory = $false)] + [ValidateSet('Communication', 'Team')] + [string]$TemplateName = 'Communication', + + [Parameter(Mandatory = $false)] + [ValidateSet('Topic', 'Showcase', 'Blank', 'Custom')] + [string]$SiteDesign = 'Showcase', + + [Parameter(Mandatory = $false)] + [ValidatePattern('(\{|\()?[A-Za-z0-9]{4}([A-Za-z0-9]{4}\-?){4}[A-Za-z0-9]{12}(\}|\()?')] + [string]$WebTemplateExtensionId, + + [Parameter(Mandatory = $false)] + [ValidatePattern('(\{|\()?[A-Za-z0-9]{4}([A-Za-z0-9]{4}\-?){4}[A-Za-z0-9]{12}(\}|\()?')] + [string]$SensitivityLabel, + + [string]$Classification, + + [Parameter(Mandatory = $true)] + [string]$TenantFilter + ) + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + $SitePath = $SiteName -replace ' ' -replace '[^A-Za-z0-9-]' + $SiteUrl = "https://$tenantName.sharepoint.com/sites/$SitePath" + + + + + switch ($TemplateName) { + 'Communication' { + $WebTemplate = 'SITEPAGEPUBLISHING#0' + } + 'Team' { + $WebTemplate = 'STS#0' + } + } + + $WebTemplateExtensionId = '00000000-0000-0000-0000-000000000000' + $DefaultSiteDesignIds = @( '96c933ac-3698-44c7-9f4a-5fd17d71af9e', '6142d2a0-63a5-4ba0-aede-d9fefca2c767', 'f6cc5403-0d63-442e-96c0-285923709ffc') + + switch ($SiteDesign) { + 'Topic' { + $SiteDesignId = '96c933ac-3698-44c7-9f4a-5fd17d71af9e' + } + 'Showcase' { + $SiteDesignId = '6142d2a0-63a5-4ba0-aede-d9fefca2c767' + } + 'Blank' { + $SiteDesignId = 'f6cc5403-0d63-442e-96c0-285923709ffc' + } + 'Custom' { + if ($WebTemplateExtensionId -match '^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$') { + if ($WebTemplateExtensionId -notin $DefaultSiteDesignIds) { + $WebTemplateExtensionId = $SiteDesign + $SiteDesignId = '00000000-0000-0000-0000-000000000000' + } else { + $SiteDesignId = $WebTemplateExtensionId + } + } else { + $SiteDesignId = '96c933ac-3698-44c7-9f4a-5fd17d71af9e' + } + } + } + + # Create the request body + $Request = @{ + Title = $SiteName + Url = $SiteUrl + Lcid = 1033 + ShareByEmailEnabled = $false + Description = $SiteDescription + WebTemplate = $WebTemplate + SiteDesignId = $SiteDesignId + Owner = $SiteOwner + WebTemplateExtensionId = $WebTemplateExtensionId + } + + # Set the sensitivity label if provided + if ($SensitivityLabel) { + $Request.SensitivityLabel = $SensitivityLabel + } + if ($Classification) { + $Request.Classification = $Classification + } + + Write-Verbose ($Request | ConvertTo-Json -Compress -Depth 10) + + $body = @{ + request = $Request + } + + # Create the site + if ($PSCmdlet.ShouldProcess($SiteName, 'Create new SharePoint site')) { + $AddedHeaders = @{ + 'accept' = 'application/json;odata.metadata=none' + 'odata-version' = '4.0' + } + New-GraphPostRequest -scope "$AdminUrl/.default" -uri "$AdminUrl/_api/SPSiteManager/create" -Body ($body | ConvertTo-Json -Compress -Depth 10) -tenantid $TenantFilter -ContentType 'application/json' -AddedHeaders $AddedHeaders + } +} diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 7316e34c2246..3d52dfeadcb1 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -12,11 +12,11 @@ }, "requiredResourceAccess": [ { - "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", - "resourceAccess": [ - { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } - ] - }, + "resourceAppId": "aeb86249-8ea3-49e2-900b-54cc8e308f85", + "resourceAccess": [ + { "id": "fc946a4f-bc4d-413b-a090-b2c86113ec4f", "type": "Scope" } + ] + }, { "resourceAppId": "fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd", "resourceAccess": [ @@ -151,7 +151,8 @@ { "id": "b6890674-9dd5-4e42-bb15-5af07f541ae1", "type": "Role" }, { "id": "9e4862a5-b68f-479e-848a-4e07e25c9916", "type": "Scope" }, { "id": "bb6f654c-d7fd-4ae3-85c3-fc380934f515", "type": "Scope" }, - { "id": "e0a7cdbb-08b0-4697-8264-0069786e9674", "type": "Scope" } + { "id": "e0a7cdbb-08b0-4697-8264-0069786e9674", "type": "Scope" }, + { "id": "19da66cb-0fb0-4390-b071-ebc76a349482", "type": "Role" } ] }, { diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 new file mode 100644 index 000000000000..383a65a5b854 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -0,0 +1,88 @@ +function Set-CIPPSPOTenant { + <# + .SYNOPSIS + Set Sharepoint Tenant properties + + .DESCRIPTION + Set Sharepoint Tenant properties via SPO API + + .PARAMETER TenantFilter + Tenant to apply settings to + + .PARAMETER Identity + Tenant Identity (Get from Get-CIPPSPOTenant) + + .PARAMETER Properties + Hashtable of tenant properties to change + + .PARAMETER SharepointPrefix + Prefix for the sharepoint tenant + + .EXAMPLE + $Properties = @{ + 'EnableAIPIntegration' = $true + } + Get-CippSPOTenant -TenantFilter 'contoso.onmicrosoft.com' | Set-CIPPSPOTenant -Properties $Properties + + .FUNCTIONALITY + Internal + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [string]$TenantFilter, + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [Alias('_ObjectIdentity_')] + [string]$Identity, + [Parameter(Mandatory = $true)] + [hashtable]$Properties, + [Parameter(ValueFromPipelineByPropertyName = $true)] + [string]$SharepointPrefix + ) + + process { + if (!$SharepointPrefix) { + # get sharepoint admin site + $tenantName = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/root' -asApp $true -tenantid $TenantFilter).id.Split('.')[0] + } else { + $tenantName = $SharepointPrefix + } + $Identity = $Identity -replace "`n", ' ' + $AdminUrl = "https://$($tenantName)-admin.sharepoint.com" + $AllowedTypes = @('Boolean', 'String', 'Int32') + $SetProperty = [System.Collections.Generic.List[string]]::new() + $x = 114 + foreach ($Property in $Properties.Keys) { + # Get property type + $PropertyType = $Properties[$Property].GetType().Name + if ($PropertyType -in $AllowedTypes) { + if ($PropertyType -eq 'Boolean') { $Properties[$Property] = $Properties[$Property].ToString().ToLower() } + $xml = @" + + $($Properties[$Property]) + +"@ + $SetProperty.Add($xml) + $x++ + } + } + + if (($SetProperty | Measure-Object).Count -eq 0) { + Write-Error 'No valid properties found' + return + } + + # Query tenant settings + $XML = @" + $($SetProperty -join '') +"@ + $AdditionalHeaders = @{ + 'Accept' = 'application/json;odata=verbose' + } + + if ($PSCmdlet.ShouldProcess(($Properties.Keys -join ', '), 'Set Tenant Properties')) { + New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders + } + } +} \ No newline at end of file From 64cd24079b48b68af86c65da42dc312d2bfb2ef8 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 09:54:07 -0400 Subject: [PATCH 17/33] Update Set-CIPPSPOTenant.ps1 --- Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 index 383a65a5b854..ad6a9b115321 100644 --- a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -57,10 +57,14 @@ function Set-CIPPSPOTenant { # Get property type $PropertyType = $Properties[$Property].GetType().Name if ($PropertyType -in $AllowedTypes) { - if ($PropertyType -eq 'Boolean') { $Properties[$Property] = $Properties[$Property].ToString().ToLower() } + if ($PropertyType -eq 'Boolean') { + $PropertyToSet = $Properties[$Property].ToString().ToLower() + } else { + $PropertyToSet = $Properties[$Property] + } $xml = @" - $($Properties[$Property]) + $($PropertyToSet) "@ $SetProperty.Add($xml) @@ -85,4 +89,4 @@ function Set-CIPPSPOTenant { New-GraphPostRequest -scope "$AdminURL/.default" -tenantid $TenantFilter -Uri "$AdminURL/_vti_bin/client.svc/ProcessQuery" -Type POST -Body $XML -ContentType 'text/xml' -AddedHeaders $AdditionalHeaders } } -} \ No newline at end of file +} From 93f0ebb22a7be64fdedb9f84ea84908359a02b18 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 16:20:57 +0200 Subject: [PATCH 18/33] generate siteid --- .../MEM/Invoke-AddDefenderDeployment.ps1 | 18 +++---- .../Teams-Sharepoint/Invoke-ListSites.ps1 | 22 +++++++-- .../Invoke-AddTenantAllowBlockList.ps1 | 48 ++++++++++--------- 3 files changed, 50 insertions(+), 38 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 index 7bb3f446bd11..bc94e7cf5851 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddDefenderDeployment.ps1 @@ -33,9 +33,9 @@ Function Invoke-AddDefenderDeployment { allowPartnerToCollectIOSPersonalApplicationMetadata = [bool]$Compliance.ConnectIosCompliance androidMobileApplicationManagementEnabled = [bool]$Compliance.ConnectAndroidCompliance iosMobileApplicationManagementEnabled = [bool]$Compliance.appSync - microsoftDefenderForEndpointAttachEnabled = [bool]$compliance.AllowMEMEnforceCompliance + microsoftDefenderForEndpointAttachEnabled = [bool]$true } | ConvertTo-Json -Compress - $SettingsRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $tenant -type POST -body $SettingsObj + $SettingsRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/mobileThreatDefenseConnectors/' -tenantid $tenant -type POST -body $SettingsObj -AsApp $true "$($Tenant): Successfully set Defender Compliance and Reporting settings" $Settings = switch ($PolicySettings) { @@ -79,8 +79,7 @@ Function Invoke-AddDefenderDeployment { Write-Host ($CheckExististing | ConvertTo-Json) if ('Default AV Policy' -in $CheckExististing.Name) { "$($Tenant): AV Policy already exists. Skipping" - } - else { + } else { $PolBody = ConvertTo-Json -Depth 10 -Compress -InputObject @{ name = 'Default AV Policy' description = '' @@ -138,8 +137,7 @@ Function Invoke-AddDefenderDeployment { $CheckExististingASR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant if ('ASR Default rules' -in $CheckExististingASR.Name) { "$($Tenant): ASR Policy already exists. Skipping" - } - else { + } else { Write-Host $ASRbody $ASRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $ASRbody Write-Host ($ASRRequest.id) @@ -215,9 +213,8 @@ Function Invoke-AddDefenderDeployment { $CheckExististingEDR = New-GraphGETRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant if ('EDR Configuration' -in $CheckExististingEDR.Name) { "$($Tenant): EDR Policy already exists. Skipping" - } - else { - $EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $EDRbody + } else { + #$EDRRequest = New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' -tenantid $tenant -type POST -body $EDRbody if ($ASR.AssignTo -ne 'none') { $AssignBody = if ($ASR.AssignTo -ne 'AllDevicesAndUsers') { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.' + $($asr.AssignTo) + 'AssignmentTarget"}}]}' } else { '{"assignments":[{"id":"","target":{"@odata.type":"#microsoft.graph.allDevicesAssignmentTarget"}},{"id":"","target":{"@odata.type":"#microsoft.graph.allLicensedUsersAssignmentTarget"}}]}' } $assign = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/configurationPolicies('$($EDRRequest.id)')/assign" -tenantid $tenant -type POST -body $AssignBody @@ -226,8 +223,7 @@ Function Invoke-AddDefenderDeployment { "$($Tenant): Successfully added EDR Settings" } - } - catch { + } catch { "Failed to add policy for $($Tenant): $($_.Exception.Message)" Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Failed adding policy $($Displayname). Error: $($_.Exception.Message)" -Sev 'Error' continue 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 7b5ac0cd37b7..d94c6b0ce4bd 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 @@ -29,7 +29,7 @@ Function Invoke-ListSites { } else { $ParsedRequest = $Result } - $GraphRequest = $ParsedRequest | Select-Object @{ Name = 'UPN'; Expression = { $_.'Owner Principal Name' } }, + $GraphRequest = $ParsedRequest | Select-Object AutoMapUrl, @{ Name = 'UPN'; Expression = { $_.'Owner Principal Name' } }, @{ Name = 'displayName'; Expression = { $_.'Owner Display Name' } }, @{ Name = 'LastActive'; Expression = { $_.'Last Activity Date' } }, @{ Name = 'FileCount'; Expression = { [int]$_.'File Count' } }, @@ -41,14 +41,28 @@ Function Invoke-ListSites { #Temporary workaround for url as report is broken. #This API is so stupid its great. - $URLs = (New-GraphGetRequest -uri 'https://graph.microsoft.com/v1.0/sites/getAllSites?$select=SharePointIds' -asapp $true -tenantid $TenantFilter).SharePointIds - + $URLs = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/sites/getAllSites?$select=SharePointIds,name,webUrl,displayName,siteCollection' -asapp $true -tenantid $TenantFilter + $int = 0 + if ($Type -eq 'SharePointSiteUsage') { + $Requests = foreach ($url in $URLs) { + @{ + id = $int++ + method = 'GET' + url = "sites/$($url.sharepointIds.siteId)/lists?`$select=id,name,list,parentReference" + } + } + $Requests = (New-GraphBulkRequest -tenantid $TenantFilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true).body.value | Where-Object { $_.list.template -eq 'DocumentLibrary' } + } $GraphRequest = foreach ($site in $GraphRequest) { - $site.URL = ($URLs | Where-Object { $_.siteId -eq $site.SiteId }).siteUrl + $SiteURLs = ($URLs.SharePointIds | Where-Object { $_.siteId -eq $site.SiteId }) + $site.URL = $SiteURLs.siteUrl + $ListId = ($Requests | Where-Object { $_.parentReference.siteId -like "*$($SiteURLs.siteId)*" }).id + $site.AutoMapUrl = "tenantId=$($SiteUrls.tenantId)&webId={$($SiteUrls.webId)}&siteid={$($SiteURLs.siteId)}&webUrl=$($SiteURLs.siteUrl)&listId={$($ListId)}" $site } $StatusCode = [HttpStatusCode]::OK + } catch { $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message $StatusCode = [HttpStatusCode]::Forbidden diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 index 00c2cffc02e7..ff1464ea8e3b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-AddTenantAllowBlockList.ps1 @@ -14,40 +14,42 @@ Function Invoke-AddTenantAllowBlockList { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -message 'Accessed this API' -Sev 'Debug' $blocklistobj = $Request.body - + if ($Request.body.tenantId -eq 'AllTenants') { $Tenants = (Get-Tenants).defaultDomainName } else { $Tenants = @($Request.body.tenantId) } # Write to the Azure Functions log stream. Write-Host 'PowerShell HTTP trigger function processed a request.' - try { - $ExoRequest = @{ - tenantid = $Request.body.tenantid - cmdlet = 'New-TenantAllowBlockListItems' - cmdParams = @{ - Entries = [string[]]$blocklistobj.entries - ListType = [string]$blocklistobj.listType - Notes = [string]$blocklistobj.notes - $blocklistobj.listMethod = [bool]$true + $Results = [System.Collections.Generic.List[string]]::new() + foreach ($Tenant in $Tenants) { + try { + $ExoRequest = @{ + tenantid = $Tenant + cmdlet = 'New-TenantAllowBlockListItems' + cmdParams = @{ + Entries = [string[]]$blocklistobj.entries + ListType = [string]$blocklistobj.listType + Notes = [string]$blocklistobj.notes + $blocklistobj.listMethod = [bool]$true + } } - } - if ($blocklistobj.NoExpiration -eq $true) { - $ExoRequest.cmdParams.NoExpiration = $true - } + if ($blocklistobj.NoExpiration -eq $true) { + $ExoRequest.cmdParams.NoExpiration = $true + } - New-ExoRequest @ExoRequest + New-ExoRequest @ExoRequest - $result = "Successfully added $($blocklistobj.Entries) as type $($blocklistobj.ListType) to the $($blocklistobj.listMethod) list" - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Request.body.tenantid -message $result -Sev 'Info' - } catch { - $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message - $result = "Failed to create blocklist. Error: $ErrorMessage" - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Request.body.tenantid -message $result -Sev 'Error' + $results.add("Successfully added $($blocklistobj.Entries) as type $($blocklistobj.ListType) to the $($blocklistobj.listMethod) list for $tenant") + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Tenant -message $result -Sev 'Info' + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + $results.add("Failed to create blocklist. Error: $ErrorMessage") + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APIName -tenant $Tenant -message $result -Sev 'Error' + } } - # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK Body = @{ - 'Results' = $result + 'Results' = $results 'Request' = $ExoRequest } }) From ed2378f5708599714c201f9885f976cc88c54710 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 12:57:49 -0400 Subject: [PATCH 19/33] CPV tweaks --- .../Public/Add-CIPPApplicationPermission.ps1 | 3 ++- .../Public/Add-CIPPDelegatedPermission.ps1 | 16 +++++++++++++--- .../CIPPCore/Public/AdditionalPermissions.json | 15 +++++++++++++++ Modules/CIPPCore/Public/SAMManifest.json | 3 +-- 4 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 Modules/CIPPCore/Public/AdditionalPermissions.json diff --git a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 index f0f4c6badf6d..8fb1f23c3537 100644 --- a/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPApplicationPermission.ps1 @@ -6,7 +6,8 @@ function Add-CIPPApplicationPermission { $Tenantfilter ) if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify application permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } Set-Location (Get-Item $PSScriptRoot).FullName if ($RequiredResourceAccess -eq 'CIPPDefaults') { diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 921488c45f08..4ac1639877de 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -9,11 +9,14 @@ function Add-CIPPDelegatedPermission { Set-Location (Get-Item $PSScriptRoot).FullName if ($ApplicationId -eq $ENV:ApplicationID -and $Tenantfilter -eq $env:TenantID) { - return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + #return @('Cannot modify delgated permissions for CIPP-SAM on partner tenant') + $RequiredResourceAccess = 'CIPPDefaults' } if ($RequiredResourceAccess -eq 'CIPPDefaults') { $RequiredResourceAccess = (Get-Content '.\SAMManifest.json' | ConvertFrom-Json).requiredResourceAccess + $AdditionalPermissions = Get-Content '.\AdditionalPermissions.json' | ConvertFrom-Json + $RequiredResourceAccess = $RequiredResourceAccess + ($AdditionalPermissions | Where-Object { $RequiredResourceAccess.resourceAppId -notcontains $_.resourceAppId }) } $Translator = Get-Content '.\PermissionsTranslator.json' | ConvertFrom-Json $ServicePrincipalList = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals?`$select=AppId,id,displayName&`$top=999" -tenantid $Tenantfilter -skipTokenCache $true @@ -22,10 +25,17 @@ function Add-CIPPDelegatedPermission { $CurrentDelegatedScopes = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals/$($ourSVCPrincipal.id)/oauth2PermissionGrants" -skipTokenCache $true -tenantid $Tenantfilter - foreach ($App in $requiredResourceAccess) { + foreach ($App in $RequiredResourceAccess) { $svcPrincipalId = $ServicePrincipalList | Where-Object -Property AppId -EQ $App.resourceAppId + $AdditionalScopes = ($AdditionalPermissions | Where-Object -Property resourceAppId -EQ $App.resourceAppId).resourceAccess if (!$svcPrincipalId) { continue } - $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + if ($AdditionalScopes) { + $NewScope = (($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value + $AdditionalScopes.id | Select-Object -Unique) -join ' ' + Write-Host "NEW SCOPE: $NewScope" + } else { + $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' + } + $OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id) if (!$OldScope) { diff --git a/Modules/CIPPCore/Public/AdditionalPermissions.json b/Modules/CIPPCore/Public/AdditionalPermissions.json new file mode 100644 index 000000000000..4983c6f5fd03 --- /dev/null +++ b/Modules/CIPPCore/Public/AdditionalPermissions.json @@ -0,0 +1,15 @@ +[ + { + "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", + "resourceAccess": [{ "id": "AllProfiles.Manage", "type": "Scope" }] + }, + { + "resourceAppId": "fb78d390-0c51-40cd-8e17-fdbfab77341b", + "resourceAccess": [ + { "id": "AdminApi.AccessAsUser.All", "type": "Scope" }, + { "id": "FfoPowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "RemotePowerShell.AccessAsUser.All", "type": "Scope" }, + { "id": "VivaFeatureAccessPolicy.Manage.All", "type": "Scope" } + ] + } +] diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index 3d52dfeadcb1..fc75cb8b8644 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -172,8 +172,7 @@ { "resourceAppId": "00000003-0000-0ff1-ce00-000000000000", "resourceAccess": [ - { "id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", "type": "Scope" }, - { "id": "ec4fc4c8-872e-442b-a2a2-d095575807b3", "type": "Scope" } + { "id": "56680e0d-d2a3-4ae1-80d8-3c4f2100e3d0", "type": "Scope" } ] }, { From 634f64537789689214c883972c171a059268b475 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 22:18:47 +0200 Subject: [PATCH 20/33] convert to string for backup tasks --- .../CIPP/Core/Invoke-ExecListBackup.ps1 | 5 +++- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 8 +++---- .../CIPPCore/Public/New-CIPPBackupTask.ps1 | 24 ++++++++++++------- Scheduler_UserTasks/function.json | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 index 90b7e41bc4c9..f04ea258bba7 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecListBackup.ps1 @@ -10,7 +10,10 @@ Function Invoke-ExecListBackup { [CmdletBinding()] param($Request, $TriggerMetadata) - $Result = Get-CIPPBackup -type $Request.body.Type -TenantFilter $Request.body.TenantFilter + $Result = Get-CIPPBackup -type $Request.query.Type -TenantFilter $Request.query.TenantFilter + if ($request.query.NameOnly) { + $Result = $Result | Select-Object RowKey, timestamp + } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API 'Alerts' -message $request.body.text -Sev $request.body.Severity # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 21e25cb7813a..f497e7daa823 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -60,10 +60,10 @@ function New-CIPPBackup { RowKey = $RowKey TenantFilter = $TenantFilter } - Write-Host "ScheduledBackupValues: $($ScheduledBackupValues | ConvertTo-Json -Compress -Depth 100)" - Write-Host "Scheduled backup value psproperties: $($ScheduledBackupValues.psobject.Properties.Name)" - foreach ($ScheduledBackup in $ScheduledBackupValues.psobject.Properties.Name) { - $entity[$ScheduledBackup] = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter + Write-Host "Scheduled backup value psproperties: $(([pscustomobject]$ScheduledBackupValues).psobject.Properties)" + foreach ($ScheduledBackup in ([pscustomobject]$ScheduledBackupValues).psobject.Properties.Name) { + $BackupResult = New-CIPPBackupTask -Task $ScheduledBackup -TenantFilter $TenantFilter | ConvertTo-Json -Depth 100 -Compress | Out-String + $entity[$ScheduledBackup] = "$BackupResult" } $Table = Get-CippTable -tablename 'ScheduledBackup' try { diff --git a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 index 3bf2ff778156..d06a6ca94c5a 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -7,19 +7,24 @@ function New-CIPPBackupTask { $BackupData = switch ($Task) { 'users' { - $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + Write-Host "Backup users for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter } 'groups' { - $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + Write-Host "Backup groups for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter } 'ca' { - $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + Write-Host "Backup Conditional Access Policies for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter } 'namedlocations' { - $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + Write-Host "Backup Named Locations for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter } 'authstrengths' { - $BackupData = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter + Write-Host "Backup Authentication Strength Policies for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter } 'intuneconfig' { #alert @@ -29,17 +34,20 @@ function New-CIPPBackupTask { 'intuneprotection' {} 'CippWebhookAlerts' { + Write-Host "Backup Webhook Alerts for $TenantFilter" $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' - $BackupData = Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } + Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } } 'CippScriptedAlerts' { + Write-Host "Backup Scripted Alerts for $TenantFilter" $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' - $BackupData = Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } + Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } } 'CippStandards' { + Write-Host "Backup Standards for $TenantFilter" $Table = Get-CippTable -tablename 'standards' $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" - $BackupData = (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + (Get-CIPPAzDataTableEntity @Table -Filter $Filter) } } diff --git a/Scheduler_UserTasks/function.json b/Scheduler_UserTasks/function.json index f7af84092121..017acb166958 100644 --- a/Scheduler_UserTasks/function.json +++ b/Scheduler_UserTasks/function.json @@ -2,7 +2,7 @@ "bindings": [ { "name": "Timer", - "schedule": "0 */15 * * * *", + "schedule": "0 */5 * * * *", "direction": "in", "type": "timerTrigger" }, From ca3b627474e0cdc13438e3f97e4e87700eadecc5 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Fri, 5 Jul 2024 22:23:53 +0200 Subject: [PATCH 21/33] fixes backup list --- Modules/CIPPCore/Public/Get-CIPPBackup.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 index e463202983a1..c172f40f1c90 100644 --- a/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPBackup.ps1 @@ -4,6 +4,7 @@ function Get-CIPPBackup { [string]$Type, [string]$TenantFilter ) + Write-Host "Getting backup for $Type with TenantFilter $TenantFilter" $Table = Get-CippTable -tablename "$($Type)Backup" if ($TenantFilter) { $Filter = "PartitionKey eq '$($Type)Backup' and TenantFilter eq '$($TenantFilter)'" From c355a54d572ffb626a52f70f7757bde5128746d4 Mon Sep 17 00:00:00 2001 From: John Duprey Date: Fri, 5 Jul 2024 20:14:03 -0400 Subject: [PATCH 22/33] Extension data sync --- .../CIPPCore/Public/Add-CIPPScheduledTask.ps1 | 8 +- .../Push-ExtensionSyncData.ps1 | 6 + .../Register-CippExtensionScheduledTasks.ps1 | 71 +++++++ .../Sync-CippExtensionData.ps1 | 178 ++++++++++++++++++ .../Public/Hudu/Get-HuduFieldMapping.ps1 | 12 +- 5 files changed, 266 insertions(+), 9 deletions(-) create mode 100644 Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 create mode 100644 Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 create mode 100644 Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 index 952e3a68bc93..7e629e038798 100644 --- a/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPScheduledTask.ps1 @@ -2,7 +2,8 @@ function Add-CIPPScheduledTask { [CmdletBinding()] param( [pscustomobject]$Task, - [bool]$Hidden + [bool]$Hidden, + [string]$SyncType = $null ) $Table = Get-CIPPTable -TableName 'ScheduledTasks' @@ -49,10 +50,13 @@ function Add-CIPPScheduledTask { Hidden = [bool]$Hidden Results = 'Planned' } + if ($SyncType) { + $entity.SyncType = $SyncType + } try { Add-CIPPAzDataTableEntity @Table -Entity $entity -Force } catch { return "Could not add task: $($_.Exception.Message)" } return "Successfully added task: $($entity.Name)" -} \ No newline at end of file +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 new file mode 100644 index 000000000000..ebee9385b07a --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Push-ExtensionSyncData.ps1 @@ -0,0 +1,6 @@ +function Push-ExtensionSyncData { + param( + $TenantFilter, + $Extension + ) +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 new file mode 100644 index 000000000000..32aff8ef8ff7 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Register-CippExtensionScheduledTasks.ps1 @@ -0,0 +1,71 @@ +function Register-CIPPExtensionScheduledTasks { + Param( + [switch]$Reschedule + ) + + # get extension configuration and mappings table + $Table = Get-CIPPTable -TableName Extensionsconfig + $Config = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop) + $MappingsTable = Get-CIPPTable -TableName CippMapping + + # Get existing scheduled usertasks + $ScheduledTasksTable = Get-CIPPTable -TableName ScheduledTasks + $ScheduledTasks = Get-CIPPAzDataTableEntity @ScheduledTasksTable -Filter 'Hidden eq true' | Where-Object { $_.Command -match 'Sync-CippExtensionData' } + $Tenants = Get-Tenants -IncludeErrors + + $Extensions = @('Hudu') + + foreach ($Extension in $Extensions) { + $ExtensionConfig = $Config.$Extension + if ($ExtensionConfig.Enabled -eq $true) { + $Mappings = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)Mapping'" + $FieldMapping = Get-CIPPAzDataTableEntity @MappingsTable -Filter "PartitionKey eq '$($Extension)FieldMapping'" + $FieldSync = @{} + $SyncTypes = [System.Collections.Generic.List[string]]::new() + + foreach ($Mapping in $FieldMapping) { + $FieldSync[$Mapping.RowKey] = !([string]::IsNullOrEmpty($Mapping.IntegrationId)) + } + + $SyncTypes.Add('Overview') + + if ($FieldSync.Users) { + $SyncTypes.Add('Users') + $SyncTypes.Add('Mailboxes') + } + if ($FieldSync.Devices) { + $SyncTypes.Add('Devices') + } + + foreach ($Mapping in $Mappings) { + $Tenant = $Tenants | Where-Object { $_.customerId -eq $Mapping.RowKey } + + foreach ($SyncType in $SyncTypes) { + $ExistingTask = $ScheduledTasks | Where-Object { $_.Tenant -eq $Tenant.defaultDomainName -and $_.SyncType -eq $SyncType } + if (!$ExistingTask -or $Reschedule.IsPresent) { + $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + $Task = @{ + Name = "Extension Sync - $SyncType" + Command = @{ + value = 'Sync-CippExtensionData' + label = 'Sync-CippExtensionData' + } + Parameters = @{ + TenantFilter = $Tenant.defaultDomainName + SyncType = $SyncType + } + Recurrence = '1d' + ScheduledTime = $unixtime + TenantFilter = $Tenant.defaultDomainName + } + if ($ExistingTask) { + $Task.RowKey = $ExistingTask.RowKey + } + $null = Add-CIPPScheduledTask -Task $Task -hidden $true -SyncType $SyncType + } + } + } + } + } + +} diff --git a/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 new file mode 100644 index 000000000000..7cb1c9017a78 --- /dev/null +++ b/Modules/CippExtensions/Public/Extension Functions/Sync-CippExtensionData.ps1 @@ -0,0 +1,178 @@ +function Sync-CippExtensionData { + <# + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + $TenantFilter, + $SyncType + ) + + $Table = Get-CIPPTable -TableName ExtensionSync + $Extensions = Get-CIPPAzDataTableEntity @Table -Filter "PartitionKey eq '$($SyncType)'" + $LastSync = $Extensions | Where-Object { $_.RowKey -eq $TenantFilter } + $CacheTable = Get-CIPPTable -tablename 'CacheExtensionSync' + + if (!$LastSync) { + $LastSync = @{ + PartitionKey = $SyncType + RowKey = $TenantFilter + Status = 'Not Synced' + Error = '' + LastSync = 'Never' + } + Add-CIPPAzDataTableEntity @Table -Entity $LastSync + } + + try { + switch ($SyncType) { + 'Overview' { + # Build bulk requests array. + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'TenantDetails' + method = 'GET' + url = '/organization' + }, + @{ + id = 'AllRoles' + method = 'GET' + url = '/directoryRoles?$top=999' + }, + @{ + id = 'Domains' + method = 'GET' + url = '/domains$top=999' + }, + @{ + id = 'Licenses' + method = 'GET' + url = '/subscribedSkus?$top=999' + }, + @{ + id = 'Groups' + method = 'GET' + url = '/groups?$top=999&$select=id,createdDateTime,displayName,description,mail,mailEnabled,mailNickname,resourceProvisioningOptions,securityEnabled,visibility,organizationId,onPremisesSamAccountName,membershipRule,grouptypes,onPremisesSyncEnabled,resourceProvisioningOptions,userPrincipalName' + }, + @{ + id = 'ConditionalAccess' + method = 'GET' + url = '/identity/conditionalAccess/policies' + }, + @{ + id = 'SecureScoreControlProfiles' + method = 'GET' + url = '/security/secureScoreControlProfiles?$top=999' + }, + @{ + id = 'Subscriptions' + method = 'GET' + url = '/directory/subscriptions?$top=999' + } + ) + + $SingleGraphQueries = @(@{ + id = 'SecureScore' + graphRequest = @{ + uri = 'https://graph.microsoft.com/beta/security/secureScores?$top=1' + noPagination = $true + } + }) + } + 'Users' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Users' + method = 'GET' + url = '/users?$top=999&$select=id,accountEnabled,businessPhones,city,createdDateTime,companyName,country,department,displayName,faxNumber,givenName,isResourceAccount,jobTitle,mail,mailNickname,mobilePhone,onPremisesDistinguishedName,officeLocation,onPremisesLastSyncDateTime,otherMails,postalCode,preferredDataLocation,preferredLanguage,proxyAddresses,showInAddressList,state,streetAddress,surname,usageLocation,userPrincipalName,userType,assignedLicenses,onPremisesSyncEnabled' + } + ) + } + 'Devices' { + [System.Collections.Generic.List[PSCustomObject]]$TenantRequests = @( + @{ + id = 'Devices' + method = 'GET' + url = '/deviceManagement/managedDevices?$top=999' + }, + @{ + id = 'DeviceCompliancePolicies' + method = 'GET' + url = '/deviceManagement/deviceCompliancePolicies' + }, + @{ + id = 'DeviceApps' + method = 'GET' + url = '/deviceAppManagement/mobileApps' + } + ) + } + 'Mailboxes' { + $Select = 'id,ExchangeGuid,ArchiveGuid,UserPrincipalName,DisplayName,PrimarySMTPAddress,RecipientType,RecipientTypeDetails,EmailAddresses,WhenSoftDeleted,IsInactiveMailbox' + $ExoRequest = @{ + tenantid = $TenantFilter + cmdlet = 'Get-Mailbox' + cmdParams = @{} + Select = $Select + } + $Mailboxes = (New-ExoRequest @ExoRequest) | Select-Object id, ExchangeGuid, ArchiveGuid, WhenSoftDeleted, @{ Name = 'UPN'; Expression = { $_.'UserPrincipalName' } }, + + @{ Name = 'displayName'; Expression = { $_.'DisplayName' } }, + @{ Name = 'primarySmtpAddress'; Expression = { $_.'PrimarySMTPAddress' } }, + @{ Name = 'recipientType'; Expression = { $_.'RecipientType' } }, + @{ Name = 'recipientTypeDetails'; Expression = { $_.'RecipientTypeDetails' } }, + @{ Name = 'AdditionalEmailAddresses'; Expression = { ($_.'EmailAddresses' | Where-Object { $_ -clike 'smtp:*' }).Replace('smtp:', '') -join ', ' } } + + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = 'Mailboxes' + RowKey = 'Mailboxes' + Data = [string]($Mailboxes | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + if ($TenantRequests) { + try { + $TenantResults = New-GraphBulkRequest -Requests $TenantRequests -tenantid $TenantFilter + } catch { + Throw "Failed to fetch bulk company data: $_" + } + + if ($SingleGraphQueries) { + foreach ($SingleGraphQuery in $SingleGraphQueries) { + $Request = $SingleGraphQuery.graphRequest + $Data = New-GraphGetRequest @Request -tenantid $TenantFilter + $Entity = @{ + PartitionKey = $TenantFilter + SyncType = $SyncType + RowKey = $SingleGraphQuery.id + Data = [string]($Data | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + + $TenantResults | Select-Object id, body | ForEach-Object { + $Entity = @{ + PartitionKey = $TenantFilter + RowKey = $_.id + SyncType = $SyncType + Data = [string]($_.body.value | ConvertTo-Json -Depth 10 -Compress) + } + Add-CIPPAzDataTableEntity @CacheTable -Entity $Entity -Force + } + } + $LastSync.LastSync = [datetime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ssZ') + $LastSync.Status = 'Completed' + $LastSync.Error = '' + } catch { + $LastSync.Status = 'Failed' + $LastSync.Error = [string](Get-CippException -Exception $_ | ConvertTo-Json -Compress) + throw "Failed to sync data: $($_.Exception.Message)" + } finally { + Add-CIPPAzDataTableEntity @Table -Entity $LastSync -Force + } +} diff --git a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 index 64b4700af62f..bb12d561104a 100644 --- a/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 @@ -4,13 +4,13 @@ function Get-HuduFieldMapping { $CIPPMapping ) - $Mappings = Get-ExtensionMapping -Extension 'HuduFields' + $Mappings = Get-ExtensionMapping -Extension 'HuduField' $CIPPFieldHeaders = @( [PSCustomObject]@{ Title = 'Hudu Asset Layouts' FieldType = 'Layouts' - Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Field' + Description = 'Use the table below to map your Hudu Asset Layouts to the correct CIPP Data Type. A new Rich Text asset layout field will be created if it does not exist.' } ) $CIPPFields = @( @@ -31,8 +31,6 @@ function Get-HuduFieldMapping { } ) - - $Tenants = Get-Tenants -IncludeErrors $Table = Get-CIPPTable -TableName Extensionsconfig try { $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json -ea stop).Hudu @@ -46,8 +44,8 @@ function Get-HuduFieldMapping { $_.Exception.message } - Write-LogMessage -Message "Could not get Hudu Companies, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' - $HuduCompanies = @(@{name = "Could not get Hudu Companies, error: $Message"; value = '-1' }) + Write-LogMessage -Message "Could not get Hudu Asset Layouts, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $AssetLayouts = @(@{name = "Could not get Hudu Asset Layouts, error: $Message"; value = '-1' }) } $Unset = [PSCustomObject]@{ @@ -65,4 +63,4 @@ function Get-HuduFieldMapping { return $MappingObj -} \ No newline at end of file +} From e3bb4d384f597af67a872545653bebc41ec970e9 Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:48:00 -0500 Subject: [PATCH 23/33] Remove-CIPPCalendarInvites --- .../Public/Remove-CIPPCalendarInvites.ps1 | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 diff --git a/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 b/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 new file mode 100644 index 000000000000..22e57c2acff8 --- /dev/null +++ b/Modules/CIPPCore/Public/Remove-CIPPCalendarInvites.ps1 @@ -0,0 +1,21 @@ +function Remove-CIPPCalendarInvites { + [CmdletBinding()] + param( + $userid, + $tenantFilter, + $username, + $APIName = 'Remove Calendar Invites', + $ExecutingUser + ) + + try { + + New-ExoRequest -tenantid $tenantFilter -cmdlet 'Remove-CalendarEvents' -Anchor $username -cmdParams @{Identity = $username; QueryWindowInDays = 730 ; CancelOrganizedMeetings = $true ; Confirm = $false} + Write-LogMessage -user $ExecutingUser -API $APIName -message "Cancelled all calendar invites for $($username)" -Sev 'Info' -tenant $tenantFilter + "Cancelled all calendar invites for $($username)" + + } catch { + Write-LogMessage -user $ExecutingUser -API $APIName -message "Could not cancel calendar invites for $($username): $($_.Exception.Message)" -Sev 'Error' -tenant $tenantFilter + return "Could not cancel calendar invites for $($username). Error: $($_.Exception.Message)" + } +} From 2aa378818a13e7160699f3e3bb483a872331f7fc Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:53:30 -0500 Subject: [PATCH 24/33] Added removeCalendarInvites --- Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 b/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 index 9087bd30deff..ba5ab6432fb8 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 +++ b/Modules/CIPPCore/Public/Invoke-CIPPOffboardingJob.ps1 @@ -72,6 +72,9 @@ function Invoke-CIPPOffboardingJob { { $_.'removeMobile' -eq 'true' } { Remove-CIPPMobileDevice -userid $userid -username $Username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName } + { $_.'removeCalendarInvites' -eq 'true' } { + Remove-CIPPCalendarInvites -userid $userid -username $Username -tenantFilter $Tenantfilter -ExecutingUser $ExecutingUser -APIName $APIName + } { $_.'removePermissions' } { if ($RunScheduled) { Remove-CIPPMailboxPermissions -PermissionsLevel @('FullAccess', 'SendAs', 'SendOnBehalf') -userid 'AllUsers' -AccessUser $UserName -TenantFilter $TenantFilter -APIName $APINAME -ExecutingUser $ExecutingUser @@ -90,4 +93,4 @@ function Invoke-CIPPOffboardingJob { } return $Return -} \ No newline at end of file +} From fdb9dfc66094b9887e0f09af53b03ce3788e491f Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:54:23 -0500 Subject: [PATCH 25/33] Roles for modifying calendar invites --- Cache_SAMSetup/SAMManifest.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index e2a457b734fb..f94959dc3ac6 100644 --- a/Cache_SAMSetup/SAMManifest.json +++ b/Cache_SAMSetup/SAMManifest.json @@ -165,7 +165,11 @@ "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", "type": "Scope" }, - { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" } + { "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", "type": "Scope" }, + { "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", "type": "Scope" }, + { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" }, + { "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", "type": "Role" }, + { "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", "type": "Role" } ] }, { From b5019885b80e7ba81e80d5474f2396695717d715 Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:54:45 -0500 Subject: [PATCH 26/33] Roles for removing calendar invites --- Modules/CIPPCore/Public/SAMManifest.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index fc75cb8b8644..50a03c019af1 100644 --- a/Modules/CIPPCore/Public/SAMManifest.json +++ b/Modules/CIPPCore/Public/SAMManifest.json @@ -166,7 +166,11 @@ "resourceAppId": "00000002-0000-0ff1-ce00-000000000000", "resourceAccess": [ { "id": "ab4f2b77-0b06-4fc1-a9de-02113fc2ab7c", "type": "Scope" }, - { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" } + { "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", "type": "Scope" }, + { "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", "type": "Scope" }, + { "id": "dc50a0fb-09a3-484d-be87-e023b12c6440", "type": "Role" }, + { "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", "type": "Role" }, + { "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", "type": "Role" } ] }, { From de6d14e2001a78d36ff8ab9b6d8770ec9fd9e843 Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:56:25 -0500 Subject: [PATCH 27/33] Roles for removing calendar invites --- Cache_SAMSetup/PermissionsTranslator.json | 38 +++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/Cache_SAMSetup/PermissionsTranslator.json b/Cache_SAMSetup/PermissionsTranslator.json index 27c5a7e6463f..ecc57bd2649d 100644 --- a/Cache_SAMSetup/PermissionsTranslator.json +++ b/Cache_SAMSetup/PermissionsTranslator.json @@ -1004,8 +1004,15 @@ "description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user.", "displayName": "Read and write calendars in all mailboxes", "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", - "origin": "Application", - "value": "Calendars.ReadWrite" + "origin": "Application (Office 365 Exchange Online)", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail.", + "displayName": "Read and write all user mailbox settings", + "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", + "origin": "Application (Office 365 Exchange Online)", + "value": "Mailbox.Settings.ReadWrite" }, { "description": "Allows the app to read your organization's user flows, without a signed-in user.", @@ -5286,6 +5293,24 @@ "userConsentDisplayName": "Read Threat and Vulnerability Management vulnerability information", "value": "Exchange.Manage" }, + { + "description": "Allows the app to create, read, update and delete events in all calendars in the organization user has permissions to access. This includes delegate and shared calendars", + "displayName": "Read and write user and shared calendars", + "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars", + "userConsentDisplayName": "Read and write to your and shared calendars", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings. Does not include permission to send mail.", + "displayName": "Read and write user mailbox settings", + "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create, and delete your mailbox settings.", + "userConsentDisplayName": "Read and write to your mailbox settings", + "value": "MailboxSettings.ReadWrite" + }, { "description": "Allows the app to have full control of all site collections on behalf of the signed-in user.", "displayName": "Manage Sharepoint Online", @@ -5312,5 +5337,14 @@ "userConsentDescription": "Access Microsoft Teams and Skype for Business data as the signed in user", "userConsentDisplayName": "Access Microsoft Teams and Skype for Business data based on the user's role membership", "value": "user_impersonation" + }, + { + "description": "Read and write all on-premises directory synchronization information", + "displayName": "Read and write all on-premises directory synchronization information", + "id": "c2d95988-7604-4ba1-aaed-38a5f82a51c7", + "Origin": "Delegated", + "userConsentDescription": "Access Microsoft Teams and Skype for Business data as the signed in user", + "userConsentDisplayName": "Access Microsoft Teams and Skype for Business data based on the user's role membership", + "value": "OnPremDirectorySynchronization.ReadWrite.All" } ] From 24a77cb5d7b3f907216e7fd2e9ebb35b802ce748 Mon Sep 17 00:00:00 2001 From: chase-vgo <168204519+chase-vgo@users.noreply.github.com> Date: Sat, 6 Jul 2024 16:56:37 -0500 Subject: [PATCH 28/33] Roles for removing calendar invites --- .../Public/PermissionsTranslator.json | 38 +++++++++++++------ 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/Modules/CIPPCore/Public/PermissionsTranslator.json b/Modules/CIPPCore/Public/PermissionsTranslator.json index aa7947e9374d..204fe8532c4f 100644 --- a/Modules/CIPPCore/Public/PermissionsTranslator.json +++ b/Modules/CIPPCore/Public/PermissionsTranslator.json @@ -1004,8 +1004,15 @@ "description": "Allows the app to create, read, update, and delete events of all calendars without a signed-in user.", "displayName": "Read and write calendars in all mailboxes", "id": "ef54d2bf-783f-4e0f-bca1-3210c0444d99", - "origin": "Application", - "value": "Calendars.ReadWrite" + "origin": "Application (Office 365 Exchange Online)", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings without a signed-in user. Does not include permission to send mail.", + "displayName": "Read and write all user mailbox settings", + "id": "f9156939-25cd-4ba8-abfe-7fabcf003749", + "origin": "Application (Office 365 Exchange Online)", + "value": "Mailbox.Settings.ReadWrite" }, { "description": "Allows the app to read your organization's user flows, without a signed-in user.", @@ -5286,6 +5293,24 @@ "userConsentDisplayName": "Read Threat and Vulnerability Management vulnerability information", "value": "Exchange.Manage" }, + { + "description": "Allows the app to create, read, update and delete events in all calendars in the organization user has permissions to access. This includes delegate and shared calendars", + "displayName": "Read and write user and shared calendars", + "id": "bbd1ca91-75e0-4814-ad94-9c5dbbae3415", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create and delete events in all calendars in your organization you have permissions to access. This includes delegate and shared calendars", + "userConsentDisplayName": "Read and write to your and shared calendars", + "value": "Calendars.ReadWrite.All" + }, + { + "description": "Allows the app to create, read, update, and delete user's mailbox settings. Does not include permission to send mail.", + "displayName": "Read and write user mailbox settings", + "id": "2e83d72d-8895-4b66-9eea-abb43449ab8b", + "Origin": "Delegated (Office 365 Exchange Online)", + "userConsentDescription": "Allows the app to read, update, create, and delete your mailbox settings.", + "userConsentDisplayName": "Read and write to your mailbox settings", + "value": "MailboxSettings.ReadWrite" + }, { "description": "Allows the app to have full control of all site collections on behalf of the signed-in user.", "displayName": "Manage Sharepoint Online", @@ -5295,15 +5320,6 @@ "userConsentDisplayName": "Allows the app to have full control of all site collections on your behalf.", "value": "AllSites.FullControl" }, - { - "description": "Required for Request-SPOPeronalSite", - "displayName": "Manage sharepoint profiles", - "id": "ec4fc4c8-872e-442b-a2a2-d095575807b3", - "Origin": "Delegated (Office 365 SharePoint Online)", - "userConsentDescription": "", - "userConsentDisplayName": "Manage sharepoint profiles", - "value": "AllProfiles.Manage" - }, { "description": "Allows to read the LAPs passwords.", "displayName": "Manage LAPs passwords", From a76ac093cc85a4aebd502408aa9ec766324f4f58 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Sun, 7 Jul 2024 19:49:44 +0200 Subject: [PATCH 29/33] commiting changes to copy app wizard. --- .../Public/Add-CIPPDelegatedPermission.ps1 | 6 +++ .../Push-ExecAddMultiTenantApp.ps1 | 2 +- .../Push-ExecApplicationCopy.ps1 | 13 +++++ .../Invoke-ExecAddMultiTenantApp.ps1 | 15 ++---- .../Public/New-CIPPApplicationCopy.ps1 | 46 +++++++++++++++++ Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 8 +-- .../Invoke-CIPPStandardAppDeploy.ps1 | 50 +++++++++++++++++++ 7 files changed, 125 insertions(+), 15 deletions(-) create mode 100644 Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 create mode 100644 Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 create mode 100644 Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 4ac1639877de..7701a5ff68cb 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -3,6 +3,7 @@ function Add-CIPPDelegatedPermission { param( $RequiredResourceAccess, $ApplicationId, + $NoTranslateRequired, $Tenantfilter ) Write-Host 'Adding Delegated Permissions' @@ -33,6 +34,11 @@ function Add-CIPPDelegatedPermission { $NewScope = (($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value + $AdditionalScopes.id | Select-Object -Unique) -join ' ' Write-Host "NEW SCOPE: $NewScope" } else { + if ($NoTranslateRequired) { + $NewScope = $App.resourceAccess | ForEach-Object { $_.id } -join ' ' + } else { + $NewScope = ($Translator | Where-Object { $_.id -in $App.resourceAccess.id }).value -join ' ' + } $NewScope = ($Translator | Where-Object { $_.id -in $App.ResourceAccess.id }).value -join ' ' } diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 index 62e04ac8ba5b..3f2009a0a950 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecAddMultiTenantApp.ps1 @@ -18,4 +18,4 @@ function Push-ExecAddMultiTenantApp($QueueItem, $TriggerMetadata) { } catch { Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 new file mode 100644 index 000000000000..6437940809db --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecApplicationCopy.ps1 @@ -0,0 +1,13 @@ +function Push-ExecApplicationCopy($QueueItem, $TriggerMetadata) { + <# + .FUNCTIONALITY + Entrypoint + #> + try { + $Queueitem = $QueueItem | ConvertTo-Json -Depth 10 | ConvertFrom-Json + Write-Host "$($Queueitem | ConvertTo-Json -Depth 10)" + New-CIPPApplicationCopy -App $queueitem.AppId -Tenant $Queueitem.Tenant + } catch { + Write-LogMessage -message "Error adding application to tenant $($Queueitem.Tenant) - $($_.Exception.Message)" -tenant $Queueitem.Tenant -API 'Add Multitenant App' -sev Error + } +} 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 08124ad41683..4cb38d9f9dc8 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 @@ -18,15 +18,10 @@ function Invoke-ExecAddMultiTenantApp { $Results = try { if ($request.body.CopyPermissions -eq $true) { - try { - $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/applications(appId='$($Request.body.AppId)')" -tenantid $ENV:tenantid -NoAuthCheck $true - $DelegateResourceAccess = $Existingapp.requiredResourceAccess - $ApplicationResourceAccess = $Existingapp.requiredResourceAccess - } catch { - 'Failed to get existing permissions. The app does not exist in the partner tenant.' - } + $Command = 'ExecApplicationCopy' + } else { + $Command = 'ExecAddMultiTenantApp' } - #This needs to be moved to a queue. if ('allTenants' -in $Request.body.SelectedTenants.defaultDomainName) { $TenantFilter = (Get-Tenants).defaultDomainName } else { @@ -36,7 +31,7 @@ function Invoke-ExecAddMultiTenantApp { foreach ($Tenant in $TenantFilter) { try { Push-OutputBinding -Name QueueItem -Value ([pscustomobject]@{ - FunctionName = 'ExecAddMultiTenantApp' + FunctionName = $Command Tenant = $tenant appId = $Request.body.appid applicationResourceAccess = $ApplicationResourceAccess @@ -59,4 +54,4 @@ function Invoke-ExecAddMultiTenantApp { Body = @{ Results = @($Results) } }) -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 b/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 new file mode 100644 index 000000000000..da8b584954f8 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPApplicationCopy.ps1 @@ -0,0 +1,46 @@ +function New-CIPPApplicationCopy { + [CmdletBinding()] + param( + $App, + $Tenant + ) + $CurrentInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $env:TenantID -NoAuthCheck $true + try { + $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/Applications(appId='$($app)')" -tenantid $ENV:tenantid -NoAuthCheck $true + $Type = 'Application' + } catch { + $ExistingApp = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($app)')/oauth2PermissionGrants" -tenantid $ENV:tenantid -NoAuthCheck $true + $ExistingAppRoleAssignments = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/servicePrincipals(appId='$($app)')/appRoleAssignments" -tenantid $ENV:tenantid -NoAuthCheck $true + $Type = 'ServicePrincipal' + } + if (!$ExistingApp) { + Write-LogMessage -message "Failed to add $App to tenant. This app does not exist." -tenant $tenant -API 'Application Copy' -sev error + continue + } + if ($Type -eq 'Application') { + $DelegateResourceAccess = $Existingapp.requiredResourceAccess + $ApplicationResourceAccess = $Existingapp.requiredResourceAccess + $NoTranslateRequired = $false + } else { + $DelegateResourceAccess = $ExistingApp | Group-Object -Property resourceId | ForEach-Object { + [pscustomobject]@{ resourceAppId = ($CurrentInfo | Where-Object -Property id -EQ $_.Name).appId; resourceAccess = @($_.Group | ForEach-Object { [pscustomobject]@{ id = $_.scope; type = 'Scope' } } ) + } + } + $ApplicationResourceAccess = $ExistingappRoleAssignments | Group-Object -Property ResourceId | ForEach-Object { + [pscustomobject]@{ resourceAppId = ($CurrentInfo | Where-Object -Property id -EQ $_.Name).appId; resourceAccess = @($_.Group | ForEach-Object { [pscustomobject]@{ id = $_.appRoleId; type = 'Role' } } ) + } + } + $NoTranslateRequired = $true + } + $TenantInfo = New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/servicePrincipals?$top=999' -tenantid $Tenant -NoAuthCheck $true + + if ($App -Notin $TenantInfo.appId) { + $PostResults = New-GraphPostRequest 'https://graph.microsoft.com/beta/servicePrincipals' -type POST -tenantid $Tenant -body "{ `"appId`": `"$($App)`" }" + Write-LogMessage -message "Added $App as a service principal" -tenant $tenant -API 'Application Copy' -sev Info + } + Add-CIPPApplicationPermission -RequiredResourceAccess $ApplicationResourceAccess -ApplicationId $App -Tenantfilter $Tenant + Add-CIPPDelegatedPermission -RequiredResourceAccess $DelegateResourceAccess -ApplicationId $App -Tenantfilter $Tenant -NoTranslateRequired $NoTranslateRequired + Write-LogMessage -message "Added permissions to $app" -tenant $tenant -API 'Application Copy' -sev Info + + return $Results +} diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index f497e7daa823..0893e0e89b15 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -11,7 +11,7 @@ function New-CIPPBackup { $BackupData = switch ($backupType) { #If backup type is CIPP, create CIPP backup. - 'CIPP' { + 'CIPP' { try { $BackupTables = @( 'bpa' @@ -27,7 +27,7 @@ function New-CIPPBackup { Get-CIPPAzDataTableEntity @Table | Select-Object *, @{l = 'table'; e = { $CSVTable } } } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' - $CSVfile + $CSVfile $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') $entity = [PSCustomObject]@{ PartitionKey = 'CIPPBackup' @@ -43,7 +43,7 @@ function New-CIPPBackup { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for CIPP: $($_.Exception.Message)" -Sev 'Error' [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } } - + } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup: $($_.Exception.Message)" -Sev 'Error' [pscustomobject]@{'Results' = "Backup Creation failed: $($_.Exception.Message)" } @@ -51,7 +51,7 @@ function New-CIPPBackup { } #If Backup type is ConditionalAccess, create Conditional Access backup. - 'Scheduled' { + 'Scheduled' { #Do a sub switch here based on the ScheduledBackupValues? #Store output in tablestorage for Recovery $RowKey = $TenantFilter + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 new file mode 100644 index 000000000000..c6c2509795ac --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardAppDeploy.ps1 @@ -0,0 +1,50 @@ +function Invoke-CIPPStandardAppDeploy { + <# + .FUNCTIONALITY + Internal + .APINAME + AppDeploy + .CAT + Entra Standards + .TAG + "lowimpact" + "CIS" + .HELPTEXT + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access + .DOCSDESCRIPTION + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access. This is a tenant wide setting and overrules any settings set on the site level + .ADDEDCOMPONENT + .LABEL + Disable Resharing by External Users + .IMPACT + High Impact + .POWERSHELLEQUIVALENT + Update-MgBetaAdminSharepointSetting + .RECOMMENDEDBY + "CIS" + .DOCSDESCRIPTION + Disables the ability for external users to share files they don't own. Sharing links can only be made for People with existing access + .UPDATECOMMENTBLOCK + Run the Tools\Update-StandardsComments.ps1 script to update this comment block + #> + + param($Tenant, $Settings) + + If ($Settings.remediate -eq $true) { + $AppsToAdd = $Settings.appids -split ',' + foreach ($App In $AppsToAdd) { + try { + New-CIPPApplicationCopy -App $App -Tenant $Tenant + Write-LogMessage -API 'Standards' -tenant $tenant -message "Added $App to $Tenant and update it's permissions" -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to add app $App" -sev Error + } + } + } +} + + + + + From e0ffa585d526962f787ba5834948ea2194d3667a Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Mon, 8 Jul 2024 01:40:29 +0200 Subject: [PATCH 30/33] major update to splitting data across tables --- .../Public/Add-CIPPAzDataTableEntity.ps1 | 122 ++++++++++++++---- .../Endpoint/MEM/Invoke-AddIntuneTemplate.ps1 | 77 +---------- .../Invoke-ListIntuneTemplates.ps1 | 4 +- .../Public/Get-CIPPAzDatatableEntity.ps1 | 66 ++++++++-- .../CIPPCore/Public/New-CIPPBackupTask.ps1 | 30 ++++- .../Public/New-CIPPIntuneTemplate.ps1 | 83 ++++++++++++ Scheduler_UserTasks/function.json | 2 +- 7 files changed, 268 insertions(+), 116 deletions(-) create mode 100644 Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index befa8155df6c..8c3045f17579 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -6,45 +6,122 @@ function Add-CIPPAzDataTableEntity { [switch]$Force, [switch]$CreateTableIfNotExists ) - + + $MaxRowSize = 1mb - 100kb + $MaxSize = 30kb # maximum size of a property value + foreach ($SingleEnt in $Entity) { try { Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt -ErrorAction Stop } catch [System.Exception] { if ($_.Exception.ErrorCode -eq 'PropertyValueTooLarge' -or $_.Exception.ErrorCode -eq 'EntityTooLarge') { try { - $MaxSize = 30kb - $largePropertyName = $null + $largePropertyNames = @() + $entitySize = 0 foreach ($key in $SingleEnt.Keys) { - if ($SingleEnt[$key].Length -gt $MaxSize) { - $largePropertyName = $key - break + $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) + $entitySize = $entitySize + $propertySize + if ($propertySize -gt $MaxSize) { + $largePropertyNames = $largePropertyNames + $key } } - if ($largePropertyName) { - $dataString = $SingleEnt[$largePropertyName] - $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) - $splitData = 0..($splitCount - 1) | ForEach-Object { - $start = $_ * $MaxSize - $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) - } + if ($largePropertyNames.Count -gt 0) { + foreach ($largePropertyName in $largePropertyNames) { + $dataString = $SingleEnt[$largePropertyName] + $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) + $splitData = @() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData = $splitData + $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) + } + + $splitPropertyNames = @() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames = $splitPropertyNames + "${largePropertyName}_Part$i" + } - $splitPropertyNames = 1..$splitData.Count | ForEach-Object { - "${largePropertyName}_Part$_" + $splitInfo = @{ + OriginalHeader = $largePropertyName + SplitHeaders = $splitPropertyNames + } + $SingleEnt['SplitOverProps'] = ($splitInfo | ConvertTo-Json).ToString() + $SingleEnt.Remove($largePropertyName) + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] + } } + } + + # Check if the entity is still too large + $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) + if ($entitySize -gt $MaxRowSize) { + $rows = @() + $originalPartitionKey = $SingleEnt.PartitionKey + $originalRowKey = $SingleEnt.RowKey + $entityIndex = 0 + + while ($entitySize -gt $MaxRowSize) { + $newEntity = @{} + $newEntity['PartitionKey'] = $originalPartitionKey + $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" + $newEntity['OriginalEntityId'] = $originalRowKey + $newEntity['PartIndex'] = $entityIndex + $entityIndex++ - $splitInfo = @{ - OriginalHeader = $largePropertyName - SplitHeaders = $splitPropertyNames + $propertiesToRemove = @() + foreach ($key in $SingleEnt.Keys) { + $newEntitySize = [System.Text.Encoding]::UTF8.GetByteCount($($newEntity | ConvertTo-Json)) + if ($newEntitySize -lt $MaxRowSize) { + $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) + if ($propertySize -gt $MaxRowSize) { + $dataString = $SingleEnt[$key] + $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) + $splitData = @() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData = $splitData + $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) + } + + $splitPropertyNames = @() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames = $splitPropertyNames + "${key}_Part$i" + } + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $newEntity[$splitPropertyNames[$i]] = $splitData[$i] + } + } else { + $newEntity[$key] = $SingleEnt[$key] + } + $propertiesToRemove = $propertiesToRemove + $key + } + } + + foreach ($prop in $propertiesToRemove) { + $SingleEnt.Remove($prop) + } + + $rows = $rows + $newEntity + $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) } - $SingleEnt['SplitOverProps'] = ($splitInfo | ConvertTo-Json).ToString() - $SingleEnt.Remove($largePropertyName) - for ($i = 0; $i -lt $splitData.Count; $i++) { - $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] + if ($SingleEnt.Count -gt 0) { + $SingleEnt['RowKey'] = "$($originalRowKey)-part$entityIndex" + $SingleEnt['OriginalEntityId'] = $originalRowKey + $SingleEnt['PartIndex'] = $entityIndex + $SingleEnt['PartitionKey'] = $originalPartitionKey + + $rows = $rows + $SingleEnt } + foreach ($row in $rows) { + Write-Host 'Size is larger than 70kb, splitting entity into multiple rows.' + Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey)" + Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row + } + } else { Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt } @@ -53,7 +130,6 @@ function Add-CIPPAzDataTableEntity { } } else { Write-Host "THE ERROR IS $($_.Exception.ErrorCode)" - throw $_ } } 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 498441852f6f..847c5f1174c7 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 @@ -41,82 +41,13 @@ Function Invoke-AddIntuneTemplate { $TenantFilter = $Request.Query.TenantFilter $URLName = $Request.Query.URLName $ID = $Request.Query.id - switch ($URLName) { - 'deviceCompliancePolicies' { - $Type = 'deviceCompliancePolicies' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'managedAppPolicies' { - $Type = 'AppProtection' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'configurationPolicies' { - $Type = 'Catalog' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')?`$expand=settings" -tenantid $tenantfilter | Select-Object name, description, settings, platforms, technologies, templateReference - $TemplateJson = $Template | ConvertTo-Json -Depth 100 - $DisplayName = $Template.name - - } - 'windowsDriverUpdateProfiles' { - $Type = 'windowsDriverUpdateProfiles' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' - Write-Host ($Template | ConvertTo-Json) - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - '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' - Write-Host ($Template | ConvertTo-Json) - $DisplayName = $Template.displayName - $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress - } - 'groupPolicyConfigurations' { - $Type = 'Admin' - $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter - $DisplayName = $Template.displayName - $TemplateJsonItems = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues?`$expand=definition" -tenantid $tenantfilter - $TemplateJsonSource = foreach ($TemplateJsonItem in $TemplateJsonItems) { - $presentationValues = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues('$($TemplateJsonItem.id)')/presentationValues?`$expand=presentation" -tenantid $tenantfilter | ForEach-Object { - $obj = $_ - if ($obj.id) { - $PresObj = @{ - id = $obj.id - 'presentation@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')/presentations('$($obj.presentation.id)')" - } - if ($obj.values) { $PresObj['values'] = $obj.values } - if ($obj.value) { $PresObj['value'] = $obj.value } - if ($obj.'@odata.type') { $PresObj['@odata.type'] = $obj.'@odata.type' } - [pscustomobject]$PresObj - } - } - [PSCustomObject]@{ - 'definition@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')" - enabled = $TemplateJsonItem.enabled - presentationValues = @($presentationValues) - } - } - $inputvar = [pscustomobject]@{ - added = @($TemplateJsonSource) - updated = @() - deletedIds = @() - - } - - - $TemplateJson = (ConvertTo-Json -InputObject $inputvar -Depth 100 -Compress) - } - } + $Template = New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $ID $object = [PSCustomObject]@{ - Displayname = $DisplayName + Displayname = $Template.DisplayName Description = $Template.Description - RAWJson = $TemplateJson - Type = $Type + RAWJson = $Template.TemplateJson + Type = $Template.Type GUID = $GUID } | ConvertTo-Json $Table = Get-CippTable -tablename 'templates' diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 index c94431612970..a11384cf8e85 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntuneTemplates.ps1 @@ -31,7 +31,7 @@ Function Invoke-ListIntuneTemplates { $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json if ($Request.query.View) { $Templates = $Templates | ForEach-Object { - $data = $_.RAWJson | ConvertFrom-Json + $data = $_.RAWJson | ConvertFrom-Json -Depth 100 $data | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $_.Displayname -Force $data | Add-Member -NotePropertyName 'description' -NotePropertyValue $_.Description -Force $data | Add-Member -NotePropertyName 'Type' -NotePropertyValue $_.Type -Force @@ -46,7 +46,7 @@ Function Invoke-ListIntuneTemplates { # Associate values to output bindings by calling 'Push-OutputBinding'. Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ StatusCode = [HttpStatusCode]::OK - Body = ($Templates | ConvertTo-Json -Depth 10) + Body = ($Templates | ConvertTo-Json -Depth 100) }) } diff --git a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 index 4ce96415966d..b72e56a0f477 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 @@ -7,23 +7,65 @@ function Get-CIPPAzDataTableEntity { $First, $Skip, $Sort, - $Count + $Count ) + $Results = Get-AzDataTableEntity @PSBoundParameters - $Results = $Results | ForEach-Object { - $entity = $_ + $mergedResults = @{} + + foreach ($entity in $Results) { + if ($entity.OriginalEntityId) { + $entityId = $entity.OriginalEntityId + if (-not $mergedResults.ContainsKey($entityId)) { + $mergedResults[$entityId] = @{ + Parts = @() + } + } + $mergedResults[$entityId]['Parts'] = $mergedResults[$entityId]['Parts'] + @($entity) + } else { + $mergedResults[$entity.RowKey] = @{ + Entity = $entity + Parts = @() + } + } + } + + $finalResults = @() + foreach ($entityId in $mergedResults.Keys) { + $entityData = $mergedResults[$entityId] + if ($entityData.Parts.Count -gt 0) { + $fullEntity = [PSCustomObject]@{} + $parts = $entityData.Parts | Sort-Object PartIndex + foreach ($part in $parts) { + foreach ($key in $part.PSObject.Properties.Name) { + if ($key -notin @('OriginalEntityId', 'PartIndex', 'PartitionKey', 'RowKey', 'ETag', 'Timestamp')) { + if ($fullEntity.PSObject.Properties[$key]) { + $fullEntity | Add-Member -MemberType NoteProperty -Name $key -Value ($fullEntity.$key + $part.$key) -Force + } else { + $fullEntity | Add-Member -MemberType NoteProperty -Name $key -Value $part.$key + } + } + } + } + $fullEntity | Add-Member -MemberType NoteProperty -Name 'PartitionKey' -Value $parts[0].PartitionKey -Force + $fullEntity | Add-Member -MemberType NoteProperty -Name 'RowKey' -Value $entityId -Force + $finalResults = $finalResults + @($fullEntity) + } else { + $finalResults = $finalResults + @($entityData.Entity) + } + } + + foreach ($entity in $finalResults) { if ($entity.SplitOverProps) { $splitInfo = $entity.SplitOverProps | ConvertFrom-Json - $mergedData = -join ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ }) + $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force - $propsToRemove = $splitInfo.SplitHeaders + "SplitOverProps" - $entity = $entity | Select-Object * -ExcludeProperty $propsToRemove - $entity - } - else { - $entity + $propsToRemove = $splitInfo.SplitHeaders + 'SplitOverProps' + foreach ($prop in $propsToRemove) { + $entity.PSObject.Properties.Remove($prop) + } } } - - return $Results + + return $finalResults } diff --git a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 index d06a6ca94c5a..cbd216411068 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -27,12 +27,32 @@ function New-CIPPBackupTask { New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter } 'intuneconfig' { - #alert + $GraphURLS = @("https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?`$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,microsoft.graph.unsupportedDeviceConfiguration/originalEntityTypeName&`$expand=assignments&top=1000" + 'https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles' + "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?`$expand=assignments&top=999" + "https://graph.microsoft.com/beta/deviceAppManagement/mobileAppConfigurations?`$expand=assignments&`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" + 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' + ) + + $GraphURLS | ForEach-Object { + $URLName = (($_).split('?') | Select-Object -First 1) -replace 'https://graph.microsoft.com/beta/deviceManagement/', '' + New-GraphGetRequest -uri "$($_)" -tenantid $TenantFilter + } | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $_.ID + } + } + 'intunecompliance' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'deviceCompliancePolicies' -ID $_.ID + } + } + + 'intuneprotection' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'managedAppPolicies' -ID $_.ID + } } - 'intunecompliance' {} - 'intuneprotection' {} - 'CippWebhookAlerts' { Write-Host "Backup Webhook Alerts for $TenantFilter" $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' @@ -43,7 +63,7 @@ function New-CIPPBackupTask { $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } } - 'CippStandards' { + 'CippStandards' { Write-Host "Backup Standards for $TenantFilter" $Table = Get-CippTable -tablename 'standards' $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" diff --git a/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 new file mode 100644 index 000000000000..0707b9824400 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPIntuneTemplate.ps1 @@ -0,0 +1,83 @@ +function New-CIPPIntuneTemplate { + param( + $urlname, + $id, + $TenantFilter, + $ActionResults, + $CIPPURL + ) + switch ($URLName) { + 'deviceCompliancePolicies' { + $Type = 'deviceCompliancePolicies' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)?`$expand=scheduledActionsForRule(`$expand=scheduledActionConfigurations)" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'managedAppPolicies' { + $Type = 'AppProtection' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceAppManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'configurationPolicies' { + $Type = 'Catalog' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')?`$expand=settings" -tenantid $tenantfilter | Select-Object name, description, settings, platforms, technologies, templateReference + $TemplateJson = $Template | ConvertTo-Json -Depth 100 + $DisplayName = $Template.name + + } + 'windowsDriverUpdateProfiles' { + $Type = 'windowsDriverUpdateProfiles' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)/$($ID)" -tenantid $tenantfilter | Select-Object * -ExcludeProperty id, lastModifiedDateTime, '@odata.context', 'ScopeTagIds', 'supportsScopeTags', 'createdDateTime' + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + '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' + $DisplayName = $Template.displayName + $TemplateJson = ConvertTo-Json -InputObject $Template -Depth 100 -Compress + } + 'groupPolicyConfigurations' { + $Type = 'Admin' + $Template = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')" -tenantid $tenantfilter + $DisplayName = $Template.displayName + $TemplateJsonItems = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues?`$expand=definition" -tenantid $tenantfilter + $TemplateJsonSource = foreach ($TemplateJsonItem in $TemplateJsonItems) { + $presentationValues = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$($ID)')/definitionValues('$($TemplateJsonItem.id)')/presentationValues?`$expand=presentation" -tenantid $tenantfilter | ForEach-Object { + $obj = $_ + if ($obj.id) { + $PresObj = @{ + id = $obj.id + 'presentation@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')/presentations('$($obj.presentation.id)')" + } + if ($obj.values) { $PresObj['values'] = $obj.values } + if ($obj.value) { $PresObj['value'] = $obj.value } + if ($obj.'@odata.type') { $PresObj['@odata.type'] = $obj.'@odata.type' } + [pscustomobject]$PresObj + } + } + [PSCustomObject]@{ + 'definition@odata.bind' = "https://graph.microsoft.com/beta/deviceManagement/groupPolicyDefinitions('$($TemplateJsonItem.definition.id)')" + enabled = $TemplateJsonItem.enabled + presentationValues = @($presentationValues) + } + } + $inputvar = [pscustomobject]@{ + added = @($TemplateJsonSource) + updated = @() + deletedIds = @() + + } + + + $TemplateJson = (ConvertTo-Json -InputObject $inputvar -Depth 100 -Compress) + } + } + return [PSCustomObject]@{ + TemplateJson = $TemplateJson + DisplayName = $DisplayName + Description = $Template.description + Type = $Type + } +} diff --git a/Scheduler_UserTasks/function.json b/Scheduler_UserTasks/function.json index 017acb166958..f7af84092121 100644 --- a/Scheduler_UserTasks/function.json +++ b/Scheduler_UserTasks/function.json @@ -2,7 +2,7 @@ "bindings": [ { "name": "Timer", - "schedule": "0 */5 * * * *", + "schedule": "0 */15 * * * *", "direction": "in", "type": "timerTrigger" }, From 8f88e9d16841494ee8f1c2d93c679fb84b9b2f70 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Mon, 8 Jul 2024 13:12:20 +0200 Subject: [PATCH 31/33] Improve storing large storage --- Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 | 8 ++++---- Scheduler_UserTasks/function.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 8c3045f17579..651f54a42817 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -7,7 +7,7 @@ function Add-CIPPAzDataTableEntity { [switch]$CreateTableIfNotExists ) - $MaxRowSize = 1mb - 100kb + $MaxRowSize = 500000 - 100 #Maximum size of an entity $MaxSize = 30kb # maximum size of a property value foreach ($SingleEnt in $Entity) { @@ -63,6 +63,7 @@ function Add-CIPPAzDataTableEntity { $entityIndex = 0 while ($entitySize -gt $MaxRowSize) { + Write-Host "Entity size is $entitySize. Splitting entity into multiple parts." $newEntity = @{} $newEntity['PartitionKey'] = $originalPartitionKey $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" @@ -117,8 +118,7 @@ function Add-CIPPAzDataTableEntity { } foreach ($row in $rows) { - Write-Host 'Size is larger than 70kb, splitting entity into multiple rows.' - Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey)" + Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey). Our size is $([System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)))" Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row } } else { @@ -129,7 +129,7 @@ function Add-CIPPAzDataTableEntity { throw "Error processing entity: $($_.Exception.Message)." } } else { - Write-Host "THE ERROR IS $($_.Exception.ErrorCode)" + Write-Host "THE ERROR IS $($_.Exception.ErrorCode). The size of the entity is $entitySize." throw $_ } } diff --git a/Scheduler_UserTasks/function.json b/Scheduler_UserTasks/function.json index f7af84092121..017acb166958 100644 --- a/Scheduler_UserTasks/function.json +++ b/Scheduler_UserTasks/function.json @@ -2,7 +2,7 @@ "bindings": [ { "name": "Timer", - "schedule": "0 */15 * * * *", + "schedule": "0 */5 * * * *", "direction": "in", "type": "timerTrigger" }, From 68f12478c43bcd3df49e56f75f3a7a1c2c0a6d5f Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Mon, 8 Jul 2024 15:13:29 +0200 Subject: [PATCH 32/33] fixed issue with entityid --- .../Public/Add-CIPPAzDataTableEntity.ps1 | 8 +- Modules/CIPPCore/Public/New-CIPPBackup.ps1 | 2 +- Modules/CIPPCore/Public/New-CIPPRestore.ps1 | 17 +++ .../CIPPCore/Public/New-CIPPRestoreTask.ps1 | 127 ++++++++++++++++++ 4 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 Modules/CIPPCore/Public/New-CIPPRestore.ps1 create mode 100644 Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 651f54a42817..797f8846b7c0 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -66,7 +66,11 @@ function Add-CIPPAzDataTableEntity { Write-Host "Entity size is $entitySize. Splitting entity into multiple parts." $newEntity = @{} $newEntity['PartitionKey'] = $originalPartitionKey - $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" + if ($entityIndex -eq 0) { + $newEntity['RowKey'] = $originalRowKey + } else { + $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" + } $newEntity['OriginalEntityId'] = $originalRowKey $newEntity['PartIndex'] = $entityIndex $entityIndex++ @@ -118,7 +122,7 @@ function Add-CIPPAzDataTableEntity { } foreach ($row in $rows) { - Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey). Our size is $([System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)))" + Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey). Our size is $([System.Text.Encoding]::UTF8.GetByteCount($($row | ConvertTo-Json)))" Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row } } else { diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index 0893e0e89b15..b99530c6de79 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -68,7 +68,7 @@ function New-CIPPBackup { $Table = Get-CippTable -tablename 'ScheduledBackup' try { $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force - Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup for Conditional Access Policies' -Sev 'Debug' + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' $Result } catch { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Failed to create backup for Conditional Access Policies: $($_.Exception.Message)" -Sev 'Error' diff --git a/Modules/CIPPCore/Public/New-CIPPRestore.ps1 b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 new file mode 100644 index 000000000000..399377a02451 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPRestore.ps1 @@ -0,0 +1,17 @@ +function New-CIPPRestore { + [CmdletBinding()] + param ( + $TenantFilter, + $RestoreValues, + $APIName = 'CIPP Restore', + $ExecutingUser + ) + + Write-Host "Scheduled Restore psproperties: $(([pscustomobject]$ScheduledBackupValues).psobject.Properties)" + Write-LogMessage -user $ExecutingUser -API $APINAME -message 'Restored backup' -Sev 'Debug' + $RestoreData = foreach ($ScheduledBackup in ([pscustomobject]$ScheduledBackupValues).psobject.Properties.Name | Where-Object { $_ -notin 'email', 'webhook', 'psa', 'backup', 'overwrite' }) { + New-CIPPRestoreTask -Task $ScheduledBackup -TenantFilter $TenantFilter -backup $RestoreValues.backup.value -overwrite $RestoreValues.overwrite + } + return $RestoreData +} + diff --git a/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 new file mode 100644 index 000000000000..64b9c4701cd9 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPRestoreTask.ps1 @@ -0,0 +1,127 @@ +function New-CIPPRestoreTask { + [CmdletBinding()] + param ( + $Task, + $TenantFilter, + $backup, + $overwrite + ) + $Table = Get-CippTable -tablename 'ScheduledBackup' + $BackupData = Get-CIPPAzDataTableEntity @Table -Filter "RowKey eq '$backup'" + $RestoreData = switch ($Task) { + 'users' { + Write-Host "Restore users for $TenantFilter" + $currentUsers = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + $BackupUsers | ForEach-Object { + try { + if ($overwrite) { + $currentUsers | Where-Object { $_.id -eq $_.id } | ForEach-Object { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/users/$($_.id)" -tenantid $TenantFilter -body $_ -type PATCH + Write-LogMessage -message "Restored $($_.userprincipalname) from backup" -Sev 'info' + "Restored $($_.userprincipalname) from backup" + } + } else { + if ($currentUsers.id -notin $_.id) { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/beta/users' -tenantid $TenantFilter -body $_ -type POST + Write-LogMessage -message "Restored $($_.userprincipalname) from backup" -Sev 'info' + "Restored $($_.userprincipalname) from backup" + + } else { + Write-LogMessage -message "User $($_.userPrincipalName) already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' + "User $($_.userPrincipalName) already exists in tenant $TenantFilter and overwrite is disabled" + } + } + } catch { + "Could not restore user $($_.userPrincipalName): $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore user $($_.userPrincipalName): $($_.Exception.Message) " -Sev 'error' + } + } + } + 'groups' { + Write-Host "Restore groups for $TenantFilter" + $Groups = New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + $BackupGroups | ForEach-Object { + try { + if ($overwrite) { + $currentUsers | Where-Object { $_.id -eq $_.id } | ForEach-Object { + New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/groups/$($_.id)" -tenantid $TenantFilter -body $_ -type PATCH + Write-LogMessage -message "Restored $($_.userprincipalname) from backup" -Sev 'info' + "Restored group $($_.displayName) from backup" + } + } else { + if ($currentUsers.id -notin $_.id) { + New-GraphPOSTRequest -uri 'https://graph.microsoft.com/groups/' -tenantid $TenantFilter -body $_ -type POST + Write-LogMessage -message "Restored $($_.userprincipalname) from backup" -Sev 'info' + "Restored group $($_.displayName) from backup" + + } else { + Write-LogMessage -message "group $($_.group) already exists in tenant $TenantFilter and overwrite is disabled" -Sev 'info' + "group $($_.displayName) already exists in tenant $TenantFilter and overwrite is disabled" + } + } + } catch { + "Could not restore user $($_.userPrincipalName): $($_.Exception.Message) " + Write-LogMessage -user $ExecutingUser -API $APINAME -message "Could not restore user $($_.userPrincipalName): $($_.Exception.Message) " -Sev 'error' + } + } + } + 'ca' { + Write-Host "Backup Conditional Access Policies for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/policies?$top=999' -tenantid $TenantFilter + } + 'namedlocations' { + Write-Host "Backup Named Locations for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/namedLocations?$top=999' -tenantid $TenantFilter + } + 'authstrengths' { + Write-Host "Backup Authentication Strength Policies for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/conditionalAccess/authenticationStrength/policies' -tenantid $TenantFilter + } + 'intuneconfig' { + $GraphURLS = @("https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?`$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,microsoft.graph.unsupportedDeviceConfiguration/originalEntityTypeName&`$expand=assignments&top=1000" + 'https://graph.microsoft.com/beta/deviceManagement/windowsDriverUpdateProfiles' + "https://graph.microsoft.com/beta/deviceManagement/groupPolicyConfigurations?`$expand=assignments&top=999" + "https://graph.microsoft.com/beta/deviceAppManagement/mobileAppConfigurations?`$expand=assignments&`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" + 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' + ) + + $GraphURLS | ForEach-Object { + $URLName = (($_).split('?') | Select-Object -First 1) -replace 'https://graph.microsoft.com/beta/deviceManagement/', '' + New-GraphGetRequest -uri "$($_)" -tenantid $TenantFilter + } | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName $URLName -ID $_.ID + } + } + 'intunecompliance' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'deviceCompliancePolicies' -ID $_.ID + } + } + + 'intuneprotection' { + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/deviceAppManagement/managedAppPolicies?$top=999' -tenantid $TenantFilter | ForEach-Object { + New-CIPPIntuneTemplate -TenantFilter $TenantFilter -URLName 'managedAppPolicies' -ID $_.ID + } + } + + 'CippWebhookAlerts' { + Write-Host "Backup Webhook Alerts for $TenantFilter" + $WebhookTable = Get-CIPPTable -TableName 'WebhookRules' + Get-CIPPAzDataTableEntity @WebhookTable | Where-Object { $TenantFilter -in ($_.Tenants | ConvertFrom-Json).fullvalue.defaultDomainName } + } + 'CippScriptedAlerts' { + Write-Host "Backup Scripted Alerts for $TenantFilter" + $ScheduledTasks = Get-CIPPTable -TableName 'ScheduledTasks' + Get-CIPPAzDataTableEntity @ScheduledTasks | Where-Object { $_.hidden -eq $true -and $_.command -like 'Get-CippAlert*' -and $TenantFilter -in $_.Tenant } + } + 'CippStandards' { + Write-Host "Backup Standards for $TenantFilter" + $Table = Get-CippTable -tablename 'standards' + $Filter = "PartitionKey eq 'standards' and RowKey eq '$($TenantFilter)'" + (Get-CIPPAzDataTableEntity @Table -Filter $Filter) + } + + } + return $RestoreData +} + From 09931d90224aaa36a8e3bcf70cdc5f7ae01a2235 Mon Sep 17 00:00:00 2001 From: KelvinTegelaar Date: Mon, 8 Jul 2024 16:52:49 +0200 Subject: [PATCH 33/33] fixesd backup data --- .../Public/Add-CIPPAzDataTableEntity.ps1 | 48 ++++++++++--------- .../Push-ExecScheduledCommand.ps1 | 4 +- .../Public/Get-CIPPAzDatatableEntity.ps1 | 26 ++++++---- 3 files changed, 44 insertions(+), 34 deletions(-) diff --git a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index 797f8846b7c0..ffd25c2b50a8 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -7,57 +7,61 @@ function Add-CIPPAzDataTableEntity { [switch]$CreateTableIfNotExists ) - $MaxRowSize = 500000 - 100 #Maximum size of an entity - $MaxSize = 30kb # maximum size of a property value + $MaxRowSize = 500000 - 100 # Maximum size of an entity + $MaxSize = 30kb # Maximum size of a property value foreach ($SingleEnt in $Entity) { try { - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt -ErrorAction Stop + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt -ErrorAction Stop } catch [System.Exception] { if ($_.Exception.ErrorCode -eq 'PropertyValueTooLarge' -or $_.Exception.ErrorCode -eq 'EntityTooLarge') { try { - $largePropertyNames = @() + $largePropertyNames = [System.Collections.ArrayList]::new() $entitySize = 0 foreach ($key in $SingleEnt.Keys) { $propertySize = [System.Text.Encoding]::UTF8.GetByteCount($SingleEnt[$key].ToString()) $entitySize = $entitySize + $propertySize if ($propertySize -gt $MaxSize) { - $largePropertyNames = $largePropertyNames + $key + $largePropertyNames.Add($key) } + } if ($largePropertyNames.Count -gt 0) { + $splitInfoList = [System.Collections.ArrayList]@() foreach ($largePropertyName in $largePropertyNames) { $dataString = $SingleEnt[$largePropertyName] $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) - $splitData = @() + $splitData = [System.Collections.ArrayList]@() for ($i = 0; $i -lt $splitCount; $i++) { $start = $i * $MaxSize - $splitData = $splitData + $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null } - $splitPropertyNames = @() + $splitPropertyNames = [System.Collections.ArrayList]@() for ($i = 0; $i -lt $splitData.Count; $i++) { - $splitPropertyNames = $splitPropertyNames + "${largePropertyName}_Part$i" + $splitPropertyNames.Add("${largePropertyName}_Part$i") > $null } $splitInfo = @{ OriginalHeader = $largePropertyName SplitHeaders = $splitPropertyNames } - $SingleEnt['SplitOverProps'] = ($splitInfo | ConvertTo-Json).ToString() + $splitInfoList.Add($splitInfo) > $null $SingleEnt.Remove($largePropertyName) for ($i = 0; $i -lt $splitData.Count; $i++) { $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] } } + + $SingleEnt['SplitOverProps'] = ($splitInfoList | ConvertTo-Json).ToString() } # Check if the entity is still too large $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) if ($entitySize -gt $MaxRowSize) { - $rows = @() + $rows = [System.Collections.ArrayList]@() $originalPartitionKey = $SingleEnt.PartitionKey $originalRowKey = $SingleEnt.RowKey $entityIndex = 0 @@ -75,7 +79,7 @@ function Add-CIPPAzDataTableEntity { $newEntity['PartIndex'] = $entityIndex $entityIndex++ - $propertiesToRemove = @() + $propertiesToRemove = [System.Collections.ArrayList]@() foreach ($key in $SingleEnt.Keys) { $newEntitySize = [System.Text.Encoding]::UTF8.GetByteCount($($newEntity | ConvertTo-Json)) if ($newEntitySize -lt $MaxRowSize) { @@ -83,15 +87,15 @@ function Add-CIPPAzDataTableEntity { if ($propertySize -gt $MaxRowSize) { $dataString = $SingleEnt[$key] $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) - $splitData = @() + $splitData = [System.Collections.ArrayList]@() for ($i = 0; $i -lt $splitCount; $i++) { $start = $i * $MaxSize - $splitData = $splitData + $dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start)) + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null } - $splitPropertyNames = @() + $splitPropertyNames = [System.Collections.ArrayList]@() for ($i = 0; $i -lt $splitData.Count; $i++) { - $splitPropertyNames = $splitPropertyNames + "${key}_Part$i" + $splitPropertyNames.Add("${key}_Part$i") > $null } for ($i = 0; $i -lt $splitData.Count; $i++) { @@ -100,7 +104,7 @@ function Add-CIPPAzDataTableEntity { } else { $newEntity[$key] = $SingleEnt[$key] } - $propertiesToRemove = $propertiesToRemove + $key + $propertiesToRemove.Add($key) > $null } } @@ -108,7 +112,7 @@ function Add-CIPPAzDataTableEntity { $SingleEnt.Remove($prop) } - $rows = $rows + $newEntity + $rows.Add($newEntity) > $null $entitySize = [System.Text.Encoding]::UTF8.GetByteCount($($SingleEnt | ConvertTo-Json)) } @@ -118,19 +122,19 @@ function Add-CIPPAzDataTableEntity { $SingleEnt['PartIndex'] = $entityIndex $SingleEnt['PartitionKey'] = $originalPartitionKey - $rows = $rows + $SingleEnt + $rows.Add($SingleEnt) > $null } foreach ($row in $rows) { Write-Host "current entity is $($row.RowKey) with $($row.PartitionKey). Our size is $([System.Text.Encoding]::UTF8.GetByteCount($($row | ConvertTo-Json)))" - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $row } } else { - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt + Add-AzDataTableEntity -Context $Context -Force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt } } catch { - throw "Error processing entity: $($_.Exception.Message)." + throw "Error processing entity: $($_.Exception.Message) Linenumner: $($_.InvocationInfo.ScriptLineNumber)" } } else { Write-Host "THE ERROR IS $($_.Exception.ErrorCode). The size of the entity is $entitySize." diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index e94a5d904bf1..f157dd0884b9 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -14,7 +14,7 @@ function Push-ExecScheduledCommand { Write-Host "Started Task: $($Item.Command) for tenant: $tenant" try { try { - Write-Host "Starting task: $($Item.Command) with parameters: " + Write-Host "Starting task: $($Item.Command) with parameters: $($commandParameters | ConvertTo-Json)" $results = & $Item.Command @commandParameters } catch { $results = "Task Failed: $($_.Exception.Message)" @@ -112,4 +112,4 @@ function Push-ExecScheduledCommand { if ($TaskType -ne 'Alert') { Write-LogMessage -API 'Scheduler_UserTasks' -tenant $tenant -message "Successfully executed task: $($task.Name)" -sev Info } -} \ No newline at end of file +} diff --git a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 index b72e56a0f477..0781e820a125 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 @@ -13,23 +13,25 @@ function Get-CIPPAzDataTableEntity { $Results = Get-AzDataTableEntity @PSBoundParameters $mergedResults = @{} + # First pass: Collect all parts and complete entities foreach ($entity in $Results) { if ($entity.OriginalEntityId) { $entityId = $entity.OriginalEntityId if (-not $mergedResults.ContainsKey($entityId)) { $mergedResults[$entityId] = @{ - Parts = @() + Parts = New-Object 'System.Collections.ArrayList' } } - $mergedResults[$entityId]['Parts'] = $mergedResults[$entityId]['Parts'] + @($entity) + $mergedResults[$entityId]['Parts'].Add($entity) > $null } else { $mergedResults[$entity.RowKey] = @{ Entity = $entity - Parts = @() + Parts = New-Object 'System.Collections.ArrayList' } } } + # Second pass: Reassemble entities from parts $finalResults = @() foreach ($entityId in $mergedResults.Keys) { $entityData = $mergedResults[$entityId] @@ -38,7 +40,7 @@ function Get-CIPPAzDataTableEntity { $parts = $entityData.Parts | Sort-Object PartIndex foreach ($part in $parts) { foreach ($key in $part.PSObject.Properties.Name) { - if ($key -notin @('OriginalEntityId', 'PartIndex', 'PartitionKey', 'RowKey', 'ETag', 'Timestamp')) { + if ($key -notin @('OriginalEntityId', 'PartIndex', 'PartitionKey', 'RowKey')) { if ($fullEntity.PSObject.Properties[$key]) { $fullEntity | Add-Member -MemberType NoteProperty -Name $key -Value ($fullEntity.$key + $part.$key) -Force } else { @@ -55,15 +57,19 @@ function Get-CIPPAzDataTableEntity { } } + # Third pass: Process split properties and remerge them foreach ($entity in $finalResults) { if ($entity.SplitOverProps) { - $splitInfo = $entity.SplitOverProps | ConvertFrom-Json - $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) - $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force - $propsToRemove = $splitInfo.SplitHeaders + 'SplitOverProps' - foreach ($prop in $propsToRemove) { - $entity.PSObject.Properties.Remove($prop) + $splitInfoList = $entity.SplitOverProps | ConvertFrom-Json + foreach ($splitInfo in $splitInfoList) { + $mergedData = [string]::Join('', ($splitInfo.SplitHeaders | ForEach-Object { $entity.$_ })) + $entity | Add-Member -NotePropertyName $splitInfo.OriginalHeader -NotePropertyValue $mergedData -Force + $propsToRemove = $splitInfo.SplitHeaders + foreach ($prop in $propsToRemove) { + $entity.PSObject.Properties.Remove($prop) + } } + $entity.PSObject.Properties.Remove('SplitOverProps') } }