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,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}'.")