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,410 @@
1
+ # Auto-generated from api-model.json — run scripts/generate_commands.py to regenerate.
2
+ # Container Security APIs (JWT Bearer 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="cs", help="Container Security APIs — images, containers, registries, sensors, reports")
11
+
12
+ # ── image ─────────────────────────────────────────────────────────────────────
13
+ _image = typer.Typer(help="Container image operations")
14
+ app.add_typer(_image, name="image")
15
+
16
+
17
+ @_image.command("list")
18
+ def image_list(
19
+ ctx: typer.Context,
20
+ filter: str | None = typer.Option(None, "--filter", help="Qualys QQL filter"),
21
+ page_number: int = typer.Option(1, "--page-number"),
22
+ page_size: int = typer.Option(50, "--page-size"),
23
+ sort: str | None = typer.Option(None, "--server-sort", help="Server-side sort, e.g. created:desc. Use --sort/--sort-by for client-side post-processing."),
24
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
25
+ format: str = typer.Option("table", "--format", "-f"),
26
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
27
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
28
+ ):
29
+ """List images in the account (max 10,000).
30
+
31
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/images/fetch_a_list_of_images_in_your_account.htm
32
+ """
33
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
34
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
35
+ if filter:
36
+ params["filter"] = filter
37
+ if sort:
38
+ params["sort"] = sort
39
+ data = client.get_json("/csapi/v1.3/images", params)
40
+ output(data, format=format, limit=limit, output_file=output_file)
41
+
42
+
43
+ @_image.command("get")
44
+ def image_get(
45
+ ctx: typer.Context,
46
+ image_sha: str = typer.Argument(..., help="Image SHA value"),
47
+ scan_details: str | None = typer.Option(None, "--scan-details", help="malware,secrets,compliance"),
48
+ format: str = typer.Option("table", "--format", "-f"),
49
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
50
+ ):
51
+ """Get details of a specific image.
52
+
53
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/images/fetch_image_details.htm
54
+ """
55
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
56
+ params: dict = {}
57
+ if scan_details:
58
+ params["scanDetails"] = scan_details
59
+ data = client.get_json(f"/csapi/v1.3/images/{image_sha}", params)
60
+ output(data, format=format)
61
+
62
+
63
+ @_image.command("vulns")
64
+ def image_vulns(
65
+ ctx: typer.Context,
66
+ image_sha: str = typer.Argument(..., help="Image SHA value"),
67
+ filter: str | None = typer.Option(None, "--filter"),
68
+ page_number: int = typer.Option(1, "--page-number"),
69
+ page_size: int = typer.Option(50, "--page-size"),
70
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
71
+ format: str = typer.Option("table", "--format", "-f"),
72
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
73
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
74
+ ):
75
+ """Get vulnerability details for an image.
76
+
77
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/images/fetch_vulnerability_details_for_an_image.htm
78
+ """
79
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
80
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
81
+ if filter:
82
+ params["filter"] = filter
83
+ data = client.get_json(f"/csapi/v1.3/images/{image_sha}/vuln", params)
84
+ output(data, format=format, limit=limit, output_file=output_file)
85
+
86
+
87
+ @_image.command("software")
88
+ def image_software(
89
+ ctx: typer.Context,
90
+ image_sha: str = typer.Argument(..., help="Image SHA value"),
91
+ page_number: int = typer.Option(1, "--page-number"),
92
+ page_size: int = typer.Option(50, "--page-size"),
93
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
94
+ format: str = typer.Option("table", "--format", "-f"),
95
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
96
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
97
+ ):
98
+ """List software installed on an image.
99
+
100
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/images/fetch_a_list_of_software_installed_on_an_image.htm
101
+ """
102
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
103
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
104
+ data = client.get_json(f"/csapi/v1.3/images/{image_sha}/software", params)
105
+ output(data, format=format, limit=limit, output_file=output_file)
106
+
107
+
108
+ @_image.command("delete")
109
+ def image_delete(
110
+ ctx: typer.Context,
111
+ filter: str = typer.Option(..., "--filter", help="QQL filter to select images to delete"),
112
+ yes: bool = typer.Option(False, "--yes", "-y"),
113
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
114
+ ):
115
+ """Delete images matching a filter.
116
+
117
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/images/delete_images_in_your_account.htm
118
+ """
119
+ if not yes:
120
+ typer.confirm("Delete images matching filter?", abort=True)
121
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
122
+ client.post_json("/csapi/v1.3/images/delete", {"filter": filter})
123
+ output_success(f"Deleted images matching filter '{filter}'.")
124
+
125
+
126
+ # ── container ─────────────────────────────────────────────────────────────────
127
+ _container = typer.Typer(help="Container operations")
128
+ app.add_typer(_container, name="container")
129
+
130
+
131
+ @_container.command("list")
132
+ def container_list(
133
+ ctx: typer.Context,
134
+ filter: str | None = typer.Option(None, "--filter", help="Qualys QQL filter"),
135
+ page_number: int = typer.Option(1, "--page-number"),
136
+ page_size: int = typer.Option(50, "--page-size"),
137
+ sort: str | None = typer.Option(None, "--server-sort", help="Server-side sort (passed to API). Use --sort/--sort-by for client-side."),
138
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
139
+ format: str = typer.Option("table", "--format", "-f"),
140
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
141
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
142
+ ):
143
+ """List containers in the account.
144
+
145
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/containers/fetch_a_list_of_containers_in_your_account.htm
146
+ """
147
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
148
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
149
+ if filter:
150
+ params["filter"] = filter
151
+ if sort:
152
+ params["sort"] = sort
153
+ data = client.get_json("/csapi/v1.3/containers", params)
154
+ output(data, format=format, limit=limit, output_file=output_file)
155
+
156
+
157
+ @_container.command("get")
158
+ def container_get(
159
+ ctx: typer.Context,
160
+ container_sha: str = typer.Argument(..., help="Container SHA"),
161
+ format: str = typer.Option("table", "--format", "-f"),
162
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
163
+ ):
164
+ """Get details of a specific container.
165
+
166
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/containers/fetch_container_details.htm
167
+ """
168
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
169
+ data = client.get_json(f"/csapi/v1.3/containers/{container_sha}")
170
+ output(data, format=format)
171
+
172
+
173
+ @_container.command("vulns")
174
+ def container_vulns(
175
+ ctx: typer.Context,
176
+ container_sha: str = typer.Argument(..., help="Container SHA"),
177
+ filter: str | None = typer.Option(None, "--filter"),
178
+ page_number: int = typer.Option(1, "--page-number"),
179
+ page_size: int = typer.Option(50, "--page-size"),
180
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
181
+ format: str = typer.Option("table", "--format", "-f"),
182
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
183
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
184
+ ):
185
+ """Get vulnerability details for a container.
186
+
187
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/containers/fetch_vulnerability_details_for_a_container.htm
188
+ """
189
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
190
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
191
+ if filter:
192
+ params["filter"] = filter
193
+ data = client.get_json(f"/csapi/v1.3/containers/{container_sha}/vuln", params)
194
+ output(data, format=format, limit=limit, output_file=output_file)
195
+
196
+
197
+ @_container.command("software")
198
+ def container_software(
199
+ ctx: typer.Context,
200
+ container_sha: str = typer.Argument(..., help="Container SHA"),
201
+ page_number: int = typer.Option(1, "--page-number"),
202
+ page_size: int = typer.Option(50, "--page-size"),
203
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
204
+ format: str = typer.Option("table", "--format", "-f"),
205
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
206
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
207
+ ):
208
+ """List software installed on a container.
209
+
210
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/containers/fetch_a_list_of_software_installed_on_a_container.htm
211
+ """
212
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
213
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
214
+ data = client.get_json(f"/csapi/v1.3/containers/{container_sha}/software", params)
215
+ output(data, format=format, limit=limit, output_file=output_file)
216
+
217
+
218
+ # ── registry ──────────────────────────────────────────────────────────────────
219
+ _registry = typer.Typer(help="Registry operations")
220
+ app.add_typer(_registry, name="registry")
221
+
222
+
223
+ @_registry.command("list")
224
+ def registry_list(
225
+ ctx: typer.Context,
226
+ filter: str | None = typer.Option(None, "--filter"),
227
+ page_number: int = typer.Option(1, "--page-number"),
228
+ page_size: int = typer.Option(50, "--page-size"),
229
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
230
+ format: str = typer.Option("table", "--format", "-f"),
231
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
232
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
233
+ ):
234
+ """List registries.
235
+
236
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/registries/fetch_a_list_of_registries_in_your_account.htm
237
+ """
238
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
239
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
240
+ if filter:
241
+ params["filter"] = filter
242
+ data = client.get_json("/csapi/v1.3/registry", params)
243
+ output(data, format=format, limit=limit, output_file=output_file)
244
+
245
+
246
+ @_registry.command("get")
247
+ def registry_get(
248
+ ctx: typer.Context,
249
+ registry_uuid: str = typer.Argument(..., help="Registry UUID"),
250
+ format: str = typer.Option("table", "--format", "-f"),
251
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
252
+ ):
253
+ """Get registry details.
254
+
255
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/registries/fetch_registry_details.htm
256
+ """
257
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
258
+ data = client.get_json(f"/csapi/v1.3/registry/{registry_uuid}")
259
+ output(data, format=format)
260
+
261
+
262
+ @_registry.command("delete")
263
+ def registry_delete(
264
+ ctx: typer.Context,
265
+ registry_uuids: str = typer.Argument(..., help="Comma-separated registry UUIDs"),
266
+ yes: bool = typer.Option(False, "--yes", "-y"),
267
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
268
+ ):
269
+ """Delete one or more registries.
270
+
271
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/registries/delete_registries_in_your_account.htm
272
+ """
273
+ if not yes:
274
+ typer.confirm("Delete registry/registries?", abort=True)
275
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
276
+ uuids = [u.strip() for u in registry_uuids.split(",")]
277
+ client.post_json("/csapi/v1.3/registry/delete", {"registryUuids": uuids})
278
+ output_success(f"Deleted {len(uuids)} registry/registries.")
279
+
280
+
281
+ # ── sensor ────────────────────────────────────────────────────────────────────
282
+ _sensor = typer.Typer(help="Sensor operations")
283
+ app.add_typer(_sensor, name="sensor")
284
+
285
+
286
+ @_sensor.command("list")
287
+ def sensor_list(
288
+ ctx: typer.Context,
289
+ filter: str | None = typer.Option(None, "--filter"),
290
+ page_number: int = typer.Option(1, "--page-number"),
291
+ page_size: int = typer.Option(50, "--page-size"),
292
+ sort: str | None = typer.Option(None, "--server-sort", help="Server-side sort (passed to API). Use --sort/--sort-by for client-side."),
293
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
294
+ format: str = typer.Option("table", "--format", "-f"),
295
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
296
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
297
+ ):
298
+ """List sensors.
299
+
300
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/sensors/fetch_a_list_of_sensors_in_your_account.htm
301
+ """
302
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
303
+ params: dict = {"pageNumber": page_number, "pageSize": page_size}
304
+ if filter:
305
+ params["filter"] = filter
306
+ if sort:
307
+ params["sort"] = sort
308
+ data = client.get_json("/csapi/v1.3/sensors", params)
309
+ output(data, format=format, limit=limit, output_file=output_file)
310
+
311
+
312
+ @_sensor.command("get")
313
+ def sensor_get(
314
+ ctx: typer.Context,
315
+ sensor_uuid: str = typer.Argument(..., help="Sensor UUID"),
316
+ format: str = typer.Option("table", "--format", "-f"),
317
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
318
+ ):
319
+ """Get sensor details.
320
+
321
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/sensors/fetch_sensor_details.htm
322
+ """
323
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
324
+ data = client.get_json(f"/csapi/v1.3/sensors/{sensor_uuid}")
325
+ output(data, format=format)
326
+
327
+
328
+ # ── report ────────────────────────────────────────────────────────────────────
329
+ _report = typer.Typer(help="Vulnerability report operations")
330
+ app.add_typer(_report, name="report")
331
+
332
+
333
+ @_report.command("list")
334
+ def report_list(
335
+ ctx: typer.Context,
336
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
337
+ format: str = typer.Option("table", "--format", "-f"),
338
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
339
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
340
+ ):
341
+ """List vulnerability reports.
342
+
343
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/vulnerability_reports/fetch_a_list_of_reports.htm
344
+ """
345
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
346
+ data = client.get_json("/csapi/v1.3/reports")
347
+ output(data, format=format, limit=limit, output_file=output_file)
348
+
349
+
350
+ @_report.command("create")
351
+ def report_create(
352
+ ctx: typer.Context,
353
+ name: str = typer.Option(..., "--name", help="Report title (max 150 chars)"),
354
+ template_name: str = typer.Option(..., "--template",
355
+ help="CS_IMAGE_VULNERABILITY|CS_CONTAINER_VULNERABILITY|CS_IMAGE_SECRETS|CS_IMAGE_MALWARE"),
356
+ expire_after: str = typer.Option(..., "--expire-after", help="Days until expiry: 1|7|30|90"),
357
+ filter: str | None = typer.Option(None, "--filter", help="QQL filter"),
358
+ description: str | None = typer.Option(None, "--description"),
359
+ format: str = typer.Option("table", "--format", "-f"),
360
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
361
+ ):
362
+ """Create a new vulnerability report request.
363
+
364
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/vulnerability_reports/create_a_report_request.htm
365
+ """
366
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
367
+ body: dict = {"name": name, "templateName": template_name, "expireAfter": expire_after}
368
+ if filter:
369
+ body["filter"] = filter
370
+ if description:
371
+ body["description"] = description
372
+ data = client.post_json("/csapi/v1.3/reports", body)
373
+ output(data, format=format)
374
+
375
+
376
+ @_report.command("download")
377
+ def report_download(
378
+ ctx: typer.Context,
379
+ report_uuid: str = typer.Argument(..., help="Report UUID"),
380
+ output_file: str = typer.Option("cs-report.json", "--output-file", "-o"),
381
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
382
+ ):
383
+ """Download a report in JSON format.
384
+
385
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/vulnerability_reports/fetch_a_report_in_json_format.htm
386
+ """
387
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
388
+ resp = client.request("GET", f"/csapi/v1.3/reports/{report_uuid}/download")
389
+ from pathlib import Path
390
+ Path(output_file).write_bytes(resp.content)
391
+ output_success(f"Report saved to {output_file} ({len(resp.content):,} bytes)")
392
+
393
+
394
+ @_report.command("delete")
395
+ def report_delete(
396
+ ctx: typer.Context,
397
+ report_uuids: str = typer.Argument(..., help="Comma-separated report UUIDs"),
398
+ yes: bool = typer.Option(False, "--yes", "-y"),
399
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
400
+ ):
401
+ """Delete one or more reports.
402
+
403
+ Source: https://docs.qualys.com/en/cs/latest/mergedProjects/container_security_apis/vulnerability_reports/delete_reports.htm
404
+ """
405
+ if not yes:
406
+ typer.confirm("Delete report(s)?", abort=True)
407
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
408
+ uuids = [u.strip() for u in report_uuids.split(",")]
409
+ client.post_json("/csapi/v1.3/reports/delete", {"uuids": uuids})
410
+ output_success(f"Deleted {len(uuids)} report(s).")