qualys-cli 0.1.1__py3-none-any.whl
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/__init__.py +1 -0
- qualys_cli/__main__.py +8 -0
- qualys_cli/audit.py +616 -0
- qualys_cli/auth.py +187 -0
- qualys_cli/cache.py +123 -0
- qualys_cli/cli.py +1168 -0
- qualys_cli/client.py +1043 -0
- qualys_cli/commands/__init__.py +0 -0
- qualys_cli/commands/asset.py +183 -0
- qualys_cli/commands/ca.py +403 -0
- qualys_cli/commands/cs.py +410 -0
- qualys_cli/commands/csam.py +752 -0
- qualys_cli/commands/etm.py +170 -0
- qualys_cli/commands/pc.py +255 -0
- qualys_cli/commands/pm.py +412 -0
- qualys_cli/commands/scanauth.py +291 -0
- qualys_cli/commands/sub.py +163 -0
- qualys_cli/commands/tc.py +539 -0
- qualys_cli/commands/user.py +104 -0
- qualys_cli/commands/vm.py +562 -0
- qualys_cli/commands/was.py +1278 -0
- qualys_cli/config.py +331 -0
- qualys_cli/extras.py +702 -0
- qualys_cli/flair.py +202 -0
- qualys_cli/formatters.py +896 -0
- qualys_cli/history.py +133 -0
- qualys_cli/http_server.py +126 -0
- qualys_cli/mcp_server.py +341 -0
- qualys_cli/metrics.py +106 -0
- qualys_cli/platforms.py +67 -0
- qualys_cli/queries.py +137 -0
- qualys_cli-0.1.1.data/data/share/qualys-cli/docs/usage.html +1230 -0
- qualys_cli-0.1.1.dist-info/METADATA +319 -0
- qualys_cli-0.1.1.dist-info/RECORD +37 -0
- qualys_cli-0.1.1.dist-info/WHEEL +4 -0
- qualys_cli-0.1.1.dist-info/entry_points.txt +2 -0
- qualys_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Hand-edited — do NOT regenerate from api-model.json (codegen will skip this file).
|
|
2
|
+
# Enterprise TruRisk Management API (JWT auth, JSON responses)
|
|
3
|
+
# Originally generated from api-model.json; corrected:
|
|
4
|
+
# - report list → POST /reports/list (was incorrect GET /reports)
|
|
5
|
+
# - bulk-delete → DELETE /reports with raw JSON-array body
|
|
6
|
+
# (was incorrect POST /reports/delete with {reportIds:[...]})
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
from .. import client as _c
|
|
12
|
+
from ..formatters import DEFAULT_LIMIT, output, output_success
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(name="etm", help="Enterprise TruRisk Management APIs (JWT Bearer auth)")
|
|
15
|
+
|
|
16
|
+
_report = typer.Typer(help="ETM report operations")
|
|
17
|
+
app.add_typer(_report, name="report")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@_report.command("list")
|
|
21
|
+
def report_list(
|
|
22
|
+
ctx: typer.Context,
|
|
23
|
+
offset: int = typer.Option(0, "--offset", help="Pagination offset (0-based)"),
|
|
24
|
+
page_size: int = typer.Option(50, "--page-size", help="Items per page on the API"),
|
|
25
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
26
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
27
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
28
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
29
|
+
):
|
|
30
|
+
"""List active ETM reports.
|
|
31
|
+
|
|
32
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/list_of_active_reports.htm
|
|
33
|
+
"""
|
|
34
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
35
|
+
data = client.post_json(
|
|
36
|
+
"/etm/api/rest/v1/reports/list",
|
|
37
|
+
{"offset": offset, "limit": page_size},
|
|
38
|
+
)
|
|
39
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@_report.command("create")
|
|
43
|
+
def report_create(
|
|
44
|
+
ctx: typer.Context,
|
|
45
|
+
report_format: str = typer.Option(..., "--report-format", help="JSON|PARQUET"),
|
|
46
|
+
name: str | None = typer.Option(None, "--name"),
|
|
47
|
+
description: str | None = typer.Option(None, "--description"),
|
|
48
|
+
asset_qql: str | None = typer.Option(None, "--asset-qql", help="Asset QQL filter"),
|
|
49
|
+
findings_qql: str | None = typer.Option(None, "--findings-qql", help="Findings QQL filter"),
|
|
50
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
51
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
52
|
+
):
|
|
53
|
+
"""Submit a finding report request.
|
|
54
|
+
|
|
55
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/submit_finding_report.htm
|
|
56
|
+
"""
|
|
57
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
58
|
+
body: dict = {"reportFormat": report_format}
|
|
59
|
+
if name:
|
|
60
|
+
body["name"] = name
|
|
61
|
+
if description:
|
|
62
|
+
body["description"] = description
|
|
63
|
+
if asset_qql:
|
|
64
|
+
body["assetQql"] = asset_qql
|
|
65
|
+
if findings_qql:
|
|
66
|
+
body["findingsQql"] = findings_qql
|
|
67
|
+
data = client.post_json("/etm/api/rest/v1/reports/findings", body)
|
|
68
|
+
output(data, format=format)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@_report.command("get")
|
|
72
|
+
def report_get(
|
|
73
|
+
ctx: typer.Context,
|
|
74
|
+
report_id: str = typer.Argument(..., help="Report ID"),
|
|
75
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
76
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
77
|
+
):
|
|
78
|
+
"""Get details of a specific ETM report.
|
|
79
|
+
|
|
80
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/get_report_details.htm
|
|
81
|
+
"""
|
|
82
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
83
|
+
data = client.get_json(f"/etm/api/rest/v1/reports/{report_id}")
|
|
84
|
+
output(data, format=format)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@_report.command("download")
|
|
88
|
+
def report_download(
|
|
89
|
+
ctx: typer.Context,
|
|
90
|
+
report_id: str = typer.Argument(..., help="Report ID"),
|
|
91
|
+
output_file: str = typer.Option("etm-report.zip", "--output-file", "-o"),
|
|
92
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
93
|
+
):
|
|
94
|
+
"""Download all resources for an ETM report as a ZIP.
|
|
95
|
+
|
|
96
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/download_all_resources_zip.htm
|
|
97
|
+
"""
|
|
98
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
99
|
+
resp = client.request("GET", f"/etm/api/rest/v1/reports/{report_id}/download")
|
|
100
|
+
from pathlib import Path
|
|
101
|
+
Path(output_file).write_bytes(resp.content)
|
|
102
|
+
output_success(f"Report saved to {output_file} ({len(resp.content):,} bytes)")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@_report.command("download-resource")
|
|
106
|
+
def report_download_resource(
|
|
107
|
+
ctx: typer.Context,
|
|
108
|
+
report_id: str = typer.Argument(..., help="Report ID"),
|
|
109
|
+
resource_name: str = typer.Argument(..., help="Resource name (e.g. part_15097524224131306.json)"),
|
|
110
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
111
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
112
|
+
):
|
|
113
|
+
"""Download a specific resource file from an ETM report.
|
|
114
|
+
|
|
115
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/download_report_using_resource_name.htm
|
|
116
|
+
"""
|
|
117
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
118
|
+
resp = client.request("GET", f"/etm/api/rest/v1/reports/{report_id}/resources/{resource_name}")
|
|
119
|
+
from pathlib import Path
|
|
120
|
+
out = output_file or resource_name
|
|
121
|
+
Path(out).write_bytes(resp.content)
|
|
122
|
+
output_success(f"Resource saved to {out} ({len(resp.content):,} bytes)")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@_report.command("delete")
|
|
126
|
+
def report_delete(
|
|
127
|
+
ctx: typer.Context,
|
|
128
|
+
report_id: str = typer.Argument(..., help="Report ID"),
|
|
129
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
130
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
131
|
+
):
|
|
132
|
+
"""Delete an ETM report.
|
|
133
|
+
|
|
134
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/delete_report_id.htm
|
|
135
|
+
"""
|
|
136
|
+
if not yes:
|
|
137
|
+
typer.confirm(f"Delete ETM report {report_id}?", abort=True)
|
|
138
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
139
|
+
client.delete_json(f"/etm/api/rest/v1/reports/{report_id}")
|
|
140
|
+
output_success(f"Report {report_id} deleted.")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@_report.command("bulk-delete")
|
|
144
|
+
def report_bulk_delete(
|
|
145
|
+
ctx: typer.Context,
|
|
146
|
+
report_ids: str = typer.Argument(..., help="Comma-separated report IDs"),
|
|
147
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
148
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
149
|
+
):
|
|
150
|
+
"""Bulk delete ETM reports.
|
|
151
|
+
|
|
152
|
+
Source: https://docs.qualys.com/en/etm/latest/mergedProjects/etm_apis/reports/bulk_delete_reports.htm
|
|
153
|
+
"""
|
|
154
|
+
ids = [r.strip() for r in report_ids.split(",") if r.strip()]
|
|
155
|
+
if not yes:
|
|
156
|
+
typer.confirm(f"Delete {len(ids)} ETM report(s)?", abort=True)
|
|
157
|
+
client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
|
|
158
|
+
# Doc requires DELETE /reports with the body as a raw JSON array, not a wrapper object.
|
|
159
|
+
resp = client.request(
|
|
160
|
+
"DELETE", "/etm/api/rest/v1/reports",
|
|
161
|
+
json=ids,
|
|
162
|
+
headers={"Accept": "application/json", "Content-Type": "application/json"},
|
|
163
|
+
)
|
|
164
|
+
if resp.status_code == 204 or not resp.content:
|
|
165
|
+
output_success(f"Deleted {len(ids)} ETM report(s).")
|
|
166
|
+
else:
|
|
167
|
+
try:
|
|
168
|
+
output(resp.json())
|
|
169
|
+
except ValueError:
|
|
170
|
+
output_success(f"Deleted {len(ids)} ETM report(s).")
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# Hand-edited — do NOT regenerate from api-model.json (codegen will skip this file).
|
|
2
|
+
# Policy Compliance API (Basic auth, XML responses)
|
|
3
|
+
# Originally generated from api-model.json; corrections:
|
|
4
|
+
# - policy export: --id (was wrongly --policy-id), supports --title alternative;
|
|
5
|
+
# added --show-user-controls, --show-appendix
|
|
6
|
+
# - policy list: --ids (was sending wrong key policy_ids)
|
|
7
|
+
# - scan launch: --option-id OR --option-title required (was silently optional);
|
|
8
|
+
# added --scanner, --asset-groups, --priority
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from .. import client as _c
|
|
14
|
+
from ..formatters import DEFAULT_LIMIT, output, output_success
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(name="pc", help="Policy Compliance APIs (Basic auth)")
|
|
17
|
+
|
|
18
|
+
# ── scan ─────────────────────────────────────────────────────────────────────
|
|
19
|
+
_scan = typer.Typer(help="PC scan operations")
|
|
20
|
+
app.add_typer(_scan, name="scan")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@_scan.command("list")
|
|
24
|
+
def scan_list(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
state: str | None = typer.Option(None, "--state"),
|
|
27
|
+
launched_after: str | None = typer.Option(None, "--launched-after"),
|
|
28
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
29
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
30
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
31
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
32
|
+
):
|
|
33
|
+
"""List compliance scans.
|
|
34
|
+
|
|
35
|
+
Source: https://docs.qualys.com/en/vm/api/scans/pc_scans/list_pc_scans.htm
|
|
36
|
+
"""
|
|
37
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
38
|
+
params: dict = {"action": "list"}
|
|
39
|
+
if state:
|
|
40
|
+
params["state"] = state
|
|
41
|
+
if launched_after:
|
|
42
|
+
params["launched_after_datetime"] = launched_after
|
|
43
|
+
data = client.get_xml("/api/2.0/fo/scan/compliance/", params)
|
|
44
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@_scan.command("launch")
|
|
48
|
+
def scan_launch(
|
|
49
|
+
ctx: typer.Context,
|
|
50
|
+
title: str = typer.Option(..., "--title"),
|
|
51
|
+
ip: str = typer.Option(..., "--ip"),
|
|
52
|
+
option_id: str | None = typer.Option(
|
|
53
|
+
None, "--option-id", help="Option profile ID (required if --option-title not given)"
|
|
54
|
+
),
|
|
55
|
+
option_title: str | None = typer.Option(
|
|
56
|
+
None, "--option-title", help="Option profile title (required if --option-id not given)"
|
|
57
|
+
),
|
|
58
|
+
scanner_name: str | None = typer.Option(
|
|
59
|
+
None, "--scanner", help="Scanner appliance name(s)"
|
|
60
|
+
),
|
|
61
|
+
asset_groups: str | None = typer.Option(
|
|
62
|
+
None, "--asset-groups", help="Asset group IDs, comma-separated (alternative to --ip)"
|
|
63
|
+
),
|
|
64
|
+
priority: int | None = typer.Option(
|
|
65
|
+
None, "--priority", help="0–9", min=0, max=9
|
|
66
|
+
),
|
|
67
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
68
|
+
):
|
|
69
|
+
"""Launch a compliance scan.
|
|
70
|
+
|
|
71
|
+
The Qualys API requires an option profile to be selected — either by ID
|
|
72
|
+
(--option-id) or by exact title (--option-title). Pass at least one.
|
|
73
|
+
|
|
74
|
+
Source: https://docs.qualys.com/en/vm/api/scans/pc_scans/launch_pc_scan.htm
|
|
75
|
+
"""
|
|
76
|
+
if not option_id and not option_title:
|
|
77
|
+
raise typer.BadParameter(
|
|
78
|
+
"Provide --option-id or --option-title (one is required by the API)."
|
|
79
|
+
)
|
|
80
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
81
|
+
params: dict = {"action": "launch", "scan_title": title, "ip": ip}
|
|
82
|
+
if option_id:
|
|
83
|
+
params["option_id"] = option_id
|
|
84
|
+
if option_title:
|
|
85
|
+
params["option_title"] = option_title
|
|
86
|
+
if scanner_name:
|
|
87
|
+
params["scanner_appliances"] = scanner_name
|
|
88
|
+
if asset_groups:
|
|
89
|
+
params["asset_groups"] = asset_groups
|
|
90
|
+
if priority is not None:
|
|
91
|
+
params["priority"] = str(priority)
|
|
92
|
+
data = client.post_xml("/api/2.0/fo/scan/compliance/", data=params)
|
|
93
|
+
output(data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ── scan-schedule ─────────────────────────────────────────────────────────────
|
|
97
|
+
_sched = typer.Typer(help="Compliance scan schedule operations")
|
|
98
|
+
app.add_typer(_sched, name="scan-schedule")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@_sched.command("list")
|
|
102
|
+
def sched_list(
|
|
103
|
+
ctx: typer.Context,
|
|
104
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
105
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
106
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
107
|
+
):
|
|
108
|
+
"""List compliance scan schedules.
|
|
109
|
+
|
|
110
|
+
Source: https://docs.qualys.com/en/vm/api/scans/pc_schedules/list_compliance_scan_schedules.htm
|
|
111
|
+
"""
|
|
112
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
113
|
+
data = client.get_xml("/api/2.0/fo/schedule/scan/compliance/", {"action": "list"})
|
|
114
|
+
output(data, format=format, limit=limit)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@_sched.command("delete")
|
|
118
|
+
def sched_delete(
|
|
119
|
+
ctx: typer.Context,
|
|
120
|
+
id_: str = typer.Argument(..., help="Schedule ID"),
|
|
121
|
+
yes: bool = typer.Option(False, "--yes", "-y"),
|
|
122
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
123
|
+
):
|
|
124
|
+
"""Delete a compliance scan schedule.
|
|
125
|
+
|
|
126
|
+
Source: https://docs.qualys.com/en/vm/api/scans/pc_schedules/Delete_a_compliance_scan_schedule.htm
|
|
127
|
+
"""
|
|
128
|
+
if not yes:
|
|
129
|
+
typer.confirm(f"Delete schedule {id_}?", abort=True)
|
|
130
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
131
|
+
data = client.post_xml("/api/2.0/fo/schedule/scan/compliance/",
|
|
132
|
+
data={"action": "delete", "id": id_})
|
|
133
|
+
output(data)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# ── policy ────────────────────────────────────────────────────────────────────
|
|
137
|
+
_policy = typer.Typer(help="Policy operations")
|
|
138
|
+
app.add_typer(_policy, name="policy")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@_policy.command("list")
|
|
142
|
+
def policy_list(
|
|
143
|
+
ctx: typer.Context,
|
|
144
|
+
ids: str | None = typer.Option(None, "--ids", help="Comma-separated policy ID(s)"),
|
|
145
|
+
title: str | None = typer.Option(None, "--title"),
|
|
146
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
147
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
148
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
149
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
150
|
+
):
|
|
151
|
+
"""List compliance policies.
|
|
152
|
+
|
|
153
|
+
Source: https://docs.qualys.com/en/vm/api/pc/policies/list_policies.htm
|
|
154
|
+
"""
|
|
155
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
156
|
+
params: dict = {"action": "list"}
|
|
157
|
+
if ids:
|
|
158
|
+
params["ids"] = ids
|
|
159
|
+
if title:
|
|
160
|
+
params["title"] = title
|
|
161
|
+
data = client.get_xml("/api/2.0/fo/compliance/policy/", params)
|
|
162
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@_policy.command("export")
|
|
166
|
+
def policy_export(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
id_: str | None = typer.Argument(None, help="Policy ID (omit if using --title)"),
|
|
169
|
+
title: str | None = typer.Option(None, "--title", help="Look up policy by title instead of ID"),
|
|
170
|
+
show_user_controls: bool = typer.Option(False, "--show-user-controls", help="Include user-defined controls"),
|
|
171
|
+
show_appendix: bool = typer.Option(False, "--show-appendix", help="Include the policy appendix"),
|
|
172
|
+
output_file: str = typer.Option("policy.xml", "--output-file", "-o"),
|
|
173
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
174
|
+
):
|
|
175
|
+
"""Export a policy to XML.
|
|
176
|
+
|
|
177
|
+
Source: https://docs.qualys.com/en/vm/api/pc/policies/policy_export.htm
|
|
178
|
+
"""
|
|
179
|
+
if not id_ and not title:
|
|
180
|
+
raise typer.BadParameter("Provide a policy ID as an argument, or --title.")
|
|
181
|
+
params: dict = {"action": "export"}
|
|
182
|
+
if id_:
|
|
183
|
+
params["id"] = id_
|
|
184
|
+
if title:
|
|
185
|
+
params["title"] = title
|
|
186
|
+
if show_user_controls:
|
|
187
|
+
params["show_user_controls"] = "1"
|
|
188
|
+
if show_appendix:
|
|
189
|
+
params["show_appendix"] = "1"
|
|
190
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
191
|
+
resp = client.request("GET", "/api/2.0/fo/compliance/policy/", params=params)
|
|
192
|
+
from pathlib import Path
|
|
193
|
+
Path(output_file).write_bytes(resp.content)
|
|
194
|
+
output_success(f"Policy exported to {output_file}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ── posture ───────────────────────────────────────────────────────────────────
|
|
198
|
+
_posture = typer.Typer(help="Compliance posture operations")
|
|
199
|
+
app.add_typer(_posture, name="posture")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@_posture.command("list")
|
|
203
|
+
def posture_list(
|
|
204
|
+
ctx: typer.Context,
|
|
205
|
+
policy_id: str = typer.Option(
|
|
206
|
+
..., "--policy-id",
|
|
207
|
+
help="Policy ID to evaluate posture against (required by the API).",
|
|
208
|
+
),
|
|
209
|
+
host_id: str | None = typer.Option(None, "--host-id"),
|
|
210
|
+
control_id: str | None = typer.Option(None, "--control-id"),
|
|
211
|
+
status: str | None = typer.Option(None, "--status", help="Pass|Fail|Error"),
|
|
212
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
213
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
214
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
215
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
216
|
+
):
|
|
217
|
+
"""List compliance posture info.
|
|
218
|
+
|
|
219
|
+
Source: https://docs.qualys.com/en/vm/api/pc/posture/list_posture_info.htm
|
|
220
|
+
"""
|
|
221
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
222
|
+
params: dict = {"action": "list", "policy_id": policy_id}
|
|
223
|
+
if host_id:
|
|
224
|
+
params["host_id"] = host_id
|
|
225
|
+
if control_id:
|
|
226
|
+
params["control_id"] = control_id
|
|
227
|
+
if status:
|
|
228
|
+
params["status"] = status
|
|
229
|
+
data = client.get_xml("/api/2.0/fo/compliance/posture/info/", params)
|
|
230
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# ── option-profile ────────────────────────────────────────────────────────────
|
|
234
|
+
_op = typer.Typer(help="PC option profile operations")
|
|
235
|
+
app.add_typer(_op, name="option-profile")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@_op.command("list")
|
|
239
|
+
def op_list(
|
|
240
|
+
ctx: typer.Context,
|
|
241
|
+
ids: str | None = typer.Option(None, "--ids"),
|
|
242
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
243
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
244
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
245
|
+
):
|
|
246
|
+
"""List PC option profiles.
|
|
247
|
+
|
|
248
|
+
Source: https://docs.qualys.com/en/vm/api/scans/ops/ops_pc/PC_Option_Profile.htm
|
|
249
|
+
"""
|
|
250
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
251
|
+
params: dict = {"action": "list"}
|
|
252
|
+
if ids:
|
|
253
|
+
params["ids"] = ids
|
|
254
|
+
data = client.get_xml("/api/2.0/fo/subscription/option_profile/pc/", params)
|
|
255
|
+
output(data, format=format, limit=limit)
|