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
|
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.")
|