binalyze-air-sdk 1.0.1__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.
- binalyze_air/__init__.py +77 -0
- binalyze_air/apis/__init__.py +27 -0
- binalyze_air/apis/authentication.py +27 -0
- binalyze_air/apis/auto_asset_tags.py +75 -0
- binalyze_air/apis/endpoints.py +22 -0
- binalyze_air/apis/event_subscription.py +97 -0
- binalyze_air/apis/evidence.py +53 -0
- binalyze_air/apis/evidences.py +216 -0
- binalyze_air/apis/interact.py +36 -0
- binalyze_air/apis/params.py +40 -0
- binalyze_air/apis/settings.py +27 -0
- binalyze_air/apis/user_management.py +74 -0
- binalyze_air/apis/users.py +68 -0
- binalyze_air/apis/webhooks.py +231 -0
- binalyze_air/base.py +133 -0
- binalyze_air/client.py +1338 -0
- binalyze_air/commands/__init__.py +146 -0
- binalyze_air/commands/acquisitions.py +387 -0
- binalyze_air/commands/assets.py +363 -0
- binalyze_air/commands/authentication.py +37 -0
- binalyze_air/commands/auto_asset_tags.py +231 -0
- binalyze_air/commands/baseline.py +396 -0
- binalyze_air/commands/cases.py +603 -0
- binalyze_air/commands/event_subscription.py +102 -0
- binalyze_air/commands/evidences.py +988 -0
- binalyze_air/commands/interact.py +58 -0
- binalyze_air/commands/organizations.py +221 -0
- binalyze_air/commands/policies.py +203 -0
- binalyze_air/commands/settings.py +29 -0
- binalyze_air/commands/tasks.py +56 -0
- binalyze_air/commands/triage.py +360 -0
- binalyze_air/commands/user_management.py +126 -0
- binalyze_air/commands/users.py +101 -0
- binalyze_air/config.py +245 -0
- binalyze_air/exceptions.py +50 -0
- binalyze_air/http_client.py +306 -0
- binalyze_air/models/__init__.py +285 -0
- binalyze_air/models/acquisitions.py +251 -0
- binalyze_air/models/assets.py +439 -0
- binalyze_air/models/audit.py +273 -0
- binalyze_air/models/authentication.py +70 -0
- binalyze_air/models/auto_asset_tags.py +117 -0
- binalyze_air/models/baseline.py +232 -0
- binalyze_air/models/cases.py +276 -0
- binalyze_air/models/endpoints.py +76 -0
- binalyze_air/models/event_subscription.py +172 -0
- binalyze_air/models/evidence.py +66 -0
- binalyze_air/models/evidences.py +349 -0
- binalyze_air/models/interact.py +136 -0
- binalyze_air/models/organizations.py +294 -0
- binalyze_air/models/params.py +128 -0
- binalyze_air/models/policies.py +250 -0
- binalyze_air/models/settings.py +84 -0
- binalyze_air/models/tasks.py +149 -0
- binalyze_air/models/triage.py +143 -0
- binalyze_air/models/user_management.py +97 -0
- binalyze_air/models/users.py +82 -0
- binalyze_air/queries/__init__.py +134 -0
- binalyze_air/queries/acquisitions.py +156 -0
- binalyze_air/queries/assets.py +105 -0
- binalyze_air/queries/audit.py +417 -0
- binalyze_air/queries/authentication.py +56 -0
- binalyze_air/queries/auto_asset_tags.py +60 -0
- binalyze_air/queries/baseline.py +185 -0
- binalyze_air/queries/cases.py +293 -0
- binalyze_air/queries/endpoints.py +25 -0
- binalyze_air/queries/event_subscription.py +55 -0
- binalyze_air/queries/evidence.py +140 -0
- binalyze_air/queries/evidences.py +280 -0
- binalyze_air/queries/interact.py +28 -0
- binalyze_air/queries/organizations.py +223 -0
- binalyze_air/queries/params.py +115 -0
- binalyze_air/queries/policies.py +150 -0
- binalyze_air/queries/settings.py +20 -0
- binalyze_air/queries/tasks.py +82 -0
- binalyze_air/queries/triage.py +231 -0
- binalyze_air/queries/user_management.py +83 -0
- binalyze_air/queries/users.py +69 -0
- binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
- binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
- binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
- binalyze_air_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,363 @@
|
|
1
|
+
"""
|
2
|
+
Asset-related commands for the Binalyze AIR SDK.
|
3
|
+
Fixed to match API documentation exactly.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import List, Union, Optional, Dict, Any
|
7
|
+
|
8
|
+
from ..base import Command
|
9
|
+
from ..models.assets import AssetFilter
|
10
|
+
from ..http_client import HTTPClient
|
11
|
+
|
12
|
+
|
13
|
+
class RebootAssetsCommand(Command[Dict[str, Any]]):
|
14
|
+
"""Command to reboot assets by filter - FIXED to match API documentation."""
|
15
|
+
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
http_client: HTTPClient,
|
19
|
+
asset_filter: AssetFilter
|
20
|
+
):
|
21
|
+
self.http_client = http_client
|
22
|
+
self.asset_filter = asset_filter
|
23
|
+
|
24
|
+
def execute(self) -> Dict[str, Any]:
|
25
|
+
"""Execute the reboot command with correct payload structure."""
|
26
|
+
# Use the correct payload structure as per API documentation
|
27
|
+
payload = {
|
28
|
+
"filter": self.asset_filter.to_filter_dict()
|
29
|
+
}
|
30
|
+
|
31
|
+
# FIXED: Correct endpoint URL (confirmed from API docs)
|
32
|
+
return self.http_client.post("assets/tasks/reboot", json_data=payload)
|
33
|
+
|
34
|
+
|
35
|
+
class ShutdownAssetsCommand(Command[Dict[str, Any]]):
|
36
|
+
"""Command to shutdown assets by filter - FIXED to match API documentation."""
|
37
|
+
|
38
|
+
def __init__(
|
39
|
+
self,
|
40
|
+
http_client: HTTPClient,
|
41
|
+
asset_filter: AssetFilter
|
42
|
+
):
|
43
|
+
self.http_client = http_client
|
44
|
+
self.asset_filter = asset_filter
|
45
|
+
|
46
|
+
def execute(self) -> Dict[str, Any]:
|
47
|
+
"""Execute the shutdown command with correct payload structure."""
|
48
|
+
payload = {
|
49
|
+
"filter": self.asset_filter.to_filter_dict()
|
50
|
+
}
|
51
|
+
|
52
|
+
# FIXED: Correct endpoint URL (following same pattern as reboot)
|
53
|
+
return self.http_client.post("assets/tasks/shutdown", json_data=payload)
|
54
|
+
|
55
|
+
|
56
|
+
class IsolateAssetsCommand(Command[Dict[str, Any]]):
|
57
|
+
"""Command to isolate assets by filter - FIXED to match API documentation."""
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
http_client: HTTPClient,
|
62
|
+
asset_filter: AssetFilter,
|
63
|
+
isolation_settings: Optional[Dict[str, Any]] = None
|
64
|
+
):
|
65
|
+
self.http_client = http_client
|
66
|
+
self.asset_filter = asset_filter
|
67
|
+
self.isolation_settings = isolation_settings or {}
|
68
|
+
|
69
|
+
def execute(self) -> Dict[str, Any]:
|
70
|
+
"""Execute the isolation command with correct payload structure."""
|
71
|
+
payload = {
|
72
|
+
"enabled": True, # Required field for isolation
|
73
|
+
"filter": self.asset_filter.to_filter_dict()
|
74
|
+
}
|
75
|
+
|
76
|
+
# Add isolation settings if provided
|
77
|
+
if self.isolation_settings:
|
78
|
+
payload.update(self.isolation_settings)
|
79
|
+
|
80
|
+
# FIXED: Correct endpoint URL and payload
|
81
|
+
return self.http_client.post("assets/tasks/isolation", json_data=payload)
|
82
|
+
|
83
|
+
|
84
|
+
class UnisolateAssetsCommand(Command[Dict[str, Any]]):
|
85
|
+
"""Command to unisolate (remove isolation from) assets - FIXED to match API documentation."""
|
86
|
+
|
87
|
+
def __init__(
|
88
|
+
self,
|
89
|
+
http_client: HTTPClient,
|
90
|
+
asset_filter: AssetFilter
|
91
|
+
):
|
92
|
+
self.http_client = http_client
|
93
|
+
self.asset_filter = asset_filter
|
94
|
+
|
95
|
+
def execute(self) -> Dict[str, Any]:
|
96
|
+
"""Execute the unisolate command with correct payload structure."""
|
97
|
+
payload = {
|
98
|
+
"enabled": False, # Disable isolation for unisolate
|
99
|
+
"filter": self.asset_filter.to_filter_dict()
|
100
|
+
}
|
101
|
+
|
102
|
+
# FIXED: Correct endpoint URL and payload
|
103
|
+
return self.http_client.post("assets/tasks/isolation", json_data=payload)
|
104
|
+
|
105
|
+
|
106
|
+
class LogRetrievalCommand(Command[Dict[str, Any]]):
|
107
|
+
"""Command to retrieve logs from assets - FIXED endpoint URL and payload structure."""
|
108
|
+
|
109
|
+
def __init__(
|
110
|
+
self,
|
111
|
+
http_client: HTTPClient,
|
112
|
+
asset_filter: AssetFilter,
|
113
|
+
log_settings: Optional[Dict[str, Any]] = None
|
114
|
+
):
|
115
|
+
self.http_client = http_client
|
116
|
+
self.asset_filter = asset_filter
|
117
|
+
self.log_settings = log_settings or {}
|
118
|
+
|
119
|
+
def execute(self) -> Dict[str, Any]:
|
120
|
+
"""Execute the log retrieval command with correct endpoint and payload."""
|
121
|
+
payload = {
|
122
|
+
"filter": self.asset_filter.to_filter_dict()
|
123
|
+
}
|
124
|
+
|
125
|
+
# Add log retrieval settings if provided
|
126
|
+
if self.log_settings:
|
127
|
+
payload.update(self.log_settings)
|
128
|
+
|
129
|
+
# FIXED: Correct endpoint URL to match API specification
|
130
|
+
return self.http_client.post("assets/tasks/retrieve-logs", json_data=payload)
|
131
|
+
|
132
|
+
|
133
|
+
class VersionUpdateCommand(Command[Dict[str, Any]]):
|
134
|
+
"""Command to update version on assets - FIXED to match API documentation."""
|
135
|
+
|
136
|
+
def __init__(
|
137
|
+
self,
|
138
|
+
http_client: HTTPClient,
|
139
|
+
asset_filter: AssetFilter,
|
140
|
+
update_settings: Optional[Dict[str, Any]] = None
|
141
|
+
):
|
142
|
+
self.http_client = http_client
|
143
|
+
self.asset_filter = asset_filter
|
144
|
+
self.update_settings = update_settings or {}
|
145
|
+
|
146
|
+
def execute(self) -> Dict[str, Any]:
|
147
|
+
"""Execute the version update command with correct payload structure."""
|
148
|
+
payload = {
|
149
|
+
"filter": self.asset_filter.to_filter_dict()
|
150
|
+
}
|
151
|
+
|
152
|
+
# Add version update settings if provided
|
153
|
+
if self.update_settings:
|
154
|
+
payload.update(self.update_settings)
|
155
|
+
|
156
|
+
# FIXED: Correct endpoint URL (following tasks pattern)
|
157
|
+
return self.http_client.post("assets/tasks/version-update", json_data=payload)
|
158
|
+
|
159
|
+
|
160
|
+
class UninstallAssetsCommand(Command[Dict[str, Any]]):
|
161
|
+
"""Command to uninstall assets without purging data - FIXED endpoint URL and HTTP method."""
|
162
|
+
|
163
|
+
def __init__(
|
164
|
+
self,
|
165
|
+
http_client: HTTPClient,
|
166
|
+
asset_filter: AssetFilter
|
167
|
+
):
|
168
|
+
self.http_client = http_client
|
169
|
+
self.asset_filter = asset_filter
|
170
|
+
|
171
|
+
def execute(self) -> Dict[str, Any]:
|
172
|
+
"""Execute the uninstall command with correct endpoint, HTTP method and payload structure."""
|
173
|
+
payload = {
|
174
|
+
"filter": self.asset_filter.to_filter_dict()
|
175
|
+
}
|
176
|
+
|
177
|
+
# FIXED: Correct endpoint URL and HTTP method (DELETE, not POST)
|
178
|
+
return self.http_client.delete("assets/uninstall-without-purge", json_data=payload)
|
179
|
+
|
180
|
+
|
181
|
+
class PurgeAndUninstallAssetsCommand(Command[Dict[str, Any]]):
|
182
|
+
"""Command to purge and uninstall assets - FIXED endpoint URL and HTTP method."""
|
183
|
+
|
184
|
+
def __init__(
|
185
|
+
self,
|
186
|
+
http_client: HTTPClient,
|
187
|
+
asset_filter: AssetFilter
|
188
|
+
):
|
189
|
+
self.http_client = http_client
|
190
|
+
self.asset_filter = asset_filter
|
191
|
+
|
192
|
+
def execute(self) -> Dict[str, Any]:
|
193
|
+
"""Execute the purge and uninstall command with correct endpoint, HTTP method and payload."""
|
194
|
+
payload = {
|
195
|
+
"filter": self.asset_filter.to_filter_dict()
|
196
|
+
}
|
197
|
+
|
198
|
+
# FIXED: Correct endpoint URL and HTTP method (DELETE, not POST)
|
199
|
+
return self.http_client.delete("assets/purge-and-uninstall", json_data=payload)
|
200
|
+
|
201
|
+
|
202
|
+
class AddTagsToAssetsCommand(Command[Dict[str, Any]]):
|
203
|
+
"""Command to add tags to assets - FIXED endpoint URL and payload structure."""
|
204
|
+
|
205
|
+
def __init__(
|
206
|
+
self,
|
207
|
+
http_client: HTTPClient,
|
208
|
+
asset_filter: AssetFilter,
|
209
|
+
tags: List[str]
|
210
|
+
):
|
211
|
+
self.http_client = http_client
|
212
|
+
self.asset_filter = asset_filter
|
213
|
+
self.tags = tags
|
214
|
+
|
215
|
+
def execute(self) -> Dict[str, Any]:
|
216
|
+
"""Execute the add tags command with correct endpoint and payload structure."""
|
217
|
+
payload = {
|
218
|
+
"filter": self.asset_filter.to_filter_dict(),
|
219
|
+
"tags": self.tags
|
220
|
+
}
|
221
|
+
|
222
|
+
# FIXED: Correct endpoint URL (from API documentation)
|
223
|
+
return self.http_client.post("assets/tags", json_data=payload)
|
224
|
+
|
225
|
+
|
226
|
+
class RemoveTagsFromAssetsCommand(Command[Dict[str, Any]]):
|
227
|
+
"""Command to remove tags from assets - FIXED endpoint URL and HTTP method."""
|
228
|
+
|
229
|
+
def __init__(
|
230
|
+
self,
|
231
|
+
http_client: HTTPClient,
|
232
|
+
asset_filter: AssetFilter,
|
233
|
+
tags: List[str]
|
234
|
+
):
|
235
|
+
self.http_client = http_client
|
236
|
+
self.asset_filter = asset_filter
|
237
|
+
self.tags = tags
|
238
|
+
|
239
|
+
def execute(self) -> Dict[str, Any]:
|
240
|
+
"""Execute the remove tags command with correct endpoint, HTTP method and payload structure."""
|
241
|
+
payload = {
|
242
|
+
"filter": self.asset_filter.to_filter_dict(),
|
243
|
+
"tags": self.tags
|
244
|
+
}
|
245
|
+
|
246
|
+
# FIXED: Correct endpoint URL and HTTP method (DELETE, not POST)
|
247
|
+
return self.http_client.delete("assets/tags", json_data=payload)
|
248
|
+
|
249
|
+
|
250
|
+
# Convenience functions for backward compatibility
|
251
|
+
def create_asset_filter_from_endpoint_ids(
|
252
|
+
endpoint_ids: Union[str, List[str]],
|
253
|
+
organization_ids: Optional[List[Union[int, str]]] = None
|
254
|
+
) -> AssetFilter:
|
255
|
+
"""Create an AssetFilter from endpoint IDs for backward compatibility."""
|
256
|
+
if isinstance(endpoint_ids, str):
|
257
|
+
endpoint_ids = [endpoint_ids]
|
258
|
+
|
259
|
+
org_ids = [int(org_id) for org_id in (organization_ids or [0])]
|
260
|
+
|
261
|
+
return AssetFilter(
|
262
|
+
included_endpoint_ids=endpoint_ids,
|
263
|
+
organization_ids=org_ids,
|
264
|
+
managed_status=["managed"] # Default to managed assets
|
265
|
+
)
|
266
|
+
|
267
|
+
|
268
|
+
# Backward compatibility command classes that use endpoint IDs directly
|
269
|
+
class IsolateAssetsByIdCommand(Command[Dict[str, Any]]):
|
270
|
+
"""Legacy command to isolate assets by endpoint IDs - uses correct API structure."""
|
271
|
+
|
272
|
+
def __init__(
|
273
|
+
self,
|
274
|
+
http_client: HTTPClient,
|
275
|
+
endpoint_ids: Union[str, List[str]],
|
276
|
+
organization_ids: Optional[List[Union[int, str]]] = None
|
277
|
+
):
|
278
|
+
self.http_client = http_client
|
279
|
+
asset_filter = create_asset_filter_from_endpoint_ids(endpoint_ids, organization_ids)
|
280
|
+
self.command = IsolateAssetsCommand(http_client, asset_filter)
|
281
|
+
|
282
|
+
def execute(self) -> Dict[str, Any]:
|
283
|
+
"""Execute through the correct filter-based command."""
|
284
|
+
return self.command.execute()
|
285
|
+
|
286
|
+
|
287
|
+
class UnisolateAssetsByIdCommand(Command[Dict[str, Any]]):
|
288
|
+
"""Legacy command to unisolate assets by endpoint IDs - uses correct API structure."""
|
289
|
+
|
290
|
+
def __init__(
|
291
|
+
self,
|
292
|
+
http_client: HTTPClient,
|
293
|
+
endpoint_ids: Union[str, List[str]],
|
294
|
+
organization_ids: Optional[List[Union[int, str]]] = None
|
295
|
+
):
|
296
|
+
self.http_client = http_client
|
297
|
+
asset_filter = create_asset_filter_from_endpoint_ids(endpoint_ids, organization_ids)
|
298
|
+
self.command = UnisolateAssetsCommand(http_client, asset_filter)
|
299
|
+
|
300
|
+
def execute(self) -> Dict[str, Any]:
|
301
|
+
"""Execute through the correct filter-based command."""
|
302
|
+
return self.command.execute()
|
303
|
+
|
304
|
+
|
305
|
+
class RebootAssetsByIdCommand(Command[Dict[str, Any]]):
|
306
|
+
"""Legacy command to reboot assets by endpoint IDs - uses correct API structure."""
|
307
|
+
|
308
|
+
def __init__(
|
309
|
+
self,
|
310
|
+
http_client: HTTPClient,
|
311
|
+
endpoint_ids: Union[str, List[str]],
|
312
|
+
organization_ids: Optional[List[Union[int, str]]] = None
|
313
|
+
):
|
314
|
+
self.http_client = http_client
|
315
|
+
asset_filter = create_asset_filter_from_endpoint_ids(endpoint_ids, organization_ids)
|
316
|
+
self.command = RebootAssetsCommand(http_client, asset_filter)
|
317
|
+
|
318
|
+
def execute(self) -> Dict[str, Any]:
|
319
|
+
"""Execute through the correct filter-based command."""
|
320
|
+
return self.command.execute()
|
321
|
+
|
322
|
+
|
323
|
+
class ShutdownAssetsByIdCommand(Command[Dict[str, Any]]):
|
324
|
+
"""Legacy command to shutdown assets by endpoint IDs - uses correct API structure."""
|
325
|
+
|
326
|
+
def __init__(
|
327
|
+
self,
|
328
|
+
http_client: HTTPClient,
|
329
|
+
endpoint_ids: Union[str, List[str]],
|
330
|
+
organization_ids: Optional[List[Union[int, str]]] = None
|
331
|
+
):
|
332
|
+
self.http_client = http_client
|
333
|
+
asset_filter = create_asset_filter_from_endpoint_ids(endpoint_ids, organization_ids)
|
334
|
+
self.command = ShutdownAssetsCommand(http_client, asset_filter)
|
335
|
+
|
336
|
+
def execute(self) -> Dict[str, Any]:
|
337
|
+
"""Execute through the correct filter-based command."""
|
338
|
+
return self.command.execute()
|
339
|
+
|
340
|
+
|
341
|
+
# Export the main corrected classes
|
342
|
+
__all__ = [
|
343
|
+
# Main corrected commands
|
344
|
+
'RebootAssetsCommand',
|
345
|
+
'ShutdownAssetsCommand',
|
346
|
+
'IsolateAssetsCommand',
|
347
|
+
'UnisolateAssetsCommand',
|
348
|
+
'LogRetrievalCommand',
|
349
|
+
'VersionUpdateCommand',
|
350
|
+
'UninstallAssetsCommand',
|
351
|
+
'PurgeAndUninstallAssetsCommand',
|
352
|
+
'AddTagsToAssetsCommand',
|
353
|
+
'RemoveTagsFromAssetsCommand',
|
354
|
+
|
355
|
+
# Legacy compatibility commands
|
356
|
+
'IsolateAssetsByIdCommand',
|
357
|
+
'UnisolateAssetsByIdCommand',
|
358
|
+
'RebootAssetsByIdCommand',
|
359
|
+
'ShutdownAssetsByIdCommand',
|
360
|
+
|
361
|
+
# Utility functions
|
362
|
+
'create_asset_filter_from_endpoint_ids'
|
363
|
+
]
|
@@ -0,0 +1,37 @@
|
|
1
|
+
"""
|
2
|
+
Authentication-related commands for the Binalyze AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Dict, Any, Union
|
6
|
+
|
7
|
+
from ..base import Command
|
8
|
+
from ..models.authentication import LoginRequest, LoginResponse
|
9
|
+
from ..http_client import HTTPClient
|
10
|
+
|
11
|
+
|
12
|
+
class LoginCommand(Command[LoginResponse]):
|
13
|
+
"""Command to login user."""
|
14
|
+
|
15
|
+
def __init__(self, http_client: HTTPClient, request: Union[LoginRequest, Dict[str, Any]]):
|
16
|
+
self.http_client = http_client
|
17
|
+
self.request = request
|
18
|
+
|
19
|
+
def execute(self) -> LoginResponse:
|
20
|
+
"""Execute the login command."""
|
21
|
+
# Handle both dict and model objects
|
22
|
+
if isinstance(self.request, dict):
|
23
|
+
payload = self.request
|
24
|
+
else:
|
25
|
+
payload = {
|
26
|
+
"username": self.request.username,
|
27
|
+
"password": self.request.password
|
28
|
+
}
|
29
|
+
|
30
|
+
response = self.http_client.post("auth/login", json_data=payload)
|
31
|
+
|
32
|
+
if response.get("success"):
|
33
|
+
result = response.get("result", {})
|
34
|
+
return LoginResponse(**result)
|
35
|
+
else:
|
36
|
+
# This will typically raise an exception via http_client error handling
|
37
|
+
raise Exception(f"Login failed: {response.get('error', 'Unknown error')}")
|
@@ -0,0 +1,231 @@
|
|
1
|
+
"""
|
2
|
+
Auto Asset Tags-related commands for the Binalyze AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Dict, Any, Union
|
6
|
+
|
7
|
+
from ..base import Command
|
8
|
+
from ..models.auto_asset_tags import (
|
9
|
+
AutoAssetTag, CreateAutoAssetTagRequest, UpdateAutoAssetTagRequest,
|
10
|
+
StartTaggingRequest, TaggingResult, TaggingResponse
|
11
|
+
)
|
12
|
+
from ..http_client import HTTPClient
|
13
|
+
from ..exceptions import ServerError, ValidationError
|
14
|
+
from ..queries.auto_asset_tags import GetAutoAssetTagQuery
|
15
|
+
|
16
|
+
|
17
|
+
class CreateAutoAssetTagCommand(Command[AutoAssetTag]):
|
18
|
+
"""Command to create auto asset tag."""
|
19
|
+
|
20
|
+
def __init__(self, http_client: HTTPClient, request: Union[CreateAutoAssetTagRequest, Dict[str, Any]]):
|
21
|
+
self.http_client = http_client
|
22
|
+
self.request = request
|
23
|
+
|
24
|
+
def execute(self) -> AutoAssetTag:
|
25
|
+
"""Execute the create auto asset tag command."""
|
26
|
+
# Handle both dict and model objects
|
27
|
+
if isinstance(self.request, dict):
|
28
|
+
payload = self.request
|
29
|
+
else:
|
30
|
+
payload = self.request.model_dump(by_alias=True, exclude_none=False)
|
31
|
+
|
32
|
+
# Validate payload format before sending to API
|
33
|
+
self._validate_payload_format(payload)
|
34
|
+
|
35
|
+
try:
|
36
|
+
response = self.http_client.post("auto-asset-tag", json_data=payload)
|
37
|
+
|
38
|
+
if response.get("success"):
|
39
|
+
tag_data = response.get("result", {})
|
40
|
+
return AutoAssetTag(**tag_data)
|
41
|
+
|
42
|
+
raise Exception(f"Failed to create auto asset tag: {response.get('error', 'Unknown error')}")
|
43
|
+
|
44
|
+
except ServerError as e:
|
45
|
+
if e.status_code == 500:
|
46
|
+
# Provide better error message for format issues
|
47
|
+
raise ValidationError(
|
48
|
+
f"Auto asset tag creation failed due to invalid format. "
|
49
|
+
f"Please ensure all conditions follow the required nested structure:\n"
|
50
|
+
f"- Each condition group must have 'operator' (and/or) and 'conditions' array\n"
|
51
|
+
f"- Individual conditions must have 'field', 'operator', and 'value'\n"
|
52
|
+
f"- Required fields: linuxConditions, windowsConditions, macosConditions\n"
|
53
|
+
f"Example format: {{\n"
|
54
|
+
f" 'linuxConditions': {{\n"
|
55
|
+
f" 'operator': 'and',\n"
|
56
|
+
f" 'conditions': [{{\n"
|
57
|
+
f" 'operator': 'or',\n"
|
58
|
+
f" 'conditions': [{{\n"
|
59
|
+
f" 'field': 'hostname',\n"
|
60
|
+
f" 'operator': 'contains',\n"
|
61
|
+
f" 'value': 'test'\n"
|
62
|
+
f" }}]\n"
|
63
|
+
f" }}]\n"
|
64
|
+
f" }}\n"
|
65
|
+
f"}}\n"
|
66
|
+
f"Original error: {str(e)}"
|
67
|
+
)
|
68
|
+
else:
|
69
|
+
# Re-raise other server errors
|
70
|
+
raise
|
71
|
+
|
72
|
+
def _validate_payload_format(self, payload: Dict[str, Any]) -> None:
|
73
|
+
"""Validate the payload format before sending to API."""
|
74
|
+
required_fields = ["tag"]
|
75
|
+
condition_fields = ["linuxConditions", "windowsConditions", "macosConditions"]
|
76
|
+
|
77
|
+
# Check required fields
|
78
|
+
for field in required_fields:
|
79
|
+
if field not in payload:
|
80
|
+
raise ValidationError(f"Missing required field: {field}")
|
81
|
+
|
82
|
+
# Validate at least one condition is provided
|
83
|
+
has_conditions = any(field in payload for field in condition_fields)
|
84
|
+
if not has_conditions:
|
85
|
+
raise ValidationError(
|
86
|
+
f"At least one condition type must be provided: {', '.join(condition_fields)}"
|
87
|
+
)
|
88
|
+
|
89
|
+
# Validate condition structure for provided conditions
|
90
|
+
for condition_type in condition_fields:
|
91
|
+
if condition_type in payload:
|
92
|
+
self._validate_condition_structure(payload[condition_type], condition_type)
|
93
|
+
|
94
|
+
def _validate_condition_structure(self, condition: Dict[str, Any], condition_type: str) -> None:
|
95
|
+
"""Validate the structure of a condition group."""
|
96
|
+
if not isinstance(condition, dict):
|
97
|
+
raise ValidationError(f"{condition_type} must be an object")
|
98
|
+
|
99
|
+
# Check for required fields in condition group
|
100
|
+
if "operator" not in condition:
|
101
|
+
raise ValidationError(f"{condition_type}.operator is required")
|
102
|
+
|
103
|
+
if condition["operator"] not in ["and", "or"]:
|
104
|
+
raise ValidationError(f"{condition_type}.operator must be 'and' or 'or'")
|
105
|
+
|
106
|
+
if "conditions" not in condition:
|
107
|
+
raise ValidationError(f"{condition_type}.conditions array is required")
|
108
|
+
|
109
|
+
if not isinstance(condition["conditions"], list):
|
110
|
+
raise ValidationError(f"{condition_type}.conditions must be an array")
|
111
|
+
|
112
|
+
# Validate each nested condition
|
113
|
+
for i, nested_condition in enumerate(condition["conditions"]):
|
114
|
+
if not isinstance(nested_condition, dict):
|
115
|
+
raise ValidationError(f"{condition_type}.conditions[{i}] must be an object")
|
116
|
+
|
117
|
+
# Check if it's a group condition or individual condition
|
118
|
+
if "conditions" in nested_condition:
|
119
|
+
# It's a nested group - validate recursively
|
120
|
+
self._validate_condition_structure(nested_condition, f"{condition_type}.conditions[{i}]")
|
121
|
+
else:
|
122
|
+
# It's an individual condition - validate fields
|
123
|
+
required_condition_fields = ["field", "operator", "value"]
|
124
|
+
for field in required_condition_fields:
|
125
|
+
if field not in nested_condition:
|
126
|
+
raise ValidationError(
|
127
|
+
f"{condition_type}.conditions[{i}].{field} is required"
|
128
|
+
)
|
129
|
+
|
130
|
+
|
131
|
+
class UpdateAutoAssetTagCommand(Command[AutoAssetTag]):
|
132
|
+
"""Command to update auto asset tag."""
|
133
|
+
|
134
|
+
def __init__(self, http_client: HTTPClient, tag_id: str, request: Union[UpdateAutoAssetTagRequest, Dict[str, Any]]):
|
135
|
+
self.http_client = http_client
|
136
|
+
self.tag_id = tag_id
|
137
|
+
self.request = request
|
138
|
+
|
139
|
+
def execute(self) -> AutoAssetTag:
|
140
|
+
"""Execute the update auto asset tag command."""
|
141
|
+
# Handle both dict and model objects
|
142
|
+
if isinstance(self.request, dict):
|
143
|
+
payload = self.request
|
144
|
+
else:
|
145
|
+
payload = self.request.model_dump(by_alias=True, exclude_none=True)
|
146
|
+
|
147
|
+
try:
|
148
|
+
response = self.http_client.put(f"auto-asset-tag/{self.tag_id}", json_data=payload)
|
149
|
+
|
150
|
+
if response.get("success"):
|
151
|
+
tag_data = response.get("result", {})
|
152
|
+
return AutoAssetTag(**tag_data)
|
153
|
+
|
154
|
+
raise Exception(f"Failed to update auto asset tag: {response.get('error', 'Unknown error')}")
|
155
|
+
|
156
|
+
except ServerError as e:
|
157
|
+
# API-002 Workaround: If update fails with server error, provide helpful message
|
158
|
+
# with alternative approach (delete + recreate)
|
159
|
+
if "Auto asset tag update is currently unavailable" in str(e):
|
160
|
+
# Get the current tag data for reference
|
161
|
+
try:
|
162
|
+
get_query = GetAutoAssetTagQuery(self.http_client, self.tag_id)
|
163
|
+
current_tag = get_query.execute()
|
164
|
+
|
165
|
+
# Merge current data with updates
|
166
|
+
updated_data = current_tag.model_dump()
|
167
|
+
updated_data.update(payload)
|
168
|
+
|
169
|
+
raise ValidationError(
|
170
|
+
f"Auto asset tag update is currently unavailable due to a server bug. "
|
171
|
+
f"WORKAROUND: To update this tag, please:\n"
|
172
|
+
f"1. Delete the existing tag (ID: {self.tag_id})\n"
|
173
|
+
f"2. Create a new tag with the updated data:\n"
|
174
|
+
f" Tag: {updated_data.get('tag', current_tag.tag)}\n"
|
175
|
+
f" Organization IDs: {updated_data.get('organizationIds', current_tag.organization_ids)}\n"
|
176
|
+
f" Linux Conditions: {updated_data.get('linuxConditions', current_tag.linux_conditions)}\n"
|
177
|
+
f" Windows Conditions: {updated_data.get('windowsConditions', current_tag.windows_conditions)}\n"
|
178
|
+
f" macOS Conditions: {updated_data.get('macosConditions', current_tag.macos_conditions)}\n"
|
179
|
+
f"\nUse client.auto_asset_tags.delete('{self.tag_id}') then client.auto_asset_tags.create(new_data)"
|
180
|
+
)
|
181
|
+
except Exception:
|
182
|
+
# If we can't get current data, provide generic workaround message
|
183
|
+
raise ValidationError(
|
184
|
+
f"Auto asset tag update is currently unavailable due to a server bug. "
|
185
|
+
f"WORKAROUND: Delete the existing tag (ID: {self.tag_id}) and create a new one with updated values."
|
186
|
+
)
|
187
|
+
else:
|
188
|
+
# Re-raise if it's a different server error
|
189
|
+
raise
|
190
|
+
|
191
|
+
|
192
|
+
class DeleteAutoAssetTagCommand(Command[Dict[str, Any]]):
|
193
|
+
"""Command to delete auto asset tag."""
|
194
|
+
|
195
|
+
def __init__(self, http_client: HTTPClient, tag_id: str):
|
196
|
+
self.http_client = http_client
|
197
|
+
self.tag_id = tag_id
|
198
|
+
|
199
|
+
def execute(self) -> Dict[str, Any]:
|
200
|
+
"""Execute the delete auto asset tag command."""
|
201
|
+
response = self.http_client.delete(f"auto-asset-tag/{self.tag_id}")
|
202
|
+
|
203
|
+
if response.get("success"):
|
204
|
+
return response
|
205
|
+
|
206
|
+
raise Exception(f"Failed to delete auto asset tag: {response.get('error', 'Unknown error')}")
|
207
|
+
|
208
|
+
|
209
|
+
class StartTaggingCommand(Command[TaggingResponse]):
|
210
|
+
"""Command to start tagging process."""
|
211
|
+
|
212
|
+
def __init__(self, http_client: HTTPClient, request: Union[StartTaggingRequest, Dict[str, Any]]):
|
213
|
+
self.http_client = http_client
|
214
|
+
self.request = request
|
215
|
+
|
216
|
+
def execute(self) -> TaggingResponse:
|
217
|
+
"""Execute the start tagging command."""
|
218
|
+
# Handle both dict and model objects
|
219
|
+
if isinstance(self.request, dict):
|
220
|
+
payload = self.request
|
221
|
+
else:
|
222
|
+
payload = self.request.model_dump(by_alias=True, exclude_none=True)
|
223
|
+
|
224
|
+
response = self.http_client.post("auto-asset-tag/start-tagging", json_data=payload)
|
225
|
+
|
226
|
+
if response.get("success"):
|
227
|
+
result_data = response.get("result", [])
|
228
|
+
# Handle the list response from the API
|
229
|
+
return TaggingResponse.from_api_result(result_data)
|
230
|
+
|
231
|
+
raise Exception(f"Failed to start tagging process: {response.get('error', 'Unknown error')}")
|