bt-cli 0.4.13__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.
- bt_cli/__init__.py +3 -0
- bt_cli/cli.py +830 -0
- bt_cli/commands/__init__.py +1 -0
- bt_cli/commands/configure.py +415 -0
- bt_cli/commands/learn.py +229 -0
- bt_cli/commands/quick.py +784 -0
- bt_cli/core/__init__.py +1 -0
- bt_cli/core/auth.py +213 -0
- bt_cli/core/client.py +313 -0
- bt_cli/core/config.py +393 -0
- bt_cli/core/config_file.py +420 -0
- bt_cli/core/csv_utils.py +91 -0
- bt_cli/core/errors.py +247 -0
- bt_cli/core/output.py +205 -0
- bt_cli/core/prompts.py +87 -0
- bt_cli/core/rest_debug.py +221 -0
- bt_cli/data/CLAUDE.md +94 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +108 -0
- bt_cli/data/skills/entitle/SKILL.md +170 -0
- bt_cli/data/skills/epmw/SKILL.md +144 -0
- bt_cli/data/skills/pra/SKILL.md +150 -0
- bt_cli/data/skills/pws/SKILL.md +198 -0
- bt_cli/entitle/__init__.py +1 -0
- bt_cli/entitle/client/__init__.py +5 -0
- bt_cli/entitle/client/base.py +443 -0
- bt_cli/entitle/commands/__init__.py +24 -0
- bt_cli/entitle/commands/accounts.py +53 -0
- bt_cli/entitle/commands/applications.py +39 -0
- bt_cli/entitle/commands/auth.py +68 -0
- bt_cli/entitle/commands/bundles.py +218 -0
- bt_cli/entitle/commands/integrations.py +60 -0
- bt_cli/entitle/commands/permissions.py +70 -0
- bt_cli/entitle/commands/policies.py +97 -0
- bt_cli/entitle/commands/resources.py +131 -0
- bt_cli/entitle/commands/roles.py +74 -0
- bt_cli/entitle/commands/users.py +123 -0
- bt_cli/entitle/commands/workflows.py +187 -0
- bt_cli/entitle/models/__init__.py +31 -0
- bt_cli/entitle/models/bundle.py +28 -0
- bt_cli/entitle/models/common.py +37 -0
- bt_cli/entitle/models/integration.py +30 -0
- bt_cli/entitle/models/permission.py +27 -0
- bt_cli/entitle/models/policy.py +25 -0
- bt_cli/entitle/models/resource.py +29 -0
- bt_cli/entitle/models/role.py +28 -0
- bt_cli/entitle/models/user.py +24 -0
- bt_cli/entitle/models/workflow.py +55 -0
- bt_cli/epmw/__init__.py +1 -0
- bt_cli/epmw/client/__init__.py +5 -0
- bt_cli/epmw/client/base.py +848 -0
- bt_cli/epmw/commands/__init__.py +33 -0
- bt_cli/epmw/commands/audits.py +250 -0
- bt_cli/epmw/commands/auth.py +55 -0
- bt_cli/epmw/commands/computers.py +140 -0
- bt_cli/epmw/commands/events.py +233 -0
- bt_cli/epmw/commands/groups.py +215 -0
- bt_cli/epmw/commands/policies.py +673 -0
- bt_cli/epmw/commands/quick.py +348 -0
- bt_cli/epmw/commands/requests.py +224 -0
- bt_cli/epmw/commands/roles.py +78 -0
- bt_cli/epmw/commands/tasks.py +38 -0
- bt_cli/epmw/commands/users.py +219 -0
- bt_cli/epmw/models/__init__.py +1 -0
- bt_cli/pra/__init__.py +1 -0
- bt_cli/pra/client/__init__.py +5 -0
- bt_cli/pra/client/base.py +618 -0
- bt_cli/pra/commands/__init__.py +30 -0
- bt_cli/pra/commands/auth.py +55 -0
- bt_cli/pra/commands/import_export.py +442 -0
- bt_cli/pra/commands/jump_clients.py +139 -0
- bt_cli/pra/commands/jump_groups.py +146 -0
- bt_cli/pra/commands/jump_items.py +638 -0
- bt_cli/pra/commands/jumpoints.py +95 -0
- bt_cli/pra/commands/policies.py +197 -0
- bt_cli/pra/commands/quick.py +470 -0
- bt_cli/pra/commands/teams.py +81 -0
- bt_cli/pra/commands/users.py +87 -0
- bt_cli/pra/commands/vault.py +564 -0
- bt_cli/pra/models/__init__.py +27 -0
- bt_cli/pra/models/common.py +12 -0
- bt_cli/pra/models/jump_client.py +25 -0
- bt_cli/pra/models/jump_group.py +15 -0
- bt_cli/pra/models/jump_item.py +72 -0
- bt_cli/pra/models/jumpoint.py +19 -0
- bt_cli/pra/models/team.py +14 -0
- bt_cli/pra/models/user.py +17 -0
- bt_cli/pra/models/vault.py +45 -0
- bt_cli/pws/__init__.py +1 -0
- bt_cli/pws/client/__init__.py +5 -0
- bt_cli/pws/client/base.py +356 -0
- bt_cli/pws/client/beyondinsight.py +869 -0
- bt_cli/pws/client/passwordsafe.py +1786 -0
- bt_cli/pws/commands/__init__.py +33 -0
- bt_cli/pws/commands/accounts.py +372 -0
- bt_cli/pws/commands/assets.py +311 -0
- bt_cli/pws/commands/auth.py +166 -0
- bt_cli/pws/commands/clouds.py +221 -0
- bt_cli/pws/commands/config.py +344 -0
- bt_cli/pws/commands/credentials.py +347 -0
- bt_cli/pws/commands/databases.py +306 -0
- bt_cli/pws/commands/directories.py +199 -0
- bt_cli/pws/commands/functional.py +298 -0
- bt_cli/pws/commands/import_export.py +452 -0
- bt_cli/pws/commands/platforms.py +118 -0
- bt_cli/pws/commands/quick.py +1646 -0
- bt_cli/pws/commands/search.py +256 -0
- bt_cli/pws/commands/secrets.py +1343 -0
- bt_cli/pws/commands/systems.py +389 -0
- bt_cli/pws/commands/users.py +415 -0
- bt_cli/pws/commands/workgroups.py +166 -0
- bt_cli/pws/config.py +18 -0
- bt_cli/pws/models/__init__.py +19 -0
- bt_cli/pws/models/account.py +186 -0
- bt_cli/pws/models/asset.py +102 -0
- bt_cli/pws/models/common.py +132 -0
- bt_cli/pws/models/system.py +121 -0
- bt_cli-0.4.13.dist-info/METADATA +417 -0
- bt_cli-0.4.13.dist-info/RECORD +121 -0
- bt_cli-0.4.13.dist-info/WHEEL +4 -0
- bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
"""EPMW policies commands - manage policies, revisions, and application groups."""
|
|
2
|
+
|
|
3
|
+
import json as json_lib
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from bt_cli.core.output import OutputFormat, print_api_error, print_error, print_json, print_success, print_warning
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(no_args_is_help=True, help="EPM Windows policies management")
|
|
15
|
+
revisions_app = typer.Typer(no_args_is_help=True, help="Policy revisions management")
|
|
16
|
+
appgroups_app = typer.Typer(no_args_is_help=True, help="Policy application groups (policy editor)")
|
|
17
|
+
app.add_typer(revisions_app, name="revisions")
|
|
18
|
+
app.add_typer(appgroups_app, name="appgroups")
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ============================================================================
|
|
24
|
+
# Policy Commands
|
|
25
|
+
# ============================================================================
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("list")
|
|
29
|
+
def list_policies(
|
|
30
|
+
output: OutputFormat = typer.Option(
|
|
31
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
32
|
+
),
|
|
33
|
+
):
|
|
34
|
+
"""List all policies."""
|
|
35
|
+
from bt_cli.epmw.client import get_client
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
client = get_client()
|
|
39
|
+
policies = client.list_policies()
|
|
40
|
+
|
|
41
|
+
if output == OutputFormat.JSON:
|
|
42
|
+
print_json(policies)
|
|
43
|
+
else:
|
|
44
|
+
# Build mapping of policyId -> list of group names
|
|
45
|
+
groups = client.list_groups()
|
|
46
|
+
policy_groups: dict[str, list[str]] = {}
|
|
47
|
+
for group in groups:
|
|
48
|
+
policy_id = group.get("policyId")
|
|
49
|
+
if policy_id:
|
|
50
|
+
if policy_id not in policy_groups:
|
|
51
|
+
policy_groups[policy_id] = []
|
|
52
|
+
policy_groups[policy_id].append(group.get("name", "Unknown"))
|
|
53
|
+
|
|
54
|
+
table = Table(title="Policies")
|
|
55
|
+
table.add_column("ID", style="cyan", no_wrap=True, min_width=36)
|
|
56
|
+
table.add_column("Name", style="green")
|
|
57
|
+
table.add_column("Rev", justify="right")
|
|
58
|
+
table.add_column("Modified By", style="dim")
|
|
59
|
+
table.add_column("Assigned To", style="blue")
|
|
60
|
+
table.add_column("Checked Out By", style="yellow")
|
|
61
|
+
|
|
62
|
+
for policy in policies:
|
|
63
|
+
has_draft = policy.get("hasOpenDraft", False)
|
|
64
|
+
draft_user = policy.get("draftUser", "")
|
|
65
|
+
if has_draft and draft_user:
|
|
66
|
+
checkout_display = draft_user.split("@")[0] if "@" in draft_user else draft_user
|
|
67
|
+
elif has_draft:
|
|
68
|
+
checkout_display = "[dim]Unknown[/dim]"
|
|
69
|
+
else:
|
|
70
|
+
checkout_display = "-"
|
|
71
|
+
|
|
72
|
+
# Get assigned groups for this policy
|
|
73
|
+
policy_id = policy.get("id", "")
|
|
74
|
+
assigned_groups = policy_groups.get(policy_id, [])
|
|
75
|
+
if assigned_groups:
|
|
76
|
+
assigned_display = ", ".join(assigned_groups)
|
|
77
|
+
else:
|
|
78
|
+
assigned_display = "[dim]-[/dim]"
|
|
79
|
+
|
|
80
|
+
table.add_row(
|
|
81
|
+
str(policy_id), # Show full UUID
|
|
82
|
+
policy.get("name", ""),
|
|
83
|
+
str(policy.get("revision", "")),
|
|
84
|
+
(policy.get("lastModifiedUser", "") or "-").split("@")[0],
|
|
85
|
+
assigned_display,
|
|
86
|
+
checkout_display,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
console.print(table)
|
|
90
|
+
except httpx.HTTPStatusError as e:
|
|
91
|
+
print_api_error(e, "list policies")
|
|
92
|
+
raise typer.Exit(1)
|
|
93
|
+
except httpx.RequestError as e:
|
|
94
|
+
print_api_error(e, "list policies")
|
|
95
|
+
raise typer.Exit(1)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
print_api_error(e, "list policies")
|
|
98
|
+
raise typer.Exit(1)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@app.command("get")
|
|
102
|
+
def get_policy(
|
|
103
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
104
|
+
output: OutputFormat = typer.Option(
|
|
105
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
106
|
+
),
|
|
107
|
+
):
|
|
108
|
+
"""Get policy details."""
|
|
109
|
+
from bt_cli.epmw.client import get_client
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
client = get_client()
|
|
113
|
+
policy = client.get_policy(policy_id)
|
|
114
|
+
|
|
115
|
+
if output == OutputFormat.JSON:
|
|
116
|
+
print_json(policy)
|
|
117
|
+
else:
|
|
118
|
+
console.print(Panel(
|
|
119
|
+
f"[bold]Name:[/bold] {policy.get('name', '')}\n"
|
|
120
|
+
f"[bold]ID:[/bold] {policy.get('id', '')}\n"
|
|
121
|
+
f"[bold]Revision:[/bold] {policy.get('revision', '')}\n"
|
|
122
|
+
f"[bold]Description:[/bold] {policy.get('description', '-')}\n"
|
|
123
|
+
f"[bold]Assigned:[/bold] {'Yes' if policy.get('isAssignedToGroup') else 'No'}\n"
|
|
124
|
+
f"[bold]Has Draft:[/bold] {'Yes' if policy.get('hasOpenDraft') else 'No'}\n"
|
|
125
|
+
f"[bold]Draft User:[/bold] {policy.get('draftUser', '-')}\n"
|
|
126
|
+
f"[bold]Modified:[/bold] {policy.get('lastModifiedDate', '-')}\n"
|
|
127
|
+
f"[bold]Modified By:[/bold] {policy.get('lastModifiedUser', '-')}",
|
|
128
|
+
title=f"Policy: {policy.get('name', '')}",
|
|
129
|
+
))
|
|
130
|
+
except httpx.HTTPStatusError as e:
|
|
131
|
+
print_api_error(e, "get policy")
|
|
132
|
+
raise typer.Exit(1)
|
|
133
|
+
except httpx.RequestError as e:
|
|
134
|
+
print_api_error(e, "get policy")
|
|
135
|
+
raise typer.Exit(1)
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print_api_error(e, "get policy")
|
|
138
|
+
raise typer.Exit(1)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command("create")
|
|
142
|
+
def create_policy(
|
|
143
|
+
name: str = typer.Option(..., "--name", "-n", help="Policy name"),
|
|
144
|
+
file: str = typer.Option(..., "--file", "-f", help="Path to policy XML file"),
|
|
145
|
+
description: str = typer.Option("", "--description", "-d", help="Policy description"),
|
|
146
|
+
output: OutputFormat = typer.Option(
|
|
147
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
148
|
+
),
|
|
149
|
+
):
|
|
150
|
+
"""Create a new policy from an XML file.
|
|
151
|
+
|
|
152
|
+
The policy file should be in EPM Windows policy XML format. You can get a
|
|
153
|
+
template by downloading an existing policy:
|
|
154
|
+
|
|
155
|
+
bt epmw policies download <policy_id> > template.xml
|
|
156
|
+
"""
|
|
157
|
+
from bt_cli.epmw.client import get_client
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# Read policy file
|
|
161
|
+
try:
|
|
162
|
+
with open(file, "r") as f:
|
|
163
|
+
policy_content = f.read()
|
|
164
|
+
except FileNotFoundError:
|
|
165
|
+
print_error(f"Policy file not found: {file}")
|
|
166
|
+
raise typer.Exit(1)
|
|
167
|
+
|
|
168
|
+
client = get_client()
|
|
169
|
+
policy_id = client.create_policy(name, description, policy_content)
|
|
170
|
+
|
|
171
|
+
if output == OutputFormat.JSON:
|
|
172
|
+
# Fetch full policy details for JSON output
|
|
173
|
+
policy = client.get_policy(policy_id)
|
|
174
|
+
print_json(policy)
|
|
175
|
+
else:
|
|
176
|
+
print_success(f"Created policy '{name}' (ID: {policy_id})")
|
|
177
|
+
except httpx.HTTPStatusError as e:
|
|
178
|
+
print_api_error(e, "create policy")
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
except httpx.RequestError as e:
|
|
181
|
+
print_api_error(e, "create policy")
|
|
182
|
+
raise typer.Exit(1)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print_api_error(e, "create policy")
|
|
185
|
+
raise typer.Exit(1)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@app.command("delete")
|
|
189
|
+
def delete_policy(
|
|
190
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
191
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
192
|
+
):
|
|
193
|
+
"""Delete a policy."""
|
|
194
|
+
from bt_cli.epmw.client import get_client
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
client = get_client()
|
|
198
|
+
|
|
199
|
+
if not force:
|
|
200
|
+
policy = client.get_policy(policy_id)
|
|
201
|
+
policy_name = policy.get("name", policy_id)
|
|
202
|
+
if not typer.confirm(f"Delete policy '{policy_name}'?"):
|
|
203
|
+
print_warning("Cancelled.")
|
|
204
|
+
raise typer.Exit(0)
|
|
205
|
+
|
|
206
|
+
client.delete_policy(policy_id)
|
|
207
|
+
print_success(f"Deleted policy {policy_id[:8]}...")
|
|
208
|
+
except httpx.HTTPStatusError as e:
|
|
209
|
+
print_api_error(e, "delete policy")
|
|
210
|
+
raise typer.Exit(1)
|
|
211
|
+
except httpx.RequestError as e:
|
|
212
|
+
print_api_error(e, "delete policy")
|
|
213
|
+
raise typer.Exit(1)
|
|
214
|
+
except typer.Exit:
|
|
215
|
+
raise
|
|
216
|
+
except Exception as e:
|
|
217
|
+
print_api_error(e, "delete policy")
|
|
218
|
+
raise typer.Exit(1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("update")
|
|
222
|
+
def update_policy(
|
|
223
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
224
|
+
name: str = typer.Option(..., "--name", "-n", help="New policy name"),
|
|
225
|
+
description: str = typer.Option("", "--description", "-d", help="New description"),
|
|
226
|
+
output: OutputFormat = typer.Option(
|
|
227
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
228
|
+
),
|
|
229
|
+
):
|
|
230
|
+
"""Update a policy's name and description."""
|
|
231
|
+
from bt_cli.epmw.client import get_client
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
client = get_client()
|
|
235
|
+
result = client.update_policy(policy_id, name, description)
|
|
236
|
+
|
|
237
|
+
if output == OutputFormat.JSON:
|
|
238
|
+
print_json(result)
|
|
239
|
+
else:
|
|
240
|
+
print_success(f"Updated policy '{name}' (ID: {policy_id[:8]}...)")
|
|
241
|
+
except httpx.HTTPStatusError as e:
|
|
242
|
+
print_api_error(e, "update policy")
|
|
243
|
+
raise typer.Exit(1)
|
|
244
|
+
except httpx.RequestError as e:
|
|
245
|
+
print_api_error(e, "update policy")
|
|
246
|
+
raise typer.Exit(1)
|
|
247
|
+
except Exception as e:
|
|
248
|
+
print_api_error(e, "update policy")
|
|
249
|
+
raise typer.Exit(1)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
@app.command("revert")
|
|
253
|
+
def revert_policy(
|
|
254
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
255
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
256
|
+
):
|
|
257
|
+
"""Revert a policy to discard draft changes."""
|
|
258
|
+
from bt_cli.epmw.client import get_client
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
client = get_client()
|
|
262
|
+
|
|
263
|
+
if not force:
|
|
264
|
+
policy = client.get_policy(policy_id)
|
|
265
|
+
policy_name = policy.get("name", policy_id)
|
|
266
|
+
if not policy.get("hasOpenDraft"):
|
|
267
|
+
print_warning(f"Policy '{policy_name}' has no draft to revert.")
|
|
268
|
+
raise typer.Exit(0)
|
|
269
|
+
if not typer.confirm(f"Revert draft changes for policy '{policy_name}'?"):
|
|
270
|
+
print_warning("Cancelled.")
|
|
271
|
+
raise typer.Exit(0)
|
|
272
|
+
|
|
273
|
+
client.revert_policy(policy_id)
|
|
274
|
+
print_success(f"Reverted policy {policy_id[:8]}... - draft changes discarded")
|
|
275
|
+
except httpx.HTTPStatusError as e:
|
|
276
|
+
print_api_error(e, "revert policy")
|
|
277
|
+
raise typer.Exit(1)
|
|
278
|
+
except httpx.RequestError as e:
|
|
279
|
+
print_api_error(e, "revert policy")
|
|
280
|
+
raise typer.Exit(1)
|
|
281
|
+
except typer.Exit:
|
|
282
|
+
raise
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print_api_error(e, "revert policy")
|
|
285
|
+
raise typer.Exit(1)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@app.command("download")
|
|
289
|
+
def download_policy(
|
|
290
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
291
|
+
):
|
|
292
|
+
"""Download policy content (XML format)."""
|
|
293
|
+
from bt_cli.epmw.client import get_client
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
client = get_client()
|
|
297
|
+
content = client.download_policy(policy_id)
|
|
298
|
+
# Policy content is XML
|
|
299
|
+
typer.echo(content)
|
|
300
|
+
except httpx.HTTPStatusError as e:
|
|
301
|
+
print_api_error(e, "download policy")
|
|
302
|
+
raise typer.Exit(1)
|
|
303
|
+
except httpx.RequestError as e:
|
|
304
|
+
print_api_error(e, "download policy")
|
|
305
|
+
raise typer.Exit(1)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
print_api_error(e, "download policy")
|
|
308
|
+
raise typer.Exit(1)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@app.command("groups")
|
|
312
|
+
def list_policy_groups(
|
|
313
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
314
|
+
output: OutputFormat = typer.Option(
|
|
315
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
316
|
+
),
|
|
317
|
+
):
|
|
318
|
+
"""List groups assigned to a policy."""
|
|
319
|
+
from bt_cli.epmw.client import get_client
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
client = get_client()
|
|
323
|
+
groups = client.get_policy_groups(policy_id)
|
|
324
|
+
|
|
325
|
+
if output == OutputFormat.JSON:
|
|
326
|
+
print_json(groups)
|
|
327
|
+
else:
|
|
328
|
+
if not groups:
|
|
329
|
+
print_warning("No groups assigned to this policy.")
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
table = Table(title="Assigned Groups")
|
|
333
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
334
|
+
table.add_column("Name", style="green")
|
|
335
|
+
table.add_column("Description", style="dim")
|
|
336
|
+
|
|
337
|
+
for group in groups:
|
|
338
|
+
table.add_row(
|
|
339
|
+
str(group.get("id", ""))[:8] + "...",
|
|
340
|
+
group.get("name", ""),
|
|
341
|
+
group.get("description", "-"),
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
console.print(table)
|
|
345
|
+
except httpx.HTTPStatusError as e:
|
|
346
|
+
print_api_error(e, "list policy groups")
|
|
347
|
+
raise typer.Exit(1)
|
|
348
|
+
except httpx.RequestError as e:
|
|
349
|
+
print_api_error(e, "list policy groups")
|
|
350
|
+
raise typer.Exit(1)
|
|
351
|
+
except Exception as e:
|
|
352
|
+
print_api_error(e, "list policy groups")
|
|
353
|
+
raise typer.Exit(1)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ============================================================================
|
|
357
|
+
# Revisions Subcommands
|
|
358
|
+
# ============================================================================
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
@revisions_app.command("list")
|
|
362
|
+
def list_revisions(
|
|
363
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
364
|
+
output: OutputFormat = typer.Option(
|
|
365
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
366
|
+
),
|
|
367
|
+
):
|
|
368
|
+
"""List all revisions of a policy."""
|
|
369
|
+
from bt_cli.epmw.client import get_client
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
client = get_client()
|
|
373
|
+
revisions = client.list_policy_revisions(policy_id)
|
|
374
|
+
|
|
375
|
+
if output == OutputFormat.JSON:
|
|
376
|
+
print_json(revisions)
|
|
377
|
+
else:
|
|
378
|
+
if not revisions:
|
|
379
|
+
print_warning("No revisions found.")
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
table = Table(title="Policy Revisions")
|
|
383
|
+
table.add_column("Revision ID", style="cyan", no_wrap=True)
|
|
384
|
+
table.add_column("Rev #", justify="right")
|
|
385
|
+
table.add_column("Comment", style="dim")
|
|
386
|
+
table.add_column("Modified", style="blue")
|
|
387
|
+
table.add_column("Modified By", style="green")
|
|
388
|
+
|
|
389
|
+
for rev in revisions:
|
|
390
|
+
table.add_row(
|
|
391
|
+
str(rev.get("id", ""))[:8] + "...",
|
|
392
|
+
str(rev.get("revision", "")),
|
|
393
|
+
rev.get("comment", "-") or "-",
|
|
394
|
+
rev.get("lastModifiedDate", "-") or "-",
|
|
395
|
+
(rev.get("lastModifiedUser", "") or "-").split("@")[0],
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
console.print(table)
|
|
399
|
+
except httpx.HTTPStatusError as e:
|
|
400
|
+
print_api_error(e, "list revisions")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
except httpx.RequestError as e:
|
|
403
|
+
print_api_error(e, "list revisions")
|
|
404
|
+
raise typer.Exit(1)
|
|
405
|
+
except Exception as e:
|
|
406
|
+
print_api_error(e, "list revisions")
|
|
407
|
+
raise typer.Exit(1)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
@revisions_app.command("get")
|
|
411
|
+
def get_revision(
|
|
412
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
413
|
+
revision_id: str = typer.Argument(..., help="Revision ID (UUID)"),
|
|
414
|
+
output: OutputFormat = typer.Option(
|
|
415
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
416
|
+
),
|
|
417
|
+
):
|
|
418
|
+
"""Download a specific policy revision."""
|
|
419
|
+
from bt_cli.epmw.client import get_client
|
|
420
|
+
|
|
421
|
+
try:
|
|
422
|
+
client = get_client()
|
|
423
|
+
content = client.get_policy_revision(policy_id, revision_id)
|
|
424
|
+
|
|
425
|
+
if output == OutputFormat.JSON:
|
|
426
|
+
print_json(content)
|
|
427
|
+
else:
|
|
428
|
+
typer.echo(content)
|
|
429
|
+
except httpx.HTTPStatusError as e:
|
|
430
|
+
print_api_error(e, "get revision")
|
|
431
|
+
raise typer.Exit(1)
|
|
432
|
+
except httpx.RequestError as e:
|
|
433
|
+
print_api_error(e, "get revision")
|
|
434
|
+
raise typer.Exit(1)
|
|
435
|
+
except Exception as e:
|
|
436
|
+
print_api_error(e, "get revision")
|
|
437
|
+
raise typer.Exit(1)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@revisions_app.command("upload")
|
|
441
|
+
def upload_revision(
|
|
442
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
443
|
+
file: str = typer.Argument(..., help="Path to JSON file with policy content"),
|
|
444
|
+
comment: str = typer.Option("", "--comment", "-c", help="Revision comment"),
|
|
445
|
+
output: OutputFormat = typer.Option(
|
|
446
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
447
|
+
),
|
|
448
|
+
):
|
|
449
|
+
"""Upload a new policy revision from a JSON file."""
|
|
450
|
+
from bt_cli.epmw.client import get_client
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# Read and parse the JSON file
|
|
454
|
+
with open(file, "r") as f:
|
|
455
|
+
content = json_lib.load(f)
|
|
456
|
+
|
|
457
|
+
client = get_client()
|
|
458
|
+
result = client.upload_policy_revision(policy_id, content, comment)
|
|
459
|
+
|
|
460
|
+
if output == OutputFormat.JSON:
|
|
461
|
+
print_json(result)
|
|
462
|
+
else:
|
|
463
|
+
rev_id = result.get("id", "") if result else ""
|
|
464
|
+
print_success(f"Uploaded new revision for policy {policy_id[:8]}...")
|
|
465
|
+
if rev_id:
|
|
466
|
+
console.print(f" Revision ID: {rev_id[:8]}...")
|
|
467
|
+
except FileNotFoundError:
|
|
468
|
+
print_api_error(FileNotFoundError(f"File not found: {file}"), "upload revision")
|
|
469
|
+
raise typer.Exit(1)
|
|
470
|
+
except json_lib.JSONDecodeError as e:
|
|
471
|
+
print_api_error(e, "upload revision (invalid JSON)")
|
|
472
|
+
raise typer.Exit(1)
|
|
473
|
+
except httpx.HTTPStatusError as e:
|
|
474
|
+
print_api_error(e, "upload revision")
|
|
475
|
+
raise typer.Exit(1)
|
|
476
|
+
except httpx.RequestError as e:
|
|
477
|
+
print_api_error(e, "upload revision")
|
|
478
|
+
raise typer.Exit(1)
|
|
479
|
+
except Exception as e:
|
|
480
|
+
print_api_error(e, "upload revision")
|
|
481
|
+
raise typer.Exit(1)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ============================================================================
|
|
485
|
+
# Application Groups Subcommands (Policy Editor)
|
|
486
|
+
# ============================================================================
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
@appgroups_app.command("list")
|
|
490
|
+
def list_app_groups(
|
|
491
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
492
|
+
output: OutputFormat = typer.Option(
|
|
493
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
494
|
+
),
|
|
495
|
+
):
|
|
496
|
+
"""List application groups in a policy."""
|
|
497
|
+
from bt_cli.epmw.client import get_client
|
|
498
|
+
|
|
499
|
+
try:
|
|
500
|
+
client = get_client()
|
|
501
|
+
groups = client.list_application_groups(policy_id)
|
|
502
|
+
|
|
503
|
+
if output == OutputFormat.JSON:
|
|
504
|
+
print_json(groups)
|
|
505
|
+
else:
|
|
506
|
+
if not groups:
|
|
507
|
+
print_warning("No application groups found in this policy.")
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
table = Table(title="Application Groups")
|
|
511
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
512
|
+
table.add_column("Name", style="green")
|
|
513
|
+
table.add_column("Description", style="dim")
|
|
514
|
+
table.add_column("Hidden", justify="center")
|
|
515
|
+
|
|
516
|
+
for group in groups:
|
|
517
|
+
table.add_row(
|
|
518
|
+
str(group.get("id", ""))[:8] + "...",
|
|
519
|
+
group.get("name", ""),
|
|
520
|
+
group.get("description", "-") or "-",
|
|
521
|
+
"[yellow]Yes[/yellow]" if group.get("hidden") else "[dim]No[/dim]",
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
console.print(table)
|
|
525
|
+
except httpx.HTTPStatusError as e:
|
|
526
|
+
print_api_error(e, "list application groups")
|
|
527
|
+
raise typer.Exit(1)
|
|
528
|
+
except httpx.RequestError as e:
|
|
529
|
+
print_api_error(e, "list application groups")
|
|
530
|
+
raise typer.Exit(1)
|
|
531
|
+
except Exception as e:
|
|
532
|
+
print_api_error(e, "list application groups")
|
|
533
|
+
raise typer.Exit(1)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
@appgroups_app.command("get")
|
|
537
|
+
def get_app_group(
|
|
538
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
539
|
+
app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
|
|
540
|
+
output: OutputFormat = typer.Option(
|
|
541
|
+
OutputFormat.JSON, "--output", "-o", help="Output format"
|
|
542
|
+
),
|
|
543
|
+
):
|
|
544
|
+
"""Get details of an application group."""
|
|
545
|
+
from bt_cli.epmw.client import get_client
|
|
546
|
+
|
|
547
|
+
try:
|
|
548
|
+
client = get_client()
|
|
549
|
+
group = client.get_application_group(policy_id, app_group_id)
|
|
550
|
+
|
|
551
|
+
if output == OutputFormat.JSON:
|
|
552
|
+
print_json(group)
|
|
553
|
+
else:
|
|
554
|
+
console.print(Panel(
|
|
555
|
+
f"[bold]Name:[/bold] {group.get('name', '')}\n"
|
|
556
|
+
f"[bold]ID:[/bold] {group.get('id', '')}\n"
|
|
557
|
+
f"[bold]Description:[/bold] {group.get('description', '-') or '-'}\n"
|
|
558
|
+
f"[bold]Hidden:[/bold] {'Yes' if group.get('hidden') else 'No'}",
|
|
559
|
+
title=f"Application Group: {group.get('name', '')}",
|
|
560
|
+
))
|
|
561
|
+
except httpx.HTTPStatusError as e:
|
|
562
|
+
print_api_error(e, "get application group")
|
|
563
|
+
raise typer.Exit(1)
|
|
564
|
+
except httpx.RequestError as e:
|
|
565
|
+
print_api_error(e, "get application group")
|
|
566
|
+
raise typer.Exit(1)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
print_api_error(e, "get application group")
|
|
569
|
+
raise typer.Exit(1)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
@appgroups_app.command("create")
|
|
573
|
+
def create_app_group(
|
|
574
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
575
|
+
name: str = typer.Option(..., "--name", "-n", help="Application group name"),
|
|
576
|
+
description: str = typer.Option("", "--description", "-d", help="Description"),
|
|
577
|
+
hidden: bool = typer.Option(False, "--hidden", help="Mark group as hidden"),
|
|
578
|
+
output: OutputFormat = typer.Option(
|
|
579
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
580
|
+
),
|
|
581
|
+
):
|
|
582
|
+
"""Create an application group in a policy."""
|
|
583
|
+
from bt_cli.epmw.client import get_client
|
|
584
|
+
|
|
585
|
+
try:
|
|
586
|
+
client = get_client()
|
|
587
|
+
result = client.create_application_group(policy_id, name, description, hidden)
|
|
588
|
+
|
|
589
|
+
if output == OutputFormat.JSON:
|
|
590
|
+
print_json(result)
|
|
591
|
+
else:
|
|
592
|
+
group_id = result.get("id", "") if result else ""
|
|
593
|
+
print_success(f"Created application group '{name}'")
|
|
594
|
+
if group_id:
|
|
595
|
+
console.print(f" ID: {group_id[:8]}...")
|
|
596
|
+
except httpx.HTTPStatusError as e:
|
|
597
|
+
print_api_error(e, "create application group")
|
|
598
|
+
raise typer.Exit(1)
|
|
599
|
+
except httpx.RequestError as e:
|
|
600
|
+
print_api_error(e, "create application group")
|
|
601
|
+
raise typer.Exit(1)
|
|
602
|
+
except Exception as e:
|
|
603
|
+
print_api_error(e, "create application group")
|
|
604
|
+
raise typer.Exit(1)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
@appgroups_app.command("update")
|
|
608
|
+
def update_app_group(
|
|
609
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
610
|
+
app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
|
|
611
|
+
name: str = typer.Option(..., "--name", "-n", help="New name"),
|
|
612
|
+
description: str = typer.Option("", "--description", "-d", help="New description"),
|
|
613
|
+
hidden: bool = typer.Option(False, "--hidden", help="Mark group as hidden"),
|
|
614
|
+
output: OutputFormat = typer.Option(
|
|
615
|
+
OutputFormat.TABLE, "--output", "-o", help="Output format"
|
|
616
|
+
),
|
|
617
|
+
):
|
|
618
|
+
"""Update an application group."""
|
|
619
|
+
from bt_cli.epmw.client import get_client
|
|
620
|
+
|
|
621
|
+
try:
|
|
622
|
+
client = get_client()
|
|
623
|
+
result = client.update_application_group(
|
|
624
|
+
policy_id, app_group_id, name, description, hidden
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if output == OutputFormat.JSON:
|
|
628
|
+
print_json(result)
|
|
629
|
+
else:
|
|
630
|
+
print_success(f"Updated application group '{name}'")
|
|
631
|
+
except httpx.HTTPStatusError as e:
|
|
632
|
+
print_api_error(e, "update application group")
|
|
633
|
+
raise typer.Exit(1)
|
|
634
|
+
except httpx.RequestError as e:
|
|
635
|
+
print_api_error(e, "update application group")
|
|
636
|
+
raise typer.Exit(1)
|
|
637
|
+
except Exception as e:
|
|
638
|
+
print_api_error(e, "update application group")
|
|
639
|
+
raise typer.Exit(1)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@appgroups_app.command("delete")
|
|
643
|
+
def delete_app_group(
|
|
644
|
+
policy_id: str = typer.Argument(..., help="Policy ID (UUID)"),
|
|
645
|
+
app_group_id: str = typer.Argument(..., help="Application group ID (UUID)"),
|
|
646
|
+
force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
|
|
647
|
+
):
|
|
648
|
+
"""Delete an application group."""
|
|
649
|
+
from bt_cli.epmw.client import get_client
|
|
650
|
+
|
|
651
|
+
try:
|
|
652
|
+
client = get_client()
|
|
653
|
+
|
|
654
|
+
if not force:
|
|
655
|
+
group = client.get_application_group(policy_id, app_group_id)
|
|
656
|
+
group_name = group.get("name", app_group_id)
|
|
657
|
+
if not typer.confirm(f"Delete application group '{group_name}'?"):
|
|
658
|
+
print_warning("Cancelled.")
|
|
659
|
+
raise typer.Exit(0)
|
|
660
|
+
|
|
661
|
+
client.delete_application_group(policy_id, app_group_id)
|
|
662
|
+
print_success(f"Deleted application group {app_group_id[:8]}...")
|
|
663
|
+
except httpx.HTTPStatusError as e:
|
|
664
|
+
print_api_error(e, "delete application group")
|
|
665
|
+
raise typer.Exit(1)
|
|
666
|
+
except httpx.RequestError as e:
|
|
667
|
+
print_api_error(e, "delete application group")
|
|
668
|
+
raise typer.Exit(1)
|
|
669
|
+
except typer.Exit:
|
|
670
|
+
raise
|
|
671
|
+
except Exception as e:
|
|
672
|
+
print_api_error(e, "delete application group")
|
|
673
|
+
raise typer.Exit(1)
|