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.
@@ -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)