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.
File without changes
@@ -0,0 +1,183 @@
1
+ # Auto-generated from api-model.json — run scripts/generate_commands.py to regenerate.
2
+ # Asset Management API (Basic auth, XML responses)
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from .. import client as _c
8
+ from ..formatters import DEFAULT_LIMIT, output
9
+
10
+ app = typer.Typer(name="asset", help="Asset Management APIs (Basic auth)")
11
+
12
+ # ── ip ───────────────────────────────────────────────────────────────────────
13
+ _ip = typer.Typer(help="IP address operations")
14
+ app.add_typer(_ip, name="ip")
15
+
16
+
17
+ @_ip.command("list")
18
+ def ip_list(
19
+ ctx: typer.Context,
20
+ ips: str | None = typer.Option(None, "--ips", help="Filter by IP/range"),
21
+ network_id: str | None = typer.Option(None, "--network-id"),
22
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
23
+ format: str = typer.Option("table", "--format", "-f"),
24
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
25
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
26
+ ):
27
+ """List IPs in the subscription.
28
+
29
+ Source: https://docs.qualys.com/en/vm/api/assets/asset_ips/list_ips.htm
30
+ """
31
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
32
+ params: dict = {"action": "list"}
33
+ if ips:
34
+ params["ips"] = ips
35
+ if network_id:
36
+ params["network_id"] = network_id
37
+ data = client.get_xml("/api/2.0/fo/asset/ip/", params)
38
+ output(data, format=format, limit=limit, output_file=output_file)
39
+
40
+
41
+ @_ip.command("add")
42
+ def ip_add(
43
+ ctx: typer.Context,
44
+ ips: str = typer.Argument(..., help="IP(s) or range(s) to add"),
45
+ tracking_method: str = typer.Option("IP", "--tracking", help="IP|DNS|NETBIOS"),
46
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
47
+ ):
48
+ """Add IPs to the subscription.
49
+
50
+ Source: https://docs.qualys.com/en/vm/api/assets/asset_ips/add_ips.htm
51
+ """
52
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
53
+ data = client.post_xml("/api/2.0/fo/asset/ip/",
54
+ data={"action": "add", "ips": ips, "tracking_method": tracking_method})
55
+ output(data)
56
+
57
+
58
+ # ── network ───────────────────────────────────────────────────────────────────
59
+ _network = typer.Typer(help="Network operations")
60
+ app.add_typer(_network, name="network")
61
+
62
+
63
+ @_network.command("list")
64
+ def network_list(
65
+ ctx: typer.Context,
66
+ ids: str | None = typer.Option(None, "--ids"),
67
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
68
+ format: str = typer.Option("table", "--format", "-f"),
69
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
70
+ ):
71
+ """List networks.
72
+
73
+ Source: https://docs.qualys.com/en/vm/api/assets/networks/list_networks.htm
74
+ """
75
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
76
+ params: dict = {"action": "list"}
77
+ if ids:
78
+ params["ids"] = ids
79
+ data = client.get_xml("/api/2.0/fo/network/", params)
80
+ output(data, format=format, limit=limit)
81
+
82
+
83
+ @_network.command("create")
84
+ def network_create(
85
+ ctx: typer.Context,
86
+ name: str = typer.Option(..., "--name"),
87
+ description: str | None = typer.Option(None, "--description"),
88
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
89
+ ):
90
+ """Create a network.
91
+
92
+ Source: https://docs.qualys.com/en/vm/api/assets/networks/create_network.htm
93
+ """
94
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
95
+ params: dict = {"action": "create", "name": name}
96
+ if description:
97
+ params["description"] = description
98
+ data = client.post_xml("/api/2.0/fo/network/", data=params)
99
+ output(data)
100
+
101
+
102
+ # ── domain ────────────────────────────────────────────────────────────────────
103
+ _domain = typer.Typer(help="Asset domain operations")
104
+ app.add_typer(_domain, name="domain")
105
+
106
+
107
+ @_domain.command("list")
108
+ def domain_list(
109
+ ctx: typer.Context,
110
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
111
+ format: str = typer.Option("table", "--format", "-f"),
112
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
113
+ ):
114
+ """List asset domains.
115
+
116
+ Source: https://docs.qualys.com/en/vm/api/scans/maps/domains_list.htm
117
+ """
118
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
119
+ data = client.get_xml("/msp/asset_domain_list.php")
120
+ output(data, format=format, limit=limit)
121
+
122
+
123
+ @_domain.command("create")
124
+ def domain_create(
125
+ ctx: typer.Context,
126
+ name: str = typer.Option(..., "--name"),
127
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
128
+ ):
129
+ """Create an asset domain.
130
+
131
+ Source: https://docs.qualys.com/en/vm/api/assets/domain_v2/Create_Domain.htm
132
+ """
133
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
134
+ data = client.post_xml("/api/2.0/fo/asset/domain/", data={"action": "create", "name": name})
135
+ output(data)
136
+
137
+
138
+ # ── group ─────────────────────────────────────────────────────────────────────
139
+ _group = typer.Typer(help="Asset group operations")
140
+ app.add_typer(_group, name="group")
141
+
142
+
143
+ @_group.command("list")
144
+ def group_list(
145
+ ctx: typer.Context,
146
+ ids: str | None = typer.Option(None, "--ids"),
147
+ title: str | None = typer.Option(None, "--title"),
148
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
149
+ format: str = typer.Option("table", "--format", "-f"),
150
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
151
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
152
+ ):
153
+ """List asset groups.
154
+
155
+ Source: https://docs.qualys.com/en/vm/api/assets/asset_groups/list_asset_groups.htm
156
+ """
157
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
158
+ params: dict = {"action": "list"}
159
+ if ids:
160
+ params["ids"] = ids
161
+ if title:
162
+ params["title"] = title
163
+ data = client.get_xml("/api/2.0/fo/asset/group/", params)
164
+ output(data, format=format, limit=limit, output_file=output_file)
165
+
166
+
167
+ @_group.command("delete")
168
+ def group_delete(
169
+ ctx: typer.Context,
170
+ ids: str = typer.Argument(...),
171
+ yes: bool = typer.Option(False, "--yes", "-y"),
172
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
173
+ ):
174
+ """Delete asset group(s).
175
+
176
+ Source: https://docs.qualys.com/en/vm/api/assets/asset_groups/delete_asset_group.htm
177
+ """
178
+ if not yes:
179
+ typer.confirm(f"Delete asset group(s) {ids}?", abort=True)
180
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
181
+ data = client.post_xml("/api/2.0/fo/asset/group/",
182
+ data={"action": "delete", "ids": ids})
183
+ output(data)
@@ -0,0 +1,403 @@
1
+ # Hand-edited — do NOT regenerate from api-model.json (codegen will skip this file).
2
+ # Cloud Agent APIs (Basic auth for agent ops, JWT for activation-key/config)
3
+ # Originally generated from api-model.json; corrections:
4
+ # - agent count/list: filter is now overridable via --tag and --filter so
5
+ # accounts without the default "Cloud Agent" tag can still get data
6
+ # - bulk-activate/deactivate: path /am/hostasset → /am/asset (per docs)
7
+ # - act-key create: body field renamed `modules` → `licenses` (per docs)
8
+ # - config create: body shape {"name": x} → {"basicDetails": {"profileName": x}}
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="ca", help="Cloud Agent APIs — agent lifecycle, activation keys, config profiles")
17
+
18
+ # ── agent ─────────────────────────────────────────────────────────────────────
19
+ _agent = typer.Typer(help="Cloud agent operations")
20
+ app.add_typer(_agent, name="agent")
21
+
22
+
23
+ def _agent_filter_xml(
24
+ tag: str | None,
25
+ tag_id: str | None,
26
+ tracking_method: str | None,
27
+ extra: list[tuple[str, str, str]] | None = None,
28
+ fields: str | None = None,
29
+ ) -> str:
30
+ """Build a hostasset ServiceRequest body with at least one filter criterion.
31
+
32
+ Defaults to `tagName EQUALS "Cloud Agent"` (Qualys-recommended) when no
33
+ explicit filter is provided. Users can override with --tag (different name),
34
+ --tag-id (more reliable), --tracking-method, or --filter.
35
+ """
36
+ parts: list[str] = [
37
+ '<?xml version="1.0" encoding="UTF-8"?>',
38
+ "<ServiceRequest>",
39
+ "<filters>",
40
+ ]
41
+ if tag_id:
42
+ parts.append(f'<Criteria field="tagId" operator="EQUALS">{tag_id}</Criteria>')
43
+ elif tracking_method:
44
+ parts.append(
45
+ f'<Criteria field="trackingMethod" operator="EQUALS">{tracking_method}</Criteria>'
46
+ )
47
+ elif tag:
48
+ parts.append(f'<Criteria field="tagName" operator="EQUALS">{tag}</Criteria>')
49
+ else:
50
+ parts.append('<Criteria field="tagName" operator="EQUALS">Cloud Agent</Criteria>')
51
+ for f, op, v in (extra or []):
52
+ parts.append(f'<Criteria field="{f}" operator="{op}">{v}</Criteria>')
53
+ parts.append("</filters>")
54
+ if fields:
55
+ parts.append(f"<fields><set>{fields}</set></fields>")
56
+ parts.append("</ServiceRequest>")
57
+ return "\n".join(parts)
58
+
59
+
60
+ @_agent.command("list")
61
+ def agent_list(
62
+ ctx: typer.Context,
63
+ tag: str | None = typer.Option(
64
+ None, "--tag",
65
+ help='Tag NAME filter (default: "Cloud Agent" — override if your account uses a different tag)',
66
+ ),
67
+ tag_id: str | None = typer.Option(
68
+ None, "--tag-id", help="Tag ID filter (more reliable than name)"
69
+ ),
70
+ tracking_method: str | None = typer.Option(
71
+ None, "--tracking-method",
72
+ help="Filter by tracking method (e.g. AGENT) — alternative to --tag",
73
+ ),
74
+ fields: str | None = typer.Option(
75
+ None, "--fields", help="Comma-separated fields to include in the response"
76
+ ),
77
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
78
+ format: str = typer.Option("table", "--format", "-f"),
79
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
80
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
81
+ ):
82
+ """List cloud agents (POST search).
83
+
84
+ Returns assets matching a tag filter. Defaults to the Qualys-standard
85
+ "Cloud Agent" tag; pass --tag, --tag-id, or --tracking-method AGENT if
86
+ your account uses a different identifier.
87
+
88
+ Source: https://docs.qualys.com/en/ca/api/agents/list_agents.htm
89
+ """
90
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
91
+ xml_body = _agent_filter_xml(tag, tag_id, tracking_method, fields=fields)
92
+ data = client.post_xml("/qps/rest/2.0/search/am/hostasset", data=xml_body)
93
+ output(data, format=format, limit=limit, output_file=output_file)
94
+
95
+
96
+ @_agent.command("count")
97
+ def agent_count(
98
+ ctx: typer.Context,
99
+ tag: str | None = typer.Option(
100
+ None, "--tag",
101
+ help='Tag NAME filter (default: "Cloud Agent" — override if your account uses a different tag)',
102
+ ),
103
+ tag_id: str | None = typer.Option(
104
+ None, "--tag-id", help="Tag ID filter (more reliable than name)"
105
+ ),
106
+ tracking_method: str | None = typer.Option(
107
+ None, "--tracking-method",
108
+ help="Filter by tracking method (e.g. AGENT) — alternative to --tag",
109
+ ),
110
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
111
+ format: str = typer.Option("table", "--format", "-f"),
112
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
113
+ ):
114
+ """Get count of cloud agents in the account.
115
+
116
+ Defaults to filtering by the Qualys-standard "Cloud Agent" tag. If your
117
+ subscription doesn't have that tag (or you use a custom one), pass
118
+ --tag <NAME>, --tag-id <ID>, or --tracking-method AGENT instead.
119
+
120
+ Source: https://docs.qualys.com/en/ca/api/agents/agent_count.htm
121
+ """
122
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
123
+ xml_body = _agent_filter_xml(tag, tag_id, tracking_method)
124
+ data = client.post_xml("/qps/rest/2.0/count/am/hostasset", data=xml_body)
125
+ output(data, format=format, output_file=output_file)
126
+
127
+
128
+ @_agent.command("activate")
129
+ def agent_activate(
130
+ ctx: typer.Context,
131
+ asset_id: str = typer.Argument(..., help="Asset ID of the agent"),
132
+ module: str = typer.Option(..., "--module", help="Module(s): AGENT_VM,AGENT_PC,AGENT_FIM,AGENT_EDR,AGENT_SCA"),
133
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
134
+ ):
135
+ """Activate a cloud agent for one or more modules.
136
+
137
+ Source: https://docs.qualys.com/en/ca/api/agents/activate_agent.htm
138
+ """
139
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
140
+ xml_body = '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest></ServiceRequest>'
141
+ data = client.post_xml(f"/qps/rest/2.0/activate/am/asset/{asset_id}?module={module}", data=xml_body)
142
+ output(data)
143
+
144
+
145
+ @_agent.command("deactivate")
146
+ def agent_deactivate(
147
+ ctx: typer.Context,
148
+ asset_id: str = typer.Argument(..., help="Asset ID of the agent"),
149
+ module: str = typer.Option(..., "--module", help="Module(s): AGENT_VM,AGENT_PC,AGENT_FIM,AGENT_EDR,AGENT_SCA"),
150
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
151
+ ):
152
+ """Deactivate a cloud agent for one or more modules.
153
+
154
+ Source: https://docs.qualys.com/en/ca/api/agents/deactivate_agent.htm
155
+ """
156
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
157
+ xml_body = '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest></ServiceRequest>'
158
+ data = client.post_xml(f"/qps/rest/2.0/deactivate/am/asset/{asset_id}?module={module}", data=xml_body)
159
+ output(data)
160
+
161
+
162
+ @_agent.command("uninstall")
163
+ def agent_uninstall(
164
+ ctx: typer.Context,
165
+ asset_id: str = typer.Argument(..., help="Asset ID of the agent"),
166
+ yes: bool = typer.Option(False, "--yes", "-y"),
167
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
168
+ ):
169
+ """Uninstall a cloud agent.
170
+
171
+ Source: https://docs.qualys.com/en/ca/api/agents/uninstall_agent.htm
172
+ """
173
+ if not yes:
174
+ typer.confirm(f"Uninstall agent {asset_id}?", abort=True)
175
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
176
+ xml_body = '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest></ServiceRequest>'
177
+ data = client.post_xml(f"/qps/rest/2.0/uninstall/am/asset/{asset_id}", data=xml_body)
178
+ output(data)
179
+
180
+
181
+ @_agent.command("bulk-activate")
182
+ def agent_bulk_activate(
183
+ ctx: typer.Context,
184
+ module: str = typer.Option(..., "--module", help="Module(s) to activate"),
185
+ tag_name: str = typer.Option("Cloud Agent", "--tag", help="Tag name filter"),
186
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
187
+ ):
188
+ """Bulk activate agents matching a tag.
189
+
190
+ Source: https://docs.qualys.com/en/ca/api/agents/bulk_activation.htm
191
+ """
192
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
193
+ xml_body = (
194
+ '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest><filters>'
195
+ f'<Criteria field="tagName" operator="EQUALS">{tag_name}</Criteria>'
196
+ "</filters></ServiceRequest>"
197
+ )
198
+ data = client.post_xml(f"/qps/rest/2.0/activate/am/asset?module={module}", data=xml_body)
199
+ output(data)
200
+
201
+
202
+ @_agent.command("bulk-deactivate")
203
+ def agent_bulk_deactivate(
204
+ ctx: typer.Context,
205
+ module: str = typer.Option(..., "--module", help="Module(s) to deactivate"),
206
+ tag_name: str = typer.Option("Cloud Agent", "--tag", help="Tag name filter"),
207
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
208
+ ):
209
+ """Bulk deactivate agents matching a tag.
210
+
211
+ Source: https://docs.qualys.com/en/ca/api/agents/bulk_deactivation.htm
212
+ """
213
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
214
+ xml_body = (
215
+ '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest><filters>'
216
+ f'<Criteria field="tagName" operator="EQUALS">{tag_name}</Criteria>'
217
+ "</filters></ServiceRequest>"
218
+ )
219
+ data = client.post_xml(f"/qps/rest/2.0/deactivate/am/asset?module={module}", data=xml_body)
220
+ output(data)
221
+
222
+
223
+ # ── scan ──────────────────────────────────────────────────────────────────────
224
+ _scan = typer.Typer(help="On-demand scan operations")
225
+ app.add_typer(_scan, name="scan")
226
+
227
+
228
+ @_scan.command("launch-ods")
229
+ def scan_launch_ods(
230
+ ctx: typer.Context,
231
+ scan_type: str = typer.Option(..., "--scan-type",
232
+ help="Inventory_Scan|Vulnerability_Scan|PolicyCompliance_Scan|UDC_Scan|SCA_Scan|SWCA_scan"),
233
+ tag_name: str = typer.Option("Cloud Agent", "--tag", help="Tag name filter for target assets"),
234
+ override_cpu: bool = typer.Option(False, "--override-cpu", help="Override CPU throttle limits"),
235
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
236
+ ):
237
+ """Launch an on-demand scan for agents matching a tag.
238
+
239
+ Source: https://docs.qualys.com/en/ca/api/scans/ods_for_multiple_asset.htm
240
+ """
241
+ client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
242
+ xml_body = (
243
+ '<?xml version="1.0" encoding="UTF-8"?><ServiceRequest><filters>'
244
+ f'<Criteria field="tagName" operator="EQUALS">{tag_name}</Criteria>'
245
+ "</filters></ServiceRequest>"
246
+ )
247
+ path = f"/qps/rest/1.0/ods/ca/agentasset?scan={scan_type}&overrideConfigCpu={str(override_cpu).lower()}"
248
+ data = client.post_xml(path, data=xml_body)
249
+ output(data)
250
+
251
+
252
+ # ── act-key ───────────────────────────────────────────────────────────────────
253
+ _act_key = typer.Typer(help="Activation key operations (new UI — JWT auth)")
254
+ app.add_typer(_act_key, name="act-key")
255
+
256
+
257
+ @_act_key.command("list")
258
+ def act_key_list(
259
+ ctx: typer.Context,
260
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
261
+ format: str = typer.Option("table", "--format", "-f"),
262
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
263
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
264
+ ):
265
+ """Search/list activation keys.
266
+
267
+ Source: https://docs.qualys.com/en/ca/api/act_key/act_key_apis.htm
268
+ """
269
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
270
+ data = client.post_json("/caui/v1/activation-keys/manage", {})
271
+ output(data, format=format, limit=limit, output_file=output_file)
272
+
273
+
274
+ @_act_key.command("get")
275
+ def act_key_get(
276
+ ctx: typer.Context,
277
+ key_id: str = typer.Argument(..., help="Activation key ID"),
278
+ format: str = typer.Option("table", "--format", "-f"),
279
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
280
+ ):
281
+ """Get activation key details.
282
+
283
+ Source: https://docs.qualys.com/en/ca/api/act_key/get_act_key_details.htm
284
+ """
285
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
286
+ data = client.get_json(f"/caui/v1/activation-keys/manage/{key_id}")
287
+ output(data, format=format)
288
+
289
+
290
+ @_act_key.command("create")
291
+ def act_key_create(
292
+ ctx: typer.Context,
293
+ title: str = typer.Option(..., "--title", help="Activation key title"),
294
+ licenses: str | None = typer.Option(
295
+ None, "--licenses",
296
+ help="Comma-separated license types (e.g. VM,PC,FIM,EDR,SCA)",
297
+ ),
298
+ network_id: str | None = typer.Option(None, "--network-id"),
299
+ expire_date: str | None = typer.Option(
300
+ None, "--expire-date", help="ISO datetime e.g. 2026-12-31T23:59:59Z"
301
+ ),
302
+ max_agents: int | None = typer.Option(
303
+ None, "--max-agents", help="Maximum agents allowed for this key"
304
+ ),
305
+ tags: str | None = typer.Option(None, "--tags", help="Comma-separated tag IDs"),
306
+ format: str = typer.Option("table", "--format", "-f"),
307
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
308
+ ):
309
+ """Create a new activation key.
310
+
311
+ Source: https://docs.qualys.com/en/ca/api/act_key/create_act_key.htm
312
+ """
313
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
314
+ body: dict = {"title": title}
315
+ if licenses:
316
+ body["licenses"] = [m.strip() for m in licenses.split(",") if m.strip()]
317
+ if network_id:
318
+ body["networkId"] = network_id
319
+ if expire_date:
320
+ body["expireDate"] = expire_date
321
+ if max_agents is not None:
322
+ body["maxAgentsAllowed"] = max_agents
323
+ if tags:
324
+ body["tags"] = [t.strip() for t in tags.split(",") if t.strip()]
325
+ data = client.post_json("/caui/v1/activation-keys/manage/activation-key", body)
326
+ output(data, format=format)
327
+
328
+
329
+ @_act_key.command("delete")
330
+ def act_key_delete(
331
+ ctx: typer.Context,
332
+ key_id: str = typer.Argument(..., help="Activation key ID"),
333
+ yes: bool = typer.Option(False, "--yes", "-y"),
334
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
335
+ ):
336
+ """Delete an activation key.
337
+
338
+ Source: https://docs.qualys.com/en/ca/api/act_key/delete_act_key.htm
339
+ """
340
+ if not yes:
341
+ typer.confirm(f"Delete activation key {key_id}?", abort=True)
342
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
343
+ client.delete_json(f"/caui/v1/activation-keys/manage/{key_id}")
344
+ output_success(f"Activation key {key_id} deleted.")
345
+
346
+
347
+ # ── config ────────────────────────────────────────────────────────────────────
348
+ _config = typer.Typer(help="Configuration profile operations (new UI — JWT auth)")
349
+ app.add_typer(_config, name="config")
350
+
351
+
352
+ @_config.command("list")
353
+ def config_list(
354
+ ctx: typer.Context,
355
+ limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
356
+ format: str = typer.Option("table", "--format", "-f"),
357
+ output_file: str | None = typer.Option(None, "--output-file", "-o"),
358
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
359
+ ):
360
+ """List configuration profiles.
361
+
362
+ Source: https://docs.qualys.com/en/ca/api/config/config_apis.htm
363
+ """
364
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
365
+ data = client.get_json("/caui/v1/config-profiles")
366
+ output(data, format=format, limit=limit, output_file=output_file)
367
+
368
+
369
+ @_config.command("create")
370
+ def config_create(
371
+ ctx: typer.Context,
372
+ name: str = typer.Option(..., "--name", help="Profile name"),
373
+ format: str = typer.Option("table", "--format", "-f"),
374
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
375
+ ):
376
+ """Create a configuration profile.
377
+
378
+ Source: https://docs.qualys.com/en/ca/api/config/config_api/create_config_profile.htm
379
+ """
380
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
381
+ data = client.post_json(
382
+ "/caui/v1/config-profiles",
383
+ {"basicDetails": {"profileName": name}},
384
+ )
385
+ output(data, format=format)
386
+
387
+
388
+ @_config.command("delete")
389
+ def config_delete(
390
+ ctx: typer.Context,
391
+ profile_id: str = typer.Argument(..., help="Profile ID"),
392
+ yes: bool = typer.Option(False, "--yes", "-y"),
393
+ verbose: int = typer.Option(0, "--verbose", "-v", count=True),
394
+ ):
395
+ """Delete a configuration profile.
396
+
397
+ Source: https://docs.qualys.com/en/ca/api/config/config_api/delete_profile.htm
398
+ """
399
+ if not yes:
400
+ typer.confirm(f"Delete config profile {profile_id}?", abort=True)
401
+ client = _c.make(ctx.obj, auth_type="jwt", verbose=verbose, use_gateway=True)
402
+ client.delete_json(f"/caui/v1/config-profiles/{profile_id}")
403
+ output_success(f"Config profile {profile_id} deleted.")