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,869 @@
1
+ """BeyondInsight API client for asset and system management."""
2
+
3
+ from typing import Any, Optional, TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from .base import PasswordSafeClient
7
+
8
+
9
+ class BeyondInsightMixin:
10
+ """Mixin class providing BeyondInsight API methods.
11
+
12
+ Add to PasswordSafeClient to provide asset, system, and workgroup operations.
13
+ """
14
+
15
+ # =========================================================================
16
+ # Assets
17
+ # =========================================================================
18
+
19
+ def list_assets(
20
+ self: "PasswordSafeClient",
21
+ workgroup_id: Optional[int] = None,
22
+ limit: Optional[int] = None,
23
+ ) -> list[dict[str, Any]]:
24
+ """List assets by workgroup.
25
+
26
+ Assets must be retrieved by workgroup ID.
27
+
28
+ Args:
29
+ workgroup_id: Workgroup ID (if None, fetches from all workgroups)
30
+ limit: Maximum number of results
31
+
32
+ Returns:
33
+ List of asset objects
34
+ """
35
+ params = {}
36
+ if limit:
37
+ params["limit"] = limit
38
+
39
+ if workgroup_id:
40
+ return self.paginate(f"/Workgroups/{workgroup_id}/Assets", params=params)
41
+ else:
42
+ # Fetch assets from all workgroups
43
+ all_assets = []
44
+ workgroups = self.list_workgroups()
45
+ for wg in workgroups:
46
+ wg_id = wg.get("WorkgroupID", wg.get("ID"))
47
+ if wg_id:
48
+ try:
49
+ assets = self.paginate(f"/Workgroups/{wg_id}/Assets", params=params)
50
+ all_assets.extend(assets)
51
+ except Exception:
52
+ pass # Skip workgroups we can't access
53
+ return all_assets
54
+
55
+ def get_asset(self: "PasswordSafeClient", asset_id: int) -> dict[str, Any]:
56
+ """Get an asset by ID.
57
+
58
+ Args:
59
+ asset_id: Asset ID
60
+
61
+ Returns:
62
+ Asset object
63
+ """
64
+ return self.get(f"/Assets/{asset_id}")
65
+
66
+ def create_asset(
67
+ self: "PasswordSafeClient",
68
+ workgroup_id: int,
69
+ ip_address: str,
70
+ asset_name: Optional[str] = None,
71
+ dns_name: Optional[str] = None,
72
+ domain_name: Optional[str] = None,
73
+ mac_address: Optional[str] = None,
74
+ asset_type: Optional[str] = None,
75
+ description: Optional[str] = None,
76
+ ) -> dict[str, Any]:
77
+ """Create a new asset in a workgroup.
78
+
79
+ Args:
80
+ workgroup_id: Workgroup ID to add asset to
81
+ ip_address: IP address of the asset
82
+ asset_name: Optional name for the asset
83
+ dns_name: Optional DNS name
84
+ domain_name: Optional domain name
85
+ mac_address: Optional MAC address
86
+ asset_type: Optional asset type
87
+ description: Optional description
88
+
89
+ Returns:
90
+ Created asset object
91
+ """
92
+ data: dict[str, Any] = {"IPAddress": ip_address}
93
+ if asset_name:
94
+ data["AssetName"] = asset_name
95
+ if dns_name:
96
+ data["DnsName"] = dns_name
97
+ if domain_name:
98
+ data["DomainName"] = domain_name
99
+ if mac_address:
100
+ data["MACAddress"] = mac_address
101
+ if asset_type:
102
+ data["AssetType"] = asset_type
103
+ if description:
104
+ data["Description"] = description
105
+
106
+ return self.post(f"/Workgroups/{workgroup_id}/Assets", json=data)
107
+
108
+ def update_asset(
109
+ self: "PasswordSafeClient",
110
+ asset_id: int,
111
+ **kwargs: Any,
112
+ ) -> dict[str, Any]:
113
+ """Update an asset.
114
+
115
+ Args:
116
+ asset_id: Asset ID to update
117
+ **kwargs: Fields to update (asset_name, dns_name, etc.)
118
+
119
+ Returns:
120
+ Updated asset object
121
+ """
122
+ # Convert snake_case to PascalCase for API
123
+ data = {}
124
+ for key, value in kwargs.items():
125
+ if value is not None:
126
+ pascal_key = "".join(word.capitalize() for word in key.split("_"))
127
+ data[pascal_key] = value
128
+
129
+ return self.put(f"/Assets/{asset_id}", json=data)
130
+
131
+ def delete_asset(self: "PasswordSafeClient", asset_id: int) -> dict[str, Any]:
132
+ """Delete an asset by ID.
133
+
134
+ Args:
135
+ asset_id: Asset ID to delete
136
+
137
+ Returns:
138
+ Empty response on success
139
+ """
140
+ return self.delete(f"/Assets/{asset_id}")
141
+
142
+ def delete_asset_by_workgroup(
143
+ self: "PasswordSafeClient",
144
+ workgroup_id: int,
145
+ asset_name: str,
146
+ ) -> dict[str, Any]:
147
+ """Delete an asset by workgroup and asset name.
148
+
149
+ Args:
150
+ workgroup_id: Workgroup ID
151
+ asset_name: Asset name to delete
152
+
153
+ Returns:
154
+ Empty response on success
155
+ """
156
+ return self.delete(f"/Workgroups/{workgroup_id}/Assets/{asset_name}")
157
+
158
+ def search_assets(
159
+ self: "PasswordSafeClient",
160
+ search_term: str,
161
+ limit: Optional[int] = None,
162
+ ) -> list[dict[str, Any]]:
163
+ """Search for assets.
164
+
165
+ Args:
166
+ search_term: Search term
167
+ limit: Maximum number of results
168
+
169
+ Returns:
170
+ List of matching asset objects
171
+ """
172
+ data = {"SearchTerm": search_term}
173
+ if limit:
174
+ data["Limit"] = limit
175
+
176
+ response = self.post("/Assets/Search", json=data)
177
+ if isinstance(response, list):
178
+ return response
179
+ return response.get("Data", response.get("data", []))
180
+
181
+ # =========================================================================
182
+ # Managed Systems
183
+ # =========================================================================
184
+
185
+ def list_managed_systems(
186
+ self: "PasswordSafeClient",
187
+ workgroup_id: Optional[int] = None,
188
+ search: Optional[str] = None,
189
+ limit: Optional[int] = None,
190
+ ) -> list[dict[str, Any]]:
191
+ """List managed systems.
192
+
193
+ Args:
194
+ workgroup_id: Optional workgroup filter
195
+ search: Optional search filter
196
+ limit: Maximum number of results
197
+
198
+ Returns:
199
+ List of managed system objects
200
+ """
201
+ params = {}
202
+ if search:
203
+ params["search"] = search
204
+ if limit:
205
+ params["limit"] = limit
206
+
207
+ if workgroup_id:
208
+ return self.paginate(
209
+ f"/Workgroups/{workgroup_id}/ManagedSystems", params=params
210
+ )
211
+ return self.paginate("/ManagedSystems", params=params)
212
+
213
+ def get_managed_system(
214
+ self: "PasswordSafeClient", system_id: int
215
+ ) -> dict[str, Any]:
216
+ """Get a managed system by ID.
217
+
218
+ Args:
219
+ system_id: Managed system ID
220
+
221
+ Returns:
222
+ Managed system object
223
+ """
224
+ return self.get(f"/ManagedSystems/{system_id}")
225
+
226
+ def get_managed_system_by_name(
227
+ self: "PasswordSafeClient", system_name: str
228
+ ) -> Optional[dict[str, Any]]:
229
+ """Get a managed system by name.
230
+
231
+ Args:
232
+ system_name: System name to search for
233
+
234
+ Returns:
235
+ Managed system object or None if not found
236
+ """
237
+ systems = self.list_managed_systems(search=system_name)
238
+ for system in systems:
239
+ if system.get("SystemName", "").lower() == system_name.lower():
240
+ return system
241
+ return None
242
+
243
+ def create_managed_system(
244
+ self: "PasswordSafeClient",
245
+ system_name: str,
246
+ platform_id: int,
247
+ workgroup_id: Optional[int] = None,
248
+ asset_id: Optional[int] = None,
249
+ contact_email: Optional[str] = None,
250
+ description: Optional[str] = None,
251
+ port: Optional[int] = None,
252
+ timeout: Optional[int] = None,
253
+ functional_account_id: Optional[int] = None,
254
+ elevation_command: Optional[str] = None,
255
+ auto_management_flag: Optional[bool] = None,
256
+ check_password_flag: Optional[bool] = None,
257
+ change_password_after_any_release_flag: Optional[bool] = None,
258
+ reset_password_on_mismatch_flag: Optional[bool] = None,
259
+ change_frequency_type: Optional[str] = None,
260
+ change_frequency_days: Optional[int] = None,
261
+ change_time: Optional[str] = None,
262
+ password_rule_id: Optional[int] = None,
263
+ ) -> dict[str, Any]:
264
+ """Create a new managed system.
265
+
266
+ Args:
267
+ system_name: Name for the managed system
268
+ platform_id: Platform ID (determines OS/system type)
269
+ workgroup_id: Workgroup to add system to
270
+ asset_id: Optional asset ID to link
271
+ contact_email: Contact email address
272
+ description: System description
273
+ port: Connection port
274
+ timeout: Connection timeout
275
+ functional_account_id: Functional account for management
276
+ elevation_command: Command for privilege elevation
277
+ auto_management_flag: Enable automatic password management
278
+ check_password_flag: Enable password checking
279
+ change_password_after_any_release_flag: Change password after release
280
+ reset_password_on_mismatch_flag: Reset on password mismatch
281
+ change_frequency_type: Password change frequency type
282
+ change_frequency_days: Days between password changes
283
+ change_time: Time of day for password changes
284
+ password_rule_id: Password rule ID for password generation
285
+
286
+ Returns:
287
+ Created managed system object
288
+ """
289
+ data: dict[str, Any] = {
290
+ "SystemName": system_name,
291
+ "PlatformID": platform_id,
292
+ }
293
+
294
+ if workgroup_id is not None:
295
+ data["WorkgroupID"] = workgroup_id
296
+ if asset_id is not None:
297
+ data["AssetID"] = asset_id
298
+ if contact_email:
299
+ data["ContactEmail"] = contact_email
300
+ if description:
301
+ data["Description"] = description
302
+ if port is not None:
303
+ data["Port"] = port
304
+ if timeout is not None:
305
+ data["Timeout"] = timeout
306
+ if functional_account_id is not None:
307
+ data["FunctionalAccountID"] = functional_account_id
308
+ if elevation_command:
309
+ data["ElevationCommand"] = elevation_command
310
+ if auto_management_flag is not None:
311
+ data["AutoManagementFlag"] = auto_management_flag
312
+ if check_password_flag is not None:
313
+ data["CheckPasswordFlag"] = check_password_flag
314
+ if change_password_after_any_release_flag is not None:
315
+ data["ChangePasswordAfterAnyReleaseFlag"] = change_password_after_any_release_flag
316
+ if reset_password_on_mismatch_flag is not None:
317
+ data["ResetPasswordOnMismatchFlag"] = reset_password_on_mismatch_flag
318
+ if change_frequency_type:
319
+ data["ChangeFrequencyType"] = change_frequency_type
320
+ if change_frequency_days is not None:
321
+ data["ChangeFrequencyDays"] = change_frequency_days
322
+ if change_time:
323
+ data["ChangeTime"] = change_time
324
+ if password_rule_id is not None:
325
+ data["PasswordRuleID"] = password_rule_id
326
+
327
+ # Determine endpoint based on relationship
328
+ if workgroup_id:
329
+ return self.post(f"/Workgroups/{workgroup_id}/ManagedSystems", json=data)
330
+ elif asset_id:
331
+ return self.post(f"/Assets/{asset_id}/ManagedSystems", json=data)
332
+ else:
333
+ return self.post("/ManagedSystems", json=data)
334
+
335
+ def update_managed_system(
336
+ self: "PasswordSafeClient",
337
+ system_id: int,
338
+ **kwargs: Any,
339
+ ) -> dict[str, Any]:
340
+ """Update a managed system.
341
+
342
+ Args:
343
+ system_id: System ID to update
344
+ **kwargs: Fields to update (SystemName, Description, etc.)
345
+
346
+ Returns:
347
+ Updated managed system object
348
+ """
349
+ # Convert snake_case to PascalCase for API
350
+ data = {}
351
+ for key, value in kwargs.items():
352
+ if value is not None:
353
+ pascal_key = "".join(word.capitalize() for word in key.split("_"))
354
+ data[pascal_key] = value
355
+
356
+ return self.put(f"/ManagedSystems/{system_id}", json=data)
357
+
358
+ def delete_managed_system(
359
+ self: "PasswordSafeClient", system_id: int
360
+ ) -> dict[str, Any]:
361
+ """Delete a managed system.
362
+
363
+ Args:
364
+ system_id: System ID to delete
365
+
366
+ Returns:
367
+ Empty response on success
368
+ """
369
+ return self.delete(f"/ManagedSystems/{system_id}")
370
+
371
+ # =========================================================================
372
+ # Workgroups
373
+ # =========================================================================
374
+
375
+ def list_workgroups(
376
+ self: "PasswordSafeClient",
377
+ search: Optional[str] = None,
378
+ limit: Optional[int] = None,
379
+ ) -> list[dict[str, Any]]:
380
+ """List all workgroups.
381
+
382
+ Args:
383
+ search: Optional search filter
384
+ limit: Maximum number of results
385
+
386
+ Returns:
387
+ List of workgroup objects
388
+ """
389
+ params = {}
390
+ if search:
391
+ params["search"] = search
392
+ if limit:
393
+ params["limit"] = limit
394
+
395
+ return self.paginate("/Workgroups", params=params)
396
+
397
+ def get_workgroup(self: "PasswordSafeClient", workgroup_id: int) -> dict[str, Any]:
398
+ """Get a workgroup by ID.
399
+
400
+ Args:
401
+ workgroup_id: Workgroup ID
402
+
403
+ Returns:
404
+ Workgroup object
405
+ """
406
+ return self.get(f"/Workgroups/{workgroup_id}")
407
+
408
+ def create_workgroup(
409
+ self: "PasswordSafeClient",
410
+ name: str,
411
+ description: Optional[str] = None,
412
+ ) -> dict[str, Any]:
413
+ """Create a new workgroup.
414
+
415
+ Args:
416
+ name: Workgroup name
417
+ description: Optional description
418
+
419
+ Returns:
420
+ Created workgroup object
421
+ """
422
+ data = {"Name": name}
423
+ if description:
424
+ data["Description"] = description
425
+
426
+ return self.post("/Workgroups", json=data)
427
+
428
+ def delete_workgroup(self: "PasswordSafeClient", workgroup_id: int) -> dict[str, Any]:
429
+ """Delete a workgroup.
430
+
431
+ Args:
432
+ workgroup_id: Workgroup ID to delete
433
+
434
+ Returns:
435
+ Empty response on success
436
+ """
437
+ return self.delete(f"/Workgroups/{workgroup_id}")
438
+
439
+ # =========================================================================
440
+ # Platforms
441
+ # =========================================================================
442
+
443
+ def list_platforms(
444
+ self: "PasswordSafeClient",
445
+ search: Optional[str] = None,
446
+ ) -> list[dict[str, Any]]:
447
+ """List all platforms (OS types).
448
+
449
+ Args:
450
+ search: Optional search filter
451
+
452
+ Returns:
453
+ List of platform objects
454
+ """
455
+ params = {}
456
+ if search:
457
+ params["search"] = search
458
+
459
+ return self.paginate("/Platforms", params=params)
460
+
461
+ def get_platform(self: "PasswordSafeClient", platform_id: int) -> dict[str, Any]:
462
+ """Get a platform by ID.
463
+
464
+ Args:
465
+ platform_id: Platform ID
466
+
467
+ Returns:
468
+ Platform object
469
+ """
470
+ return self.get(f"/Platforms/{platform_id}")
471
+
472
+ # =========================================================================
473
+ # Databases
474
+ # =========================================================================
475
+
476
+ def list_databases(
477
+ self: "PasswordSafeClient",
478
+ asset_id: Optional[int] = None,
479
+ limit: Optional[int] = None,
480
+ ) -> list[dict[str, Any]]:
481
+ """List databases.
482
+
483
+ Args:
484
+ asset_id: Optional asset ID to filter by
485
+ limit: Maximum number of results (None for all)
486
+
487
+ Returns:
488
+ List of database objects
489
+ """
490
+ endpoint = f"/Assets/{asset_id}/Databases" if asset_id else "/Databases"
491
+
492
+ # If limit is set, do a single request
493
+ if limit is not None:
494
+ result = self.get(endpoint, params={"limit": limit})
495
+ if isinstance(result, list):
496
+ return result
497
+ if isinstance(result, dict):
498
+ return result.get("Data", result.get("results", []))
499
+ return []
500
+
501
+ return self.paginate(endpoint)
502
+
503
+ def get_database(self: "PasswordSafeClient", database_id: int) -> dict[str, Any]:
504
+ """Get a database by ID.
505
+
506
+ Args:
507
+ database_id: Database ID
508
+
509
+ Returns:
510
+ Database object
511
+ """
512
+ return self.get(f"/Databases/{database_id}")
513
+
514
+ def create_database(
515
+ self: "PasswordSafeClient",
516
+ asset_id: int,
517
+ platform_id: int,
518
+ instance_name: str,
519
+ port: Optional[int] = None,
520
+ is_default_instance: bool = False,
521
+ version: Optional[str] = None,
522
+ template: Optional[str] = None,
523
+ ) -> dict[str, Any]:
524
+ """Create a database on an asset.
525
+
526
+ Database platforms include: PostgreSQL (79), MySQL (10), MS SQL Server (11),
527
+ Oracle (8), MongoDB (74), etc.
528
+
529
+ Args:
530
+ asset_id: Asset ID to create database on
531
+ platform_id: Database platform ID
532
+ instance_name: Database instance name
533
+ port: Connection port (uses platform default if not specified)
534
+ is_default_instance: Whether this is the default instance
535
+ version: Database version
536
+ template: Database template
537
+
538
+ Returns:
539
+ Created database object
540
+ """
541
+ data: dict[str, Any] = {
542
+ "PlatformID": platform_id,
543
+ "InstanceName": instance_name,
544
+ }
545
+ if port is not None:
546
+ data["Port"] = port
547
+ if is_default_instance:
548
+ data["IsDefaultInstance"] = is_default_instance
549
+ if version:
550
+ data["Version"] = version
551
+ if template:
552
+ data["Template"] = template
553
+
554
+ return self.post(f"/Assets/{asset_id}/Databases", json=data)
555
+
556
+ def delete_database(self: "PasswordSafeClient", database_id: int) -> dict[str, Any]:
557
+ """Delete a database.
558
+
559
+ Args:
560
+ database_id: Database ID to delete
561
+
562
+ Returns:
563
+ Empty response on success
564
+ """
565
+ return self.delete(f"/Databases/{database_id}")
566
+
567
+ def create_database_managed_system(
568
+ self: "PasswordSafeClient",
569
+ database_id: int,
570
+ platform_id: int,
571
+ system_name: Optional[str] = None,
572
+ port: Optional[int] = None,
573
+ functional_account_id: Optional[int] = None,
574
+ description: Optional[str] = None,
575
+ auto_management_flag: bool = False,
576
+ check_password_flag: bool = True,
577
+ change_password_after_any_release_flag: bool = True,
578
+ reset_password_on_mismatch_flag: bool = True,
579
+ change_frequency_type: Optional[str] = None,
580
+ change_frequency_days: Optional[int] = None,
581
+ change_time: Optional[str] = None,
582
+ ) -> dict[str, Any]:
583
+ """Create a managed system for a database.
584
+
585
+ This is the correct way to create managed systems for database platforms
586
+ like PostgreSQL, MySQL, Oracle, etc.
587
+
588
+ Args:
589
+ database_id: Database ID to create managed system for
590
+ platform_id: Platform ID (must match database platform)
591
+ system_name: Name for the managed system
592
+ port: Connection port
593
+ functional_account_id: Functional account for password management
594
+ description: System description
595
+ auto_management_flag: Enable automatic password management
596
+ check_password_flag: Enable password checking
597
+ change_password_after_any_release_flag: Change password after release
598
+ reset_password_on_mismatch_flag: Reset on password mismatch
599
+ change_frequency_type: Password change frequency (first, last, xdays)
600
+ change_frequency_days: Days between changes (if xdays)
601
+ change_time: Time for password changes (HH:MM format)
602
+
603
+ Returns:
604
+ Created managed system object
605
+ """
606
+ data: dict[str, Any] = {
607
+ "PlatformID": platform_id,
608
+ }
609
+ if system_name:
610
+ data["SystemName"] = system_name
611
+ if port is not None:
612
+ data["Port"] = port
613
+ if description:
614
+ data["Description"] = description
615
+ if auto_management_flag:
616
+ data["AutoManagementFlag"] = auto_management_flag
617
+ if functional_account_id is not None:
618
+ data["FunctionalAccountID"] = functional_account_id
619
+ if change_frequency_type:
620
+ data["ChangeFrequencyType"] = change_frequency_type
621
+ if change_frequency_days is not None:
622
+ data["ChangeFrequencyDays"] = change_frequency_days
623
+ if change_time:
624
+ data["ChangeTime"] = change_time
625
+ if check_password_flag is not None:
626
+ data["CheckPasswordFlag"] = check_password_flag
627
+ if change_password_after_any_release_flag is not None:
628
+ data["ChangePasswordAfterAnyReleaseFlag"] = change_password_after_any_release_flag
629
+ if reset_password_on_mismatch_flag is not None:
630
+ data["ResetPasswordOnMismatchFlag"] = reset_password_on_mismatch_flag
631
+
632
+ return self.post(f"/Databases/{database_id}/ManagedSystems", json=data)
633
+
634
+ # =========================================================================
635
+ # Directories (Entra ID, Active Directory)
636
+ # =========================================================================
637
+
638
+ def list_directories(self: "PasswordSafeClient") -> list[dict[str, Any]]:
639
+ """List directories (Active Directory, Entra ID).
640
+
641
+ Returns:
642
+ List of directory objects
643
+ """
644
+ return self.paginate("/Directories")
645
+
646
+ def get_directory(self: "PasswordSafeClient", directory_id: int) -> dict[str, Any]:
647
+ """Get a directory by ID.
648
+
649
+ Args:
650
+ directory_id: Directory ID
651
+
652
+ Returns:
653
+ Directory object
654
+ """
655
+ return self.get(f"/Directories/{directory_id}")
656
+
657
+ def create_directory_managed_system(
658
+ self: "PasswordSafeClient",
659
+ workgroup_id: int,
660
+ platform_id: int,
661
+ host_name: str,
662
+ domain_name: Optional[str] = None,
663
+ system_name: Optional[str] = None,
664
+ functional_account_id: Optional[int] = None,
665
+ description: Optional[str] = None,
666
+ auto_management_flag: bool = False,
667
+ account_name_format: int = 1,
668
+ check_password_flag: bool = False,
669
+ change_password_after_any_release_flag: bool = False,
670
+ reset_password_on_mismatch_flag: bool = False,
671
+ change_frequency_type: Optional[str] = None,
672
+ change_frequency_days: Optional[int] = None,
673
+ change_time: Optional[str] = None,
674
+ ) -> dict[str, Any]:
675
+ """Create a managed system for a directory (Entra ID, Active Directory).
676
+
677
+ This is used for directory platforms like Microsoft Entra ID (84) and
678
+ Active Directory (25).
679
+
680
+ Args:
681
+ workgroup_id: Workgroup ID to add the system to
682
+ platform_id: Platform ID (84=Entra ID, 25=Active Directory)
683
+ host_name: Host name (domain name for Entra ID)
684
+ domain_name: Domain name
685
+ system_name: Name for the managed system
686
+ functional_account_id: Functional account for management
687
+ description: System description
688
+ auto_management_flag: Enable automatic password management
689
+ account_name_format: 0=Domain/Account, 1=UPN, 2=SAM
690
+ check_password_flag: Enable password checking
691
+ change_password_after_any_release_flag: Change password after release
692
+ reset_password_on_mismatch_flag: Reset on password mismatch
693
+ change_frequency_type: Password change frequency (first, last, xdays)
694
+ change_frequency_days: Days between changes (if xdays)
695
+ change_time: Time for password changes (HH:MM format)
696
+
697
+ Returns:
698
+ Created managed system object
699
+ """
700
+ data: dict[str, Any] = {
701
+ "EntityTypeID": 3, # Directory
702
+ "PlatformID": platform_id,
703
+ "HostName": host_name,
704
+ "AccountNameFormat": account_name_format,
705
+ }
706
+ if domain_name:
707
+ data["DomainName"] = domain_name
708
+ if system_name:
709
+ data["SystemName"] = system_name
710
+ if description:
711
+ data["Description"] = description
712
+ if auto_management_flag:
713
+ data["AutoManagementFlag"] = auto_management_flag
714
+ if functional_account_id is not None:
715
+ data["FunctionalAccountID"] = functional_account_id
716
+ if change_frequency_type:
717
+ data["ChangeFrequencyType"] = change_frequency_type
718
+ if change_frequency_days is not None:
719
+ data["ChangeFrequencyDays"] = change_frequency_days
720
+ if change_time:
721
+ data["ChangeTime"] = change_time
722
+ elif functional_account_id is not None:
723
+ data["FunctionalAccountID"] = functional_account_id
724
+ if check_password_flag is not None:
725
+ data["CheckPasswordFlag"] = check_password_flag
726
+ if change_password_after_any_release_flag is not None:
727
+ data["ChangePasswordAfterAnyReleaseFlag"] = change_password_after_any_release_flag
728
+ if reset_password_on_mismatch_flag is not None:
729
+ data["ResetPasswordOnMismatchFlag"] = reset_password_on_mismatch_flag
730
+
731
+ return self.post(f"/Workgroups/{workgroup_id}/ManagedSystems", json=data)
732
+
733
+ def create_cloud_managed_system(
734
+ self: "PasswordSafeClient",
735
+ workgroup_id: int,
736
+ platform_id: int,
737
+ host_name: str,
738
+ system_name: Optional[str] = None,
739
+ access_url: Optional[str] = None,
740
+ functional_account_id: Optional[int] = None,
741
+ description: Optional[str] = None,
742
+ auto_management_flag: bool = False,
743
+ check_password_flag: bool = False,
744
+ change_password_after_any_release_flag: bool = False,
745
+ reset_password_on_mismatch_flag: bool = False,
746
+ change_frequency_type: Optional[str] = None,
747
+ change_frequency_days: Optional[int] = None,
748
+ change_time: Optional[str] = None,
749
+ ) -> dict[str, Any]:
750
+ """Create a managed system for a cloud platform (AWS, Azure, GCP).
751
+
752
+ This is used for cloud platforms like Amazon (47).
753
+
754
+ Args:
755
+ workgroup_id: Workgroup ID to add the system to
756
+ platform_id: Platform ID (47=Amazon)
757
+ host_name: Host name / identifier for the cloud account
758
+ system_name: Name for the managed system
759
+ access_url: Access URL (e.g., https://account-id.signin.aws.amazon.com/console)
760
+ functional_account_id: Functional account for management
761
+ description: System description
762
+ auto_management_flag: Enable automatic password management
763
+ check_password_flag: Enable password checking
764
+ change_password_after_any_release_flag: Change password after release
765
+ reset_password_on_mismatch_flag: Reset on password mismatch
766
+ change_frequency_type: Password change frequency (first, last, xdays)
767
+ change_frequency_days: Days between changes (if xdays)
768
+ change_time: Time for password changes (HH:MM format)
769
+
770
+ Returns:
771
+ Created managed system object
772
+ """
773
+ data: dict[str, Any] = {
774
+ "EntityTypeID": 4, # Cloud
775
+ "PlatformID": platform_id,
776
+ "HostName": host_name,
777
+ }
778
+ if system_name:
779
+ data["SystemName"] = system_name
780
+ if access_url:
781
+ data["AccessURL"] = access_url
782
+ if description:
783
+ data["Description"] = description
784
+ if auto_management_flag:
785
+ data["AutoManagementFlag"] = auto_management_flag
786
+ if functional_account_id is not None:
787
+ data["FunctionalAccountID"] = functional_account_id
788
+ if change_frequency_type:
789
+ data["ChangeFrequencyType"] = change_frequency_type
790
+ if change_frequency_days is not None:
791
+ data["ChangeFrequencyDays"] = change_frequency_days
792
+ if change_time:
793
+ data["ChangeTime"] = change_time
794
+ elif functional_account_id is not None:
795
+ data["FunctionalAccountID"] = functional_account_id
796
+ if check_password_flag is not None:
797
+ data["CheckPasswordFlag"] = check_password_flag
798
+ if change_password_after_any_release_flag is not None:
799
+ data["ChangePasswordAfterAnyReleaseFlag"] = change_password_after_any_release_flag
800
+ if reset_password_on_mismatch_flag is not None:
801
+ data["ResetPasswordOnMismatchFlag"] = reset_password_on_mismatch_flag
802
+
803
+ return self.post(f"/Workgroups/{workgroup_id}/ManagedSystems", json=data)
804
+
805
+ # =========================================================================
806
+ # Smart Rules
807
+ # =========================================================================
808
+
809
+ def list_smart_rules(
810
+ self: "PasswordSafeClient",
811
+ search: Optional[str] = None,
812
+ ) -> list[dict[str, Any]]:
813
+ """List all smart rules.
814
+
815
+ Args:
816
+ search: Optional search filter
817
+
818
+ Returns:
819
+ List of smart rule objects
820
+ """
821
+ params = {}
822
+ if search:
823
+ params["search"] = search
824
+
825
+ return self.paginate("/SmartRules", params=params)
826
+
827
+ def get_smart_rule(self: "PasswordSafeClient", rule_id: int) -> dict[str, Any]:
828
+ """Get a smart rule by ID.
829
+
830
+ Args:
831
+ rule_id: Smart rule ID
832
+
833
+ Returns:
834
+ Smart rule object
835
+ """
836
+ return self.get(f"/SmartRules/{rule_id}")
837
+
838
+ # =========================================================================
839
+ # Attributes
840
+ # =========================================================================
841
+
842
+ def list_attributes(
843
+ self: "PasswordSafeClient",
844
+ attribute_type: Optional[str] = None,
845
+ ) -> list[dict[str, Any]]:
846
+ """List attributes.
847
+
848
+ Args:
849
+ attribute_type: Filter by type (Asset, ManagedSystem, ManagedAccount)
850
+
851
+ Returns:
852
+ List of attribute objects
853
+ """
854
+ params = {}
855
+ if attribute_type:
856
+ params["type"] = attribute_type
857
+
858
+ return self.paginate("/Attributes", params=params)
859
+
860
+ def get_attribute(self: "PasswordSafeClient", attribute_id: int) -> dict[str, Any]:
861
+ """Get an attribute by ID.
862
+
863
+ Args:
864
+ attribute_id: Attribute ID
865
+
866
+ Returns:
867
+ Attribute object
868
+ """
869
+ return self.get(f"/Attributes/{attribute_id}")