FreshService.Tools.psm1
# Function to generate the appropriate Urgency and Impact values for a given Priority based on freshservice's default Priority Matrix function Get-FsMatrixPriority { <# .SYNOPSIS Get the FreshService MatrixPriority from the requested Priority .DESCRIPTION When FreshService is configured with "Priority Matrix" enabled, the supplied priority is ignored, and is instead calculated. The calculation is based on the Urgency and Impact of the requested ticket. The associated assets can override the Impact as well. The Priority can be overwritten for each combination for Urgency and Impact values, however, this module will focus on the default combination. This function looks up the required values for Urgency and Impact based on the provided Priority against the default matrix configuration. .PARAMETER TicketPriority The desired, resultant priority of your ticket .INPUTS None .OUTPUTS Magic value that can be used as Urgency and Impact when raising a ticket .EXAMPLE PS C:\> $MatrixMagic = Get-FsMatrixPriority -TicketPriority 3 .LINK https://github.com/jberkers42/Elastic.Helper #> [CmdletBinding()] Param ( [int] [Parameter(Mandatory=$true, ValueFromPipeline=$true)] [ValidateRange(1,4)] $TicketPriority ) begin { $PriorityMatrix = [PSCustomObject]@{ 4 = @(3, 3) 3 = @(3, 2) 2 = @(2, 2) 1 = @(1, 1) } Write-Debug "Configured priority matrix (Priority: [Urgency, Impact]) :: $($PriorityMatrix | ConvertTo-Json -Compress)" } process { Write-Verbose "Looking up 'Magic' value for Priority: $TicketPriority" return $PriorityMatrix.$TicketPriority } } Function Write-ModuleCredential { <# .SYNOPSIS Writes the specified API Credential to the configuration directory of FreshService.Tools. Prompts to overwrite if the credential exists. .PARAMETER AppId The object identifier for an application from Lrt.Config.Input (example: "FreshService") .PARAMETER AppName The value of the "Name" field of an application from Lrt.Config.Input (example: "FreshService") .PARAMETER Username The value of the "Username" part of the credential object .PARAMETER UserCredential Switch from asking for an API Key to ask for a User Password. The value of the "Username" part of the credential object .EXAMPLE PS C:\> #> [CmdletBinding()] Param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [string] $AppId, [Parameter(Mandatory = $true, Position = 2)] [ValidateNotNullOrEmpty()] [string] $AppName, [Parameter(Mandatory = $false, Position = 3)] [ValidateNotNullOrEmpty()] [string] $Username, [Parameter(Mandatory = $false, Position = 4)] [switch] $UserCredential = $false ) $LocalAppData = [Environment]::GetFolderPath("LocalApplicationData") # Configuration directory: config.json & api keys will be stored in Local ApplicationDatas $ConfigDirPath = Join-Path ` -Path $LocalAppData ` -ChildPath $ModuleName # Determine the filename and save location for this key if ($UserCredential) { $KeyFileName = $AppId + ".Credential.xml" } else { $KeyFileName = $AppId + ".ApiKey.xml" } $KeyPath = Join-Path -Path $ConfigDirPath -ChildPath $KeyFileName # Prompt to Overwrite existing key if(Test-Path -Path $KeyPath) { $OverWrite = Confirm-YesNo -Message " Credential Exists for $KeyFileName, overwrite?" -ForegroundColor Yellow if (! $OverWrite) { return $null } } # Prompt for Key / Password $Key = "" if ($UserCredential) { $Key = Read-Host -AsSecureString -Prompt " > Password for $AppName" } else { while ($Key.Length -lt 10) { $Key = Read-Host -AsSecureString -Prompt " > API Key for $AppName" if ($Key.Length -lt 10) { # Hint - API Keys should probably be longer, if possible $SaveColour = $host.UI.RawUI.ForegroundColor $host.UI.RawUI.ForegroundColor = 'Magenta' Write-Output " Key less than 10 characters." -ForegroundColor Magenta $host.UI.RawUI.ForegroundColor = $SaveColour } } } # Create credential - with username if provided if (! [string]::IsNullOrEmpty($Username)) { $_cred = [PSCredential]::new($Username, $Key) } else { $_cred = [PSCredential]::new($AppId, $Key) } $OutObject = [PSCustomObject]@{ Valid = $null Error = $null } Try { Export-Clixml -Path $ConfigDirPath\$KeyFileName -InputObject $_cred return } Catch { $OutObject.Valid = $false $OutObject.Error = $_.ErrorDetails.Message return $OutObject } } # Function to get AgentId by Agent Name function Get-FsAgent { <# .DESCRIPTION Gets details of the Agent, given the name or Id. If neither are provided, all Agents are returned. .SYNOPSIS Get details of a Agent, or a list of all Agents .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Agent .PARAMETER Email Email Address of the Agent .PARAMETER Filter Filter Query for list of agents .PARAMETER Id Display Id of the Agent (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Agent properties if found Returns a list of PSCustomObject containing all Agents Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAgent -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of the Agent to retrieve details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ByEmail', HelpMessage = 'Email Address of the Agent to retrieve details for')] [string] $Email, [Parameter(Mandatory=$true, ParameterSetName = 'ByFilter', HelpMessage = 'Filter criteria to match agents to return')] [string] $Filter, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Id of the Agent to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($PSBoundParameters.ContainsKey('Id')) { $AgentApiUrl_Get = [io.path]::combine($Uri, "agents/", $Id) } else { $AgentApiUrl_Get = [io.path]::combine($Uri, "agents") } $Parameters = @() if (-not $id -and -not $name ) { $Parameters += "per_page={0}" -f $PerPage } if ($Name) { # An Agent name was specified $AgentFilter = 'query="name:''' + $Name + '''"' $Parameters += $AgentFilter } if ($Email) { # An Agent e-mail address was specified $AgentFilter = 'query="email:''' + $Email + '''"' $Parameters += $AgentFilter } if ($Filter) { # An Agent filter was specified $AgentFilter = 'query="' + $Filter + '"' $Parameters += $AgentFilter } $ParamString = $Parameters -join '&' $AgentApiUrl_Get += "?" + $ParamString Write-Verbose "API URL $AgentApiUrl_Get" } Process { $Agents = $null # API Call to get the tag $Agents = Invoke-FreshServiceApiRequest -Uri $AgentApiUrl_Get -Credential $Credential if ($Agents -is [HashTable] -and $Agents.Error) { if ($Agents.Code -eq 404 -and $Id) { # An Agent with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Agents API. Response Code: $($Agents.Code) Note: $($Agents.Note)" -ErrorId $Agents.Code -CategoryReason $Agents.Note return $Agents } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Agent Write-Output $Agents.Agent } else { Write-Output $Agents.Agents } } } # Function to get AgentId by Agent Name function Get-FsAgentGroup { <# .DESCRIPTION Gets details of the Agent Group, given the name or Id. If neither are provided, all Agents Groups are returned. .SYNOPSIS Get details of a Agent Group, or a list of all Agent Groups .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Agent Group .PARAMETER regex Use the Name parameter as a regular expression .PARAMETER Id Id of the Agent Group (the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Agent Group properties if found Returns a list of PSCustomObject containing all Agent Groups Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAgentGroup -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-Credential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of the Agent Group to retrieve details for')] [string] $Name, [Parameter(Mandatory=$false, ParameterSetName = 'ByName', HelpMessage = 'Treat the Name parameter as a regex value')] [switch] $regex, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Id of the Agent to retrieve details for')] [int64] $Id ) Begin { # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($PSBoundParameters.ContainsKey('Id')) { $GroupApiUrl_Get = [io.path]::combine($Uri, "groups/", $Id) } else { $GroupApiUrl_Get = [io.path]::combine($Uri, "groups") } Write-Verbose "API URL $GroupApiUrl_Get" } Process { $Groups = $null # API Call to get the tag $Groups = Invoke-FreshServiceApiRequest -Uri $GroupApiUrl_Get -Credential $Credential if ($Groups -is [HashTable] -and $Groups.Error) { if ($Groups.Code -eq 404 -and $Id) { # An Agent with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Agents API. Response Code: $($Groups.Code) Note: $($Groups.Note)" -ErrorId $Groups.Code -CategoryReason $Groups.Note return $Groups } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($Group in $Groups.groups) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $Group.name" if ($PSBoundParameters.ContainsKey('regex')) { if ($Group.name -match $Name) { Write-Output $Group } } else { if ($Group.name -eq $Name) { Write-Output $Group } } } } elseif ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Agent Write-Output $Groups.group } else { Write-Output $Groups.groups } } } function Invoke-FreshServiceAlertRequest { <# .SYNOPSIS Invoke the FreshService Alert interface .DESCRIPTION This function is intended to be called by other functions for specific resources/interactions .PARAMETER Uri Alert URL for the specific Alert Integration. Obtain from Admin -> IT Operations Management -> ALert Profiles -> Profiles -> Integrations .PARAMETER Credential PSCredential Object with the Auth Key stored in the Password property of the object. .PARAMETER Method Valid HTTP Method to use: GET, POST (Default), DELETE, PUT .PARAMETER Body PSCustomObject containing data to be sent as HTTP Request Body in JSON format. .OUTPUTS PSCustomObject containing results if successful. May be $null if no data is returned ErrorObject containing details of error if one is encountered. #> [CmdletBinding()] param( [Parameter(Mandatory=$true, HelpMessage = 'Full URI to requeste resource, including URI parameters')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing the Auth Key in the Password property')] [PSCredential] $Credential, [Parameter(Mandatory=$false, HelpMessage = 'Method to use when making the request. Defaults to GET')] [ValidateSet("Post","Get","Put","Delete")] [string] $Method = "POST", [Parameter(Mandatory=$true, HelpMessage = 'PsCustomObject containing data that will be sent as the Json Body')] [PsCustomObject] $Body ) Begin { $Me = $MyInvocation.MyCommand.Name Write-Verbose $Me if (($Method -eq 'GET') -and $Body) { throw "Cannot specify Request Body for Method GET." } $Header = @{} $AuthKey = $Credential.GetNetworkCredential().Password $Header.Add('Authorization', ("auth-key {0}" -f $AuthKey)) $Header.Add('Content-Type', 'application/json') } Process { # Setup Error Object structure $ErrorObject = [PSCustomObject]@{ Code = $null Error = $false Type = $null Note = $null Raw = $_ } $Results = $null # Enforce TLSv1.2 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 # Make the Alert Call # Make the Alert Call, using the supplied Body. Contents of $Body are the responsibility of the calling code. Write-Verbose "$Me : Body supplied" try { $Results = Invoke-RestMethod -Method $Method -Uri $Uri -Headers $Header -Body ($Body|ConvertTo-Json -Depth 10) -ResponseHeadersVariable ResponseHeaders } catch { $Exception = $_.Exception Write-Verbose "$Me : Exception : $($Exception.StatusCode)" $ErrorObject.Error = $true $ErrorObject.Code = $Exception.StatusCode $ErrorObject.Note = $Exception.Message $ErrorObject.Raw = $Exception Write-Debug ($ErrorObject | ConvertTo-Json -Depth 2) return $ErrorObject } Write-Debug ($ResponseHeaders | ConvertTo-Json -Depth 5) Write-Debug ($Results | ConvertTo-Json -Depth 10) Write-Output $Results } End { } } # Function to get AssetId by Asset Name function Get-FsAsset { <# .DESCRIPTION Gets details of the Asset, given the name or Id. If neither are provided, all Assets are returned. .SYNOPSIS Get details of a Asset, or a list of all Assets .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name (string) Name of the Asset .PARAMETER IncludeTypeFields (switch) Should Type fields be included in the result? .PARAMETER Id (int64) Display Id of the Asset (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Asset properties if found Returns a list of PSCustomObject containing all Assets Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of the Asset/company to retrieve details for')] [string] $Name, [Parameter(Mandatory=$false, HelpMessage = 'Include fields that are specific to the asset type')] [switch] $IncludeTypeFields, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the Asset/company to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($PSBoundParameters.ContainsKey('Id')) { $AssetApiUrl_Get = [io.path]::combine($Uri, "assets/", $Id) } else { $AssetApiUrl_Get = [io.path]::combine($Uri, "assets") } $Parameters = @() if (-not $id -and -not $name ) { $Parameters += "per_page={0}" -f $PerPage } if ($Name) { # An asset name was specified $AssetFilter = 'query="name:''' + $Name + '''"' $Parameters += $AssetFilter } if ($IncludeTypeFields) { $Parameters += 'include=type_fields' } $ParamString = $Parameters -join '&' $AssetApiUrl_Get += "?" + $ParamString Write-Debug "API URL $AssetApiUrl_Get" } Process { $Assets = $null # API Call to get the tag $Assets = Invoke-FreshServiceApiRequest -Uri $AssetApiUrl_Get -Credential $Credential if ($Assets -is [HashTable] -and $Assets.Error) { if ($Assets.Code -eq 404 -and $Id) { # An Asset with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Assets API. Response Code: $($Assets.Code) Note: $($Assets.Note)" -ErrorId $Assets.Code -CategoryReason $Assets.Note return $Assets } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($Asset in $Assets.Assets) { # Check each Asset's name to see if it is the one we are looking for Write-Debug "Evaluating against: $Asset.name" if ($Asset.name -eq $Name) { Write-Output $Asset } } } elseif ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Asset Write-Output $Assets.Asset } else { Write-Output $Assets.Assets } } } # Function to get AssetId by Asset Types Name function Get-FsAssetTypes { <# .DESCRIPTION Gets details of the Asset Types, given the name or Id. If neither are provided, all Asset Types are returned. .SYNOPSIS Get details of a Asset Type, or a list of all Asset Types .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Asset Type .PARAMETER Id Display Id of the Asset Type (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Asset Type properties if found Returns a list of PSCustomObject containing all Asset Types Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the Asset Type to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($PSBoundParameters.ContainsKey('Id')) { $AssetTypeApiUrl_Get = [io.path]::combine($Uri, "asset_types/", $Id) } else { $AssetTypeApiUrl_Get = [io.path]::combine($Uri, "asset_types") } $Parameters = @() if (-not $Id) { $Parameters += "per_page={0}" -f $PerPage } $ParamString = $Parameters -join '&' $AssetTypeApiUrl_Get += "?" + $ParamString Write-Debug "API URL $AssetTypeApiUrl_Get" } Process { $AssetTypes = $null # API Call to get the tag $AssetTypes = Invoke-FreshServiceApiRequest -Uri $AssetTypeApiUrl_Get -Credential $Credential if ($AssetTypes -is [HashTable] -and $AssetTypes.Error) { if ($AssetTypes.Code -eq 404 -and $Id) { # An Asset Type with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Asset Types API. Response Code: $($AssetTypes.Code) Note: $($AssetTypes.Note)" -ErrorId $AssetTypes.Code -CategoryReason $AssetTypes.Note return $AssetTypes } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Asset Type Write-Output $AssetTypes.asset_type } else { Write-Output $AssetTypes.asset_types } } } # Function to get DepartmentId by Department Name function Get-FsChange { <# .DESCRIPTION Gets details of a specific Change, given the Id, or a list of Changes given a set of criteria. .SYNOPSIS Get details of a Change, or a list of all Changes matching a set of criteria .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Id Id of the Change .PARAMETER UpdatedSince Only list Changes updated since .OUTPUTS Returns a PSCustomObject containing the Change properties if found Returns a list of PSCustomObject containing all matching Changes Returns $null if no match Returns ErrorObject if an error is encountered (Error = $true) .EXAMPLE Return all Changes C:\ PS> Get-FsChange -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) .EXAMPLE Return all Changes created since 2021-08-18 C:\ PS> Get-FsChange -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -UpdatedSince '2021-08-18' #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ById', ValueFromPipeline = $true, HelpMessage = 'Id of the Change to retrieve details for')] $Id, [Parameter(Mandatory=$true, ParameterSetName = 'UpdatedSince', HelpMessage = 'Return only Changes updated since this date')] [string] $UpdatedSince ) Begin { $Me = $MyInvocation.MyCommand.Name # Use Max Page Size to reduce the number of requests $PerPage = 100 $Parameters = @{} $Parameters.Add('per_page', $PerPage) If ($UpdatedSince) { $Parameters.Add("updated_since", $UpdatedSince) } $ParamList = @() foreach ($Parameter in $Parameters.Keys) { $ParamList += '{0}={1}' -f $Parameter, $Parameters.Item($Parameter) } $ParamString = $ParamList -join '&' $_int = 0 } Process { # Setup Error Object structure $ErrorObject = [PSCustomObject]@{ Code = $null Error = $false Type = $null Note = $null Raw = $_ } if($Id) { if ($Id.GetType().Name -eq 'PSCustomObject') { if ($Id.id) { Write-Verbose "[$Me]: PipeInput object has property named id." $ChangeId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } elseif ($Id.GetType().Name -eq 'HashTable' ) { if ($Id.ContainsKey('id')) { Write-Verbose "[$Me]: PipeInput object has property named id." $ChangeId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } else { # Check if ID value is an integer if ([int]::TryParse($Id, [ref]$_int)) { Write-Verbose "[$Me]: Id parses as integer." $ChangeId = $Id } else { Write-Verbose "[$Me]: Id does not parse as integer." $ErrorObject.Error = $true $ErrorObject.Note = "Id String [$Id] not a valid Change ID." return $ErrorObject } } } # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($ChangeId) { $ChangeApiUrl_Get = [io.path]::combine($Uri, "changes/", $ChangeId) } else { $ChangeApiUrl_Get = [io.path]::combine($Uri, "changes") $ChangeApiUrl_Get += '?' + $ParamString } Write-Debug "API URL $ChangeApiUrl_Get" $Changes = $null # API Call to get the tag $Changes = Invoke-FreshServiceApiRequest -Uri $ChangeApiUrl_Get -Credential $Credential if ($Changes -is [HashTable] -and $Changes.Error) { if ($Changes.Code -eq 404 -and $ChangeId) { # Change with specified ID does not exist, return nothing } else { # An error was encountered Write-Error -Message "Error calling Change API. Response Code: $($Changes.Code) Note: $($Changes.Note)" -ErrorId $Changes.Code -CategoryReason $Changes.Note return $Changes } } if ($ChangeId) { # We should have got only one Change Write-Output $Changes.Change } else { Write-Output $Changes.Changes } } } # Function to get AssetId by Change Fields Name function Get-FsChangeFields { <# .DESCRIPTION Gets details of the Change Fields, given the name or Id. If neither are provided, all Change Fields are returned. .SYNOPSIS Get details of a Change Field, or a list of all Change Fields .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Change Field .PARAMETER Id Display Id of the Change Field (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Change Field properties if found Returns a list of PSCustomObject containing all Change Fields Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of field to get details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the Change Field to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI $ChangeFieldApiUrl_Get = [io.path]::combine($Uri, "change_form_fields") $Parameters = @() if (-not $Id) { $Parameters += "per_page={0}" -f $PerPage } $ParamString = $Parameters -join '&' $ChangeFieldApiUrl_Get += "?" + $ParamString Write-Debug "API URL $ChangeFieldApiUrl_Get" } Process { $ChangeFields = $null # API Call to get the tag $ChangeFields = Invoke-FreshServiceApiRequest -Uri $ChangeFieldApiUrl_Get -Credential $Credential if ($ChangeFields -is [HashTable] -and $ChangeFields.Error) { if ($ChangeFields.Code -eq 404 -and $Id) { # An Change Field with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Change Fields API. Response Code: $($ChangeFields.Code) Note: $($ChangeFields.Note)" -ErrorId $ChangeFields.Code -CategoryReason $ChangeFields.Note return $ChangeFields } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($ChangeField in $ChangeFields.change_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($ChangeField.name)" if ($ChangeField.name -eq $Name) { Write-Output $ChangeField } } } elseif ($PSBoundParameters.ContainsKey('Id')) { foreach ($ChangeField in $ChangeFields.change_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($ChangeField.id)" if ($ChangeField.id -eq $Id) { Write-Output $ChangeField } } } else { Write-Output $ChangeFields.change_fields } } } # Function to get DepartmentId by Department Name function Get-FsDepartment { <# .DESCRIPTION Gets details of the department, given the name or Id. If neither are provided, all departments are returned. .SYNOPSIS Get details of a department, or a list of all departments .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the department .PARAMETER Id Id of the Department .OUTPUTS Returns a PSCustomObject containing the Department properties if found Returns a list of PSCustomObject containing all Departments Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsDepartment -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [ValidateScript({ $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') { [System.UriBuilder]$_ } })] [System.UriBuilder] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of the department/company to retrieve details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the department/company to retrieve details for')] [int64] $Id ) Begin { # Add the departments endpoint to the base URI provided if ($PSBoundParameters.ContainsKey('Id')) { $Uri.Path = [io.path]::combine($Uri.Path, "departments", $Id) } else { $Uri.Path = [io.path]::combine($Uri.Path, "departments") } } Process { # Build up the URI Query $UriQuery = [System.Web.HttpUtility]::ParseQueryString([string]$Uri.Query) if ($PSBoundParameters.ContainsKey('Name')) { # Add the Name query to the Query String $UriQuery.Add('query','name:''' + $Name + '''') } # Add the updated Uri Query string to the URI $Uri.Query = $UriQuery.ToString() Write-Debug "API URL $($Uri.Uri)" $Departments = $null # API Call to get the tag $Departments = Invoke-FreshServiceApiRequest -Uri $Uri -Credential $Credential if ($Departments -is [HashTable] -and $Departments.Error) { if ($Departments.Code -eq 404 -and $Id) { # A department with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Departments API. Response Code: $($Departments.Code) Note: $($Departments.Note)" -ErrorId $Departments.Code -CategoryReason $Departments.Note return $Departments } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one department Write-Output $Departments.department } else { Write-Output $Departments.departments } } } # Function to get AssetId by Department Fields Name function Get-FsDepartmentFields { <# .DESCRIPTION Gets details of the Department Fields, given the name or Id. If neither are provided, all Department Fields are returned. .SYNOPSIS Get details of a Department Field, or a list of all Department Fields .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Department Field .OUTPUTS Returns a PSCustomObject containing the Department Field properties if found Returns a list of PSCustomObject containing all Department Fields Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of field to get details for')] [string] $Name ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI $DepartmentFieldApiUrl_Get = [io.path]::combine($Uri, "department_fields") $Parameters = @() $Parameters += "per_page={0}" -f $PerPage $ParamString = $Parameters -join '&' $DepartmentFieldApiUrl_Get += "?" + $ParamString Write-Debug "API URL $DepartmentFieldApiUrl_Get" } Process { $DepartmentFields = $null # API Call to get the tag $DepartmentFields = Invoke-FreshServiceApiRequest -Uri $DepartmentFieldApiUrl_Get -Credential $Credential if ($DepartmentFields -is [HashTable] -and $DepartmentFields.Error) { if ($DepartmentFields.Code -eq 404 -and $Id) { # An Department Field with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Department Fields API. Response Code: $($DepartmentFields.Code) Note: $($DepartmentFields.Note)" -ErrorId $DepartmentFields.Code -CategoryReason $DepartmentFields.Note return $DepartmentFields } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($DepartmentField in $DepartmentFields.department_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($DepartmentField.name)" if ($DepartmentField.name -eq $Name) { Write-Output $DepartmentField } } } else { Write-Output $DepartmentFields.department_fields } } } function Invoke-FreshServiceApiRequest { <# .SYNOPSIS Invoke the FreshService API .DESCRIPTION This function is intended to be called by other functions for specific resources/interactions .PARAMETER Uri Base API URL for the API Call .PARAMETER Credential PSCredential Object with the API Key stored in the Password property of the object. .PARAMETER Method Valid HTTP Method to use: GET (Default), POST, DELETE, PUT .PARAMETER Body PSCustomObject containing data to be sent as HTTP Request Body in JSON format. .PARAMETER Depth How deep are we going? .OUTPUTS PSCustomObject containing results if successful. May be $null if no data is returned ErrorObject containing details of error if one is encountered. #> [CmdletBinding()] param( [Parameter(Mandatory=$true, HelpMessage = 'Full URI to requeste resource, including URI parameters')] [ValidateScript({ $TypeName = $_ | Get-Member | Select-Object -ExpandProperty TypeName -Unique if ($TypeName -eq 'System.String' -or $TypeName -eq 'System.UriBuilder') { [System.UriBuilder]$_ } })] [System.UriBuilder] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing the API Key in the Password property')] [PSCredential] $Credential, [Parameter(Mandatory=$false, HelpMessage = 'Method to use when making the request. Defaults to GET')] [ValidateSet("Post","Get","Put","Delete")] [string] $Method = "GET", [Parameter(Mandatory=$false, HelpMessage = 'PsCustomObject containing data that will be sent as the Json Body')] [PsCustomObject] $Body, [Parameter(Mandatory=$false, HelpMessage = 'How deep are we?')] [int] $Depth = 0, [Parameter(Mandatory=$false, HelpMessage = 'Time in seconds before the API call throws an error due to timeout')] [ValidateRange(1, [int]::MaxValue)] [int] $TimeoutSec = 5 ) Begin { $Me = $MyInvocation.MyCommand.Name Write-Verbose $Me if (($Method -eq 'GET') -and $Body) { throw "Cannot specify Request Body for Method GET." } $Header = @{} $ApiKey = $Credential.GetNetworkCredential().Password $Creds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $ApiKey,$null))) $Header.Add('Authorization', ("Basic {0}" -f $Creds)) $Header.Add('Content-Type', 'application/json') $MaxRelLink = 10 $RateLimitInterval = 60 $OldRelLink = '' } Process { # Setup Error Object structure $ErrorObject = [PSCustomObject]@{ Code = $null Error = $false Type = $null Note = $null Raw = $_ } $Results = $null # Enforce TLSv1.2 [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 # Make the API Call if ($Body) { # Make the API Call, using the supplied Body. Contents of $Body are the responsibility of the calling code. Write-Verbose "$Me : Body supplied" try { $Results = Invoke-RestMethod -Method $Method -Uri $Uri.Uri -Headers $Header -Body ($Body|ConvertTo-Json -Depth 10) -FollowRelLink -MaximumFollowRelLink $MaxRelLink -ResponseHeadersVariable ResponseHeaders -TimeoutSec $TimeoutSec } catch { $Exception = $_.Exception Write-Verbose "$Me : Exception : $($Exception.StatusCode)" $ErrorObject.Error = $true $ErrorObject.Code = $Exception.StatusCode $ErrorObject.Note = $Exception.Message $ErrorObject.Raw = $Exception if ($PSBoundParameters.Keys.Contains("Debug")) { Write-Debug ($ErrorObject | ConvertTo-Json -Depth 3) } return $ErrorObject } if ($PSBoundParameters.Keys.Contains("Debug")) { Write-Debug ($ResponseHeaders | ConvertTo-Json -Depth 5) } if ($PSBoundParameters.Keys.Contains("Debug")) { Write-Debug ($Results | ConvertTo-Json -Depth 10) } } else { # Make the API Call without a body. This is for GET requests, where details of what we want to get is in the URI Write-Verbose "$Me : No Body supplied" try { $Results = Invoke-RestMethod -Method $Method -Uri $Uri.Uri -Headers $Header -FollowRelLink -MaximumFollowRelLink $MaxRelLink -ResponseHeadersVariable ResponseHeaders -TimeoutSec $TimeoutSec } catch { $Exception = $_.Exception Write-Verbose "$Me : Exception : $($Exception.StatusCode)" $ErrorObject.Error = $true $ErrorObject.Code = $Exception.Response.StatusCode.value__ $ErrorObject.Note = $Exception.Message $ErrorObject.Raw = $Exception if ($PSBoundParameters.Keys.Contains("Debug")) { Write-Debug ($ErrorObject | ConvertTo-Json -Depth 3) } Throw "$Me : Encountered error getting response. $($ErrorObject.Code) : $($ErrorObject.Note) from: $RelLink" return $ErrorObject } } Write-Verbose ($ResponseHeaders | ConvertTo-Json -Depth 5) Write-Output $Results # Check if we have a link to a next page if (($ResponseHeaders.ContainsKey('Link')) -and ($null -ne $ResponseHeaders.Link) -and ('' -ne $ResponseHeaders.Link)) { $Depth += 1 Write-Verbose "Next Link: $($ResponseHeaders.Link) at Depth: $Depth" # Extract the URL from the link text which looks like '<https::domain.freshservice.com/api/v2/tickets?per_page=100&page=21>; Rel="next"' $RelLink = [regex]::match($ResponseHeaders.Link,'\<([^\>]+)\>.*').Groups[1].Value # If the link has not changed, don't follow it if ($RelLink -ne $OldRelLink) { # Check Rate Limiting $RateLimitMax = $ResponseHeaders.'X-RateLimit-Total' $RateLimitRemaining = $ResponseHeaders.'X-RateLimit-Remaining' $RateLimitUsedCurrentRequest = $ResponseHeaders.'X-RateLimit-Used-CurrentRequest' Write-Verbose "RateLimitMax: $RateLimitMax; RateLimitRemaining: $RateLimitRemaining; RateLimitUsedCurrentRequest: $RateLimitUsedCurrentRequest" if (($RateLimitUsedCurrentRequest * $MaxRelLink) -ge $RateLimitRemaining) { # Nearing Rate Limit Write-Verbose "Sleeping to evade API Rate Limit" Start-Sleep -Seconds $RateLimitInterval } Write-Verbose "Requesting Next set of results from $RelLink" # Make a nested call to myself to get the next batch of results within API limits # Since you cannot have multiple pages of results for "Creating" resources, a $Body is never required here $Results = Invoke-FreshServiceApiRequest -Uri $RelLink -Credential $Credential -Method $Method -Depth $Depth Write-Output $Results if ($Results -is [HashTable] -and $Results.ContainsKey('Error') -and $Results.Error) { if ($PSBoundParameters.Keys.Contains("Debug")) { Write-Debug ($Results | ConvertTo-Json -depth 10) } Throw "$Me : Encountered error getting additional results. $($ErrorObject.Code) : $($ErrorObject.Note) from: $RelLink" } else { Write-Output $Results } $OldRelLink = $RelLink } } } End { Write-Verbose "Returning from Depth: $Depth" return } } <# function New-FreshServiceConfiguration { [CmdletBinding(SupportsShouldProcess)] # FreshService Configuration Values $FreshService = @{} $FreshService.Add("ApiUrl","") $FreshService.Add("ApiKey","") $FreshService.Add("DefaultTicketGroupId","") $FreshService.Add("DefaultTicketSource","") $FreshService.Add("DefaultCustomFields", @{}) # Add as Subsection in Config file $FstConfig = @{} $FstConfig.Add("FreshService",$FreshService) if ($PSCmdlet.ShouldProcess($FstConfigFile, 'Write Configuration')) { # Write the configuration Write-Verbose "Writing Configuration" } else { # Write some output } Return $FstConfig } #> function Update-FreshServiceConfiguration { [CmdletBinding(SupportsShouldProcess=$true)] param ( [string] [Parameter(Mandatory=$true)] $APIUrl, [pscredential] [Parameter(Mandatory=$true)] $ApiKey, [Int64] [Parameter(Mandatory=$true)] $DefaultTicketGroupId, [int] [Parameter(Mandatory=$true)] $DefaultTicketSource, [pscustomobject] [Parameter(Mandatory=$false)] $DefaultCustomFields ) # Where is the configuration saved? $ConfigDirPath = Join-Path ` -Path ([Environment]::GetFolderPath("LocalApplicationData"))` -ChildPath $ModuleName $ConfigFileInfo = [System.IO.FileInfo]::new((Join-Path -Path $ConfigDirPath -ChildPath $PreferencesFileName)) # Create Config Dir (if it does not exist) if (! (Test-Path -Path $ConfigDirPath)) { New-Item -Path $LocalAppData -Name $ModuleInfo.Name -ItemType Directory | Out-Null Write-Verbose "Created configuration directory at $ConfigDirPath" } $FstConfig.FreshService.ApiUrl = $APIUrl $FstConfig.FreshService.ApiKey = $ApiKey $FstConfig.FreshService.DefaultTicketGroupId = $DefaultTicketGroupId $FstConfig.FreshService.DefaultTicketSource = $DefaultTicketSource $FstConfig.FreshService.DefaultCustomFields = $DefaultCustomFields if ($ConfigFileInfo.Exists) { if ($PSCmdlet.ShouldProcess($ConfigFileInfo, "Overwrite configuration")) { # Overwrite Configuration } } else { if ($PSCmdlet.ShouldProcess($ConfigFileInfo, "Save Configuration")) { # Save Configuration } } } # Function to get DepartmentId by Department Name function Get-FsProblem { <# .DESCRIPTION Gets details of a specific Problem, given the Id, or a list of Problems given a set of criteria. .SYNOPSIS Get details of a Problem, or a list of all Problems matching a set of criteria .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Id Id of the Problem .PARAMETER UpdatedSince Only list Problems updated since .OUTPUTS Returns a PSCustomObject containing the Problem properties if found Returns a list of PSCustomObject containing all matching Problems Returns $null if no match Returns ErrorObject if an error is encountered (Error = $true) .EXAMPLE Return all Problems C:\ PS> Get-FsProblem -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) .EXAMPLE Return all Problems created since 2021-08-18 C:\ PS> Get-FsProblem -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -UpdatedSince '2021-08-18' #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ById', ValueFromPipeline = $true, HelpMessage = 'Id of the Problem to retrieve details for')] $Id, [Parameter(Mandatory=$true, ParameterSetName = 'UpdatedSince', HelpMessage = 'Return only Problems updated since this date')] [string] $UpdatedSince ) Begin { $Me = $MyInvocation.MyCommand.Name # Use Max Page Size to reduce the number of requests $PerPage = 100 $Parameters = @{} $Parameters.Add('per_page', $PerPage) If ($UpdatedSince) { $Parameters.Add("updated_since", $UpdatedSince) } $ParamList = @() foreach ($Parameter in $Parameters.Keys) { $ParamList += '{0}={1}' -f $Parameter, $Parameters.Item($Parameter) } $ParamString = $ParamList -join '&' $_int = 0 } Process { # Setup Error Object structure $ErrorObject = [PSCustomObject]@{ Code = $null Error = $false Type = $null Note = $null Raw = $_ } if($Id) { if ($Id.GetType().Name -eq 'PSCustomObject') { if ($Id.id) { Write-Verbose "[$Me]: PipeInput object has property named id." $ProblemId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } elseif ($Id.GetType().Name -eq 'HashTable' ) { if ($Id.ContainsKey('id')) { Write-Verbose "[$Me]: PipeInput object has property named id." $ProblemId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } else { # Check if ID value is an integer if ([int]::TryParse($Id, [ref]$_int)) { Write-Verbose "[$Me]: Id parses as integer." $ProblemId = $Id } else { Write-Verbose "[$Me]: Id does not parse as integer." $ErrorObject.Error = $true $ErrorObject.Note = "Id String [$Id] not a valid Problem ID." return $ErrorObject } } } # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($ProblemId) { $ProblemApiUrl_Get = [io.path]::combine($Uri, "problems/", $ProblemId) } else { $ProblemApiUrl_Get = [io.path]::combine($Uri, "problems") $ProblemApiUrl_Get += '?' + $ParamString } Write-Debug "API URL $ProblemApiUrl_Get" $Problems = $null # API Call to get the tag $Problems = Invoke-FreshServiceApiRequest -Uri $ProblemApiUrl_Get -Credential $Credential if ($Problems -is [HashTable] -and $Problems.Error) { if ($Problems.Code -eq 404 -and $Id) { # Problem with specified ID does not exist, return nothing } else { # An error was encountered Write-Error -Message "Error calling Problem API. Response Code: $($Problems.Code) Note: $($Problems.Note)" -ErrorId $Problems.Code -CategoryReason $Problems.Note return $Problems } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Problem Write-Output $Problems.Problem } else { Write-Output $Problems.Problems } } } # Function to get AssetId by Problem Fields Name function Get-FsProblemFields { <# .DESCRIPTION Gets details of the Problem Fields, given the name or Id. If neither are provided, all Problem Fields are returned. .SYNOPSIS Get details of a Problem Field, or a list of all Problem Fields .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Problem Field .PARAMETER Id Display Id of the Problem Field (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Problem Field properties if found Returns a list of PSCustomObject containing all Problem Fields Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of field to get details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the Problem Field to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI $ProblemFieldApiUrl_Get = [io.path]::combine($Uri, "problem_form_fields") $Parameters = @() if (-not $Id) { $Parameters += "per_page={0}" -f $PerPage } $ParamString = $Parameters -join '&' $ProblemFieldApiUrl_Get += "?" + $ParamString Write-Debug "API URL $ProblemFieldApiUrl_Get" } Process { $ProblemFields = $null # API Call to get the tag $ProblemFields = Invoke-FreshServiceApiRequest -Uri $ProblemFieldApiUrl_Get -Credential $Credential if ($ProblemFields -is [HashTable] -and $ProblemFields.Error) { if ($ProblemFields.Code -eq 404 -and $Id) { # An Problem Field with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Problem Fields API. Response Code: $($ProblemFields.Code) Note: $($ProblemFields.Note)" -ErrorId $ProblemFields.Code -CategoryReason $ProblemFields.Note return $ProblemFields } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($ProblemField in $ProblemFields.problem_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($ProblemField.name)" if ($ProblemField.name -eq $Name) { Write-Output $ProblemField } } } elseif ($PSBoundParameters.ContainsKey('Id')) { foreach ($ProblemField in $ProblemFields.problem_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($ProblemField.id)" if ($ProblemField.id -eq $Id) { Write-Output $ProblemField } } } else { Write-Output $ProblemFields.problem_fields } } } # Function to get RequesterId by Requester Name function Get-FsRequester { <# .DESCRIPTION Gets details of the Requester, given the name or Id. If neither are provided, all Requesters are returned. .SYNOPSIS Get details of a Requester, or a list of all Requesters .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Requester .PARAMETER Email Email address of the Requester .PARAMETER Filter Filter Query for list of Requesters .PARAMETER Id Display Id of the Requester (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Requester properties if found Returns a list of PSCustomObject containing all Requesters Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsRequester -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of the Requester to retrieve details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ByEmail', HelpMessage = 'Email Address of the Requester to retrieve details for')] [string] $Email, [Parameter(Mandatory=$true, ParameterSetName = 'ByFilter', HelpMessage = 'Filter criteria to match Requesters to return')] [string] $Filter, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Id of the Requester to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($PSBoundParameters.ContainsKey('Id')) { $RequesterApiUrl_Get = [io.path]::combine($Uri, "requesters/", $Id) } else { $RequesterApiUrl_Get = [io.path]::combine($Uri, "requesters") } $Parameters = @() if (-not $id -and -not $name ) { $Parameters += "per_page={0}" -f $PerPage } if ($Name) { # An Requester name was specified $RequesterFilter = 'query="name:''' + $Name + '''"' $Parameters += $RequesterFilter } if ($Email) { # An Requester e-mail address was specified $RequesterFilter = 'query="primary_email:''' + $Email + '''"' $Parameters += $RequesterFilter } if ($Filter) { # An Requester filter was specified $RequesterFilter = 'query="' + $Filter + '"' $Parameters += $RequesterFilter } $ParamString = $Parameters -join '&' $RequesterApiUrl_Get += "?" + $ParamString Write-Verbose "API URL $RequesterApiUrl_Get" } Process { $Requesters = $null # API Call to get the tag $Requesters = Invoke-FreshServiceApiRequest -Uri $RequesterApiUrl_Get -Credential $Credential if ($Requesters -is [HashTable] -and $Requesters.Error) { if ($Requesters.Code -eq 404 -and $Id) { # An Requester with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Requesters API. Response Code: $($Requesters.Code) Note: $($Requesters.Note)" -ErrorId $Requesters.Code -CategoryReason $Requesters.Note return $Requesters } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Requester Write-Output $Requesters.Requester } else { Write-Output $Requesters.Requesters } } } # Function to update Requesters by Requester Id function Update-FsRequester { <# .DESCRIPTION Update the details of a requester, given the requester Id. .SYNOPSIS Update details of a Requester, or a list of all Requesters .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Id Display Id of the Requester (not the internal ID that FreshService uses) .PARAMETER Body Hashtable containing the updates to the given requester. .OUTPUTS Returns a PSCustomObject containing the Requester's updated properties/details Returns a list of PSCustomObject containing all Requester's updated properties/details Returns ErrorObject if an error is encountered .EXAMPLE PS C:\>Update-FsRequester -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Id 888 -Body $Body .EXAMPLE PS C:\>$Requesters | Update-FsRequester -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Body $Body .EXAMPLE PS C:\>$RequesterChanges Id Body -- ---- 8 {last_name} 9 {last_name} PS C:\>$RequesterChanges[0].Body Name Value ---- ----- last_name Potter PS C:\>$RequesterChanges | Update-FsRequester -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) #> [CmdletBinding(DefaultParameterSetName = 'All', SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory = $true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'All', HelpMessage = 'Id of the Requester to retrieve details for', ValueFromPipelineByPropertyName = $true)] [int64] $Id, [Parameter(Mandatory = $true, ParameterSetName = 'All', HelpMessage = 'Hashtable containing the updates to apply to the given requester', ValueFromPipelineByPropertyName = $true)] [Hashtable] $Body, [Switch]$Force ) Begin { if ($Force){ $ConfirmPreference = 'None' } } Process { $RequesterApiUrl_Put = [io.path]::combine($Uri, "requesters/", $Id) Write-Verbose "API URL {$RequesterApiUrl_Put}" $Requesters = $null # API Call to 'Put' changes if ($PSCmdlet.ShouldProcess("Requester w/ ID: $Id", "Update FreshService Requester")) { $Requesters = Invoke-FreshServiceApiRequest -Uri $RequesterApiUrl_Put -Credential $Credential -Method Put -Body $Body } if ($Requesters -is [PSCustomObject] -and $Requesters.Error) { # An error was encountered Write-Error -Message "Error calling Requesters API. Response Code: $($Requesters.Code) Note: $($Requesters.Note)" -ErrorId $Requesters.Code -CategoryReason $Requesters.Note return $Requesters } Write-Output $Requesters.Requester } } # Function to add a new member to a Requester Group by Requester ID function Add-FsRequesterGroupMember { <# .DESCRIPTION Adds a Requester to a group .SYNOPSIS Add a new member to a requester group .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential Credential Object containing the API Key in the Password field .PARAMETER Id Display Id of the Requester Group (not the internal ID that FreshService uses) .PARAMETER MemberId Display Id of the Requester to add to the Group (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject with Response, Code, and Message Returns ErrorObject if an error is encountered .EXAMPLE Add-FsRequesterGroupMember -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -Id 1 -MemberId 56 #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory = $true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory = $true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'All', ValueFromPipelineByPropertyName = $true, HelpMessage = 'Id of the Requester Group to add the member to')] [int64] $Id, [Parameter(Mandatory = $true, ParameterSetName = 'All', ValueFromPipelineByPropertyName = $true, HelpMessage = 'Id of the Requester to add to the Requester Group')] [int64] $MemberId ) Begin { # Nothing to pre-process } Process { # Build up the URI based on whether we are looking up by Name or Id, or getting all $GroupApiUrl_Post = [io.path]::combine($Uri, 'requester_groups', $Id, 'members', $MemberId) Write-Verbose "API URL $GroupApiUrl_Post" ### Response ### $Response = $null try { $Response = Invoke-FreshServiceApiRequest -Uri $GroupApiUrl_Post -Credential $Credential -Method Post } catch { # An error was encountered Write-Error -Message "Error calling Groups API. Response Code: $($Response.Code) Note: $($Response.Note)" -ErrorId $Response.Code -CategoryReason $Response.Note return $Response } # We should have gotten status 200 $Response = 'create object' | Select-Object Response,Code,Message $Response.Code = 200 $Response.Message = 'User added successfully' $Response.Response = 'OK' Write-Output $Response } } # Function to get Requester Group by Requester Group ID function Get-FsRequesterGroup { <# .DESCRIPTION Gets details of the Requester Group, given the Id. If neither are provided, all Requester Groups are returned. .SYNOPSIS Get details of a Requester Group, or a list of all Requester Groups .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Id Display Id of the Requester Group (not the internal ID that FreshService uses) .PARAMETER Members Retrieve Mebers for a given Requester Group ID. Note: This will make a second API call .OUTPUTS Returns a PSCustomObject containing the Requester Group properties if found Returns a list of PSCustomObject containing all Requester Groups Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsRequesterGroup -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory = $true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory = $true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'ByName', HelpMessage = 'Name of the Requester Group to retrieve details for')] [string] $Name, [Parameter(Mandatory = $false, ParameterSetName = 'ByName', HelpMessage = 'Treat the Name parameter as a regex value')] [switch] $regex, [Parameter(Mandatory = $true, ParameterSetName = 'ById', ValueFromPipelineByPropertyName = $true, HelpMessage = 'Id of the Requester Group to retrieve details for')] [int64] $Id, [Parameter(Mandatory = $false, ParameterSetName = 'ById', HelpMessage = 'Include Members in the Requester Group details. Requires Id')] [Parameter(Mandatory = $false, ParameterSetName = 'ByName', HelpMessage = 'Include Members in the Requester Group details. Requires Id')] [Switch] $Members ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI based on whether we are looking up by Name or Id, or getting all $GroupApiUrl_Get = [io.path]::combine($Uri, ("requester_groups?per_page={0}" -f $PerPage)) Write-Verbose "API URL $GroupApiUrl_Get" } Process { if ($PSBoundParameters.ContainsKey('Id')) { $GroupApiUrl_Get = [io.path]::combine($Uri, "requester_groups/", $Id, ("?per_page={0}" -f $PerPage)) } ### Groups ### $Groups = $null # API Call to get the tag $Groups = Invoke-FreshServiceApiRequest -Uri $GroupApiUrl_Get -Credential $Credential if ($Groups -is [HashTable] -and $Groups.Error) { if ($Groups.Code -eq 404 -and $Id) { # A Group with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Groups API. Response Code: $($Groups.Code) Note: $($Groups.Note)" -ErrorId $Groups.Code -CategoryReason $Groups.Note return $Groups } } if ($PSBoundParameters.ContainsKey('Name')) { $Groups.requester_groups = $Groups.requester_groups | ForEach-Object { $Group = $_ # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $Group.name" if ($PSBoundParameters.ContainsKey('regex')) { if ($Group.name -match $Name) { Write-Output $Group } } else { if ($Group.name -eq $Name) { Write-Output $Group } } } ### Members ### if ($PSBoundParameters.ContainsKey('Members')) { $Groups.requester_groups = $Groups.requester_groups | ForEach-Object { $Group = $_ $MembersApiUrl_Get = [io.path]::combine($Uri, "requester_groups/", $Group.id, ('members?per_page={0}' -f $PerPage)) Write-Verbose "API URL $MembersApiUrl_Get" [PSCustomObject] $Members = $null # API Call to get the tag $Members = Invoke-FreshServiceApiRequest -Uri $MembersApiUrl_Get -Credential $Credential if ($Members -is [HashTable] -and $Members.Error) { if ($Members.Code -eq 404 -and $Id) { # A Group with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Groups API. Response Code: $($Members.Code) Note: $($Members.Note)" -ErrorId $Members.Code -CategoryReason $Members.Note } } else { # Join the results $Group = $Group | Select-Object id,name,description,type,requesters $Group.requesters = $Members.requesters } $Group } } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Group ### Members ### if ($PSBoundParameters.ContainsKey('Members')) { $MembersApiUrl_Get = [io.path]::combine($Uri, "requester_groups/", $Id, ('members?per_page={0}' -f $PerPage)) Write-Verbose "API URL $MembersApiUrl_Get" [PSCustomObject] $Members = $null # API Call to get the tag $Members = Invoke-FreshServiceApiRequest -Uri $MembersApiUrl_Get -Credential $Credential if ($Members -is [HashTable] -and $Members.Error) { if ($Members.Code -eq 404 -and $Id) { # A Group with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Groups API. Response Code: $($Members.Code) Note: $($Members.Note)" -ErrorId $Members.Code -CategoryReason $Members.Note } } else { # Join the results $Groups.requester_group = $Groups.requester_group | Select-Object id,name,description,type,requesters $Groups.requester_group.requesters = $Members.requesters } } Write-Output $Groups.requester_group } else { Write-Output $Groups.requester_groups } } } # Function to add a new member to a Requester Group by Requester ID function Remove-FsRequesterGroupMember { <# .DESCRIPTION Removes a Requester from a group .SYNOPSIS Removes a current member from a requester group .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential Credential Object containing the API Key in the Password field .PARAMETER Id Display Id of the Requester Group (not the internal ID that FreshService uses) .PARAMETER MemberId Display Id of the Requester to remove from the Group (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject with Response, Code, and Message Returns ErrorObject if an error is encountered .EXAMPLE Remove-FsRequesterGroupMember -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -Id 1 -MemberId 56 #> [CmdletBinding(DefaultParameterSetName = 'All', SupportsShouldProcess = $true, ConfirmImpact = 'High')] param( [Parameter(Mandatory = $true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory = $true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'All', ValueFromPipelineByPropertyName = $true, HelpMessage = 'Id of the Requester Group to remove the member from')] [int64] $Id, [Parameter(Mandatory = $true, ParameterSetName = 'All', ValueFromPipelineByPropertyName = $true, HelpMessage = 'Id of the Requester to remove from the Requester Group')] [int64] $MemberId, [Switch]$Force ) Begin { if ($Force){ $ConfirmPreference = 'None' } } Process { # Build up the URI based on whether we are looking up by Name or Id, or getting all $GroupApiUrl_Delete = [io.path]::combine($Uri, 'requester_groups', $Id, 'members', $MemberId) Write-Verbose "API URL $GroupApiUrl_Delete" ### Response ### $Response = $null try { if ($PSCmdlet.ShouldProcess("Requester ID: $MemberId, Requester Group ID: $Id", "Remove Requester From Group")) { $Response = Invoke-FreshServiceApiRequest -Uri $GroupApiUrl_Delete -Credential $Credential -Method Delete } } catch { # An error was encountered Write-Error -Message "Error calling Groups API. Response Code: $($Response.Code) Note: $($Response.Note)" -ErrorId $Response.Code -CategoryReason $Response.Note return $Response } # We should have gotten status 200 $Response = 'create object' | Select-Object Response,Code,Message $Response.Code = 204 $Response.Message = 'User removed successfully' $Response.Response = 'OK' Write-Output $Response } } # Function to get DepartmentId by Department Name function Get-FsTicket { <# .DESCRIPTION Gets details of a specific ticket, given the Id, or a list of tickets given a set of criteria. .SYNOPSIS Get details of a ticket, or a list of all tickets matching a set of criteria .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Id Id of the Ticket .PARAMETER Filter Lucene formatted filter query to match tickets. The parameter values must be surrounded by single quotes. Note: Queries can be combined using AND or OR. https://domain.freshservice.com/api/v2/tickets/filter?query="priority: 1 AND status: 2 OR urgency: 3" Supported operators 1. priority: 1 (priority equal to 1) 2. priority:> 1 (priority greater than 1) 3. priority :< 1 (priority less than 1) Formatting 1. String fields to be enclosed in single quotes ('') 2. Number fields to be given as number without quotes. 3. Date and date_time fields to be enclosed in single quotes('yyyy-mm-dd') 4. only :> and :< are supported for date and date_time fields. Both fields expect input in the same format as 'yyyy-mm-dd' .PARAMETER UpdatedSince Only list tickets updated since .OUTPUTS Returns a PSCustomObject containing the Ticket properties if found Returns a list of PSCustomObject containing all matching Tickets Returns $null if no match Returns ErrorObject if an error is encountered (Error = $true) .EXAMPLE Return all tickets C:\ PS> Get-FsTicket -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) .EXAMPLE Return all tickets created since 2021-08-18 C:\ PS> Get-FsTicket -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -UpdatedSince '2021-08-18' .EXAMPLE Return all tickets created since 2021-08-18, using filter criteria C:\ PS> Get-FsTicket -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -Filter "created_at:>'2021-08-18'" .EXAMPLE Return all tickets created between 2021-08-07 and 2021-08-14, using filter criteria C:\ PS> Get-FsTicket -Uri "https://ipsec.freshservice.com/api/v2" -Credential (Get-PSCredential) -Filter "created_at:>'2021-08-07' AND created_at:<'2021-08-14'" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ById', ValueFromPipeline = $true, HelpMessage = 'Id of the Ticket to retrieve details for')] $Id, [Parameter(Mandatory=$true, ParameterSetName = 'ByFilter', HelpMessage = 'Criteria to match tickets by')] [string] $Filter, [Parameter(Mandatory=$false, HelpMessage = 'Include statistics in output')] [switch] $IncludeStats, [Parameter(Mandatory=$true, ParameterSetName = 'UpdatedSince', HelpMessage = 'Return only tickets updated since this date')] [string] $UpdatedSince ) Begin { $Me = $MyInvocation.MyCommand.Name # Use Max Page Size to reduce the number of requests $PerPage = 100 $Parameters = @{} if ($MyInvocation.ExpectingInput) { Write-Verbose "[$Me]: Accepting Pipeline Input" } elseif ($PSBoundParameters.ContainsKey('Id')) { Write-Verbose "[$Me]: Accepting Id Parameter" } else { # Only do per_page if we are not getting a single ticket $Parameters.Add('per_page', $PerPage) } If ($Filter) { $Parameters.Add('query', [System.Web.HttpUtility]::UrlEncode( '"' + $Filter + '"')) } If ($UpdatedSince) { $Parameters.Add("updated_since", $UpdatedSince) } if ($IncludeStats) { $Parameters.Add('include', 'stats') } $ParamList = @() foreach ($Parameter in $Parameters.Keys) { $ParamList += '{0}={1}' -f $Parameter, $Parameters.Item($Parameter) } $ParamString = $ParamList -join '&' $_int = 0 } Process { # Setup Error Object structure $ErrorObject = [PSCustomObject]@{ Code = $null Error = $false Type = $null Note = $null Raw = $_ } if($Id) { if ($Id.GetType().Name -eq 'PSCustomObject') { if ($Id.id) { Write-Verbose "[$Me]: PipeInput object has property named id." $TicketId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } elseif ($Id.GetType().Name -eq 'HashTable' ) { if ($Id.ContainsKey('id')) { Write-Verbose "[$Me]: PipeInput object has property named id." $TicketId = $Id.id } else { Write-Verbose "[$Me]: No Property found named id." $ErrorObject.Error = $true $ErrorObject.Note = "Supplied object does not have a property named id." return $ErrorObject } } else { # Check if ID value is an integer if ([int]::TryParse($Id, [ref]$_int)) { Write-Verbose "[$Me]: Id parses as integer." $TicketId = $Id } else { Write-Verbose "[$Me]: Id does not parse as integer." $ErrorObject.Error = $true $ErrorObject.Note = "Id String [$Id] not a valid Ticket ID." return $ErrorObject } } } # Build up the URI based on whether we are looking up by Name or Id, or getting all if ($TicketId) { $TicketApiUrl_Get = [io.path]::combine($Uri, "tickets/", $TicketId) } elseif ($PSBoundParameters.ContainsKey('Filter')) { $TicketApiUrl_Get = [io.path]::combine($Uri, "tickets/filter") } else { $TicketApiUrl_Get = [io.path]::combine($Uri, "tickets") } $TicketApiUrl_Get += '?' + $ParamString Write-Debug "API URL $TicketApiUrl_Get" $Tickets = $null # API Call to get the tag $Tickets = Invoke-FreshServiceApiRequest -Uri $TicketApiUrl_Get -Credential $Credential if ($Tickets -is [HashTable] -and $Tickets.Error) { if ($Tickets.Code -eq 404 -and $TicketId) { # Ticket with specified ID does not exist, return nothing } else { # An error was encountered Write-Error -Message "Error calling Ticket API. Response Code: $($Tickets.Code) Note: $($Tickets.Note)" -ErrorId $Tickets.Code -CategoryReason $Tickets.Note return $Tickets } } if ($PSBoundParameters.ContainsKey('Id')) { # We should have got only one Ticket Write-Output $Tickets.ticket } else { Write-Output $Tickets.tickets } } } # Function to get AssetId by Ticket Fields Name function Get-FsTicketFieldChoice { <# .DESCRIPTION Gets details of the Ticket Fields, given the name or Id. If neither are provided, all Ticket Fields are returned. .SYNOPSIS Get details of a Ticket Field, or a list of all Ticket Fields .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER FieldName Name of the Ticket Field .PARAMETER Name Name of the Ticket Field Choice .PARAMETER Id Id of the Ticket Field Choice .OUTPUTS Returns a PSCustomObject containing the Ticket Field properties if found Returns a list of PSCustomObject containing all Ticket Fields Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, HelpMessage = 'Name of field to get choices for')] [string] $FieldName, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of choice to get details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Id of choice to retrieve details for')] [int64] $Id ) Begin { } Process { $TicketField = $null # API Call to get the details of the field, including choices $TicketField = Get-FsTicketFields -Uri $Uri -Credential $Credential -Name $FieldName if ($TicketField -is [HashTable] -and $TicketField.Error) { if ($TicketField.Code -eq 404 -and $Id) { # An Ticket Field with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Ticket Fields API. Response Code: $($TicketField.Code) Note: $($TicketField.Note)" -ErrorId $TicketField.Code -CategoryReason $TicketField.Note return $TicketField } } if ($PSBoundParameters.ContainsKey('Name')) { $TicketField.choices | Where-Object -Property value -eq $Name } elseif ($PSBoundParameters.ContainsKey('Id')) { $TicketField.choices | Where-Object -Property id -eq $Id } else { Write-Output $TicketField.choices } } } # Function to get AssetId by Ticket Fields Name function Get-FsTicketFields { <# .DESCRIPTION Gets details of the Ticket Fields, given the name or Id. If neither are provided, all Ticket Fields are returned. .SYNOPSIS Get details of a Ticket Field, or a list of all Ticket Fields .PARAMETER Uri The base URL for the customer's FreshService environment .PARAMETER Credential PSCredential Object containing the API Key in the Password field .PARAMETER Name Name of the Ticket Field .PARAMETER Id Display Id of the Ticket Field (not the internal ID that FreshService uses) .OUTPUTS Returns a PSCustomObject containing the Ticket Field properties if found Returns a list of PSCustomObject containing all Ticket Fields Returns $null if no match Returns ErrorObject if an error is encountered .EXAMPLE Get-FsAsset -Uri "https://ipsec.freshservice.com/api/v2" -FsApiKey (Get-PSCredential) -Name "IPSec" #> [CmdletBinding(DefaultParameterSetName = 'All')] param( [Parameter(Mandatory=$true, HelpMessage = 'Base Uri for the FreshService API for your organisation')] [string] $Uri, [Parameter(Mandatory=$true, HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [PsCredential] $Credential, [Parameter(Mandatory=$true, ParameterSetName = 'ByName', HelpMessage = 'Name of field to get details for')] [string] $Name, [Parameter(Mandatory=$true, ParameterSetName = 'ById', HelpMessage = 'Name of the Ticket Field to retrieve details for')] [int64] $Id ) Begin { # Use Max Page Size to reduce the number of requests $PerPage = 100 # Build up the URI $TicketFieldApiUrl_Get = [io.path]::combine($Uri, "ticket_form_fields") $Parameters = @() if (-not $Id) { $Parameters += "per_page={0}" -f $PerPage } $ParamString = $Parameters -join '&' $TicketFieldApiUrl_Get += "?" + $ParamString Write-Debug "API URL $TicketFieldApiUrl_Get" } Process { $TicketFields = $null # API Call to get the tag $TicketFields = Invoke-FreshServiceApiRequest -Uri $TicketFieldApiUrl_Get -Credential $Credential if ($TicketFields -is [HashTable] -and $TicketFields.Error) { if ($TicketFields.Code -eq 404 -and $Id) { # An Ticket Field with the specified ID does not exist (Not Found) } else { # An error was encountered Write-Error -Message "Error calling Ticket Fields API. Response Code: $($TicketFields.Code) Note: $($TicketFields.Note)" -ErrorId $TicketFields.Code -CategoryReason $TicketFields.Note return $TicketFields } } if ($PSBoundParameters.ContainsKey('Name')) { foreach ($TicketField in $TicketFields.ticket_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($TicketField.name)" if ($TicketField.name -eq $Name) { Write-Output $TicketField } } } elseif ($PSBoundParameters.ContainsKey('Id')) { foreach ($TicketField in $TicketFields.ticket_fields) { # Check each Department's name to see if it is the one we are looking for Write-Debug "Evaluating against: $($TicketField.id)" if ($TicketField.id -eq $Id) { Write-Output $TicketField } } } else { Write-Output $TicketFields.ticket_fields } } } # Function to create a new ticket in FreshService Function New-FsTicket { <# .DESCRIPTION Create a Ticket in FreshService using the supplied details. .PARAMETER ApiUrl (string): The URL to use for FreshService .PARAMETER ApiKey (string): The API Key used to authenticate to FreshService .PARAMETER Subject (string): The Subject of the new ticket. .PARAMETER Requester (string): The name of the Requester in FreshService .PARAMETER RequesterEmail (string): The email address of the Requester in FreshService .PARAMETER Description (string): The body of the incident description. .PARAMETER TicketPriority (string): The Priority to set the FreshService Ticket to 1 = Low, 2 = Medium, 3 = High , 4 = Urgent .PARAMETER TicketStatus (string): The Status that the ticket will be set to 2 = Open, 3 = Pending, 4 = Resolved, 5 = Closed .PARAMETER TicketSource (string): The Numeric ID of the source associated with this ticket. LogRhythm = 1001 in our deployment. Use the FreshService API to discover other values. .PARAMETER AssignGroup (string): The Numeric ID of the group to assign this ticket to on creation. This is obtained using the FreshService API. .PARAMETER Category (string): The Category of ticket to assign. Device = Device Incident. Security = Security Incident. .PARAMETER DepartmentId (string): The Numeric ID of the department this ticket is assocated with. Use the FreshService API to determine a DepartmentId from a Department Name .PARAMETER Assets (array of hastable): List of Assets to associate to the ticket .PARAMETER Tags (string): Tag(s) to add to the ticket .PARAMETER CustomFields (pscustomobject): Custom Fields/values to be added to ticket .OUTPUTS Returns TicketId if a ticket was successfully created. .EXAMPLE New-FsTicket -FsApiUrl FsApiUrl -FsApiKey FsApiKey -Subject "Ticket Subject" -Requester "Requester Name" -RequesterEmail "requester@company.com" -Description "Formatted description body of the ticket" -TicketPriority 4 -TicketStatus 2 -TicketSource 1001 -Group 10003675 -Category "Security" -Department 10034567 #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low', DefaultParameterSetName = 'PriorityMatrix')] param( [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Base Uri for the FreshService API for your organisation')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $Uri, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'PSCredential Object containing FreshService API Key in Password field')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [PsCredential] $Credential, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'The Subject of the new Ticket')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $Subject, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'ID of the Ticket Requester (FreshService ID)')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] [string] $Requester, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Email address of Ticket Requester')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $RequesterEmail, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'The Ticket Description - ie. The main body of the ticket')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $Description, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'The Priority of the new Ticket')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [ValidateSet("1","2","3","4")] [Int] $TicketPriority, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Status to assign to ticket when creating')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [ValidateSet("2","3","4","5")] [string] $TicketStatus, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Source of ticket (Phone, email, specific system)')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $TicketSource, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Group (Queue) to assign ticket to')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $Group, [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Ticket Category')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $Category, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Ticket Type')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] [ValidateSet('Incident')] # According to the docs, 'incident' is the only supported type. Docs use a lowercase 'i' but API only accepts a capital 'I' [string] $Type = 'Incident', [Parameter(Mandatory = $true, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Id of Department (Customer) to which Ticket belongs')] [Parameter(Mandatory = $true, ParameterSetName = 'NoPriorityMatrix')] [string] $DepartmentId, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Custom Fields with values to apply to ticket')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] [PsCustomObject] $CustomFields, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Assets to be associated to ticket')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] $Assets, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Tags to be applied to ticket')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] $Tags, [Parameter(Mandatory = $false, ParameterSetName = 'PriorityMatrix', HelpMessage = 'Is FreshService configured with Priority Matrix enabed?')] [Parameter(Mandatory = $false, ParameterSetName = 'NoPriorityMatrix')] [System.Boolean] [Parameter(Mandatory=$false)] $PriorityMatrix = $true ) Begin { # Where tickets are created $FsTicketEndpoint = "/tickets/" $FsTicketUrl = $Uri + $FsTicketEndpoint $Method = 'POST' # If PriorityMatrix is enabled in FreshService (assumed default), override the Urgency and Impact to get the desired ticket priority if ($PriorityMatrix) { $Urgency, $Impact = Get-FsMatrixPriority -TicketPriority $TicketPriority } Write-Debug "Assets: $Assets" } Process { # Build up ticket contents $TicketAttributes = @{} $TicketAttributes.Add('name', $Requester) $TicketAttributes.Add('email', $RequesterEmail) $TicketAttributes.Add('subject', $Subject) $TicketAttributes.Add('description', $Description) $TicketAttributes.Add('priority', [int]$TicketPriority) $TicketAttributes.Add('urgency', [int]$Urgency) $TicketAttributes.Add('impact', [int]$Impact) $TicketAttributes.Add('type', $Type) $TicketAttributes.Add('status', [int]$TicketStatus) $TicketAttributes.Add('source', [int]$TicketSource) $TicketAttributes.Add('group_id', [int64]$Group) $TicketAttributes.Add('department_id', [int64]$DepartmentId) $TicketAttributes.Add('category', $Category) # Add Custom Fields if supplied if ($CustomFields) { $TicketAttributes.Add('custom_fields', $CustomFields) } # Add Tags if supplied if ($Tags) { $TicketAttributes.Add('tags', $Tags) } # Add Assets if supplied if ($Assets) { $TicketAttributes.Add('assets', $Assets) } # Ticket attributes need to be inside a ticket object, then converted to JSON $TicketAttributes = @{'ticket' = $TicketAttributes} $Json = ConvertTo-Json -Depth 5 $TicketAttributes Write-Debug $Json try { # Create the ticket if ($PSCmdlet.ShouldProcess("Ticket: $Subject", "Create FreshService Ticket?")) { $Ticket = Invoke-FreshServiceApiRequest -Uri $FsTicketUrl -Credential $Credential -Method $Method -Body $TicketAttributes } } catch { # Error Handling $result = $_ $message = $result.Exception.Message $code = $result.Exception.Response.StatusCode.value__ Write-Debug "Message from Server: $code" if ($message -eq "The remote server returned an error: (400) Bad Request.") { Write-Output "API call Unsuccessful." Write-Error "Error: Unable to communicate to FreshService-API. Please check the service instance is up and healthy." throw "ExecutionFailure" } elseif ($message -eq "The underlying connection was closed: The connection was closed unexpectedly.") { Write-Output "Invalid API Key." Write-Error "Error: Invalid or Incorrect API key provided." throw "ExecutionFailure" } elseif ($message -eq "Unable to connect to the remote server") { Write-Output "Invalid API URL." Write-Error "Error: Could not resolve API URL. Invalid or Incorrect API URL." throw "ExecutionFailure" } elseif($message -eq "The remote server returned an error: (401) Unauthorized.") { Write-Output "Invalid API Key." Write-Error "Error: Invalid or Incorrect API key provided." throw "ExecutionFailure" } else{ Write-Output $message write-error "API Call Unsuccessful." throw "ExecutionFailure" } } if ((-not $Ticket ) -or ($Ticket.id)) { Write-Error "An unknown error occurred creating the ticket; Ticket API did not return an ID. API responded: $Ticket" Exit 1 } else { Write-Verbose "Created Case Number: $($Ticket.ticket.id) , Title: $($Ticket.ticket.subject) " } $TicketId = $Ticket.ticket.id return $TicketId } } Export-ModuleMember -Function Get-FsAgent, Get-FsAgentGroup, Invoke-FreshServiceAlertRequest, Get-FsAsset, Get-FsAssetTypes, Get-FsChange, Get-FsChangeFields, Get-FsDepartment, Get-FsDepartmentFields, Invoke-FreshServiceApiRequest, New-FreshServiceConfiguration, Update-FreshServiceConfiguration, Get-FsProblem, Get-FsProblemFields, Get-FsRequester, Update-FsRequester, Add-FsRequesterGroupMember, Get-FsRequesterGroup, Remove-FsRequesterGroupMember, Get-FsTicket, Get-FsTicketFieldChoice, Get-FsTicketFields, New-FsTicket # SIG # Begin signature block # MIIoHgYJKoZIhvcNAQcCoIIoDzCCKAsCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCSN6+GBPebDfKw # 6MCZBeVClLO7oHi2jfXRih82dgX5j6CCISEwggWNMIIEdaADAgECAhAOmxiO+dAt # 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa # Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD # ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E # MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy # unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF # xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1 # 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB # MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR # WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6 # nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB # YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S # UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x # q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB # NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP # TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC # AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp # Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv # bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0 # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB # LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc # Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov # Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy # oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW # juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF # mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z # twGpn1eqXijiuZQwggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqG # SIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy # dXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy # RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg # Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXH # JQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMf # UBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w # 1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRk # tFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYb # qMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUm # cJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP6 # 5x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzK # QtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo # 80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjB # Jgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXche # MBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB # /wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU # 7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoG # CCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29j # c3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdp # Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDig # NqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9v # dEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI # hvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd # 4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiC # qBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl # /Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeC # RK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYT # gAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/ # a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37 # xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmL # NriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0 # YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJ # RyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIG # sDCCBJigAwIBAgIQCK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQsw # CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu # ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw # HhcNMjEwNDI5MDAwMDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0 # ZWQgRzQgQ29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjAN # BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zr # PYGXcMW7xIUmMJ+kjmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHM # gQM+TXAkZLON4gh9NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8Irg # nQnAZaf6mIBJNYc9URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyC # EUhSaN4QvRRXXegYE2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0 # p6MDDnSlrzm2q2AS4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQa # khCBj7A7CdfHmzJawv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0 # XLyTRSiDNipmKF+wc86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960I # HnWmZcy740hQ83eRGv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2 # FKZbS110YU0/EpF23r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBH # X8mBUHOFECMhWWCKZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q2 # 7IwyCQLMbDwMVhECAwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD # VR0OBBYEFGg34Ou2O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1k # TN8uZz/nupiuHA9PMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcD # AzB3BggrBgEFBQcBAQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2lj # ZXJ0LmNvbTBBBggrBgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29t # L0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmww # HAYDVR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIB # ADojRD2NCHbuj7w6mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6j # fCbVN7w6XUhtldU/SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmI # moqKwba9oUgYftzYgBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtf # JqGVWEjVGv7XJz/9kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrx # oj7bQ7gzyE84FJKZ9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3 # LIU/Gs4m6Ri+kAewQ3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx # 4b6cpwoG1iZnt5LmTl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9 # Oj9FpsToFpFSi0HASIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+I # Cw2/O/TOHnuO77Xry7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug # 0wcCampAMEhLNKhRILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5 # Vzu0nAPthkX0tGFuv2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwDCCBKigAwIBAgIQ # DE1pckuU+jwqSj0pB4A9WjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEX # MBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0 # ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDkyMTAw # MDAwMFoXDTMzMTEyMTIzNTk1OVowRjELMAkGA1UEBhMCVVMxETAPBgNVBAoTCERp # Z2lDZXJ0MSQwIgYDVQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIi # MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDP7KUmOsap8mu7jcENmtuh6BSF # dDMaJqzQHFUeHjZtvJJVDGH0nQl3PRWWCC9rZKT9BoMW15GSOBwxApb7crGXOlWv # M+xhiummKNuQY1y9iVPgOi2Mh0KuJqTku3h4uXoW4VbGwLpkU7sqFudQSLuIaQyI # xvG+4C99O7HKU41Agx7ny3JJKB5MgB6FVueF7fJhvKo6B332q27lZt3iXPUv7Y3U # TZWEaOOAy2p50dIQkUYp6z4m8rSMzUy5Zsi7qlA4DeWMlF0ZWr/1e0BubxaompyV # R4aFeT4MXmaMGgokvpyq0py2909ueMQoP6McD1AGN7oI2TWmtR7aeFgdOej4TJEQ # ln5N4d3CraV++C0bH+wrRhijGfY59/XBT3EuiQMRoku7mL/6T+R7Nu8GRORV/zbq # 5Xwx5/PCUsTmFntafqUlc9vAapkhLWPlWfVNL5AfJ7fSqxTlOGaHUQhr+1NDOdBk # +lbP4PQK5hRtZHi7mP2Uw3Mh8y/CLiDXgazT8QfU4b3ZXUtuMZQpi+ZBpGWUwFjl # 5S4pkKa3YWT62SBsGFFguqaBDwklU/G/O+mrBw5qBzliGcnWhX8T2Y15z2LF7OF7 # ucxnEweawXjtxojIsG4yeccLWYONxu71LHx7jstkifGxxLjnU15fVdJ9GSlZA076 # XepFcxyEftfO4tQ6dwIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1Ud # EwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZn # gQwBBAIwCwYJYIZIAYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCP # nshvMB0GA1UdDgQWBBRiit7QYfyPMRTtlwvNPSqUFN9SnDBaBgNVHR8EUzBRME+g # TaBLhklodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRS # U0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCB # gDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUF # BzAChkxodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVk # RzRSU0E0MDk2U0hBMjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUA # A4ICAQBVqioa80bzeFc3MPx140/WhSPx/PmVOZsl5vdyipjDd9Rk/BX7NsJJUSx4 # iGNVCUY5APxp1MqbKfujP8DJAJsTHbCYidx48s18hc1Tna9i4mFmoxQqRYdKmEIr # UPwbtZ4IMAn65C3XCYl5+QnmiM59G7hqopvBU2AJ6KO4ndetHxy47JhB8PYOgPvk # /9+dEKfrALpfSo8aOlK06r8JSRU1NlmaD1TSsht/fl4JrXZUinRtytIFZyt26/+Y # siaVOBmIRBTlClmia+ciPkQh0j8cwJvtfEiy2JIMkU88ZpSvXQJT657inuTTH4YB # ZJwAwuladHUNPeF5iL8cAZfJGSOA1zZaX5YWsWMMxkZAO85dNdRZPkOaGK7DycvD # +5sTX2q1x+DzBcNZ3ydiK95ByVO5/zQQZ/YmMph7/lxClIGUgp2sCovGSxVK05iQ # RWAzgOAj3vgDpPZFR+XOuANCR+hBNnF3rf2i6Jd0Ti7aHh2MWsgemtXC8MYiqE+b # vdgcmlHEL5r2X6cnl7qWLoVXwGDneFZ/au/ClZpLEQLIgpzJGgV8unG1TnqZbPTo # ntRamMifv427GFxD9dAq6OJi7ngE273R+1sKqHB+8JeEeOMIA11HLGOoJTiXAdI/ # Otrl5fbmm9x+LMz/F0xNAKLY1gEOuIvu5uByVYksJxlh9ncBjDCCB2IwggVKoAMC # AQICEA6ZYkUs3x6FUkpZyx3fNW8wDQYJKoZIhvcNAQELBQAwaTELMAkGA1UEBhMC # VVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBU # cnVzdGVkIEc0IENvZGUgU2lnbmluZyBSU0E0MDk2IFNIQTM4NCAyMDIxIENBMTAe # Fw0yMjExMTEwMDAwMDBaFw0yMzExMTAyMzU5NTlaMGcxCzAJBgNVBAYTAkFVMREw # DwYDVQQIEwhWaWN0b3JpYTEVMBMGA1UEBxMMTm90dGluZyBIaWxsMRYwFAYDVQQK # Ew1JUFNlYyBQdHkgTHRkMRYwFAYDVQQDEw1JUFNlYyBQdHkgTHRkMIICIjANBgkq # hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuJ4EsEL1x5o2E2m4pFTCeVzGv5URXuSd # Q25eyXUbMGb7VsTbft4MYM6ySZe7I1Yk7Jm/rsSsjDuud9XbvLJB3qX/kdVCFMLn # y2On85sFUQ67ZWHGh5ub9tIJzkJN7BlJ0u2Rw09AeLAArF3yqeQffu/YegMSH8rY # eb8DLvFr9lxKjoWXg5khH3yXWaI0N0g/FHKWL9URn6HSStY90y4ilQCcjwjwA8Vp # gIU9eCDj4kagwdU5ZvOazbzQXJqvzMB09ccLegf2MrPoMseRSksL0BBo5kkq5jiw # Fc4HIf2/RYrjfVWtFLq33tvMDifpI/ZuhiBooaAEe3OA10s8+A4Ps3OkSP2hd/Hq # UXGQZY1FN3fDRworMkEaaqQioeaaSePAIwdGTz2Z2aCY4b5taHWiOM/5+jhCr6Gq # S8TXI1ODtZl2V11/KfKJDjiTes2lQ4zShHNAg40/aTOQxmsvv1zwlM9Dqf8GRDC1 # OThWrPc40s6RztXa9CnC3EQgxIUQs/qQwkToS3dJGcr/bsCmVCHPQKDjHYIHbU8n # CMjSaG1YDIfdRW9MyyAFghtzjDDEyyN+LJCOdEnq4WgiaNIHNDsYABTAy9Nc2u4u # VmQE387ZqbAww2Doo/eiEnPU5y1JgXvxhqgdL2Fg20gXagfBqysTuuExuh/AoSuM # TEO02MRZ+78CAwEAAaOCAgYwggICMB8GA1UdIwQYMBaAFGg34Ou2O/hfEYb7/mF7 # CIhl9E5CMB0GA1UdDgQWBBTZSP/AY1Bcm0dUbWSS+LJDbsyIHzAOBgNVHQ8BAf8E # BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwgbUGA1UdHwSBrTCBqjBToFGgT4ZN # aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNp # Z25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcmwwU6BRoE+GTWh0dHA6Ly9jcmw0 # LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5nUlNBNDA5 # NlNIQTM4NDIwMjFDQTEuY3JsMD4GA1UdIAQ3MDUwMwYGZ4EMAQQBMCkwJwYIKwYB # BQUHAgEWG2h0dHA6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzCBlAYIKwYBBQUHAQEE # gYcwgYQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBcBggr # BgEFBQcwAoZQaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1 # c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAyMUNBMS5jcnQwDAYDVR0T # AQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAFQZrq6Y5WS74dGqSjbSXHqiQeQhc # emYWRNkPOnA4duyDdKNerNSkYDhqunW3igVhLkjYzV3rxPceL67+mUdy4ppf9g1Q # e8f6cM/wELnKmXH/aCGBVvBEjVJnt3LTQLGUGNu28Np1Hdfg7H0SqRlSuvLCaibr # C87bRHgzCLGixw/Bo2d/SuKze2EXTtt85DLvx7TCOE894bUuhaHUnz207+SUhRnm # W4pvHel8wJWfDN7QXANFvaV667cOlPZ9Kz2Jrxgf389Pj2XPrfIL/oH3U5OY6Q0/ # dHpFrT8s30MpG5iFQKbYn8W4790Gt/tjJVAFhMxYfnylT+ppemmvrltG/0qah903 # /IxMgSVI9I/23Gs83i7ppeCNnncF/5JRfD2qkQqKRHaJx/6rMjV0MDmV+m16bllZ # jEfnXIxPyB1P4tJEwiGtbKhuEX9WbdE5msxy64FFU/4Ucxa8dIjllcr1MMfwJttX # QDLtBYXDFrtb4r26FGRe3M+mjmZ0skzWPEuZpnjR0wK8Ep8HzEdB5WzLB/7lf70n # YEWfuE1CjviDRz9hesjHZZ7nvPNXuraIRqPdNWnI8eJ7sT+jnL9nP3/h/uDvuHZx # LAUBvRo6iNOOmO34/TZNCV0fBpZEBUBLJTBmFZoBTMV8z156RL7QXjCK1cLgXuCU # OfJLfjrNc5RZvtAxggZTMIIGTwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQK # Ew5EaWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBD # b2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEzODQgMjAyMSBDQTECEA6ZYkUs3x6FUkpZ # yx3fNW8wDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAigAoAAoQKA # ADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgELMQ4wDAYK # KwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg+U9lktubpEGUFJmJcrPsHlUsa86r # I/q0LS6071/NP7EwDQYJKoZIhvcNAQEBBQAEggIAMIC5ujX9UWIJaQueFRKa48Nn # ovNnu30qJiqLKnv5u2OIxfNoMcaG06HEM4BLfLsKtLAY2NVU3TI2G35erudUuUuO # PLXQiBKX8CHc6CwsU6EPPzChCg9w6foRRxLaIKmzmkeV6RD04u9wRqDGaz9yHYZ1 # 8eTv8KhjJH1xk+OJS8EUibk4pkqAFw4gC3kGZnS6kOHDz8oY6/tmNwecxaH9JpbE # yhgX2UKx56flfU9X6iIdJVcgvR4HpbLkYIp4hGlFpBIfKtlEeUsYYj35kWIFzmbK # QHMYq6c8I5pdQ02C98/T6b8JhwaJGkCCTbNvGG4tqs5hhZ3fag6kjNFeEFwUSTIr # pWIYQflDhVI3r+xWzXISGGbvC9HoobeKGMefy32cRn2HaZJt7fX7yqC4mAhwv/iu # 1DdsF8SrbvaE3pV+QWFWwlvD0x2+97Hql3dK5NDKJg842VGmDjp67/nNek8C8l05 # TCaxevjuww0BJqiGrqJ6sNxUmshkg5jhHhxP+x+O0lHGWhqjLUdXOCcjc6eRHjlV # BJF2uzEXy1oLrZccDfdDAjyG1U+Lnp86fzccYX0yCsYS5DFzx9O5QokXx38ZckiJ # Hhwl6j6JM/cjiwqCLecFukJkG66JQNAId3TAGPtv5nYUV1KWOszxPAUiXDtbjc0k # j6EJs9BE4EqtCW+jCTahggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBj # MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMT # MkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5n # IENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIxMjE1MDIxNzIyWjAv # BgkqhkiG9w0BCQQxIgQgmY7uvNxvo7cyxfPsuL7uZ0Vskh0k24uva3+o4jNqUSQw # DQYJKoZIhvcNAQEBBQAEggIAZ6/xD9XwgmRK5GI3ITK3n5sLAzTk/BBot+0aixOh # 5j/+MtSwk2iL2hydotGOlaiBRCxZm/L6UqF8mU3AU+zaV6uXyvh1OQeF3oLfskwP # fuOdK4z1Y9gaD7oIELID+KJhO77ofXFE6xo6vBThpTs1HbL7eOkB+13DvhYOYpgg # TbclbPJSDfJXjQLwD+1lU7w2mAHX4A/A3sNA38X6+fGPdIOlef3EeqyCtQKCsxn0 # kraUnIAf3YZRFaZdXSiIm9yLxUQs4ID7TmZSqIeaS3kIq92XpZRrYx3WGKhbGdRI # YxI5z0VhcfiBPD4rg5caQosH9lTa0TvOh596mb8tpxsntlVA1jDimLg2v6TwKee1 # +rUk4pGHwQqzQ2+x3h5VtEd4GaG5Nabu+no6F7PyStYulCgDIVskyzpDUR/4zrmF # KoZpSGWPGdRefILV33pL3PJCelg+T54aLTA9SRBt9EgqqEyqSj0FTRHxjZpqqO8z # dNcP0cHXYPmVKYTD/s9WuVGKkm1KU3J5BbyXsVb1eJTXpuiUoLjU06aGwMTxg1rs # tb7jNxub7zRC7LBiNMmQimWzdQ1KwJCB1P5PcI2BK7Ei5X2xJMFwgOJB6v56CT2G # hgYqsqLv9b8gqt48LoEP70NFHAHmYZxVvMn8yy1YIIyZRXuK73gMljQ6eX6pWB7a # mXk= # SIG # End signature block |