qualys-mcp 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +19 -4
- package/dist/qualys-client.js +17 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Read-only MCP server for the Qualys VMDR API. Exposes host assets, vulnerability
|
|
|
9
9
|
| `qualys_vuln_overview` | Estate-wide counts by tag: every client's open vulnerabilities by severity in one call, with optional CVSS threshold |
|
|
10
10
|
| `qualys_vuln_summary` | Aggregated vulnerability counts by severity, per client tag and per machine — the dashboard/reporting tool |
|
|
11
11
|
| `qualys_list_tags` | Asset tags and tag hierarchies (how clients are segmented) |
|
|
12
|
-
| `qualys_asset_inventory` | Full CSAM/Global AssetView inventory: all assets with criticality, tags, last logged-on user, agent check-in, sources (
|
|
12
|
+
| `qualys_asset_inventory` | Full CSAM/Global AssetView inventory: all assets with criticality, tags, last logged-on user, agent check-in, sources (filterable by tag, OS, etc.) |
|
|
13
13
|
| `qualys_host_detections` | Vulnerabilities detected per host (QID, severity, status, optional evidence; tag filterable) — the drill-down tool |
|
|
14
14
|
| `qualys_knowledgebase` | QID details: title, CVEs, threat, impact, solution |
|
|
15
15
|
| `qualys_api_call` | Raw GET/POST to any other Qualys endpoint — disabled unless `QUALYS_ENABLE_RAW_API=true` |
|
|
@@ -93,7 +93,7 @@ Qualys severity (1–5) is not CVSS. If your remediation policy is CVSS-based
|
|
|
93
93
|
|
|
94
94
|
## Notes
|
|
95
95
|
|
|
96
|
-
- `qualys_asset_inventory` uses the CSAM/Global AssetView gateway API (JWT auth, JSON) and answers "show me all machines" — including agent-tracked assets, criticality scores and tags. Filter with
|
|
96
|
+
- `qualys_asset_inventory` uses the CSAM/Global AssetView gateway API (JWT auth, JSON) and answers "show me all machines" — including agent-tracked assets, criticality scores and tags. Filter with structured criteria sent in the request body, e.g. `filters: [{"field": "tags.name", "value": "Client A"}]` — this endpoint does not accept QQL strings. Criteria are ANDed; if a field isn't filterable the API error lists every valid field. Paginate with `hasMore`/`lastSeenAssetId`. Note the gateway API has a much stricter rate quota than the VM API — filter rather than sweep repeatedly.
|
|
97
97
|
- For per-machine critical vulnerability counts, combine it with `qualys_host_detections` using `severities="5"` (or `"4,5"`).
|
|
98
98
|
- The Qualys VM API v2 returns XML; this server converts it to JSON.
|
|
99
99
|
- Rate/concurrency limits (HTTP 409/429) are retried automatically using the `X-RateLimit-ToWait-Sec` header, up to 3 times.
|
package/dist/index.js
CHANGED
|
@@ -28,11 +28,23 @@ const truncationProperties = {
|
|
|
28
28
|
const tools = [
|
|
29
29
|
{
|
|
30
30
|
name: 'qualys_asset_inventory',
|
|
31
|
-
description: 'Full asset inventory from Qualys CSAM/Global AssetView — the same data as the Asset Management UI: every asset (agent-tracked and scanned) with name, IP, OS, asset criticality score, TruRisk score, tags, last logged-on user, agent last check-in and last scan dates. This is the primary tool for "show me all machines/assets". Filter with
|
|
31
|
+
description: 'Full asset inventory from Qualys CSAM/Global AssetView — the same data as the Asset Management UI: every asset (agent-tracked and scanned) with name, IP, OS, asset criticality score, TruRisk score, tags, last logged-on user, agent last check-in and last scan dates. This is the primary tool for "show me all machines/assets". Filter with structured criteria (ANDed), e.g. [{"field": "tags.name", "value": "Client A"}] or [{"field": "operatingSystem.category1", "value": "Windows"}]. Returns a compact summary per asset by default; set full_details=true for complete raw records (large — software, ports, volumes). For counts of critical vulnerabilities per machine, combine with qualys_host_detections (severities="5").',
|
|
32
32
|
inputSchema: {
|
|
33
33
|
type: 'object',
|
|
34
34
|
properties: {
|
|
35
|
-
|
|
35
|
+
filters: {
|
|
36
|
+
type: 'array',
|
|
37
|
+
description: 'Filter criteria, ANDed together. Verified fields: tags.name, operatingSystem.category1 ("Windows"/"Linux"), operatingSystem.category2 ("Server"/"Desktop"). If a field is not filterable, the API error helpfully lists every valid field name — read it and retry. Note: QQL strings are NOT supported by this endpoint.',
|
|
38
|
+
items: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
properties: {
|
|
41
|
+
field: { type: 'string', description: 'Field name, e.g. "tags.name"' },
|
|
42
|
+
operator: { type: 'string', description: 'Default "EQUALS"' },
|
|
43
|
+
value: { type: 'string', description: 'Value to match, e.g. "Gordon Moody"' },
|
|
44
|
+
},
|
|
45
|
+
required: ['field', 'value'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
36
48
|
page_size: { type: 'number', description: 'Assets per page (default 100, max 300)' },
|
|
37
49
|
last_seen_asset_id: { type: 'number', description: 'Pagination cursor: pass the lastSeenAssetId from the previous response to get the next page (response also has hasMore=1/0)' },
|
|
38
50
|
full_details: { type: 'boolean', description: 'Return complete raw asset records instead of the compact summary. Default false.' },
|
|
@@ -377,12 +389,15 @@ async function handleTool(name, args) {
|
|
|
377
389
|
}
|
|
378
390
|
case 'qualys_asset_inventory': {
|
|
379
391
|
const fullDetails = args.full_details === true;
|
|
392
|
+
const filters = args.filters;
|
|
393
|
+
const body = filters?.length
|
|
394
|
+
? { filters: filters.map((f) => ({ field: f.field, operator: f.operator ?? 'EQUALS', value: f.value })) }
|
|
395
|
+
: undefined;
|
|
380
396
|
const result = (await client.gatewayRequest('/rest/2.0/search/am/asset', {
|
|
381
|
-
filter: args.filter,
|
|
382
397
|
pageSize: Math.min(args.page_size ?? 100, 300),
|
|
383
398
|
lastSeenAssetId: args.last_seen_asset_id,
|
|
384
399
|
includeFields: fullDetails ? undefined : INVENTORY_FIELDS,
|
|
385
|
-
}));
|
|
400
|
+
}, body));
|
|
386
401
|
const assets = result?.assetListData?.asset ?? [];
|
|
387
402
|
return JSON.stringify({
|
|
388
403
|
count: result?.count,
|
package/dist/qualys-client.js
CHANGED
|
@@ -108,7 +108,7 @@ export class QualysClient {
|
|
|
108
108
|
this.tokenExpiresAt = Date.now() + 3.5 * 60 * 60 * 1000;
|
|
109
109
|
return this.token;
|
|
110
110
|
}
|
|
111
|
-
async gatewayRequest(path, query = {}, method = 'POST') {
|
|
111
|
+
async gatewayRequest(path, query = {}, body, method = 'POST') {
|
|
112
112
|
const url = new URL(this.gatewayUrl + (path.startsWith('/') ? path : '/' + path));
|
|
113
113
|
for (const [key, value] of Object.entries(query)) {
|
|
114
114
|
if (value !== undefined && value !== null && value !== '') {
|
|
@@ -117,15 +117,27 @@ export class QualysClient {
|
|
|
117
117
|
}
|
|
118
118
|
for (let attempt = 0;; attempt++) {
|
|
119
119
|
const token = await this.getToken(attempt > 0);
|
|
120
|
+
const headers = { Authorization: `Bearer ${token}`, Accept: 'application/json' };
|
|
121
|
+
if (body !== undefined)
|
|
122
|
+
headers['Content-Type'] = 'application/json';
|
|
120
123
|
const response = await fetch(url, {
|
|
121
124
|
method,
|
|
122
|
-
headers
|
|
125
|
+
headers,
|
|
126
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
123
127
|
});
|
|
124
128
|
if (response.status === 401 && attempt < 1)
|
|
125
129
|
continue;
|
|
126
|
-
if (response.status === 429
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
if (response.status === 429) {
|
|
131
|
+
const toWait = Number(response.headers.get('x-ratelimit-towait-sec')) || 0;
|
|
132
|
+
if (attempt < MAX_RETRIES && toWait <= 120) {
|
|
133
|
+
await new Promise((resolve) => setTimeout(resolve, (toWait || 10 * (attempt + 1)) * 1000));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const rl = (name) => response.headers.get(name) ?? 'n/a';
|
|
137
|
+
throw new Error(`Qualys gateway API rate limit reached for ${url.pathname} as user "${this.username}" ` +
|
|
138
|
+
`(X-RateLimit-Limit: ${rl('x-ratelimit-limit')}, X-RateLimit-Remaining: ${rl('x-ratelimit-remaining')}, ` +
|
|
139
|
+
`X-RateLimit-ToWait-Sec: ${rl('x-ratelimit-towait-sec')}${toWait ? `, ~${Math.ceil(toWait / 3600)} hour(s)` : ''}). ` +
|
|
140
|
+
'The VM API tools (qualys_vuln_overview/vuln_summary/host_detections/knowledgebase/list_tags) use a separate limit and still work.');
|
|
129
141
|
}
|
|
130
142
|
const text = await response.text();
|
|
131
143
|
if (!response.ok) {
|
package/package.json
CHANGED