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.
Files changed (82) hide show
  1. binalyze_air/__init__.py +77 -0
  2. binalyze_air/apis/__init__.py +27 -0
  3. binalyze_air/apis/authentication.py +27 -0
  4. binalyze_air/apis/auto_asset_tags.py +75 -0
  5. binalyze_air/apis/endpoints.py +22 -0
  6. binalyze_air/apis/event_subscription.py +97 -0
  7. binalyze_air/apis/evidence.py +53 -0
  8. binalyze_air/apis/evidences.py +216 -0
  9. binalyze_air/apis/interact.py +36 -0
  10. binalyze_air/apis/params.py +40 -0
  11. binalyze_air/apis/settings.py +27 -0
  12. binalyze_air/apis/user_management.py +74 -0
  13. binalyze_air/apis/users.py +68 -0
  14. binalyze_air/apis/webhooks.py +231 -0
  15. binalyze_air/base.py +133 -0
  16. binalyze_air/client.py +1338 -0
  17. binalyze_air/commands/__init__.py +146 -0
  18. binalyze_air/commands/acquisitions.py +387 -0
  19. binalyze_air/commands/assets.py +363 -0
  20. binalyze_air/commands/authentication.py +37 -0
  21. binalyze_air/commands/auto_asset_tags.py +231 -0
  22. binalyze_air/commands/baseline.py +396 -0
  23. binalyze_air/commands/cases.py +603 -0
  24. binalyze_air/commands/event_subscription.py +102 -0
  25. binalyze_air/commands/evidences.py +988 -0
  26. binalyze_air/commands/interact.py +58 -0
  27. binalyze_air/commands/organizations.py +221 -0
  28. binalyze_air/commands/policies.py +203 -0
  29. binalyze_air/commands/settings.py +29 -0
  30. binalyze_air/commands/tasks.py +56 -0
  31. binalyze_air/commands/triage.py +360 -0
  32. binalyze_air/commands/user_management.py +126 -0
  33. binalyze_air/commands/users.py +101 -0
  34. binalyze_air/config.py +245 -0
  35. binalyze_air/exceptions.py +50 -0
  36. binalyze_air/http_client.py +306 -0
  37. binalyze_air/models/__init__.py +285 -0
  38. binalyze_air/models/acquisitions.py +251 -0
  39. binalyze_air/models/assets.py +439 -0
  40. binalyze_air/models/audit.py +273 -0
  41. binalyze_air/models/authentication.py +70 -0
  42. binalyze_air/models/auto_asset_tags.py +117 -0
  43. binalyze_air/models/baseline.py +232 -0
  44. binalyze_air/models/cases.py +276 -0
  45. binalyze_air/models/endpoints.py +76 -0
  46. binalyze_air/models/event_subscription.py +172 -0
  47. binalyze_air/models/evidence.py +66 -0
  48. binalyze_air/models/evidences.py +349 -0
  49. binalyze_air/models/interact.py +136 -0
  50. binalyze_air/models/organizations.py +294 -0
  51. binalyze_air/models/params.py +128 -0
  52. binalyze_air/models/policies.py +250 -0
  53. binalyze_air/models/settings.py +84 -0
  54. binalyze_air/models/tasks.py +149 -0
  55. binalyze_air/models/triage.py +143 -0
  56. binalyze_air/models/user_management.py +97 -0
  57. binalyze_air/models/users.py +82 -0
  58. binalyze_air/queries/__init__.py +134 -0
  59. binalyze_air/queries/acquisitions.py +156 -0
  60. binalyze_air/queries/assets.py +105 -0
  61. binalyze_air/queries/audit.py +417 -0
  62. binalyze_air/queries/authentication.py +56 -0
  63. binalyze_air/queries/auto_asset_tags.py +60 -0
  64. binalyze_air/queries/baseline.py +185 -0
  65. binalyze_air/queries/cases.py +293 -0
  66. binalyze_air/queries/endpoints.py +25 -0
  67. binalyze_air/queries/event_subscription.py +55 -0
  68. binalyze_air/queries/evidence.py +140 -0
  69. binalyze_air/queries/evidences.py +280 -0
  70. binalyze_air/queries/interact.py +28 -0
  71. binalyze_air/queries/organizations.py +223 -0
  72. binalyze_air/queries/params.py +115 -0
  73. binalyze_air/queries/policies.py +150 -0
  74. binalyze_air/queries/settings.py +20 -0
  75. binalyze_air/queries/tasks.py +82 -0
  76. binalyze_air/queries/triage.py +231 -0
  77. binalyze_air/queries/user_management.py +83 -0
  78. binalyze_air/queries/users.py +69 -0
  79. binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
  80. binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
  81. binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
  82. 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')}")