Skip to navigation

ServiceNow

ServiceNow Integration

Pensar Console integrates with ServiceNow to file security findings as records on your own instance — incident or, where the Security Incident Response plugin is installed, sn_si_incident — and to keep their state in sync with the finding in Pensar.

Unlike Linear or Slack, there is no Pensar-wide ServiceNow application to approve. A ServiceNow OAuth client lives inside a single instance, in that instance’s Application Registry, so the client ID and secret can only be created by someone with admin access to your instance. Everything below that requires work in ServiceNow — registering the OAuth application and, for inbound status sync, writing a Business Rule — exists for that reason.

Records created by Pensar are attributed to whichever identity you connect with: the ServiceNow user who approves the OAuth authorization, or the service account behind basic authentication. Pensar inherits that user’s roles — it can only read and write what they can.

Prerequisites

Before connecting ServiceNow, ensure you have:

  1. Admin access to your ServiceNow instance — enough to open System OAuth → Application Registry, and (for inbound status sync) to create a Script Include and a Business Rule
  2. Admin or owner access to the Pensar Console workspace you want to connect
  3. Your instance URL, in the form https://<instance>.service-now.com

Pensar accepts the instance URL as an origin only — https://acme.service-now.com, not https://acme.service-now.com/now/nav/ui. It must use HTTPS, and any path, query string or trailing slash is rejected rather than silently trimmed.

Connecting ServiceNow

Both options start the same way:

  1. Navigate to Settings → Integrations → Issue tracking in your Pensar Console workspace
  2. Find the Issue sync card, locate the ServiceNow row and click Manage
  3. Enter your instance URL, then choose an authentication method

OAuth gives Pensar a short-lived access token that it refreshes on its own, and a token you can revoke from inside ServiceNow at any time. Prefer it over basic authentication.

1. Register the OAuth application in ServiceNow

  1. In your ServiceNow instance, navigate to All → System OAuth → Application Registry
  2. Click New
  3. Choose Create an OAuth API endpoint for external clients
  4. Fill in:
    • Name: Pensar (or your preferred name)
    • Client ID: leave the generated value
    • Client Secret: leave the generated value, or set your own
    • Redirect URL: https://console.pensar.dev/api/auth/servicenow-callback
  5. Click Submit, then reopen the record and copy the Client ID and Client Secret

If your workspace is on a Console host other than console.pensar.dev, substitute that host — the path /api/auth/servicenow-callback is the same. The redirect URL must match exactly, including the scheme; ServiceNow rejects the authorization request otherwise.

2. Connect from Pensar

  1. Back in the ServiceNow section of the Issue sync card, paste the Client ID and Client Secret alongside your instance URL and submit
  2. You’ll be redirected to your instance’s authorization screen (/oauth_auth.do)
  3. Sign in as the user Pensar should act as, and approve the request
  4. You’ll be redirected back to Pensar Console with ServiceNow connected

There is no scope list to review on that screen. A ServiceNow OAuth client has no scopes of its own — it inherits the roles of the user who approves it. Approve as an account that holds the roles Pensar needs (see Roles), and no more.

Pensar refreshes the access token automatically before it expires. Two ServiceNow settings on the Application Registry record govern how long that can continue:

Application Registry fieldServiceNow defaultEffect
Access Token Lifespan1,800 secondsHow often Pensar silently refreshes
Refresh Token Lifespan8,640,000 seconds (100 days)How long before you must reconnect from Pensar

Some instances are configured to rotate the refresh token on every use and some are not. Pensar handles both — it stores a rotated token when ServiceNow returns one and keeps the existing one when it doesn’t.

Option B — Basic authentication

Where OAuth is not an option, Pensar can authenticate as a dedicated ServiceNow service account.

  1. In ServiceNow, navigate to All → User Administration → Users and click New
  2. Create a dedicated account — for example pensar.integration — and set a strong password
  3. Check Web service access only so the account cannot sign in to the UI, and ensure multi-factor authentication is not enforced for it (basic authentication cannot satisfy an MFA challenge)
  4. Grant it the roles below
  5. Enter the username and password in Pensar’s ServiceNow setup form

Basic authentication stores a long-lived shared credential. Pensar encrypts it at rest, but there is no rotation and no revocation path from Pensar’s side: Disconnect clears Pensar’s copy of the credential, it does not disable the account. Changing the password in ServiceNow breaks the integration until you reconnect with the new one, and any leak has to be handled by deactivating the account in ServiceNow. Use OAuth unless something prevents it.

Roles

Whichever method you use, the connected identity needs to:

  • Create and update records on the table you file into — itil covers incident; Security Incident Response tables such as sn_si_incident need the corresponding sn_si roles (sn_si.basic at minimum, plus an analyst role to write)
  • Read sys_db_object — Pensar verifies the credential against it and uses it to discover which filing tables exist on your instance
  • Read sys_choice — to resolve your table’s own state choices when mapping status
  • Read sys_dictionary — to check whether the target table declares close_code and close_notes before writing them

The last three are dictionary tables that most authenticated ServiceNow users can already read. If your instance restricts them, Pensar will connect but the table picker will come up empty.

What Gets Synced

Once connected and enabled, Pensar inserts a record on the routed table for each matching finding, setting four fields:

ServiceNow fieldValue
short_descriptionThe finding’s title, prefixed with [Pensar Security]
descriptionA plain-text summary of the finding
impactDerived from the finding’s severity
urgencyDerived from the finding’s severity

Nothing else is set. Pensar does not populate assignment_group, caller_id, category or configuration_item — your own assignment rules and Business Rules run on the new record exactly as they would on any other insert.

The description field renders literally in ServiceNow, so Pensar writes it as plain text rather than the Markdown it sends to other trackers. It contains:

  • The severity
  • The finding’s description
  • A Details block: the Pensar reference, the affected location, the line number(s), the branch, and the CWE classification (or Not classified)
  • A link back to the finding in the Pensar Console
  • A link to the related pull request, when there is one

Severity Mapping

ServiceNow derives priority from the impact/urgency pair through your instance’s priority lookup rules, so those two are what Pensar sets:

Pensar severityimpacturgencyResulting priority (out-of-box matrix)
Critical1 — High1 — High1 — Critical
High2 — Medium1 — High2 — High
Medium2 — Medium2 — Medium3 — Moderate
Low3 — Low3 — Low5 — Planning

The priority column shows what the out-of-box priority matrix produces. If your instance customises the lookup rules, your priorities will differ — Pensar sets impact and urgency and lets ServiceNow decide the rest.

Opening a finding in ServiceNow

When a ServiceNow record already exists for a finding, the finding’s actions menu includes View in ServiceNow. Otherwise you can file one from the same menu with Open as ServiceNow record. That does not require auto-sync to be enabled, only that ServiceNow is connected and a table is selected (the workspace default, or a per-repository override). If a record is already linked, Pensar opens it instead of creating a duplicate.

Choosing the Table

Pensar files into one table per repository, with a workspace default plus optional per-repository overrides — the same model as Linear teams.

The picker offers the filing tables your instance actually has:

TableWhen it appears
incidentPresent on every standard instance
sn_si_incidentOnly when the Security Incident Response plugin is installed

Pensar reads this from the instance rather than offering both blind, so an instance without Security Incident Response will only show Incident.

Default Table

  1. In the ServiceNow section, set the workspace-level Table
  2. This is the fallback Pensar files into for any repository without an override, and for findings that aren’t tied to a specific repository

Per-Repository Overrides

Each repository can override the default routing:

SettingDescription
Sync to ServiceNowToggle whether findings from this repository are filed as ServiceNow records
TableThe ServiceNow table findings from this repository are filed in (falls back to the default table)
Severity FilterThe minimum severity required before a finding is filed

Select a table before enabling sync for a repository. Repositories without an override inherit the workspace default.

Because the table is part of the routing, two repositories can file into different tables — and Pensar remembers which table each record lives on, so status sync keeps working after you change the routing.

Status Mapping

incident and sn_si_incident number their state field differently, and most instances customise both. A hardcoded number would file into the wrong state silently, so Pensar does not use one. Instead it reads the active state choices for the table out of your instance and matches them on their label:

Pensar statusMatches a state label containing (case-insensitive)
False positivecancel, false positive, not reproducible, duplicate, not applicable
Closedclosed, resolved, complete
In reviewprogress, analysis, review, assess, work
Opennew, draft, open

The order matters, and it is the order in the table: the dismissal patterns are tried first, so a state labelled Closed - Duplicate reads as a false positive rather than a fix. The first pattern that matches wins.

The same matcher runs in both directions, so an outbound transition and an inbound webhook can never disagree about what a label means.

If no active state on your table matches — a fully renamed state list, for example — Pensar logs a warning and leaves the record alone. It will never guess a numeric state. If a Pensar status isn’t reaching ServiceNow, check that your table has an active state whose label matches one of the patterns above, and rename or add one if not.

Closure Fields

When Pensar closes a record, it also writes close_code and close_notes — but only on tables that declare those columns, checked against the table and its parents before writing:

Pensar statusclose_codeclose_notes
ClosedSolved (Permanently)Closed in Pensar: the finding was remediated.
False positiveNot Solved (Not Reproducible)Closed in Pensar: the finding was marked a false positive.

Two-Way Status Sync

Outbound sync (Pensar → ServiceNow) needs no setup: closing or reopening a finding in Pensar transitions the linked record.

Inbound sync (ServiceNow → Pensar) does. ServiceNow has no application-level webhook Pensar can subscribe to, so your instance has to tell Pensar when a state changes. That takes four steps: copy the endpoint and secret out of Pensar, store them as system properties, add a Script Include that signs the payload, and add a Business Rule that sends it.

Make sure Reflect ServiceNow status changes is enabled in the ServiceNow section of the Issue sync card — that toggle is what allows an inbound change to move a Pensar finding.

1. Copy the webhook URL and signing secret

The ServiceNow section of the Issue sync card shows both. The URL is specific to your workspace and looks like:

https://api.pensar.dev/webhooks/servicenow/<your-workspace-id>

The signing secret is generated when you connect. Treat it like a password.

2. Store them as system properties

Keeping both values in sys_properties means rotating the secret is a one-field edit rather than a script change.

  1. In ServiceNow, navigate to All → System Properties → All Properties and click New
  2. Create:
NameTypeValue
pensar.webhook.urlstringThe webhook URL from step 1
pensar.webhook.secretstringThe signing secret from step 1
  1. On pensar.webhook.secret, set Read roles to admin and check Private so the value isn’t captured in update sets

Paste both values with no leading or trailing whitespace. A stray newline in the secret changes the signature and every delivery will be rejected.

3. Add the signing Script Include

Pensar authenticates the webhook with an HMAC-SHA256 signature over the raw request body, sent as a hex digest. ServiceNow’s built-in GlideDigest produces plain hashes rather than HMACs, and Packages.javax.crypto is blocked by the script sandbox on current releases, so the signature is computed in script.

Create a Script Include:

  • Name: PensarSignature
  • Application: Global
  • Accessible from: All application scopes
  • Client callable: unchecked
  • Active: checked
var PensarSignature = Class.create();
PensarSignature.prototype = {
initialize: function() {},
/** Hex HMAC-SHA256 of `message` keyed with `secret`. */
hmacSha256Hex: function(secret, message) {
var key = this._utf8(secret),
ipad = [],
opad = [],
i;
if (key.length > 64) key = this._sha256(key);
while (key.length < 64) key.push(0);
for (i = 0; i < 64; i++) {
ipad.push(key[i] ^ 0x36);
opad.push(key[i] ^ 0x5c);
}
var inner = this._sha256(ipad.concat(this._utf8(message)));
return this._hex(this._sha256(opad.concat(inner)));
},
_K: [
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
],
_rotr: function(x, n) {
return ((x >>> n) | (x << (32 - n))) >>> 0;
},
/** UTF-8 encode a string to a byte array. */
_utf8: function(str) {
var out = [], i, c, c2;
for (i = 0; i < str.length; i++) {
c = str.charCodeAt(i);
if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
c2 = str.charCodeAt(i + 1);
if (c2 >= 0xdc00 && c2 <= 0xdfff) {
c = 0x10000 + ((c - 0xd800) << 10) + (c2 - 0xdc00);
i++;
}
}
if (c >= 0xd800 && c <= 0xdfff) c = 0xfffd;
if (c < 0x80) out.push(c);
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
else out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
}
return out;
},
_sha256: function(bytes) {
var H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
];
var m = bytes.slice(), bitLen = bytes.length * 8, w = [], i, t;
m.push(0x80);
while (m.length % 64 !== 56) m.push(0);
m.push(0, 0, 0, 0,
(bitLen >>> 24) & 0xff, (bitLen >>> 16) & 0xff,
(bitLen >>> 8) & 0xff, bitLen & 0xff);
for (i = 0; i < m.length; i += 64) {
for (t = 0; t < 16; t++) {
w[t] = ((m[i + t * 4] << 24) | (m[i + t * 4 + 1] << 16) |
(m[i + t * 4 + 2] << 8) | m[i + t * 4 + 3]) >>> 0;
}
for (t = 16; t < 64; t++) {
var s0 = (this._rotr(w[t - 15], 7) ^ this._rotr(w[t - 15], 18) ^ (w[t - 15] >>> 3)) >>> 0;
var s1 = (this._rotr(w[t - 2], 17) ^ this._rotr(w[t - 2], 19) ^ (w[t - 2] >>> 10)) >>> 0;
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) >>> 0;
}
var a = H[0], b = H[1], c = H[2], d = H[3],
e = H[4], f = H[5], g = H[6], h = H[7];
for (t = 0; t < 64; t++) {
var S1 = (this._rotr(e, 6) ^ this._rotr(e, 11) ^ this._rotr(e, 25)) >>> 0;
var ch = ((e & f) ^ (~e & g)) >>> 0;
var t1 = (h + S1 + ch + this._K[t] + w[t]) >>> 0;
var S0 = (this._rotr(a, 2) ^ this._rotr(a, 13) ^ this._rotr(a, 22)) >>> 0;
var mj = ((a & b) ^ (a & c) ^ (b & c)) >>> 0;
var t2 = (S0 + mj) >>> 0;
h = g; g = f; f = e; e = (d + t1) >>> 0;
d = c; c = b; b = a; a = (t1 + t2) >>> 0;
}
H[0] = (H[0] + a) >>> 0; H[1] = (H[1] + b) >>> 0;
H[2] = (H[2] + c) >>> 0; H[3] = (H[3] + d) >>> 0;
H[4] = (H[4] + e) >>> 0; H[5] = (H[5] + f) >>> 0;
H[6] = (H[6] + g) >>> 0; H[7] = (H[7] + h) >>> 0;
}
var out = [];
for (i = 0; i < 8; i++) {
out.push((H[i] >>> 24) & 0xff, (H[i] >>> 16) & 0xff,
(H[i] >>> 8) & 0xff, H[i] & 0xff);
}
return out;
},
_hex: function(bytes) {
var s = '', i;
for (i = 0; i < bytes.length; i++) {
s += (bytes[i] < 16 ? '0' : '') + bytes[i].toString(16);
}
return s;
},
type: 'PensarSignature'
};

4. Add the Business Rule

Create one Business Rule per table you file into:

  • Name: Pensar — notify on state change
  • Table: the table Pensar files into (Incident [incident] or Security Incident [sn_si_incident])
  • Advanced: checked
  • When to run → When: after, with Update checked
  • When to run → Advanced → Condition: current.state.changes()
  • Active: checked
(function executeRule(current, previous /*null when async*/ ) {
var endpoint = gs.getProperty('pensar.webhook.url');
var secret = gs.getProperty('pensar.webhook.secret');
if (!endpoint || !secret) {
gs.error('Pensar: pensar.webhook.url / pensar.webhook.secret are not set; skipping.');
return;
}
// The signature covers this exact string, so sign it and send it unmodified.
var body = JSON.stringify({
table: current.getTableName(),
sys_id: current.getUniqueValue(),
state: current.getValue('state'),
updated_by: current.getValue('sys_updated_by') || gs.getUserName()
});
var signature = new PensarSignature().hmacSha256Hex(secret, body);
var request = new sn_ws.RESTMessageV2();
request.setEndpoint(endpoint);
request.setHttpMethod('post');
request.setRequestHeader('Content-Type', 'application/json');
request.setRequestHeader('x-pensar-signature', 'sha256=' + signature);
request.setRequestBody(body);
try {
var response = request.execute();
var status = response.getStatusCode();
if (status >= 300) {
gs.error('Pensar webhook returned HTTP ' + status + ': ' + response.getBody());
}
} catch (err) {
gs.error('Pensar webhook call failed: ' + err);
}
})(current, previous);

Setting When to after makes the user’s save wait on the HTTP round trip. If you’d rather keep the network call off the transaction, change When to async — the script above is unchanged, and previous is simply unused. If you create the Business Rule inside a scoped application rather than Global, call the Script Include as new global.PensarSignature().

Pensar looks the incoming state value up in your table’s own choice list, resolves its label, and runs it through the status mapping above. A change that resolves to the status the finding already has is a no-op, so Pensar’s own outbound transitions don’t bounce back as inbound ones.

Rotating the Secret

If you regenerate the signing secret in Pensar, every delivery signed with the old one is rejected. Update pensar.webhook.secret in ServiceNow to the new value at the same time — there is nothing to change in the Script Include or the Business Rule.

Disconnecting ServiceNow

To remove the ServiceNow integration:

  1. Navigate to Settings → Integrations → Issue tracking
  2. Find the ServiceNow row in the Issue sync card and click Manage
  3. Click Disconnect

Disconnecting clears Pensar’s stored credential and webhook secret and stops both directions of sync. It does not reach into your instance: existing records are left in place, and you should also deactivate the Business Rule, and revoke the OAuth token (System OAuth → Manage Tokens) or deactivate the service account, on the ServiceNow side.

Troubleshooting

Authentication Failed (401)

OAuth. The access token expired and could not be refreshed. Common causes:

  1. The Refresh Token Lifespan on the Application Registry record elapsed — reconnect from Pensar
  2. The client secret was regenerated in ServiceNow — reconnect with the new secret
  3. The token was revoked under System OAuth → Manage Tokens
  4. The approving user was deactivated or lost the roles Pensar needs

Basic authentication. The credential no longer works. Check that the password hasn’t been changed or expired, that the account isn’t locked out, and that MFA isn’t being enforced for it. Reconnect with the current password.

Either. Confirm the instance URL is still correct and that the instance isn’t hibernating — a paused developer instance rejects everything until it’s woken up.

No Table in the Picker

Pensar lists only the filing tables it can see on your instance.

  1. If Security Incident is missing, the Security Incident Response plugin isn’t installed — file into incident instead
  2. If the picker is empty, the connected identity can’t read sys_db_object. Grant it read access, or connect as an account that has it
  3. Reconnect after changing roles — the picker reads the instance with the credential you connected with

Issues Not Being Created

  1. Confirm a workspace-level Table is set, or that the repository has a table override
  2. Check that Sync to ServiceNow is enabled for the repository
  3. Verify the finding’s severity meets the repository’s Severity Filter
  4. Confirm the connected identity can insert on the target table (itil for incident)

Status Changes Not Reflecting

If a state change in ServiceNow doesn’t move the Pensar finding, work down this list:

  1. Reflect ServiceNow status changes is turned off in the ServiceNow section — inbound sync is disabled while it is
  2. Signature mismatch. The webhook rejects any body whose x-pensar-signature doesn’t verify. Check that pensar.webhook.secret matches the secret currently shown in Pensar (rotating one without the other is the usual cause), that neither value has stray whitespace, and that nothing in your Business Rule modifies body between signing and setRequestBody
  3. No matching state label. If the new state’s label matches none of the patterns in Status Mapping, Pensar logs it and deliberately leaves the finding alone. Rename the state, or add one whose label matches
  4. The Business Rule didn’t fire. Its condition is current.state.changes(), so an update that doesn’t change state sends nothing. Confirm the rule is Active, is on the right table, and check System Logs → Outbound HTTP Requests for the delivery
  5. The record wasn’t filed by Pensar. Status sync only applies to records Pensar created and linked to a finding
  6. Outbound HTTP is blocked. If your instance restricts egress, the REST message never leaves — the gs.error lines in the Business Rule will show it in System Logs → All

If the reverse direction is the problem — a finding closed in Pensar leaving the record open — see the note in Status Mapping about tables whose state labels match nothing.

Need Help?

If you encounter issues setting up your ServiceNow integration, please contact our support team at team@pensar.dev.