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,618 @@
1
+ """PRA API client with OAuth 2.0 authentication."""
2
+
3
+ import logging
4
+ import time
5
+ from typing import Any, Dict, List, Optional
6
+ from urllib.parse import urljoin
7
+
8
+ import httpx
9
+
10
+ from bt_cli.core.config import PRAConfig, load_pra_config
11
+ from bt_cli.core.rest_debug import get_event_hooks
12
+ from bt_cli.core.client import _warn_ssl_disabled
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class PRAClient:
18
+ """Client for BeyondTrust Privileged Remote Access Configuration API.
19
+
20
+ Uses OAuth 2.0 client credentials flow for authentication.
21
+ API base path: /api/config/v1
22
+ """
23
+
24
+ def __init__(self, config: PRAConfig):
25
+ """Initialize PRA client.
26
+
27
+ Args:
28
+ config: PRA configuration with API URL and OAuth credentials
29
+ """
30
+ self.config = config
31
+ self.base_url = config.api_url.rstrip("/")
32
+ self.api_base = f"{self.base_url}/api/config/v1"
33
+ self._token: Optional[str] = None
34
+ self._token_expires: float = 0
35
+
36
+ if not config.verify_ssl:
37
+ _warn_ssl_disabled()
38
+
39
+ self._client = httpx.Client(
40
+ verify=config.verify_ssl,
41
+ timeout=config.timeout,
42
+ event_hooks=get_event_hooks(),
43
+ )
44
+
45
+ def _get_token(self) -> str:
46
+ """Get OAuth access token, refreshing if needed."""
47
+ if self._token and time.time() < self._token_expires - 60:
48
+ return self._token
49
+
50
+ token_url = f"{self.base_url}/oauth2/token"
51
+ response = self._client.post(
52
+ token_url,
53
+ data={
54
+ "grant_type": "client_credentials",
55
+ "client_id": self.config.client_id,
56
+ "client_secret": self.config.client_secret,
57
+ },
58
+ )
59
+ response.raise_for_status()
60
+ data = response.json()
61
+ self._token = data["access_token"]
62
+ expires_in = data.get("expires_in", 3600)
63
+ self._token_expires = time.time() + expires_in
64
+ return self._token
65
+
66
+ def _headers(self) -> Dict[str, str]:
67
+ """Get request headers with auth token."""
68
+ return {
69
+ "Authorization": f"Bearer {self._get_token()}",
70
+ "Accept": "application/json",
71
+ "Content-Type": "application/json",
72
+ }
73
+
74
+ def get(
75
+ self,
76
+ endpoint: str,
77
+ params: Optional[Dict[str, Any]] = None,
78
+ ) -> Any:
79
+ """Make GET request to API.
80
+
81
+ Args:
82
+ endpoint: API endpoint path (e.g., "/jumpoint")
83
+ params: Optional query parameters
84
+
85
+ Returns:
86
+ JSON response data
87
+ """
88
+ url = f"{self.api_base}{endpoint}"
89
+ response = self._client.get(url, headers=self._headers(), params=params)
90
+ response.raise_for_status()
91
+ return response.json()
92
+
93
+ def get_paginated(
94
+ self,
95
+ endpoint: str,
96
+ params: Optional[Dict[str, Any]] = None,
97
+ per_page: int = 100,
98
+ ) -> List[Any]:
99
+ """Get all items from a paginated endpoint.
100
+
101
+ Args:
102
+ endpoint: API endpoint path
103
+ params: Optional query parameters
104
+ per_page: Items per page (max 100)
105
+
106
+ Returns:
107
+ List of all items across all pages
108
+ """
109
+ all_items = []
110
+ current_page = 1
111
+ params = params or {}
112
+
113
+ while True:
114
+ params["per_page"] = per_page
115
+ params["current_page"] = current_page
116
+
117
+ url = f"{self.api_base}{endpoint}"
118
+ response = self._client.get(url, headers=self._headers(), params=params)
119
+ response.raise_for_status()
120
+
121
+ items = response.json()
122
+ if not items:
123
+ break
124
+
125
+ all_items.extend(items)
126
+
127
+ # Check pagination headers
128
+ last_page = int(response.headers.get("X-BT-Pagination-Last-Page", 1))
129
+ if current_page >= last_page:
130
+ break
131
+
132
+ current_page += 1
133
+
134
+ return all_items
135
+
136
+ def post(
137
+ self,
138
+ endpoint: str,
139
+ json: Optional[Dict[str, Any]] = None,
140
+ ) -> Any:
141
+ """Make POST request to API.
142
+
143
+ Args:
144
+ endpoint: API endpoint path
145
+ json: JSON body data
146
+
147
+ Returns:
148
+ JSON response data or None for 204 responses
149
+ """
150
+ url = f"{self.api_base}{endpoint}"
151
+ response = self._client.post(url, headers=self._headers(), json=json)
152
+ response.raise_for_status()
153
+ if response.status_code == 204:
154
+ return None
155
+ return response.json()
156
+
157
+ def patch(
158
+ self,
159
+ endpoint: str,
160
+ json: Optional[Dict[str, Any]] = None,
161
+ ) -> Any:
162
+ """Make PATCH request to API.
163
+
164
+ Args:
165
+ endpoint: API endpoint path
166
+ json: JSON body data
167
+
168
+ Returns:
169
+ JSON response data
170
+ """
171
+ url = f"{self.api_base}{endpoint}"
172
+ response = self._client.patch(url, headers=self._headers(), json=json)
173
+ response.raise_for_status()
174
+ return response.json()
175
+
176
+ def delete(self, endpoint: str) -> None:
177
+ """Make DELETE request to API.
178
+
179
+ Args:
180
+ endpoint: API endpoint path
181
+ """
182
+ url = f"{self.api_base}{endpoint}"
183
+ response = self._client.delete(url, headers=self._headers())
184
+ response.raise_for_status()
185
+
186
+ def close(self) -> None:
187
+ """Close the HTTP client."""
188
+ self._client.close()
189
+
190
+ def __enter__(self) -> "PRAClient":
191
+ return self
192
+
193
+ def __exit__(self, *args) -> None:
194
+ self.close()
195
+
196
+ # Convenience methods for common resources
197
+
198
+ def list_jumpoints(self) -> List[Dict[str, Any]]:
199
+ """List all Jumpoints."""
200
+ return self.get_paginated("/jumpoint")
201
+
202
+ def get_jumpoint(self, jumpoint_id: int) -> Dict[str, Any]:
203
+ """Get a specific Jumpoint."""
204
+ return self.get(f"/jumpoint/{jumpoint_id}")
205
+
206
+ def list_jump_groups(self) -> List[Dict[str, Any]]:
207
+ """List all Jump Groups."""
208
+ return self.get_paginated("/jump-group")
209
+
210
+ def get_jump_group(self, group_id: int) -> Dict[str, Any]:
211
+ """Get a specific Jump Group."""
212
+ return self.get(f"/jump-group/{group_id}")
213
+
214
+ def create_jump_group(
215
+ self,
216
+ name: str,
217
+ code_name: str,
218
+ comments: Optional[str] = None,
219
+ ) -> Dict[str, Any]:
220
+ """Create a new Jump Group.
221
+
222
+ Args:
223
+ name: Display name for the jump group
224
+ code_name: Short code name (lowercase, underscores)
225
+ comments: Optional description
226
+
227
+ Returns:
228
+ Created jump group data
229
+ """
230
+ data = {
231
+ "name": name,
232
+ "code_name": code_name,
233
+ }
234
+ if comments:
235
+ data["comments"] = comments
236
+ return self.post("/jump-group", json=data)
237
+
238
+ def delete_jump_group(self, group_id: int) -> None:
239
+ """Delete a Jump Group."""
240
+ self.delete(f"/jump-group/{group_id}")
241
+
242
+ def list_jump_clients(
243
+ self,
244
+ jump_group_id: Optional[int] = None,
245
+ name: Optional[str] = None,
246
+ hostname: Optional[str] = None,
247
+ ) -> List[Dict[str, Any]]:
248
+ """List Jump Clients with optional filters."""
249
+ params = {}
250
+ if jump_group_id:
251
+ params["jump_group_id"] = jump_group_id
252
+ if name:
253
+ params["name"] = name
254
+ if hostname:
255
+ params["hostname"] = hostname
256
+ return self.get_paginated("/jump-client", params)
257
+
258
+ def get_jump_client(self, client_id: int) -> Dict[str, Any]:
259
+ """Get a specific Jump Client."""
260
+ return self.get(f"/jump-client/{client_id}")
261
+
262
+ def delete_jump_client(self, client_id: int) -> None:
263
+ """Delete a Jump Client."""
264
+ self.delete(f"/jump-client/{client_id}")
265
+
266
+ def list_shell_jumps(
267
+ self,
268
+ jump_group_id: Optional[int] = None,
269
+ jumpoint_id: Optional[int] = None,
270
+ ) -> List[Dict[str, Any]]:
271
+ """List Shell Jump items."""
272
+ params = {}
273
+ if jump_group_id:
274
+ params["jump_group_id"] = jump_group_id
275
+ if jumpoint_id:
276
+ params["jumpoint_id"] = jumpoint_id
277
+ return self.get_paginated("/jump-item/shell-jump", params)
278
+
279
+ def get_shell_jump(self, item_id: int) -> Dict[str, Any]:
280
+ """Get a Shell Jump item."""
281
+ return self.get(f"/jump-item/shell-jump/{item_id}")
282
+
283
+ def create_shell_jump(
284
+ self,
285
+ name: str,
286
+ hostname: str,
287
+ jumpoint_id: int,
288
+ jump_group_id: int,
289
+ protocol: str = "ssh",
290
+ port: int = 22,
291
+ username: Optional[str] = None,
292
+ tag: Optional[str] = None,
293
+ comments: Optional[str] = None,
294
+ ) -> Dict[str, Any]:
295
+ """Create a Shell Jump item."""
296
+ data = {
297
+ "name": name,
298
+ "hostname": hostname,
299
+ "jumpoint_id": jumpoint_id,
300
+ "jump_group_id": jump_group_id,
301
+ "protocol": protocol,
302
+ "port": port,
303
+ }
304
+ if username:
305
+ data["username"] = username
306
+ if tag:
307
+ data["tag"] = tag
308
+ if comments:
309
+ data["comments"] = comments
310
+ return self.post("/jump-item/shell-jump", json=data)
311
+
312
+ def update_shell_jump(
313
+ self,
314
+ item_id: int,
315
+ name: Optional[str] = None,
316
+ hostname: Optional[str] = None,
317
+ jump_group_id: Optional[int] = None,
318
+ protocol: Optional[str] = None,
319
+ port: Optional[int] = None,
320
+ username: Optional[str] = None,
321
+ tag: Optional[str] = None,
322
+ comments: Optional[str] = None,
323
+ ) -> Dict[str, Any]:
324
+ """Update a Shell Jump item.
325
+
326
+ Args:
327
+ item_id: Shell Jump ID to update
328
+ name: New name (optional)
329
+ hostname: New hostname (optional)
330
+ jump_group_id: New jump group ID (optional)
331
+ protocol: New protocol - ssh or telnet (optional)
332
+ port: New port (optional)
333
+ username: New username (optional)
334
+ tag: New tag (optional)
335
+ comments: New comments (optional)
336
+
337
+ Returns:
338
+ Updated shell jump data
339
+ """
340
+ data = {}
341
+ if name is not None:
342
+ data["name"] = name
343
+ if hostname is not None:
344
+ data["hostname"] = hostname
345
+ if jump_group_id is not None:
346
+ data["jump_group_id"] = jump_group_id
347
+ if protocol is not None:
348
+ data["protocol"] = protocol
349
+ if port is not None:
350
+ data["port"] = port
351
+ if username is not None:
352
+ data["username"] = username
353
+ if tag is not None:
354
+ data["tag"] = tag
355
+ if comments is not None:
356
+ data["comments"] = comments
357
+ return self.patch(f"/jump-item/shell-jump/{item_id}", json=data)
358
+
359
+ def delete_shell_jump(self, item_id: int) -> None:
360
+ """Delete a Shell Jump item."""
361
+ self.delete(f"/jump-item/shell-jump/{item_id}")
362
+
363
+ def list_rdp_jumps(
364
+ self,
365
+ jump_group_id: Optional[int] = None,
366
+ jumpoint_id: Optional[int] = None,
367
+ ) -> List[Dict[str, Any]]:
368
+ """List Remote RDP Jump items."""
369
+ params = {}
370
+ if jump_group_id:
371
+ params["jump_group_id"] = jump_group_id
372
+ if jumpoint_id:
373
+ params["jumpoint_id"] = jumpoint_id
374
+ return self.get_paginated("/jump-item/remote-rdp", params)
375
+
376
+ def get_rdp_jump(self, item_id: int) -> Dict[str, Any]:
377
+ """Get a Remote RDP Jump item."""
378
+ return self.get(f"/jump-item/remote-rdp/{item_id}")
379
+
380
+ def create_rdp_jump(
381
+ self,
382
+ name: str,
383
+ hostname: str,
384
+ jumpoint_id: int,
385
+ jump_group_id: int,
386
+ rdp_username: Optional[str] = None,
387
+ domain: Optional[str] = None,
388
+ tag: Optional[str] = None,
389
+ comments: Optional[str] = None,
390
+ ) -> Dict[str, Any]:
391
+ """Create a Remote RDP Jump item."""
392
+ data = {
393
+ "name": name,
394
+ "hostname": hostname,
395
+ "jumpoint_id": jumpoint_id,
396
+ "jump_group_id": jump_group_id,
397
+ }
398
+ if rdp_username:
399
+ data["rdp_username"] = rdp_username
400
+ if domain:
401
+ data["domain"] = domain
402
+ if tag:
403
+ data["tag"] = tag
404
+ if comments:
405
+ data["comments"] = comments
406
+ return self.post("/jump-item/remote-rdp", json=data)
407
+
408
+ def delete_rdp_jump(self, item_id: int) -> None:
409
+ """Delete a Remote RDP Jump item."""
410
+ self.delete(f"/jump-item/remote-rdp/{item_id}")
411
+
412
+ def list_vnc_jumps(
413
+ self,
414
+ jump_group_id: Optional[int] = None,
415
+ ) -> List[Dict[str, Any]]:
416
+ """List Remote VNC Jump items."""
417
+ params = {}
418
+ if jump_group_id:
419
+ params["jump_group_id"] = jump_group_id
420
+ return self.get_paginated("/jump-item/remote-vnc", params)
421
+
422
+ def list_web_jumps(
423
+ self,
424
+ jump_group_id: Optional[int] = None,
425
+ ) -> List[Dict[str, Any]]:
426
+ """List Web Jump items."""
427
+ params = {}
428
+ if jump_group_id:
429
+ params["jump_group_id"] = jump_group_id
430
+ return self.get_paginated("/jump-item/web-jump", params)
431
+
432
+ def list_protocol_tunnels(
433
+ self,
434
+ jump_group_id: Optional[int] = None,
435
+ tunnel_type: Optional[str] = None,
436
+ ) -> List[Dict[str, Any]]:
437
+ """List Protocol Tunnel Jump items (TCP, MSSQL, K8s)."""
438
+ params = {}
439
+ if jump_group_id:
440
+ params["jump_group_id"] = jump_group_id
441
+ return self.get_paginated("/jump-item/protocol-tunnel-jump", params)
442
+
443
+ def get_protocol_tunnel(self, item_id: int) -> Dict[str, Any]:
444
+ """Get a Protocol Tunnel Jump item."""
445
+ return self.get(f"/jump-item/protocol-tunnel-jump/{item_id}")
446
+
447
+ def create_protocol_tunnel(
448
+ self,
449
+ name: str,
450
+ hostname: str,
451
+ jumpoint_id: int,
452
+ jump_group_id: int,
453
+ tunnel_type: str = "tcp",
454
+ tunnel_definitions: Optional[str] = None,
455
+ username: Optional[str] = None,
456
+ database: Optional[str] = None,
457
+ url: Optional[str] = None,
458
+ ca_certificates: Optional[str] = None,
459
+ tag: Optional[str] = None,
460
+ comments: Optional[str] = None,
461
+ ) -> Dict[str, Any]:
462
+ """Create a Protocol Tunnel Jump item.
463
+
464
+ Args:
465
+ name: Jump item name
466
+ hostname: Target hostname (auto-set for k8s from url)
467
+ jumpoint_id: Jumpoint ID (must be Linux for k8s)
468
+ jump_group_id: Jump Group ID
469
+ tunnel_type: 'tcp', 'mssql', 'psql', 'mysql', or 'k8s'
470
+ tunnel_definitions: Port pairs for TCP (e.g., "22;24;26;28")
471
+ username: Database username (for mssql, psql, mysql)
472
+ database: Database name (for mssql, psql, mysql)
473
+ url: K8s API URL (required for k8s)
474
+ ca_certificates: K8s CA cert (required for k8s)
475
+ tag: Optional tag
476
+ comments: Optional comments
477
+ """
478
+ data = {
479
+ "name": name,
480
+ "hostname": hostname,
481
+ "jumpoint_id": jumpoint_id,
482
+ "jump_group_id": jump_group_id,
483
+ "tunnel_type": tunnel_type,
484
+ }
485
+ if tunnel_definitions:
486
+ data["tunnel_definitions"] = tunnel_definitions
487
+ if username:
488
+ data["username"] = username
489
+ if database:
490
+ data["database"] = database
491
+ if url:
492
+ data["url"] = url
493
+ if ca_certificates:
494
+ data["ca_certificates"] = ca_certificates
495
+ if tag:
496
+ data["tag"] = tag
497
+ if comments:
498
+ data["comments"] = comments
499
+ return self.post("/jump-item/protocol-tunnel-jump", json=data)
500
+
501
+ def delete_protocol_tunnel(self, item_id: int) -> None:
502
+ """Delete a Protocol Tunnel Jump item."""
503
+ self.delete(f"/jump-item/protocol-tunnel-jump/{item_id}")
504
+
505
+ # Vault operations
506
+
507
+ def list_vault_accounts(
508
+ self,
509
+ account_type: Optional[str] = None,
510
+ name: Optional[str] = None,
511
+ ) -> List[Dict[str, Any]]:
512
+ """List Vault accounts.
513
+
514
+ Args:
515
+ account_type: Filter by type (username_password, ssh, windows_local, etc.)
516
+ name: Filter by name
517
+ """
518
+ params = {}
519
+ if account_type:
520
+ params["type"] = account_type
521
+ if name:
522
+ params["name"] = name
523
+ return self.get_paginated("/vault/account", params)
524
+
525
+ def get_vault_account(self, account_id: int) -> Dict[str, Any]:
526
+ """Get a Vault account."""
527
+ return self.get(f"/vault/account/{account_id}")
528
+
529
+ def checkout_vault_account(self, account_id: int) -> Dict[str, Any]:
530
+ """Check out a Vault account's credentials."""
531
+ return self.post(f"/vault/account/{account_id}/check-out")
532
+
533
+ def checkin_vault_account(self, account_id: int) -> None:
534
+ """Check in a Vault account."""
535
+ self.post(f"/vault/account/{account_id}/check-in")
536
+
537
+ def force_checkin_vault_account(self, account_id: int) -> None:
538
+ """Force check in a Vault account (admin)."""
539
+ self.post(f"/vault/account/{account_id}/force-check-in")
540
+
541
+ def rotate_vault_account(self, account_id: int) -> None:
542
+ """Schedule credential rotation for a Vault account."""
543
+ self.post(f"/vault/account/{account_id}/rotate")
544
+
545
+ def create_vault_account(self, data: Dict[str, Any]) -> Dict[str, Any]:
546
+ """Create a Vault account.
547
+
548
+ Args:
549
+ data: Account data including:
550
+ - name: Account name (required)
551
+ - type: Account type - username_password, ssh, ssh_ca (required)
552
+ - username: Username (for username_password type)
553
+ - password: Password (for username_password type)
554
+ - private_key: SSH private key (for ssh type)
555
+ - description: Account description
556
+ - personal: Whether this is a personal account (default: false)
557
+ """
558
+ return self.post("/vault/account", data)
559
+
560
+ def delete_vault_account(self, account_id: int) -> None:
561
+ """Delete a Vault account."""
562
+ self.delete(f"/vault/account/{account_id}")
563
+
564
+ def list_vault_account_groups(self) -> List[Dict[str, Any]]:
565
+ """List Vault account groups."""
566
+ return self.get_paginated("/vault/account-group")
567
+
568
+ def get_vault_account_group(self, group_id: int) -> Dict[str, Any]:
569
+ """Get a Vault account group."""
570
+ return self.get(f"/vault/account-group/{group_id}")
571
+
572
+ # User operations
573
+
574
+ def list_users(self) -> List[Dict[str, Any]]:
575
+ """List all users."""
576
+ return self.get_paginated("/user")
577
+
578
+ def get_user(self, user_id: int) -> Dict[str, Any]:
579
+ """Get a user."""
580
+ return self.get(f"/user/{user_id}")
581
+
582
+ # Team operations
583
+
584
+ def list_teams(self) -> List[Dict[str, Any]]:
585
+ """List all teams."""
586
+ return self.get_paginated("/team")
587
+
588
+ def get_team(self, team_id: int) -> Dict[str, Any]:
589
+ """Get a team."""
590
+ return self.get(f"/team/{team_id}")
591
+
592
+ # Policy operations
593
+
594
+ def list_jump_policies(self) -> List[Dict[str, Any]]:
595
+ """List Jump Policies."""
596
+ return self.get_paginated("/jump-policy")
597
+
598
+ def get_jump_policy(self, policy_id: int) -> Dict[str, Any]:
599
+ """Get a Jump Policy."""
600
+ return self.get(f"/jump-policy/{policy_id}")
601
+
602
+ def list_session_policies(self) -> List[Dict[str, Any]]:
603
+ """List Session Policies."""
604
+ return self.get_paginated("/session-policy")
605
+
606
+ def list_group_policies(self) -> List[Dict[str, Any]]:
607
+ """List Group Policies."""
608
+ return self.get_paginated("/group-policy")
609
+
610
+ def get_group_policy(self, policy_id: int) -> Dict[str, Any]:
611
+ """Get a Group Policy."""
612
+ return self.get(f"/group-policy/{policy_id}")
613
+
614
+
615
+ def get_client() -> PRAClient:
616
+ """Get a PRA client with configuration from environment."""
617
+ config = load_pra_config()
618
+ return PRAClient(config)
@@ -0,0 +1,30 @@
1
+ """PRA CLI commands."""
2
+
3
+ import typer
4
+
5
+ from .auth import app as auth_app
6
+ from .jumpoints import app as jumpoints_app
7
+ from .jump_groups import app as jump_groups_app
8
+ from .jump_clients import app as jump_clients_app
9
+ from .jump_items import app as jump_items_app
10
+ from .vault import app as vault_app
11
+ from .users import app as users_app
12
+ from .teams import app as teams_app
13
+ from .policies import app as policies_app
14
+ from .quick import app as quick_app
15
+ from .import_export import import_app, export_app
16
+
17
+ app = typer.Typer(no_args_is_help=True, help="Privileged Remote Access commands")
18
+
19
+ app.add_typer(auth_app, name="auth", help="Authentication")
20
+ app.add_typer(jumpoints_app, name="jumpoints", help="Jumpoints (connection proxies)")
21
+ app.add_typer(jump_groups_app, name="jump-groups", help="Jump Groups")
22
+ app.add_typer(jump_clients_app, name="jump-clients", help="Jump Clients (agents)")
23
+ app.add_typer(jump_items_app, name="jump-items", help="Jump Items (shell, RDP, VNC, tunnels)")
24
+ app.add_typer(vault_app, name="vault", help="Vault accounts and credentials")
25
+ app.add_typer(users_app, name="users", help="Users")
26
+ app.add_typer(teams_app, name="teams", help="Teams")
27
+ app.add_typer(policies_app, name="policies", help="Policies (jump, session, group)")
28
+ app.add_typer(quick_app, name="quick", help="Quick commands - common multi-step operations")
29
+ app.add_typer(import_app, name="import", help="Import resources from CSV")
30
+ app.add_typer(export_app, name="export", help="Export sample CSV templates")
@@ -0,0 +1,55 @@
1
+ """PRA authentication commands."""
2
+
3
+ import os
4
+
5
+ import httpx
6
+ import typer
7
+
8
+ from bt_cli.core.output import print_success, print_error, print_warning, print_api_error
9
+
10
+ app = typer.Typer(no_args_is_help=True)
11
+
12
+
13
+ @app.command()
14
+ def test():
15
+ """Test PRA API connection."""
16
+ from bt_cli.pra.client import get_client
17
+
18
+ try:
19
+ client = get_client()
20
+ # Try to list jumpoints as a connection test
21
+ jumpoints = client.list_jumpoints()
22
+ print_success(f"Connected to PRA API")
23
+ print_success(f"Found {len(jumpoints)} jumpoints")
24
+ except ValueError as e:
25
+ print_error(f"Configuration error: {e}")
26
+ raise typer.Exit(1)
27
+ except httpx.HTTPStatusError as e:
28
+ print_api_error(e, "test connection")
29
+ raise typer.Exit(1)
30
+ except httpx.RequestError as e:
31
+ print_api_error(e, "test connection")
32
+ raise typer.Exit(1)
33
+ except Exception as e:
34
+ print_api_error(e, "test connection")
35
+ raise typer.Exit(1)
36
+
37
+
38
+ @app.command()
39
+ def status():
40
+ """Show PRA configuration status."""
41
+ api_url = os.getenv("BT_PRA_API_URL", "")
42
+ client_id = os.getenv("BT_PRA_CLIENT_ID", "")
43
+ client_secret = os.getenv("BT_PRA_CLIENT_SECRET", "")
44
+
45
+ typer.echo("PRA Configuration:")
46
+ typer.echo(f" API URL: {api_url or '(not set)'}")
47
+ typer.echo(f" Client ID: {client_id[:8] + '...' if client_id else '(not set)'}")
48
+ typer.echo(f" Client Secret: {'*' * 8 if client_secret else '(not set)'}")
49
+
50
+ if not api_url:
51
+ print_warning("BT_PRA_API_URL is not set")
52
+ if not client_id:
53
+ print_warning("BT_PRA_CLIENT_ID is not set")
54
+ if not client_secret:
55
+ print_warning("BT_PRA_CLIENT_SECRET is not set")