Skip to content
Merged
113 changes: 84 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,100 @@
# Polarity Qualys Integration
The Polarity Qualys Integration queries the Qualys Cloud Platform's Host Detection List for IP Addresses and QIDs. The Host Detections list will only get queried when searching for IP Addresses and QIDs, the host detection API does not enable other searching at this time. T

> *NOTE:* QIDs can be searched onDemand by prefixing the QID with `QID: <your_qid>`

### How to Review Polarity - Qualys Integration
***Host Detections***
* **Summary View** -> Number of Host Dections associated with QID or IP Address
* **Detail View**
* *Host Information*
* Asset ID -> Asset ID from Qualys
* Operating System -> Host OS
* DNS -> Host DNS/Domain
* Last Scan Information
* *Detections List*
* List of all detections associated with Host

<div style="display:flex; align-items: flex-start;">
<img width="370" style="margin-right: 7px" title="Host List Detections" alt="Host List Detections" src="./assets/Host List Detections.png">
</div>

## About Qualys
The Qualys Cloud Platform helps businesses simplify security operations and lower the cost of compliance by delivering critical security intelligence on demand and automating the full spectrum of auditing, compliance and protection for IT systems and web applications.

The Polarity Qualys Integration queries the Qualys Cloud Platform for IP addresses, domains, CVEs, and QIDs. Host detection results are returned for IP addresses and QIDs. CVE lookups query the Qualys KnowledgeBase and return matching vulnerability records along with any associated host detections.

## About Qualys

The Qualys Cloud Platform helps businesses simplify security operations and lower the cost of compliance by delivering critical security intelligence on demand and automating the full spectrum of auditing, compliance, and protection for IT systems and web applications.

To learn more about Qualys, visit the [official website](https://www.qualys.com/).

## Supported Entity Types

| Entity Type | Description |
|---|---|
| IPv4 | Returns host detection list and scan history for the IP address |
| IPv6 | Returns host detection list for the IPv6 address |
| Domain | Returns host detection list for hosts matching the domain name |
| CVE | Returns KnowledgeBase vulnerability records matching the CVE and associated host detections |
| Qualys ID (QID) | Returns KnowledgeBase records and host detections for the matched QID |
| Custom QID Value | User-configured type for matching arbitrary strings and extracting a QID value |

## QID Entity Type

The built-in **Qualys ID (QID)** data type automatically recognizes QID strings in the following formats (case-insensitive):

| Format | Example |
|---|---|
| `QID<number>` (no separator) | `QID12345` |
| `QID:<number>` | `QID:12345` |
| `QID: <number>` | `QID: 12345` |
| `QID : <number>` | `QID : 12345` |
| `QID-<number>` | `QID-12345` |
| `QID_<number>` | `QID_12345` |
| `QID <number>` | `QID 12345` |
| `qid:<number>` (lowercase) | `qid:38623` |

The numeric QID value is extracted automatically and used for the API lookup.

## Custom QID Value Data Type

The **Custom QID Value** data type is disabled by default. It is designed for environments where QID values appear embedded in custom string formats not covered by the built-in QID pattern (e.g., internal ticket references, asset tags, or CMDB identifiers).

When enabled, Polarity will match text against the regex you configure in the **Custom QID Value Regex** option. The integration then extracts a numeric QID from the matched string and looks it up in Qualys.

**To enable it:**
1. Go to **Integration Options → Qualys → Data Types**
2. Enable the **Custom QID Value** type and enter a regex pattern that matches your custom format
3. Optionally set the **Custom QID Value Regex** option to extract the QID number (see below)

## Integration Limitations

### Host Detection List Lookup Limits
Qualys' Host Detection List API only allows lookups on IP Addresses and QIDs, so only IP Addresses and QIDs will show Host Detection List results.
Qualys' Host Detection List API filters results by the query parameter. When searching by QID, only the detection entry for that specific QID is returned per host — not the host's full detection list. Searching by IP address returns all detections for that host.

## Integration Options

All options are admin-only and cannot be edited by regular users.

## Qualys Integration User Options
### Qualys URL
The URL of the Qualys you would like to connect to (including http:// or https://)
*(Required)* The base URL of your Qualys subscription, including the protocol (e.g., `https://qualysapi.qualys.com`). Do not include a trailing slash.

### Qualys Username
The Username for your Qualys Account
*(Required)* The username for your Qualys account.

### Qualys Password
The Password associated with the Qualys Account
*(Required)* The password associated with your Qualys account.

### Enable Scan Launch
*(Default: disabled)* When enabled, a **Launch Scan** button appears in the Scans tab for IP address entities, allowing analysts to initiate a Qualys VM scan directly from Polarity. Requires the **Scan Option Profile** to be configured.

### Scan Option Profile
The Qualys option profile title or numeric ID to use when launching scans (e.g., `Initial Options` or `43165`). Required when **Enable Scan Launch** is enabled. You can find the Scan Option Profile by navigating to the "Scans" page and then click on the "Option Profiles" tab. The "Title" column is the name of the scan option profile. Do not include the word `(default)` if selecting the default Scan Option Profile.

### Scanner Appliance Name
The name of the scanner appliance to target when launching scans (e.g., `scanner1`). Leave blank to use the account's default scanner for the target IP.

### Custom QID Value Regex
When the **Custom QID Value** data type is enabled, this regex is used to extract the numeric QID from the matched entity string.

- If the regex contains a **capture group**, the first capture group's value is used as the QID (e.g., `TICKET-(\d+)` would extract `42` from `TICKET-42`).
- If the regex has **no capture group**, the full match is used as the QID.
- If left **blank**, the integration falls back to extracting the last contiguous sequence of digits found in the matched string (e.g., `ASSET-00038623` → `38623`).

**Examples:**

| Custom type regex (Data Types) | Custom QID Value Regex (option) | Matched string | Extracted QID |
|---|---|---|---|
| `TICKET-\d+` | `TICKET-(\d+)` | `TICKET-38623` | `38623` |
| `VULN#\d{4,8}` | *(blank)* | `VULN#12345` | `12345` |
| `asset-tag-\d+` | `(\d+)$` | `asset-tag-00091` | `00091` |

## Installation Instructions

Installation instructions for integrations are provided on the [PolarityIO GitHub Page](https://polarityio.github.io/).

## Polarity
Polarity is a memory-augmentation platform that improves and accelerates analyst decision making. For more information about the Polarity platform please see:
https://polarity.io/

Polarity is a memory-augmentation platform that improves and accelerates analyst decision making. For more information about the Polarity platform please see:

https://polarity.io/
52 changes: 33 additions & 19 deletions components/block.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,26 @@ polarity.export = PolarityComponent.extend({
this.set('scanState', '');
this.set('scanSubState', '');
this.set('scanDuration', '');
this.get('block').notifyPropertyChange('data');

this.sendIntegrationMessage({ action: 'LAUNCH_SCAN', entityValue }, (err, result) => {
this.set('isScanLaunching', false);
if (err) {
this.set('scanLaunchError', err.detail || 'Scan launch failed. Check Polarity logs.');
} else {
this.set('scanRef', (result && result.scanRef) || '');
}
this.get('block').notifyPropertyChange('data');
});
this.sendIntegrationMessage({ action: 'LAUNCH_SCAN', entityValue })
.then((result) => {
// Sanitize scanRef: extract just the scan/XXXXX.XXXXX portion to handle
// xml2js charkey collision that can prefix the value with garbage characters.
const rawScanRef = result.scanRef || '';
const scanRefMatch = rawScanRef.match(/scan\/\d+\.\d+/);
const scanRef = scanRefMatch ? scanRefMatch[0] : rawScanRef.trim();
this.set('scanRef', scanRef);
})
.catch((err) => {
this.set(
'scanLaunchError',
err.detail || 'Scan launch failed. Check Polarity logs.'
);
})
.finally(() => {
this.set('isScanLaunching', false);
this.get('block').notifyPropertyChange('data');
});
},

checkScanStatus: function () {
Expand All @@ -87,18 +96,23 @@ polarity.export = PolarityComponent.extend({
this.set('isCheckingStatus', true);
this.get('block').notifyPropertyChange('data');

this.sendIntegrationMessage({ action: 'CHECK_SCAN_STATUS', scanRef }, (err, result) => {
this.set('isCheckingStatus', false);
if (err) {
this.set('scanLaunchError', err.detail || 'Status check failed. Check Polarity logs.');
this.set('scanState', '');
} else {
this.sendIntegrationMessage({ action: 'CHECK_SCAN_STATUS', scanRef })
.then((result) => {
this.set('scanState', (result && result.state) || 'Unknown');
this.set('scanSubState', (result && result.subState) || '');
this.set('scanDuration', (result && result.duration) || '');
}
this.get('block').notifyPropertyChange('data');
});
})
.catch((err) => {
this.set(
'scanLaunchError',
err.detail || 'Status check failed. Check Polarity logs.'
);
this.set('scanState', '');
})
.finally(() => {
this.get('block').notifyPropertyChange('data');
this.set('isCheckingStatus', false);
});
},
toggleExpandableTitle: function (
displayFieldIndex,
Expand Down
33 changes: 27 additions & 6 deletions config/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,28 @@
"name": "Qualys",
"acronym": "QLS",
"description": "The Polarity Qualys Integration queries the Qualys Cloud Platform's Host Detection List and KnowledgeBase for IP Addresses, IPv6 Addresses, Domains, CVEs, and QIDs.",
"entityTypes": [
"dataTypes": [
"IPv4",
"IPv6",
"domain",
"cve"
],
"customTypes": [
"cve",
{
"type": "custom",
"name": "Qualys ID (QID)",
"description": "Matches QID values prefixed with QID followed by a 1 to 8 digit number (e.g., QID1234, QID-1234, QID:1234, QID 1234)",
"key": "qid",
"regex": "(?:QID|qid):\\s*\\d{1,8}"
"regex": "(?:QID|qid)(?:\\s*[:_-]\\s*|\\s*)\\d{1,8}",
"editable": false,
"enabled": true
},
{
"type": "custom",
"name": "Custom QID Value",
"description": "Match custom QID values and extract the numeric QID via the \"Custom QID Value Regex\" integration option",
"key": "customQid",
"regex": "(?:QID|qid)(?:\\s*[:_-]\\s*|\\s*)\\d{1,8}",
"editable": true,
"enabled": false
}
],
"defaultColor": "light-purple",
Expand Down Expand Up @@ -92,11 +104,20 @@
"type": "text",
"userCanEdit": false,
"adminOnly": true
},
{
"key": "customQidValueRegex",
"name": "Custom Type Value Regex",
"description": "When the Custom Type entity type is enabled, this regex is used to extract the QID numeric value from the matched entity string. Leave blank to extract the last sequence of digits found in the match.",
"default": "",
"type": "text",
"userCanEdit": false,
"adminOnly": true
}
],
"reducer": {
"details": {
"file": "./reducers/details.json"
}
}
}
}
13 changes: 11 additions & 2 deletions integration.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const createRequestWithDefaults = require('./src/createRequestWithDefaults');
const { validateStringOptions, validateUrlOption } = require('./src/validateOptions');
const {
validateStringOptions,
validateUrlOption,
validateCustomQidValueRegex
} = require('./src/validateOptions');
const { parseErrorToReadableJSON } = require('./src/dataTransformations');
const { getLookupResults } = require('./src/getLookupResults');
const launchScan = require('./src/launchScan');
Expand Down Expand Up @@ -52,7 +56,10 @@ const validateOptions = async (options, callback) => {

const urlValidationErrors = validateUrlOption(options.url.value);

const errors = stringValidationErrors.concat(urlValidationErrors);
const customTypeRegexErrors = validateCustomQidValueRegex(options);
const errors = stringValidationErrors
.concat(urlValidationErrors)
.concat(customTypeRegexErrors);

callback(null, errors);
};
Expand All @@ -68,6 +75,7 @@ const onMessage = async (payload, options, cb) => {
requestWithDefaults,
Logger
);
Logger.debug({ scanResult: result }, 'Scan Result');
return cb(null, result);
}

Expand All @@ -78,6 +86,7 @@ const onMessage = async (payload, options, cb) => {
requestWithDefaults,
Logger
);
Logger.debug({ scanStatus: result }, 'Scan Status');
return cb(null, result);
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "qualys",
"version": "3.3.3",
"version": "3.4.0",
"main": "./integration.js",
"private": true,
"license": "MIT",
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const QUERY_PATHS_BY_TYPE = {
}
};

QUERY_PATHS_BY_TYPE.customQid = { ...QUERY_PATHS_BY_TYPE.qid };

const SEARCH_COLUMN_NAMES_BY_TYPE = {
cve: ['title', 'category', 'diagnosis', 'solution', 'cves', 'vender_references'],
qid: ['qid'],
Expand Down
Loading
Loading