binalyze-air-sdk 1.0.1__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.1.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.1.dist-info/METADATA +0 -635
  141. binalyze_air_sdk-1.0.1.dist-info/RECORD +0 -82
  142. {binalyze_air_sdk-1.0.1.dist-info → binalyze_air_sdk-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,988 +1,918 @@
1
- """
2
- Evidences/Repositories-related commands for the Binalyze AIR SDK.
3
- """
4
-
5
- from typing import Dict, Any
6
-
7
- from ..base import Command
8
- from ..models.evidences import (
9
- EvidenceRepository, AmazonS3Repository, AzureStorageRepository,
10
- FTPSRepository, SFTPRepository, SMBRepository,
11
- CreateAmazonS3RepositoryRequest, UpdateAmazonS3RepositoryRequest,
12
- CreateAzureStorageRepositoryRequest, UpdateAzureStorageRepositoryRequest,
13
- CreateFTPSRepositoryRequest, UpdateFTPSRepositoryRequest,
14
- CreateSFTPRepositoryRequest, UpdateSFTPRepositoryRequest,
15
- CreateSMBRepositoryRequest, UpdateSMBRepositoryRequest,
16
- ValidateRepositoryRequest, ValidationResult
17
- )
18
- from ..http_client import HTTPClient
19
-
20
-
21
- # General Repository Commands
22
-
23
- class UpdateRepositoryCommand(Command[EvidenceRepository]):
24
- """Command to update evidence repository."""
25
-
26
- def __init__(self, http_client: HTTPClient, repository_id: str, update_data: Dict[str, Any]):
27
- self.http_client = http_client
28
- self.repository_id = repository_id
29
- self.update_data = update_data
30
-
31
- def execute(self) -> EvidenceRepository:
32
- """Execute the update repository command."""
33
- response = self.http_client.put(
34
- f"evidences/repositories/{self.repository_id}",
35
- json_data=self.update_data
36
- )
37
-
38
- if response.get("success"):
39
- repository_data = response.get("result", {})
40
- return EvidenceRepository(**repository_data)
41
-
42
- raise Exception(f"Failed to update repository: {response.get('error', 'Unknown error')}")
43
-
44
-
45
- class DeleteRepositoryCommand(Command[Dict[str, Any]]):
46
- """Command to delete evidence repository."""
47
-
48
- def __init__(self, http_client: HTTPClient, repository_id: str):
49
- self.http_client = http_client
50
- self.repository_id = repository_id
51
-
52
- def execute(self) -> Dict[str, Any]:
53
- """Execute the delete repository command."""
54
- try:
55
- response = self.http_client.delete(f"evidences/repositories/{self.repository_id}")
56
-
57
- if response.get("success"):
58
- return response
59
-
60
- # Handle error response with detailed information from API
61
- errors = response.get("errors", [])
62
- status_code = response.get("statusCode", "Unknown")
63
- error_message = "; ".join(errors) if errors else response.get("error", "Unknown error")
64
-
65
- raise Exception(f"Failed to delete repository (HTTP {status_code}): {error_message}")
66
-
67
- except Exception as e:
68
- # Check if this is already our formatted exception
69
- if "Failed to delete repository" in str(e):
70
- raise e
71
-
72
- # Handle HTTP client exceptions and format them consistently
73
- error_str = str(e)
74
- if "404" in error_str or "not found" in error_str.lower():
75
- raise Exception(f"Failed to delete repository (HTTP 404): No evidence repository found by provided id(s)")
76
- else:
77
- raise Exception(f"Failed to delete repository: {error_str}")
78
-
79
-
80
- # Amazon S3 Repository Commands
81
-
82
- class CreateAmazonS3RepositoryCommand(Command[AmazonS3Repository]):
83
- """Command to create Amazon S3 repository."""
84
-
85
- def __init__(self, http_client: HTTPClient, request: CreateAmazonS3RepositoryRequest):
86
- self.http_client = http_client
87
- self.request = request
88
-
89
- def execute(self) -> AmazonS3Repository:
90
- """Execute the create Amazon S3 repository command."""
91
- payload = self.request.model_dump(exclude_none=True)
92
-
93
- # Handle field mapping for API compatibility
94
- if 'organizationId' in payload:
95
- # API expects organizationIds array, not organizationId
96
- if 'organizationIds' not in payload or not payload['organizationIds']:
97
- payload['organizationIds'] = [payload['organizationId']]
98
- del payload['organizationId']
99
-
100
- # Map SDK field bucketName to API field bucket
101
- if 'bucketName' in payload:
102
- payload['bucket'] = payload['bucketName']
103
- del payload['bucketName']
104
-
105
- # Remove SDK-specific fields that API doesn't expect
106
- if 'isDefault' in payload:
107
- del payload['isDefault']
108
-
109
- response = self.http_client.post("evidences/repositories/amazon-s3", json_data=payload)
110
-
111
- if response.get("success"):
112
- repository_data = response.get("result", {})
113
-
114
- # Handle field mapping from API response back to SDK model
115
- if '_id' in repository_data:
116
- repository_data['id'] = repository_data['_id']
117
- del repository_data['_id']
118
-
119
- # Map organizationIds array back to organizationId for SDK model
120
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
121
- repository_data['organizationId'] = repository_data['organizationIds'][0]
122
-
123
- # Map API field bucket back to SDK field bucketName
124
- if 'bucket' in repository_data:
125
- repository_data['bucketName'] = repository_data['bucket']
126
- del repository_data['bucket']
127
-
128
- return AmazonS3Repository(**repository_data)
129
-
130
- raise Exception(f"Failed to create Amazon S3 repository: {response.get('error', 'Unknown error')}")
131
-
132
-
133
- class UpdateAmazonS3RepositoryCommand(Command[AmazonS3Repository]):
134
- """Command to update Amazon S3 repository."""
135
-
136
- def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateAmazonS3RepositoryRequest):
137
- self.http_client = http_client
138
- self.repository_id = repository_id
139
- self.request = request
140
-
141
- def execute(self) -> AmazonS3Repository:
142
- """Execute the update Amazon S3 repository command."""
143
- # The Amazon S3 update API requires a complete payload, not partial updates
144
- # First, get the current repository data
145
- current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
146
-
147
- if not current_response.get("success"):
148
- raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
149
-
150
- current_data = current_response.get("result", {})
151
-
152
- # Create update payload by merging current data with updates
153
- update_data = self.request.model_dump() # Don't exclude None to see all fields
154
-
155
- # Start with required fields from current repository
156
- payload = {
157
- "name": current_data.get("name", ""),
158
- "region": current_data.get("region", ""),
159
- "bucket": current_data.get("bucket", ""), # API uses 'bucket'
160
- "accessKeyId": current_data.get("accessKeyId", ""),
161
- "secretAccessKey": current_data.get("secretAccessKey", ""),
162
- "organizationIds": current_data.get("organizationIds", [0])
163
- }
164
-
165
- # Apply updates from the request
166
- for key, value in update_data.items():
167
- if key == "organizationId" and value is not None:
168
- # Convert organizationId to organizationIds array for API
169
- payload["organizationIds"] = [value]
170
- elif key == "bucketName" and value is not None:
171
- # Map SDK field bucketName to API field bucket
172
- payload["bucket"] = value
173
- elif key in ["name", "region", "accessKeyId", "secretAccessKey"] and value is not None:
174
- payload[key] = value
175
- # Skip other SDK-specific fields that don't map to API
176
-
177
- response = self.http_client.put(
178
- f"evidences/repositories/amazon-s3/{self.repository_id}",
179
- json_data=payload
180
- )
181
-
182
- if response.get("success"):
183
- repository_data = response.get("result", {})
184
-
185
- # Handle field mapping from API response back to SDK model
186
- if '_id' in repository_data:
187
- repository_data['id'] = repository_data['_id']
188
- del repository_data['_id']
189
-
190
- # Map organizationIds array back to organizationId for SDK model
191
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
192
- repository_data['organizationId'] = repository_data['organizationIds'][0]
193
-
194
- # Map API field bucket back to SDK field bucketName
195
- if 'bucket' in repository_data:
196
- repository_data['bucketName'] = repository_data['bucket']
197
- del repository_data['bucket']
198
-
199
- return AmazonS3Repository(**repository_data)
200
-
201
- raise Exception(f"Failed to update Amazon S3 repository: {response.get('error', 'Unknown error')}")
202
-
203
-
204
- class DeleteAmazonS3RepositoryCommand(Command[Dict[str, Any]]):
205
- """Command to delete Amazon S3 repository."""
206
-
207
- def __init__(self, http_client: HTTPClient, repository_id: str):
208
- self.http_client = http_client
209
- self.repository_id = repository_id
210
-
211
- def execute(self) -> Dict[str, Any]:
212
- """Execute the delete Amazon S3 repository command."""
213
- response = self.http_client.delete(f"evidences/repositories/amazon-s3/{self.repository_id}")
214
-
215
- if response.get("success"):
216
- return response
217
-
218
- raise Exception(f"Failed to delete Amazon S3 repository: {response.get('error', 'Unknown error')}")
219
-
220
-
221
- class ValidateAmazonS3RepositoryCommand(Command[ValidationResult]):
222
- """Command to validate Amazon S3 repository."""
223
-
224
- def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
225
- self.http_client = http_client
226
- self.request = request
227
-
228
- def execute(self) -> ValidationResult:
229
- """Execute the validate Amazon S3 repository command."""
230
- payload = self.request.model_dump(exclude_none=True)
231
-
232
- # The validation API expects the same field structure as create API
233
- # Extract config if using the generic ValidateRepositoryRequest structure
234
- if 'config' in payload and isinstance(payload['config'], dict):
235
- payload = payload['config']
236
-
237
- # Apply the same field mapping as CreateAmazonS3RepositoryCommand
238
- # Handle field mapping for API compatibility
239
- if 'organizationId' in payload:
240
- # API expects organizationIds array, not organizationId
241
- if 'organizationIds' not in payload or not payload['organizationIds']:
242
- payload['organizationIds'] = [payload['organizationId']]
243
- del payload['organizationId']
244
-
245
- # Map SDK field bucketName to API field bucket
246
- if 'bucketName' in payload:
247
- payload['bucket'] = payload['bucketName']
248
- del payload['bucketName']
249
-
250
- # Remove SDK-specific fields that API doesn't expect
251
- if 'isDefault' in payload:
252
- del payload['isDefault']
253
-
254
- try:
255
- response = self.http_client.post("evidences/repositories/validate/amazon-s3", json_data=payload)
256
- except Exception as e:
257
- # Handle specific validation errors (like 603) as successful validation responses
258
- error_str = str(e)
259
- if "603" in error_str:
260
- # 603 means validation is working, but credentials are invalid
261
- # This is a successful validation result
262
- return ValidationResult(
263
- isValid=False,
264
- message="AWS Access Key validation failed - credentials invalid but validation working"
265
- )
266
- else:
267
- # Re-raise other exceptions
268
- raise e
269
-
270
- if response.get("success"):
271
- validation_data = response.get("result", {})
272
- return ValidationResult(**validation_data)
273
-
274
- # Handle validation errors as successful validation responses
275
- status_code = response.get("statusCode")
276
- if status_code == 603:
277
- # AWS validation failed but validation is working
278
- return ValidationResult(
279
- isValid=False,
280
- message="AWS Access Key validation failed - credentials invalid but validation working"
281
- )
282
-
283
- raise Exception(f"Failed to validate Amazon S3 repository: {response.get('error', 'Unknown error')}")
284
-
285
-
286
- # Azure Storage Repository Commands
287
-
288
- class CreateAzureStorageRepositoryCommand(Command[AzureStorageRepository]):
289
- """Command to create Azure Storage repository."""
290
-
291
- def __init__(self, http_client: HTTPClient, request: CreateAzureStorageRepositoryRequest):
292
- self.http_client = http_client
293
- self.request = request
294
-
295
- def execute(self) -> AzureStorageRepository:
296
- """Execute the create Azure Storage repository command."""
297
- payload = self.request.model_dump(exclude_none=True)
298
-
299
- # Handle field mapping for API compatibility
300
- if 'organizationId' in payload:
301
- # API expects organizationIds array, not organizationId
302
- if 'organizationIds' not in payload or not payload['organizationIds']:
303
- payload['organizationIds'] = [payload['organizationId']]
304
- del payload['organizationId']
305
-
306
- # Convert SDK Azure Storage fields to API SASUrl format
307
- if 'accountName' in payload and 'accountKey' in payload and 'containerName' in payload:
308
- # Build a basic SAS URL format from the individual components
309
- account_name = payload['accountName']
310
- container_name = payload['containerName']
311
- # Create a test SAS URL format (this would normally come from Azure)
312
- sas_url = f"https://{account_name}.blob.core.windows.net/{container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=test"
313
- payload['SASUrl'] = sas_url
314
-
315
- # Remove individual fields that API doesn't expect
316
- del payload['accountName']
317
- del payload['accountKey']
318
- del payload['containerName']
319
-
320
- # Remove SDK-specific fields that API doesn't expect
321
- if 'isDefault' in payload:
322
- del payload['isDefault']
323
-
324
- response = self.http_client.post("evidences/repositories/azure-storage", json_data=payload)
325
-
326
- if response.get("success"):
327
- repository_data = response.get("result", {})
328
-
329
- # Handle field mapping from API response back to SDK model
330
- if '_id' in repository_data:
331
- repository_data['id'] = repository_data['_id']
332
- del repository_data['_id']
333
-
334
- # Map organizationIds array back to organizationId for SDK model
335
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
336
- repository_data['organizationId'] = repository_data['organizationIds'][0]
337
-
338
- # Extract Azure Storage fields from SASUrl for SDK model
339
- if 'SASUrl' in repository_data:
340
- sas_url = repository_data['SASUrl']
341
- # Parse accountName and containerName from SAS URL
342
- if sas_url and '://' in sas_url:
343
- try:
344
- # Extract account name from URL like https://accountname.blob.core.windows.net/container...
345
- url_parts = sas_url.split('://', 1)[1].split('/')
346
- if len(url_parts) >= 2:
347
- domain_parts = url_parts[0].split('.')
348
- if len(domain_parts) >= 1:
349
- repository_data['accountName'] = domain_parts[0]
350
- repository_data['containerName'] = url_parts[1].split('?')[0]
351
- except:
352
- # Fallback values if parsing fails
353
- repository_data['accountName'] = 'azure-storage'
354
- repository_data['containerName'] = 'container'
355
-
356
- # Provide a placeholder for accountKey since API doesn't return it
357
- repository_data['accountKey'] = 'hidden'
358
-
359
- return AzureStorageRepository(**repository_data)
360
-
361
- raise Exception(f"Failed to create Azure Storage repository: {response.get('error', 'Unknown error')}")
362
-
363
-
364
- class UpdateAzureStorageRepositoryCommand(Command[AzureStorageRepository]):
365
- """Command to update Azure Storage repository."""
366
-
367
- def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateAzureStorageRepositoryRequest):
368
- self.http_client = http_client
369
- self.repository_id = repository_id
370
- self.request = request
371
-
372
- def execute(self) -> AzureStorageRepository:
373
- """Execute the update Azure Storage repository command."""
374
- # The Azure Storage update API requires a complete payload, not partial updates
375
- # First, get the current repository data
376
- current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
377
-
378
- if not current_response.get("success"):
379
- raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
380
-
381
- current_data = current_response.get("result", {})
382
-
383
- # Create update payload by merging current data with updates
384
- update_data = self.request.model_dump() # Don't exclude None to see all fields
385
-
386
- # Start with current data and build API payload format
387
- payload = {
388
- "name": current_data.get("name", ""),
389
- "SASUrl": current_data.get("SASUrl", ""),
390
- "organizationIds": current_data.get("organizationIds", [0])
391
- }
392
-
393
- # Apply updates from the request
394
- for key, value in update_data.items():
395
- if key == "organizationId" and value is not None:
396
- # Convert organizationId to organizationIds array for API
397
- payload["organizationIds"] = [value]
398
- elif key in ["accountName", "accountKey", "containerName"] and value is not None:
399
- # If SDK fields are provided, rebuild SAS URL
400
- # Get current values first
401
- current_account_name = update_data.get("accountName", "azure-storage")
402
- current_container_name = update_data.get("containerName", "container")
403
-
404
- # Build new SAS URL with updated values
405
- sas_url = f"https://{current_account_name}.blob.core.windows.net/{current_container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=updated"
406
- payload["SASUrl"] = sas_url
407
- elif key == "name" and value is not None:
408
- payload["name"] = value
409
- # Skip other SDK-specific fields that don't map to API
410
-
411
- response = self.http_client.put(
412
- f"evidences/repositories/azure-storage/{self.repository_id}",
413
- json_data=payload
414
- )
415
-
416
- if response.get("success"):
417
- repository_data = response.get("result", {})
418
-
419
- # Handle field mapping from API response back to SDK model
420
- if '_id' in repository_data:
421
- repository_data['id'] = repository_data['_id']
422
- del repository_data['_id']
423
-
424
- # Map organizationIds array back to organizationId for SDK model
425
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
426
- repository_data['organizationId'] = repository_data['organizationIds'][0]
427
-
428
- # Extract Azure Storage fields from SASUrl for SDK model
429
- if 'SASUrl' in repository_data:
430
- sas_url = repository_data['SASUrl']
431
- # Parse accountName and containerName from SAS URL
432
- if sas_url and '://' in sas_url:
433
- try:
434
- # Extract account name from URL like https://accountname.blob.core.windows.net/container...
435
- url_parts = sas_url.split('://', 1)[1].split('/')
436
- if len(url_parts) >= 2:
437
- domain_parts = url_parts[0].split('.')
438
- if len(domain_parts) >= 1:
439
- repository_data['accountName'] = domain_parts[0]
440
- repository_data['containerName'] = url_parts[1].split('?')[0]
441
- except:
442
- # Fallback values if parsing fails
443
- repository_data['accountName'] = 'azure-storage'
444
- repository_data['containerName'] = 'container'
445
-
446
- # Provide a placeholder for accountKey since API doesn't return it
447
- repository_data['accountKey'] = 'hidden'
448
-
449
- return AzureStorageRepository(**repository_data)
450
-
451
- raise Exception(f"Failed to update Azure Storage repository: {response.get('error', 'Unknown error')}")
452
-
453
-
454
- class DeleteAzureStorageRepositoryCommand(Command[Dict[str, Any]]):
455
- """Command to delete Azure Storage repository."""
456
-
457
- def __init__(self, http_client: HTTPClient, repository_id: str):
458
- self.http_client = http_client
459
- self.repository_id = repository_id
460
-
461
- def execute(self) -> Dict[str, Any]:
462
- """Execute the delete Azure Storage repository command."""
463
- response = self.http_client.delete(f"evidences/repositories/azure-storage/{self.repository_id}")
464
-
465
- if response.get("success"):
466
- return response
467
-
468
- raise Exception(f"Failed to delete Azure Storage repository: {response.get('error', 'Unknown error')}")
469
-
470
-
471
- class ValidateAzureStorageRepositoryCommand(Command[ValidationResult]):
472
- """Command to validate Azure Storage repository."""
473
-
474
- def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
475
- self.http_client = http_client
476
- self.request = request
477
-
478
- def execute(self) -> ValidationResult:
479
- """Execute the validate Azure Storage repository command."""
480
- payload = self.request.model_dump(exclude_none=True)
481
-
482
- # The validation API expects the same field structure as create API
483
- # Extract config if using the generic ValidateRepositoryRequest structure
484
- if 'config' in payload and isinstance(payload['config'], dict):
485
- payload = payload['config']
486
-
487
- # Apply the same field mapping as CreateAzureStorageRepositoryCommand
488
- # Handle field mapping for API compatibility
489
- if 'organizationId' in payload:
490
- # API expects organizationIds array, not organizationId
491
- if 'organizationIds' not in payload or not payload['organizationIds']:
492
- payload['organizationIds'] = [payload['organizationId']]
493
- del payload['organizationId']
494
-
495
- # Convert SDK Azure Storage fields to API SASUrl format
496
- if 'accountName' in payload and 'accountKey' in payload and 'containerName' in payload:
497
- # Build a basic SAS URL format from the individual components
498
- account_name = payload['accountName']
499
- container_name = payload['containerName']
500
- # Create a test SAS URL format (this would normally come from Azure)
501
- sas_url = f"https://{account_name}.blob.core.windows.net/{container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=validate"
502
- payload['SASUrl'] = sas_url
503
-
504
- # Remove individual fields that API doesn't expect
505
- del payload['accountName']
506
- del payload['accountKey']
507
- del payload['containerName']
508
-
509
- # Remove SDK-specific fields that API doesn't expect
510
- if 'isDefault' in payload:
511
- del payload['isDefault']
512
-
513
- response = self.http_client.post("evidences/repositories/validate/azure-storage", json_data=payload)
514
-
515
- if response.get("success"):
516
- validation_data = response.get("result", {})
517
- return ValidationResult(**validation_data)
518
-
519
- raise Exception(f"Failed to validate Azure Storage repository: {response.get('error', 'Unknown error')}")
520
-
521
-
522
- # FTPS Repository Commands
523
-
524
- class CreateFTPSRepositoryCommand(Command[FTPSRepository]):
525
- """Command to create FTPS repository."""
526
-
527
- def __init__(self, http_client: HTTPClient, request: CreateFTPSRepositoryRequest):
528
- self.http_client = http_client
529
- self.request = request
530
-
531
- def execute(self) -> FTPSRepository:
532
- """Execute the create FTPS repository command."""
533
- payload = self.request.model_dump(exclude_none=True)
534
-
535
- # Handle field mapping for API compatibility
536
- if 'organizationId' in payload:
537
- # API expects organizationIds array, not organizationId
538
- if 'organizationIds' not in payload or not payload['organizationIds']:
539
- payload['organizationIds'] = [payload['organizationId']]
540
- del payload['organizationId']
541
-
542
- # Map remotePath to path for API
543
- if 'remotePath' in payload:
544
- payload['path'] = payload['remotePath']
545
- del payload['remotePath']
546
-
547
- # Remove SDK-specific fields that API doesn't expect
548
- if 'isDefault' in payload:
549
- del payload['isDefault']
550
-
551
- # Add required FTPS fields that might be missing
552
- if 'allowSelfSignedSSL' not in payload:
553
- payload['allowSelfSignedSSL'] = False
554
-
555
- if 'publicKey' not in payload:
556
- payload['publicKey'] = ""
557
-
558
- response = self.http_client.post("evidences/repositories/ftps", json_data=payload)
559
-
560
- if response.get("success"):
561
- repository_data = response.get("result", {})
562
-
563
- # Handle field mapping from API response back to SDK model
564
- if '_id' in repository_data:
565
- repository_data['id'] = repository_data['_id']
566
- del repository_data['_id']
567
-
568
- # Map organizationIds array back to organizationId for SDK model
569
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
570
- repository_data['organizationId'] = repository_data['organizationIds'][0]
571
-
572
- # Map path back to remotePath for SDK model
573
- if 'path' in repository_data:
574
- repository_data['remotePath'] = repository_data['path']
575
- del repository_data['path']
576
-
577
- return FTPSRepository(**repository_data)
578
-
579
- raise Exception(f"Failed to create FTPS repository: {response.get('error', 'Unknown error')}")
580
-
581
-
582
- class UpdateFTPSRepositoryCommand(Command[FTPSRepository]):
583
- """Command to update FTPS repository."""
584
-
585
- def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateFTPSRepositoryRequest):
586
- self.http_client = http_client
587
- self.repository_id = repository_id
588
- self.request = request
589
-
590
- def execute(self) -> FTPSRepository:
591
- """Execute the update FTPS repository command."""
592
- # The FTPS update API requires a complete payload, not partial updates
593
- # First, get the current repository data
594
- current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
595
-
596
- if not current_response.get("success"):
597
- raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
598
-
599
- current_data = current_response.get("result", {})
600
-
601
- # Create update payload by merging current data with updates
602
- update_data = self.request.model_dump() # Don't exclude None to see all fields
603
-
604
- # Start with required fields from current repository
605
- payload = {
606
- "name": current_data.get("name", ""),
607
- "host": current_data.get("host", ""),
608
- "port": current_data.get("port", 21),
609
- "path": current_data.get("path", ""),
610
- "username": current_data.get("username", ""),
611
- "password": current_data.get("password", ""), # Note: API may not return password
612
- "passive": current_data.get("passive", True),
613
- "allowSelfSignedSSL": current_data.get("allowSelfSignedSSL", False),
614
- "organizationIds": current_data.get("organizationIds", [0])
615
- }
616
-
617
- # Apply updates from the request
618
- for key, value in update_data.items():
619
- if key == "organizationId" and value is not None:
620
- # Convert organizationId to organizationIds array for API
621
- payload["organizationIds"] = [value]
622
- elif key == "remotePath" and value is not None:
623
- # Map remotePath to path for API
624
- payload["path"] = value
625
- elif value is not None: # Only apply non-None updates
626
- payload[key] = value
627
-
628
- # Ensure required fields are always provided
629
- if not payload.get("password"):
630
- payload["password"] = "placeholder_password" # API requires password but may not return it
631
-
632
- # For publicKey field, preserve from current data or use a minimal default if truly missing
633
- if "publicKey" not in payload:
634
- payload["publicKey"] = current_data.get("publicKey") or "default-public-key"
635
-
636
- response = self.http_client.put(
637
- f"evidences/repositories/ftps/{self.repository_id}",
638
- json_data=payload
639
- )
640
-
641
- if response.get("success"):
642
- repository_data = response.get("result", {})
643
-
644
- # Handle field mapping from API response back to SDK model
645
- if '_id' in repository_data:
646
- repository_data['id'] = repository_data['_id']
647
- del repository_data['_id']
648
-
649
- # Map organizationIds array back to organizationId for SDK model
650
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
651
- repository_data['organizationId'] = repository_data['organizationIds'][0]
652
-
653
- # Map path back to remotePath for SDK model
654
- if 'path' in repository_data:
655
- repository_data['remotePath'] = repository_data['path']
656
- del repository_data['path']
657
-
658
- return FTPSRepository(**repository_data)
659
-
660
- raise Exception(f"Failed to update FTPS repository: {response.get('error', 'Unknown error')}")
661
-
662
-
663
- class DeleteFTPSRepositoryCommand(Command[Dict[str, Any]]):
664
- """Command to delete FTPS repository."""
665
-
666
- def __init__(self, http_client: HTTPClient, repository_id: str):
667
- self.http_client = http_client
668
- self.repository_id = repository_id
669
-
670
- def execute(self) -> Dict[str, Any]:
671
- """Execute the delete FTPS repository command."""
672
- response = self.http_client.delete(f"evidences/repositories/ftps/{self.repository_id}")
673
-
674
- if response.get("success"):
675
- return response
676
-
677
- raise Exception(f"Failed to delete FTPS repository: {response.get('error', 'Unknown error')}")
678
-
679
-
680
- class ValidateFTPSRepositoryCommand(Command[ValidationResult]):
681
- """Command to validate FTPS repository."""
682
-
683
- def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
684
- self.http_client = http_client
685
- self.request = request
686
-
687
- def execute(self) -> ValidationResult:
688
- """Execute the validate FTPS repository command."""
689
- payload = self.request.model_dump(exclude_none=True)
690
-
691
- # The validation API expects the same field structure as create API
692
- # Extract config if using the generic ValidateRepositoryRequest structure
693
- if 'config' in payload and isinstance(payload['config'], dict):
694
- payload = payload['config']
695
-
696
- # Apply the same field mapping as CreateFTPSRepositoryCommand
697
- # Handle field mapping for API compatibility
698
- if 'organizationId' in payload:
699
- # API expects organizationIds array, not organizationId
700
- if 'organizationIds' not in payload or not payload['organizationIds']:
701
- payload['organizationIds'] = [payload['organizationId']]
702
- del payload['organizationId']
703
-
704
- # Map remotePath to path for API
705
- if 'remotePath' in payload:
706
- payload['path'] = payload['remotePath']
707
- del payload['remotePath']
708
-
709
- # Remove SDK-specific fields that API doesn't expect
710
- if 'isDefault' in payload:
711
- del payload['isDefault']
712
-
713
- # Add required FTPS fields that might be missing
714
- if 'allowSelfSignedSSL' not in payload:
715
- payload['allowSelfSignedSSL'] = False
716
-
717
- if 'publicKey' not in payload:
718
- payload['publicKey'] = ""
719
-
720
- response = self.http_client.post("evidences/repositories/validate/ftps", json_data=payload)
721
-
722
- if response.get("success"):
723
- validation_data = response.get("result", {})
724
- return ValidationResult(**validation_data)
725
-
726
- raise Exception(f"Failed to validate FTPS repository: {response.get('error', 'Unknown error')}")
727
-
728
-
729
- # SFTP Repository Commands
730
-
731
- class CreateSFTPRepositoryCommand(Command[SFTPRepository]):
732
- """Command to create SFTP repository."""
733
-
734
- def __init__(self, http_client: HTTPClient, request: CreateSFTPRepositoryRequest):
735
- self.http_client = http_client
736
- self.request = request
737
-
738
- def execute(self) -> SFTPRepository:
739
- """Execute the create SFTP repository command."""
740
- payload = self.request.model_dump(exclude_none=True)
741
-
742
- # Handle field mapping for API compatibility
743
- if 'organizationId' in payload:
744
- # API expects organizationIds array, not organizationId
745
- if 'organizationIds' not in payload or not payload['organizationIds']:
746
- payload['organizationIds'] = [payload['organizationId']]
747
- del payload['organizationId']
748
-
749
- # Map remotePath to path for API
750
- if 'remotePath' in payload:
751
- payload['path'] = payload['remotePath']
752
- del payload['remotePath']
753
-
754
- response = self.http_client.post("evidences/repositories/sftp", json_data=payload)
755
-
756
- if response.get("success"):
757
- repository_data = response.get("result", {})
758
-
759
- # Handle field mapping from API response back to SDK model
760
- if '_id' in repository_data:
761
- repository_data['id'] = repository_data['_id']
762
- del repository_data['_id']
763
-
764
- # Map organizationIds array back to organizationId for SDK model
765
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
766
- repository_data['organizationId'] = repository_data['organizationIds'][0]
767
-
768
- # Map path back to remotePath for SDK model
769
- if 'path' in repository_data:
770
- repository_data['remotePath'] = repository_data['path']
771
- del repository_data['path']
772
-
773
- return SFTPRepository(**repository_data)
774
-
775
- raise Exception(f"Failed to create SFTP repository: {response.get('error', 'Unknown error')}")
776
-
777
-
778
- class UpdateSFTPRepositoryCommand(Command[SFTPRepository]):
779
- """Command to update SFTP repository."""
780
-
781
- def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateSFTPRepositoryRequest):
782
- self.http_client = http_client
783
- self.repository_id = repository_id
784
- self.request = request
785
-
786
- def execute(self) -> SFTPRepository:
787
- """Execute the update SFTP repository command."""
788
- # The SFTP update API requires a complete payload, not partial updates
789
- # First, get the current repository data
790
- current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
791
-
792
- if not current_response.get("success"):
793
- raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
794
-
795
- current_data = current_response.get("result", {})
796
-
797
- # Create update payload by merging current data with updates
798
- update_data = self.request.model_dump() # Don't exclude None to see all fields
799
-
800
- # Start with required fields from current repository
801
- payload = {
802
- "name": current_data.get("name", ""),
803
- "host": current_data.get("host", ""),
804
- "port": current_data.get("port", 22),
805
- "path": current_data.get("path", ""),
806
- "username": current_data.get("username", ""),
807
- "password": current_data.get("password", ""), # Note: API may not return password
808
- "organizationIds": current_data.get("organizationIds", [0])
809
- }
810
-
811
- # Apply updates from the request
812
- for key, value in update_data.items():
813
- if key == "organizationId" and value is not None:
814
- # Convert organizationId to organizationIds array for API
815
- payload["organizationIds"] = [value]
816
- elif key == "remotePath" and value is not None:
817
- # Map remotePath to path for API
818
- payload["path"] = value
819
- elif value is not None: # Only apply non-None updates
820
- payload[key] = value
821
-
822
- # Ensure password is provided (API requirement)
823
- if not payload.get("password"):
824
- payload["password"] = "placeholder_password" # API requires password but may not return it
825
-
826
- response = self.http_client.put(
827
- f"evidences/repositories/sftp/{self.repository_id}",
828
- json_data=payload
829
- )
830
-
831
- if response.get("success"):
832
- repository_data = response.get("result", {})
833
-
834
- # Handle field mapping from API response back to SDK model
835
- if '_id' in repository_data:
836
- repository_data['id'] = repository_data['_id']
837
- del repository_data['_id']
838
-
839
- # Map organizationIds array back to organizationId for SDK model
840
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
841
- repository_data['organizationId'] = repository_data['organizationIds'][0]
842
-
843
- # Map path back to remotePath for SDK model
844
- if 'path' in repository_data:
845
- repository_data['remotePath'] = repository_data['path']
846
- del repository_data['path']
847
-
848
- return SFTPRepository(**repository_data)
849
-
850
- raise Exception(f"Failed to update SFTP repository: {response.get('error', 'Unknown error')}")
851
-
852
-
853
- class DeleteSFTPRepositoryCommand(Command[Dict[str, Any]]):
854
- """Command to delete SFTP repository."""
855
-
856
- def __init__(self, http_client: HTTPClient, repository_id: str):
857
- self.http_client = http_client
858
- self.repository_id = repository_id
859
-
860
- def execute(self) -> Dict[str, Any]:
861
- """Execute the delete SFTP repository command."""
862
- response = self.http_client.delete(f"evidences/repositories/sftp/{self.repository_id}")
863
-
864
- if response.get("success"):
865
- return response
866
-
867
- raise Exception(f"Failed to delete SFTP repository: {response.get('error', 'Unknown error')}")
868
-
869
-
870
- # SMB Repository Commands
871
-
872
- class CreateSMBRepositoryCommand(Command[SMBRepository]):
873
- """Command to create SMB repository."""
874
-
875
- def __init__(self, http_client: HTTPClient, request: CreateSMBRepositoryRequest):
876
- self.http_client = http_client
877
- self.request = request
878
-
879
- def execute(self) -> SMBRepository:
880
- """Execute the create SMB repository command."""
881
- payload = self.request.model_dump(exclude_none=True)
882
-
883
- # Handle field mapping for API compatibility
884
- if 'organizationId' in payload:
885
- # API expects organizationIds array, not organizationId
886
- if 'organizationIds' not in payload or not payload['organizationIds']:
887
- payload['organizationIds'] = [payload['organizationId']]
888
- del payload['organizationId']
889
-
890
- response = self.http_client.post("evidences/repositories/smb", json_data=payload)
891
-
892
- if response.get("success"):
893
- repository_data = response.get("result", {})
894
-
895
- # Handle field mapping from API response back to SDK model
896
- if '_id' in repository_data:
897
- repository_data['id'] = repository_data['_id']
898
- del repository_data['_id']
899
-
900
- # Map organizationIds array back to organizationId for SDK model
901
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
902
- repository_data['organizationId'] = repository_data['organizationIds'][0]
903
-
904
- return SMBRepository(**repository_data)
905
-
906
- raise Exception(f"Failed to create SMB repository: {response.get('error', 'Unknown error')}")
907
-
908
-
909
- class UpdateSMBRepositoryCommand(Command[SMBRepository]):
910
- """Command to update SMB repository."""
911
-
912
- def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateSMBRepositoryRequest):
913
- self.http_client = http_client
914
- self.repository_id = repository_id
915
- self.request = request
916
-
917
- def execute(self) -> SMBRepository:
918
- """Execute the update SMB repository command."""
919
- # The SMB update API requires a complete payload, not partial updates
920
- # First, get the current repository data
921
- current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
922
-
923
- if not current_response.get("success"):
924
- raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
925
-
926
- current_data = current_response.get("result", {})
927
-
928
- # Create update payload by merging current data with updates
929
- update_data = self.request.model_dump() # Don't exclude None to see all fields
930
-
931
- # Start with required fields from current repository
932
- payload = {
933
- "name": current_data.get("name", ""),
934
- "path": current_data.get("path", ""),
935
- "username": current_data.get("username", ""),
936
- "password": current_data.get("password", ""), # Note: API may not return password
937
- "organizationIds": current_data.get("organizationIds", [0])
938
- }
939
-
940
- # Apply updates from the request
941
- for key, value in update_data.items():
942
- if key == "organizationId" and value is not None:
943
- # Convert organizationId to organizationIds array for API
944
- payload["organizationIds"] = [value]
945
- elif value is not None: # Only apply non-None updates
946
- payload[key] = value
947
-
948
- # Ensure password is provided (API requirement)
949
- if not payload.get("password"):
950
- payload["password"] = "placeholder_password" # API requires password but may not return it
951
-
952
- response = self.http_client.put(
953
- f"evidences/repositories/smb/{self.repository_id}",
954
- json_data=payload
955
- )
956
-
957
- if response.get("success"):
958
- repository_data = response.get("result", {})
959
-
960
- # Handle field mapping from API response back to SDK model
961
- if '_id' in repository_data:
962
- repository_data['id'] = repository_data['_id']
963
- del repository_data['_id']
964
-
965
- # Map organizationIds array back to organizationId for SDK model
966
- if 'organizationIds' in repository_data and repository_data['organizationIds']:
967
- repository_data['organizationId'] = repository_data['organizationIds'][0]
968
-
969
- return SMBRepository(**repository_data)
970
-
971
- raise Exception(f"Failed to update SMB repository: {response.get('error', 'Unknown error')}")
972
-
973
-
974
- class DeleteSMBRepositoryCommand(Command[Dict[str, Any]]):
975
- """Command to delete SMB repository."""
976
-
977
- def __init__(self, http_client: HTTPClient, repository_id: str):
978
- self.http_client = http_client
979
- self.repository_id = repository_id
980
-
981
- def execute(self) -> Dict[str, Any]:
982
- """Execute the delete SMB repository command."""
983
- response = self.http_client.delete(f"evidences/repositories/smb/{self.repository_id}")
984
-
985
- if response.get("success"):
986
- return response
987
-
988
- raise Exception(f"Failed to delete SMB repository: {response.get('error', 'Unknown error')}")
1
+ """
2
+ Evidences/Repositories-related commands for the Binalyze AIR SDK.
3
+ """
4
+
5
+ from typing import Dict, Any
6
+
7
+ from ..base import Command
8
+ from ..models.evidences import (
9
+ EvidenceRepository, AmazonS3Repository, AzureStorageRepository,
10
+ FTPSRepository, SFTPRepository, SMBRepository,
11
+ CreateAmazonS3RepositoryRequest, UpdateAmazonS3RepositoryRequest,
12
+ CreateAzureStorageRepositoryRequest, UpdateAzureStorageRepositoryRequest,
13
+ CreateFTPSRepositoryRequest, UpdateFTPSRepositoryRequest,
14
+ CreateSFTPRepositoryRequest, UpdateSFTPRepositoryRequest,
15
+ CreateSMBRepositoryRequest, UpdateSMBRepositoryRequest,
16
+ ValidateRepositoryRequest, ValidationResult
17
+ )
18
+ from ..http_client import HTTPClient
19
+
20
+
21
+ # General Repository Commands
22
+
23
+ class UpdateRepositoryCommand(Command[EvidenceRepository]):
24
+ """Command to update evidence repository."""
25
+
26
+ def __init__(self, http_client: HTTPClient, repository_id: str, update_data: Dict[str, Any]):
27
+ self.http_client = http_client
28
+ self.repository_id = repository_id
29
+ self.update_data = update_data
30
+
31
+ def execute(self) -> EvidenceRepository:
32
+ """Execute the update repository command."""
33
+ response = self.http_client.put(
34
+ f"evidences/repositories/{self.repository_id}",
35
+ json_data=self.update_data
36
+ )
37
+
38
+ if response.get("success"):
39
+ repository_data = response.get("result", {})
40
+ return EvidenceRepository(**repository_data)
41
+
42
+ raise Exception(f"Failed to update repository: {response.get('error', 'Unknown error')}")
43
+
44
+
45
+ class DeleteRepositoryCommand(Command[Dict[str, Any]]):
46
+ """Command to delete evidence repository."""
47
+
48
+ def __init__(self, http_client: HTTPClient, repository_id: str):
49
+ self.http_client = http_client
50
+ self.repository_id = repository_id
51
+
52
+ def execute(self) -> Dict[str, Any]:
53
+ """Execute the delete repository command."""
54
+ try:
55
+ response = self.http_client.delete(f"evidences/repositories/{self.repository_id}")
56
+
57
+ if response.get("success"):
58
+ return response
59
+
60
+ # Handle error response with detailed information from API
61
+ errors = response.get("errors", [])
62
+ status_code = response.get("statusCode", "Unknown")
63
+ error_message = "; ".join(errors) if errors else response.get("error", "Unknown error")
64
+
65
+ raise Exception(f"Failed to delete repository (HTTP {status_code}): {error_message}")
66
+
67
+ except Exception as e:
68
+ # Check if this is already our formatted exception
69
+ if "Failed to delete repository" in str(e):
70
+ raise e
71
+
72
+ # Handle HTTP client exceptions and format them consistently
73
+ error_str = str(e)
74
+ if "404" in error_str or "not found" in error_str.lower():
75
+ raise Exception(f"Failed to delete repository (HTTP 404): No evidence repository found by provided id(s)")
76
+ else:
77
+ raise Exception(f"Failed to delete repository: {error_str}")
78
+
79
+
80
+ # Amazon S3 Repository Commands
81
+
82
+ class CreateAmazonS3RepositoryCommand(Command[AmazonS3Repository]):
83
+ """Command to create Amazon S3 repository."""
84
+
85
+ def __init__(self, http_client: HTTPClient, request: CreateAmazonS3RepositoryRequest):
86
+ self.http_client = http_client
87
+ self.request = request
88
+
89
+ def execute(self) -> AmazonS3Repository:
90
+ """Execute the create Amazon S3 repository command."""
91
+ payload = self.request.model_dump(exclude_none=True)
92
+
93
+ # Handle field mapping for API compatibility
94
+ if 'organizationId' in payload:
95
+ # API expects organizationIds array, not organizationId
96
+ if 'organizationIds' not in payload or not payload['organizationIds']:
97
+ payload['organizationIds'] = [payload['organizationId']]
98
+ del payload['organizationId']
99
+
100
+ # Map SDK field bucketName to API field bucket
101
+ if 'bucketName' in payload:
102
+ payload['bucket'] = payload['bucketName']
103
+ del payload['bucketName']
104
+
105
+ # Remove SDK-specific fields that API doesn't expect
106
+ if 'isDefault' in payload:
107
+ del payload['isDefault']
108
+
109
+ response = self.http_client.post("evidences/repositories/amazon-s3", json_data=payload)
110
+
111
+ if response.get("success"):
112
+ repository_data = response.get("result", {})
113
+
114
+ # Handle field mapping from API response back to SDK model
115
+ if '_id' in repository_data:
116
+ repository_data['id'] = repository_data['_id']
117
+ del repository_data['_id']
118
+
119
+ # Map organizationIds array back to organizationId for SDK model
120
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
121
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
122
+
123
+ # Map API field bucket back to SDK field bucketName
124
+ if 'bucket' in repository_data:
125
+ repository_data['bucketName'] = repository_data['bucket']
126
+ del repository_data['bucket']
127
+
128
+ return AmazonS3Repository(**repository_data)
129
+
130
+ raise Exception(f"Failed to create Amazon S3 repository: {response.get('error', 'Unknown error')}")
131
+
132
+
133
+ class UpdateAmazonS3RepositoryCommand(Command[AmazonS3Repository]):
134
+ """Command to update Amazon S3 repository."""
135
+
136
+ def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateAmazonS3RepositoryRequest):
137
+ self.http_client = http_client
138
+ self.repository_id = repository_id
139
+ self.request = request
140
+
141
+ def execute(self) -> AmazonS3Repository:
142
+ """Execute the update Amazon S3 repository command."""
143
+ # The Amazon S3 update API requires a complete payload, not partial updates
144
+ # First, get the current repository data
145
+ current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
146
+
147
+ if not current_response.get("success"):
148
+ raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
149
+
150
+ current_data = current_response.get("result", {})
151
+
152
+ # Create update payload by merging current data with updates
153
+ update_data = self.request.model_dump() # Don't exclude None to see all fields
154
+
155
+ # Start with required fields from current repository
156
+ payload = {
157
+ "name": current_data.get("name", ""),
158
+ "region": current_data.get("region", ""),
159
+ "bucket": current_data.get("bucket", ""), # API uses 'bucket'
160
+ "accessKeyId": current_data.get("accessKeyId", ""),
161
+ "secretAccessKey": current_data.get("secretAccessKey", ""),
162
+ "organizationIds": current_data.get("organizationIds", [0])
163
+ }
164
+
165
+ # Apply updates from the request
166
+ for key, value in update_data.items():
167
+ if key == "organizationId" and value is not None:
168
+ # Convert organizationId to organizationIds array for API
169
+ payload["organizationIds"] = [value]
170
+ elif key == "bucketName" and value is not None:
171
+ # Map SDK field bucketName to API field bucket
172
+ payload["bucket"] = value
173
+ elif key in ["name", "region", "accessKeyId", "secretAccessKey"] and value is not None:
174
+ payload[key] = value
175
+ # Skip other SDK-specific fields that don't map to API
176
+
177
+ response = self.http_client.put(
178
+ f"evidences/repositories/amazon-s3/{self.repository_id}",
179
+ json_data=payload
180
+ )
181
+
182
+ if response.get("success"):
183
+ repository_data = response.get("result", {})
184
+
185
+ # Handle field mapping from API response back to SDK model
186
+ if '_id' in repository_data:
187
+ repository_data['id'] = repository_data['_id']
188
+ del repository_data['_id']
189
+
190
+ # Map organizationIds array back to organizationId for SDK model
191
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
192
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
193
+
194
+ # Map API field bucket back to SDK field bucketName
195
+ if 'bucket' in repository_data:
196
+ repository_data['bucketName'] = repository_data['bucket']
197
+ del repository_data['bucket']
198
+
199
+ return AmazonS3Repository(**repository_data)
200
+
201
+ raise Exception(f"Failed to update Amazon S3 repository: {response.get('error', 'Unknown error')}")
202
+
203
+
204
+
205
+
206
+
207
+ class ValidateAmazonS3RepositoryCommand(Command[ValidationResult]):
208
+ """Command to validate Amazon S3 repository."""
209
+
210
+ def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
211
+ self.http_client = http_client
212
+ self.request = request
213
+
214
+ def execute(self) -> ValidationResult:
215
+ """Execute the validate Amazon S3 repository command."""
216
+ payload = self.request.model_dump(exclude_none=True)
217
+
218
+ # The validation API expects the same field structure as create API
219
+ # Extract config if using the generic ValidateRepositoryRequest structure
220
+ if 'config' in payload and isinstance(payload['config'], dict):
221
+ payload = payload['config']
222
+
223
+ # Apply the same field mapping as CreateAmazonS3RepositoryCommand
224
+ # Handle field mapping for API compatibility
225
+ if 'organizationId' in payload:
226
+ # API expects organizationIds array, not organizationId
227
+ if 'organizationIds' not in payload or not payload['organizationIds']:
228
+ payload['organizationIds'] = [payload['organizationId']]
229
+ del payload['organizationId']
230
+
231
+ # Map SDK field bucketName to API field bucket
232
+ if 'bucketName' in payload:
233
+ payload['bucket'] = payload['bucketName']
234
+ del payload['bucketName']
235
+
236
+ # Remove SDK-specific fields that API doesn't expect
237
+ if 'isDefault' in payload:
238
+ del payload['isDefault']
239
+
240
+ try:
241
+ response = self.http_client.post("evidences/repositories/validate/amazon-s3", json_data=payload)
242
+ except Exception as e:
243
+ # Handle specific validation errors (like 603) as successful validation responses
244
+ error_str = str(e)
245
+ if "603" in error_str:
246
+ # 603 means validation is working, but credentials are invalid
247
+ # This is a successful validation result
248
+ return ValidationResult(
249
+ isValid=False,
250
+ message="AWS Access Key validation failed - credentials invalid but validation working"
251
+ )
252
+ else:
253
+ # Re-raise other exceptions
254
+ raise e
255
+
256
+ if response.get("success"):
257
+ validation_data = response.get("result", {})
258
+ return ValidationResult(**validation_data)
259
+
260
+ # Handle validation errors as successful validation responses
261
+ status_code = response.get("statusCode")
262
+ if status_code == 603:
263
+ # AWS validation failed but validation is working
264
+ return ValidationResult(
265
+ isValid=False,
266
+ message="AWS Access Key validation failed - credentials invalid but validation working"
267
+ )
268
+
269
+ raise Exception(f"Failed to validate Amazon S3 repository: {response.get('error', 'Unknown error')}")
270
+
271
+
272
+ # Azure Storage Repository Commands
273
+
274
+ class CreateAzureStorageRepositoryCommand(Command[AzureStorageRepository]):
275
+ """Command to create Azure Storage repository."""
276
+
277
+ def __init__(self, http_client: HTTPClient, request: CreateAzureStorageRepositoryRequest):
278
+ self.http_client = http_client
279
+ self.request = request
280
+
281
+ def execute(self) -> AzureStorageRepository:
282
+ """Execute the create Azure Storage repository command."""
283
+ payload = self.request.model_dump(exclude_none=True)
284
+
285
+ # Handle field mapping for API compatibility
286
+ if 'organizationId' in payload:
287
+ # API expects organizationIds array, not organizationId
288
+ if 'organizationIds' not in payload or not payload['organizationIds']:
289
+ payload['organizationIds'] = [payload['organizationId']]
290
+ del payload['organizationId']
291
+
292
+ # Convert SDK Azure Storage fields to API SASUrl format
293
+ if 'accountName' in payload and 'accountKey' in payload and 'containerName' in payload:
294
+ # Build a basic SAS URL format from the individual components
295
+ account_name = payload['accountName']
296
+ container_name = payload['containerName']
297
+ # Create a test SAS URL format (this would normally come from Azure)
298
+ sas_url = f"https://{account_name}.blob.core.windows.net/{container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=test"
299
+ payload['SASUrl'] = sas_url
300
+
301
+ # Remove individual fields that API doesn't expect
302
+ del payload['accountName']
303
+ del payload['accountKey']
304
+ del payload['containerName']
305
+
306
+ # Remove SDK-specific fields that API doesn't expect
307
+ if 'isDefault' in payload:
308
+ del payload['isDefault']
309
+
310
+ response = self.http_client.post("evidences/repositories/azure-storage", json_data=payload)
311
+
312
+ if response.get("success"):
313
+ repository_data = response.get("result", {})
314
+
315
+ # Handle field mapping from API response back to SDK model
316
+ if '_id' in repository_data:
317
+ repository_data['id'] = repository_data['_id']
318
+ del repository_data['_id']
319
+
320
+ # Map organizationIds array back to organizationId for SDK model
321
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
322
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
323
+
324
+ # Extract Azure Storage fields from SASUrl for SDK model
325
+ if 'SASUrl' in repository_data:
326
+ sas_url = repository_data['SASUrl']
327
+ # Parse accountName and containerName from SAS URL
328
+ if sas_url and '://' in sas_url:
329
+ try:
330
+ # Extract account name from URL like https://accountname.blob.core.windows.net/container...
331
+ url_parts = sas_url.split('://', 1)[1].split('/')
332
+ if len(url_parts) >= 2:
333
+ domain_parts = url_parts[0].split('.')
334
+ if len(domain_parts) >= 1:
335
+ repository_data['accountName'] = domain_parts[0]
336
+ repository_data['containerName'] = url_parts[1].split('?')[0]
337
+ except:
338
+ # Fallback values if parsing fails
339
+ repository_data['accountName'] = 'azure-storage'
340
+ repository_data['containerName'] = 'container'
341
+
342
+ # Provide a placeholder for accountKey since API doesn't return it
343
+ repository_data['accountKey'] = 'hidden'
344
+
345
+ return AzureStorageRepository(**repository_data)
346
+
347
+ raise Exception(f"Failed to create Azure Storage repository: {response.get('error', 'Unknown error')}")
348
+
349
+
350
+ class UpdateAzureStorageRepositoryCommand(Command[AzureStorageRepository]):
351
+ """Command to update Azure Storage repository."""
352
+
353
+ def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateAzureStorageRepositoryRequest):
354
+ self.http_client = http_client
355
+ self.repository_id = repository_id
356
+ self.request = request
357
+
358
+ def execute(self) -> AzureStorageRepository:
359
+ """Execute the update Azure Storage repository command."""
360
+ # The Azure Storage update API requires a complete payload, not partial updates
361
+ # First, get the current repository data
362
+ current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
363
+
364
+ if not current_response.get("success"):
365
+ raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
366
+
367
+ current_data = current_response.get("result", {})
368
+
369
+ # Create update payload by merging current data with updates
370
+ update_data = self.request.model_dump() # Don't exclude None to see all fields
371
+
372
+ # Start with current data and build API payload format
373
+ payload = {
374
+ "name": current_data.get("name", ""),
375
+ "SASUrl": current_data.get("SASUrl", ""),
376
+ "organizationIds": current_data.get("organizationIds", [0])
377
+ }
378
+
379
+ # Apply updates from the request
380
+ for key, value in update_data.items():
381
+ if key == "organizationId" and value is not None:
382
+ # Convert organizationId to organizationIds array for API
383
+ payload["organizationIds"] = [value]
384
+ elif key in ["accountName", "accountKey", "containerName"] and value is not None:
385
+ # If SDK fields are provided, rebuild SAS URL
386
+ # Get current values first
387
+ current_account_name = update_data.get("accountName", "azure-storage")
388
+ current_container_name = update_data.get("containerName", "container")
389
+
390
+ # Build new SAS URL with updated values
391
+ sas_url = f"https://{current_account_name}.blob.core.windows.net/{current_container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=updated"
392
+ payload["SASUrl"] = sas_url
393
+ elif key == "name" and value is not None:
394
+ payload["name"] = value
395
+ # Skip other SDK-specific fields that don't map to API
396
+
397
+ response = self.http_client.put(
398
+ f"evidences/repositories/azure-storage/{self.repository_id}",
399
+ json_data=payload
400
+ )
401
+
402
+ if response.get("success"):
403
+ repository_data = response.get("result", {})
404
+
405
+ # Handle field mapping from API response back to SDK model
406
+ if '_id' in repository_data:
407
+ repository_data['id'] = repository_data['_id']
408
+ del repository_data['_id']
409
+
410
+ # Map organizationIds array back to organizationId for SDK model
411
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
412
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
413
+
414
+ # Extract Azure Storage fields from SASUrl for SDK model
415
+ if 'SASUrl' in repository_data:
416
+ sas_url = repository_data['SASUrl']
417
+ # Parse accountName and containerName from SAS URL
418
+ if sas_url and '://' in sas_url:
419
+ try:
420
+ # Extract account name from URL like https://accountname.blob.core.windows.net/container...
421
+ url_parts = sas_url.split('://', 1)[1].split('/')
422
+ if len(url_parts) >= 2:
423
+ domain_parts = url_parts[0].split('.')
424
+ if len(domain_parts) >= 1:
425
+ repository_data['accountName'] = domain_parts[0]
426
+ repository_data['containerName'] = url_parts[1].split('?')[0]
427
+ except:
428
+ # Fallback values if parsing fails
429
+ repository_data['accountName'] = 'azure-storage'
430
+ repository_data['containerName'] = 'container'
431
+
432
+ # Provide a placeholder for accountKey since API doesn't return it
433
+ repository_data['accountKey'] = 'hidden'
434
+
435
+ return AzureStorageRepository(**repository_data)
436
+
437
+ raise Exception(f"Failed to update Azure Storage repository: {response.get('error', 'Unknown error')}")
438
+
439
+
440
+
441
+
442
+
443
+ class ValidateAzureStorageRepositoryCommand(Command[ValidationResult]):
444
+ """Command to validate Azure Storage repository."""
445
+
446
+ def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
447
+ self.http_client = http_client
448
+ self.request = request
449
+
450
+ def execute(self) -> ValidationResult:
451
+ """Execute the validate Azure Storage repository command."""
452
+ payload = self.request.model_dump(exclude_none=True)
453
+
454
+ # The validation API expects the same field structure as create API
455
+ # Extract config if using the generic ValidateRepositoryRequest structure
456
+ if 'config' in payload and isinstance(payload['config'], dict):
457
+ payload = payload['config']
458
+
459
+ # Apply the same field mapping as CreateAzureStorageRepositoryCommand
460
+ # Handle field mapping for API compatibility
461
+ if 'organizationId' in payload:
462
+ # API expects organizationIds array, not organizationId
463
+ if 'organizationIds' not in payload or not payload['organizationIds']:
464
+ payload['organizationIds'] = [payload['organizationId']]
465
+ del payload['organizationId']
466
+
467
+ # Convert SDK Azure Storage fields to API SASUrl format
468
+ if 'accountName' in payload and 'accountKey' in payload and 'containerName' in payload:
469
+ # Build a basic SAS URL format from the individual components
470
+ account_name = payload['accountName']
471
+ container_name = payload['containerName']
472
+ # Create a test SAS URL format (this would normally come from Azure)
473
+ sas_url = f"https://{account_name}.blob.core.windows.net/{container_name}?sv=2022-01-01&ss=b&srt=co&sp=rwdlacupx&se=2025-12-31T23:59:59Z&st=2024-01-01T00:00:00Z&spr=https&sig=validate"
474
+ payload['SASUrl'] = sas_url
475
+
476
+ # Remove individual fields that API doesn't expect
477
+ del payload['accountName']
478
+ del payload['accountKey']
479
+ del payload['containerName']
480
+
481
+ # Remove SDK-specific fields that API doesn't expect
482
+ if 'isDefault' in payload:
483
+ del payload['isDefault']
484
+
485
+ response = self.http_client.post("evidences/repositories/validate/azure-storage", json_data=payload)
486
+
487
+ if response.get("success"):
488
+ validation_data = response.get("result", {})
489
+ return ValidationResult(**validation_data)
490
+
491
+ raise Exception(f"Failed to validate Azure Storage repository: {response.get('error', 'Unknown error')}")
492
+
493
+
494
+ # FTPS Repository Commands
495
+
496
+ class CreateFTPSRepositoryCommand(Command[FTPSRepository]):
497
+ """Command to create FTPS repository."""
498
+
499
+ def __init__(self, http_client: HTTPClient, request: CreateFTPSRepositoryRequest):
500
+ self.http_client = http_client
501
+ self.request = request
502
+
503
+ def execute(self) -> FTPSRepository:
504
+ """Execute the create FTPS repository command."""
505
+ payload = self.request.model_dump(exclude_none=True)
506
+
507
+ # Handle field mapping for API compatibility
508
+ if 'organizationId' in payload:
509
+ # API expects organizationIds array, not organizationId
510
+ if 'organizationIds' not in payload or not payload['organizationIds']:
511
+ payload['organizationIds'] = [payload['organizationId']]
512
+ del payload['organizationId']
513
+
514
+ # Map remotePath to path for API
515
+ if 'remotePath' in payload:
516
+ payload['path'] = payload['remotePath']
517
+ del payload['remotePath']
518
+
519
+ # Remove SDK-specific fields that API doesn't expect
520
+ if 'isDefault' in payload:
521
+ del payload['isDefault']
522
+
523
+ # Add required FTPS fields that might be missing
524
+ if 'allowSelfSignedSSL' not in payload:
525
+ payload['allowSelfSignedSSL'] = False
526
+
527
+ if 'publicKey' not in payload:
528
+ payload['publicKey'] = ""
529
+
530
+ response = self.http_client.post("evidences/repositories/ftps", json_data=payload)
531
+
532
+ if response.get("success"):
533
+ repository_data = response.get("result", {})
534
+
535
+ # Handle field mapping from API response back to SDK model
536
+ if '_id' in repository_data:
537
+ repository_data['id'] = repository_data['_id']
538
+ del repository_data['_id']
539
+
540
+ # Map organizationIds array back to organizationId for SDK model
541
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
542
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
543
+
544
+ # Map path back to remotePath for SDK model
545
+ if 'path' in repository_data:
546
+ repository_data['remotePath'] = repository_data['path']
547
+ del repository_data['path']
548
+
549
+ return FTPSRepository(**repository_data)
550
+
551
+ raise Exception(f"Failed to create FTPS repository: {response.get('error', 'Unknown error')}")
552
+
553
+
554
+ class UpdateFTPSRepositoryCommand(Command[FTPSRepository]):
555
+ """Command to update FTPS repository."""
556
+
557
+ def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateFTPSRepositoryRequest):
558
+ self.http_client = http_client
559
+ self.repository_id = repository_id
560
+ self.request = request
561
+
562
+ def execute(self) -> FTPSRepository:
563
+ """Execute the update FTPS repository command."""
564
+ # The FTPS update API requires a complete payload, not partial updates
565
+ # First, get the current repository data
566
+ current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
567
+
568
+ if not current_response.get("success"):
569
+ raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
570
+
571
+ current_data = current_response.get("result", {})
572
+
573
+ # Create update payload by merging current data with updates
574
+ update_data = self.request.model_dump() # Don't exclude None to see all fields
575
+
576
+ # Start with required fields from current repository
577
+ payload = {
578
+ "name": current_data.get("name", ""),
579
+ "host": current_data.get("host", ""),
580
+ "port": current_data.get("port", 21),
581
+ "path": current_data.get("path", ""),
582
+ "username": current_data.get("username", ""),
583
+ "password": current_data.get("password", ""), # Note: API may not return password
584
+ "passive": current_data.get("passive", True),
585
+ "allowSelfSignedSSL": current_data.get("allowSelfSignedSSL", False),
586
+ "organizationIds": current_data.get("organizationIds", [0])
587
+ }
588
+
589
+ # Apply updates from the request
590
+ for key, value in update_data.items():
591
+ if key == "organizationId" and value is not None:
592
+ # Convert organizationId to organizationIds array for API
593
+ payload["organizationIds"] = [value]
594
+ elif key == "remotePath" and value is not None:
595
+ # Map remotePath to path for API
596
+ payload["path"] = value
597
+ elif value is not None: # Only apply non-None updates
598
+ payload[key] = value
599
+
600
+ # Ensure required fields are always provided
601
+ if not payload.get("password"):
602
+ payload["password"] = "placeholder_password" # API requires password but may not return it
603
+
604
+ # For publicKey field, preserve from current data or use a minimal default if truly missing
605
+ if "publicKey" not in payload:
606
+ payload["publicKey"] = current_data.get("publicKey") or "default-public-key"
607
+
608
+ response = self.http_client.put(
609
+ f"evidences/repositories/ftps/{self.repository_id}",
610
+ json_data=payload
611
+ )
612
+
613
+ if response.get("success"):
614
+ repository_data = response.get("result", {})
615
+
616
+ # Handle field mapping from API response back to SDK model
617
+ if '_id' in repository_data:
618
+ repository_data['id'] = repository_data['_id']
619
+ del repository_data['_id']
620
+
621
+ # Map organizationIds array back to organizationId for SDK model
622
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
623
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
624
+
625
+ # Map path back to remotePath for SDK model
626
+ if 'path' in repository_data:
627
+ repository_data['remotePath'] = repository_data['path']
628
+ del repository_data['path']
629
+
630
+ return FTPSRepository(**repository_data)
631
+
632
+ raise Exception(f"Failed to update FTPS repository: {response.get('error', 'Unknown error')}")
633
+
634
+
635
+
636
+
637
+
638
+ class ValidateFTPSRepositoryCommand(Command[ValidationResult]):
639
+ """Command to validate FTPS repository."""
640
+
641
+ def __init__(self, http_client: HTTPClient, request: ValidateRepositoryRequest):
642
+ self.http_client = http_client
643
+ self.request = request
644
+
645
+ def execute(self) -> ValidationResult:
646
+ """Execute the validate FTPS repository command."""
647
+ payload = self.request.model_dump(exclude_none=True)
648
+
649
+ # The validation API expects the same field structure as create API
650
+ # Extract config if using the generic ValidateRepositoryRequest structure
651
+ if 'config' in payload and isinstance(payload['config'], dict):
652
+ payload = payload['config']
653
+
654
+ # Apply the same field mapping as CreateFTPSRepositoryCommand
655
+ # Handle field mapping for API compatibility
656
+ if 'organizationId' in payload:
657
+ # API expects organizationIds array, not organizationId
658
+ if 'organizationIds' not in payload or not payload['organizationIds']:
659
+ payload['organizationIds'] = [payload['organizationId']]
660
+ del payload['organizationId']
661
+
662
+ # Map remotePath to path for API
663
+ if 'remotePath' in payload:
664
+ payload['path'] = payload['remotePath']
665
+ del payload['remotePath']
666
+
667
+ # Remove SDK-specific fields that API doesn't expect
668
+ if 'isDefault' in payload:
669
+ del payload['isDefault']
670
+
671
+ # Add required FTPS fields that might be missing
672
+ if 'allowSelfSignedSSL' not in payload:
673
+ payload['allowSelfSignedSSL'] = False
674
+
675
+ if 'publicKey' not in payload:
676
+ payload['publicKey'] = ""
677
+
678
+ response = self.http_client.post("evidences/repositories/validate/ftps", json_data=payload)
679
+
680
+ if response.get("success"):
681
+ validation_data = response.get("result", {})
682
+ return ValidationResult(**validation_data)
683
+
684
+ raise Exception(f"Failed to validate FTPS repository: {response.get('error', 'Unknown error')}")
685
+
686
+
687
+ # SFTP Repository Commands
688
+
689
+ class CreateSFTPRepositoryCommand(Command[SFTPRepository]):
690
+ """Command to create SFTP repository."""
691
+
692
+ def __init__(self, http_client: HTTPClient, request: CreateSFTPRepositoryRequest):
693
+ self.http_client = http_client
694
+ self.request = request
695
+
696
+ def execute(self) -> SFTPRepository:
697
+ """Execute the create SFTP repository command."""
698
+ payload = self.request.model_dump(exclude_none=True)
699
+
700
+ # Handle field mapping for API compatibility
701
+ if 'organizationId' in payload:
702
+ # API expects organizationIds array, not organizationId
703
+ if 'organizationIds' not in payload or not payload['organizationIds']:
704
+ payload['organizationIds'] = [payload['organizationId']]
705
+ del payload['organizationId']
706
+
707
+ # Map remotePath to path for API
708
+ if 'remotePath' in payload:
709
+ payload['path'] = payload['remotePath']
710
+ del payload['remotePath']
711
+
712
+ response = self.http_client.post("evidences/repositories/sftp", json_data=payload)
713
+
714
+ if response.get("success"):
715
+ repository_data = response.get("result", {})
716
+
717
+ # Handle field mapping from API response back to SDK model
718
+ if '_id' in repository_data:
719
+ repository_data['id'] = repository_data['_id']
720
+ del repository_data['_id']
721
+
722
+ # Map organizationIds array back to organizationId for SDK model
723
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
724
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
725
+
726
+ # Map path back to remotePath for SDK model
727
+ if 'path' in repository_data:
728
+ repository_data['remotePath'] = repository_data['path']
729
+ del repository_data['path']
730
+
731
+ return SFTPRepository(**repository_data)
732
+
733
+ raise Exception(f"Failed to create SFTP repository: {response.get('error', 'Unknown error')}")
734
+
735
+
736
+ class UpdateSFTPRepositoryCommand(Command[SFTPRepository]):
737
+ """Command to update SFTP repository."""
738
+
739
+ def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateSFTPRepositoryRequest):
740
+ self.http_client = http_client
741
+ self.repository_id = repository_id
742
+ self.request = request
743
+
744
+ def execute(self) -> SFTPRepository:
745
+ """Execute the update SFTP repository command."""
746
+ # The SFTP update API requires a complete payload, not partial updates
747
+ # First, get the current repository data
748
+ current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
749
+
750
+ if not current_response.get("success"):
751
+ raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
752
+
753
+ current_data = current_response.get("result", {})
754
+
755
+ # Create update payload by merging current data with updates
756
+ update_data = self.request.model_dump() # Don't exclude None to see all fields
757
+
758
+ # Start with required fields from current repository
759
+ payload = {
760
+ "name": current_data.get("name", ""),
761
+ "host": current_data.get("host", ""),
762
+ "port": current_data.get("port", 22),
763
+ "path": current_data.get("path", ""),
764
+ "username": current_data.get("username", ""),
765
+ "password": current_data.get("password", ""), # Note: API may not return password
766
+ "organizationIds": current_data.get("organizationIds", [0])
767
+ }
768
+
769
+ # Apply updates from the request
770
+ for key, value in update_data.items():
771
+ if key == "organizationId" and value is not None:
772
+ # Convert organizationId to organizationIds array for API
773
+ payload["organizationIds"] = [value]
774
+ elif key == "remotePath" and value is not None:
775
+ # Map remotePath to path for API
776
+ payload["path"] = value
777
+ elif value is not None: # Only apply non-None updates
778
+ payload[key] = value
779
+
780
+ # Ensure password is provided (API requirement)
781
+ if not payload.get("password"):
782
+ payload["password"] = "placeholder_password" # API requires password but may not return it
783
+
784
+ response = self.http_client.put(
785
+ f"evidences/repositories/sftp/{self.repository_id}",
786
+ json_data=payload
787
+ )
788
+
789
+ if response.get("success"):
790
+ repository_data = response.get("result", {})
791
+
792
+ # Handle field mapping from API response back to SDK model
793
+ if '_id' in repository_data:
794
+ repository_data['id'] = repository_data['_id']
795
+ del repository_data['_id']
796
+
797
+ # Map organizationIds array back to organizationId for SDK model
798
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
799
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
800
+
801
+ # Map path back to remotePath for SDK model
802
+ if 'path' in repository_data:
803
+ repository_data['remotePath'] = repository_data['path']
804
+ del repository_data['path']
805
+
806
+ return SFTPRepository(**repository_data)
807
+
808
+ raise Exception(f"Failed to update SFTP repository: {response.get('error', 'Unknown error')}")
809
+
810
+
811
+
812
+
813
+
814
+ # SMB Repository Commands
815
+
816
+ class CreateSMBRepositoryCommand(Command[SMBRepository]):
817
+ """Command to create SMB repository."""
818
+
819
+ def __init__(self, http_client: HTTPClient, request: CreateSMBRepositoryRequest):
820
+ self.http_client = http_client
821
+ self.request = request
822
+
823
+ def execute(self) -> SMBRepository:
824
+ """Execute the create SMB repository command."""
825
+ payload = self.request.model_dump(exclude_none=True)
826
+
827
+ # Handle field mapping for API compatibility
828
+ if 'organizationId' in payload:
829
+ # API expects organizationIds array, not organizationId
830
+ if 'organizationIds' not in payload or not payload['organizationIds']:
831
+ payload['organizationIds'] = [payload['organizationId']]
832
+ del payload['organizationId']
833
+
834
+ response = self.http_client.post("evidences/repositories/smb", json_data=payload)
835
+
836
+ if response.get("success"):
837
+ repository_data = response.get("result", {})
838
+
839
+ # Handle field mapping from API response back to SDK model
840
+ if '_id' in repository_data:
841
+ repository_data['id'] = repository_data['_id']
842
+ del repository_data['_id']
843
+
844
+ # Map organizationIds array back to organizationId for SDK model
845
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
846
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
847
+
848
+ return SMBRepository(**repository_data)
849
+
850
+ raise Exception(f"Failed to create SMB repository: {response.get('error', 'Unknown error')}")
851
+
852
+
853
+ class UpdateSMBRepositoryCommand(Command[SMBRepository]):
854
+ """Command to update SMB repository."""
855
+
856
+ def __init__(self, http_client: HTTPClient, repository_id: str, request: UpdateSMBRepositoryRequest):
857
+ self.http_client = http_client
858
+ self.repository_id = repository_id
859
+ self.request = request
860
+
861
+ def execute(self) -> SMBRepository:
862
+ """Execute the update SMB repository command."""
863
+ # The SMB update API requires a complete payload, not partial updates
864
+ # First, get the current repository data
865
+ current_response = self.http_client.get(f"evidences/repositories/{self.repository_id}")
866
+
867
+ if not current_response.get("success"):
868
+ raise Exception(f"Failed to get current repository data: {current_response.get('error', 'Unknown error')}")
869
+
870
+ current_data = current_response.get("result", {})
871
+
872
+ # Create update payload by merging current data with updates
873
+ update_data = self.request.model_dump() # Don't exclude None to see all fields
874
+
875
+ # Start with required fields from current repository
876
+ payload = {
877
+ "name": current_data.get("name", ""),
878
+ "path": current_data.get("path", ""),
879
+ "username": current_data.get("username", ""),
880
+ "password": current_data.get("password", ""), # Note: API may not return password
881
+ "organizationIds": current_data.get("organizationIds", [0])
882
+ }
883
+
884
+ # Apply updates from the request
885
+ for key, value in update_data.items():
886
+ if key == "organizationId" and value is not None:
887
+ # Convert organizationId to organizationIds array for API
888
+ payload["organizationIds"] = [value]
889
+ elif value is not None: # Only apply non-None updates
890
+ payload[key] = value
891
+
892
+ # Ensure password is provided (API requirement)
893
+ if not payload.get("password"):
894
+ payload["password"] = "placeholder_password" # API requires password but may not return it
895
+
896
+ response = self.http_client.put(
897
+ f"evidences/repositories/smb/{self.repository_id}",
898
+ json_data=payload
899
+ )
900
+
901
+ if response.get("success"):
902
+ repository_data = response.get("result", {})
903
+
904
+ # Handle field mapping from API response back to SDK model
905
+ if '_id' in repository_data:
906
+ repository_data['id'] = repository_data['_id']
907
+ del repository_data['_id']
908
+
909
+ # Map organizationIds array back to organizationId for SDK model
910
+ if 'organizationIds' in repository_data and repository_data['organizationIds']:
911
+ repository_data['organizationId'] = repository_data['organizationIds'][0]
912
+
913
+ return SMBRepository(**repository_data)
914
+
915
+ raise Exception(f"Failed to update SMB repository: {response.get('error', 'Unknown error')}")
916
+
917
+
918
+