kailash 0.3.1__py3-none-any.whl → 0.4.0__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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +293 -12
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.1.dist-info/RECORD +0 -136
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1093 @@
|
|
1
|
+
"""
|
2
|
+
Advanced session management with security tracking.
|
3
|
+
|
4
|
+
This module provides comprehensive session management capabilities including
|
5
|
+
concurrent session limits, device tracking, anomaly detection, and automatic
|
6
|
+
session cleanup with security event integration.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import hashlib
|
10
|
+
import json
|
11
|
+
import logging
|
12
|
+
import secrets
|
13
|
+
import threading
|
14
|
+
from dataclasses import dataclass
|
15
|
+
from datetime import UTC, datetime, timedelta
|
16
|
+
from enum import Enum
|
17
|
+
from typing import Any, Dict, List, Optional, Set
|
18
|
+
|
19
|
+
from kailash.nodes.base import Node, NodeParameter, register_node
|
20
|
+
from kailash.nodes.mixins import LoggingMixin, PerformanceMixin, SecurityMixin
|
21
|
+
from kailash.nodes.security.audit_log import AuditLogNode
|
22
|
+
from kailash.nodes.security.security_event import SecurityEventNode
|
23
|
+
|
24
|
+
logger = logging.getLogger(__name__)
|
25
|
+
|
26
|
+
|
27
|
+
class SessionStatus(Enum):
|
28
|
+
"""Session status enumeration."""
|
29
|
+
|
30
|
+
ACTIVE = "active"
|
31
|
+
IDLE = "idle"
|
32
|
+
EXPIRED = "expired"
|
33
|
+
TERMINATED = "terminated"
|
34
|
+
SUSPICIOUS = "suspicious"
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class DeviceInfo:
|
39
|
+
"""Device information for session tracking."""
|
40
|
+
|
41
|
+
device_id: str
|
42
|
+
device_type: str # desktop, mobile, tablet
|
43
|
+
os_name: str
|
44
|
+
os_version: str
|
45
|
+
browser_name: str
|
46
|
+
browser_version: str
|
47
|
+
user_agent: str
|
48
|
+
fingerprint: str
|
49
|
+
|
50
|
+
|
51
|
+
@dataclass
|
52
|
+
class SessionData:
|
53
|
+
"""Complete session data."""
|
54
|
+
|
55
|
+
session_id: str
|
56
|
+
user_id: str
|
57
|
+
created_at: datetime
|
58
|
+
last_activity: datetime
|
59
|
+
expires_at: datetime
|
60
|
+
ip_address: str
|
61
|
+
device_info: DeviceInfo
|
62
|
+
status: SessionStatus
|
63
|
+
|
64
|
+
# Security tracking
|
65
|
+
login_method: str # password, mfa, sso
|
66
|
+
risk_score: float
|
67
|
+
anomaly_flags: List[str]
|
68
|
+
|
69
|
+
# Activity tracking
|
70
|
+
page_views: int
|
71
|
+
actions_performed: int
|
72
|
+
data_accessed_mb: float
|
73
|
+
|
74
|
+
# Geo-location (if available)
|
75
|
+
country: Optional[str] = None
|
76
|
+
city: Optional[str] = None
|
77
|
+
|
78
|
+
# Session metadata
|
79
|
+
metadata: Dict[str, Any] = None
|
80
|
+
|
81
|
+
|
82
|
+
@register_node()
|
83
|
+
class SessionManagementNode(SecurityMixin, PerformanceMixin, LoggingMixin, Node):
|
84
|
+
"""Advanced session management with security tracking.
|
85
|
+
|
86
|
+
This node provides comprehensive session management including:
|
87
|
+
- Concurrent session limits per user
|
88
|
+
- Device fingerprinting and tracking
|
89
|
+
- Idle and absolute session timeouts
|
90
|
+
- Anomaly detection for sessions
|
91
|
+
- Geographic location tracking
|
92
|
+
- Session hijacking detection
|
93
|
+
- Automatic cleanup and security logging
|
94
|
+
|
95
|
+
Example:
|
96
|
+
>>> session_mgr = SessionManagementNode(
|
97
|
+
... max_sessions=3,
|
98
|
+
... idle_timeout=timedelta(minutes=30),
|
99
|
+
... absolute_timeout=timedelta(hours=8),
|
100
|
+
... track_devices=True
|
101
|
+
... )
|
102
|
+
>>>
|
103
|
+
>>> # Create new session
|
104
|
+
>>> device_info = {
|
105
|
+
... "device_type": "desktop",
|
106
|
+
... "os_name": "Windows",
|
107
|
+
... "browser_name": "Chrome",
|
108
|
+
... "user_agent": "Mozilla/5.0..."
|
109
|
+
... }
|
110
|
+
>>>
|
111
|
+
>>> result = session_mgr.run(
|
112
|
+
... action="create",
|
113
|
+
... user_id="user123",
|
114
|
+
... ip_address="192.168.1.100",
|
115
|
+
... device_info=device_info
|
116
|
+
... )
|
117
|
+
>>> print(f"Session ID: {result['session_id']}")
|
118
|
+
>>>
|
119
|
+
>>> # Validate session
|
120
|
+
>>> validation = session_mgr.run(
|
121
|
+
... action="validate",
|
122
|
+
... session_id=result['session_id']
|
123
|
+
... )
|
124
|
+
>>> print(f"Valid: {validation['valid']}")
|
125
|
+
"""
|
126
|
+
|
127
|
+
def __init__(
|
128
|
+
self,
|
129
|
+
name: str = "session_management",
|
130
|
+
max_sessions: int = 3,
|
131
|
+
idle_timeout: timedelta = timedelta(minutes=30),
|
132
|
+
absolute_timeout: timedelta = timedelta(hours=8),
|
133
|
+
track_devices: bool = True,
|
134
|
+
enable_geo_tracking: bool = False,
|
135
|
+
anomaly_detection: bool = True,
|
136
|
+
cleanup_interval: int = 300, # 5 minutes
|
137
|
+
**kwargs,
|
138
|
+
):
|
139
|
+
"""Initialize session management node.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
name: Node name
|
143
|
+
max_sessions: Maximum concurrent sessions per user
|
144
|
+
idle_timeout: Idle session timeout
|
145
|
+
absolute_timeout: Absolute session timeout
|
146
|
+
track_devices: Enable device tracking and fingerprinting
|
147
|
+
enable_geo_tracking: Enable geographic location tracking
|
148
|
+
anomaly_detection: Enable session anomaly detection
|
149
|
+
cleanup_interval: Cleanup interval in seconds
|
150
|
+
**kwargs: Additional node parameters
|
151
|
+
"""
|
152
|
+
# Set attributes before calling super().__init__()
|
153
|
+
self.max_sessions = max_sessions
|
154
|
+
self.idle_timeout = idle_timeout
|
155
|
+
self.absolute_timeout = absolute_timeout
|
156
|
+
self.track_devices = track_devices
|
157
|
+
self.enable_geo_tracking = enable_geo_tracking
|
158
|
+
self.anomaly_detection = anomaly_detection
|
159
|
+
self.cleanup_interval = cleanup_interval
|
160
|
+
|
161
|
+
# Initialize parent classes
|
162
|
+
super().__init__(name=name, **kwargs)
|
163
|
+
|
164
|
+
# Initialize audit logging and security events
|
165
|
+
self.audit_log_node = AuditLogNode(name=f"{name}_audit_log")
|
166
|
+
self.security_event_node = SecurityEventNode(name=f"{name}_security_events")
|
167
|
+
|
168
|
+
# Session storage
|
169
|
+
self.sessions: Dict[str, SessionData] = {}
|
170
|
+
self.user_sessions: Dict[str, Set[str]] = {} # user_id -> set of session_ids
|
171
|
+
self.device_sessions: Dict[str, Set[str]] = (
|
172
|
+
{}
|
173
|
+
) # device_fingerprint -> set of session_ids
|
174
|
+
|
175
|
+
# Thread locks for concurrent access
|
176
|
+
self._sessions_lock = threading.Lock()
|
177
|
+
|
178
|
+
# Session statistics
|
179
|
+
self.session_stats = {
|
180
|
+
"total_sessions_created": 0,
|
181
|
+
"active_sessions": 0,
|
182
|
+
"expired_sessions_cleaned": 0,
|
183
|
+
"concurrent_limit_hits": 0,
|
184
|
+
"anomalies_detected": 0,
|
185
|
+
"devices_tracked": 0,
|
186
|
+
"sessions_terminated": 0,
|
187
|
+
}
|
188
|
+
|
189
|
+
# Last cleanup time
|
190
|
+
self._last_cleanup = datetime.now(UTC)
|
191
|
+
|
192
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
193
|
+
"""Get node parameters for validation and documentation.
|
194
|
+
|
195
|
+
Returns:
|
196
|
+
Dictionary mapping parameter names to NodeParameter objects
|
197
|
+
"""
|
198
|
+
return {
|
199
|
+
"action": NodeParameter(
|
200
|
+
name="action",
|
201
|
+
type=str,
|
202
|
+
description="Session action to perform",
|
203
|
+
required=True,
|
204
|
+
),
|
205
|
+
"session_id": NodeParameter(
|
206
|
+
name="session_id",
|
207
|
+
type=str,
|
208
|
+
description="Session ID for operations",
|
209
|
+
required=False,
|
210
|
+
),
|
211
|
+
"user_id": NodeParameter(
|
212
|
+
name="user_id",
|
213
|
+
type=str,
|
214
|
+
description="User ID for session operations",
|
215
|
+
required=False,
|
216
|
+
),
|
217
|
+
"ip_address": NodeParameter(
|
218
|
+
name="ip_address",
|
219
|
+
type=str,
|
220
|
+
description="Client IP address",
|
221
|
+
required=False,
|
222
|
+
),
|
223
|
+
"device_info": NodeParameter(
|
224
|
+
name="device_info",
|
225
|
+
type=dict,
|
226
|
+
description="Device information for tracking",
|
227
|
+
required=False,
|
228
|
+
default={},
|
229
|
+
),
|
230
|
+
}
|
231
|
+
|
232
|
+
def run(
|
233
|
+
self,
|
234
|
+
action: str,
|
235
|
+
session_id: Optional[str] = None,
|
236
|
+
user_id: Optional[str] = None,
|
237
|
+
ip_address: Optional[str] = None,
|
238
|
+
device_info: Optional[Dict[str, Any]] = None,
|
239
|
+
**kwargs,
|
240
|
+
) -> Dict[str, Any]:
|
241
|
+
"""Run session management operation.
|
242
|
+
|
243
|
+
Args:
|
244
|
+
action: Session action (create, validate, update, terminate, cleanup)
|
245
|
+
session_id: Session ID for operations
|
246
|
+
user_id: User ID for session operations
|
247
|
+
ip_address: Client IP address
|
248
|
+
device_info: Device information
|
249
|
+
**kwargs: Additional parameters
|
250
|
+
|
251
|
+
Returns:
|
252
|
+
Dictionary containing operation results
|
253
|
+
"""
|
254
|
+
start_time = datetime.now(UTC)
|
255
|
+
device_info = device_info or {}
|
256
|
+
|
257
|
+
try:
|
258
|
+
# Validate and sanitize inputs
|
259
|
+
safe_params = self.validate_and_sanitize_inputs(
|
260
|
+
{
|
261
|
+
"action": action,
|
262
|
+
"session_id": session_id or "",
|
263
|
+
"user_id": user_id or "",
|
264
|
+
"ip_address": ip_address or "",
|
265
|
+
"device_info": device_info,
|
266
|
+
}
|
267
|
+
)
|
268
|
+
|
269
|
+
action = safe_params["action"]
|
270
|
+
session_id = safe_params["session_id"] or None
|
271
|
+
user_id = safe_params["user_id"] or None
|
272
|
+
ip_address = safe_params["ip_address"] or None
|
273
|
+
device_info = safe_params["device_info"]
|
274
|
+
|
275
|
+
self.log_node_execution("session_operation_start", action=action)
|
276
|
+
|
277
|
+
# Perform periodic cleanup
|
278
|
+
self._maybe_cleanup_sessions()
|
279
|
+
|
280
|
+
# Route to appropriate action handler
|
281
|
+
if action == "create":
|
282
|
+
if not user_id or not ip_address:
|
283
|
+
return {
|
284
|
+
"success": False,
|
285
|
+
"error": "user_id and ip_address required for create",
|
286
|
+
}
|
287
|
+
result = self._create_session(user_id, ip_address, device_info)
|
288
|
+
self.session_stats["total_sessions_created"] += 1
|
289
|
+
|
290
|
+
elif action == "validate":
|
291
|
+
if not session_id:
|
292
|
+
return {
|
293
|
+
"success": False,
|
294
|
+
"error": "session_id required for validate",
|
295
|
+
}
|
296
|
+
result = self._validate_session(session_id)
|
297
|
+
|
298
|
+
elif action == "update":
|
299
|
+
if not session_id:
|
300
|
+
return {"success": False, "error": "session_id required for update"}
|
301
|
+
result = self._update_session_activity(session_id, kwargs)
|
302
|
+
|
303
|
+
elif action == "terminate":
|
304
|
+
if session_id:
|
305
|
+
result = self._terminate_session(
|
306
|
+
session_id, kwargs.get("reason", "user_logout")
|
307
|
+
)
|
308
|
+
elif user_id:
|
309
|
+
result = self._terminate_user_sessions(
|
310
|
+
user_id, kwargs.get("reason", "admin_action")
|
311
|
+
)
|
312
|
+
else:
|
313
|
+
return {
|
314
|
+
"success": False,
|
315
|
+
"error": "session_id or user_id required for terminate",
|
316
|
+
}
|
317
|
+
|
318
|
+
elif action == "cleanup":
|
319
|
+
result = self._cleanup_expired_sessions()
|
320
|
+
|
321
|
+
elif action == "list":
|
322
|
+
if not user_id:
|
323
|
+
return {"success": False, "error": "user_id required for list"}
|
324
|
+
result = self._list_user_sessions(user_id)
|
325
|
+
|
326
|
+
elif action == "stats":
|
327
|
+
result = self._get_session_statistics()
|
328
|
+
|
329
|
+
else:
|
330
|
+
result = {"success": False, "error": f"Unknown action: {action}"}
|
331
|
+
|
332
|
+
# Add timing information
|
333
|
+
processing_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
|
334
|
+
result["processing_time_ms"] = processing_time
|
335
|
+
result["timestamp"] = start_time.isoformat()
|
336
|
+
|
337
|
+
self.log_node_execution(
|
338
|
+
"session_operation_complete",
|
339
|
+
action=action,
|
340
|
+
success=result.get("success", False),
|
341
|
+
processing_time_ms=processing_time,
|
342
|
+
)
|
343
|
+
|
344
|
+
return result
|
345
|
+
|
346
|
+
except Exception as e:
|
347
|
+
self.log_error_with_traceback(e, "session_management")
|
348
|
+
raise
|
349
|
+
|
350
|
+
def _create_session(
|
351
|
+
self, user_id: str, ip_address: str, device_info: Dict[str, Any]
|
352
|
+
) -> Dict[str, Any]:
|
353
|
+
"""Create new user session.
|
354
|
+
|
355
|
+
Args:
|
356
|
+
user_id: User ID
|
357
|
+
ip_address: Client IP address
|
358
|
+
device_info: Device information
|
359
|
+
|
360
|
+
Returns:
|
361
|
+
Session creation result
|
362
|
+
"""
|
363
|
+
with self._sessions_lock:
|
364
|
+
# Check concurrent session limit
|
365
|
+
user_session_count = len(self.user_sessions.get(user_id, set()))
|
366
|
+
if user_session_count >= self.max_sessions:
|
367
|
+
self.session_stats["concurrent_limit_hits"] += 1
|
368
|
+
|
369
|
+
# Terminate oldest session
|
370
|
+
oldest_session = self._get_oldest_user_session(user_id)
|
371
|
+
if oldest_session:
|
372
|
+
self._terminate_session_internal(
|
373
|
+
oldest_session, "concurrent_limit_exceeded"
|
374
|
+
)
|
375
|
+
self._log_security_event(
|
376
|
+
user_id,
|
377
|
+
"session_limit_exceeded",
|
378
|
+
"medium",
|
379
|
+
{"max_sessions": self.max_sessions},
|
380
|
+
)
|
381
|
+
|
382
|
+
# Generate session ID
|
383
|
+
session_id = self._generate_session_id()
|
384
|
+
|
385
|
+
# Process device information
|
386
|
+
device = self._process_device_info(device_info, ip_address)
|
387
|
+
|
388
|
+
# Calculate session risk score
|
389
|
+
risk_score = self._calculate_session_risk(user_id, ip_address, device)
|
390
|
+
|
391
|
+
# Create session data
|
392
|
+
current_time = datetime.now(UTC)
|
393
|
+
session_data = SessionData(
|
394
|
+
session_id=session_id,
|
395
|
+
user_id=user_id,
|
396
|
+
created_at=current_time,
|
397
|
+
last_activity=current_time,
|
398
|
+
expires_at=current_time + self.absolute_timeout,
|
399
|
+
ip_address=ip_address,
|
400
|
+
device_info=device,
|
401
|
+
status=SessionStatus.ACTIVE,
|
402
|
+
login_method="password", # Default, should be set by caller
|
403
|
+
risk_score=risk_score,
|
404
|
+
anomaly_flags=[],
|
405
|
+
page_views=0,
|
406
|
+
actions_performed=0,
|
407
|
+
data_accessed_mb=0.0,
|
408
|
+
metadata={},
|
409
|
+
)
|
410
|
+
|
411
|
+
# Add geo-location if enabled
|
412
|
+
if self.enable_geo_tracking:
|
413
|
+
location = self._get_ip_location(ip_address)
|
414
|
+
session_data.country = location.get("country")
|
415
|
+
session_data.city = location.get("city")
|
416
|
+
|
417
|
+
# Store session
|
418
|
+
self.sessions[session_id] = session_data
|
419
|
+
|
420
|
+
# Update user sessions mapping
|
421
|
+
if user_id not in self.user_sessions:
|
422
|
+
self.user_sessions[user_id] = set()
|
423
|
+
self.user_sessions[user_id].add(session_id)
|
424
|
+
|
425
|
+
# Update device sessions mapping if tracking enabled
|
426
|
+
if self.track_devices:
|
427
|
+
device_fingerprint = device.fingerprint
|
428
|
+
if device_fingerprint not in self.device_sessions:
|
429
|
+
self.device_sessions[device_fingerprint] = set()
|
430
|
+
self.session_stats["devices_tracked"] += 1
|
431
|
+
self.device_sessions[device_fingerprint].add(session_id)
|
432
|
+
|
433
|
+
# Update statistics
|
434
|
+
self.session_stats["active_sessions"] += 1
|
435
|
+
|
436
|
+
# Audit log session creation
|
437
|
+
self._audit_session_operation("create", session_data)
|
438
|
+
|
439
|
+
# Check for anomalies
|
440
|
+
if self.anomaly_detection:
|
441
|
+
anomalies = self._detect_session_anomalies(session_data)
|
442
|
+
if anomalies:
|
443
|
+
session_data.anomaly_flags.extend(anomalies)
|
444
|
+
self.session_stats["anomalies_detected"] += len(anomalies)
|
445
|
+
self._log_security_event(
|
446
|
+
user_id,
|
447
|
+
"session_anomaly_detected",
|
448
|
+
"medium",
|
449
|
+
{"anomalies": anomalies, "session_id": session_id},
|
450
|
+
)
|
451
|
+
|
452
|
+
return {
|
453
|
+
"success": True,
|
454
|
+
"session_id": session_id,
|
455
|
+
"expires_at": session_data.expires_at.isoformat(),
|
456
|
+
"risk_score": risk_score,
|
457
|
+
"device_tracked": self.track_devices,
|
458
|
+
"anomalies": session_data.anomaly_flags,
|
459
|
+
"concurrent_sessions": len(self.user_sessions[user_id]),
|
460
|
+
}
|
461
|
+
|
462
|
+
def _validate_session(self, session_id: str) -> Dict[str, Any]:
|
463
|
+
"""Validate session and check for anomalies.
|
464
|
+
|
465
|
+
Args:
|
466
|
+
session_id: Session ID to validate
|
467
|
+
|
468
|
+
Returns:
|
469
|
+
Session validation result
|
470
|
+
"""
|
471
|
+
with self._sessions_lock:
|
472
|
+
if session_id not in self.sessions:
|
473
|
+
return {"success": True, "valid": False, "reason": "session_not_found"}
|
474
|
+
|
475
|
+
session_data = self.sessions[session_id]
|
476
|
+
current_time = datetime.now(UTC)
|
477
|
+
|
478
|
+
# Check if session is expired
|
479
|
+
if current_time > session_data.expires_at:
|
480
|
+
session_data.status = SessionStatus.EXPIRED
|
481
|
+
self._cleanup_session_internal(session_id)
|
482
|
+
return {"success": True, "valid": False, "reason": "session_expired"}
|
483
|
+
|
484
|
+
# Check idle timeout
|
485
|
+
idle_time = current_time - session_data.last_activity
|
486
|
+
if idle_time > self.idle_timeout:
|
487
|
+
session_data.status = SessionStatus.IDLE
|
488
|
+
if idle_time > self.idle_timeout * 2: # Grace period
|
489
|
+
self._cleanup_session_internal(session_id)
|
490
|
+
return {
|
491
|
+
"success": True,
|
492
|
+
"valid": False,
|
493
|
+
"reason": "session_idle_timeout",
|
494
|
+
}
|
495
|
+
|
496
|
+
# Check for suspicious activity
|
497
|
+
if session_data.status == SessionStatus.SUSPICIOUS:
|
498
|
+
return {"success": True, "valid": False, "reason": "session_suspicious"}
|
499
|
+
|
500
|
+
# Session is valid
|
501
|
+
return {
|
502
|
+
"success": True,
|
503
|
+
"valid": True,
|
504
|
+
"session_data": {
|
505
|
+
"user_id": session_data.user_id,
|
506
|
+
"created_at": session_data.created_at.isoformat(),
|
507
|
+
"last_activity": session_data.last_activity.isoformat(),
|
508
|
+
"expires_at": session_data.expires_at.isoformat(),
|
509
|
+
"status": session_data.status.value,
|
510
|
+
"risk_score": session_data.risk_score,
|
511
|
+
"anomaly_flags": session_data.anomaly_flags,
|
512
|
+
"device_type": session_data.device_info.device_type,
|
513
|
+
"location": (
|
514
|
+
f"{session_data.city}, {session_data.country}"
|
515
|
+
if session_data.city
|
516
|
+
else None
|
517
|
+
),
|
518
|
+
},
|
519
|
+
}
|
520
|
+
|
521
|
+
def _update_session_activity(
|
522
|
+
self, session_id: str, activity_data: Dict[str, Any]
|
523
|
+
) -> Dict[str, Any]:
|
524
|
+
"""Update session activity and check for anomalies.
|
525
|
+
|
526
|
+
Args:
|
527
|
+
session_id: Session ID
|
528
|
+
activity_data: Activity data to record
|
529
|
+
|
530
|
+
Returns:
|
531
|
+
Update result
|
532
|
+
"""
|
533
|
+
with self._sessions_lock:
|
534
|
+
if session_id not in self.sessions:
|
535
|
+
return {"success": False, "error": "session_not_found"}
|
536
|
+
|
537
|
+
session_data = self.sessions[session_id]
|
538
|
+
current_time = datetime.now(UTC)
|
539
|
+
|
540
|
+
# Update last activity
|
541
|
+
session_data.last_activity = current_time
|
542
|
+
session_data.status = SessionStatus.ACTIVE
|
543
|
+
|
544
|
+
# Update activity counters
|
545
|
+
if "page_views" in activity_data:
|
546
|
+
session_data.page_views += activity_data["page_views"]
|
547
|
+
|
548
|
+
if "actions_performed" in activity_data:
|
549
|
+
session_data.actions_performed += activity_data["actions_performed"]
|
550
|
+
|
551
|
+
if "data_accessed_mb" in activity_data:
|
552
|
+
session_data.data_accessed_mb += activity_data["data_accessed_mb"]
|
553
|
+
|
554
|
+
# Check for new anomalies
|
555
|
+
if self.anomaly_detection:
|
556
|
+
new_anomalies = self._detect_activity_anomalies(
|
557
|
+
session_data, activity_data
|
558
|
+
)
|
559
|
+
if new_anomalies:
|
560
|
+
session_data.anomaly_flags.extend(new_anomalies)
|
561
|
+
self.session_stats["anomalies_detected"] += len(new_anomalies)
|
562
|
+
|
563
|
+
# Mark session as suspicious if too many anomalies
|
564
|
+
if len(session_data.anomaly_flags) > 3:
|
565
|
+
session_data.status = SessionStatus.SUSPICIOUS
|
566
|
+
self._log_security_event(
|
567
|
+
session_data.user_id,
|
568
|
+
"session_marked_suspicious",
|
569
|
+
"high",
|
570
|
+
{
|
571
|
+
"session_id": session_id,
|
572
|
+
"anomalies": session_data.anomaly_flags,
|
573
|
+
},
|
574
|
+
)
|
575
|
+
|
576
|
+
return {
|
577
|
+
"success": True,
|
578
|
+
"session_updated": True,
|
579
|
+
"status": session_data.status.value,
|
580
|
+
"new_anomalies": new_anomalies if self.anomaly_detection else [],
|
581
|
+
"total_anomalies": len(session_data.anomaly_flags),
|
582
|
+
}
|
583
|
+
|
584
|
+
def _terminate_session(
|
585
|
+
self, session_id: str, reason: str = "user_logout"
|
586
|
+
) -> Dict[str, Any]:
|
587
|
+
"""Terminate user session.
|
588
|
+
|
589
|
+
Args:
|
590
|
+
session_id: Session ID to terminate
|
591
|
+
reason: Termination reason
|
592
|
+
|
593
|
+
Returns:
|
594
|
+
Termination result
|
595
|
+
"""
|
596
|
+
with self._sessions_lock:
|
597
|
+
if session_id not in self.sessions:
|
598
|
+
return {"success": False, "error": "session_not_found"}
|
599
|
+
|
600
|
+
session_data = self.sessions[session_id]
|
601
|
+
self._terminate_session_internal(session_id, reason)
|
602
|
+
|
603
|
+
# Audit log termination
|
604
|
+
self._audit_session_operation("terminate", session_data, {"reason": reason})
|
605
|
+
|
606
|
+
return {
|
607
|
+
"success": True,
|
608
|
+
"session_terminated": True,
|
609
|
+
"reason": reason,
|
610
|
+
"user_id": session_data.user_id,
|
611
|
+
}
|
612
|
+
|
613
|
+
def _terminate_user_sessions(
|
614
|
+
self, user_id: str, reason: str = "admin_action"
|
615
|
+
) -> Dict[str, Any]:
|
616
|
+
"""Terminate all sessions for a user.
|
617
|
+
|
618
|
+
Args:
|
619
|
+
user_id: User ID
|
620
|
+
reason: Termination reason
|
621
|
+
|
622
|
+
Returns:
|
623
|
+
Termination result
|
624
|
+
"""
|
625
|
+
with self._sessions_lock:
|
626
|
+
if user_id not in self.user_sessions:
|
627
|
+
return {"success": True, "sessions_terminated": 0}
|
628
|
+
|
629
|
+
session_ids = list(self.user_sessions[user_id])
|
630
|
+
terminated_count = 0
|
631
|
+
|
632
|
+
for session_id in session_ids:
|
633
|
+
if session_id in self.sessions:
|
634
|
+
self._terminate_session_internal(session_id, reason)
|
635
|
+
terminated_count += 1
|
636
|
+
|
637
|
+
# Log security event for mass termination
|
638
|
+
if terminated_count > 1:
|
639
|
+
self._log_security_event(
|
640
|
+
user_id,
|
641
|
+
"mass_session_termination",
|
642
|
+
"medium",
|
643
|
+
{"sessions_terminated": terminated_count, "reason": reason},
|
644
|
+
)
|
645
|
+
|
646
|
+
return {
|
647
|
+
"success": True,
|
648
|
+
"sessions_terminated": terminated_count,
|
649
|
+
"reason": reason,
|
650
|
+
}
|
651
|
+
|
652
|
+
def _terminate_session_internal(self, session_id: str, reason: str) -> None:
|
653
|
+
"""Internal session termination.
|
654
|
+
|
655
|
+
Args:
|
656
|
+
session_id: Session ID
|
657
|
+
reason: Termination reason
|
658
|
+
"""
|
659
|
+
if session_id not in self.sessions:
|
660
|
+
return
|
661
|
+
|
662
|
+
session_data = self.sessions[session_id]
|
663
|
+
|
664
|
+
# Update status
|
665
|
+
session_data.status = SessionStatus.TERMINATED
|
666
|
+
|
667
|
+
# Remove from active sessions
|
668
|
+
self._cleanup_session_internal(session_id)
|
669
|
+
|
670
|
+
# Update statistics
|
671
|
+
self.session_stats["sessions_terminated"] += 1
|
672
|
+
|
673
|
+
def _cleanup_session_internal(self, session_id: str) -> None:
|
674
|
+
"""Internal session cleanup.
|
675
|
+
|
676
|
+
Args:
|
677
|
+
session_id: Session ID to cleanup
|
678
|
+
"""
|
679
|
+
if session_id not in self.sessions:
|
680
|
+
return
|
681
|
+
|
682
|
+
session_data = self.sessions[session_id]
|
683
|
+
|
684
|
+
# Remove from user sessions
|
685
|
+
if session_data.user_id in self.user_sessions:
|
686
|
+
self.user_sessions[session_data.user_id].discard(session_id)
|
687
|
+
if not self.user_sessions[session_data.user_id]:
|
688
|
+
del self.user_sessions[session_data.user_id]
|
689
|
+
|
690
|
+
# Remove from device sessions
|
691
|
+
if self.track_devices:
|
692
|
+
device_fingerprint = session_data.device_info.fingerprint
|
693
|
+
if device_fingerprint in self.device_sessions:
|
694
|
+
self.device_sessions[device_fingerprint].discard(session_id)
|
695
|
+
if not self.device_sessions[device_fingerprint]:
|
696
|
+
del self.device_sessions[device_fingerprint]
|
697
|
+
|
698
|
+
# Remove session
|
699
|
+
del self.sessions[session_id]
|
700
|
+
|
701
|
+
# Update statistics
|
702
|
+
if self.session_stats["active_sessions"] > 0:
|
703
|
+
self.session_stats["active_sessions"] -= 1
|
704
|
+
|
705
|
+
def _cleanup_expired_sessions(self) -> Dict[str, Any]:
|
706
|
+
"""Clean up expired and idle sessions.
|
707
|
+
|
708
|
+
Returns:
|
709
|
+
Cleanup result
|
710
|
+
"""
|
711
|
+
current_time = datetime.now(UTC)
|
712
|
+
expired_sessions = []
|
713
|
+
idle_sessions = []
|
714
|
+
|
715
|
+
with self._sessions_lock:
|
716
|
+
for session_id, session_data in list(self.sessions.items()):
|
717
|
+
# Check for expired sessions
|
718
|
+
if current_time > session_data.expires_at:
|
719
|
+
expired_sessions.append(session_id)
|
720
|
+
continue
|
721
|
+
|
722
|
+
# Check for idle sessions beyond grace period
|
723
|
+
idle_time = current_time - session_data.last_activity
|
724
|
+
if idle_time > self.idle_timeout * 2: # Grace period
|
725
|
+
idle_sessions.append(session_id)
|
726
|
+
|
727
|
+
# Clean up expired and idle sessions
|
728
|
+
for session_id in expired_sessions + idle_sessions:
|
729
|
+
self._cleanup_session_internal(session_id)
|
730
|
+
|
731
|
+
# Update statistics
|
732
|
+
total_cleaned = len(expired_sessions) + len(idle_sessions)
|
733
|
+
self.session_stats["expired_sessions_cleaned"] += total_cleaned
|
734
|
+
|
735
|
+
# Update last cleanup time
|
736
|
+
self._last_cleanup = current_time
|
737
|
+
|
738
|
+
return {
|
739
|
+
"success": True,
|
740
|
+
"expired_sessions_cleaned": len(expired_sessions),
|
741
|
+
"idle_sessions_cleaned": len(idle_sessions),
|
742
|
+
"total_cleaned": total_cleaned,
|
743
|
+
}
|
744
|
+
|
745
|
+
def _maybe_cleanup_sessions(self) -> None:
|
746
|
+
"""Perform cleanup if enough time has passed."""
|
747
|
+
current_time = datetime.now(UTC)
|
748
|
+
if (current_time - self._last_cleanup).total_seconds() > self.cleanup_interval:
|
749
|
+
self._cleanup_expired_sessions()
|
750
|
+
|
751
|
+
def _list_user_sessions(self, user_id: str) -> Dict[str, Any]:
|
752
|
+
"""List all sessions for a user.
|
753
|
+
|
754
|
+
Args:
|
755
|
+
user_id: User ID
|
756
|
+
|
757
|
+
Returns:
|
758
|
+
List of user sessions
|
759
|
+
"""
|
760
|
+
with self._sessions_lock:
|
761
|
+
if user_id not in self.user_sessions:
|
762
|
+
return {"success": True, "sessions": []}
|
763
|
+
|
764
|
+
sessions_list = []
|
765
|
+
for session_id in self.user_sessions[user_id]:
|
766
|
+
if session_id in self.sessions:
|
767
|
+
session_data = self.sessions[session_id]
|
768
|
+
sessions_list.append(
|
769
|
+
{
|
770
|
+
"session_id": session_id,
|
771
|
+
"created_at": session_data.created_at.isoformat(),
|
772
|
+
"last_activity": session_data.last_activity.isoformat(),
|
773
|
+
"status": session_data.status.value,
|
774
|
+
"ip_address": session_data.ip_address,
|
775
|
+
"device_type": session_data.device_info.device_type,
|
776
|
+
"device_os": f"{session_data.device_info.os_name} {session_data.device_info.os_version}",
|
777
|
+
"browser": f"{session_data.device_info.browser_name} {session_data.device_info.browser_version}",
|
778
|
+
"location": (
|
779
|
+
f"{session_data.city}, {session_data.country}"
|
780
|
+
if session_data.city
|
781
|
+
else None
|
782
|
+
),
|
783
|
+
"risk_score": session_data.risk_score,
|
784
|
+
"anomaly_count": len(session_data.anomaly_flags),
|
785
|
+
}
|
786
|
+
)
|
787
|
+
|
788
|
+
return {
|
789
|
+
"success": True,
|
790
|
+
"sessions": sessions_list,
|
791
|
+
"total_sessions": len(sessions_list),
|
792
|
+
}
|
793
|
+
|
794
|
+
def _get_session_statistics(self) -> Dict[str, Any]:
|
795
|
+
"""Get session management statistics.
|
796
|
+
|
797
|
+
Returns:
|
798
|
+
Session statistics
|
799
|
+
"""
|
800
|
+
with self._sessions_lock:
|
801
|
+
# Calculate additional statistics
|
802
|
+
device_count = len(self.device_sessions) if self.track_devices else 0
|
803
|
+
user_count = len(self.user_sessions)
|
804
|
+
|
805
|
+
# Session status distribution
|
806
|
+
status_distribution = {}
|
807
|
+
for session_data in self.sessions.values():
|
808
|
+
status = session_data.status.value
|
809
|
+
status_distribution[status] = status_distribution.get(status, 0) + 1
|
810
|
+
|
811
|
+
return {
|
812
|
+
"success": True,
|
813
|
+
"statistics": {
|
814
|
+
**self.session_stats,
|
815
|
+
"current_active_sessions": len(self.sessions),
|
816
|
+
"unique_users": user_count,
|
817
|
+
"unique_devices": device_count,
|
818
|
+
"status_distribution": status_distribution,
|
819
|
+
"max_sessions_per_user": self.max_sessions,
|
820
|
+
"idle_timeout_minutes": self.idle_timeout.total_seconds() / 60,
|
821
|
+
"absolute_timeout_hours": self.absolute_timeout.total_seconds()
|
822
|
+
/ 3600,
|
823
|
+
},
|
824
|
+
}
|
825
|
+
|
826
|
+
def _generate_session_id(self) -> str:
|
827
|
+
"""Generate secure session ID.
|
828
|
+
|
829
|
+
Returns:
|
830
|
+
Session ID
|
831
|
+
"""
|
832
|
+
return secrets.token_urlsafe(32)
|
833
|
+
|
834
|
+
def _process_device_info(
|
835
|
+
self, device_info: Dict[str, Any], ip_address: str
|
836
|
+
) -> DeviceInfo:
|
837
|
+
"""Process and create device information.
|
838
|
+
|
839
|
+
Args:
|
840
|
+
device_info: Raw device information
|
841
|
+
ip_address: Client IP address
|
842
|
+
|
843
|
+
Returns:
|
844
|
+
Processed device information
|
845
|
+
"""
|
846
|
+
# Extract device information with defaults
|
847
|
+
device_type = device_info.get("device_type", "unknown")
|
848
|
+
os_name = device_info.get("os_name", "unknown")
|
849
|
+
os_version = device_info.get("os_version", "unknown")
|
850
|
+
browser_name = device_info.get("browser_name", "unknown")
|
851
|
+
browser_version = device_info.get("browser_version", "unknown")
|
852
|
+
user_agent = device_info.get("user_agent", "unknown")
|
853
|
+
|
854
|
+
# Generate device fingerprint
|
855
|
+
fingerprint_data = f"{device_type}:{os_name}:{os_version}:{browser_name}:{browser_version}:{user_agent}"
|
856
|
+
fingerprint = hashlib.sha256(fingerprint_data.encode()).hexdigest()[:16]
|
857
|
+
|
858
|
+
# Create device ID (more stable than fingerprint)
|
859
|
+
device_id = device_info.get("device_id", fingerprint)
|
860
|
+
|
861
|
+
return DeviceInfo(
|
862
|
+
device_id=device_id,
|
863
|
+
device_type=device_type,
|
864
|
+
os_name=os_name,
|
865
|
+
os_version=os_version,
|
866
|
+
browser_name=browser_name,
|
867
|
+
browser_version=browser_version,
|
868
|
+
user_agent=user_agent,
|
869
|
+
fingerprint=fingerprint,
|
870
|
+
)
|
871
|
+
|
872
|
+
def _calculate_session_risk(
|
873
|
+
self, user_id: str, ip_address: str, device: DeviceInfo
|
874
|
+
) -> float:
|
875
|
+
"""Calculate session risk score.
|
876
|
+
|
877
|
+
Args:
|
878
|
+
user_id: User ID
|
879
|
+
ip_address: IP address
|
880
|
+
device: Device information
|
881
|
+
|
882
|
+
Returns:
|
883
|
+
Risk score (0-1)
|
884
|
+
"""
|
885
|
+
risk_score = 0.0
|
886
|
+
|
887
|
+
# Check for new device
|
888
|
+
if self.track_devices and device.fingerprint not in self.device_sessions:
|
889
|
+
risk_score += 0.3
|
890
|
+
|
891
|
+
# Check for multiple active sessions
|
892
|
+
user_session_count = len(self.user_sessions.get(user_id, set()))
|
893
|
+
if user_session_count >= self.max_sessions - 1:
|
894
|
+
risk_score += 0.2
|
895
|
+
|
896
|
+
# Check for unusual device type
|
897
|
+
if device.device_type == "unknown":
|
898
|
+
risk_score += 0.2
|
899
|
+
|
900
|
+
# Check for mobile devices (potentially higher risk)
|
901
|
+
if device.device_type == "mobile":
|
902
|
+
risk_score += 0.1
|
903
|
+
|
904
|
+
# Geo-location checks would go here
|
905
|
+
# For now, add base risk for any session
|
906
|
+
risk_score += 0.1
|
907
|
+
|
908
|
+
return min(1.0, risk_score)
|
909
|
+
|
910
|
+
def _get_ip_location(self, ip_address: str) -> Dict[str, str]:
|
911
|
+
"""Get geographic location for IP address.
|
912
|
+
|
913
|
+
Args:
|
914
|
+
ip_address: IP address
|
915
|
+
|
916
|
+
Returns:
|
917
|
+
Location information
|
918
|
+
"""
|
919
|
+
# In a real implementation, this would use a geo-IP service
|
920
|
+
# For now, return mock data
|
921
|
+
return {"country": "Unknown", "city": "Unknown"}
|
922
|
+
|
923
|
+
def _detect_session_anomalies(self, session_data: SessionData) -> List[str]:
|
924
|
+
"""Detect anomalies in session creation.
|
925
|
+
|
926
|
+
Args:
|
927
|
+
session_data: Session data to analyze
|
928
|
+
|
929
|
+
Returns:
|
930
|
+
List of anomaly indicators
|
931
|
+
"""
|
932
|
+
anomalies = []
|
933
|
+
|
934
|
+
# Check for multiple concurrent sessions
|
935
|
+
user_session_count = len(self.user_sessions.get(session_data.user_id, set()))
|
936
|
+
if user_session_count >= self.max_sessions:
|
937
|
+
anomalies.append("max_concurrent_sessions")
|
938
|
+
|
939
|
+
# Check for new device
|
940
|
+
if (
|
941
|
+
self.track_devices
|
942
|
+
and session_data.device_info.fingerprint not in self.device_sessions
|
943
|
+
):
|
944
|
+
anomalies.append("new_device")
|
945
|
+
|
946
|
+
# Check for unusual IP address
|
947
|
+
# In a real implementation, this would check against user's IP history
|
948
|
+
if session_data.ip_address.startswith(
|
949
|
+
"10."
|
950
|
+
) or session_data.ip_address.startswith("192.168."):
|
951
|
+
# Internal IP - potentially lower risk
|
952
|
+
pass
|
953
|
+
else:
|
954
|
+
# External IP - check against known IPs
|
955
|
+
anomalies.append("external_ip")
|
956
|
+
|
957
|
+
# Check for high risk score
|
958
|
+
if session_data.risk_score > 0.7:
|
959
|
+
anomalies.append("high_risk_score")
|
960
|
+
|
961
|
+
return anomalies
|
962
|
+
|
963
|
+
def _detect_activity_anomalies(
|
964
|
+
self, session_data: SessionData, activity_data: Dict[str, Any]
|
965
|
+
) -> List[str]:
|
966
|
+
"""Detect anomalies in session activity.
|
967
|
+
|
968
|
+
Args:
|
969
|
+
session_data: Session data
|
970
|
+
activity_data: New activity data
|
971
|
+
|
972
|
+
Returns:
|
973
|
+
List of anomaly indicators
|
974
|
+
"""
|
975
|
+
anomalies = []
|
976
|
+
|
977
|
+
# Check for excessive page views
|
978
|
+
new_page_views = activity_data.get("page_views", 0)
|
979
|
+
if new_page_views > 100: # More than 100 page views in one update
|
980
|
+
anomalies.append("excessive_page_views")
|
981
|
+
|
982
|
+
# Check for excessive actions
|
983
|
+
new_actions = activity_data.get("actions_performed", 0)
|
984
|
+
if new_actions > 500: # More than 500 actions in one update
|
985
|
+
anomalies.append("excessive_actions")
|
986
|
+
|
987
|
+
# Check for large data access
|
988
|
+
new_data_mb = activity_data.get("data_accessed_mb", 0)
|
989
|
+
if new_data_mb > 100: # More than 100MB in one update
|
990
|
+
anomalies.append("large_data_access")
|
991
|
+
|
992
|
+
# Check session duration vs activity
|
993
|
+
session_duration = (
|
994
|
+
datetime.now(UTC) - session_data.created_at
|
995
|
+
).total_seconds() / 60
|
996
|
+
if session_duration < 5 and (new_page_views > 50 or new_actions > 100):
|
997
|
+
anomalies.append("rapid_activity")
|
998
|
+
|
999
|
+
return anomalies
|
1000
|
+
|
1001
|
+
def _get_oldest_user_session(self, user_id: str) -> Optional[str]:
|
1002
|
+
"""Get oldest session for user.
|
1003
|
+
|
1004
|
+
Args:
|
1005
|
+
user_id: User ID
|
1006
|
+
|
1007
|
+
Returns:
|
1008
|
+
Oldest session ID or None
|
1009
|
+
"""
|
1010
|
+
if user_id not in self.user_sessions:
|
1011
|
+
return None
|
1012
|
+
|
1013
|
+
oldest_session = None
|
1014
|
+
oldest_time = None
|
1015
|
+
|
1016
|
+
for session_id in self.user_sessions[user_id]:
|
1017
|
+
if session_id in self.sessions:
|
1018
|
+
session_data = self.sessions[session_id]
|
1019
|
+
if oldest_time is None or session_data.created_at < oldest_time:
|
1020
|
+
oldest_time = session_data.created_at
|
1021
|
+
oldest_session = session_id
|
1022
|
+
|
1023
|
+
return oldest_session
|
1024
|
+
|
1025
|
+
def _audit_session_operation(
|
1026
|
+
self,
|
1027
|
+
operation: str,
|
1028
|
+
session_data: SessionData,
|
1029
|
+
metadata: Optional[Dict[str, Any]] = None,
|
1030
|
+
) -> None:
|
1031
|
+
"""Audit session operation.
|
1032
|
+
|
1033
|
+
Args:
|
1034
|
+
operation: Operation performed
|
1035
|
+
session_data: Session data
|
1036
|
+
metadata: Additional metadata
|
1037
|
+
"""
|
1038
|
+
audit_entry = {
|
1039
|
+
"action": f"session_{operation}",
|
1040
|
+
"user_id": session_data.user_id,
|
1041
|
+
"resource_type": "session",
|
1042
|
+
"resource_id": session_data.session_id,
|
1043
|
+
"metadata": {
|
1044
|
+
"operation": operation,
|
1045
|
+
"ip_address": session_data.ip_address,
|
1046
|
+
"device_type": session_data.device_info.device_type,
|
1047
|
+
"risk_score": session_data.risk_score,
|
1048
|
+
**(metadata or {}),
|
1049
|
+
},
|
1050
|
+
"ip_address": session_data.ip_address,
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
try:
|
1054
|
+
self.audit_log_node.run(**audit_entry)
|
1055
|
+
except Exception as e:
|
1056
|
+
self.log_with_context("WARNING", f"Failed to audit session operation: {e}")
|
1057
|
+
|
1058
|
+
def _log_security_event(
|
1059
|
+
self, user_id: str, event_type: str, severity: str, metadata: Dict[str, Any]
|
1060
|
+
) -> None:
|
1061
|
+
"""Log security event.
|
1062
|
+
|
1063
|
+
Args:
|
1064
|
+
user_id: User ID
|
1065
|
+
event_type: Type of security event
|
1066
|
+
severity: Event severity
|
1067
|
+
metadata: Event metadata
|
1068
|
+
"""
|
1069
|
+
security_event = {
|
1070
|
+
"event_type": event_type,
|
1071
|
+
"severity": severity,
|
1072
|
+
"description": f"Session management: {event_type}",
|
1073
|
+
"metadata": {"session_management": True, **metadata},
|
1074
|
+
"user_id": user_id,
|
1075
|
+
"source_ip": metadata.get("ip_address", "unknown"),
|
1076
|
+
}
|
1077
|
+
|
1078
|
+
try:
|
1079
|
+
self.security_event_node.run(**security_event)
|
1080
|
+
except Exception as e:
|
1081
|
+
self.log_with_context("WARNING", f"Failed to log security event: {e}")
|
1082
|
+
|
1083
|
+
def get_session_stats(self) -> Dict[str, Any]:
|
1084
|
+
"""Get session management statistics.
|
1085
|
+
|
1086
|
+
Returns:
|
1087
|
+
Dictionary with session statistics
|
1088
|
+
"""
|
1089
|
+
return self._get_session_statistics()["statistics"]
|
1090
|
+
|
1091
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
1092
|
+
"""Async execution method for enterprise integration."""
|
1093
|
+
return self.run(**kwargs)
|