# Synack

## Import Synack Vulns

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FTGaTbgafxONX6oPdpVHv%2FSynack-1.png?alt=media&#x26;token=69e27e2e-f5c3-4ea9-a7bb-9c0cb5ca4080" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from [Synack](https://www.synack.com/) on a time-based schedule.

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

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (0 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_synack\_asset\_library\_key** - if assets module is enabled, specify the library where Synack assets will be created. This key can be extracted from the Assets module by navigating to the relevant library.
  * **af\_synack\_project\_custom\_field\_key** - custom field key used to determine if an AF project is a Synack project. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_project\_custom\_field\_type** - custom field type used to determine if an AF project is a Synack project. Must be "multi-select" or "select" depending on your custom field configuration. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_project\_custom\_field\_value** - custom field value used to determine if an AF project is a Synack project. Used for correlating Synack vulns through Listing Codename -> Project Code.
  * **af\_synack\_writeups\_library\_key** - specify the Writeups library where Synack vulns will be created. This key can be extracted from the Writeups module by navigating to the relevant library.
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **skip\_updating\_synack\_status** - set to "yes" to skip updating Synack vulns to "Ticketed" status
  * **synack\_fetch\_changes\_from\_in\_days** - the number of days (in the past) to fetch vulnerabilities e.g. set to "1" for fetching new/updated Synack vulns in past 24-hours. Should match your Flow Cron String.
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."
  * **synack\_vuln\_acknowledged\_status\_id** - the id for the Synack vuln status for new vulnerabilities. For example, the id for the "Pending Review" status in prod could be "4024". Check this with your Synack account manager.
  * **synack\_vuln\_acknowledged\_status\_name** - the name for the Synack vuln status for new vulnerabilities. E.g. "Pending Review". Check this with your Synack account manager.
  * **synack\_vuln\_identified\_status\_id** - the id for the Synack vuln status for *Ticketed* vulnerabilities. For example, the id for the "Ticketed" status in prod could be "4025". Check this with your Synack account manager.

**Action 1 - Get Synack Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities?filter\[status\_id]\[]={{synack\_vuln\_identified\_status\_id}}\&page\[size]=5\&page\[number]={page}\&filter\[updated\_since]={isoDate}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.page) {
  data.page = 1;
}
if (!data.updated_since) {
  const getVulnChangesFrom = '-' + secrets.synack_fetch_changes_from_in_days + ' days';
  data.updated_since = Date.datetime('now', getVulnChangesFrom, 'isostring');
}
if (!data.vulns) {
  data.vulns = [];
}

let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities?';
url = url + 'filter[status_id][]=' + Number.parseInt(secrets.synack_vuln_identified_status_id);
url = url + '&page[size]=5';
url = url + '&page[number]=' + data.page;
url = url + '&filter[updated_since]=' + data.updated_since;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    page: data.page,
    vulns: data.vulns,
    updated_since: data.updated_since
  }
};
```

* **Response Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.page) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.page',
    }
  };
}
if (!data.updated_since) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.updated_since',
    }
  };
}

if (response.jsonBody
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'No vulns left to process',
    },
    data: {
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody 
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length >= 1
  && response.jsonBody.length <= 5
){
  Logger.info('Found ' + response.jsonBody.length + ' vulns');

  for (let x = 0; x < response.jsonBody.length; x++) {
    const vuln = response.jsonBody[x];
    Array.push(data.vulns, vuln);
  }

  data.page = data.page + 1;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vuln pages',
    },
    data: {
      page: data.page,
      vulns: data.vulns,
      updated_since: data.updated_since
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 2 - Get Synack AF Projects**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/projects?skip={skip}\&limit=50\&q={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (data.vulns && data.vulns.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No vulnerabilities to create',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/projects?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';

if (secrets.af_synack_project_custom_field_key && secrets.af_synack_project_custom_field_value) {
  if (secrets.af_synack_project_custom_field_type === 'multi-select') {
    url = url + '&q={custom_fields: {$elemMatch: {name: { $eq: "' + 
      secrets.af_synack_project_custom_field_key + '" }, value: { $in: ["' + 
      secrets.af_synack_project_custom_field_value + '"] } } } }';
  }
  else if (secrets.af_synack_project_custom_field_type === 'select') {
    url = url + '&q={custom_fields: {$elemMatch: {name: { $eq: "' + 
      secrets.af_synack_project_custom_field_key + '" }, value: { $eq: "' + 
      secrets.af_synack_project_custom_field_value + '" } } } }';
  }
}

if (!data.synackListingIdToAFProjectId) {
  data.synackListingIdToAFProjectId = {};
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack projects',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.projects && Array.isArray(response.jsonBody.projects)
  && response.jsonBody.projects.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all projects',
    },
    data: {
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.projects && Array.isArray(response.jsonBody.projects)
  && response.jsonBody.projects.length <= 50
){
  Logger.info('Found ' + response.jsonBody.projects.length + ' Synack projects');

  for (let x = 0; x < response.jsonBody.projects.length; x++) {
    const project = response.jsonBody.projects[x];

    if (data.synackListingIdToAFProjectId && project.project_code && project.project_id) {
      data.synackListingIdToAFProjectId[project.project_code] = project.project_id;
    }
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more Synack projects',
    },
    data: {
      skip: data.skip,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retrieving Synack projects',
    }
  };
}
```

**Action 3 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 4 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
    vulns: data.vulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 5 - Convert Markdown to Rich Text for Update Vulns**

* **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\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.vulnsToUpdate && data.existingVulns.length > 0) {
  data.vulnsToUpdate = [];

  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

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

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

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

    if (vuln.id && synackIdToExistingVuln[vuln.id]) {
      const existingVuln = synackIdToExistingVuln[vuln.id];
      const updateVuln = {
        vuln_id: synackIdToExistingVuln[vuln.id].vulnerability_id,
        asset_library_ids: [
          secrets.af_synack_asset_library_key
        ]
      };
      if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
        updateVuln.project_id = data.synackListingIdToAFProjectId[vuln.listing.codename];
      }
      else {
        // no matching project in AF to handle the Synack vuln
        continue;
      }

      let updated = false;

      if (vuln.vulnerability_status?.text) {
        let currentStatus;
        if (existingVuln.vulnerability_custom_fields) {
          for (let y = 0; y < existingVuln.vulnerability_custom_fields.length; y++) {
            const customField = existingVuln.vulnerability_custom_fields[y];

            if (customField.key === 'synack_vuln_status' && customField.value) {
              currentStatus = customField.value;
            }
          }
        }
        
        if (vuln.vulnerability_status.text !== currentStatus) {
          updateVuln.custom_fields = [
            {
              key: "synack_vuln_status",
              value: vuln.vulnerability_status.text
            }
          ];
          updated = true;
        }
      }

      if (vuln.title && existingVuln.vulnerability_title 
        && vuln.title !== existingVuln.vulnerability_title
      ) {
        updateVuln.title = vuln.title;
        updated = true;
      }
      
      if (vuln.description) {
        if (existingVuln.vulnerability_description_html
          && vuln.description !== existingVuln.vulnerability_description_html
        ){
          updateVuln.description = vuln.description;
          //updated = true;
        }
      }
      if (vuln.impact) {
        if (existingVuln.vulnerability_attack_scenario_html
          && vuln.impact !== existingVuln.vulnerability_attack_scenario_html
        ){
          updateVuln.attack_scenario = vuln.impact;
          //updated = true;
        }
      }
      if (vuln.recommended_fix) {
        if (existingVuln.vulnerability_remediation_recommendation_html
          && vuln.recommended_fix !== existingVuln.vulnerability_remediation_recommendation_html
        ){
          updateVuln.remediation_recommendation = vuln.recommended_fix;
          //updated = true;
        }
      }
      if (vuln.exploitable_locations && Array.isArray(vuln.exploitable_locations)) {
        const assets = {};
        const assetComponentMap = {};

        const newAssets = [];

        for (let y = 0; y < vuln.exploitable_locations.length; y++) {
          const asset = vuln.exploitable_locations[y];
          let assetName;

          if (asset.type === 'ip' && asset.address) {
            assetName = asset.address;
          }
          else if (asset.type === 'url' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.type === 'app-location' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.type === 'other' && asset.value) {
            assetName = asset.value;
          }
          else if (asset.value) {
            assetName = asset.value;
          }

          if (assetName && !assets[assetName]) {
            assets[assetName] = {
              components: []
            };
          }

          let component;
          let assetComponent;

          if (assetName && asset.protocol && asset.port) {
            component = asset.protocol + ':' + asset.port;
            assetComponent = assetName + ':' + component;
          }
          if (assetName && assets[assetName]?.components 
            && component && assetComponent && !assetComponentMap[assetComponent]
          ) {
            const newComponent = {
              name: component
            };

            Array.push(assets[assetName].components, newComponent);
            assetComponentMap[assetComponent] = assetComponent;
          }      
        }

        const parsedAssets = Object.keys(assets);

        for (let y = 0; y < Array.length(parsedAssets); y++) {
          const key = parsedAssets[y];
          const value = assets[key];

          const newAsset = {
            assetName: key,
            components: assets[key].components
          };

          if (newAsset.assetName) {
            Array.push(newAssets, newAsset);
          }
        }

        const existingAssets = {};
        if (existingVuln.vulnerability_affected_assets) {
          for (let y = 0; y < existingVuln.vulnerability_affected_assets.length; y++) {
            const existingAsset = existingVuln.vulnerability_affected_assets[y];

            if (existingAsset.asset?.name) {
              existingAssets[existingAsset.asset.name] = existingAsset.asset.name;
            } 
          }
        }

        let assetsChanged = false;
        for (let y = 0; y < newAssets.length; y++) {
          if (newAssets[y].assetName && !existingAssets[newAssets[y].assetName]) {
            assetsChanged = true;
          }
        }

        if (assetsChanged) {
          updateVuln.affected_assets = newAssets;
          updated = true;
        }
      }

      let steps_to_reproduce = '';
      if (vuln.validation_steps && Array.isArray(vuln.validation_steps)) {
        for (let x = 0; x < vuln.validation_steps.length; x++) {
          const step = vuln.validation_steps[x];

          if (step.detail) {
            steps_to_reproduce = steps_to_reproduce + step.detail + '\n\n';
          }
        }
      }
      if (vuln.http_requests && Array.isArray(vuln.http_requests)) {
        for (let x = 0; x < vuln.http_requests.length; x++) {
          steps_to_reproduce = steps_to_reproduce + 'HTTP Request\n\n';
          const httpRequest = vuln.http_requests[x];
          
          if (httpRequest.http_request) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Request: ' + httpRequest.http_request + '\n\n';
          }
          if (httpRequest.vuln_param) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Vulnerable Parameter: ' + httpRequest.vuln_param + '\n\n';
          }
          if (httpRequest.attack_payload) {
            steps_to_reproduce = steps_to_reproduce 
              + 'Attack Payload: ' + httpRequest.attack_payload + '\n\n';
          }
        }
      }
      if (steps_to_reproduce && existingVuln.vulnerability_steps_to_reproduce_HTML
        && steps_to_reproduce !== existingVuln.vulnerability_steps_to_reproduce_HTML
      ) {
        updateVuln.steps_to_reproduce = steps_to_reproduce;
        //updated = true;
      }
      let tags = [];
      let priority;
      let likelihood_of_exploitation;
      if (vuln.cvss_version === '3.1') {
        if (vuln.cvss_final) {
          const tag = 'CVSSv3.1 Base Score: ' + vuln.cvss_final;
          Array.push(tags, tag);
        }
        if (vuln.cvss_vector) {
          const tag = vuln.cvss_vector;
          Array.push(tags, tag);
        }
        if (vuln.cvss_final) {
          if (vuln.cvss_final > 0 && vuln.cvss_final <= 3.9) {
            priority = 'Low';
            likelihood_of_exploitation = 2;
          }
          else if (vuln.cvss_final >= 4 && vuln.cvss_final <= 6.9) {
            priority = 'Medium';
            likelihood_of_exploitation = 4;
          }
          else if (vuln.cvss_final >= 7 && vuln.cvss_final <= 8.9) {
            priority = 'High';
            likelihood_of_exploitation = 7;
          }
          else if (vuln.cvss_final >= 9 && vuln.cvss_final <= 10) {
            priority = 'Critical';
            likelihood_of_exploitation = 9;
          }
        }
      }
      if (likelihood_of_exploitation && existingVuln.vulnerability_likelihood_of_exploitation 
        && likelihood_of_exploitation !== existingVuln.vulnerability_likelihood_of_exploitation
      ) {
        updateVuln.likelihood_of_exploitation = likelihood_of_exploitation;
        updated = true;
      }
      if (vuln.tag_list && Array.isArray(vuln.tag_list)) {
        for (let x = 0; x < vuln.tag_list.length; x++) {
          const tag = vuln.tag_list[x];

          if (tag.name) {
            Array.push(tags, tag.name);
          }
        }
      }
      if (vuln.cve_ids && Array.isArray(vuln.cve_ids)) {
        for (let x = 0; x < vuln.cve_ids.length; x++) {
          let cve = "";
          let tag = "" + vuln.cve_ids[x];
          if (!String.startsWith(tag, 'CVE-')) {
            cve = cve + 'CVE-';
          }
          cve = cve + vuln.cve_ids[x];
          Array.push(tags, cve);
        }
      }
      if (vuln.cwe_ids && Array.isArray(vuln.cwe_ids)) {
        for (let x = 0; x < vuln.cwe_ids.length; x++) {
          let cwe = "";
          let tag = "" + vuln.cwe_ids[x];
          if (!String.startsWith(tag, 'CWE-')) {
            cwe = cwe + 'CWE-';
          }
          cwe = cwe + vuln.cwe_ids[x];
          Array.push(tags, cwe);
        }
      }
      if (vuln.category?.display) {
        const tag = 'Category: ' + vuln.category.display;
        Array.push(tags, tag);
      }
      if (vuln.category?.parent) {
        const tag = 'Parent Category: ' + vuln.category.parent;
        Array.push(tags, tag);
      }
      if (existingVuln.vulnerability_tags 
        && JSON.stringify(existingVuln.vulnerability_tags) !== JSON.stringify(tags)
      ) {
        updateVuln.tags = tags;
        updated = true;
      }
      if (priority && existingVuln.vulnerability_priority 
        && priority !== existingVuln.vulnerability_priority
      ) {
        updateVuln.priority = priority;
        updated = true;
      }
      
      if (updated === true) {
        Array.push(data.vulnsToUpdate, updateVuln);
      }
    }
  }
}

if (data.vulnsToUpdate && data.vulnsToUpdate.length > 0) {
  const vulnMap = {};
  
  for (let x = 0; x < data.vulnsToUpdate.length; x++) {
    const vuln = data.vulnsToUpdate[x];

    if (vuln.vuln_id) {
      if (vuln.description) {
        vulnMap[vuln.vuln_id + 'description'] = vuln.description;
      }
      if (vuln.attack_scenario) {
        vulnMap[vuln.vuln_id + 'attack_scenario'] = vuln.attack_scenario;
      }
      if (vuln.remediation_recommendation) {
        vulnMap[vuln.vuln_id + 'remediation_recommendation'] = vuln.remediation_recommendation;
      }
      if (vuln.steps_to_reproduce) {
        vulnMap[vuln.vuln_id + 'steps_to_reproduce'] = vuln.steps_to_reproduce;
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: vulnMap
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to update',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const vulnMap = response.jsonBody;
  
  for (let x = 0; x < data.vulnsToUpdate.length; x++) {
    const vuln = data.vulnsToUpdate[x];

    if (vuln.vuln_id) {
      if (vulnMap[vuln.vuln_id + 'description']) {
        vuln.description = vulnMap[vuln.vuln_id + 'description'];
      }
      if (vulnMap[vuln.vuln_id + 'attack_scenario']) {
        vuln.attack_scenario = vulnMap[vuln.vuln_id + 'attack_scenario'];
      }
      if (vulnMap[vuln.vuln_id + 'remediation_recommendation']) {
        vuln.remediation_recommendation = vulnMap[vuln.vuln_id + 'remediation_recommendation'];
      }
      if (vulnMap[vuln.vuln_id + 'steps_to_reproduce']) {
        vuln.steps_to_reproduce = vulnMap[vuln.vuln_id + 'steps_to_reproduce'];
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 6 - Update AF Vulnerabilities**

* **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\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (data.vulnsToUpdate && data.vulnsToUpdate[0]) {
  const vulnId = JSON.parse(JSON.stringify(data.vulnsToUpdate[0].vuln_id));
  delete data.vulnsToUpdate[0].vuln_id;

  return {
    decision: {
      status: 'continue',
      message: 'Update vulnerability',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId,
      body: data.vulnsToUpdate[0]
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to update',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdate);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdate: data.vulnsToUpdate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 7 - Convert Markdown to Rich Text for Create Vulns**

* **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\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.vulnsToCreate) {
  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

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

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

  data.vulnsToCreate = [];
  data.vulnsToUpdateToTicketed = [];

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

    if (vuln.id && !synackIdToExistingVuln[vuln.id]) {
      if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
        Array.push(data.vulnsToCreate, vuln);
      }
    }
  }
}

if (data.vulnsToCreate && data.vulnsToCreate.length > 0) {
  data.vulnsToCreateUpdated = [];

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

    const newVuln = {
      synackVulnId: vuln.id,
      title: 'Synack Vulnerability',
      priority: 'Info',
      likelihood_of_exploitation: 1,
      description: '<p>N/A</p>',
      attack_scenario: '<p>N/A</p>',
      remediation_recommendation: '<p>N/A</p>',
      steps_to_reproduce: '<p>N/A</p>',
      affected_assets: [],
      is_visible: true,
      import_source: 'Synack',
      import_source_id: vuln.id,
      import_to_library: secrets.af_synack_writeups_library_key,
      tags: [],
      asset_library_ids: [
        secrets.af_synack_asset_library_key
      ],
      custom_fields: [
        {
          key: "synack_vuln_id",
          value: vuln.id
        },
        {
          key: "synack_vuln_link",
          value: vuln.link
        },
        {
          key: "synack_vuln_status",
          value: vuln.vulnerability_status?.text
        }
      ]
    };

    if (vuln.listing?.codename && data.synackListingIdToAFProjectId[vuln.listing.codename]) {
      newVuln.projectId = data.synackListingIdToAFProjectId[vuln.listing.codename];
    }

    if (vuln.title) {
      newVuln.title = vuln.title;
    }
    if (vuln.description) {
      newVuln.description = vuln.description;
    }
    if (vuln.impact) {
      newVuln.attack_scenario = vuln.impact;
    }
    if (vuln.recommended_fix) {
      newVuln.remediation_recommendation = vuln.recommended_fix;
    }
    if (vuln.exploitable_locations && Array.isArray(vuln.exploitable_locations)) {
      const assets = {};
      const assetComponentMap = {};

      for (let x = 0; x < vuln.exploitable_locations.length; x++) {
        const asset = vuln.exploitable_locations[x];
        let assetName;

        if (asset.type === 'ip' && asset.address) {
          assetName = asset.address;
        }
        else if (asset.type === 'url' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.type === 'app-location' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.type === 'other' && asset.value) {
          assetName = asset.value;
        }
        else if (asset.value) {
          assetName = asset.value;
        }

        if (assetName && !assets[assetName]) {
          assets[assetName] = {
            components: []
          };
        }

        let component;
        let assetComponent;

        if (assetName && asset.protocol && asset.port) {
          component = asset.protocol + ':' + asset.port;
          assetComponent = assetName + ':' + component;
        }
        if (assetName && assets[assetName]?.components 
          && component && assetComponent && !assetComponentMap[assetComponent]
        ) {
          const newComponent = {
            name: component
          };

          Array.push(assets[assetName].components, newComponent);
          assetComponentMap[assetComponent] = assetComponent;
        }      
      }

      const newAssets = Object.keys(assets);

      for (let x = 0; x < Array.length(newAssets); x++) {
        const key = newAssets[x];
        const value = assets[key];

        const newAsset = {
          assetName: key,
          components: assets[key].components
        };

        if (newAsset.assetName) {
          Array.push(newVuln.affected_assets, newAsset);
        }
      }
    }

    if (vuln.validation_steps && Array.isArray(vuln.validation_steps)) {
      newVuln.steps_to_reproduce = '';

      for (let x = 0; x < vuln.validation_steps.length; x++) {
        const step = vuln.validation_steps[x];

        if (step.detail) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce + step.detail + '\n\n';
        }
      }
    }
    if (vuln.http_requests && Array.isArray(vuln.http_requests)) {
      for (let x = 0; x < vuln.http_requests.length; x++) {
        newVuln.steps_to_reproduce = newVuln.steps_to_reproduce + 'HTTP Request\n\n';
        const httpRequest = vuln.http_requests[x];
        
        if (httpRequest.http_request) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Request: ' + httpRequest.http_request + '\n\n';
        }
        if (httpRequest.vuln_param) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Vulnerable Parameter: ' + httpRequest.vuln_param + '\n\n';
        }
        if (httpRequest.attack_payload) {
          newVuln.steps_to_reproduce = newVuln.steps_to_reproduce 
            + 'Attack Payload: ' + httpRequest.attack_payload + '\n\n';
        }
      }
    }
    if (vuln.cvss_version === '3.1') {
      if (vuln.cvss_final) {
        const tag = 'CVSSv3.1 Base Score: ' + vuln.cvss_final;
        Array.push(newVuln.tags, tag);
      }
      if (vuln.cvss_vector) {
        const tag = vuln.cvss_vector;
        Array.push(newVuln.tags, tag);
      }
      if (vuln.cvss_final) {
        if (vuln.cvss_final > 0 && vuln.cvss_final <= 3.9) {
          newVuln.priority = 'Low';
          newVuln.likelihood_of_exploitation = 2;
        }
        else if (vuln.cvss_final >= 4 && vuln.cvss_final <= 6.9) {
          newVuln.priority = 'Medium';
          newVuln.likelihood_of_exploitation = 4;
        }
        else if (vuln.cvss_final >= 7 && vuln.cvss_final <= 8.9) {
          newVuln.priority = 'High';
          newVuln.likelihood_of_exploitation = 7;
        }
        else if (vuln.cvss_final >= 9 && vuln.cvss_final <= 10) {
          newVuln.priority = 'Critical';
          newVuln.likelihood_of_exploitation = 9;
        }
      }
    }
    if (vuln.tag_list && Array.isArray(vuln.tag_list)) {
      for (let x = 0; x < vuln.tag_list.length; x++) {
        const tag = vuln.tag_list[x];

        if (tag.name) {
          Array.push(newVuln.tags, tag.name);
        }
      }
    }
    if (vuln.cve_ids && Array.isArray(vuln.cve_ids)) {
      for (let x = 0; x < vuln.cve_ids.length; x++) {
        let cve = "";
        let tag = "" + vuln.cve_ids[x];
        if (!String.startsWith(tag, 'CVE-')) {
          cve = cve + 'CVE-';
        }
        cve = cve + vuln.cve_ids[x];
        Array.push(newVuln.tags, cve);
      }
    }
    if (vuln.cwe_ids && Array.isArray(vuln.cwe_ids)) {
      for (let x = 0; x < vuln.cwe_ids.length; x++) {
        let cwe = "";
        let tag = "" + vuln.cwe_ids[x];
        if (!String.startsWith(tag, 'CWE-')) {
          cwe = cwe + 'CWE-';
        }
        cwe = cwe + vuln.cwe_ids[x];
        Array.push(newVuln.tags, cwe);
      }
    }
    if (vuln.category?.display) {
      const tag = 'Category: ' + vuln.category.display;
      Array.push(newVuln.tags, tag);
    }
    if (vuln.category?.parent) {
      const tag = 'Parent Category: ' + vuln.category.parent;
      Array.push(newVuln.tags, tag);
    }

    Array.push(data.vulnsToCreateUpdated, newVuln);
  }
  
  const vulnMap = {};
  
  for (let x = 0; x < data.vulnsToCreateUpdated.length; x++) {
    const vuln = data.vulnsToCreateUpdated[x];

    if (vuln.synackVulnId) {
      if (vuln.description) {
        vulnMap[vuln.synackVulnId + 'description'] = vuln.description;
      }
      if (vuln.attack_scenario) {
        vulnMap[vuln.synackVulnId + 'attack_scenario'] = vuln.attack_scenario;
      }
      if (vuln.remediation_recommendation) {
        vulnMap[vuln.synackVulnId + 'remediation_recommendation'] = vuln.remediation_recommendation;
      }
      if (vuln.steps_to_reproduce) {
        vulnMap[vuln.synackVulnId + 'steps_to_reproduce'] = vuln.steps_to_reproduce;
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: vulnMap
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to create'
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const vulnMap = response.jsonBody;
  
  for (let x = 0; x < data.vulnsToCreateUpdated.length; x++) {
    const vuln = data.vulnsToCreateUpdated[x];

    if (vuln.synackVulnId) {
      if (vulnMap[vuln.synackVulnId + 'description']) {
        vuln.description = vulnMap[vuln.synackVulnId + 'description'];
      }
      if (vulnMap[vuln.synackVulnId + 'attack_scenario']) {
        vuln.attack_scenario = vulnMap[vuln.synackVulnId + 'attack_scenario'];
      }
      if (vulnMap[vuln.synackVulnId + 'remediation_recommendation']) {
        vuln.remediation_recommendation = vulnMap[vuln.synackVulnId + 'remediation_recommendation'];
      }
      if (vulnMap[vuln.synackVulnId + 'steps_to_reproduce']) {
        vuln.steps_to_reproduce = vulnMap[vuln.synackVulnId + 'steps_to_reproduce'];
      }
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 8 - Create AF Vulnerabilities**

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

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.synackListingIdToAFProjectId) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.synackListingIdToAFProjectId',
    }
  };
}
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (data.vulnsToCreateUpdated && data.vulnsToCreateUpdated[0]) {
  let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability';
  delete data.vulnsToCreateUpdated[0].synackVulnId;
  
  return {
    decision: {
      status: 'continue',
      message: 'Create vulnerability',
    },
    request: {
      url: url,
      body: data.vulnsToCreateUpdated[0]
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities to create'
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerability?.vulnerability_id) {
  const vulnId = response.jsonBody.vulnerability.vulnerability_id;

  Array.shift(data.vulnsToCreateUpdated);

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

      if (customField.key === 'synack_vuln_id' && customField.value) {
        Array.push(data.vulnsToUpdateToTicketed, {
          afVulnId: vulnId,
          synackVulnId: customField.value
        });
      }
    }
  }

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      existingVulns: data.existingVulns,
      synackListingIdToAFProjectId: data.synackListingIdToAFProjectId,
      vulns: data.vulns,
      vulnsToCreate: data.vulnsToCreate,
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToCreateUpdated: data.vulnsToCreateUpdated
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 9 - Update Synack Vulns to Ticketed Status**

* **Method**: PUT
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (secrets.skip_updating_synack_status === 'yes') {
  return {
    decision: {
      status: 'finish',
      message: 'Skipping updating Synack ticketed status',
    }
  };
}

if (!data.vulnsToUpdateToTicketed) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnsToUpdateToTicketed',
    }
  };
}

if (!data.vulnsToUpdateStatus) {
  data.vulnsToUpdateStatus = [];
}

if (data.vulnsToUpdateToTicketed && data.vulnsToUpdateToTicketed[0]
  && data.vulnsToUpdateToTicketed[0].afVulnId && data.vulnsToUpdateToTicketed[0].synackVulnId
) {
  const afVulnId = data.vulnsToUpdateToTicketed[0].afVulnId;
  const synackVulnId = data.vulnsToUpdateToTicketed[0].synackVulnId;
  const synackVulnStatusName = secrets.synack_vuln_acknowledged_status_name;
  const synackVulnStatusId = secrets.synack_vuln_acknowledged_status_id;

  Array.push(data.vulnsToUpdateStatus, {
    afVulnId: afVulnId,
    synackVulnStatusName: synackVulnStatusName
  });
  
  const url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId;
  const body = {
    status_id: synackVulnStatusId
  };

  return {
    decision: {
      status: 'continue',
      message: 'Updating status for Synack vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No Synack vulnerabilities to process',
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  Array.shift(data.vulnsToUpdateToTicketed);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdateToTicketed: data.vulnsToUpdateToTicketed,
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 10 - Update AF Vulns to Ticketed Status**

* **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\_auth
* **Request Script**:

```javascript
if (!data.vulnsToUpdateStatus) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulnsToUpdateStatus',
    }
  };
}

if (data.vulnsToUpdateStatus && data.vulnsToUpdateStatus[0]
  && data.vulnsToUpdateStatus[0].afVulnId && data.vulnsToUpdateStatus[0].synackVulnStatusName
) {
  const afVulnId = data.vulnsToUpdateStatus[0].afVulnId;
  const synackVulnStatusName = data.vulnsToUpdateStatus[0].synackVulnStatusName;
  
  const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + afVulnId;
  const body = {
    custom_fields: [
      {
        key: 'synack_vuln_status',
        value: synackVulnStatusName
      }
    ]
  };

  return {
    decision: {
      status: 'continue',
      message: 'Updating Synack status for AF vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully completed flow',
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdateStatus);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdateStatus: data.vulnsToUpdateStatus
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status for AF vulnerability'
    }
  };
}
```

## Update Synack when Vuln is Ready for Retest

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2F32I2VPWeI2HYnOy1rw2l%2FSynack-2.png?alt=media&#x26;token=362a7a20-70d5-4e53-ad8e-46139c90ab78" alt=""><figcaption></figcaption></figure>

The purpose of this example is to update a vulnerability in [Synack](https://www.synack.com/) when a vulnerability is marked as Ready for Retest in AF.

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

* **Type**: Event - vulnerability-updated
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."

**Action 1 - Update Synack Vuln to Retest Status**

* **Method**: POST
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/patch\_verifications
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (data.vulnerability_retest !== 'Yes') {
  return {
    decision: {
      status: 'finish',
      message: 'Vulnerability is not ready for retest',
    }
  };
}

let synackVulnId;
let synackIsRetest = false;

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

    if (customField.key === 'synack_vuln_status' && customField.value === 'Patch Requested') {
      synackIsRetest = true;
    }
    else if (customField.key === 'synack_vuln_id' && customField.value) {
      synackVulnId = customField.value;
    }
  }
}
if (!synackVulnId) {
  return {
    decision: {
      status: 'finish',
      message: 'Synack vulnerability id not found',
    }
  };
}
if (synackIsRetest) {
  return {
    decision: {
      status: 'finish',
      message: 'Synack vulnerability is already set to Patch Requested',
    }
  };
}

const url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId + '/patch_verifications';
const body = {
  message: 'AttackForge - Retest Requested'
};

return {
  decision: {
    status: 'continue',
    message: 'Update Synack vuln status to Patch Requested',
  },
  request: {
    url: url,
    body: body
  },
  data: {
    vuln: data
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.id) {
  return {
    decision: {
      status: 'continue',
      message: 'Updated Synack vuln status',
    },
    data: {
      vuln: data.vuln
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 2 - Update AF Vuln with Synack Retest Status**

* **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\_auth
* **Request Script**:

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

const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + data.vuln.vulnerability_id;
const body = {
  custom_fields: [
    {
      key: 'synack_vuln_status',
      value: 'Patch Requested'
    }
  ]
};

return {
  decision: {
    status: 'continue',
    message: 'Updating Synack status for AF vulnerability',
  },
  request: {
    url: url,
    body: body
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully updated vuln status to Patch Requested'
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error updating Synack vulnerability status for AF vulnerability'
    }
  };
}
```

## Close Vuln in AF when Synack Vuln is Fixed

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FdGHPR6Ht4eupctLbI93Z%2FSynack-3.png?alt=media&#x26;token=72349f60-c031-42d9-bb29-dd38f78ca872" alt=""><figcaption></figcaption></figure>

The purpose of this example is to close a vulnerability in AF when its fixed in [Synack](https://www.synack.com/).

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

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (15 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."
  * **synack\_fetch\_changes\_from\_in\_days** - the number of days (in the past) to fetch vulnerabilities e.g. set to "1" for fetching updated Synack vulns in past 24-hours. Should match your Flow Cron String.

**Action 1 - Get Synack Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities?page\[size]=5\&page\[number]={page}\&filter\[updated\_since]={isoDate}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.page) {
  data.page = 1;
}
if (!data.updated_since) {
  const getVulnChangesFrom = '-' + secrets.synack_fetch_changes_from_in_days + ' days';
  data.updated_since = Date.datetime('now', getVulnChangesFrom, 'isostring');
}
if (!data.vulns) {
  data.vulns = [];
}

let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities?';
url = url + '&page[size]=5';
url = url + '&page[number]=' + data.page;
url = url + '&filter[updated_since]=' + data.updated_since;

return {
  decision: {
    status: 'continue',
    message: 'Fetching Synack vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    page: data.page,
    vulns: data.vulns,
    updated_since: data.updated_since
  }
};
```

* **Response Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}
if (!data.page) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.page',
    }
  };
}
if (!data.updated_since) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.updated_since',
    }
  };
}

if (response.jsonBody
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'No vulns left to process',
    },
    data: {
      vulns: data.vulns
    }
  };
}
else if (response.jsonBody 
  && Array.isArray(response.jsonBody)
  && response.jsonBody.length >= 1
  && response.jsonBody.length <= 5
){
  Logger.info('Found ' + response.jsonBody.length + ' vulns');

  for (let x = 0; x < response.jsonBody.length; x++) {
    const vuln = response.jsonBody[x];
    Array.push(data.vulns, vuln);
  }

  data.page = data.page + 1;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vuln pages',
    },
    data: {
      page: data.page,
      vulns: data.vulns,
      updated_since: data.updated_since
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 2 - Get Synack Patch Verified Vulns**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/patch\_verifications
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.vulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vulns',
    }
  };
}

if (!data.patchVerifiedVulns) {
  data.patchVerifiedVulns = [];
}

if (data.vulns[0] && data.vulns[0].id) {
  let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + data.vulns[0].id + '/patch_verifications';

  return {
    decision: {
      status: 'continue',
      message: 'Fetching Synack vulnerability patch verifications',
    },
    request: {
      url: url
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns,
      vuln: data.vulns[0]
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No vulnerabilities left to get patch verifications'
    },
    data: {
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
```

* **Response Script**:

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

if (response.jsonBody && Array.isArray(response.jsonBody) && response.jsonBody.length === 0 ){
  Logger.info('Found 0 patch verifications');

  Array.shift(data.vulns);

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vulns',
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody && Array.isArray(response.jsonBody) && response.jsonBody.length > 0 ){
  Logger.info('Found ' + response.jsonBody.length + ' patch verifications');

  Array.shift(data.vulns);

  for (let x = 0; x < response.jsonBody.length; x++) {
    const patchVerification = response.jsonBody[x];

    if (patchVerification.status?.status_text === 'Verified') {
      Array.push(data.patchVerifiedVulns, data.vuln);
    }
  }

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more vulns',
    },
    data: {
      vulns: data.vulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retreiving vuln patch verification',
    }
  };
}
```

**Action 3 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}
if (data.patchVerifiedVulns && data.patchVerifiedVulns.length === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No vulnerabilities to close',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    patchVerifiedVulns: data.patchVerifiedVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 4 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns,
    patchVerifiedVulns: data.patchVerifiedVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 5 - Close AF Vulns**

* **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\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.patchVerifiedVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.patchVerifiedVulns',
    }
  };
}

if (!data.vulnsToUpdate && data.existingVulns.length > 0) {
  data.vulnsToUpdate = [];

  const synackIdToExistingVuln = {};
  
  for (let x = 0; x < data.existingVulns.length; x++) {
    const vuln = data.existingVulns[x];

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

        if (customField.key === 'synack_vuln_id' && customField.value){
          synackIdToExistingVuln[customField.value] = vuln;
        }
      }
    }
  }

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

    if (vuln.id && synackIdToExistingVuln[vuln.id]) {
      const existingVuln = synackIdToExistingVuln[vuln.id];

      let afVulnIsClosed = false;
      if (existingVuln.vulnerability_status === 'Closed') {
        afVulnIsClosed = true;
      }

      if (!afVulnIsClosed) {
        Array.push(data.vulnsToUpdate, existingVuln.vulnerability_id);
      }
    }
  }
}

if (data.vulnsToUpdate && data.vulnsToUpdate[0]) {
  const vulnId = data.vulnsToUpdate[0];

  return {
    decision: {
      status: 'continue',
      message: 'Update vulnerability',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId,
      body: {
        status: 'Closed'
      }
    },
    data: {
      vulnsToUpdate: data.vulnsToUpdate,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Successfully completed flow',
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.result?.result === 'Vulnerability Updated') {
  Array.shift(data.vulnsToUpdate);
  Logger.info('Remaining vulns to close: ' + data.vulnsToUpdate.length);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all vulnerabilities until none left',
    },
    data: {
      vulnsToUpdate: data.vulnsToUpdate,
      existingVulns: data.existingVulns,
      patchVerifiedVulns: data.patchVerifiedVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

## Get Latest Synack Vuln Comments

<figure><img src="https://372186556-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-M8s1QY2Q6YTHB4a6DMu%2Fuploads%2FVVHmy3Jnn0kO1oE1SSuo%2FSynack-4.png?alt=media&#x26;token=c67e02c5-896f-46e2-b3ed-835720eacbd2" alt=""><figcaption></figcaption></figure>

The purpose of this example is to fetch vulnerability comments in [Synack](https://www.synack.com/) and create remediation notes in AF.

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

* **Type**: Schedule
* **Cron String**: Recommend at start of day, every 24-hours (30 9 \* \* \*)
* **Secrets**:
  * **af\_auth** - your AttackForge API key
  * **af\_tenant** - your AF tenant hostname e.g. "demo.attackforge.com"
  * **synack\_tenant** - for prod use "api.synack.com". For non-prod - seek guidance from your Synack account manager.
  * **synack\_token** - the API token created in Synack. E.g. "Bearer BLJZ\_clib-mzHkBKwauhiQkma...."

**Action 1 - Get Pending AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?pendingVulnerabilities=true\&skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?pendingVulnerabilities=true';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + '&skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

if (!data.existingVulns) {
  data.existingVulns = [];
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching pending vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all pending vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' pending vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more pending vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 2 - Get Visible AF Synack Vulns**

* **Method**: GET
* **URL**: https\://{{af-tenant}}/api/ss/vulnerabilities?skip={skip}\&limit=50\&q\_vulnerability={\<CUSTOM-QUERY>}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_auth
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'finish',
      message: 'Missing data.existingVulns',
    }
  };
}

let url = 'https://' + secrets.af_tenant + '/api/ss/vulnerabilities?';

let skip = 0;
if (data.skip !== undefined && data.skip !== null) {
  skip = data.skip;
}

url = url + 'skip=' + skip + '&limit=50';
url = url + '&q_vulnerability={custom_fields: {$elemMatch: {name: {$eq: "synack_vuln_id"}, value: {$ne: null}}}}';

return {
  decision: {
    status: 'continue',
    message: 'Fetching visible vulnerabilities',
  },
  request: {
    url: url
  },
  data: {
    skip: skip,
    existingVulns: data.existingVulns
  }
};
```

* **Response Script**:

```javascript
if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length === 0
){
  return {
    decision: {
      status: 'continue',
      message: 'Found all visible vulnerabilities',
    },
    data: {
      existingVulns: data.existingVulns
    }
  };
}
else if (response.jsonBody?.vulnerabilities && Array.isArray(response.jsonBody.vulnerabilities)
  && response.jsonBody.vulnerabilities.length <= 50
){
  Logger.info('Found ' + response.jsonBody.vulnerabilities.length + ' visible vulns');

  for (let x = 0; x < response.jsonBody.vulnerabilities.length; x++) {
    const vuln = response.jsonBody.vulnerabilities[x];
    
    Array.push(data.existingVulns, vuln);
  }

  data.skip = data.skip + 50;

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more visible vulns',
    },
    data: {
      skip: data.skip,
      existingVulns: data.existingVulns
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

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

**Action 3 - Get Synack Vuln Comments**

* **Method**: GET
* **URL**: https\://{{synack-tenant}}/v1/vulnerabilities/{id}/comments
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Authorization; Type = Secret; Value = synack\_token
* **Request Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}

if (!data.commentsToCreate) {
  data.commentsToCreate = [];
}

if (data.existingVulns && data.existingVulns[0]) {
  const vuln = data.existingVulns[0];
  let synackVulnId;
  let projectId;

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

      if (customField.key === 'synack_vuln_id' && customField.value) {
        synackVulnId = customField.value;
      }
    }
  }

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

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

  if (!synackVulnId) {
    return {
      decision: {
        status: 'abort',
        message: 'Missing synackVulnId',
      }
    };
  }
  else if (!projectId) {
    return {
      decision: {
        status: 'abort',
        message: 'Missing projectId',
      }
    };
  }

  let url = 'https://' + secrets.synack_tenant + '/v1/vulnerabilities/' + synackVulnId + '/comments';

  return {
    decision: {
      status: 'continue',
      message: 'Fetching Synack vulnerability comments',
    },
    request: {
      url: url
    },
    data: {
      existingVulns: data.existingVulns,
      commentsToCreate: data.commentsToCreate,
      vuln: vuln
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No existing comments left to process'
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
```

* **Response Script**:

```javascript
if (!data.existingVulns) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.existingVulns',
    }
  };
}
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}
if (!data.vuln) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.vuln',
    }
  };
}

if (response.jsonBody && Array.isArray(response.jsonBody)){
  const remediationNotes = [];

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

      if (note.note_html) {
        Array.push(remediationNotes, note.note_html);
      }
    }
  }

  let projectId;

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

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

  const comments = response.jsonBody;

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

    let commentExists = false;
    let commentText = '';
    let commentTimestamp = '';
    if (comment.created_at) {
      commentTimestamp = Date.datetime(comment.created_at * 1000);
    }

    if (comment.body && comment.created_at && comment.User?.name) {
      commentText = commentText + 'Synack: ' + comment.User.name 
        + ' on ' + commentTimestamp + '\n\n' + comment.body;
    }

    for (let y = 0; y < remediationNotes.length; y++) {
      const note = remediationNotes[y];

      if (String.includes(note, commentTimestamp) === true) {
        commentExists = true;
      }
    }

    if (!commentExists) {
      Array.push(data.commentsToCreate, {
        projectId: projectId,
        vulnId: data.vuln.vulnerability_id,
        note: commentText,
        commentId: comment.id
      });
    }
  }

  Array.shift(data.existingVulns);

  return {
    decision: {
      status: 'repeat',
      message: 'Fetch more Synack vulnerability comments',
    },
    data: {
      existingVulns: data.existingVulns,
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error retrieving Synack vulnerability comments',
    }
  };
}
```

**Action 4 - Convert Markdown to Rich Text for Comments**

* **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\_auth
* **Request Script**:

```javascript
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}

if (data.commentsToCreate && data.commentsToCreate.length > 0) {
  const commentMap = {};
  
  for (let x = 0; x < data.commentsToCreate.length; x++) {
    const comment = data.commentsToCreate[x];

    if (comment.commentId && comment.note) {
      commentMap[comment.commentId] = comment.note;
    }
  }

  return {
    decision: {
      status: 'continue',
      message: 'Convert markdown to rich text',
    },
    request: {
      url: 'https://' + secrets.af_tenant + '/api/ss/utils/markdown-to-richtext',
      body: commentMap
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'No remediation notes to create'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody) {
  const commentMap = response.jsonBody;
  
  for (let x = 0; x < data.commentsToCreate.length; x++) {
    const comment = data.commentsToCreate[x];

    if (comment.commentId && commentMap[comment.commentId]) {
      comment.note = commentMap[comment.commentId];
    }
  }
  
  return {
    decision: {
      status: 'continue',
      message: 'Converted markdown fields to rich text',
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error converting markdown to rich text'
    }
  };
}
```

**Action 5 - 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\_auth
* **Request Script**:

```javascript
if (!data.commentsToCreate) {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.commentsToCreate',
    }
  };
}

if (data.commentsToCreate && data.commentsToCreate[0]) {
  const note = data.commentsToCreate[0].note;
  const vulnId = data.commentsToCreate[0].vulnId;
  const projectId = data.commentsToCreate[0].projectId;

  const url = 'https://' + secrets.af_tenant + '/api/ss/vulnerability/' + vulnId + '/remediationNote';
  const body = {
    note: note,
    note_type: 'RICHTEXT',
    projectId: projectId
  };

  return {
    decision: {
      status: 'continue',
      message: 'Creating remediation note on vulnerability',
    },
    request: {
      url: url,
      body: body
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'No remediation notes left to create'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.jsonBody?.note?.id) {
  Array.shift(data.commentsToCreate);

  return {
    decision: {
      status: 'repeat',
      message: 'Process all remediation notes until none left',
    },
    data: {
      commentsToCreate: data.commentsToCreate
    }
  };
}
else {
  Logger.error(JSON.stringify(response));

  return {
    decision: {
      status: 'abort',
      message: 'Error creating remediation note'
    }
  };
}
```
