# 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="/files/WkwFpHQn7PjJMuRvcerL" 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](/attackforge-enterprise/modules/projects.md)
  * 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="/files/E7vq0JgHmbWmqObmQLlj" 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="/files/YNYqPuUnC5Kd0ojTh4WO" 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="/files/FQtFfl0Ik0OdMCjAPqG3" 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="/files/hCpRpX4GwJNbQ1iQ7ymy" 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="/files/p6idnfH4UtYOzAEmlwBg" 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="/files/Pfs7ruL5Ce0NSzq5Dddd" 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',
    }
  };
}
```


---

# 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/atlassian-jira.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.
