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.
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)| 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) |
- Navigate to All > System Definition > Tables.
- Click New.
- Set:
| Field | Value |
|---|---|
| Name | u_avr_staging |
| Label | AVR Staging |
| Extends table | Import Set Row (sys_import_set_row) |
| Create access controls | checked |
- Click Submit.
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 |
- Navigate to All > System Import Sets > Administration > Data Sources.
- Click New.
- Set:
| Field | Value |
|---|---|
| Name | AVR REST Import |
| Import set table name | u_avr_staging |
| Import set table label | AVR Staging |
| Active | true |
- Click Submit.
This ensures POST .../insertMultiple maps JSON keys to column element names (u_*), not labels.
- Navigate to All > System Definition > Tables.
- Search for table
sys_rest_insert_multiple. - Open that table list.
- Click New.
- Set:
| Field | Value |
|---|---|
| Name | AVR insertMultiple |
| Source table | u_avr_staging |
| Transformation / Mode | Synchronous |
| Active | checked |
- Click Submit.
Stay on the record you just created.
- Scroll to related list Column Mapping (or Column Mappings).
- Click New.
- 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.
- Click Submit.
You only need one Column Mapping row with Column name for the entire staging table.
- Navigate to All > System Import Sets > Administration > Transform Maps.
- Click New.
- 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 |
- Click Submit.
Leave the Field Maps related list empty. All logic runs in the onBefore script via GlideRecord.
- Open AVR Findings Import.
- Go to Transform Scripts related list (or Transformation script tab) and click New.
- Set:
| Field | Value |
|---|---|
| When | onBefore |
| Order | 100 |
| Active | checked |
- Paste the script below into Script.
- Click Submit.
(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.
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 sourceIf your instance uses source_application_id instead, replace source_app_id with source_application_id in upsertAppRelease (query + setValue).
- Navigate to All > Application Vulnerability Response > Administration > Normalized Severity Maps.
- Click New (or duplicate an existing scanner map).
- Set Source to match the
u_sourcevalues Synqly sends (defaultSynqly AVR). If staging rows omitu_source, the transform script falls back toAVR Integration—configure a map for whichever source name your rows use. - 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.
| Symptom | Likely cause |
|---|---|
| 401 on import POST | API 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 POST | REST Insert Multiple Column Mapping uses Label instead of Column name — see section 3.1 |
| 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) |