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,291 @@
|
|
|
1
|
+
# Hand-edited — do NOT regenerate from api-model.json (codegen will skip this file).
|
|
2
|
+
# Scan Authentication API (Basic auth, XML responses)
|
|
3
|
+
# Originally generated from api-model.json; corrections:
|
|
4
|
+
# - record list: added --type to route per-type queries to /api/2.0/fo/auth/<type>/
|
|
5
|
+
# (without --type uses the generic /api/2.0/fo/auth/ that returns all types)
|
|
6
|
+
# - vault create: exposed common type-specific fields (--appid, --safe, --url,
|
|
7
|
+
# --username, --password, --token, --namespace, --role, --secret-id) plus a
|
|
8
|
+
# repeatable --field key=value escape hatch for any other type-specific param
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from .. import client as _c
|
|
14
|
+
from ..formatters import DEFAULT_LIMIT, output
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(name="scanauth", help="Scan Authentication APIs (Basic auth)")
|
|
17
|
+
|
|
18
|
+
# ── record ────────────────────────────────────────────────────────────────────
|
|
19
|
+
_record = typer.Typer(help="Authentication record operations")
|
|
20
|
+
app.add_typer(_record, name="record")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_RECORD_TYPES = (
|
|
24
|
+
"unix", "windows", "oracle", "oracle_listener", "ms_sql", "mysql", "db2",
|
|
25
|
+
"snmp", "http", "postgresql", "vmware", "ibm_was", "tomcat", "jboss",
|
|
26
|
+
"mongodb", "cisco", "checkpoint_firewall", "palo_alto_firewall",
|
|
27
|
+
"ad_workspace", "azure", "aws", "gcp", "kubernetes", "docker",
|
|
28
|
+
"nginx", "neo4j", "sybase", "mariadb", "informix", "as400",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@_record.command("list")
|
|
33
|
+
def record_list(
|
|
34
|
+
ctx: typer.Context,
|
|
35
|
+
type_: str | None = typer.Option(
|
|
36
|
+
None, "--type",
|
|
37
|
+
help=f"Auth record type — selects the per-type endpoint. One of: {', '.join(_RECORD_TYPES)}",
|
|
38
|
+
),
|
|
39
|
+
ids: str | None = typer.Option(None, "--ids", help="Filter by record IDs (comma-separated)"),
|
|
40
|
+
title: str | None = typer.Option(None, "--title", help="Filter by title substring"),
|
|
41
|
+
id_min: int | None = typer.Option(None, "--id-min"),
|
|
42
|
+
id_max: int | None = typer.Option(None, "--id-max"),
|
|
43
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
44
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
45
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
46
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
47
|
+
):
|
|
48
|
+
"""List authentication records.
|
|
49
|
+
|
|
50
|
+
Without --type, lists records across all types (/api/2.0/fo/auth/).
|
|
51
|
+
With --type, queries the per-type endpoint (e.g. /api/2.0/fo/auth/unix/).
|
|
52
|
+
|
|
53
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/record_list/list_records.htm
|
|
54
|
+
"""
|
|
55
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
56
|
+
params: dict = {"action": "list"}
|
|
57
|
+
if ids:
|
|
58
|
+
params["ids"] = ids
|
|
59
|
+
if title:
|
|
60
|
+
params["title"] = title
|
|
61
|
+
if id_min is not None:
|
|
62
|
+
params["id_min"] = id_min
|
|
63
|
+
if id_max is not None:
|
|
64
|
+
params["id_max"] = id_max
|
|
65
|
+
path = f"/api/2.0/fo/auth/{type_}/" if type_ else "/api/2.0/fo/auth/"
|
|
66
|
+
data = client.post_xml(path, data=params)
|
|
67
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ── vault ─────────────────────────────────────────────────────────────────────
|
|
71
|
+
_vault = typer.Typer(help="Authentication vault operations")
|
|
72
|
+
app.add_typer(_vault, name="vault")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@_vault.command("list")
|
|
76
|
+
def vault_list(
|
|
77
|
+
ctx: typer.Context,
|
|
78
|
+
title: str | None = typer.Option(None, "--title"),
|
|
79
|
+
type_: str | None = typer.Option(None, "--type", help="Vault type (e.g. CyberArk AIM)"),
|
|
80
|
+
modified: str | None = typer.Option(None, "--modified", help="Modified on/after YYYY-MM-DD"),
|
|
81
|
+
orderby: str | None = typer.Option(None, "--orderby", help="id|title|system_name|last_modified"),
|
|
82
|
+
sortorder: str | None = typer.Option(None, "--sortorder", help="asc|desc"),
|
|
83
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
84
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
85
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
86
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
87
|
+
):
|
|
88
|
+
"""List authentication vaults.
|
|
89
|
+
|
|
90
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/vaults/list_vaults.htm
|
|
91
|
+
"""
|
|
92
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
93
|
+
params: dict = {"action": "list"}
|
|
94
|
+
if title:
|
|
95
|
+
params["title"] = title
|
|
96
|
+
if type_:
|
|
97
|
+
params["type"] = type_
|
|
98
|
+
if modified:
|
|
99
|
+
params["modified"] = modified
|
|
100
|
+
if orderby:
|
|
101
|
+
params["orderby"] = orderby
|
|
102
|
+
if sortorder:
|
|
103
|
+
params["sortorder"] = sortorder
|
|
104
|
+
data = client.post_xml("/api/2.0/fo/vault/", data=params)
|
|
105
|
+
output(data, format=format, limit=limit, output_file=output_file)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@_vault.command("create")
|
|
109
|
+
def vault_create(
|
|
110
|
+
ctx: typer.Context,
|
|
111
|
+
title: str = typer.Option(..., "--title"),
|
|
112
|
+
type_: str = typer.Option(..., "--type", help="Vault type (e.g. CyberArk AIM)"),
|
|
113
|
+
comments: str | None = typer.Option(None, "--comments"),
|
|
114
|
+
# ── Type-specific fields ───────────────────────────────────────────────
|
|
115
|
+
# CyberArk AIM: appid + safe + url (required by the API for that type)
|
|
116
|
+
# Thycotic Secret Server: api_url + token + secret_id
|
|
117
|
+
# HashiCorp KV: url + namespace + role
|
|
118
|
+
# Other types: pass arbitrary fields with repeated --field key=value.
|
|
119
|
+
appid: str | None = typer.Option(
|
|
120
|
+
None, "--appid", help="CyberArk AIM: application ID (required for that type)"
|
|
121
|
+
),
|
|
122
|
+
safe: str | None = typer.Option(
|
|
123
|
+
None, "--safe", help="CyberArk AIM / PIM: safe name (required for those types)"
|
|
124
|
+
),
|
|
125
|
+
url: str | None = typer.Option(
|
|
126
|
+
None, "--url", help="Vault server URL (required for most types)"
|
|
127
|
+
),
|
|
128
|
+
username: str | None = typer.Option(None, "--username", help="Vault auth username"),
|
|
129
|
+
password: str | None = typer.Option(None, "--password", help="Vault auth password"),
|
|
130
|
+
token: str | None = typer.Option(None, "--token", help="API token (Thycotic, HashiCorp, etc.)"),
|
|
131
|
+
namespace: str | None = typer.Option(None, "--namespace", help="HashiCorp namespace"),
|
|
132
|
+
role: str | None = typer.Option(None, "--role", help="HashiCorp role"),
|
|
133
|
+
secret_id: str | None = typer.Option(None, "--secret-id", help="HashiCorp / Thycotic secret ID"),
|
|
134
|
+
field: list[str] | None = typer.Option(
|
|
135
|
+
None, "--field",
|
|
136
|
+
help="Arbitrary type-specific field as key=value (repeatable). "
|
|
137
|
+
"Use for any param not exposed as a dedicated flag.",
|
|
138
|
+
),
|
|
139
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
140
|
+
):
|
|
141
|
+
"""Create an authentication vault.
|
|
142
|
+
|
|
143
|
+
Each vault type has its own required fields. Common ones are exposed as
|
|
144
|
+
dedicated flags; for any type-specific param not listed here, use
|
|
145
|
+
`--field key=value` (repeat as needed).
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
vault create --title "PROD CA" --type "CyberArk AIM" \\
|
|
149
|
+
--appid MyApp --safe MySafe --url https://cyberark.example.com
|
|
150
|
+
|
|
151
|
+
vault create --title "PROD KV" --type "HashiCorp KV" \\
|
|
152
|
+
--url https://vault.example.com --namespace prod --role qualys
|
|
153
|
+
|
|
154
|
+
vault create --title "Custom" --type "Some Vault" \\
|
|
155
|
+
--field foo=bar --field baz=qux
|
|
156
|
+
|
|
157
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/vaults/create_vault.htm
|
|
158
|
+
"""
|
|
159
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
160
|
+
params: dict = {"action": "create", "title": title, "type": type_}
|
|
161
|
+
if comments:
|
|
162
|
+
params["comments"] = comments
|
|
163
|
+
# Type-specific fields — only sent when the user passes them.
|
|
164
|
+
for k, v in (
|
|
165
|
+
("appid", appid),
|
|
166
|
+
("safe", safe),
|
|
167
|
+
("url", url),
|
|
168
|
+
("username", username),
|
|
169
|
+
("password", password),
|
|
170
|
+
("token", token),
|
|
171
|
+
("namespace", namespace),
|
|
172
|
+
("role", role),
|
|
173
|
+
("secret_id", secret_id),
|
|
174
|
+
):
|
|
175
|
+
if v is not None:
|
|
176
|
+
params[k] = v
|
|
177
|
+
# Arbitrary --field key=value passthrough
|
|
178
|
+
for entry in (field or []):
|
|
179
|
+
if "=" not in entry:
|
|
180
|
+
raise typer.BadParameter(
|
|
181
|
+
f"--field expects key=value, got {entry!r}",
|
|
182
|
+
)
|
|
183
|
+
k, _, v = entry.partition("=")
|
|
184
|
+
k = k.strip()
|
|
185
|
+
if not k:
|
|
186
|
+
raise typer.BadParameter(f"--field has empty key: {entry!r}")
|
|
187
|
+
if k in params:
|
|
188
|
+
raise typer.BadParameter(
|
|
189
|
+
f"--field {k}=… conflicts with the dedicated --{k.replace('_','-')} flag",
|
|
190
|
+
)
|
|
191
|
+
params[k] = v
|
|
192
|
+
data = client.post_xml("/api/2.0/fo/vault/index.php", data=params)
|
|
193
|
+
output(data)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── neo4j ─────────────────────────────────────────────────────────────────────
|
|
197
|
+
_neo4j = typer.Typer(help="Neo4j authentication record operations")
|
|
198
|
+
app.add_typer(_neo4j, name="neo4j")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@_neo4j.command("list")
|
|
202
|
+
def neo4j_list(
|
|
203
|
+
ctx: typer.Context,
|
|
204
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
205
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
206
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
207
|
+
):
|
|
208
|
+
"""List Neo4j authentication records.
|
|
209
|
+
|
|
210
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/record_types/neo4j.htm
|
|
211
|
+
"""
|
|
212
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
213
|
+
data = client.get_xml("/api/2.0/fo/auth/neo4j/", {"action": "list"})
|
|
214
|
+
output(data, format=format, limit=limit)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@_neo4j.command("create")
|
|
218
|
+
def neo4j_create(
|
|
219
|
+
ctx: typer.Context,
|
|
220
|
+
title: str = typer.Option(..., "--title"),
|
|
221
|
+
username: str = typer.Option(..., "--username"),
|
|
222
|
+
password: str = typer.Option(..., "--password"),
|
|
223
|
+
ips: str = typer.Option(..., "--ips"),
|
|
224
|
+
port: int = typer.Option(..., "--port"),
|
|
225
|
+
database: str | None = typer.Option(None, "--database"),
|
|
226
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
227
|
+
):
|
|
228
|
+
"""Create a Neo4j authentication record.
|
|
229
|
+
|
|
230
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/record_types/neo4j.htm
|
|
231
|
+
"""
|
|
232
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
233
|
+
params: dict = {
|
|
234
|
+
"action": "create",
|
|
235
|
+
"title": title,
|
|
236
|
+
"username": username,
|
|
237
|
+
"password": password,
|
|
238
|
+
"ips": ips,
|
|
239
|
+
"port": port,
|
|
240
|
+
}
|
|
241
|
+
if database:
|
|
242
|
+
params["database"] = database
|
|
243
|
+
data = client.post_xml("/api/2.0/fo/auth/neo4j/", data=params)
|
|
244
|
+
output(data)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── nginx ─────────────────────────────────────────────────────────────────────
|
|
248
|
+
_nginx = typer.Typer(help="Nginx authentication record operations")
|
|
249
|
+
app.add_typer(_nginx, name="nginx")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@_nginx.command("list")
|
|
253
|
+
def nginx_list(
|
|
254
|
+
ctx: typer.Context,
|
|
255
|
+
limit: int = typer.Option(DEFAULT_LIMIT, "--limit", "-n"),
|
|
256
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
257
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
258
|
+
):
|
|
259
|
+
"""List Nginx authentication records.
|
|
260
|
+
|
|
261
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/record_types/nginx_record.htm
|
|
262
|
+
"""
|
|
263
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
264
|
+
data = client.get_xml("/api/2.0/fo/auth/nginx/", {"action": "list"})
|
|
265
|
+
output(data, format=format, limit=limit)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@_nginx.command("create")
|
|
269
|
+
def nginx_create(
|
|
270
|
+
ctx: typer.Context,
|
|
271
|
+
title: str = typer.Option(..., "--title"),
|
|
272
|
+
ips: str = typer.Option(..., "--ips"),
|
|
273
|
+
unix_base_path: str | None = typer.Option(None, "--unix-base-path"),
|
|
274
|
+
unix_conf_path: str | None = typer.Option(None, "--unix-conf-path"),
|
|
275
|
+
unix_prefix_path: str | None = typer.Option(None, "--unix-prefix-path"),
|
|
276
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
277
|
+
):
|
|
278
|
+
"""Create an Nginx authentication record.
|
|
279
|
+
|
|
280
|
+
Source: https://docs.qualys.com/en/vm/api/scanauth/record_types/nginx_record.htm
|
|
281
|
+
"""
|
|
282
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
283
|
+
params: dict = {"action": "create", "title": title, "ips": ips}
|
|
284
|
+
if unix_base_path:
|
|
285
|
+
params["unix_base_path"] = unix_base_path
|
|
286
|
+
if unix_conf_path:
|
|
287
|
+
params["unix_conf_path"] = unix_conf_path
|
|
288
|
+
if unix_prefix_path:
|
|
289
|
+
params["unix_prefix_path"] = unix_prefix_path
|
|
290
|
+
data = client.post_xml("/api/2.0/fo/auth/nginx/", data=params)
|
|
291
|
+
output(data)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Subscription Management APIs (Basic auth, XML responses).
|
|
3
|
+
|
|
4
|
+
Covers subscription info retrieval, full config export/import, and user
|
|
5
|
+
preference export/import.
|
|
6
|
+
|
|
7
|
+
Hand-edited corrections:
|
|
8
|
+
- user-prefs export: switched POST → GET (API rejects POST with HTTP 400
|
|
9
|
+
"Request method POST is incompatible with action=export").
|
|
10
|
+
- user-prefs export: --user-id is now a required option mapped to the
|
|
11
|
+
`user_id` query param (was --user → `user_login`, both wrong: the API
|
|
12
|
+
requires the numeric user_id, not username, with HTTP 400 otherwise).
|
|
13
|
+
|
|
14
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
import typer
|
|
21
|
+
|
|
22
|
+
from .. import client as _c
|
|
23
|
+
from ..formatters import guard_output_file, output, output_success
|
|
24
|
+
|
|
25
|
+
app = typer.Typer(
|
|
26
|
+
name="sub",
|
|
27
|
+
help="Subscription Management APIs — info, config export/import, user preferences",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
_user_prefs = typer.Typer(help="User preference export/import operations")
|
|
31
|
+
app.add_typer(_user_prefs, name="user-prefs")
|
|
32
|
+
|
|
33
|
+
_BASE = "/api/2.0/fo/subscription/index.php"
|
|
34
|
+
_PREFS = "/api/2.0/fo/user_prefs/"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Subscription info
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
@app.command("info")
|
|
42
|
+
def sub_info(
|
|
43
|
+
ctx: typer.Context,
|
|
44
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
45
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
46
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Show subscription details: product entitlements, user counts, scanner info.
|
|
49
|
+
|
|
50
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
51
|
+
"""
|
|
52
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
53
|
+
data = client.get_xml(_BASE, params={"action": "export"})
|
|
54
|
+
output(data, format=format, output_file=output_file)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Subscription config export / import
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
@app.command("export")
|
|
62
|
+
def sub_export(
|
|
63
|
+
ctx: typer.Context,
|
|
64
|
+
output_file: str | None = typer.Option(None, "--output-file", "-o"),
|
|
65
|
+
format: str = typer.Option("table", "--format", "-f"),
|
|
66
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Export full subscription configuration.
|
|
69
|
+
|
|
70
|
+
Without --output-file, prints parsed data to stdout (JSON when piped).
|
|
71
|
+
With --output-file, writes raw XML to the specified file.
|
|
72
|
+
|
|
73
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
74
|
+
"""
|
|
75
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
76
|
+
if output_file:
|
|
77
|
+
guard_output_file()
|
|
78
|
+
resp = client.request("GET", _BASE, params={"action": "export"})
|
|
79
|
+
Path(output_file).write_bytes(resp.content)
|
|
80
|
+
output_success(f"Subscription config exported to {output_file} ({len(resp.content):,} bytes)")
|
|
81
|
+
else:
|
|
82
|
+
data = client.get_xml(_BASE, params={"action": "export"})
|
|
83
|
+
output(data, format=format)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("import")
|
|
87
|
+
def sub_import(
|
|
88
|
+
ctx: typer.Context,
|
|
89
|
+
input_file: str = typer.Argument(..., help="XML file produced by 'sub export'"),
|
|
90
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
91
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Import subscription configuration from a previously exported XML file.
|
|
94
|
+
|
|
95
|
+
INFO-type settings are ignored by the API; only mutable settings are applied.
|
|
96
|
+
|
|
97
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
98
|
+
"""
|
|
99
|
+
if not yes:
|
|
100
|
+
typer.confirm(f"Import subscription config from '{input_file}'?", abort=True)
|
|
101
|
+
xml_bytes = Path(input_file).read_bytes()
|
|
102
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
103
|
+
client.request(
|
|
104
|
+
"POST", _BASE,
|
|
105
|
+
data={"action": "import"},
|
|
106
|
+
files={"file": ("export.xml", xml_bytes, "text/xml")},
|
|
107
|
+
)
|
|
108
|
+
output_success(f"Subscription config imported from '{input_file}'.")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# User preferences export / import
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
@_user_prefs.command("export")
|
|
116
|
+
def user_prefs_export(
|
|
117
|
+
ctx: typer.Context,
|
|
118
|
+
user_id: str = typer.Option(
|
|
119
|
+
..., "--user-id", help="Numeric Qualys user ID (required by the API)",
|
|
120
|
+
),
|
|
121
|
+
output_file: str = typer.Option("user-prefs-export.xml", "--output-file", "-o"),
|
|
122
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Export user preferences to an XML file.
|
|
125
|
+
|
|
126
|
+
The /user_prefs/ endpoint requires GET for action=export — POST returns
|
|
127
|
+
HTTP 400 "Request method POST is incompatible with action=export". The
|
|
128
|
+
`user_id` query parameter (numeric, not username) is also required —
|
|
129
|
+
omitting it returns HTTP 400 "Missing required parameter(s): user_id".
|
|
130
|
+
|
|
131
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
132
|
+
"""
|
|
133
|
+
guard_output_file()
|
|
134
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
135
|
+
resp = client.request(
|
|
136
|
+
"GET", _PREFS,
|
|
137
|
+
params={"action": "export", "user_id": user_id},
|
|
138
|
+
)
|
|
139
|
+
Path(output_file).write_bytes(resp.content)
|
|
140
|
+
output_success(f"User preferences exported to {output_file} ({len(resp.content):,} bytes)")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@_user_prefs.command("import")
|
|
144
|
+
def user_prefs_import(
|
|
145
|
+
ctx: typer.Context,
|
|
146
|
+
input_file: str = typer.Argument(..., help="XML file produced by 'user-prefs export'"),
|
|
147
|
+
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation"),
|
|
148
|
+
verbose: int = typer.Option(0, "--verbose", "-v", count=True),
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Import user preferences from a previously exported XML file.
|
|
151
|
+
|
|
152
|
+
Source: https://www.qualys.com/docs/qualys-subscription-api-user-guide.pdf
|
|
153
|
+
"""
|
|
154
|
+
if not yes:
|
|
155
|
+
typer.confirm(f"Import user preferences from '{input_file}'?", abort=True)
|
|
156
|
+
xml_bytes = Path(input_file).read_bytes()
|
|
157
|
+
client = _c.make(ctx.obj, auth_type="basic", verbose=verbose)
|
|
158
|
+
client.request(
|
|
159
|
+
"POST", _PREFS,
|
|
160
|
+
data={"action": "import"},
|
|
161
|
+
files={"file": ("prefs.xml", xml_bytes, "text/xml")},
|
|
162
|
+
)
|
|
163
|
+
output_success(f"User preferences imported from '{input_file}'.")
|