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,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)
|