Scripts/Send-ToFreshservice.ps1
|
<#
.SYNOPSIS Attaches an IT support diagnostic report to an existing Freshservice ticket as a note. .DESCRIPTION Posts a note (private by default) to an existing Freshservice ticket, with the specified file attached. This is intentionally scoped to attaching evidence to a ticket that already exists - it does not create new tickets, since ticket creation typically requires organization- specific required fields (priority, status, custom fields) that this script cannot safely guess at. Creating the ticket is left to your normal Freshservice workflow; this script closes the gap of manually downloading a report file and re-uploading it through the web UI. Credentials are read from environment variables only - never from command-line arguments - so they cannot leak into shell/PowerShell history or be visible to other users via process listings. See README.md in integrations/ for setup instructions. Targets PowerShell 5.1 for compatibility with the rest of this repository, which means multipart/form-data upload is built using System.Net.Http.HttpClient directly rather than Invoke-RestMethod's -Form parameter (only available in PowerShell 6.1+). .NOTES Author: it-support-ops repository Run as: Standard user (no elevation required) Compatibility: Windows 10, Windows 11, PowerShell 5.1+ Required env: FRESHSERVICE_DOMAIN, FRESHSERVICE_API_KEY .EXAMPLE $env:FRESHSERVICE_DOMAIN = "yourcompany.freshservice.com" $env:FRESHSERVICE_API_KEY = "your-api-key" .\Send-ToFreshservice.ps1 -TicketId 12345 -FilePath C:\IT-Diagnostics\report.txt .EXAMPLE .\Send-ToFreshservice.ps1 -TicketId 12345 -FilePath report.txt -Message "Diagnostic report attached" -Public Posts a public note (visible to the requester) instead of the default private one. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Interactive console tool by design: status and error messages are meant for a human reading the console, not for pipeline consumption.')] [CmdletBinding()] param( # Ticket ID to attach the report to. [Parameter(Mandatory = $true)] [int]$TicketId, # Path to the file to attach. [Parameter(Mandatory = $true)] [string]$FilePath, # Note text. Defaults to a generic "diagnostic report attached" message. [Parameter(Mandatory = $false)] [string]$Message = "Diagnostic report attached via it-support-ops.", # Make the note public (visible to the requester). Default: private. [Parameter(Mandatory = $false)] [switch]$Public ) $ErrorActionPreference = "Stop" # --------------------------------------------------------------------------- # VALIDATION # --------------------------------------------------------------------------- # Fail clearly and early rather than making a request that will confusingly # fail server-side. Every check here has a specific, actionable message. if (-not (Test-Path -LiteralPath $FilePath -PathType Leaf)) { Write-Error "File not found: $FilePath" exit 1 } $domain = $env:FRESHSERVICE_DOMAIN $apiKey = $env:FRESHSERVICE_API_KEY if ([string]::IsNullOrWhiteSpace($domain)) { Write-Error "FRESHSERVICE_DOMAIN environment variable is not set. Run:`n `$env:FRESHSERVICE_DOMAIN = `"yourcompany.freshservice.com`"`nSee README.md in integrations/ for setup instructions." exit 1 } if ([string]::IsNullOrWhiteSpace($apiKey)) { Write-Error "FRESHSERVICE_API_KEY environment variable is not set. Run:`n `$env:FRESHSERVICE_API_KEY = `"your-api-key`"`nSee README.md in integrations/ for how to generate an API key." exit 1 } $isPrivate = -not $Public.IsPresent if ($Public.IsPresent) { Write-Host "NOTE: This note will be PUBLIC and visible to the ticket requester." } # --------------------------------------------------------------------------- # SEND # --------------------------------------------------------------------------- # Freshservice API v2: POST /api/v2/tickets/{id}/notes, multipart/form-data # for attachments, Basic auth with the API key as username and a literal # "X" as password (the standard Freshworks API auth convention). $url = "https://$domain/api/v2/tickets/$TicketId/notes" Add-Type -AssemblyName System.Net.Http $httpClient = $null $fileStream = $null $content = $null try { $httpClient = New-Object System.Net.Http.HttpClient $authBytes = [System.Text.Encoding]::ASCII.GetBytes("${apiKey}:X") $authValue = [Convert]::ToBase64String($authBytes) $httpClient.DefaultRequestHeaders.Authorization = ` New-Object System.Net.Http.Headers.AuthenticationHeaderValue("Basic", $authValue) $content = New-Object System.Net.Http.MultipartFormDataContent $content.Add((New-Object System.Net.Http.StringContent($Message)), "body") $privateString = if ($isPrivate) { "true" } else { "false" } $content.Add((New-Object System.Net.Http.StringContent($privateString)), "private") $fileStream = [System.IO.File]::OpenRead((Resolve-Path -LiteralPath $FilePath)) $fileContent = New-Object System.Net.Http.StreamContent($fileStream) $fileName = [System.IO.Path]::GetFileName($FilePath) $content.Add($fileContent, "attachments[]", $fileName) Write-Host "Attaching $FilePath to ticket #$TicketId on $domain ..." $response = $httpClient.PostAsync($url, $content).GetAwaiter().GetResult() $statusCode = [int]$response.StatusCode $responseBody = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult() switch ($statusCode) { { $_ -in 200, 201 } { Write-Host "SUCCESS: Note added to ticket #$TicketId." } 401 { Write-Error "Authentication failed (401). Check FRESHSERVICE_API_KEY is correct and has not been regenerated/revoked." exit 1 } 403 { Write-Error "Forbidden (403). This API key does not have permission to add notes to this ticket." exit 1 } 404 { Write-Error "Ticket #$TicketId not found (404). Check the ticket ID and that FRESHSERVICE_DOMAIN points to the correct account." exit 1 } 429 { Write-Error "Rate limited (429). Freshservice's API has a per-account rate limit - wait a moment and try again." exit 1 } default { Write-Error "Unexpected response (HTTP ${statusCode}): $responseBody" exit 1 } } } finally { if ($null -ne $fileStream) { $fileStream.Dispose() } if ($null -ne $content) { $content.Dispose() } if ($null -ne $httpClient) { $httpClient.Dispose() } } |