binalyze-air-sdk 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. binalyze_air/__init__.py +77 -0
  2. binalyze_air/apis/__init__.py +27 -0
  3. binalyze_air/apis/authentication.py +27 -0
  4. binalyze_air/apis/auto_asset_tags.py +75 -0
  5. binalyze_air/apis/endpoints.py +22 -0
  6. binalyze_air/apis/event_subscription.py +97 -0
  7. binalyze_air/apis/evidence.py +53 -0
  8. binalyze_air/apis/evidences.py +216 -0
  9. binalyze_air/apis/interact.py +36 -0
  10. binalyze_air/apis/params.py +40 -0
  11. binalyze_air/apis/settings.py +27 -0
  12. binalyze_air/apis/user_management.py +74 -0
  13. binalyze_air/apis/users.py +68 -0
  14. binalyze_air/apis/webhooks.py +231 -0
  15. binalyze_air/base.py +133 -0
  16. binalyze_air/client.py +1338 -0
  17. binalyze_air/commands/__init__.py +146 -0
  18. binalyze_air/commands/acquisitions.py +387 -0
  19. binalyze_air/commands/assets.py +363 -0
  20. binalyze_air/commands/authentication.py +37 -0
  21. binalyze_air/commands/auto_asset_tags.py +231 -0
  22. binalyze_air/commands/baseline.py +396 -0
  23. binalyze_air/commands/cases.py +603 -0
  24. binalyze_air/commands/event_subscription.py +102 -0
  25. binalyze_air/commands/evidences.py +988 -0
  26. binalyze_air/commands/interact.py +58 -0
  27. binalyze_air/commands/organizations.py +221 -0
  28. binalyze_air/commands/policies.py +203 -0
  29. binalyze_air/commands/settings.py +29 -0
  30. binalyze_air/commands/tasks.py +56 -0
  31. binalyze_air/commands/triage.py +360 -0
  32. binalyze_air/commands/user_management.py +126 -0
  33. binalyze_air/commands/users.py +101 -0
  34. binalyze_air/config.py +245 -0
  35. binalyze_air/exceptions.py +50 -0
  36. binalyze_air/http_client.py +306 -0
  37. binalyze_air/models/__init__.py +285 -0
  38. binalyze_air/models/acquisitions.py +251 -0
  39. binalyze_air/models/assets.py +439 -0
  40. binalyze_air/models/audit.py +273 -0
  41. binalyze_air/models/authentication.py +70 -0
  42. binalyze_air/models/auto_asset_tags.py +117 -0
  43. binalyze_air/models/baseline.py +232 -0
  44. binalyze_air/models/cases.py +276 -0
  45. binalyze_air/models/endpoints.py +76 -0
  46. binalyze_air/models/event_subscription.py +172 -0
  47. binalyze_air/models/evidence.py +66 -0
  48. binalyze_air/models/evidences.py +349 -0
  49. binalyze_air/models/interact.py +136 -0
  50. binalyze_air/models/organizations.py +294 -0
  51. binalyze_air/models/params.py +128 -0
  52. binalyze_air/models/policies.py +250 -0
  53. binalyze_air/models/settings.py +84 -0
  54. binalyze_air/models/tasks.py +149 -0
  55. binalyze_air/models/triage.py +143 -0
  56. binalyze_air/models/user_management.py +97 -0
  57. binalyze_air/models/users.py +82 -0
  58. binalyze_air/queries/__init__.py +134 -0
  59. binalyze_air/queries/acquisitions.py +156 -0
  60. binalyze_air/queries/assets.py +105 -0
  61. binalyze_air/queries/audit.py +417 -0
  62. binalyze_air/queries/authentication.py +56 -0
  63. binalyze_air/queries/auto_asset_tags.py +60 -0
  64. binalyze_air/queries/baseline.py +185 -0
  65. binalyze_air/queries/cases.py +293 -0
  66. binalyze_air/queries/endpoints.py +25 -0
  67. binalyze_air/queries/event_subscription.py +55 -0
  68. binalyze_air/queries/evidence.py +140 -0
  69. binalyze_air/queries/evidences.py +280 -0
  70. binalyze_air/queries/interact.py +28 -0
  71. binalyze_air/queries/organizations.py +223 -0
  72. binalyze_air/queries/params.py +115 -0
  73. binalyze_air/queries/policies.py +150 -0
  74. binalyze_air/queries/settings.py +20 -0
  75. binalyze_air/queries/tasks.py +82 -0
  76. binalyze_air/queries/triage.py +231 -0
  77. binalyze_air/queries/user_management.py +83 -0
  78. binalyze_air/queries/users.py +69 -0
  79. binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
  80. binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
  81. binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
  82. binalyze_air_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,156 @@
1
+ """
2
+ Acquisition-related queries for the Binalyze AIR SDK.
3
+ """
4
+
5
+ from typing import List, Optional
6
+
7
+ from ..base import Query
8
+ from ..models.acquisitions import (
9
+ AcquisitionProfile, AcquisitionProfileDetails, AcquisitionFilter,
10
+ AcquisitionProfilePlatformDetails, NetworkCaptureConfig, EDiscoveryPattern
11
+ )
12
+ from ..http_client import HTTPClient
13
+
14
+
15
+ class ListAcquisitionProfilesQuery(Query[List[AcquisitionProfile]]):
16
+ """Query to list acquisition profiles with optional filtering."""
17
+
18
+ def __init__(
19
+ self,
20
+ http_client: HTTPClient,
21
+ filter_params: Optional[AcquisitionFilter] = None,
22
+ organization_ids: Optional[List[int]] = None,
23
+ all_organizations: bool = False
24
+ ):
25
+ self.http_client = http_client
26
+ # Initialize filter with default organization IDs if not provided
27
+ if filter_params is None:
28
+ filter_params = AcquisitionFilter()
29
+
30
+ # Set organization parameters if not already set in filter
31
+ if filter_params.organization_ids is None and organization_ids is not None:
32
+ filter_params.organization_ids = organization_ids
33
+ elif filter_params.organization_ids is None:
34
+ filter_params.organization_ids = [0] # Default to organization 0
35
+
36
+ # Set all_organizations parameter if not already set in filter
37
+ if filter_params.all_organizations is None and all_organizations:
38
+ filter_params.all_organizations = all_organizations
39
+
40
+ self.filter_params = filter_params
41
+
42
+ def execute(self) -> List[AcquisitionProfile]:
43
+ """Execute the query to list acquisition profiles."""
44
+ # Use filter's parameter generation
45
+ params = self.filter_params.to_params()
46
+
47
+ response = self.http_client.get("acquisitions/profiles", params=params)
48
+
49
+ entities = response.get("result", {}).get("entities", [])
50
+
51
+ # Convert to AcquisitionProfile objects
52
+ profiles = []
53
+ for entity_data in entities:
54
+ mapped_data = {
55
+ "id": entity_data.get("_id"),
56
+ "name": entity_data.get("name"),
57
+ "organization_ids": entity_data.get("organizationIds", []), # Keep as integers
58
+ "created_at": entity_data.get("createdAt"),
59
+ "updated_at": entity_data.get("updatedAt"),
60
+ "created_by": entity_data.get("createdBy"),
61
+ "deletable": entity_data.get("deletable", True),
62
+ "average_time": entity_data.get("averageTime"),
63
+ "last_used_at": entity_data.get("lastUsedAt"),
64
+ "last_used_by": entity_data.get("lastUsedBy"),
65
+ "has_event_log_records_evidence": entity_data.get("hasEventLogRecordsEvidence"),
66
+ }
67
+
68
+ # Remove None values
69
+ mapped_data = {k: v for k, v in mapped_data.items() if v is not None}
70
+
71
+ profiles.append(AcquisitionProfile(**mapped_data))
72
+
73
+ return profiles
74
+
75
+
76
+ class GetAcquisitionProfileQuery(Query[AcquisitionProfileDetails]):
77
+ """Query to get a specific acquisition profile by ID."""
78
+
79
+ def __init__(self, http_client: HTTPClient, profile_id: str):
80
+ self.http_client = http_client
81
+ self.profile_id = profile_id
82
+
83
+ def execute(self) -> AcquisitionProfileDetails:
84
+ """Execute the query to get acquisition profile details."""
85
+ response = self.http_client.get(f"acquisitions/profiles/{self.profile_id}")
86
+
87
+ entity_data = response.get("result", {})
88
+
89
+ # Parse platform configurations
90
+ def parse_platform_config(platform_data: dict) -> AcquisitionProfilePlatformDetails:
91
+ network_capture = None
92
+ if "networkCapture" in platform_data:
93
+ nc_data = platform_data["networkCapture"]
94
+ network_capture = NetworkCaptureConfig(
95
+ enabled=nc_data.get("enabled", False),
96
+ duration=nc_data.get("duration", 60),
97
+ pcap=nc_data.get("pcap", {"enabled": False}),
98
+ network_flow=nc_data.get("networkFlow", {"enabled": False})
99
+ )
100
+
101
+ return AcquisitionProfilePlatformDetails(
102
+ evidence_list=platform_data.get("evidenceList", []),
103
+ artifact_list=platform_data.get("artifactList"),
104
+ custom_content_profiles=platform_data.get("customContentProfiles", []),
105
+ network_capture=network_capture
106
+ )
107
+
108
+ # Parse platform configurations
109
+ windows_config = None
110
+ linux_config = None
111
+ macos_config = None
112
+ aix_config = None
113
+
114
+ if "windows" in entity_data:
115
+ windows_config = parse_platform_config(entity_data["windows"])
116
+
117
+ if "linux" in entity_data:
118
+ linux_config = parse_platform_config(entity_data["linux"])
119
+
120
+ if "macos" in entity_data:
121
+ macos_config = parse_platform_config(entity_data["macos"])
122
+
123
+ if "aix" in entity_data:
124
+ aix_config = parse_platform_config(entity_data["aix"])
125
+
126
+ # Parse eDiscovery patterns
127
+ e_discovery = None
128
+ if "eDiscovery" in entity_data and "patterns" in entity_data["eDiscovery"]:
129
+ patterns = [
130
+ EDiscoveryPattern(
131
+ pattern=pattern.get("pattern", ""),
132
+ category=pattern.get("category", "")
133
+ )
134
+ for pattern in entity_data["eDiscovery"]["patterns"]
135
+ ]
136
+ e_discovery = {"patterns": patterns}
137
+
138
+ mapped_data = {
139
+ "id": entity_data.get("_id"),
140
+ "name": entity_data.get("name"),
141
+ "organization_ids": [str(org_id) for org_id in entity_data.get("organizationIds", [])],
142
+ "created_at": entity_data.get("createdAt"),
143
+ "updated_at": entity_data.get("updatedAt"),
144
+ "created_by": entity_data.get("createdBy"),
145
+ "deletable": entity_data.get("deletable", True),
146
+ "windows": windows_config,
147
+ "linux": linux_config,
148
+ "macos": macos_config,
149
+ "aix": aix_config,
150
+ "e_discovery": e_discovery,
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 AcquisitionProfileDetails(**mapped_data)
@@ -0,0 +1,105 @@
1
+ """
2
+ Asset-related queries for the Binalyze AIR SDK.
3
+ """
4
+
5
+ from typing import List, Dict, Any, Optional
6
+
7
+ from ..base import Query, PaginatedResponse, APIResponse, PaginatedList
8
+ from ..models.assets import Asset, AssetDetail, AssetTask, AssetFilter, AssetTaskFilter
9
+ from ..http_client import HTTPClient
10
+
11
+
12
+ class ListAssetsQuery(Query[List[Asset]]):
13
+ """Query to list assets with optional filtering."""
14
+
15
+ def __init__(self, http_client: HTTPClient, filter_params: Optional[AssetFilter] = None):
16
+ self.http_client = http_client
17
+ self.filter_params = filter_params or AssetFilter()
18
+
19
+ def execute(self) -> List[Asset]:
20
+ """Execute the query to list assets."""
21
+ params = self.filter_params.to_params()
22
+
23
+ # Set default organization_ids if not provided
24
+ if not self.filter_params.organization_ids:
25
+ params["filter[organizationIds]"] = "0"
26
+
27
+ # Ensure consistent sorting to match API defaults
28
+ if "sortBy" not in params:
29
+ params["sortBy"] = "createdAt"
30
+ if "sortType" not in params:
31
+ params["sortType"] = "ASC"
32
+
33
+ response = self.http_client.get("assets", params=params)
34
+
35
+ # Parse the paginated response
36
+ entities = response.get("result", {}).get("entities", [])
37
+
38
+ # Convert to Asset objects using Pydantic parsing with aliases
39
+ assets = []
40
+ for entity_data in entities:
41
+ # Let Pydantic handle the field mapping via aliases
42
+ asset = Asset.model_validate(entity_data)
43
+ assets.append(asset)
44
+
45
+ return assets
46
+
47
+
48
+ class GetAssetQuery(Query[AssetDetail]):
49
+ """Query to get a specific asset by ID."""
50
+
51
+ def __init__(self, http_client: HTTPClient, asset_id: str):
52
+ self.http_client = http_client
53
+ self.asset_id = asset_id
54
+
55
+ def execute(self) -> AssetDetail:
56
+ """Execute the query to get asset details."""
57
+ response = self.http_client.get(f"assets/{self.asset_id}")
58
+
59
+ entity_data = response.get("result", {})
60
+
61
+ # Let Pydantic handle the field mapping via aliases
62
+ return AssetDetail.model_validate(entity_data)
63
+
64
+
65
+ class GetAssetTasksQuery(Query[List[AssetTask]]):
66
+ """Query to get tasks for a specific asset with optional filtering."""
67
+
68
+ def __init__(self, http_client: HTTPClient, asset_id: str, filter_params: Optional[AssetTaskFilter] = None):
69
+ self.http_client = http_client
70
+ self.asset_id = asset_id
71
+ self.filter_params = filter_params or AssetTaskFilter()
72
+
73
+ def execute(self) -> List[AssetTask]:
74
+ """Execute the query to get asset tasks."""
75
+ # Get filter parameters
76
+ params = self.filter_params.to_params()
77
+
78
+ # Provide sensible defaults that mirror the HTTP test defaults if not supplied
79
+ # Default pagination
80
+ if "pageNumber" not in params:
81
+ params["pageNumber"] = 1
82
+ if "pageSize" not in params:
83
+ params["pageSize"] = 10
84
+ # Default sorting
85
+ if "sortBy" not in params:
86
+ params["sortBy"] = "createdAt"
87
+ if "sortType" not in params:
88
+ params["sortType"] = "ASC"
89
+
90
+ # Make request with parameters
91
+ response = self.http_client.get(f"assets/{self.asset_id}/tasks", params=params)
92
+
93
+ result_meta = response.get("result", {})
94
+ entities_data = result_meta.get("entities", [])
95
+
96
+ # Convert to AssetTask objects using Pydantic parsing with aliases
97
+ paginated_tasks = PaginatedList(
98
+ [AssetTask.model_validate(entity) for entity in entities_data],
99
+ total_entity_count=result_meta.get("totalEntityCount"),
100
+ current_page=result_meta.get("currentPage", params.get("pageNumber", 1)),
101
+ page_size=result_meta.get("pageSize", params.get("pageSize", len(entities_data))),
102
+ total_page_count=result_meta.get("totalPageCount"),
103
+ )
104
+
105
+ return paginated_tasks
@@ -0,0 +1,417 @@
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
417
+ }