qualys-cli 0.1.6__tar.gz → 0.1.7__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-0.1.6 → qualys_cli-0.1.7}/CHANGELOG.md +14 -1
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/PKG-INFO +1 -1
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/pyproject.toml +1 -1
- qualys_cli-0.1.7/src/qualys_cli/__init__.py +1 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/_docs/usage.html +2 -2
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/client.py +9 -8
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_features.py +18 -0
- qualys_cli-0.1.6/src/qualys_cli/__init__.py +0 -1
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/.gitignore +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/LICENSE +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/README.md +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/__main__.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/audit.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/auth.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/cache.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/cli.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/__init__.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/asset.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/ca.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/cs.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/csam.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/etm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/pc.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/pm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/scanauth.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/sub.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/tc.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/user.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/vm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/commands/was.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/config.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/extras.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/flair.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/formatters.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/history.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/http_server.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/mcp_server.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/metrics.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/platforms.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/src/qualys_cli/queries.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/__init__.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/conftest.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_agent_mode.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_all_commands_smoke.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_asset.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_audit.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_ca.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_client.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_csam.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_error_handling.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_etm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_flair.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_formatters.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_global_flags.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_help.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_high_fixes.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_main.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_med_fixes.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_pc.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_pm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_resiliency.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_review_fixes.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_scanauth.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_security_fixes.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_sub.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_tc.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_user.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_vm.py +0 -0
- {qualys_cli-0.1.6 → qualys_cli-0.1.7}/tests/test_was.py +0 -0
|
@@ -6,6 +6,18 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.7] — 2026-07-17
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `--dry-run` / `--explain` now renders JSON **array** request bodies instead
|
|
13
|
+
of dropping them to `null`. Commands that send a bare JSON array — e.g.
|
|
14
|
+
`etm report bulk-delete`, which `DELETE`s `/reports` with a raw
|
|
15
|
+
`["id", …]` body — previously showed `"body": null` in the explain output.
|
|
16
|
+
Because that JSON is the documented contract parsed by the `mcp`, `serve`,
|
|
17
|
+
and `batch` consumers, the actual payload was invisible to them and to
|
|
18
|
+
anyone auditing a request before sending it. Array bodies are now shown
|
|
19
|
+
(and secret-redacted) in both the human panel and the machine-readable JSON.
|
|
20
|
+
|
|
9
21
|
## [0.1.6] — 2026-07-17
|
|
10
22
|
|
|
11
23
|
### Security
|
|
@@ -217,7 +229,8 @@ Initial public release.
|
|
|
217
229
|
- Ruff lint clean (E, F, I, UP, B, C4, SIM rule sets).
|
|
218
230
|
- Codegen pipeline (`scripts/`) is opt-in via the `codegen` extra.
|
|
219
231
|
|
|
220
|
-
[Unreleased]: https://github.com/qualys/qualys-cli/compare/v0.1.
|
|
232
|
+
[Unreleased]: https://github.com/qualys/qualys-cli/compare/v0.1.6...HEAD
|
|
233
|
+
[0.1.6]: https://github.com/qualys/qualys-cli/compare/v0.1.5...v0.1.6
|
|
221
234
|
[0.1.5]: https://github.com/qualys/qualys-cli/compare/v0.1.4...v0.1.5
|
|
222
235
|
[0.1.4]: https://github.com/qualys/qualys-cli/compare/v0.1.2...v0.1.4
|
|
223
236
|
[0.1.2]: https://github.com/qualys/qualys-cli/compare/v0.1.1...v0.1.2
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qualys-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: Unified command-line interface for the Qualys Cloud Platform REST APIs (VM, PC, PM, ETM, WAS, TotalCloud, Cloud Agent, Container Security, CSAM, Subscription, Asset, and User management).
|
|
5
5
|
Project-URL: Homepage, https://www.qualys.com
|
|
6
6
|
Author-email: Qualys <support@qualys.com>
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "qualys-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.7"
|
|
8
8
|
description = "Unified command-line interface for the Qualys Cloud Platform REST APIs (VM, PC, PM, ETM, WAS, TotalCloud, Cloud Agent, Container Security, CSAM, Subscription, Asset, and User management)."
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = { file = "LICENSE" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.7"
|
|
@@ -612,7 +612,7 @@ body.filtering.no-match section.module{display:none}
|
|
|
612
612
|
<aside class="sidebar" aria-label="Documentation navigation">
|
|
613
613
|
<div class="side-header">
|
|
614
614
|
<img src="https://ik.imagekit.io/qualys/image/logo/qualys.svg" alt="Qualys">
|
|
615
|
-
<span class="ver">v0.1.
|
|
615
|
+
<span class="ver">v0.1.7</span>
|
|
616
616
|
</div>
|
|
617
617
|
<div class="side-search">
|
|
618
618
|
<input type="search" id="filter" placeholder="Search commands…"
|
|
@@ -696,7 +696,7 @@ body.filtering.no-match section.module{display:none}
|
|
|
696
696
|
<div class="hero-stats">
|
|
697
697
|
<div class="hero-stat"><span class="n">28</span><span class="l">Modules</span></div>
|
|
698
698
|
<div class="hero-stat"><span class="n">228</span><span class="l">Commands</span></div>
|
|
699
|
-
<div class="hero-stat"><span class="n">v0.1.
|
|
699
|
+
<div class="hero-stat"><span class="n">v0.1.7</span><span class="l">Version</span></div>
|
|
700
700
|
<div class="hero-stat"><span class="n">3.12+</span><span class="l">Python</span></div>
|
|
701
701
|
<div class="hero-stat"><span class="n">MIT</span><span class="l">License</span></div>
|
|
702
702
|
</div>
|
|
@@ -408,11 +408,12 @@ class QualysClient:
|
|
|
408
408
|
url fully-qualified request URL (profile + path)
|
|
409
409
|
auth "basic" or "jwt"
|
|
410
410
|
params query string as a JSON object (redacted)
|
|
411
|
-
body request body normalised to a JSON object
|
|
412
|
-
possible. If the original body was a
|
|
413
|
-
parse it back so callers see structure,
|
|
414
|
-
If the body is non-JSON (XML,
|
|
415
|
-
surfaced as a string preview.
|
|
411
|
+
body request body normalised to a JSON value (object or
|
|
412
|
+
array) whenever possible. If the original body was a
|
|
413
|
+
JSON string we parse it back so callers see structure,
|
|
414
|
+
not a string. If the body is non-JSON (XML,
|
|
415
|
+
form-encoded), it is surfaced as a string preview.
|
|
416
|
+
``null`` if no body.
|
|
416
417
|
headers request headers (Authorization redacted)
|
|
417
418
|
|
|
418
419
|
This contract is the public surface that MCP / serve / batch
|
|
@@ -432,7 +433,7 @@ class QualysClient:
|
|
|
432
433
|
|
|
433
434
|
# Redact secrets in the rendered body / params
|
|
434
435
|
params = _audit.redact_mapping(dict(kwargs.get("params") or {}))
|
|
435
|
-
if isinstance(body, dict):
|
|
436
|
+
if isinstance(body, (dict, list)):
|
|
436
437
|
body = _audit.redact_mapping(body)
|
|
437
438
|
elif isinstance(body, str):
|
|
438
439
|
# Try to round-trip a string body back into a dict — ``post_xml``
|
|
@@ -457,7 +458,7 @@ class QualysClient:
|
|
|
457
458
|
lines.append(f"[dim]auth:[/dim] {self.auth_type}")
|
|
458
459
|
if params:
|
|
459
460
|
lines.append(f"[dim]params:[/dim] {_json.dumps(params, default=str)}")
|
|
460
|
-
if isinstance(body, dict):
|
|
461
|
+
if isinstance(body, (dict, list)):
|
|
461
462
|
lines.append(f"[dim]body:[/dim] {_json.dumps(body, default=str, indent=2)}")
|
|
462
463
|
elif isinstance(body, str):
|
|
463
464
|
preview = body if len(body) < 400 else body[:400] + "…"
|
|
@@ -468,7 +469,7 @@ class QualysClient:
|
|
|
468
469
|
if not _console.is_terminal:
|
|
469
470
|
payload = {
|
|
470
471
|
"method": method, "url": url, "auth": self.auth_type,
|
|
471
|
-
"params": params, "body": body if isinstance(body, (dict, str)) else None,
|
|
472
|
+
"params": params, "body": body if isinstance(body, (dict, list, str)) else None,
|
|
472
473
|
"headers": hdrs,
|
|
473
474
|
}
|
|
474
475
|
print(_json.dumps(payload, default=str))
|
|
@@ -180,6 +180,24 @@ def test_dry_run_via_cli(monkeypatch):
|
|
|
180
180
|
assert not bad.called
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
@respx.mock
|
|
184
|
+
def test_dry_run_renders_list_body(monkeypatch):
|
|
185
|
+
"""A JSON *array* request body (e.g. `etm report bulk-delete`) must appear
|
|
186
|
+
in the --explain output, not be flattened to null.
|
|
187
|
+
|
|
188
|
+
The dry-run JSON is the contract MCP/serve/batch callers parse; a list
|
|
189
|
+
body silently rendered as null hid the actual payload. Regression guard.
|
|
190
|
+
"""
|
|
191
|
+
_env(monkeypatch)
|
|
192
|
+
respx.post(f"{BASE}/auth").mock(return_value=httpx.Response(200, content=b"tok"))
|
|
193
|
+
result = runner.invoke(
|
|
194
|
+
app, ["--dry-run", "etm", "report", "bulk-delete", "101,102,103", "--yes"]
|
|
195
|
+
)
|
|
196
|
+
assert result.exit_code == 0, result.output
|
|
197
|
+
payload = json.loads(result.output.strip().splitlines()[-1])
|
|
198
|
+
assert payload["body"] == ["101", "102", "103"], payload
|
|
199
|
+
|
|
200
|
+
|
|
183
201
|
# ── Audit HMAC chain ────────────────────────────────────────────────────────
|
|
184
202
|
|
|
185
203
|
def test_audit_chain_intact(monkeypatch, tmp_path):
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.6"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|