qualys-cli-mcp 0.1.0__tar.gz
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.
- qualys_cli_mcp-0.1.0/.gitignore +10 -0
- qualys_cli_mcp-0.1.0/CHANGELOG.md +30 -0
- qualys_cli_mcp-0.1.0/LICENSE +21 -0
- qualys_cli_mcp-0.1.0/PKG-INFO +219 -0
- qualys_cli_mcp-0.1.0/README.md +186 -0
- qualys_cli_mcp-0.1.0/pyproject.toml +108 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/__init__.py +1 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/__main__.py +4 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/auth.py +110 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/server.py +1028 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/tenant.py +67 -0
- qualys_cli_mcp-0.1.0/qualys_mcp/tools.py +250 -0
- qualys_cli_mcp-0.1.0/tests/test_auth.py +114 -0
- qualys_cli_mcp-0.1.0/tests/test_server_unit.py +212 -0
- qualys_cli_mcp-0.1.0/tests/test_tenant.py +75 -0
- qualys_cli_mcp-0.1.0/tests/test_tools.py +48 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `qualys-cli-mcp` are documented here. Format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres
|
|
5
|
+
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
### Security
|
|
10
|
+
- Block argparse-style abbreviations of the managed `--profile`/`--config`
|
|
11
|
+
flags (e.g. `--prof`, `--conf`) so a caller can no longer override the
|
|
12
|
+
injected credential profile — closes a multi-tenant isolation bypass.
|
|
13
|
+
- Redact credentials and enforce the `QUALYS_MCP_MAX_RESPONSE` size cap on the
|
|
14
|
+
raw (non-JSON) output path, so a verbose `-v`/`-vv` HTTP dump can no longer
|
|
15
|
+
leak a live `Authorization: Bearer …` token or return an unbounded payload.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- PyPI packaging: `pyproject.toml` build metadata, sdist file selection, and a
|
|
19
|
+
ruff/mypy/pytest quality gate.
|
|
20
|
+
- `scripts/publish.sh` for manual, token-based publishing to PyPI/TestPyPI.
|
|
21
|
+
- GitHub Actions `ci.yml` (lint, type-check, test, build) and `release.yml`
|
|
22
|
+
(tag-triggered Trusted Publishing to PyPI).
|
|
23
|
+
- `PUBLISHING.md` release runbook.
|
|
24
|
+
|
|
25
|
+
## [0.1.0]
|
|
26
|
+
|
|
27
|
+
- Initial release: MCP server wrapping `qualys-cli` with two generic tools
|
|
28
|
+
(`qualys_cli`, `qualys_cli_help`) plus 16 curated typed tools, per-request
|
|
29
|
+
module/write validation, OAuth scope checks, multi-tenant support, and an
|
|
30
|
+
append-only audit log.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 qualys-cli-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qualys-cli-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server that wraps qualys-cli — gives any LLM access to the Qualys Cloud Platform
|
|
5
|
+
Project-URL: Homepage, https://github.com/qualys/qualys-cli-mcp
|
|
6
|
+
Project-URL: Documentation, https://github.com/qualys/qualys-cli-mcp#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/qualys/qualys-cli-mcp/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/qualys/qualys-cli-mcp/blob/main/CHANGELOG.md
|
|
9
|
+
Author: qualys-cli-mcp contributors
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: llm,mcp,model-context-protocol,qualys,security,vulnerability-management
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Information Technology
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
20
|
+
Classifier: Operating System :: POSIX
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Security
|
|
27
|
+
Classifier: Topic :: System :: Systems Administration
|
|
28
|
+
Classifier: Topic :: Utilities
|
|
29
|
+
Requires-Python: >=3.11
|
|
30
|
+
Requires-Dist: fastmcp>=2.0
|
|
31
|
+
Requires-Dist: qualys-cli>=0.1.1
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# qualys-cli-mcp
|
|
35
|
+
|
|
36
|
+
MCP server that gives any LLM access to the Qualys Cloud Security Platform via [qualys-cli](https://stash.intranet.qualys.com/projects/AET/repos/qualys-cli).
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
LLM (Claude, GPT, Llama, etc.)
|
|
42
|
+
│ MCP protocol (stdio or SSE)
|
|
43
|
+
▼
|
|
44
|
+
qualys-cli-mcp (this server — 2 tools)
|
|
45
|
+
│ subprocess
|
|
46
|
+
▼
|
|
47
|
+
qualys-cli (auth, retry, pagination, 273 API endpoints)
|
|
48
|
+
│ HTTPS
|
|
49
|
+
▼
|
|
50
|
+
Qualys Cloud Platform
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The MCP server is a thin pass-through. It executes `qualys-cli` commands as subprocesses and returns parsed JSON. All authentication, retry logic, pagination, and error handling is done by `qualys-cli`.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install qualys-cli-mcp
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This automatically pulls `qualys-cli` as a dependency.
|
|
62
|
+
|
|
63
|
+
Or install from source:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install git+https://stash.intranet.qualys.com/scm/aet/qualys-cli-mcp.git
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Configure Credentials
|
|
70
|
+
|
|
71
|
+
Credentials are stored securely in the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager). No environment variables or plain-text config files needed.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
qualys-cli configure
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This prompts for your username and password, auto-detects your Qualys platform, and stores everything securely. One-time setup.
|
|
78
|
+
|
|
79
|
+
## Usage with Claude Desktop
|
|
80
|
+
|
|
81
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"mcpServers": {
|
|
86
|
+
"qualys": {
|
|
87
|
+
"command": "qualys-mcp"
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
No credentials in the config — they're pulled from the OS keyring at runtime.
|
|
94
|
+
|
|
95
|
+
## Usage with VS Code / Cursor
|
|
96
|
+
|
|
97
|
+
Add to your MCP settings:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"qualys": {
|
|
103
|
+
"command": "qualys-mcp"
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Tools
|
|
110
|
+
|
|
111
|
+
The server exposes 2 tools:
|
|
112
|
+
|
|
113
|
+
### `qualys_cli(command)`
|
|
114
|
+
|
|
115
|
+
Execute any qualys-cli command and get JSON back.
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
qualys_cli("vm scan list --state Running --format json")
|
|
119
|
+
qualys_cli("vm host detection --ips 10.0.0.1 --severities 4,5 --format json")
|
|
120
|
+
qualys_cli("csam asset list --filter asset.riskScore=GREATER=900 --format json --limit 10")
|
|
121
|
+
qualys_cli("pm job list --format json")
|
|
122
|
+
qualys_cli("was finding list --severity 5 --format json")
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### `qualys_cli_help()`
|
|
126
|
+
|
|
127
|
+
Get the full command reference — all modules, resources, actions, common patterns, and gotchas.
|
|
128
|
+
|
|
129
|
+
## Supported Modules
|
|
130
|
+
|
|
131
|
+
| Module | Coverage | Auth |
|
|
132
|
+
|--------|----------|------|
|
|
133
|
+
| VM (Vulnerability Management) | Scans, hosts, detections, KB, appliances, reports | Basic |
|
|
134
|
+
| PC (Policy Compliance) | Scans, policies, posture | Basic |
|
|
135
|
+
| PM (Patch Management) | Jobs, patches, mitigations, reports | JWT |
|
|
136
|
+
| ETM (Enterprise TruRisk) | Reports, findings | JWT |
|
|
137
|
+
| WAS (Web App Scanning) | Webapps, scans, findings, schedules, reports | Basic |
|
|
138
|
+
| TC (TotalCloud / CSPM) | Connectors, evaluations, rules, CDR | JWT |
|
|
139
|
+
| CA (Cloud Agent) | Agents, activation keys, config profiles | Basic/JWT |
|
|
140
|
+
| CS (Container Security) | Images, containers, registries, sensors | JWT |
|
|
141
|
+
| CSAM (Asset Management) | Assets, EASM, vulnerabilities, domains | JWT |
|
|
142
|
+
| Asset | IPs, networks, groups | Basic |
|
|
143
|
+
| ScanAuth | Auth records, vaults | Basic |
|
|
144
|
+
| User | User management | Basic |
|
|
145
|
+
| Sub | Subscription config | Basic |
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
All optional — the server works with zero configuration after `qualys-cli configure`.
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Description |
|
|
152
|
+
|----------|---------|-------------|
|
|
153
|
+
| `QUALYS_PROFILE` | `default` | qualys-cli profile name |
|
|
154
|
+
| `QUALYS_CLI_PATH` | auto-detected | Path to qualys-cli binary |
|
|
155
|
+
| `QUALYS_MCP_TIMEOUT` | `120` | Command timeout (seconds) |
|
|
156
|
+
| `QUALYS_MCP_MAX_RESPONSE` | `800000` | Max response bytes before truncation |
|
|
157
|
+
| `QUALYS_MCP_MAX_RETRIES` | `2` | Retries on transient failures (rate limit, timeout, 5xx) |
|
|
158
|
+
| `QUALYS_MCP_ALLOWED_MODULES` | all | Comma-separated module whitelist |
|
|
159
|
+
| `QUALYS_MCP_DENY_WRITE` | `false` | `1` to block destructive commands |
|
|
160
|
+
| `QUALYS_MCP_AUDIT_LOG` | `~/.config/qualys-cli/mcp-audit.jsonl` | Audit log path (`off` to disable) |
|
|
161
|
+
|
|
162
|
+
## Security Controls
|
|
163
|
+
|
|
164
|
+
### Command Injection Prevention
|
|
165
|
+
|
|
166
|
+
All commands are validated before execution:
|
|
167
|
+
- Shell metacharacters (`;`, `|`, `` ` ``, `$`, `{}`, etc.) are blocked
|
|
168
|
+
- Only known qualys-cli modules are accepted
|
|
169
|
+
- Commands executed via explicit argv list — never through a shell
|
|
170
|
+
|
|
171
|
+
### Credential Security
|
|
172
|
+
|
|
173
|
+
- Credentials stored in OS keyring only — never in env vars, config files, or MCP client config
|
|
174
|
+
- Error messages scrubbed before returning to LLM (passwords, tokens, Bearer values redacted)
|
|
175
|
+
- ANSI escape codes stripped from all error output
|
|
176
|
+
|
|
177
|
+
### Module Whitelist
|
|
178
|
+
|
|
179
|
+
Restrict which Qualys modules are accessible:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
export QUALYS_MCP_ALLOWED_MODULES="vm,csam,ca"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Read-Only Mode
|
|
186
|
+
|
|
187
|
+
Block all write/destructive operations:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
export QUALYS_MCP_DENY_WRITE=1
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Blocked actions: `launch`, `create`, `delete`, `cancel`, `update`, `import`,
|
|
194
|
+
`activate`, `deactivate`, `uninstall`, `purge`, `enable`, `disable`, etc.
|
|
195
|
+
|
|
196
|
+
### Response Truncation
|
|
197
|
+
|
|
198
|
+
Responses exceeding 800KB are automatically truncated to stay under the MCP 1MB transport limit. Key fields (QID, TITLE, SEVERITY, CVE_LIST) are always preserved. Verbose fields (SOLUTION, DIAGNOSIS, CONSEQUENCE) are stripped first.
|
|
199
|
+
|
|
200
|
+
### Resilience
|
|
201
|
+
|
|
202
|
+
- Automatic retry on transient failures (rate limit, timeout, server errors) with exponential backoff
|
|
203
|
+
- Configurable command timeout (default 120s) with process kill on expiry
|
|
204
|
+
- Append-only audit log of every tool call
|
|
205
|
+
|
|
206
|
+
### Inherited from qualys-cli
|
|
207
|
+
|
|
208
|
+
- Exponential backoff with jitter on 429/5xx
|
|
209
|
+
- JWT auto-refresh on 401
|
|
210
|
+
- HMAC-signed audit log with tamper detection
|
|
211
|
+
- OS keyring credential storage
|
|
212
|
+
- Secret redaction in all logs
|
|
213
|
+
- Correlation IDs on every request
|
|
214
|
+
- Body size guards (250MB cap)
|
|
215
|
+
- Idempotent-only retries (POST/PUT/PATCH never auto-retry)
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# qualys-cli-mcp
|
|
2
|
+
|
|
3
|
+
MCP server that gives any LLM access to the Qualys Cloud Security Platform via [qualys-cli](https://stash.intranet.qualys.com/projects/AET/repos/qualys-cli).
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
LLM (Claude, GPT, Llama, etc.)
|
|
9
|
+
│ MCP protocol (stdio or SSE)
|
|
10
|
+
▼
|
|
11
|
+
qualys-cli-mcp (this server — 2 tools)
|
|
12
|
+
│ subprocess
|
|
13
|
+
▼
|
|
14
|
+
qualys-cli (auth, retry, pagination, 273 API endpoints)
|
|
15
|
+
│ HTTPS
|
|
16
|
+
▼
|
|
17
|
+
Qualys Cloud Platform
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The MCP server is a thin pass-through. It executes `qualys-cli` commands as subprocesses and returns parsed JSON. All authentication, retry logic, pagination, and error handling is done by `qualys-cli`.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install qualys-cli-mcp
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This automatically pulls `qualys-cli` as a dependency.
|
|
29
|
+
|
|
30
|
+
Or install from source:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install git+https://stash.intranet.qualys.com/scm/aet/qualys-cli-mcp.git
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Configure Credentials
|
|
37
|
+
|
|
38
|
+
Credentials are stored securely in the OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager). No environment variables or plain-text config files needed.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
qualys-cli configure
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
This prompts for your username and password, auto-detects your Qualys platform, and stores everything securely. One-time setup.
|
|
45
|
+
|
|
46
|
+
## Usage with Claude Desktop
|
|
47
|
+
|
|
48
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"qualys": {
|
|
54
|
+
"command": "qualys-mcp"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No credentials in the config — they're pulled from the OS keyring at runtime.
|
|
61
|
+
|
|
62
|
+
## Usage with VS Code / Cursor
|
|
63
|
+
|
|
64
|
+
Add to your MCP settings:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"qualys": {
|
|
70
|
+
"command": "qualys-mcp"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Tools
|
|
77
|
+
|
|
78
|
+
The server exposes 2 tools:
|
|
79
|
+
|
|
80
|
+
### `qualys_cli(command)`
|
|
81
|
+
|
|
82
|
+
Execute any qualys-cli command and get JSON back.
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
qualys_cli("vm scan list --state Running --format json")
|
|
86
|
+
qualys_cli("vm host detection --ips 10.0.0.1 --severities 4,5 --format json")
|
|
87
|
+
qualys_cli("csam asset list --filter asset.riskScore=GREATER=900 --format json --limit 10")
|
|
88
|
+
qualys_cli("pm job list --format json")
|
|
89
|
+
qualys_cli("was finding list --severity 5 --format json")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### `qualys_cli_help()`
|
|
93
|
+
|
|
94
|
+
Get the full command reference — all modules, resources, actions, common patterns, and gotchas.
|
|
95
|
+
|
|
96
|
+
## Supported Modules
|
|
97
|
+
|
|
98
|
+
| Module | Coverage | Auth |
|
|
99
|
+
|--------|----------|------|
|
|
100
|
+
| VM (Vulnerability Management) | Scans, hosts, detections, KB, appliances, reports | Basic |
|
|
101
|
+
| PC (Policy Compliance) | Scans, policies, posture | Basic |
|
|
102
|
+
| PM (Patch Management) | Jobs, patches, mitigations, reports | JWT |
|
|
103
|
+
| ETM (Enterprise TruRisk) | Reports, findings | JWT |
|
|
104
|
+
| WAS (Web App Scanning) | Webapps, scans, findings, schedules, reports | Basic |
|
|
105
|
+
| TC (TotalCloud / CSPM) | Connectors, evaluations, rules, CDR | JWT |
|
|
106
|
+
| CA (Cloud Agent) | Agents, activation keys, config profiles | Basic/JWT |
|
|
107
|
+
| CS (Container Security) | Images, containers, registries, sensors | JWT |
|
|
108
|
+
| CSAM (Asset Management) | Assets, EASM, vulnerabilities, domains | JWT |
|
|
109
|
+
| Asset | IPs, networks, groups | Basic |
|
|
110
|
+
| ScanAuth | Auth records, vaults | Basic |
|
|
111
|
+
| User | User management | Basic |
|
|
112
|
+
| Sub | Subscription config | Basic |
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
All optional — the server works with zero configuration after `qualys-cli configure`.
|
|
117
|
+
|
|
118
|
+
| Variable | Default | Description |
|
|
119
|
+
|----------|---------|-------------|
|
|
120
|
+
| `QUALYS_PROFILE` | `default` | qualys-cli profile name |
|
|
121
|
+
| `QUALYS_CLI_PATH` | auto-detected | Path to qualys-cli binary |
|
|
122
|
+
| `QUALYS_MCP_TIMEOUT` | `120` | Command timeout (seconds) |
|
|
123
|
+
| `QUALYS_MCP_MAX_RESPONSE` | `800000` | Max response bytes before truncation |
|
|
124
|
+
| `QUALYS_MCP_MAX_RETRIES` | `2` | Retries on transient failures (rate limit, timeout, 5xx) |
|
|
125
|
+
| `QUALYS_MCP_ALLOWED_MODULES` | all | Comma-separated module whitelist |
|
|
126
|
+
| `QUALYS_MCP_DENY_WRITE` | `false` | `1` to block destructive commands |
|
|
127
|
+
| `QUALYS_MCP_AUDIT_LOG` | `~/.config/qualys-cli/mcp-audit.jsonl` | Audit log path (`off` to disable) |
|
|
128
|
+
|
|
129
|
+
## Security Controls
|
|
130
|
+
|
|
131
|
+
### Command Injection Prevention
|
|
132
|
+
|
|
133
|
+
All commands are validated before execution:
|
|
134
|
+
- Shell metacharacters (`;`, `|`, `` ` ``, `$`, `{}`, etc.) are blocked
|
|
135
|
+
- Only known qualys-cli modules are accepted
|
|
136
|
+
- Commands executed via explicit argv list — never through a shell
|
|
137
|
+
|
|
138
|
+
### Credential Security
|
|
139
|
+
|
|
140
|
+
- Credentials stored in OS keyring only — never in env vars, config files, or MCP client config
|
|
141
|
+
- Error messages scrubbed before returning to LLM (passwords, tokens, Bearer values redacted)
|
|
142
|
+
- ANSI escape codes stripped from all error output
|
|
143
|
+
|
|
144
|
+
### Module Whitelist
|
|
145
|
+
|
|
146
|
+
Restrict which Qualys modules are accessible:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
export QUALYS_MCP_ALLOWED_MODULES="vm,csam,ca"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Read-Only Mode
|
|
153
|
+
|
|
154
|
+
Block all write/destructive operations:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
export QUALYS_MCP_DENY_WRITE=1
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Blocked actions: `launch`, `create`, `delete`, `cancel`, `update`, `import`,
|
|
161
|
+
`activate`, `deactivate`, `uninstall`, `purge`, `enable`, `disable`, etc.
|
|
162
|
+
|
|
163
|
+
### Response Truncation
|
|
164
|
+
|
|
165
|
+
Responses exceeding 800KB are automatically truncated to stay under the MCP 1MB transport limit. Key fields (QID, TITLE, SEVERITY, CVE_LIST) are always preserved. Verbose fields (SOLUTION, DIAGNOSIS, CONSEQUENCE) are stripped first.
|
|
166
|
+
|
|
167
|
+
### Resilience
|
|
168
|
+
|
|
169
|
+
- Automatic retry on transient failures (rate limit, timeout, server errors) with exponential backoff
|
|
170
|
+
- Configurable command timeout (default 120s) with process kill on expiry
|
|
171
|
+
- Append-only audit log of every tool call
|
|
172
|
+
|
|
173
|
+
### Inherited from qualys-cli
|
|
174
|
+
|
|
175
|
+
- Exponential backoff with jitter on 429/5xx
|
|
176
|
+
- JWT auto-refresh on 401
|
|
177
|
+
- HMAC-signed audit log with tamper detection
|
|
178
|
+
- OS keyring credential storage
|
|
179
|
+
- Secret redaction in all logs
|
|
180
|
+
- Correlation IDs on every request
|
|
181
|
+
- Body size guards (250MB cap)
|
|
182
|
+
- Idempotent-only retries (POST/PUT/PATCH never auto-retry)
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
MIT
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "qualys-cli-mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MCP server that wraps qualys-cli — gives any LLM access to the Qualys Cloud Platform"
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["qualys", "mcp", "model-context-protocol", "llm", "security", "vulnerability-management"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "qualys-cli-mcp contributors" },
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: System Administrators",
|
|
20
|
+
"Intended Audience :: Information Technology",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Operating System :: POSIX",
|
|
23
|
+
"Operating System :: MacOS",
|
|
24
|
+
"Operating System :: Microsoft :: Windows",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.11",
|
|
28
|
+
"Programming Language :: Python :: 3.12",
|
|
29
|
+
"Programming Language :: Python :: 3.13",
|
|
30
|
+
"Topic :: Security",
|
|
31
|
+
"Topic :: System :: Systems Administration",
|
|
32
|
+
"Topic :: Utilities",
|
|
33
|
+
]
|
|
34
|
+
dependencies = [
|
|
35
|
+
"fastmcp>=2.0",
|
|
36
|
+
"qualys-cli>=0.1.1",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.scripts]
|
|
40
|
+
qualys-mcp = "qualys_mcp.server:main"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/qualys/qualys-cli-mcp"
|
|
44
|
+
Documentation = "https://github.com/qualys/qualys-cli-mcp#readme"
|
|
45
|
+
Issues = "https://github.com/qualys/qualys-cli-mcp/issues"
|
|
46
|
+
Changelog = "https://github.com/qualys/qualys-cli-mcp/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"pytest>=8",
|
|
51
|
+
"pytest-cov>=5",
|
|
52
|
+
"ruff>=0.4",
|
|
53
|
+
"mypy>=1.10",
|
|
54
|
+
"build>=1.2",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.wheel]
|
|
58
|
+
packages = ["qualys_mcp"]
|
|
59
|
+
|
|
60
|
+
[tool.hatch.build.targets.sdist]
|
|
61
|
+
include = [
|
|
62
|
+
"qualys_mcp",
|
|
63
|
+
"tests",
|
|
64
|
+
"README.md",
|
|
65
|
+
"CHANGELOG.md",
|
|
66
|
+
"LICENSE",
|
|
67
|
+
"pyproject.toml",
|
|
68
|
+
]
|
|
69
|
+
exclude = [
|
|
70
|
+
".claude",
|
|
71
|
+
"config",
|
|
72
|
+
"docs",
|
|
73
|
+
"evals",
|
|
74
|
+
"scripts",
|
|
75
|
+
"PUBLISHING.md",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
[tool.ruff]
|
|
79
|
+
line-length = 100
|
|
80
|
+
target-version = "py311"
|
|
81
|
+
extend-exclude = [".venv", "scripts", "tests/__pycache__"]
|
|
82
|
+
|
|
83
|
+
[tool.ruff.lint]
|
|
84
|
+
select = ["E", "F", "I", "UP", "B", "C4", "SIM"]
|
|
85
|
+
ignore = [
|
|
86
|
+
"B904", # raise ... from err — not always meaningful for pass-through errors
|
|
87
|
+
"SIM108", # ternary instead of if-else — sometimes if-else is clearer
|
|
88
|
+
"SIM102", # nested if — sometimes clearer than a combined condition
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.lint.per-file-ignores]
|
|
92
|
+
# Help text, log strings, and long dict literals push past the line limit;
|
|
93
|
+
# readability wins over reflowing them.
|
|
94
|
+
"qualys_mcp/*.py" = ["E501"]
|
|
95
|
+
"tests/*.py" = ["E501"]
|
|
96
|
+
|
|
97
|
+
[tool.mypy]
|
|
98
|
+
python_version = "3.11"
|
|
99
|
+
files = ["qualys_mcp"]
|
|
100
|
+
|
|
101
|
+
[[tool.mypy.overrides]]
|
|
102
|
+
# Third-party libs without type stubs
|
|
103
|
+
module = ["fastmcp", "fastmcp.*"]
|
|
104
|
+
ignore_missing_imports = true
|
|
105
|
+
|
|
106
|
+
[tool.pytest.ini_options]
|
|
107
|
+
testpaths = ["tests"]
|
|
108
|
+
addopts = "-ra --strict-markers"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""qualys-mcp — MCP server that wraps qualys-cli as a pass-through tool."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# qualys_mcp/auth.py
|
|
2
|
+
"""Authentication providers for multi-tenant qualys-mcp.
|
|
3
|
+
|
|
4
|
+
Two modes:
|
|
5
|
+
1. API keys (StaticTokenVerifier) - simple, good for server-to-server
|
|
6
|
+
2. OAuth 2.1 (JWTVerifier) - validates JWT Bearer tokens from an external IdP
|
|
7
|
+
|
|
8
|
+
Both use FastMCP's built-in auth infrastructure. Inside tool functions,
|
|
9
|
+
call `get_access_token()` to get the authenticated client's identity.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger("qualys-mcp")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_api_key_verifier(
|
|
21
|
+
keys: dict[str, dict[str, Any]],
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""Create a StaticTokenVerifier for API key authentication.
|
|
24
|
+
|
|
25
|
+
Note: StaticTokenVerifier stores keys in plain text in memory. The security
|
|
26
|
+
boundary is the config file itself (which should be file-permission restricted).
|
|
27
|
+
For deployments requiring hashed key storage, replace with a custom TokenVerifier.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
keys: mapping of API key string to {"client_id": str, "scopes": list[str]}
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
A FastMCP StaticTokenVerifier that validates Bearer tokens against the key set.
|
|
34
|
+
"""
|
|
35
|
+
from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
|
|
36
|
+
|
|
37
|
+
token_map: dict[str, dict[str, Any]] = {}
|
|
38
|
+
for key_value, meta in keys.items():
|
|
39
|
+
token_map[key_value] = {
|
|
40
|
+
"client_id": meta["client_id"],
|
|
41
|
+
"scopes": meta.get("scopes", []),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
verifier = StaticTokenVerifier(tokens=token_map)
|
|
45
|
+
log.info("API key auth: %d keys configured", len(token_map))
|
|
46
|
+
return verifier
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def create_oauth_provider(
|
|
50
|
+
issuer_url: str,
|
|
51
|
+
audience: str,
|
|
52
|
+
algorithm: str | None = None,
|
|
53
|
+
) -> Any:
|
|
54
|
+
"""Create a JWTVerifier for OAuth 2.1 token validation.
|
|
55
|
+
|
|
56
|
+
Validates Bearer tokens (JWTs) issued by the configured authorization server.
|
|
57
|
+
Uses JWKS discovery from the issuer's .well-known endpoint.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
issuer_url: OAuth issuer URL (e.g. "https://auth.cisco.com")
|
|
61
|
+
audience: Expected audience claim in tokens
|
|
62
|
+
algorithm: Allowed signing algorithm (default: "RS256")
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A FastMCP JWTVerifier that validates JWTs against the issuer's JWKS.
|
|
66
|
+
"""
|
|
67
|
+
from fastmcp.server.auth.providers.jwt import JWTVerifier
|
|
68
|
+
|
|
69
|
+
verifier = JWTVerifier(
|
|
70
|
+
jwks_uri=f"{issuer_url.rstrip('/')}/.well-known/jwks.json",
|
|
71
|
+
issuer=issuer_url,
|
|
72
|
+
audience=audience,
|
|
73
|
+
algorithm=algorithm or "RS256",
|
|
74
|
+
)
|
|
75
|
+
log.info("OAuth auth: issuer=%s audience=%s", issuer_url, audience)
|
|
76
|
+
return verifier
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def load_api_keys_from_tenant_config(config_path: str) -> dict[str, dict[str, Any]]:
|
|
80
|
+
"""Load API keys from the tenant config file.
|
|
81
|
+
|
|
82
|
+
Reads [api_keys] section:
|
|
83
|
+
[api_keys.<name>]
|
|
84
|
+
key = "bearer-token-value"
|
|
85
|
+
client_id = "tenant-client-id"
|
|
86
|
+
scopes = ["qualys:read"]
|
|
87
|
+
"""
|
|
88
|
+
import tomllib
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
|
|
91
|
+
path = Path(config_path)
|
|
92
|
+
if not path.is_file():
|
|
93
|
+
return {}
|
|
94
|
+
|
|
95
|
+
with open(path, "rb") as f:
|
|
96
|
+
data = tomllib.load(f)
|
|
97
|
+
|
|
98
|
+
keys: dict[str, dict[str, Any]] = {}
|
|
99
|
+
for name, cfg in data.get("api_keys", {}).items():
|
|
100
|
+
for required in ("key", "client_id"):
|
|
101
|
+
if required not in cfg:
|
|
102
|
+
raise ValueError(f"api_keys.{name} missing required field {required!r}")
|
|
103
|
+
key_value = cfg["key"]
|
|
104
|
+
if key_value in keys:
|
|
105
|
+
log.warning("duplicate API key in config (api_keys.%s overwrites previous)", name)
|
|
106
|
+
keys[key_value] = {
|
|
107
|
+
"client_id": cfg["client_id"],
|
|
108
|
+
"scopes": cfg.get("scopes", ["qualys:read"]),
|
|
109
|
+
}
|
|
110
|
+
return keys
|