binalyze-air-sdk 1.0.2__py3-none-any.whl → 1.0.3__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 (142) hide show
  1. binalyze_air/__init__.py +77 -77
  2. binalyze_air/apis/__init__.py +67 -27
  3. binalyze_air/apis/acquisitions.py +107 -0
  4. binalyze_air/apis/api_tokens.py +49 -0
  5. binalyze_air/apis/assets.py +161 -0
  6. binalyze_air/apis/audit_logs.py +26 -0
  7. binalyze_air/apis/{authentication.py → auth.py} +29 -27
  8. binalyze_air/apis/auto_asset_tags.py +79 -75
  9. binalyze_air/apis/backup.py +177 -0
  10. binalyze_air/apis/baseline.py +46 -0
  11. binalyze_air/apis/cases.py +225 -0
  12. binalyze_air/apis/cloud_forensics.py +116 -0
  13. binalyze_air/apis/event_subscription.py +96 -96
  14. binalyze_air/apis/evidence.py +249 -53
  15. binalyze_air/apis/interact.py +153 -36
  16. binalyze_air/apis/investigation_hub.py +234 -0
  17. binalyze_air/apis/license.py +104 -0
  18. binalyze_air/apis/logger.py +83 -0
  19. binalyze_air/apis/multipart_upload.py +201 -0
  20. binalyze_air/apis/notifications.py +115 -0
  21. binalyze_air/apis/organizations.py +267 -0
  22. binalyze_air/apis/params.py +44 -39
  23. binalyze_air/apis/policies.py +186 -0
  24. binalyze_air/apis/preset_filters.py +79 -0
  25. binalyze_air/apis/recent_activities.py +71 -0
  26. binalyze_air/apis/relay_server.py +104 -0
  27. binalyze_air/apis/settings.py +395 -27
  28. binalyze_air/apis/tasks.py +80 -0
  29. binalyze_air/apis/triage.py +197 -0
  30. binalyze_air/apis/user_management.py +183 -74
  31. binalyze_air/apis/webhook_executions.py +50 -0
  32. binalyze_air/apis/webhooks.py +322 -230
  33. binalyze_air/base.py +207 -133
  34. binalyze_air/client.py +217 -1337
  35. binalyze_air/commands/__init__.py +175 -145
  36. binalyze_air/commands/acquisitions.py +661 -387
  37. binalyze_air/commands/api_tokens.py +55 -0
  38. binalyze_air/commands/assets.py +324 -362
  39. binalyze_air/commands/{authentication.py → auth.py} +36 -36
  40. binalyze_air/commands/auto_asset_tags.py +230 -230
  41. binalyze_air/commands/backup.py +47 -0
  42. binalyze_air/commands/baseline.py +32 -396
  43. binalyze_air/commands/cases.py +609 -602
  44. binalyze_air/commands/cloud_forensics.py +88 -0
  45. binalyze_air/commands/event_subscription.py +101 -101
  46. binalyze_air/commands/evidences.py +918 -988
  47. binalyze_air/commands/interact.py +172 -58
  48. binalyze_air/commands/investigation_hub.py +315 -0
  49. binalyze_air/commands/license.py +183 -0
  50. binalyze_air/commands/logger.py +126 -0
  51. binalyze_air/commands/multipart_upload.py +363 -0
  52. binalyze_air/commands/notifications.py +45 -0
  53. binalyze_air/commands/organizations.py +200 -221
  54. binalyze_air/commands/policies.py +175 -203
  55. binalyze_air/commands/preset_filters.py +55 -0
  56. binalyze_air/commands/recent_activities.py +32 -0
  57. binalyze_air/commands/relay_server.py +144 -0
  58. binalyze_air/commands/settings.py +431 -29
  59. binalyze_air/commands/tasks.py +95 -56
  60. binalyze_air/commands/triage.py +224 -360
  61. binalyze_air/commands/user_management.py +351 -126
  62. binalyze_air/commands/webhook_executions.py +77 -0
  63. binalyze_air/config.py +244 -244
  64. binalyze_air/exceptions.py +49 -49
  65. binalyze_air/http_client.py +426 -305
  66. binalyze_air/models/__init__.py +287 -285
  67. binalyze_air/models/acquisitions.py +365 -250
  68. binalyze_air/models/api_tokens.py +73 -0
  69. binalyze_air/models/assets.py +438 -438
  70. binalyze_air/models/audit.py +247 -272
  71. binalyze_air/models/audit_logs.py +14 -0
  72. binalyze_air/models/{authentication.py → auth.py} +69 -69
  73. binalyze_air/models/auto_asset_tags.py +227 -116
  74. binalyze_air/models/backup.py +138 -0
  75. binalyze_air/models/baseline.py +231 -231
  76. binalyze_air/models/cases.py +275 -275
  77. binalyze_air/models/cloud_forensics.py +145 -0
  78. binalyze_air/models/event_subscription.py +170 -171
  79. binalyze_air/models/evidence.py +65 -65
  80. binalyze_air/models/evidences.py +367 -348
  81. binalyze_air/models/interact.py +266 -135
  82. binalyze_air/models/investigation_hub.py +265 -0
  83. binalyze_air/models/license.py +150 -0
  84. binalyze_air/models/logger.py +83 -0
  85. binalyze_air/models/multipart_upload.py +352 -0
  86. binalyze_air/models/notifications.py +138 -0
  87. binalyze_air/models/organizations.py +293 -293
  88. binalyze_air/models/params.py +153 -127
  89. binalyze_air/models/policies.py +260 -249
  90. binalyze_air/models/preset_filters.py +79 -0
  91. binalyze_air/models/recent_activities.py +70 -0
  92. binalyze_air/models/relay_server.py +121 -0
  93. binalyze_air/models/settings.py +538 -84
  94. binalyze_air/models/tasks.py +215 -149
  95. binalyze_air/models/triage.py +141 -142
  96. binalyze_air/models/user_management.py +200 -97
  97. binalyze_air/models/webhook_executions.py +33 -0
  98. binalyze_air/queries/__init__.py +121 -133
  99. binalyze_air/queries/acquisitions.py +155 -155
  100. binalyze_air/queries/api_tokens.py +46 -0
  101. binalyze_air/queries/assets.py +186 -105
  102. binalyze_air/queries/audit.py +400 -416
  103. binalyze_air/queries/{authentication.py → auth.py} +55 -55
  104. binalyze_air/queries/auto_asset_tags.py +59 -59
  105. binalyze_air/queries/backup.py +66 -0
  106. binalyze_air/queries/baseline.py +21 -185
  107. binalyze_air/queries/cases.py +292 -292
  108. binalyze_air/queries/cloud_forensics.py +137 -0
  109. binalyze_air/queries/event_subscription.py +54 -54
  110. binalyze_air/queries/evidence.py +139 -139
  111. binalyze_air/queries/evidences.py +279 -279
  112. binalyze_air/queries/interact.py +140 -28
  113. binalyze_air/queries/investigation_hub.py +329 -0
  114. binalyze_air/queries/license.py +85 -0
  115. binalyze_air/queries/logger.py +58 -0
  116. binalyze_air/queries/multipart_upload.py +180 -0
  117. binalyze_air/queries/notifications.py +71 -0
  118. binalyze_air/queries/organizations.py +222 -222
  119. binalyze_air/queries/params.py +154 -115
  120. binalyze_air/queries/policies.py +149 -149
  121. binalyze_air/queries/preset_filters.py +60 -0
  122. binalyze_air/queries/recent_activities.py +44 -0
  123. binalyze_air/queries/relay_server.py +42 -0
  124. binalyze_air/queries/settings.py +533 -20
  125. binalyze_air/queries/tasks.py +125 -81
  126. binalyze_air/queries/triage.py +230 -230
  127. binalyze_air/queries/user_management.py +193 -83
  128. binalyze_air/queries/webhook_executions.py +39 -0
  129. binalyze_air_sdk-1.0.3.dist-info/METADATA +752 -0
  130. binalyze_air_sdk-1.0.3.dist-info/RECORD +132 -0
  131. {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/WHEEL +1 -1
  132. binalyze_air/apis/endpoints.py +0 -22
  133. binalyze_air/apis/evidences.py +0 -216
  134. binalyze_air/apis/users.py +0 -68
  135. binalyze_air/commands/users.py +0 -101
  136. binalyze_air/models/endpoints.py +0 -76
  137. binalyze_air/models/users.py +0 -82
  138. binalyze_air/queries/endpoints.py +0 -25
  139. binalyze_air/queries/users.py +0 -69
  140. binalyze_air_sdk-1.0.2.dist-info/METADATA +0 -706
  141. binalyze_air_sdk-1.0.2.dist-info/RECORD +0 -82
  142. {binalyze_air_sdk-1.0.2.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,37 +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
1
+ """
2
+ Auth-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.auth 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
37
  raise Exception(f"Login failed: {response.get('error', 'Unknown error')}")
@@ -1,231 +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
-
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)
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)
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.organizationIds)}\n"
176
+ f" Linux Conditions: {updated_data.get('linuxConditions', current_tag.linuxConditions)}\n"
177
+ f" Windows Conditions: {updated_data.get('windowsConditions', current_tag.windowsConditions)}\n"
178
+ f" macOS Conditions: {updated_data.get('macosConditions', current_tag.macosConditions)}\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
231
  raise Exception(f"Failed to start tagging process: {response.get('error', 'Unknown error')}")
@@ -0,0 +1,47 @@
1
+ """
2
+ Backup commands for the Binalyze AIR SDK.
3
+ """
4
+
5
+ from typing import Dict, Any
6
+
7
+ from ..base import Command
8
+ from ..models.backup import BackupNowRequest
9
+ from ..http_client import HTTPClient
10
+
11
+
12
+ class BackupNowCommand(Command[Dict[str, Any]]):
13
+ """Command to create an immediate backup."""
14
+
15
+ def __init__(self, http_client: HTTPClient, request: BackupNowRequest):
16
+ self.http_client = http_client
17
+ self.request = request
18
+
19
+ def execute(self) -> Dict[str, Any]:
20
+ """Execute the command."""
21
+ # Create the filter structure based on the API specification
22
+ filter_data = {}
23
+
24
+ if self.request.include_endpoint_ids:
25
+ filter_data["includedEndpointIds"] = self.request.include_endpoint_ids
26
+ if self.request.exclude_endpoint_ids:
27
+ filter_data["excludedEndpointIds"] = self.request.exclude_endpoint_ids
28
+ if self.request.organization_ids:
29
+ filter_data["organizationIds"] = self.request.organization_ids
30
+
31
+ payload = {"filter": filter_data}
32
+
33
+ response = self.http_client.post("backup/now", json_data=payload)
34
+ return response
35
+
36
+
37
+ class DeleteBackupCommand(Command[Dict[str, Any]]):
38
+ """Command to delete a backup."""
39
+
40
+ def __init__(self, http_client: HTTPClient, backup_id: str):
41
+ self.http_client = http_client
42
+ self.backup_id = backup_id
43
+
44
+ def execute(self) -> Dict[str, Any]:
45
+ """Execute the command."""
46
+ response = self.http_client.delete(f"backup/{self.backup_id}")
47
+ return response