qualys-mcp 0.3.0 → 0.4.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 +4 -6
- package/dist/index.js +8 -142
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,14 +10,11 @@ Read-only MCP server for the Qualys VMDR API. Exposes host assets, vulnerability
|
|
|
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
12
|
| `qualys_asset_inventory` | Full CSAM/Global AssetView inventory: all assets with criticality, tags, last logged-on user, agent check-in, sources (QQL filterable) |
|
|
13
|
-
| `qualys_list_hosts` | Scanned host assets (ID, IP, DNS, OS, last scan dates, optional tags; tag filterable) |
|
|
14
13
|
| `qualys_host_detections` | Vulnerabilities detected per host (QID, severity, status, optional evidence; tag filterable) — the drill-down tool |
|
|
15
14
|
| `qualys_knowledgebase` | QID details: title, CVEs, threat, impact, solution |
|
|
16
|
-
| `
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| `qualys_list_asset_groups` | Asset groups (typically clients/sites) and their IP ranges |
|
|
20
|
-
| `qualys_api_call` | Raw GET/POST to any other Qualys endpoint (read-only actions only) |
|
|
15
|
+
| `qualys_api_call` | Raw GET/POST to any other Qualys endpoint — disabled unless `QUALYS_ENABLE_RAW_API=true` |
|
|
16
|
+
|
|
17
|
+
The tool set is deliberately lean, focused on vulnerability reporting across a tag-segmented client base. The raw API tool covers anything else (scans, reports, asset groups, schedules) but is off by default so shared deployments stay strictly read-only.
|
|
21
18
|
|
|
22
19
|
## Quick start (Claude Desktop / Cowork)
|
|
23
20
|
|
|
@@ -53,6 +50,7 @@ Three required environment variables:
|
|
|
53
50
|
| `QUALYS_PASSWORD` | Qualys API user password |
|
|
54
51
|
| `QUALYS_BASE_URL` | Your platform's API server (see below) |
|
|
55
52
|
| `QUALYS_GATEWAY_URL` | Optional — the gateway host used by `qualys_asset_inventory` (CSAM/GAV API). Derived automatically from `QUALYS_BASE_URL` (e.g. `https://gateway.qg1.apps.qualys.co.uk` for UK1); only set this if your platform's gateway differs. |
|
|
53
|
+
| `QUALYS_ENABLE_RAW_API` | Optional — set to `true` to expose `qualys_api_call` for arbitrary Qualys API endpoints. Leave unset for team deployments. |
|
|
56
54
|
|
|
57
55
|
The API server depends on which Qualys platform your subscription lives on — check your Qualys login URL:
|
|
58
56
|
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { QualysClient, simplify, stripEmpty } from './qualys-client.js';
|
|
|
6
6
|
const username = process.env.QUALYS_USERNAME || '';
|
|
7
7
|
const password = process.env.QUALYS_PASSWORD || '';
|
|
8
8
|
const baseUrl = process.env.QUALYS_BASE_URL || '';
|
|
9
|
+
const rawApiEnabled = process.env.QUALYS_ENABLE_RAW_API === 'true';
|
|
9
10
|
if (!username || !password || !baseUrl) {
|
|
10
11
|
console.error('Missing required environment variables: QUALYS_USERNAME, QUALYS_PASSWORD, QUALYS_BASE_URL.');
|
|
11
12
|
console.error('QUALYS_BASE_URL is your platform API server, e.g. https://qualysapi.qg1.apps.qualys.co.uk (UK1),');
|
|
@@ -78,30 +79,6 @@ const tools = [
|
|
|
78
79
|
properties: {},
|
|
79
80
|
},
|
|
80
81
|
},
|
|
81
|
-
{
|
|
82
|
-
name: 'qualys_list_hosts',
|
|
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.',
|
|
84
|
-
inputSchema: {
|
|
85
|
-
type: 'object',
|
|
86
|
-
properties: {
|
|
87
|
-
ids: { type: 'string', description: 'Comma-separated host IDs or ranges, e.g. "123,150-200"' },
|
|
88
|
-
ips: { type: 'string', description: 'Comma-separated IPs or ranges, e.g. "10.0.0.1,10.0.1.0-10.0.1.255"' },
|
|
89
|
-
ag_ids: { type: 'string', description: 'Comma-separated asset group IDs (see qualys_list_asset_groups)' },
|
|
90
|
-
ag_titles: { type: 'string', description: 'Comma-separated asset group titles' },
|
|
91
|
-
details: {
|
|
92
|
-
type: 'string',
|
|
93
|
-
description: 'Level of detail: "Basic" (default), "Basic/AGs", "All", "All/AGs", or "None" (IDs only)',
|
|
94
|
-
},
|
|
95
|
-
os_pattern: { type: 'string', description: 'PCRE regex to match against host operating system, e.g. "Windows Server 2012"' },
|
|
96
|
-
no_vm_scan_since: { type: 'string', description: 'Only hosts NOT scanned since this date (YYYY-MM-DD) — useful to find stale/unscanned assets' },
|
|
97
|
-
vm_scan_since: { type: 'string', description: 'Only hosts scanned since this date (YYYY-MM-DD)' },
|
|
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' },
|
|
101
|
-
...truncationProperties,
|
|
102
|
-
},
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
82
|
{
|
|
106
83
|
name: 'qualys_host_detections',
|
|
107
84
|
description: 'List vulnerability detections per host — the core VMDR data. Returns each host with its detected vulnerabilities (QID, severity, status, first/last found, results evidence). Filter by host, QID, severity or detection status. Look up QID titles and fixes with qualys_knowledgebase. Output can be large: keep truncation_limit modest and filter where possible.',
|
|
@@ -141,57 +118,6 @@ const tools = [
|
|
|
141
118
|
},
|
|
142
119
|
},
|
|
143
120
|
},
|
|
144
|
-
{
|
|
145
|
-
name: 'qualys_list_scans',
|
|
146
|
-
description: 'List vulnerability scans in Qualys VMDR with their reference, title, launch date, status and target. Useful for checking scan history and whether scheduled scans are completing.',
|
|
147
|
-
inputSchema: {
|
|
148
|
-
type: 'object',
|
|
149
|
-
properties: {
|
|
150
|
-
scan_ref: { type: 'string', description: 'Specific scan reference, e.g. "scan/1234567890.12345"' },
|
|
151
|
-
state: { type: 'string', description: 'Comma-separated states: "Running", "Paused", "Canceled", "Finished", "Error", "Queued", "Loading"' },
|
|
152
|
-
type: { type: 'string', description: 'Scan type: "On-Demand", "Scheduled" or "API"' },
|
|
153
|
-
target: { type: 'string', description: 'Filter by scan target IPs' },
|
|
154
|
-
launched_after_datetime: { type: 'string', description: 'Only scans launched after this datetime (YYYY-MM-DD or ISO 8601)' },
|
|
155
|
-
launched_before_datetime: { type: 'string', description: 'Only scans launched before this datetime' },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
},
|
|
159
|
-
{
|
|
160
|
-
name: 'qualys_list_reports',
|
|
161
|
-
description: 'List generated reports in Qualys with their ID, title, type, output format, status and expiry. Use qualys_fetch_report to download a finished report.',
|
|
162
|
-
inputSchema: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: {
|
|
165
|
-
id: { type: 'number', description: 'Specific report ID' },
|
|
166
|
-
state: { type: 'string', description: 'Report state, e.g. "Finished", "Running", "Errors"' },
|
|
167
|
-
user_login: { type: 'string', description: 'Filter by the user who launched the report' },
|
|
168
|
-
},
|
|
169
|
-
},
|
|
170
|
-
},
|
|
171
|
-
{
|
|
172
|
-
name: 'qualys_fetch_report',
|
|
173
|
-
description: 'Download a finished Qualys report by ID. Only sensible for text-based formats (CSV, XML) — the content is returned inline, truncated to max_chars. Check qualys_list_reports first for the report status and format.',
|
|
174
|
-
inputSchema: {
|
|
175
|
-
type: 'object',
|
|
176
|
-
properties: {
|
|
177
|
-
id: { type: 'number', description: 'Report ID (must be in Finished state)' },
|
|
178
|
-
max_chars: { type: 'number', description: 'Maximum characters of report content to return (default 100000)' },
|
|
179
|
-
},
|
|
180
|
-
required: ['id'],
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
name: 'qualys_list_asset_groups',
|
|
185
|
-
description: 'List asset groups in Qualys with their ID, title and IP ranges. Asset groups typically map to clients or sites — use their IDs to scope qualys_list_hosts and qualys_host_detections.',
|
|
186
|
-
inputSchema: {
|
|
187
|
-
type: 'object',
|
|
188
|
-
properties: {
|
|
189
|
-
ids: { type: 'string', description: 'Comma-separated asset group IDs' },
|
|
190
|
-
show_attributes: { type: 'string', description: 'Comma-separated attributes to show, e.g. "ALL" or "ID,TITLE,IP_SET". Default "ID,TITLE".' },
|
|
191
|
-
...truncationProperties,
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
},
|
|
195
121
|
{
|
|
196
122
|
name: 'qualys_api_call',
|
|
197
123
|
description: 'Make a raw call to any Qualys API endpoint not covered by the other tools, e.g. /api/2.0/fo/report/template/scan/?action=list, /api/2.0/fo/schedule/scan/?action=list, or Asset Management endpoints under /qps/rest/2.0/. Only use list/read actions — do not launch scans or modify data. Params are sent as query string (GET) or form body (POST; some endpoints require POST).',
|
|
@@ -210,7 +136,9 @@ const tools = [
|
|
|
210
136
|
},
|
|
211
137
|
},
|
|
212
138
|
];
|
|
213
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
139
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
140
|
+
tools: rawApiEnabled ? tools : tools.filter((t) => t.name !== 'qualys_api_call'),
|
|
141
|
+
}));
|
|
214
142
|
function toIso(ms) {
|
|
215
143
|
return typeof ms === 'number' ? new Date(ms).toISOString() : undefined;
|
|
216
144
|
}
|
|
@@ -463,25 +391,6 @@ async function handleTool(name, args) {
|
|
|
463
391
|
assets: fullDetails ? assets.map((a) => stripEmpty(a)) : assets.map(compactAsset),
|
|
464
392
|
}, null, 2);
|
|
465
393
|
}
|
|
466
|
-
case 'qualys_list_hosts': {
|
|
467
|
-
const result = await client.request('/api/2.0/fo/asset/host/', {
|
|
468
|
-
action: 'list',
|
|
469
|
-
echo_request: 0,
|
|
470
|
-
ids: args.ids,
|
|
471
|
-
ips: args.ips,
|
|
472
|
-
ag_ids: args.ag_ids,
|
|
473
|
-
ag_titles: args.ag_titles,
|
|
474
|
-
details: args.details ?? 'Basic',
|
|
475
|
-
os_pattern: args.os_pattern,
|
|
476
|
-
no_vm_scan_since: args.no_vm_scan_since,
|
|
477
|
-
vm_scan_since: args.vm_scan_since,
|
|
478
|
-
show_tags: args.show_tags ? 1 : undefined,
|
|
479
|
-
...tagParams(args),
|
|
480
|
-
truncation_limit: args.truncation_limit ?? 100,
|
|
481
|
-
id_min: args.id_min,
|
|
482
|
-
});
|
|
483
|
-
return JSON.stringify(simplify(result.parsed), null, 2);
|
|
484
|
-
}
|
|
485
394
|
case 'qualys_host_detections': {
|
|
486
395
|
const result = await client.request('/api/2.0/fo/asset/host/vm/detection/', {
|
|
487
396
|
action: 'list',
|
|
@@ -514,53 +423,10 @@ async function handleTool(name, args) {
|
|
|
514
423
|
});
|
|
515
424
|
return JSON.stringify(simplify(result.parsed), null, 2);
|
|
516
425
|
}
|
|
517
|
-
case 'qualys_list_scans': {
|
|
518
|
-
const result = await client.request('/api/2.0/fo/scan/', {
|
|
519
|
-
action: 'list',
|
|
520
|
-
echo_request: 0,
|
|
521
|
-
scan_ref: args.scan_ref,
|
|
522
|
-
state: args.state,
|
|
523
|
-
type: args.type,
|
|
524
|
-
target: args.target,
|
|
525
|
-
launched_after_datetime: args.launched_after_datetime,
|
|
526
|
-
launched_before_datetime: args.launched_before_datetime,
|
|
527
|
-
});
|
|
528
|
-
return JSON.stringify(simplify(result.parsed), null, 2);
|
|
529
|
-
}
|
|
530
|
-
case 'qualys_list_reports': {
|
|
531
|
-
const result = await client.request('/api/2.0/fo/report/', {
|
|
532
|
-
action: 'list',
|
|
533
|
-
echo_request: 0,
|
|
534
|
-
id: args.id,
|
|
535
|
-
state: args.state,
|
|
536
|
-
user_login: args.user_login,
|
|
537
|
-
});
|
|
538
|
-
return JSON.stringify(simplify(result.parsed), null, 2);
|
|
539
|
-
}
|
|
540
|
-
case 'qualys_fetch_report': {
|
|
541
|
-
const result = await client.request('/api/2.0/fo/report/', {
|
|
542
|
-
action: 'fetch',
|
|
543
|
-
id: args.id,
|
|
544
|
-
});
|
|
545
|
-
const maxChars = args.max_chars ?? 100_000;
|
|
546
|
-
const content = result.text ?? JSON.stringify(simplify(result.parsed), null, 2);
|
|
547
|
-
if (content.length > maxChars) {
|
|
548
|
-
return content.slice(0, maxChars) + `\n\n[Truncated: ${content.length} total characters, showing first ${maxChars}]`;
|
|
549
|
-
}
|
|
550
|
-
return content;
|
|
551
|
-
}
|
|
552
|
-
case 'qualys_list_asset_groups': {
|
|
553
|
-
const result = await client.request('/api/2.0/fo/asset/group/', {
|
|
554
|
-
action: 'list',
|
|
555
|
-
echo_request: 0,
|
|
556
|
-
ids: args.ids,
|
|
557
|
-
show_attributes: args.show_attributes ?? 'ID,TITLE',
|
|
558
|
-
truncation_limit: args.truncation_limit ?? 200,
|
|
559
|
-
id_min: args.id_min,
|
|
560
|
-
});
|
|
561
|
-
return JSON.stringify(simplify(result.parsed), null, 2);
|
|
562
|
-
}
|
|
563
426
|
case 'qualys_api_call': {
|
|
427
|
+
if (!rawApiEnabled) {
|
|
428
|
+
throw new Error('The raw API tool is disabled. Set QUALYS_ENABLE_RAW_API=true to enable it.');
|
|
429
|
+
}
|
|
564
430
|
const path = args.path;
|
|
565
431
|
if (!path.startsWith('/'))
|
|
566
432
|
throw new Error('path must start with /');
|
|
@@ -598,7 +464,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
598
464
|
async function main() {
|
|
599
465
|
const transport = new StdioServerTransport();
|
|
600
466
|
await server.connect(transport);
|
|
601
|
-
console.error(`Qualys MCP server running (${baseUrl})`);
|
|
467
|
+
console.error(`Qualys MCP server running (${baseUrl}, raw API ${rawApiEnabled ? 'enabled' : 'disabled'})`);
|
|
602
468
|
}
|
|
603
469
|
main().catch((error) => {
|
|
604
470
|
console.error('Fatal error:', error);
|
package/package.json
CHANGED