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
bt_cli/core/config.py ADDED
@@ -0,0 +1,393 @@
1
+ """Multi-product configuration management for BeyondTrust CLI."""
2
+
3
+ import os
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Optional
6
+
7
+ from dotenv import load_dotenv
8
+
9
+ from .config_file import get_layered_config, get_secret_from_keyring
10
+
11
+
12
+ @dataclass
13
+ class ProductConfig:
14
+ """Base configuration shared by all products."""
15
+
16
+ api_url: str
17
+ verify_ssl: bool = True
18
+ timeout: float = 30.0
19
+
20
+
21
+ @dataclass
22
+ class PWSConfig(ProductConfig):
23
+ """Password Safe configuration.
24
+
25
+ Supports two authentication methods:
26
+ - API Key: Uses PS-Auth header format
27
+ - OAuth: Uses client_credentials grant flow
28
+ """
29
+
30
+ api_key: Optional[str] = None
31
+ client_id: Optional[str] = None
32
+ client_secret: Optional[str] = None
33
+ run_as: Optional[str] = None
34
+ api_version: str = "3.1"
35
+
36
+ @property
37
+ def auth_method(self) -> str:
38
+ """Determine which auth method to use."""
39
+ if self.api_key:
40
+ return "api_key"
41
+ if self.client_id and self.client_secret:
42
+ return "oauth"
43
+ raise ValueError("PWS requires either API key or OAuth credentials")
44
+
45
+ def validate(self) -> None:
46
+ """Validate configuration."""
47
+ if not self.api_url:
48
+ raise ValueError("BT_PWS_API_URL is required")
49
+ if not self.api_key and not (self.client_id and self.client_secret):
50
+ raise ValueError(
51
+ "PWS requires either BT_PWS_API_KEY or both BT_PWS_CLIENT_ID and BT_PWS_CLIENT_SECRET"
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class EntitleConfig(ProductConfig):
57
+ """Entitle configuration.
58
+
59
+ Uses simple Bearer token authentication.
60
+ """
61
+
62
+ api_key: str = ""
63
+
64
+ def validate(self) -> None:
65
+ """Validate configuration."""
66
+ if not self.api_key:
67
+ raise ValueError("BT_ENTITLE_API_KEY is required")
68
+ if not self.api_url:
69
+ raise ValueError("BT_ENTITLE_API_URL is required")
70
+
71
+
72
+ @dataclass
73
+ class PRAConfig(ProductConfig):
74
+ """Privileged Remote Access configuration.
75
+
76
+ Uses OAuth 2.0 client credentials authentication.
77
+ API base path: /api/config/v1
78
+ """
79
+
80
+ client_id: str = ""
81
+ client_secret: str = ""
82
+
83
+ def validate(self) -> None:
84
+ """Validate configuration."""
85
+ if not self.api_url:
86
+ raise ValueError("BT_PRA_API_URL is required")
87
+ if not self.client_id or not self.client_secret:
88
+ raise ValueError(
89
+ "PRA requires both BT_PRA_CLIENT_ID and BT_PRA_CLIENT_SECRET"
90
+ )
91
+
92
+
93
+ @dataclass
94
+ class EPMWConfig(ProductConfig):
95
+ """EPM Windows configuration.
96
+
97
+ Uses OAuth 2.0 client credentials authentication.
98
+ API base path: /management-api/v3
99
+ Token endpoint: /oauth/token
100
+ """
101
+
102
+ client_id: str = ""
103
+ client_secret: str = ""
104
+
105
+ def validate(self) -> None:
106
+ """Validate configuration."""
107
+ if not self.api_url:
108
+ raise ValueError("BT_EPM_API_URL is required")
109
+ if not self.client_id or not self.client_secret:
110
+ raise ValueError(
111
+ "EPMW requires both BT_EPM_CLIENT_ID and BT_EPM_CLIENT_SECRET"
112
+ )
113
+
114
+
115
+ def _get_bool(value: Optional[str], default: bool = True) -> bool:
116
+ """Parse boolean from environment variable."""
117
+ if value is None:
118
+ return default
119
+ return value.lower() not in ("false", "0", "no", "off")
120
+
121
+
122
+ def _get_float(value: Optional[str], default: float, min_val: float = 0.1, max_val: float = 600.0) -> float:
123
+ """Parse float from environment variable with range validation.
124
+
125
+ Args:
126
+ value: String value to parse
127
+ default: Default if None or invalid
128
+ min_val: Minimum allowed value (default 0.1 seconds)
129
+ max_val: Maximum allowed value (default 600 seconds / 10 minutes)
130
+
131
+ Returns:
132
+ Float value within valid range, or default if invalid
133
+ """
134
+ if value is None:
135
+ return default
136
+ try:
137
+ result = float(value)
138
+ # Validate range - use default if out of bounds
139
+ if result < min_val or result > max_val:
140
+ return default
141
+ return result
142
+ except ValueError:
143
+ return default
144
+
145
+
146
+ def _resolve_value(value: Any) -> Any:
147
+ """Resolve keyring references in config values.
148
+
149
+ If value is a string starting with 'keyring://', fetch from keyring.
150
+ """
151
+ if isinstance(value, str) and value.startswith("keyring://"):
152
+ # Format: keyring://service/key
153
+ parts = value[len("keyring://"):].split("/", 1)
154
+ if len(parts) == 2:
155
+ service, key = parts
156
+ resolved = get_secret_from_keyring(service, key)
157
+ if resolved:
158
+ return resolved
159
+ # Return None if keyring lookup fails
160
+ return None
161
+ return value
162
+
163
+
164
+ def _to_bool(value: Any) -> bool:
165
+ """Convert value to boolean."""
166
+ if isinstance(value, bool):
167
+ return value
168
+ if isinstance(value, str):
169
+ return value.lower() not in ("false", "0", "no", "off", "")
170
+ return bool(value)
171
+
172
+
173
+ def _to_float(value: Any, default: float = 30.0, min_val: float = 0.1, max_val: float = 600.0) -> float:
174
+ """Convert value to float with range validation.
175
+
176
+ Args:
177
+ value: Value to convert
178
+ default: Default if None or invalid
179
+ min_val: Minimum allowed value (default 0.1 seconds)
180
+ max_val: Maximum allowed value (default 600 seconds / 10 minutes)
181
+
182
+ Returns:
183
+ Float value within valid range, or default if invalid
184
+ """
185
+ if value is None:
186
+ return default
187
+ try:
188
+ result = float(value)
189
+ # Validate range - use default if out of bounds
190
+ if result < min_val or result > max_val:
191
+ return default
192
+ return result
193
+ except (ValueError, TypeError):
194
+ return default
195
+
196
+
197
+ def _get_profile() -> Optional[str]:
198
+ """Get the active profile from CLI or environment."""
199
+ # Try to get from CLI module
200
+ try:
201
+ from ..cli import get_active_profile
202
+ profile = get_active_profile()
203
+ if profile:
204
+ return profile
205
+ except ImportError:
206
+ pass
207
+
208
+ # Fall back to environment variable
209
+ return os.getenv("BT_PROFILE")
210
+
211
+
212
+ def load_pws_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PWSConfig:
213
+ """Load Password Safe configuration.
214
+
215
+ Configuration sources (in order of precedence):
216
+ 1. Environment variables
217
+ 2. Config file (~/.bt-cli/config.yaml)
218
+
219
+ Environment variables:
220
+ BT_PWS_API_URL - API endpoint URL (required)
221
+ BT_PWS_API_KEY - API key for PS-Auth authentication
222
+ BT_PWS_CLIENT_ID - OAuth client ID
223
+ BT_PWS_CLIENT_SECRET - OAuth client secret
224
+ BT_PWS_VERIFY_SSL - SSL verification (default: true)
225
+ BT_PWS_TIMEOUT - Request timeout in seconds (default: 30)
226
+ BT_PWS_RUN_AS - Username for impersonation
227
+ BT_PWS_API_VERSION - API version (default: 3.1)
228
+ """
229
+ if env_file:
230
+ load_dotenv(env_file)
231
+ else:
232
+ load_dotenv()
233
+
234
+ # Get layered config (file + env vars)
235
+ profile = profile or _get_profile()
236
+ layered = get_layered_config("pws", profile)
237
+
238
+ # Resolve any keyring references
239
+ for key in ["api_key", "client_secret"]:
240
+ if key in layered:
241
+ layered[key] = _resolve_value(layered[key])
242
+
243
+ config = PWSConfig(
244
+ api_url=layered.get("api_url") or os.getenv("BT_PWS_API_URL", ""),
245
+ api_key=layered.get("api_key") or os.getenv("BT_PWS_API_KEY"),
246
+ client_id=layered.get("client_id") or os.getenv("BT_PWS_CLIENT_ID"),
247
+ client_secret=layered.get("client_secret") or os.getenv("BT_PWS_CLIENT_SECRET"),
248
+ verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PWS_VERIFY_SSL")),
249
+ timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PWS_TIMEOUT"), 30.0),
250
+ run_as=layered.get("run_as") or os.getenv("BT_PWS_RUN_AS"),
251
+ api_version=layered.get("api_version") or os.getenv("BT_PWS_API_VERSION", "3.1"),
252
+ )
253
+ config.validate()
254
+ return config
255
+
256
+
257
+ def load_entitle_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> EntitleConfig:
258
+ """Load Entitle configuration.
259
+
260
+ Configuration sources (in order of precedence):
261
+ 1. Environment variables
262
+ 2. Config file (~/.bt-cli/config.yaml)
263
+
264
+ Environment variables:
265
+ BT_ENTITLE_API_URL - API endpoint URL (default: https://api.us.entitle.io)
266
+ BT_ENTITLE_API_KEY - API key for Bearer authentication (required)
267
+ BT_ENTITLE_VERIFY_SSL - SSL verification (default: true)
268
+ BT_ENTITLE_TIMEOUT - Request timeout in seconds (default: 30)
269
+ """
270
+ if env_file:
271
+ load_dotenv(env_file)
272
+ else:
273
+ load_dotenv()
274
+
275
+ # Get layered config (file + env vars)
276
+ profile = profile or _get_profile()
277
+ layered = get_layered_config("entitle", profile)
278
+
279
+ # Resolve any keyring references
280
+ if "api_key" in layered:
281
+ layered["api_key"] = _resolve_value(layered["api_key"])
282
+
283
+ config = EntitleConfig(
284
+ api_url=layered.get("api_url") or os.getenv("BT_ENTITLE_API_URL", "https://api.us.entitle.io"),
285
+ api_key=layered.get("api_key") or os.getenv("BT_ENTITLE_API_KEY", ""),
286
+ verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_ENTITLE_VERIFY_SSL")),
287
+ timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_ENTITLE_TIMEOUT"), 30.0),
288
+ )
289
+ config.validate()
290
+ return config
291
+
292
+
293
+ def load_pra_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> PRAConfig:
294
+ """Load PRA configuration.
295
+
296
+ Configuration sources (in order of precedence):
297
+ 1. Environment variables
298
+ 2. Config file (~/.bt-cli/config.yaml)
299
+
300
+ Environment variables:
301
+ BT_PRA_API_URL - API endpoint URL (required, e.g., https://host.beyondtrustcloud.com)
302
+ BT_PRA_CLIENT_ID - OAuth client ID (required)
303
+ BT_PRA_CLIENT_SECRET - OAuth client secret (required)
304
+ BT_PRA_VERIFY_SSL - SSL verification (default: true)
305
+ BT_PRA_TIMEOUT - Request timeout in seconds (default: 30)
306
+ """
307
+ if env_file:
308
+ load_dotenv(env_file)
309
+ else:
310
+ load_dotenv()
311
+
312
+ # Get layered config (file + env vars)
313
+ profile = profile or _get_profile()
314
+ layered = get_layered_config("pra", profile)
315
+
316
+ # Resolve any keyring references
317
+ if "client_secret" in layered:
318
+ layered["client_secret"] = _resolve_value(layered["client_secret"])
319
+
320
+ config = PRAConfig(
321
+ api_url=layered.get("api_url") or os.getenv("BT_PRA_API_URL", ""),
322
+ client_id=layered.get("client_id") or os.getenv("BT_PRA_CLIENT_ID", ""),
323
+ client_secret=layered.get("client_secret") or os.getenv("BT_PRA_CLIENT_SECRET", ""),
324
+ verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_PRA_VERIFY_SSL")),
325
+ timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_PRA_TIMEOUT"), 30.0),
326
+ )
327
+ config.validate()
328
+ return config
329
+
330
+
331
+ def load_epmw_config(env_file: Optional[str] = None, profile: Optional[str] = None) -> EPMWConfig:
332
+ """Load EPM Windows configuration.
333
+
334
+ Configuration sources (in order of precedence):
335
+ 1. Environment variables
336
+ 2. Config file (~/.bt-cli/config.yaml)
337
+
338
+ Environment variables:
339
+ BT_EPM_API_URL - API endpoint URL (required, e.g., https://host-services.epm.bt3ng.com)
340
+ BT_EPM_CLIENT_ID - OAuth client ID (required)
341
+ BT_EPM_CLIENT_SECRET - OAuth client secret (required)
342
+ BT_EPM_VERIFY_SSL - SSL verification (default: true)
343
+ BT_EPM_TIMEOUT - Request timeout in seconds (default: 30)
344
+ """
345
+ if env_file:
346
+ load_dotenv(env_file)
347
+ else:
348
+ load_dotenv()
349
+
350
+ # Get layered config (file + env vars)
351
+ profile = profile or _get_profile()
352
+ layered = get_layered_config("epmw", profile)
353
+
354
+ # Resolve any keyring references
355
+ if "client_secret" in layered:
356
+ layered["client_secret"] = _resolve_value(layered["client_secret"])
357
+
358
+ config = EPMWConfig(
359
+ api_url=layered.get("api_url") or os.getenv("BT_EPM_API_URL", ""),
360
+ client_id=layered.get("client_id") or os.getenv("BT_EPM_CLIENT_ID", ""),
361
+ client_secret=layered.get("client_secret") or os.getenv("BT_EPM_CLIENT_SECRET", ""),
362
+ verify_ssl=_to_bool(layered.get("verify_ssl")) if "verify_ssl" in layered else _get_bool(os.getenv("BT_EPM_VERIFY_SSL")),
363
+ timeout=_to_float(layered.get("timeout")) if "timeout" in layered else _get_float(os.getenv("BT_EPM_TIMEOUT"), 30.0),
364
+ )
365
+ config.validate()
366
+ return config
367
+
368
+
369
+ def load_config(product: str, env_file: Optional[str] = None) -> ProductConfig:
370
+ """Load configuration for a specific product.
371
+
372
+ Args:
373
+ product: Product name ('pws', 'entitle', 'pra', 'epmw')
374
+ env_file: Optional path to .env file
375
+
376
+ Returns:
377
+ Product-specific configuration object
378
+
379
+ Raises:
380
+ ValueError: If product is unknown or configuration is invalid
381
+ """
382
+ loaders = {
383
+ "pws": load_pws_config,
384
+ "entitle": load_entitle_config,
385
+ "pra": load_pra_config,
386
+ "epmw": load_epmw_config,
387
+ }
388
+
389
+ loader = loaders.get(product.lower())
390
+ if not loader:
391
+ raise ValueError(f"Unknown product: {product}. Available: {list(loaders.keys())}")
392
+
393
+ return loader(env_file)