> For the complete documentation index, see [llms.txt](https://support.attackforge.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://support.attackforge.com/attackforge-enterprise/modules/flows/qualys.md).

# Qualys

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

## Qualys WAS - Initiate Scan \[Step 1 of 5]

<figure><img src="/files/0udbibnIN8gVNdHFQrLC" 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 Qualys 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
  * fallback\_protocol - when an asset name doesn't have a protocol, set a default protocol e.g. https\://
  * qualys\_vm\_launch\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys WAS - Create Web Object and Launch Scan \[Step 2 of 5\]](#qualys-was-create-web-object-and-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - 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: 'Entity: project, skipping to next action.'
    },
    data: {
      projects: [
        {
          project_id: data.project.project_id,
          project_name: data.project.project_name,
          assets: data.project.project_scope
        }
      ]
    }
  };
}
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;

    if (!project_id){
      if (secrets.logging_level === 'debug'){
        Logger.debug('Current project: ', projects[data.counter]);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: project_id not found in current project. Please check the log.'
        }
      };
    }

    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?.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
    }
  };
}

// Initialise the array
data.projects[data.counter].assets = [];

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

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 2 - Create Web App and Launch WAS Scan**

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

```javascript
let project_id;
let project_name;
let project_scope_assets;

// Project
if (data?.project) {
  if (data.project.project_id) {
    project_id = data.project.project_id;
  }
  if (data.project.project_scope) {
    project_scope_assets = data.project.project_scope;
  }
  if (data.project.project_name) {
    project_name = data.project.project_name;
  }
}

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

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

// Normalizes assets to canonical WAS URLs. Assets without a protocol default to https://.
const valid_project_scope_assets = [];

if (Array.isArray(project_scope_assets)) {
  for (let x = 0; x < Array.length(project_scope_assets); x++) {
    const url = parseAssetToURL(project_scope_assets[x]);
    if (url) {
      if (secrets.logging_level === 'debug') {
        Logger.debug('valid_asset: ', url);
      }
      Array.push(valid_project_scope_assets, url);
    } 
    else {
      if (secrets.logging_level === 'debug') {
        Logger.debug('invalid_asset: ', project_scope_assets[x]);
      }
    }
  }
}

if (Array.length(valid_project_scope_assets) === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'No valid IP or hostname assets found in project scope after filtering.'
    }
  };
}

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

return {
  decision: {
    status: 'continue',
    message: 'Sending project and assets to Qualys WAS Scan Flow: ' + secrets.qualys_vm_launch_scan_flow_trigger_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_launch_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      project_name: project_name,
      assets: valid_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);
      if (!(num >= 0 && num <= 255)) return false;
    }
  }
  return true;
}

// READ:
// Accepts hostname, IPv4, host:port, host/path, or full http(s):// URL.
// Assets without an explicit protocol default to https://. Please update this as needed in "secret.fallback_protocol"
// Returns null if the host portion is not a valid hostname or IPv4.
function parseAssetToURL(raw) {
  const str = String.trim(raw);
  if (str.length === 0) return null;

  let protocol = secrets.fallback_protocol || 'https';
  let rest = str;

  if (String.includes(str, '://')) {
    const parts = String.split(str, '://');
    const proto = parts[0];
    if (proto !== 'http' && proto !== 'https') return null;
    protocol = proto;
    rest = parts[1];
  }

  const slashParts = String.split(rest, '/');
  let hostPort = slashParts[0];
  let path = '';
  for (let i = 1; i < Array.length(slashParts); i++) {
    path = path + '/' + slashParts[i];
  }

  let host = hostPort;
  let portStr = '';
  const portMatch = String.match(hostPort, m/^(.+):(\d+)$/);
  if (portMatch) {
    host = portMatch[1];
    portStr = portMatch[2];
    const portNum = Number.parseInt(portStr);
    if (!(portNum >= 1 && portNum <= 65535)) return null;
  }

  if (!isValidHostname(host) && !isValidIPv4(host)) return null;

  return protocol + '://' + host + (portStr ? ':' + portStr : '') + path;
}
```

* **Response Script**:

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

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

<figure><img src="/files/8kBWloQFsu9AE5peZ3ER" alt=""><figcaption></figcaption></figure>

The purpose of this example is to initiate scheduling a VM scan of the assets on the project scope in Qualys 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
  * qualys\_vm\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Launch Scan \[Step 2 of 5\]](#qualys-vm-launch-scan-step-2-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - 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: 'Entity: project, skipping to next action.'
    },
    data: {
      projects: [
        {
          project_id: data.project.project_id,
          project_scope: data.project.project_scope,
          project_name: data.project.project_name
        }
      ]
    }
  };
}
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;

    if (!project_id) {
      if (secrets.logging_level === 'debug'){
        Logger.debug('Current project: ', projects[data.counter]);
      }
      return {
        decision: {
          status: 'abort',
          message: 'Error: project_id not found in current project. Please check the log.'
        }
      };
    }

    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?.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].project_scope = [];

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

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 2 - Trigger Qualys VM Scan Flow**

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

```javascript
const projects = data?.projects;

if (!Array.isArray(projects) || Array.length(projects) === 0) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('projects: ', JSON.stringify(projects));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.projects is missing or empty. Please check the log.'
    }
  };
}

const project = projects?[0];
const project_id = project?.project_id;
const project_name = project?.project_name;

if (!project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: the next project is missing project_id.'
    }
  };
}

const valid_project_scope_assets = buildValidAssets(project?.project_scope);

if (secrets.logging_level === 'debug') {
  Logger.debug('Triggering Qualys VM scan for project ' + project_id, JSON.stringify(valid_project_scope_assets));
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering Qualys VM scan for project: ' + project_id
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_scan_flow_trigger_id,
    body: {
      project_id: project_id,
      project_name: project_name,
      assets: valid_project_scope_assets
    }
  },
  data: {
    projects: projects
  }
};

// Validates a DNS hostname: 1-253 chars, dot-separated labels of 1-63 chars,
// alphanumeric/hyphen only, no leading/trailing hyphen on a label.
function isValidHostname(str) {
  if (String.length(str) === 0 || String.length(str) > 253) { return false; }
  const labels = String.split(str, '.');
  if (labels && Array.isArray(labels) && Array.length(labels) > 0) {
    for (let x = 0; x < Array.length(labels); x++) {
      const label = labels[x];
      if (String.length(label) === 0 || String.length(label) > 63) { return false; }
      if (label[0] === '-' || label[String.length(label) - 1] === '-') { return false; }
      if (!(label =~ m/^[A-Za-z0-9-]+$/)) { return false; }
    }
  }
  return true;
}

// Validates a dotted-quad IPv4 address with each octet in 0-255 and no leading zeros.
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 < Array.length(labels); x++) {
      const label = labels[x];
      if (String.length(label) > 1 && label[0] === '0') { return false; }
      const num = Number.parseInt(label);
      if (!(num >= 0 && num <= 255)) { return false; }
    }
  }
  return true;
}

// Extracts the bare host from a raw scope asset for a VM/VMDR scan.
// Qualys VM targets are host assets addressed by IP or FQDN — NOT URLs — so any
// protocol prefix, path or port is stripped and only the host is kept.
// Accepts hostname, IPv4, host:port, host/path, or a full http(s):// URL.
// Returns null if the host portion is not a valid hostname or IPv4.
function parseAssetToHost(raw) {
  const str = String.trim(raw);
  if (String.length(str) === 0) { return null; }

  let rest = str;

  // Drop any protocol prefix — VM targets carry no http(s):// scheme.
  if (String.includes(rest, '://')) {
    const parts = String.split(rest, '://');
    rest = parts[1];
  }

  // Drop any path component — keep only the host[:port] portion.
  const slashParts = String.split(rest, '/');
  let hostPort = slashParts[0];

  // Drop any port — host assets are addressed by IP/FQDN only.
  let host = hostPort;
  const portMatch = String.match(hostPort, m/^(.+):(\d+)$/);
  if (portMatch && portMatch[1] && portMatch[2]) {
    host = portMatch[1];
  }

  if (!isValidHostname(host) && !isValidIPv4(host)) { return null; }

  return host;
}

// Normalises a project_scope array into the list of valid VM host assets
// (bare IP / FQDN, no protocol). Order is preserved; duplicates are kept.
function buildValidAssets(scope) {
  const assets = [];
  if (!Array.isArray(scope)) {
    return assets;
  }
  for (let i = 0; i < Array.length(scope); i++) {
    const host = parseAssetToHost(scope[i]);
    if (host !== null && host !== undefined) {
      Array.push(assets, host);
    }
  }
  return assets;
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error submitting project to qualys_vm_scan_flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error submitting project to Qualys VM 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
      }
    };
  }
}

return {
  decision: {
    status: 'finish',
    message: 'Completed sending project to Qualys VM Scan Flow.',
  }
};
```

## Qualys WAS - Create Web Object and Launch Scan \[Step 2 of 5]

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

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

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
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_profile\_id - the Qualys scan option profile ID to use.
  * logging\_level - set to "debug" for additional logging

**Action 1 - Search Existing Web Apps**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/webapp
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

```javascript
if (!data?.jsonBody && !data.assets){
  const err_msg = "Error while receiving request. No data.jsonBody or data.assets exists.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const project_id = data.jsonBody?.project_id ?? data.project_id;
const project_name = data.jsonBody?.project_name ?? data.project_name;
const assets = data?.jsonBody?.assets ?? data.assets;

if (!project_id || !project_name || !assets) {
  const err_msg = "Error while receiving request. project_id, project_name, assets must all exist in jsonBody.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

// Search every asset and compare the list of returned assets [repeat]
if (Array.isArray(assets) && assets[0]) {
  const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');
  const xml_body = "<ServiceRequest>"
  + "<filters>"
  + "<Criteria field='url' operator='EQUALS'>" + assets[0] + "</Criteria>"
  + "</filters>"
  + "</ServiceRequest>";

  return {
    decision: {
      status: 'continue',
      message: 'Searching web app: ' + assets[0] + ' from the Qualys environment.',
    },
    request: {
      url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/webapp',
      body: xml_body,
      headers: {
        Authorization: qualys_auth_base64
      }
    },
    data: {
      project_id: project_id,
      project_name: project_name,
      assets: assets,
      webapp_ids: data?.webapp_ids ?? [],
      assets_to_be_created: data?.assets_to_be_created ?? []
    }
  };
} 
else {
  const err_msg = "Error when attempting to create web app request. assets[0] does not exist.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  const err_msg = "HTTP error returned from Qualys while searching Web App. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS'){
  const err_msg = "Error on creating web app on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

const count = (Array.find(xml_response, getCount)).count[0]['#text'];
if (count === undefined){
  const err_msg = "Error: failed to retrieve count from the response. Count: " + count;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Search Existing Web App- Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const webapp_ids = data?.webapp_ids ?? [];
const assets_to_be_created = data?.assets_to_be_created ?? [];
if (Number.parseInt(count) === 0) {
  Array.push(assets_to_be_created, data.assets[0]);
}
if (Number.parseInt(count) > 0) {
  const webapp_id = Array.find(xml_response, findWebAppId)?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
  Array.push(webapp_ids, webapp_id);
}
Array.shift(data.assets);
if (Array.length(data.assets) > 0){
  return {
    decision: {
      status: 'repeat',
      message: 'Retreiving next assset id',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets: data.assets,
      assets_to_be_created: assets_to_be_created,
      webapp_ids: webapp_ids
    }
  };
}
return {
  decision: {
    status: 'continue',
    message: 'Successfully collected webapp_ids of Web Apps. Continuing to next action.',
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    assets_to_be_created: assets_to_be_created,
    webapp_ids: webapp_ids
  }
};

function findStatus (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function getCount(obj){
  if (Object.keys(obj)[0] === 'count'){
    return obj;
  }
}
function findWebAppId (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
  }
}
```

**Action 2 - Create Web Object**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/create/was/webapp
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

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

if (data.assets_to_be_created && Array.length(data.assets_to_be_created) === 0) {
  if (data?.webapp_ids && Array.length(data?.webapp_ids) === 0) {
    return {
      decision: {
        status: 'finish',
        message: 'No assets were processed and no assets webapp_ids array: ' + data?.webapp_ids + '. Exiting the flow.'
      }
    };
  }
  return {
    decision:{
      status: 'next',
      message: 'No web app to be created on Qualys. Continuing to Launching Scan.'
    }
  };
}

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');
let xml_webapp = "";
let assets_to_be_created;

if (Array.isArray(data.assets_to_be_created)){
  assets_to_be_created = data.assets_to_be_created;
}
if (assets_to_be_created[0]){
  xml_webapp = xml_webapp + "<name><![CDATA[" + assets_to_be_created[0] + "]]></name>" + "<url><![CDATA[" + assets_to_be_created[0] + "]]></url>";
} 
else {
  const err_msg = "Error processing data object. assets_to_be_created[0] is: " + assets_to_be_created?[0];
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_body = "<ServiceRequest>"
+ "    <data>"
+ "        <WebApp>"
+ xml_webapp
+ "        </WebApp>"
+ "    </data>"
+ "</ServiceRequest>";

return {
  decision: {
    status: 'continue',
    message: 'Requesting Create Web App on Qualys WAS.',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/create/was/webapp',
    body: xml_body,
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    assets_to_be_created: assets_to_be_created,
    webapp_ids: data?.webapp_ids
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = "Error while creating web app on Qualys." + " Response :" + response.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS'){
  const err_msg = "Error on creating web app on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      flow_status: {
        step: 'Create Web Object- Request Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

const webapp_id = Array.find(xml_response, findWebAppId)?.data?[0]?.WebApp?[0]?.id?[0]?['#text'];
if (webapp_id){
  Array.push(data.webapp_ids, webapp_id);
}

Array.shift(data.assets_to_be_created);
if (Array.length(data.assets_to_be_created) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Repeating',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets_to_be_created: data.assets_to_be_created,
      webapp_ids: data.webapp_ids || []
    }
  };
} 
else {
  if (Array.length(data.webapp_ids) === 0) {
    const err_msg = "Error: No assets were created for this project. webapp_ids: " + JSON.stringify(data.webapp_ids);
    return {
      decision: {
        status: 'next',
        message: err_msg,
      },
      data: {
        project_id: data?.project_id,
        project_name: data?.project_name,
        flow_status: {
          step: 'Create Web Object [repeat]- Response Script',
          status: 'VALIDATION_ERROR',
          message: err_msg
        }
      }
    };
  }
  return {
    decision: {
      status: 'continue',
      message: 'No more asset to create, continuing to next action',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      webapp_ids: data?.webapp_ids
    }
  };
}

function findStatus (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function findWebAppId (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj;
  }
}
```

**Action 3 - Launch Scan**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/launch/was/wasscan
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/html
* **Request Script**:

```javascript
if (!data?.webapp_ids){
  const err_msg = "Data is missing webapp_ids. webapp_ids:" + data?.webapp_ids;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const date = Date.format(Date.datetime('now'), 'default');
const scan_name =  "Discover Scan on: " + data.project_name + " " + date;

function scanXmlBodyGenerator (arr){
  let multiscan = false;
  if (!Array.isArray(arr) || Array.length(arr) === 0) return;
  const xml_upper = "<ServiceRequest><data><WasScan><name>" + scan_name + "</name><type>DISCOVERY</type>";
  const xml_lower = "<profile><id>" + secrets.qualys_profile_id + "</id></profile></WasScan></data></ServiceRequest>";

  let xml_middle = "";

  if (Array.length(arr) === 1) {
    // Single Target
    xml_middle = "<target>"
      + "<webApp><id>" + arr[0] + "</id></webApp>"
      + "<webAppAuthRecord><isDefault>true</isDefault></webAppAuthRecord>"
      + "<scannerAppliance><type>EXTERNAL</type></scannerAppliance>"
      + "</target>";
  }

  if (Array.length(arr) > 1) {
    // Multi Target - build <set> by looping
    multiscan = true;
    let set_xml = "";
    for (let i = 0; i < Array.length(arr); i++) {
      set_xml = set_xml + "<WebApp><id>" + arr[i] + "</id></WebApp>";
    }
    xml_middle = "<target>"
      + "<scannerAppliance><type>EXTERNAL</type></scannerAppliance>"
      + "<webApps><set>" + set_xml + "</set></webApps>"
      + "<profileOption>DEFAULT</profileOption>"
      + "</target>";
  }

  const xml_body = xml_upper + xml_middle + xml_lower;
  return {xml_body: xml_body, multiscan: multiscan};
}

const scanResult = scanXmlBodyGenerator(data.webapp_ids);
const xml_body = scanResult ? scanResult.xml_body : undefined;

if (!xml_body) {
  const err_msg = "xml_body generation has been unsuccessful. xml_body:" + xml_body;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Proceeding to launch scan ' + scan_name,
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/launch/was/wasscan',
    body: xml_body,
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_name: data.project_name,
    project_id: data.project_id,
    multiscan: scanResult.multiscan,
    scan_time: date
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  const err_msg = "HTTP error returned from Qualys while Launching Web App Scan. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let status;
if (xml_response) {
  status = (Array.find(xml_response, findStatus))?.responseCode?[0]?['#text'];
}
if (status !== 'SUCCESS') {
  const err_msg = "Error on Launching web app scan on Qualys." + " status :" + status;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Launch Scan- Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const scanIdObj = Array.find(xml_response, findScanId);
const scan_id = scanIdObj ? scanIdObj.data[0].WasScan[0].id[0]['#text'] : undefined;
if (secrets.logging_level === 'debug') {
  Logger.debug('scan_id: ', JSON.stringify(scan_id));
}

return {
  decision: {
    status: 'continue',
    message: 'Qualys WAS Scan launched',
  },
  data: {
    project_name: data.project_name,
    project_id: data.project_id,
    scan_id: scan_id,
    multiscan: data.multiscan,
    scan_time: data.scan_time
  },
};

function findStatus(obj) {
  if (Object.keys(obj)[0] === 'responseCode') {
    return obj;
  }
}
function findScanId(obj) {
  if (Object.keys(obj)[0] === 'data') {
    return obj;
  }
}
```

**Action 4 - Update Project with Scan Id**

* **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 || !data.scan_id || data.multiscan === undefined) {
  const err_msg = "multiscan, project_id and scan_id must be present in data.";
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Update Project with Scan Id- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Update project with new was scan_id',
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: 'qualys_was_active_scan',
          value: String.from(data.scan_id)
        },
        {
          key: 'qualys_was_multiscan',
          value: String.from(data.multiscan)
        },
        {
          key: 'qualys_was_scan_time',
          value: String.from(data.scan_time)
        }
      ]
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug') {
    Logger.debug("Error while updating project with new Qualys WAS scan id");
  }
  const err_msg = "Error while updating project with new Qualys WAS scan id. HTTP Status: " + response?.statusCode;
  return {
    decision: {
      status: 'next',
      message: err_msg,
    },
    data: {
      flow_status: {
        step: 'Update Project with Scan Id- Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
} 
else {
  return {
    decision:{
      status: 'finish',
      message: 'Successfully updated project with new qualys WAS scan id.'
    }
  };
}
```

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

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

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

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
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_option\_id - the Qualys scan option profile ID to use.
  * scan\_name\_prefix - optional prefix for the scan title (defaults to "AttackForge Qualys VM Scan").
  * logging\_level - set to "debug" for additional logging

**Action 1 - Launch Qualys VM Scan**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/scan/
* **Headers**:
  * Key = Content-Type; Type = Value; Value = application/x-www-form-urlencoded
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
const curr_time = Date.datetime('now');
const safe_time = String.split(String.replaceAll(String.replaceAll(curr_time, ':', '-'), ' ', '+'), '.')[0];
const scan_name_prefix = secrets.scan_name_prefix || 'AttackForge Qualys VM Scan';

const project_id = data.jsonBody?.project_id;
const project_name = data?.jsonBody?.project_name;
const project_scope_assets = data.jsonBody?.assets;

if (!project_id) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_id: ', project_id);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id is missing. Please check the log.'
    }
  };
}
if (!project_name) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_name: ', project_name);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_name is missing. Please check the log.'
    }
  };
}
if (!project_scope_assets || !Array.isArray(project_scope_assets)) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('project_scope_assets: ', JSON.stringify(project_scope_assets));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_scope_assets must be a non-empty array. Please check the log.'
    }
  };
}
if (!secrets.qualys_auth || secrets.qualys_auth === undefined) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('qualys_auth: ', secrets.qualys_auth);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: qualys_auth must be registered in secrets. Please check the log.'
    }
  };
}

// Qualys uses ip= for IPs/ranges/CIDR and fqdn= for hostnames
const ip_targets = [];
const fqdn_targets = [];

for (let x = 0; x < Array.length(project_scope_assets); x++) {
  const asset = String.trim(project_scope_assets[x]);
  if (!asset) continue;
  if (isValidCIDR(asset) || isValidIPv4(asset)) {
    Array.push(ip_targets, asset);
  } 
  else if (isValidHostname(asset)) {
    Array.push(fqdn_targets, asset);
  } 
  else {
    if (secrets.logging_level === 'debug') {
      Logger.debug('invalid_asset: ', asset);
    }
  }
}

if (Array.length(ip_targets) === 0 && Array.length(fqdn_targets) === 0) {
  return {
    decision: {
      status: 'abort',
      message: 'No valid targets found in assets array.'
    }
  };
}

const scan_title = String.replaceAll(scan_name_prefix + ' ' + project_name + ' ' + safe_time, ' ', '+');

// Build Qualys scan launch URL with correct parameter for each asset type
let qualys_url = 'https://' + secrets.qualys_api_url
  + '/api/3.0/fo/scan/?action=launch'
  + '&scan_title=' + scan_title
  + '&option_id=' + secrets.qualys_option_id;

const qualys_auth_base64 = "Basic " + String.encode(secrets.qualys_auth, 'base64');

if (Array.length(ip_targets) > 0) {
  qualys_url = qualys_url + '&ip=' + Array.join(ip_targets, ',');
}
if (Array.length(fqdn_targets) > 0) {
  qualys_url = qualys_url + '&fqdn=' + Array.join(fqdn_targets, ',') + "&iscanner_name=External";
}

if (secrets.logging_level === 'debug') {
  Logger.debug('ip_targets: ', JSON.stringify(ip_targets));
  Logger.debug('fqdn_targets: ', JSON.stringify(fqdn_targets));
  Logger.debug('qualys_url: ', qualys_url);
}

return {
  decision: {
    status: 'continue',
    message: 'Launching Qualys VM scan for project: ' + project_id,
  },
  request: {
    url: qualys_url,
    method: 'POST',
    headers: {
      Authorization: qualys_auth_base64
    }
  },
  data: {
    project_id: project_id
  }
};

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);
      if (!(num >= 0 && num <= 255)) return false;
    }
  }
  return true;
}

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 !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Qualys scan launch HTTP error: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error launching Qualys VM scan. HTTP status: ' + (response?.statusCode || 'unknown')
    }
  };
}

// Qualys returns HTTP 200 even for errors — check XML body for error codes.
// response.body contains the raw XML text.
const xml = response.body;

if (!xml) {
  return {
    decision: {
      status: 'abort',
      message: 'Empty response body from Qualys scan launch.'
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Qualys launch response: ', xml);
}

const res_parsed = XML.parse(xml);
let res_response;
if (res_parsed && res_parsed[0] && res_parsed[0]['SIMPLE_RETURN'][0]['RESPONSE']) {
  res_response = res_parsed[0]['SIMPLE_RETURN'][0]['RESPONSE'];
}

// Find the ITEM_LIST block (avoids fragile index like [2])
let item_list_entry;
if (res_response) {
  item_list_entry = Array.find(res_response, findItemList);
}

if (!item_list_entry) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('res_response: ', JSON.stringify(res_response));
  }
  return {
    decision: { status: 'abort', message: 'Error: ITEM_LIST not found in response. Please check the log.' }
  };
}

const items = item_list_entry['ITEM_LIST'];
const scan_meta = {};
for (let i = 0; i < Array.length(items); i++) {
  const fields = items[i]['ITEM'];
  const key = fields[0]['KEY'][0]['#text'];
  const value = fields[1]['VALUE'][0]['#text'];
  scan_meta[key] = value;
}

const scan_ref = String.split(scan_meta['REFERENCE'], 'scan/')[1];

if (!scan_ref) {
  return {
    decision: { 
      status: 'abort', 
      message: 'Error: scan_ref is missing from ITEM_LIST. Please check the log.' 
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Qualys VM scan launched. scan_ref: ' + scan_ref,
  },
  data: {
    project_id: data?.project_id,
    scan_ref: scan_ref
  }
};

function findItemList(obj){
  return Object.keys(obj)[0] === 'ITEM_LIST';
}
```

**Action 2 - 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_ref){
  return {
    decision:{
      status: 'abort',
      message: 'Error: scan_ref is required.'
    }
  };
}
if (!data?.project_id){
  return {
    decision:{
      status: 'abort',
      message: 'Error: project_id is required.'
    }
  };
}

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

* **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 "qualys_vm_active_scan" custom_field'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully updated project with "qualys_vm_active_scan" custom_field'
  }
};
```

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

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

The purpose of this example is to poll the scan status for any active Qualys 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
  * qualys\_vm\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Poll and Fetch Findings \[Step 4 of 5\]](#qualys-vm-poll-and-fetch-findings-step-4-of-5)
  * qualys\_was\_poll\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys WAS - Poll and Fetch Findings \[Step 4 of 5\]](#qualys-was-poll-and-fetch-findings-step-4-of-5)
  * logging\_level - set to "debug" for additional logging

**Action 1 - Get Qualys Active 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
if (!secrets.af_hostname){
  return {
    decision: {
      status: 'abort',
      message: 'af_hostname is required to request AttackForge projects.'
    }
  };
}

const limit = 50;
const skip = data?.skip ?? 0;

return {
  decision: {
    status: 'continue',
    message: 'Getting all projects with an active Qualys scan.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/projects?order=created:desc&skip='+skip+'&limit='+limit
  },
  data:{
    limit: limit,
    skip: skip,
    active_vm_scan_projects: data?.active_vm_scan_projects || [],
    active_was_scan_projects: data?.active_was_scan_projects || [],
  }
};
```

* **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. Please check the log.'
    }
  };
}

if (data && (!data?.active_vm_scan_projects || !data?.active_was_scan_projects)) {
  data.active_vm_scan_projects = data.active_vm_scan_projects || [];
  data.active_was_scan_projects = data.active_was_scan_projects || [];
}

const projects = response?.jsonBody?.projects;
const count = projects ? Array.length(projects) : 0;
let repeat = false;

if (count === data?.limit) {
  repeat = true;
}

if (!projects || Array.length(projects) === 0) {
  if (data?.active_vm_scan_projects 
    && Array.length(data.active_vm_scan_projects) === 0 
    && Array.length(data.active_was_scan_projects) === 0
  ) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('active_vm_scan_projects: ', JSON.stringify(data.active_vm_scan_projects));
      Logger.debug('active_was_scan_projects: ', JSON.stringify(data.active_was_scan_projects));
    }
    return {
      decision: {
        status: 'finish',
        message: 'No projects with an active Qualys VM or WAS scan found.'
      }
    };
  }
  return {
    decision: {
      status: 'continue',
      message: 'No new projects found - continuing to next action.'
    },
    data: {
      active_vm_scan_projects: data?.active_vm_scan_projects,
      active_was_scan_projects: data?.active_was_scan_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 active_vm_scan_cf = Array.find(cfs, find_active_vm_scan);
  const active_was_scan_cf = Array.find(cfs, find_active_was_scan);

  if (active_vm_scan_cf?.value) {
    Array.push(data.active_vm_scan_projects, {
      project_id: project?.project_id,
      project_name: project?.project_name,
      scan_ref: active_vm_scan_cf?.value
    });
  }
  if (active_was_scan_cf?.value) {
    const was_multiscan = Array.find(cfs, find_was_multiscan);
    const was_scan_time = Array.find(cfs, find_was_scan_time);
    Array.push(data.active_was_scan_projects, {
      project_id: project?.project_id,
      project_name: project?.project_name,
      scan_ref: active_was_scan_cf?.value,
      multiscan: was_multiscan?.value,
      scan_time: was_scan_time?.value
    });
  }
}

if (repeat) {
  return {
    decision: {
      status: 'repeat',
      message: 'Requesting more projects. Currently fetched ' + data?.skip + ' projects.'
    },
    data: {
      limit: data.limit,
      skip: data.limit + data.skip,
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}

if (Array.length(data.active_vm_scan_projects) === 0 
  && Array.length(data.active_was_scan_projects) === 0
) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('active_vm_scan_projects: ', JSON.stringify(data.active_vm_scan_projects));
    Logger.debug('active_was_scan_projects: ', JSON.stringify(data.active_was_scan_projects));
  }
  return {
    decision: {
      status: 'finish',
      message: 'No projects with an active Qualys VM or WAS scan found.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Found ' + Array.length(data.active_vm_scan_projects) + ' active VM scan project(s) and Found ' + Array.length(data.active_was_scan_projects) + ' active WAS scan project(s).'
  },
  data: {
    active_vm_scan_projects: data.active_vm_scan_projects,
    active_was_scan_projects: data.active_was_scan_projects,
  }
};

function find_active_vm_scan(field) {
  return field.key === 'qualys_vm_active_scan';
}
function find_active_was_scan(field) {
  return field.key === 'qualys_was_active_scan';
}
function find_was_multiscan(field) {
  return field.key === 'qualys_was_multiscan';
}
function find_was_scan_time(field) {
  return field.key === 'qualys_was_scan_time';
}
```

**Action 2 - Trigger Qualys VM Poll Flow**

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

```javascript
if (data?.active_vm_scan_projects && Array.length(data.active_vm_scan_projects) > 0) {
  const curr_project = data.active_vm_scan_projects[0];

  if (!curr_project?.project_id || !curr_project?.project_name) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_id or project_name missing in current project. Please check the log.'
      }
    };
  }

  if (curr_project?.scan_ref === undefined) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: scan_ref missing in current project. Please check the log.'
      }
    };
  }

  return {
    decision: {
      status: 'continue',
      message: 'Triggering Qualys VM Poll Flow for project: ' + curr_project.project_id + '.'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_vm_poll_flow_trigger_id,
      body: {
        project: {
          project_id: curr_project.project_id,
          project_name: curr_project.project_name,
          scan_ref: curr_project.scan_ref
        }
      }
    },
    data: {
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}
else {
  return {
    decision: {
      status: 'next',
      message: 'No VM Scans to poll, continuing to next action.'
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering poll flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error triggering Qualys VM Poll Flow. Please check the log.'
    }
  };
}

if (!data?.active_vm_scan_projects) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: Should not reach here.');
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.active_vm_scan_projects missing. Please check the log.'
    }
  };
}

Array.shift(data.active_vm_scan_projects);
if (Array.length(data.active_vm_scan_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Triggering next Qualys VM Poll Flow.',
      delay: 1000
    },
    data: {
      active_vm_scan_projects: data.active_vm_scan_projects,
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'continue',
      message: 'Completed triggering Qualys VM Poll Flow for all active scan projects. Continuing to next action.'
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
```

**Action 3 - Trigger Qualys WAS Poll Flow**

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

```javascript
if (data?.active_was_scan_projects && Array.length(data.active_was_scan_projects) > 0) {
  const curr_project = data.active_was_scan_projects[0];

  if (!curr_project?.project_id 
    || !curr_project?.project_name 
    || !curr_project?.multiscan 
    || !curr_project?.scan_time
  ) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_id, project_name, multiscan, and scan_time must all be present in current project. Please check the log.'
      }
    };
  }

  if (curr_project?.scan_ref === undefined) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Current project: ', JSON.stringify(curr_project));
    }
    return {
      decision: {
        status: 'abort',
        message: 'Error: scan_ref missing in current project. Please check the log.'
      }
    };
  }

  if (curr_project.scan_ref === ""){
    return {
      decision: {
        status: 'next',
        message: 'No WAS Scan to poll for project: ' + curr_project.project_id
      }
    };
  }

  return {
    decision: {
      status: 'continue',
      message: 'Triggering Qualys WAS Poll Flow for project: ' + curr_project.project_id + '.'
    },
    request: {
      url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_was_poll_flow_trigger_id,
      body: {
        project: {
          project_id: curr_project.project_id,
          project_name: curr_project.project_name,
          scan_ref: curr_project.scan_ref,
          multiscan: curr_project.multiscan,
          scan_time: curr_project.scan_time
        }
      }
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects,
    }
  };
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No WAS scans to poll, exiting the process.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering poll flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error triggering Qualys WAS Poll Flow. Please check the log.'
    }
  };
}

if (!data?.active_was_scan_projects) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: Should not reach here.');
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.active_was_scan_projects missing. Please check the log.'
    }
  };
}

Array.shift(data.active_was_scan_projects);
if (Array.length(data.active_was_scan_projects) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Triggering next Qualys WAS Poll Flow.',
      delay: 1000
    },
    data: {
      active_was_scan_projects: data.active_was_scan_projects
    }
  };
}
else {
  return {
    decision: {
      status: 'finish',
      message: 'Completed triggering Qualys WAS Poll Flow for all active scan projects.'
    }
  };
}
```

## Qualys WAS - Poll and Fetch Findings \[Step 4 of 5]

<figure><img src="/files/6RzWr7fn3bYVzypXE46i" alt=""><figcaption></figcaption></figure>

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

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
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_import\_vuln\_trigger\_flow\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM & WAS - Import Vulnerabilities \[Step 5 of 5\]](#qualys-vm-and-was-import-vulnerabilities-step-5-of-5)
  * qualys\_web\_base\_url - your Qualys web console base URL e.g. *<https://qualysguard.qualys.com>*
  * admin\_email - the email address to send error and alert notifications
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll WAS Scan status**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/wasscan
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/xml
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (!secrets.qualys_auth){
  const err_msg = 'Error: qualys_auth missing from secrets.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: "N/A",
      project_name: "N/A",
      scan_ref: "N/A",
      flow_status: {
        step: 'Poll WAS Scan status - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

const project = data?.jsonBody?.project;
if (!project?.scan_ref || !project?.project_id || !project?.project_name) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project: ', JSON.stringify(project));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: scan_ref, project_id, project_name must all be present in the project. Please check the log.'
    }
  };
}

let field;
let criteria_value;
let operator;
if (project?.multiscan === "true"){
  field = "name";
  criteria_value = project.project_name + ' ' + project.scan_time;
  operator = 'CONTAINS';
} 
else {
  field = "id";
  criteria_value = project.scan_ref;
  operator = 'EQUALS';
}

const xml = "<ServiceRequest><filters><Criteria field='" + field + "' operator='" + operator + "'>" + criteria_value + "</Criteria></filters></ServiceRequest>";

return {
  decision: {
    status: 'continue',
    message: 'Polling Qualys WAS scan status',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/wasscan',
    body: xml,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: project.project_id,
    scan_ref: project.scan_ref,
    project_name: project.project_name,
    multiscan: project.multiscan
  }
};
```

* **Response Script**:

```javascript
if (response.statusCode !== 200){
  const err_msg = 'HTTP error returned from Qualys while polling for WAS scan status. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Poll WAS Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (responseCode !== 'SUCCESS'){
  const err_msg = 'Qualys WAS returned a non-SUCCESS responseCode while polling scan status: ' + responseCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Poll WAS Scan Status - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

// Get All WasScan — multiscan supported
const wasScans = (Array.find(xml_response, findData)).data;
const successful_was_scans   = [];
const unsuccessful_was_scans = [];

// WasScanStatus: skip still-in-progress scans
const processing_status = ['SUBMITTED', 'RUNNING'];

// WasScanResultStatus (consolidatedStatus) classification for FINISHED scans
const successful_consol_status = ['SUCCESSFUL', 'FINISHED'];
const partial_consol_status    = ['CANCELED_WITH_RESULTS', 'TIME_LIMIT_REACHED', 'TIME_LIMIT_EXCEEDED', 'MAX_LINKS_CRAWLED'];
const hard_fail_consol_status  = ['NO_HOST_ALIVE', 'NO_WEB_SERVICE', 'SERVICE_ERROR', 'SCAN_INTERNAL_ERROR', 'SCAN_RESULTS_INVALID', 'SCAN_NOT_LAUNCHED', 'ERROR'];
const soft_fail_consol_status  = ['SCANNER_NOT_AVAILABLE', 'CANCELED', 'CANCELING', 'DELETED'];

for (let i = 0; i < Array.length(wasScans); i++) {
  const status        = (Array.find(wasScans[i].WasScan, findstatus)).status[0]['#text'];
  const consol_status = (Array.find(wasScans[i].WasScan, findConsolStatus)).consolidatedStatus[0]['#text'];
  const launched_date = (Array.find(wasScans[i].WasScan, findLaunchedDate)).launchedDate[0]['#text'];
  const web_app_id    = (Array.find(wasScans[i].WasScan, findTarget)).target[0].webApp[0].id[0]['#text'];
  const scan_id       = wasScans[i].WasScan[0].id[0]['#text'];

  if (Array.includes(processing_status, status)) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Scan ' + scan_id + ' with status ' + status + ' is still in progress. Skipping.');
    }
    continue;
  }

  // ERROR and CANCELED are terminal by WasScanStatus alone — no consol_status needed
  if (status === 'ERROR') {
    Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    continue;
  }

  if (status === 'CANCELED') {
    Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'soft' });
    continue;
  }

  if (status === 'FINISHED') {
    // Success
    if (Array.includes(successful_consol_status, consol_status)) {
      Array.push(successful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, launched_date: launched_date, web_app_id: web_app_id, is_partial: false });
    }
    // Partial Success
    else if (Array.includes(partial_consol_status, consol_status)) {
      Array.push(successful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, launched_date: launched_date, web_app_id: web_app_id, is_partial: true });
    }
    // Hard Fail
    else if (Array.includes(hard_fail_consol_status, consol_status)) {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    }
    // Soft Fail
    else if (Array.includes(soft_fail_consol_status, consol_status)) {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'soft' });
    }
    // Unrecognised status
    else {
      Array.push(unsuccessful_was_scans, { scan_id: scan_id, status: status, consolidated_status: consol_status, fail_type: 'hard' });
    }
    continue;
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('successful_was_scans: ' + JSON.stringify(successful_was_scans));
  Logger.debug('unsuccessful_was_scans: ' + JSON.stringify(unsuccessful_was_scans));
}

if (Array.length(successful_was_scans) === 0 && Array.length(unsuccessful_was_scans) === 0) {
  return {
    decision: {
      status: 'finish',
      message: 'No scans are yet finished or failed. Exiting — will be re-polled.'
    }
  };
}

// All scans failed — build collective message listing every failure
if (Array.length(successful_was_scans) === 0 && Array.length(unsuccessful_was_scans) > 0) {
  const fail_summaries = [];
  for (let i = 0; i < Array.length(unsuccessful_was_scans); i++) {
    const f = unsuccessful_was_scans[i];
    Array.push(fail_summaries, 'Scan ' + f.scan_id + ': status=' + f.status + ' (consolidatedStatus=' + f.consolidated_status + ') aborted status: ' + f.consolidated_status + '.');
  }
  const err_msg = 'All WAS scans ended unsuccessfully. ' + Array.join(fail_summaries, ' ');
  return {
    decision: {
      status: 'next',
      message: 'All WAS scans ended unsuccessfully. Proceeding to email report action.'
    },
    data: {
      project_id:   data?.project_id,
      project_name: data?.project_name,
      scan_ref:     data?.scan_ref,
      unsuccessful_was_scans: unsuccessful_was_scans,
      flow_status: {
        step:    'Poll WAS Scan Status - Response Script',
        status:  unsuccessful_was_scans[0].status,
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully collected ' + Array.length(successful_was_scans) + ' finished scan(s). Continuing to find findings.'
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    successful_was_scans:   successful_was_scans,
    unsuccessful_was_scans: unsuccessful_was_scans
  }
};

function findResponseCode (obj){
  if (Object.keys(obj)[0] === 'responseCode'){
    return obj;
  }
}
function findData (obj){
  if (Object.keys(obj)[0] === 'data'){
    return obj;
  }
}
function findstatus (wasScan){
  if (Object.keys(wasScan)[0] === 'status'){
    return wasScan;
  }
}
function findConsolStatus (wasScan){
  if (Object.keys(wasScan)[0] === 'consolidatedStatus'){
    return wasScan;
  }
}
function findLaunchedDate (wasScan){
  if (Object.keys(wasScan)[0] === 'launchedDate'){
    return wasScan;
  }
}
function findTarget (wasScan){
  if (Object.keys(wasScan)[0] === 'target'){
    return wasScan;
  }
}
```

**Action 2 - Search Findings**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/qps/rest/3.0/search/was/finding
* **Headers**:
  * Key = Content-Type; Type = Value; Value = text/xml
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (!secrets.qualys_auth){
  const err_msg = 'Error: qualys_auth missing from secrets.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status?.step + '. Proceeding to report action.'
    },
    data: data
  };
}

const curr_scan = data?.successful_was_scans?[0];

if (curr_scan) {
  const xml_body = "<ServiceRequest> <filters> <Criteria field='webApp.id' operator='EQUALS'>" + curr_scan.web_app_id + "</Criteria> <Criteria field='lastDetectedDate' operator='EQUALS'>" + curr_scan.launched_date + "</Criteria> <Criteria field='status' operator='NOT EQUALS'>FIXED</Criteria> </filters> </ServiceRequest>";

  return {
    decision: {
      status: 'continue',
      message: 'Requesting Qualys search findings on scan: ' + data.scan_ref,
    },
    request: {
      url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/search/was/finding',
      body: xml_body,
      headers: {
        Authorization: auth_header
      }
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      successful_was_scans:   data.successful_was_scans,
      unsuccessful_was_scans: data.unsuccessful_was_scans,
      final_result:           data.final_result || []
    },
  };
} 
else {
  const err_msg = 'Error: curr_scan not found. curr_scan: ' + data?.successful_was_scans?[0];
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
```

* **Response Script**:

```javascript
if (response.statusCode !== 200) {
  const err_msg = 'HTTP error fetching WAS findings. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { status: 'next', message: err_msg },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { step: 'Search Findings - Response Script', status: 'HTTP_ERROR', message: err_msg }
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (!xml_response){
  const err_msg = 'Error: ServiceResponse does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Search Findings - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
if (responseCode !== 'SUCCESS') {
  const err_msg = 'Qualys WAS findings search returned non-SUCCESS responseCode: ' + responseCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'Search Findings - Response Script', 
        status: 'VALIDATION_ERROR', 
        message: err_msg 
      }
    }
  };
}

const rawData     = Array.find(xml_response, findData);
const rawFindings = rawData ? rawData.data : null;

// Pre-populate webApp_groups from findings accumulated in previous repeats
const webApp_groups = {};
const accumulated = data.final_result || [];
for (let a = 0; a < Array.length(accumulated); a++) {
  const grp = accumulated[a];
  webApp_groups[grp.scan.scan_id] = grp;
}

if (rawFindings && Array.length(rawFindings) > 0) {
  for (let i = 0; i < Array.length(rawFindings); i++) {
    const f = rawFindings[i].Finding;

    const qid            = String.from(extractField(f, 'qid'));
    const name           = extractField(f, 'name');
    const severity       = extractField(f, 'severity');
    const type           = extractField(f, 'type');
    const firstDetected  = extractField(f, 'firstDetectedDate');
    const detectionScore = extractField(f, 'detectionScore');

    const webAppArr = extractFieldRaw(f, 'webApp');
    const webAppId  = String.from(extractField(webAppArr, 'id'));
    const webAppUrl = extractField(webAppArr, 'url');

    const status         = extractField(f, 'status') || '';
    const potential      = extractField(f, 'potential');
    const is_ignored     = extractField(f, 'isIgnored');
    const ignored_reason = extractField(f, 'ignoredReason') || '';
    const last_detected_at = extractField(f, 'lastDetectedDate') || '';

    if (status === 'FIXED') { 
      continue; 
    }

    const finding = {
      finding_id:       extractField(f, 'id'),
      qid:              qid,
      name:             name,
      risk_factor:      mapRiskFactor(severity),
      category:         type,
      uri:              webAppUrl,
      detection_score:  detectionScore,
      status:           status,
      potential:        potential,
      is_ignored:       is_ignored,
      ignored_reason:   ignored_reason,
      last_detected_at: last_detected_at,
      description:      '',
      solution:         '',
      cves:             [],
      cwe:              [],
      owasp:            [],
      cvss:             null,
      cvss_vector:      '',
      cvss3:            null,
      cvss3_vector:     '',
      evidence:         '',
      proof:            '',
      request_headers:  '',
      response_headers: '',
      payload:          '',
      input_name:       '',
      input_type:       '',
      group:            '',
      wasc:             '',
      times_detected:   null
    };

    if (!webApp_groups[webAppId]) {
      webApp_groups[webAppId] = {
        scan: {
          scan_id:    webAppId,
          target:     webAppUrl,
          started_at: firstDetected
        },
        findings: [finding]
      };
    } 
    else {
      Array.push(webApp_groups[webAppId].findings, finding);
    }
  }
}

// Shift the processed scan off the queue
Array.splice(data.successful_was_scans, 0, 1);

// Rebuild accumulated final_result
const final_result = [];
const keys = Object.keys(webApp_groups);
for (let k = 0; k < Array.length(keys); k++) {
  Array.push(final_result, webApp_groups[keys[k]]);
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Scans remaining: ' + Array.length(data.successful_was_scans) + '. Total findings so far: ' + Array.length(final_result));
}

// More scans to process — repeat the request with the next scan
if (Array.length(data.successful_was_scans) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Processed scan. ' + Array.length(data.successful_was_scans) + ' scan(s) remaining.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      successful_was_scans:   data.successful_was_scans,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      final_result:           final_result
    }
  };
}

// All scans processed — check if we have any findings at all
if (Array.length(final_result) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No findings returned from Qualys WAS.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      flow_status: { 
        step: 'Search Findings - Response Script', 
        status: 'NO_FINDINGS', 
        message: 'No findings returned from Qualys WAS Scan for scan: ' + data.scan_ref 
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Collected WAS findings across ' + Array.length(final_result) + ' web app(s).'
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    final_result:           final_result,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function mapRiskFactor(severity) {
  if (severity === 5) return 'critical';
  if (severity === 4) return 'high';
  if (severity === 3) return 'medium';
  if (severity === 2) return 'low';
  return 'info';
}

function findResponseCode(obj) { 
  if (Object.keys(obj)[0] === 'responseCode') return obj; 
}
function findData(obj) {
  if (Object.keys(obj)[0] === 'data') return obj;
}
```

**Action 3 - Get Findings**

* **Method**: GET
* **URL**: https\:///{{qualys\_api\_url}}/qps/rest/3.0/get/was/finding/{id}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = AttackForge
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id and final_result must be present in data.'
    }
  };
}

// Build enrichment queue on first call — one entry per finding_id
if (!data.enrichment_queue) {
  const queue = [];

  for (let r = 0; r < Array.length(data.final_result); r++) {
    const findings = data.final_result[r].findings;
    for (let f = 0; f < Array.length(findings); f++) {
      const finding = findings[f];
      if (finding.finding_id) {
        Array.push(queue, { 
          finding_id: finding.finding_id, 
          qid: finding.qid 
        });
      }
    }
  }

  data.enrichment_queue = queue;
  data.enrichment_total = Array.length(queue);
  data.enrichment_done  = 0;

  if (secrets.logging_level === 'debug') {
    Logger.debug('Built enrichment queue: ' + Array.length(queue) + ' findings to fetch.');
  }
}

if (Array.length(data.enrichment_queue) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'All WAS findings enriched. Proceeding to import.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      final_result:           data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const current = data.enrichment_queue[0];
const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Fetching WAS finding detail for finding ID ' + current.finding_id + ' (QID: ' + current.qid + '). ' + (data.enrichment_done + 1) + '/' + data.enrichment_total + '.'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/qps/rest/3.0/get/was/finding/' + current.finding_id,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    final_result:           data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || [],
    enrichment_queue:       data.enrichment_queue,
    enrichment_total:       data.enrichment_total,
    enrichment_done:        data.enrichment_done
  }
};
```

* **Response Script**:

```javascript
const current = data?.enrichment_queue?[0];

if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('HTTP error fetching WAS finding ' + current.finding_id + ': ' + response?.statusCode);
  }
  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;

  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'HTTP error for finding ID ' + current.finding_id + '. Skipping. Remaining: ' + Array.length(data.enrichment_queue) + '.'
      },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: {
      status: 'next',
      message: 'Enrichment complete with some HTTP errors. Proceeding to import.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

let xml_response;
if (response.body) {
  const parsed = XML.parse(response.body);
  if (parsed[0] && parsed[0].ServiceResponse) {
    xml_response = parsed[0].ServiceResponse;
  }
}
let responseCode;
if (xml_response) {
  responseCode = (Array.find(xml_response, findResponseCode))?.responseCode?[0]?['#text'];
}
if (!xml_response){
  const err_msg = 'Error: ServiceResponse does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Get Findings - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}
if (responseCode !== 'SUCCESS') {
  if (secrets.logging_level === 'debug') {
    Logger.debug('API error for finding ' + current.finding_id + ': responseCode=' + responseCode);
  }

  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;

  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: {
        status: 'repeat',
        message: 'API error for finding ID ' + current.finding_id + '. Skipping. Remaining: ' + Array.length(data.enrichment_queue) + '.'
      },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: {
      status: 'next',
      message: 'Enrichment complete with some API errors. Proceeding to import.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

const rawData   = Array.find(xml_response, findData);
const findingElement = rawData ? rawData.data[0].Finding : null;

if (!findingElement) {
  Array.splice(data.enrichment_queue, 0, 1);
  data.enrichment_done = data.enrichment_done + 1;
  if (Array.length(data.enrichment_queue) > 0) {
    return {
      decision: { status: 'repeat', message: 'No Finding element in response. Skipping.' },
      data: {
        project_id:             data?.project_id,
        project_name:           data?.project_name,
        scan_ref:               data?.scan_ref,
        final_result:           data?.final_result,
        unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
        enrichment_queue:       data?.enrichment_queue,
        enrichment_total:       data?.enrichment_total,
        enrichment_done:        data?.enrichment_done
      }
    };
  }
  return {
    decision: { status: 'next', message: 'Enrichment complete. Proceeding to import.' },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || []
    }
  };
}

// ── Parse shared fields (same for all findings with this QID) ──

const enrichedQid = String.from(extractField(findingElement, 'qid'));
const group       = extractField(findingElement, 'group') || '';

// CWE: <cwe><list><long>123</long></list></cwe>
const cwe = [];
const cweRaw  = extractFieldRaw(findingElement, 'cwe');
if (cweRaw) {
  const cweList = extractFieldRaw(cweRaw, 'list');
  if (cweList) {
    for (let c = 0; c < Array.length(cweList); c++) {
      if (cweList[c].long) {
        Array.push(cwe, cweList[c].long[0]['#text']);
      }
    }
  }
}

// OWASP: <owasp><list><OWASP><name/><code/><url/></OWASP></list></owasp>
// Step 5 expects [{ year, category }] — Qualys provides code (e.g. A01) without year
// Storing as { year: 'OWASP', category: code } so tag renders as OWASP-OWASP-A01
const owasp = [];
const owaspRaw = extractFieldRaw(findingElement, 'owasp');
if (owaspRaw) {
  const owaspList = extractFieldRaw(owaspRaw, 'list');
  if (owaspList) {
    for (let o = 0; o < Array.length(owaspList); o++) {
      if (owaspList[o].OWASP) {
        const owaspEntry = owaspList[o].OWASP;
        const code = extractField(owaspEntry, 'code') || '';
        const name = extractField(owaspEntry, 'name') || '';
        Array.push(owasp, { year: 'OWASP', category: code, name: name });
      }
    }
  }
}

// cvssV3: <cvssV3><base/><attackVector/></cvssV3>
const cvssV3Raw = extractFieldRaw(findingElement, 'cvssV3');
let cvss3        = null;
let cvss3_vector = '';
if (cvssV3Raw) {
  const base         = extractField(cvssV3Raw, 'base');
  const attackVector = extractField(cvssV3Raw, 'attackVector');
  if (base !== null)                        cvss3        = base;
  if (String.length(attackVector) > 0)      cvss3_vector = attackVector;
}

// Vulnerable parameter name
const input_name = extractField(findingElement, 'param') || '';

// Number of times the finding has been detected
const times_detected = extractField(findingElement, 'timesDetected');

// WASC stored as custom_field string
const wasc = [];
const wascRaw = extractFieldRaw(findingElement, 'wasc');
if (wascRaw) {
  const wascList = extractFieldRaw(wascRaw, 'list');
  if (wascList) {
    for (let w = 0; w < Array.length(wascList); w++) {
      if (wascList[w].WASC) {
        const wascEntry = wascList[w].WASC;
        const code = extractField(wascEntry, 'code') || '';
        const name = extractField(wascEntry, 'name') || '';
        Array.push(wasc, code + ' - ' + name);
      }
    }
  }
}

let uri             = '';
let payload         = '';
let request_headers = '';
let evidence        = '';

const resultListRaw = extractFieldRaw(findingElement, 'resultList');
if (resultListRaw) {
  const resultList = extractFieldRaw(resultListRaw, 'list');
  if (resultList && Array.length(resultList) > 0) {
    const firstResult = resultList[0].Result;
    if (firstResult) {
      const payloadsRaw = extractFieldRaw(firstResult, 'payloads');
      if (payloadsRaw) {
        const payloadList = extractFieldRaw(payloadsRaw, 'list');
        if (payloadList && Array.length(payloadList) > 0) {
          const firstPayload = payloadList[0].PayloadInstance;
          if (firstPayload) {
            payload = extractField(firstPayload, 'payload') || '';
            const requestRaw = extractFieldRaw(firstPayload, 'request');
            if (requestRaw) {
              uri             = extractField(requestRaw, 'link')    || '';
              request_headers = extractField(requestRaw, 'headers') || '';
            }
            evidence = extractField(firstPayload, 'response') || '';
          }
        }
      }
    }
  }
}

// enrichment
// QID level fields fan out to every finding sharing this QID.
// Instance fields (uri, payload, evidence, etc.) apply only to the specific finding fetched.

for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    if (findings[f].qid === enrichedQid) {
      // QID-level — same value for all findings with this QID
      findings[f].cwe   = cwe;
      findings[f].owasp = owasp;
      findings[f].group = group;
      if (Array.length(wasc) > 0)            findings[f].wasc         = Array.join(wasc, '; ');
      if (cvss3 !== null)                    findings[f].cvss3        = cvss3;
      if (String.length(cvss3_vector) > 0)   findings[f].cvss3_vector = cvss3_vector;
    }

    if (findings[f].finding_id === current.finding_id) {
      // Instance-level — specific to this individual finding
      if (String.length(uri) > 0)             findings[f].uri             = uri;
      if (String.length(payload) > 0)         findings[f].payload         = payload;
      if (String.length(request_headers) > 0) findings[f].request_headers = request_headers;
      if (String.length(evidence) > 0)        findings[f].evidence        = evidence;
      if (String.length(input_name) > 0)      findings[f].input_name      = input_name;
      if (times_detected !== null)            findings[f].times_detected  = times_detected;
    }
  }
}

Array.splice(data.enrichment_queue, 0, 1);
data.enrichment_done = data.enrichment_done + 1;

if (secrets.logging_level === 'debug') {
  Logger.debug('Enriched finding ' + current.finding_id + ' (QID ' + enrichedQid + '). Progress: ' + data.enrichment_done + '/' + data.enrichment_total + '.');
}

if (Array.length(data.enrichment_queue) > 0) {
  return {
    decision: {
      status: 'repeat',
      message: 'Enriched finding ' + current.finding_id + ' (' + data.enrichment_done + '/' + data.enrichment_total + '). Fetching next.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      final_result:           data?.final_result,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      enrichment_queue:       data?.enrichment_queue,
      enrichment_total:       data?.enrichment_total,
      enrichment_done:        data?.enrichment_done
    }
  };
}

return {
  decision: {
    status: 'next',
    message: 'All ' + data.enrichment_total + ' findings enriched. Proceeding to import.'
  },
  data: {
    project_id:             data?.project_id,
    project_name:           data?.project_name,
    scan_ref:               data?.scan_ref,
    final_result:           data?.final_result,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function findResponseCode(obj) {
  if (Object.keys(obj)[0] === 'responseCode') return obj; 
}
function findData(obj) { 
  if (Object.keys(obj)[0] === 'data')         return obj;
}
```

**Action 4 - Get Knowledgebase and Enrich**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/4.0/fo/knowledge\_base/vuln/?action=list\&details=All\&ids={{qids}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = AttackForge
  * Key = Accept; Type = Value; Value = application/xml
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to import.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  return {
    decision: {
      status: 'next',
      message: 'Error: project_id and final_result must be present in data.'
    },
    data:{
      project_id:   data.project_id,
      project_name: data.project_name,
      flow_status:{
        step: 'Get Knowledgebase and Enrich- Request Script',
        status: 'VALIDATION_ERROR',
        message: 'project_id and final_result must both be present in this step.'
      }
    }
  };
}

// Collect all unique QIDs across all findings
const qid_seen = {};
const qids = [];
for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    const qid = String.from(findings[f].qid);
    if (qid && !qid_seen[qid]) {
      qid_seen[qid] = true;
      Array.push(qids, qid);
    }
  }
}

if (Array.length(qids) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No QIDs found in findings. Skipping KB enrichment.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const qids_str = Array.join(qids, ',');
const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

if (secrets.logging_level === 'debug') {
  Logger.debug('Fetching KB for ' + Array.length(qids) + ' unique QID(s): ' + qids_str);
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching Qualys KB for ' + Array.length(qids) + ' unique QID(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/knowledge_base/vuln/?action=list&details=All&ids=' + qids_str,
    method: 'GET',
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    final_result: data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = 'HTTP error fetching KB: ' + response?.statusCode + '. Proceeding to import without KB enrichment.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

const parsed = XML.parse(response.body);
let kbRoot;
if (parsed && parsed[0]) {
  kbRoot = parsed[0].KNOWLEDGE_BASE_VULN_LIST_OUTPUT;
}
if (!kbRoot){
  const err_msg = 'Error: KNOWLEDGE_BASE_VULN_LIST_OUTPUT does not exist in parsed XML body.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      flow_status: {
        step: 'Get Knowledgebase and Enrich - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const kbResponseEl = Array.find(kbRoot, findResponse);
const vulnListEl   = kbResponseEl ? Array.find(kbResponseEl.RESPONSE, findVulnList) : null;

if (!vulnListEl) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('No VULN_LIST in KB response. Passing through.');
  }
  return {
    decision: { status: 'next', message: 'No VULN_LIST in KB response. Proceeding to import.' },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || []
    }
  };
}

// Build QID → KB data map
const kb_map  = {};
const vulnList = vulnListEl.VULN_LIST;

for (let i = 0; i < Array.length(vulnList); i++) {
  if (Object.keys(vulnList[i])[0] !== 'VULN') continue;
  const vuln = vulnList[i].VULN;

  const qid         = String.from(extractField(vuln, 'QID') || '');
  const diagnosis   = extractField(vuln, 'DIAGNOSIS')   || '';
  const consequence = extractField(vuln, 'CONSEQUENCE') || '';
  const solution    = extractField(vuln, 'SOLUTION')    || '';

  const cves       = [];
  const cveListRaw = extractFieldRaw(vuln, 'CVE_LIST');
  if (cveListRaw) {
    for (let c = 0; c < Array.length(cveListRaw); c++) {
      if (Object.keys(cveListRaw[c])[0] === 'CVE') {
        const cveId = extractField(cveListRaw[c].CVE, 'ID');
        if (cveId) Array.push(cves, cveId);
      }
    }
  }

  if (qid) {
    kb_map[qid] = {
      description: diagnosis,
      consequence: consequence,
      solution:    solution,
      cves:        cves
    };
  }
}

// Apply KB enrichment to all findings
let enriched_count = 0;
for (let r = 0; r < Array.length(data.final_result); r++) {
  const findings = data.final_result[r].findings;
  for (let f = 0; f < Array.length(findings); f++) {
    const finding = findings[f];
    const kb = kb_map[String.from(finding.qid)];
    if (kb) {
      if (String.length(kb.description) > 0) finding.description = kb.description;
      if (String.length(kb.consequence) > 0) finding.consequence = kb.consequence;
      if (String.length(kb.solution)    > 0) finding.solution    = kb.solution;
      if (Array.length(kb.cves)         > 0) finding.cves        = kb.cves;
      enriched_count = enriched_count + 1;
    }
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('KB enrichment applied to ' + enriched_count + ' finding(s).');
}

return {
  decision: {
    status: 'next',
    message: 'KB enrichment complete. Enriched ' + enriched_count + ' finding(s). Proceeding to import.'
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    final_result: data.final_result,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};

function extractField(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName][0]['#text'];
    }
  }
  return null;
}

function extractFieldRaw(arr, fieldName) {
  for (let j = 0; j < Array.length(arr); j++) {
    if (Object.keys(arr[j])[0] === fieldName) {
      return arr[j][fieldName];
    }
  }
  return null;
}

function findResponse(obj) { 
  if (Object.keys(obj)[0] === 'RESPONSE')  return obj; 
}
function findVulnList(obj) { 
  if (Object.keys(obj)[0] === 'VULN_LIST') return obj; 
}
```

**Action 5 - Trigger Import Flow**

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

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.final_result) {
  const err_msg = 'Error: project_id and final_result must be present in data. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      final_result: data.final_result,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Trigger Import Flow- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

if (Array.length(data.final_result) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No WAS findings to import. Flow complete.'
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      no_findings:            "true"
    }
  };
}

let totalFindings = 0;
for (let r = 0; r < Array.length(data.final_result); r++) {
  if (data.final_result[r].findings) {
    totalFindings = totalFindings + Array.length(data.final_result[r].findings);
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Triggering WAS import for project ' + data.project_id + ': ' + totalFindings + ' finding(s) across ' + Array.length(data.final_result) + ' web apps.'); 
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering import of ' + totalFindings + ' WAS finding(s) for project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_import_vuln_trigger_flow_id,
    body: {
      project_id:   data.project_id,
      project_name: data.project_name,
      final_result: data.final_result
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering WAS import flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error triggering import flow. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to cleanup and email report.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Trigger Import Flow - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error triggering import flow for project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('WAS import flow triggered successfully for project: ' + data?.project_id + '.');
}

return {
  decision: {
    status: 'continue',
    message: 'WAS import flow triggered successfully for project: ' + data?.project_id + '. Proceeding to cleanup.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || []
  }
};
```

**Action 6 - No Findings only - Add Note to Project**

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

```javascript
if (!data?.flow_status || data.flow_status?.status !== 'NO_FINDINGS'){
  return {
    decision:{
      status: 'next',
      message: 'Not a NO_FINDINGS flow, skipping action.'
    },
    data: data
  };
}

if (!data?.project_id){
  const err_msg = 'Error: data.project_id is missing in NO_FINDINGS path. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data.project_id,
      project_name:           data.project_name,
      scan_ref:               data.scan_ref,
      unsuccessful_was_scans: data.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'No Findings only - Add Note to Project- Request Script', 
        status: 'VALIDATION_ERROR', 
        message: err_msg 
      }
    }
  };
}

const now = Date.format(Date.datetime('now'), 'default');

return {
  decision: {
    status: 'continue',
    message: 'Requesting create new note on project: ' + data.project_id,
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id + '/note',
    body: {
      note: "No findings from Qualys WAS Scan. \nScan Reference: " + data.scan_ref +'\nDate: '+now,
      is_private: false,
      is_exported_to_report: false
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || [],
    flow_status: data.flow_status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  const err_msg = 'HTTP error creating note to project. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: { 
      status: 'next', 
      message: err_msg 
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: { 
        step: 'No Findings only - Add Note to Project- Response Script', 
        status: 'HTTP_ERROR', 
        message: err_msg 
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully added no findings note to project: '+ data?.project_name,
  },
  data: {
    project_id:             data?.project_id,
    project_name:           data?.project_name,
    scan_ref:               data?.scan_ref,
    unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
    flow_status: data?.flow_status
  }
};
```

**Action 7 - Update Project to Remove WAS Custom Fields**

* **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) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.project_id is missing. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Clearing Qualys WAS custom fields from project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        { key: 'qualys_was_active_scan', value: null },
        { key: 'qualys_was_scan_time',   value: null },
        { key: 'qualys_was_multiscan',   value: null }
      ]
    }
  },
  data: {
    project_id:             data.project_id,
    project_name:           data.project_name,
    scan_ref:               data.scan_ref,
    unsuccessful_was_scans: data.unsuccessful_was_scans || []
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error clearing WAS custom fields: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error clearing WAS custom fields from project: ' + data?.project_id + '. Proceeding to email report action.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans || [],
      flow_status: {
        step: 'Update Project - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error clearing WAS custom fields from project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

// fire warning email for succeeded but some scans failed
if (data?.unsuccessful_was_scans && Array.length(data.unsuccessful_was_scans) > 0) {
  const fail_summaries = [];
  for (let i = 0; i < Array.length(data.unsuccessful_was_scans); i++) {
    const f = data.unsuccessful_was_scans[i];
    Array.push(fail_summaries, 'Scan ' + f.scan_id + ': status=' + f.status + ' (consolidatedStatus=' + f.consolidated_status + ') aborted status: ' + f.consolidated_status + '.');
  }
  const err_msg = 'Import completed for successful scans, but ' + Array.length(data?.unsuccessful_was_scans) + ' scan(s) also failed. ' + Array.join(fail_summaries, ' ');
  return {
    decision: {
      status: 'next',
      message: 'Import completed but ' + Array.length(data?.unsuccessful_was_scans) + ' scan(s) failed. Proceeding to email report action.'
    },
    data: {
      project_id:             data?.project_id,
      project_name:           data?.project_name,
      scan_ref:               data?.scan_ref,
      unsuccessful_was_scans: data?.unsuccessful_was_scans,
      flow_status: {
        step:    'Update Project - Response Script',
        status:  'PARTIAL_FAILURE',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully cleared WAS custom fields from project: ' + data?.project_id + '.'
  },
  data: {
    project_id:   data?.project_id,
    project_name: data?.project_name,
    scan_ref:     data?.scan_ref
  }
};
```

**Action 8 - Send Email for Error Reporting**

* **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?.flow_status) {
  if (data.project_name && data.scan_ref && data.project_id) {
    const curr_date = Date.datetime('now');
    const curr_year = Date.format(curr_date, 'yyyy');
    const current_display_date = Date.format(curr_date, 'default');
    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; } 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> </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'> </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>";

    let content = '<p><strong>Attention: Qualys WAS Poll and Get Findings Flow has detected an error.</strong></p>'
    + '<p>Please check the AttackForge Flow log and Qualys WAS scan log for details.</p>'
    + '<br>'
    + '<table class="styled-table" align="center">'
    + '<tr><th width="200">Field</th><th width="200">Value</th></tr>'
    + '<tr><td> Scan Reference </td><td>' + data.scan_ref + '</td></tr>'
    + '<tr><td> Project </td><td>' + data.project_name + '</td></tr>'
    + '<tr><td> Step </td><td>' + data.flow_status?.step + '</td></tr>'
    + '<tr><td> Status </td><td>' + data.flow_status?.status + '</td></tr>'
    + '<tr><td> Message </td><td>' + data.flow_status?.message + '</td></tr>'
    + '<tr><td> Time </td><td>' + current_display_date + '</td></tr>'
    + '</table>'
    + '<br>';

    if (data.unsuccessful_was_scans && Array.length(data.unsuccessful_was_scans) > 0) {
      content = content
        + '<p><strong>Failed Scans (' + Array.length(data.unsuccessful_was_scans) + '):</strong></p>'
        + '<table class="styled-table" align="center">'
        + '<tr><th width="120">Scan ID</th><th width="120">Status</th><th width="200">Consolidated Status</th><th width="100">Fail Type</th></tr>';
      for (let i = 0; i < Array.length(data.unsuccessful_was_scans); i++) {
        const f = data.unsuccessful_was_scans[i];
        content = content + '<tr><td>' + f.scan_id + '</td><td>' + f.status + '</td><td>' + f.consolidated_status + '</td><td>' + (f.fail_type || '') + '</td></tr>';
      }
      content = content + '</table><br>';
    }

    content = content
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.af_hostname+'/flows/'+secrets.was_poll_flow_id+'/home">View Flow Run</a></td></tr></table>'
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.qualys_web_base_url+'was/#/scans/scans?searchPivotToken=SCAN&source=&pageSize=50&pageNumber=0">View Qualys WAS Board</a></td></tr></table>'
    + '<br>';

    return {
      decision: {
        status: 'continue',
        message: 'Sending WAS error report email.'
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/email',
        body: {
          to: [secrets.admin_email],
          subject: '[Flow Notification] Non successful scan discovered from Qualys WAS Scanning',
          html: emailHeader + content + emailFooter
        }
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_name, scan_ref, project_id must all be present in data. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No flow_status found. Flow completed successfully. Exiting.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error sending WAS report email: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'finish',
      message: 'Error sending report email. Please check the log.'
    }
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully sent WAS error report email to admin. Reached end of the flow.'
  }
};
```

## Qualys VM - Poll and Fetch Findings \[Step 4 of 5]

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

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

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
  * current\_flow\_id - the Id for this Flow in AttackForge once it's created (used for error reporting in email)
  * qualys\_api\_url - your Qualys API hostname e.g. qualysapi.qualys.com.
  * qualys\_auth - Qualys credentials in `username:password` format.
  * qualys\_import\_vuln\_trigger\_flow\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM & WAS - Import Vulnerabilities \[Step 5 of 5\]](#qualys-vm-and-was-import-vulnerabilities-step-5-of-5)
  * qualys\_vm\_launch\_scan\_flow\_trigger\_id - the [Trigger URL](https://support.attackforge.com/attackforge-enterprise/modules/flows#http-trigger-url) for [Qualys VM - Launch Scan \[Step 2 of 5\]](#qualys-vm-launch-scan-step-2-of-5)
  * qualys\_web\_base\_url - your Qualys web console base URL e.g. *<https://qualysguard.qualys.com>*
  * admin\_email - the email address to send error and alert notifications
  * logging\_level - set to "debug" for additional logging

**Action 1 - Poll Scan Status**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/scan/vm/summary/?action=list\&scan\_reference=scan/{{scan\_ref}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
const project = data?.jsonBody?.project;

if (!project?.scan_ref || !project?.project_id || !project?.project_name) {
  if (secrets.logging_level === 'debug'){
    Logger.debug('project: ', JSON.stringify(project));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: scan_ref, project_id, project_name must all be present in the project. Please check the log.'
    }
  };
}
if (!secrets.qualys_auth || secrets.qualys_auth === ""){
  if (secrets.logging_level === 'debug'){
    Logger.debug('secrets.qualys_auth: ', secrets.qualys_auth);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: secrets.qualys_auth must be present in the secrets. Please check the log.'
    }
  };
}

const auth_header  = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
return {
  decision: {
    status: 'continue',
    message: 'Requesting VM Scan Summary (Polling).',
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/2.0/fo/scan/vm/summary/?action=list&scan_reference=scan/' + project.scan_ref,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: project.project_id,
    scan_ref: project.scan_ref,
    project_name: project.project_name
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  let err_message = '';
  const err_body = XML.parse(response.body)[0];
  if (err_body?.SIMPLE_RETURN?[0]) {
    const err_response = err_body?.SIMPLE_RETURN?[0]?.RESPONSE;
    if (err_response) {
      function findText(obj) {
        return Object.keys(obj)[0] === 'TEXT';
      }
      const err_message_object = Array.find(err_response, findText);
      if (err_message_object) {
        err_message = err_message_object['TEXT'][0]['#text'];
      }
    }
  }
  if (secrets.logging_level === 'debug') {
    Logger.debug('Poll Scan Status failed. HTTP ' + response.statusCode + ': ' + err_message);
  }
  return {
    decision: {
      status: 'next',
      message: 'Error while polling scan status. HTTP ' + response.statusCode + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      flow_status: {
        step: 'Poll Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + response.statusCode + (err_message ? ': ' + err_message : '') + '.'
      }
    }
  };
}

if (!response?.body) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: response.body does not exist. Please check the log.'
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      flow_status: {
        step: 'Poll Scan Status - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body received from Qualys scan status poll.'
      }
    }
  };
}

const xml = response?.body;
const xml_parsed = XML.parse(xml)[0];
const flat = {};
const stack = [];

// Initialise the stack
Array.push(stack, { node: xml_parsed, parentKey: '' });
// Collision keys are ommited such as ID, Name Count.
while (Array.length(stack) > 0) {
  const item = Array.shift(stack);
  const node = item.node;
  const parentKey = item.parentKey;

  if (Array.isArray(node)) {
    for (let i = 0; i < Array.length(node); i++) {
      Array.push(stack, { node: node[i], parentKey: parentKey });
    }
  } 
  else if (node !== null && typeof node === 'object') {
    const entries = Object.entries(node);
    if (Array.length(entries) === 1 && entries[0][0] === '#text') {
      if (parentKey) {
        flat[parentKey] = entries[0][1];
      }
    } 
    else {
      for (let i = 0; i < Array.length(entries); i++) {
        const key = entries[i][0];
        const value = entries[i][1];
        const newKey = parentKey ? parentKey + '_' + key : key;
        Array.push(stack, { node: value, parentKey: key });
      }
    }
  }
}

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

// Extract IP ranges and DNS targets directly from xml_parsed
let targets_ip = [];
let targets_dns = [];

let targets_node = null;
const targets_search_stack = [xml_parsed];
while (Array.length(targets_search_stack) > 0 && targets_node === null) {
  const targets_search_item = Array.shift(targets_search_stack);
  if (Array.isArray(targets_search_item)) {
    for (let i = 0; i < Array.length(targets_search_item); i++) {
      Array.push(targets_search_stack, targets_search_item[i]);
    }
  } 
  else if (targets_search_item !== null && typeof targets_search_item === 'object') {
    const targets_search_keys = Object.keys(targets_search_item);
    if (Array.includes(targets_search_keys, 'TARGETS')) {
      targets_node = targets_search_item['TARGETS'];
    } 
    else {
      const targets_search_entries = Object.entries(targets_search_item);
      for (let i = 0; i < Array.length(targets_search_entries); i++) {
        Array.push(targets_search_stack, targets_search_entries[i][1]);
      }
    }
  }
}

if (targets_node) {
  const ip_range_list = [];
  const ip_stack = [targets_node];
  while (Array.length(ip_stack) > 0) {
    const ip_item = Array.shift(ip_stack);
    if (Array.isArray(ip_item)) {
      for (let i = 0; i < Array.length(ip_item); i++) {
        Array.push(ip_stack, ip_item[i]);
      }
    } 
    else if (ip_item !== null && typeof ip_item === 'object') {
      const ip_keys = Object.keys(ip_item);
      if (Array.includes(ip_keys, 'RANGE')) {
        const range_arr = ip_item['RANGE'];
        if (Array.isArray(range_arr)) {
          for (let i = 0; i < Array.length(range_arr); i++) {
            if (range_arr[i]['#text'] !== undefined) {
              Array.push(ip_range_list, String.from(range_arr[i]['#text']));
            }
          }
        }
      } 
      else if (Array.includes(ip_keys, 'IP_CSV')) {
        const csv_arr = ip_item['IP_CSV'];
        if (Array.isArray(csv_arr) && Array.length(csv_arr) > 0 && csv_arr[0] !== null) {
          Array.push(ip_range_list, String.from(csv_arr[0]['#text'] || ''));
        }
      } 
      else {
        const ip_entries = Object.entries(ip_item);
        for (let i = 0; i < Array.length(ip_entries); i++) {
          Array.push(ip_stack, ip_entries[i][1]);
        }
      }
    }
  }
  targets_ip = ip_range_list;

  const dns_stack = [targets_node];
  while (Array.length(dns_stack) > 0) {
    const dns_item = Array.shift(dns_stack);
    if (Array.isArray(dns_item)) {
      for (let i = 0; i < Array.length(dns_item); i++) {
        Array.push(dns_stack, dns_item[i]);
      }
    } 
    else if (dns_item !== null && typeof dns_item === 'object') {
      const dns_keys = Object.keys(dns_item);
      if (Array.includes(dns_keys, 'DNS_CSV')) {
        const csv_arr = dns_item['DNS_CSV'];
        if (Array.isArray(csv_arr) && Array.length(csv_arr) > 0 && csv_arr[0] !== null) {
          const dns_csv_str = String.from(csv_arr[0]['#text'] || '');
          targets_dns = String.split(dns_csv_str, ',');
        }
      } 
      else {
        const dns_entries = Object.entries(dns_item);
        for (let i = 0; i < Array.length(dns_entries); i++) {
          Array.push(dns_stack, dns_entries[i][1]);
        }
      }
    }
  }
}

if (flat['STATUS']){
  // prepare body to parse here
  const dataObject = {
    project_id: data.project_id,
    scan_ref: data.scan_ref,
    project_name: data.project_name,
    status: flat['STATUS']
  };

  const processing_status = ['RUNNING', 'QUEUED'];
  if (Array.includes(processing_status, flat['STATUS'])){
    if (secrets.logging_level === 'debug'){
      Logger.debug('Scan status: ', flat['STATUS']);
    }
    return {
      decision:{
        status:'finish',
        message:'Scan in status: ' + flat['STATUS'] + '. No further actions necessary. Exiting the flow.'
      }
    };
  }

  // If status successful
  if (flat['STATUS'] === 'FINISHED'){
    return {
      decision: {
        status: 'continue',
        message: 'Scan finished successfully. Proceeding to fetching scan results.'
      },
      data: dataObject
    };
  }

  // skip all the way to notes
  if (flat['STATUS'] === 'NOVULNSFOUND'){
    dataObject.flow_status = {
      step: 'Poll Scan Status- Response Script',
      status: 'NOVULNSFOUND',
      message: 'No vulnerability found from scan: ' + data.scan_ref
    };
    return {
      decision: {
        status: 'continue',
        message: 'No vulnerability found from scan: ' + data.scan_ref + '. Proceeding to adding no vulnerability found note to project: ' + data.project_id
      },
      data: dataObject
    };
  }

  if (flat['STATUS'] === 'INTERRUPTED'){
    let combined_targets = [];
    combined_targets = Array.concat(combined_targets, targets_ip);
    combined_targets = Array.concat(combined_targets, targets_dns);
    dataObject.targets = combined_targets;
    return {
      decision: {
        status: 'continue',
        message: 'Detected '+flat['STATUS']+' status on scan/'+ data.scan_ref+'. Proceeding to automatic relaunch of the scan.'
      },
      data: dataObject
    };
  }

  const email_report_status = ['ERROR', 'NOHOSTALIVE', 'CANCELED', 'PAUSED'];

  if (Array.includes(email_report_status, flat['STATUS'])) {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Unsuccessful scan. Scan status: ', flat['STATUS']);
    }
    dataObject.flow_status = {
      step: 'Poll Scan Status - Response Script',
      status: flat['STATUS'],
      message: 'Scan ended with status: ' + flat['STATUS'] + '.'
    };
    return {
      decision: {
        status: 'next',
        message: 'Unsuccessful scan status detected: ' + flat['STATUS'] + '. Proceeding to email report action.'
      },
      data: dataObject
    };
  }

  if (secrets.logging_level === 'debug') {
    Logger.debug('Unrecognised scan status detected: ', flat['STATUS']);
    Logger.debug('data: ', JSON.stringify(data));
  }
  dataObject.flow_status = {
    step: 'Poll Scan Status - Response Script',
    status: flat['STATUS'],
    message: 'Unrecognised scan status detected: ' + flat['STATUS'] + '.'
  };
  return {
    decision: {
      status: 'next',
      message: 'Error: detected unrecognised scan status: ' + flat['STATUS'] + '. Proceeding to email report action.'
    },
    data: dataObject
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('STATUS field not found in XML response.');
  Logger.debug('response: ', JSON.stringify(response));
}
return {
  decision: {
    status: 'next',
    message: 'Error: STATUS field not found in Qualys API response. Proceeding to email report action.'
  },
  data: {
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    flow_status: {
      step: 'Poll Scan Status - Response Script',
      status: 'UNKNOWN',
      message: 'STATUS field not found in Qualys API response.'
    }
  }
};
```

**Action 2 - If Interrupted - Get Project Assets**

* **Method**: GET
* **URL**: https\://{{af\_hostname}}/api/ss/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.status !== 'INTERRUPTED') {
  return {
    decision: {
      status: 'next',
      message: data.status + ' status detected. Skipping to next action.'
    },
    data: data
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug(data.status + ' status detected. Initiating retry phase.');
}

if (!secrets.af_hostname || !data?.project_id) {
  const err_msg = 'secrets.af_hostname and data.project_id must be present. secrets.af_hostname: '+ secrets?.af_hostname + '. data.project_id: ' + data?.project_id;
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Request Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Fetching asset information from project: ' + data.project_id + '.',
  },
  request: {
    url: 'https://'+secrets.af_hostname+'/api/ss/project/'+data?.project_id,
  },
  data:{
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    status: data?.status,
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: ' + JSON.stringify(response));
  }
  const err_msg = 'Error while fetching project asset. Please check the log.';
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP Error:' + response?.statusCode + '. ' + err_msg
      }
    }
  };
}

if (secrets.logging_level === 'debug'){
  Logger.debug(data.status + ' status detected. Initiating retry phase.');
}

let assets;
if (response?.jsonBody){
  if (response.jsonBody?.project?.project_scope){
    assets = response.jsonBody.project.project_scope;
  } 
  else {
    if (secrets.logging_level === 'debug') {
      Logger.debug('Missing project or project_scope: ' + JSON.stringify(response.jsonBody?.project));
    }
    const err_msg = 'Error while fetching project asset. Missing project or project_scope. Please check the log.';
    return {
      decision: {
        status: 'next',
        message: err_msg
      },
      data:{
        project_id: data?.project_id,
        scan_ref: data?.scan_ref,
        project_name: data?.project_name,
        status: data?.status,
        flow_status: {
          step: '[Error route] Get Project Assets - Response Script',
          status: 'VALIDATION_ERROR',
          message: err_msg
        }
      }
    };
  }
} 
else {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error: ' + JSON.stringify(response));
  }
  const err_msg = 'Error while fetching project asset. response.jsonBody is not present. Please check the log.';
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data:{
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      flow_status: {
        step: '[Error route] Get Project Assets - Response Script',
        status: 'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully fetched valid project assets from project: '+ data.project_id + '.',
  },
  data: {
    project_id: data?.project_id,
    scan_ref: data?.scan_ref,
    project_name: data?.project_name,
    status: data?.status,
    assets: assets
  }
};
```

**Action 3 - Re-launch Interrupted Scan**

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

```javascript
if (data.status !== 'INTERRUPTED') {
  return {
    decision: {
      status: 'next',
      message: data.status + ' status detected. Skipping to next action.'
    },
    data: data
  };
}

if (!data.scan_ref || !data.project_id || !data.assets || !data.project_name) {
  const err_message = "Error while relaunching Interrupted Scan. scan_ref, project_id, project_name, and targets must all be present in data object.";
  return {
    decision:{
      status: 'next',
      message: err_message
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status,
      assets: data.assets,
      flow_status: {
        step: 'Re-launch Interrupted Scan- Request Script',
        status: 'VALIDATION_ERROR',
        message: err_message
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Relaunching Interrupted Scan: ' + data.scan_ref + ' on assets: ' + JSON.stringify(data.assets),
  },
  request: {
    url: 'https://'+secrets.af_hostname+'/api/flows/'+secrets.qualys_launch_scan_flow_trigger_id,
    body: {
      project_id: data.project_id,
      project_name: data.project_name,
      assets: data.assets
    }
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202){
  if (secrets.logging_level === 'debug'){
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'Error while relaunching the scan. Please check the log.',
    },
    data: {
      project_id: data?.project_id,
      scan_ref: data?.scan_ref,
      project_name: data?.project_name,
      status: data?.status,
      targets: data?.targets,
      flow_status: {
        step: 'Re-launch Interrupted Scan- Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + response.statusCode + '. Error while relaunching the interrupted scan.'
      }
    },
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully requested relaunch of the interrupted scan.',
  }
};
```

**Action 4 - Fetch Scan Result**

* **Method**: POST
* **URL**: https\://{{qualys\_api\_url}}/api/3.0/fo/scan/?action=fetch\&scan\_ref=scan/{{scan\_ref}}\&output\_format=json
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

const success_status = ['FINISHED'];
if (!Array.includes(success_status, data.status)) {
  return {
    decision: {
      status: 'next',
      message: 'Status ' + data.status + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status
    }
  };
}

if (!secrets.qualys_auth || secrets.qualys_auth === undefined || !secrets.qualys_api_url || secrets.qualys_api_url === undefined) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('qualys_auth: ', secrets.qualys_auth);
    Logger.debug('qualys_api_url: ', secrets.qualys_api_url);
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: qualys_auth and qualys_api_url must be registered in secrets. Please check the log.'
    }
  };
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');

return {
  decision: {
    status: 'continue',
    message: 'Fetching scan results for scan_ref: ' + data.scan_ref + '.'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/scan/?action=fetch&scan_ref=scan/' + data.scan_ref + '&output_format=json',
    method: 'GET',
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching scan result: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error fetching Qualys scan result. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error fetching Qualys scan result.'
      }
    }
  };
}

const body = response?.body;
const results = body ? JSON.parse(body) : response?.jsonBody;

if (!results) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Empty response body from Qualys scan fetch. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body from Qualys scan fetch.'
      }
    }
  };
}

if (!results || !Array.isArray(results)) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('results: ', JSON.stringify(results));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Invalid JSON array returned from Qualys scan fetch. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Scan Result - Response Script',
        status: 'HTTP_ERROR',
        message: 'Invalid JSON array returned from Qualys scan fetch.'
      }
    }
  };
}

const qid_seen = {};
const unique_qids = [];
for (let i = 0; i < Array.length(results); i++) {
  const qid = results[i]?.qid;
  if (qid && !qid_seen[String.from(qid)]) {
    qid_seen[String.from(qid)] = true;
    Array.push(unique_qids, String.from(qid));
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Fetched ' + Array.length(results) + ' raw detections, ' + Array.length(unique_qids) + ' unique QIDs.');
}

return {
  decision: {
    status: 'continue',
    message: 'Fetched ' + Array.length(results) + ' raw detection(s). Proceeding to KnowledgeBase enrichment.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    raw_detections: results,
    qids: unique_qids
  }
};
```

**Action 5 - Fetch Knowledgebase**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/4.0/fo/knowledge\_base/vuln/?action=list\&details=All\&ids={{qids\_param}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

// No QIDs means nothing to enrich - pass empty records forward
if (!data?.qids || Array.length(data.qids) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No QIDs to enrich. Skipping KnowledgeBase lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: []
    }
  };
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
const qids_param = Array.join(data.qids, ',');

return {
  decision: {
    status: 'continue',
    message: 'Fetching KnowledgeBase metadata for ' + Array.length(data.qids) + ' unique QID(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/4.0/fo/knowledge_base/vuln/?action=list&details=All&ids=' + qids_param,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    raw_detections: data.raw_detections,
    qids: data.qids
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error fetching KnowledgeBase: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error fetching Qualys KnowledgeBase. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Knowledgebase - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error fetching Qualys KnowledgeBase.'
      }
    }
  };
}

const kb_xml = response?.body;
if (!kb_xml) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('response: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error: Empty response body from Qualys KnowledgeBase API. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Fetch Knowledgebase - Response Script',
        status: 'HTTP_ERROR',
        message: 'Empty response body from Qualys KnowledgeBase API.'
      }
    }
  };
}

const xml_parsed = XML.parse(kb_xml)[0];
if (secrets.logging_level === 'debug') {
  Logger.debug('KB XML parsed: ', JSON.stringify(xml_parsed));
}

// nav is cursor to VULN_LIST
let nav = xml_parsed;
if (nav && nav.KNOWLEDGE_BASE_VULN_LIST_OUTPUT){
  nav = nav.KNOWLEDGE_BASE_VULN_LIST_OUTPUT;
  if (Array.isArray(nav)) nav = nav[0];
  if (nav && nav.RESPONSE) {
    const response_items = Array.isArray(nav.RESPONSE) ? nav.RESPONSE : [nav.RESPONSE];
    nav = null;
    for (let i = 0; i < Array.length(response_items); i++){
      if (Object.keys(response_items[i])[0] === 'VULN_LIST'){
        nav = response_items[i].VULN_LIST;
        break;
      }
    }
  }
}

let vuln_array = [];
if (Array.isArray(nav)) {
  for (let i = 0; i < Array.length(nav); i++) {
    const item = nav[i];
    if (item && item.VULN && Array.isArray(item.VULN)) {
      Array.push(vuln_array, flattenVuln(item.VULN));
    }
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('KB vuln count: ', Array.length(vuln_array));
}

// Build QID -> KB metadata map
const kb_map = {};
for (let i = 0; i < Array.length(vuln_array); i++) {
  const vuln = vuln_array[i];
  if (!vuln) continue;
  const qid_str = extractText(vuln.QID);

  if (!qid_str) continue;

  // Severity mapping: Qualys 1-5 -> Tenable Risk/Severity 0-4
  const sl_raw = Number.parseInt(extractText(vuln.SEVERITY_LEVEL)) || 1;
  let risk = 'None';
  if (sl_raw === 2) risk = 'Low';
  else if (sl_raw === 3) risk = 'Medium';
  else if (sl_raw === 4) risk = 'High';
  else if (sl_raw === 5) risk = 'Critical';
  const severity_str = String.from(sl_raw > 1 ? sl_raw - 1 : 0);

  // CVE list: comma-separated IDs
  let cve_str = '';
  if (vuln.CVE_LIST && vuln.CVE_LIST?[0]?.CVE) {
    const cve_nodes = Array.isArray(vuln.CVE_LIST[0].CVE) ? vuln.CVE_LIST[0].CVE : [vuln.CVE_LIST[0].CVE];
    const cve_ids = [];
    for (let j = 0; j < Array.length(cve_nodes); j++) {
      const id = extractText(cve_nodes[j]?.ID);
      if (id) Array.push(cve_ids, id);
    }
    cve_str = Array.join(cve_ids, ', ');
  }

  // CVSS v2
  let cvss_base = '0.0';
  let cvss_temporal = '';
  let cvss_vector = '';
  if (vuln.CVSS) {
    cvss_base = extractText(vuln.CVSS.BASE) || '0.0';
    cvss_temporal = extractText(vuln.CVSS.TEMPORAL) || '';
    cvss_vector = extractText(vuln.CVSS.VECTOR_STRING) || '';
  }

  // CVSS v3 (element name is CVSS_V3 per DTD)
  let cvss3_base = '0.0';
  let cvss3_temporal = '';
  let cvss3_vector = '';
  if (vuln.CVSS_V3) {
    cvss3_base = extractText(vuln.CVSS_V3.BASE) || '0.0';
    cvss3_temporal = extractText(vuln.CVSS_V3.TEMPORAL) || '';
    cvss3_vector = extractText(vuln.CVSS_V3.VECTOR_STRING) || '';
  }

  // See Also: vendor references and bugtraq URLs
  const see_also_urls = [];
  if (vuln.VENDOR_REFERENCE_LIST && vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE) {
    const refs = Array.isArray(vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE) ? vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE : [vuln.VENDOR_REFERENCE_LIST.VENDOR_REFERENCE];
    for (let j = 0; j < Array.length(refs); j++) {
      const url = extractText(refs[j]?.URL);
      if (url) Array.push(see_also_urls, url);
    }
  }
  if (vuln.BUGTRAQ_LIST && vuln.BUGTRAQ_LIST.BUGTRAQ) {
    const bugs = Array.isArray(vuln.BUGTRAQ_LIST.BUGTRAQ) ? vuln.BUGTRAQ_LIST.BUGTRAQ : [vuln.BUGTRAQ_LIST.BUGTRAQ];
    for (let j = 0; j < Array.length(bugs); j++) {
      const url = extractText(bugs[j]?.URL);
      if (url) Array.push(see_also_urls, url);
    }
  }

  const discovery_remote = vuln.DISCOVERY && extractText(vuln.DISCOVERY.REMOTE) === '1' ? 'true' : 'false';

  kb_map[qid_str] = {
    title:          extractText(vuln.TITLE),
    risk:           risk,
    severity:       severity_str,
    category:       extractText(vuln.CATEGORY),
    diagnosis:      extractText(vuln.DIAGNOSIS),
    consequence:    extractText(vuln.CONSEQUENCE) || '',
    solution:       extractText(vuln.SOLUTION) || 'N/A',
    cve:            cve_str,
    cvss:           cvss_base,
    cvss_temporal:  cvss_temporal,
    cvss_vector:    cvss_vector,
    cvss3:          cvss3_base,
    cvss3_temporal: cvss3_temporal,
    cvss3_vector:   cvss3_vector,
    see_also:       Array.join(see_also_urls, ', '),
    patchable:      extractText(vuln.PATCHABLE) === '1' ? 'true' : 'false',
    published:      extractText(vuln.PUBLISHED_DATETIME) || '',
    last_modified:  extractText(vuln.LAST_SERVICE_MODIFICATION_DATETIME) || '',
    discovery_remote:    discovery_remote
  };
}

// Join raw_detections with KB metadata to produce AttackForge-compatible records
const raw_detections = data?.raw_detections || [];
const records = [];
if (secrets.logging_level === 'debug') {
  Logger.debug('raw_detections count:', Array.length(data.raw_detections || []));
}
for (let i = 0; i < Array.length(raw_detections); i++) {
  const det = raw_detections[i];
  if (!det) continue;

  const qid = String.from(det.qid || '');
  const kb = kb_map[qid] || {};

  Array.push(records, {
    'QualysId':                            qid,
    'CVE':                                 kb.cve || '',
    'CVSS Base Score':                     kb.cvss || '0.0',
    'CVSS3 Base Score':                    kb.cvss3 || '0.0',
    'CVSS3 Vector':                        kb.cvss3_vector || '',
    'Risk':                                kb.risk || 'None',
    'Host':                                det.dns || det.ip || '',
    'IP Address':                          det.ip || '',
    'FQDN':                                det.dns || '',
    'Name':                                kb.title || 'QID ' + qid,
    'Synopsis':                            kb.diagnosis || '',
    'Description':                         kb.consequence || '',
    'Solution':                            kb.solution || 'N/A',
    'See Also':                            kb.see_also || '',
    'Result':                              det.result || '',
    'Vulnerability State':                 'Active',
    'Category':                            kb.category || '',
    'Vulnerability Priority Rating (VPR)': 'null',
    'Exploit Available':                   'false',
    'Patch Available':                     kb.patchable || 'false',
    'Remote':                              kb.discovery_remote
  });
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Built ' + Array.length(records) + ' enriched record(s).');
}

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

return {
  decision: {
    status: 'continue',
    message: 'Built ' + Array.length(records) + ' enriched record(s). Proceeding to import trigger.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    records: records
  }
};

function extractText(node){
  if (!node) return '';
  if (node['#text'] !== undefined) return String.from(node['#text']);
  if (typeof node === 'string') return node;
  if (typeof node === 'number') return String.from(node);
  if (Array.isArray(node) && Array.length(node) > 0) {
    const node_obj = node[0];
    if (node_obj && node_obj['#text'] !== undefined) return String.from(node_obj['#text']);
    if (typeof node_obj === 'string') return node_obj;
  }
  return '';
}

// XML.parse gives each VULN as an array of {fieldname: [value]} objects — flatten to a plain object
function flattenVuln(arr) {
  const obj = {};
  for (let i = 0; i < Array.length(arr); i++) {
    const k = Object.keys(arr[i])[0];
    obj[k] = arr[i][k];
  }
  return obj;
}
```

**Action 6 - Fetch Knowledgebase QVS**

* **Method**: GET
* **URL**: https\://{{qualys\_api\_url}}/api/2.0/fo/knowledge\_base/qvs/?action=list\&details=All\&cve={{cves\_param}}
* **Headers**:
  * Key = X-Requested-With; Type = Value; Value = HTTP
* **Request Script**:

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id || !data?.project_name || !data?.scan_ref) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('data: ', JSON.stringify(data));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: project_id, project_name, scan_ref must all be present in data. Please check the log.'
    }
  };
}

const records = data.records || [];

if (Array.length(records) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No records to enrich. Skipping QVS lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: records
    }
  };
}

// Collect unique CVEs across all records
const cve_seen = {};
const unique_cves = [];
for (let i = 0; i < Array.length(records); i++) {
  const cve_field = records[i]['CVE'] || '';
  if (!cve_field) continue;
  const parts = String.split(cve_field, ',');
  for (let j = 0; j < Array.length(parts); j++) {
    const cve = String.trim(parts[j]);
    if (cve && !cve_seen[cve]) {
      cve_seen[cve] = true;
      Array.push(unique_cves, cve);
    }
  }
}

if (Array.length(unique_cves) === 0) {
  return {
    decision: {
      status: 'next',
      message: 'No CVEs found in records. Skipping QVS lookup.'
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      records: records
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('Unique CVEs for QVS lookup: ', Array.join(unique_cves, ', '));
}

const auth_header = 'Basic ' + String.encode(secrets.qualys_auth, 'base64');
const cves_param = Array.join(unique_cves, ',');

return {
  decision: {
    status: 'continue',
    message: 'Fetching QVS scores for ' + Array.length(unique_cves) + ' unique CVE(s).'
  },
  request: {
    url: 'https://' + secrets.qualys_api_url + '/api/2.0/fo/knowledge_base/qvs/?action=list&details=All&cve=' + cves_param,
    headers: {
      Authorization: auth_header
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    records: records
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('QVS fetch failed: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'continue',
      message: 'QVS fetch failed (HTTP ' + (response?.statusCode || 'unknown') + '). Continuing without QVS enrichment.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      records: data?.records || [],
    }
  };
}

const body = response?.body;
const qvs_data = body ? JSON.parse(body) : response?.jsonBody;

if (!qvs_data) {
  return {
    decision: {
      status: 'continue',
      message: 'Empty QVS response. Continuing without QVS enrichment.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      records: data?.records || []
    }
  };
}

if (secrets.logging_level === 'debug') {
  Logger.debug('QVS response: ', JSON.stringify(qvs_data));
}

const enriched_records = data?.records || [];

for (let i = 0; i < Array.length(enriched_records); i++) {
  const rec = enriched_records[i];
  if (!rec || !rec['CVE']) continue;

  const cves = String.split(rec['CVE'], ',');
  let max_qvs = 0;
  let max_epss = 0;
  let has_exploit = false;
  let has_malware = false;
  let is_cisa_kev = false;
  let max_cvss3 = 0;
  let best_cvss3_score = '';
  let best_cvss3_vector = '';
  let max_cvss2 = 0;
  let best_cvss2_score = '';
  let max_cvss4 = 0;
  let best_cvss4_score = '';
  let best_cvss4_vector = '';

  const maturity_seen = {};
  const maturity_list = [];
  const malware_seen = {};
  const malware_list = [];
  const actor_seen = {};
  const actor_list = [];

  for (let j = 0; j < Array.length(cves); j++) {
    const cve = String.trim(cves[j]);
    if (!cve || !qvs_data[cve]) continue;

    const entry = qvs_data[cve];

    const qvs_score = Number.parseFloat(entry?.base?.qvs || '0');
    if (qvs_score > max_qvs) max_qvs = qvs_score;

    const cvss_str = entry?.contributingFactors?.cvss || '';
    const cvss_version = entry?.contributingFactors?.cvssVersion || '';
    const cvss_vector = entry?.contributingFactors?.cvssString || '';
    if (cvss_str) {
      const cvss_val = Number.parseFloat(cvss_str);
      if (String.includes(cvss_version, 'v4') && cvss_val > max_cvss4) {
        max_cvss4 = cvss_val;
        best_cvss4_score = cvss_str;
        best_cvss4_vector = cvss_vector;
      } 
      else if (String.includes(cvss_version, 'v3') && cvss_val > max_cvss3) {
        max_cvss3 = cvss_val;
        best_cvss3_score = cvss_str;
        best_cvss3_vector = cvss_vector;
      } 
      else if (String.includes(cvss_version, 'v2') && cvss_val > max_cvss2) {
        max_cvss2 = cvss_val;
        best_cvss2_score = cvss_str;
      }
    }

    const epss_arr = entry?.contributingFactors?.epss || [];
    if (Array.length(epss_arr) > 0) {
      const epss_val = Number.parseFloat(epss_arr[0] || '0');
      if (epss_val > max_epss) max_epss = epss_val;
    }

    const maturity_raw = entry?.contributingFactors?.exploitMaturity || [];
    const maturity_parts = String.split(Array.join(maturity_raw, ','), ',');
    for (let k = 0; k < Array.length(maturity_parts); k++) {
      const m = String.trim(maturity_parts[k]);
      if (!m) continue;
      if (!maturity_seen[m]) { maturity_seen[m] = true; Array.push(maturity_list, m); }
      if (m === 'weaponized') has_exploit = true;
    }

    const malware_raw = entry?.contributingFactors?.malwareName || [];
    const malware_parts = String.split(Array.join(malware_raw, ','), ',');
    for (let k = 0; k < Array.length(malware_parts); k++) {
      const m = String.trim(malware_parts[k]);
      if (!m) continue;
      has_malware = true;
      if (!malware_seen[m]) { malware_seen[m] = true; Array.push(malware_list, m); }
    }

    const cisa_arr = entry?.contributingFactors?.cisaVuln || [];
    if (Array.includes(cisa_arr, 'YES')) is_cisa_kev = true;

    const actor_raw = entry?.contributingFactors?.threatActors || [];
    const actor_parts = String.split(Array.join(actor_raw, ','), ',');
    for (let k = 0; k < Array.length(actor_parts); k++) {
      const a = String.trim(actor_parts[k]);
      if (!a) continue;
      if (!actor_seen[a]) { actor_seen[a] = true; Array.push(actor_list, a); }
    }
  }

  if (max_qvs > 0) {
    const scaled = Math.round(max_qvs / 10);
    rec['Vulnerability Priority Rating (VPR)'] = String.from(scaled < 1 ? 1 : scaled);
  }

  if (best_cvss3_score && (rec['CVSS3 Base Score'] === '0.0' || !rec['CVSS3 Base Score'])) {
    rec['CVSS3 Base Score'] = best_cvss3_score;
  }
  if (best_cvss3_vector && !rec['CVSS3 Vector']) {
    rec['CVSS3 Vector'] = best_cvss3_vector;
  }
  if (best_cvss2_score && (rec['CVSS Base Score'] === '0.0' || !rec['CVSS Base Score'])) {
    rec['CVSS Base Score'] = best_cvss2_score;
  }
  if (best_cvss4_score) {
    rec['CVSS4 Base Score'] = best_cvss4_score;
  }
  if (best_cvss4_vector) {
    rec['CVSS4 Vector'] = best_cvss4_vector;
  }

  if (has_exploit) rec['Exploit Available'] = 'true';
  if (has_malware) rec['Exploited by Malware'] = 'true';
  if (is_cisa_kev) rec['In The News'] = 'true';

  const intel_lines = [];
  if (max_epss > 0) {
    Array.push(intel_lines, 'EPSS Score: ' + String.from(max_epss) + ' (' + String.from(Math.round(max_epss * 100)) + '% exploitation probability)');
  }
  if (is_cisa_kev) {
    Array.push(intel_lines, 'CISA Known Exploited Vulnerability: YES');
  }
  if (Array.length(maturity_list) > 0) {
    Array.push(intel_lines, 'Exploit Maturity: ' + Array.join(maturity_list, ', '));
  }
  if (Array.length(malware_list) > 0) {
    Array.push(intel_lines, 'Associated Malware: ' + Array.join(malware_list, ', '));
  }
  if (Array.length(actor_list) > 0) {
    Array.push(intel_lines, 'Threat Actors: ' + Array.join(actor_list, ', '));
  }

  if (Array.length(intel_lines) > 0) {
    const existing_output = rec['Plugin Output'] || '';
    rec['Plugin Output'] = existing_output + (existing_output ? '\n\n' : '') + '--- Qualys Threat Intelligence ---\n' + Array.join(intel_lines, '\n');
  }
}

if (secrets.logging_level === 'debug') {
  Logger.debug('QVS enrichment complete for ' + Array.length(enriched_records) + ' record(s).');
}

return {
  decision: {
    status: 'continue',
    message: 'QVS enrichment complete. Proceeding to import trigger.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    records: enriched_records
  }
};
```

**Action 7 - Trigger Import Vulnerabilities Flow**

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

```javascript
if (data?.flow_status) {
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (data.status !== 'FINISHED') {
  return {
    decision: {
      status: 'next',
      message: 'Status ' + data.status + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status
    }
  };
}

if (!data?.records || Array.length(data.records) === 0) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('No records found for project: ', data?.project_id);
  }
  return {
    decision: {
      status: 'continue',
      message: 'No records to import. Proceeding to add note and clear active scan.'
    },
    data: {
      project_id:  data?.project_id,
      project_name: data?.project_name,
      scan_ref:    data?.scan_ref,
      status:      data.status,
      no_records:  "true"
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Triggering import of ' + Array.length(data.records) + ' record(s) for project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/flows/' + secrets.qualys_import_vuln_trigger_flow_id,
    body: {
      project_id: data.project_id,
      records: data.records
    }
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 202) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error triggering import flow: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error triggering import flow. HTTP status: ' + (response?.statusCode || 'unknown') + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Trigger Import Vulnerabilities Flow - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error triggering import flow.'
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully triggered import flow.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status
  }
};
```

**Action 8 - No Findings - Add Note to Project**

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

```javascript
if (data?.flow_status && data.flow_status?.status !== 'NOVULNSFOUND') {
  return {
    decision: {
      status: 'next',
      message: 'Error status detected. Skipping no-findings note.'
    },
    data: data
  };
}

// Only add a note when step 6 set no_records (records array was empty after a successful scan).
if ((!data?.no_records || data.no_records !== 'true') && data?.flow_status?.status !== 'NOVULNSFOUND') {
  return {
    decision: {
      status: 'continue',
      message: 'Findings were imported. Skipping no-findings note.'
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      status:       data.status
    }
  };
}

if (!data?.project_id) {
  const err_msg = 'Error: project_id is missing in no-findings path. Please check the log.';
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id:   data.project_id,
      project_name: data.project_name,
      scan_ref:     data.scan_ref,
      flow_status: {
        step:    'No Findings - Add Note to Project - Request Script',
        status:  'VALIDATION_ERROR',
        message: err_msg
      }
    }
  };
}

const now = Date.format(Date.datetime('now'), 'default');

return {
  decision: {
    status: 'continue',
    message: 'Adding no-findings note to project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id + '/note',
    body: {
      note: 'No vulnerabilities found by Qualys VM Scan.\nScan Reference: ' + data.scan_ref + '\nDate: ' + now,
      is_private: false,
      is_exported_to_report: false
    }
  },
  data: {
    project_id:   data.project_id,
    project_name: data.project_name,
    scan_ref:     data.scan_ref,
    status:       data.status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  const err_msg = 'HTTP error creating note on project. HTTP Status: ' + response?.statusCode;
  if (secrets.logging_level === 'debug') {
    Logger.debug(err_msg);
  }
  return {
    decision: {
      status: 'next',
      message: err_msg
    },
    data: {
      project_id:   data?.project_id,
      project_name: data?.project_name,
      scan_ref:     data?.scan_ref,
      status:       data?.status,
      flow_status: {
        step:    'No Findings - Add Note to Project - Response Script',
        status:  'HTTP_ERROR',
        message: err_msg
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully added no-findings note to project: ' + data?.project_name + '.'
  },
  data: {
    project_id:   data?.project_id,
    project_name: data?.project_name,
    scan_ref:     data?.scan_ref,
    status:       data?.status
  }
};
```

**Action 9 - Clear Active Scan**

* **Method**: PUT
* **URL**: https\://{{af\_hostname}}/api/ss/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?.flow_status) {
  if (secrets.logging_level === 'debug') {
    Logger.debug(JSON.stringify(data));
  }
  return {
    decision: {
      status: 'next',
      message: 'Found flow_status from step: ' + data.flow_status.step + '. Proceeding to email report action.'
    },
    data: data
  };
}

if (!data?.project_id) {
  return {
    decision: {
      status: 'abort',
      message: 'Error: data.project_id is missing. Please check the log.'
    }
  };
}

const clear_status = ['FINISHED', 'NOVULNSFOUND', 'CANCELED', 'NOHOSTALIVE'];

// INTERRUPTED and PAUSED to be captured here
if (!Array.includes(clear_status, data.status)){
  return {
    decision: {
      status: 'next',
      message: 'Status ' + (data.status || 'unknown') + ' detected. Skipping to report.'
    },
    data: {
      project_id: data.project_id,
      scan_ref: data.scan_ref,
      project_name: data.project_name,
      status: data.status,
      flow_status: data.flow_status
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Clearing qualys_vm_active_scan from project: ' + data.project_id + '.'
  },
  request: {
    url: 'https://' + secrets.af_hostname + '/api/ss/project/' + data.project_id,
    body: {
      custom_fields: [
        {
          key: 'qualys_vm_active_scan',
          value: null
        }
      ]
    }
  },
  data: {
    project_id: data.project_id,
    project_name: data.project_name,
    scan_ref: data.scan_ref,
    status: data.status,
    flow_status: data.flow_status
  }
};
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('Error clearing active scan field: ', JSON.stringify(response));
  }
  return {
    decision: {
      status: 'next',
      message: 'Error clearing qualys_vm_active_scan from project: ' + data?.project_id + '. Proceeding to email report action.'
    },
    data: {
      project_id: data?.project_id,
      project_name: data?.project_name,
      scan_ref: data?.scan_ref,
      status: data?.status,
      flow_status: {
        step: 'Clear Active Scan - Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error clearing qualys_vm_active_scan from project: ' + (data?.project_id || 'unknown') + '.'
      }
    }
  };
}

return {
  decision: {
    status: 'continue',
    message: 'Successfully cleared qualys_vm_active_scan from project: ' + data?.project_id + '.'
  },
  data: {
    project_id: data?.project_id,
    project_name: data?.project_name,
    scan_ref: data?.scan_ref,
    status: data?.status,
    flow_status: data?.flow_status
  }
};
```

**Action 10 - Send Report Email**

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

```javascript
if (data?.flow_status) {
  if (data.project_name && data.scan_ref && data.project_id) {
    const curr_date = Date.datetime('now');
    const curr_year = Date.format(curr_date, 'yyyy');
    const current_display_date = Date.format(curr_date, 'default');
    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,iVBORw0KGgoAAAANSUhEUgAAAlgAAABnCAYAAADc1TyNAAAEDmlDQ1BrQ0dDb2xvclNwYWNlR2VuZXJpY1JHQgAAOI2NVV1oHFUUPpu5syskzoPUpqaSDv41lLRsUtGE2uj+ZbNt3CyTbLRBkMns3Z1pJjPj/KRpKT4UQRDBqOCT4P9bwSchaqvtiy2itFCiBIMo+ND6R6HSFwnruTOzu5O4a73L3PnmnO9+595z7t4LkLgsW5beJQIsGq4t5dPis8fmxMQ6dMF90A190C0rjpUqlSYBG+PCv9rt7yDG3tf2t/f/Z+uuUEcBiN2F2Kw4yiLiZQD+FcWyXYAEQfvICddi+AnEO2ycIOISw7UAVxieD/Cyz5mRMohfRSwoqoz+xNuIB+cj9loEB3Pw2448NaitKSLLRck2q5pOI9O9g/t/tkXda8Tbg0+PszB9FN8DuPaXKnKW4YcQn1Xk3HSIry5ps8UQ/2W5aQnxIwBdu7yFcgrxPsRjVXu8HOh0qao30cArp9SZZxDfg3h1wTzKxu5E/LUxX5wKdX5SnAzmDx4A4OIqLbB69yMesE1pKojLjVdoNsfyiPi45hZmAn3uLWdpOtfQOaVmikEs7ovj8hFWpz7EV6mel0L9Xy23FMYlPYZenAx0yDB1/PX6dledmQjikjkXCxqMJS9WtfFCyH9XtSekEF+2dH+P4tzITduTygGfv58a5VCTH5PtXD7EFZiNyUDBhHnsFTBgE0SQIA9pfFtgo6cKGuhooeilaKH41eDs38Ip+f4At1Rq/sjr6NEwQqb/I/DQqsLvaFUjvAx+eWirddAJZnAj1DFJL0mSg/gcIpPkMBkhoyCSJ8lTZIxk0TpKDjXHliJzZPO50dR5ASNSnzeLvIvod0HG/mdkmOC0z8VKnzcQ2M/Yz2vKldduXjp9bleLu0ZWn7vWc+l0JGcaai10yNrUnXLP/8Jf59ewX+c3Wgz+B34Df+vbVrc16zTMVgp9um9bxEfzPU5kPqUtVWxhs6OiWTVW+gIfywB9uXi7CGcGW/zk98k/kmvJ95IfJn/j3uQ+4c5zn3Kfcd+AyF3gLnJfcl9xH3OfR2rUee80a+6vo7EK5mmXUdyfQlrYLTwoZIU9wsPCZEtP6BWGhAlhL3p2N6sTjRdduwbHsG9kq32sgBepc+xurLPW4T9URpYGJ3ym4+8zA05u44QjST8ZIoVtu3qE7fWmdn5LPdqvgcZz8Ww8BWJ8X3w0PhQ/wnCDGd+LvlHs8dRy6bLLDuKMaZ20tZrqisPJ5ONiCq8yKhYM5cCgKOu66Lsc0aYOtZdo5QCwezI4wm9J/v0X23mlZXOfBjj8Jzv3WrY5D+CsA9D7aMs2gGfjve8ArD6mePZSeCfEYt8CONWDw8FXTxrPqx/r9Vt4biXeANh8vV7/+/16ffMD1N8AuKD/A/8leAvFY9bLAAAAOGVYSWZNTQAqAAAACAABh2kABAAAAAEAAAAaAAAAAAACoAIABAAAAAEAAAJYoAMABAAAAAEAAABnAAAAAOLtLv4AADQ/SURBVHgB7Z0HvBXF9cfvAykC9oqiYgNjj778Y9eoqAkW1GgUC2JDjGKsMcbYS2woYAmKLfZeExVr7CViF0GsKFZEEIX7//6e78ll3+zu7N699Z3z+fze3p05c+bMb2d3z8zO7tvlanjy+XwXsE4SF9GfH6yWpIzpGgPGgDFgDBgDxoAxUCcMkDQNfB7cnKTB6PcCX4ELQc8kZU3XGDAGjAFjwBgwBoyBhmSAoGgVcCOYCSRXJmko+j3BFBVEPgKHg65JbJiuMWAMGAPGgDFgDBgDdcUAgU9ncBAYD1xyVpLGYUBrsD5xGSLtTrBWEpumawwYA8aAMWAMGAPGQF0xQLCzLXgaRMkxSRqFIS2Mfze6wfelaf3WEknsmq4xYAwYA8aAMWAMGAM1zQBByjbgaeCRfZM0BoNNQIvj4+RjFI4Atj4rCcGmawyUIwMEOGyfIIMdAAAASUVORK5CYII=' 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>";

    const content = '<p><strong>Attention: Qualys Scan Poll and Import Flow has detected an error.</strong></p>'
    + '<p>Please check the AttackForge Flow log and Qualys scan log for details.</p>'
    + '<br>'
    + '<table class="styled-table" align="center">'
    + '<tr><th width="200">Field</th><th width="200">Value</th></tr>'
    + '<tr><td> Scan Reference </td><td>' + data.scan_ref + '</td></tr>'
    + '<tr><td> Step </td><td>' + data.flow_status.step + '</td></tr>'
    + '<tr><td> Status </td><td>' + data.flow_status.status + '</td></tr>'
    + '<tr><td> Message </td><td>' + data.flow_status.message + '</td></tr>'
    + '<tr><td> Time </td><td>' + current_display_date + '</td></tr>'
    + '</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>'
    + '<table class="styled-button" align="center"><tr><td style="padding: 15px 30px;"><a href="https://'+secrets.qualys_web_base_url+'/fo/scan/scanList.php">View Qualys Scan Board</a></td></tr></table>'
    + '<br>';

    return {
      decision: {
        status: 'continue',
        message: 'Sending error report email.',
      },
      request: {
        url: 'https://' + secrets.af_hostname + '/api/ss/email',
        body: {
          to: [secrets.admin_email],
          subject: '[Flow Notification] Non successful Scan reference discovered from Qualys VM Scanning',
          html: emailHeader + content + emailFooter
        },
      }
    };
  } 
  else {
    return {
      decision: {
        status: 'abort',
        message: 'Error: project_name, scan_ref, project_id must all be present in data. Please check the log.'
      }
    };
  }
} 
else {
  return {
    decision: {
      status: 'finish',
      message: 'No flow_status found. Current scan status: ' + (data?.status || 'unknown') + '. Exiting the flow.'
    }
  };
}
```

* **Response Script**:

```javascript
if (response?.statusCode !== 200){
  return {
    decision: {
      status: 'abort',
      message: 'Error sending error email',
    },
    data: {
      project_id: data.project_id,
      project_name: data.project_name,
      scan_ref: data.scan_ref,
      status: data.status,
      flow_status: {
        step: 'Send Report Email- Response Script',
        status: 'HTTP_ERROR',
        message: 'HTTP ' + (response?.statusCode || 'unknown') + ' error sending report email to: ' + (secrets.admin_email || 'unknown') + '.'
      }
    },
  };
}

return {
  decision: {
    status: 'finish',
    message: 'Successfully sent email to admin. Reached end of the flow.',
  }
};
```

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

<figure><img src="/files/1WDSPS3LCdX4CtZ0Vu7E" alt=""><figcaption></figcaption></figure>

The purpose of this example is to import vulnerabilities from a Qualys 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
  * import\_to\_library - optional AttackForge Writeup library. Must be either "Imported Vulnerabilities", "Main Vulnerabilities", "Project Vulnerabilities" or key for a custom library (if custom libraries are used).
  * logging\_level - set to "debug" for additional logging

**Action 1 - Validate Transform Group**

* **Script**:

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

// Detect source: Qualys VM sends records[], Qualys WAS sends final_result[]
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) {
  if (secrets.logging_level === 'debug') {
    Logger.debug('jsonBody: ', JSON.stringify(data.jsonBody));
  }
  return {
    decision: {
      status: 'abort',
      message: 'Error: Unable to detect source format. Expected data.jsonBody.records (VM) or data.jsonBody.final_result (WAS). Please check the log.'
    }
  };
}

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

// Normalize 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 scan wrapper { scan: {...}, findings: [...] }
  for (let r = 0; r < Array.length(data.jsonBody.final_result); r++) {
    const raw = data.jsonBody.final_result[r];
    const 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: 'finish',
      message: 'No ' + sourceType + ' items found in input. Nothing to import.'
    }
  };
}

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

// Group by qid
const groupKeys  = [];
const groupVulns = [];

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

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

  let idx = -1;
  for (let g = 0; g < Array.length(groupKeys); g++) {
    if (groupKeys[g] === qid) {
      idx = g;
      break;
    }
  }

  if (idx === -1) {
    // New group — build base vuln
    const vuln = item.type === 'VM'
      ? buildVMVuln(item.record, qid, priority, assetName)
      : buildWASVuln(item.finding, item.scanId, item.target, item.scanStartedAt, qid, priority, assetName);

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

    if (note) {
      vuln.steps_to_reproduce = vuln.steps_to_reproduce
        ? vuln.steps_to_reproduce + '\n' + note.note
        : note.note;
    }

    Array.push(groupKeys, qid);
    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);

    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,
    project_id:      data.jsonBody.project_id,
    source_type:     sourceType
  }
};

// ── VM builder ──

function buildVMVuln(record, qid, priority, assetName) {
  const title = record['Name'] || 'QID ' + qid;

  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: 'qualys_ID',        value: qid },
    { key: 'qualys_scan_type',  value: 'VM' },
    { key: 'category',     value: record['Category']  || '' },
    { key: 'exploit_available', value: record['Exploit Available'] || 'false' },
    { key: 'patch_available',   value: record['Patch Available']   || 'false' },
    { key: 'remote', value: record['Remote'] || 'false'}
  ];

  if (record['CVSS4 Vector'] && String.length(record['CVSS4 Vector']) > 0) {
    Array.push(tags, record['CVSS4 Vector']);
  } 
  else if (record['CVSS3 Vector'] && String.length(record['CVSS3 Vector']) > 0) {
    Array.push(tags, record['CVSS3 Vector']);
  }

  const cvss4 = Number.parseFloat(record['CVSS4 Base Score'] || '0');
  const cvss3 = Number.parseFloat(record['CVSS3 Base Score'] || '0');
  const cvss2 = Number.parseFloat(record['CVSS Base Score']  || '0');
  const bestCvss = cvss4 > 0 ? cvss4 : cvss3 > 0 ? cvss3 : cvss2;
  let likelihood = 1;
  if (bestCvss > 0) {
    likelihood = Math.round(bestCvss);
    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,
    cvssv4_vector:              record['CVSS4 Vector'] || '',
    is_zeroday:                 is_zeroday,
    import_source:              'Qualys VM',
    import_source_id:           'VM-' + qid,
    is_visible:                 true
  };

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

function buildVMNote(record, assetName) {
  if (record['Result'] && String.length(record['Result']) > 0) {
    let prefix = '[' + assetName;
    if (record['Port'] && record['Port'] !== '0' && String.length(record['Port']) > 0) {
      prefix = prefix + ':' + record['Port'];
      if (record['Protocol'] && String.length(record['Protocol']) > 0) {
        prefix = prefix + '/' + record['Protocol'];
      }
    }
    prefix = prefix + ']';
    return {
      note: prefix + '\n' + record['Result'],
      type: 'PLAINTEXT'
    };
  }
  return null;
}

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' && String.length(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');
}

// ── WAS builder ──
//   Expected finding fields from Qualys WAS poll flow:
//   qid, name, risk_factor (critical/high/medium/low/info), description, solution,
//   cves (string[]), cvss (float), cvss_vector, cvss3 (float), cvss3_vector,
//   uri, evidence, proof, request_headers, response_headers, payload,
//   input_name, input_type, category, cwe (int[]),
//   owasp ([{ year, category }])

function buildWASVuln(finding, scanId, target, scanStartedAt, qid, priority, assetName) {
  const title = finding.name || 'QID ' + qid;

  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: 'qualys_ID',        value: qid },
    { key: 'qualys_finding_id', value: String.from(finding.finding_id || '') },
    { key: 'qualys_scan_type',  value: 'WAS' },
    { key: 'scan_id',           value: scanId },
    { key: 'target_url',        value: target },
    { key: 'category',     value: finding.category || '' }
  ];

  if (finding.cvss3 !== null && finding.cvss3 !== undefined) {
    Array.push(custom_fields, { key: 'cvss3_base_score', value: finding.cvss3 + '' });
  }
  if (finding.cvss3_vector && String.length(finding.cvss3_vector) > 0) {
    Array.push(custom_fields, { key: 'cvss3_vector', value: finding.cvss3_vector });
  }
  if (finding.cvss !== null && finding.cvss !== undefined) {
    Array.push(custom_fields, { key: 'cvss2_base_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.cwe && Array.isArray(finding.cwe) && Array.length(finding.cwe) > 0) {
    Array.push(custom_fields, { key: 'cwe', value: Array.join(finding.cwe, ', ') });
  }
  if (finding.owasp && Array.isArray(finding.owasp) && Array.length(finding.owasp) > 0) {
    const owaspLabels = [];
    for (let j = 0; j < Array.length(finding.owasp); j++) {
      const o = finding.owasp[j];
      if (o && o.year && o.category) {
        Array.push(owaspLabels, o.year + '-' + o.category);
      }
    }
    if (Array.length(owaspLabels) > 0) {
      Array.push(custom_fields, { key: 'owasp', value: Array.join(owaspLabels, ', ') });
    }
  }
  if (finding.wasc && String.length(finding.wasc) > 0) {
    Array.push(custom_fields, { key: 'wasc', value: finding.wasc });
  }
  if (finding.group && String.length(finding.group) > 0) {
    Array.push(custom_fields, { key: 'qualys_group', value: finding.group });
  }
  if (finding.last_detected_at && String.length(finding.last_detected_at) > 0) {
    Array.push(custom_fields, { key: 'last_detected_at', value: finding.last_detected_at });
  }

  // Likelihood from CVSS3 > CVSS2 > default 1
  const cvssScore = finding.cvss3 !== null && finding.cvss3 !== undefined ? finding.cvss3
                  : finding.cvss  !== null && finding.cvss  !== undefined ? finding.cvss
                  : null;
  let likelihood = 1;
  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:            finding.consequence || '',
    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:              'Qualys WAS',
    import_source_id:           'WAS-' + qid,
    created:                    scanStartedAt,
    is_visible:                 true
  };

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

function buildWASNote(finding) {
  const parts = [];
  if (finding.uri && String.length(finding.uri) > 0) {
    Array.push(parts, '[' + finding.uri + ']');
  }
  if (finding.evidence && String.length(finding.evidence) > 0) {
    Array.push(parts, finding.evidence);
  }
  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;
}

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';
  if (risk === 'High')     return 'High';
  if (risk === 'Medium')   return 'Medium';
  if (risk === 'Low')      return 'Low';
  return 'Info';
}

function mapPriorityWAS(risk_factor) {
  if (risk_factor === 'critical') return 'Critical';
  if (risk_factor === 'high')     return 'High';
  if (risk_factor === 'medium')   return 'Medium';
  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 || 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 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. HTTP status: ' + (response?.statusCode || 'unknown') + '. Imported so far: ' + (data?.imported_count || 0) + '/' + (data?.total_count || 0) + '. Please check the log.'
    }
  };
}
if (data?.vulns_to_import) {
  Array.splice(data.vulns_to_import, 0, 1);
}
if (data?.imported_count !== undefined) {
  data.imported_count = (data.imported_count || 0) + 1;
}

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

if (data?.vulns_to_import !== undefined && 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 ' + data?.source_type + ' vulnerabilities imported successfully. Total: ' + data?.imported_count + '.'
  }
};
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## 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, and the optional `goal` query parameter:

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

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

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.
