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,412 @@
1
+ # Auto-generated from api-model.json — run scripts/generate_commands.py to regenerate.
2
+ # Patch Management API (JWT auth, JSON responses)
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .. import client as _c
8
+ from ..formatters import DEFAULT_LIMIT, output, output_success
9
+
10
+ app = typer.Typer(name="pm", help="Patch Management APIs (JWT Bearer auth)")
11
+
12
+ # ── job ─────────────────────────────────────────────────────────────────────
13
+ _job = typer.Typer(help="Deployment job operations")
14
+ app.add_typer(_job, name="job")
15
+
16
+
17
+ @_job.command("list")
18
+ def job_list(
19
+ ctx: typer.Context,
20
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
21
+ all_: bool = typer.Option(False, "--all", help="Fetch all pages"),
22
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
23
+ format: str = typer.Option("table", "--format", "-f", help="table|json|yaml"),
24
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
25
+ ):
26
+ """List deployment jobs.
27
+
28
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/get_deployment_job_list.htm
29
+ """
30
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
31
+ if all_:
32
+ rows: list = []
33
+ for page in client.paginate_json("/pm/v1/deploymentjobs"):
34
+ items = page if isinstance(page, list) else page.get("data", [page])
35
+ rows.extend(items)
36
+ data: object = rows
37
+ else:
38
+ data = client.get_json("/pm/v1/deploymentjobs", {"pageSize": limit})
39
+ output(data, format=format, limit=None if all_ else limit, output_file=output_file)
40
+
41
+
42
+ @_job.command("get")
43
+ def job_get(
44
+ ctx: typer.Context,
45
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
46
+ format: str = typer.Option("table", "--format", "-f"),
47
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
48
+ ):
49
+ """Get a deployment job by ID.
50
+
51
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/get_deployment_job_by_id.htm
52
+ """
53
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
54
+ data = client.get_json(f"/pm/v1/deploymentjob/{job_id}")
55
+ output(data, format=format)
56
+
57
+
58
+ @_job.command("create")
59
+ def job_create(
60
+ ctx: typer.Context,
61
+ name: str = typer.Option(..., "--name", help="Job name"),
62
+ schedule_type: str = typer.Option(..., "--schedule-type", help="On-demand|Once|Daily|Weekly|Monthly"),
63
+ platform: str = typer.Option("Windows", "--platform", help="Windows|Linux"),
64
+ start_date_time: str | None = typer.Option(None, "--start", help="Start datetime YYYY-MM-DD HH:MM:SS"),
65
+ tag_ids: str | None = typer.Option(None, "--tag-ids", help="Comma-separated tag UUIDs"),
66
+ format: str = typer.Option("table", "--format", "-f"),
67
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
68
+ ):
69
+ """Create a deployment job.
70
+
71
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/create_deployment_job.htm
72
+ """
73
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
74
+ body: dict = {"name": name, "scheduleType": schedule_type, "platform": platform}
75
+ if start_date_time:
76
+ body["startDateTime"] = start_date_time
77
+ if tag_ids:
78
+ body["tagIds"] = [t.strip() for t in tag_ids.split(",")]
79
+ data = client.post_json("/pm/v1/deploymentjob", body)
80
+ output(data, format=format)
81
+
82
+
83
+ @_job.command("update")
84
+ def job_update(
85
+ ctx: typer.Context,
86
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
87
+ name: str | None = typer.Option(None, "--name"),
88
+ schedule_type: str | None = typer.Option(None, "--schedule-type"),
89
+ format: str = typer.Option("table", "--format", "-f"),
90
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
91
+ ):
92
+ """Update a deployment job.
93
+
94
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/update_deployment_jobs.htm
95
+ """
96
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
97
+ body: dict = {}
98
+ if name:
99
+ body["name"] = name
100
+ if schedule_type:
101
+ body["scheduleType"] = schedule_type
102
+ data = client.patch_json(f"/pm/v1/deploymentjob/update/{job_id}", body)
103
+ output(data, format=format)
104
+
105
+
106
+ @_job.command("delete")
107
+ def job_delete(
108
+ ctx: typer.Context,
109
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
110
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
111
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
112
+ ):
113
+ """Delete a deployment job.
114
+
115
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/delete_deployment_jobs.htm
116
+ """
117
+ if not yes:
118
+ typer.confirm(f"Delete deployment job {job_id}?", abort=True)
119
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
120
+ client.delete_json(f"/pm/v1/deploymentjobs/{job_id}")
121
+ output_success(f"Deployment job {job_id} deleted.")
122
+
123
+
124
+ @_job.command("change-status")
125
+ def job_change_status(
126
+ ctx: typer.Context,
127
+ job_ids: str = typer.Argument(..., help="Comma-separated deployment job IDs"),
128
+ action: str = typer.Argument(..., help="Enabled|Disabled"),
129
+ format: str = typer.Option("table", "--format", "-f"),
130
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
131
+ ):
132
+ """Enable or disable one or more deployment jobs.
133
+
134
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/change_the_status_of_deployment_jobs.htm
135
+ """
136
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
137
+ ids = [j.strip() for j in job_ids.split(",") if j.strip()]
138
+ data = client.post_json(
139
+ f"/pm/v1/deploymentjobs/update/status/{action}", {"jobIds": ids}
140
+ )
141
+ output(data, format=format)
142
+
143
+
144
+ @_job.command("runs")
145
+ def job_runs(
146
+ ctx: typer.Context,
147
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
148
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
149
+ format: str = typer.Option("table", "--format", "-f"),
150
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
151
+ ):
152
+ """List runs for a deployment job.
153
+
154
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/get_list_of_runs_for_particular_deployment_job.htm
155
+ """
156
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
157
+ data = client.get_json(f"/pm/v1/deploymentjob/{job_id}/runs")
158
+ output(data, format=format, limit=limit)
159
+
160
+
161
+ @_job.command("summary")
162
+ def job_summary(
163
+ ctx: typer.Context,
164
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
165
+ format: str = typer.Option("table", "--format", "-f"),
166
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
167
+ ):
168
+ """Get result summary for a deployment job.
169
+
170
+ Source: https://docs.qualys.com/en/pm/api/deployment_job_resource/get_deployment_job_result_summary.htm
171
+ """
172
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
173
+ data = client.get_json(
174
+ f"/pm/v1/deploymentjob/{job_id}/deploymentjobresult/summary"
175
+ )
176
+ output(data, format=format)
177
+
178
+
179
+ # ── mitigation ──────────────────────────────────────────────────────────────
180
+ _mtg = typer.Typer(help="Mitigation job operations")
181
+ app.add_typer(_mtg, name="mitigation")
182
+
183
+
184
+ @_mtg.command("list")
185
+ def mitigation_list(
186
+ ctx: typer.Context,
187
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
188
+ format: str = typer.Option("table", "--format", "-f"),
189
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
190
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
191
+ ):
192
+ """List mitigation deployment jobs.
193
+
194
+ Source: https://docs.qualys.com/en/pm/api/mitigation_apis/get_list_of_all_mitigation_jobs.htm
195
+ """
196
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
197
+ # The mitigation jobs endpoint is under the same /pm/v1/deploymentjobs path
198
+ # but filtered by type. Try /pm/v1/deploymentjobs with type filter first.
199
+ data = client.get_json("/pm/v1/deploymentjobs", {"type": "Mitigate"})
200
+ output(data, format=format, limit=limit, output_file=output_file)
201
+
202
+
203
+ @_mtg.command("create")
204
+ def mitigation_create(
205
+ ctx: typer.Context,
206
+ name: str = typer.Option(..., "--name"),
207
+ schedule_type: str = typer.Option(..., "--schedule-type"),
208
+ format: str = typer.Option("table", "--format", "-f"),
209
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
210
+ ):
211
+ """Create a mitigation job.
212
+
213
+ Source: https://docs.qualys.com/en/pm/api/mitigation_apis/create_mitigation_job.htm
214
+ """
215
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
216
+ data = client.post_json("/pm/v1/deploymentjob", {"name": name, "scheduleType": schedule_type, "type": "Mitigate"})
217
+ output(data, format=format)
218
+
219
+
220
+ @_mtg.command("delete")
221
+ def mitigation_delete(
222
+ ctx: typer.Context,
223
+ job_id: str = typer.Argument(...),
224
+ yes: bool = typer.Option(False, "--yes", "-y"),
225
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
226
+ ):
227
+ """Delete mitigation jobs.
228
+
229
+ Source: https://docs.qualys.com/en/pm/api/mitigation_apis/delete_mitigation_jobs.htm
230
+ """
231
+ if not yes:
232
+ typer.confirm(f"Delete mitigation job {job_id}?", abort=True)
233
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
234
+ client.delete_json("/pm/v1/deploymentjobs", params={"ids": job_id})
235
+ output_success(f"Mitigation job {job_id} deleted.")
236
+
237
+
238
+ # ── patch ────────────────────────────────────────────────────────────────────
239
+ _patch = typer.Typer(help="Patch catalog operations")
240
+ app.add_typer(_patch, name="patch")
241
+
242
+
243
+ @_patch.command("list")
244
+ def patch_list(
245
+ ctx: typer.Context,
246
+ platform: str | None = typer.Option(None, "--platform", help="Windows|Linux"),
247
+ severity: str | None = typer.Option(None, "--severity", help="Critical|Important|Moderate|Low"),
248
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
249
+ all_: bool = typer.Option(False, "--all"),
250
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
251
+ format: str = typer.Option("table", "--format", "-f"),
252
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
253
+ ):
254
+ """List patches from the patch catalog.
255
+
256
+ Source: https://docs.qualys.com/en/pm/api/patch_catalog_resource/get_patch_catalog_patches.htm
257
+ """
258
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
259
+ params: dict = {}
260
+ if platform:
261
+ params["platform"] = platform
262
+ if severity:
263
+ params["severityList"] = severity
264
+ if all_:
265
+ rows: list = []
266
+ for page in client.paginate_json("/pm/v1/patchcatalog/patches", params):
267
+ items = page if isinstance(page, list) else page.get("data", [page])
268
+ rows.extend(items)
269
+ output(rows, format=format, output_file=output_file)
270
+ else:
271
+ params["pageSize"] = limit
272
+ data = client.get_json("/pm/v1/patchcatalog/patches", params)
273
+ output(data, format=format, limit=limit, output_file=output_file)
274
+
275
+
276
+ @_patch.command("count")
277
+ def patch_count(
278
+ ctx: typer.Context,
279
+ platform: str | None = typer.Option(None, "--platform"),
280
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
281
+ format: str = typer.Option("table", "--format", "-f"),
282
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
283
+ ):
284
+ """Get patch count.
285
+
286
+ Source: https://docs.qualys.com/en/pm/api/scan_result_patch_resource/get_patch_count.htm
287
+ """
288
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
289
+ params: dict = {}
290
+ if platform:
291
+ params["platform"] = platform
292
+ data = client.get_json("/pm/v1/patches/count", params)
293
+ output(data, format=format, output_file=output_file)
294
+
295
+
296
+ @_patch.command("products")
297
+ def patch_products(
298
+ ctx: typer.Context,
299
+ patch_id: str = typer.Argument(..., help="Patch ID to list products for"),
300
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
301
+ format: str = typer.Option("table", "--format", "-f"),
302
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
303
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
304
+ ):
305
+ """List products affected by a specific patch.
306
+
307
+ Source: https://docs.qualys.com/en/pm/api/patch_catalog_resource/get_patch_catalog_products.htm
308
+ """
309
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
310
+ data = client.get_json(f"/pm/v1/patchcatalog/patch/{patch_id}/products")
311
+ output(data, format=format, limit=limit, output_file=output_file)
312
+
313
+
314
+ # ── report ───────────────────────────────────────────────────────────────────
315
+ _report = typer.Typer(help="Patch Management report operations")
316
+ app.add_typer(_report, name="report")
317
+
318
+
319
+ @_report.command("list")
320
+ def report_list(
321
+ ctx: typer.Context,
322
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
323
+ format: str = typer.Option("table", "--format", "-f"),
324
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
325
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
326
+ ):
327
+ """List generated PM reports.
328
+
329
+ Source: https://docs.qualys.com/en/pm/api/patch_report_resource/get_list_of_generated_reports.htm
330
+ """
331
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
332
+ data = client.get_json("/pm/v1/reports")
333
+ output(data, format=format, limit=limit, output_file=output_file)
334
+
335
+
336
+ @_report.command("generate")
337
+ def report_generate(
338
+ ctx: typer.Context,
339
+ job_id: str = typer.Argument(..., help="Deployment job ID"),
340
+ report_format: str = typer.Option("CSV", "--report-format", help="CSV|PDF"),
341
+ name: str | None = typer.Option(None, "--name"),
342
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
343
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
344
+ ):
345
+ """Generate a deployment job progress report.
346
+
347
+ Source: https://docs.qualys.com/en/pm/api/patch_report_resource/generate_patch_insights_report.htm
348
+ """
349
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
350
+ body: dict = {"reportFormat": report_format}
351
+ if name:
352
+ body["name"] = name
353
+ data = client.post_json(f"/pm/v1/report/deplymentjobprogress/{job_id}", body)
354
+ output(data, output_file=output_file)
355
+
356
+
357
+ @_report.command("download")
358
+ def report_download(
359
+ ctx: typer.Context,
360
+ report_id: str = typer.Argument(..., help="Report ID"),
361
+ output_file: str = typer.Option("report.csv", "--output-file", "-o", help="Save path"),
362
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
363
+ ):
364
+ """Download a generated report to a local file.
365
+
366
+ Source: https://docs.qualys.com/en/pm/api/patch_report_resource/get_report_in_csv_format.htm
367
+ """
368
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
369
+ resp = client.request("GET", f"/pm/v1/reports/{report_id}/download")
370
+ from pathlib import Path
371
+ Path(output_file).write_bytes(resp.content)
372
+ from ..formatters import output_success
373
+ output_success(f"Report saved to {output_file} ({len(resp.content):,} bytes)")
374
+
375
+
376
+ @_report.command("columns")
377
+ def report_columns(
378
+ ctx: typer.Context,
379
+ platform: str = typer.Option(..., "--platform", help="Windows|Linux"),
380
+ report_section: str = typer.Option(..., "--report-section", help="Asset|Patch|JobProgress"),
381
+ format: str = typer.Option("table", "--format", "-f"),
382
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
383
+ ):
384
+ """Fetch available report columns.
385
+
386
+ Source: https://docs.qualys.com/en/pm/api/patch_report_resource/get_list_of_report_columns.htm
387
+ """
388
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
389
+ data = client.get_json("/pm/v1/report/columns", {"platform": platform, "reportSection": report_section})
390
+ output(data, format=format)
391
+
392
+
393
+ # ── asset ────────────────────────────────────────────────────────────────────
394
+ _asset = typer.Typer(help="PM asset operations")
395
+ app.add_typer(_asset, name="asset")
396
+
397
+
398
+ @_asset.command("list")
399
+ def asset_list(
400
+ ctx: typer.Context,
401
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
402
+ format: str = typer.Option("table", "--format", "-f"),
403
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
404
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
405
+ ):
406
+ """List PM-managed assets.
407
+
408
+ Source: https://docs.qualys.com/en/pm/api/scan_result_asset_resource/get_pm_assets.htm
409
+ """
410
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
411
+ data = client.get_json("/pm/v1/assets", {"pageSize": limit})
412
+ output(data, format=format, limit=limit, output_file=output_file)