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,848 @@
|
|
|
1
|
+
"""EPM Windows API client with OAuth 2.0 authentication."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from bt_cli.core.config import EPMWConfig, load_epmw_config
|
|
10
|
+
from bt_cli.core.rest_debug import get_event_hooks
|
|
11
|
+
from bt_cli.core.client import _warn_ssl_disabled
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EPMWClient:
|
|
17
|
+
"""Client for BeyondTrust EPM Windows Management API.
|
|
18
|
+
|
|
19
|
+
Uses OAuth 2.0 client credentials flow for authentication.
|
|
20
|
+
Token endpoint: /oauth/token
|
|
21
|
+
API base path: /management-api/v3
|
|
22
|
+
Policy editor path: /policyeditor/v3
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, config: EPMWConfig):
|
|
26
|
+
"""Initialize EPMW client.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config: EPMW configuration with API URL and OAuth credentials
|
|
30
|
+
"""
|
|
31
|
+
self.config = config
|
|
32
|
+
self.base_url = config.api_url.rstrip("/")
|
|
33
|
+
self.api_base = f"{self.base_url}/management-api/v3"
|
|
34
|
+
self.policy_editor_base = f"{self.base_url}/policyeditor/v3"
|
|
35
|
+
self._token: Optional[str] = None
|
|
36
|
+
self._token_expires: float = 0
|
|
37
|
+
|
|
38
|
+
if not config.verify_ssl:
|
|
39
|
+
_warn_ssl_disabled()
|
|
40
|
+
|
|
41
|
+
self._client = httpx.Client(
|
|
42
|
+
verify=config.verify_ssl,
|
|
43
|
+
timeout=config.timeout,
|
|
44
|
+
event_hooks=get_event_hooks(),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def _get_token(self) -> str:
|
|
48
|
+
"""Get OAuth access token, refreshing if needed."""
|
|
49
|
+
if self._token and time.time() < self._token_expires - 60:
|
|
50
|
+
return self._token
|
|
51
|
+
|
|
52
|
+
token_url = f"{self.base_url}/oauth/token"
|
|
53
|
+
response = self._client.post(
|
|
54
|
+
token_url,
|
|
55
|
+
data={
|
|
56
|
+
"grant_type": "client_credentials",
|
|
57
|
+
"client_id": self.config.client_id,
|
|
58
|
+
"client_secret": self.config.client_secret,
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
response.raise_for_status()
|
|
62
|
+
data = response.json()
|
|
63
|
+
self._token = data["access_token"]
|
|
64
|
+
expires_in = data.get("expires_in", 3600)
|
|
65
|
+
self._token_expires = time.time() + expires_in
|
|
66
|
+
return self._token
|
|
67
|
+
|
|
68
|
+
def _headers(self) -> Dict[str, str]:
|
|
69
|
+
"""Get request headers with auth token."""
|
|
70
|
+
return {
|
|
71
|
+
"Authorization": f"Bearer {self._get_token()}",
|
|
72
|
+
"Accept": "application/json",
|
|
73
|
+
"Content-Type": "application/json",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def get(
|
|
77
|
+
self,
|
|
78
|
+
endpoint: str,
|
|
79
|
+
params: Optional[Dict[str, Any]] = None,
|
|
80
|
+
) -> Any:
|
|
81
|
+
"""Make GET request to API.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
endpoint: API endpoint path (e.g., "/Computers")
|
|
85
|
+
params: Optional query parameters
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
JSON response data
|
|
89
|
+
"""
|
|
90
|
+
url = f"{self.api_base}{endpoint}"
|
|
91
|
+
response = self._client.get(url, headers=self._headers(), params=params)
|
|
92
|
+
response.raise_for_status()
|
|
93
|
+
return response.json()
|
|
94
|
+
|
|
95
|
+
def get_paginated(
|
|
96
|
+
self,
|
|
97
|
+
endpoint: str,
|
|
98
|
+
params: Optional[Dict[str, Any]] = None,
|
|
99
|
+
page_size: int = 100,
|
|
100
|
+
) -> List[Any]:
|
|
101
|
+
"""Get all items from a paginated endpoint.
|
|
102
|
+
|
|
103
|
+
EPMW uses pageNumber/pageSize pagination with response format:
|
|
104
|
+
{"data": [...], "totalCount": N, "pageNumber": N, "pageSize": N}
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
endpoint: API endpoint path
|
|
108
|
+
params: Optional query parameters
|
|
109
|
+
page_size: Items per page (default 100)
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
List of all items across all pages
|
|
113
|
+
"""
|
|
114
|
+
all_items = []
|
|
115
|
+
current_page = 1
|
|
116
|
+
params = params or {}
|
|
117
|
+
|
|
118
|
+
while True:
|
|
119
|
+
params["pageSize"] = page_size
|
|
120
|
+
params["pageNumber"] = current_page
|
|
121
|
+
|
|
122
|
+
url = f"{self.api_base}{endpoint}"
|
|
123
|
+
response = self._client.get(url, headers=self._headers(), params=params)
|
|
124
|
+
response.raise_for_status()
|
|
125
|
+
|
|
126
|
+
result = response.json()
|
|
127
|
+
|
|
128
|
+
# Handle paginated response format
|
|
129
|
+
if isinstance(result, dict) and "data" in result:
|
|
130
|
+
items = result.get("data", [])
|
|
131
|
+
total_count = result.get("totalCount", 0)
|
|
132
|
+
else:
|
|
133
|
+
# Non-paginated response
|
|
134
|
+
items = result if isinstance(result, list) else [result]
|
|
135
|
+
total_count = len(items)
|
|
136
|
+
|
|
137
|
+
if not items:
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
all_items.extend(items)
|
|
141
|
+
|
|
142
|
+
# Check if we've fetched all items
|
|
143
|
+
if len(all_items) >= total_count:
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
current_page += 1
|
|
147
|
+
|
|
148
|
+
return all_items
|
|
149
|
+
|
|
150
|
+
def post(
|
|
151
|
+
self,
|
|
152
|
+
endpoint: str,
|
|
153
|
+
json: Optional[Dict[str, Any]] = None,
|
|
154
|
+
) -> Any:
|
|
155
|
+
"""Make POST request to API.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
endpoint: API endpoint path
|
|
159
|
+
json: JSON body data
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
JSON response data or None for 204 responses
|
|
163
|
+
"""
|
|
164
|
+
url = f"{self.api_base}{endpoint}"
|
|
165
|
+
response = self._client.post(url, headers=self._headers(), json=json)
|
|
166
|
+
response.raise_for_status()
|
|
167
|
+
if response.status_code == 204:
|
|
168
|
+
return None
|
|
169
|
+
if response.text:
|
|
170
|
+
return response.json()
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
def put(
|
|
174
|
+
self,
|
|
175
|
+
endpoint: str,
|
|
176
|
+
json: Optional[Dict[str, Any]] = None,
|
|
177
|
+
) -> Any:
|
|
178
|
+
"""Make PUT request to API.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
endpoint: API endpoint path
|
|
182
|
+
json: JSON body data
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
JSON response data
|
|
186
|
+
"""
|
|
187
|
+
url = f"{self.api_base}{endpoint}"
|
|
188
|
+
response = self._client.put(url, headers=self._headers(), json=json)
|
|
189
|
+
response.raise_for_status()
|
|
190
|
+
if response.status_code == 204 or not response.text:
|
|
191
|
+
return None
|
|
192
|
+
return response.json()
|
|
193
|
+
|
|
194
|
+
def patch(
|
|
195
|
+
self,
|
|
196
|
+
endpoint: str,
|
|
197
|
+
json: Optional[Dict[str, Any]] = None,
|
|
198
|
+
) -> Any:
|
|
199
|
+
"""Make PATCH request to API.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
endpoint: API endpoint path
|
|
203
|
+
json: JSON body data
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
JSON response data
|
|
207
|
+
"""
|
|
208
|
+
url = f"{self.api_base}{endpoint}"
|
|
209
|
+
response = self._client.patch(url, headers=self._headers(), json=json)
|
|
210
|
+
response.raise_for_status()
|
|
211
|
+
if response.status_code == 204 or not response.text:
|
|
212
|
+
return None
|
|
213
|
+
return response.json()
|
|
214
|
+
|
|
215
|
+
def delete(self, endpoint: str) -> None:
|
|
216
|
+
"""Make DELETE request to API.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
endpoint: API endpoint path
|
|
220
|
+
"""
|
|
221
|
+
url = f"{self.api_base}{endpoint}"
|
|
222
|
+
response = self._client.delete(url, headers=self._headers())
|
|
223
|
+
response.raise_for_status()
|
|
224
|
+
|
|
225
|
+
def close(self) -> None:
|
|
226
|
+
"""Close the HTTP client."""
|
|
227
|
+
self._client.close()
|
|
228
|
+
|
|
229
|
+
def __enter__(self) -> "EPMWClient":
|
|
230
|
+
return self
|
|
231
|
+
|
|
232
|
+
def __exit__(self, *args) -> None:
|
|
233
|
+
self.close()
|
|
234
|
+
|
|
235
|
+
# Convenience methods for common resources
|
|
236
|
+
|
|
237
|
+
def list_computers(
|
|
238
|
+
self,
|
|
239
|
+
filter_str: Optional[str] = None,
|
|
240
|
+
sort_by: Optional[str] = None,
|
|
241
|
+
sort_order: Optional[str] = None,
|
|
242
|
+
) -> List[Dict[str, Any]]:
|
|
243
|
+
"""List all computers.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
filter_str: OData filter string
|
|
247
|
+
sort_by: Field to sort by
|
|
248
|
+
sort_order: 'asc' or 'desc'
|
|
249
|
+
"""
|
|
250
|
+
params = {}
|
|
251
|
+
if filter_str:
|
|
252
|
+
params["filter"] = filter_str
|
|
253
|
+
if sort_by:
|
|
254
|
+
params["sortBy"] = sort_by
|
|
255
|
+
if sort_order:
|
|
256
|
+
params["sortOrder"] = sort_order
|
|
257
|
+
return self.get_paginated("/Computers", params)
|
|
258
|
+
|
|
259
|
+
def get_computer(self, computer_id: str) -> Dict[str, Any]:
|
|
260
|
+
"""Get a specific computer."""
|
|
261
|
+
return self.get(f"/Computers/{computer_id}")
|
|
262
|
+
|
|
263
|
+
def delete_computer(self, computer_id: str) -> None:
|
|
264
|
+
"""Delete a computer."""
|
|
265
|
+
self.delete(f"/Computers/{computer_id}")
|
|
266
|
+
|
|
267
|
+
def archive_computer(self, computer_id: str) -> None:
|
|
268
|
+
"""Archive a computer."""
|
|
269
|
+
self.post(f"/Computers/{computer_id}/archive")
|
|
270
|
+
|
|
271
|
+
def unarchive_computer(self, computer_id: str) -> None:
|
|
272
|
+
"""Unarchive a computer."""
|
|
273
|
+
self.post(f"/Computers/{computer_id}/Unarchive")
|
|
274
|
+
|
|
275
|
+
def list_groups(self) -> List[Dict[str, Any]]:
|
|
276
|
+
"""List all groups."""
|
|
277
|
+
return self.get_paginated("/Groups")
|
|
278
|
+
|
|
279
|
+
def get_group(self, group_id: str) -> Dict[str, Any]:
|
|
280
|
+
"""Get a specific group."""
|
|
281
|
+
return self.get(f"/Groups/{group_id}")
|
|
282
|
+
|
|
283
|
+
def create_group(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
284
|
+
"""Create a new group."""
|
|
285
|
+
return self.post("/Groups", json=data)
|
|
286
|
+
|
|
287
|
+
def update_group(self, group_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
288
|
+
"""Update a group."""
|
|
289
|
+
return self.put(f"/Groups/{group_id}", json=data)
|
|
290
|
+
|
|
291
|
+
def delete_group(self, group_id: str) -> None:
|
|
292
|
+
"""Delete a group."""
|
|
293
|
+
self.delete(f"/Groups/{group_id}")
|
|
294
|
+
|
|
295
|
+
def assign_policy_to_group(
|
|
296
|
+
self, group_id: str, policy_revision_id: str
|
|
297
|
+
) -> None:
|
|
298
|
+
"""Assign a policy revision to a group."""
|
|
299
|
+
self.post(
|
|
300
|
+
f"/Groups/{group_id}/AssignPolicyRevision",
|
|
301
|
+
json={"policyRevisionId": policy_revision_id},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def assign_computers_to_group(
|
|
305
|
+
self, group_id: str, computer_ids: List[str]
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Assign computers to a group."""
|
|
308
|
+
self.post(
|
|
309
|
+
f"/Groups/{group_id}/AssignComputers",
|
|
310
|
+
json={"computerIds": computer_ids},
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
def list_policies(self) -> List[Dict[str, Any]]:
|
|
314
|
+
"""List all policies."""
|
|
315
|
+
return self.get_paginated("/Policies")
|
|
316
|
+
|
|
317
|
+
def get_policy(self, policy_id: str) -> Dict[str, Any]:
|
|
318
|
+
"""Get a specific policy."""
|
|
319
|
+
return self.get(f"/Policies/{policy_id}")
|
|
320
|
+
|
|
321
|
+
def download_policy(self, policy_id: str) -> str:
|
|
322
|
+
"""Download policy content (returns XML).
|
|
323
|
+
|
|
324
|
+
Args:
|
|
325
|
+
policy_id: Policy ID (UUID)
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Policy content as XML string
|
|
329
|
+
"""
|
|
330
|
+
url = f"{self.api_base}/Policies/{policy_id}/Content"
|
|
331
|
+
response = self._client.get(url, headers=self._headers())
|
|
332
|
+
response.raise_for_status()
|
|
333
|
+
return response.text
|
|
334
|
+
|
|
335
|
+
def create_policy(
|
|
336
|
+
self, name: str, description: str = "", policy_file: str = ""
|
|
337
|
+
) -> str:
|
|
338
|
+
"""Create a new policy.
|
|
339
|
+
|
|
340
|
+
The API requires multipart form data with a file upload.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
name: Policy name
|
|
344
|
+
description: Optional description
|
|
345
|
+
policy_file: Policy XML content (required by API)
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Created policy ID (UUID string)
|
|
349
|
+
"""
|
|
350
|
+
url = f"{self.api_base}/Policies"
|
|
351
|
+
headers = self._headers()
|
|
352
|
+
# Remove Content-Type to let httpx set it for multipart
|
|
353
|
+
del headers["Content-Type"]
|
|
354
|
+
|
|
355
|
+
data = {"Name": name}
|
|
356
|
+
if description:
|
|
357
|
+
data["Description"] = description
|
|
358
|
+
|
|
359
|
+
files = {"PolicyFile": ("policy.xml", policy_file, "application/xml")}
|
|
360
|
+
|
|
361
|
+
response = self._client.post(url, headers=headers, data=data, files=files)
|
|
362
|
+
response.raise_for_status()
|
|
363
|
+
# API returns just the policy ID as a string
|
|
364
|
+
return response.text.strip('"')
|
|
365
|
+
|
|
366
|
+
def delete_policy(self, policy_id: str) -> None:
|
|
367
|
+
"""Delete a policy.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
policy_id: Policy ID (UUID)
|
|
371
|
+
"""
|
|
372
|
+
self.delete(f"/Policies/{policy_id}")
|
|
373
|
+
|
|
374
|
+
def update_policy(self, policy_id: str, name: str, description: str = "") -> Dict[str, Any]:
|
|
375
|
+
"""Update a policy.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
policy_id: Policy ID (UUID)
|
|
379
|
+
name: New policy name
|
|
380
|
+
description: New description
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Updated policy object
|
|
384
|
+
"""
|
|
385
|
+
data = {"name": name}
|
|
386
|
+
if description:
|
|
387
|
+
data["description"] = description
|
|
388
|
+
return self.put(f"/Policies/{policy_id}", json=data)
|
|
389
|
+
|
|
390
|
+
def revert_policy(self, policy_id: str) -> None:
|
|
391
|
+
"""Revert a policy to discard draft changes.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
policy_id: Policy ID (UUID)
|
|
395
|
+
"""
|
|
396
|
+
self.patch(f"/Policies/{policy_id}")
|
|
397
|
+
|
|
398
|
+
def list_policy_revisions(self, policy_id: str) -> List[Dict[str, Any]]:
|
|
399
|
+
"""List all revisions of a policy.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
policy_id: Policy ID (UUID)
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of revision objects
|
|
406
|
+
"""
|
|
407
|
+
return self.get_paginated(f"/Policies/{policy_id}/revisions")
|
|
408
|
+
|
|
409
|
+
def get_policy_revision(self, policy_id: str, revision_id: str) -> Any:
|
|
410
|
+
"""Download a specific policy revision.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
policy_id: Policy ID (UUID)
|
|
414
|
+
revision_id: Revision ID (UUID)
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Policy revision content
|
|
418
|
+
"""
|
|
419
|
+
return self.get(f"/Policies/{policy_id}/revisions/{revision_id}")
|
|
420
|
+
|
|
421
|
+
def upload_policy_revision(
|
|
422
|
+
self,
|
|
423
|
+
policy_id: str,
|
|
424
|
+
content: Dict[str, Any],
|
|
425
|
+
comment: str = "",
|
|
426
|
+
) -> Dict[str, Any]:
|
|
427
|
+
"""Upload a new policy revision.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
policy_id: Policy ID (UUID)
|
|
431
|
+
content: Policy content (JSON structure)
|
|
432
|
+
comment: Optional revision comment
|
|
433
|
+
|
|
434
|
+
Returns:
|
|
435
|
+
Upload result with revision info
|
|
436
|
+
"""
|
|
437
|
+
data = {"content": content}
|
|
438
|
+
if comment:
|
|
439
|
+
data["comment"] = comment
|
|
440
|
+
return self.post(f"/Policies/{policy_id}/revisions", json=data)
|
|
441
|
+
|
|
442
|
+
def get_policy_groups(self, policy_id: str) -> List[Dict[str, Any]]:
|
|
443
|
+
"""Get groups assigned to a policy.
|
|
444
|
+
|
|
445
|
+
Note: The API doesn't have a direct endpoint for this, so we get all groups
|
|
446
|
+
and filter by policyId.
|
|
447
|
+
|
|
448
|
+
Args:
|
|
449
|
+
policy_id: Policy ID (UUID)
|
|
450
|
+
|
|
451
|
+
Returns:
|
|
452
|
+
List of group objects that have this policy assigned
|
|
453
|
+
"""
|
|
454
|
+
all_groups = self.list_groups()
|
|
455
|
+
return [g for g in all_groups if g.get("policyId") == policy_id]
|
|
456
|
+
|
|
457
|
+
# Policy Editor methods (different base path: /policyeditor/v3)
|
|
458
|
+
|
|
459
|
+
def _policy_editor_get(
|
|
460
|
+
self,
|
|
461
|
+
endpoint: str,
|
|
462
|
+
params: Optional[Dict[str, Any]] = None,
|
|
463
|
+
) -> Any:
|
|
464
|
+
"""Make GET request to policy editor API."""
|
|
465
|
+
url = f"{self.policy_editor_base}{endpoint}"
|
|
466
|
+
response = self._client.get(url, headers=self._headers(), params=params)
|
|
467
|
+
response.raise_for_status()
|
|
468
|
+
return response.json()
|
|
469
|
+
|
|
470
|
+
def _policy_editor_post(
|
|
471
|
+
self,
|
|
472
|
+
endpoint: str,
|
|
473
|
+
json: Optional[Dict[str, Any]] = None,
|
|
474
|
+
) -> Any:
|
|
475
|
+
"""Make POST request to policy editor API."""
|
|
476
|
+
url = f"{self.policy_editor_base}{endpoint}"
|
|
477
|
+
response = self._client.post(url, headers=self._headers(), json=json)
|
|
478
|
+
response.raise_for_status()
|
|
479
|
+
if response.status_code == 204 or not response.text:
|
|
480
|
+
return None
|
|
481
|
+
return response.json()
|
|
482
|
+
|
|
483
|
+
def _policy_editor_put(
|
|
484
|
+
self,
|
|
485
|
+
endpoint: str,
|
|
486
|
+
json: Optional[Dict[str, Any]] = None,
|
|
487
|
+
) -> Any:
|
|
488
|
+
"""Make PUT request to policy editor API."""
|
|
489
|
+
url = f"{self.policy_editor_base}{endpoint}"
|
|
490
|
+
response = self._client.put(url, headers=self._headers(), json=json)
|
|
491
|
+
response.raise_for_status()
|
|
492
|
+
if response.status_code == 204 or not response.text:
|
|
493
|
+
return None
|
|
494
|
+
return response.json()
|
|
495
|
+
|
|
496
|
+
def _policy_editor_delete(self, endpoint: str) -> None:
|
|
497
|
+
"""Make DELETE request to policy editor API."""
|
|
498
|
+
url = f"{self.policy_editor_base}{endpoint}"
|
|
499
|
+
response = self._client.delete(url, headers=self._headers())
|
|
500
|
+
response.raise_for_status()
|
|
501
|
+
|
|
502
|
+
def list_application_groups(self, policy_id: str) -> List[Dict[str, Any]]:
|
|
503
|
+
"""List application groups in a policy.
|
|
504
|
+
|
|
505
|
+
Args:
|
|
506
|
+
policy_id: Policy ID (UUID)
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
List of application group objects
|
|
510
|
+
"""
|
|
511
|
+
result = self._policy_editor_get(
|
|
512
|
+
f"/policy/{policy_id}/windows/applicationgroups"
|
|
513
|
+
)
|
|
514
|
+
if isinstance(result, list):
|
|
515
|
+
return result
|
|
516
|
+
return result.get("data", result.get("applicationGroups", []))
|
|
517
|
+
|
|
518
|
+
def get_application_group(
|
|
519
|
+
self, policy_id: str, app_group_id: str
|
|
520
|
+
) -> Dict[str, Any]:
|
|
521
|
+
"""Get a specific application group.
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
policy_id: Policy ID (UUID)
|
|
525
|
+
app_group_id: Application group ID (UUID)
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Application group object
|
|
529
|
+
"""
|
|
530
|
+
return self._policy_editor_get(
|
|
531
|
+
f"/policy/{policy_id}/windows/applicationgroups/{app_group_id}"
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
def create_application_group(
|
|
535
|
+
self,
|
|
536
|
+
policy_id: str,
|
|
537
|
+
name: str,
|
|
538
|
+
description: str = "",
|
|
539
|
+
hidden: bool = False,
|
|
540
|
+
) -> Dict[str, Any]:
|
|
541
|
+
"""Create an application group in a policy.
|
|
542
|
+
|
|
543
|
+
Args:
|
|
544
|
+
policy_id: Policy ID (UUID)
|
|
545
|
+
name: Application group name
|
|
546
|
+
description: Optional description
|
|
547
|
+
hidden: Whether the group is hidden
|
|
548
|
+
|
|
549
|
+
Returns:
|
|
550
|
+
Created application group object
|
|
551
|
+
"""
|
|
552
|
+
data = {
|
|
553
|
+
"name": name,
|
|
554
|
+
"description": description,
|
|
555
|
+
"hidden": hidden,
|
|
556
|
+
}
|
|
557
|
+
return self._policy_editor_post(
|
|
558
|
+
f"/policy/{policy_id}/windows/applicationgroups",
|
|
559
|
+
json=data,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
def update_application_group(
|
|
563
|
+
self,
|
|
564
|
+
policy_id: str,
|
|
565
|
+
app_group_id: str,
|
|
566
|
+
name: str,
|
|
567
|
+
description: str = "",
|
|
568
|
+
hidden: bool = False,
|
|
569
|
+
) -> Dict[str, Any]:
|
|
570
|
+
"""Update an application group.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
policy_id: Policy ID (UUID)
|
|
574
|
+
app_group_id: Application group ID (UUID)
|
|
575
|
+
name: New name
|
|
576
|
+
description: New description
|
|
577
|
+
hidden: Whether the group is hidden
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
Updated application group object
|
|
581
|
+
"""
|
|
582
|
+
data = {
|
|
583
|
+
"name": name,
|
|
584
|
+
"description": description,
|
|
585
|
+
"hidden": hidden,
|
|
586
|
+
}
|
|
587
|
+
return self._policy_editor_put(
|
|
588
|
+
f"/policy/{policy_id}/windows/applicationgroups/{app_group_id}",
|
|
589
|
+
json=data,
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
def delete_application_group(self, policy_id: str, app_group_id: str) -> None:
|
|
593
|
+
"""Delete an application group.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
policy_id: Policy ID (UUID)
|
|
597
|
+
app_group_id: Application group ID (UUID)
|
|
598
|
+
"""
|
|
599
|
+
self._policy_editor_delete(
|
|
600
|
+
f"/policy/{policy_id}/windows/applicationgroups/{app_group_id}"
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
def list_roles(self) -> List[Dict[str, Any]]:
|
|
604
|
+
"""List all roles."""
|
|
605
|
+
return self.get_paginated("/Roles")
|
|
606
|
+
|
|
607
|
+
def get_role(self, role_id: str) -> Dict[str, Any]:
|
|
608
|
+
"""Get a specific role."""
|
|
609
|
+
return self.get(f"/Roles/{role_id}")
|
|
610
|
+
|
|
611
|
+
def list_users(self) -> List[Dict[str, Any]]:
|
|
612
|
+
"""List all users."""
|
|
613
|
+
return self.get_paginated("/Users")
|
|
614
|
+
|
|
615
|
+
def get_user(self, user_id: str) -> Dict[str, Any]:
|
|
616
|
+
"""Get a specific user."""
|
|
617
|
+
return self.get(f"/Users/{user_id}")
|
|
618
|
+
|
|
619
|
+
def create_user(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
620
|
+
"""Create a new user."""
|
|
621
|
+
return self.post("/Users", json=data)
|
|
622
|
+
|
|
623
|
+
def update_user(self, user_id: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
624
|
+
"""Update a user."""
|
|
625
|
+
return self.put(f"/Users/{user_id}", json=data)
|
|
626
|
+
|
|
627
|
+
def enable_user(self, user_id: str) -> None:
|
|
628
|
+
"""Enable a user."""
|
|
629
|
+
self.patch(f"/Users/{user_id}/Enable")
|
|
630
|
+
|
|
631
|
+
def disable_user(self, user_id: str) -> None:
|
|
632
|
+
"""Disable a user."""
|
|
633
|
+
self.patch(f"/Users/{user_id}/Disable")
|
|
634
|
+
|
|
635
|
+
def assign_roles_to_user(self, user_id: str, role_ids: List[str]) -> None:
|
|
636
|
+
"""Assign roles to a user."""
|
|
637
|
+
self.post(f"/Users/{user_id}/AssignRoles", json={"roleIds": role_ids})
|
|
638
|
+
|
|
639
|
+
def list_admin_requests(self) -> List[Dict[str, Any]]:
|
|
640
|
+
"""List admin access requests."""
|
|
641
|
+
return self.get_paginated("/AdminAccessRequest")
|
|
642
|
+
|
|
643
|
+
def get_admin_request(self, request_id: str) -> Dict[str, Any]:
|
|
644
|
+
"""Get a specific admin access request."""
|
|
645
|
+
return self.get(f"/AdminAccessRequest/{request_id}")
|
|
646
|
+
|
|
647
|
+
def create_admin_request(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
648
|
+
"""Create an admin access request."""
|
|
649
|
+
return self.post("/AdminAccessRequest", json=data)
|
|
650
|
+
|
|
651
|
+
def approve_admin_request(
|
|
652
|
+
self,
|
|
653
|
+
request_id: str,
|
|
654
|
+
message: str,
|
|
655
|
+
duration: int = 1800,
|
|
656
|
+
performed_by: str = "api-admin",
|
|
657
|
+
) -> Dict[str, Any]:
|
|
658
|
+
"""Approve an admin access request.
|
|
659
|
+
|
|
660
|
+
Args:
|
|
661
|
+
request_id: Request UUID
|
|
662
|
+
message: Approval message (required by API)
|
|
663
|
+
duration: Approval duration in seconds (default: 1800 = 30 min)
|
|
664
|
+
performed_by: Username performing the decision
|
|
665
|
+
|
|
666
|
+
Returns:
|
|
667
|
+
Response with requestId
|
|
668
|
+
"""
|
|
669
|
+
return self.post(
|
|
670
|
+
"/AdminAccessRequest/approval",
|
|
671
|
+
json={
|
|
672
|
+
"RequestId": request_id,
|
|
673
|
+
"Decision": "Approved",
|
|
674
|
+
"DecisionPerformedByUser": performed_by,
|
|
675
|
+
"Duration": str(duration),
|
|
676
|
+
"Message": message,
|
|
677
|
+
},
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
def deny_admin_request(
|
|
681
|
+
self,
|
|
682
|
+
request_id: str,
|
|
683
|
+
message: str,
|
|
684
|
+
performed_by: str = "api-admin",
|
|
685
|
+
) -> Dict[str, Any]:
|
|
686
|
+
"""Deny an admin access request.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
request_id: Request UUID
|
|
690
|
+
message: Denial message (required by API)
|
|
691
|
+
performed_by: Username performing the decision
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
Response with requestId
|
|
695
|
+
"""
|
|
696
|
+
return self.post(
|
|
697
|
+
"/AdminAccessRequest/approval",
|
|
698
|
+
json={
|
|
699
|
+
"RequestId": request_id,
|
|
700
|
+
"Decision": "Denied",
|
|
701
|
+
"DecisionPerformedByUser": performed_by,
|
|
702
|
+
"Message": message,
|
|
703
|
+
},
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
# Audit methods
|
|
707
|
+
|
|
708
|
+
def list_activity_audits(self) -> List[Dict[str, Any]]:
|
|
709
|
+
"""List activity audit records."""
|
|
710
|
+
return self.get_paginated("/ActivityAudits")
|
|
711
|
+
|
|
712
|
+
def get_activity_audit(self, audit_id: int) -> Dict[str, Any]:
|
|
713
|
+
"""Get a specific activity audit record."""
|
|
714
|
+
return self.get(f"/ActivityAudits/{audit_id}")
|
|
715
|
+
|
|
716
|
+
def list_authorization_requests(self) -> List[Dict[str, Any]]:
|
|
717
|
+
"""List authorization requests (JIT app requests)."""
|
|
718
|
+
return self.get_paginated("/AuthorizationRequest")
|
|
719
|
+
|
|
720
|
+
def get_authorization_request(self, request_id: str) -> Dict[str, Any]:
|
|
721
|
+
"""Get a specific authorization request."""
|
|
722
|
+
return self.get(f"/AuthorizationRequest/{request_id}")
|
|
723
|
+
|
|
724
|
+
def list_authorization_request_audits(self) -> List[Dict[str, Any]]:
|
|
725
|
+
"""List authorization request audit records."""
|
|
726
|
+
return self.get_paginated("/AuthorizationRequestAudits")
|
|
727
|
+
|
|
728
|
+
def get_authorization_request_audit(self, audit_id: str) -> Dict[str, Any]:
|
|
729
|
+
"""Get a specific authorization request audit record."""
|
|
730
|
+
return self.get(f"/AuthorizationRequestAudits/{audit_id}")
|
|
731
|
+
|
|
732
|
+
# Task methods
|
|
733
|
+
|
|
734
|
+
def get_task(self, task_id: str) -> Dict[str, Any]:
|
|
735
|
+
"""Get task status."""
|
|
736
|
+
return self.get(f"/Tasks/{task_id}")
|
|
737
|
+
|
|
738
|
+
# Event methods
|
|
739
|
+
|
|
740
|
+
def list_events_from_date(
|
|
741
|
+
self,
|
|
742
|
+
start_date: str,
|
|
743
|
+
record_size: int = 1000,
|
|
744
|
+
) -> List[Dict[str, Any]]:
|
|
745
|
+
"""Get events from a start date.
|
|
746
|
+
|
|
747
|
+
Args:
|
|
748
|
+
start_date: Start date in ISO format (e.g., 2022-08-12T17:34:28.694Z)
|
|
749
|
+
record_size: Max records to return (1-1000, default 1000)
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
List of events
|
|
753
|
+
"""
|
|
754
|
+
params = {
|
|
755
|
+
"StartDate": start_date,
|
|
756
|
+
"RecordSize": min(max(record_size, 1), 1000),
|
|
757
|
+
}
|
|
758
|
+
response = self.get("/Events/FromStartDate", params=params)
|
|
759
|
+
# Response may be a list directly or wrapped
|
|
760
|
+
if isinstance(response, list):
|
|
761
|
+
return response
|
|
762
|
+
return response.get("data", response.get("events", []))
|
|
763
|
+
|
|
764
|
+
def search_events(
|
|
765
|
+
self,
|
|
766
|
+
start_date: str,
|
|
767
|
+
end_date: str,
|
|
768
|
+
computer_groups: Optional[List[str]] = None,
|
|
769
|
+
operating_system: Optional[str] = None,
|
|
770
|
+
event_actions: Optional[List[str]] = None,
|
|
771
|
+
event_codes: Optional[List[str]] = None,
|
|
772
|
+
event_types: Optional[List[str]] = None,
|
|
773
|
+
application_type: Optional[str] = None,
|
|
774
|
+
hostname: Optional[str] = None,
|
|
775
|
+
host_domain: Optional[str] = None,
|
|
776
|
+
username: Optional[str] = None,
|
|
777
|
+
user_domain: Optional[str] = None,
|
|
778
|
+
workstyle_name: Optional[str] = None,
|
|
779
|
+
application_group_name: Optional[str] = None,
|
|
780
|
+
on_demand_rule: Optional[bool] = None,
|
|
781
|
+
page_size: int = 100,
|
|
782
|
+
page_number: int = 1,
|
|
783
|
+
) -> Dict[str, Any]:
|
|
784
|
+
"""Search events with filters.
|
|
785
|
+
|
|
786
|
+
Args:
|
|
787
|
+
start_date: Start date in ISO format
|
|
788
|
+
end_date: End date in ISO format
|
|
789
|
+
computer_groups: List of group IDs (UUIDs)
|
|
790
|
+
operating_system: OS name filter
|
|
791
|
+
event_actions: List of event actions
|
|
792
|
+
event_codes: List of event codes
|
|
793
|
+
event_types: List of event types
|
|
794
|
+
application_type: Application type filter
|
|
795
|
+
hostname: Computer hostname filter
|
|
796
|
+
host_domain: Computer domain filter
|
|
797
|
+
username: User name filter
|
|
798
|
+
user_domain: User domain filter
|
|
799
|
+
workstyle_name: Policy workstyle filter
|
|
800
|
+
application_group_name: Policy app group filter
|
|
801
|
+
on_demand_rule: On-demand rule filter
|
|
802
|
+
page_size: Records per page (max 200)
|
|
803
|
+
page_number: Page number
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
Dict with events and pagination info
|
|
807
|
+
"""
|
|
808
|
+
params: Dict[str, Any] = {
|
|
809
|
+
"TimePeriod.StartDate": start_date,
|
|
810
|
+
"TimePeriod.EndDate": end_date,
|
|
811
|
+
"Pagination.PageSize": min(page_size, 200),
|
|
812
|
+
"Pagination.PageNumber": page_number,
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if computer_groups:
|
|
816
|
+
params["ComputerGroups"] = computer_groups
|
|
817
|
+
if operating_system:
|
|
818
|
+
params["OperatingSystem"] = operating_system
|
|
819
|
+
if event_actions:
|
|
820
|
+
params["Events.EventAction"] = event_actions
|
|
821
|
+
if event_codes:
|
|
822
|
+
params["Events.EventCode"] = event_codes
|
|
823
|
+
if event_types:
|
|
824
|
+
params["Events.EventType"] = event_types
|
|
825
|
+
if application_type:
|
|
826
|
+
params["Application.ApplicationType"] = application_type
|
|
827
|
+
if hostname:
|
|
828
|
+
params["Computers.HostName"] = hostname
|
|
829
|
+
if host_domain:
|
|
830
|
+
params["Computers.HostDomain"] = host_domain
|
|
831
|
+
if username:
|
|
832
|
+
params["Users.UserName"] = username
|
|
833
|
+
if user_domain:
|
|
834
|
+
params["Users.UserDomain"] = user_domain
|
|
835
|
+
if workstyle_name:
|
|
836
|
+
params["Policies.WorkstyleName"] = workstyle_name
|
|
837
|
+
if application_group_name:
|
|
838
|
+
params["Policies.ApplicationGroupName"] = application_group_name
|
|
839
|
+
if on_demand_rule is not None:
|
|
840
|
+
params["Policies.OnDemandRule"] = on_demand_rule
|
|
841
|
+
|
|
842
|
+
return self.get("/Events/search", params=params)
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def get_client() -> EPMWClient:
|
|
846
|
+
"""Get an EPMW client with configuration from environment."""
|
|
847
|
+
config = load_epmw_config()
|
|
848
|
+
return EPMWClient(config)
|