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,72 @@
1
+ """Jump Item models for various connection types."""
2
+
3
+ from typing import Optional, Literal
4
+
5
+ from .common import PRABaseModel
6
+
7
+
8
+ class BaseJumpItem(PRABaseModel):
9
+ """Base class for all Jump Items."""
10
+
11
+ id: int
12
+ name: str
13
+ jumpoint_id: int
14
+ jump_group_id: int
15
+ jump_group_type: Optional[str] = "shared"
16
+ tag: Optional[str] = None
17
+ comments: Optional[str] = None
18
+ jump_policy_id: Optional[int] = None
19
+ session_policy_id: Optional[int] = None
20
+
21
+
22
+ class ShellJump(BaseJumpItem):
23
+ """Shell Jump item for SSH/Telnet connections."""
24
+
25
+ hostname: str
26
+ protocol: Literal["ssh", "telnet"] = "ssh"
27
+ port: int = 22
28
+ terminal: Optional[str] = "xterm"
29
+ keep_alive: Optional[int] = None
30
+ username: Optional[str] = None
31
+
32
+
33
+ class RdpJump(BaseJumpItem):
34
+ """Remote RDP Jump item."""
35
+
36
+ hostname: str
37
+ quality: Optional[str] = "video"
38
+ console: Optional[bool] = False
39
+ ignore_untrusted: Optional[bool] = False
40
+ rdp_username: Optional[str] = None
41
+ domain: Optional[str] = None
42
+ session_forensics: Optional[bool] = False
43
+ endpoint_id: Optional[int] = None
44
+ secure_app_type: Optional[str] = None
45
+
46
+
47
+ class VncJump(BaseJumpItem):
48
+ """Remote VNC Jump item."""
49
+
50
+ hostname: str
51
+ port: int = 5900
52
+
53
+
54
+ class WebJump(BaseJumpItem):
55
+ """Web Jump item for web-based access."""
56
+
57
+ url: str
58
+ verify_certificate: Optional[bool] = True
59
+ username: Optional[str] = None
60
+
61
+
62
+ class ProtocolTunnel(BaseJumpItem):
63
+ """Protocol Tunnel Jump item (TCP, MSSQL, PostgreSQL, MySQL, K8s)."""
64
+
65
+ hostname: str
66
+ tunnel_type: Literal["tcp", "mssql", "psql", "mysql", "k8s"] = "tcp"
67
+ tunnel_listen_address: Optional[str] = "127.0.0.1"
68
+ tunnel_definitions: Optional[str] = None # For TCP: "22;24;26;28"
69
+ username: Optional[str] = None # For database tunnels (mssql, psql, mysql)
70
+ database: Optional[str] = None # For database tunnels (mssql, psql, mysql)
71
+ url: Optional[str] = None # For K8s
72
+ ca_certificates: Optional[str] = None # For K8s
@@ -0,0 +1,19 @@
1
+ """Jumpoint model."""
2
+
3
+ from typing import Optional
4
+ from pydantic import Field
5
+
6
+ from .common import PRABaseModel
7
+
8
+
9
+ class Jumpoint(PRABaseModel):
10
+ """Jumpoint - connection proxy for remote access."""
11
+
12
+ id: int
13
+ name: str
14
+ code_name: Optional[str] = None
15
+ platform: Optional[str] = None
16
+ shell_jump_enabled: Optional[bool] = None
17
+ protocol_tunnel_enabled: Optional[bool] = None
18
+ rdp_service_account_id: Optional[int] = None
19
+ comments: Optional[str] = None
@@ -0,0 +1,14 @@
1
+ """Team model."""
2
+
3
+ from typing import Optional
4
+
5
+ from .common import PRABaseModel
6
+
7
+
8
+ class Team(PRABaseModel):
9
+ """PRA team."""
10
+
11
+ id: int
12
+ name: str
13
+ code_name: Optional[str] = None
14
+ comments: Optional[str] = None
@@ -0,0 +1,17 @@
1
+ """User model."""
2
+
3
+ from typing import Optional
4
+
5
+ from .common import PRABaseModel
6
+
7
+
8
+ class User(PRABaseModel):
9
+ """PRA user."""
10
+
11
+ id: int
12
+ username: str
13
+ display_name: Optional[str] = None
14
+ email: Optional[str] = None
15
+ enabled: Optional[bool] = True
16
+ security_provider_id: Optional[int] = None
17
+ last_login_time: Optional[str] = None
@@ -0,0 +1,45 @@
1
+ """Vault account models."""
2
+
3
+ from typing import Optional, Literal
4
+
5
+ from .common import PRABaseModel
6
+
7
+
8
+ class VaultAccount(PRABaseModel):
9
+ """Vault account for credential storage."""
10
+
11
+ id: int
12
+ name: str
13
+ type: Literal[
14
+ "username_password",
15
+ "ssh",
16
+ "ssh_ca",
17
+ "windows_local",
18
+ "windows_domain",
19
+ "opaque_token",
20
+ "x509_ca",
21
+ "x509_csr",
22
+ ]
23
+ description: Optional[str] = None
24
+ username: Optional[str] = None
25
+ personal: Optional[bool] = False
26
+ owner_user_id: Optional[int] = None
27
+ account_group_id: Optional[int] = None
28
+ account_policy_id: Optional[int] = None
29
+ last_checkout_time: Optional[str] = None
30
+
31
+
32
+ class VaultAccountGroup(PRABaseModel):
33
+ """Vault account group."""
34
+
35
+ id: int
36
+ name: str
37
+ description: Optional[str] = None
38
+
39
+
40
+ class VaultCredential(PRABaseModel):
41
+ """Checked out vault credential."""
42
+
43
+ password: Optional[str] = None
44
+ private_key: Optional[str] = None
45
+ passphrase: Optional[str] = None
bt_cli/pws/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Password Safe product module."""
@@ -0,0 +1,5 @@
1
+ """Password Safe API client."""
2
+
3
+ from .base import PasswordSafeClient, get_client
4
+
5
+ __all__ = ["PasswordSafeClient", "get_client"]
@@ -0,0 +1,356 @@
1
+ """Password Safe API client."""
2
+
3
+ import logging
4
+ from typing import Any, Optional
5
+
6
+ import httpx
7
+
8
+ from ...core.config import PWSConfig, load_pws_config
9
+ from ...core.auth import AuthStrategy, PSAuthKeyAuth, OAuthClientCredentials
10
+ from ...core.rest_debug import get_event_hooks
11
+ from ...core.client import _warn_ssl_disabled
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class PasswordSafeClient:
17
+ """HTTP client for BeyondTrust Password Safe API.
18
+
19
+ Supports two authentication methods:
20
+ 1. API Key: Uses PS-Auth header
21
+ 2. OAuth: Uses Bearer token from client credentials flow
22
+
23
+ The client maintains a session after authentication via SignAppIn.
24
+ """
25
+
26
+ def __init__(self, config: PWSConfig):
27
+ """Initialize the Password Safe client.
28
+
29
+ Args:
30
+ config: Configuration with API URL and credentials
31
+ """
32
+ self.config = config
33
+ self._client: Optional[httpx.Client] = None
34
+ self._auth: AuthStrategy = self._create_auth(config)
35
+ self._session_active: bool = False
36
+
37
+ def _create_auth(self, config: PWSConfig) -> AuthStrategy:
38
+ """Create appropriate auth strategy based on config.
39
+
40
+ Args:
41
+ config: PWS configuration
42
+
43
+ Returns:
44
+ Auth strategy instance
45
+ """
46
+ if config.auth_method == "api_key":
47
+ return PSAuthKeyAuth(config.api_key, config.run_as)
48
+ return OAuthClientCredentials(config.client_id, config.client_secret)
49
+
50
+ def __enter__(self) -> "PasswordSafeClient":
51
+ """Context manager entry - create HTTP client."""
52
+ if not self.config.verify_ssl:
53
+ _warn_ssl_disabled()
54
+
55
+ self._client = httpx.Client(
56
+ base_url=self.config.api_url,
57
+ timeout=self.config.timeout,
58
+ verify=self.config.verify_ssl,
59
+ event_hooks=get_event_hooks(),
60
+ )
61
+ return self
62
+
63
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
64
+ """Context manager exit - close HTTP client and sign out."""
65
+ if self._session_active:
66
+ try:
67
+ self._auth.sign_out(self._client)
68
+ except Exception as e:
69
+ logger.debug(f"Sign out failed (best effort): {e}")
70
+ finally:
71
+ self._session_active = False
72
+ if self._client:
73
+ self._client.close()
74
+ self._client = None
75
+
76
+ def _ensure_client(self) -> httpx.Client:
77
+ """Ensure HTTP client is initialized."""
78
+ if self._client is None:
79
+ self._client = httpx.Client(
80
+ base_url=self.config.api_url,
81
+ timeout=self.config.timeout,
82
+ verify=self.config.verify_ssl,
83
+ event_hooks=get_event_hooks(),
84
+ )
85
+ return self._client
86
+
87
+ def _get_auth_headers(self) -> dict[str, str]:
88
+ """Get authentication headers.
89
+
90
+ Returns:
91
+ Dictionary of headers for authentication
92
+ """
93
+ headers = {
94
+ "Content-Type": "application/json",
95
+ "Accept": "application/json",
96
+ }
97
+ headers.update(self._auth.get_headers())
98
+ return headers
99
+
100
+ def _request(
101
+ self,
102
+ method: str,
103
+ path: str,
104
+ params: Optional[dict[str, Any]] = None,
105
+ json: Optional[dict[str, Any]] = None,
106
+ data: Optional[dict[str, Any]] = None,
107
+ headers: Optional[dict[str, str]] = None,
108
+ files: Optional[dict[str, Any]] = None,
109
+ ) -> Any:
110
+ """Make an HTTP request to the API.
111
+
112
+ Args:
113
+ method: HTTP method (GET, POST, PUT, DELETE)
114
+ path: API endpoint path
115
+ params: Query parameters
116
+ json: JSON body data
117
+ data: Form data (for OAuth token endpoint)
118
+ headers: Additional headers
119
+ files: File upload data
120
+
121
+ Returns:
122
+ Response data as dictionary
123
+
124
+ Raises:
125
+ httpx.HTTPStatusError: If request fails
126
+ """
127
+ client = self._ensure_client()
128
+
129
+ # Build headers
130
+ request_headers = self._get_auth_headers()
131
+ if headers:
132
+ request_headers.update(headers)
133
+
134
+ # Handle file uploads
135
+ if files:
136
+ request_headers.pop("Content-Type", None)
137
+
138
+ # Filter out None values from params
139
+ if params:
140
+ params = {k: v for k, v in params.items() if v is not None}
141
+
142
+ response = client.request(
143
+ method=method,
144
+ url=path,
145
+ params=params,
146
+ json=json,
147
+ data=data,
148
+ headers=request_headers,
149
+ files=files,
150
+ )
151
+ response.raise_for_status()
152
+
153
+ # Handle empty responses (204 No Content)
154
+ if response.status_code == 204 or not response.content:
155
+ return {}
156
+
157
+ try:
158
+ return response.json()
159
+ except Exception:
160
+ return {"content": response.text}
161
+
162
+ def get(
163
+ self, path: str, params: Optional[dict[str, Any]] = None
164
+ ) -> Any:
165
+ """Make a GET request.
166
+
167
+ Args:
168
+ path: API endpoint path
169
+ params: Query parameters
170
+
171
+ Returns:
172
+ Response data
173
+ """
174
+ return self._request("GET", path, params=params)
175
+
176
+ def post(
177
+ self,
178
+ path: str,
179
+ json: Optional[dict[str, Any]] = None,
180
+ params: Optional[dict[str, Any]] = None,
181
+ data: Optional[dict[str, Any]] = None,
182
+ headers: Optional[dict[str, str]] = None,
183
+ files: Optional[dict[str, Any]] = None,
184
+ ) -> Any:
185
+ """Make a POST request.
186
+
187
+ Args:
188
+ path: API endpoint path
189
+ json: JSON body data
190
+ params: Query parameters
191
+ data: Form data
192
+ headers: Additional headers
193
+ files: File upload data
194
+
195
+ Returns:
196
+ Response data
197
+ """
198
+ return self._request(
199
+ "POST", path, params=params, json=json, data=data, headers=headers, files=files
200
+ )
201
+
202
+ def put(
203
+ self,
204
+ path: str,
205
+ json: Optional[dict[str, Any]] = None,
206
+ params: Optional[dict[str, Any]] = None,
207
+ data: Optional[dict[str, Any]] = None,
208
+ headers: Optional[dict[str, str]] = None,
209
+ files: Optional[dict[str, Any]] = None,
210
+ ) -> Any:
211
+ """Make a PUT request.
212
+
213
+ Args:
214
+ path: API endpoint path
215
+ json: JSON body data
216
+ params: Query parameters
217
+ data: Form data
218
+ headers: Additional headers
219
+ files: File upload data
220
+
221
+ Returns:
222
+ Response data
223
+ """
224
+ return self._request(
225
+ "PUT", path, params=params, json=json, data=data, headers=headers, files=files
226
+ )
227
+
228
+ def delete(
229
+ self, path: str, params: Optional[dict[str, Any]] = None
230
+ ) -> Any:
231
+ """Make a DELETE request.
232
+
233
+ Args:
234
+ path: API endpoint path
235
+ params: Query parameters
236
+
237
+ Returns:
238
+ Response data
239
+ """
240
+ return self._request("DELETE", path, params=params)
241
+
242
+ def authenticate(self) -> dict[str, Any]:
243
+ """Authenticate and establish a session.
244
+
245
+ Uses API Key or OAuth based on configuration.
246
+
247
+ Returns:
248
+ Session information from SignAppIn response
249
+ """
250
+ client = self._ensure_client()
251
+ response = self._auth.authenticate(client)
252
+ self._session_active = True
253
+ return response
254
+
255
+ def sign_out(self) -> None:
256
+ """Sign out and end the API session."""
257
+ if self._session_active and self._client:
258
+ self._auth.sign_out(self._client)
259
+ self._session_active = False
260
+
261
+ def is_authenticated(self) -> bool:
262
+ """Check if client has an active session.
263
+
264
+ Returns:
265
+ True if session is active
266
+ """
267
+ return self._session_active
268
+
269
+ def paginate(
270
+ self,
271
+ path: str,
272
+ params: Optional[dict[str, Any]] = None,
273
+ page_size: int = 100,
274
+ max_pages: Optional[int] = None,
275
+ ) -> list[dict[str, Any]]:
276
+ """Paginate through all results from an endpoint.
277
+
278
+ Handles different response formats:
279
+ - Direct list: [items]
280
+ - Object with results key: {"results": [...], ...}
281
+
282
+ Args:
283
+ path: API endpoint path
284
+ params: Query parameters
285
+ page_size: Number of items per page
286
+ max_pages: Maximum number of pages to fetch (None for all)
287
+
288
+ Returns:
289
+ Complete list of all items
290
+ """
291
+ all_items: list[dict[str, Any]] = []
292
+ params = params or {}
293
+ page = 1
294
+
295
+ while True:
296
+ params["limit"] = page_size
297
+ params["offset"] = (page - 1) * page_size
298
+
299
+ response = self.get(path, params=params)
300
+
301
+ # Handle different response formats
302
+ if isinstance(response, list):
303
+ items = response
304
+ elif "Data" in response:
305
+ # BeyondTrust format: {"TotalCount": N, "Data": [...]}
306
+ items = response["Data"]
307
+ elif "results" in response:
308
+ items = response["results"]
309
+ elif "data" in response:
310
+ items = response["data"]
311
+ elif "items" in response:
312
+ items = response["items"]
313
+ else:
314
+ # Assume the response itself is the data
315
+ items = [response] if response else []
316
+
317
+ all_items.extend(items)
318
+
319
+ # Check if we've reached the end
320
+ if len(items) < page_size:
321
+ break
322
+
323
+ page += 1
324
+ if max_pages and page > max_pages:
325
+ break
326
+
327
+ return all_items
328
+
329
+
330
+ # Import mixins at module level
331
+ from .beyondinsight import BeyondInsightMixin
332
+ from .passwordsafe import PasswordSafeMixin
333
+
334
+
335
+ class FullPasswordSafeClient(PasswordSafeClient, BeyondInsightMixin, PasswordSafeMixin):
336
+ """Full Password Safe client with all API methods.
337
+
338
+ Combines:
339
+ - Base HTTP client functionality
340
+ - BeyondInsight methods (assets, systems, workgroups, platforms)
341
+ - Password Safe methods (accounts, credentials, requests, sessions)
342
+ """
343
+
344
+ pass
345
+
346
+
347
+ def get_client() -> FullPasswordSafeClient:
348
+ """Create a configured Password Safe client with all API methods.
349
+
350
+ Convenience function for CLI commands.
351
+
352
+ Returns:
353
+ FullPasswordSafeClient instance with BeyondInsight and Password Safe methods
354
+ """
355
+ config = load_pws_config()
356
+ return FullPasswordSafeClient(config)