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.
- binalyze_air/__init__.py +77 -0
- binalyze_air/apis/__init__.py +27 -0
- binalyze_air/apis/authentication.py +27 -0
- binalyze_air/apis/auto_asset_tags.py +75 -0
- binalyze_air/apis/endpoints.py +22 -0
- binalyze_air/apis/event_subscription.py +97 -0
- binalyze_air/apis/evidence.py +53 -0
- binalyze_air/apis/evidences.py +216 -0
- binalyze_air/apis/interact.py +36 -0
- binalyze_air/apis/params.py +40 -0
- binalyze_air/apis/settings.py +27 -0
- binalyze_air/apis/user_management.py +74 -0
- binalyze_air/apis/users.py +68 -0
- binalyze_air/apis/webhooks.py +231 -0
- binalyze_air/base.py +133 -0
- binalyze_air/client.py +1338 -0
- binalyze_air/commands/__init__.py +146 -0
- binalyze_air/commands/acquisitions.py +387 -0
- binalyze_air/commands/assets.py +363 -0
- binalyze_air/commands/authentication.py +37 -0
- binalyze_air/commands/auto_asset_tags.py +231 -0
- binalyze_air/commands/baseline.py +396 -0
- binalyze_air/commands/cases.py +603 -0
- binalyze_air/commands/event_subscription.py +102 -0
- binalyze_air/commands/evidences.py +988 -0
- binalyze_air/commands/interact.py +58 -0
- binalyze_air/commands/organizations.py +221 -0
- binalyze_air/commands/policies.py +203 -0
- binalyze_air/commands/settings.py +29 -0
- binalyze_air/commands/tasks.py +56 -0
- binalyze_air/commands/triage.py +360 -0
- binalyze_air/commands/user_management.py +126 -0
- binalyze_air/commands/users.py +101 -0
- binalyze_air/config.py +245 -0
- binalyze_air/exceptions.py +50 -0
- binalyze_air/http_client.py +306 -0
- binalyze_air/models/__init__.py +285 -0
- binalyze_air/models/acquisitions.py +251 -0
- binalyze_air/models/assets.py +439 -0
- binalyze_air/models/audit.py +273 -0
- binalyze_air/models/authentication.py +70 -0
- binalyze_air/models/auto_asset_tags.py +117 -0
- binalyze_air/models/baseline.py +232 -0
- binalyze_air/models/cases.py +276 -0
- binalyze_air/models/endpoints.py +76 -0
- binalyze_air/models/event_subscription.py +172 -0
- binalyze_air/models/evidence.py +66 -0
- binalyze_air/models/evidences.py +349 -0
- binalyze_air/models/interact.py +136 -0
- binalyze_air/models/organizations.py +294 -0
- binalyze_air/models/params.py +128 -0
- binalyze_air/models/policies.py +250 -0
- binalyze_air/models/settings.py +84 -0
- binalyze_air/models/tasks.py +149 -0
- binalyze_air/models/triage.py +143 -0
- binalyze_air/models/user_management.py +97 -0
- binalyze_air/models/users.py +82 -0
- binalyze_air/queries/__init__.py +134 -0
- binalyze_air/queries/acquisitions.py +156 -0
- binalyze_air/queries/assets.py +105 -0
- binalyze_air/queries/audit.py +417 -0
- binalyze_air/queries/authentication.py +56 -0
- binalyze_air/queries/auto_asset_tags.py +60 -0
- binalyze_air/queries/baseline.py +185 -0
- binalyze_air/queries/cases.py +293 -0
- binalyze_air/queries/endpoints.py +25 -0
- binalyze_air/queries/event_subscription.py +55 -0
- binalyze_air/queries/evidence.py +140 -0
- binalyze_air/queries/evidences.py +280 -0
- binalyze_air/queries/interact.py +28 -0
- binalyze_air/queries/organizations.py +223 -0
- binalyze_air/queries/params.py +115 -0
- binalyze_air/queries/policies.py +150 -0
- binalyze_air/queries/settings.py +20 -0
- binalyze_air/queries/tasks.py +82 -0
- binalyze_air/queries/triage.py +231 -0
- binalyze_air/queries/user_management.py +83 -0
- binalyze_air/queries/users.py +69 -0
- binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
- binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
- binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
- 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
|
+
}
|