diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6c2ff60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "githubPullRequests.ignoredPullRequestBranches": [ + "master" + ] +} \ No newline at end of file diff --git a/ServiceNow/Private/Get-ServiceNowAuth.ps1 b/ServiceNow/Private/Get-ServiceNowAuth.ps1 index 9f221a5..e2a3ebd 100644 --- a/ServiceNow/Private/Get-ServiceNowAuth.ps1 +++ b/ServiceNow/Private/Get-ServiceNowAuth.ps1 @@ -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 @@ -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 + } # check if we need a new access token if ( $ServiceNowSession.ExpiresOn -lt (Get-Date) -and $ServiceNowSession.RefreshToken -and $ServiceNowSession.ClientCredential ) { @@ -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)) @@ -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)) } @@ -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" } } diff --git a/ServiceNow/Private/Invoke-ServiceNowRestMethod.ps1 b/ServiceNow/Private/Invoke-ServiceNowRestMethod.ps1 index 9345968..0b5d897 100644 --- a/ServiceNow/Private/Invoke-ServiceNowRestMethod.ps1 +++ b/ServiceNow/Private/Invoke-ServiceNowRestMethod.ps1 @@ -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', @@ -48,6 +48,9 @@ function Invoke-ServiceNowRestMethod { [parameter()] [string] $FilterString, + [parameter()] + [string] $Namespace, + [parameter()] [object[]] $Sort = @('opened_at', 'desc'), @@ -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 + } $params.Method = $Method $params.ContentType = 'application/json' @@ -93,8 +100,7 @@ function Invoke-ServiceNowRestMethod { if ( $SysId ) { $params.Uri += "/$SysId" } - } - else { + } else { $params.Uri += $UriLeaf } @@ -153,8 +159,7 @@ function Invoke-ServiceNowRestMethod { try { $response = Invoke-WebRequest @params Write-Debug $response - } - catch { + } catch { $ProgressPreference = $oldProgressPreference throw $_ } @@ -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 ) } @@ -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" @@ -215,16 +217,14 @@ 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 $_ } @@ -232,8 +232,7 @@ 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 } } @@ -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' } @@ -283,4 +281,4 @@ function Invoke-ServiceNowRestMethod { } $records -} \ No newline at end of file +} diff --git a/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 b/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 new file mode 100644 index 0000000..94c90c8 --- /dev/null +++ b/ServiceNow/Public/New-ServiceNowCatalogItem.ps1 @@ -0,0 +1,105 @@ +<# +.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 CatalogItem + Name or ID of the catalog item that will be created + +.PARAMETER Variables + Key/value pairs of variable names and their values + +.PARAMETER CheckoutImmediately + If provided, a second Post for cart checkout to submit_order API Endpoint will be sent + +.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-ServiceNowCatalogItem -CatalogItem "Standard Laptop" -Variables @{'acrobat' = 'true'; 'photoshop' = 'true'; ' Additional_software_requirements' = 'Testing Service catalog API' } + + Raise a new catalog request using Item Name + +.EXAMPLE + New-ServiceNowCatalogItem -CatalogItem "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)] + param + ( + [Parameter(Mandatory)] + [string]$CatalogItem, + [Parameter(Mandatory)] + [Alias('Variables')] + [hashtable]$InputData, + [Parameter()][Hashtable]$Connection, + [Parameter()][hashtable]$ServiceNowSession = $script:ServiceNowSession, + [Parameter()][switch]$CheckoutImmediately, + [Parameter()][switch]$PassThru + ) + + begin { + if (-not $PSBoundParameters.ContainsKey('CheckoutImmediately')) { + $CheckoutImmediately = $false + } + if ($CatalogItem -match '^[a-zA-Z0-9]{32}$') { + #Verify the sys_id of the Catalog Item + $CatalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -ID $CatalogItem -Property sys_id + if ([string]::IsNullOrEmpty($CatalogItemID)) { throw "Unable to find catalog item by ID '$($CatalogItem)'" } else { Write-Verbose "Found $($catalogitemid) via lookup from '$($CatalogItem)'" } + } else { + #Lookup the sys_id of the Catalog Item + $CatalogItemID = Get-ServiceNowRecord -Table sc_cat_item -AsValue -Filter @('name', '-eq', $CatalogItem ) -Property sys_id + if ([string]::IsNullOrEmpty($CatalogItemID)) { throw "Unable to find catalog item by name '$($CatalogItem)'" } else { Write-Verbose "Found $($catalogitemid) via lookup from '$($CatalogItem)'" } + } + } + process { + + $AddItemToCart = @{ + Method = 'Post' + UriLeaf = "/servicecatalog/items/{0}/add_to_cart" -f $CatalogItemID + Values = @{'sysparm_quantity' = 1; 'variables' = $InputData } + Namespace = 'sn_sc' + Connection = $Connection + ServiceNowSession = $ServiceNowSession + } + + if ( $PSCmdlet.ShouldProcess($CatalogItemID, 'Create new catalog item request') ) { + + $AddItemCartResponse = Invoke-ServiceNowRestMethod @AddItemToCart + + if ($AddItemCartResponse.cart_id -and $CheckoutImmediately) { + $SubmitOrder = @{ + Method = 'Post' + UriLeaf = "/servicecatalog/cart/submit_order" + Namespace = 'sn_sc' + Connection = $Connection + ServiceNowSession = $ServiceNowSession + } + + $SubmitOrderResponse = Invoke-ServiceNowRestMethod @SubmitOrder + + if ($PassThru) { + $SubmitOrderResponse | Select-Object @{'n' = 'number'; 'e' = { $_.request_number } }, request_id + } + } + } else { + $AddItemToCart | Out-String + } + } +} \ No newline at end of file diff --git a/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 b/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 new file mode 100644 index 0000000..bf45b33 --- /dev/null +++ b/ServiceNow/Public/Submit-ServiceNowCatalogOrder.ps1 @@ -0,0 +1,63 @@ +<# +.SYNOPSIS + Submit a catalog request using Service Catalog API + +.DESCRIPTION + Checks out the user cart, based on the current check-out type (one-step or two-step). Reference: https://developer.servicenow.com/dev.do#!/reference/api/zurich/rest/c_ServiceCatalogAPI#servicecat-POST-cart-sub_order?navFilter=serv + +.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 + Submit-ServiceNowCatalogOrder + + Checks out the user cart, based on the current check-out type (one-step or two-step). + +.EXAMPLE + Submit-ServiceNowCatalogOrder -PassThru + + Checks out the user cart, based on the current check-out type (one-step or two-step) and returns the request numbers as an object. + +.INPUTS + InputData + +.OUTPUTS + PSCustomObject if PassThru provided +#> +function Submit-ServiceNowCatalogOrder { + [CmdletBinding(SupportsShouldProcess)] + param + ( + [Parameter()][Hashtable]$Connection, + [Parameter()][hashtable]$ServiceNowSession = $script:ServiceNowSession, + [Parameter()][switch]$PassThru + ) + + process { + + if ( $PSCmdlet.ShouldProcess('POST cart to Submit_Order API') ) { + $SubmitOrder = @{ + Method = 'Post' + UriLeaf = "/servicecatalog/cart/submit_order" + Namespace = 'sn_sc' + Connection = $Connection + ServiceNowSession = $ServiceNowSession + } + + $SubmitOrderResponse = Invoke-ServiceNowRestMethod @SubmitOrder + + if ($PassThru) { + $SubmitOrderResponse | Select-Object @{'n' = 'number'; 'e' = { $_.request_number } }, request_id + } + + } else { + Write-Output "Checks out the user cart, based on the current check-out type (one-step or two-step).`n`nIf one-step checkout, the method checks out (saves) the cart and returns the request number and the request order ID. If two-step checkout, the method returns the cart order status and all the information required for two-step checkout." + } + } +} \ No newline at end of file diff --git a/ServiceNow/ServiceNow.psd1 b/ServiceNow/ServiceNow.psd1 index d23710c..afd8a89 100644 --- a/ServiceNow/ServiceNow.psd1 +++ b/ServiceNow/ServiceNow.psd1 @@ -136,4 +136,3 @@ PrivateData = @{ # DefaultCommandPrefix = '' } -