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,417 +1,401 @@
1
- """
2
- Audit-related queries for the Binalyze AIR SDK.
3
- """
4
-
5
- from typing import List, Optional, Dict, Any
6
- from datetime import datetime
7
-
8
- from ..base import Query
9
- from ..models.audit import (
10
- AuditLog, AuditSummary, AuditUserActivity, AuditSystemEvent,
11
- AuditRetentionPolicy, AuditFilter, AuditLogsFilter, AuditLevel, AuditCategory, AuditAction
12
- )
13
- from ..http_client import HTTPClient
14
-
15
-
16
- class ListAuditLogsQuery(Query[List[AuditLog]]):
17
- """Query to list audit logs with optional filtering - UPDATED for new POST-based API."""
18
-
19
- def __init__(self, http_client: HTTPClient, filter_params: Optional[AuditLogsFilter] = None, organization_ids: Optional[int] = None):
20
- self.http_client = http_client
21
- # Initialize filter with default organization IDs if not provided
22
- if filter_params is None:
23
- filter_params = AuditLogsFilter()
24
-
25
- # Set organization parameters if not already set in filter
26
- # Changed from List[int] to int to match new API spec
27
- if filter_params.organization_ids is None and organization_ids is not None:
28
- filter_params.organization_ids = organization_ids
29
- elif filter_params.organization_ids is None:
30
- filter_params.organization_ids = 0 # Default to organization 0
31
-
32
- self.filter_params = filter_params
33
-
34
- def execute(self) -> List[AuditLog]:
35
- """Execute the query to list audit logs using NEW POST-based API."""
36
- # Use new JSON body format for POST request
37
- json_body = self.filter_params.to_json_body()
38
-
39
- # If no explicit filter provided, ensure we have minimum required structure
40
- if not json_body:
41
- json_body = {
42
- "pageNumber": 1,
43
- "filter": {
44
- "organizationIds": self.filter_params.organization_ids or 0
45
- }
46
- }
47
-
48
- print(f"[INFO] Using POST /audit-logs with body: {json_body}")
49
-
50
- try:
51
- # NEW: Use POST method with JSON body instead of GET with query params
52
- response = self.http_client.post("audit-logs", data=json_body)
53
-
54
- # Handle response structure - may be different from old GET endpoint
55
- if isinstance(response, dict):
56
- # Try different response structures
57
- entities = (
58
- response.get("result", {}).get("entities", []) or # Standard structure
59
- response.get("entities", []) or # Direct entities
60
- response.get("data", []) or # Alternative structure
61
- response.get("logs", []) or # Logs structure
62
- []
63
- )
64
- else:
65
- entities = []
66
-
67
- print(f"[INFO] POST /audit-logs returned {len(entities)} audit logs")
68
-
69
- except Exception as e:
70
- print(f"[WARN] POST /audit-logs failed, trying fallback to GET method: {e}")
71
-
72
- # FALLBACK: Try old GET method for backward compatibility
73
- try:
74
- params = self.filter_params.to_params() # This will show deprecation warning
75
- response = self.http_client.get("audit-logs", params=params)
76
- entities = response.get("result", {}).get("entities", [])
77
- print(f"[INFO] Fallback GET /audit-logs returned {len(entities)} audit logs")
78
- except Exception as fallback_e:
79
- print(f"[WARN] Both POST and GET methods failed: POST={e}, GET={fallback_e}")
80
- return []
81
-
82
- logs = []
83
- for entity_data in entities:
84
- mapped_data = {
85
- "id": entity_data.get("_id") or entity_data.get("id"),
86
- "timestamp": entity_data.get("createdAt") or entity_data.get("timestamp"),
87
- "user_id": entity_data.get("userId"),
88
- "username": entity_data.get("performedBy") or entity_data.get("username"),
89
- "organization_id": entity_data.get("organizationId", 0),
90
- "category": entity_data.get("type") or entity_data.get("category"),
91
- "action": entity_data.get("action"),
92
- "resource_type": entity_data.get("resourceType"),
93
- "resource_id": entity_data.get("resourceId"),
94
- "resource_name": entity_data.get("resourceName"),
95
- "level": entity_data.get("level", "info"),
96
- "message": entity_data.get("description") or entity_data.get("message"),
97
- "details": entity_data.get("details", {}),
98
- "ip_address": entity_data.get("ipAddress"),
99
- "user_agent": entity_data.get("userAgent"),
100
- "session_id": entity_data.get("sessionId"),
101
- "correlation_id": entity_data.get("correlationId"),
102
- "success": entity_data.get("success", True),
103
- "error_code": entity_data.get("errorCode"),
104
- "duration": entity_data.get("duration"),
105
- "tags": entity_data.get("tags", []),
106
- }
107
-
108
- # Remove None values
109
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
110
-
111
- logs.append(AuditLog(**mapped_data))
112
-
113
- return logs
114
-
115
-
116
- class GetAuditLogQuery(Query[AuditLog]):
117
- """Query to get a specific audit log by ID."""
118
-
119
- def __init__(self, http_client: HTTPClient, log_id: str):
120
- self.http_client = http_client
121
- self.log_id = log_id
122
-
123
- def execute(self) -> AuditLog:
124
- """Execute the query to get audit log details."""
125
- response = self.http_client.get(f"audit-logs/{self.log_id}")
126
-
127
- entity_data = response.get("result", {})
128
-
129
- mapped_data = {
130
- "id": entity_data.get("_id"),
131
- "timestamp": entity_data.get("createdAt"),
132
- "user_id": entity_data.get("userId"),
133
- "username": entity_data.get("performedBy"),
134
- "organization_id": entity_data.get("organizationId", 0),
135
- "category": entity_data.get("type"),
136
- "action": entity_data.get("action"),
137
- "resource_type": entity_data.get("resourceType"),
138
- "resource_id": entity_data.get("resourceId"),
139
- "resource_name": entity_data.get("resourceName"),
140
- "level": entity_data.get("level", "info"),
141
- "message": entity_data.get("description"),
142
- "details": entity_data.get("details", {}),
143
- "ip_address": entity_data.get("ipAddress"),
144
- "user_agent": entity_data.get("userAgent"),
145
- "session_id": entity_data.get("sessionId"),
146
- "correlation_id": entity_data.get("correlationId"),
147
- "success": entity_data.get("success", True),
148
- "error_code": entity_data.get("errorCode"),
149
- "duration": entity_data.get("duration"),
150
- "tags": entity_data.get("tags", []),
151
- }
152
-
153
- # Remove None values
154
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
155
-
156
- return AuditLog(**mapped_data)
157
-
158
-
159
- class GetAuditSummaryQuery(Query[AuditSummary]):
160
- """Query to get audit summary for a date range."""
161
-
162
- def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime):
163
- self.http_client = http_client
164
- self.organization_id = organization_id
165
- self.start_date = start_date
166
- self.end_date = end_date
167
-
168
- def execute(self) -> AuditSummary:
169
- """Execute the query to get audit summary."""
170
- params = {
171
- "organizationId": str(self.organization_id),
172
- "startDate": self.start_date.isoformat(),
173
- "endDate": self.end_date.isoformat()
174
- }
175
-
176
- response = self.http_client.get("audit/summary", params=params)
177
-
178
- entity_data = response.get("result", {})
179
-
180
- mapped_data = {
181
- "organization_id": entity_data.get("organizationId", self.organization_id),
182
- "date": entity_data.get("date", self.start_date),
183
- "total_events": entity_data.get("totalEvents", 0),
184
- "successful_events": entity_data.get("successfulEvents", 0),
185
- "failed_events": entity_data.get("failedEvents", 0),
186
- "authentication_events": entity_data.get("authenticationEvents", 0),
187
- "authorization_events": entity_data.get("authorizationEvents", 0),
188
- "data_access_events": entity_data.get("dataAccessEvents", 0),
189
- "system_change_events": entity_data.get("systemChangeEvents", 0),
190
- "user_action_events": entity_data.get("userActionEvents", 0),
191
- "api_call_events": entity_data.get("apiCallEvents", 0),
192
- "unique_users": entity_data.get("uniqueUsers", 0),
193
- "unique_ips": entity_data.get("uniqueIps", 0),
194
- "top_users": entity_data.get("topUsers", []),
195
- "top_actions": entity_data.get("topActions", []),
196
- "error_summary": entity_data.get("errorSummary", []),
197
- }
198
-
199
- # Remove None values
200
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
201
-
202
- return AuditSummary(**mapped_data)
203
-
204
-
205
- class GetUserActivityQuery(Query[List[AuditUserActivity]]):
206
- """Query to get user activity audit logs."""
207
-
208
- def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime, user_id: Optional[str] = None):
209
- self.http_client = http_client
210
- self.organization_id = organization_id
211
- self.start_date = start_date
212
- self.end_date = end_date
213
- self.user_id = user_id
214
-
215
- def execute(self) -> List[AuditUserActivity]:
216
- """Execute the query to get user activity."""
217
- params = {
218
- "organizationId": str(self.organization_id),
219
- "startDate": self.start_date.isoformat(),
220
- "endDate": self.end_date.isoformat()
221
- }
222
-
223
- if self.user_id:
224
- params["userId"] = self.user_id
225
-
226
- response = self.http_client.get("audit/user-activity", params=params)
227
-
228
- entities = response.get("result", {}).get("entities", [])
229
-
230
- activities = []
231
- for entity_data in entities:
232
- mapped_data = {
233
- "user_id": entity_data.get("userId"),
234
- "username": entity_data.get("username"),
235
- "organization_id": entity_data.get("organizationId", self.organization_id),
236
- "date": entity_data.get("date"),
237
- "login_count": entity_data.get("loginCount", 0),
238
- "action_count": entity_data.get("actionCount", 0),
239
- "failed_login_count": entity_data.get("failedLoginCount", 0),
240
- "last_login": entity_data.get("lastLogin"),
241
- "last_action": entity_data.get("lastAction"),
242
- "unique_ips": entity_data.get("uniqueIps", []),
243
- "actions_by_category": entity_data.get("actionsByCategory", {}),
244
- "risk_score": entity_data.get("riskScore", 0.0),
245
- }
246
-
247
- # Remove None values
248
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
249
-
250
- activities.append(AuditUserActivity(**mapped_data))
251
-
252
- return activities
253
-
254
-
255
- class GetSystemEventsQuery(Query[List[AuditSystemEvent]]):
256
- """Query to get system events audit logs."""
257
-
258
- def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime, severity: Optional[AuditLevel] = None):
259
- self.http_client = http_client
260
- self.organization_id = organization_id
261
- self.start_date = start_date
262
- self.end_date = end_date
263
- self.severity = severity
264
-
265
- def execute(self) -> List[AuditSystemEvent]:
266
- """Execute the query to get system events."""
267
- params = {
268
- "organizationId": str(self.organization_id),
269
- "startDate": self.start_date.isoformat(),
270
- "endDate": self.end_date.isoformat()
271
- }
272
-
273
- if self.severity:
274
- params["severity"] = self.severity.value
275
-
276
- response = self.http_client.get("audit/system-events", params=params)
277
-
278
- entities = response.get("result", {}).get("entities", [])
279
-
280
- events = []
281
- for entity_data in entities:
282
- mapped_data = {
283
- "id": entity_data.get("_id"),
284
- "timestamp": entity_data.get("timestamp"),
285
- "event_type": entity_data.get("eventType"),
286
- "severity": entity_data.get("severity", "info"),
287
- "component": entity_data.get("component"),
288
- "message": entity_data.get("message"),
289
- "details": entity_data.get("details", {}),
290
- "organization_id": entity_data.get("organizationId", self.organization_id),
291
- "resolved": entity_data.get("resolved", False),
292
- "resolved_by": entity_data.get("resolvedBy"),
293
- "resolved_at": entity_data.get("resolvedAt"),
294
- }
295
-
296
- # Remove None values
297
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
298
-
299
- events.append(AuditSystemEvent(**mapped_data))
300
-
301
- return events
302
-
303
-
304
- class GetAuditRetentionPolicyQuery(Query[AuditRetentionPolicy]):
305
- """Query to get audit retention policy."""
306
-
307
- def __init__(self, http_client: HTTPClient, organization_id: int):
308
- self.http_client = http_client
309
- self.organization_id = organization_id
310
-
311
- def execute(self) -> AuditRetentionPolicy:
312
- """Execute the query to get audit retention policy."""
313
- response = self.http_client.get(f"audit/retention-policy/{self.organization_id}")
314
-
315
- entity_data = response.get("result", {})
316
-
317
- mapped_data = {
318
- "organization_id": entity_data.get("organizationId", self.organization_id),
319
- "retention_days": entity_data.get("retentionDays", 365),
320
- "auto_archive": entity_data.get("autoArchive", True),
321
- "archive_location": entity_data.get("archiveLocation"),
322
- "compress_archives": entity_data.get("compressArchives", True),
323
- "delete_after_archive": entity_data.get("deleteAfterArchive", False),
324
- "created_at": entity_data.get("createdAt"),
325
- "updated_at": entity_data.get("updatedAt"),
326
- "created_by": entity_data.get("createdBy"),
327
- }
328
-
329
- # Remove None values
330
- mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
331
-
332
- return AuditRetentionPolicy(**mapped_data)
333
-
334
-
335
- class ExportAuditLogsQuery(Query[Dict[str, Any]]):
336
- """Query to export audit logs with filtering - UPDATED for new API."""
337
-
338
- def __init__(self, http_client: HTTPClient, filter_params: Optional[AuditLogsFilter] = None, format: str = "json", organization_ids: Optional[int] = None):
339
- self.http_client = http_client
340
- # Initialize filter with default organization IDs if not provided
341
- if filter_params is None:
342
- filter_params = AuditLogsFilter()
343
-
344
- # Set organization parameters if not already set in filter
345
- # Changed from List[int] to int to match new API spec
346
- if filter_params.organization_ids is None and organization_ids is not None:
347
- filter_params.organization_ids = organization_ids
348
- elif filter_params.organization_ids is None:
349
- filter_params.organization_ids = 0 # Default to organization 0
350
-
351
- self.filter_params = filter_params
352
- self.format = format
353
-
354
- def execute(self) -> Dict[str, Any]:
355
- """Execute the query to export audit logs."""
356
- # Use filter's parameter generation
357
- params = self.filter_params.to_params()
358
-
359
- # Export endpoint returns binary data, not JSON - handle appropriately
360
- try:
361
- # Use raw HTTP request for binary data
362
- import requests
363
- url = f"{self.http_client.config.host}/api/public/audit-logs/export"
364
- headers = {
365
- "Authorization": f"Bearer {self.http_client.config.api_token}",
366
- "User-Agent": "binalyze-air-sdk/1.0.0"
367
- }
368
-
369
- raw_response = requests.get(
370
- url,
371
- headers=headers,
372
- params=params,
373
- verify=self.http_client.config.verify_ssl,
374
- timeout=self.http_client.config.timeout
375
- )
376
-
377
- if raw_response.status_code == 200:
378
- # For binary/compressed data, return metadata about the export
379
- content_type = raw_response.headers.get("content-type", "application/octet-stream")
380
- content_length = len(raw_response.content) if raw_response.content else 0
381
-
382
- return {
383
- "success": True,
384
- "statusCode": 200,
385
- "errors": [],
386
- "result": {
387
- "exported": True,
388
- "format": "binary",
389
- "content_type": content_type,
390
- "content_length": content_length,
391
- "data_preview": raw_response.content[:100].decode('utf-8', errors='ignore') if raw_response.content else ""
392
- }
393
- }
394
- else:
395
- try:
396
- error_data = raw_response.json()
397
- return {
398
- "success": False,
399
- "statusCode": raw_response.status_code,
400
- "errors": error_data.get("errors", [f"Export failed with status {raw_response.status_code}"]),
401
- "result": None
402
- }
403
- except:
404
- return {
405
- "success": False,
406
- "statusCode": raw_response.status_code,
407
- "errors": [f"Export failed with status {raw_response.status_code}: {raw_response.text}"],
408
- "result": None
409
- }
410
-
411
- except Exception as e:
412
- return {
413
- "success": False,
414
- "statusCode": 500,
415
- "errors": [f"Export request failed: {str(e)}"],
416
- "result": None
1
+ """
2
+ Audit-related queries for the Binalyze AIR SDK.
3
+ """
4
+
5
+ from typing import List, Optional, Dict, Any
6
+ from datetime import datetime
7
+
8
+ from ..base import Query
9
+ from ..models.audit import (
10
+ AuditLog, AuditSummary, AuditUserActivity, AuditSystemEvent,
11
+ AuditRetentionPolicy, AuditFilter, AuditLogsFilter, AuditLevel, AuditCategory, AuditAction
12
+ )
13
+ from ..http_client import HTTPClient
14
+
15
+
16
+ class ListAuditLogsQuery(Query[List[AuditLog]]):
17
+ """Query to list audit logs with optional filtering - UPDATED for new POST-based API."""
18
+
19
+ def __init__(self, http_client: HTTPClient, filter_params: Optional[AuditLogsFilter] = None, organization_ids: Optional[int] = None):
20
+ self.http_client = http_client
21
+ # Initialize filter with default organization IDs if not provided
22
+ if filter_params is None:
23
+ filter_params = AuditLogsFilter()
24
+
25
+ # Set organization parameters if not already set in filter
26
+ # Changed from List[int] to int to match new API spec
27
+ if filter_params.organization_ids is None and organization_ids is not None:
28
+ filter_params.organization_ids = organization_ids
29
+ elif filter_params.organization_ids is None:
30
+ filter_params.organization_ids = 0 # Default to organization 0
31
+
32
+ self.filter_params = filter_params
33
+
34
+ def execute(self) -> List[AuditLog]:
35
+ """Execute the query to list audit logs using GET method with query parameters."""
36
+ # Use GET method with query parameters as per API specification
37
+ params = self.filter_params.to_params()
38
+
39
+ print(f"[INFO] Using GET /audit-logs with params: {params}")
40
+
41
+ try:
42
+ response = self.http_client.get("audit-logs", params=params)
43
+
44
+ # Handle response structure
45
+ if isinstance(response, dict):
46
+ entities = (
47
+ response.get("result", {}).get("entities", []) or # Standard structure
48
+ response.get("entities", []) or # Direct entities
49
+ response.get("data", []) or # Alternative structure
50
+ response.get("logs", []) or # Logs structure
51
+ []
52
+ )
53
+ else:
54
+ entities = []
55
+
56
+ print(f"[INFO] GET /audit-logs returned {len(entities)} audit logs")
57
+
58
+ except Exception as e:
59
+ print(f"[WARN] GET /audit-logs failed: {e}")
60
+ return []
61
+
62
+ logs = []
63
+ for entity_data in entities:
64
+ mapped_data = {
65
+ "id": entity_data.get("_id") or entity_data.get("id"),
66
+ "timestamp": entity_data.get("createdAt") or entity_data.get("timestamp"),
67
+ "user_id": entity_data.get("userId"),
68
+ "username": entity_data.get("performedBy") or entity_data.get("username"),
69
+ "organization_id": entity_data.get("organizationId", 0),
70
+ "category": entity_data.get("type") or entity_data.get("category"),
71
+ "action": entity_data.get("action"),
72
+ "resource_type": entity_data.get("resourceType"),
73
+ "resource_id": entity_data.get("resourceId"),
74
+ "resource_name": entity_data.get("resourceName"),
75
+ "level": entity_data.get("level", "info"),
76
+ "message": entity_data.get("description") or entity_data.get("message"),
77
+ "details": entity_data.get("details", {}),
78
+ "ip_address": entity_data.get("ipAddress"),
79
+ "user_agent": entity_data.get("userAgent"),
80
+ "session_id": entity_data.get("sessionId"),
81
+ "correlation_id": entity_data.get("correlationId"),
82
+ "success": entity_data.get("success", True),
83
+ "error_code": entity_data.get("errorCode"),
84
+ "duration": entity_data.get("duration"),
85
+ "tags": entity_data.get("tags", []),
86
+ }
87
+
88
+ # Remove None values
89
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
90
+
91
+ logs.append(AuditLog(**mapped_data))
92
+
93
+ return logs
94
+
95
+
96
+ class GetAuditLogQuery(Query[AuditLog]):
97
+ """Query to get a specific audit log by ID."""
98
+
99
+ def __init__(self, http_client: HTTPClient, log_id: str):
100
+ self.http_client = http_client
101
+ self.log_id = log_id
102
+
103
+ def execute(self) -> AuditLog:
104
+ """Execute the query to get audit log details."""
105
+ response = self.http_client.get(f"audit-logs/{self.log_id}")
106
+
107
+ entity_data = response.get("result", {})
108
+
109
+ mapped_data = {
110
+ "id": entity_data.get("_id"),
111
+ "timestamp": entity_data.get("createdAt"),
112
+ "user_id": entity_data.get("userId"),
113
+ "username": entity_data.get("performedBy"),
114
+ "organization_id": entity_data.get("organizationId", 0),
115
+ "category": entity_data.get("type"),
116
+ "action": entity_data.get("action"),
117
+ "resource_type": entity_data.get("resourceType"),
118
+ "resource_id": entity_data.get("resourceId"),
119
+ "resource_name": entity_data.get("resourceName"),
120
+ "level": entity_data.get("level", "info"),
121
+ "message": entity_data.get("description"),
122
+ "details": entity_data.get("details", {}),
123
+ "ip_address": entity_data.get("ipAddress"),
124
+ "user_agent": entity_data.get("userAgent"),
125
+ "session_id": entity_data.get("sessionId"),
126
+ "correlation_id": entity_data.get("correlationId"),
127
+ "success": entity_data.get("success", True),
128
+ "error_code": entity_data.get("errorCode"),
129
+ "duration": entity_data.get("duration"),
130
+ "tags": entity_data.get("tags", []),
131
+ }
132
+
133
+ # Remove None values
134
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
135
+
136
+ return AuditLog(**mapped_data)
137
+
138
+
139
+ class GetAuditSummaryQuery(Query[AuditSummary]):
140
+ """Query to get audit summary for a date range."""
141
+
142
+ def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime):
143
+ self.http_client = http_client
144
+ self.organization_id = organization_id
145
+ self.start_date = start_date
146
+ self.end_date = end_date
147
+
148
+ def execute(self) -> AuditSummary:
149
+ """Execute the query to get audit summary."""
150
+ params = {
151
+ "organizationId": str(self.organization_id),
152
+ "startDate": self.start_date.isoformat(),
153
+ "endDate": self.end_date.isoformat()
154
+ }
155
+
156
+ response = self.http_client.get("audit/summary", params=params)
157
+
158
+ entity_data = response.get("result", {})
159
+
160
+ mapped_data = {
161
+ "organization_id": entity_data.get("organizationId", self.organization_id),
162
+ "date": entity_data.get("date", self.start_date),
163
+ "total_events": entity_data.get("totalEvents", 0),
164
+ "successful_events": entity_data.get("successfulEvents", 0),
165
+ "failed_events": entity_data.get("failedEvents", 0),
166
+ "authentication_events": entity_data.get("authenticationEvents", 0),
167
+ "authorization_events": entity_data.get("authorizationEvents", 0),
168
+ "data_access_events": entity_data.get("dataAccessEvents", 0),
169
+ "system_change_events": entity_data.get("systemChangeEvents", 0),
170
+ "user_action_events": entity_data.get("userActionEvents", 0),
171
+ "api_call_events": entity_data.get("apiCallEvents", 0),
172
+ "unique_users": entity_data.get("uniqueUsers", 0),
173
+ "unique_ips": entity_data.get("uniqueIps", 0),
174
+ "top_users": entity_data.get("topUsers", []),
175
+ "top_actions": entity_data.get("topActions", []),
176
+ "error_summary": entity_data.get("errorSummary", []),
177
+ }
178
+
179
+ # Remove None values
180
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
181
+
182
+ return AuditSummary(**mapped_data)
183
+
184
+
185
+ class GetUserActivityQuery(Query[List[AuditUserActivity]]):
186
+ """Query to get user activity audit logs."""
187
+
188
+ def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime, user_id: Optional[str] = None):
189
+ self.http_client = http_client
190
+ self.organization_id = organization_id
191
+ self.start_date = start_date
192
+ self.end_date = end_date
193
+ self.user_id = user_id
194
+
195
+ def execute(self) -> List[AuditUserActivity]:
196
+ """Execute the query to get user activity."""
197
+ params = {
198
+ "organizationId": str(self.organization_id),
199
+ "startDate": self.start_date.isoformat(),
200
+ "endDate": self.end_date.isoformat()
201
+ }
202
+
203
+ if self.user_id:
204
+ params["userId"] = self.user_id
205
+
206
+ response = self.http_client.get("audit/user-activity", params=params)
207
+
208
+ entities = response.get("result", {}).get("entities", [])
209
+
210
+ activities = []
211
+ for entity_data in entities:
212
+ mapped_data = {
213
+ "user_id": entity_data.get("userId"),
214
+ "username": entity_data.get("username"),
215
+ "organization_id": entity_data.get("organizationId", self.organization_id),
216
+ "date": entity_data.get("date"),
217
+ "login_count": entity_data.get("loginCount", 0),
218
+ "action_count": entity_data.get("actionCount", 0),
219
+ "failed_login_count": entity_data.get("failedLoginCount", 0),
220
+ "last_login": entity_data.get("lastLogin"),
221
+ "last_action": entity_data.get("lastAction"),
222
+ "unique_ips": entity_data.get("uniqueIps", []),
223
+ "actions_by_category": entity_data.get("actionsByCategory", {}),
224
+ "risk_score": entity_data.get("riskScore", 0.0),
225
+ }
226
+
227
+ # Remove None values
228
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
229
+
230
+ activities.append(AuditUserActivity(**mapped_data))
231
+
232
+ return activities
233
+
234
+
235
+ class GetSystemEventsQuery(Query[List[AuditSystemEvent]]):
236
+ """Query to get system events audit logs."""
237
+
238
+ def __init__(self, http_client: HTTPClient, organization_id: int, start_date: datetime, end_date: datetime, severity: Optional[AuditLevel] = None):
239
+ self.http_client = http_client
240
+ self.organization_id = organization_id
241
+ self.start_date = start_date
242
+ self.end_date = end_date
243
+ self.severity = severity
244
+
245
+ def execute(self) -> List[AuditSystemEvent]:
246
+ """Execute the query to get system events."""
247
+ params = {
248
+ "organizationId": str(self.organization_id),
249
+ "startDate": self.start_date.isoformat(),
250
+ "endDate": self.end_date.isoformat()
251
+ }
252
+
253
+ if self.severity:
254
+ # Handle both enum and string values - FIXED
255
+ if hasattr(self.severity, 'value'):
256
+ params["severity"] = self.severity.value
257
+ else:
258
+ params["severity"] = self.severity
259
+
260
+ response = self.http_client.get("audit/system-events", params=params)
261
+
262
+ entities = response.get("result", {}).get("entities", [])
263
+
264
+ events = []
265
+ for entity_data in entities:
266
+ mapped_data = {
267
+ "id": entity_data.get("_id"),
268
+ "timestamp": entity_data.get("timestamp"),
269
+ "event_type": entity_data.get("eventType"),
270
+ "severity": entity_data.get("severity", "info"),
271
+ "component": entity_data.get("component"),
272
+ "message": entity_data.get("message"),
273
+ "details": entity_data.get("details", {}),
274
+ "organization_id": entity_data.get("organizationId", self.organization_id),
275
+ "resolved": entity_data.get("resolved", False),
276
+ "resolved_by": entity_data.get("resolvedBy"),
277
+ "resolved_at": entity_data.get("resolvedAt"),
278
+ }
279
+
280
+ # Remove None values
281
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
282
+
283
+ events.append(AuditSystemEvent(**mapped_data))
284
+
285
+ return events
286
+
287
+
288
+ class GetAuditRetentionPolicyQuery(Query[AuditRetentionPolicy]):
289
+ """Query to get audit retention policy."""
290
+
291
+ def __init__(self, http_client: HTTPClient, organization_id: int):
292
+ self.http_client = http_client
293
+ self.organization_id = organization_id
294
+
295
+ def execute(self) -> AuditRetentionPolicy:
296
+ """Execute the query to get audit retention policy."""
297
+ response = self.http_client.get(f"audit/retention-policy/{self.organization_id}")
298
+
299
+ entity_data = response.get("result", {})
300
+
301
+ mapped_data = {
302
+ "organization_id": entity_data.get("organizationId", self.organization_id),
303
+ "retention_days": entity_data.get("retentionDays", 365),
304
+ "auto_archive": entity_data.get("autoArchive", True),
305
+ "archive_location": entity_data.get("archiveLocation"),
306
+ "compress_archives": entity_data.get("compressArchives", True),
307
+ "delete_after_archive": entity_data.get("deleteAfterArchive", False),
308
+ "created_at": entity_data.get("createdAt"),
309
+ "updated_at": entity_data.get("updatedAt"),
310
+ "created_by": entity_data.get("createdBy"),
311
+ }
312
+
313
+ # Remove None values
314
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
315
+
316
+ return AuditRetentionPolicy(**mapped_data)
317
+
318
+
319
+ class ExportAuditLogsQuery(Query[Dict[str, Any]]):
320
+ """Query to export audit logs with filtering - UPDATED for new API."""
321
+
322
+ def __init__(self, http_client: HTTPClient, filter_params: Optional[AuditLogsFilter] = None, format: str = "json", organization_ids: Optional[int] = None):
323
+ self.http_client = http_client
324
+ # Initialize filter with default organization IDs if not provided
325
+ if filter_params is None:
326
+ filter_params = AuditLogsFilter()
327
+
328
+ # Set organization parameters if not already set in filter
329
+ # Changed from List[int] to int to match new API spec
330
+ if filter_params.organization_ids is None and organization_ids is not None:
331
+ filter_params.organization_ids = organization_ids
332
+ elif filter_params.organization_ids is None:
333
+ filter_params.organization_ids = 0 # Default to organization 0
334
+
335
+ self.filter_params = filter_params
336
+ self.format = format
337
+
338
+ def execute(self) -> Dict[str, Any]:
339
+ """Execute the query to export audit logs."""
340
+ # Use filter's parameter generation
341
+ params = self.filter_params.to_params()
342
+
343
+ # Export endpoint returns binary data, not JSON - handle appropriately
344
+ try:
345
+ # Use raw HTTP request for binary data
346
+ import requests
347
+ url = f"{self.http_client.config.host}/api/public/audit-logs/export"
348
+ headers = {
349
+ "Authorization": f"Bearer {self.http_client.config.api_token}",
350
+ "User-Agent": "binalyze-air-sdk/1.0.0"
351
+ }
352
+
353
+ raw_response = requests.get(
354
+ url,
355
+ headers=headers,
356
+ params=params,
357
+ verify=self.http_client.config.verify_ssl,
358
+ timeout=self.http_client.config.timeout
359
+ )
360
+
361
+ if raw_response.status_code == 200:
362
+ # For binary/compressed data, return metadata about the export
363
+ content_type = raw_response.headers.get("content-type", "application/octet-stream")
364
+ content_length = len(raw_response.content) if raw_response.content else 0
365
+
366
+ return {
367
+ "success": True,
368
+ "statusCode": 200,
369
+ "errors": [],
370
+ "result": {
371
+ "exported": True,
372
+ "format": "binary",
373
+ "content_type": content_type,
374
+ "content_length": content_length,
375
+ "data_preview": raw_response.content[:100].decode('utf-8', errors='ignore') if raw_response.content else ""
376
+ }
377
+ }
378
+ else:
379
+ try:
380
+ error_data = raw_response.json()
381
+ return {
382
+ "success": False,
383
+ "statusCode": raw_response.status_code,
384
+ "errors": error_data.get("errors", [f"Export failed with status {raw_response.status_code}"]),
385
+ "result": None
386
+ }
387
+ except:
388
+ return {
389
+ "success": False,
390
+ "statusCode": raw_response.status_code,
391
+ "errors": [f"Export failed with status {raw_response.status_code}: {raw_response.text}"],
392
+ "result": None
393
+ }
394
+
395
+ except Exception as e:
396
+ return {
397
+ "success": False,
398
+ "statusCode": 500,
399
+ "errors": [f"Export request failed: {str(e)}"],
400
+ "result": None
417
401
  }