qualys-mcp 0.2.0 → 0.3.0
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 +17 -2
- package/dist/index.js +253 -0
- package/dist/qualys-client.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,9 +6,12 @@ Read-only MCP server for the Qualys VMDR API. Exposes host assets, vulnerability
|
|
|
6
6
|
|
|
7
7
|
| Tool | Purpose |
|
|
8
8
|
|---|---|
|
|
9
|
+
| `qualys_vuln_overview` | Estate-wide counts by tag: every client's open vulnerabilities by severity in one call, with optional CVSS threshold |
|
|
10
|
+
| `qualys_vuln_summary` | Aggregated vulnerability counts by severity, per client tag and per machine — the dashboard/reporting tool |
|
|
11
|
+
| `qualys_list_tags` | Asset tags and tag hierarchies (how clients are segmented) |
|
|
9
12
|
| `qualys_asset_inventory` | Full CSAM/Global AssetView inventory: all assets with criticality, tags, last logged-on user, agent check-in, sources (QQL filterable) |
|
|
10
|
-
| `qualys_list_hosts` | Scanned host assets (ID, IP, DNS, OS, last scan dates, optional tags) |
|
|
11
|
-
| `qualys_host_detections` | Vulnerabilities detected per host (QID, severity, status, evidence) |
|
|
13
|
+
| `qualys_list_hosts` | Scanned host assets (ID, IP, DNS, OS, last scan dates, optional tags; tag filterable) |
|
|
14
|
+
| `qualys_host_detections` | Vulnerabilities detected per host (QID, severity, status, optional evidence; tag filterable) — the drill-down tool |
|
|
12
15
|
| `qualys_knowledgebase` | QID details: title, CVEs, threat, impact, solution |
|
|
13
16
|
| `qualys_list_scans` | Scan history and status |
|
|
14
17
|
| `qualys_list_reports` | Generated reports |
|
|
@@ -78,6 +81,18 @@ npm run inspector # MCP Inspector UI
|
|
|
78
81
|
npm run build # compile to dist/
|
|
79
82
|
```
|
|
80
83
|
|
|
84
|
+
## Reporting across a client base
|
|
85
|
+
|
|
86
|
+
Clients segmented by asset tags? The intended flow for "state of vulnerabilities across our clients" is:
|
|
87
|
+
|
|
88
|
+
1. `qualys_vuln_overview` — one call sweeps the whole estate and returns a client-by-client table: tag, host count, open vulnerabilities by severity. Use `exclude_tags` to drop system tags (e.g. "Cloud Agent") from the grouping.
|
|
89
|
+
2. `qualys_vuln_summary` with `tags: "Client A"` — expand one client into its per-machine breakdown, sorted worst-first. Aggregation happens inside the server, so even thousands of detections come back as a few KB of counts.
|
|
90
|
+
3. `qualys_host_detections` with `tags`/`ids` — drill into the individual findings on a machine, then `qualys_knowledgebase` for the fix.
|
|
91
|
+
|
|
92
|
+
### CVSS-based thresholds (Cyber Essentials)
|
|
93
|
+
|
|
94
|
+
Qualys severity (1–5) is not CVSS. If your remediation policy is CVSS-based — e.g. Cyber Essentials' "fix CVSS 7.0+ within 14 days" — pass `cvss_min: 7` to `qualys_vuln_overview` or `qualys_vuln_summary`. The server joins each detection against the KnowledgeBase CVSS v3 base score (v2 where no v3 exists) and counts only vulnerabilities at or above the threshold. QIDs with no CVSS data are excluded and reported in the response's `cvssFilter` block. Scores are cached for the server's lifetime, so repeated dashboard queries are fast.
|
|
95
|
+
|
|
81
96
|
## Notes
|
|
82
97
|
|
|
83
98
|
- `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 QQL, e.g. `tags.name:"Client A"`, `operatingSystem.category1:"Windows"`, `criticality.score>=3`. Paginate with `hasMore`/`lastSeenAssetId`.
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,46 @@ const tools = [
|
|
|
38
38
|
},
|
|
39
39
|
},
|
|
40
40
|
},
|
|
41
|
+
{
|
|
42
|
+
name: 'qualys_vuln_overview',
|
|
43
|
+
description: 'Estate-wide vulnerability overview grouped by asset tag — one call answers "how many open vulnerabilities does each client have?". Sweeps all detection data server-side, groups counts by each host\'s tags, and returns a table of tag → host count and vulnerability counts by severity. Set cvss_min=7 to count only CVSS 7.0+ vulnerabilities (the Cyber Essentials remediation threshold) — scores are joined from the KnowledgeBase (CVSS v3 base, falling back to v2). Hosts with multiple tags are counted under each tag. Use qualys_vuln_summary to expand one client into its per-machine breakdown.',
|
|
44
|
+
inputSchema: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
cvss_min: { type: 'number', description: 'Only count vulnerabilities with CVSS base score >= this value, e.g. 7 for Cyber Essentials. Omit to count everything.' },
|
|
48
|
+
tags: { type: 'string', description: 'Optionally restrict the sweep to hosts with these comma-separated tags. Default: whole estate.' },
|
|
49
|
+
exclude_tags: { type: 'string', description: 'Comma-separated tag names to omit from the grouping (e.g. system tags like "Cloud Agent")' },
|
|
50
|
+
severities: { type: 'string', description: 'Only count these Qualys severity levels, e.g. "4,5". Usually unnecessary when cvss_min is set.' },
|
|
51
|
+
status: { type: 'string', description: 'Detection statuses to count. Default "New,Active,Re-Opened" (open vulnerabilities).' },
|
|
52
|
+
max_hosts: { type: 'number', description: 'Safety cap on hosts to sweep (default 10000)' },
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'qualys_vuln_summary',
|
|
58
|
+
description: 'Aggregated vulnerability counts by severity — the dashboard tool. Give it one or more client tags (or IPs/asset group IDs) and it pulls ALL matching detection data server-side and returns compact counts: totals by severity (1-5, confirmed vs potential) plus a per-machine breakdown sorted worst-first. Use this to report on the state of vulnerabilities per client; use qualys_host_detections to drill into the individual findings on a specific machine afterwards. Call qualys_list_tags first to see available client tags.',
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: 'object',
|
|
61
|
+
properties: {
|
|
62
|
+
tags: { type: 'string', description: 'Comma-separated asset tag names to scope to, e.g. "Client A" — tags are how clients are segmented' },
|
|
63
|
+
tag_include_selector: { type: 'string', description: '"any" (default) or "all" — whether hosts must have any or all of the tags' },
|
|
64
|
+
ips: { type: 'string', description: 'Alternatively scope by comma-separated IPs or ranges' },
|
|
65
|
+
ag_ids: { type: 'string', description: 'Alternatively scope by comma-separated asset group IDs' },
|
|
66
|
+
cvss_min: { type: 'number', description: 'Only count vulnerabilities with CVSS base score >= this value, e.g. 7 for Cyber Essentials (CVSS v3 base, v2 fallback, joined from the KnowledgeBase)' },
|
|
67
|
+
severities: { type: 'string', description: 'Only count these Qualys severity levels, e.g. "4,5". Default: all.' },
|
|
68
|
+
status: { type: 'string', description: 'Detection statuses to count. Default "New,Active,Re-Opened" (open vulnerabilities). Add "Fixed" to include remediated ones.' },
|
|
69
|
+
max_hosts: { type: 'number', description: 'Safety cap on hosts to aggregate (default 5000)' },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: 'qualys_list_tags',
|
|
75
|
+
description: 'List asset tags defined in Qualys, including tag hierarchies (parent tags with their children). Tags are how clients are segmented — use this to discover the client list, then feed tag names into qualys_vuln_summary or qualys_asset_inventory. Returns the first 100 tags.',
|
|
76
|
+
inputSchema: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {},
|
|
79
|
+
},
|
|
80
|
+
},
|
|
41
81
|
{
|
|
42
82
|
name: 'qualys_list_hosts',
|
|
43
83
|
description: 'List scanned host assets in Qualys VMDR with their ID, IP, DNS name, OS and last scan dates. Use this to find host IDs before pulling their vulnerability detections. Filter by IPs, asset group or scan recency.',
|
|
@@ -56,6 +96,8 @@ const tools = [
|
|
|
56
96
|
no_vm_scan_since: { type: 'string', description: 'Only hosts NOT scanned since this date (YYYY-MM-DD) — useful to find stale/unscanned assets' },
|
|
57
97
|
vm_scan_since: { type: 'string', description: 'Only hosts scanned since this date (YYYY-MM-DD)' },
|
|
58
98
|
show_tags: { type: 'boolean', description: 'Include asset tags for each host. Default false.' },
|
|
99
|
+
tags: { type: 'string', description: 'Comma-separated asset tag names to scope to (clients are segmented by tags)' },
|
|
100
|
+
tag_include_selector: { type: 'string', description: '"any" (default) or "all" — whether hosts must have any or all of the tags' },
|
|
59
101
|
...truncationProperties,
|
|
60
102
|
},
|
|
61
103
|
},
|
|
@@ -78,6 +120,9 @@ const tools = [
|
|
|
78
120
|
},
|
|
79
121
|
show_igs: { type: 'boolean', description: 'Include information-gathered (IG) detections as well as vulnerabilities. Default false.' },
|
|
80
122
|
detection_updated_since: { type: 'string', description: 'Only detections updated since this date (YYYY-MM-DD)' },
|
|
123
|
+
tags: { type: 'string', description: 'Comma-separated asset tag names to scope to (clients are segmented by tags)' },
|
|
124
|
+
tag_include_selector: { type: 'string', description: '"any" (default) or "all" — whether hosts must have any or all of the tags' },
|
|
125
|
+
include_results: { type: 'boolean', description: 'Include the detection RESULTS evidence text (verbose). Default false.' },
|
|
81
126
|
...truncationProperties,
|
|
82
127
|
},
|
|
83
128
|
},
|
|
@@ -195,8 +240,213 @@ function compactAsset(a) {
|
|
|
195
240
|
});
|
|
196
241
|
}
|
|
197
242
|
const INVENTORY_FIELDS = 'assetId,assetName,address,dnsName,operatingSystem,criticality,riskScore,lastLoggedOnUser,tag,agent,sensor,inventory,assetType,lastModifiedDate';
|
|
243
|
+
function tagParams(args) {
|
|
244
|
+
if (!args.tags)
|
|
245
|
+
return {};
|
|
246
|
+
return {
|
|
247
|
+
use_tags: 1,
|
|
248
|
+
tag_set_by: 'name',
|
|
249
|
+
tag_include_selector: args.tag_include_selector ?? 'any',
|
|
250
|
+
tag_set_include: args.tags,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function sweepDetections(args, showTags, maxHosts) {
|
|
254
|
+
const hosts = [];
|
|
255
|
+
let idMin;
|
|
256
|
+
let truncated = false;
|
|
257
|
+
while (true) {
|
|
258
|
+
const result = await client.request('/api/2.0/fo/asset/host/vm/detection/', {
|
|
259
|
+
action: 'list',
|
|
260
|
+
echo_request: 0,
|
|
261
|
+
...tagParams(args),
|
|
262
|
+
ips: args.ips,
|
|
263
|
+
ag_ids: args.ag_ids,
|
|
264
|
+
severities: args.severities,
|
|
265
|
+
status: args.status ?? 'New,Active,Re-Opened',
|
|
266
|
+
show_results: 0,
|
|
267
|
+
show_igs: 0,
|
|
268
|
+
show_tags: showTags ? 1 : undefined,
|
|
269
|
+
truncation_limit: Math.min(1000, maxHosts),
|
|
270
|
+
id_min: idMin,
|
|
271
|
+
});
|
|
272
|
+
const resp = simplify(result.parsed);
|
|
273
|
+
const pageHosts = resp?.HOST_LIST?.HOST ?? [];
|
|
274
|
+
for (const h of pageHosts) {
|
|
275
|
+
const detections = h?.DETECTION_LIST?.DETECTION ?? [];
|
|
276
|
+
hosts.push({
|
|
277
|
+
id: String(h.ID),
|
|
278
|
+
ip: h.IP,
|
|
279
|
+
dns: h.DNS,
|
|
280
|
+
os: h.OS,
|
|
281
|
+
lastScan: h.LAST_SCAN_DATETIME,
|
|
282
|
+
tags: (h.TAGS?.TAG ?? []).map((t) => String(t.NAME)),
|
|
283
|
+
detections: detections.map((d) => ({ qid: String(d.QID), sev: String(d.SEVERITY), type: String(d.TYPE) })),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
const warningUrl = resp?.WARNING?.[0]?.URL;
|
|
287
|
+
const nextIdMin = warningUrl?.match(/id_min=(\d+)/)?.[1];
|
|
288
|
+
if (!nextIdMin)
|
|
289
|
+
break;
|
|
290
|
+
if (hosts.length >= maxHosts) {
|
|
291
|
+
truncated = true;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
idMin = Number(nextIdMin);
|
|
295
|
+
}
|
|
296
|
+
return { hosts, truncated };
|
|
297
|
+
}
|
|
298
|
+
const cvssCache = new Map();
|
|
299
|
+
async function fetchCvssScores(qids) {
|
|
300
|
+
const missing = [...new Set(qids)].filter((q) => !cvssCache.has(q));
|
|
301
|
+
const num = (x) => {
|
|
302
|
+
const n = parseFloat(String(x ?? ''));
|
|
303
|
+
return Number.isFinite(n) ? n : undefined;
|
|
304
|
+
};
|
|
305
|
+
for (let i = 0; i < missing.length; i += 400) {
|
|
306
|
+
const chunk = missing.slice(i, i + 400);
|
|
307
|
+
const result = await client.request('/api/2.0/fo/knowledge_base/vuln/', { action: 'list', echo_request: 0, ids: chunk.join(','), details: 'Basic' }, 'POST');
|
|
308
|
+
const resp = simplify(result.parsed);
|
|
309
|
+
const vulns = resp?.VULN_LIST?.VULN ?? [];
|
|
310
|
+
for (const v of vulns) {
|
|
311
|
+
cvssCache.set(String(v.QID), { v2: num(v.CVSS?.BASE), v3: num(v.CVSS_V3?.BASE) });
|
|
312
|
+
}
|
|
313
|
+
for (const q of chunk)
|
|
314
|
+
if (!cvssCache.has(q))
|
|
315
|
+
cvssCache.set(q, {});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function applyCvssFilter(hosts, cvssMin) {
|
|
319
|
+
await fetchCvssScores(hosts.flatMap((h) => h.detections.map((d) => d.qid)));
|
|
320
|
+
const unknown = new Set();
|
|
321
|
+
let excludedUnknown = 0;
|
|
322
|
+
for (const h of hosts) {
|
|
323
|
+
h.detections = h.detections.filter((d) => {
|
|
324
|
+
const score = cvssCache.get(d.qid);
|
|
325
|
+
const base = score?.v3 ?? score?.v2;
|
|
326
|
+
if (base === undefined) {
|
|
327
|
+
unknown.add(d.qid);
|
|
328
|
+
excludedUnknown += 1;
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return base >= cvssMin;
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
basis: `CVSS v3 base score >= ${cvssMin} (v2 base used where no v3 score exists)`,
|
|
336
|
+
unknownCvssQids: unknown.size,
|
|
337
|
+
detectionsExcludedUnknownCvss: excludedUnknown,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function countBySeverity(detections, totals) {
|
|
341
|
+
const counts = { confirmed: {}, potential: {} };
|
|
342
|
+
for (const d of detections) {
|
|
343
|
+
const bucket = d.type === 'Potential' ? 'potential' : 'confirmed';
|
|
344
|
+
counts[bucket][d.sev] = (counts[bucket][d.sev] ?? 0) + 1;
|
|
345
|
+
if (totals)
|
|
346
|
+
totals[bucket][d.sev] = (totals[bucket][d.sev] ?? 0) + 1;
|
|
347
|
+
}
|
|
348
|
+
return counts;
|
|
349
|
+
}
|
|
350
|
+
const hostWeight = (c, total) => (c.confirmed['5'] ?? 0) * 1e6 + (c.confirmed['4'] ?? 0) * 1e3 + total;
|
|
198
351
|
async function handleTool(name, args) {
|
|
199
352
|
switch (name) {
|
|
353
|
+
case 'qualys_vuln_summary': {
|
|
354
|
+
const maxHosts = args.max_hosts ?? 5000;
|
|
355
|
+
const { hosts, truncated } = await sweepDetections(args, false, maxHosts);
|
|
356
|
+
const cvssInfo = args.cvss_min !== undefined ? await applyCvssFilter(hosts, args.cvss_min) : undefined;
|
|
357
|
+
const totals = { confirmed: {}, potential: {} };
|
|
358
|
+
const rows = hosts.map((h) => {
|
|
359
|
+
const counts = countBySeverity(h.detections, totals);
|
|
360
|
+
return {
|
|
361
|
+
id: h.id,
|
|
362
|
+
ip: h.ip,
|
|
363
|
+
dns: h.dns,
|
|
364
|
+
os: h.os,
|
|
365
|
+
lastScan: h.lastScan,
|
|
366
|
+
confirmed: counts.confirmed,
|
|
367
|
+
potential: counts.potential,
|
|
368
|
+
total: h.detections.length,
|
|
369
|
+
};
|
|
370
|
+
});
|
|
371
|
+
rows.sort((a, b) => hostWeight(b, b.total) - hostWeight(a, a.total));
|
|
372
|
+
return JSON.stringify(stripEmpty({
|
|
373
|
+
scope: {
|
|
374
|
+
tags: args.tags,
|
|
375
|
+
ips: args.ips,
|
|
376
|
+
ag_ids: args.ag_ids,
|
|
377
|
+
severities: args.severities,
|
|
378
|
+
status: args.status ?? 'New,Active,Re-Opened',
|
|
379
|
+
},
|
|
380
|
+
cvssFilter: cvssInfo,
|
|
381
|
+
hostCount: rows.length,
|
|
382
|
+
totalsBySeverity: totals,
|
|
383
|
+
truncated: truncated
|
|
384
|
+
? `Stopped at max_hosts=${maxHosts} — counts are incomplete; narrow the scope or raise max_hosts`
|
|
385
|
+
: undefined,
|
|
386
|
+
hosts: rows,
|
|
387
|
+
}), null, 2);
|
|
388
|
+
}
|
|
389
|
+
case 'qualys_vuln_overview': {
|
|
390
|
+
const maxHosts = args.max_hosts ?? 10000;
|
|
391
|
+
const { hosts, truncated } = await sweepDetections(args, true, maxHosts);
|
|
392
|
+
const cvssInfo = args.cvss_min !== undefined ? await applyCvssFilter(hosts, args.cvss_min) : undefined;
|
|
393
|
+
const excluded = new Set((args.exclude_tags ?? '')
|
|
394
|
+
.split(',')
|
|
395
|
+
.map((t) => t.trim())
|
|
396
|
+
.filter(Boolean));
|
|
397
|
+
const overall = { confirmed: {}, potential: {} };
|
|
398
|
+
const byTag = new Map();
|
|
399
|
+
for (const h of hosts) {
|
|
400
|
+
countBySeverity(h.detections, overall);
|
|
401
|
+
const tags = h.tags.filter((t) => !excluded.has(t));
|
|
402
|
+
for (const tag of tags.length > 0 ? tags : ['(untagged)']) {
|
|
403
|
+
let row = byTag.get(tag);
|
|
404
|
+
if (!row) {
|
|
405
|
+
row = { hosts: 0, counts: { confirmed: {}, potential: {} }, total: 0 };
|
|
406
|
+
byTag.set(tag, row);
|
|
407
|
+
}
|
|
408
|
+
row.hosts += 1;
|
|
409
|
+
countBySeverity(h.detections, row.counts);
|
|
410
|
+
row.total += h.detections.length;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const tagRows = [...byTag.entries()]
|
|
414
|
+
.map(([tag, r]) => ({ tag, hosts: r.hosts, confirmed: r.counts.confirmed, potential: r.counts.potential, total: r.total }))
|
|
415
|
+
.sort((a, b) => hostWeight(b, b.total) - hostWeight(a, a.total));
|
|
416
|
+
return JSON.stringify(stripEmpty({
|
|
417
|
+
scope: {
|
|
418
|
+
tags: args.tags,
|
|
419
|
+
severities: args.severities,
|
|
420
|
+
status: args.status ?? 'New,Active,Re-Opened',
|
|
421
|
+
exclude_tags: args.exclude_tags,
|
|
422
|
+
},
|
|
423
|
+
cvssFilter: cvssInfo,
|
|
424
|
+
hostCount: hosts.length,
|
|
425
|
+
note: 'Hosts carrying multiple tags are counted under each tag, so tag rows can overlap.',
|
|
426
|
+
truncated: truncated
|
|
427
|
+
? `Stopped at max_hosts=${maxHosts} — counts are incomplete; narrow the scope or raise max_hosts`
|
|
428
|
+
: undefined,
|
|
429
|
+
overallBySeverity: overall,
|
|
430
|
+
byTag: tagRows,
|
|
431
|
+
}), null, 2);
|
|
432
|
+
}
|
|
433
|
+
case 'qualys_list_tags': {
|
|
434
|
+
const result = await client.request('/qps/rest/2.0/search/am/tag', {}, 'POST');
|
|
435
|
+
const resp = simplify(result.parsed);
|
|
436
|
+
const rawTags = resp?.data?.Tag ?? [];
|
|
437
|
+
const tags = (Array.isArray(rawTags) ? rawTags : [rawTags]).map((t) => stripEmpty({
|
|
438
|
+
id: t.id,
|
|
439
|
+
name: t.name,
|
|
440
|
+
created: t.created,
|
|
441
|
+
children: (() => {
|
|
442
|
+
const kids = t.children?.list?.TagSimple;
|
|
443
|
+
if (!kids)
|
|
444
|
+
return undefined;
|
|
445
|
+
return (Array.isArray(kids) ? kids : [kids]).map((k) => ({ id: k.id, name: k.name }));
|
|
446
|
+
})(),
|
|
447
|
+
}));
|
|
448
|
+
return JSON.stringify({ count: resp?.count, hasMoreRecords: resp?.hasMoreRecords, tags }, null, 2);
|
|
449
|
+
}
|
|
200
450
|
case 'qualys_asset_inventory': {
|
|
201
451
|
const fullDetails = args.full_details === true;
|
|
202
452
|
const result = (await client.gatewayRequest('/rest/2.0/search/am/asset', {
|
|
@@ -226,6 +476,7 @@ async function handleTool(name, args) {
|
|
|
226
476
|
no_vm_scan_since: args.no_vm_scan_since,
|
|
227
477
|
vm_scan_since: args.vm_scan_since,
|
|
228
478
|
show_tags: args.show_tags ? 1 : undefined,
|
|
479
|
+
...tagParams(args),
|
|
229
480
|
truncation_limit: args.truncation_limit ?? 100,
|
|
230
481
|
id_min: args.id_min,
|
|
231
482
|
});
|
|
@@ -243,7 +494,9 @@ async function handleTool(name, args) {
|
|
|
243
494
|
severities: args.severities,
|
|
244
495
|
status: args.status,
|
|
245
496
|
show_igs: args.show_igs ? 1 : undefined,
|
|
497
|
+
show_results: args.include_results ? undefined : 0,
|
|
246
498
|
detection_updated_since: args.detection_updated_since,
|
|
499
|
+
...tagParams(args),
|
|
247
500
|
truncation_limit: args.truncation_limit ?? 20,
|
|
248
501
|
id_min: args.id_min,
|
|
249
502
|
});
|
package/dist/qualys-client.js
CHANGED
|
@@ -61,7 +61,7 @@ export class QualysClient {
|
|
|
61
61
|
'X-Requested-With': 'qualys-mcp',
|
|
62
62
|
};
|
|
63
63
|
let body;
|
|
64
|
-
if (method === 'POST') {
|
|
64
|
+
if (method === 'POST' && [...search.keys()].length > 0) {
|
|
65
65
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
|
66
66
|
body = search.toString();
|
|
67
67
|
}
|
package/package.json
CHANGED