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,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,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,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)
|