diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000000..b0c2c5085172 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +insert_final_newline = true + +[*.{ps1, psd1, psm1}] +indent_size = 4 +end_of_line = crlf +trim_trailing_whitespace = true + +[*.json] +indent_size = 2 +end_of_line = crlf +trim_trailing_whitespace = true + +[*.{md, txt}] +end_of_line = crlf +max_line_length = off +trim_trailing_whitespace = false diff --git a/.github/workflows/dev_cippacnqv.yml b/.github/workflows/dev_cippacnqv.yml new file mode 100644 index 000000000000..67a58b70c7ff --- /dev/null +++ b/.github/workflows/dev_cippacnqv.yml @@ -0,0 +1,39 @@ +# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action +# More GitHub Actions for Azure: https://github.com/Azure/actions + +name: Build and deploy Powershell project to Azure Function App - cippacnqv + +on: + push: + branches: + - dev + workflow_dispatch: + +env: + AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root + +jobs: + deploy: + runs-on: windows-latest + permissions: + id-token: write #This is required for requesting the JWT + + steps: + - name: 'Checkout GitHub Action' + uses: actions/checkout@v4 + + - name: Login to Azure + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZUREAPPSERVICE_CLIENTID_6085081ED1124B799258E9FF743FF4B9 }} + tenant-id: ${{ secrets.AZUREAPPSERVICE_TENANTID_9BDB2DDBFAFA4BC19C20A58B204BFAF3 }} + subscription-id: ${{ secrets.AZUREAPPSERVICE_SUBSCRIPTIONID_02B5224812794971B05EDD557AF2B867 }} + + - name: 'Run Azure Functions Action' + uses: Azure/functions-action@v1 + id: fa + with: + app-name: 'cippacnqv' + slot-name: 'Production' + package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }} + \ No newline at end of file diff --git a/Cache_SAMSetup/SAMManifest.json b/Cache_SAMSetup/SAMManifest.json index 82bab306ef89..b6b291da57b4 100644 --- a/Cache_SAMSetup/SAMManifest.json +++ b/Cache_SAMSetup/SAMManifest.json @@ -157,7 +157,9 @@ { "id": "885f682f-a990-4bad-a642-36736a74b0c7", "type": "Scope" }, { "id": "913b9306-0ce1-42b8-9137-6a7df690a760", "type": "Role" }, { "id": "cb8f45a0-5c2e-4ea1-b803-84b870a7d7ec", "type": "Scope" }, - { "id": "4c06a06a-098a-4063-868e-5dfee3827264", "type": "Scope" } + { "id": "4c06a06a-098a-4063-868e-5dfee3827264", "type": "Scope" }, + { "id": "1bfefb4e-e0b5-418b-a88f-73c46d2cc8e9", "type": "Role" }, + { "id": "e67e6727-c080-415e-b521-e3f35d5248e9", "type": "Scope" } ] }, { diff --git a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 index 1336247272bb..acdaba4e0cee 100644 --- a/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 +++ b/Modules/CIPPCore/Public/Authentication/Test-CIPPAccess.ps1 @@ -3,7 +3,7 @@ function Test-CIPPAccess { $Request, [switch]$TenantList ) - + if ($Request.Params.CIPPEndpoint -eq 'ExecSAMSetup') { return $true } if (!$Request.Headers.'x-ms-client-principal') { # Direct API Access $CustomRoles = @('CIPP-API') @@ -47,7 +47,6 @@ function Test-CIPPAccess { $Permission.AllowedTenants | Where-Object { $Permission.BlockedTenants -notcontains $_ } } } - Write-Information ($LimitedTenantList | ConvertTo-Json) return $LimitedTenantList } @@ -77,11 +76,10 @@ function Test-CIPPAccess { } else { $Tenant = ($Tenants | Where-Object { $Request.Query.TenantFilter -eq $_.customerId -or $Request.Body.TenantFilter -eq $_.customerId -or $Request.Query.TenantFilter -eq $_.defaultDomainName -or $Request.Body.TenantFilter -eq $_.defaultDomainName }).customerId if ($Role.AllowedTenants -contains 'AllTenants') { - $AllowedTenants = $Tenants + $AllowedTenants = $Tenants.customerId } else { $AllowedTenants = $Role.AllowedTenants } - if ($Tenant) { $TenantAllowed = $AllowedTenants -contains $Tenant -and $Role.BlockedTenants -notcontains $Tenant if (!$TenantAllowed) { continue } diff --git a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 index ef4357efefd5..209432df45dd 100644 --- a/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 +++ b/Modules/CIPPCore/Public/CippQueue/Invoke-ListCippQueue.ps1 @@ -41,6 +41,7 @@ function Invoke-ListCippQueue { $TotalCompleted = $TaskStatus.Completed ?? 0 $TotalFailed = $TaskStatus.Failed ?? 0 $TotalRunning = $TaskStatus.Running ?? 0 + if ($Queue.TotalTasks -eq 0) { $Queue.TotalTasks = 1 } [PSCustomObject]@{ PartitionKey = $Queue.PartitionKey diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogBundleProcessing.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogBundleProcessing.ps1 new file mode 100644 index 000000000000..d8bf577eac0d --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-AuditLogBundleProcessing.ps1 @@ -0,0 +1,48 @@ +function Push-AuditLogBundleProcessing { + Param($Item) + $TenantFilter = $Item.TenantFilter + Write-Information "Audit log tenant filter: $TenantFilter" + $ConfigTable = get-cipptable -TableName 'WebhookRules' + $ConfigEntries = Get-CIPPAzDataTableEntity @ConfigTable + #$WebhookIncoming = Get-CIPPTable -TableName 'WebhookIncoming' + $SchedulerConfig = Get-CIPPTable -TableName 'SchedulerConfig' + $CIPPURL = Get-CIPPAzDataTableEntity @SchedulerConfig -Filter "PartitionKey eq 'webhookcreation'" | Select-Object -First 1 -ExpandProperty CIPPURL + + $Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') } | ForEach-Object { + [pscustomobject]@{ + Tenants = ($_.Tenants | ConvertFrom-Json).fullValue + Conditions = $_.Conditions + Actions = $_.Actions + LogType = $_.Type + } + } + + if (($Configuration | Measure-Object).Count -eq 0) { + Write-Information "No configuration found for tenant $TenantFilter" + return + } + + $LogTypes = $Configuration.LogType | Select-Object -Unique + foreach ($LogType in $LogTypes) { + Write-Information "Querying for log type: $LogType" + try { + $DataToProcess = (Test-CIPPAuditLogRules -TenantFilter $TenantFilter -LogType $LogType).DataToProcess + + Write-Information "Webhook: Data to process found: $($DataToProcess.count) items" + foreach ($AuditLog in $DataToProcess) { + Write-Information "Processing $($item.operation)" + $Webhook = @{ + Data = $AuditLog + CIPPURL = [string]$CIPPURL + TenantFilter = $TenantFilter + } + #Add-CIPPAzDataTableEntity @WebhookIncoming -Entity $Entity -Force + #Write-Information ($AuditLog | ConvertTo-Json -Depth 10) + Invoke-CippWebhookProcessing @Webhook + } + } catch { + #Write-LogMessage -API 'Webhooks' -message 'Error processing webhooks' -sev Error -LogData (Get-CippException -Exception $_) + Write-Host ( 'Audit log error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message) + } + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-PublicWebhookProcess.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-PublicWebhookProcess.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-PublicWebhookProcess.ps1 rename to Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-PublicWebhookProcess.ps1 diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-Schedulerwebhookcreation.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-Schedulerwebhookcreation.ps1 similarity index 82% rename from Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-Schedulerwebhookcreation.ps1 rename to Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-Schedulerwebhookcreation.ps1 index 8c56ccb98971..47aa38b1a072 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-Schedulerwebhookcreation.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Webhooks/Push-Schedulerwebhookcreation.ps1 @@ -23,7 +23,7 @@ function Push-Schedulerwebhookcreation { foreach ($Tenant in $Tenants) { Write-Host "Working on $Tenant - $($Row.tenantid)" #use the queueitem to see if we already have a webhook for this tenant + webhooktype. If we do, delete this row from SchedulerConfig. - $Webhook = Get-CIPPAzDataTableEntity @WebhookTable -Filter "PartitionKey eq '$Tenant' and Version eq '2' and Resource eq '$($Row.webhookType)'" + $Webhook = Get-CIPPAzDataTableEntity @WebhookTable -Filter "PartitionKey eq '$Tenant' and Version eq '3' and Resource eq '$($Row.webhookType)'" if ($Webhook) { Write-Host "Found existing webhook for $Tenant - $($Row.webhookType)" if ($Row.tenantid -ne 'AllTenants') { @@ -32,17 +32,14 @@ function Push-Schedulerwebhookcreation { } else { Write-Host "No existing webhook for $Tenant - $($Row.webhookType) - Time to create." try { - $NewSub = New-CIPPGraphSubscription -TenantFilter $Tenant -EventType $Row.webhookType -BaseURL $Row.CIPPURL -auditLogAPI $true + $NewSub = New-CIPPGraphSubscription -TenantFilter $Tenant -EventType $Row.webhookType -auditLogAPI $true if ($NewSub.Success -and $Row.tenantid -ne 'AllTenants') { Remove-AzDataTableEntity @Table -Entity $Row } else { Write-Host "Failed to create webhook for $Tenant - $($Row.webhookType) - $($_.Exception.Message)" - Write-LogMessage -message "Failed to create webhook for $Tenant - $($Row.webhookType)" -Sev 'Error' -LogData $_.Exception } } catch { Write-Host "Failed to create webhook for $Tenant - $($Row.webhookType): $($_.Exception.Message)" - Write-LogMessage -message "Failed to create webhook for $Tenant - $($Row.webhookType)" -Sev 'Error' -LogData $_.Exception - } } 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 a0b21d1f8de9..90b7e41bc4c9 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 @@ -1,6 +1,6 @@ using namespace System.Net -Function Invoke-ExecAddAlert { +Function Invoke-ExecListBackup { <# .FUNCTIONALITY Entrypoint diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 new file mode 100644 index 000000000000..2d04df48933c --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Core/Invoke-ExecSetCIPPAutoBackup.ps1 @@ -0,0 +1,43 @@ +using namespace System.Net + +Function Invoke-ExecSetCIPPAutoBackup { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + CIPP.Backup.Read + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + $unixtime = [int64](([datetime]::UtcNow) - (Get-Date '1/1/1970')).TotalSeconds + if ($Request.query.Enabled -eq 'True') { + $Table = Get-CIPPTable -TableName 'ScheduledTasks' + $AutomatedCIPPBackupTask = Get-AzDataTableEntity @table -Filter "Name eq 'Automated CIPP Backup'" + $task = @{ + RowKey = $AutomatedCIPPBackupTask.RowKey + PartitionKey = 'ScheduledTask' + } + Remove-AzDataTableEntity @Table -Entity $task | Out-Null + + $TaskBody = @{ + TenantFilter = 'AllTenants' + Name = 'Automated CIPP Backup' + Command = @{ + value = 'New-CIPPBackup' + label = 'New-CIPPBackup' + } + Parameters = @{ backupType = 'CIPP' } + ScheduledTime = $unixtime + Recurrence = '1d' + } + Add-CIPPScheduledTask -Task $TaskBody -hidden $false + $Result = @{ 'Results' = 'Scheduled Task Successfully created' } + } + 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]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Result + }) + +} diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-RemoveScheduledItem.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-RemoveScheduledItem.ps1 index 257902f03726..f21b1b88e275 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-RemoveScheduledItem.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Scheduler/Invoke-RemoveScheduledItem.ps1 @@ -14,8 +14,6 @@ Function Invoke-RemoveScheduledItem { RowKey = $Request.Query.ID PartitionKey = 'ScheduledTask' } - - $Table = Get-CIPPTable -TableName 'ScheduledTasks' Remove-AzDataTableEntity @Table -Entity $task diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 index dafe35d226f8..a1e92d2c3f89 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Settings/Invoke-ExecCustomRole.ps1 @@ -43,12 +43,12 @@ function Invoke-ExecCustomRole { if ($Role.AllowedTenants) { $Role.AllowedTenants = @($Role.AllowedTenants | ConvertFrom-Json) } else { - $Role | Add-Member -NotePropertyName AllowedTenants -NotePropertyValue @() + $Role | Add-Member -NotePropertyName AllowedTenants -NotePropertyValue @() -Force } if ($Role.BlockedTenants) { $Role.BlockedTenants = @($Role.BlockedTenants | ConvertFrom-Json) } else { - $Role | Add-Member -NotePropertyName BlockedTenants -NotePropertyValue @() + $Role | Add-Member -NotePropertyName BlockedTenants -NotePropertyValue @() -Force } $Role } diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSAMSetup.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSAMSetup.ps1 index 3818794ddbb0..9f38b50965a0 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSAMSetup.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/CIPP/Setup/Invoke-ExecSAMSetup.ps1 @@ -11,7 +11,7 @@ Function Invoke-ExecSAMSetup { param($Request, $TriggerMetadata) $UserCreds = ([System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($request.headers.'x-ms-client-principal')) | ConvertFrom-Json) - if ($Request.query.error) { + if ($Request.Query.error) { Add-Type -AssemblyName System.Web Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ ContentType = 'text/html' @@ -61,25 +61,25 @@ Function Invoke-ExecSAMSetup { $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-10) try { - if ($Request.query.count -lt 1 ) { $Results = 'No authentication code found. Please go back to the wizard.' } + if ($Request.Query.count -lt 1 ) { $Results = 'No authentication code found. Please go back to the wizard.' } - if ($request.body.setkeys) { + if ($Request.Body.setkeys) { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { - if ($request.body.TenantId) { $Secret.TenantId = $Request.body.tenantid } - if ($request.body.RefreshToken) { $Secret.RefreshToken = $Request.body.RefreshToken } - if ($request.body.applicationid) { $Secret.ApplicationId = $Request.body.ApplicationId } - if ($request.body.ApplicationSecret) { $Secret.ApplicationSecret = $Request.body.ApplicationSecret } + if ($Request.Body.TenantId) { $Secret.TenantId = $Request.Body.tenantid } + if ($Request.Body.RefreshToken) { $Secret.RefreshToken = $Request.Body.RefreshToken } + if ($Request.Body.applicationid) { $Secret.ApplicationId = $Request.Body.ApplicationId } + if ($Request.Body.ApplicationSecret) { $Secret.ApplicationSecret = $Request.Body.ApplicationSecret } Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force } else { - if ($request.body.tenantid) { Set-AzKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $request.body.tenantid -AsPlainText -Force) } - if ($request.body.RefreshToken) { Set-AzKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $request.body.RefreshToken -AsPlainText -Force) } - if ($request.body.applicationid) { Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $request.body.applicationid -AsPlainText -Force) } - if ($request.body.applicationsecret) { Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $request.body.applicationsecret -AsPlainText -Force) } + if ($Request.Body.tenantid) { Set-AzKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $Request.Body.tenantid -AsPlainText -Force) } + if ($Request.Body.RefreshToken) { Set-AzKeyVaultSecret -VaultName $kv -Name 'RefreshToken' -SecretValue (ConvertTo-SecureString -String $Request.Body.RefreshToken -AsPlainText -Force) } + if ($Request.Body.applicationid) { Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationid -AsPlainText -Force) } + if ($Request.Body.applicationsecret) { Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $Request.Body.applicationsecret -AsPlainText -Force) } } $Results = @{ Results = 'The keys have been replaced. Please perform a permissions check.' } } - if ($Request.query.error -eq 'invalid_client') { $Results = 'Client ID was not found in Azure. Try waiting 10 seconds to try again, if you have gotten this error after 5 minutes, please restart the process.' } - if ($request.query.code) { + if ($Request.Query.error -eq 'invalid_client') { $Results = 'Client ID was not found in Azure. Try waiting 10 seconds to try again, if you have gotten this error after 5 minutes, please restart the process.' } + if ($Request.Query.code) { try { $TenantId = $Rows.tenantid if (!$TenantId) { $TenantId = $ENV:TenantId } @@ -89,11 +89,11 @@ Function Invoke-ExecSAMSetup { if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $clientsecret = $Secret.ApplicationSecret } else { - $clientsecret = Get-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -AsPlainText + $clientsecret = Get-AzKeyVaultSecret -VaultName $kv -Name 'ApplicationSecret' -AsPlainText } if (!$clientsecret) { $clientsecret = $ENV:ApplicationSecret } - Write-Host "client_id=$appid&scope=https://graph.microsoft.com/.default+offline_access+openid+profile&code=$($request.query.code)&grant_type=authorization_code&redirect_uri=$($url)&client_secret=$clientsecret" -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" - $RefreshToken = Invoke-RestMethod -Method POST -Body "client_id=$appid&scope=https://graph.microsoft.com/.default+offline_access+openid+profile&code=$($request.query.code)&grant_type=authorization_code&redirect_uri=$($url)&client_secret=$clientsecret" -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" + Write-Host "client_id=$appid&scope=https://graph.microsoft.com/.default+offline_access+openid+profile&code=$($Request.Query.code)&grant_type=authorization_code&redirect_uri=$($url)&client_secret=$clientsecret" -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" + $RefreshToken = Invoke-RestMethod -Method POST -Body "client_id=$appid&scope=https://graph.microsoft.com/.default+offline_access+openid+profile&code=$($Request.Query.code)&grant_type=authorization_code&redirect_uri=$($url)&client_secret=$clientsecret" -Uri "https://login.microsoftonline.com/$TenantId/oauth2/v2.0/token" -ContentType 'application/x-www-form-urlencoded' if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { $Secret.RefreshToken = $RefreshToken.refresh_token @@ -113,7 +113,7 @@ Function Invoke-ExecSAMSetup { $Results = "Authentication failed. $($_.Exception.message)" } } - if ($request.query.CreateSAM) { + if ($Request.Query.CreateSAM) { $Rows = @{ RowKey = 'setup' PartitionKey = 'setup' @@ -126,7 +126,7 @@ Function Invoke-ExecSAMSetup { Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null $Rows = Get-CIPPAzDataTableEntity @Table | Where-Object -Property Timestamp -GT (Get-Date).AddMinutes(-10) - if ($Request.query.partnersetup) { + if ($Request.Query.partnersetup) { $SetupPhase = $Rows.partnersetup = $true Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null } @@ -136,46 +136,46 @@ Function Invoke-ExecSAMSetup { Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null $Results = @{ message = "Your code is $($DeviceLogon.user_code). Enter the code" ; step = $step; url = $DeviceLogon.verification_uri } } - if ($Request.query.CheckSetupProcess -and $request.query.step -eq 1) { + if ($Request.Query.CheckSetupProcess -and $Request.Query.step -eq 1) { $SAMSetup = $Rows.SamSetup | ConvertFrom-Json -ErrorAction SilentlyContinue $Token = (New-DeviceLogin -clientid '1b730954-1685-4b74-9bfd-dac224a7b894' -Scope 'https://graph.microsoft.com/.default' -device_code $SAMSetup.device_code) - if ($token.Access_Token) { + if ($Token.access_token) { $step = 2 $URL = ($Request.headers.'x-ms-original-url').split('?') | Select-Object -First 1 $PartnerSetup = $Rows.partnersetup - $TenantId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/organization' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method GET -ContentType 'application/json').value.id + $TenantId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/organization' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method GET -ContentType 'application/json').value.id $SetupPhase = $rows.tenantid = [string]($TenantId) Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null if ($PartnerSetup) { $app = Get-Content '.\Cache_SAMSetup\SAMManifest.json' | ConvertFrom-Json $App.web.redirectUris = @($App.web.redirectUris + $URL) $app = $app | ConvertTo-Json -Depth 15 - $AppId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/applications' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body $app -ContentType 'application/json') + $AppId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/applications' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body $app -ContentType 'application/json') $rows.appid = [string]($AppId.appId) Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null $attempt = 0 do { try { try { - $SPNDefender = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body "{ `"appId`": `"fc780465-2017-40d4-a0c5-307022471b92`" }" -ContentType 'application/json') + $SPNDefender = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body "{ `"appId`": `"fc780465-2017-40d4-a0c5-307022471b92`" }" -ContentType 'application/json') } catch { Write-Host "didn't deploy spn for defender, probably already there." } try { - $SPNTeams = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body "{ `"appId`": `"48ac35b8-9aa8-4d74-927d-1f4a14a0b239`" }" -ContentType 'application/json') + $SPNTeams = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body "{ `"appId`": `"48ac35b8-9aa8-4d74-927d-1f4a14a0b239`" }" -ContentType 'application/json') } catch { Write-Host "didn't deploy spn for Teams, probably already there." } try { - $SPNPartnerCenter = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body "{ `"appId`": `"fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd`" }" -ContentType 'application/json') + $SPNPartnerCenter = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body "{ `"appId`": `"fa3d9a0c-3fb0-42cc-9193-47c7ecd2edbd`" }" -ContentType 'application/json') } catch { Write-Host "didn't deploy spn for PartnerCenter, probably already there." } - $SPN = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body "{ `"appId`": `"$($AppId.appId)`" }" -ContentType 'application/json') + $SPN = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/servicePrincipals' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body "{ `"appId`": `"$($AppId.appId)`" }" -ContentType 'application/json') Start-Sleep 3 - $GroupID = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/groups?`$filter=startswith(displayName,'AdminAgents')" -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method Get -ContentType 'application/json').value.id + $GroupID = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/groups?`$filter=startswith(displayName,'AdminAgents')" -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method Get -ContentType 'application/json').value.id Write-Host "Id is $GroupID" - $AddingToAdminAgent = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/groups/$($GroupID)/members/`$ref" -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body "{ `"@odata.id`": `"https://graph.microsoft.com/v1.0/directoryObjects/$($SPN.id)`"}" -ContentType 'application/json') + $AddingToAdminAgent = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/groups/$($GroupID)/members/`$ref" -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body "{ `"@odata.id`": `"https://graph.microsoft.com/v1.0/directoryObjects/$($SPN.id)`"}" -ContentType 'application/json') Write-Host 'Added to adminagents' $attempt ++ } catch { @@ -184,21 +184,22 @@ Function Invoke-ExecSAMSetup { } until ($attempt -gt 5) } else { $app = Get-Content '.\Cache_SAMSetup\SAMManifestNoPartner.json' - $AppId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/applications' -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body $app -ContentType 'application/json') - $rows.appid = [string]($AppId.appId) + $AppId = (Invoke-RestMethod 'https://graph.microsoft.com/v1.0/applications' -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body $app -ContentType 'application/json') + $Rows.appid = [string]($AppId.appId) Add-CIPPAzDataTableEntity @Table -Entity $Rows -Force | Out-Null } - $AppPassword = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications/$($AppID.id)/addPassword" -Headers @{ authorization = "Bearer $($Token.Access_Token)" } -Method POST -Body '{"passwordCredential":{"displayName":"CIPPInstall"}}' -ContentType 'application/json').secretText + $AppPassword = (Invoke-RestMethod "https://graph.microsoft.com/v1.0/applications/$($AppId.id)/addPassword" -Headers @{ authorization = "Bearer $($Token.access_token)" } -Method POST -Body '{"passwordCredential":{"displayName":"CIPPInstall"}}' -ContentType 'application/json').secretText if ($env:AzureWebJobsStorage -eq 'UseDevelopmentStorage=true') { - $Secret.TenantId = $Request.body.tenantid - $Secret.ApplicationId = $Request.body.ApplicationId - $Secret.ApplicationSecret = $Request.body.ApplicationSecret + $Secret.TenantId = $TenantId + $Secret.ApplicationId = $AppId.appId + $Secret.ApplicationSecret = $AppPassword Add-CIPPAzDataTableEntity @DevSecretsTable -Entity $Secret -Force + Write-Information ($Secret | ConvertTo-Json -Depth 5) } else { Set-AzKeyVaultSecret -VaultName $kv -Name 'tenantid' -SecretValue (ConvertTo-SecureString -String $TenantId -AsPlainText -Force) - Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Appid.appid -AsPlainText -Force) + Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationid' -SecretValue (ConvertTo-SecureString -String $Appid.appId -AsPlainText -Force) Set-AzKeyVaultSecret -VaultName $kv -Name 'applicationsecret' -SecretValue (ConvertTo-SecureString -String $AppPassword -AsPlainText -Force) } $Results = @{'message' = 'Created application. Waiting 30 seconds for Azure propagation'; step = $step } @@ -208,7 +209,7 @@ Function Invoke-ExecSAMSetup { } } - switch ($request.query.step) { + switch ($Request.Query.step) { 2 { $step = 2 $TenantId = $Rows.tenantid 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 d01025dc080a..60d86236ca22 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 @@ -61,6 +61,13 @@ Function Invoke-AddIntuneTemplate { $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' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 index 2df6b1541877..aca7c04ca269 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Endpoint/MEM/Invoke-AddPolicy.ps1 @@ -82,6 +82,19 @@ Function Invoke-AddPolicy { } $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON } + 'windowsDriverUpdateProfiles' { + $TemplateTypeURL = 'windowsDriverUpdateProfiles' + $PolicyName = ($RawJSON | ConvertFrom-Json).Name + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant + if ($PolicyName -in $CheckExististing.name) { + $ExistingID = $CheckExististing | Where-Object -Property Name -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenant -type PUT -body $RawJSON + + } else { + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($PolicyName) via template" -Sev 'info' + } + } } Write-LogMessage -user $Request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($Displayname)" -Sev 'Info' diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 index ad887810061a..1c1c575a598f 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecJITAdmin.ps1 @@ -103,6 +103,11 @@ Function Invoke-ExecJITAdmin { } Parameters = $Parameters ScheduledTime = $Request.Body.StartDate + PostExecution = @{ + Webhook = [bool]$Request.Body.PostExecution.Webhook + Email = [bool]$Request.Body.PostExecution.Email + PSA = [bool]$Request.Body.PostExecution.PSA + } } Add-CIPPScheduledTask -Task $TaskBody -hidden $false Set-CIPPUserJITAdminProperties -TenantFilter $Request.Body.TenantFilter -UserId $Request.Body.UserId -Expiration $Expiration diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 new file mode 100644 index 000000000000..b52a1595f513 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecPerUserMFA.ps1 @@ -0,0 +1,28 @@ +function Invoke-ExecPerUserMFA { + <# + .FUNCTIONALITY + Entrypoint + + .ROLE + Identity.User.ReadWrite + #> + Param( + $Request, + $TriggerMetadata + ) + + $Request = @{ + userId = $Request.Body.userId + TenantFilter = $Request.Body.TenantFilter + State = $Request.Body.State + executingUser = $Request.Headers.'x-ms-client-principal' + } + $Result = Set-CIPPPerUserMFA @Request + $Body = @{ + Results = @($Result) + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 new file mode 100644 index 000000000000..c83484ec17e7 --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-ListAuditLogTest.ps1 @@ -0,0 +1,32 @@ +function Invoke-ListAuditLogTest { + <# + .FUNCTIONALITY + Entrypoint + + .ROLE + Tenant.Alert.Read + #> + Param($Request, $TriggerMetadata) + + $AuditLogQuery = @{ + TenantFilter = $Request.Query.TenantFilter + LogType = $Request.Query.LogType + ShowAll = $true + } + $TestResults = Test-CIPPAuditLogRules @AuditLogQuery + $Body = @{ + Results = @($TestResults.DataToProcess) + Metadata = @{ + TenantFilter = $AuditLogQuery.TenantFilter + LogType = $AuditLogQuery.LogType + TotalLogs = $TestResults.TotalLogs + MatchedLogs = $TestResults.MatchedLogs + MatchedRules = $TestResults.MatchedRules + } + } + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $Body + }) + +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 index 78cded90ff19..3a4e60373847 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Administration/Alerts/Invoke-PublicWebhooks.ps1 @@ -65,165 +65,7 @@ function Invoke-PublicWebhooks { } Add-CIPPAzDataTableEntity @WebhookIncoming -Entity $Entity } else { - if ($request.headers.'x-ms-original-url' -notlike '*version=2*') { - return "Not replying to this webhook or processing it, as it's not a version 2 webhook." - } else { - try { - foreach ($ReceivedItem In $Request.body) { - $ReceivedItem = [pscustomobject]$ReceivedItem - $TenantFilter = (Get-Tenants | Where-Object -Property customerId -EQ $ReceivedItem.TenantId).defaultDomainName - Write-Host "Webhook TenantFilter: $TenantFilter" - $ConfigTable = get-cipptable -TableName 'WebhookRules' - $Configuration = (Get-CIPPAzDataTableEntity @ConfigTable) | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') -and $_.Type -eq $ReceivedItem.ContentType } | ForEach-Object { - [pscustomobject]@{ - Tenants = ($_.Tenants | ConvertFrom-Json).fullValue - Conditions = $_.Conditions - Actions = $_.Actions - LogType = $_.Type - } - } - if (!$Configuration.Tenants) { - Write-Host 'No tenants found for this webhook, probably an old entry. Skipping.' - continue - } - Write-Host "Webhook: The received content-type for $($TenantFilter) is $($ReceivedItem.ContentType)" - if ($ReceivedItem.ContentType -in $Configuration.LogType) { - $Data = New-GraphPostRequest -type GET -uri "https://manage.office.com/api/v1.0/$($ReceivedItem.tenantId)/activity/feed/audit/$($ReceivedItem.contentid)" -tenantid $TenantFilter -scope 'https://manage.office.com/.default' - } else { - Write-Host "No data to download for $($ReceivedItem.ContentType)" - continue - } - - $PreProccessedData = $Data | Select-Object *, CIPPAction, CIPPClause, CIPPGeoLocation, CIPPBadRepIP, CIPPHostedIP, CIPPIPDetected, CIPPLocationInfo, CIPPExtendedProperties, CIPPDeviceProperties, CIPPParameters, CIPPModifiedProperties -ErrorAction SilentlyContinue - $LocationTable = Get-CIPPTable -TableName 'knownlocationdb' - $ProcessedData = foreach ($Data in $PreProccessedData) { - if ($Data.ExtendedProperties) { - $Data.CIPPExtendedProperties = ($Data.ExtendedProperties | ConvertTo-Json) - $Data.ExtendedProperties | ForEach-Object { $data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } - } - if ($Data.DeviceProperties) { - $Data.CIPPDeviceProperties = ($Data.DeviceProperties | ConvertTo-Json) - $Data.DeviceProperties | ForEach-Object { $data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } - } - if ($Data.parameters) { - $Data.CIPPParameters = ($Data.parameters | ConvertTo-Json) - $Data.parameters | ForEach-Object { $data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } - } - if ($Data.ModifiedProperties) { - $Data.CIPPModifiedProperties = ($Data.ModifiedProperties | ConvertTo-Json) - $Data.ModifiedProperties | ForEach-Object { $data | Add-Member -NotePropertyName "$($_.Name)" -NotePropertyValue "$($_.NewValue)" -Force -ErrorAction SilentlyContinue } - } - if ($Data.ModifiedProperties) { $Data.ModifiedProperties | ForEach-Object { $data | Add-Member -NotePropertyName $("Previous_Value_$($_.Name)") -NotePropertyValue "$($_.OldValue)" -Force -ErrorAction SilentlyContinue } } - - if ($data.clientip) { - if ($data.clientip -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$') { - $data.clientip = $data.clientip -replace ':\d+$', '' # Remove the port number if present - } - $Location = Get-CIPPAzDataTableEntity @LocationTable -Filter "RowKey eq '$($data.clientIp)'" | Select-Object -Last 1 - if ($Location) { - Write-Host 'Webhook: Got IP from cache' - $Country = $Location.CountryOrRegion - $City = $Location.City - $Proxy = $Location.Proxy - $hosting = $Location.Hosting - $ASName = $Location.ASName - } else { - Write-Host 'Webhook: We have to do a lookup' - $Location = Get-CIPPGeoIPLocation -IP $data.clientip - $Country = if ($Location.CountryCode) { $Location.CountryCode } else { 'Unknown' } - $City = if ($Location.City) { $Location.City } else { 'Unknown' } - $Proxy = if ($Location.Proxy -ne $null) { $Location.Proxy } else { 'Unknown' } - $hosting = if ($Location.Hosting -ne $null) { $Location.Hosting } else { 'Unknown' } - $ASName = if ($Location.ASName) { $Location.ASName } else { 'Unknown' } - $IP = $data.ClientIP - $LocationInfo = @{ - RowKey = [string]$data.clientip - PartitionKey = [string]$data.id - Tenant = [string]$TenantFilter - CountryOrRegion = "$Country" - City = "$City" - Proxy = "$Proxy" - Hosting = "$hosting" - ASName = "$ASName" - } - try { - $null = Add-CIPPAzDataTableEntity @LocationTable -Entity $LocationInfo -Force - } catch { - Write-Host "Webhook: Failed to add location info for $($data.clientip) to cache: $($_.Exception.Message)" - - } - } - $Data.CIPPGeoLocation = $Country - $Data.CIPPBadRepIP = $Proxy - $Data.CIPPHostedIP = $hosting - $Data.CIPPIPDetected = $IP - $Data.CIPPLocationInfo = ($Location | ConvertTo-Json) - } - $Data | Select-Object * -ExcludeProperty ExtendedProperties, DeviceProperties, parameters - } - - #Filter data based on conditions. - $Where = $Configuration | ForEach-Object { - $conditions = $_.Conditions | ConvertFrom-Json | Where-Object { $_.Input.value -ne '' } - $actions = $_.Actions - $conditionStrings = [System.Collections.Generic.List[string]]::new() - $CIPPClause = [System.Collections.Generic.List[string]]::new() - foreach ($condition in $conditions) { - $value = if ($condition.Input.value -is [array]) { - $arrayAsString = $condition.Input.value | ForEach-Object { - "'$_'" - } - "@($($arrayAsString -join ', '))" - } else { "'$($condition.Input.value)'" } - - $conditionStrings.Add("`$(`$_.$($condition.Property.label)) -$($condition.Operator.value) $value") - $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $value") - } - $finalCondition = $conditionStrings -join ' -AND ' - - [PSCustomObject]@{ - clause = $finalCondition - expectedAction = $actions - CIPPClause = $CIPPClause - } - - } - Write-Host "Webhook: The list of operations in the data are $($ProcessedData.operation -join ', ')" - - $DataToProcess = foreach ($clause in $Where) { - Write-Host "Webhook: Processing clause: $($clause.clause)" - Write-Host "Webhook: If this clause would be true, the action would be: $($clause.expectedAction)" - $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } - if ($ReturnedData) { - $ReturnedData = foreach ($item in $ReturnedData) { - $item.CIPPAction = $clause.expectedAction - $item.CIPPClause = $clause.CIPPClause -join ' and ' - $item - } - } - $ReturnedData - } - - Write-Host "Webhook: Data to process found: $($DataToProcess.count) items" - foreach ($Item in $DataToProcess) { - Write-Host "Processing $($item.operation)" - ## Push webhook data to table - $Entity = [PSCustomObject]@{ - PartitionKey = 'Webhook' - RowKey = [string]$item.id - Type = 'AuditLog' - Data = [string]($Item | ConvertTo-Json -Depth 10) - CIPPURL = $CIPPURL - TenantFilter = $TenantFilter - FunctionName = 'PublicWebhookProcess' - } - Add-CIPPAzDataTableEntity @WebhookIncoming -Entity $Entity -Force - } - } - } catch { - Write-Host "Webhook Failed: $($_.Exception.Message). Line number $($_.InvocationInfo.ScriptLineNumber)" - } - } + return 'Not replying to this webhook or processing it' } $Body = 'Webhook Recieved' $StatusCode = [HttpStatusCode]::OK diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 index df3e849a19e8..22e078617475 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPA.ps1 @@ -20,14 +20,18 @@ Function Invoke-ListBPA { # Get all possible JSON files for reports, find the correct one, select the Columns $JSONFields = @() $Columns = $null -(Get-ChildItem -Path 'Config\*.BPATemplate.json' -Recurse | Select-Object -ExpandProperty FullName | ForEach-Object { - $Template = $(Get-Content $_) | ConvertFrom-Json + $BPATemplateTable = Get-CippTable -tablename 'templates' + $Filter = "PartitionKey eq 'BPATemplate'" + $Templates = (Get-CIPPAzDataTableEntity @BPATemplateTable -Filter $Filter).JSON | ConvertFrom-Json + + $Templates | ForEach-Object { + $Template = $_ if ($Template.Name -eq $NAME) { $JSONFields = $Template.Fields | Where-Object { $_.StoreAs -eq 'JSON' } | ForEach-Object { $_.name } $Columns = $Template.fields.FrontendFields | Where-Object -Property name -NE $null $Style = $Template.Style } - }) + } if ($Request.query.tenantFilter -ne 'AllTenants' -and $Style -eq 'Tenant') { diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 index 7f7c110fcd9f..376a1f4e592b 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Standards/Invoke-ListBPATemplates.ps1 @@ -14,17 +14,27 @@ Function Invoke-ListBPATemplates { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' Write-Host 'PowerShell HTTP trigger function processed a request.' - Write-Host $Request.query.id + + $Table = Get-CippTable -tablename 'templates' - $Templates = Get-ChildItem 'Config\*.BPATemplate.json' + $Templates = Get-ChildItem 'Config\*.BPATemplate.json' | ForEach-Object { + $Entity = @{ + JSON = "$(Get-Content $_)" + RowKey = "$($_.name)" + PartitionKey = 'BPATemplate' + GUID = "$($_.name)" + } + Add-CIPPAzDataTableEntity @Table -Entity $Entity -Force + } + + $Filter = "PartitionKey eq 'BPATemplate'" + $Templates = (Get-CIPPAzDataTableEntity @Table -Filter $Filter).JSON | ConvertFrom-Json if ($Request.Query.RawJson) { - $Templates = $Templates | ForEach-Object { - $(Get-Content $_) | ConvertFrom-Json - } + $Templates } else { $Templates = $Templates | ForEach-Object { - $Template = $(Get-Content $_) | ConvertFrom-Json + $Template = $_ @{ Data = $Template.fields Name = $Template.Name diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tools/Invoke-AddBPATemplate.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tools/Invoke-AddBPATemplate.ps1 new file mode 100644 index 000000000000..15c2d49afc0d --- /dev/null +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Tenant/Tools/Invoke-AddBPATemplate.ps1 @@ -0,0 +1,41 @@ +using namespace System.Net + +Function Invoke-AddBPATemplate { + <# + .FUNCTIONALITY + Entrypoint + .ROLE + Tenant.BestPracticeAnalyser.ReadWrite + #> + [CmdletBinding()] + param($Request, $TriggerMetadata) + + $APIName = $TriggerMetadata.FunctionName + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Accessed this API' -Sev 'Debug' + + try { + + $Table = Get-CippTable -tablename 'templates' + $Table.Force = $true + Add-CIPPAzDataTableEntity @Table -Entity @{ + JSON = "$($Request.body | ConvertTo-Json -Depth 10)" + RowKey = $Request.body.name + PartitionKey = 'BPATemplate' + GUID = $Request.body.name + } + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "Created BPA named $($Request.body.name)" -Sev 'Debug' + + $body = [pscustomobject]@{'Results' = 'Successfully added template' } + } catch { + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message "BPA Template Creation failed: $($_.Exception.Message)" -Sev 'Error' + $body = [pscustomobject]@{'Results' = "BPA Template Creation failed: $($_.Exception.Message)" } + } + + + # Associate values to output bindings by calling 'Push-OutputBinding'. + Push-OutputBinding -Name Response -Value ([HttpResponseContext]@{ + StatusCode = [HttpStatusCode]::OK + Body = $body + }) + +} diff --git a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 index dbccf5a4004e..6bbf36a98274 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Invoke-ListIntunePolicy.ps1 @@ -26,7 +26,8 @@ Function Invoke-ListIntunePolicy { $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$($urlname)('$ID')" -tenantid $tenantfilter } else { - $GraphURLS = @("https://graph.microsoft.com/beta/deviceManagement/deviceConfigurations?`$select=id,displayName,lastModifiedDateTime,roleScopeTagIds,microsoft.graph.unsupportedDeviceConfiguration/originalEntityTypeName&`$expand=assignments&top=1000", + $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=1000" "https://graph.microsoft.com/beta/deviceAppManagement/mobileAppConfigurations?`$expand=assignments&`$filter=microsoft.graph.androidManagedStoreAppConfiguration/appSupportsOemConfig%20eq%20true" 'https://graph.microsoft.com/beta/deviceManagement/configurationPolicies' diff --git a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 index 319f9a81e726..28d526a9944d 100644 --- a/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPMFAState.ps1 @@ -6,18 +6,17 @@ function Get-CIPPMFAState { $APIName = 'Get MFA Status', $ExecutingUser ) - - $users = foreach ($user in (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$select=id,UserPrincipalName,DisplayName,accountEnabled,assignedLicenses' -tenantid $TenantFilter)) { + $PerUserMFAState = Get-CIPPPerUserMFA -TenantFilter $TenantFilter -AllUsers $true + $users = foreach ($user in (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/users?$top=999&$select=id,UserPrincipalName,DisplayName,accountEnabled,assignedLicenses' -tenantid $TenantFilter)) { [PSCustomObject]@{ - UserPrincipalName = $user.UserPrincipalName - isLicensed = [boolean]$user.assignedLicenses.skuid - accountEnabled = $user.accountEnabled - DisplayName = $user.DisplayName - ObjectId = $user.id - StrongAuthenticationRequirements = @{StrongAuthenticationRequirement = @{state = 'See Documentation' } } + UserPrincipalName = $user.UserPrincipalName + isLicensed = [boolean]$user.assignedLicenses.skuid + accountEnabled = $user.accountEnabled + DisplayName = $user.DisplayName + ObjectId = $user.id } } - + $SecureDefaultsState = (New-GraphGetRequest -Uri 'https://graph.microsoft.com/beta/policies/identitySecurityDefaultsEnforcementPolicy' -tenantid $TenantFilter ).IsEnabled $CAState = New-Object System.Collections.ArrayList @@ -62,7 +61,6 @@ function Get-CIPPMFAState { Write-Host 'Processing users' $UserCAState = New-Object System.Collections.ArrayList foreach ($CA in $CAState) { - Write-Host 'Looping CAState' if ($CA -like '*All Users*') { if ($ExcludeAllUsers -contains $_.ObjectId) { $UserCAState.Add("Excluded from $($policy.displayName) - All Users") | Out-Null } else { $UserCAState.Add($CA) | Out-Null } @@ -75,7 +73,7 @@ function Get-CIPPMFAState { } } - $PerUser = if ($_.StrongAuthenticationRequirements.StrongAuthenticationRequirement.state -ne $null) { $_.StrongAuthenticationRequirements.StrongAuthenticationRequirement.state } else { 'Disabled' } + $PerUser = if ($PerUserMFAState -eq $null) { $null } else { ($PerUserMFAState | Where-Object -Property UserPrincipalName -EQ $_.UserPrincipalName).PerUserMFAState } $MFARegUser = if (($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.UserPrincipalName).IsMFARegistered -eq $null) { $false } else { ($MFARegistration | Where-Object -Property UserPrincipalName -EQ $_.UserPrincipalName) } diff --git a/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 new file mode 100644 index 000000000000..5c525962009f --- /dev/null +++ b/Modules/CIPPCore/Public/Get-CIPPPerUserMFA.ps1 @@ -0,0 +1,34 @@ +function Get-CIPPPerUserMFA { + [CmdletBinding()] + param( + $TenantFilter, + $userId, + $executingUser, + $AllUsers = $false + ) + try { + if ($AllUsers -eq $true) { + $AllUsers = New-graphGetRequest -Uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=UserPrincipalName,Id" -tenantid $tenantfilter + $Requests = foreach ($id in $AllUsers.userPrincipalName) { + @{ + id = $int++ + method = 'GET' + url = "users/$id/authentication/requirements" + } + } + $Requests = New-GraphBulkRequest -tenantid $tenantfilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true + if ($Requests.body) { + $UsersWithoutMFA = $Requests.body | Select-Object peruserMFAState, @{Name = 'UserPrincipalName'; Expression = { [System.Web.HttpUtility]::UrlDecode($_.'@odata.context'.split("'")[1]) } } + return $UsersWithoutMFA + } + } else { + $MFAState = New-graphGetRequest -Uri "https://graph.microsoft.com/beta/users/$($userId)/authentication/requirements" -tenantid $tenantfilter + return [PSCustomObject]@{ + PerUserMFAState = $MFAState.perUserMfaState + UserPrincipalName = $userId + } + } + } catch { + "Failed to get MFA State for $id : $_" + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Get-CIPPSchemaExtensions.ps1 b/Modules/CIPPCore/Public/Get-CIPPSchemaExtensions.ps1 index 26794daffea7..b85edb06af86 100644 --- a/Modules/CIPPCore/Public/Get-CIPPSchemaExtensions.ps1 +++ b/Modules/CIPPCore/Public/Get-CIPPSchemaExtensions.ps1 @@ -30,6 +30,10 @@ function Get-CIPPSchemaExtensions { name = 'autoExpandingArchiveEnabled' type = 'Boolean' } + @{ + name = 'perUserMfaState' + type = 'String' + } ) } ) @@ -40,8 +44,8 @@ function Get-CIPPSchemaExtensions { $SchemaFound = $true $Schema = $Schemas | Where-Object { $_.id -match $SchemaDefinition.id } $Patch = @{} - if (Compare-Object -ReferenceObject ($SchemaDefinition.properties | Select-Object name, type) -DifferenceObject $Schema.properties) { - $Patch.properties = $Properties + if (Compare-Object -ReferenceObject ($SchemaDefinition.properties | Select-Object name, type) -DifferenceObject ($Schema.properties | Select-Object name, type)) { + $Patch.properties = $SchemaDefinitions.Properties } if ($Schema.status -ne 'Available') { $Patch.status = 'Available' @@ -49,9 +53,10 @@ function Get-CIPPSchemaExtensions { if ($Schema.targetTypes -ne $SchemaDefinition.targetTypes) { $Patch.targetTypes = $SchemaDefinition.targetTypes } - if ($Patch.Keys.Count -gt 0) { + if ($Patch -and $Patch.Keys.Count -gt 0) { Write-Information "Updating $($Schema.id)" $Json = ConvertTo-Json -Depth 5 -InputObject $Patch + Write-Information $Json New-GraphPOSTRequest -type PATCH -Uri "https://graph.microsoft.com/v1.0/schemaExtensions/$($Schema.id)" -Body $Json -AsApp $true -NoAuthCheck $true } else { $Schema diff --git a/Modules/CIPPCore/Public/Get-SlackAlertBlocks.ps1 b/Modules/CIPPCore/Public/Get-SlackAlertBlocks.ps1 index 8f63195f905f..60e42aee5ced 100644 --- a/Modules/CIPPCore/Public/Get-SlackAlertBlocks.ps1 +++ b/Modules/CIPPCore/Public/Get-SlackAlertBlocks.ps1 @@ -151,11 +151,7 @@ function Get-SlackAlertBlocks { $Fields = [system.collections.generic.list[object]]::new() foreach ($Key in $Payload.RawData.PSObject.Properties.Name) { # if value is json continue to next property - if ($Payload.RawData.$Key -is [string] -and ![string]::IsNullOrEmpty($Payload.RawData.$Key) -and (Test-Json -Json $Payload.RawData.$Key -ErrorAction SilentlyContinue)) { - continue - } - - if ([string]::IsNullOrEmpty($Payload.RawData.$Key)) { + if ($Payload.RawData.$Key -is [string] -and ([string]::IsNullOrEmpty($Payload.RawData.$Key) -or (Test-Json $Payload.RawData.$Key -ErrorAction SilentlyContinue))) { continue } # if value is date object @@ -164,6 +160,22 @@ function Get-SlackAlertBlocks { type = 'mrkdwn' text = "*$($Key):*`n" + $Payload.RawData.$Key.ToString('yyyy-MM-dd @ hh:mm:ss tt') }) + } elseif ($Payload.RawData.$Key -is [array] -and $Payload.RawData.$Key.Count -gt 0) { + foreach ($SubKey in $Payload.RawData.$Key) { + if ([string]::IsNullOrEmpty($SubKey)) { + continue + } elseif ($SubKey -is [datetime]) { + $Fields.Add(@{ + type = 'mrkdwn' + text = "*$($Key):*`n" + $SubKey.ToString('yyyy-MM-dd @ hh:mm:ss tt') + }) + } else { + $Fields.Add(@{ + type = 'mrkdwn' + text = "*$($Key):*`n" + $SubKey + }) + } + } } elseif ($Payload.RawData.$Key.PSObject.Properties.Name -is [array] -and $Payload.RawData.$Key.PSObject.Properties.Name.Count -gt 0) { foreach ($SubKey in $Payload.RawData.$Key.PSObject.Properties.Name) { if ([string]::IsNullOrEmpty($Payload.RawData.$Key.$SubKey)) { @@ -173,6 +185,16 @@ function Get-SlackAlertBlocks { type = 'mrkdwn' text = "*$($Key)/$($SubKey):*`n" + $Payload.RawData.$Key.$SubKey.ToString('yyyy-MM-dd @ hh:mm:ss tt') }) + } elseif (Test-Json $Payload.RawData.$Key.$SubKey -ErrorAction SilentlyContinue) { + # parse json and iterate through properties + continue + <#$SubKeyData = $Payload.RawData.$Key.$SubKey | ConvertFrom-Json + foreach ($SubSubKey in $SubKeyData.PSObject.Properties.Name) { + $Fields.Add(@{ + type = 'mrkdwn' + text = "*$($Key)/$($SubKey)/$($SubSubKey):*`n" + $SubKeyData.$SubSubKey + }) + }#> } else { $Fields.Add(@{ type = 'mrkdwn' diff --git a/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 index 4a05cf778fe7..8ef890eaf10d 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-ExoRequest.ps1 @@ -22,15 +22,25 @@ function New-ExoRequest ($tenantid, $cmdlet, $cmdParams, $useSystemMailbox, $Anc if ($cmdparams.Identity) { $Anchor = $cmdparams.Identity } if ($cmdparams.anr) { $Anchor = $cmdparams.anr } if ($cmdparams.User) { $Anchor = $cmdparams.User } + if ($cmdparams.mailbox) { $Anchor = $cmdparams.mailbox } if (!$Anchor -or $useSystemMailbox) { - if (!$Tenant.initialDomainName) { + if (!$Tenant.initialDomainName -or $Tenant.initialDomainName -notlike '*onmicrosoft.com*') { $OnMicrosoft = (New-GraphGetRequest -uri 'https://graph.microsoft.com/beta/domains?$top=999' -tenantid $tenantid -NoAuthCheck $NoAuthCheck | Where-Object -Property isInitial -EQ $true).id } else { $OnMicrosoft = $Tenant.initialDomainName } $anchor = "UPN:SystemMailbox{8cc370d3-822a-4ab8-a926-bb94bd0641a9}@$($OnMicrosoft)" } + #if the anchor is a GUID, try looking up the user. + if ($Anchor -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$') { + $Anchor = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users/$Anchor" -tenantid $tenantid -NoAuthCheck $NoAuthCheck + if ($Anchor) { + $Anchor = $Anchor.UserPrincipalName + } else { + Write-Error "Failed to find user with GUID $Anchor" + } + } } Write-Host "Using $Anchor" $Headers = @{ diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 index ebe6af9675b9..f37be378e39d 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphBulkRequest.ps1 @@ -33,7 +33,8 @@ function New-GraphBulkRequest { $req = @{} # Use select to create hashtables of id, method and url for each call $req['requests'] = ($Requests[$i..($i + 19)]) - Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body ($req | ConvertTo-Json -Depth 10) + $ReqBody = ($req | ConvertTo-Json -Depth 10) + Invoke-RestMethod -Uri $URL -Method POST -Headers $headers -ContentType 'application/json; charset=utf-8' -Body $ReqBody } foreach ($MoreData in $ReturnedData.Responses | Where-Object { $_.body.'@odata.nextLink' }) { diff --git a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 index 28e88e204d79..7c4eb8927b35 100644 --- a/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 +++ b/Modules/CIPPCore/Public/GraphHelper/New-GraphGetRequest.ps1 @@ -12,7 +12,8 @@ function New-GraphGetRequest { $NoAuthCheck, $skipTokenCache, [switch]$ComplexFilter, - [switch]$CountOnly + [switch]$CountOnly, + [switch]$IncludeResponseHeaders ) if ($NoAuthCheck -or (Get-AuthorisedRequest -Uri $uri -TenantID $tenantid)) { @@ -43,16 +44,40 @@ function New-GraphGetRequest { $ReturnedData = do { try { - $Data = (Invoke-RestMethod -Uri $nextURL -Method GET -Headers $headers -ContentType 'application/json; charset=utf-8') + $GraphRequest = @{ + Uri = $nextURL + Method = 'GET' + Headers = $headers + ContentType = 'application/json; charset=utf-8' + } + if ($IncludeResponseHeaders) { + $GraphRequest.ResponseHeadersVariable = 'ResponseHeaders' + } + $Data = (Invoke-RestMethod @GraphRequest) if ($CountOnly) { $Data.'@odata.count' - $nextURL = $null + $NextURL = $null } else { - if ($data.value) { $data.value } else { ($Data) } - if ($noPagination) { $nextURL = $null } else { $nextURL = $data.'@odata.nextLink' } + if ($Data.PSObject.Properties.Name -contains 'value') { $data.value } else { ($Data) } + if ($noPagination) { + $nextURL = $null + } else { + $NextPageUriFound = $false + if ($IncludeResponseHeaders) { + if ($ResponseHeaders.NextPageUri) { + $NextURL = $ResponseHeaders.NextPageUri + $NextPageUriFound = $true + } + } + if (!$NextPageUriFound) { + $nextURL = $data.'@odata.nextLink' + } + } } } catch { - $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + try { + $Message = ($_.ErrorDetails.Message | ConvertFrom-Json -ErrorAction SilentlyContinue).error.message + } catch { $Message = $null } if ($Message -eq $null) { $Message = $($_.Exception.Message) } if ($Message -ne 'Request not applicable to target tenant.' -and $Tenant) { $Tenant.LastGraphError = $Message @@ -61,7 +86,7 @@ function New-GraphGetRequest { } throw $Message } - } until ($null -eq $NextURL -or ' ' -eq $NextURL) + } until ([string]::IsNullOrEmpty($NextURL) -or $NextURL -is [object[]] -or ' ' -eq $NextURL) $Tenant.LastGraphError = '' Update-AzDataTableEntity @TenantsTable -Entity $Tenant return $ReturnedData diff --git a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 index b0e960829858..3c9acd37e9cd 100644 --- a/Modules/CIPPCore/Public/New-CIPPBackup.ps1 +++ b/Modules/CIPPCore/Public/New-CIPPBackup.ps1 @@ -23,10 +23,26 @@ function New-CIPPBackup { ) $CSVfile = foreach ($CSVTable in $BackupTables) { $Table = Get-CippTable -tablename $CSVTable - Get-CIPPAzDataTableEntity @Table | Select-Object *, @{l = 'table'; e = { $CSVTable } } + Get-CIPPAzDataTableEntity @Table } Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created backup' -Sev 'Debug' $CSVfile + $RowKey = 'CIPPBackup' + '_' + (Get-Date).ToString('yyyy-MM-dd-HHmm') + $entity = [PSCustomObject]@{ + PartitionKey = 'CIPPBackup' + RowKey = $RowKey + TenantFilter = 'CIPPBackup' + Backup = [string]($CSVfile | ConvertTo-Json -Compress -Depth 100) + } + $Table = Get-CippTable -tablename 'CIPPBackup' + try { + $Result = Add-CIPPAzDataTableEntity @Table -entity $entity -Force + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -message 'Created CIPP Backup' -Sev 'Debug' + } catch { + 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)" } diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 8fc9e31f7bdf..89bd2ade79d2 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -79,7 +79,7 @@ function Send-CIPPAlert { } catch { Write-Information "Could not send alerts to webhook: $($_.Exception.message)" - Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to webhook: $($_.Exception.message)" -tenant $TenantFilter -sev info + Write-LogMessage -API 'Webhook Alerts' -message "Could not send alerts to webhook: $($_.Exception.message)" -tenant $TenantFilter -sev error -LogData (Get-CippException -Exception $_) } } Write-Information 'Trying to send to PSA' diff --git a/Modules/CIPPCore/Public/Set-CIPPPerUserMFA.ps1 b/Modules/CIPPCore/Public/Set-CIPPPerUserMFA.ps1 new file mode 100644 index 000000000000..6c2d544dcfd9 --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPPerUserMFA.ps1 @@ -0,0 +1,67 @@ +function Set-CIPPPerUserMFA { + <# + .SYNOPSIS + Change Per-User MFA State for a User + + .DESCRIPTION + Change the Per-User MFA State for a user via the /users/{id}/authentication/requirements endpoint + + .PARAMETER TenantFilter + Tenant where the user resides + + .PARAMETER userId + One or more User IDs to set the MFA state for (GUID or UserPrincipalName) + + .PARAMETER State + State to set the user to (enabled, disabled, enforced) + + .PARAMETER executingUser + User executing the command + + .EXAMPLE + Set-CIPPPerUserMFA -TenantFilter 'contoso.onmicrosoft.com' -userId user@contoso.onmicrosoft.com -State 'disabled' -executingUser 'mspuser@partner.com' + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [string[]]$userId, + [ValidateSet('enabled', 'disabled', 'enforced')] + $State = 'enabled', + [string]$executingUser = 'CIPP' + ) + try { + $int = 0 + $Body = @{ + perUserMFAstate = $State + } + $Requests = foreach ($id in $userId) { + @{ + id = $int++ + method = 'PATCH' + url = "users/$id/authentication/requirements" + body = $Body + 'headers' = @{ + 'Content-Type' = 'application/json' + } + } + } + $Requests = New-GraphBulkRequest -tenantid $tenantfilter -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) + "Successfully set Per user MFA State for $userId" + + $Users = foreach ($id in $userId) { + @{ + userId = $id + Properties = @{ + perUserMfaState = $State + } + } + } + Set-CIPPUserSchemaProperties -TenantFilter $TenantFilter -Users $Users + Write-LogMessage -user $executingUser -API 'Set-CIPPPerUserMFA' -message "Successfully set Per user MFA State to $State for $id" -Sev 'Info' -tenant $TenantFilter + } catch { + "Failed to set MFA State for $id : $_" + Write-LogMessage -user $executingUser -API 'Set-CIPPPerUserMFA' -message "Failed to set MFA State to $State for $id : $_" -Sev 'Error' -tenant $TenantFilter + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Set-CIPPUserSchemaProperties.ps1 b/Modules/CIPPCore/Public/Set-CIPPUserSchemaProperties.ps1 new file mode 100644 index 000000000000..b006a27069ef --- /dev/null +++ b/Modules/CIPPCore/Public/Set-CIPPUserSchemaProperties.ps1 @@ -0,0 +1,46 @@ +function Set-CIPPUserSchemaProperties { + <# + .SYNOPSIS + Set Schema Properties for a user + + .DESCRIPTION + Uses scheam extensions to set properties for a user + + .PARAMETER TenantFilter + Tenant for user + + .PARAMETER UserId + One or more user ids to set properties for + + .PARAMETER Properties + Hashtable of properties to set + + #> + [CmdletBinding(SupportsShouldProcess = $true)] + Param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [object]$Users + ) + + $Schema = Get-CIPPSchemaExtensions | Where-Object { $_.id -match '_cippUser' } + $int = 0 + $Requests = foreach ($User in $Users) { + @{ + id = $int++ + method = 'PATCH' + url = "users/$($User.userId)" + body = @{ + "$($Schema.id)" = $User.Properties + } + 'headers' = @{ + 'Content-Type' = 'application/json' + } + } + } + + if ($PSCmdlet.ShouldProcess("User: $($Users.userId -join ', ')", 'Set Schema Properties')) { + $Requests = New-GraphBulkRequest -tenantid $tenantfilter -Requests @($Requests) + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 index 39af5a0f9402..9263b386bb23 100644 --- a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardIntuneTemplate.ps1 @@ -98,6 +98,19 @@ function Invoke-CIPPStandardIntuneTemplate { Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($PolicyName) via template" -Sev 'info' } } + 'windowsDriverUpdateProfiles' { + $TemplateTypeURL = 'windowsDriverUpdateProfiles' + $PolicyName = ($RawJSON | ConvertFrom-Json).Name + $CheckExististing = New-GraphGETRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant + if ($PolicyName -in $CheckExististing.name) { + $ExistingID = $CheckExististing | Where-Object -Property Name -EQ $PolicyName + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL/$($ExistingID.Id)" -tenantid $tenant -type PUT -body $RawJSON + + } else { + $CreateRequest = New-GraphPOSTRequest -uri "https://graph.microsoft.com/beta/deviceManagement/$TemplateTypeURL" -tenantid $tenant -type POST -body $RawJSON + Write-LogMessage -user $request.headers.'x-ms-client-principal' -API $APINAME -tenant $($Tenant) -message "Added policy $($PolicyName) via template" -Sev 'info' + } + } } diff --git a/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 new file mode 100644 index 000000000000..c83204529423 --- /dev/null +++ b/Modules/CIPPCore/Public/Standards/Invoke-CIPPStandardPerUserMFA.ps1 @@ -0,0 +1,41 @@ +function Invoke-CIPPStandardPerUserMFA { + <# + .FUNCTIONALITY + Internal + #> + param($Tenant, $Settings) + + $GraphRequest = New-GraphGetRequest -uri "https://graph.microsoft.com/beta/users?`$top=999&`$select=UserPrincipalName,accountEnabled" -scope 'https://graph.microsoft.com/.default' -tenantid $Tenant | Where-Object { $_.AccountEnabled -EQ $true } + $int = 0 + $Requests = foreach ($id in $GraphRequest.userPrincipalName) { + @{ + id = $int++ + method = 'GET' + url = "/users/$id/authentication/requirements" + } + } + $UsersWithoutMFA = (New-GraphBulkRequest -tenantid $tenant -scope 'https://graph.microsoft.com/.default' -Requests @($Requests) -asapp $true).body | Where-Object { $_.perUserMfaState -ne 'enforced' } | Select-Object peruserMFAState, @{Name = 'UserPrincipalName'; Expression = { [System.Web.HttpUtility]::UrlDecode($_.'@odata.context'.split("'")[1]) } } + + If ($Settings.remediate -eq $true) { + if ($UsersWithoutMFA) { + try { + $MFAMessage = Set-CIPPPeruserMFA -TenantFilter $Tenant -UserId $UsersWithoutMFA.UserPrincipalName -State 'enforced' + Write-LogMessage -API 'Standards' -tenant $tenant -message $MFAMessage -sev Info + } catch { + $ErrorMessage = Get-NormalizedError -Message $_.Exception.Message + Write-LogMessage -API 'Standards' -tenant $tenant -message "Failed to enforce MFA for all users: $ErrorMessage" -sev Error + } + } + } + if ($Settings.alert -eq $true) { + + if ($UsersWithoutMFA) { + Write-LogMessage -API 'Standards' -tenant $tenant -message "The following accounts do not have Legacy MFA Enforced: $($UsersWithoutMFA.UserPrincipalName -join ', ')" -sev Alert + } else { + Write-LogMessage -API 'Standards' -tenant $tenant -message 'No accounts do not have legacy per user MFA Enforced' -sev Info + } + } + if ($Settings.report -eq $true) { + Add-CIPPBPAField -FieldName 'LegacyMFAUsers' -FieldValue $UsersWithoutMFA -StoreAs json -Tenant $tenant + } +} diff --git a/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContent.ps1 b/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContent.ps1 new file mode 100644 index 000000000000..7a751bda4aef --- /dev/null +++ b/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContent.ps1 @@ -0,0 +1,27 @@ +function Get-CIPPAuditLogContent { + <# + .SYNOPSIS + Get the content of an audit log. + .PARAMETER ContentUri + The URI of the content to get. + .PARAMETER TenantFilter + The tenant to filter on. + .EXAMPLE + Get-CIPPAuditLogContent -ContentUri 'https://manage.office.com/api/v1.0/contoso.com/activity/feed/subscriptions/content?contentType=Audit.General&PublisherIdentifier=00000000-0000-0000-0000-000000000000' -TenantFilter 'contoso.com' + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + Param( + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [string[]]$ContentUri, + [Parameter(ValueFromPipelineByPropertyName = $true, Mandatory = $true)] + [string]$TenantFilter + ) + + Process { + foreach ($Uri in $ContentUri) { + New-GraphPOSTRequest -type GET -uri $Uri -tenantid $TenantFilter -scope 'https://manage.office.com/.default' + } + } +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContentBundles.ps1 b/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContentBundles.ps1 new file mode 100644 index 000000000000..363c31402feb --- /dev/null +++ b/Modules/CIPPCore/Public/Webhooks/Get-CIPPAuditLogContentBundles.ps1 @@ -0,0 +1,99 @@ +function Get-CIPPAuditLogContentBundles { + <# + .SYNOPSIS + Get the available audit log bundles + .DESCRIPTION + Query the Office 365 Activity Log API for available content bundles. + .PARAMETER TenantFilter + The tenant to filter on. + .PARAMETER ContentType + The type of content to get. + .PARAMETER StartTime + The start time to filter on. + .PARAMETER EndTime + The end time to filter on. + .PARAMETER ShowAll + Show all content, default is only show new content + .EXAMPLE + Get-CIPPAuditLogContentBundles -TenantFilter 'contoso.com' -ContentType 'Audit.AzureActiveDirectory' + .FUNCTIONALITY + Internal + #> + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$TenantFilter, + [Parameter(Mandatory = $true)] + [ValidateSet('Audit.AzureActiveDirectory', 'Audit.Exchange')] + [string]$ContentType, + [datetime]$StartTime, + [datetime]$EndTime, + [switch]$ShowAll + ) + + if ($TenantFilter -eq 'AllTenants') { + throw 'AllTenants is not a valid tenant filter for webhooks' + } + + if (!($TenantFilter -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')) { + $DefaultDomainName = $TenantFilter + $TenantFilter = (Get-Tenants | Where-Object { $_.defaultDomainName -eq $TenantFilter }).customerId + } else { + $DefaultDomainName = (Get-Tenants | Where-Object { $_.customerId -eq $TenantFilter }).defaultDomainName + } + + $WebhookTable = Get-CippTable -TableName 'webhookTable' + $WebhookConfig = Get-CIPPAzDataTableEntity @WebhookTable -Filter "PartitionKey eq '$DefaultDomainName' and Version eq '3' and Resource eq '$ContentType'" + if (!$WebhookConfig) { + throw "No webhook config found for $DefaultDomainName - $ContentType" + } + + $Parameters = @{ + 'contentType' = $ContentType + 'PublisherIdentifier' = $env:TenantId + } + + if (!$ShowAll.IsPresent) { + if ($null -ne $WebhookConfig.LastContentCreated) { + $StartTime = $WebhookConfig.LastContentCreated.DateTime.ToLocalTime() + $EndTime = $WebhookConfig.LastContentCreated.DateTime.ToLocalTime().AddHours(4) + } + } + + if ($StartTime) { + $Parameters.Add('startTime', $StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')) + if ($EndTime) { + $Parameters.Add('endTime', $EndTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')) + } else { + $Parameters.Add('endTime', ($StartTime).AddHours(24).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss')) + } + } + + $GraphQuery = [System.UriBuilder]('https://manage.office.com/api/v1.0/{0}/activity/feed/subscriptions/content' -f $TenantFilter) + $ParamCollection = [System.Web.HttpUtility]::ParseQueryString([String]::Empty) + foreach ($Item in ($Parameters.GetEnumerator())) { + $ParamCollection.Add($Item.Key, $Item.Value) + } + $GraphQuery.Query = $ParamCollection.ToString() + + Write-Verbose "GET [ $($GraphQuery.ToString()) ]" + $LogBundles = New-GraphGetRequest -uri $GraphQuery.ToString() -tenantid $TenantFilter -scope 'https://manage.office.com/.default' -IncludeResponseHeaders + $AuditLogContents = $LogBundles | Select-Object contentUri, contentCreated, @{Name = 'TenantFilter'; Expression = { $TenantFilter } } + + if (!$ShowAll.IsPresent) { + $LastContent = ($AuditLogContents | Sort-Object contentCreated -Descending | Select-Object -First 1 -ExpandProperty contentCreated) | Get-Date + if ($WebhookConfig.LastContentCreated) { + $AuditLogContents = $AuditLogContents | Where-Object { ($_.contentCreated | Get-Date).ToLocalTime() -gt $StartTime } + } + if ($LastContent) { + if ($WebhookConfig.PSObject.Properties.Name -contains 'LastContentCreated') { + $WebhookConfig.LastContentCreated = [datetime]$LastContent + } else { + $WebhookConfig | Add-Member -MemberType NoteProperty -Name LastContentCreated -Value '' + $WebhookConfig.LastContentCreated = [datetime]$LastContent + } + $null = Add-CIPPAzDataTableEntity @WebhookTable -Entity $WebhookConfig -Force + } + } + return $AuditLogContents +} \ No newline at end of file diff --git a/Modules/CIPPCore/Public/Invoke-CIPPGraphWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookProcessing.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-CIPPGraphWebhookProcessing.ps1 rename to Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookProcessing.ps1 diff --git a/Modules/CIPPCore/Public/Invoke-CIPPGraphWebhookRenewal.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-CIPPGraphWebhookRenewal.ps1 rename to Modules/CIPPCore/Public/Webhooks/Invoke-CIPPGraphWebhookRenewal.ps1 diff --git a/Modules/CIPPCore/Public/Invoke-CIPPPartnerWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-CIPPPartnerWebhookProcessing.ps1 rename to Modules/CIPPCore/Public/Webhooks/Invoke-CIPPPartnerWebhookProcessing.ps1 diff --git a/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 similarity index 97% rename from Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 rename to Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 index a1787ddbae34..d5db2cf47076 100644 --- a/Modules/CIPPCore/Public/Invoke-CIPPWebhookProcessing.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Invoke-CIPPWebhookProcessing.ps1 @@ -43,7 +43,7 @@ function Invoke-CippWebhookProcessing { "No Inbox Rules found for $username. We have not disabled any rules." } "Completed BEC Remediate for $username" - Write-LogMessage -API 'BECRemediate' -tenant $tenantfilter -message "Executed Remediation for $username" -sev 'Info' + Write-LogMessage -API 'BECRemediate' -tenant $tenantfilter -message "Executed Remediation for $username" -sev 'Info' } 'cippcommand' { $CommandSplat = @{} @@ -85,7 +85,7 @@ function Invoke-CippWebhookProcessing { ActionsTaken = [string]($ActionResults | ConvertTo-Json -Depth 15 -Compress) } | ConvertTo-Json -Depth 15 -Compress Write-Host 'Sending Webhook Content' - + #Write-Host $JsonContent Send-CIPPAlert -Type 'webhook' -Title $GenerateJSON.Title -JSONContent $JsonContent -TenantFilter $TenantFilter } } diff --git a/Modules/CIPPCore/Public/Invoke-RemoveWebhookAlert.ps1 b/Modules/CIPPCore/Public/Webhooks/Invoke-RemoveWebhookAlert.ps1 similarity index 100% rename from Modules/CIPPCore/Public/Invoke-RemoveWebhookAlert.ps1 rename to Modules/CIPPCore/Public/Webhooks/Invoke-RemoveWebhookAlert.ps1 diff --git a/Modules/CIPPCore/Public/New-CIPPGraphSubscription.ps1 b/Modules/CIPPCore/Public/Webhooks/New-CIPPGraphSubscription.ps1 similarity index 86% rename from Modules/CIPPCore/Public/New-CIPPGraphSubscription.ps1 rename to Modules/CIPPCore/Public/Webhooks/New-CIPPGraphSubscription.ps1 index 535156115d0b..eebd1163d61f 100644 --- a/Modules/CIPPCore/Public/New-CIPPGraphSubscription.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/New-CIPPGraphSubscription.ps1 @@ -21,42 +21,32 @@ function New-CIPPGraphSubscription { if ($auditLogAPI) { $CIPPID = (New-Guid).GUID $Resource = $EventType - $CIPPAuditURL = "$BaseURL/API/Publicwebhooks?EventType=$EventType&CIPPID=$CIPPID&version=2" - $AuditLogParams = @{ - webhook = @{ - 'address' = $CIPPAuditURL - } - } | ConvertTo-Json - #List existing webhook subscriptions in table - $WebhookFilter = "PartitionKey eq '$($TenantFilter)' and Resource eq '$Resource' and Version eq '2'" + $WebhookFilter = "PartitionKey eq '$($TenantFilter)' and Resource eq '$Resource' and Version eq '3'" $ExistingWebhooks = Get-CIPPAzDataTableEntity @WebhookTable -Filter $WebhookFilter $MatchedWebhook = $ExistingWebhooks try { if (!$MatchedWebhook) { $WebhookRow = @{ - PartitionKey = [string]$TenantFilter - RowKey = [string]$CIPPID - Resource = $Resource - Expiration = 'Does Not Expire' - WebhookNotificationUrl = [string]$CIPPAuditURL - Version = '2' + PartitionKey = [string]$TenantFilter + RowKey = [string]$CIPPID + Resource = [string]$Resource + Expiration = [string]'Does Not Expire' + Version = [string]'3' } Add-CIPPAzDataTableEntity @WebhookTable -Entity $WebhookRow Write-Host "Creating webhook subscription for $EventType" - $AuditLog = New-GraphPOSTRequest -uri "https://manage.office.com/api/v1.0/$($TenantFilter)/activity/feed/subscriptions/start?contentType=$EventType&PublisherIdentifier=$($TenantFilter)" -tenantid $TenantFilter -type POST -scope 'https://manage.office.com/.default' -body $AuditLogparams -verbose + $AuditLog = New-GraphPOSTRequest -type POST -uri "https://manage.office.com/api/v1.0/$($TenantFilter)/activity/feed/subscriptions/start?contentType=$EventType&PublisherIdentifier=$($env:TenantId)" -tenantid $TenantFilter -scope 'https://manage.office.com/.default' -body '{}' -verbose Write-LogMessage -user $ExecutingUser -API $APIName -message "Created Webhook subscription for $($TenantFilter) for the log $($EventType)" -Sev 'Info' -tenant $TenantFilter - } else { - Write-LogMessage -user $ExecutingUser -API $APIName -message "No webhook creation required for $($TenantFilter). Already exists" -Sev 'Info' -tenant $TenantFilter } - return @{ success = $true; message = "Created Webhook subscription for $($TenantFilter) for the log $($EventType)" } + return @{ Success = $true; message = "Created Webhook subscription for $($TenantFilter) for the log $($EventType)" } } catch { if ($_.Exception.Message -like '*already exists*') { return @{ success = $true; message = "Webhook exists for $($TenantFilter) for the log $($EventType)" } Write-LogMessage -user $ExecutingUser -API $APIName -message "Webhook subscription for $($TenantFilter) already exists" -Sev 'Info' -tenant $TenantFilter } else { Remove-AzDataTableEntity @WebhookTable -Entity @{ PartitionKey = $TenantFilter; RowKey = [string]$CIPPID } | Out-Null - Write-LogMessage -user $ExecutingUser -API $APIName -message "Failed to create Webhook Subscription for $($TenantFilter): $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter + Write-LogMessage -user $ExecutingUser -API $APIName -message "Failed to create Webhook Subscription for $($TenantFilter): $($_.Exception.Message)" -Sev 'Error' -tenant $TenantFilter -LogData (Get-CippException -Exception $_) return @{ success = $false; message = "Failed to create Webhook Subscription for $($TenantFilter): $($_.Exception.Message)" } } } @@ -95,11 +85,9 @@ function New-CIPPGraphSubscription { if ($Existing.WebhookUrl) { $Action = 'Updated' $Method = 'PUT' - Write-Host 'updating webhook' } else { $Action = 'Created' $Method = 'POST' - Write-Host 'creating webhook' } $Uri = 'https://api.partnercenter.microsoft.com/webhooks/v1/registration' diff --git a/Modules/CIPPCore/Public/Remove-CIPPGraphSubscription.ps1 b/Modules/CIPPCore/Public/Webhooks/Remove-CIPPGraphSubscription.ps1 similarity index 99% rename from Modules/CIPPCore/Public/Remove-CIPPGraphSubscription.ps1 rename to Modules/CIPPCore/Public/Webhooks/Remove-CIPPGraphSubscription.ps1 index 88138b99ccc3..e64f366d7830 100644 --- a/Modules/CIPPCore/Public/Remove-CIPPGraphSubscription.ps1 +++ b/Modules/CIPPCore/Public/Webhooks/Remove-CIPPGraphSubscription.ps1 @@ -13,7 +13,7 @@ function Remove-CIPPGraphSubscription { if ($Cleanup) { #list all subscriptions on the management API $Subscriptions = New-GraphPOSTRequest -type GET -uri "https://manage.office.com/api/v1.0/$($TenantFilter)/activity/feed/subscriptions/list" -scope 'https://manage.office.com/.default' -tenantid $TenantFilter -verbose - foreach ($Sub in $Subscriptions | Where-Object { $_.webhook.address -like '*CIPP*' -and $_.webhook.address -notlike '*version=2*' }) { + foreach ($Sub in $Subscriptions | Where-Object { $_.webhook.address -like '*CIPP*' -and $_.webhook.address -notlike '*version=3*' }) { Try { $AuditLog = New-GraphPOSTRequest -uri "https://manage.office.com/api/v1.0/$($TenantFilter)/activity/feed/subscriptions/stop?contentType=$($sub.contentType)" -scope 'https://manage.office.com/.default' -tenantid $TenantFilter -type POST -body '{}' -verbose Try { diff --git a/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 new file mode 100644 index 000000000000..763efa62656b --- /dev/null +++ b/Modules/CIPPCore/Public/Webhooks/Test-CIPPAuditLogRules.ps1 @@ -0,0 +1,190 @@ +function Test-CIPPAuditLogRules { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + $TenantFilter, + [Parameter(Mandatory = $true)] + [ValidateSet('Audit.AzureActiveDirectory', 'Audit.Exchange')] + $LogType, + [switch]$ShowAll + ) + + $Results = [PSCustomObject]@{ + TotalLogs = 0 + MatchedLogs = 0 + MatchedRules = @() + DataToProcess = @() + } + + $ExtendedPropertiesIgnoreList = @( + 'OAuth2:Authorize' + 'OAuth2:Token' + 'SAS:EndAuth' + 'SAS:ProcessAuth' + ) + + $TrustedIPTable = Get-CIPPTable -TableName 'trustedIps' + $ConfigTable = Get-CIPPTable -TableName 'WebhookRules' + $ConfigEntries = Get-CIPPAzDataTableEntity @ConfigTable + $Configuration = $ConfigEntries | Where-Object { ($_.Tenants -match $TenantFilter -or $_.Tenants -match 'AllTenants') } | ForEach-Object { + [pscustomobject]@{ + Tenants = ($_.Tenants | ConvertFrom-Json).fullValue + Conditions = $_.Conditions + Actions = $_.Actions + LogType = $_.Type + } + } + $ContentBundleQuery = @{ + TenantFilter = $TenantFilter + ContentType = $LogType + ShowAll = $ShowAll.IsPresent + } + Write-Information 'Getting data from Office 365 Management Activity API' + $Data = Get-CIPPAuditLogContentBundles @ContentBundleQuery | Get-CIPPAuditLogContent + $LogCount = ($Data | Measure-Object).Count + Write-Information "Logs to process: $LogCount" + $Results.TotalLogs = $LogCount + if ($LogCount -gt 0) { + $PreProccessedData = $Data | Select-Object *, CIPPAction, CIPPClause, CIPPGeoLocation, CIPPBadRepIP, CIPPHostedIP, CIPPIPDetected, CIPPLocationInfo, CIPPExtendedProperties, CIPPDeviceProperties, CIPPParameters, CIPPModifiedProperties -ErrorAction SilentlyContinue + $LocationTable = Get-CIPPTable -TableName 'knownlocationdb' + $ProcessedData = foreach ($Data in $PreProccessedData) { + try { + if ($Data.ExtendedProperties) { + $Data.CIPPExtendedProperties = ($Data.ExtendedProperties | ConvertTo-Json) + if ($Data.CIPPExtendedProperties.RequestType -in $ExtendedPropertiesIgnoreList) { + Write-Information 'No need to process this operation as its in our ignore list' + continue + } + $Data.ExtendedProperties | ForEach-Object { $Data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } + } + if ($Data.DeviceProperties) { + $Data.CIPPDeviceProperties = ($Data.DeviceProperties | ConvertTo-Json) + $Data.DeviceProperties | ForEach-Object { $Data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } + } + if ($Data.parameters) { + $Data.CIPPParameters = ($Data.parameters | ConvertTo-Json) + $Data.parameters | ForEach-Object { $Data | Add-Member -NotePropertyName $_.Name -NotePropertyValue $_.Value -Force -ErrorAction SilentlyContinue } + } + if ($Data.ModifiedProperties) { + $Data.CIPPModifiedProperties = ($Data.ModifiedProperties | ConvertTo-Json) + try { + $Data.ModifiedProperties | ForEach-Object { $Data | Add-Member -NotePropertyName "$($_.Name)" -NotePropertyValue "$($_.NewValue)" -Force -ErrorAction SilentlyContinue } + } catch { + #Write-Information ($Data.ModifiedProperties | ConvertTo-Json -Depth 10) + } + try { + $Data.ModifiedProperties | ForEach-Object { $Data | Add-Member -NotePropertyName $("Previous_Value_$($_.Name)") -NotePropertyValue "$($_.OldValue)" -Force -ErrorAction SilentlyContinue } + } catch { + #Write-Information ($Data.ModifiedProperties | ConvertTo-Json -Depth 10) + } + } + + if ($Data.clientip) { + if ($Data.clientip -match '^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+$') { + $Data.clientip = $Data.clientip -replace ':\d+$', '' # Remove the port number if present + } + # Check if IP is on trusted IP list + $TrustedIP = Get-CIPPAzDataTableEntity @TrustedIPTable -Filter "PartitionKey eq '$TenantFilter' and RowKey eq '$($Data.clientip)' and state eq 'Trusted'" + if ($TrustedIP) { + Write-Information "IP $($Data.clientip) is trusted" + continue + } + + $Location = Get-CIPPAzDataTableEntity @LocationTable -Filter "RowKey eq '$($Data.clientIp)'" | Select-Object -Last 1 + if ($Location) { + $Country = $Location.CountryOrRegion + $City = $Location.City + $Proxy = $Location.Proxy + $hosting = $Location.Hosting + $ASName = $Location.ASName + } else { + try { + $Location = Get-CIPPGeoIPLocation -IP $Data.clientip + } catch { + Write-Information "Unable to get IP location for $($Data.clientip): $($_.Exception.Messge)" + } + $Country = if ($Location.CountryCode) { $Location.CountryCode } else { 'Unknown' } + $City = if ($Location.City) { $Location.City } else { 'Unknown' } + $Proxy = if ($Location.Proxy -ne $null) { $Location.Proxy } else { 'Unknown' } + $hosting = if ($Location.Hosting -ne $null) { $Location.Hosting } else { 'Unknown' } + $ASName = if ($Location.ASName) { $Location.ASName } else { 'Unknown' } + $IP = $Data.ClientIP + $LocationInfo = @{ + RowKey = [string]$Data.clientip + PartitionKey = [string]$Data.id + Tenant = [string]$TenantFilter + CountryOrRegion = "$Country" + City = "$City" + Proxy = "$Proxy" + Hosting = "$hosting" + ASName = "$ASName" + } + try { + $null = Add-CIPPAzDataTableEntity @LocationTable -Entity $LocationInfo -Force + } catch { + Write-Information "Failed to add location info for $($Data.clientip) to cache: $($_.Exception.Message)" + + } + } + $Data.CIPPGeoLocation = $Country + $Data.CIPPBadRepIP = $Proxy + $Data.CIPPHostedIP = $hosting + $Data.CIPPIPDetected = $IP + $Data.CIPPLocationInfo = ($Location | ConvertTo-Json) + } + $Data | Select-Object * -ExcludeProperty ExtendedProperties, DeviceProperties, parameters + } catch { + Write-Information "Audit log: Error processing data: $($_.Exception.Message)`r`n$($_.InvocationInfo.PositionMessage)" + Write-LogMessage -API 'Webhooks' -message 'Error Processing Audit Log Data' -LogData (Get-CippException -Exception $_) -sev Error -tenant $TenantFilter + } + } + + #Filter data based on conditions. + $Where = $Configuration | Where-Object { $_.LogType -eq $LogType } | ForEach-Object { + $conditions = $_.Conditions | ConvertFrom-Json | Where-Object { $_.Input.value -ne '' } + $actions = $_.Actions + $conditionStrings = [System.Collections.Generic.List[string]]::new() + $CIPPClause = [System.Collections.Generic.List[string]]::new() + foreach ($condition in $conditions) { + $value = if ($condition.Input.value -is [array]) { + $arrayAsString = $condition.Input.value | ForEach-Object { + "'$_'" + } + "@($($arrayAsString -join ', '))" + } else { "'$($condition.Input.value)'" } + + $conditionStrings.Add("`$(`$_.$($condition.Property.label)) -$($condition.Operator.value) $value") + $CIPPClause.Add("$($condition.Property.label) is $($condition.Operator.label) $value") + } + $finalCondition = $conditionStrings -join ' -AND ' + + [PSCustomObject]@{ + clause = $finalCondition + expectedAction = $actions + CIPPClause = $CIPPClause + } + + } + Write-Information "Webhook: The list of operations in the data are $(($ProcessedData.operation | Select-Object -Unique) -join ', ')" + + $MatchedRules = [System.Collections.Generic.List[string]]::new() + $DataToProcess = foreach ($clause in $Where) { + Write-Information "Webhook: Processing clause: $($clause.clause)" + Write-Information "Webhook: If this clause would be true, the action would be: $($clause.expectedAction)" + $ReturnedData = $ProcessedData | Where-Object { Invoke-Expression $clause.clause } + if ($ReturnedData) { + $ReturnedData = foreach ($item in $ReturnedData) { + $item.CIPPAction = $clause.expectedAction + $item.CIPPClause = $clause.CIPPClause -join ' and ' + $MatchedRules.Add($clause.CIPPClause -join ' and ') + $item + } + } + $ReturnedData + } + $Results.MatchedRules = $MatchedRules | Select-Object -Unique + $Results.MatchedLogs = ($DataToProcess | Measure-Object).Count + $Results.DataToProcess = $DataToProcess + } + $Results +} \ No newline at end of file diff --git a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 b/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 index ebf491cec05a..9828682c6348 100644 --- a/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 +++ b/Modules/CippExtensions/NinjaOne/Invoke-NinjaOneTenantSync.ps1 @@ -267,7 +267,7 @@ function Invoke-NinjaOneTenantSync { @{ id = 'Users' method = 'GET' - url = '/users' + url = '/users?$top=999' }, @{ id = 'TenantDetails' @@ -292,7 +292,7 @@ function Invoke-NinjaOneTenantSync { @{ id = 'Devices' method = 'GET' - url = '/deviceManagement/managedDevices' + url = '/deviceManagement/managedDevices?$top=999' }, @{ id = 'DeviceCompliancePolicies' @@ -317,12 +317,12 @@ function Invoke-NinjaOneTenantSync { @{ id = 'SecureScore' method = 'GET' - url = '/security/secureScores' + url = '/security/secureScores?$top=999' }, @{ id = 'SecureScoreControlProfiles' method = 'GET' - url = '/security/secureScoreControlProfiles' + url = '/security/secureScoreControlProfiles?$top=999' }, @{ id = 'Subscriptions' diff --git a/Modules/CippExtensions/Public/Get-ExtensionRateLimit.ps1 b/Modules/CippExtensions/Public/Get-ExtensionRateLimit.ps1 index 2a0e718402f4..242d4dd86390 100644 --- a/Modules/CippExtensions/Public/Get-ExtensionRateLimit.ps1 +++ b/Modules/CippExtensions/Public/Get-ExtensionRateLimit.ps1 @@ -24,7 +24,6 @@ function Get-ExtensionRateLimit($ExtensionName, $ExtensionPartitionKey, $RateLim } if (($ActiveJobs | Measure-Object).count -ge $RateLimit) { Write-Host "Rate Limiting. Currently $($ActiveJobs.count) Active Jobs" - Start-Sleep -Seconds $WaitTime $CurrentMap = Get-ExtensionRateLimit -ExtensionName $ExtensionName -ExtensionPartitionKey $ExtensionPartitionKey -RateLimit $RateLimit -WaitTime $WaitTime } diff --git a/Scheduler_PollAuditLogs/function.json b/Scheduler_PollAuditLogs/function.json new file mode 100644 index 000000000000..f30537d11b34 --- /dev/null +++ b/Scheduler_PollAuditLogs/function.json @@ -0,0 +1,15 @@ +{ + "bindings": [ + { + "name": "Timer", + "schedule": "0 */15 * * * *", + "direction": "in", + "type": "timerTrigger" + }, + { + "name": "starter", + "type": "durableClient", + "direction": "in" + } + ] +} diff --git a/Scheduler_PollAuditLogs/run.ps1 b/Scheduler_PollAuditLogs/run.ps1 new file mode 100644 index 000000000000..9fb8105f809e --- /dev/null +++ b/Scheduler_PollAuditLogs/run.ps1 @@ -0,0 +1,31 @@ +param($Timer) + +try { + $webhookTable = Get-CIPPTable -tablename webhookTable + $Webhooks = Get-CIPPAzDataTableEntity @webhookTable -Filter "Version eq '3'" | Where-Object { $_.Resource -match '^Audit' } + if (($Webhooks | Measure-Object).Count -eq 0) { + Write-Host 'No webhook subscriptions found. Exiting.' + return + } + + try { + $RunningQueue = Invoke-ListCippQueue | Where-Object { $_.Reference -eq 'AuditLogCollection' -and $_.Status -ne 'Completed' -and $_.Status -ne 'Failed' -and $_.Timestamp.DateTime.ToLocalTime() -lt (Get-Date).AddMinutes(-10) } + if ($RunningQueue) { + Write-Host 'Audit log collection already running' + return + } + } catch {} + + $Queue = New-CippQueueEntry -Name 'Audit Log Collection' -Reference 'AuditLogCollection' -TotalTasks ($Webhooks | Sort-Object -Property PartitionKey -Unique | Measure-Object).Count + $Batch = $Webhooks | Sort-Object -Property PartitionKey -Unique | Select-Object @{Name = 'TenantFilter'; Expression = { $_.PartitionKey } }, @{Name = 'QueueId'; Expression = { $Queue.RowKey } }, @{Name = 'FunctionName'; Expression = { 'AuditLogBundleProcessing' } } + $InputObject = [PSCustomObject]@{ + OrchestratorName = 'AuditLogs' + Batch = @($Batch) + SkipLog = $true + } + $InstanceId = Start-NewOrchestration -FunctionName 'CIPPOrchestrator' -InputObject ($InputObject | ConvertTo-Json -Depth 5 -Compress) + Write-Host "Started orchestration with ID = '$InstanceId'" +} catch { + Write-LogMessage -API 'Webhooks' -message 'Error processing webhooks' -sev Error -LogData (Get-CippException -Exception $_) + Write-Host ( 'Webhook error {0} line {1} - {2}' -f $_.InvocationInfo.ScriptName, $_.InvocationInfo.ScriptLineNumber, $_.Exception.Message) +} diff --git a/SendStats/run.ps1 b/SendStats/run.ps1 index b4427d230338..2e30b113ab55 100644 --- a/SendStats/run.ps1 +++ b/SendStats/run.ps1 @@ -17,6 +17,7 @@ $SendingObject = [PSCustomObject]@{ SetupComplete = $SetupComplete RunningVersionAPI = $APIVersion.trim() CountOfTotalTenants = $tenantcount + uid = $env:TenantID } | ConvertTo-Json Invoke-RestMethod -Uri 'https://management.cipp.app/api/stats' -Method POST -Body $SendingObject -ContentType 'application/json' \ No newline at end of file diff --git a/version_latest.txt b/version_latest.txt index dfa102a57492..a94a88fbb889 100644 --- a/version_latest.txt +++ b/version_latest.txt @@ -1 +1 @@ -5.7.4 +5.8.5 \ No newline at end of file