# 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="/files/37mEB72oNrCTWa0mcrVC" 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="/files/dxYhM6Fflj07luYhE0Eh" 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="/files/S2vOPotq00GKzoX7AVVk" 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="/files/v86auWLDFpIpM8Dt1thZ" 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="/files/pnUij3rad3oUyeTLw8TJ" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/naxnOcHLslw61fumHo2l" 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="/files/ipkbT5rZKGWAODnOtgDK" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="/files/o38rhTkFZJ2hEQGk0Rgk" 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="/files/jpyyUY0emlwCM0CNtswn" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/stY6wWuIOgDvGL5invZM" 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="/files/iUtkOIdERW4ozp2BPsgr" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" 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="/files/YvbuJ5fqussou7kmErhK" 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="/files/NgGmvPUmmtrQB05Oec6a" 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="/files/YvCp8NxJ4070wxOhFg73" 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="/files/oeWCNYOA9YvBAIZmmT7g" 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="/files/s257tPvLkEpO6xo9hPlQ" alt=""><figcaption></figcaption></figure>

3. Click `New`.

<figure><img src="/files/zYPNW7bWwY5RldbUNHMd" 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="/files/vi4Z9q4IjEV0r2iUrJa9" 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="/files/78hYyeuLjvBMMnCLgmRo" alt=""><figcaption></figcaption></figure>

**Configure Business Rule**

1. Navigate to `Business Rules`.

<figure><img src="/files/CeHZp7M9iQ04sYnwTo0q" 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="/files/K9DTtsqpvU3mR0l1juKr" 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="/files/p85u1NJxsX3K3qAp6j0b" 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="/files/H7ULEvis4UPPSeJxkdgf" 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="/files/pnUij3rad3oUyeTLw8TJ" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/naxnOcHLslw61fumHo2l" 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="/files/ipkbT5rZKGWAODnOtgDK" alt=""><figcaption></figcaption></figure>

**Create Scripted REST API**

1. Navigate to `Scripted REST APIs`

<figure><img src="/files/o38rhTkFZJ2hEQGk0Rgk" 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="/files/jpyyUY0emlwCM0CNtswn" alt=""><figcaption></figcaption></figure>

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

<figure><img src="/files/stY6wWuIOgDvGL5invZM" 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="/files/sUcg0SSiiCevAdzDg7Yq" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" 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="/files/stY6wWuIOgDvGL5invZM" 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="/files/tjLVyOGF7MewZjQUvHaZ" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/MNYx4RTD1Igv6X7OovvT" 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.',
  }
};
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://support.attackforge.com/attackforge-enterprise/modules/flows/servicenow.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
