Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

Friday, June 20, 2025

Enterprise Tips – Secure Credentials, Proxy Settings, and MID Server Options for PowerShell + ServiceNow API

Introduction

So far in this series, we've explored how to connect PowerShell to the ServiceNow Table API, handle errors, and optimize performance. But in enterprise environments, you'll run into real-world constraints like:

  • Secure credential storage
  • Network proxies
  • Internal ServiceNow instances behind firewalls
  • Compliance restrictions

In this final article, we cover how to run secure, robust API integrations in production environments using best practices and ServiceNow architecture features.

1. Securely Store and Use Credentials

Hardcoding usernames and passwords in scripts is a security risk. Before picking a storage method, it's worth connecting this back to Part 1 of this series: if you're authenticating with OAuth's Client Credentials grant (the current recommended approach), what you actually need to protect is a Client ID and Client Secret, not an end user's password. That's a meaningfully smaller attack surface than the credential-storage problem this section might otherwise imply — but it still needs protecting properly rather than living in plaintext in a script.

SecretManagement (Recommended Starting Point)

Microsoft's Microsoft.PowerShell.SecretManagement module is the current, cross-platform-friendly way to abstract secret storage — it works the same way whether the underlying vault is a local encrypted store, Azure Key Vault, HashiCorp Vault, or something else, so your script logic doesn't change if the backend does.

Install-Module Microsoft.PowerShell.SecretManagement -Scope CurrentUser
Install-Module Microsoft.PowerShell.SecretStore -Scope CurrentUser

# One-time: register a local vault and store the secret
Register-SecretVault -Name LocalVault -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault
Set-Secret -Name "SNOW_CLIENT_SECRET" -Vault LocalVault

# In your script, retrieve it at runtime
$clientSecret = Get-Secret -Name "SNOW_CLIENT_SECRET" -Vault LocalVault -AsPlainText

Worth knowing: Microsoft has marked SecretManagement as feature-complete — it's still fully supported for security fixes, but not gaining new capabilities, specifically because the PowerShell team sees the industry moving toward passwordless and federated authentication (Entra ID, managed identities, hardware keys) rather than stored username/password secrets generally. That's not a reason to avoid it today — it's stable, well-documented, and does the job — but it's worth knowing this is a maturity/stability story, not an actively-evolving one, when planning long-term.

Windows Credential Manager (Simpler, Windows-Only)

For local automation on a Windows-only environment, the CredentialManager module wraps Windows Credential Manager directly:

Install-Module CredentialManager -Scope CurrentUser

# One-time setup
New-StoredCredential -Target "SNOW_API_CRED" -UserName "admin" -Password "your_password" -Persist LocalMachine

# In your script
$creds = Get-StoredCredential -Target "SNOW_API_CRED"
$user = $creds.Username
$pass = $creds.Password

This is simpler to set up than SecretManagement, but it doesn't support rotation, expiration, or audit logging the way a proper vault does, and it's Windows-specific — it won't work if any of your automation needs to run cross-platform under PowerShell 7+.

Secure Vaults (Enterprise)

If you're in a DevOps setup with centralized secret governance requirements, integrate with a dedicated vault directly — Azure Key Vault, HashiCorp Vault, or AWS Secrets Manager, each of which also has a SecretManagement extension vault available, letting you use the same Get-Secret/Set-Secret pattern above regardless of which one your organization standardizes on.

2. Use Proxy Settings When Required

Corporate environments often require internet access via proxy. PowerShell supports this:

$proxy = New-Object System.Net.WebProxy("http://proxy.company.com:8080")
$handler = New-Object System.Net.Http.HttpClientHandler
$handler.Proxy = $proxy

$client = [System.Net.Http.HttpClient]::new($handler)
$response = $client.GetAsync($url).Result

Or for Invoke-RestMethod (basic use):

Invoke-RestMethod -Uri $url -Proxy "http://proxy.company.com:8080" -Headers $headers

If the proxy requires authentication — common in corporate environments — pass credentials explicitly rather than assuming the default session context will satisfy it:

$proxyCred = Get-Secret -Name "PROXY_CRED" -Vault LocalVault
Invoke-RestMethod -Uri $url -Proxy "http://proxy.company.com:8080" -ProxyCredential $proxyCred -Headers $headers

3. Using MID Server as an Alternative to Direct API Calls

If ServiceNow is hosted internally or API access is restricted externally, a MID Server is the best approach.

What's a MID Server?

A Management, Instrumentation, and Discovery (MID) Server is a lightweight Java process that sits inside your network and acts as a secure bridge between ServiceNow and internal systems.

Use Cases:

  • When the target system is on-premises and ServiceNow's cloud instance can't reach it directly.
  • When you don't want to expose public API endpoints on your internal PowerShell-driven systems.
  • When calls need to run behind a proxy or firewall that only your internal network can traverse.

It's worth being precise about direction here, since these two mechanisms solve different problems:

  • A Scripted REST API is an inbound endpoint — it lets external systems (including a PowerShell script) call into ServiceNow. It processes whatever it receives using standard server-side script; it doesn't inherently involve a MID Server unless that processing logic separately needs to reach out to another on-prem system as part of handling the request.
  • A MID Server is what lets ServiceNow reach out to internal systems it can't call directly. If PowerShell needs to trigger something on an internal system via ServiceNow, or ServiceNow needs to pull data from an internal system, that's the MID Server's job — not the Scripted REST API's.

MID Server & Orchestration

You can use Orchestration + MID Server to trigger PowerShell scripts (via a PowerShell Probe) from a Workflow, and pull results back into ServiceNow. This is a well-established pattern, but it's tied to the classic Workflow engine. On current instances, the equivalent — and increasingly the more supported path going forward — is a Flow Designer flow calling the underlying probe API directly, or a vendor-provided Spoke if one exists for what you're trying to reach. Both approaches route through the same MID Server; the difference is which orchestration layer triggers it.

Bonus Tips

  • Understand rate limiting before assuming a number. ServiceNow doesn't enforce a single universal default rate limit like "100 calls/minute" — Rate Limit Rules are configured per-instance, typically per-hour rather than per-minute, and often aren't enabled at all unless a platform admin has explicitly set them up for specific users, roles, or APIs. Confirm what's actually configured on your instance with your platform team rather than assuming a fixed ceiling — building around an imagined limit can lead to either unnecessary self-throttling or an unpleasant surprise when a real one turns out to be lower than expected.
  • Rotate OAuth Client Secrets periodically, the same way you'd rotate a password.
  • Use roles and ACLs to limit what any given integration account can actually reach in ServiceNow — least privilege applies to service accounts as much as human users.
  • Log sensitive API interactions securely — don't let request/response logging become its own accidental data exposure.

Conclusion

Running PowerShell integrations with ServiceNow at scale requires more than just syntax — it takes planning for security, scalability, and reliability. By using proper secret storage (increasingly a Client Secret rather than a user password, if you followed this series' authentication recommendation), handling proxies correctly, and understanding what a MID Server actually does versus a Scripted REST API, you ensure your automation is enterprise-ready and compliant.

Optimizing Performance – Pagination, Filtering, and Query Design in PowerShell + ServiceNow API

Introduction

Once you've connected PowerShell to the ServiceNow Table API, the next big challenge is performance. Without the right approach, even a simple query can lead to:

  • Timeouts
  • Empty responses
  • Crashed scripts
  • Overloaded servers

This article covers three techniques to optimize your integration — pagination, field filtering, and efficient sysparm_query usage — plus a fix to a URL-encoding approach that trips up a lot of PowerShell + REST tutorials, including earlier drafts of this one.

1. Use Pagination (sysparm_limit and sysparm_offset)

By default, ServiceNow doesn't return all records in one call — and if you try to force it by omitting limits entirely, your script will time out on any table of meaningful size.

Best Practice

$limit = 100
$offset = 0
$headers = @{ "Authorization" = "Bearer $accessToken" }
$instance = "dev12345"
$url = "https://$instance.service-now.com/api/now/table/incident"

do {
    $pagedUrl = "$url?sysparm_limit=$limit&sysparm_offset=$offset"
    $response = Invoke-RestMethod -Uri $pagedUrl -Headers $headers
    $results = $response.result

    foreach ($record in $results) {
        Write-Output $record.number
    }

    $offset += $limit
} while ($results.Count -gt 0)

This loop pulls 100 records at a time — scalable, safe, and efficient. For genuinely large tables pulled on a schedule, consider adding a brief pause between pages (Start-Sleep -Milliseconds 200 or similar) — it costs very little wall-clock time overall and avoids hammering the instance with back-to-back requests, which matters more the more of these scripts you have running concurrently across environments.

2. Use sysparm_fields to Limit Response Size

By default, every API call returns all fields — even large ones like work_notes or attachments that you may not need for a given script.

Fix:

$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_fields=number,short_description,state"

This dramatically reduces payload size and speeds up execution — the effect compounds with pagination, since a smaller per-record payload means each page transfers and parses faster too.

3. Optimize Your sysparm_query Filter

Filtering is where most performance issues happen — especially when:

  • You use dot-walked fields.
  • You use LIKE queries.
  • You don't filter by time or indexed fields.

Bad:

caller_id.nameLIKEjohn

Causes implicit joins and slowdowns.

Good:

caller_id=681ccaf9c0a8016401c5a33be04be441

Add Time-Based Filters

Always use sys_updated_on or closed_at to narrow large tables:

sys_updated_on>javascript:gs.daysAgoStart(30)

A Correction Worth Making: How You URL-Encode the Query Matters

If you're building a sysparm_query value dynamically and need to URL-encode it before appending it to the request, it's common to see this pattern in PowerShell + REST tutorials:

$encodedQuery = [System.Web.HttpUtility]::UrlEncode($query)

This looks reasonable, but it has a real portability problem: System.Web.HttpUtility requires the System.Web assembly to be explicitly loaded first with Add-Type -AssemblyName System.Web — without that line, this throws an "unable to find type" error. It's also a largely Windows/.NET Framework-era namespace, which makes it an unreliable choice if any of your scripts might run under PowerShell 7+ (pwsh), including cross-platform or in a Linux-based CI/CD pipeline.

The more portable fix, available natively in both Windows PowerShell and PowerShell 7+ with no assembly loading required:

$encodedQuery = [System.Uri]::EscapeDataString($query)

This is also more RFC-compliant for query string encoding — HttpUtility.UrlEncode encodes spaces as + (an older HTML form-encoding convention), while EscapeDataString uses standard percent-encoding throughout. For a sysparm_query value being appended to a URL, EscapeDataString is the safer default.

Bonus: Combine All Techniques

$limit = 100
$offset = 0
$query = "active=true^sys_updated_on>javascript:gs.daysAgoStart(30)"
$encodedQuery = [System.Uri]::EscapeDataString($query)

do {
    $url = "https://$instance.service-now.com/api/now/table/incident?sysparm_query=$encodedQuery&sysparm_limit=$limit&sysparm_offset=$offset&sysparm_fields=number,short_description,state"

    $response = Invoke-RestMethod -Uri $url -Headers $headers
    $results = $response.result

    foreach ($incident in $results) {
        Write-Host "$($incident.number): $($incident.short_description)"
    }

    $offset += $limit
} while ($results.Count -gt 0)

Summary Checklist

Optimization Benefit
sysparm_limit + sysparm_offset Prevents timeouts, enables large pulls
sysparm_fields Reduces payload, faster API
Use sys_id instead of names Avoids joins
Filter on sys_updated_on Narrows down queries
Avoid dot-walked or LIKE filters Prevents performance bottlenecks
[System.Uri]::EscapeDataString() for encoding Portable across PowerShell versions, no assembly loading needed

Conclusion

A well-optimized query can save hours in execution time and avoid failed automations. These techniques are essential for scaling your PowerShell + ServiceNow integration reliably — and getting the small details right, like which URL-encoding method you reach for, is what separates a script that works on your machine from one that works reliably wherever it actually needs to run.

In the next article, we'll tackle real-world security and enterprise deployment tips, including proxies, secrets, and MID Server considerations.

Handling Common API Errors and Timeouts When Connecting to ServiceNow

ServiceNow common API errors and timeouts

Integrating PowerShell with the ServiceNow Table API mostly works — right up until a large table like incident, task, or cmdb_ci throws something unexpected: a script that crashes with no clear reason, a response that's missing fields you expected, or a call that just times out. This article covers the real causes behind the most common failures, and — just as importantly — the actual HTTP behavior involved, since getting that wrong leads to error-handling code that silently doesn't work.

Problem 1: "Transaction Cancelled – Maximum Execution Time Exceeded"

This happens when the server-side query takes too long to process — commonly on large tables, broad queries, or when heavy business logic runs on every matched record. The actual response comes back as HTTP 500 Internal Server Error, with this JSON body:

{
  "error": {
    "message": "Transaction cancelled: maximum execution time exceeded",
    "detail": "Transaction cancelled: maximum execution time exceeded Check logs for error trace or enable glide.rest.debug property to verify REST request processing"
  },
  "status": "failure"
}

This comes back as a real HTTP 500 error, not a 200. That distinction matters for how you handle it in PowerShell, covered in Problem 3 below. The default timeout is controlled by the glide.rest.max_execution_time_in_seconds property (60 seconds out of the box).

Why This Happens

  • You're pulling too many records at once.
  • Filters use unindexed or dot-walked fields (see Problem 2).
  • Long-running Business Rules or Flows are slowing down the underlying query.
  • You forgot to paginate or narrow the date range.

Fix It With PowerShell

# Correct use of pagination and fields
$limit = 100
$offset = 0
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=$limit&sysparm_offset=$offset&sysparm_fields=number,short_description,state"

$response = Invoke-RestMethod -Uri $url -Headers $headers
$response.result

Tip: always use sysparm_limit, sysparm_offset, and sysparm_fields together to reduce payload size and processing time per request — smaller, paginated calls are far less likely to hit the execution time ceiling than one large unbounded query.

Problem 2: Dot-Walked Field Filters Kill Performance

A filter like this:

caller_id.department.name=Finance

...is slow and prone to timing out, because dot-walking through reference fields in a query introduces implicit joins under the hood. The deeper the dot-walk, the more expensive the query gets.

Fix

Use direct sys_id values instead of dot-walking through display names:

$filter = "caller_id=6816f79cc0a8016401c5a33be04be441"
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_query=$filter"

If you don't already know the sys_id you need, resolve it with a separate, targeted lookup first, rather than filtering the large table by a dot-walked value on every call.

Problem 3: Handling Real Errors in PowerShell Correctly

Since a timeout or server error comes back as a real HTTP 500 (or 400, 403, 404, depending on the failure), Invoke-RestMethod treats it as a terminating error by default in Windows PowerShell. That means an unhandled call doesn't quietly continue with an empty result — it stops your script entirely at that line, which is its own problem if you're not expecting it.

Add Proper Error Handling

try {
    $response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers -ErrorAction Stop
    Write-Host "✅ Records returned: $($response.result.Count)"
}
catch {
    $statusCode = $_.Exception.Response.StatusCode.value__
    $errorBody = $_.ErrorDetails.Message
    Write-Host "❌ API call failed with status $statusCode"
    Write-Host "Details: $errorBody"
}

On PowerShell 7+, Invoke-RestMethod also supports -SkipHttpErrorCheck, which lets you inspect $response.StatusCode directly without throwing — useful if you want to branch on specific status codes without a try/catch block:

$response = Invoke-RestMethod -Uri $url -Headers $headers -SkipHttpErrorCheck -StatusCodeVariable statusCode
if ($statusCode -ne 200) {
    Write-Host "❌ API Error ($statusCode): $($response.error.message)"
} else {
    Write-Host "✅ Records returned: $($response.result.Count)"
}

Problem 4: A 200 That Still Isn't What You Expected

There is a real scenario where a genuinely successful 200 OK response doesn't give you what you expected: Access Control Rules silently filtering data out of the response. If the account making the API call doesn't have read access to certain fields or records, ServiceNow doesn't return an error for that — it simply omits or blanks out what the caller isn't allowed to see. The call succeeds, the status is 200, and the response can still look "off" — fewer records than expected, or fields present in the schema but empty in every row.

If a script consistently returns fewer records or thinner data than expected with no error at all, checking the calling account's role and ACL access on the target table is usually a faster diagnosis than assuming it's a scripting bug.

Extra Debugging Tools

  • Enable REST logs in ServiceNow. Set glide.rest.debug = true temporarily (turn it back off afterward — this is verbose and not meant to run permanently).
  • Check syslog for REST errors. Navigate to System Logs > Errors.
  • Use Postman to isolate the failure. If a call fails from PowerShell but succeeds with identical parameters in Postman, the problem is in your PowerShell handling — headers, encoding, or error handling — not the API call itself.

Conclusion

Performance and error handling both matter here, but get the mechanics right first: ServiceNow's REST Table API returns real, standard HTTP status codes for real errors — it doesn't hide a 500-level failure behind a 200. The practical risk in PowerShell isn't a disguised error slipping past your checks; it's an unhandled terminating exception stopping your script cold, or an ACL quietly limiting what a 200 actually contains. Handle both, and paginate your queries with sysparm_limit/sysparm_offset/sysparm_fields, and most of what shows up here goes away.

Getting Started – PowerShell + ServiceNow Table API with Authentication

Introduction

ServiceNow's Table API provides full CRUD access to any record in the platform. Combine that with PowerShell, and you unlock the ability to automate ticketing, compliance tracking, CMDB updates, and more — right from the command line.

In this article, you'll learn how to:

  • Authenticate using Basic Auth, OAuth2 password grant (and why to avoid it), and OAuth2 Client Credentials grant (the current recommended approach)
  • Make your first Table API call from PowerShell
  • Parse and handle JSON responses
  • Set the stage for advanced integration in later posts

1. Basic Authentication Setup

This is the simplest approach, but it's not recommended for production. Basic Auth sends credentials with every single request, which is why it's generally treated as a legacy pattern for anything beyond quick local testing.

# Replace with your instance and credentials
$instance = "dev12345"
$user = "admin"
$pass = "your_password"
$base64Auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("$user`:$pass"))

# Define headers and URL
$headers = @{
    "Authorization" = "Basic $base64Auth"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"

# Call the API
$response = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

# Output the result
$response.result

⚠️ Tip: Avoid hardcoding passwords, even for quick testing. Basic Auth is fine for a five-minute experiment against a Personal Developer Instance — don't build anything real on top of it.

2. OAuth2 Password Grant — and Why to Avoid It for New Work

This is the grant type shown in most older ServiceNow + PowerShell tutorials, including earlier drafts of this one. It's worth understanding, but it's no longer the right choice for anything new:

Prerequisites:

  • OAuth enabled in ServiceNow
  • A registered app with Client ID and Client Secret
  • A user account with API access
# Credentials and endpoint
$clientId = "your_client_id"
$clientSecret = "your_client_secret"
$username = "admin"
$password = "your_password"
$instance = "dev12345"
$tokenUrl = "https://$instance.service-now.com/oauth_token.do"

# Build request body
$body = @{
    grant_type = "password"
    client_id = $clientId
    client_secret = $clientSecret
    username = $username
    password = $password
}

# Get token
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$accessToken = $response.access_token

# Make Table API request
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"
$data = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

$data.result

Why this isn't actually the secure upgrade it looks like: this is the Resource Owner Password Credentials ("password") grant — and it's explicitly deprecated per current OAuth 2.0 security guidance, not just an older option among equals. Notice that it still requires the literal user password in the request body, which means switching from Basic Auth to this grant type doesn't actually solve the "avoid hardcoding passwords" problem — it just moves the same password from a request header into a request body. If you're on an older instance that doesn't yet support the grant type below, this remains a working fallback. For anything new, use Client Credentials instead.

3. OAuth2 Client Credentials Grant (Recommended)

Available since the Washington DC release, this grant is purpose-built for exactly this scenario — a script or service authenticating as itself, with no specific human user's password involved at all.

Prerequisites:

  • OAuth enabled in ServiceNow
  • A registered application configured for the Client Credentials grant, with an associated OAuth Application User (a dedicated service account used as the identity context for the token — configure this with the "Web service access only" / "Internal Integration User" option so it can't log into the UI and isn't subject to normal password expiration policies)
  • On Zurich and later, register this through System OAuth > Application Registry > New Inbound Integration Experience > New Integration > OAuth – Client Credentials grant. On older releases, use Create an OAuth API endpoint for external clients instead — the underlying mechanism is the same, just a different registration screen.
# Credentials and endpoint — no username or password needed
$clientId = "your_client_id"
$clientSecret = "your_client_secret"
$instance = "dev12345"
$tokenUrl = "https://$instance.service-now.com/oauth_token.do"

# Build request body
$body = @{
    grant_type    = "client_credentials"
    client_id     = $clientId
    client_secret = $clientSecret
}

# Get token
$response = Invoke-RestMethod -Uri $tokenUrl -Method Post -Body $body -ContentType "application/x-www-form-urlencoded"
$accessToken = $response.access_token

# Make Table API request
$headers = @{
    "Authorization" = "Bearer $accessToken"
    "Accept"        = "application/json"
}
$url = "https://$instance.service-now.com/api/now/table/incident?sysparm_limit=1"
$data = Invoke-RestMethod -Uri $url -Method Get -Headers $headers

$data.result

Notice what's missing compared to the password grant: no username, no password. The script authenticates as the registered application itself, using the Client ID and Client Secret only. The Client Secret is still a credential worth protecting properly — that part of the "avoid hardcoding secrets" advice still applies, and we'll cover secure secret handling in Part 4 — but you're no longer storing an actual human user's password anywhere in the integration at all, which meaningfully reduces what's at risk if the script or its configuration ever leaks.

4. What You Should See

A single incident record, structured in JSON, regardless of which authentication method above you used:

{
  "result": [
    {
      "number": "INC0010001",
      "short_description": "Sample Incident",
      "state": "1",
      "sys_id": "abc123..."
    }
  ]
}

Conclusion

Connecting PowerShell to ServiceNow via the Table API is a powerful step toward automation. Whether you're managing incidents, risks, or CMDB items, understanding authentication methods — and which ones are actually current — is key. Basic Auth is fine for a quick local test; the password grant still works but is on its way out; Client Credentials is the one worth building on for anything you intend to keep running.

In future articles, we'll make this integration enterprise-grade with error handling, secure credential storage, and better performance.