# Tenable

## Tenable WAS - Initiate Project Scope Scan \[Step 1 of 5]

<figure><img src="/files/m78H7BW5ByfOFP9HBLM3" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a Web Application scan of the assets on the project scope in Tenable when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * was\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable WAS - Launch Scan \[Step 2 of 5\] Flow](#tenable-was-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Determine Action Entity**&#x20;

* **Script**:

```javascript
if (data?.project){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on project'
    },
    data: {
      project: data.project
    }
  };
}
else if (data?.projects){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on projects'
    },
    data: {
      projects: data.projects
    }
  };
}
else {
  return {
    decision:{
      status: 'abort',
      message: 'Flow called on an unsupported entity'
    }
  };
}
```

**Action 2 - Get Project(s)**

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

```javascript
if (data.project) {
  return {
    decision:{
      status: 'next',
      message: 'Only one project, skipping to next action.'
    },
    data: {
      project: data.project
    }
  };
}
else if (data.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter){
    data.counter = 0;
  }

  if (Array.isArray(projects) 
    && Array.length(projects) > 0 
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;
    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (!data?.projects && !data?.project) {
  return {
    decision:{
      status: 'abort',
      message: 'data.projects and/or data.project missing'
    }
  };
}
if (data?.counter === undefined) {
  return {
    decision:{
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

data.projects[data.counter]?.assets = [];
if (project.project_scope){
  data.projects[data.counter].assets = project.project_scope;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data:  {
      projects: data.projects
    }
  };
}
```

**Action 3 - Trigger WAS Scan Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let body;
let project_id;
let project_scope_assets;

// Project
if (data?.project?.project_id && data.project.project_scope) {
  project_scope_assets = data.project.project_scope;
  project_id = data.project.project_id;
}
// Projects
else if (data?.projects && Array.length(data.projects) > 0){
  const curr_project = data.projects[0];
  if (curr_project.assets && curr_project.project_id) {
    project_scope_assets = curr_project.assets;
    project_id = curr_project.project_id;
  }
}

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'project_id is missing'
    }
  };
}
if (!project_scope_assets) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is missing'
    }
  };
}

const valid_project_scope_assets = [];
if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < project_scope_assets.length; x++) {
    let asset = project_scope_assets[x];
    if (String.startsWith(asset, 'http://') || String.startsWith(asset, 'https://')) {
      Array.push(valid_project_scope_assets, asset);
    }
    else if (isValidHostname(asset)) {
      asset = 'https://' + asset;
      Array.push(valid_project_scope_assets, asset);
    }
  }
}
project_scope_assets = valid_project_scope_assets;

if (Array.isArray(project_scope_assets) && project_scope_assets.length === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is empty'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Project id and asset/scope sent to was_scan_flow: ' + secrets.was_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.was_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      assets: project_scope_assets 
    }
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      return num >= 0 && num <= 255;
    }
  }
}

function isValidCIDR(str) {
  const match = String.match(str, m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/);
  if (!match) return false;
  const octets = [+match[1], +match[2], +match[3], +match[4]];
  const prefix = +match[5];
  let isValid = true;
  if (octets && Array.isArray(octets) && Array.length(octets) > 0) {
    for (let x = 0; x < octets.length; x++) {
      const o = octets[x];
      if (!(o >= 0 && o <= 255)) {
        isValid = false;
      }
    }
  }
  if (!(prefix >= 0 && prefix <= 32)) {
    isValid = false;
  }
  return isValid;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submiting project to was_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submiting project to was_scan_flow. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision:{
        status: 'repeat',
        message: 'Sending next request...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
  else {
    return {
      decision: {
        status: 'finish',
        message: 'Completed sending all projects to WAS Scan Flow.',
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed sending project to WAS Scan Flow.',
    }
  };
}
```

## Tenable VM - Initiate Project Scope Scan \[Step 1 of 5]

<figure><img src="/files/sX6Hi8tFFeF3Nf4eQe8J" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a VM scan of the assets on the project scope in Tenable when a user clicks on an [Action](https://support.attackforge.com/attackforge-enterprise/actions) in AttackForge.

This example Flow can be downloaded from our [Flows GitHub Repository](https://github.com/AttackForge/Flows) and [imported](#importing-exporting-flows) into your AttackForge.

**Initial Set Up**

* **Action**:&#x20;
  * Entities: Projects and Project
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * vm\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM - Launch Scan \[Step 2 of 5\] Flow](#tenable-vm-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Determine Action Entity**

* **Script**:

```javascript
if (data?.project){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on project'
    },
    data: {
      project: data.project
    }
  };
}
else if (data?.projects){
  return {
    decision:{
      status: 'continue',
      message: 'Action called on projects'
    },
    data: {
      projects: data.projects
    }
  };
}
else {
  return {
    decision:{
      status: 'abort',
      message: 'Flow called on an unsupported entity'
    }
  };
}
```

**Action 2 - Get Project(s)**

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

```javascript
if (data.project) {
  return {
    decision:{
      status: 'next',
      message: 'Only one project, skipping to next action.'
    },
    data: {
      project: data.project
    }
  };
}
else if (data.projects){
  const projects = data.projects;

  // Initialise counter
  if (!data.counter){
    data.counter = 0;
  }

  if (Array.isArray(projects) 
    && Array.length(projects) > 0 
    && projects?[data.counter]?.project_id
  ){
    const project_id = projects[data.counter].project_id;
    return {
      decision: {
        status: 'continue',
        message: 'Fetching project: ' + project_id
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/project/' + project_id
      },
      data: {
        projects: projects,
        counter: data.counter
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('projects: ', JSON.stringify(projects));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error while validating "projects" object. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Missing data.projects and/or data.project'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching projects: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error fetching projects. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}

if (!data?.projects && !data?.project) {
  return {
    decision:{
      status: 'abort',
      message: 'data.projects and/or data.project missing'
    }
  };
}
if (data?.counter === undefined) {
  return {
    decision:{
      status: 'abort',
      message: 'data.counter missing'
    }
  };
}

const project = response.jsonBody?.project;

if (!project) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Project not found');
    Logger.debug('jsonBody: ', JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No projects matched with ' + data.projects[data.counter].project_id
    }
  };
}

data.projects[data.counter].assets = [];
if (project.project_scope){
  data.projects[data.counter].assets = project.project_scope;
}

if (Array.length(data.projects) > (data.counter + 1)) {
  data.counter = data.counter + 1;
  return {
    decision: {
      status: 'repeat',
      message: 'Fetching next project.'
    },
    data: {
      projects: data.projects,
      counter: data.counter
    }
  };
}
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('Fetched project scope for all projects. ', 'Projects: ', JSON.stringify(data.projects));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Fetched project scope for all projects, continuing to next step.'
    },
    data:  {
      projects: data.projects
    }
  };
}
```

**Action 3 - Trigger VM Scan Flow**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
let body;
let project_id;
let project_scope_assets;

// Project
if (data?.project?.project_id && data.project.project_scope) {
  project_scope_assets = data.project.project_scope;
  project_id = data.project.project_id;
}
// Projects
else if (data?.projects && Array.length(data.projects) > 0){
  const curr_project = data.projects[0];
  if (curr_project.assets && curr_project.project_id) {
    project_scope_assets = curr_project.assets;
    project_id = curr_project.project_id;
  }
}

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'project_id is missing'
    }
  };
}
if (!project_scope_assets) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is missing'
    }
  };
}

const valid_project_scope_assets = [];
if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < project_scope_assets.length; x++) {
    const asset = project_scope_assets[x];
    if (isValidCIDR(asset) || isValidIPv4(asset) || isValidHostname(asset)) {
      Array.push(valid_project_scope_assets, asset);
    }
  }
}
project_scope_assets = valid_project_scope_assets;

if (Array.isArray(project_scope_assets) && project_scope_assets.length === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'project_scope_assets is empty'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Project id and asset/scope sent to vm_scan_flow: ' + secrets.vm_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.vm_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      assets: project_scope_assets 
    }
  }
};

function isValidHostname(str) {
  if (str.length === 0 || str.length > 253) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length === 0 || label.length > 63) return false;
      if (label[0] === '-' || label[label.length - 1] === '-') return false;
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) return false;
    }
  }
  return true;
}

function isValidIPv4(str) {
  if (!(str =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/)) return false;
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < labels.length; x++) {
      const label = labels[x];
      if (label.length > 1 && label[0] === '0') return false;
      const num = Number.parseInt(label);
      return num >= 0 && num <= 255;
    }
  }
}

function isValidCIDR(str) {
  const match = String.match(str, m/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/);
  if (!match) return false;
  const octets = [+match[1], +match[2], +match[3], +match[4]];
  const prefix = +match[5];
  let isValid = true;
  if (octets && Array.isArray(octets) && Array.length(octets) > 0) {
    for (let x = 0; x < octets.length; x++) {
      const o = octets[x];
      if (!(o >= 0 && o <= 255)) {
        isValid = false;
      }
    }
  }
  if (!(prefix >= 0 && prefix <= 32)) {
    isValid = false;
  }
  return isValid;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submiting project to was_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submiting project to was_scan_flow. Status: ' + (response?.statusCode || 'unknown')
    }
  };
}
if (data?.projects) {
  Array.shift(data.projects);
  if (Array.length(data.projects) > 0) {
    return {
      decision:{
        status: 'repeat',
        message: 'Sending next request...',
        delay: 1000
      },
      data: {
        projects: data.projects
      }
    };
  }
  else {
    return {
      decision: {
        status: 'finish',
        message: 'Completed sending all projects to VM Scan Flow.',
      }
    };
  }
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed sending project to VM Scan Flow.',
    }
  };
}
```

## Tenable WAS - Launch Scan \[Step 2 of 5]

<figure><img src="/files/CmaAjHuiI0SyUU0LJRCe" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a Web Application scan in Tenable.

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**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * owner\_id - the Tenable ID of the owner of the scanner.
  * tenable\_template\_id - the UUID of the Tenable-provided template resource.
  * user\_template\_id - the UUID of the Tenable user-defined template.
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Create WAS Scan Configuration**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
// user custom fields
const scan_name = 'WAS Scan on Flows'; 
const description = 'WAS scan created via AttackForge Flow';

// From body
const project_asset = data.jsonBody?.assets;
const project_id = data.jsonBody?.project_id;

if (!project_id) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.project_id must exist. Please check the log.'
    }
  };
}
if (!project_asset) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must exist. Please check the log.'
    }
  };
}
if (!Array.isArray(project_asset)) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must be an array. Please check the log.'
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug('scan_name: ', scan_name);
  Logger.debug('description: ', description);
  Logger.debug('project_asset: ', project_asset);
}

return {
  decision: {
    status: 'continue',
    message: 'Creating WAS configuration.',
  },
  request: {
    body: {
      name: scan_name,
      description: description,
      targets: project_asset,
      template_id: secrets.tenable_template_id,
      user_template_id: secrets.user_template_id,
      owner_id: secrets.owner_id
    }
  },
  data: {
    project_id: project_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 201 && response.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('Error: ' + JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error creating WAS configuration. Please check the log.',
    }
  };
}

const response_body = response.jsonBody;
const config_id = response_body?.config_id;

if (!config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response_body: ' + JSON.stringify(response_body));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No config_id returned. Please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Configuration created: ' + config_id,
  },
  data: {
    project_id: data?.project_id,
    config_id: config_id
  }
};
```

**Action 2 - Launch WAS Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.config_id must be present for WAS Scan launch. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Launching WAS scan.',
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/configs/' + data.config_id + '/scans'
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 201 && response.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('Error: ' + JSON.stringify(response.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error launching WAS scan',
    }
  };
}

const scan_id = response.jsonBody?.scan_id;

if (!scan_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response.body: ' + JSON.stringify(response.body));
  }
  return {
    decision: {
      status: 'abort',
      message: 'No scan_id returned, please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Scan launched: ' + scan_id,
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id,
    scan_id: scan_id
  }
};
```

**Action 3 - Update Project with Scan Details**

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

```javascript
if (!data?.project_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: project_id is missing'
    }
  };
}
if (!data?.scan_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: scan_id is required in order to update tenable_was_active_scan custom_field. Please check the log'
    }
  };
}
if (!data.config_id){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data:', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: config_id is required in order to update tenable_was_active_scan custom_field. Please check the log'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating project with "tenable_was_active_scan" and "tenable_was_config_id" custom_fields.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_was_active_scan",
          value: String.from(data.scan_id)
        },
        {
          key: "tenable_was_config_id",
          value: String.from(data.config_id)
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    config_id: data?.config_id,
    scan_id: data?.scan_id
  }
};
```

* **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 project with Tenable scan custom fields. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated project: ' + data?.project_id + ' with "tenable_was_active_scan" custom field: '+ data?.scan_id +' and "tenable_was_config_id custom field: "' + data?.config_id,
  }
};
```

## Tenable VM - Launch Scan \[Step 2 of 5]

<figure><img src="/files/2IK8a9eXiRcua3GnDZSG" alt=""><figcaption></figcaption></figure>

The purpose of this example is to create and launch a VM scan in Tenable.

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**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * scan\_name\_prefix - the prefix for the scan name in Tenable e.g. AttackForge Project Scan
  * vm\_network\_scan\_template\_uuid - the UUID for the Tenable VM scan template
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Create VM Scan Configuration**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/scans>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
const curr_time = Date.datetime('now');
// Users can customise scan name by configuring scan name pattern in secret or as a variable
const scan_name = secrets.scan_name_prefix + curr_time;
// Users can set description of scan here.
const description = 'Example description';

// From body
const project_asset = data.jsonBody?.assets;
const project_id = data.jsonBody?.project_id;

if (!project_id) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.project_id must exist. Please check the log.'
    }
  };
}
if (!project_asset) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must exist. Please check the log.'
    }
  };
}
if (!Array.isArray(project_asset)) {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.jsonBody.assets must be an array. Please check the log.'
    }
  };
}

// multiple targets - simply separate it with comma in target "target1, target2"
let target_list = '';
for (let x = 0; x < project_asset.length; x++) {
  const asset = project_asset[x];
  if (x === project_asset.length - 1) {
    target_list = target_list + asset;
  }
  else {
    target_list = target_list + asset + ',';
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Sending a request for create VM scan.'
  },
  request: {
    body: {
      settings: {
        name: scan_name,
        description: description,
        text_targets: target_list
      },
      uuid: secrets.vm_network_scan_template_uuid
    },
  },
  data: {
    project_id: project_id
  }
};
```

* **Response Script**:

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

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

const scan_id = response.jsonBody.scan.id;

return {
  decision: {
    status: 'continue',
    message: 'Scan id created in Tenable',
  },
  data: {
    project_id: data?.project_id,
    scan_id: scan_id
  }
};
```

**Action 2 - Launch VM Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id}/launch>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data.scan_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.scan_id is required for launching Tenable VM Scan. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Launching Tenable VM Scan on scan_id:' + data.scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/scans/' + data.scan_id + '/launch'
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

* **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 Launching the VM Scan, please check the log.',
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully launched VM Scan.',
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

**Action 3 - Update Project with Scan Details**

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

```javascript
if (!data?.scan_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: scan_id is required.'
    }
  };
}
if (!data?.project_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: project_id is required.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Updating "tenable_vm_active_scan" on project ' + data.project_id + '.',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_vm_active_scan",
          value: String.from(data.scan_id)
        }
      ]
    }
  }
};
```

* **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 project with "tenable_vm_active_scan" custom_field'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully completed updating project with "tenable_vm_active_scan" custom_field'
  }
};
```

## Tenable VM & WAS - Find Active Scans to Poll Status \[Step 3 of 5]

<figure><img src="/files/qBPeMNHtk8L913nDohNF" alt=""><figcaption></figcaption></figure>

The purpose of this example is to poll the scan status for any active Tenable scans, and for any completed scans - trigger a download of the vulnerabilities.

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**:&#x20;
  * Every hour
    * CRON String: 0 0/1 \* \* \*
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * vm\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM - Poll Scan Status \[Step 4 of 5\] Flow](#tenable-vm-poll-scan-status-step-4-of-5)
  * was\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable WAS - Poll Scan Status \[Step 4 of 5\] Flow](#tenable-was-poll-scan-status-step-4-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Tenable VM & WAS Scan Projects**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/projects?order=created:desc\&limit=500
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Getting all projects with tenable active scan ids.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/projects?order=created:desc&limit=500'
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  return {
    decision: { 
      status: 'abort', 
      message: 'Error fetching projects.'
    }
  };
}

const projects = response?.jsonBody?.projects;

if (!projects || projects && Array.length(projects) === 0) {
  return {
    decision: { 
      status: 'abort', 
      message: 'No projects found.'
    }
  };
}

const vm_projects = [];
const was_projects = [];

for (let i = 0; i < Array.length(projects); i++) {
  const project = projects[i];
  const cfs = project.project_custom_fields;

  if (!cfs || Array.length(cfs) === 0) {
    continue;
  }

  const vm_scan_id = Array.find(cfs, find_vm_scan);
  const was_scan_id = Array.find(cfs, find_was_scan);
  const was_cf_config = Array.find(cfs, find_was_config);

  if (vm_scan_id) {
    Array.push(vm_projects, {
      project_id: project.project_id,
      scan_id: vm_scan_id.value
    });
  }

  if (was_scan_id && was_cf_config) {
    Array.push(was_projects, {
      project_id: project.project_id,
      scan_id: was_scan_id.value,
      config_id: was_cf_config.value
    });
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched ' + Array.length(vm_projects) + ' VM Projects, and ' + Array.length(was_projects) + ' WAS Projects.'
  },
  data: {
    vm_projects: vm_projects,
    was_projects: was_projects
  }
};

function find_vm_scan(field){
  return field.key === 'tenable_vm_active_scan';
}

function find_was_scan(field){
  return field.key === 'tenable_was_active_scan';
}

function find_was_config(field){
  return field.key === 'tenable_was_config_id';
}
```

**Action 2 - Tenable VM - Get Scan Status + Trigger Download**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{vm\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.vm_projects && Array.length(data.vm_projects) > 0) {
  return {
    decision: {
      status: 'continue',
      message: 'Requesting latest scan status and triggering download for: ' + JSON.stringify(data.vm_projects[0])
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.vm_poll_flow_trigger_id,
      body: {
        project: data.vm_projects[0]
      }
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'No active VM scan ids found, proceeding to next action.'
    }
  };
}
```

* **Response Script**:

```javascript
if (!response?.statusCode === 202) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error while requesting latest scan status update. Please check the log.'
    }
  };
}
if (!data?.vm_projects) {
  return{
    decision:{
      status: 'abort',
      message: 'data.vm_projects missing'
    }
  };
}

Array.shift(data.vm_projects);

if (Array.length(data.vm_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Successfully requested latest scan status update. Repeating the action',
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };  
}
else {
  return {
    decision: {
      status: 'continue',
      message: 'Successfully requested latest scan status update. Continuing to next action.',
    },
    data: {
      vm_projects: data?.vm_projects,
      was_projects: data?.was_projects
    }
  };
}
```

**Action 3 - Tenable WAS - Get Scan Status + Trigger Download**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/flows/{{was\_poll\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.was_projects && Array.length(data.was_projects) > 0) {
  return {
    decision: {
      status: 'continue',
      message: 'Requesting scan update for project: ' + JSON.stringify(data.was_projects[0]),
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.was_poll_flow_trigger_id,
      body: {
        project: data.was_projects[0]
      }
    },
    data: {
      was_projects: data?.was_projects
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed processing scans'
    }
  };
}
```

* **Response Script**:

```javascript
if (!response?.statusCode === 202) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while requesting latest scan status update. Please check the log.'
    }
  };
}
if (!data?.was_projects) {
  return{
    decision: {
      status: 'abort',
      message: 'data.was_projects missing'
    }
  };
}

Array.shift(data.was_projects);

if (Array.length(data.was_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Successfully requested latest scan status update. Repeating the action',
    },
    data: {
      was_projects: data.was_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed processing scans'
    }
  };
}
```

## Tenable WAS - Poll Scan Status \[Step 4 of 5]

<figure><img src="/files/yxduxVsHFr6nlfuR5mzY" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a Web Application scan in Tenable.

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**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * apikey - your AttackForge user API key
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * admin\_user\_id - the Id for the user in AttackForge who receives error emails (used for error reporting)
  * import\_vuln\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5\] Flow](#tenable-vm-and-was-import-vulnerabilities-step-5-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll Scan Status**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/was/v2/scans/{scan\\_id}>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data?.jsonBody?.project?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.project_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.scan_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.scan_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.config_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.config_id missing'
    }
  };
}

const project_id = data.jsonBody.project.project_id;
const scan_id = data.jsonBody.project.scan_id;
const config_id = data.jsonBody.project.config_id;

return {
  decision: {
    status: 'continue',
    message: 'Checking scan status for scan_id: ' + scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/scans/' + scan_id
  },
  data: {
    project_id: project_id,
    scan_id: scan_id,
    config_id: config_id
  }
};
```

* **Response Script**:

```javascript
// Error
if (response.statusCode !== 200){
  if (response.jsonBody?.reasons?[0]?.code === 'NOT_FOUND') {
    if (secrets.logging_level === 'debug') {
      Logger.debug('code: ', response.jsonBody.reasons[0].code);
      Logger.debug(JSON.stringify(response));
    }
    if (!data.retry_counter) {
      data.retry_counter = 0;
    }
    data.retry_counter = data.retry_counter + 1;

    if (data.retry_counter > 3) {
      if (secrets.logging_level === 'debug'){
        Logger.debug('response: ', JSON.stringify(response));
      }
      return {
        decision: {
          status: 'abort',
          message: 'Retry failed 3 times. Exiting the process. Please check the log.',
        }
      };
    }

    return {
      decision: {
        status: 'repeat',
        message: 'Scan not found with scan_id, retrying...',
      },
      data: {
        project_id: data?.project_id,
        scan_id: data?.scan_id,
        config_id: data?.config_id,
        retry_counter: data?.retry_counter,
        scan_type: data?.scan_type
      }
    };
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error getting scan status',
    }
  };
}

// Success
const response_body = response.jsonBody;
const status = response_body?.status;
const error_statuses = ['failed', 'aborted', 'cancelled'];

if (Array.includes(error_statuses, status)){
  return {
    decision: {
      status: 'abort',
      message: 'Scan ended with status: ' + status,
    }
  };
}

if (status === 'completed') {
  // Single-target
  if (response_body.target && !response_body?.target_scans){
    data.scan_type = 'single-target';
  }
  // Multi-target
  if (response_body?.target_scans?.completed > 0){
    data.scan_type = 'multi-target';
  }

  return {
    decision: {
      status: 'continue',
      message: 'Scan ' + (data.scan_type || 'unknown') + ' completed.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
} 
else {
  // If the scan returns incomplete and not failed - then exit the flow.
  // Next scheduled project scan flow will trigger this polling again.
  if (secrets.logging_level === 'debug'){
    Logger.debug('Scan Status: ', status);
  }
  return {
    decision:{
      status: 'finish',
      message: 'Polling completed. Scan status: ' + status + '. Exiting the flow.'
    }
  };
}
```

**Action 2 - Search Scans (multi-target only)**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans/search>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.scan_type === 'multi-target') {
  return {
    decision: {
      status: 'continue',
      message: 'Searching children scan_ids with config_id',
    },
    request: {
      url: 'https://cloud.tenable.com/was/v2/configs/' + data.config_id + '/scans/search'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
} 
else {
  return {
    decision: {
      status: 'next',
      message: 'Not a multi-target scan, proceeding to exporting step.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  if (secrets.logging_level === 'debug'){
    Logger.debug('data.config_id: ', data.config_id);
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error searching scan with config_id: ' + data.config_id + '. Please check the log.',
    }
  };
}

const items = response?.jsonBody?.items;

if (!items){
  return {
    decision:{
      status: 'abort',
      message: 'Items not found in response body.'
    }
  };
}

const children_scan_ids = [];
const aborted_scan_ids = [];

for (let i = 0; i < Array.length(items); i++) {
  if (items[i]?.parent_id === data.scan_id && items[i].status === 'completed' && items[i].scan_id) {
    Array.push(children_scan_ids, items[i].scan_id);
  }
  if (items[i].status === 'aborted' && items[i].scan_id) {
    Array.push(aborted_scan_ids, {
      scan_id: items[i].scan_id, 
      status: "aborted"
    });
  }
}

// Warn the aborted scan ids.
// Treating aborted scan as failed but completed scan.
if (secrets.logging_level === 'debug'){
  Logger.debug('Children Scan Ids: ', children_scan_ids);
  Logger.warn('Aborted Scan Ids: ', aborted_scan_ids);
}

if (Array.length(children_scan_ids) > 0){
  return {
    decision: {
      status: 'continue',
      message: 'Continue to next action',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: children_scan_ids,
      aborted_scan_ids: aborted_scan_ids
    }
  };
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'No children found.'
    }
  };
}
```

**Action 3 - Export Scan Report**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/was/v2/scans/{scan\\_id}/report>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
// Initialise
if (!data.final_result){
  data.final_result = [];
}
let current_scan;

// multi-target
if (data?.children_scan_ids 
  && Array.isArray(data.children_scan_ids) 
  && Array.length(data.children_scan_ids) > 0
) {
  current_scan = data.children_scan_ids[0];
}
else {
  // single-target
  current_scan = data.scan_id;
}

if (!current_scan){
  if (secrets.logging_level === 'debug'){
    if (data?.children_scan_ids){
      Logger.debug('data.children_scan_ids: ', JSON.stringify(data.children_scan_ids));
    } 
    else {
      Logger.debug('data.scan_id: ', data.scan_id);
    }
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: current_scan must be present. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Exporting scan report.'
  },
  request: {
    url: 'https://cloud.tenable.com/was/v2/scans/' + current_scan + '/report'
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error exporting scan report',
    }
  };
}

// Single-target will also be recorded under final_result
if (data?.final_result) {
  Array.push(data.final_result, response.jsonBody);
}

if (data?.children_scan_ids){
  Array.shift(data.children_scan_ids);
  if (Array.length(data.children_scan_ids) > 0){
    return {
      decision: {
        status: 'repeat',
        message: 'Repeating process on next children scan.',
      },
      data: {
        project_id: data?.project_id,
        scan_id: data?.scan_id,
        config_id: data?.config_id,
        retry_counter: data?.retry_counter,
        scan_type: data?.scan_type,
        children_scan_ids: data?.children_scan_ids,
        aborted_scan_ids: data?.aborted_scan_ids,
        final_result: data?.final_result
      }
    };
  }
}

return {
  decision: {
    status: 'continue',
    message: 'Scan report exported successfully.',
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

**Action 4 - Import Scan Report Result**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{import\_vuln\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data.project_id) {
  return {
    decision: {
      status: 'continue',
      message: 'Import scan results'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.import_vuln_flow_trigger_id,
      body: {
        final_result: data.final_result,
        project_id: data.project_id
      },
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: data?.children_scan_ids,
      aborted_scan_ids: data?.aborted_scan_ids,
      final_result: data?.final_result
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 202){
  return {
    decision: {
      status: 'continue',
      message: 'Successfully called Importing Scan flow. Proceeding to next action.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      config_id: data?.config_id,
      retry_counter: data?.retry_counter,
      scan_type: data?.scan_type,
      children_scan_ids: data?.children_scan_ids,
      aborted_scan_ids: data?.aborted_scan_ids,
      final_result: data?.final_result
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error while calling Import Scan flow. Please check the log.'
    }
  };
}
```

**Action 5 - Update Project to Remove Table Scan Details**

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

```javascript
if (!data?.project_id) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return{
    decision:{
      status: 'abort',
      message: 'Error: data.project_id must exist to update project. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Removing tenable_active_scan from project: ' + data.project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_was_active_scan",
          value: null
        },
        {
          key: "tenable_was_config_id",
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update project: ' + data?.project_id + '. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully removed tenable_was_active_scan from project: ' + data?.project_id
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    config_id: data?.config_id,
    retry_counter: data?.retry_counter,
    scan_type: data?.scan_type,
    children_scan_ids: data?.children_scan_ids,
    aborted_scan_ids: data?.aborted_scan_ids,
    final_result: data?.final_result
  }
};
```

**Action 6 - Send Email Report on Aborted Scan**

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

```javascript
if (data?.aborted_scan_ids 
  && Array.isArray(data.aborted_scan_ids) 
  && Array.length(data.aborted_scan_ids) > 0
) {
  let aborted_scan_id_table = "";
  for (let i = 0; i < Array.length(data.aborted_scan_ids); i++) {
    if (data.aborted_scan_ids[i]?.scan_id && data.aborted_scan_ids[i]?.status) {
      aborted_scan_id_table = aborted_scan_id_table + '<tr><td>' + data.aborted_scan_ids[i].scan_id + '</td><td>' + data.aborted_scan_ids[i].status + '</td></tr>';
    }
  }

  const curr_year = Date.format(Date.datetime('now'), 'yyyy');
  const emailHeader = "<!doctype html> <html lang='en' xmlns:v='urn:schemas-microsoft-com:vml' style='color-scheme: light dark'> <head> <meta charset='utf-8'> <meta name='x-apple-disable-message-reformatting'> <meta name='viewport' content='width=device-width, initial-scale=1'> <meta name='format-detection' content='telephone=no, date=no, address=no, email=no, url=no'> <meta name='color-scheme' content='light dark'> <meta name='supported-color-schemes' content='light dark'> <link rel='preconnect' href='https://fonts.googleapis.com'> <link rel='preconnect' href='https://fonts.gstatic.com' crossorigin> <link href='https://fonts.googleapis.com/css2?family=Nunito+Sans:wght@400;700&display=swap' rel='stylesheet' media='screen'> <style> .body-sub { margin-top: 25px; border-top-width: 1px; padding-top: 25px; border-top-color: #eaeaec; border-top-style: solid; } body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } .sm-w-full { width: 100% !important; } /*@media (prefers-color-scheme: dark) { body, .email-body, .email-body_inner, .email-content, .email-wrapper, .email-masthead, .email-footer { background-color: #1e293b !important; color: #fff !important; } p, ul, ol, blockquote, h1, h2, h3 { color: #fff !important; } } :root { color-scheme: light dark; }*/ /*@media (max-width: 600px) { .sm-w-full { width: 100% !important; } }*/ h1 { margin-top: 0; text-align: left; font-size: 24px; font-weight: 700; color: #333333 } p { margin-bottom: 5px; margin-top: 6px; font-size: 16px; line-height: 24px; color: #51545e } .styled-table { width: 600px; border-collapse: collapse; } .styled-table th { border-collapse: collapse; border: 1px solid white; } .styled-table td { border-collapse: collapse; border: 1px solid white; padding-left: 20px; } .styled-button { border: none; border-collapse: collapse; } .styled-button td { border: 1px solid; border-radius: 5px; border-color: transparent; background-color: #469cf0 !important; padding: 15px 30px; } .styled-button a { background-color: #469cf0 !important; display: inline-block; font-size: 17px; color: #ffffff; text-decoration: none; font-family: sans-serif; } </style> <!--[if mso]> <style> .styled-button td { border: 1px solid; border-radius: 5px; border-color: #469cf0; background-color: #469cf0 !important; mso-padding-alt: 15px 30px; } </style> <![endif]--> </head> <body style='margin: 0; width: 100%; background-color: #f2f4f6; padding: 0; -webkit-font-smoothing: antialiased; word-break: break-word'> <div role='article' aria-roledescription='email' aria-label lang='en'> <table class='email-wrapper' style='width: 100%; background-color: #1e293b; font-family: &quot;Inter&quot;, ui-sans-serif, system-ui, -apple-system, &quot;Segoe UI&quot;, sans-serif;' cellpadding='0' cellspacing='0' role='none' > <tr> <td align='center'> <table class='email-content' style='width: 100%' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' class='email-masthead' style='display: flex; justify-content: center; gap: 8px; padding-top: 25px; padding-bottom: 25px; text-align: center; font-size: 16px'> <img src='data:image/png;base64,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' alt='AttackForge Logo' style='width: 200px'> </td> </tr> <tr> <td class='email-body' style='width: 100%; background-color: #fff'> <table align='center' class='email-body_inner sm-w-full' style='margin-left: auto; margin-right: auto; width: 800px; background-color: #fff' cellpadding='0' cellspacing='0' role='none'> <tr> <td style='padding: 45px'>";
  const emailFooter = "</td> </tr> </table> </td> </tr> <tr> <td> <table align='center' class='email-footer sm-w-full' style='margin-left: auto; margin-right: auto; width: 570px; text-align: center' cellpadding='0' cellspacing='0' role='none'> <tr> <td align='center' style='padding: 45px; font-size: 16px'> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'> This is a system generated email and reply is not monitored. <br> For any queries please reach out to <span style='text-decoration-line: underline'>support@attackforge.com</span> </p> <p style='margin-bottom: 20px; margin-top: 6px; text-align: center; font-size: 12px; line-height: 24px; color: #fff'>&copy; 2018-"+ curr_year +" AttackForge&reg;</p> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> </div> </body> </html>";
  
  // Modify the email script based on requirements
  const content = '<p><strong>Attention: Flow has detected aborted scan ids.</strong></p>'
  + '<p>Please check on the log history of the flow run.</p>'
  + '<br>'
  + '<table class="styled-table" align="center">'
  + '<tr><th width="300">Scan Id</th><th width="300">Status</th></tr>'
  + aborted_scan_id_table
  + '</table>'
  + '<br>'
  + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.af_hostname+'/flows/'+secrets.current_flow_id+'/home">View Flow Run</a></td></tr></table>'  
  + '<br>';
  
  return {
    decision: {
      status: 'continue',
      message: 'Send error email',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/ss/email',
      body: {
        to: secrets.admin_user_id,
        subject: '[Flow Notification] Aborted Scan Ids Discovered from Tenable Web App Scanning',
        html: emailHeader + content + emailFooter
      },
    }
  };
} 
else {
  return {
    decision:{
      status: 'finish',
      message: 'No aborted scan_ids found. Exiting the flow.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 200){
  return{
    decision: {
      status: 'finish',
      message: 'Successfully reported aborted scan ids to the admin user. Exiting the flow.'
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error while reporting aborted scan ids to the admin user. Please check the log.'
    }
  };
}
```

## Tenable VM - Poll Scan Status \[Step 4 of 5]

<figure><img src="/files/wPDYRvvOM64dWZt3zMw8" alt=""><figcaption></figcaption></figure>

The purpose of this example is to export the results of a VM scan in Tenable.

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**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * import\_vuln\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5\] Flow](#tenable-vm-and-was-import-vulnerabilities-step-5-of-5)
  * tenable\_auth - your [Tenable API Key](https://docs.tenable.com/vulnerability-management/Content/Settings/my-account/GenerateAPIKey.htm)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Latest Scan Status**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id/latest-status>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (!data?.jsonBody?.project?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.project_id missing'
    }
  };
}
if (!data?.jsonBody?.project?.scan_id) {
  return {
    decision: {
      status: 'abort',
      message: 'data.jsonBody.project.scan_id missing'
    }
  };
}

const project_id = data.jsonBody.project.project_id;
const scan_id = data.jsonBody.project.scan_id;

return {
  decision: {
    status: 'continue',
    message: 'Getting latest VM Scan status for scan_id: ' + scan_id,
  },
  request: {
    url: 'https://cloud.tenable.com/scans/' + scan_id + '/latest-status',
  },
  data: {
    project_id: project_id,
    scan_id: scan_id
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Error'
    }
  };
}

const error_status = ['aborted', 'canceled', 'paused', 'pausing', 'stopped', 'stopping'];

if (response.jsonBody?.status !== 'completed') {
  if (Array.includes(error_status, response.jsonBody?.status)) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Error: Current project :' , data.project_id , ', Current scan_id :' , data.scan_id, ' has status: ', response.jsonBody?.status);
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error'
      }
    };
  } 
  else {
    if (secrets.logging_level === 'debug'){
      Logger.debug('Scan pending, exiting process.');
    }
    return {
      decision: {
        status: 'finish',
        message: 'Scan pending, exiting.'
      }
    };
  }
}

if (response.jsonBody?.status === 'completed') {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Completed scan for project :' , data.project_id, ', scan_id :' , data.scan_id, ', scan status :' , response.jsonBody?.status);
  }
  return {
    decision: {
      status: 'continue',
      message: 'Scan ' + data?.scan_id + ' is complete.',
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
}
```

**Action 2 - Export Scan**

* **Method**: POST
* **URL**: <https://cloud.tenable.com/was/v2/configs/{config\\_id}/scans/search>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.scan_id){
  return {
    decision: {
      status: 'continue',
      message: 'Requesting export of scan_id: ' + data.scan_id,
    },
    request: {
      url: 'https://cloud.tenable.com/scans/' + data.scan_id + '/export',
      body: {
        format: "csv"
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };  
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.scan_id is required for exporting scan request. Please check the log.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Error'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('File generated for scan_id: ', data.scan_id, 'file name: ', data.file);
}

// Give reasonable delay for Tenable File System to register file - guide: delay 1000+
return {
  decision: {
    status: 'continue',
    message: 'File generated',
    delay: 2000
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id,
    file: response.jsonBody?.file
  }
};
```

**Action 3 - Download Exported Scan**

* **Method**: GET
* **URL**: <https://cloud.tenable.com/scans/{scan\\_id}/export/{file\\_id}/download>
* **Headers**:
  * Key = X-ApiKeys; Type = Secret; Value = tenable\_auth
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = Accept; Type = Value; Value = application/octet-stream
* **Request Script**:

```javascript
if (data?.file){
  return {
    decision: {
      status: 'continue',
      message: 'Requesting download of the exported scan file: ' + data?.file,
    },
    request: {
      url: 'https://cloud.tenable.com/scans/' + data?.scan_id + '/export/' + data?.file + '/download'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file
    }
  };
} 
else {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.file must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode === 200){
  const base64 = String.encode(response.body, 'base64');
  return {
    decision: {
      status: 'continue',
      message: 'Successfully downloaded scan report ' + data?.file
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file,
      base64: base64
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while requesting download of exported scan on file: '+ data?.file + '. Please check the log.'
    }
  };
}
```

**Action 4 - CSV to JSON**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/ss/utils/parse-csv
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
  * Key = X-SSAPI-KEY; Type = Secret; Value = af\_key
* **Request Script**:

```javascript
if (data?.base64){
  return {
    decision: {
      status: 'continue',
      message: 'Convert CSV data to JSON',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/ss/utils/parse-csv',
      body: {
        base64: data.base64,
        options: {
          columns: true
        }
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      file: data?.file
    }
  };
} 
else {
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.base64 must be present.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 200){
  return {
    decision: {
      status: 'continue',
      message: 'Successfully converted the encoded csv to json type. Proceeding to next action.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id,
      records: response?.jsonBody?.records
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return{
    decision: {
      status: 'abort',
      message: 'Error while converting the base64 encoded csv to json type. Please check the log.'
    }
  };  
}
```

**Action 5 - Import Scan Results**

* **Method**: POST
* **URL**: https\://{{af\_hostname}}/api/flows/{{import\_vuln\_flow\_trigger\_id}}
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/json
* **Request Script**:

```javascript
if (data?.records && data?.project_id){
  return {
    decision: {
      status: 'continue',
      message: 'Triggering flow to import vulnerabilities.',
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.import_vuln_trigger_flow_id,
      body: {
        records: data.records,
        project_id: data.project_id
      }
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project_id: ', data?.project_id);
    Logger.debug('records: ', data?.records);
  }
  return {
    decision:{
      status: 'abort',
      message: 'Error: data.records and data.project_id must be present in order to import vulnerabilities.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode === 202) {
  return {
    decision: {
      status: 'continue',
      message: 'Successfully sent request to import scan results.'
    },
    data: {
      project_id: data?.project_id,
      scan_id: data?.scan_id
    }
  };
} 
else {
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error while sending request to import scan results. Please check the log.'
    }
  };
}
```

**Action 6 - Update Project to Remove Tenable Details**

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

```javascript
return {
  decision: {
    status: 'continue',
    message: 'Removing tenable_vm_active_scan from project: ' + data?.project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data?.project_id,
    body: {
      custom_fields: [
        {
          key: "tenable_vm_active_scan",
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data?.project_id,
    scan_id: data?.scan_id
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  return {
    decision: {
      status: 'abort',
      message: 'Failed to update project: ' + data?.project_id
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully removed tenable_vm_active_scan from project: ' + data?.project_id
  }
};
```

## Tenable VM & WAS - Import Vulnerabilities \[Step 5 of 5]

<figure><img src="/files/rpcMmq1ilvwtxBEhScAJ" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from a Tenable Web Application or VM scan.

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**:&#x20;
  * Method: POST
* **Secrets**:
  * af\_hostname - your AttackForge tenant hostname e.g. demo.attackforge.com
  * af\_key - your AttackForge user API key
  * logging\_level - set to "debug" for additional logging

**Action 1 - Validate, Detect, Transform & Group Findings**

* **Script**:

```javascript
if (!secrets?.af_hostname) {
  return {
    decision: { 
      status: 'abort', 
      message: 'secrets.af_hostname is required.' 
    }
  };
}
if (!data?.jsonBody?.project_id) {
  return {
    decision: { 
      status: 'abort', 
      message: 'data.jsonBody.project_id is required.' 
    }
  };
}

// Detecting format VM or WAS
const isVM  = data.jsonBody?.records && Array.isArray(data.jsonBody.records);
const isWAS = !isVM && data.jsonBody?.final_result && Array.isArray(data.jsonBody.final_result);

if (!isVM && !isWAS) {
  return {
    decision: {
      status: 'abort',
      message: 'Unable to detect source format. Expected data.jsonBody.records (VM) or data.jsonBody.final_result (WAS).'
    }
  };
}

const sourceType = isVM ? 'VM' : 'WAS';

// Normalise into flat work items
const items = [];

if (isVM) {
  for (let i = 0; i < Array.length(data.jsonBody.records); i++) {
    Array.push(items, { 
      type: 'VM', 
      record: data.jsonBody.records[i] 
    });
  }
} 
else {
  // WAS: each entry in final_result is a HTTP response wrapper from the export step
  // The WAS report JSON, once parsed, has scan + findings at the root of the object

  for (let r = 0; r < Array.length(data.jsonBody.final_result); r++) {
    const raw = data.jsonBody.final_result[r];
    let entry;
    entry = raw?.scan ? raw : (raw?.data || raw);
    const scanId = entry?.scan?.scan_id || '';
    const target = entry?.scan?.target || '';
    const scanStartedAt = entry?.scan?.started_at || '';
    
    if (entry?.findings && Array.isArray(entry.findings)) {
      for (let i = 0; i < Array.length(entry.findings); i++) {
        Array.push(items, { 
          type: 'WAS', 
          finding: entry.findings[i], 
          scanId: scanId, 
          target: target, 
          scanStartedAt: scanStartedAt 
        });
      }
    }
  }
}

if (Array.length(items) === 0) {
  return {
    decision: { 
      status: 'abort', 
      message: 'No ' + sourceType + ' items found in input data.' 
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug(sourceType + ' item count: ' + Array.length(items));
}

// Group by pluginId + '|' + priority
const groupKeys  = [];
const groupVulns = [];

for (let i = 0; i < Array.length(items); i++) {
  const item = items[i];
  let pluginId;
  let priority;
  let assetName;

  if (item.type === 'VM') {
    pluginId  = item.record['Plugin ID'] || '0';
    priority  = mapPriorityVM(item.record['Risk']);
    assetName = item.record['FQDN'] || item.record['Host'] || item.record['IP Address'] || 'Unknown';
  }
  else {
    pluginId  = item.finding.plugin_id != null ? item.finding.plugin_id + '' : '0';
    priority  = mapPriorityWAS(item.finding.risk_factor);
    assetName = item.target || item.finding.uri || 'Unknown';
  }

  const key = pluginId + '|' + priority;

  // Find existing group
  let idx = -1;
  for (let g = 0; g < Array.length(groupKeys); g++) {
    if (groupKeys[g] === key) { 
      idx = g; 
      break; 
    }
  }

  if (idx === -1) {
    // New group — build base vuln from this item
    const vuln = item.type === 'VM'
      ? buildVMVuln(item.record, pluginId, priority, assetName)
      : buildWASVuln(item.finding, item.scanId, item.target, item.scanStartedAt, pluginId, priority, assetName);
    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);
    // User can modify where note can be placed. Currently it gets pushed into Steps to Reproduce
    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }
    Array.push(groupKeys, key);
    Array.push(groupVulns, vuln);
  } 
  else {
    // Existing group — add asset + evidence
    const vuln = groupVulns[idx];

    let assetExists = false;
    for (let a = 0; a < Array.length(vuln.affected_assets); a++) {
      if (vuln.affected_assets[a].assetName === assetName) { 
        assetExists = true; 
        break; 
      }
    }
    if (!assetExists) {
      Array.push(vuln.affected_assets, { 
        assetName: assetName 
      });
    }

    const note = item.type === 'VM'
      ? buildVMNote(item.record, assetName)
      : buildWASNote(item.finding);

    // User can modify where note can be placed. Currently it gets pushed into Steps to Reproduce
    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }
  }
}

if (Array.length(groupVulns) === 0) {
  return {
    decision: { 
      status: 'finish', 
      message: 'No vulnerabilities to import after grouping.' 
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.');
}

return {
  decision: {
    status: 'continue',
    message: 'Grouped ' + Array.length(items) + ' ' + sourceType + ' items into ' + Array.length(groupVulns) + ' vulnerabilities.'
  },
  data: {
    vulns_to_import: groupVulns,
    total_count: Array.length(groupVulns),
    imported_count: 0,
    source_type: sourceType
  }
};

// Functions below
// VM vuln builder
function buildVMVuln(record, pluginId, priority, assetName) {
  const title = record['Name'] || 'Untitled VM Finding';

  const tags = [];
  if (record['CVE'] && String.length(record['CVE']) > 0) {
    const cves = String.split(record['CVE'], ',');
    for (let j = 0; j < Array.length(cves); j++) {
      const cve = String.trim(cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }

  const custom_fields = [
    { key: 'tenable_plugin_id', value: pluginId },
    { key: 'tenable_scan_type', value: 'VM' },
    { key: 'vuln_state',        value: record['Vulnerability State'] || '' },
    { key: 'first_found',       value: record['First Found']         || '' },
    { key: 'last_found',        value: record['Last Found']          || '' },
    { key: 'plugin_family',     value: record['Plugin Family']       || '' },
    { key: 'exploit_available', value: record['Exploit Available']   || 'false' },
    { key: 'patch_available',   value: record['Patch Available']     || 'false' }
  ];

  if (record['CVSS3 Vector'] && String.length(record['CVSS3 Vector']) > 0) {
    Array.push(custom_fields, { key: 'cvss3_vector', value: record['CVSS3 Vector'] });
  }
  if (record['CVSS3 Base Score'] && record['CVSS3 Base Score'] !== '0.0') {
    Array.push(custom_fields, { key: 'cvss3_base_score', value: record['CVSS3 Base Score'] });
  }
  if (record['CVSS Base Score'] && record['CVSS Base Score'] !== '0.0') {
    Array.push(custom_fields, { key: 'cvss2_base_score', value: record['CVSS Base Score'] });
  }
  const vpr = record['Vulnerability Priority Rating (VPR)'];
  if (vpr && vpr !== 'null' && String.length(vpr) > 0) {
    Array.push(custom_fields, { key: 'vpr_score', value: vpr });
  }
  if (record['See Also'] && String.length(record['See Also']) > 0) {
    Array.push(custom_fields, { key: 'reference', value: record['See Also'] });
  }
  const frameworks = [];
  if (record['Metasploit'] === 'true') {
    Array.push(frameworks, 'Metasploit');
  }
  if (record['CANVAS'] === 'true') {
    Array.push(frameworks, 'CANVAS');
  }
  if (record['Core Exploits'] === 'true') {
    Array.push(frameworks, 'Core Exploits');
  }
  if (record['D2 Elliot'] === 'true') {
    Array.push(frameworks, 'D2 Elliot');
  }
  if (record['ExploitHub'] === 'true') {
    Array.push(frameworks, 'ExploitHub');
  }
  if (Array.length(frameworks) > 0) {
    Array.push(custom_fields, { 
      key: 'exploit_frameworks', 
      value: Array.join(frameworks, ', ') 
    });
  }

  let likelihood = 1;
  if (vpr && vpr !== 'null') {
    const vprScore = Number.parseFloat(vpr);
    if (!Number.isNaN(vprScore)) {
      likelihood = Math.round(vprScore);
      if (likelihood < 1) {
        likelihood = 1;
      }
      if (likelihood > 10) {
        likelihood = 10;
      }
    }
  }

  let solution = record['Solution'] || '';
  if (solution === 'N/A' || solution === 'n/a') {
    solution = '';
  }

  const is_zeroday = record['Exploit Available'] === 'true' && record['Patch Available'] !== 'true';

  const vuln = {
    projectId:                  data.jsonBody?.project_id,
    title:                      title,
    priority:                   priority,
    description:                record['Description'] || record['Synopsis'] || '',
    attack_scenario:            '',
    steps_to_reproduce:         buildVMSteps(record),
    remediation_recommendation: solution,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    is_zeroday:                 is_zeroday,
    import_source:              'Tenable VM',
    import_source_id:           'VM-' + pluginId,
    created:                    record['First Found'] || record['Last Found'] || '',
    is_visible:                 true
  };
  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

// Per-item VM evidence note
// prefixed with asset + port
function buildVMNote(record, assetName) {
  if (record['Plugin Output'] && String.length(record['Plugin Output']) > 0) {
    let prefix = '[' + assetName;
    if (record['Port'] && record['Port'] !== '0') {
      prefix = prefix + ':' + record['Port'];
      if (record['Protocol']) {
        prefix = prefix + '/' + record['Protocol'];
      }
    }
    prefix = prefix + ']';
    return { 
      note: prefix + '\n' + record['Plugin Output'], 
      type: 'PLAINTEXT' 
    };
  }
  return null;
}

// WAS vuln builder
function buildWASVuln(finding, scanId, target, scanStartedAt, pluginId, priority, assetName) {
  const title = finding.name || 'Untitled WAS Finding';
  let remediation = finding.solution || '';
  if (remediation === 'N/A' || remediation === 'n/a') {
    remediation = '';
  }

  const tags = [];
  if (finding.cves && Array.isArray(finding.cves)) {
    for (let j = 0; j < Array.length(finding.cves); j++) {
      const cve = String.trim(finding.cves[j]);
      if (String.length(cve) > 0) {
        Array.push(tags, cve);
      }
    }
  }
  if (finding.cwe && Array.isArray(finding.cwe)) {
    for (let j = 0; j < Array.length(finding.cwe); j++) {
      Array.push(tags, 'CWE-' + finding.cwe[j]);
    }
  }
  if (finding.owasp && Array.isArray(finding.owasp)) {
    for (let j = 0; j < Array.length(finding.owasp); j++) {
      const o = finding.owasp[j];
      if (o && o.year && o.category) {
        Array.push(tags, 'OWASP-' + o.year + '-' + o.category);
      }
    }
  }

  const custom_fields = [
    { key: 'tenable_plugin_id',       value: pluginId },
    { key: 'tenable_scan_type',       value: 'WAS' },
    { key: 'scan_id',                 value: scanId },
    { key: 'target_url',              value: target },
    { key: 'plugin_family',           value: finding.family || '' },
    { key: 'plugin_publication_date', value: finding.plugin_publication_date || '' }
  ];

  if (finding.cvssv4 !== null && finding.cvssv4 !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvssv4_score',  
      value: finding.cvssv4 + '' 
    });
  }
  if (finding.cvssv4_vector && String.length(finding.cvssv4_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvssv4_vector', 
      value: finding.cvssv4_vector 
    });
  }
  if (finding.cvssv3 !== null && finding.cvssv3 !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvssv3_score',  
      value: finding.cvssv3 + '' 
    });
  }
  if (finding.cvssv3_vector && String.length(finding.cvssv3_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvssv3_vector', 
      value: finding.cvssv3_vector 
    });
  }
  if (finding.cvss !== null && finding.cvss !== undefined) {
    Array.push(custom_fields, { 
      key: 'cvss2_score',
      value: finding.cvss + '' 
    });
  }
  if (finding.cvss_vector && String.length(finding.cvss_vector) > 0) {
    Array.push(custom_fields, { 
      key: 'cvss2_vector',
      value: finding.cvss_vector 
    });
  }
  if (finding.see_also && Array.isArray(finding.see_also) && Array.length(finding.see_also) > 0) {
    Array.push(custom_fields, { 
      key: 'reference', 
      value: Array.join(finding.see_also, '\n') 
    });
  }
  if (finding.wasc && Array.isArray(finding.wasc) && Array.length(finding.wasc) > 0) {
    Array.push(custom_fields, { 
      key: 'wasc', 
      value: Array.join(finding.wasc, ', ') 
    });
  }
  if (finding.input_name && String.length(finding.input_name) > 0) {
    Array.push(custom_fields, { 
      key: 'input_name', 
      value: finding.input_name 
    });
  }
  if (finding.input_type && String.length(finding.input_type) > 0) {
    Array.push(custom_fields, { 
      key: 'input_type', 
      value: finding.input_type 
    });
  }

  let likelihood = 1;
  const cvssScore = finding.cvssv4 !== null && finding.cvssv4 !== undefined ? finding.cvssv4
                  : finding.cvssv3 !== null && finding.cvssv3 !== undefined ? finding.cvssv3
                  : finding.cvss   !== null && finding.cvss   !== undefined ? finding.cvss
                  : null;
  if (cvssScore !== null) {
    likelihood = Math.round(cvssScore);
    if (likelihood < 1) {
      likelihood = 1;
    }
    if (likelihood > 10) {
      likelihood = 10;
    }
  }

  const vuln = {
    projectId:                  data.jsonBody?.project_id,
    title:                      title,
    priority:                   priority,
    description:                finding.description || finding.synopsis || '',
    attack_scenario:            '',
    steps_to_reproduce:         buildWASSteps(finding),
    remediation_recommendation: remediation,
    likelihood_of_exploitation: likelihood,
    affected_assets:            [{ assetName: assetName }],
    tags:                       tags,
    custom_fields:              custom_fields,
    is_zeroday:                 false,
    import_source:              'Tenable WAS',
    import_source_id:           'WAS-' + pluginId,
    created:                    scanStartedAt,
    is_visible:                 true
  };

  if (secrets?.import_to_library) {
    vuln.import_to_library = secrets.import_to_library;
  }
  return vuln;
}

// WAS evidence note
function buildWASNote(finding) {
  const parts = [];
  const uri = finding.uri || '';
  if (String.length(uri) > 0) {
    Array.push(parts, '[' + uri + ']');
  }
  if (finding.output && String.length(finding.output) > 0) {
    Array.push(parts, finding.output);
  }
  if (finding.proof && String.length(finding.proof) > 0) {
    Array.push(parts, 'Proof: ' + finding.proof);
  }
  if (finding.request_headers && String.length(finding.request_headers) > 0) {
    Array.push(parts, 'Request Headers:\n' + finding.request_headers);
  }
  if (finding.response_headers && String.length(finding.response_headers) > 0) {
    Array.push(parts, 'Response Headers:\n' + finding.response_headers);
  }
  if (finding.payload && String.length(finding.payload) > 0) {
    Array.push(parts, 'Payload: ' + finding.payload);
  }
  if (Array.length(parts) > 0) {
    return { 
      note: Array.join(parts, '\n'), 
      type: 'PLAINTEXT' 
    };
  }
  return null;
}

// Steps-to-reproduce
function buildVMSteps(record) {
  const parts = [];
  if (record['OS'] && String.length(record['OS']) > 0) {
    Array.push(parts, 'OS: ' + record['OS']);
  }
  if (record['Port'] && record['Port'] !== '0') {
    let portLine = 'Port: ' + record['Port'];
    if (record['Protocol'] && String.length(record['Protocol']) > 0) {
      portLine = portLine + '/' + record['Protocol'];
    }
    Array.push(parts, portLine);
  }
  return Array.join(parts, '\n');
}

function buildWASSteps(finding) {
  const parts = [];
  if (finding.uri && String.length(finding.uri) > 0) {
    Array.push(parts, 'URL: ' + finding.uri);
  }
  if (finding.input_name && String.length(finding.input_name) > 0) {
    let inputLine = 'Input: ' + finding.input_name;
    if (finding.input_type && String.length(finding.input_type) > 0) {
      inputLine = inputLine + ' (' + finding.input_type + ')';
    }
    Array.push(parts, inputLine);
  }
  return Array.join(parts, '\n');
}

// ── Priority helpers ──
function mapPriorityVM(risk) {
  if (risk === 'Critical') {
    return 'Critical';
  }
  else if (risk === 'High') {
    return 'High';
  }
  else if (risk === 'Medium') {
    return 'Medium';
  }
  else if (risk === 'Low') {
    return 'Low';
  }
  return 'Info';
}

function mapPriorityWAS(risk_factor) {
  if (risk_factor === 'critical') {
    return 'Critical';
  }
  else if (risk_factor === 'high') {
    return 'High';
  }
  else if (risk_factor === 'medium') {
    return 'Medium';
  }
  else if (risk_factor === 'low') {
    return 'Low';
  }
  return 'Info';
}
```

**Action 2 - Import Vulnerabilities**

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

```javascript
if (!data?.vulns_to_import || data.vulns_to_import && Array.length(data.vulns_to_import) === 0) {
  return {
    decision: { 
      status: 'next', 
      message: 'No vulnerabilities to import. Skipping.' 
    },
    data: data
  };
}

const vuln = data.vulns_to_import[0];
const remaining = Array.length(data.vulns_to_import) - 1;

if (secrets.logging_level === 'debug') {
  Logger.debug('Importing grouped vulnerability: ' + (vuln.title || 'Untitled') + ' (' + Array.length(vuln.affected_assets) + ' asset(s)). ' + remaining + ' remaining.');
}

return {
  decision: {
    status: 'continue',
    message: 'Importing vulnerability (' + (data.imported_count + 1) + '/' + data.total_count + '): ' + (vuln.title || 'Untitled')
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/vulnerability',
    body: vuln
  },
  data: data
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Import error response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error importing vulnerability. Status code: ' + (response?.statusCode || 'unknown') + '. Imported so far: ' + (data?.imported_count || 0) + '/' + (data?.total_count || 0)
    }
  };
}

if (data?.vulns_to_import) {
  Array.splice(data.vulns_to_import, 0, 1);
}
if (data?.imported_count) {
  data.imported_count = (data.imported_count || 0) + 1;
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Vulnerability imported successfully. Progress: ' + data?.imported_count + '/' + data?.total_count);
}

if (data?.vulns_to_import && Array.length(data.vulns_to_import) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Imported (' + data?.imported_count + '/' + data?.total_count + '). Processing next vulnerability.'
    },
    data: data
  };
}

return {
  decision: {
    status: 'finish',
    message: 'All grouped vulnerabilities imported successfully. Total: ' + data?.imported_count
  },
  data: data
};
```


---

# 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/tenable.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.
