# 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](/guides/provider-configuration/appsec-servicenow-setup) 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](/guides/provider-configuration/appsec-servicenow-setup#appsec-sn-token-step-6-import-set-policy)).

**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


```text
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

| Table | Natural key |
|  --- | --- |
| `sn_vul_app_release` | `source_app_id` (any source — reuses existing scanner rows) |
| `sn_vul_app_vul_entry` | `id` (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:


| Field | Value |
|  --- | --- |
| Name | `u_avr_staging` |
| Label | `AVR Staging` |
| Extends table | **Import Set Row** (`sys_import_set_row`) |
| Create access controls | checked |


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.

| Label | Column name | Max length | Purpose |
|  --- | --- | --- | --- |
| Source | `u_source` | 100 | Integration name (Synqly default: `Synqly AVR`) |
| Source application ID | `u_source_application_id` | 255 | **Required** — external app id (e.g. GitHub repo id) |
| Application name | `u_application_name` | 255 | Display name (e.g. `org/repo`) — maps to release `name` |
| Application description | `u_application_description` | 4000 | Optional app/repo metadata |
| CMDB CI | `u_cmdb_ci` | 32 | Optional existing CI sys_id (32-char hex) |
| Business application | `u_business_application` | 255 | Optional name to lookup `cmdb_ci_business_app` |
| Vulnerability ID | `u_vulnerability_id` | 255 | **Required** — GH-GHSA, CVE, QID, etc. |
| Vulnerability name | `u_vulnerability_name` | 255 | Entry title |
| Vulnerability summary | `u_vulnerability_summary` | 4000 | Entry / finding description |
| Primary CWE | `u_primary_cwe` | 40 | Optional, e.g. `CWE-468` |
| Source severity | `u_source_severity` | 40 | Scanner severity (`high`, `4`, etc.) |
| Source AVIT ID | `u_source_avit_id` | 255 | **Required** — unique finding id from scanner |
| Scan type | `u_scan_type` | 40 | e.g. `Static`, `Dynamic`, `SCA` |
| Location | `u_location` | 500 | File:line, URL, path |
| Source link | `u_source_link` | 1024 | Deep link to finding |
| Source finding status | `u_source_finding_status` | 40 | e.g. `open`, `fixed`, `dismissed` |
| State | `u_state` | 40 | AVIT state override (e.g. `1` = Open) |
| First found | `u_first_found_dt` | 40 | `YYYY-MM-DD` or `YYYY-MM-DD HH:MM:SS` |
| Last found | `u_last_found_dt` | 40 | `YYYY-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:


| Field | Value |
|  --- | --- |
| Name | `AVR REST Import` |
| Import set table name | `u_avr_staging` |
| Import set table label | `AVR Staging` |
| Active | true |


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:


| Field | Value |
|  --- | --- |
| Name | `AVR insertMultiple` |
| Source table | `u_avr_staging` |
| Transformation / Mode | **Synchronous** |
| Active | checked |


1. Click **Submit**.


### 3.1 Column mapping (related list)

Stay on the record you just created.

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


| Field | Value |
|  --- | --- |
| Column mapping | **Column 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:


| Field | Value |
|  --- | --- |
| Name | `AVR Findings Import` |
| Source table | `u_avr_staging` |
| Target table | **Application Vulnerable Item** (`sn_vul_app_vulnerable_item`) |
| Active | checked |
| Run business rules | checked |


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:


| Field | Value |
|  --- | --- |
| When | `onBefore` |
| Order | `100` |
| Active | checked |


1. Paste the script below into **Script**.
2. Click **Submit**.


#### onBefore script


```javascript
(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:


```text
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 severity | Normalized |
|  --- | --- |
| Critical / 5 | 1 - Critical |
| High / 4 | 2 - High |
| Medium / 3 | 3 - Medium |
| Low / 2 | 4 - Low |
| Informational / 1 | 5 - None |


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

## Troubleshooting

| Symptom | Likely cause |
|  --- | --- |
| **401** on import POST | **API key path:** missing Import Set REST API Access Policy — see [step 6](/guides/provider-configuration/appsec-servicenow-setup#appsec-sn-token-step-6-import-set-policy). **Basic auth:** check integration user roles (`import_transformer`, AVR write roles) and ACLs on import and AVR tables |
| Empty staging rows after POST | REST Insert Multiple **Column Mapping** uses **Label** instead of **Column name** — see [section 3.1](#31-column-mapping-related-list) |
| Missing app release, vuln entry, or AVIT | Required 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 update | Expected when the script updates an existing AVIT via GlideRecord |
| Severity not normalized | Normalized Severity Map **Source** does not match `u_source` on staging rows (Synqly default: `Synqly AVR`) |