Skip to content

ServiceNow Application Vulnerability Findings Import Setup Guide

This guide configures your ServiceNow instance to receive application vulnerability findings via the Import Set REST API. Findings are first posted to a staging table where a transform onBefore script (described below) creates or updates Discovered Applications, Application Vulnerability Entries, and Application Vulnerable Items (AVITs).

Complete the ServiceNow Application Vulnerability Response Configuration Guide first. You need a service account, matching role assignments, and credentials in Synqly.

REST API key (API Key tab): Also create REST API Access Policies for Table API in the main guide and Import Set API (step 6).

Basic credentials: Assign the same roles on the integration user. REST API Access Policies are not used for basic auth—access is enforced through roles and ACLs.

Overview

create_findings
   ↓  POST /api/now/import/u_avr_staging/insertMultiple
Staging table (u_avr_staging)
   ↓  Transform map (onBefore script only — no Field Maps)
   ├── sn_vul_app_release           (Discovered Application)
   ├── sn_vul_app_vul_entry         (Application Vulnerability Entry)
   └── sn_vul_app_vulnerable_item   (AVIT / finding)

Upsert keys

TableNatural key
sn_vul_app_releasesource_app_id (any source — reuses existing scanner rows)
sn_vul_app_vul_entryid (global — reuses GH-GHSA/CVE rows from GitHub etc.)
sn_vul_app_vulnerable_item(application_release, vulnerability, source_avit_id)

1. Create staging table and columns

1.1 Create the table

  1. Navigate to All > System Definition > Tables.
  2. Click New.
  3. Set:
FieldValue
Nameu_avr_staging
LabelAVR Staging
Extends tableImport Set Row (sys_import_set_row)
Create access controlschecked
  1. Click Submit.

1.2 Add columns

Open the new table, go to the Columns related list, and click New for each column below.

All columns are type String unless noted.

LabelColumn nameMax lengthPurpose
Sourceu_source100Integration name (Synqly default: Synqly AVR)
Source application IDu_source_application_id255Required — external app id (e.g. GitHub repo id)
Application nameu_application_name255Display name (e.g. org/repo) — maps to release name
Application descriptionu_application_description4000Optional app/repo metadata
CMDB CIu_cmdb_ci32Optional existing CI sys_id (32-char hex)
Business applicationu_business_application255Optional name to lookup cmdb_ci_business_app
Vulnerability IDu_vulnerability_id255Required — GH-GHSA, CVE, QID, etc.
Vulnerability nameu_vulnerability_name255Entry title
Vulnerability summaryu_vulnerability_summary4000Entry / finding description
Primary CWEu_primary_cwe40Optional, e.g. CWE-468
Source severityu_source_severity40Scanner severity (high, 4, etc.)
Source AVIT IDu_source_avit_id255Required — unique finding id from scanner
Scan typeu_scan_type40e.g. Static, Dynamic, SCA
Locationu_location500File:line, URL, path
Source linku_source_link1024Deep link to finding
Source finding statusu_source_finding_status40e.g. open, fixed, dismissed
Stateu_state40AVIT state override (e.g. 1 = Open)
First foundu_first_found_dt40YYYY-MM-DD or YYYY-MM-DD HH:MM:SS
Last foundu_last_found_dt40YYYY-MM-DD or YYYY-MM-DD HH:MM:SS

2. Create data source

  1. Navigate to All > System Import Sets > Administration > Data Sources.
  2. Click New.
  3. Set:
FieldValue
NameAVR REST Import
Import set table nameu_avr_staging
Import set table labelAVR Staging
Activetrue
  1. Click Submit.

3. Configure REST Insert Multiple (required for batch API)

This ensures POST .../insertMultiple maps JSON keys to column element names (u_*), not labels.

  1. Navigate to All > System Definition > Tables.
  2. Search for table sys_rest_insert_multiple.
  3. Open that table list.
  4. Click New.
  5. Set:
FieldValue
NameAVR insertMultiple
Source tableu_avr_staging
Transformation / ModeSynchronous
Activechecked
  1. Click Submit.

Stay on the record you just created.

  1. Scroll to related list Column Mapping (or Column Mappings).
  2. Click New.
  3. Set:
FieldValue
Column mappingColumn name

Do not use Label — the default Label mapping causes empty staging rows when JSON uses u_source_application_id, u_vulnerability_id, and other element names.

  1. Click Submit.

You only need one Column Mapping row with Column name for the entire staging table.

4. Create transform map (no Field Maps)

4.1 Create the map

  1. Navigate to All > System Import Sets > Administration > Transform Maps.
  2. Click New.
  3. Set:
FieldValue
NameAVR Findings Import
Source tableu_avr_staging
Target tableApplication Vulnerable Item (sn_vul_app_vulnerable_item)
Activechecked
Run business ruleschecked
  1. Click Submit.

4.2 Do not add Field Maps

Leave the Field Maps related list empty. All logic runs in the onBefore script via GlideRecord.

4.3 Add onBefore transform script

  1. Open AVR Findings Import.
  2. Go to Transform Scripts related list (or Transformation script tab) and click New.
  3. Set:
FieldValue
WhenonBefore
Order100
Activechecked
  1. Paste the script below into Script.
  2. Click Submit.

onBefore script

(function runTransformScript(source, map, log, target) {

    var DEFAULT_SOURCE = 'AVR Integration';
    var APP_RELEASE_TABLE = 'sn_vul_app_release';
    var APP_VUL_ENTRY_TABLE = 'sn_vul_app_vul_entry';
    var AVIT_TABLE = 'sn_vul_app_vulnerable_item';
    var BUSINESS_APP_TABLE = 'cmdb_ci_business_app';

    function str(field) {
        if (field == null || field === '')
            return '';
        return field.toString().trim();
    }

    function isSysId(value) {
        return /^[a-f0-9]{32}$/i.test(str(value));
    }

    function parseDateParts(dtStr) {
        dtStr = str(dtStr);
        if (!dtStr)
            return { dtTm: '', date: '' };
        // Accept YYYY-MM-DD or YYYY-MM-DD HH:MM:SS
        if (dtStr.length >= 19)
            return { dtTm: dtStr.substr(0, 19), date: dtStr.substr(0, 10) };
        if (dtStr.length >= 10)
            return { dtTm: dtStr.substr(0, 10) + ' 00:00:00', date: dtStr.substr(0, 10) };
        return { dtTm: '', date: '' };
    }

    function resolveBusinessAppSysId(nameOrSysId) {
        var raw = str(nameOrSysId);
        if (!raw)
            return '';

        if (isSysId(raw))
            return raw.toLowerCase();

        var ba = new GlideRecord(BUSINESS_APP_TABLE);
        ba.addQuery('name', raw);
        ba.setLimit(1);
        ba.query();
        if (ba.next())
            return ba.getValue('sys_id');

        log.warn('Business application not found by name: ' + raw);
        return '';
    }

    function upsertAppRelease(sourceName, sourceAppId, appName, appDesc, cmdbCi, businessAppSysId, log) {
        var rel = new GlideRecord(APP_RELEASE_TABLE);

        // Reuse by source_app_id (any source) — avoids duplicate GitHub + integration rows
        if (sourceAppId) {
            rel.addQuery('source_app_id', sourceAppId);
            rel.orderByDesc('sys_updated_on');
            rel.setLimit(1);
            rel.query();
        }

        var isNew = !rel.next();
        if (isNew) {
            rel.initialize();
            rel.source = sourceName;
        }

        // Always set keys + display (insert AND update)
        if (sourceAppId && rel.isValidField('source_app_id'))
            rel.setValue('source_app_id', sourceAppId);

        var displayName = appName || appDesc || ('App ' + sourceAppId);
        if (rel.isValidField('name'))
            rel.setValue('name', displayName);

        if (appDesc && rel.isValidField('description'))
            rel.description = appDesc;

        var ciSysId = isSysId(cmdbCi) ? cmdbCi : businessAppSysId;
        if (isSysId(ciSysId)) {
            if (rel.isValidField('configuration_item'))
                rel.configuration_item = ciSysId;
            if (rel.isValidField('business_application') && !rel.business_application)
                rel.business_application = ciSysId;
        } else if (isSysId(businessAppSysId) && rel.isValidField('business_application'))
            rel.business_application = businessAppSysId;

        if (isNew && !rel.source)
            rel.source = sourceName;

        var ok = isNew ? rel.insert() : rel.update();
        var sysId = rel.getValue('sys_id');

        if (!isSysId(sysId) || ok === false || ok === null) {
            log.error('Failed to upsert app release: ' + rel.getLastErrorMessage());
            return '';
        }
        return sysId;
    }

    function upsertAppVulEntry(sourceName, vulnId, vulnName, vulnSummary, sourceSeverity, primaryCwe, log) {
        var entry = new GlideRecord(APP_VUL_ENTRY_TABLE);

        // Reuse by id globally (GH-GHSA/CVE already imported by GitHub, etc.)
        entry.addQuery('id', vulnId);
        entry.setLimit(1);
        entry.query();

        var isNew = !entry.next();
        if (!isNew) {
            var changed = false;
            if (vulnSummary && entry.isValidField('summary') && entry.summary != vulnSummary) {
                entry.summary = vulnSummary;
                changed = true;
            }
            if (changed) {
                if (!entry.update()) {
                    log.error('Failed to update app vul entry id=' + vulnId + ': ' + entry.getLastErrorMessage());
                    return '';
                }
            }
            return entry.getValue('sys_id');
        }

        // Create only when id does not exist anywhere
        entry.initialize();
        entry.source = sourceName;
        entry.id = vulnId;
        entry.name = vulnName || vulnId;

        if (vulnSummary) {
            if (entry.isValidField('summary'))
                entry.summary = vulnSummary;
            else if (entry.isValidField('short_description'))
                entry.short_description = vulnSummary;
        }

        if (sourceSeverity && entry.isValidField('source_severity'))
            entry.source_severity = sourceSeverity;

        if (primaryCwe && entry.isValidField('primary_cwe')) {
            var cweId = primaryCwe.replace(/^CWE-/i, 'CWE-');
            if (!/^CWE-/i.test(cweId))
                cweId = 'CWE-' + cweId.replace(/^CWE-/i, '');
            var cweGr = new GlideRecord('sn_vul_cwe');
            if (cweGr.isValid()) {
                cweGr.addQuery('id', cweId);
                cweGr.setLimit(1);
                cweGr.query();
                if (cweGr.next())
                    entry.primary_cwe = cweGr.getValue('sys_id');
            }
        }

        var sysId = entry.insert();
        if (!isSysId(sysId)) {
            log.error('Failed to insert app vul entry id=' + vulnId + ': ' + entry.getLastErrorMessage());
            return '';
        }
        return sysId;
    }

    function resolveAvitState(sourceState, sourceFindingStatus) {
        if (sourceState)
            return sourceState;

        var status = str(sourceFindingStatus).toLowerCase();
        if (!status)
            return '1'; // Open

        if (status === 'open' || status === 'new')
            return '1';
        if (status === 'fixed' || status === 'closed' || status === 'resolved')
            return '3';
        if (status === 'dismissed' || status === 'suppressed')
            return '5';

        return '1';
    }

    function populateAvitFields(avitGr, sourceName, appReleaseSysId, vulnEntrySysId, fields) {
        avitGr.application_release = appReleaseSysId;
        avitGr.vulnerability = vulnEntrySysId;
        avitGr.source = sourceName;

        if (fields.sourceAvitId && avitGr.isValidField('source_avit_id'))
            avitGr.source_avit_id = fields.sourceAvitId;

        if (fields.sourceSeverity && avitGr.isValidField('source_severity'))
            avitGr.source_severity = fields.sourceSeverity;

        if (fields.scanType && avitGr.isValidField('scan_type'))
            avitGr.scan_type = fields.scanType;

        if (fields.location && avitGr.isValidField('location'))
            avitGr.location = fields.location;

        if (fields.sourceLink && avitGr.isValidField('source_link'))
            avitGr.source_link = fields.sourceLink;

        if (fields.summary) {
            if (avitGr.isValidField('summary'))
                avitGr.summary = fields.summary;
            else if (avitGr.isValidField('short_description'))
                avitGr.short_description = fields.summary;
        }

        if (fields.sourceFindingStatus && avitGr.isValidField('source_remediation_status'))
            avitGr.source_remediation_status = fields.sourceFindingStatus;

        avitGr.state = resolveAvitState(fields.state, fields.sourceFindingStatus);

        var first = parseDateParts(fields.firstFound);
        if (first.dtTm) {
            if (avitGr.isValidField('first_found_dt_tm'))
                avitGr.first_found_dt_tm = first.dtTm;
            if (avitGr.isValidField('first_found'))
                avitGr.first_found = first.date;
        }

        var last = parseDateParts(fields.lastFound);
        if (last.dtTm) {
            if (avitGr.isValidField('last_found_dt_tm'))
                avitGr.last_found_dt_tm = last.dtTm;
            if (avitGr.isValidField('last_found'))
                avitGr.last_found = last.date;
        }

        if (isSysId(fields.cmdbCi) && avitGr.isValidField('cmdb_ci'))
            avitGr.cmdb_ci = fields.cmdbCi;
    }

    // --- Validate required fields ---
    var sourceName = str(source.u_source) || DEFAULT_SOURCE;
    var sourceAppId = str(source.u_source_application_id);
    var vulnId = str(source.u_vulnerability_id);
    var sourceAvitId = str(source.u_source_avit_id);

    if (!sourceAppId) {
        ignore = true;
        log.error('Missing u_source_application_id');
        return;
    }
    if (!vulnId) {
        ignore = true;
        log.error('Missing u_vulnerability_id');
        return;
    }
    if (!sourceAvitId) {
        ignore = true;
        log.error('Missing u_source_avit_id');
        return;
    }

    var appName = str(source.u_application_name);
    var appDesc = str(source.u_application_description);
    var cmdbCi = str(source.u_cmdb_ci);
    var businessAppInput = str(source.u_business_application) || appName;
    var businessAppSysId = resolveBusinessAppSysId(businessAppInput);

    if (isSysId(cmdbCi) && !businessAppSysId)
        businessAppSysId = cmdbCi;

    // --- 1) Upsert Discovered Application ---
    var appReleaseSysId = upsertAppRelease(
        sourceName,
        sourceAppId,
        appName,
        appDesc,
        cmdbCi,
        businessAppSysId,
        log
    );
    if (!isSysId(appReleaseSysId)) {
        ignore = true;
        return;
    }

    // --- 2) Upsert Application Vulnerability Entry ---
    var vulnEntrySysId = upsertAppVulEntry(
        sourceName,
        vulnId,
        str(source.u_vulnerability_name),
        str(source.u_vulnerability_summary),
        str(source.u_source_severity),
        str(source.u_primary_cwe),
        log
    );
    if (!isSysId(vulnEntrySysId)) {
        ignore = true;
        return;
    }

    var avitFields = {
        sourceAvitId: sourceAvitId,
        sourceSeverity: str(source.u_source_severity),
        scanType: str(source.u_scan_type),
        location: str(source.u_location),
        sourceLink: str(source.u_source_link),
        summary: str(source.u_vulnerability_summary) || str(source.u_vulnerability_name),
        sourceFindingStatus: str(source.u_source_finding_status),
        state: str(source.u_state),
        firstFound: str(source.u_first_found_dt),
        lastFound: str(source.u_last_found_dt),
        cmdbCi: isSysId(cmdbCi) ? cmdbCi : businessAppSysId
    };

    // --- 3) Upsert AVIT ---
    var avit = new GlideRecord(AVIT_TABLE);
    avit.addQuery('application_release', appReleaseSysId);
    avit.addQuery('vulnerability', vulnEntrySysId);
    avit.addQuery('source_avit_id', sourceAvitId);
    avit.orderByDesc('sys_updated_on');
    avit.setLimit(1);
    avit.query();

    if (avit.next()) {
        populateAvitFields(avit, sourceName, appReleaseSysId, vulnEntrySysId, avitFields);
        avit.update();
        log.info('Updated AVIT: ' + avit.getValue('number') + ' sys_id=' + avit.getValue('sys_id'));
        ignore = true;
        return;
    }

    // --- 4) New AVIT — populate target for transform map insert ---
    populateAvitFields(target, sourceName, appReleaseSysId, vulnEntrySysId, avitFields);

})(source, map, log, target);

Expected behavior: Updates to existing AVITs show staging row state Ignored (the script updated records via GlideRecord). New AVITs may show Inserted on the staging row. Verify success on the AVIT, app release, vulnerability entry, and Import Log—not only the staging row state.

Field name note (source_app_id)

The script writes source_app_id on sn_vul_app_release. The UI label may read Source application ID. Confirm the element name on your instance:

sys_dictionary.list → name = sn_vul_app_release → element contains source

If your instance uses source_application_id instead, replace source_app_id with source_application_id in upsertAppRelease (query + setValue).

5. Configure Normalized Severity Map

  1. Navigate to All > Application Vulnerability Response > Administration > Normalized Severity Maps.
  2. Click New (or duplicate an existing scanner map).
  3. Set Source to match the u_source values Synqly sends (default Synqly AVR). If staging rows omit u_source, the transform script falls back to AVR Integration—configure a map for whichever source name your rows use.
  4. Map severity values, for example:
Source severityNormalized
Critical / 51 - Critical
High / 42 - High
Medium / 33 - Medium
Low / 24 - Low
Informational / 15 - None

Adjust mappings to match the OCSF finding severity values your integration sends in u_source_severity.


Troubleshooting

SymptomLikely cause
401 on import POSTAPI key path: missing Import Set REST API Access Policy — see step 6. Basic auth: check integration user roles (import_transformer, AVR write roles) and ACLs on import and AVR tables
Empty staging rows after POSTREST Insert Multiple Column Mapping uses Label instead of Column name — see section 3.1
Missing app release, vuln entry, or AVITRequired fields missing on staging row (u_source_application_id, u_vulnerability_id, u_source_avit_id); check transform script log and Import Log
Staging row state Ignored on updateExpected when the script updates an existing AVIT via GlideRecord
Severity not normalizedNormalized Severity Map Source does not match u_source on staging rows (Synqly default: Synqly AVR)