# Bugcrowd

## Create Vulnerability on Bugcrowd Submission

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2Fw0IlkST87llZw0uPGthu%2Fbugcrowd-1.png?alt=media&#x26;token=25e052a8-ea10-4aa3-87f9-969415128b70" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a vulnerability on a new a Bugcrowd submission.

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

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * af\_project\_id - your AttackForge Project Id
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get Bugcrowd Submission**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=target,file\\_attachments\\&fields\\[target]=name,category\\&fields\\[submission]=remediation\\_advice,description,bug\\_url,severity,source,state,title,vrt\\_id,vulnerability\\_references>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
// Validate HMAC
const bcSecret = secrets.bugcrowd_secret;
const raw_digest = data.headers['X-Bugcrowd-Digest'];
const raw_body = data.body || '';

if (!raw_digest || !(raw_digest =~ m/timestamp=[0-9]+;sha256=[a-f0-9]+/i)) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing or invalid X-Bugcrowd-Digest'
    }
  };
}

const at_time = String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$1');
const expected_digest =  String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$2');
const payloadHMAC = 'sha256=' + String.toLowerCase(String.hmac(raw_body + at_time, bcSecret, 'SHA256','base16'));

if (expected_digest !== payloadHMAC) {
  Logger.error('Invalid HMAC, aborting process.');
  
  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

if (data.jsonBody.included?[0].id) {
  let options = '?include=target,file_attachments';
  options = options + '&fields[target]=name,category';
  options = options + '&fields[submission]=remediation_advice,description,bug_url,severity,source,state,title,vrt_id,vulnerability_references';
  
  return {
    decision: {
      status: 'continue',
      message: 'Fetching Bugcrowd Submission data with submission id',
    },
    request: {
      url: 'https://api.bugcrowd.com/submissions/' + data.jsonBody.included[0].id + options
    }
  };
}
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing Bugcrowd Submission Id',
    }
  };
}
```

* **Response Script**:

```javascript
const responseBody = response?.jsonBody;

if (!responseBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response?.jsonBody:', JSON.stringify(response?.jsonBody));
  }
  return {
    desicison: {
      status: 'abort',
      message: 'No jsonBody found from response'
    }
  };
}

if (response.headers['Content-Type'] !== 'application/json; charset=utf-8') {
  return {
    desicison: {
      status: 'abort',
      message: 'Content-Type is expected to be application/json; charset=utf-8'
    }
  };
}

if (responseBody?.data?.attributes?.state !== 'new') {
  return {
    desicison: {
      status: 'abort',
      message: 'The submission state must be "new"'
    } 
  };
}

if (response.statusCode === 200) {
  Logger.info('Successful submission data fetch');
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved Bugcrowd Submission'
    },
    data: responseBody
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving Bugcrowd Submission'
    },
  };
}
```

**Action 2 - Prepare Vuln Data from Bugcrowd Submission**

* **Script**:

```javascript
let cwe_id;

return {
  decision: {
    status: 'continue',
  },
  data: {
    vulnerability: prepareInputData(data),
    cwe_id: cwe_id
  }
};

// Preparing Bugcrowd data for Vulnerability Creation
function prepareInputData (submissionFetched) {
  if (!submissionFetched?.data?.attributes) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Missing data.attribute for Vulnerability Submission Details');
    }
    
    return {
      decision: {
        status: 'abort',
        message: 'Missing data.attribute for Vulnerability Submission Details'
      }
    };
  }

  let vuln = {};
  
  vuln.projectId = secrets.af_project_id;
  vuln.import_source = "Bugcrowd";
  vuln.file_attachment = [];
  vuln.affected_assets = [];
  
  let affected_assets_obj = {};
  affected_assets_obj.components = [];
  let asset_components_obj = {};

  const submission = submissionFetched.data.attributes;
  
  asset_components_obj.name = submission.bug_url ? submission.bug_url : '';
  if (secrets.logging_level === 'debug') {
    Logger.debug('submission.bug_url:', submission?.bug_url);
  }
  
  if (submission.title){
    vuln.title = submission.title;
  }
  
  // if vrt_id exists - overwrite title
  if (submission.vrt_id) {
    const title_raw = String.replaceAll(String.replaceAll(submission.vrt_id, '_', ' '), '.', ' - ');
    const split_title = String.split(title_raw, ' ');
    if (secrets.logging_level === 'debug') {
      Logger.debug('split_title:', split_title);
    }
    
    let final_title = '';
    
    for (let i = 0; i < Array.length(split_title); i++){
      const split = split_title[i];
      if (split && split[0] && split[0] =~ m/[a-z]/){
        const first_letter_capital = String.toUpperCase(split[0]);
        final_title = final_title 
          + first_letter_capital 
          + String.substring(split_title[i], 1, split_title[i].length) 
          + ' ';
      }
      else {
        final_title = final_title + ' ' + split_title[i] + ' ';
      }
    }
    
    vuln.title = String.trim(final_title);
    if (secrets.logging_level === 'debug') {
      Logger.debug('vuln.title:', vuln.title);
    }
    
    vuln.original_title = submission.title;
  }

  if (submission.description){
    vuln.description = submission.description;
  }

  if (submission.vulnerability_references) {
    // Retrieve CWE
    const formatted_refs = formatVulnReferences(submission.vulnerability_references);
    const new_line_formatted = Array.join(formatted_refs.formatted_vuln_refs, '\n');
    vuln.description = vuln.description 
      + '\n\n' 
      + '<strong>Vulnerability References</strong>' 
      + '\n' 
      + new_line_formatted;
    cwe_id = formatted_refs.cwe_id ? formatted_refs.cwe_id : '';
  }

  if (submission.remediation_advice) {
    vuln.remediation_recommendation = submission.remediation_advice;
  }

  // Bugcrowd severity mapping
  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };
  
  if (submission.severity){
    vuln.severity = severityMap[submission.severity] || 'Info';
  } else {
    vuln.severity = 'Info';
  }

  if (data.data?.id && data.data.links?.self) {
    vuln.custom = {
      bugcrowd_submission_id: data.data.id,
      bugcrowd_submission_url: 'https://tracker.bugcrowd.com/' + secrets.bugcrowd_engagement_name + data.data.links.self
    };
  }

  if (data.included){
    for (let i = 0; i < data.included.length; i++){
      if (data.included?[i].type === 'target' 
        && data.included[i].attributes?.name 
        && data.included[i].attributes.category
      ) {
        affected_assets_obj.assetName = data.included[i].attributes?.name;
        affected_assets_obj.tags = [];
        
        Array.push(affected_assets_obj.tags, data.included[i].attributes.category);
      }
      if (data.included?[i].type === 'file_attachment' 
        && data.included[i].attributes?.file_name 
        && data.included[i].attributes.file_type 
        && data.included[i].attributes.download_url
      ) {
        const s3_signed_url = data.included[i].attributes.s3_signed_url;
        const download_url = s3_signed_url ? s3_signed_url : data.included[i].attributes.download_url;
        const file_name = data.included?[i].attributes?.file_name;
        
        Array.push(vuln.file_attachment, {
          file_name: file_name,
          download_url: download_url
        });
      }
    }
  }

  // Push Vuln Asset Objects
  if (secrets.logging_level === 'debug') {
    Logger.debug('Vuln object created:', JSON.stringify(vuln));
  }
  Array.push(affected_assets_obj.components, asset_components_obj);
  Array.push(vuln.affected_assets, affected_assets_obj);

  return vuln;
}

function formatVulnReferences (ref_str){
  if (!ref_str) {
    return [];
  }
  
  const lines = String.split(ref_str, '\n');
  const result = [];

  for (let i = 0; i < Array.length(lines); i++){
    let line = lines[i];
    line = String.replaceAll(line, '* ', '');
    
    if (String.includes(line, 'https://cwe.mitre.org/data/definitions/')) {
      const cwe_url = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '$2');
      cwe_id = String.split(cwe_url, 'https://cwe.mitre.org/data/definitions/')[1];
    }
    
    line = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>');
    Array.push(result, line);
  }
  
  return {
    formatted_vuln_refs: result,
    cwe_id: cwe_id
  };
}
```

**Action 3 - Get CWE Information**&#x20;

* **Method**: GET
* **URL**: <https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}>
* **Request Script**:

```javascript
if (data?.cwe_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching CWE [ ' + data.cwe_id + ' ] details',
    },
    request: {
      url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + data.cwe_id
    },
    data: {
      vulnerability: data.vulnerability
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No CWE found. Skip fetching CWE details.',
    }
  };
}
```

* **Response Script**:

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

if (response.statusCode === 200 && response?.jsonBody?.Weaknesses[0]) {
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved CWE details'
    },
    data: {
      vulnerability: data.vulnerability,
      cwe: response?.jsonBody?.Weaknesses[0]
    }
  };
}

else if (response.statusCode === 404){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Failed retreiving CWE details, the provided cwe_id may not be recommended for vulnerability mapping.'
    },
    data: data
  };
}

else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving CWE details'
    },
  };
}
```

**Action 4 - Create Vulnerability**

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

```javascript
if (!data.vulnerability) {
  return {
    decision: {
      status: 'abort',
      message: 'Bugcrowd Submission data does not exist on data.vulnerability',
    }
  };
}

const submission = data.vulnerability;
const cwe = data.cwe;

const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability';

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

function buildRequestBody (submission, cwe) {
  const vuln = {
    projectId: submission.projectId, 
    title: "New Submission",
    affected_assets: submission.affected_assets,
    priority: submission.severity,
    likelihood_of_exploitation: 1,
    description: "",
    attack_scenario: "TBD",
    remediation_recommendation: "TBD",
    steps_to_reproduce: "TBD",
    tags: [],
    is_visible: true,
    import_source: "TBD",
    custom_fields: []
  };

  // custom field source to update comments
  Array.push(vuln.custom_fields, {
    key: 'source',
    value: 'Bugcrowd'
  });

  if (submission.import_source){
    vuln.import_source = submission?.import_source;
  }

  if (submission.custom?.bugcrowd_submission_id && submission.custom?.bugcrowd_submission_url) {
    Array.push(vuln.custom_fields, {
      key: 'bugcrowd_submission_id',
      value: submission.custom.bugcrowd_submission_id
    });
    Array.push(vuln.custom_fields, {
      key: "bugcrowd_submission_url",
      value: submission.custom.bugcrowd_submission_url
    });
  }

  if (submission.target?.name && submission.target.category) {
    Array.push(vuln.custom_fields, {
      key: 'target_name',
      value: submission.target.name
    });
    Array.push(vuln.custom_fields, {
      key: 'target_category',
      value: submission.target.category
    });
  }

  if (submission.tags && Array.isArray(submission.tags)) {
    for (let i = 0; i < submission.tags.length; i++){
      Array.push(vuln.tags, submission.tags[i]);
    }
  }

  if (submission.remediation_recommendation) {
    vuln.remediation_recommendation = submission.remediation_recommendation + '\n';
  }

  if (cwe) {
    if (cwe.ID) {
      const name = cwe;
      Array.push(vuln.tags, 'CWE-' + cwe.ID);
    }
    if (cwe.Name) {
      const name = cwe.Name;
      vuln.title = name;
    }
    if (cwe.Description) {
      let description = cwe.Description;
      if (secrets.logging_level === 'debug') {
        Logger.debug('description', description);
      }
      vuln.description = vuln.description + formatVulnInfo(description) + '\n';
    }
    if (cwe.ExtendedDescription) {
      let extendedDescription = cwe.ExtendedDescription;
      vuln.description = vuln.description + ' ' + formatVulnInfo(extendedDescription) + '\n';      
    }
    if (cwe.BackgroundDetails) {
      const backgroundDetails = cwe.BackgroundDetails;
      vuln.attack_scenario = "";
      if (Array.isArray(backgroundDetails)) {
        for (let x = 0; x < backgroundDetails.length; x++) {
          const attackScenario = backgroundDetails[x];
          vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(attackScenario);
        }
      }
      else {
        vuln.attack_scenario = vuln.attack_scenario + formatVulnInfo(backgroundDetails);
      }
    }
    if (cwe.PotentialMitigations) {
      const potentialMitigations = cwe.PotentialMitigations;
      if (vuln.remediation_recommentation === "TBD"){
        vuln.remediation_recommendation = "";
      }
      if (Array.isArray(potentialMitigations)) {
        for (let x = 0; x < potentialMitigations.length; x++) {
          const mitigation = potentialMitigations[x];
          if (mitigation.Description) {
            vuln.remediation_recommendation = vuln.remediation_recommendation 
              + formatVulnInfo(mitigation.Description);
          } 
        }
      }
      else {
        if (potentialMitigations.Description) {
          vuln.remediation_recommendation = vuln.remediation_recommendation 
            + formatVulnInfo(potentialMitigations.Description);
        }
      }
    }
    if (submission.title && submission.vulnerability_information) {
      vuln.steps_to_reproduce = '<p>' 
        + submission.title + '</p><p>' 
        + formatVulnInfo(submission_information) 
        + '</p>';
    }
  }
  else {
    if (submission.title) {
      vuln.title = submission.title;
    }
    if (submission.vulnerability_information) {
      vuln.description = formatVulnInfo(submission.vulnerability_information);
    }
  }

  let description = '';
  if (submission.original_title) {
    description = description + 'Bugcrowd Submission Title: ' + submission.original_title + '\n';
  }
  if (submission.description) {
    description = description + '\nBugcrowd Description: ' + submission.description;
  }
  
  vuln.description = vuln.description + description + '\n';

  if (secrets.logging_level === 'debug') {
    Logger.debug('Vuln payload:', JSON.stringify(vuln));
  }

  return vuln;
}

function formatVulnInfo (value) {
  if (String.startsWith(value, "\n\n")) {
    value = String.substring(value, 2);
  }
  else if (String.startsWith(value, "\n")) {
    value = String.substring(value, 1);
  }

  value = String.replaceAll(value, "\n\n", "<br/>");
  value = String.replaceAll(value, "\n", "<br/>");
  
  return value;
};
```

* **Response Script**:

```javascript
let body;

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

if (data?.vulnerability?.file_attachment && Array.length(data.vulnerability.file_attachment) > 0 && response?.jsonBody?.vulnerability?.vulnerability_id){
  return {
    decision: {
      status: 'continue',
      message: 'File attachment found, continue.'
    },
    data: {
      files: data.vulnerability.file_attachment,
      vulnId: response?.jsonBody?.vulnerability?.vulnerability_id
      }
  };
}

if (Array.length(data.vulnerability.file_attachment) === 0 && response.statusCode === 200) {
  return {
    decision: {
      status: 'finish',
      message: 'Created Vulnerability'
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed creating Vulnerability'
    },
  };
}
```

**Action 5 - Download Evidence from Bugcrowd Submission**

* **Method**: GET
* **URL**: <https://defined-in-script>
* **Request Script**:

```javascript
const fileIds = data.fileIds || [];

if (!data.retry) {
  data.retry = 0;
}

if (data.files && Array.length(data.files) > 0) {
  const files = data.files;

  if(!data.iteration) {
    data.iteration = 0;
  }

  let i = data.iteration;
  
  if (i < Array.length(files)){
    return {
      decision: {
        status: 'continue',
        message: 'Downloading next attachment'
      },
      request: {
        url: files[i].download_url
      },
      data: {
        vulnId: data.vulnId,
        iteration: i,
        count: Array.length(files),
        files: files,
        fileIds: fileIds,
        retry: data.retry
      }
    }; 
  }
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'No more file to fetch, proceeding to next step.'
    },
    data: {
      vuln_id: data.vuln_id,
      files: files
    }
  }; 
}

```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  const file_name = data.files?[data.iteration].file_name;
  Logger.error('Failed downloading file', file_name);
  
  if (secrets.logging_level === 'debug') {
    Logger.debug(JSON.stringify(response));
  }

  if (!data.retry) {
    data.retry = 0;
  }
  
  if (data.retry < 3) {
    data.retry = data.retry + 1;
    return {
      decision: {
        status: 'repeat',
        message: 'Retrying to download file: ' + file_name,
        delay: 1000
      },
      data: data
    };
  }

  if (Array.length(data.files) >= data.iteration + 1){
    data.iteration = data.iteration + 1;
    data.retry = 0;
    
    return {
      decision: {
        status: 'repeat',
        message: 'Failed downloading file: ' + file_name + ', continuing to next file.'
      },
      data: data
    };
  } else {
    return {
      decision: {
        status: 'continue',
        message: 'Failed downloading file: ' + file_name + ', please check error logs. Proceeding to to upload step.'
      },
      data: data
    };
  }
}

const nextIteration = data.iteration + 1;
let fileIds = data.fileIds;
let fileId = response.fileId;
if (secrets.logging_level === 'debug') {
  Logger.info("Iteration:", nextIteration, JSON.stringify(fileIds));
}

Array.push(fileIds, {
  name: data.files[data.iteration].file_name, 
  fileId: fileId
});

if (nextIteration < data.count){
  data.iteration = nextIteration;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next file attachment.'
    },
    data: data
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'Completed fetching attachments, proceeding to next step.'
    },
    data: {
      fileIds: fileIds,
      vulnId: data.vulnId
    } 
  };
}
```

**Action 6 - Upload Vulnerability Evidence**&#x20;

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

```javascript
if (!data.fileIds) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.fileIds',
    }
  };
}
if (!data.vulnId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnId',
    }
  };
}
if (data.fileIds.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No file to upload. Exiting process.',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vulnId + '/evidence';

return {
  decision: {
    status: 'continue',
    message: 'Uploading evidence to Vulnerability',
  },
  request: {
    url: url,
    multipart: {
      fields: [
        {
          name: 'file',
          fileId: data.fileIds[0].fileId
        }
      ]
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200 || response.jsonBody?.status === 'error'){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while uploading evidence: ' + data.fileIds[0] + '. error: ' + response?.jsonBody?.error
    }
  };
}

Array.shift(data.fileIds);

if (Array.length(data.fileIds) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Next attachment found, proceeding to uploading.'
    },
    data: data
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No attachments left to upload. Exiting flow.'
    }
  };
}
```

## Update Vulnerability on Bugcrowd Submission Update

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FAhS21hmYaP3uf0SHtFB7%2Fbugcrowd-2.png?alt=media&#x26;token=7074bead-df2e-45b0-a54c-15a14c142e77" alt=""><figcaption></figcaption></figure>

The purpose of this example is to update a vulnerability when a Bugcrowd submission is updated.

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

* **HTTP Trigger**
  * **Method:** POST
  * **Authentication**: None
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Determine Update Details**

* **Script**:

```javascript
// Validate HMAC
const bcSecret = secrets.bugcrowd_secret;
const raw_digest = data.headers['X-Bugcrowd-Digest'];
const raw_body = data.body || '';

if (!raw_digest || !(raw_digest =~ m/timestamp=[0-9]+;sha256=[a-f0-9]+/i)) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing or invalid X-Bugcrowd-Digest'
    }
  };
}

const at_time = String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$1');
const expected_digest =  String.replaceAll(raw_digest, m/timestamp=([0-9]+);(sha256=[a-f0-9]+)/g, '$2');
const payloadHMAC = 'sha256=' + String.toLowerCase(String.hmac(raw_body + at_time, bcSecret, 'SHA256','base16'));

if (expected_digest !== payloadHMAC) {
  Logger.error('Invalid HMAC, aborting process.');
  return {
    decision: {
      status: 'abort',
      message: 'Invalid HMAC',
    }
  };
}

let changes_keys;
let changed_type;
let changed_to = {};
let bc_sub_id;
let cwe_id;
const bugcrowd_submission_url_name = secrets.bugcrowd_engagement_name;

if (!data.jsonBody){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No jsonBody found in data.',
    }
  };
}

const changes = data.jsonBody?.data?.attributes?.data?.changes;

if (!changes){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data.jsonBody?.data?.attributes?.data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No data.jsonBody?.data?.attributes?.data?.changes found in data.',
    }
  };
}

changes_keys = Object.keys(changes);

if (data.jsonBody?.data?.relationships?.resource?.data?.type === 'submission'){
  bc_sub_id = data.jsonBody.data.relationships.resource.data.id;
}

if (!bc_sub_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Bugcrowd submission id not found, aborting the process.'
    }
  };
}

if (Array.length(changes_keys) === 0) {
  Logger.error('Error: No changes found in data');
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(data?.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Event does not contain recognised change from Bugcrowd submission. Please check the log.'
    }
  };
}

// Case 1 - target change
if (determineUpdateType(['target_id'], changes_keys)){
  Logger.info('Found change type: Target');
  changed_type = 'target';
  if (changes.target_id?.to){
    changed_to.target_id = changes.target_id.to;
  }
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('changes.target_id: ', JSON.stringify(changes.target_id));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Changed target does not have an id, aborting the update.'
      }
    };
  }
}

// Case 2 - bug_url change
else if (determineUpdateType(['encrypted_bug_url'], changes_keys)){
  Logger.info('Found change type: bug_url');
  changed_type = 'bug_url';
  if (data.jsonBody?.included?[0].attributes?.bug_url){
    changed_to.component_name = data.jsonBody.included[0].attributes.bug_url;
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug(JSON.stringify(data?.jsonBody));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Updated bug_url not found.'
      }
    };
  }
}

// Case 3 - remediation advice change
else if (determineUpdateType(['remediation_advice'], changes_keys)){
  Logger.info('Found change type: remediation_advice');
  changed_type = 'remediation_advice';
  if (changes.remediation_advice?.to && changes.remediation_advice?.from){
    changed_to.remediation_recommendation = changes.remediation_advice.to;
    changed_to.remediation_recommendation_old = changes.remediation_advice.from;
  }
}

// Case 4 - References change
else if (determineUpdateType(['vulnerability_references'], changes_keys)){
  Logger.info('Found change type: vulnerability_references');
  if (changes.vulnerability_references?.to){
    changed_type = 'vulnerability_references';
    changed_to.vulnerability_references = changes.vulnerability_references.to;
  }
}

// Case 5 - CVSS change
else if (determineUpdateType(['cvss_vector_id', 'severity'], changes_keys)){
  changed_type = 'cvss';
  if (changes.cvss_vector_id?.to){
    changed_to.cvss_vector_id = changes.cvss_vector_id.to;
  }
  if (changes.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 6 - Marked as vrt update
else if (determineUpdateType(['vrt_id', 'cvss_vector_id', 'severity'], changes_keys) ||
  determineUpdateType(['vrt_id', 'cvss_vector_id'], changes_keys) ||
  determineUpdateType(['vrt_id', 'severity'], changes_keys)){

  changed_type = 'vrt';
  if (changes.vrt_id?.to){
    changed_to.vrt_id = convertVrtToTitleStyle(changes.vrt_id.to);
  }

  if (data.jsonBody?.included && data.jsonBody?.included[0]){
    const included = data.jsonBody?.included[0];
    if (included?.attributes){
      if (included.attributes?.remediation_advice){
        changed_to.remediation_recommendation = included.attributes?.remediation_advice;
      }
      if (included.attributes?.vulnerability_references){
        changed_to.vulnerability_references = included.attributes?.vulnerability_references;
        // Retrieve CWE ID if it exists
        const formatted_refs = findCweIdFromVulnRef(included.attributes?.vulnerability_references);
        const cwe_id = formatted_refs?.cwe_id;
        if (cwe_id){
          changed_to.cwe_id = cwe_id;
        }
      }
    }
  }
  if (changes.cvss_vector_id?.to){
    changed_to.cvss_vector_id = changes.cvss_vector_id.to;
  }
  if (changes.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 7 - Marked as duplicate update
else if (determineUpdateType(['vrt_id', 'duplicate', 'cvss_vector_id', 'duplicate_of_id', 'severity'], changes_keys) || determineUpdateType(['vrt_id', 'duplicate', 'cvss_vector_id', 'duplicate_of_id'], changes_keys)){
  Logger.info('Found change type: marked as duplicate.');
  changed_type = 'duplicate';
  if (changes?.duplicate_of_id?.to){
    changed_to.duplicate_of_id = changes.duplicate_of_id.to;
  }
}

// Case 8 - Title Change
else if (determineUpdateType(['title'], changes_keys)){
  changed_type = 'title';
  if (changes?.title?.to && changes?.title?.from){
    changed_to.title = changes.title.to;
    changed_to.title_old = changes.title.from;
  }
}

// Case 8 - Severity Change
else if (determineUpdateType(['severity'], changes_keys)){
  changed_type = 'severity';
  if (changes?.severity?.to){
    changed_to.severity = changes.severity.to;
  }
}

// Case 8 - Description Change
else if (determineUpdateType(['encrypted_description'], changes_keys)){
  changed_type = 'description';
  if (data.jsonBody?.included[0].attributes?.description){ 
    changed_to.description = data.jsonBody?.included[0].attributes?.description;
    changed_to.remediation_recommendation = data.jsonBody?.included[0].attributes?.remediation_advice;
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Found update details, proceeding to next step.',
  },
  data: {
    changed_type: changed_type,
    changed_to: changed_to,
    bc_sub_id: bc_sub_id,
    bugcrowd_submission_url_name: bugcrowd_submission_url_name
  },
};

function determineUpdateType(updateKeys, incomingKeys) {
  if (Array.length(updateKeys) !== Array.length(incomingKeys)) return false;
  for (let i = 0; i < Array.length(incomingKeys); i++) {
    if (!Array.includes(updateKeys, (incomingKeys[i]))) {
      return false;
    }
  }
  return true;
}

function convertVrtToTitleStyle(vrt){
  const changed_vrt_title = String.replaceAll(String.replaceAll(vrt, '_', ' '), '.', ' - '); 
  const split_title = String.split(changed_vrt_title, ' ');
  let final_title = '';
  
  for (let i = 0; i < split_title.length; i++){
    const split = split_title[i];
    if (split && split[0] && split[0] =~ m/[a-z]/){
      const first_letter_capital = String.toUpperCase(split[0]);
      final_title = final_title 
        + first_letter_capital 
        + String.substring(split_title[i], 1, split_title[i].length) 
        + ' ';
    }
    else {
      final_title = final_title + ' ' + split_title[i] + ' ';
    }
  }
  return String.trim(final_title);
}

function findCweIdFromVulnRef (ref_str){
  if (!ref_str) {
    return {cwe_id: ''};
  }
  
  const lines = String.split(ref_str, '\n');
  const result = [];

  for (let i = 0; i < Array.length(lines); i++){
    let line = lines[i];
    line = String.replaceAll(line, '* ', '');
    
    if (String.includes(line, 'https://cwe.mitre.org/data/definitions/')) {
      const cwe_url = String.replaceAll(line, m/\[([^\]]+)\]\(([^)]+)\)/g, '$2');
      cwe_id = String.split(cwe_url, 'https://cwe.mitre.org/data/definitions/')[1];

      // fallback cwe_id
      const raw = (cwe_url && cwe_url !== line) ? cwe_url : line;
      const id = String.replaceAll(raw, m/.*definitions\/([0-9]+).*/g, '$1');

      if (id && id =~ m/^[0-9]+$/){
        cwe_id = id;
      }
    }
  }
  return {
    cwe_id: cwe_id
  };
}
```

**Action 2 - Get Bugcrowd Target Information**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=target,cvss\\_vector>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.changed_type !== 'target' && data.changed_type !== 'bug_url'){
  return {
    decision: {
      status: 'next',
      message: 'Update is not on target or bug_url, proceeding to next step.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching the Bugcrowd submission with id: '+ data.bc_sub_id
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions/' + data.bc_sub_id + '?include=target,cvss_vector'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching Bugcrowd submission id: ' + data.bc_sub_id,
    }
  };
}

let cvss_vector_string;
const included = response.jsonBody?.included;

if (included) {
  for (let i = 0; i < Array.length(included); i++){
    if (included?[i].type === 'target' && included[i].id === data.changed_to?.target_id){
      if (included[i].attributes?.name && included[i].attributes.category){
        data.changed_to.target_name = included[i].attributes.name;
        data.changed_to.target_category = included[i].attributes.category;
      } 
      else {
        if (secrets.logging_level){
          Logger.debug(included?[i].attributes);
        }
        return {
          decision: {
            status: 'abort',
            message: 'Error: included[i].attributes must contain name and category of the Bugcrowd target.'
          }
        };
      }
    }
  }
}

if (response.jsonBody?.data?.attributes){
  const sub_attributes = response.jsonBody.data.attributes;
  if (sub_attributes.bug_url && data?.changed_type !== 'bug_url'){
    data.changed_to.component_name = sub_attributes.bug_url;
  }
  if (sub_attributes.title){
    data.changed_to.original_title = sub_attributes.title;
  }
  if (sub_attributes.description){
    data.changed_to.description = sub_attributes.description;
  }
  if (sub_attributes.remediation_recommendation){
    data.changed_to.remediation_recommendation = sub_attributes.remediation_recommendation;
  }
  if (sub_attributes.severity){
    data.changed_to.severity = sub_attributes.severity;
  }
}

// Prepare creating new vulnerability.
function prepareInputData (input){
  let vuln = {};
  vuln.import_source = 'Bugcrowd';
  vuln.file_attachment = [];
  vuln.affected_assets = {};
  vuln.tags = [];

  if (response.jsonBody?.vulnerabilities?.vulnerability_title){
    vuln.title = response.jsonBody.vulnerabilities.vulnerability_title;
  }
  if (input?.changed_to?.description){
    vuln.description = data.changed_to.original_title + '\n' + input.changed_to.description;
  }
  if (input?.changed_to?.target_name){
    vuln.affected_assets.assetName = input.changed_to.target_name;
  }
  if (input?.changed_to?.target_category){
    vuln.affected_assets.components_tag = input.changed_to.target_category;
  }

  // Bugcrowd bug_url update
  if (input?.changed_to?.component_name){
    vuln.affected_assets.component_name = input.changed_to.component_name;
    const target_information = Array.find(included, findTargetInfo)?.attributes;
    if (target_information?.name && target_information.category){
      vuln.affected_assets.assetName = target_information.name;
      vuln.affected_assets.components_tag = target_information.category;
    } 
    else {
      if (secrets.logging_level){
        Logger.debug('target_information', target_information);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: target_information must contain name and category of the Bugcrowd target.'
        }
      };
    }
  }

  if (response.jsonBody?.data?.attributes?.remediation_advice){
    vuln.remediation_recommendation = response.jsonBody.data.attributes.remediation_advice;
  } 
  else {
    if (secrets.logging_level){
      Logger.debug('response.jsonBody?.data?.attributes', response.jsonBody.data?.attributes);
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: response.jsonBody?.data?.attributes must contain remediation_advice.'
      }
    };
  }

  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };
  
  if (input.changed_to.severity){
    vuln.severity = severityMap[input.changed_to.severity] || 'Info';
  } 
  else {
    vuln.severity = 'Info';
  }
  
  if (data.bc_sub_id && data.bugcrowd_submission_url_name) {
    vuln.custom = {
      bugcrowd_submission_id: data.bc_sub_id,
      bugcrowd_submission_url: 'https://tracker.bugcrowd.com/'+ data.bugcrowd_submission_url_name + '/submissions/' + data.bc_sub_id
    };
  }

  const cvss = Array.find(included, findCvssVectorData);
  if (cvss && cvss.attributes){
    cvss_vector_string = 'CVSS:' 
      + cvss.attributes.version
      + '/AV:' + cvss.attributes.attack_vector
      + '/AC:'+ cvss.attributes.attack_complexity
      + '/PR:'+ cvss.attributes.privileges_required
      + '/UI:'+ cvss.attributes.user_interaction
      + '/S:' + cvss.attributes.authorization_scope
      + '/C:' + cvss.attributes.confidentiality_impact
      + '/I:' + cvss.attributes.integrity_impact
      + '/A:' + cvss.attributes.availability_impact
      + ' [' + cvss.attributes.score + ']';
    Array.push(vuln.tags, cvss_vector_string);
  }
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln', vuln);
  }
  return vuln;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched Bugcrowd Submission and new target details.',
  },
  data: {
    changed_type: data?.changed_type,
    bc_sub_id: data?.bc_sub_id,
    vulnerability: prepareInputData(data)
  },
};

function findCvssVectorData (array){
  return array.type === 'cvss_vector';
}

function findTargetInfo (array){
  return array.type === 'target';
}
```

**Action 3 - Find Original Vulnerability**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"bugcrowd\_submission\_id"},value:{$eq:{bc\_sub\_id}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (!data?.bc_sub_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Missing bc_sub_id from data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing bugcrowd submission id, aborting.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability from AttackForge with bugcrowd_submission_id.',
  },
  request: {
    url: 'https://'+ secrets.af_tenant +'/api/ss/vulnerabilities?q={custom_fields:{$elemMatch:{name:{$eq:"bugcrowd_submission_id"},value:{$eq:"' + data.bc_sub_id + '"}}}}&limit=1',
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching AttackForge Vulnerability, please check the bugcrowd_submission_id in data.',
    }
  };
}

const original_vuln = response.jsonBody?.vulnerabilities?[0];
if (!original_vuln){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response?.jsonBody?.vulnerabilities));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: original_vuln must be present under response.jsonBody?.vulnerabilities?[0].',
    }
  };
}
else if (!original_vuln.vulnerability_id){
  return {
    decision: {
      status: 'abort',
      message: 'Error while retrieving the value of vulnerability_id.',
    }
  };
}

const fileId = [];

// General Fields from Vulnerability
if (original_vuln?.vulnerability_id){
  data.vuln_id = original_vuln.vulnerability_id;
}
if (original_vuln?.vulnerability_project_id){
  data.project_id = original_vuln.vulnerability_project_id;
}
if (original_vuln?.vulnerability_description_html){
  data.vuln_description = original_vuln.vulnerability_description_html;
}
if (original_vuln?.vulnerability_remediation_recommendation_html){
  data.remediation_recommendation = original_vuln.vulnerability_remediation_recommendation_html;
}
if (original_vuln?.vulnerability_attack_scenario_html){
  data.attack_scenario = original_vuln.vulnerability_attack_scenario_html;
}

// TODO: Is this needed?
if (data.changed_type === 'remediation_advice' && original_vuln?.vulnerability_attack_scenario){
  data.attack_scenario = original_vuln?.vulnerability_attack_scenario;
}

if (original_vuln?.vulnerability_title !== data.title){
  data.title = original_vuln.vulnerability_title;
}

// Check if attachments exist
if (original_vuln?.vulnerability_evidence){
  const evidences = original_vuln.vulnerability_evidence;
  for (let i = 0; i < Array.length(evidences); i++){
    if (evidences?[i].file_id){
      Array.push(fileId, evidences[i].file_id);
    }
  }
}

if (secrets.logging_level === 'debug'){
  Logger.debug('fileId: ', fileId);
}
data.fileId = fileId || [];

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved the original vulnerability, proceeding to next step.',
  },
  data: data
};
```

**Action 4 - Get Bugcrowd CVSS Vector**&#x20;

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/submissions/{id}?include=cvss\\_vector>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.changed_type !== 'cvss' && data.changed_type !== 'vrt'){
  return {
    decision: {
      status: 'next',
      message: 'Change type not cvss or vrt. Skipping CVSS Vector String step.'
    }
  };
}

const bu_sub_id = data?.bc_sub_id;
if (!bu_sub_id){
  return {
    decision: {
      status: 'abort',
      message: 'Error: Bugcrowd submission id not provided.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Bugcrowd submission.',
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions/' + bu_sub_id + '?include=cvss_vector'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed to retrieve Bugcrowd submission data.'
    }
  };
}

if (!(response.jsonBody?.included?[0] && response.jsonBody.included[0].type === 'cvss_vector')){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response?.jsonBody?.included: ', JSON.stringify(response?.jsonBody?.included));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed to retrieve cvss_vector data from Bugcrowd submission. Missing response?.jsonBody?.included?[0] or response?.jsonBody?.included?[0].type'
    }
  };
}

let cvss_vector_string;
const cvss = response.jsonBody.included[0].attributes;
cvss_vector_string = 'CVSS:'
  + cvss.version
  + '/AV:' + cvss.attack_vector
  + '/AC:' + cvss.attack_complexity
  + '/PR:' + cvss.privileges_required
  + '/UI:' + cvss.user_interaction
  + '/S:' + cvss.authorization_scope
  + '/C:' + cvss.confidentiality_impact
  + '/I:' + cvss.integrity_impact
  + '/A:' + cvss.availability_impact
  + ' [' + cvss.score + ']';

if (secrets.logging_level === 'debug'){
  Logger.debug('cvss_vector_string:', cvss_vector_string);
}
data.cvss_vector_string = cvss_vector_string;

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved CVSS vector string: ' + cvss_vector_string,
  },
  data: data,
};
```

**Action 5 - Convert Remediation Note to Rich-Text**

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (data.changed_type && data.changed_type !== 'remediation_advice' && data.changed_type && data.changed_type !== 'vrt'){
  return {
    decision: {
      status: 'next',
      message: 'Change type not remediation_advice or vrt, proceeding to next step.'
    },
    data: data
  };
}

if (!data.changed_to?.remediation_recommendation){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.changed_to: ',JSON.stringify(data.changed_to));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: could not get changed detail of data.changed_to?.remediation_advice.'
    }
  };
}

const markdown_object = {
  rem_advice: String.replaceAll(data.changed_to?.remediation_recommendation, '\n', '\n\n')
};

if (data.changed_to.remediation_recommendation_old){
  markdown_object.rem_advice_old = String.replaceAll(data.changed_to?.remediation_recommendation_old, '\n', '\n\n');
}

return {
  decision: {
    status: 'continue',
    message: 'Converting markdown comment body to rich-text.'
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
    body: markdown_object
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while converting Markdown to RichText',
    }
  };
}

if (!response.jsonBody?.rem_advice){
    if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody: ',JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: converted remediation note body not found under response.jsonBody.',
    },
  };
}

data.changed_to.converted_rem_advice_body = response.jsonBody?.rem_advice;

if (response.jsonBody?.rem_advice_old){
  data.changed_to.converted_rem_advice_body_old = response.jsonBody.rem_advice_old;
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully converted Markdown remediation note body to RichText.',
  },
  data: data
};
```

**Action 6 - Get CWE information**

* **Method**: GET
* **URL**: <https://cwe-api.mitre.org/api/v1/cwe/weakness/{id}>
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
// If changed vulnerability_reference has cwe fetch information.
if (data?.changed_to?.cwe_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Fetching CWE [ ' + data.changed_to.cwe_id + ' ] details',
    },
    request: {
      url: 'https://cwe-api.mitre.org/api/v1/cwe/weakness/' + data.changed_to.cwe_id
    },
    data: data
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No CWE found. Skip fetching CWE details.',
    },
    data: data
  };
}
```

* **Response Script**:

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

if (response.statusCode === 200 && response?.jsonBody?.Weaknesses[0]) {
  data.changed_to.cwe = response?.jsonBody.Weaknesses[0];
  return {
    decision: {
      status: 'continue',
      message: 'Retrieved CWE details'
    },
    data: data
  };
}

else if (response.statusCode === 404){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Failed retreiving CWE details, the provided cwe_id may not be recommended for vulnerability mapping.'
    },
    data: data
  };
}

else {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: { 
      status: 'abort',
      message: 'Failed retrieving CWE details'
    },
  };
}
```

**Action 7 - Get Vuln Id for Duplicate**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"bugcrowd\_submission\_id"},value:{$eq:{bc\_sub\_id}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
if (data.changed_type !== 'duplicate') {
  return {
    decision: {
      status: 'next',
      message: 'Not a duplicate change, proceeding to next step.'
    },
    data: data
  };
}

if (data.changed_type === 'duplicate' && !data.changed_to?.duplicate_of_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Missing duplicate_of_id from data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing bugcrowd duplicate_of_id, aborting.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability from AttackForge with bugcrowd_submission_id.',
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?q={custom_fields:{$elemMatch:{name:{$eq:"bugcrowd_submission_id"},value:{$eq:"' + data.changed_to.duplicate_of_id + '"}}}}&limit=1'
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response:', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error: Something went wrong while retrieving the duplicated Vulnerability Id for Bugcrowd submission id: ' + data.changed_to?.duplicate_of_id,
    }
  };
}

const vuln = response.jsonBody?.vulnerabilities?[0];

if (vuln && vuln.vulnerability_id && vuln.vulnerability_title){
  data.duplicate_vuln_id = vuln.vulnerability_id;
  data.duplicate_vuln_title = vuln.vulnerability_title;
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody?.vulnerabilities:', JSON.stringify(response.jsonBody?.vulnerabilities));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error: response.jsonBody?.vulnerabilities[0] must exist and contain vulnerability_id and vulnerability_title.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully retrieved duplicate Vulnerability Id: ' + vuln.vulnerability_id,
  },
  data: data
};
```

**Action 8 - Update Vulnerability on AttackForge**

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

```javascript
if (!data.vuln_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: vulnerability_id is required to update vulnerability.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating vulnerability: ' + data.vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln_id,
    body: prepareUpdateBody(data)
  },
  data: data
};

// Prepare update body
function prepareUpdateBody(data){
  let body = {
    project_id : data?.project_id,
  };

  const severityMap = {
    '1': 'Critical',
    '2': 'High',
    '3': 'Medium',
    '4': 'Low',
    '5': 'Info'
  };

  if (data?.changed_type === 'target' || data?.changed_type === 'bug_url') {
    const affected_assets = data.vulnerability?.affected_assets;
    if (!affected_assets || !affected_assets?.assetName) {
      return {
        decision: {
          status: 'abort',
          message: 'Error: data?.vulnerability?.affected_assets and assetName is required to update target.',
        }
      };
    }

    body.affected_assets = [];

    let asset_obj = {
      assetName: affected_assets?.assetName,
      components: [
        {
          name: affected_assets?.component_name
        }
      ],
      tags: [
        affected_assets?.components_tag
      ]
    };

    if (secrets.logging_level === 'debug'){
      Logger.debug('asset_obj', JSON.stringify(asset_obj));
    }
    Array.push(body.affected_assets, asset_obj);
  }

  if (data.changed_type === 'remediation_advice' || data.changed_type === 'vrt'){
    if (data?.remediation_recommendation && data.changed_to?.converted_rem_advice_body && data.changed_to?.converted_rem_advice_body_old){
      // swap out
      const rem_rec_swapout = String.replace(data.remediation_recommendation, data.changed_to.converted_rem_advice_body_old, data.changed_to.converted_rem_advice_body);
      if (rem_rec_swapout === data.remediation_recommendation && data.changed_to?.remediation_recommendation_old){
        body.remediation_recommendation = formatVulnInfo(String.replace(data.remediation_recommendation, data.changed_to.remediation_recommendation_old, data.changed_to.converted_rem_advice_body));
      } else {
        body.remediation_recommendation = formatVulnInfo(rem_rec_swapout);
      }
    }
    if (data.vuln_description){
      body.description = formatReferences(data.vuln_description);
    }
    if (data.attack_scenario){
      body.attack_scenario = data.attack_scenario;
    }
  }

  if (data.changed_type === 'cvss' || data.changed_type === 'vrt'){
    if (!body.tags){
      body.tags = [];
    }
    Array.push(body.tags, data.cvss_vector_string);
  }

  if (data.changed_type === 'vrt'){
    body.title = data.changed_to.vrt_id;
  }

  if (data.changed_type === 'vulnerability_references' || data.changed_type === 'vrt') {
    const vuln_ref_split_string = '<strong>Vulnerability References</strong>\n';
    const preserved_description = String.split(data.vuln_description, vuln_ref_split_string)[0];

    if (!data.changed_to?.vulnerability_references){
      if (secrets.logging_level === 'debug'){
        Logger.debug('data.changed_to:', JSON.stringify(data.changed_to));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data.changed_to must contain vulnerability_references.',
        }
      };
    }

    const new_description = preserved_description 
      + vuln_ref_split_string 
      + data.changed_to.vulnerability_references
      + '\n';
    body.description = formatReferences(new_description);

    if (!body.remediation_recommendation && data.remediation_recommendation){
      body.remediation_recommendation = data.remediation_recommendation;
    }
    if (data.attack_scenario){
      body.attack_scenario = data.attack_scenario;
    }
  }

  if (data.changed_type === 'duplicate'){
    body.title = '[Duplicate] ' + data.title;
    if (data.duplicate_vuln_id && data.duplicate_vuln_title){
      body.description = 'This Vulnerability was marked as a Duplicate to AttackForge Vulnerability - <a href=' 
        + 'https://'
        + secrets.af_tenant 
        + '/projects/' 
        + data.project_id 
        + '/vulnerabilities/' 
        + data.duplicate_vuln_id 
        + '>' 
        + data.duplicate_vuln_title 
        + '</a>' 
        + ' on Bugcrowd.' 
        + '\n' 
        + data.vuln_description;
    } 
    else {
      if (secrets.logging_level === 'debug'){
        Logger.debug('data:', JSON.stringify(data));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data must contain duplicate_vuln_id and duplicate_vuln_title to populate description with duplicate details.',
        }
      };
    }
  }

  if (data.changed_type === 'title'){
    if (data.vuln_description && data.changed_to?.title && data.changed_to?.title_old){
      body.description = replaceVulnDescription(data.changed_type, data.vuln_description, data.changed_to?.title);
      if (data.remediation_recommendation){
        body.remediation_recommendation = data.remediation_recommendation;
      }
    } 
    else {
      if (secrets.logging_level === 'debug'){
        Logger.debug('data.changed_to: ', JSON.stringify(data.changed_to));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: data.changed_to must contain changed title.',
        }
      };
    }
  }

  if (secrets.logging_level === 'debug'){
    Logger.debug('body',JSON.stringify(body));
  }

  if (data.changed_to?.cwe) {
    const cwe = data.changed_to.cwe;
    if (cwe.ID) {
      const name = cwe;
      if (!body.tags){
        body.tags = [];
      }
      Array.push(body.tags, 'CWE-' + cwe.ID);
    }
    if (cwe.Name) {
      const name = cwe.Name;
      body.title = name;
    }
    if (cwe.Description) {
      let description = cwe.Description;
      if (secrets.logging_level === 'debug') {
        Logger.debug('description', description);
      }
      body.description = body.description + formatVulnInfo(description) + '\n';
    }
    if (cwe.ExtendedDescription) {
      let extendedDescription = cwe.ExtendedDescription;
      body.description = body.description + ' ' + formatVulnInfo(extendedDescription) + '\n';      
    }
    if (cwe.BackgroundDetails) {
      const backgroundDetails = cwe.BackgroundDetails;
      body.attack_scenario = "";
      if (Array.isArray(backgroundDetails)) {
        for (let x = 0; x < backgroundDetails.length; x++) {
          const attackScenario = backgroundDetails[x];
          body.attack_scenario = body.attack_scenario + formatVulnInfo(attackScenario);
        }
      }
      else {
        body.attack_scenario = body.attack_scenario + formatVulnInfo(backgroundDetails);
      }
    }
    if (cwe.PotentialMitigations) {
      const potentialMitigations = cwe.PotentialMitigations;
      body.remediation_recommendation = body.remediation_recommendation ? body.remediation_recommendation + '\n' : "";
      if (Array.isArray(potentialMitigations)) {
        for (let x = 0; x < potentialMitigations.length; x++) {
          const mitigation = potentialMitigations[x];
          if (mitigation.Description) {
            body.remediation_recommendation = body.remediation_recommendation 
              + formatVulnInfo(mitigation.Description);
          } 
        }
      }
      else {
        if (potentialMitigations.Description) {
          body.remediation_recommendation = body.remediation_recommendation 
            + formatVulnInfo(potentialMitigations.Description);
        }
      }
    }
  }

  if (data.changed_type === 'severity' || data.changed_to?.severity){
    if (data.changed_to?.severity){
      body.priority = severityMap[data.changed_to.severity] || 'Info';
    } else {
      body.priority = 'Info';
    }
  }

  if (data.changed_type === 'description'){
    if (data.changed_to?.remediation_recommendation){
      body.remediation_recommendation = data.changed_to?.remediation_recommendation;
    }
    if (data.changed_to?.description){
      body.description = replaceVulnDescription(data.changed_type, data.vuln_description, data.changed_to.description);
    }
  }

  return body;
}

function formatVulnInfo (value) {
  if (String.startsWith(value, "\n\n")) {
    value = String.substring(value, 2);
  }
  else if (String.startsWith(value, "\n")) {
    value = String.substring(value, 1);
  }
  value = String.replaceAll(value, "\n\n", "<br/>");
  value = String.replaceAll(value, "\n", "<br/>");
  
  return value;
};

function formatReferences(vuln_ref){
  return String.replaceAll(vuln_ref, m/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" rel="noopener noreferrer" target="_blank">$1</a>');
}

function replaceVulnDescription (type, vuln_description, text){

  const title = String.replaceAll(
    vuln_description,
    m/[\s\S]*Bugcrowd\sSubmission\sTitle:\s*([^\n]*)[\s\S]*/gi,
    '$1'
  );

  const description_block = String.replaceAll(
    vuln_description,
    m/[\s\S]*Bugcrowd\sDescription:\s*([\s\S]*?)(?=\n\n<strong>\s*Vulnerability\sReferences)/gi,
    '$1'
  );

  const split_desc = String.split(description_block, '<strong>Vulnerability References</strong>');

  let new_description;
  if (type === 'title'){
    new_description = 'BugCrowd Submission Title: ' + text + '\n' + '\nBugCrowd Description: ' + split_desc[0] + '\n\n<strong>Vulnerability References</strong>' + split_desc[1];
  } else if (type === 'description'){
    new_description = 'BugCrowd Submission Title: ' + title + '\n' + '\nBugCrowd Description: ' + text + '\n\n<strong>Vulnerability References</strong>' + split_desc[1];
  }
  Logger.debug('new_description', new_description);
  return formatReferences(new_description);
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while updating the Vulnerability.',
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed updating Vulnerability',
    }
  };
}
```

## Create Bugcrowd Comment on New Remediation Note

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FDAVwTwhKQQn9N3bS7IMV%2Fbugcrowd-3.png?alt=media&#x26;token=f042dfea-5493-49fa-9324-02c5abdc29ce" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a comment in Bugcrowd when a remediation note is created.

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-remediation-note-created
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_visibility - must be one of the following: *everyone, bugcrowd\_and\_researcher, bugcrowd\_and\_customer, customer, bugcrowd*
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get AttackForge Project ID and Bugcrowd Submission ID**

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

```javascript
const vuln_id = data.remediation_note_vulnerability?.vulnerability_id;

if (!vuln_id){
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.remediation_note_vulnerability?.vulnerability_id'
    }
  };
}
if (!data.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.remediation_note_details'
    }
  };
}
if (data.remediation_note_details =~ m/said on Bugcrowd\s\[https:\/\/tracker\.bugcrowd\.com\/.*\/submissions\/.*\]/) {
  return {
    decision: {
      status: 'finish',
      message: 'This remediation note is posted from Bugcrowd, no action required.'
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug(data?.remediation_note_details);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability ID: ' + vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vuln_id
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Retrieving Vulnerabiliy data was not successful.'
    }
  };
}
if (!response.jsonBody?.vulnerability) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while fetching Vulnerability information. Missing response.jsonBody?.vulnerability.'
    }
  };
}

const vuln = response.jsonBody.vulnerability;
const vuln_project_id = vuln.vulnerability_projects?[0].id;

if (!vuln_project_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln.vulnerability_projects: ', JSON.stringify(vuln.vulnerability_projects));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: vuln_project_id must be present, aborting.'
    }
  };
}
if (!vuln?.vulnerability_custom_fields) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln: ', JSON.stringify(vuln));
  }
  return {
    decision: {
      status: 'abort',
      message: 'bugcrowd_submission_id not found in vuln.'
    }
  };
}
  
data.vuln_project_id = vuln_project_id;

let bugcrowd_submission_id;
const vuln_custom_fields = vuln.vulnerability_custom_fields;

for (let i = 0; i < Array.length(vuln_custom_fields); i++){
  const customField = vuln_custom_fields[i];
  if (customField.key === 'bugcrowd_submission_id' && customField.value) {
    bugcrowd_submission_id = customField.value;
    return {
      decision: {
        status: 'continue',
        message: 'Vulnerability ID is from ' + bugcrowd_submission_id,
      },
      data: {
        note: data,
        bugcrowd_submission_id: bugcrowd_submission_id
      }
    };
  }
}
```

**Action 2 - Format Remediation Note**

* **Script**:

```javascript
const rem_note = data.note?.remediation_note_details_html 
  ? data.note?.remediation_note_details_html 
  : data.note?.remediation_note_details;

if (!rem_note){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No remediation note detail found.',
    },
  };
}

// Add your custom style to be converted to Bugcrowd Markdown
const rules = [
  ['<p>', '\n'],
  ['</p>', ''],

  // quotes
  ['<blockquote>', '\n>'],
  ['</blockquote>', '\n'],

  // bold/italic
  ['<strong>', '**'],
  ['</strong>', '**'],
  ['<em>', '_'],
  ['</em>', '_'],

  // tags not directly applicable to Bugcrowd
  ['<u>', ''], ['</u>', ''],
  ['<s>', ''], ['</s>', ''],
  [m/<span[^>]*>/gi, ''], ['</span>', ''],

  // code
  [m/<code[^>]*>/gi, '`'],
  ['</code>', '`'],
];

let rem_note_reformat = applyRules(data.note?.remediation_note_details_html, rules);

if (rem_note_reformat =~ m/<a href/){
  rem_note_reformat = formatHyperlink(rem_note_reformat);
}

data.note.remediation_note_details_reformatted = rem_note_reformat;

return {
  decision: {
    status: 'continue',
    message: 'Successfully reformatted remediation note to Bugcrowd comment format.',
  },
  data: data
};

function applyRules (text, rules){
  let applied_text = text;
  for (let i = 0; i < Array.length(rules); i++){
    applied_text = String.replaceAll(applied_text, rules[i][0], rules[i][1]);
  }
  return applied_text;
}

function formatHyperlink (text){
  return String.replaceAll(
    text,
    m/<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi,
    '[$2]($1)'
  );
}
```

**Action 3 - Create Bugcrowd Submission Comment**

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/comments>
* **Headers:**
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let note;
let bc_submission_id;

// Bugcrowd Visibilities: 
// "everyone" 
// "bugcrowd_and_researcher" 
// "bugcrowd_and_customer" 
// "customer" 
// "bugcrowd"
let visibility = secrets.bugcrowd_visibility;

const rem_note = data.note?.remediation_note_details_reformatted 
  ? data.note?.remediation_note_details_reformatted 
  : data.note?.remediation_note_details;

if (!data.note?.remediation_note_created) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.note?.remediation_note_created was not found.'
    }
  };
}

const author_date_time_string = data.note?.remediation_note_user?.first_name 
  + ' on ' + Date.format(data.note.remediation_note_created, 'dddd d mmmm yyyy');

note = '[' 
  + author_date_time_string 
  + ' said on AttackForge](https://andy.attackforge.dev/projects/' 
  + data.note.vuln_project_id
  + '/vulnerabilities/' 
  + data.note.remediation_note_vulnerability.vulnerability_id 
  + '#' 
  + data.note.remediation_note_created 
  + ') : ' 
  + rem_note;

if (!data.bugcrowd_submission_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: bugcrowd_submission_id was not found on data.'
    }
  };
}

bc_submission_id = data?.bugcrowd_submission_id;

const requestBody = {
  data: {
    type: "comment",
    attributes: {
        body: note,
        visibility_scope: visibility
    },
    relationships: {
      submission: {
        data: {
          type: "submission",
          id: bc_submission_id
        }
      }
    }
  }
};

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Bugcrowd',
  },
  request: {
    body: requestBody,
  }
};
```

* **Response Script**:

```javascript
if (!response.statusCode === 201){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating comment on Bugcrowd, error code: ' + response.statusCode
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Created comment on Bugcrowd.'
  }
};
```

## Create Bugcrowd Comment on Updated Remediation Note

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F0Foc7zFxb1SHsjFRiwMN%2Fbugcrowd-4.png?alt=media&#x26;token=9ca54972-7577-48cd-b273-807838ca8df3" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create a comment in Bugcrowd when a remediation note is updated.

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-remediation-note-updated
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_visibility - must be one of the following: *everyone, bugcrowd\_and\_researcher, bugcrowd\_and\_customer, customer, bugcrowd*
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get AttackForge Project ID and Bugcrowd Submission ID**

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

```javascript
if (data.remediation_note_is_deleted) {
  return {
    decision: 'finish',
    message: 'Deleted messages are preserved by design. Finishing the process.'
  };
}
if (!data.remediation_note_vulnerability?.vulnerability_id){
  return {
    decision: {
      status: 'abort',
      message: 'vulnerability_id not found'
    }
  };
}
if (!data.remediation_note_details) {
  return {
    decision: {
      status: 'abort',
      message: 'data.remediation_note_details not found'
    }
  };
}

if (secrets.logger_level === 'debug'){
  Logger.debug(data.remediation_note_details);
}

if (data.remediation_note_details =~ m/said on Bugcrowd\s\[https:\/\/tracker\.bugcrowd\.com\/.*\/submissions\/.*\]\n\n\>/) {
  return {
    decision: {
      status: 'finish',
      message: 'This remediation note is posted from Bugcrowd, no action required.'
    }
  };
}

const vuln_id = data.remediation_note_vulnerability.vulnerability_id;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Vulnerability ID: ' + vuln_id,
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vuln_id,
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Retrieving Vulnerabiliy data was not successful.'
    }
  };
}
if (!response.jsonBody?.vulnerability) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Missing response.jsonBody?.vulnerability.'
    }
  };
}

let bugcrowd_submission_id;
const vuln = response.jsonBody.vulnerability;

if (!vuln.vulnerability_custom_fields) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('vuln.vulnerability_custom_fields: ', JSON.stringify(vuln.vulnerability_custom_fields));
  }
  return {
    decision: {
      status: 'abort',
      message: 'bugcrowd_submission_id not found'
    }
  };
}

const vuln_project_id = vuln.vulnerability_projects?[0].id;
data.vuln_project_id = vuln_project_id;

for (let i = 0; i < Array.length(vuln.vulnerability_custom_fields); i++){ 
  const customField = vuln.vulnerability_custom_fields[i];
  if (customField.key === 'bugcrowd_submission_id' && customField.value) {
    bugcrowd_submission_id = customField.value;
    return {
      decision: {
        status: 'continue',
        message: 'Vulnerability ID is from ' + bugcrowd_submission_id,
      },
      data: {
        note: data,
        bugcrowd_submission_id: bugcrowd_submission_id
      }
    };
  }
}

return {
  decision: {
    status: 'finish',
    message: 'Vulnerability not from BugCrowd, exiting the process.'
  }
};
```

**Action 2 - Format Remediation Note**

* **Script**:

```javascript
const rem_note = data.note?.remediation_note_details_html 
  ? data.note?.remediation_note_details_html 
  : data.note?.remediation_note_details;

if (!rem_note){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: No remediation note detail found.',
    },
  };
}

// Add your custom style to be converted to Bugcrowd Markdown
const rules = [
  ['<p>', '\n'],
  ['</p>', ''],

  // quotes
  ['<blockquote>', '\n>'],
  ['</blockquote>', '\n'],

  // bold/italic
  ['<strong>', '**'],
  ['</strong>', '**'],
  ['<em>', '_'],
  ['</em>', '_'],

  // tags not directly applicable to Bugcrowd
  ['<u>', ''], ['</u>', ''],
  ['<s>', ''], ['</s>', ''],
  [m/<span[^>]*>/gi, ''], ['</span>', ''],

  // code
  [m/<code[^>]*>/gi, '`'],
  ['</code>', '`'],
];

let rem_note_reformat = applyRules(data.note?.remediation_note_details_html, rules);

if (rem_note_reformat =~ m/<a href/){
  rem_note_reformat = formatHyperlink(rem_note_reformat);
}

data.note.remediation_note_details_reformatted = rem_note_reformat;

return {
  decision: {
    status: 'continue',
    message: 'Successfully reformatted remediation note to Bugcrowd comment format.',
  },
  data: data
};

function applyRules (text, rules){
  let applied_text = text;
  for (let i = 0; i < Array.length(rules); i++){
    applied_text = String.replaceAll(applied_text, rules[i][0], rules[i][1]);
  }
  return applied_text;
}

function formatHyperlink (text){
  return String.replaceAll(
    text,
    m/<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi,
    '[$2]($1)'
  );
}
```

**Action 3 - Create Bugcrowd Submission Comment**

* **Method**: POST
* **URL**: <https://api.bugcrowd.com/comments>
* **Headers:**
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let note;
let bc_submission_id;

// Bugcrowd Visibilities: 
// "everyone" 
// "bugcrowd_and_researcher" 
// "bugcrowd_and_customer" 
// "customer" 
// "bugcrowd"
let visibility = secrets.bugcrowd_visibility;

const rem_note = data.note?.remediation_note_details_reformatted 
  ? data.note?.remediation_note_details_reformatted 
  : data.note?.remediation_note_details;

if (!data.note?.remediation_note_created) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.note?.remediation_note_created was not found.'
    }
  };
}

const author_date_time_string = data.note?.remediation_note_user?.first_name 
  + ' on ' + Date.format(data.note.remediation_note_created, 'dddd d mmmm yyyy');

note = '[' 
  + author_date_time_string 
  + ' said on AttackForge](https://andy.attackforge.dev/projects/' 
  + data.note.vuln_project_id
  + '/vulnerabilities/' 
  + data.note.remediation_note_vulnerability.vulnerability_id 
  + '#' 
  + data.note.remediation_note_created 
  + ') : ' 
  + rem_note;

if (!data.bugcrowd_submission_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: bugcrowd_submission_id was not found on data.'
    }
  };
}

bc_submission_id = data?.bugcrowd_submission_id;

const requestBody = {
  data: {
    type: "comment",
    attributes: {
        body: note,
        visibility_scope: visibility
    },
    relationships: {
      submission: {
        data: {
          type: "submission",
          id: bc_submission_id
        }
      }
    }
  }
};

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Bugcrowd',
  },
  request: {
    body: requestBody,
  }
};
```

* **Response Script**:

```javascript
if (!response.statusCode === 201){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating comment on Bugcrowd, error code: ' + response.statusCode
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Created comment on Bugcrowd.'
  }
};
```

## Create/Update Remediation Notes on Created/Updated Bugcrowd Comments

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F58NoRZtpZs52dIzHlNa6%2Fbugcrowd-5.png?alt=media&#x26;token=e0d888d5-554f-4626-9bd5-26c3e5dea535" alt=""><figcaption></figcaption></figure>

The purpose of this example is to schedule a flow to run each day to create or update remediation notes based on comments activity in Bugcrowd.

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

* **Schedule**
  * **Cron String:** \*/30 \* \* \* \*
  * **Frequency**: Run every 30 minutes
* **Secrets**:
  * bugcrowd\_authorization\_token - your [Bugcrowd API token](https://docs.bugcrowd.com/api/getting-started/)
  * bugcrowd\_engagement\_name - your Bugcrowd engagement name
  * bugcrowd\_secret - your [Bugcrowd secret](https://docs.bugcrowd.com/api/webhooks/)
  * af\_tenant - your AttackForge tenant e.g. *acmecorp.attackforge.com*
  * af\_apikey - your [AttackForge Self-Service API token](https://support.attackforge.com/attackforge-enterprise/modules/self-service-restful-api/getting-started#accessing-the-restful-api)
  * logging\_level - logging verbosity level. Supports *debug*

**Action 1 - Get Bugcrowd Vulnerabilities**

* **Method**: GET
* **URL**: https\://{{af\_tenant}}/api/ss/vulnerabilities?q={custom\_fields:{$elemMatch:{name:{$eq:"source"},value:{$eq:"Bugcrowd"}}}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';
let query = '{custom_fields:{$elemMatch:{name:{$eq:"source"},value:{$eq:"Bugcrowd"}}}}';
url = url + 'q=' + query;

return {
  decision: {
    status: 'continue',
    message: 'Fetching AttackForge Vulnerabilities'
  },
  request: {
    url: url
  }
};
```

* **Response Script**:

```javascript
const vulns = response.jsonBody?.vulnerabilities;

if (!Array.isArray(vulns)){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerabilities found from response',
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug('Vulnerabilities created from Bugcrowd found: ', vulns);
}

return {
  decision: {
    status: 'next',
    message: 'Bugcrowd sourced vulnerabilities retrieved.',
  },
  data: vulns
};
```

**Action 2 - Format Remediation Notes**

* **Script**:

```javascript
if (!Array.isArray(data) || Array.length(data) === 0){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No vulnerabilities present in data, please check your request.',
    },
  };
}

// Collection of bc_sub_ids and its rem_notes & bc_comments
const rem_notes = {};
const vuln_dictionary = {};
const bc_sub_ids = [];
const seen = {};

for (let i = 0; i < Array.length(data); i++){
  const vuln = data[i];

  if (!vuln?.vulnerability_custom_fields){
    continue;
  }

  const bcSubVuln = Array.find(vuln.vulnerability_custom_fields, findBugcrowdSubId);
  if (!bcSubVuln || !bcSubVuln.value){
    continue;
  }
  
  const bc_sub_id = bcSubVuln.value;
  if (!vuln_dictionary[bc_sub_id]){
    vuln_dictionary[bc_sub_id] = {
      projectId: vuln.vulnerability_project_id,
      vuln_id: vuln.vulnerability_id
    };
  }

  if (!seen[bc_sub_id]) {
    seen[bc_sub_id] = true;
    Array.push(bc_sub_ids, bc_sub_id);
  }

  const notes = vuln.vulnerability_remediation_notes;
  // Skip all the vulnerabilities with no remediation note.
  if (Array.isArray(notes) && Array.length(notes) > 0){
    for (let j = 0; j < Array.length(notes); j++){
      const current_note = notes[j];
      const html = current_note?.note_html;
      const note_body = current_note?.note;

      if (!html || !note_body) {
        continue;
      }
      // must include a bugcrowd tracker URL with #<uuid> and not #undefined
      if (!(html =~ m/<a href="https:\/\/tracker.bugcrowd.com\/.*\/submissions\/.*#(?!undefined\b)([0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}).*/i)){
        continue;
      }

      const comment_id = String.replaceAll(
        html,
        m/.*#([0-9a-f-]{8}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{4}-[0-9a-f-]{12}).*/gi,
        '$1'
      );

      if (!comment_id || comment_id === html){
        if (secrets.logging_level === 'debug'){
          Logger.debug('comment_id: ', comment_id);
          Logger.debug('html: ', html);
        }
        return {
          decision: {
            status: 'abort',
            message: 'Error while retrieving comment message. comment_id should be substring of uuid from html.'
          }
        };
      }

      rem_notes[comment_id] = {
        bc_sub_id: bc_sub_id,
        af_vuln_id: vuln.vulnerability_id,
        rem_note_id: current_note.id,
        rem_note: note_body,
        rem_note_html: html,
        vuln_project_id: vuln.vulnerability_project_id
      };
    }
  }
}


return {
  decision: {
    status: 'continue',
    message: 'Processed remediation note and reformatted',
  },
  data: {
    rem_notes: rem_notes,
    bc_sub_ids: bc_sub_ids,
    vuln_dictionary: vuln_dictionary
  }
};

function findBugcrowdSubId(custom_field){
  return custom_field.key === 'bugcrowd_submission_id';
}
```

**Action 3 - Get Comments from Submission**

* **Method**: GET
* **URL**: <https://api.bugcrowd.com/submissions?fields\\[comment]=body,created\\_at,author,file\\_attachments\\&include=comments,comments.author,comments.file\\_attachments\\&page\\[limit]=25\\&page\\[offset]=0>
* **Headers**:
  * Key = Authorization; Type = Secret; Value = bugcrowd\_authorization\_token
  * Key = Accept; Type = Value; Value = application/vnd.bugcrowd+json
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.bc_comments){
  data.bc_comments = [];
}

// pagination initialisation
if (!data.page_size){
  data.page_size = 25;
}
if (!data.offset){
  data.offset = 0;
}
if (!data.total_hits){
  data.total_hits = 0;
}

let url = 'fields[comment]=body,created_at,author,file_attachments&include=comments,comments.author,comments.file_attachments&page[limit]=' 
  + data.page_size + '&page[offset]=' + data.offset;

if (secrets.logging_level === 'debug'){
  Logger.debug('url', url);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Bugcrowd submission',
  },
  request: {
    url: 'https://api.bugcrowd.com/submissions?' + url
  },
  data: {
    rem_notes: data.rem_notes,
    bc_comments: data.bc_comments,
    vuln_dictionary: data.vuln_dictionary,
    bc_sub_ids: data.bc_sub_ids,
    page_size: data.page_size,
    offset: data.offset,
    total_hits: data.total_hits
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: 'abort',
    message: 'Error while fetching comments from submissions.'
  };
}

if (!response.jsonBody?.data){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: 'abort',
    message: 'No response.jsonBody?.data was found from response returned.'
  };
}

// Initialise bc_comments array
if (!data?.bc_comments){
  data.bc_comments = [];
}

// Initialise author object to collect author information over repeated loops
if (!data.author){
  data.author = {};
}

const meta_data = response.jsonBody?.meta;
if (!meta_data){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.jsonBody', JSON.stringify(response.jsonBody));
  }
  return {
    decision: 'abort',
    message: 'Error: meta_data is required to process pagination.'
  };
}

if (!(meta_data.total_hits && meta_data.count)){
  if (secrets.logging_level === 'debug'){
    Logger.debug('meta_data', JSON.stringify(meta_data));
  }
  return {
    decision: 'abort',
    message: 'Error: meta_data must contain total_hits and count to process pagination.'
  };
}

data.total_hits = meta_data.total_hits;
data.offset = data.offset + meta_data.count;

const bugcrowd_submissions = response.jsonBody.data;

// comment_ids format: {comment_id : submission_id}
const comment_ids = {};

for (let i = 0; i < Array.length(bugcrowd_submissions); i++){
  const submission = bugcrowd_submissions[i];
  if (!submission.relationships?.comments || Array.length(submission.relationships?.comments?.data) === 0){
    continue;
  }
  const comments = submission.relationships.comments?.data;
  for (let j = 0; j < Array.length(comments); j++){
    comment_ids[comments[j].id] = submission.id;
  }
}

const included = response.jsonBody?.included;

if (!included){
  return {
    decision: 'abort',
    message: 'No response.jsonBody?.included was found from response returned.'
  };
}

for (let i = 0; i < Array.length(included); i++){
  if (included[i].type === 'identity'){
    if (!data.author[included[i].id]){
      data.author[included[i].id] = {
        name: included[i]?.attributes?.name,
        email: included[i]?.attributes?.email,
      };
    } 
    else {
      continue;
    }
  }
}

for (let i = 0; i < Array.length(included); i++){
  if (included[i].type === 'comment'){
    const comment_body = included[i]?.attributes.body;
    if (!comment_body || comment_body === '[DELETED]') {
      continue;
    }
    if (comment_body =~ m/^\[.*said on AttackForge\]/) {
      continue;
    }
    if (included[i]?.relationships?.author?.data?.id){
      const comment = {
        id : included[i].id,
        bc_sub_id : comment_ids[included[i].id],
        body : included[i].attributes?.body,
        created_at : included[i].attributes?.created_at,
        author : data.author[included[i].relationships.author.data.id].name,
        author_email : data.author[included[i].relationships.author.data.id].email
      };
      Array.push(data.bc_comments, comment);
    }
  }
}

if (data.offset < data.total_hits){
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching more submissions from Bugcrowd',
    },
    data: {
      rem_notes: data.rem_notes,
      bc_comments: data.bc_comments,
      vuln_dictionary: data.vuln_dictionary,
      offset : data.offset,
      page_size: data.page_size,
      total_hits : data.total_hits
    }
  };
} 
else {
  return {
    decision: {
      status: 'continue',
      message: 'Fetched all comments from Bugcrowd submissions.',
    },
    data: {
      rem_notes: data.rem_notes,
      bc_comments: data.bc_comments,
      vuln_dictionary: data.vuln_dictionary
    }
  };
}
```

**Action 4 - Convert Markdown to Rich Text**&#x20;

* **Method**: POST
* **URL**: https\://{{af\_tenant}}/api/ss/utils/markdown-to-richtext
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_apikey
* **Request Script**:

```javascript
const bc_object = {};

if (data.bc_comments) {
  for (let i = 0; i < Array.length(data.bc_comments); i++){
    if (data.bc_comments[i].body && data.bc_comments[i].id) {
      const new_line_converted_body = String.replaceAll(String.replaceAll(data.bc_comments[i].body, '\n\n\n', '\n\n'), '\n', '\n\n');
      bc_object[data.bc_comments[i].id] = new_line_converted_body;
    }
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Converting markdown comment body to richtext.'
  },
  request: {
    url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
    body:  bc_object
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while converting Markdown to RichText',
    },
    data: data
  };
}

if (!data) {
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: missing data',
    }
  };
}

const converted_comment_body = response.jsonBody;
if (!converted_comment_body){
  if (secrets.logging_level === 'debug'){
    Logger.debug(JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: converted comment body not found',
    }
  };
}

const comment_body_full = {};
const ids = Object.keys(converted_comment_body);

if (data.bc_comments){
  for (let i = 0; i < Array.length(data.bc_comments); i++){
    let current_id = data.bc_comments[i].id;
    comment_body_full[current_id] = {
      bc_sub_id: data.bc_comments[i].bc_sub_id,
      id: data.bc_comments[i].id,
      body: data.bc_comments[i].body,
      rich_text_body: converted_comment_body[current_id],
      created_at: data.bc_comments[i].created_at,
      author: data.bc_comments[i].author,
      author_email: data.bc_comments[i].author_email,
    };
    if (secrets.logging_level === 'debug'){
      Logger.debug('data.bc_comments[i]', JSON.stringify(data.bc_comments[i]));
    }
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully converted Markdown comment body to RichText.',
  },
  data: {
    rem_notes: data.rem_notes,
    bc_comments: comment_body_full,
    vuln_dictionary: data.vuln_dictionary
  }
};
```

**Action 5 - Create or Update Comments**

* **Script**:

```javascript
const create_required = [];
const update_required = [];

const vuln_dictionary = data.vuln_dictionary || {};
const bc_comments = data.bc_comments || {};
const rem_notes = data.rem_notes || {};

const bc_comment_ids = Object.keys(bc_comments);

for (let i = 0; i < Array.length(bc_comment_ids); i++){
  const bc_comment_id = bc_comment_ids[i];
  const current_comment = bc_comments[bc_comment_id];
  if (!current_comment) {
    continue;
  }

  const vuln = vuln_dictionary[current_comment.bc_sub_id];
  if (!vuln) {
    continue;
  }

  const existing_rem_note = rem_notes[bc_comment_id];

  // build payload copy
  const payload = {
    bc_comment_id: bc_comment_id,
    bc_sub_id: current_comment.bc_sub_id,
    body: current_comment.body,
    rich_text_body: current_comment.rich_text_body,
    created_at: current_comment.created_at,
    author: current_comment.author,
    author_email: current_comment.author_email,
    vuln_id: vuln.vuln_id,
    vuln_project_id: vuln.projectId
  };

  // if no existing remediation note on bc_comment_id 
  if (!existing_rem_note){
    Array.push(create_required, payload);
    continue;
  }

  // update decision
  const existing_html = existing_rem_note.rem_note_html;
  const bc_comment_rich_text = payload.rich_text_body;

  if (!existing_html || !bc_comment_rich_text) {
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
    continue;
  }

  if (!existing_rem_note.rem_note_id) {
    if (secrets.logging_level === 'debug'){
      Logger.debug('No existing_rem_note.rem_note_id found. This means this remediation note was deleted. Adding to create queue.');
    }
    Array.push(create_required, payload);
    continue;
  }

  const inner = extractCommentHtml(existing_html);
  if (!inner){
    // if inner extraction fails, treat it as an update
    if (secrets.logging_level === 'debug'){
      Logger.debug('Failed extracting Bugcrowd comment rich-text substring from existing_html: ', existing_html);
    }
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
    continue;
  }

  if (inner === bc_comment_rich_text) {
    continue;
  }

  if (inner !== existing_html){
    payload.rem_note_id = existing_rem_note.rem_note_id;
    Array.push(update_required, payload);
  }

  if (secrets.logging_level === 'debug'){
    Logger.debug('Should not reach here.');
  }
}

if (Array.length(create_required) === 0 && Array.length(update_required) === 0){
  return {
    decision: 'finish',
    message: 'All comments from Bugcrowd are in sync. Exiting process.'
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Determined Create/Update, proceeding to Create step.'
  },
  data: { 
    create: create_required, 
    update: update_required 
  }
};

function extractCommentHtml(html){
  if (!html) {
    return null;
  }
  const inner = String.replaceAll(
    html,
    m/[\s\S]*said on Bugcrowd\s*<\/a>\s*<\/em>\s*<\/p>\s*<blockquote>([\s\S]*?)\s*<\/blockquote>\s*$/gi,
    '$1'
  );
  if (inner === html) {
    return null;
  }
  return inner;
}
```

**Action 6 - Create Remediation Notes**

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

```javascript
if (!data.create || Array.length(data.create) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No bc_comments create left to process. Continuing to next step.',
    },
    data: {
      update: data.update
    }
  };
};

let bc_comment;

if (Array.length(data.create) > 0){
  bc_comment = data.create[0];
}

if (!bc_comment.bc_comment_id){
  Logger.error('Error: Following bc_comment does not have bc_comment_id: ', JSON.stringify(bc_comment));
  return {
    decision: {
      status: 'abort',
      message: 'No bc_comment_id found to create new remediation note. Aborting the process.',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' 
  + bc_comment.vuln_id + '/remediationNote';

const author_name = bc_comment.author === 'a Crowdcontrol user' 
  ? bc_comment.author_email 
  : bc_comment.author;

const author_date_time_string = author_name 
  + ' on ' + Date.format(bc_comment.created_at, 'dddd d mmmm yyyy');

const note_body = '<p><em><a href="https://tracker.bugcrowd.com/' 
  + secrets.bugcrowd_engagement_name 
  + '/submissions/' 
  + bc_comment.bc_sub_id 
  + '#' 
  + bc_comment.bc_comment_id 
  + '" rel="noopener noreferrer" target="_blank">' 
  + author_date_time_string 
  + ' said on Bugcrowd </a> </em> </p><blockquote>' 
  + bc_comment.rich_text_body 
  + ' </blockquote>';

const projectId = bc_comment.vuln_project_id;

Array.shift(data.create);

return {
  decision: {
    status: 'continue',
    message: 'Creating comment on Vulnerability ID ' + bc_comment.vuln_id
  },
  request: {
    url: url,
    body: {
      note: note_body,
      note_type: 'RICHTEXT',
      projectId: bc_comment.vuln_project_id
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  Logger.error(JSON.stringify(response));
  return {
    decision: {
      status: 'abort',
      message: 'Error while creating remediation note on AttackForge.'
    }
  };
}

if (data && !data.error_collection){
  data.error_collection = {
    create: [],
    update: []
  };
}

if (response.jsonBody?.status === 'error' && response.jsonBody.error){
  if (secrets.logging_level === 'debug'){
    Logger.debug(response?.jsonBody?.error);
  }
  Array.push(data.error_collection.create, response.jsonBody.error);
}

if (data?.create && Array.length(data.create) === 0) {
  if (data.update && Array.length(data.update) > 0){
    return {
      decision: {
        status: 'continue',
        message: 'No more create to repeat. Proceeding to update.'
      },
      data: {
        update: data.update
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'finish',
        message: 'No more create or update to repeat. Exiting the process.'
      }
    };
  }
}

if (data?.create && Array.length(data.create) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Processing next create request on queue.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'abort',
    message: 'Error: should never get here'
  }
};
```

**Action 7 - Update Remediation Notes**

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

```javascript
if (!data.update || Array.length(data.update) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No update request, finishing the process'
    }
  };
}

if (!(data.update?[0].vuln_id && data.update?[0].rem_note_id)) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Update data does not contain necessary information to process. Requires af_vuln_id and rem_note_id'
    }
  };
}

const current_update = data.update[0];
if (!current_update){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.update: ', data.update);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Failed retrieving first element from data.update array.'
    }
  };
}

const vulnerabilityId = current_update.vuln_id;
const remediationNoteId = current_update.rem_note_id;

const author_name = current_update?.author === 'a Crowdcontrol user' 
  ? current_update?.author_email 
  : current_update.author;

const author_date_time_string = author_name 
  + ' on ' + Date.format(current_update.created_at, 'dddd d mmmm yyyy');

const url = 'https://andy.attackforge.dev/api/ss/vulnerability/' 
  + vulnerabilityId 
  + '/remediationNote/'
  + remediationNoteId;

let note_body = '<p><em><a href="https://tracker.bugcrowd.com/' + 
  + secrets.bugcrowd_engagement_name
  + '/submissions/'
  + current_update.bc_sub_id 
  + '#' 
  + current_update.bc_comment_id;

note_body = note_body 
  + '" rel="noopener noreferrer" target="_blank">' 
  + author_date_time_string 
  + ' said on Bugcrowd </a> </em> </p> <blockquote>'
  + current_update.rich_text_body 
  + ' </blockquote>';

Array.shift(data.update);

return {
  decision: {
    status: 'continue',
    message: 'Sending update remediation note request.',
  },
  request: {
    url: url,
    body: {
      note: note_body,
      note_type: 'RICHTEXT'
    }
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while updating remediation note, statusCode: ' + response.statusCode 
    }
  };
}

if (data && !data.error_collection){
  data.error_collection = {};
  if (!data.error_collection.update){
    data.error_collection.update = [];
  }
}

if (response.jsonBody?.status === "error" && response.jsonBody?.error) {
  Logger.info('There was an error while updating Remediation Note. Please refer to the data.error_collection.');
  Array.push(data.error_collection.update, response.jsonBody?.error);
}

if (data?.update && Array.length(data.update) === 0){
  return {
    decision: {
      status: 'finish',
      message: 'Update queue empty, finishing the process.'
    },
    data: {
      error_collection: data.error_collection
    }
  };
}

if (data?.update && Array.length(data.update) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Processing next update request in queue.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'abort',
    message: 'Error: should never get here'
  }
};
```
