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.
Files changed (3) hide show
  1. package/README.md +4 -6
  2. package/dist/index.js +8 -142
  3. 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
- | `qualys_list_scans` | Scan history and status |
17
- | `qualys_list_reports` | Generated reports |
18
- | `qualys_fetch_report` | Download a finished CSV/XML report inline |
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 () => ({ tools }));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qualys-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "MCP server for the Qualys VMDR API (read-only) — hosts, vulnerability detections, KnowledgeBase, scans, reports and asset groups",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",