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" } ] diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index 6b1f6429af88..f94959dc3ac6 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": [ @@ -159,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" } ] }, { 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/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-CIPPAzDataTableEntity.ps1 b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 index befa8155df6c..ffd25c2b50a8 100644 --- a/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPAzDataTableEntity.ps1 @@ -6,54 +6,138 @@ function Add-CIPPAzDataTableEntity { [switch]$Force, [switch]$CreateTableIfNotExists ) - + + $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 { - $MaxSize = 30kb - $largePropertyName = $null + $largePropertyNames = [System.Collections.ArrayList]::new() + $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.Add($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) { + $splitInfoList = [System.Collections.ArrayList]@() + foreach ($largePropertyName in $largePropertyNames) { + $dataString = $SingleEnt[$largePropertyName] + $splitCount = [math]::Ceiling($dataString.Length / $MaxSize) + $splitData = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null + } + + $splitPropertyNames = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames.Add("${largePropertyName}_Part$i") > $null + } - $splitPropertyNames = 1..$splitData.Count | ForEach-Object { - "${largePropertyName}_Part$_" + $splitInfo = @{ + OriginalHeader = $largePropertyName + SplitHeaders = $splitPropertyNames + } + $splitInfoList.Add($splitInfo) > $null + $SingleEnt.Remove($largePropertyName) + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $SingleEnt[$splitPropertyNames[$i]] = $splitData[$i] + } } - $splitInfo = @{ - OriginalHeader = $largePropertyName - SplitHeaders = $splitPropertyNames + $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 = [System.Collections.ArrayList]@() + $originalPartitionKey = $SingleEnt.PartitionKey + $originalRowKey = $SingleEnt.RowKey + $entityIndex = 0 + + while ($entitySize -gt $MaxRowSize) { + Write-Host "Entity size is $entitySize. Splitting entity into multiple parts." + $newEntity = @{} + $newEntity['PartitionKey'] = $originalPartitionKey + if ($entityIndex -eq 0) { + $newEntity['RowKey'] = $originalRowKey + } else { + $newEntity['RowKey'] = "$($originalRowKey)-part$entityIndex" + } + $newEntity['OriginalEntityId'] = $originalRowKey + $newEntity['PartIndex'] = $entityIndex + $entityIndex++ + + $propertiesToRemove = [System.Collections.ArrayList]@() + 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 = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitCount; $i++) { + $start = $i * $MaxSize + $splitData.Add($dataString.Substring($start, [Math]::Min($MaxSize, $dataString.Length - $start))) > $null + } + + $splitPropertyNames = [System.Collections.ArrayList]@() + for ($i = 0; $i -lt $splitData.Count; $i++) { + $splitPropertyNames.Add("${key}_Part$i") > $null + } + + for ($i = 0; $i -lt $splitData.Count; $i++) { + $newEntity[$splitPropertyNames[$i]] = $splitData[$i] + } + } else { + $newEntity[$key] = $SingleEnt[$key] + } + $propertiesToRemove.Add($key) > $null + } + } + + foreach ($prop in $propertiesToRemove) { + $SingleEnt.Remove($prop) + } + + $rows.Add($newEntity) > $null + $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.Add($SingleEnt) > $null } - Add-AzDataTableEntity -context $Context -force:$Force -CreateTableIfNotExists:$CreateTableIfNotExists -Entity $SingleEnt + 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 + } + } else { + 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)" - + Write-Host "THE ERROR IS $($_.Exception.ErrorCode). The size of the entity is $entitySize." throw $_ } } diff --git a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 index 921488c45f08..7701a5ff68cb 100644 --- a/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 +++ b/Modules/CIPPCore/Public/Add-CIPPDelegatedPermission.ps1 @@ -3,17 +3,21 @@ function Add-CIPPDelegatedPermission { param( $RequiredResourceAccess, $ApplicationId, + $NoTranslateRequired, $Tenantfilter ) Write-Host 'Adding Delegated Permissions' 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 +26,22 @@ 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 { + 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 ' ' + } + $OldScope = ($CurrentDelegatedScopes | Where-Object -Property Resourceid -EQ $svcPrincipalId.id) if (!$OldScope) { 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/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/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/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/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/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/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/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecExtensionsConfig.ps1 index f40536cebcb4..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 @@ -7,25 +7,26 @@ Function Invoke-ExecExtensionsConfig { .ROLE CIPP.Extension.ReadWrite #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Scope = 'Function')] [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-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.' + 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 + 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 +36,31 @@ Function Invoke-ExecExtensionsConfig { } $Table = Get-CIPPTable -TableName Extensionsconfig - 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 '') { - Write-Host 'Not sending to keyvault. Key previously set or left blank.' + foreach ($APIKey in ([pscustomobject]$Request.Body).psobject.properties.name) { + Write-Information "Working on $apikey" + if ($Request.Body.$APIKey.APIKey -eq 'SentToKeyVault' -or $Request.Body.$APIKey.APIKey -eq '') { + 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" - if ($request.body.$APIKey.APIKey) { + 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' $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 -AsPlainText -Force -String $Request.Body.$APIKey.APIKey) } } - $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' 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/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/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/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..97ca6fe52147 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Groups/Invoke-ListGroupSenderAuthentication.ps1 @@ -0,0 +1,46 @@ +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 + $GroupType = $Request.query.Type + + $params = @{ + Identity = $groupid + } + + + try { + 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 { + $state = $true + } + + # 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 = [HttpStatusCode]::OK + Body = @{ allowedToReceiveExternal = !$state } + }) +} \ No newline at end of file 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/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/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/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/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') { 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 } }) 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..0781e820a125 100644 --- a/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPAzDatatableEntity.ps1 @@ -7,23 +7,71 @@ function Get-CIPPAzDataTableEntity { $First, $Skip, $Sort, - $Count + $Count ) + $Results = Get-AzDataTableEntity @PSBoundParameters - $Results = $Results | ForEach-Object { - $entity = $_ - if ($entity.SplitOverProps) { - $splitInfo = $entity.SplitOverProps | ConvertFrom-Json - $mergedData = -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 + $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 = New-Object 'System.Collections.ArrayList' + } + } + $mergedResults[$entityId]['Parts'].Add($entity) > $null + } else { + $mergedResults[$entity.RowKey] = @{ + Entity = $entity + Parts = New-Object 'System.Collections.ArrayList' + } + } + } + + # Second pass: Reassemble entities from 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')) { + 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) } - else { - $entity + } + + # Third pass: Process split properties and remerge them + foreach ($entity in $finalResults) { + if ($entity.SplitOverProps) { + $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') } } - - return $Results + + return $finalResults } 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)'" 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/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/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 +} 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 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 477ea7c2e690..b99530c6de79 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -4,13 +4,14 @@ function New-CIPPBackup { $backupType, $StorageOutput = 'default', $TenantFilter, + $ScheduledBackupValues, $APIName = 'CIPP Backup', $ExecutingUser ) $BackupData = switch ($backupType) { #If backup type is CIPP, create CIPP backup. - 'CIPP' { + 'CIPP' { try { $BackupTables = @( 'bpa' @@ -26,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' @@ -42,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)" } @@ -50,36 +51,28 @@ 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 + } + 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 { + $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force + 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' + [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..cbd216411068 --- /dev/null +++ b/Modules/CIPPCore/Public/New-CIPPBackupTask.ps1 @@ -0,0 +1,76 @@ +function New-CIPPBackupTask { + [CmdletBinding()] + param ( + $Task, + $TenantFilter + ) + + $BackupData = switch ($Task) { + 'users' { + Write-Host "Backup users for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999' -tenantid $TenantFilter + } + 'groups' { + Write-Host "Backup groups for $TenantFilter" + New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/groups?$top=999' -tenantid $TenantFilter + } + '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 $BackupData +} + 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/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 +} + 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/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", 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)" + } +} diff --git a/Modules/CIPPCore/Public/SAMManifest.json b/Modules/CIPPCore/Public/SAMManifest.json index d545a87d25a5..50a03c019af1 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": [ @@ -145,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" } ] }, { @@ -159,14 +166,17 @@ "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" } ] }, { "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" } ] }, { diff --git a/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 new file mode 100644 index 000000000000..ad6a9b115321 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPSPOTenant.ps1 @@ -0,0 +1,92 @@ +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') { + $PropertyToSet = $Properties[$Property].ToString().ToLower() + } else { + $PropertyToSet = $Properties[$Property] + } + $xml = @" + + $($PropertyToSet) + +"@ + $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 + } + } +} 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 + } + } + } +} + + + + + 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 + } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 index 17e735496e88..cecc5e565a4f 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardEnableLitigationHold.ps1 @@ -29,7 +29,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) { 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/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/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/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/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..bb12d561104a --- /dev/null +++ b/Modules/CippExtensions/Public/Hudu/Get-HuduFieldMapping.ps1 @@ -0,0 +1,66 @@ +function Get-HuduFieldMapping { + [CmdletBinding()] + param ( + $CIPPMapping + ) + + $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 Data Type. A new Rich Text asset layout field will be created if it does not exist.' + } + ) + $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' + } + ) + + $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 Asset Layouts, error: $Message " -Level Error -tenant 'CIPP' -API 'HuduMapping' + $AssetLayouts = @(@{name = "Could not get Hudu Asset Layouts, 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 + +} 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..9a3b6949b056 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 } + + + $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' + 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.' } 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" },