bt-cli 0.4.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bt_cli/__init__.py +3 -0
- bt_cli/cli.py +830 -0
- bt_cli/commands/__init__.py +1 -0
- bt_cli/commands/configure.py +415 -0
- bt_cli/commands/learn.py +229 -0
- bt_cli/commands/quick.py +784 -0
- bt_cli/core/__init__.py +1 -0
- bt_cli/core/auth.py +213 -0
- bt_cli/core/client.py +313 -0
- bt_cli/core/config.py +393 -0
- bt_cli/core/config_file.py +420 -0
- bt_cli/core/csv_utils.py +91 -0
- bt_cli/core/errors.py +247 -0
- bt_cli/core/output.py +205 -0
- bt_cli/core/prompts.py +87 -0
- bt_cli/core/rest_debug.py +221 -0
- bt_cli/data/CLAUDE.md +94 -0
- bt_cli/data/__init__.py +0 -0
- bt_cli/data/skills/bt/SKILL.md +108 -0
- bt_cli/data/skills/entitle/SKILL.md +170 -0
- bt_cli/data/skills/epmw/SKILL.md +144 -0
- bt_cli/data/skills/pra/SKILL.md +150 -0
- bt_cli/data/skills/pws/SKILL.md +198 -0
- bt_cli/entitle/__init__.py +1 -0
- bt_cli/entitle/client/__init__.py +5 -0
- bt_cli/entitle/client/base.py +443 -0
- bt_cli/entitle/commands/__init__.py +24 -0
- bt_cli/entitle/commands/accounts.py +53 -0
- bt_cli/entitle/commands/applications.py +39 -0
- bt_cli/entitle/commands/auth.py +68 -0
- bt_cli/entitle/commands/bundles.py +218 -0
- bt_cli/entitle/commands/integrations.py +60 -0
- bt_cli/entitle/commands/permissions.py +70 -0
- bt_cli/entitle/commands/policies.py +97 -0
- bt_cli/entitle/commands/resources.py +131 -0
- bt_cli/entitle/commands/roles.py +74 -0
- bt_cli/entitle/commands/users.py +123 -0
- bt_cli/entitle/commands/workflows.py +187 -0
- bt_cli/entitle/models/__init__.py +31 -0
- bt_cli/entitle/models/bundle.py +28 -0
- bt_cli/entitle/models/common.py +37 -0
- bt_cli/entitle/models/integration.py +30 -0
- bt_cli/entitle/models/permission.py +27 -0
- bt_cli/entitle/models/policy.py +25 -0
- bt_cli/entitle/models/resource.py +29 -0
- bt_cli/entitle/models/role.py +28 -0
- bt_cli/entitle/models/user.py +24 -0
- bt_cli/entitle/models/workflow.py +55 -0
- bt_cli/epmw/__init__.py +1 -0
- bt_cli/epmw/client/__init__.py +5 -0
- bt_cli/epmw/client/base.py +848 -0
- bt_cli/epmw/commands/__init__.py +33 -0
- bt_cli/epmw/commands/audits.py +250 -0
- bt_cli/epmw/commands/auth.py +55 -0
- bt_cli/epmw/commands/computers.py +140 -0
- bt_cli/epmw/commands/events.py +233 -0
- bt_cli/epmw/commands/groups.py +215 -0
- bt_cli/epmw/commands/policies.py +673 -0
- bt_cli/epmw/commands/quick.py +348 -0
- bt_cli/epmw/commands/requests.py +224 -0
- bt_cli/epmw/commands/roles.py +78 -0
- bt_cli/epmw/commands/tasks.py +38 -0
- bt_cli/epmw/commands/users.py +219 -0
- bt_cli/epmw/models/__init__.py +1 -0
- bt_cli/pra/__init__.py +1 -0
- bt_cli/pra/client/__init__.py +5 -0
- bt_cli/pra/client/base.py +618 -0
- bt_cli/pra/commands/__init__.py +30 -0
- bt_cli/pra/commands/auth.py +55 -0
- bt_cli/pra/commands/import_export.py +442 -0
- bt_cli/pra/commands/jump_clients.py +139 -0
- bt_cli/pra/commands/jump_groups.py +146 -0
- bt_cli/pra/commands/jump_items.py +638 -0
- bt_cli/pra/commands/jumpoints.py +95 -0
- bt_cli/pra/commands/policies.py +197 -0
- bt_cli/pra/commands/quick.py +470 -0
- bt_cli/pra/commands/teams.py +81 -0
- bt_cli/pra/commands/users.py +87 -0
- bt_cli/pra/commands/vault.py +564 -0
- bt_cli/pra/models/__init__.py +27 -0
- bt_cli/pra/models/common.py +12 -0
- bt_cli/pra/models/jump_client.py +25 -0
- bt_cli/pra/models/jump_group.py +15 -0
- bt_cli/pra/models/jump_item.py +72 -0
- bt_cli/pra/models/jumpoint.py +19 -0
- bt_cli/pra/models/team.py +14 -0
- bt_cli/pra/models/user.py +17 -0
- bt_cli/pra/models/vault.py +45 -0
- bt_cli/pws/__init__.py +1 -0
- bt_cli/pws/client/__init__.py +5 -0
- bt_cli/pws/client/base.py +356 -0
- bt_cli/pws/client/beyondinsight.py +869 -0
- bt_cli/pws/client/passwordsafe.py +1786 -0
- bt_cli/pws/commands/__init__.py +33 -0
- bt_cli/pws/commands/accounts.py +372 -0
- bt_cli/pws/commands/assets.py +311 -0
- bt_cli/pws/commands/auth.py +166 -0
- bt_cli/pws/commands/clouds.py +221 -0
- bt_cli/pws/commands/config.py +344 -0
- bt_cli/pws/commands/credentials.py +347 -0
- bt_cli/pws/commands/databases.py +306 -0
- bt_cli/pws/commands/directories.py +199 -0
- bt_cli/pws/commands/functional.py +298 -0
- bt_cli/pws/commands/import_export.py +452 -0
- bt_cli/pws/commands/platforms.py +118 -0
- bt_cli/pws/commands/quick.py +1646 -0
- bt_cli/pws/commands/search.py +256 -0
- bt_cli/pws/commands/secrets.py +1343 -0
- bt_cli/pws/commands/systems.py +389 -0
- bt_cli/pws/commands/users.py +415 -0
- bt_cli/pws/commands/workgroups.py +166 -0
- bt_cli/pws/config.py +18 -0
- bt_cli/pws/models/__init__.py +19 -0
- bt_cli/pws/models/account.py +186 -0
- bt_cli/pws/models/asset.py +102 -0
- bt_cli/pws/models/common.py +132 -0
- bt_cli/pws/models/system.py +121 -0
- bt_cli-0.4.13.dist-info/METADATA +417 -0
- bt_cli-0.4.13.dist-info/RECORD +121 -0
- bt_cli-0.4.13.dist-info/WHEEL +4 -0
- bt_cli-0.4.13.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,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")
|