# ServiceNow

## Create ServiceNow Incident

{% embed url="<https://youtu.be/eV1qxzcJ2Do?si=Q8f1ZPwFGmclXdHh>" %}

The purpose of this example is to create a [ServiceNow Incident](https://www.servicenow.com/au/products/itsm/what-is-incident-management.html) when a Vulnerability is created in AttackForge, and to update AttackForge to assign the SNOW Incident Id against the Vulnerability.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_auth - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Create SNOW Incident**&#x20;

* **Method**: POST
* **URL**: https\://\<YOUR-SNOW>/api/now/table/incident
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
let afProjectId;

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    if (data.vulnerability_projects[i].id) {
      afProjectId = data.vulnerability_projects[i].id;
      break;
    }
  }
}

if (!afProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'afProjectId is undefined'
    }
  };
}

return {
  data: {
    af_project_id: afProjectId,
    af_vuln_id: data?.vulnerability_id
  },
  request: {
    body: buildRequestBody()
  }
};

function buildRequestBody() {
  const body = {};

  if (data.vulnerability_title) {
    body.short_description = '[SECURITY][VULNERABILITY] ' + data.vulnerability_title;
  }

  let priority = 3;

  if (data.vulnerability_priority === 'Critical') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 1;
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 2;
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 3;
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 3;
  }

  body.impact = priority;
  body.urgency = priority;
  body.priority = priority;
  body.severity = priority;

  let description = '';

  if (data.vulnerability_affected_asset_name) {
    description += 'Affected Asset: ' + data.vulnerability_affected_asset_name + '\n\n';
  }
  else if (data.vulnerability_affected_assets) {
    description += 'Affected Asset(s): ';

    for (let i = 0; i < data.vulnerability_affected_assets.length; i++) {
      if (data.vulnerability_affected_assets[i].asset?.name) {
        description += '\n* ' + data.vulnerability_affected_assets[i].asset.name;
      }
    }

    description += '\n\n';
  }

  if (data.vulnerability_description) {
    description += 'Description: \n' + data.vulnerability_description + '\n\n';
  }

  if (data.vulnerability_likelihood_of_exploitation) {
    description += 'Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '\n\n';
  }

  let cvssScore;
  let cvssVector;

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Base Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Base Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Temporal Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Temporal Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSSv3.1 Environmental Score: /) {
        cvssScore = String.replace(data.vulnerability_tags[i], 'CVSSv3.1 Environmental Score: ', '');
      }

      if (data.vulnerability_tags[i] =~ m/CVSS:3.1\//) {
        cvssVector = String.replace(data.vulnerability_tags[i], 'CVSS:3.1/', '');
      }
    }
  }

  if (cvssScore && cvssVector) {
    description += 'CVSS Score: ' + cvssScore + '\nCVSS Vector: ' + cvssVector + '\n\n';
  }

  if (data.vulnerability_attack_scenario) {
    description += 'Attack Scenario (Technical Risk): \n' + data.vulnerability_attack_scenario + '\n\n';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += 'Remediation Recommendation: \n' + data.vulnerability_remediation_recommendation + '\n\n';
  }

  if (data.vulnerability_steps_to_reproduce) {
    description += 'Steps to Reproduce: \n' + data.vulnerability_steps_to_reproduce + '\n\n';
  }

  if (data.vulnerability_notes && data.vulnerability_notes.length > 0) {
    description += 'Notes:';

    for (let i = 0; i < data.vulnerability_notes.length; i++) {
      if (data.vulnerability_notes[i].note) {
        description += '\n' + data.vulnerability_notes[i].note;
      }
    }
  }

  if (data.vulnerability_tags && data.vulnerability_tags.length > 0) {
    description += 'Tags:';

    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      description += '\n* ' + data.vulnerability_tags[i];
    }
  }

  body.description = description;

  return body;
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json;charset=UTF-8') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json;charset=UTF-8'
    }
  };
}

if (!body?.result?.sys_id) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'missing SNOW Incident SysId (body.result.sys_id)',
    },
  };
}
else if (!data?.af_project_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_vuln_id',
    },
  };
}
else {
  return {
    data: {
      af_project_id: data?.af_project_id,
      af_vuln_id: data?.af_vuln_id,
      snow_incident_number: body.result.number
    } 
  };
}
```

**Action 2 - Update AF Vuln with SNOW Incident Id**

* **Method**: PUT
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-Key; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (data.snow_incident_number && data.af_vuln_id && data.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
        project_id: data.af_project_id,
        custom_fields: [
          {
            key: 'snow_incident_number',
            value: data.snow_incident_number
          }
        ]
      }
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'missing required fields'
    }
  };
}
```

* **Response Script**:

```javascript
let body;

if (response.headers['Content-Type'] === 'application/json') {
  body = JSON.parse(response.body);
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json'
    }
  };
}

if (response.statusCode === 200 && body?.result?.result === 'Vulnerability Updated') {
  return {
    decision: 'finish'
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'vulnerability not updated'
    },
  };
}
```

## ServiceNow Incident Retest -> Update Vuln to Ready for Retest

The purpose of this example is when a ServiceNow Incident is assigned the 'Resolved' status - the matching vulnerability in AttackForge is assigned as retest.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FDerF491QGL734yY3xOUm%2Fsnow-retest.png?alt=media&#x26;token=648b6997-ed55-4154-af68-8f3d7a3ada1b" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **SNOW WebHooks**
  * Configure 'incident.updated' [web hook](https://medium.com/@sebasqui1995/creating-a-webhook-in-servicenow-a-step-by-step-guide-a8de37ca22f0) in your ServiceNow
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: Enabled
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)
  * x\_user\_key - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api).

**Action 1 - Get Vulnerability**&#x20;

* **Method**: GET
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerabilities>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (!data.jsonBody?.incidentId) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident id missing',
    }
  };
}

if (!data.jsonBody?.state) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW state missing',
    }
  };
}

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"snow_incident_number"},value:{$eq:"'+ data.jsonBody?.incidentId + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerabilities?' + query
  },
  data: {
    incident: data.jsonBody
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
) {
  const vuln = response.jsonBody.vulnerabilities[0];
  
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vuln: vuln,
      incident: data.incident
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Get SNOW Incident State**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state&value='+ data.incident.state;

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident state',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?[0]?.label) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident state'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      state: response.jsonBody.result[0]
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident state'
    }
  };
}
```

**Action 3 - Update Vulnerability**&#x20;

* **Method**: PUT
* **URL**: <https://demo.attackforge.dev/api/ss/vulnerability/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = x\_user\_key
* **Request Script**:

```javascript
if (data.state.label !== 'Resolved') {
  return {
    decision: {
      status: 'finish',
      message: 'SNOW incident status not set to "Resolved"',
    }
  };
}

if (data.vuln?.vulnerability_status !== 'Open' && data.vuln?.vulnerability_retest !== 'No') {
  return {
    decision: {
      status: 'finish',
      message: 'Vuln status not set to "Open"',
    }
  };
}

let AFVulnId;
if (data.vuln.vulnerability_id) {
  AFVulnId = data.vuln.vulnerability_id;
}

if (!AFVulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln id missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability status to "Retest"',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + AFVulnId,
    body: {
      status: 'Retest'
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.result === 'Vulnerability Updated') {
  return {
    decision: {
      status: 'finish',
      message: 'Updated vulnerability status'
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed updating vulnerability status'
    },
  };
}
```

## Close ServiceNow Incident

The purpose of this example is when a vulnerability is closed in AttackForge, the matching ServiceNow Incident is also closed.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FvG4jwF6wP0VKHfrvCv6O%2Fsnow-closed.png?alt=media&#x26;token=e8fe8455-8f24-4149-9fea-b664cc021f72" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Get SNOW Incident**

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/incident?sysparm\\_query=GOTOnumber={incidentId}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status !== 'Closed') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is not Closed',
      }
  };
}

let snowIncidentId;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'snow_incident_number' && customField.value) {
      snowIncidentId = customField.value;
      break;
    }
  }
}

if (!snowIncidentId) {
  return {
      decision: { 
        status: 'finish',
        message: 'SNOW incident id missing',
      }
  };
}

const query = 'sysparm_query=GOTOnumber=' + snowIncidentId;

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/incident?' + query
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?[0]?.sys_id) {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    incident: response.jsonBody.result[0]
  } 
};
```

**Action 2 - Get SNOW Incident States**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state';

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident states',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident states'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      states: response.jsonBody.result
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident states'
    }
  };
}
```

**Action 3 - Close SNOW Incident**

* **Method**: PUT
* **URL**: <https://dev310111.service-now.com/api/now/v1/table/incident/{sys\\_id}?sysparm\\_exclude\\_ref\\_link=true>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data?.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.states) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident states missing',
    }
  };
}

let stateId;
for (let x = 0; x < data.states.length; x++) {
  const state = data.states[x];

  if (state.label === 'Closed') {
    stateId = state.value;
  }
}

if (!stateId) {
  return {
    decision: {
      status: 'abort',
      message: '"Closed" state missing',
    }
  };
}

const path = '/api/now/v1/table/incident/' + data.incident.sys_id + '?';
const query = 'sysparm_exclude_ref_link=true';

let url = 'https://dev310111.service-now.com' + path + query;

return {
  request: {
    url: url,
    body: {
      state: stateId
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.sys_id) {
  return {
    decision: 'finish',
    message: 'Updated SNOW incident to "Closed"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident status update failed'
    },
  };
}
```

## Re-Open ServiceNow Incident

The purpose of this example is when a vulnerability is re-opened in AttackForge, the matching ServiceNow Incident is also re-opened.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FaZt2qVQGeODbKIh6ldaP%2Fsnow-re-opened.png?alt=media&#x26;token=a68c6a9c-ffae-4eec-a6cc-ea0353b72676" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_auth - your [SNOW API Key](https://www.servicenow.com/docs/bundle/yokohama-platform-security/page/integrate/authentication/concept/api-authentication.html)

**Action 1 - Get SNOW Incident**

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/incident?sysparm\\_query=GOTOnumber={incidentId}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (data.vulnerability_is_deleted === true) {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is deleted',
      }
  };
}

if (data.vulnerability_status === 'Closed' || data.vulnerability_retest === 'Yes') {
  return {
      decision: { 
        status: 'finish',
        message: 'Vulnerability is Retest or Closed',
      }
  };
};

let snowIncidentId;

if (data.vulnerability_custom_fields) {
  for (let x = 0; x < data.vulnerability_custom_fields.length; x++) {
    const customField = data.vulnerability_custom_fields[x];

    if (customField.key === 'snow_incident_number' && customField.value) {
      snowIncidentId = customField.value;
      break;
    }
  }
}

if (!snowIncidentId) {
  return {
      decision: { 
        status: 'finish',
        message: 'SNOW incident id missing',
      }
  };
}

const query = 'sysparm_query=GOTOnumber=' + snowIncidentId;

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/incident?' + query
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?[0]?.sys_id) {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: { 
      status: 'abort',
      message: 'Vuln missing',
    },
  };
}

return {
  data: {
    vuln: data.vuln,
    incident: response.jsonBody.result[0]
  } 
};
```

**Action 2 - Get SNOW Incident States**&#x20;

* **Method**: GET
* **URL**: <https://dev310111.service-now.com/api/now/table/sys\\_choice?sysparm\\_query=name=incident\\&element=state\\&value={state}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

const query = 'sysparm_query=name=incident&element=state';

return {
  decision: {
    status: 'continue',
    message: 'Fetching incident states',
  },
  request: {
    url: 'https://dev310111.service-now.com/api/now/table/sys_choice?' + query
  },
  data: {
    vuln: data.vuln,
    incident: data.incident
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result) { 
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved incident states'
    },
    data: {
      vuln: data.vuln,
      incident: data.incident,
      states: response.jsonBody.result
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving incident states'
    }
  };
}
```

**Action 3 - Re-Open SNOW Incident**

* **Method**: PUT
* **URL**: <https://dev310111.service-now.com/api/now/v1/table/incident/{sys\\_id}?sysparm\\_exclude\\_ref\\_link=true>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = snow\_auth
* **Request Script**:

```javascript
if (!data?.incident) {
  return {
    decision: {
      status: 'abort',
      message: 'Incident missing',
    }
  };
}

if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Vuln missing',
    }
  };
}

if (!data?.states) {
  return {
    decision: {
      status: 'abort',
      message: 'SNOW incident states missing',
    }
  };
}

let stateId;
for (let x = 0; x < data.states.length; x++) {
  const state = data.states[x];

  if (state.label === 'New') {
    stateId = state.value;
  }
}

if (!stateId) {
  return {
    decision: {
      status: 'abort',
      message: '"New" state missing',
    }
  };
}

const path = '/api/now/v1/table/incident/' + data.incident.sys_id + '?';
const query = 'sysparm_exclude_ref_link=true';

let url = 'https://dev310111.service-now.com' + path + query;

return {
  request: {
    url: url,
    body: {
      state: stateId
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.result?.sys_id) {
  return {
    decision: 'finish',
    message: 'Updated SNOW incident to "New"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'SNOW incident status update failed'
    },
  };
}
```

## Create VR Item In ServiceNow

The purpose of this example is when a vulnerability is created in AttackForge, a vulnerability is also created in ServiceNow Vulnerability Response (VR) module.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F8ETLiikkRhcjvIh0xWYV%2FVR1.png?alt=media&#x26;token=75b80dd9-faaf-4a36-8d13-fc558d3c2b9e" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Prerequisites:**

**Configure OAuth on ServiceNow**

1. Navigate to `Inbound Integrations`

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FgjSMzmrBJuXVQv03veSw%2FScreenshot%202026-01-15%20at%209.40.59%E2%80%AFpm.png?alt=media&#x26;token=1fd9b5c4-cca0-4c3c-908c-d553c42ddb87" alt=""><figcaption></figcaption></figure>

2. Click on `New Integration`. Select `Client Credentials Grant`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FpBngpVbQgstIbCcgyBdj%2FScreenshot%202026-01-15%20at%209.42.45%E2%80%AFpm.png?alt=media&#x26;token=e90c11a2-7b1a-4436-94e2-55cde51b38e1" alt=""><figcaption></figcaption></figure>

3. Configure the credentials as required. Copy the `Client Id` and `Client Secret`. These will be referred to in the secrets within the flow.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F4AqthoJBdU7kjxDUmjdd%2FScreenshot%202026-01-15%20at%209.45.45%E2%80%AFpm.png?alt=media&#x26;token=5d78678a-4340-48a6-9c31-08f950cb214b" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FtpyrBi05DsmtSu22r5wT%2FScreenshot%202026-01-15%20at%203.30.05%E2%80%AFpm.png?alt=media&#x26;token=709cca7f-9283-4d5f-8207-042cfb762bc3" alt="" width="375"><figcaption></figcaption></figure>

2. Click on `New`. Enter a name e.g. AttackForge. Select `vulnerability_integration_svc` in Default ACLs.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F176e9WmDz0LR4pz9eeQN%2FScreenshot%202026-01-15%20at%203.36.07%E2%80%AFpm.png?alt=media&#x26;token=616bb29f-a6ab-43c9-9395-fb41e53c1d32" alt=""><figcaption></figcaption></figure>

3. Click `Submit`. Click `New`.&#x20;

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FPALwTy5dqC7uSZjDMOZR%2FScreenshot%202026-01-15%20at%203.37.28%E2%80%AFpm.png?alt=media&#x26;token=bc44589a-fd63-46d8-8afe-568f827250c2" alt=""><figcaption></figcaption></figure>

4. Enter `Create Vulnerable Item` in Name. Select `POST` for HTTP method. Enter `/create_vulnerable_item` in Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fj7pvJi1namt7y3U8oozc%2FScreenshot%202026-01-15%20at%203.38.24%E2%80%AFpm.png?alt=media&#x26;token=2ceb02c1-dc1f-42bc-9b3c-bdf619c664e0" alt=""><figcaption></figcaption></figure>

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fb5fKS5DY1giVVK8KKwvm%2FScreenshot%202026-01-15%20at%203.41.54%E2%80%AFpm.png?alt=media&#x26;token=ddde315d-0894-452a-99a8-19f30165f6dc" alt=""><figcaption></figcaption></figure>

```javascript
/*
 * Tables:
 * - sn_vul_third_party_entry: Stores vulnerability definitions from external sources
 * - sn_vul_vulnerable_item: Stores instances of vulnerabilities
 * - sn_vul_cwe: CWE (Common Weakness Enumeration) reference table
 */

(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
 let request_body;
 try {
   request_body = JSON.parse(request.body.dataString);
 } catch (error) {
   response.setStatus(400);
   response.setBody({ error: "Invalid JSON payload" });
   return;
 }

 const vul_table = "sn_vul_third_party_entry";
 const vul_item_table = "sn_vul_vulnerable_item";
 const vul_source = "AttackForge";
 const vul_entry_id = "AF-" + request_body.report_id.toString();

 function mapCVSSAttackVector(letter) {
   const mappings = {
     N: "NETWORK",
     A: "ADJACENT",
     L: "LOCAL",
     P: "PHYSICAL",
   };
   return mappings[letter] || "";
 }

 function mapCVSSScore(letter) {
   const mappings = {
     N: "NONE",
     L: "LOW",
     H: "HIGH",
     R: "REQUIRED",
     U: "UNCHANGED",
     C: "CHANGED",
   };
   return mappings[letter] || "";
 }

 let vul_entry = new GlideRecord(vul_table);
 let cwe_entry = new GlideRecord("sn_vul_cwe");
 const cvss_components = [
   request_body.cvss_calculation_method,
   `AV:${request_body.cvss_attack_vector}`,
   `AC:${request_body.cvss_attack_complexity}`,
   `PR:${request_body.cvss_privileges_required}`,
   `UI:${request_body.cvss_user_interaction}`,
   `S:${request_body.cvss_scope}`,
   `C:${request_body.cvss_confidentiality}`,
   `I:${request_body.cvss_integrity}`,
   `A:${request_body.cvss_availability}`,
 ];

 const cvss_vector_string = cvss_components.join("/");
 if (!vul_entry.get("id", vul_entry_id)) {
   vul_entry.initialize();
   vul_entry.setValue("id", vul_entry_id);
   vul_entry.setValue("source_severity", parseInt(request_body.severity_number));
   vul_entry.setValue("source", vul_source);
   vul_entry.setValue("summary", request_body.details);
   vul_entry.setValue("name", request_body.title);
   if (request_body.cvss_calculation_method.includes("CVSS:3.1") || request_body.cvss_calculation_method.includes("CVSS:3.0")) {
     vul_entry.setValue("v3_attack_vector", mapCVSSAttackVector(request_body.cvss_attack_vector));
     vul_entry.setValue("v3_attack_complexity", mapCVSSScore(request_body.cvss_attack_complexity));
     vul_entry.setValue("v3_privileges_required", mapCVSSScore(request_body.cvss_privileges_required));
     vul_entry.setValue("v3_user_interaction", mapCVSSScore(request_body.cvss_user_interaction));
     vul_entry.setValue("v3_scope_change", mapCVSSScore(request_body.cvss_scope));
     vul_entry.setValue("v3_confidentiality_impact", mapCVSSScore(request_body.cvss_confidentiality));
     vul_entry.setValue("v3_integrity_impact", mapCVSSScore(request_body.cvss_integrity));
     vul_entry.setValue("v3_availability_impact", mapCVSSScore(request_body.cvss_availability));
     vul_entry.setValue("v3_base_score", request_body.cvss_score);
     vul_entry.setValue("v3_vector_string", cvss_vector_string);
   }
   if (request_body.cwe && cwe_entry.get("cwe_id", request_body.cwe)) {
     vul_entry.setValue("cwe_id", cwe_entry.sys_id);
   }
   vul_entry.insert();
 }

 let vul_item = new GlideRecord(vul_item_table);
 vul_item.initialize();

 if (!vul_item.get("external_id", request_body.report_id.toString())) {
   vul_item.source = vul_source;
   vul_item.setValue("vulnerability", vul_entry.sys_id);
   vul_item.external_id = request_body.report_id.toString();
   vul_item.insert();
 }
 
 response.setBody({
   table_name: vul_item_table,
   sys_id: vul_item.sys_id || false,
   external_id: vul_item.number || false,
   link: vul_item.sys_id ? gs.getProperty("glide.servlet.uri") + vul_item.getLink() : false,
 });
})(request, response);
```

**Configure Severity Map**

1. Navigate to `Normalized Severity Maps`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FSA3frEA9ZRTbTsuaXpBf%2FScreenshot%202026-01-15%20at%208.21.19%E2%80%AFpm.png?alt=media&#x26;token=c30527d1-473b-4992-9c58-e827ab4348ff" alt=""><figcaption></figcaption></figure>

2. Click `New`. Enter the following severity maps. Ensure that the `Source`, `Source Value` and `Target Value` below matches exactly.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FNzdb8IfLzxRjoGMIsAPe%2FScreenshot%202026-01-15%20at%208.22.07%E2%80%AFpm.png?alt=media&#x26;token=e0bf5347-349b-4a69-b4d1-cc42a5b05bb9" alt=""><figcaption></figcaption></figure>

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Created
* **Secrets**:
  * af\_tenant - your AttackForge hostname e.g. demo.attackforge.com
  * af\_token - your AttackForge user API key
  * snow\_client\_id - your ServiceNow Client Id (see Prerequisites above)
  * snow\_client\_secret - your ServiceNow Client Id (see Prerequisites above)
  * snow\_hostname - your ServiceNow hostname e.g. company.service-now\.com
  * snow\_resource\_path - your ServiceNow Scripted REST API route (see Prerequisites above)

**Action 1 - Get OAuth Token**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const body = "grant_type=client_credentials&client_id=" 
  + secrets.snow_client_id 
  + "&client_secret=" 
  + secrets.snow_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching SNOW OAuth token',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + '/oauth_token.do',
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.access_token) {
  return {
    decision: { 
      status: 'continue',
      message: 'Found OAuth token',
    },
    data: {
      token: response.jsonBody.access_token,
      vuln: data.vuln
    } 
  };
}
else {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'Missing OAuth token',
    }
  };
}
```

**Action 2 - Format SNOW Vuln Body**

* **Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Processed AttackForge Vulnerability information for ServiceNow Vulnerability Response.',
  },
  data: {
    token: data.token,
    vuln: prepareBody(data.vuln)
  }
};

function prepareBody(vuln){
  let body = {};

  if (vuln?.vulnerability_created){
    body.submission_date_y_m_d = String.split(vuln.vulnerability_created, 'T')[0];
  }
  if (vuln?.vulnerability_id){
    body.report_id = vuln.vulnerability_id;
  }

  // SNOW map
  const priorityMap = {
    'Critical': 1, // Critical
    'High': 2, // Major
    'Medium': 3, // Minor
    'Low': 4, // Warning
    'Info': 0 // Clear(OK)
  };

  // Severity may get modified by SNOW automatically
  if (vuln?.vulnerability_priority){
    body.severity_number = priorityMap[vuln.vulnerability_priority];
  }
  if (vuln?.vulnerability_title){
    body.title = vuln.vulnerability_title;
  }
  if (vuln?.vulnerability_description){
    body.details = 'Description:\n' + vuln.vulnerability_description;
  }

  if (vuln?.vulnerability_affected_assets){
    const assets = affectedAssetsParagraph(vuln.vulnerability_affected_assets);
    if (assets){
      body.details = body.details + '\n\n' + assets;
    }
  }

  if (vuln?.vulnerability_steps_to_reproduce){
    body.details = body.details 
      + '\n\nSteps To Reproduce:\n' 
      + vuln.vulnerability_steps_to_reproduce 
      + '\n';
  }

  if (vuln?.vulnerability_custom_fields){
    const custom_description = getCustomFieldData(vuln.vulnerability_custom_fields);

    if (custom_description?.technical_impact){
      body.details = body.details + '\n\n' + custom_description.technical_impact;
    }
    if (custom_description?.critical_step){
      body.details = body.details + '\n\n' + custom_description.critical_step;
    }
    if (custom_description?.attack_narrative){
      body.details = body.details + '\n\n' + custom_description.attack_narrative;
    }
    if (custom_description?.cwe){
      body.details = body.details 
        + '\n\n' 
        + 'Common Weakness Enumeration ID: CWE-' 
        + custom_description.cwe + '\n';
      body.cwe = 'CWE-' + custom_description.cwe;
    } 
    else if (vuln?.vulnerability_tags) {
      const cwe = Array.find(vuln.vulnerability_tags, findCwe);
      if (cwe){
        body.details = body.details 
          + '\n\n' 
          + 'Common Weakness Enumeration ID: ' 
          + cwe 
          + '\n';
        body.cwe = cwe;
      }
    }
  }
  if (vuln?.vulnerability_remediation_recommendation){
    body.details = body.details 
      + '\n\nRemediation Recommendation:\n' 
      + vuln.vulnerability_remediation_recommendation;
  }

  let cvss_version;
  if (vuln?.vulnerability_cvssv3_vector){
    cvss_version = 'CVSS:3.1';
    const cvss_detail = getCvssDetail(vuln.vulnerability_cvssv3_vector);
    body.cvss_calculation_method = cvss_version;
    body.cvss_attack_vector = cvss_detail.cvss_attack_vector;
    body.cvss_attack_complexity = cvss_detail.cvss_attack_complexity;
    body.cvss_privileges_required = cvss_detail.cvss_privileges_required;
    body.cvss_user_interaction = cvss_detail.cvss_user_interaction;
    body.cvss_scope = cvss_detail.cvss_scope;
    body.cvss_confidentiality = cvss_detail.cvss_confidentiality;
    body.cvss_integrity = cvss_detail.cvss_integrity;
    body.cvss_availability = cvss_detail.cvss_availability;

    if (vuln?.vulnerability_cvssv3_base_score){
      body.cvss_score = vuln.vulnerability_cvssv3_base_score;
    }
  }

  Logger.debug('body ',JSON.stringify(body));
  return body;
}

function getCvssDetail(cvss){
  const fieldMap = {
    'AV': 'cvss_attack_vector',
    'AC': 'cvss_attack_complexity',
    'PR': 'cvss_privileges_required',
    'UI': 'cvss_user_interaction',
    'S': 'cvss_scope',
    'C': 'cvss_confidentiality',
    'I': 'cvss_integrity',
    'A': 'cvss_availability'
  };
  
  const result = {};
  const cvss_split = String.split(cvss, '/');

  for (let i = 0; i < Array.length(cvss_split); i++){
    const parts = String.split(cvss_split[i], ':');
    const key = parts[0];
    const value = parts[1];
    
    if (fieldMap[key]) {
      result[fieldMap[key]] = value;
    }
  }
  return result;
}

function findCwe(tag){
  if (String.includes(tag, 'CWE-')){
    return tag;
  }
}

function getCustomFieldData(custom_fields){
  const result = {};
  for (let i = 0; i < Array.length(custom_fields); i++){
    if (custom_fields[i].key === 'critical_steps' 
      && Array.isArray(custom_fields[i].value) 
      && Array.length(custom_fields[i].value) > 0
    ){
      let critical_step = 'Critical Steps:\n';
      for (let j = 0; j < Array.length(custom_fields[i].value); j++){
        critical_step = critical_step 
          + custom_fields[i].value[j].step 
          + ': ' 
          + custom_fields[i].value[j].details 
          + '\n';
      }
      result.critical_step = critical_step;
    }
    
    if (custom_fields[i].key === 'technical_impact') {
      if (custom_fields[i].value === '<p></p>'){
        result.technical_impact = 'Technical Impact: Not Provided';
      }
      else {
        result.technical_impact = 'Technical Impact:\n' + removeRichFormat(custom_fields[i].value);
      }
    }
    
    if (custom_fields[i].key === 'attack_narrative'){
      if (custom_fields[i].value === '<p></p>'){
        result.attack_narrative = 'Attack Narrative: Not Provided';
      }
      else {
        result.attack_narrative = 'Attack Narrative:\n' + removeRichFormat(custom_fields[i].value);
      }
    }
    if (custom_fields[i].key === 'CWE'){
      result.cwe = custom_fields[i].value;
    }
  }
  return result;
}

function affectedAssetsParagraph(affected_assets){
  let asset_string = '';
  for (let i = 0; i < Array.length(affected_assets); i++){
    if (affected_assets[i]?.asset){
      const asset = affected_assets[i].asset;
      
      if (asset?.name){
        asset_string = asset_string + 'Asset Name: ' + asset.name + '\n';
      }
      
      if (asset?.custom_fields && Array.isArray(asset.custom_fields)){
        for (let j = 0; j < Array.length(asset.custom_fields); j++){
          if (asset.custom_fields[j].key === 'urls'){
            asset_string = asset_string 
              + 'Urls: ' 
              + Array.join(asset.custom_fields[j].value, '\n') 
              + '\n';
          }
          if (asset.custom_fields[j].key === 'internet_facing'){
            asset_string = asset_string 
              + 'Internet Facing: ' 
              + asset.custom_fields[j].value 
              + '\n';
          }
        }
      }
    }
    
    if (affected_assets?[i].components){
      let asset_component_string = 'Components: ';
      const components = [];
      for (let j = 0; j < Array.length(affected_assets[i].components); j++){
        if (affected_assets[i].components?[j].name){
          Array.push(components, affected_assets[i].components[j].name);
        }
      }
      asset_component_string = asset_component_string + Array.join(components, ', ');
      asset_string = asset_string + asset_component_string + '\n';
    }
  }
  return asset_string;
}

function removeRichFormat(text){
  let result = text;
  result = String.replaceAll(result, m/<span[^>]*>/gi, '');
  result = String.replaceAll(result, m/<p[^>]*>/gi, '');
  result = String.replaceAll(result, '</span>', '');
  result = String.replaceAll(result, '</p>', '');
  return result;
}
```

**Action 3 - Create SNOW Vulnerability**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}{{snow\_resource\_path}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Creating vulnerability in ServiceNow.',
  },
  request: {
    url: 'https://' + secrets.snow_tenant + secrets.snow_resource_path,
    headers: {
      Authorization: "Bearer " + data.token
    },
    body: data.vuln
  },
  data: {
    token: data.token,
    vuln: data.vuln
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to create Vulnerable item in ServiceNow has failed. Please check the logs.'
    }
  };
}

const custom = {};
if (response.jsonBody?.result?.sys_id){
  custom.sys_id = response.jsonBody.result.sys_id;
}
if (response.jsonBody?.result?.external_id){
  custom.external_id = response.jsonBody.result.external_id;
}
if (response.jsonBody?.result?.link){
  custom.link = response.jsonBody.result.link;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully created Vulnerable Item on ServiceNow, proceeding to next step.',
  },
  data: {
    token: data?.token,
    vuln: data?.vuln,
    custom: custom
  }
};
```

**Action 4 - Insert SNOW Vuln Info on AF Vuln**

* **Method**: PUT
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerability/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_token
* **Request Script**:

```javascript
if (!data.vuln?.report_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'AttackForge Vulnerability ID not found. Please check the logs.',
    }
  };
}
if (!data.custom) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.custom'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating AF Vulnerability with custom SNOW fields.',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln.report_id,
    body: {
      custom_fields: [
        {
          key: "snow_sys_id",
          value: data.custom.sys_id
        },
        {
          key: "snow_external_id",
          value: data.custom.external_id
        },
        {
          key: "snow_link",
          value: 'https://' + secrets.snow_tenant + '/sn_vul_vulnerable_item.do?sys_id=' + data.custom.sys_id
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?.result || response.jsonBody?.result?.result !== "Vulnerability Updated"){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update Vulnerability failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability with SNOW custom fields.'
  }
};
```

## Update Vuln Status When ServiceNow VR Item Status Changes

The purpose of this example is when a Vulnerability Item changes status in the ServiceNow Vulnerability Response (VR) module, the matching vulnerability in AttackForge also updates its status.&#x20;

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FvcuVcxqRrOyuithvJQmT%2FVR2.png?alt=media&#x26;token=7286c453-00fa-482e-a219-55cac28dbd24" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: User API Key
    * **Header Key**: x-user-key
* **Secrets**:
  * af\_tenant - your AttackForge hostname e.g. demo.attackforge.com
  * af\_token - your AttackForge user API key

**Action 1 - Update Vulnerability**

* **Method**: PUT
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY: Type = Secret; Value = af\_token
* **Request Script**:

```javascript
if (!data?.jsonBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify('data: ', data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No payload information sent from ServiceNow found. Please check the debug log.'
    }
  };
}

const vuln_item = data.jsonBody;
let af_vuln_id;
let snow_vuln_state;

if (vuln_item?.third_party_id){
  af_vuln_id = String.split(vuln_item.third_party_id, 'AF-')[1];
}
if (vuln_item?.state){
  snow_vuln_state = vuln_item.state;
}

// Closed > Deferred > Resolved > In Review 
// > Awaiting Implementation > Under Investigation > Open

const snowStateMap = {
  'Closed': 'Closed',
  'Resolved': 'Closed',
  'In Review': 'Open',
  'Awaiting Implementation': 'Open',
  'Under Investigation': 'Retest',
  'Open': 'Open',
};

const af_state = snowStateMap[snow_vuln_state];

return {
  decision: {
    status: 'continue',
    message: 'Updating Vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + af_vuln_id,
    body: {
      status: af_state
    }
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.result?.result || response.jsonBody?.result?.result !== "Vulnerability Updated"){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update vulnerability status has failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability status.',
  }
};
```

**Postrequisites:**

**Create Rest Message**

1. Navigate to REST Messages.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fj6wDT0iQmNG6leYS3ScX%2FScreenshot%202026-01-15%20at%203.43.44%E2%80%AFpm.png?alt=media&#x26;token=a231d3ce-8ab1-4124-adc9-7a69eea99e27" alt="" width="375"><figcaption></figcaption></figure>

2. Click `New`. Enter Name `AttackForge Vuln Update Webhook`. The Endpoint should reference your [AttackForge Flow Trigger URL](#http-trigger-url) (see flow created above). Click `Submit`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FPlHUAfKGXLWrMZcWPL4M%2FScreenshot%202026-01-15%20at%203.45.33%E2%80%AFpm.png?alt=media&#x26;token=2c5a9166-cce5-4baf-bb16-22c949e2ce2e" alt=""><figcaption></figcaption></figure>

3. Click `New`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FlcNoFJxsZpPSwOo1cXiO%2FScreenshot%202026-01-15%20at%203.46.47%E2%80%AFpm.png?alt=media&#x26;token=e0d0fda0-e6c1-453a-b345-2a84315deaee" alt=""><figcaption></figcaption></figure>

4. Enter `POST` for Name and select `POST` for HTTP method. The Endpoint should reference your [AttackForge Flow Trigger URL](#http-trigger-url) (see flow created above).

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F0NAhm5cNXD1PgduPM46x%2FScreenshot%202026-01-15%20at%203.47.53%E2%80%AFpm.png?alt=media&#x26;token=55e694ad-2771-4942-b415-dfff3fb53793" alt=""><figcaption></figcaption></figure>

5. Click `HTTP Request` tab. Enter `Content-Type` and `X-USER-KEY` headers. The value for the `X-USER-KEY` should be your AttackForge user API key which has access to trigger the flow you created (see above). Click `Update`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F15phPle9zsSwNv6KTZIP%2FScreenshot%202026-01-15%20at%203.48.10%E2%80%AFpm.png?alt=media&#x26;token=56520e4d-2b25-41c9-9945-21c9366ccd4f" alt=""><figcaption></figcaption></figure>

**Configure Business Rule**

1. Navigate to `Business Rules`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F95TgtxSNRJ8oAXBYrscM%2FScreenshot%202026-01-15%20at%203.49.05%E2%80%AFpm.png?alt=media&#x26;token=3f92f7a3-754b-4ea0-a832-c69f8f0793e5" alt="" width="375"><figcaption></figcaption></figure>

2. Click New. Enter `Vuln Updated` for the Name. Select `Vulnerable Item [sn_vul_vulnerable_item]` for the Table. Tick `Active` and `Advanced`. In the `When to run` tab, select `after` for When, tick `Update`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FJSQcgETgiGLYGAyMvru3%2FScreenshot%202026-01-15%20at%203.51.14%E2%80%AFpm.png?alt=media&#x26;token=b2468da6-af7d-4bfd-8485-9156849d53bd" alt=""><figcaption></figcaption></figure>

3. Click on `Advanced` tab. Enter the following code, ensuring that the highlighted section in the image matches the name and HTTP method defined in ***Create Rest Message*** above. Click `Submit`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FYO5Gh6gSN0ml4gaOBGtE%2FScreenshot%202026-01-15%20at%203.52.23%E2%80%AFpm.png?alt=media&#x26;token=1945ffcd-00c4-4956-aaf7-a9bb4e6d34b9" alt=""><figcaption></figcaption></figure>

```javascript
(function executeRule(current, previous) {
    try {
		if (!current.state.changes()) {
			gs.info("State unchanged for VI: " + current.number + " - skipping");
			return;
		}
		gs.info('Vulnerability State Updated to: ', current.state.getDisplayValue());

        let r = new sn_ws.RESTMessageV2('AttackForge Vuln Update Webhook', 'POST'); 
        let payload = {
            vulnerability_id: current.sys_id.toString(),
            number: current.number.toString(),
            state: current.state.getDisplayValue(),
			third_party_id: current.vulnerability.getDisplayValue(),
            short_description: current.short_description.toString()
        };
        
        r.setRequestBody(JSON.stringify(payload));

        let response = r.execute();
        let httpStatus = response.getStatusCode();
        
        gs.info('AttackForge webhook sent for updaed Vulnerable Item: ' + current.number + 
                ', Status: ' + httpStatus);
        
    } catch (error) {
        gs.error('AttackForge webhook error: ' + error.message);
    }
})(current, previous);

/*
 * {
 *   vulnerability_id: Vulnerable Item sys_id (string)
 *   number: Vulnerable Item number (e.g., "VIT0010025")
 *   state: Display value of state field (e.g., "Open", "Closed", "Resolved")
 *   third_party_id: Reference to vulnerability entry (e.g., "AF-12345")
 *   short_description: Short description of the vulnerable item
 * }
 *
 * - current: GlideRecord object representing the updated record
 * - previous: GlideRecord object representing the record before update
 * - current.state.changes(): Built-in method to detect if state field was modified
 * - sn_ws.RESTMessageV2: ServiceNow REST client for outbound HTTP requests
 * - getDisplayValue(): Returns human-readable value instead of internal value
 */
```

## Update ServiceNow VR Item Status When Vuln Status Changes

The purpose of this example is when a vulnerability status is updated in AttackForge, the status is also updated for the linked Vulnerability Item in ServiceNow Vulnerability Response (VR) module.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FYt5hNLogwalHfOf5r8M5%2FVR3.png?alt=media&#x26;token=6c2dc582-3879-4d5e-9959-93e9e97ebb05" alt=""><figcaption></figcaption></figure>

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Prerequisites:**

**Configure OAuth on ServiceNow**

1. Navigate to `Inbound Integrations`

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FgjSMzmrBJuXVQv03veSw%2FScreenshot%202026-01-15%20at%209.40.59%E2%80%AFpm.png?alt=media&#x26;token=1fd9b5c4-cca0-4c3c-908c-d553c42ddb87" alt=""><figcaption></figcaption></figure>

2. Click on `New Integration`. Select `Client Credentials Grant`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FpBngpVbQgstIbCcgyBdj%2FScreenshot%202026-01-15%20at%209.42.45%E2%80%AFpm.png?alt=media&#x26;token=e90c11a2-7b1a-4436-94e2-55cde51b38e1" alt=""><figcaption></figcaption></figure>

3. Configure the credentials as required. Copy the `Client Id` and `Client Secret`. These will be referred to in the secrets within the flow.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F4AqthoJBdU7kjxDUmjdd%2FScreenshot%202026-01-15%20at%209.45.45%E2%80%AFpm.png?alt=media&#x26;token=5d78678a-4340-48a6-9c31-08f950cb214b" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FtpyrBi05DsmtSu22r5wT%2FScreenshot%202026-01-15%20at%203.30.05%E2%80%AFpm.png?alt=media&#x26;token=709cca7f-9283-4d5f-8207-042cfb762bc3" alt="" width="375"><figcaption></figcaption></figure>

2. Click on `New`. Enter a name e.g. AttackForge. Select `vulnerability_integration_svc` in Default ACLs.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F176e9WmDz0LR4pz9eeQN%2FScreenshot%202026-01-15%20at%203.36.07%E2%80%AFpm.png?alt=media&#x26;token=616bb29f-a6ab-43c9-9395-fb41e53c1d32" alt=""><figcaption></figcaption></figure>

3. Click `Submit`. Click `New`.&#x20;

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FPALwTy5dqC7uSZjDMOZR%2FScreenshot%202026-01-15%20at%203.37.28%E2%80%AFpm.png?alt=media&#x26;token=bc44589a-fd63-46d8-8afe-568f827250c2" alt=""><figcaption></figcaption></figure>

4. Enter `Get Vulnerable Item` in Name. Select `GET` for HTTP method. Enter `/vulnerable_item/{vulnId}` for the Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FtbW9iyrRpNmCO1Iw5gLM%2FScreenshot%202026-01-15%20at%203.39.45%E2%80%AFpm.png?alt=media&#x26;token=d2d4fb63-475a-433e-a343-156f47ac936f" alt=""><figcaption></figcaption></figure>

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fb5fKS5DY1giVVK8KKwvm%2FScreenshot%202026-01-15%20at%203.41.54%E2%80%AFpm.png?alt=media&#x26;token=ddde315d-0894-452a-99a8-19f30165f6dc" alt=""><figcaption></figcaption></figure>

```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    let vulnId = request.pathParams.vulnId;
    
    if (!vulnId) {
        response.setStatus(400);
        response.setBody({error: 'Missing vulnId parameter'});
        return;
    }

    let vulnEntry = new GlideRecord('sn_vul_entry');
    vulnEntry.addQuery('id', vulnId);
    vulnEntry.query();
    
    if (!vulnEntry.next()) {
        response.setStatus(404);
        response.setBody({error: 'Vulnerability entry not found', vulnId: vulnId});
        return;
    }
    
    let entrySysId = vulnEntry.getUniqueValue();

    let vulnItem = new GlideRecord('sn_vul_vulnerable_item');
    vulnItem.addQuery('vulnerability', entrySysId);
    vulnItem.query();
    
    if (!vulnItem.next()) {
        response.setStatus(404);
        response.setBody({error: 'No vulnerable item found', vulnId: vulnId});
        return;
    }

    response.setStatus(200);
    response.setBody({
        sys_id: vulnItem.getUniqueValue(),
        number: vulnItem.getValue('number'),
        vulnerability_id: vulnId,
        state: vulnItem.getValue('state'),
        state_label: vulnItem.getDisplayValue('state'),
        configuration_item: vulnItem.getDisplayValue('cmdb_ci'),
        updated_at: vulnItem.getValue('sys_updated_on')
    });
    
})(request, response);
```

3. Click `New`.&#x20;

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FPALwTy5dqC7uSZjDMOZR%2FScreenshot%202026-01-15%20at%203.37.28%E2%80%AFpm.png?alt=media&#x26;token=bc44589a-fd63-46d8-8afe-568f827250c2" alt=""><figcaption></figcaption></figure>

4. Enter `Update Vulnerable Item` in Name. Select `POST` for HTTP method. Enter `/update_vulnerable_item` for the Relative Path. Copy the `Resource Path` - this will be referenced later in the flow secrets. Enter the following code, the click `Update`.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F0tUpNSHSsStdM89ImBf3%2FScreenshot%202026-01-15%20at%203.40.27%E2%80%AFpm.png?alt=media&#x26;token=6209b35a-463e-452b-b818-445393562f8e" alt=""><figcaption></figcaption></figure>

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fb5fKS5DY1giVVK8KKwvm%2FScreenshot%202026-01-15%20at%203.41.54%E2%80%AFpm.png?alt=media&#x26;token=ddde315d-0894-452a-99a8-19f30165f6dc" alt=""><figcaption></figcaption></figure>

```javascript
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
    
    try {
        let requestBody = request.body.data;
        let vulnId = requestBody.vuln_id;
        let newStatus = requestBody.update_status;

        let vulnItem = new GlideRecord('sn_vul_vulnerable_item');
		vulnItem.addQuery('external_id', vulnId);
        vulnItem.query();
        
        if (vulnItem.next()) {
            let stateMapping = {
                'Open': '1',
                'Retest': '2',
                'Closed': '3'
            };
            
            vulnItem.setValue('state', stateMapping[newStatus] || 'open');
            vulnItem.update();
            
            response.setStatus(200);
            response.setBody({success: true, message: 'Updated', external_id: vulnId});
        } else {
            response.setStatus(404);
            response.setBody({success: false, message: 'Not found: ' + vulnId});
        }
        
    } catch (e) {
        response.setStatus(500);
        response.setBody({success: false, message: e.message});
    }
    
})(request, response);
```

**Initial Set Up**

> **Important**: This example requires access to the AttackForge Self-Service API and AttackForge Flows

* **Event**: Vulnerability Updated
* **Secrets**:
  * snow\_client\_id - your ServiceNow Client Id (see Prerequisites above)
  * snow\_client\_secret - your ServiceNow Client Id (see Prerequisites above)
  * snow\_hostname - your ServiceNow hostname e.g. company.service-now\.com
  * snow\_get\_vulnitem\_api - your ServiceNow Scripted REST API route for *Get Vulnerable Item* (see Prerequisites above)
  * snow\_update\_vulnitem\_api - your ServiceNow Scripted REST API route for *Update Vulnerable Item* (see Prerequisites above)

**Action 1 - Get OAuth Token**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}/oauth\_token.do
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
* **Request Script**:

```javascript
const body = "grant_type=client_credentials&client_id=" 
  + secrets.snow_client_id 
  + "&client_secret=" 
  + secrets.snow_client_secret;

return {
  decision: {
    status: 'continue',
    message: 'Fetching SNOW OAuth token',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + '/oauth_token.do',
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.access_token) {
  return {
    decision: { 
      status: 'continue',
      message: 'Found OAuth token',
    },
    data: {
      token: response.jsonBody.access_token,
      vuln: data.vuln
    } 
  };
}
else {
  Logger.error(JSON.stringify(data));

  return {
    decision: { 
      status: 'abort',
      message: 'Missing OAuth token',
    }
  };
}
```

**Action 2 - Get SNOW VR Item Status**

* **Method**: GET
* **URL**: https\://{{snow\_hostname}}{{snow\_get\_vulnitem\_api}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

let vulnId = data.vuln.vulnerability_id;

if (!vulnId){
  return {
    decision: {
      status: 'abort',
      message: 'Error: no vulnerability_id found from data.vuln.'
    }
  };
}

vulnId = 'AF-' + vulnId;

return {
  decision: {
    status: 'continue',
    message: 'Fetching ServiceNow Vulnerable Item table for ID: ' + vulnId
  },
  request: {
    url: 'https://' + secrets.snow_hostname + secrets.snow_get_vulnitem_api + vulnId,
    headers: {
      Authorization: "Bearer " + data.token
    }
  },
  data: {
    token: data.token,
    vuln: data.vuln
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed retrieving Vulnerable Item details.'
    }
  };
}

if (!response.jsonBody?.result) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: returned body does not contain vulnerable item result.'
    }
  };
}

const vuln = response.jsonBody.result;
const vulnState = vuln.state_label;
const snow_vuln_state = vulnState;

return {
  decision: {
    status: 'continue',
    message: 'Successfully found Vulnerable Item record, proceeding to next action.',
  },
  data: {
    token: data.token,
    vuln: data.vuln,
    snow_vuln_state: snow_vuln_state
  }
};
```

**Action 3 - Detect If Status Changed**

* **Script:**

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Missing data.vuln'
    }
  };
}
if (!data.snow_vuln_state){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Missing data.snow_vuln_state'
    }
  };
}

const snowStateMap = {
  'Closed': 'Closed',
  'Resolved': 'Closed',
  'In Review': 'Open',
  'Awaiting Implementation': 'Open',
  'Under Investigation': 'Retest',
  'Open': 'Open',
};

let current_af_status;

if (data.vuln.vulnerability_status){
  current_af_status = data.vuln.vulnerability_status;
  if (current_af_status === 'Open' && data.vuln.vulnerability_retest === 'Yes'){
    current_af_status = 'Retest';
  }
}

const expected_af_status = snowStateMap[data.snow_vuln_state];

if (!expected_af_status){
  return {
    decision:{
      status: 'abort',
      message: 'Error: Could not find expected AttackForge state for provided ServiceNow state: ' 
        + data.snow_vuln_state
    }
  };
}

if (expected_af_status === current_af_status) {
  return {
    decision: {
      status: 'finish',
      message: 'AttackForge Vulnerability status and ServiceNow Vulnerable Item state is in sync.'
    }
  };
}

let vuln_id;
if (data.vuln.vulnerability_id){
  vuln_id = data.vuln.vulnerability_id;
}

return {
  decision: {
    status: 'continue',
    message: 'New status found, proceeding to next step.',
  },
  data: {
    token: data.token,
    vuln_id: vuln_id,
    update_status: current_af_status
  }
};
```

**Action 4 - Update SNOW VR Item**

* **Method**: POST
* **URL**: https\://{{snow\_hostname}}{{snow\_update\_vulnitem\_api}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.token) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.token',
    }
  };
}
if (!data.vuln_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln_id',
    }
  };
}
if (!data.update_status) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.update_status',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Update vulnerability on Service Now.',
  },
  request: {
    url: 'https://' + secrets.snow_hostname + secrets.snow_update_vulnitem_api,
    headers: {
      Authorization: "Bearer " + data.token
    },
    body: {
      vuln_id: data.vuln_id,
      update_status: data.update_status
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Request to update ServiceNow Vulnerable Item state has failed. Please check the logs.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated Vulnerability status.',
  }
};
```
