From 5e3a037db46ab34ea49a945c5f5a03269c472596 Mon Sep 17 00:00:00 2001 From: James Tarran Date: Tue, 20 Jan 2026 12:16:05 +0000 Subject: [PATCH] Feat - Add feature to update existing Halo ticket when performing a user offboarding --- .../Push-ExecScheduledCommand.ps1 | 2 +- .../Users/Invoke-ExecOffboardUser.ps1 | 1 + Modules/CIPPCore/Public/Send-CIPPAlert.ps1 | 2 + .../Public/Halo/New-HaloPSATicket.ps1 | 186 +++++++++++------- .../Public/New-CippExtAlert.ps1 | 7 +- 5 files changed, 125 insertions(+), 73 deletions(-) diff --git a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 index ccc7249ed798..898fd50c7033 100644 --- a/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/Activity Triggers/Push-ExecScheduledCommand.ps1 @@ -329,7 +329,7 @@ function Push-ExecScheduledCommand { Write-Information 'Scheduler: Sending the results to the target.' Write-Information "The content of results is: $Results" switch -wildcard ($task.PostExecution) { - '*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $Tenant } + '*psa*' { Send-CIPPAlert -Type 'psa' -Title $title -HTMLContent $HTML -TenantFilter $Tenant -PSATicketId $task.PostExecution.psaTicketId } '*email*' { Send-CIPPAlert -Type 'email' -Title $title -HTMLContent $HTML -TenantFilter $Tenant } '*webhook*' { $Webhook = [PSCustomObject]@{ diff --git a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 index 0ca013970a1f..57e8a98cc9b8 100644 --- a/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 +++ b/Modules/CIPPCore/Public/Entrypoints/HTTP Functions/Identity/Administration/Users/Invoke-ExecOffboardUser.ps1 @@ -34,6 +34,7 @@ function Invoke-ExecOffboardUser { Webhook = [bool]$Request.Body.PostExecution.webhook Email = [bool]$Request.Body.PostExecution.email PSA = [bool]$Request.Body.PostExecution.psa + psaTicketId = [int]$Request.Body.PostExecution.psaTicketId } Reference = $Request.Body.reference } diff --git a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 index 3451827a67d3..1a299d05d37a 100644 --- a/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 +++ b/Modules/CIPPCore/Public/Send-CIPPAlert.ps1 @@ -9,6 +9,7 @@ function Send-CIPPAlert { $TenantFilter, $altEmail, $altWebhook, + $PSATicketId, $APIName = 'Send Alert', $Headers, $TableName, @@ -171,6 +172,7 @@ function Send-CIPPAlert { TenantId = $TenantFilter AlertText = "$HTMLContent" AlertTitle = "$($Title)" + TicketId = $PSATicketId } New-CippExtAlert -Alert $Alert Write-LogMessage -API 'Webhook Alerts' -tenant $TenantFilter -message "Sent PSA alert $title" -sev info diff --git a/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 b/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 index 0249248c88ec..fb20f437410d 100644 --- a/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 +++ b/Modules/CippExtensions/Public/Halo/New-HaloPSATicket.ps1 @@ -1,63 +1,103 @@ function New-HaloPSATicket { [CmdletBinding(SupportsShouldProcess)] param ( - $title, - $description, - $client + $Title, + $Description, + $Client, + $TicketId ) - #Get HaloPSA Token based on the config we have. + + # Load Halo configuration $Table = Get-CIPPTable -TableName Extensionsconfig $Configuration = ((Get-CIPPAzDataTableEntity @Table).config | ConvertFrom-Json).HaloPSA $TicketTable = Get-CIPPTable -TableName 'PSATickets' - $token = Get-HaloToken -configuration $Configuration - # sha hash title - $TitleHash = Get-StringHash -String $title + $Token = Get-HaloToken -configuration $Configuration + + # Helper to add a note to an existing ticket + function Add-HaloTicketNote { + param ($TicketId, $Html) + + $Object = [PSCustomObject]@{ + ticket_id = $TicketId + outcome_id = 7 + hiddenfromuser = $true + note_html = $Html + } + + if ($Configuration.Outcome) { + $Object.outcome_id = $Configuration.Outcome.value ?? $Configuration.Outcome + } + + $Body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) + + if ($PSCmdlet.ShouldProcess("HaloPSA Ticket $TicketId", 'Add note')) { + Invoke-RestMethod ` + -Uri "$($Configuration.ResourceURL)/actions" ` + -ContentType 'application/json; charset=utf-8' ` + -Method Post ` + -Body $Body ` + -Headers @{ Authorization = "Bearer $($Token.access_token)" } + } + } + + if ($TicketId) { + Write-Information "Explicit PSA Ticket ID provided: $TicketId" + + try { + $Ticket = Invoke-RestMethod ` + -Uri "$($Configuration.ResourceURL)/Tickets/$TicketId?includedetails=true&includelastaction=false" ` + -ContentType 'application/json; charset=utf-8' ` + -Method Get ` + -Headers @{ Authorization = "Bearer $($Token.access_token)" } ` + -SkipHttpErrorCheck + + if ($Ticket.id -and -not $Ticket.hasbeenclosed) { + Write-Information "Ticket $TicketId is open. Appending note." + Add-HaloTicketNote -TicketId $TicketId -Html $Description + return "Note added to HaloPSA ticket $TicketId" + } + + Write-Information "Ticket $TicketId is closed or not found. Creating new ticket." + } + catch { + $Message = $_.Exception.Message + Write-LogMessage ` + -API 'HaloPSATicket' ` + -sev Error ` + -message "Failed to update HaloPSA ticket $TicketId: $Message" ` + -LogData (Get-CippException -Exception $_) + return "Failed to update HaloPSA ticket $TicketId: $Message" + } + } + + $TitleHash = Get-StringHash -String $Title if ($Configuration.ConsolidateTickets) { - $ExistingTicket = Get-CIPPAzDataTableEntity @TicketTable -Filter "PartitionKey eq 'HaloPSA' and RowKey eq '$($client)-$($TitleHash)'" + $ExistingTicket = Get-CIPPAzDataTableEntity ` + @TicketTable ` + -Filter "PartitionKey eq 'HaloPSA' and RowKey eq '$($Client)-$($TitleHash)'" + if ($ExistingTicket) { - Write-Information "Ticket already exists in HaloPSA: $($ExistingTicket.TicketID)" - - $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets/$($ExistingTicket.TicketID)?includedetails=true&includelastaction=false&nocache=undefined&includeusersassets=false&isdetailscreen=true" -ContentType 'application/json; charset=utf-8' -Method Get -Headers @{Authorization = "Bearer $($token.access_token)" } -SkipHttpErrorCheck - if ($Ticket.id) { - if (!$Ticket.hasbeenclosed) { - Write-Information 'Ticket is still open, adding new note' - $Object = [PSCustomObject]@{ - ticket_id = $ExistingTicket.TicketID - outcome_id = 7 - hiddenfromuser = $true - note_html = $description - } - - if ($Configuration.Outcome) { - $Outcome = $Configuration.Outcome.value ?? $Configuration.Outcome - $Object.outcome_id = $Outcome - } - - $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) - try { - if ($PSCmdlet.ShouldProcess('Add note to HaloPSA ticket', 'Add note')) { - $Action = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/actions" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } - Write-Information "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" - } - return "Note added to ticket in HaloPSA: $($ExistingTicket.TicketID)" - } - catch { - $Message = if ($_.ErrorDetails.Message) { - Get-NormalizedError -Message $_.ErrorDetails.Message - } - else { - $_.Exception.message - } - Write-LogMessage -message "Failed to add note to HaloPSA ticket: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) - Write-Information "Failed to add note to HaloPSA ticket: $Message" - Write-Information "Body we tried to ship: $body" - return "Failed to add note to HaloPSA ticket: $Message" - } + Write-Information "Consolidated ticket found: $($ExistingTicket.TicketID)" + + try { + $Ticket = Invoke-RestMethod ` + -Uri "$($Configuration.ResourceURL)/Tickets/$($ExistingTicket.TicketID)?includedetails=true&includelastaction=false" ` + -ContentType 'application/json; charset=utf-8' ` + -Method Get ` + -Headers @{ Authorization = "Bearer $($Token.access_token)" } ` + -SkipHttpErrorCheck + + if ($Ticket.id -and -not $Ticket.hasbeenclosed) { + Write-Information "Consolidated ticket open. Appending note." + Add-HaloTicketNote -TicketId $ExistingTicket.TicketID -Html $Description + return "Note added to HaloPSA ticket $($ExistingTicket.TicketID)" } + + Write-Information "Consolidated ticket closed. Creating new ticket." } - else { - Write-Information 'Existing ticket could not be found. Creating a new ticket instead.' + catch { + Write-Information "Failed to read consolidated ticket. Creating new ticket." } } } @@ -69,13 +109,13 @@ function New-HaloPSATicket { id = -1 lookupdisplay = 'Enter Details Manually' } - client_id = ($client | Select-Object -Last 1) + client_id = ($Client | Select-Object -Last 1) _forcereassign = $true site_id = $null user_name = $null reportedby = $null - summary = $title - details_html = $description + summary = $Title + details_html = $Description donotapplytemplateintheapi = $true attachments = @() _novalidate = $true @@ -83,42 +123,46 @@ function New-HaloPSATicket { if ($Configuration.TicketType) { $TicketType = $Configuration.TicketType.value ?? $Configuration.TicketType - $object | Add-Member -MemberType NoteProperty -Name 'tickettype_id' -Value $TicketType -Force + $Object | Add-Member -MemberType NoteProperty -Name 'tickettype_id' -Value $TicketType -Force } - #use the token to create a new ticket in HaloPSA - $body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) - Write-Information 'Sending ticket to HaloPSA' - Write-Information $body + $Body = ConvertTo-Json -Compress -Depth 10 -InputObject @($Object) + + Write-Information 'Creating new HaloPSA ticket' + try { - if ($PSCmdlet.ShouldProcess('Send ticket to HaloPSA', 'Create ticket')) { - $Ticket = Invoke-RestMethod -Uri "$($Configuration.ResourceURL)/Tickets" -ContentType 'application/json; charset=utf-8' -Method Post -Body $body -Headers @{Authorization = "Bearer $($token.access_token)" } + if ($PSCmdlet.ShouldProcess('HaloPSA', 'Create ticket')) { + $Ticket = Invoke-RestMethod ` + -Uri "$($Configuration.ResourceURL)/Tickets" ` + -ContentType 'application/json; charset=utf-8' ` + -Method Post ` + -Body $Body ` + -Headers @{ Authorization = "Bearer $($Token.access_token)" } + Write-Information "Ticket created in HaloPSA: $($Ticket.id)" if ($Configuration.ConsolidateTickets) { $TicketObject = [PSCustomObject]@{ PartitionKey = 'HaloPSA' - RowKey = "$($client)-$($TitleHash)" - Title = $title - ClientId = $client + RowKey = "$($Client)-$($TitleHash)" + Title = $Title + ClientId = $Client TicketID = $Ticket.id } Add-CIPPAzDataTableEntity @TicketTable -Entity $TicketObject -Force Write-Information 'Ticket added to consolidation table' } + return "Ticket created in HaloPSA: $($Ticket.id)" } } catch { - $Message = if ($_.ErrorDetails.Message) { - Get-NormalizedError -Message $_.ErrorDetails.Message - } - else { - $_.Exception.message - } - Write-LogMessage -message "Failed to send ticket to HaloPSA: $Message" -API 'HaloPSATicket' -sev Error -LogData (Get-CippException -Exception $_) - Write-Information "Failed to send ticket to HaloPSA: $Message" - Write-Information "Body we tried to ship: $body" - return "Failed to send ticket to HaloPSA: $Message" + $Message = $_.Exception.Message + Write-LogMessage ` + -API 'HaloPSATicket' ` + -sev Error ` + -message "Failed to create HaloPSA ticket: $Message" ` + -LogData (Get-CippException -Exception $_) + return "Failed to create HaloPSA ticket: $Message" } } diff --git a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 index bae549bf0719..b47599f2a864 100644 --- a/Modules/CippExtensions/Public/New-CippExtAlert.ps1 +++ b/Modules/CippExtensions/Public/New-CippExtAlert.ps1 @@ -20,7 +20,12 @@ function New-CippExtAlert { Write-Host "MappedId: $MappedId" if (!$mappedId) { $MappedId = 1 } Write-Host "MappedId: $MappedId" - New-HaloPSATicket -Title $Alert.AlertTitle -Description $Alert.AlertText -Client $mappedId + New-HaloPSATicket ` + -Title $Alert.AlertTitle ` + -Description $Alert.AlertText ` + -Client $mappedId ` + -TicketId $Alert.TicketId + } } 'Gradient' {