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.
Files changed (121) hide show
  1. bt_cli/__init__.py +3 -0
  2. bt_cli/cli.py +830 -0
  3. bt_cli/commands/__init__.py +1 -0
  4. bt_cli/commands/configure.py +415 -0
  5. bt_cli/commands/learn.py +229 -0
  6. bt_cli/commands/quick.py +784 -0
  7. bt_cli/core/__init__.py +1 -0
  8. bt_cli/core/auth.py +213 -0
  9. bt_cli/core/client.py +313 -0
  10. bt_cli/core/config.py +393 -0
  11. bt_cli/core/config_file.py +420 -0
  12. bt_cli/core/csv_utils.py +91 -0
  13. bt_cli/core/errors.py +247 -0
  14. bt_cli/core/output.py +205 -0
  15. bt_cli/core/prompts.py +87 -0
  16. bt_cli/core/rest_debug.py +221 -0
  17. bt_cli/data/CLAUDE.md +94 -0
  18. bt_cli/data/__init__.py +0 -0
  19. bt_cli/data/skills/bt/SKILL.md +108 -0
  20. bt_cli/data/skills/entitle/SKILL.md +170 -0
  21. bt_cli/data/skills/epmw/SKILL.md +144 -0
  22. bt_cli/data/skills/pra/SKILL.md +150 -0
  23. bt_cli/data/skills/pws/SKILL.md +198 -0
  24. bt_cli/entitle/__init__.py +1 -0
  25. bt_cli/entitle/client/__init__.py +5 -0
  26. bt_cli/entitle/client/base.py +443 -0
  27. bt_cli/entitle/commands/__init__.py +24 -0
  28. bt_cli/entitle/commands/accounts.py +53 -0
  29. bt_cli/entitle/commands/applications.py +39 -0
  30. bt_cli/entitle/commands/auth.py +68 -0
  31. bt_cli/entitle/commands/bundles.py +218 -0
  32. bt_cli/entitle/commands/integrations.py +60 -0
  33. bt_cli/entitle/commands/permissions.py +70 -0
  34. bt_cli/entitle/commands/policies.py +97 -0
  35. bt_cli/entitle/commands/resources.py +131 -0
  36. bt_cli/entitle/commands/roles.py +74 -0
  37. bt_cli/entitle/commands/users.py +123 -0
  38. bt_cli/entitle/commands/workflows.py +187 -0
  39. bt_cli/entitle/models/__init__.py +31 -0
  40. bt_cli/entitle/models/bundle.py +28 -0
  41. bt_cli/entitle/models/common.py +37 -0
  42. bt_cli/entitle/models/integration.py +30 -0
  43. bt_cli/entitle/models/permission.py +27 -0
  44. bt_cli/entitle/models/policy.py +25 -0
  45. bt_cli/entitle/models/resource.py +29 -0
  46. bt_cli/entitle/models/role.py +28 -0
  47. bt_cli/entitle/models/user.py +24 -0
  48. bt_cli/entitle/models/workflow.py +55 -0
  49. bt_cli/epmw/__init__.py +1 -0
  50. bt_cli/epmw/client/__init__.py +5 -0
  51. bt_cli/epmw/client/base.py +848 -0
  52. bt_cli/epmw/commands/__init__.py +33 -0
  53. bt_cli/epmw/commands/audits.py +250 -0
  54. bt_cli/epmw/commands/auth.py +55 -0
  55. bt_cli/epmw/commands/computers.py +140 -0
  56. bt_cli/epmw/commands/events.py +233 -0
  57. bt_cli/epmw/commands/groups.py +215 -0
  58. bt_cli/epmw/commands/policies.py +673 -0
  59. bt_cli/epmw/commands/quick.py +348 -0
  60. bt_cli/epmw/commands/requests.py +224 -0
  61. bt_cli/epmw/commands/roles.py +78 -0
  62. bt_cli/epmw/commands/tasks.py +38 -0
  63. bt_cli/epmw/commands/users.py +219 -0
  64. bt_cli/epmw/models/__init__.py +1 -0
  65. bt_cli/pra/__init__.py +1 -0
  66. bt_cli/pra/client/__init__.py +5 -0
  67. bt_cli/pra/client/base.py +618 -0
  68. bt_cli/pra/commands/__init__.py +30 -0
  69. bt_cli/pra/commands/auth.py +55 -0
  70. bt_cli/pra/commands/import_export.py +442 -0
  71. bt_cli/pra/commands/jump_clients.py +139 -0
  72. bt_cli/pra/commands/jump_groups.py +146 -0
  73. bt_cli/pra/commands/jump_items.py +638 -0
  74. bt_cli/pra/commands/jumpoints.py +95 -0
  75. bt_cli/pra/commands/policies.py +197 -0
  76. bt_cli/pra/commands/quick.py +470 -0
  77. bt_cli/pra/commands/teams.py +81 -0
  78. bt_cli/pra/commands/users.py +87 -0
  79. bt_cli/pra/commands/vault.py +564 -0
  80. bt_cli/pra/models/__init__.py +27 -0
  81. bt_cli/pra/models/common.py +12 -0
  82. bt_cli/pra/models/jump_client.py +25 -0
  83. bt_cli/pra/models/jump_group.py +15 -0
  84. bt_cli/pra/models/jump_item.py +72 -0
  85. bt_cli/pra/models/jumpoint.py +19 -0
  86. bt_cli/pra/models/team.py +14 -0
  87. bt_cli/pra/models/user.py +17 -0
  88. bt_cli/pra/models/vault.py +45 -0
  89. bt_cli/pws/__init__.py +1 -0
  90. bt_cli/pws/client/__init__.py +5 -0
  91. bt_cli/pws/client/base.py +356 -0
  92. bt_cli/pws/client/beyondinsight.py +869 -0
  93. bt_cli/pws/client/passwordsafe.py +1786 -0
  94. bt_cli/pws/commands/__init__.py +33 -0
  95. bt_cli/pws/commands/accounts.py +372 -0
  96. bt_cli/pws/commands/assets.py +311 -0
  97. bt_cli/pws/commands/auth.py +166 -0
  98. bt_cli/pws/commands/clouds.py +221 -0
  99. bt_cli/pws/commands/config.py +344 -0
  100. bt_cli/pws/commands/credentials.py +347 -0
  101. bt_cli/pws/commands/databases.py +306 -0
  102. bt_cli/pws/commands/directories.py +199 -0
  103. bt_cli/pws/commands/functional.py +298 -0
  104. bt_cli/pws/commands/import_export.py +452 -0
  105. bt_cli/pws/commands/platforms.py +118 -0
  106. bt_cli/pws/commands/quick.py +1646 -0
  107. bt_cli/pws/commands/search.py +256 -0
  108. bt_cli/pws/commands/secrets.py +1343 -0
  109. bt_cli/pws/commands/systems.py +389 -0
  110. bt_cli/pws/commands/users.py +415 -0
  111. bt_cli/pws/commands/workgroups.py +166 -0
  112. bt_cli/pws/config.py +18 -0
  113. bt_cli/pws/models/__init__.py +19 -0
  114. bt_cli/pws/models/account.py +186 -0
  115. bt_cli/pws/models/asset.py +102 -0
  116. bt_cli/pws/models/common.py +132 -0
  117. bt_cli/pws/models/system.py +121 -0
  118. bt_cli-0.4.13.dist-info/METADATA +417 -0
  119. bt_cli-0.4.13.dist-info/RECORD +121 -0
  120. bt_cli-0.4.13.dist-info/WHEEL +4 -0
  121. bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,199 @@
1
+ """CLI commands for managing directories (Entra ID, Active Directory)."""
2
+
3
+ from typing import Optional
4
+ import json
5
+
6
+ import httpx
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from ...core.output import print_api_error
12
+ from ..client.base import get_client
13
+
14
+ app = typer.Typer(no_args_is_help=True, help="Manage directory systems (Entra ID, Active Directory)")
15
+ console = Console()
16
+
17
+ # Directory platform IDs
18
+ DIRECTORY_PLATFORMS = {
19
+ "entra": 84,
20
+ "entraid": 84,
21
+ "entra-id": 84,
22
+ "azuread": 84,
23
+ "ad": 25,
24
+ "activedirectory": 25,
25
+ }
26
+
27
+
28
+ def print_directories_table(directories: list[dict], title: str = "Directories") -> None:
29
+ """Print directories in a formatted table."""
30
+ table = Table(title=title)
31
+ table.add_column("ID", style="cyan", no_wrap=True)
32
+ table.add_column("Domain", style="green")
33
+ table.add_column("Platform", style="yellow")
34
+ table.add_column("Workgroup", style="magenta")
35
+ table.add_column("FA ID", style="blue")
36
+
37
+ for d in directories:
38
+ table.add_row(
39
+ str(d.get("DirectoryID", "")),
40
+ d.get("DomainName", "-"),
41
+ str(d.get("PlatformID", "-")),
42
+ str(d.get("WorkgroupID", "-")),
43
+ str(d.get("FunctionalAccountID", "-")),
44
+ )
45
+
46
+ console.print(table)
47
+
48
+
49
+ @app.command("list")
50
+ def list_directories(
51
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
52
+ ) -> None:
53
+ """List directories (Entra ID, Active Directory)."""
54
+ try:
55
+ with get_client() as client:
56
+ client.authenticate()
57
+ directories = client.list_directories()
58
+
59
+ if output == "json":
60
+ console.print_json(json.dumps(directories, default=str))
61
+ else:
62
+ if directories:
63
+ print_directories_table(directories)
64
+ else:
65
+ console.print("[yellow]No directories found.[/yellow]")
66
+
67
+ except httpx.HTTPStatusError as e:
68
+ print_api_error(e, "manage directories")
69
+ raise typer.Exit(1)
70
+ except httpx.RequestError as e:
71
+ print_api_error(e, "manage directories")
72
+ raise typer.Exit(1)
73
+ except Exception as e:
74
+ print_api_error(e, "manage directories")
75
+ raise typer.Exit(1)
76
+
77
+
78
+ @app.command("get")
79
+ def get_directory(
80
+ directory_id: int = typer.Argument(..., help="Directory ID"),
81
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
82
+ ) -> None:
83
+ """Get a directory by ID."""
84
+ try:
85
+ with get_client() as client:
86
+ client.authenticate()
87
+ directory = client.get_directory(directory_id)
88
+
89
+ if output == "json":
90
+ console.print_json(json.dumps(directory, default=str))
91
+ else:
92
+ console.print(f"\n[bold cyan]Directory: {directory.get('DomainName', 'Unknown')}[/bold cyan]\n")
93
+
94
+ info_table = Table(show_header=False, box=None)
95
+ info_table.add_column("Field", style="dim")
96
+ info_table.add_column("Value")
97
+
98
+ fields = [
99
+ ("ID", "DirectoryID"),
100
+ ("Domain", "DomainName"),
101
+ ("Forest", "ForestName"),
102
+ ("NetBIOS", "NetBiosName"),
103
+ ("Platform ID", "PlatformID"),
104
+ ("Workgroup ID", "WorkgroupID"),
105
+ ("Port", "Port"),
106
+ ("Use SSL", "UseSSL"),
107
+ ("Functional Account ID", "FunctionalAccountID"),
108
+ ("Auto Management", "AutoManagementFlag"),
109
+ ("Description", "Description"),
110
+ ]
111
+
112
+ for label, key in fields:
113
+ value = directory.get(key)
114
+ if value is not None:
115
+ if isinstance(value, bool):
116
+ value = "Yes" if value else "No"
117
+ info_table.add_row(label, str(value))
118
+
119
+ console.print(info_table)
120
+
121
+ except httpx.HTTPStatusError as e:
122
+ print_api_error(e, "manage directories")
123
+ raise typer.Exit(1)
124
+ except httpx.RequestError as e:
125
+ print_api_error(e, "manage directories")
126
+ raise typer.Exit(1)
127
+ except Exception as e:
128
+ print_api_error(e, "manage directories")
129
+ raise typer.Exit(1)
130
+
131
+
132
+ @app.command("add-system")
133
+ def add_managed_system(
134
+ workgroup_id: int = typer.Option(..., "--workgroup", "-w", help="Workgroup ID"),
135
+ platform: str = typer.Option(..., "--platform", "-p", help="Platform: entra, entraid, ad, or numeric ID"),
136
+ host_name: str = typer.Option(..., "--host", "-h", help="Host name (domain for Entra ID, e.g., domain.onmicrosoft.com)"),
137
+ domain_name: Optional[str] = typer.Option(None, "--domain", "-d", help="Domain name"),
138
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="System name"),
139
+ functional_account: Optional[int] = typer.Option(None, "--functional-account", "-f", help="Functional account ID"),
140
+ description: Optional[str] = typer.Option(None, "--description", help="Description"),
141
+ auto_manage: bool = typer.Option(False, "--auto-manage", help="Enable automatic password management"),
142
+ account_format: int = typer.Option(1, "--account-format", help="Account name format: 0=Domain/Account, 1=UPN, 2=SAM"),
143
+ change_frequency: Optional[str] = typer.Option(None, "--change-frequency", help="Change frequency: first, last, or xdays"),
144
+ change_days: Optional[int] = typer.Option(30, "--change-days", help="Days between changes (if xdays)"),
145
+ change_time: Optional[str] = typer.Option("23:30", "--change-time", help="Time for changes (HH:MM)"),
146
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
147
+ ) -> None:
148
+ """Create a managed system for a directory (Entra ID, Active Directory).
149
+
150
+ Example for Entra ID:
151
+ pws directories add-system -w 3 -p entra -h "domain.onmicrosoft.com" -f 11 --auto-manage
152
+ """
153
+ try:
154
+ # Resolve platform ID
155
+ if platform.isdigit():
156
+ platform_id = int(platform)
157
+ else:
158
+ platform_lower = platform.lower().replace("_", "").replace("-", "")
159
+ if platform_lower not in DIRECTORY_PLATFORMS:
160
+ console.print(f"[red]Unknown platform:[/red] {platform}")
161
+ console.print("Valid platforms: entra, entraid, ad, activedirectory, or numeric ID")
162
+ raise typer.Exit(1)
163
+ platform_id = DIRECTORY_PLATFORMS[platform_lower]
164
+
165
+ with get_client() as client:
166
+ client.authenticate()
167
+ system = client.create_directory_managed_system(
168
+ workgroup_id=workgroup_id,
169
+ platform_id=platform_id,
170
+ host_name=host_name,
171
+ domain_name=domain_name,
172
+ system_name=name,
173
+ functional_account_id=functional_account,
174
+ description=description,
175
+ auto_management_flag=auto_manage,
176
+ account_name_format=account_format,
177
+ change_frequency_type=change_frequency,
178
+ change_frequency_days=change_days,
179
+ change_time=change_time,
180
+ )
181
+
182
+ if output == "json":
183
+ console.print_json(json.dumps(system, default=str))
184
+ else:
185
+ console.print(f"[green]Created managed system:[/green] {system.get('SystemName', 'Unknown')}")
186
+ console.print(f" ID: {system.get('ManagedSystemID', 'N/A')}")
187
+ console.print(f" Directory ID: {system.get('DirectoryID', 'N/A')}")
188
+ console.print(f" Platform: {system.get('PlatformID', 'N/A')}")
189
+ console.print(f" Auto-Management: {'Yes' if system.get('AutoManagementFlag') else 'No'}")
190
+
191
+ except httpx.HTTPStatusError as e:
192
+ print_api_error(e, "manage directories")
193
+ raise typer.Exit(1)
194
+ except httpx.RequestError as e:
195
+ print_api_error(e, "manage directories")
196
+ raise typer.Exit(1)
197
+ except Exception as e:
198
+ print_api_error(e, "manage directories")
199
+ raise typer.Exit(1)
@@ -0,0 +1,298 @@
1
+ """Functional account commands for Password Safe."""
2
+
3
+ import json
4
+ import secrets
5
+ import string
6
+ from typing import Optional
7
+
8
+ import httpx
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from ...core.output import print_api_error
14
+ from ..client.base import get_client
15
+
16
+ app = typer.Typer(no_args_is_help=True, help="Functional accounts for auto-management")
17
+ console = Console()
18
+
19
+
20
+ @app.command("list")
21
+ def list_functional(
22
+ search: Optional[str] = typer.Option(None, "--search", "-s", help="Search filter"),
23
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
24
+ ) -> None:
25
+ """List functional accounts."""
26
+ try:
27
+ with get_client() as client:
28
+ client.authenticate()
29
+ accounts = client.list_functional_accounts(search=search)
30
+
31
+ if output == "json":
32
+ console.print_json(json.dumps(accounts, default=str))
33
+ else:
34
+ table = Table(title="Functional Accounts")
35
+ table.add_column("ID", style="cyan")
36
+ table.add_column("Account Name", style="green")
37
+ table.add_column("Display Name", style="yellow")
38
+ table.add_column("Domain", style="magenta")
39
+ table.add_column("Platform", style="blue")
40
+ table.add_column("Systems", style="red")
41
+
42
+ for fa in accounts:
43
+ table.add_row(
44
+ str(fa.get("FunctionalAccountID", "")),
45
+ fa.get("AccountName", ""),
46
+ fa.get("DisplayName", "-"),
47
+ fa.get("DomainName") or "-",
48
+ str(fa.get("PlatformID", "-")),
49
+ str(fa.get("SystemReferenceCount", 0)),
50
+ )
51
+
52
+ console.print(table)
53
+
54
+ except httpx.HTTPStatusError as e:
55
+ print_api_error(e, "list functional accounts")
56
+ raise typer.Exit(1)
57
+ except httpx.RequestError as e:
58
+ print_api_error(e, "list functional accounts")
59
+ raise typer.Exit(1)
60
+ except Exception as e:
61
+ print_api_error(e, "list functional accounts")
62
+ raise typer.Exit(1)
63
+
64
+
65
+ @app.command("get")
66
+ def get_functional(
67
+ account_id: int = typer.Argument(..., help="Functional account ID"),
68
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
69
+ ) -> None:
70
+ """Get a functional account by ID."""
71
+ try:
72
+ with get_client() as client:
73
+ client.authenticate()
74
+ account = client.get_functional_account(account_id)
75
+
76
+ if output == "json":
77
+ console.print_json(json.dumps(account, default=str))
78
+ else:
79
+ console.print(f"\n[bold cyan]Functional Account: {account.get('DisplayName', 'Unknown')}[/bold cyan]\n")
80
+
81
+ fields = [
82
+ ("ID", "FunctionalAccountID"),
83
+ ("Account Name", "AccountName"),
84
+ ("Display Name", "DisplayName"),
85
+ ("Domain", "DomainName"),
86
+ ("Description", "Description"),
87
+ ("Platform ID", "PlatformID"),
88
+ ("Elevation Command", "ElevationCommand"),
89
+ ("System Reference Count", "SystemReferenceCount"),
90
+ ]
91
+
92
+ for label, key in fields:
93
+ value = account.get(key)
94
+ if value is not None:
95
+ console.print(f" {label}: {value}")
96
+
97
+ except httpx.HTTPStatusError as e:
98
+ print_api_error(e, "get functional account")
99
+ raise typer.Exit(1)
100
+ except httpx.RequestError as e:
101
+ print_api_error(e, "get functional account")
102
+ raise typer.Exit(1)
103
+ except Exception as e:
104
+ print_api_error(e, "get functional account")
105
+ raise typer.Exit(1)
106
+
107
+
108
+ @app.command("create")
109
+ def create_functional(
110
+ account_name: str = typer.Option(..., "--name", "-n", help="Account username (UPN format for Entra ID)"),
111
+ platform_id: int = typer.Option(..., "--platform", "-p", help="Platform ID (1=Windows, 2=Linux, 10=MySQL, 11=MSSQL, 47=AWS, 79=PostgreSQL, 84=Entra ID)"),
112
+ display_name: Optional[str] = typer.Option(None, "--display-name", "-d", help="Display name"),
113
+ description: Optional[str] = typer.Option(None, "--description", help="Description"),
114
+ elevation: Optional[str] = typer.Option(None, "--elevation", "-e", help="Elevation command (sudo, pbrun, pmrun)"),
115
+ password: Optional[str] = typer.Option(None, "--password", help="Account password"),
116
+ key_file: Optional[str] = typer.Option(None, "--key-file", "-k", help="Path to SSH private key file"),
117
+ passphrase: Optional[str] = typer.Option(None, "--passphrase", help="Passphrase for encrypted key"),
118
+ # Entra ID specific options
119
+ application_id: Optional[str] = typer.Option(None, "--app-id", help="Azure Application (Client) ID (Entra ID)"),
120
+ tenant_id: Optional[str] = typer.Option(None, "--tenant-id", help="Azure Tenant ID (Entra ID)"),
121
+ object_id: Optional[str] = typer.Option(None, "--object-id", help="Azure Object ID (Entra ID)"),
122
+ secret: Optional[str] = typer.Option(None, "--secret", help="Client Secret (Entra ID) or Secret Access Key (AWS)"),
123
+ # AWS specific options
124
+ api_key: Optional[str] = typer.Option(None, "--api-key", help="AWS Access Key ID"),
125
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
126
+ ) -> None:
127
+ """Create a new functional account.
128
+
129
+ For Entra ID (platform 84), use:
130
+ --name "user@domain.onmicrosoft.com" --app-id <client-id> --tenant-id <tenant-id>
131
+ --object-id <object-id> --secret <client-secret>
132
+
133
+ For AWS (platform 47), use:
134
+ --name "iam-user-name" --api-key <access-key-id> --secret <secret-access-key>
135
+ """
136
+ try:
137
+ # Read SSH key from file if provided
138
+ private_key = None
139
+ if key_file:
140
+ import os
141
+ key_path = os.path.expanduser(key_file)
142
+ if not os.path.exists(key_path):
143
+ console.print(f"[red]Error:[/red] Key file not found: {key_path}")
144
+ raise typer.Exit(1)
145
+ with open(key_path, "r") as f:
146
+ private_key = f.read()
147
+
148
+ # API requires password even with SSH key - auto-generate if not provided
149
+ if not password:
150
+ password = ''.join(secrets.choice(string.ascii_letters + string.digits + "!@#$%") for _ in range(32))
151
+ console.print("[dim]Note: Auto-generated password (SSH key will be used for auth)[/dim]")
152
+
153
+ with get_client() as client:
154
+ client.authenticate()
155
+ account = client.create_functional_account(
156
+ account_name=account_name,
157
+ platform_id=platform_id,
158
+ display_name=display_name,
159
+ description=description,
160
+ elevation_command=elevation,
161
+ password=password,
162
+ private_key=private_key,
163
+ passphrase=passphrase,
164
+ application_id=application_id,
165
+ tenant_id=tenant_id,
166
+ object_id=object_id,
167
+ secret=secret,
168
+ api_key=api_key,
169
+ )
170
+
171
+ if output == "json":
172
+ console.print_json(json.dumps(account, default=str))
173
+ else:
174
+ console.print(f"[green]Created functional account:[/green] {account.get('DisplayName', account.get('AccountName'))}")
175
+ console.print(f" ID: {account.get('FunctionalAccountID')}")
176
+ console.print(f" Platform: {account.get('PlatformID')}")
177
+ if account.get('ElevationCommand'):
178
+ console.print(f" Elevation: {account.get('ElevationCommand')}")
179
+ if account.get('TenantID'):
180
+ console.print(f" Tenant ID: {account.get('TenantID')}")
181
+
182
+ except httpx.HTTPStatusError as e:
183
+ print_api_error(e, "create functional account")
184
+ raise typer.Exit(1)
185
+ except httpx.RequestError as e:
186
+ print_api_error(e, "create functional account")
187
+ raise typer.Exit(1)
188
+ except Exception as e:
189
+ print_api_error(e, "create functional account")
190
+ raise typer.Exit(1)
191
+
192
+
193
+ @app.command("update")
194
+ def update_functional(
195
+ account_id: int = typer.Argument(..., help="Functional account ID to update"),
196
+ name: Optional[str] = typer.Option(None, "--name", "-n", help="New account name"),
197
+ display_name: Optional[str] = typer.Option(None, "--display-name", "-d", help="New display name"),
198
+ description: Optional[str] = typer.Option(None, "--description", help="New description"),
199
+ domain: Optional[str] = typer.Option(None, "--domain", help="New domain name"),
200
+ elevation: Optional[str] = typer.Option(None, "--elevation", "-e", help="Elevation command (e.g., sudo, pbrun)"),
201
+ password: Optional[str] = typer.Option(None, "--password", "-p", help="New password"),
202
+ key_file: Optional[str] = typer.Option(None, "--key-file", "-k", help="Path to new SSH private key file"),
203
+ passphrase: Optional[str] = typer.Option(None, "--passphrase", help="Passphrase for SSH key"),
204
+ output: str = typer.Option("table", "--output", "-o", help="Output format: table or json"),
205
+ ) -> None:
206
+ """Update a functional account.
207
+
208
+ Examples:
209
+ bt pws functional update 7 --elevation "sudo"
210
+ bt pws functional update 7 --display-name "AWS FA" --description "Updated"
211
+ bt pws functional update 7 --key-file /path/to/new/key
212
+ """
213
+ try:
214
+ kwargs = {}
215
+ if name is not None:
216
+ kwargs["account_name"] = name
217
+ if display_name is not None:
218
+ kwargs["display_name"] = display_name
219
+ if description is not None:
220
+ kwargs["description"] = description
221
+ if domain is not None:
222
+ kwargs["domain_name"] = domain
223
+ if elevation is not None:
224
+ kwargs["elevation_command"] = elevation
225
+ if password is not None:
226
+ kwargs["password"] = password
227
+ if passphrase is not None:
228
+ kwargs["passphrase"] = passphrase
229
+
230
+ # Read SSH key from file if provided
231
+ if key_file:
232
+ import os
233
+ key_path = os.path.expanduser(key_file)
234
+ if not os.path.exists(key_path):
235
+ console.print(f"[red]Error:[/red] Key file not found: {key_path}")
236
+ raise typer.Exit(1)
237
+ with open(key_path, "r") as f:
238
+ kwargs["private_key"] = f.read()
239
+
240
+ if not kwargs:
241
+ console.print("[yellow]No updates specified.[/yellow]")
242
+ raise typer.Exit(0)
243
+
244
+ with get_client() as client:
245
+ client.authenticate()
246
+ account = client.update_functional_account(account_id, **kwargs)
247
+
248
+ if output == "json":
249
+ console.print_json(json.dumps(account, default=str))
250
+ else:
251
+ console.print(f"[green]Updated functional account ID: {account_id}[/green]")
252
+
253
+ except httpx.HTTPStatusError as e:
254
+ print_api_error(e, "update functional account")
255
+ raise typer.Exit(1)
256
+ except httpx.RequestError as e:
257
+ print_api_error(e, "update functional account")
258
+ raise typer.Exit(1)
259
+ except Exception as e:
260
+ print_api_error(e, "update functional account")
261
+ raise typer.Exit(1)
262
+
263
+
264
+ @app.command("delete")
265
+ def delete_functional(
266
+ account_id: int = typer.Argument(..., help="Functional account ID to delete"),
267
+ force: bool = typer.Option(False, "--force", "-f", help="Skip confirmation"),
268
+ ) -> None:
269
+ """Delete a functional account."""
270
+ try:
271
+ with get_client() as client:
272
+ client.authenticate()
273
+
274
+ if not force:
275
+ account = client.get_functional_account(account_id)
276
+ name = account.get("DisplayName") or account.get("AccountName") or "Unknown"
277
+ systems = account.get("SystemReferenceCount", 0)
278
+ if systems > 0:
279
+ console.print(f"[yellow]Warning:[/yellow] This FA is used by {systems} system(s)")
280
+ confirm = typer.confirm(
281
+ f"Are you sure you want to delete functional account '{name}' (ID: {account_id})?"
282
+ )
283
+ if not confirm:
284
+ console.print("[yellow]Cancelled.[/yellow]")
285
+ raise typer.Exit(0)
286
+
287
+ client.delete_functional_account(account_id)
288
+ console.print(f"[green]Deleted functional account ID: {account_id}[/green]")
289
+
290
+ except httpx.HTTPStatusError as e:
291
+ print_api_error(e, "delete functional account")
292
+ raise typer.Exit(1)
293
+ except httpx.RequestError as e:
294
+ print_api_error(e, "delete functional account")
295
+ raise typer.Exit(1)
296
+ except Exception as e:
297
+ print_api_error(e, "delete functional account")
298
+ raise typer.Exit(1)