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
bt_cli/cli.py
ADDED
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
"""Main CLI entry point for BeyondTrust Unified Admin."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import typer
|
|
7
|
+
|
|
8
|
+
# Check rich version early to give helpful error
|
|
9
|
+
try:
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
# Get rich version from metadata (rich>=14 removed __version__)
|
|
12
|
+
try:
|
|
13
|
+
from importlib.metadata import version as get_version
|
|
14
|
+
rich_version = get_version("rich")
|
|
15
|
+
except ImportError:
|
|
16
|
+
import rich
|
|
17
|
+
rich_version = getattr(rich, "__version__", "13.7.0") # Assume OK if can't check
|
|
18
|
+
|
|
19
|
+
# rich 13.7+ required for typer compatibility
|
|
20
|
+
parts = rich_version.split(".")
|
|
21
|
+
major, minor = int(parts[0]), int(parts[1]) if len(parts) > 1 else 0
|
|
22
|
+
if major < 13 or (major == 13 and minor < 7):
|
|
23
|
+
print(
|
|
24
|
+
f"Error: rich {rich_version} is too old. bt-cli requires rich>=13.7.0\n"
|
|
25
|
+
f"Fix: pip install --upgrade rich>=13.7.0",
|
|
26
|
+
file=sys.stderr,
|
|
27
|
+
)
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
except ImportError as e:
|
|
30
|
+
print(f"Error: Missing required dependency: {e}\nFix: pip install bt-cli", file=sys.stderr)
|
|
31
|
+
sys.exit(1)
|
|
32
|
+
|
|
33
|
+
from . import __version__
|
|
34
|
+
|
|
35
|
+
# Create main app
|
|
36
|
+
app = typer.Typer(
|
|
37
|
+
name="bt",
|
|
38
|
+
help="BeyondTrust Platform CLI - Manage Password Safe, Entitle, PRA, and EPM",
|
|
39
|
+
no_args_is_help=True,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
console = Console()
|
|
43
|
+
|
|
44
|
+
# Global profile option (shared across all commands)
|
|
45
|
+
_active_profile: Optional[str] = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_active_profile() -> Optional[str]:
|
|
49
|
+
"""Get the currently active profile."""
|
|
50
|
+
return _active_profile
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Lazy load product apps to avoid import errors during development
|
|
54
|
+
def _get_pws_app() -> typer.Typer:
|
|
55
|
+
"""Lazy load Password Safe commands."""
|
|
56
|
+
from .pws.commands import app as pws_app
|
|
57
|
+
return pws_app
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _get_entitle_app() -> typer.Typer:
|
|
61
|
+
"""Lazy load Entitle commands."""
|
|
62
|
+
from .entitle.commands import app as entitle_app
|
|
63
|
+
return entitle_app
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _get_pra_app() -> typer.Typer:
|
|
67
|
+
"""Lazy load PRA commands."""
|
|
68
|
+
from .pra.commands import app as pra_app
|
|
69
|
+
return pra_app
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _get_epmw_app() -> typer.Typer:
|
|
73
|
+
"""Lazy load EPM Windows commands."""
|
|
74
|
+
from .epmw.commands import app as epmw_app
|
|
75
|
+
return epmw_app
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _get_configure_app() -> typer.Typer:
|
|
79
|
+
"""Lazy load configure commands."""
|
|
80
|
+
from .commands.configure import app as configure_app
|
|
81
|
+
return configure_app
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _get_learn_app() -> typer.Typer:
|
|
85
|
+
"""Lazy load learn commands."""
|
|
86
|
+
from .commands.learn import app as learn_app
|
|
87
|
+
return learn_app
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_quick_app() -> typer.Typer:
|
|
91
|
+
"""Lazy load global quick commands."""
|
|
92
|
+
from .commands.quick import app as quick_app
|
|
93
|
+
return quick_app
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Register product CLIs
|
|
97
|
+
# These will be loaded when the subcommand is invoked
|
|
98
|
+
try:
|
|
99
|
+
app.add_typer(_get_pws_app(), name="pws", help="Password Safe commands")
|
|
100
|
+
except Exception:
|
|
101
|
+
pass # PWS module not ready yet
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
app.add_typer(_get_entitle_app(), name="entitle", help="Entitle commands")
|
|
105
|
+
except Exception:
|
|
106
|
+
pass # Entitle module not ready yet
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
app.add_typer(_get_pra_app(), name="pra", help="Privileged Remote Access commands")
|
|
110
|
+
except Exception:
|
|
111
|
+
pass # PRA module not ready yet
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
app.add_typer(_get_epmw_app(), name="epmw", help="EPM Windows commands")
|
|
115
|
+
except Exception:
|
|
116
|
+
pass # EPMW module not ready yet
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
app.add_typer(_get_configure_app(), name="configure", help="Configure bt-cli settings")
|
|
120
|
+
except Exception:
|
|
121
|
+
pass # Configure module not ready yet
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
app.add_typer(_get_learn_app(), name="learn", help="Learning log for workflows and insights")
|
|
125
|
+
except Exception:
|
|
126
|
+
pass # Learn module not ready yet
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
app.add_typer(_get_quick_app(), name="quick", help="Cross-product quick commands (Total PASM)")
|
|
130
|
+
except Exception:
|
|
131
|
+
pass # Quick module not ready yet
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@app.callback()
|
|
135
|
+
def main_callback(
|
|
136
|
+
profile: Optional[str] = typer.Option(
|
|
137
|
+
None,
|
|
138
|
+
"--profile", "-P",
|
|
139
|
+
help="Use a specific configuration profile",
|
|
140
|
+
envvar="BT_PROFILE",
|
|
141
|
+
),
|
|
142
|
+
show_rest: bool = typer.Option(
|
|
143
|
+
False,
|
|
144
|
+
"--show-rest",
|
|
145
|
+
help="Show REST API calls (method, URL, headers, body)",
|
|
146
|
+
envvar="BT_SHOW_REST",
|
|
147
|
+
),
|
|
148
|
+
) -> None:
|
|
149
|
+
"""BeyondTrust Platform CLI.
|
|
150
|
+
|
|
151
|
+
A comprehensive CLI tool for managing BeyondTrust products:
|
|
152
|
+
|
|
153
|
+
- Password Safe: Credential management, secrets, systems
|
|
154
|
+
- Entitle: Just-in-time access management
|
|
155
|
+
- PRA: Privileged Remote Access (jumpoints, vault, jump items)
|
|
156
|
+
- EPMW: EPM Windows endpoint privilege management
|
|
157
|
+
|
|
158
|
+
Configuration (in order of precedence):
|
|
159
|
+
|
|
160
|
+
1. Command-line flags
|
|
161
|
+
2. Environment variables (BT_PWS_*, BT_ENTITLE_*, etc.)
|
|
162
|
+
3. Config file (~/.bt-cli/config.yaml)
|
|
163
|
+
|
|
164
|
+
Setup:
|
|
165
|
+
|
|
166
|
+
bt configure # Interactive setup wizard
|
|
167
|
+
bt configure --product pws --api-url https://...
|
|
168
|
+
|
|
169
|
+
Examples:
|
|
170
|
+
|
|
171
|
+
# Use default profile
|
|
172
|
+
bt pws auth test
|
|
173
|
+
bt pws systems list
|
|
174
|
+
|
|
175
|
+
# Use a specific profile
|
|
176
|
+
bt --profile production pws systems list
|
|
177
|
+
bt -P dev entitle integrations list
|
|
178
|
+
|
|
179
|
+
# Show REST API calls
|
|
180
|
+
bt --show-rest pws systems list
|
|
181
|
+
"""
|
|
182
|
+
global _active_profile
|
|
183
|
+
_active_profile = profile
|
|
184
|
+
|
|
185
|
+
# Enable REST debugging if requested
|
|
186
|
+
if show_rest:
|
|
187
|
+
from .core.rest_debug import set_show_rest
|
|
188
|
+
set_show_rest(True)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
@app.command("version")
|
|
192
|
+
def version() -> None:
|
|
193
|
+
"""Show version information."""
|
|
194
|
+
console.print(f"bt-cli version {__version__}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command("find")
|
|
198
|
+
def find_command(
|
|
199
|
+
term: str = typer.Argument(..., help="Search term to find in command names/help"),
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Search for commands by name or description.
|
|
202
|
+
|
|
203
|
+
Searches all bt commands and shows matches.
|
|
204
|
+
|
|
205
|
+
Examples:
|
|
206
|
+
bt find functional # Find commands related to functional accounts
|
|
207
|
+
bt find secret # Find secret-related commands
|
|
208
|
+
bt find jump # Find jump-related commands
|
|
209
|
+
"""
|
|
210
|
+
from rich.table import Table
|
|
211
|
+
|
|
212
|
+
# Build command registry
|
|
213
|
+
commands = _get_all_commands()
|
|
214
|
+
|
|
215
|
+
# Search
|
|
216
|
+
term_lower = term.lower()
|
|
217
|
+
matches = []
|
|
218
|
+
for cmd_path, cmd_help in commands:
|
|
219
|
+
if term_lower in cmd_path.lower() or term_lower in cmd_help.lower():
|
|
220
|
+
matches.append((cmd_path, cmd_help))
|
|
221
|
+
|
|
222
|
+
if not matches:
|
|
223
|
+
console.print(f"[yellow]No commands found matching '{term}'[/yellow]")
|
|
224
|
+
console.print("\nTry: bt tree # Show all commands")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
table = Table(title=f"Commands matching '{term}'")
|
|
228
|
+
table.add_column("Command", style="cyan")
|
|
229
|
+
table.add_column("Description", style="dim")
|
|
230
|
+
|
|
231
|
+
for cmd_path, cmd_help in sorted(matches):
|
|
232
|
+
# Truncate help text
|
|
233
|
+
short_help = cmd_help[:60] + "..." if len(cmd_help) > 60 else cmd_help
|
|
234
|
+
table.add_row(cmd_path, short_help)
|
|
235
|
+
|
|
236
|
+
console.print(table)
|
|
237
|
+
console.print(f"\n[dim]{len(matches)} command(s) found[/dim]")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@app.command("tree")
|
|
241
|
+
def tree_command(
|
|
242
|
+
product: Optional[str] = typer.Argument(None, help="Filter by product: pws, pra, entitle, epmw"),
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Show command hierarchy tree.
|
|
245
|
+
|
|
246
|
+
Displays the full command tree for navigation.
|
|
247
|
+
|
|
248
|
+
Examples:
|
|
249
|
+
bt tree # Show all commands
|
|
250
|
+
bt tree pws # Show only PWS commands
|
|
251
|
+
bt tree pra # Show only PRA commands
|
|
252
|
+
"""
|
|
253
|
+
from rich.tree import Tree
|
|
254
|
+
|
|
255
|
+
if product:
|
|
256
|
+
product = product.lower()
|
|
257
|
+
if product not in ["pws", "pra", "entitle", "epmw", "quick", "configure"]:
|
|
258
|
+
console.print(f"[red]Unknown product: {product}[/red]")
|
|
259
|
+
console.print("Available: pws, pra, entitle, epmw, quick, configure")
|
|
260
|
+
raise typer.Exit(1)
|
|
261
|
+
|
|
262
|
+
tree = Tree("[bold cyan]bt[/bold cyan]")
|
|
263
|
+
|
|
264
|
+
# Top-level commands
|
|
265
|
+
if not product:
|
|
266
|
+
tree.add("[green]version[/green] - Show version")
|
|
267
|
+
tree.add("[green]whoami[/green] - Test all connections")
|
|
268
|
+
tree.add("[green]find[/green] <term> - Search commands")
|
|
269
|
+
tree.add("[green]tree[/green] - Show this tree")
|
|
270
|
+
tree.add("[green]skills[/green] - Install Claude Code skills")
|
|
271
|
+
tree.add("[green]configure[/green] - Configure products")
|
|
272
|
+
|
|
273
|
+
# PWS
|
|
274
|
+
if not product or product == "pws":
|
|
275
|
+
pws = tree.add("[bold yellow]pws[/bold yellow] - Password Safe")
|
|
276
|
+
pws.add("[green]auth[/green] test")
|
|
277
|
+
pws.add("[green]search[/green] <query> - Search across all entities")
|
|
278
|
+
|
|
279
|
+
systems = pws.add("[green]systems[/green] list|get|create|update|delete")
|
|
280
|
+
accounts = pws.add("[green]accounts[/green] list|get|create|update|delete|rotate")
|
|
281
|
+
pws.add("[green]functional[/green] list|get|create|update|delete")
|
|
282
|
+
pws.add("[green]assets[/green] list|search|get|create|update|delete")
|
|
283
|
+
pws.add("[green]credentials[/green] checkout|checkin")
|
|
284
|
+
pws.add("[green]platforms[/green] list|get")
|
|
285
|
+
pws.add("[green]workgroups[/green] list|get")
|
|
286
|
+
|
|
287
|
+
secrets = pws.add("[green]secrets[/green]")
|
|
288
|
+
secrets.add("safes list|get|create|delete")
|
|
289
|
+
secrets.add("folders list|get|create|delete")
|
|
290
|
+
secrets.add("secrets list|get|create|create-text|create-file|delete")
|
|
291
|
+
|
|
292
|
+
pws.add("[green]quick[/green] checkout|onboard|offboard")
|
|
293
|
+
|
|
294
|
+
# PRA
|
|
295
|
+
if not product or product == "pra":
|
|
296
|
+
pra = tree.add("[bold yellow]pra[/bold yellow] - Privileged Remote Access")
|
|
297
|
+
pra.add("[green]auth[/green] test")
|
|
298
|
+
pra.add("[green]jumpoint[/green] list|get")
|
|
299
|
+
pra.add("[green]jump-groups[/green] list|get|create")
|
|
300
|
+
|
|
301
|
+
ji = pra.add("[green]jump-items[/green]")
|
|
302
|
+
ji.add("shell list|get|create|update|delete")
|
|
303
|
+
ji.add("rdp list|get|create|delete")
|
|
304
|
+
|
|
305
|
+
vault = pra.add("[green]vault[/green]")
|
|
306
|
+
vault.add("accounts list|get|create|delete|checkout|checkin|get-user-data|get-public-key")
|
|
307
|
+
vault.add("groups list|get")
|
|
308
|
+
|
|
309
|
+
pra.add("[green]quick[/green] shell-jump|rdp-jump")
|
|
310
|
+
|
|
311
|
+
# Entitle
|
|
312
|
+
if not product or product == "entitle":
|
|
313
|
+
ent = tree.add("[bold yellow]entitle[/bold yellow] - Just-in-Time Access")
|
|
314
|
+
ent.add("[green]auth[/green] test")
|
|
315
|
+
ent.add("[green]integrations[/green] list|get")
|
|
316
|
+
ent.add("[green]resources[/green] list|get|create-virtual|delete")
|
|
317
|
+
ent.add("[green]roles[/green] list|get")
|
|
318
|
+
ent.add("[green]bundles[/green] list|get|create|delete")
|
|
319
|
+
ent.add("[green]workflows[/green] list|get")
|
|
320
|
+
ent.add("[green]users[/green] list|get")
|
|
321
|
+
ent.add("[green]permissions[/green] list|revoke")
|
|
322
|
+
ent.add("[green]policies[/green] list|get")
|
|
323
|
+
ent.add("[green]accounts[/green] list")
|
|
324
|
+
ent.add("[green]agents[/green] list|get|status")
|
|
325
|
+
|
|
326
|
+
# EPMW
|
|
327
|
+
if not product or product == "epmw":
|
|
328
|
+
epmw = tree.add("[bold yellow]epmw[/bold yellow] - EPM Windows")
|
|
329
|
+
epmw.add("[green]auth[/green] test")
|
|
330
|
+
epmw.add("[green]computers[/green] list|get|archive")
|
|
331
|
+
epmw.add("[green]groups[/green] list|get")
|
|
332
|
+
epmw.add("[green]policies[/green] list|get")
|
|
333
|
+
epmw.add("[green]requests[/green] list|get|approve|deny")
|
|
334
|
+
epmw.add("[green]quick[/green] approve-all")
|
|
335
|
+
|
|
336
|
+
# Quick
|
|
337
|
+
if not product or product == "quick":
|
|
338
|
+
quick = tree.add("[bold yellow]quick[/bold yellow] - Cross-product workflows")
|
|
339
|
+
quick.add("[green]pasm-onboard[/green] - Onboard to PWS + PRA")
|
|
340
|
+
quick.add("[green]pasm-offboard[/green] - Remove from PWS + PRA")
|
|
341
|
+
quick.add("[green]pasm-search[/green] - Search across PWS + PRA")
|
|
342
|
+
|
|
343
|
+
console.print(tree)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _get_all_commands() -> list[tuple[str, str]]:
|
|
347
|
+
"""Build list of all commands with their help text."""
|
|
348
|
+
commands = [
|
|
349
|
+
("bt version", "Show version information"),
|
|
350
|
+
("bt whoami", "Test all configured products and show connection info"),
|
|
351
|
+
("bt find <term>", "Search for commands by name or description"),
|
|
352
|
+
("bt tree", "Show command hierarchy tree"),
|
|
353
|
+
("bt skills", "Install Claude Code skills"),
|
|
354
|
+
("bt configure", "Configure bt-cli settings"),
|
|
355
|
+
# PWS
|
|
356
|
+
("bt pws auth test", "Test Password Safe connection"),
|
|
357
|
+
("bt pws search <query>", "Search across all PWS entities"),
|
|
358
|
+
("bt pws systems list", "List managed systems"),
|
|
359
|
+
("bt pws systems get", "Get a managed system"),
|
|
360
|
+
("bt pws systems create", "Create a managed system"),
|
|
361
|
+
("bt pws systems update", "Update a managed system"),
|
|
362
|
+
("bt pws systems delete", "Delete a managed system"),
|
|
363
|
+
("bt pws accounts list", "List managed accounts"),
|
|
364
|
+
("bt pws accounts get", "Get a managed account"),
|
|
365
|
+
("bt pws accounts create", "Create a managed account"),
|
|
366
|
+
("bt pws accounts update", "Update a managed account"),
|
|
367
|
+
("bt pws accounts delete", "Delete a managed account"),
|
|
368
|
+
("bt pws accounts rotate", "Rotate account password"),
|
|
369
|
+
("bt pws functional list", "List functional accounts (for auto-management)"),
|
|
370
|
+
("bt pws functional get", "Get a functional account"),
|
|
371
|
+
("bt pws functional create", "Create a functional account"),
|
|
372
|
+
("bt pws functional update", "Update a functional account"),
|
|
373
|
+
("bt pws functional delete", "Delete a functional account"),
|
|
374
|
+
("bt pws assets list", "List assets"),
|
|
375
|
+
("bt pws assets search", "Search assets"),
|
|
376
|
+
("bt pws assets get", "Get an asset"),
|
|
377
|
+
("bt pws assets create", "Create an asset"),
|
|
378
|
+
("bt pws assets update", "Update an asset"),
|
|
379
|
+
("bt pws assets delete", "Delete an asset"),
|
|
380
|
+
("bt pws credentials checkout", "Check out account credentials"),
|
|
381
|
+
("bt pws credentials checkin", "Check in account credentials"),
|
|
382
|
+
("bt pws secrets safes list", "List Secrets Safe safes"),
|
|
383
|
+
("bt pws secrets folders list", "List Secrets Safe folders"),
|
|
384
|
+
("bt pws secrets secrets list", "List secrets"),
|
|
385
|
+
("bt pws secrets secrets create", "Create a secret"),
|
|
386
|
+
("bt pws secrets secrets create-text", "Create a text secret (supports --file)"),
|
|
387
|
+
("bt pws secrets secrets create-file", "Create a file secret"),
|
|
388
|
+
("bt pws quick checkout", "Quick checkout workflow"),
|
|
389
|
+
("bt pws quick onboard", "Quick onboard system + account"),
|
|
390
|
+
("bt pws quick offboard", "Quick offboard system"),
|
|
391
|
+
# PRA
|
|
392
|
+
("bt pra auth test", "Test PRA connection"),
|
|
393
|
+
("bt pra jumpoint list", "List jumpoints"),
|
|
394
|
+
("bt pra jump-groups list", "List jump groups"),
|
|
395
|
+
("bt pra jump-groups create", "Create a jump group"),
|
|
396
|
+
("bt pra jump-items shell list", "List shell jump items"),
|
|
397
|
+
("bt pra jump-items shell create", "Create a shell jump item"),
|
|
398
|
+
("bt pra jump-items shell update", "Update a shell jump item"),
|
|
399
|
+
("bt pra jump-items shell delete", "Delete a shell jump item"),
|
|
400
|
+
("bt pra jump-items rdp list", "List RDP jump items"),
|
|
401
|
+
("bt pra jump-items rdp create", "Create an RDP jump item"),
|
|
402
|
+
("bt pra vault accounts list", "List vault accounts"),
|
|
403
|
+
("bt pra vault accounts get", "Get a vault account"),
|
|
404
|
+
("bt pra vault accounts create", "Create a vault account"),
|
|
405
|
+
("bt pra vault accounts checkout", "Checkout vault credentials"),
|
|
406
|
+
("bt pra vault accounts get-user-data", "Generate EC2 user-data for SSH CA"),
|
|
407
|
+
("bt pra vault accounts get-public-key", "Get SSH CA public key"),
|
|
408
|
+
# Entitle
|
|
409
|
+
("bt entitle auth test", "Test Entitle connection"),
|
|
410
|
+
("bt entitle integrations list", "List integrations"),
|
|
411
|
+
("bt entitle resources list", "List resources"),
|
|
412
|
+
("bt entitle resources create-virtual", "Create a virtual resource"),
|
|
413
|
+
("bt entitle roles list", "List roles"),
|
|
414
|
+
("bt entitle bundles list", "List bundles"),
|
|
415
|
+
("bt entitle workflows list", "List workflows"),
|
|
416
|
+
("bt entitle users list", "List users"),
|
|
417
|
+
("bt entitle permissions list", "List permissions"),
|
|
418
|
+
("bt entitle permissions revoke", "Revoke a permission"),
|
|
419
|
+
("bt entitle agents list", "List agents"),
|
|
420
|
+
("bt entitle agents status", "Show agent status summary"),
|
|
421
|
+
# EPMW
|
|
422
|
+
("bt epmw auth test", "Test EPM Windows connection"),
|
|
423
|
+
("bt epmw computers list", "List managed computers"),
|
|
424
|
+
("bt epmw computers archive", "Archive a computer"),
|
|
425
|
+
("bt epmw groups list", "List computer groups"),
|
|
426
|
+
("bt epmw policies list", "List policies"),
|
|
427
|
+
("bt epmw requests list", "List elevation requests"),
|
|
428
|
+
("bt epmw requests approve", "Approve an elevation request"),
|
|
429
|
+
("bt epmw requests deny", "Deny an elevation request"),
|
|
430
|
+
("bt epmw quick approve-all", "Approve all pending requests"),
|
|
431
|
+
# Quick
|
|
432
|
+
("bt quick pasm-onboard", "Onboard host to PWS + PRA (Total PASM)"),
|
|
433
|
+
("bt quick pasm-offboard", "Offboard host from PWS + PRA"),
|
|
434
|
+
("bt quick pasm-search", "Search across PWS + PRA"),
|
|
435
|
+
]
|
|
436
|
+
return commands
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@app.command("skills")
|
|
440
|
+
def skills(
|
|
441
|
+
path: Optional[str] = typer.Option(None, "--path", "-p", help="Target directory (default: current)"),
|
|
442
|
+
force: bool = typer.Option(False, "--force", "-f", help="Overwrite existing skills"),
|
|
443
|
+
list_only: bool = typer.Option(False, "--list", "-l", help="List available skills without installing"),
|
|
444
|
+
) -> None:
|
|
445
|
+
"""Install Claude Code skills for AI agent navigation.
|
|
446
|
+
|
|
447
|
+
Copies bt-cli skills to .claude/skills/ in the target directory.
|
|
448
|
+
These skills help AI agents (like Claude Code) navigate bt-cli commands.
|
|
449
|
+
|
|
450
|
+
Skills included:
|
|
451
|
+
/bt - Cross-product commands (PASM workflows)
|
|
452
|
+
/pws - Password Safe commands
|
|
453
|
+
/pra - PRA commands
|
|
454
|
+
/entitle - Entitle commands
|
|
455
|
+
/epmw - EPM Windows commands
|
|
456
|
+
|
|
457
|
+
Examples:
|
|
458
|
+
bt skills # Install to current directory
|
|
459
|
+
bt skills -p /my/proj # Install to specific directory
|
|
460
|
+
bt skills --list # Show available skills
|
|
461
|
+
bt skills --force # Overwrite existing skills
|
|
462
|
+
"""
|
|
463
|
+
import shutil
|
|
464
|
+
import sys
|
|
465
|
+
from pathlib import Path
|
|
466
|
+
|
|
467
|
+
# Find skills in package data
|
|
468
|
+
skills_source = _get_skills_path()
|
|
469
|
+
if not skills_source:
|
|
470
|
+
console.print("[red]Error:[/red] Skills not found in package")
|
|
471
|
+
raise typer.Exit(1)
|
|
472
|
+
|
|
473
|
+
skills_source = Path(skills_source)
|
|
474
|
+
available_skills = [d.name for d in skills_source.iterdir() if d.is_dir() and not d.name.startswith("_")]
|
|
475
|
+
|
|
476
|
+
if list_only:
|
|
477
|
+
console.print("[bold]Available bt-cli skills:[/bold]\n")
|
|
478
|
+
for skill in sorted(available_skills):
|
|
479
|
+
skill_file = skills_source / skill / "SKILL.md"
|
|
480
|
+
if skill_file.exists():
|
|
481
|
+
# Read first line of description from YAML frontmatter
|
|
482
|
+
content = skill_file.read_text()
|
|
483
|
+
desc = ""
|
|
484
|
+
if "description:" in content:
|
|
485
|
+
for line in content.split("\n"):
|
|
486
|
+
if line.startswith("description:"):
|
|
487
|
+
desc = line.split(":", 1)[1].strip()
|
|
488
|
+
break
|
|
489
|
+
console.print(f" [cyan]/{skill}[/cyan] - {desc[:60]}...")
|
|
490
|
+
console.print(f"\n[dim]Run 'bt skills' to install these to your project.[/dim]")
|
|
491
|
+
return
|
|
492
|
+
|
|
493
|
+
# Determine target directory
|
|
494
|
+
target_base = Path(path) if path else Path.cwd()
|
|
495
|
+
if not target_base.exists():
|
|
496
|
+
console.print(f"[red]Error:[/red] Directory {target_base} does not exist")
|
|
497
|
+
raise typer.Exit(1)
|
|
498
|
+
|
|
499
|
+
target_skills = target_base / ".claude" / "skills"
|
|
500
|
+
|
|
501
|
+
# Check for existing skills
|
|
502
|
+
existing = []
|
|
503
|
+
for skill in available_skills:
|
|
504
|
+
skill_target = target_skills / skill
|
|
505
|
+
if skill_target.exists() and not force:
|
|
506
|
+
existing.append(skill)
|
|
507
|
+
|
|
508
|
+
if existing and not force:
|
|
509
|
+
console.print(f"[yellow]Warning:[/yellow] The following skills already exist:")
|
|
510
|
+
for skill in existing:
|
|
511
|
+
console.print(f" - {skill}")
|
|
512
|
+
console.print("\nUse --force to overwrite, or remove them manually.")
|
|
513
|
+
raise typer.Exit(1)
|
|
514
|
+
|
|
515
|
+
# Create target directory
|
|
516
|
+
target_skills.mkdir(parents=True, exist_ok=True)
|
|
517
|
+
|
|
518
|
+
# Copy skills
|
|
519
|
+
installed = []
|
|
520
|
+
for skill in available_skills:
|
|
521
|
+
skill_source_dir = skills_source / skill
|
|
522
|
+
skill_target_dir = target_skills / skill
|
|
523
|
+
|
|
524
|
+
if skill_target_dir.exists():
|
|
525
|
+
shutil.rmtree(skill_target_dir)
|
|
526
|
+
|
|
527
|
+
shutil.copytree(skill_source_dir, skill_target_dir)
|
|
528
|
+
installed.append(skill)
|
|
529
|
+
|
|
530
|
+
# Also copy CLAUDE.md if it exists
|
|
531
|
+
claude_md_source = _get_claude_md_path()
|
|
532
|
+
if claude_md_source:
|
|
533
|
+
claude_md_target = target_base / "CLAUDE.md"
|
|
534
|
+
if not claude_md_target.exists() or force:
|
|
535
|
+
shutil.copy(claude_md_source, claude_md_target)
|
|
536
|
+
console.print(f"[green]Created[/green] CLAUDE.md")
|
|
537
|
+
|
|
538
|
+
console.print(f"\n[green]Installed {len(installed)} skills to {target_skills}[/green]\n")
|
|
539
|
+
for skill in sorted(installed):
|
|
540
|
+
console.print(f" [cyan]/{skill}[/cyan]")
|
|
541
|
+
console.print("\n[dim]Claude Code will now discover these skills automatically.[/dim]")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def _get_skills_path() -> Optional[str]:
|
|
545
|
+
"""Find skills directory in package data."""
|
|
546
|
+
import sys
|
|
547
|
+
from pathlib import Path
|
|
548
|
+
|
|
549
|
+
# Try PyInstaller bundle first
|
|
550
|
+
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
|
551
|
+
bundle_path = Path(sys._MEIPASS) / "bt_cli" / "data" / "skills"
|
|
552
|
+
if bundle_path.exists():
|
|
553
|
+
return str(bundle_path)
|
|
554
|
+
|
|
555
|
+
# Try importlib.resources (files() works with directories in 3.9+)
|
|
556
|
+
try:
|
|
557
|
+
if sys.version_info >= (3, 9):
|
|
558
|
+
from importlib.resources import files
|
|
559
|
+
data_path = files("bt_cli.data").joinpath("skills")
|
|
560
|
+
if data_path.is_dir():
|
|
561
|
+
return str(data_path)
|
|
562
|
+
else:
|
|
563
|
+
from importlib.resources import path
|
|
564
|
+
with path("bt_cli.data", "skills") as p:
|
|
565
|
+
if p.exists():
|
|
566
|
+
return str(p)
|
|
567
|
+
except (ImportError, ModuleNotFoundError, TypeError, FileNotFoundError, IsADirectoryError):
|
|
568
|
+
pass
|
|
569
|
+
|
|
570
|
+
# Fall back to source directory
|
|
571
|
+
current = Path(__file__).resolve().parent
|
|
572
|
+
candidate = current / "data" / "skills"
|
|
573
|
+
if candidate.exists():
|
|
574
|
+
return str(candidate)
|
|
575
|
+
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _get_claude_md_path() -> Optional[str]:
|
|
580
|
+
"""Find CLAUDE.md in package data."""
|
|
581
|
+
import sys
|
|
582
|
+
from pathlib import Path
|
|
583
|
+
|
|
584
|
+
# Try PyInstaller bundle first
|
|
585
|
+
if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
|
|
586
|
+
bundle_path = Path(sys._MEIPASS) / "bt_cli" / "data" / "CLAUDE.md"
|
|
587
|
+
if bundle_path.exists():
|
|
588
|
+
return str(bundle_path)
|
|
589
|
+
|
|
590
|
+
# Try importlib.resources (files() available in 3.9+)
|
|
591
|
+
try:
|
|
592
|
+
if sys.version_info >= (3, 9):
|
|
593
|
+
from importlib.resources import files
|
|
594
|
+
data_path = files("bt_cli.data").joinpath("CLAUDE.md")
|
|
595
|
+
if data_path.is_file():
|
|
596
|
+
return str(data_path)
|
|
597
|
+
else:
|
|
598
|
+
from importlib.resources import path
|
|
599
|
+
with path("bt_cli.data", "CLAUDE.md") as p:
|
|
600
|
+
if p.exists():
|
|
601
|
+
return str(p)
|
|
602
|
+
except (ImportError, ModuleNotFoundError, TypeError, FileNotFoundError):
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
# Fall back to source directory
|
|
606
|
+
current = Path(__file__).resolve().parent
|
|
607
|
+
candidate = current / "data" / "CLAUDE.md"
|
|
608
|
+
if candidate.exists():
|
|
609
|
+
return str(candidate)
|
|
610
|
+
|
|
611
|
+
return None
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
@app.command("whoami")
|
|
615
|
+
def whoami(
|
|
616
|
+
output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
|
|
617
|
+
) -> None:
|
|
618
|
+
"""Test all configured products and show connection info.
|
|
619
|
+
|
|
620
|
+
Checks each BeyondTrust product for valid configuration, tests connectivity,
|
|
621
|
+
and displays information about the authenticated API user/connection.
|
|
622
|
+
|
|
623
|
+
Examples:
|
|
624
|
+
bt whoami # Test all configured products
|
|
625
|
+
bt whoami -o json # Output as JSON
|
|
626
|
+
"""
|
|
627
|
+
import json
|
|
628
|
+
from rich.table import Table
|
|
629
|
+
|
|
630
|
+
results = []
|
|
631
|
+
|
|
632
|
+
# Test Password Safe
|
|
633
|
+
pws_result = _test_pws_connection()
|
|
634
|
+
if pws_result:
|
|
635
|
+
results.append(pws_result)
|
|
636
|
+
|
|
637
|
+
# Test Entitle
|
|
638
|
+
entitle_result = _test_entitle_connection()
|
|
639
|
+
if entitle_result:
|
|
640
|
+
results.append(entitle_result)
|
|
641
|
+
|
|
642
|
+
# Test PRA
|
|
643
|
+
pra_result = _test_pra_connection()
|
|
644
|
+
if pra_result:
|
|
645
|
+
results.append(pra_result)
|
|
646
|
+
|
|
647
|
+
# Test EPMW
|
|
648
|
+
epmw_result = _test_epmw_connection()
|
|
649
|
+
if epmw_result:
|
|
650
|
+
results.append(epmw_result)
|
|
651
|
+
|
|
652
|
+
if not results:
|
|
653
|
+
console.print("[yellow]No products configured.[/yellow]")
|
|
654
|
+
console.print("\nTo configure products, set environment variables or run:")
|
|
655
|
+
console.print(" bt configure --product <pws|entitle|pra|epmw>")
|
|
656
|
+
raise typer.Exit(1)
|
|
657
|
+
|
|
658
|
+
if output == "json":
|
|
659
|
+
console.print_json(json.dumps(results, indent=2))
|
|
660
|
+
else:
|
|
661
|
+
table = Table(title="BeyondTrust Product Connections")
|
|
662
|
+
table.add_column("Product", style="cyan")
|
|
663
|
+
table.add_column("Status", style="bold")
|
|
664
|
+
table.add_column("URL")
|
|
665
|
+
table.add_column("Auth")
|
|
666
|
+
table.add_column("User/Info")
|
|
667
|
+
|
|
668
|
+
for r in results:
|
|
669
|
+
status = "[green]Connected[/green]" if r["connected"] else f"[red]Failed[/red]"
|
|
670
|
+
table.add_row(
|
|
671
|
+
r["product"],
|
|
672
|
+
status,
|
|
673
|
+
r.get("url", "-"),
|
|
674
|
+
r.get("auth_method", "-"),
|
|
675
|
+
r.get("user_info", r.get("error", "-")),
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
console.print(table)
|
|
679
|
+
|
|
680
|
+
# Summary
|
|
681
|
+
connected = sum(1 for r in results if r["connected"])
|
|
682
|
+
console.print(f"\n[dim]{connected}/{len(results)} products connected[/dim]")
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _test_pws_connection() -> Optional[dict]:
|
|
686
|
+
"""Test PWS connection and return status."""
|
|
687
|
+
try:
|
|
688
|
+
from .core.config import load_pws_config
|
|
689
|
+
from .pws.client.base import get_client
|
|
690
|
+
|
|
691
|
+
config = load_pws_config()
|
|
692
|
+
result = {
|
|
693
|
+
"product": "Password Safe",
|
|
694
|
+
"url": config.api_url,
|
|
695
|
+
"auth_method": config.auth_method,
|
|
696
|
+
"connected": False,
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
with get_client() as client:
|
|
700
|
+
response = client.authenticate()
|
|
701
|
+
user_name = response.get("UserName", "Unknown")
|
|
702
|
+
user_id = response.get("UserId", "N/A")
|
|
703
|
+
result["connected"] = True
|
|
704
|
+
result["user_info"] = f"{user_name} (ID: {user_id})"
|
|
705
|
+
result["user_name"] = user_name
|
|
706
|
+
result["user_id"] = user_id
|
|
707
|
+
|
|
708
|
+
return result
|
|
709
|
+
except ValueError:
|
|
710
|
+
# Not configured
|
|
711
|
+
return None
|
|
712
|
+
except Exception as e:
|
|
713
|
+
return {
|
|
714
|
+
"product": "Password Safe",
|
|
715
|
+
"url": "",
|
|
716
|
+
"auth_method": "-",
|
|
717
|
+
"connected": False,
|
|
718
|
+
"error": str(e)[:50],
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def _test_entitle_connection() -> Optional[dict]:
|
|
723
|
+
"""Test Entitle connection and return status."""
|
|
724
|
+
try:
|
|
725
|
+
from .core.config import load_entitle_config
|
|
726
|
+
from .entitle.client.base import get_client
|
|
727
|
+
|
|
728
|
+
config = load_entitle_config()
|
|
729
|
+
masked_key = config.api_key[:8] + "..." if len(config.api_key) > 8 else "***"
|
|
730
|
+
result = {
|
|
731
|
+
"product": "Entitle",
|
|
732
|
+
"url": config.api_url,
|
|
733
|
+
"auth_method": f"API Key ({masked_key})",
|
|
734
|
+
"connected": False,
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
with get_client() as client:
|
|
738
|
+
apps = client.list_applications(limit=1)
|
|
739
|
+
result["connected"] = True
|
|
740
|
+
result["user_info"] = f"{len(apps)} application(s) accessible"
|
|
741
|
+
|
|
742
|
+
return result
|
|
743
|
+
except ValueError:
|
|
744
|
+
# Not configured
|
|
745
|
+
return None
|
|
746
|
+
except Exception as e:
|
|
747
|
+
return {
|
|
748
|
+
"product": "Entitle",
|
|
749
|
+
"url": "",
|
|
750
|
+
"auth_method": "-",
|
|
751
|
+
"connected": False,
|
|
752
|
+
"error": str(e)[:50],
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
def _test_pra_connection() -> Optional[dict]:
|
|
757
|
+
"""Test PRA connection and return status."""
|
|
758
|
+
try:
|
|
759
|
+
from .core.config import load_pra_config
|
|
760
|
+
from .pra.client import get_client
|
|
761
|
+
|
|
762
|
+
config = load_pra_config()
|
|
763
|
+
masked_id = config.client_id[:8] + "..." if len(config.client_id) > 8 else "***"
|
|
764
|
+
result = {
|
|
765
|
+
"product": "PRA",
|
|
766
|
+
"url": config.api_url,
|
|
767
|
+
"auth_method": f"OAuth ({masked_id})",
|
|
768
|
+
"connected": False,
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
client = get_client()
|
|
772
|
+
jumpoints = client.list_jumpoints()
|
|
773
|
+
result["connected"] = True
|
|
774
|
+
result["user_info"] = f"{len(jumpoints)} jumpoint(s) accessible"
|
|
775
|
+
|
|
776
|
+
return result
|
|
777
|
+
except ValueError:
|
|
778
|
+
# Not configured
|
|
779
|
+
return None
|
|
780
|
+
except Exception as e:
|
|
781
|
+
return {
|
|
782
|
+
"product": "PRA",
|
|
783
|
+
"url": "",
|
|
784
|
+
"auth_method": "-",
|
|
785
|
+
"connected": False,
|
|
786
|
+
"error": str(e)[:50],
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
|
|
790
|
+
def _test_epmw_connection() -> Optional[dict]:
|
|
791
|
+
"""Test EPMW connection and return status."""
|
|
792
|
+
try:
|
|
793
|
+
from .core.config import load_epmw_config
|
|
794
|
+
from .epmw.client import get_client
|
|
795
|
+
|
|
796
|
+
config = load_epmw_config()
|
|
797
|
+
masked_id = config.client_id[:8] + "..." if len(config.client_id) > 8 else "***"
|
|
798
|
+
result = {
|
|
799
|
+
"product": "EPM Windows",
|
|
800
|
+
"url": config.api_url,
|
|
801
|
+
"auth_method": f"OAuth ({masked_id})",
|
|
802
|
+
"connected": False,
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
client = get_client()
|
|
806
|
+
computers = client.list_computers()
|
|
807
|
+
result["connected"] = True
|
|
808
|
+
result["user_info"] = f"{len(computers)} computer(s) managed"
|
|
809
|
+
|
|
810
|
+
return result
|
|
811
|
+
except ValueError:
|
|
812
|
+
# Not configured
|
|
813
|
+
return None
|
|
814
|
+
except Exception as e:
|
|
815
|
+
return {
|
|
816
|
+
"product": "EPM Windows",
|
|
817
|
+
"url": "",
|
|
818
|
+
"auth_method": "-",
|
|
819
|
+
"connected": False,
|
|
820
|
+
"error": str(e)[:50],
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def run() -> None:
|
|
825
|
+
"""Run the CLI application."""
|
|
826
|
+
app()
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
if __name__ == "__main__":
|
|
830
|
+
run()
|