# Atlassian JIRA

## Create JIRA Issue

{% embed url="<https://youtu.be/-BfrTnCIoi0?si=2DqkyNw5Czb2b9X5>" %}

The purpose of this example is to create a [JIRA Issue](https://support.atlassian.com/jira-software-cloud/docs/what-is-an-issue/) when a Vulnerability is created in AttackForge, and to update AttackForge to assign the JIRA Issue Key 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).
  * jira\_auth - your [JIRA API token](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/)

**Action 1 - Create JIRA Issue**&#x20;

* **Method**: POST
* **URL**: https\://\<YOUR-JIRA>/rest/api/2/issue
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_auth
* **Request Script**:

```javascript
let jiraProjectKey = '';
let afProjectId = '';

if (data.vulnerability_projects) {
  for (let i = 0; i < data.vulnerability_projects.length; i++) {
    const project = data.vulnerability_projects[i];

    if (project.custom_fields) {
      for (let j = 0; j < project.custom_fields.length; j++) {
        const projectCustomField = project.custom_fields[j];

        if (projectCustomField.key === 'jira_project_key') {
          jiraProjectKey = projectCustomField.value;
          break;
        }
      }
    }

    if (project.id) {
      afProjectId = project.id;
    }

    if (afProjectId && jiraProjectKey) {
      break;
    }
  }
}

jiraProjectKey = String.replace(jiraProjectKey, m/"/g, '\"');
jiraProjectKey = String.replace(jiraProjectKey, m/\\"/g, '\"');

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

function buildRequestBody(jiraProjectKey) {
  let summary = '';

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

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vulnerability_title + ' \n\n ';
  }

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

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data.vulnerability_priority && 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/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vulnerability_priority + ' \n\n ';
    }
  }

  if (data.vulnerability_attack_scenario && data.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vulnerability_attack_scenario + ' \n\n ';
  }

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

    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_steps_to_reproduce) {
    description += '*Steps to Reproduce* \n\n ' + data.vulnerability_steps_to_reproduce + ' \n\n ';
  }

  let notes;

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

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data.vulnerability_tags) {
    for (let i = 0; i < data.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  Logger.debug(JSON.stringify(tags));

  if (tags.length > 0) {
    description = description + '*Tags* \n\n ';
    
    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }    
  }

  description = String.replace(description, m/"/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      project: {
          key: jiraProjectKey
      },
      summary: summary,
      description: description,
      priority: {
          name: priority
      },
      issuetype: {
          name: 'Bug'
      },
      labels: labels
    }
  };
}
```

* **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?.key) {
  Logger.error(JSON.stringify(response));

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

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.af_project_id',
    },
  };
}
else if (!data?.af_vuln_id) {
  if (data) {
    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,
      jira_issue_key: body.key
    } 
  };
}
```

**Action 2 - Update AF Vuln with JIRA Issue Key**

* **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?.jira_issue_key && data?.af_vuln_id && data?.af_project_id) {
  return {
    request: {
      url: 'https://demo.attackforge.com/api/ss/vulnerability/' + data.af_vuln_id,
      body: {
          project_id: data.af_project_id,
          custom_fields: [
            {
              key: 'jira_issue_key',
              value: data.jira_issue_key
            }
          ]
      }
    }
  };
}
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'
    },
  };
}
```

## Update JIRA Issue

{% embed url="<https://youtu.be/2bps7vEcmVA?si=LbU0ywrOa1_J33Jl>" %}

The purpose of this example is to update a [JIRA Issue](https://support.atlassian.com/jira-software-cloud/docs/what-is-an-issue/) when a Vulnerability is updated in AttackForge.

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

* **Event**: Vulnerability Updated
* **Secrets**:
  * jira\_auth - your [JIRA API token](https://developer.atlassian.com/cloud/jira/platform/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**&#x20;

* **Method**: GET
* **URL**: \<defined in Request Script>
* **Headers**:
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_auth
* **Request Script**:

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

let jiraIssueKey;

if (data.vulnerability_custom_fields) {
  for (let i = 0; i < data.vulnerability_custom_fields.length; i++) {
    if (data.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'no JIRA Issue Key found',
      }
  };
}

if (jiraIssueKey) {
  return {
    data: {
      vuln: data
    },
    request: {
      url: 'https://attackforge.atlassian.net/rest/api/3/issue/' + jiraIssueKey,
    }
  };
}
```

* **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?.key) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'finish',
      message: 'missing JIRA Issue Key (body.key) - Issue likely deleted',
    }
  };
}
else if (!data?.vuln) {
  if (data) {
    Logger.error(JSON.stringify(data));
  }

  return {
    decision: { 
      status: 'abort',
      message: 'missing data.vuln',
    },
  };
}
else {
  return {
    data: {
      vuln: data?.vuln,
      jira_issue_key: body?.key
    } 
  };
}
```

**Action 2 - Update JIRA Issue**

* **Method**: \<defined in Request Script>
* **URL**: \<defined in Request Script>
* **Headers**:
  * \<defined in Request Script>
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'finish',
      message: 'no JIRA Issue Key found',
    }
  };
}
else if (!data?.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'missing data.vuln',
    }
  };
}
else {
  let url = 'https://attackforge.atlassian.net/rest/api/2/issue/' + data.jira_issue_key;

  return {
    request: {
      url: url,
      body: buildRequestBody(),
    }
  };
}

function buildRequestBody() {
  let summary = '';

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

  summary = String.replace(summary, m/"/g, '\"');
  summary = String.replace(summary, m/\\"/g, '\"');

  let description = '*_{color:red}WARNING: Contents of this ticket may be overwritten by automated tooling!{color}_* \n ';

  if (data?.vuln?.vulnerability_title) {
    description += 'h1. Vulnerability: ' + data.vuln.vulnerability_title + ' \n\n ';
  }

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

  let cvssScore;
  let cvssVector;
  let cvssVectorEscaped;

  if (data?.vuln?.vulnerability_priority && data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      const tag = data.vuln.vulnerability_tags[i];

      if (tag =~ m/CVSSv3.1 Base Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Base Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Temporal Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Temporal Score: ', '');
      }

      if (tag =~ m/CVSSv3.1 Environmental Score:/) {
        cvssScore = String.replace(tag, 'CVSSv3.1 Environmental Score: ', '');
      }

      if (tag =~ m/CVSS: 3.1\//) {
        cvssVector = String.replace(tag, 'CVSS:3.1/', '');
        cvssVectorEscaped = String.replace(cvssVector, m/:/g, ':{anchor}');
      }
    }

    if (cvssScore && cvssVector && cvssVectorEscaped) {
      description += '*Technical Severity* \n ||*Rating*||*CVSSv3 Score*||\n|' + data.vuln.vulnerability_priority + '|' + cvssScore + '|\n\nCVSS 3.1 Vector String: [' + cvssVectorEscaped + '|https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator?version=3.1&vector=' + cvssVector + '] \n\n ';
    }
    else {
      description += '*Technical Severity* \n ||*Rating* \n |' + data.vuln.vulnerability_priority + ' \n\n ';
    }
  }

  if (data?.vuln?.vulnerability_attack_scenario && data?.vuln?.vulnerability_likelihood_of_exploitation) {
    description += '*Attack Scenario (Technical Risk)* \n\n Likelihood of Exploitation: ' + data.vuln.vulnerability_likelihood_of_exploitation + '/10 \n\n ' + data.vuln.vulnerability_attack_scenario + ' \n\n ';
  }

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

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

    description += '\n\n';
  }

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

  let notes;

  if (data?.vuln?.vulnerability_notes) {
    for (let i = 0; i < data.vuln.vulnerability_notes.length; i++) {
      if (notes === undefined) {
        notes = data.vuln.vulnerability_notes[i].note;
      }
      else {
        notes += '\n\n' + data.vuln.vulnerability_notes[i].note;
      }
    }
  }

  if (notes) {
    description += '*Notes* \n\n ' + notes + ' \n\n ';
  }

  if (data?.vuln?.vulnerability_remediation_recommendation) {
    description += '*Recommendations* \n\n ' + data.vuln.vulnerability_remediation_recommendation + ' \n\n ';
  }

  const labels = [];
  const tags = [];

  if (data?.vuln?.vulnerability_tags) {
    for (let i = 0; i < data.vuln.vulnerability_tags.length; i++) {
      let newtag = '* ' + data.vuln.vulnerability_tags[i] + '\n';
      newtag = String.replace(newtag, m/:/g, '{color:black}:{color}');

      Array.push(tags, newtag);

      let label = data.vuln.vulnerability_tags[i];
      label = String.replace(label, m/\s/g, '');

      Array.push(labels, label);
    }
  }

  if (tags.length > 0) {
    description += '*Tags* \n\n ';

    for (let i = 0; i < tags.length; i++) {
      description = description + tags[i];
    }
  }

  description = String.replace(description, m/ "/g, '\"');
  description = String.replace(description, m/\\"/g, '\"');

  let priority = 'Lowest';

  if (data?.vuln?.vulnerability_priority === 'Critical') {
    priority = 'Highest';
  }
  else if (data?.vuln?.vulnerability_priority === 'High') {
    priority = 'High';
  }
  else if (data?.vuln?.vulnerability_priority === 'Medium') {
    priority = 'Medium';
  }
  else if (data?.vuln?.vulnerability_priority === 'Low') {
    priority = 'Low';
  }
  else if (data?.vuln?.vulnerability_priority === 'Info') {
    priority = 'Lowest';
  }

  return {
    fields: {
      summary: summary,
      description: description,
      priority: {
        name: priority
      },
      issuetype: {
        name: 'Bug'
      },
      labels: labels
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish'
  };
}
else {
  Logger.info(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue update failed',
    },
  };
}
```

## JIRA Issue Retest -> Update Vuln to Ready for Retest

The purpose of this example is when a JIRA Issue is assigned the 'Retest' status - the matching vulnerability in AttackForge is also assigned as retest.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FUs5bfg05b31WZR6FpjJo%2Fretest.png?alt=media&#x26;token=316e37b9-aff9-478a-9663-1c72918ff867" 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**

* **JIRA WebHooks**
  * Configure 'Issue Updated' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * jira\_webhook\_secret - your [JIRA WebHook secret](https://support.attackforge.com/attackforge-enterprise/modules/projects)
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)
  * 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
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

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

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_key"},value:{$eq:"'+ data.jsonBody.issue.key + '"}}}}';

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

* **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,
      issue: data.issue
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - 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.issue?.fields?.status?.name !== 'Retest') {
  return {
    decision: {
      status: 'finish',
      message: 'JIRA issue status not set to "Retest"',
    }
  };
}

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 JIRA Issue

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

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FhuJQaqXHtRYOl7EVcIsb%2Fclose.png?alt=media&#x26;token=a7afb0b9-937f-4789-ab7b-7b7f36506f7c" 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**:
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/3/issue/{issueIdOrKey}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **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 jiraIssueKey;

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 === 'jira_issue_key' && customField.value) {
      jiraIssueKey = customField.value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/3/issue/' + jiraIssueKey
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.key) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: response.jsonBody.key
  } 
};
```

**Action 2 - Get JIRA Transitions**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions'
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.transitions) {
  Logger.error(JSON.stringify(response));

  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key,
    transitions: response.jsonBody.transitions
  } 
};
```

**Action 3 - Close JIRA Issue**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

if (!data?.transitions) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

let transitionId;
for (let x = 0; x < data.transitions.length; x++) {
  const transition = data.transitions[x];

  if (transition.name === 'Closed') {
    transitionId = transition.id;
  }
}

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

let url = 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions';

return {
  request: {
    url: url,
    body: {
      transition: {
        id: transitionId
      }
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish',
    message: 'Transitioned JIRA issue to "Closed"'
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA Issue transition failed'
    },
  };
}
```

## Re-Open JIRA Issue

The purpose of this example is when a vulnerability is re-opened in AttackForge, the matching JIRA Issue 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%2F1LVOQzWLf5NS5t1jsjI7%2Fre-open.png?alt=media&#x26;token=d473afef-2333-4f8c-963d-80d535e62e0d" 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**:
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get JIRA Issue**

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/3/issue/{issueIdOrKey}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **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 jiraIssueKey;

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 === 'jira_issue_key' && customField.value) {
      jiraIssueKey = customField.value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/3/issue/' + jiraIssueKey
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.key) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: response.jsonBody.key
  } 
};
```

**Action 2 - Get JIRA Transitions**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing'
    }
  };
}

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

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions',
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.transitions) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    jira_issue_key: data.jira_issue_key,
    transitions: response.jsonBody.transitions
  } 
};
```

**Action 3 - Re-Open JIRA Issue**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/transitions>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data?.jira_issue_key) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

if (!data?.transitions) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue transitions missing',
    }
  };
}

let transitionId;
for (let x = 0; x < data.transitions.length; x++) {
  const transition = data.transitions[x];

  if (transition.name === 'Open') {
    transitionId = transition.id;
  }
}

if (!transitionId) {
  return {
    decision: {
      status: 'abort',
      message: '"Open" transition missing',
    }
  };
}

let url = 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jira_issue_key + '/transitions';

return {
  request: {
    url: url,
    body: {
      transition: {
        id: transitionId
      }
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 204) {
  return {
    decision: 'finish',
    message: 'Transitioned JIRA Issue to "Open"',
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue transition failed'
    },
  };
}
```

## JIRA Issue Comment Created -> Create Vuln Remediation Note

The purpose of this example is when a comment is created on a JIRA Issue, a remediation note is created on the matching vulnerability.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FYBg5IA4HeanOO9gp2Jrx%2Fnew%20remediation%20note.png?alt=media&#x26;token=bb2d302c-a2a9-4207-b9b6-f211ea9d2bb0" 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**

* **JIRA WebHooks**
  * Configure 'Issue Comment Created' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * 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**

* **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
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

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

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_key"},value:{$eq:"'+ data.jsonBody.issue.key + '"}}}}';

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

* **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,
      issue: data.issue,
      comment: data.comment
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving vulnerability'
    }
  };
}
```

**Action 2 - Create Remediation Note**&#x20;

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

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

if (!data.comment?.body) {
  return {
    decision: {
      status: 'abort',
      message: 'Comment missing',
    }
  };
}

let afVulnProjectId;
if (data.vuln.vulnerability_projects) {
  for (let x = 0; x < data.vuln.vulnerability_projects.length; x++) {
    const project = data.vuln.vulnerability_projects[x];

    if (project.id) {
      afVulnProjectId = project.id;
      break;
    }
  }
}

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

const comment = data.comment.body;
let remediationNoteExists = false;

if (data.vuln.vulnerability_remediation_notes) {
  for (let x = 0; x < data.vuln.vulnerability_remediation_notes.length; x++) {
    const remediationNote = data.vuln.vulnerability_remediation_notes[x];

    if (remediationNote.note === comment) {
      remediationNoteExists = true;
    }
  }
}

if (remediationNoteExists) {
  return {
    decision: {
      status: 'finish',
      message: 'Remediation note already exists',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Create remediation note',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.vuln.vulnerability_id + '/remediationNote',
    body: {
      projectId: afVulnProjectId,
      note: comment
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 && response.jsonBody?.note?.id) {
  return {
    decision: {
      status: 'finish',
      message: 'Remediation note created'
    }
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating remediation note'
    },
  };
}
```

## Create JIRA Issue Comment

The purpose of this example is when a remediation note is created on a vulnerability, a comment is also created on the matching JIRA Issue.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FkDfLGMpViZa1PaXHKPmL%2Fnew%20comment.png?alt=media&#x26;token=62409065-9baa-4670-8582-39b309c268e9" 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 Remediation Note Created
* **Secrets**:
  * 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)
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get Vulnerability**

* **Method**: GET
* **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
return {
  decision: {
    status: 'continue',
    message: 'Get vulnerability',
  },
  request: {
    url: 'https://demo.attackforge.dev/api/ss/vulnerability/' + data.remediation_note_vulnerability.vulnerability_id
  },
  data: {
    note: data
  }
};
```

* **Response Script**:

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

**Action 2 - Get JIRA Comments**&#x20;

* **Method**: GET
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/comment>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

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

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

let jiraIssueKey;

if (data.vuln.vulnerability_custom_fields) {
  for (let i = 0; i < data.vuln.vulnerability_custom_fields.length; i++) {
    if (data.vuln.vulnerability_custom_fields[i].key === 'jira_issue_key') {
      jiraIssueKey = data.vuln.vulnerability_custom_fields[i].value;
      break;
    }
  }
}

if (!jiraIssueKey) {
  return {
      decision: { 
        status: 'finish',
        message: 'JIRA issue key missing',
      }
  };
}

return {
  data: {
    vuln: data.vuln,
    note: data.note,
    jiraIssueKey: jiraIssueKey
  },
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + jiraIssueKey + '/comment'
  }
};
```

* **Response Script**:

```javascript
if (!response.jsonBody?.comments) {
  return {
    decision: { 
      status: 'abort',
      message: 'JIRA issue comments missing',
    }
  };
}

return {
  data: {
    vuln: data.vuln,
    note: data.note,
    jiraIssueKey: data.jiraIssueKey,
    comments: response.jsonBody.comments
  } 
};
```

**Action 3 - Create JIRA Issue Comment**

* **Method**: POST
* **URL**: <https://cybersechub.atlassian.net/rest/api/2/issue/{issueIdOrKey}/comment>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

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

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

if (!data?.jiraIssueKey) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

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

const remediationNote = data.note.remediation_note_details;
let existingComment = false;

for (let x = 0; x < data.comments.length; x++) {
  const comment = data.comments[x];

  if (comment.body === remediationNote) {
    existingComment = true;
  }
}

if (existingComment) {
  return {
    decision: {
      status: 'finish',
      message: 'Comment already exists',
    }
  };
}

return {
  request: {
    url: 'https://cybersechub.atlassian.net/rest/api/2/issue/' + data.jiraIssueKey + '/comment',
    body: {
      body: remediationNote
    }
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 201 && response.jsonBody?.id) {
  return {
    decision: 'finish',
    message: 'Comment created'
  };
}
else {
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating comment'
    },
  };
}
```

## Upload Vulnerability Evidence to JIRA Issue

The purpose of this example is when a evidence file is uploaded to a vulnerability, the file is also uploaded to the matching JIRA Issue.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FmQ5bb3l4czeaW17UweXX%2FUpload-File-To-JIRA.png?alt=media&#x26;token=44e4e11e-9f50-439a-a6e9-7b06621dc9ce" 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 Evidence Created
* **Secrets**:
  * af\_hostname - e.g. acme.attackforge.io
  * af\_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)
  * jira\_hostname - e.g. acme.atlassian.net
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)

**Action 1 - Get Vulnerability**

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

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Get vulnerability details',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.evidence_vulnerability?.vulnerability_id
  },
  data: {
    evidence: data
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerability) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability details',
    },
    data: {
      vuln: response.jsonBody.vulnerability,
      evidence: data.evidence
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulnerability',
    }
  };
}
```

**Action 2 - Check if JIRA Issue Id exists**

* **Script**:

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

const evidence = data.evidence;
const vuln = data.vuln;

let vulnId;
let jiraIssueId;

if (vuln.vulnerability_id) {
  vulnId = vuln.vulnerability_id;
}

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

    if (customField.key === 'jira_issue_id' && customField.value) {
      jiraIssueId = customField.value;
    }
  }
}

if (!vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'vulnId missing',
    }
  };
}
if (!jiraIssueId) {
  return {
    decision: {
      status: 'finish',
      message: 'no linked JIRA Issue found on this vulnerability',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Retrieved vulnerability details',
  },
  data: {
    vulnId: vulnId,
    evidence: evidence,
    jiraIssueId: jiraIssueId
  }
};
```

**Action 3 - Get JIRA Issue**

* **Method**: GET
* **URL**: https\://{{jira\_hostname}}/rest/api/3/issue/{issueIdOrKey}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Get JIRA Issue',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/issue/' + data.jiraIssueId
  },
  data: {
    vulnId: data.vulnId,
    evidence: data.evidence,
    jiraIssueId: data.jiraIssueId
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved JIRA Issue',
    },
    data: {
      vulnId: data.vulnId,
      evidence: data.evidence,
      jiraIssueId: data.jiraIssueId,
      jiraIssue: response.jsonBody
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vulnerability',
    }
  };
}
```

**Action 4 - Check if File is already uploaded to JIRA Issue**

* **Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}
if (!data.jiraIssue) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssue is missing',
    }
  };
}

let fileUploaded = false;

const fileName = data.evidence.evidence_file_name;
const fileSize = data.evidence.evidence_file_size;

if (data.jiraIssue.fields?.attachment && data.jiraIssue.fields.attachment.length > 0) {
  for (let x = 0; x < data.jiraIssue.fields.attachment.length; x++) {
    const attachment = data.jiraIssue.fields.attachment[x];

    if (getFilenameWithoutExtension(attachment.filename) === getFilenameWithoutExtension(fileName) 
      && attachment.size === fileSize
    ) {
      fileUploaded = true;
    }
  }
}

if (fileUploaded) {
  return {
    decision: {
      status: 'finish',
      message: 'File already uploaded to JIRA Issue',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'File has not been uploaded to JIRA Issue',
  },
  data: {
    vulnId: data.vulnId,
    evidence: data.evidence,
    jiraIssueId: data.jiraIssueId
  }
};

function getFilenameWithoutExtension(filename) {
  // Matches a dot followed by one or more characters that are not a dot or slash, at the end of the string
  return String.toLowerCase(String.replace(filename, m/\.[^/.]+$/i, ''));
}
```

**Action 5 - Download Evidence File**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{vulnId}/evidence/{fileStorageName}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Options:**
  * Download Respons&#x65;**:** Yes
* **Request Script**:

```javascript
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}
if (!data.evidence) {
  return {
    decision: {
      status: 'abort',
      message: 'data.evidence is missing',
    }
  };
}
if (!data.jiraIssueId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jiraIssueId is missing',
    }
  };
}

const fileStorageName = data.evidence.evidence_file_storage_name;

return {
  decision: {
    status: 'continue',
    message: 'Download vulnerability evidence file',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.vulnId + '/evidence/' + fileStorageName
  },
  data: {
    jiraIssueId: data.jiraIssueId
  }
};
```

* **Response Script**:

```javascript
if (response.fileId) {
  return {
    decision: {
      status: 'continue',
      message: 'Downloaded vulnerability evidence file',
    },
    data: {
      fileId: response.fileId,
      jiraIssueId: data.jiraIssueId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error downloading vulnerability evidence file',
    }
  };
}
```

**Action 6 - Upload File to JIRA**

* **Method**: POST
* **URL**: https\://{{jira\_hostname}}/rest/api/3/issue/{issueIdOrKey}/attachments
* **Headers**:
  * Key = X-Atlassian-Token; Type = Value; Value = no-check
  * Key = Accept; Type = Value; Value = application/json
  * Key = Content-Type; Type = Value; Value = multipart/form-data
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

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

return {
  decision: {
    status: 'continue',
    message: 'Uploading evidence to JIRA Issue',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/issue/' + data.jiraIssueId + '/attachments',
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileId
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody && response.jsonBody[0]?.id) {
  return {
    decision: {
      status: 'finish',
      message: 'File uploaded to JIRA Issue',
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error uploading file to JIRA Issue',
    }
  };
}
```

## Upload JIRA Attachment to Vulnerability Evidence

The purpose of this example is when an attachment is uploaded to a JIRA Issue, the file is also uploaded to the matching vulnerability in AttackForge.

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F7S8pu3xfqssIngJOlcTe%2FUpload%20Evidence%20to%20AF.png?alt=media&#x26;token=77f69247-daac-41e9-99b2-5aaf6a5f8dd1" 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

* **JIRA WebHooks**
  * Configure 'Attachment Created' [web hook](https://developer.atlassian.com/server/jira/platform/webhooks/) from *https\://\<your-jira-tenant>.atlassian.net/plugins/servlet/webhooks#*
  * Append *?issueKey={issue.key}* to the end of the trigger url to ensure that your flow can identify which JIRA Issue the attachment belongs to
* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * af\_hostname - e.g. acme.attackforge.io
  * af\_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)
  * jira\_hostname - e.g. acme.atlassian.net
  * jira\_api\_key - your [JIRA API Key](https://developer.atlassian.com/cloud/jira/software/basic-auth-for-rest-apis/)
  * jira\_webhook\_secret - your JIRA webhook secret

**Action 1 - Validate Message from JIRA**

* **Script:**

```javascript
// Validate HMAC
const JIRAWebHookSecret = secrets.jira_webhook_secret;
const JIRAWebHookSignature = String.replace(data.headers['X-Hub-Signature'], "sha256=", '');
const payloadHMAC = String.toLowerCase(String.hmac(data.body, JIRAWebHookSecret, "SHA256", "base16"));

if (JIRAWebHookSignature !== payloadHMAC) {
  Logger.info('JIRAWebHookSignature: ' + JIRAWebHookSignature);
  Logger.info('hmac: ' + payloadHMAC);

  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (!data.jsonBody?.attachment?.id) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA attachment id missing',
    }
  };
}
if (!data.query?.issueKey) {
  return {
    decision: {
      status: 'abort',
      message: 'JIRA issue key missing',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'JIRA message valid',
  },
  data: {
    attachment: data.jsonBody.attachment,
    issueKey: data.query.issueKey
  }
};
```

**Action 2 - Get Vulnerability**

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

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

const query = 'q_vulnerability={custom_fields:{$elemMatch:{name:{$eq:"jira_issue_id"},value:{$eq:"'+ data.issueKey + '"}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerabilities?' + query
  },
  data: {
    attachment: data.attachment
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode === 200 
  && response.jsonBody?.count === 1 
  && response.jsonBody.vulnerabilities?[0]
  && response.jsonBody.vulnerabilities?[0].vulnerability_id
) {
  const vuln = response.jsonBody.vulnerabilities?[0];
  const vulnId = vuln.vulnerability_id;

  const vulnFiles = [];
  if (vuln.vulnerability_evidence && vuln.vulnerability_evidence.length > 0) {
    for (let x = 0; x < vuln.vulnerability_evidence.length; x++) {
      const evidence = vuln.vulnerability_evidence[x];

      if (evidence.file_name && evidence.file_size) {
        Array.push(vulnFiles, {
          name: evidence.file_name,
          size: evidence.file_size
        });
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Retrieved vulnerability'
    },
    data: {
      vulnFiles: vulnFiles,
      vulnId: vulnId,
      attachment: data.attachment
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error searching vulnerabilities',
    }
  };
}
```

**Action 3 - Check if Attachment is already uploaded to Vulnerability**

* **Script**:

```javascript
if (!data.vulnFiles) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnFiles is missing',
    }
  };
}
if (!data.attachment) {
  return {
    decision: {
      status: 'abort',
      message: 'data.attachment is missing',
    }
  };
}
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'data.vulnId is missing',
    }
  };
}

let fileUploaded = false;

const attachmentName = data.attachment.filename;
const attachmentSize = data.attachment.size;

if (data.vulnFiles.length > 0) {
  for (let x = 0; x < data.vulnFiles.length; x++) {
    const evidenceFile = data.vulnFiles[x];

    if (getFilenameWithoutExtension(evidenceFile.name) === getFilenameWithoutExtension(attachmentName) 
      && evidenceFile.size === attachmentSize
    ) {
      fileUploaded = true;
    }
  }
}

if (fileUploaded) {
  return {
    decision: {
      status: 'finish',
      message: 'File already uploaded to vulnerability',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'File has not been uploaded to vulnerability',
  },
  data: {
    vulnId: data.vulnId,
    attachmentId: data.attachment.id
  }
};

function getFilenameWithoutExtension(filename) {
  // Matches a dot followed by one or more characters that are not a dot or slash, at the end of the string
  return String.toLowerCase(String.replace(filename, m/\.[^/.]+$/i, ''));
}
```

**Action 4 - Get JIRA Attachment Download Link**

* **Method**: GET
* **URL**: https\://{{jira\_hostname}}/rest/api/3/attachment/content/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = jira\_api\_key
* **Request Script**:

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

return {
  decision: {
    status: 'continue',
    message: 'Download attachment',
  },
  request: {
    url: 'https://' + secrets.jira_hostname + '/rest/api/3/attachment/content/' + data.attachmentId
  },
  data: {
    vulnId: data.vulnId
  }
};
```

* **Response Script**:

```javascript
if (response.headers && response.headers['Location']) {
  return {
    decision: {
      status: 'continue',
      message: 'Download attachment',
    },
    data: {
      downloadLink: response.headers['Location'],
      vulnId: data.vulnId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error getting download link',
    }
  };
}
```

**Action 5 - Download JIRA Attachment**

* **Method**: GET
* **Options**:
  * Download Response: Yes
* **Request Script**:

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

return {
  decision: {
    status: 'continue',
    message: 'Download attachment',
  },
  request: {
    url: data.downloadLink
  },
  data: {
    vulnId: data.vulnId
  }
};
```

* **Response Script**:

```javascript
if (response.fileId) {
  return {
    decision: {
      status: 'continue',
      message: 'Downloaded attachment',
    },
    data: {
      fileId: response.fileId,
      vulnId: data.vulnId
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error downloading attachment',
    }
  };
}
```

**Action 6 - Upload File to Vulnerability**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/vulnerability/{id}/evidence
* **Headers**:
  * Key = Content-Type; Type = Value; Value = multipart/form-data
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_user\_key
* **Request Script**:

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

return {
  decision: {
    status: 'continue',
    message: 'Uploading attachment to vulnerability',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability/' + data.vulnId + '/evidence',
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileId
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.status === 'File Uploaded') {
  return {
    decision: {
      status: 'finish',
      message: 'File uploaded to vulnerability',
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error uploading file to vulnerability',
    }
  };
}
```
