# Bugcrowd

## Create Vulnerability on Bugcrowd Submission

<figure><img src="/files/FPH4lTxOt2TkrsX7oZHe" 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="/files/0lcF4ejRrXGHVqQurth3" 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="/files/I2NOvmt6DNaItsKGHtqH" 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="/files/MeT0UTwuvf6ld7rQfieo" 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="/files/z6IESy8hU0VdENVwSA9B" 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'
  }
};
```


---

# Agent Instructions: Querying This Documentation

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

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

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

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

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