Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions ServiceNow/Private/Get-ServiceNowAuth.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ function Get-ServiceNowAuth {
[CmdletBinding()]
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'requirement of azure automation')]

Param (
param (
[Parameter()]
[Alias('C')]
[hashtable] $Connection,

[Parameter()]
[Alias('N')]
[string] $Namespace = 'now',

[Parameter()]
[Alias('S')]
[hashtable] $ServiceNowSession
Expand All @@ -30,7 +34,11 @@ function Get-ServiceNowAuth {
process {

if ( $ServiceNowSession.Count -gt 0 ) {
$hashOut.Uri = $ServiceNowSession.BaseUri
if ($Namespace -ne 'now') {
$hashOut.Uri = $($ServiceNowSession.BaseUri -split ('api'))[0] + 'api/' + $Namespace
} else {
$hashOut.Uri = $ServiceNowSession.BaseUri
}
Comment on lines +37 to +41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking we're better served by changing the baseuri to end with /api and just append the namespace. this will involve changes here and invoke-service nowrestmethod.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and I tried to add the Namespace support functionality with as limited of breaking changes as I could. However, I didn't want to modify the New-ServiceNowSession function where BaseUri is originally declared into script scope variable $ServiceNowSession the due to a lack of familiarity with version specification and GraphQL use cases.


# check if we need a new access token
if ( $ServiceNowSession.ExpiresOn -lt (Get-Date) -and $ServiceNowSession.RefreshToken -and $ServiceNowSession.ClientCredential ) {
Expand All @@ -46,7 +54,7 @@ function Get-ServiceNowAuth {
refresh_token = $ServiceNowSession.RefreshToken.GetNetworkCredential().password
}
}

$response = Invoke-RestMethod @refreshParams

$ServiceNowSession.AccessToken = New-Object System.Management.Automation.PSCredential('AccessToken', ($response.access_token | ConvertTo-SecureString -AsPlainText -Force))
Expand All @@ -64,8 +72,7 @@ function Get-ServiceNowAuth {
$hashOut.Headers = @{
'Authorization' = 'Bearer {0}' -f $ServiceNowSession.AccessToken.GetNetworkCredential().password
}
}
else {
} else {
# issue 248
$pair = '{0}:{1}' -f $ServiceNowSession.Credential.UserName, $ServiceNowSession.Credential.GetNetworkCredential().Password
$hashOut.Headers = @{ Authorization = 'Basic ' + [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) }
Expand All @@ -75,34 +82,28 @@ function Get-ServiceNowAuth {
$hashOut.Proxy = $ServiceNowSession.Proxy
if ( $ServiceNowSession.ProxyCredential ) {
$hashOut.ProxyCredential = $ServiceNowSession.ProxyCredential
}
else {
} else {
$hashOut.ProxyUseDefaultCredentials = $true
}
}
}
elseif ( $Connection ) {
} elseif ( $Connection ) {
Write-Verbose 'connection'
# issue 248
$pair = '{0}:{1}' -f $Connection.Username, $Connection.Password
$hashOut.Headers = @{ Authorization = 'Basic ' + [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair)) }
$hashOut.Uri = 'https://{0}/api/now/v1' -f $Connection.ServiceNowUri
}
elseif ( $env:SNOW_SERVER ) {
$hashOut.Uri = 'https://{0}/api/now' -f $env:SNOW_SERVER
$hashOut.Uri = 'https://{0}/api/{1}/v1' -f $Connection.ServiceNowUri, $Namespace
} elseif ( $env:SNOW_SERVER ) {
$hashOut.Uri = 'https://{0}/api/{1}' -f $env:SNOW_SERVER, $Namespace
if ( $env:SNOW_TOKEN ) {
$hashOut.Headers = @{
'Authorization' = 'Bearer {0}' -f $env:SNOW_TOKEN
}
}
elseif ( $env:SNOW_USER -and $env:SNOW_PASS ) {
} elseif ( $env:SNOW_USER -and $env:SNOW_PASS ) {
$hashOut.Credential = New-Object System.Management.Automation.PSCredential($env:SNOW_USER, ($env:SNOW_PASS | ConvertTo-SecureString -AsPlainText -Force))
}
else {
} else {
throw 'A ServiceNow server environment variable has been set, but authentication via SNOW_TOKEN or SNOW_USER/SNOW_PASS was not found'
}
}
else {
} else {
throw "You must authenticate by either calling the New-ServiceNowSession cmdlet or passing in an Azure Automation connection object"
}
}
Expand Down
48 changes: 23 additions & 25 deletions ServiceNow/Private/Invoke-ServiceNowRestMethod.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ function Invoke-ServiceNowRestMethod {
[CmdletBinding(SupportsPaging)]
[System.Diagnostics.CodeAnalysis.SuppressMessage('PSUseBOMForUnicodeEncodedFile', '', Justification = 'issuees with *nix machines and no benefit')]

Param (
param (
[parameter()]
[ValidateSet('Get', 'Post', 'Patch', 'Delete')]
[string] $Method = 'Get',
Expand Down Expand Up @@ -48,6 +48,9 @@ function Invoke-ServiceNowRestMethod {
[parameter()]
[string] $FilterString,

[parameter()]
[string] $Namespace,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default to 'now'?


[parameter()]
[object[]] $Sort = @('opened_at', 'desc'),

Expand All @@ -74,7 +77,11 @@ function Invoke-ServiceNowRestMethod {
)

# get header/body auth values
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession
if ($namespace) {
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession -N $namespace
} else {
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession
}
Comment on lines +80 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if ($namespace) {
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession -N $namespace
} else {
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession
}
$params = Get-ServiceNowAuth -C $Connection -S $ServiceNowSession -N $namespace


$params.Method = $Method
$params.ContentType = 'application/json'
Expand All @@ -93,8 +100,7 @@ function Invoke-ServiceNowRestMethod {
if ( $SysId ) {
$params.Uri += "/$SysId"
}
}
else {
} else {
$params.Uri += $UriLeaf
}

Expand Down Expand Up @@ -153,8 +159,7 @@ function Invoke-ServiceNowRestMethod {
try {
$response = Invoke-WebRequest @params
Write-Debug $response
}
catch {
} catch {
$ProgressPreference = $oldProgressPreference
throw $_
}
Expand All @@ -174,12 +179,10 @@ function Invoke-ServiceNowRestMethod {
$content = $response.content | ConvertFrom-Json
if ( $content.PSobject.Properties.Name -contains "result" ) {
$records = @($content | Select-Object -ExpandProperty result)
}
else {
} else {
$records = @($content)
}
}
else {
} else {
# invoke-webrequest didn't throw an error per se, but we didn't get content back either
throw ('"{0} : {1}' -f $response.StatusCode, $response | Out-String )
}
Expand All @@ -190,8 +193,7 @@ function Invoke-ServiceNowRestMethod {
if ( $response.Headers.'X-Total-Count' ) {
if ($PSVersionTable.PSVersion.Major -lt 6) {
$totalRecordCount = [int]$response.Headers.'X-Total-Count'
}
else {
} else {
$totalRecordCount = [int]($response.Headers.'X-Total-Count'[0])
}
Write-Verbose "Total number of records for this query: $totalRecordCount"
Expand All @@ -215,25 +217,22 @@ function Invoke-ServiceNowRestMethod {

$end = if ( $totalRecordCount -lt $setPoint ) {
$totalRecordCount
}
else {
} else {
$setPoint
}

Write-Verbose ('getting {0}-{1} of {2}' -f ($params.body.sysparm_offset + 1), $end, $totalRecordCount)
try {
$response = Invoke-WebRequest @params -Verbose:$false
}
catch {
} catch {
$ProgressPreference = $oldProgressPreference
throw $_
}

$content = $response.content | ConvertFrom-Json
if ( $content.PSobject.Properties.Name -contains "result" ) {
$records += $content | Select-Object -ExpandProperty result
}
else {
} else {
$records += $content
}
}
Expand All @@ -249,18 +248,17 @@ function Invoke-ServiceNowRestMethod {
switch ($Method) {
'Get' {
$ConvertToDateField = @('closed_at', 'expected_start', 'follow_up', 'opened_at', 'sys_created_on', 'sys_updated_on', 'work_end', 'work_start')
ForEach ($SNResult in $records) {
ForEach ($Property in $ConvertToDateField) {
If (-not [string]::IsNullOrEmpty($SNResult.$Property)) {
Try {
foreach ($SNResult in $records) {
foreach ($Property in $ConvertToDateField) {
if (-not [string]::IsNullOrEmpty($SNResult.$Property)) {
try {
# Extract the default Date/Time formatting from the local computer's "Culture" settings, and then create the format to use when parsing the date/time from Service-Now
$CultureDateTimeFormat = (Get-Culture).DateTimeFormat
$DateFormat = $CultureDateTimeFormat.ShortDatePattern
$TimeFormat = $CultureDateTimeFormat.LongTimePattern
$DateTimeFormat = [string[]]@("$DateFormat $TimeFormat", 'yyyy-MM-dd HH:mm:ss')
$SNResult.$Property = [DateTime]::ParseExact($($SNResult.$Property), $DateTimeFormat, [System.Globalization.DateTimeFormatInfo]::InvariantInfo, [System.Globalization.DateTimeStyles]::None)
}
Catch {
} catch {
# If the local culture and universal formats both fail keep the property as a string (Do nothing)
$null = 'Silencing a PSSA alert with this line'
}
Expand All @@ -283,4 +281,4 @@ function Invoke-ServiceNowRestMethod {
}

$records
}
}
97 changes: 97 additions & 0 deletions ServiceNow/Public/New-ServiceNowCatalogItem.ps1
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be good to have a way to control when checkout occurs in case folks want to add multiple (different) items before submitting order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the CheckoutImmediately switch parameter as well as new function 'Submit-ServiceNowCatalogOrder' to support delayed checkout.

I feel that most use cases will want to use CheckoutImmediately as the default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<#
.SYNOPSIS
Submit a catalog request using Service Catalog API
.DESCRIPTION
Create a new catalog item request using Service Catalog API. Reference: https://www.servicenow.com/community/itsm-articles/submit-catalog-request-using-service-catalog-api/ta-p/2305836
.PARAMETER CatalogItemName
Name of the catalog item that will be created
.PARAMETER CatalogItemID
SysID of the catalog item that will be created
.PARAMETER Variables
Key/value pairs of variable names and their values
.PARAMETER PassThru
If provided, the new record will be returned
.PARAMETER Connection
Azure Automation Connection object containing username, password, and URL for the ServiceNow instance
.PARAMETER ServiceNowSession
ServiceNow session created by New-ServiceNowSession. Will default to script-level variable $ServiceNowSession.
.EXAMPLE
New-ServiceNowRecord -CatalogItemName "Standard Laptop" -Variables @{'acrobat' = 'true'; 'photoshop' = 'true'; ' Additional_software_requirements' = 'Testing Service catalog API' }
Raise a new catalog request using Item Name
.EXAMPLE
New-ServiceNowRecord -CatalogItemID "04b7e94b4f7b42000086eeed18110c7fd" -Variables @{'acrobat' = 'true'; 'photoshop' = 'true'; ' Additional_software_requirements' = 'Testing Service catalog API' }
Raise a new catalog request using Item ID
.INPUTS
InputData
.OUTPUTS
PSCustomObject if PassThru provided
#>
function New-ServiceNowCatalogItem {
[CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ID')]
param
(
[Parameter(Mandatory, ParameterSetName = 'Name')]
[string]$CatalogItemName,
[Parameter(Mandatory, ParameterSetName = 'ID')]
[string]$CatalogItemID,
[Parameter(Mandatory, ParameterSetName = 'Name')]
[Parameter(Mandatory, ParameterSetName = 'ID')]
[Alias('Variables')]
[hashtable]$InputData,
[Parameter()][Hashtable]$Connection,
[Parameter()][hashtable]$ServiceNowSession = $script:ServiceNowSession,
[Parameter()][switch]$PassThru
)

begin {
if ($CatalogItemName) {
#Lookup the sys_id of the Catalog Item name
$CatalogItemID = (Get-ServiceNowRecord -Table sc_cat_item -AsValue -Filter @('name', '-eq', $CatalogItemName )).sys_id
if ([string]::IsNullOrEmpty($CatalogItemID)) { throw "Unable to find catalog item by name '$($catalogitemname)'" } else { Write-Verbose "Found $($catalogitemid) via lookup from '$($CatalogItemName)'" }
}
}
process {

$AddItemToCart = @{
Method = 'Post'
UriLeaf = "/servicecatalog/items/{0}/add_to_cart" -f $CatalogItemID
Values = @{'sysparm_quantity' = 1; 'variables' = $InputData }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should make quantity a function parameter along with the other request body parameters, sysparm_also_request_for and sysparm_requested_for. https://www.servicenow.com/docs/bundle/zurich-api-reference/page/integrate/inbound-rest/concept/c_ServiceCatalogAPI.html#title_servicecat-POST-items-add_to_cart. add examples for these new parameters as well.

Namespace = 'sn_sc'
Connection = $Connection
ServiceNowSession = $ServiceNowSession
}

if ( $PSCmdlet.ShouldProcess($CatalogItemID, 'Create new catalog item request') ) {

$AddItemCartResponse = Invoke-ServiceNowRestMethod @AddItemToCart

if ($AddItemCartResponse.cart_id) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why might we not get a value for cart_id that we need this 'if'?

Copy link
Contributor Author

@CATgwalker CATgwalker Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a safety check to make sure the function doesn't continue to checkout if the Invoke results in an error when adding the item to the cart. It may not be necessary but I wanted to be absolutely sure that a checkout wasn't submitted if the Invoke error didn't return a terminating error up the call stack.

Adjusted to include the CheckoutImmediately parameter.

$SubmitOrder = @{
Method = 'Post'
UriLeaf = "/servicecatalog/cart/submit_order"
Namespace = 'sn_sc'
Connection = $Connection
ServiceNowSession = $ServiceNowSession
}

$SubmitOrderResponse = Invoke-ServiceNowRestMethod @SubmitOrder
}
if ( $PassThru ) {
$SubmitOrderResponse
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$SubmitOrderResponse
$SubmitOrderResponse | Select-Object @{'n'='number';'e'={$_.request_number}}, request_id

consider updating the output to something like the above so it can be piped directly into the other functions, eg. Get-ServiceNowRecord

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upon further review of the API documentation, I'm not sure how this would behave when a two-step checkout process is configured.

I do not have a way to test two-step checkout at the moment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: needs to be tested to validate the return behavior when multiple quantities of a catalog item or multiple catalog items are returned from the submit_order POST.

}
}
}
}
2 changes: 1 addition & 1 deletion ServiceNow/ServiceNow.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
RootModule = 'ServiceNow.psm1'

# Version number of this module.
ModuleVersion = '4.1.0'
ModuleVersion = '4.2.0'

# Supported PSEditions
# CompatiblePSEditions = @()
Expand Down