kailash 0.3.2__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.
Files changed (146) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +25 -3
  37. kailash/nodes/admin/__init__.py +35 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1519 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +1 -0
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +407 -2
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/api/auth.py +287 -6
  50. kailash/nodes/api/rest.py +151 -0
  51. kailash/nodes/auth/__init__.py +17 -0
  52. kailash/nodes/auth/directory_integration.py +1228 -0
  53. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  54. kailash/nodes/auth/mfa.py +2338 -0
  55. kailash/nodes/auth/risk_assessment.py +872 -0
  56. kailash/nodes/auth/session_management.py +1093 -0
  57. kailash/nodes/auth/sso.py +1040 -0
  58. kailash/nodes/base.py +344 -13
  59. kailash/nodes/base_cycle_aware.py +4 -2
  60. kailash/nodes/base_with_acl.py +1 -1
  61. kailash/nodes/code/python.py +283 -10
  62. kailash/nodes/compliance/__init__.py +9 -0
  63. kailash/nodes/compliance/data_retention.py +1888 -0
  64. kailash/nodes/compliance/gdpr.py +2004 -0
  65. kailash/nodes/data/__init__.py +22 -2
  66. kailash/nodes/data/async_connection.py +469 -0
  67. kailash/nodes/data/async_sql.py +757 -0
  68. kailash/nodes/data/async_vector.py +598 -0
  69. kailash/nodes/data/readers.py +767 -0
  70. kailash/nodes/data/retrieval.py +360 -1
  71. kailash/nodes/data/sharepoint_graph.py +397 -21
  72. kailash/nodes/data/sql.py +94 -5
  73. kailash/nodes/data/streaming.py +68 -8
  74. kailash/nodes/data/vector_db.py +54 -4
  75. kailash/nodes/enterprise/__init__.py +13 -0
  76. kailash/nodes/enterprise/batch_processor.py +741 -0
  77. kailash/nodes/enterprise/data_lineage.py +497 -0
  78. kailash/nodes/logic/convergence.py +31 -9
  79. kailash/nodes/logic/operations.py +14 -3
  80. kailash/nodes/mixins/__init__.py +8 -0
  81. kailash/nodes/mixins/event_emitter.py +201 -0
  82. kailash/nodes/mixins/mcp.py +9 -4
  83. kailash/nodes/mixins/security.py +165 -0
  84. kailash/nodes/monitoring/__init__.py +7 -0
  85. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  86. kailash/nodes/rag/__init__.py +284 -0
  87. kailash/nodes/rag/advanced.py +1615 -0
  88. kailash/nodes/rag/agentic.py +773 -0
  89. kailash/nodes/rag/conversational.py +999 -0
  90. kailash/nodes/rag/evaluation.py +875 -0
  91. kailash/nodes/rag/federated.py +1188 -0
  92. kailash/nodes/rag/graph.py +721 -0
  93. kailash/nodes/rag/multimodal.py +671 -0
  94. kailash/nodes/rag/optimized.py +933 -0
  95. kailash/nodes/rag/privacy.py +1059 -0
  96. kailash/nodes/rag/query_processing.py +1335 -0
  97. kailash/nodes/rag/realtime.py +764 -0
  98. kailash/nodes/rag/registry.py +547 -0
  99. kailash/nodes/rag/router.py +837 -0
  100. kailash/nodes/rag/similarity.py +1854 -0
  101. kailash/nodes/rag/strategies.py +566 -0
  102. kailash/nodes/rag/workflows.py +575 -0
  103. kailash/nodes/security/__init__.py +19 -0
  104. kailash/nodes/security/abac_evaluator.py +1411 -0
  105. kailash/nodes/security/audit_log.py +91 -0
  106. kailash/nodes/security/behavior_analysis.py +1893 -0
  107. kailash/nodes/security/credential_manager.py +401 -0
  108. kailash/nodes/security/rotating_credentials.py +760 -0
  109. kailash/nodes/security/security_event.py +132 -0
  110. kailash/nodes/security/threat_detection.py +1103 -0
  111. kailash/nodes/testing/__init__.py +9 -0
  112. kailash/nodes/testing/credential_testing.py +499 -0
  113. kailash/nodes/transform/__init__.py +10 -2
  114. kailash/nodes/transform/chunkers.py +592 -1
  115. kailash/nodes/transform/processors.py +484 -14
  116. kailash/nodes/validation.py +321 -0
  117. kailash/runtime/access_controlled.py +1 -1
  118. kailash/runtime/async_local.py +41 -7
  119. kailash/runtime/docker.py +1 -1
  120. kailash/runtime/local.py +474 -55
  121. kailash/runtime/parallel.py +1 -1
  122. kailash/runtime/parallel_cyclic.py +1 -1
  123. kailash/runtime/testing.py +210 -2
  124. kailash/utils/migrations/__init__.py +25 -0
  125. kailash/utils/migrations/generator.py +433 -0
  126. kailash/utils/migrations/models.py +231 -0
  127. kailash/utils/migrations/runner.py +489 -0
  128. kailash/utils/secure_logging.py +342 -0
  129. kailash/workflow/__init__.py +16 -0
  130. kailash/workflow/cyclic_runner.py +3 -4
  131. kailash/workflow/graph.py +70 -2
  132. kailash/workflow/resilience.py +249 -0
  133. kailash/workflow/templates.py +726 -0
  134. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
  135. kailash-0.4.0.dist-info/RECORD +223 -0
  136. kailash/api/__init__.py +0 -17
  137. kailash/api/__main__.py +0 -6
  138. kailash/api/studio_secure.py +0 -893
  139. kailash/mcp/__main__.py +0 -13
  140. kailash/mcp/server_new.py +0 -336
  141. kailash/mcp/servers/__init__.py +0 -12
  142. kailash-0.3.2.dist-info/RECORD +0 -136
  143. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,872 @@
1
+ """
2
+ AI-powered authentication risk assessment node.
3
+
4
+ This module provides comprehensive risk assessment for authentication requests
5
+ including device trust analysis, location verification, behavioral patterns,
6
+ and ML-based anomaly detection.
7
+ """
8
+
9
+ import hashlib
10
+ import json
11
+ import logging
12
+ from dataclasses import dataclass
13
+ from datetime import UTC, datetime, timedelta
14
+ from enum import Enum
15
+ from typing import Any, Dict, List, Optional, Set
16
+
17
+ from kailash.nodes.ai.llm_agent import LLMAgentNode
18
+ from kailash.nodes.base import Node, NodeParameter, register_node
19
+ from kailash.nodes.mixins import LoggingMixin, PerformanceMixin, SecurityMixin
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class RiskLevel(Enum):
25
+ """Risk assessment levels."""
26
+
27
+ LOW = "low"
28
+ MEDIUM = "medium"
29
+ HIGH = "high"
30
+ CRITICAL = "critical"
31
+
32
+
33
+ @dataclass
34
+ class RiskContext:
35
+ """Risk assessment context."""
36
+
37
+ user_id: str
38
+ ip_address: str
39
+ device_info: Dict[str, Any]
40
+ timestamp: str
41
+ location: Optional[Dict[str, Any]] = None
42
+ user_timezone: Optional[str] = None
43
+ usual_hours: Optional[Dict[str, int]] = None
44
+ usual_locations: Optional[List[str]] = None
45
+
46
+
47
+ @dataclass
48
+ class RiskAssessment:
49
+ """Risk assessment result."""
50
+
51
+ risk_score: float
52
+ risk_level: RiskLevel
53
+ risk_factors: List[str]
54
+ trust_factors: List[str]
55
+ mitigation_required: List[str]
56
+ additional_checks: List[str]
57
+ confidence: float
58
+ assessment_time: datetime
59
+
60
+
61
+ @register_node()
62
+ class RiskAssessmentNode(SecurityMixin, PerformanceMixin, LoggingMixin, Node):
63
+ """AI-powered authentication risk assessment.
64
+
65
+ This node provides comprehensive risk assessment for authentication requests:
66
+ - Device trust analysis
67
+ - Location verification and geographic anomaly detection
68
+ - Behavioral pattern analysis
69
+ - Time-based access pattern evaluation
70
+ - Velocity checking (impossible travel detection)
71
+ - ML-enhanced anomaly detection
72
+ - Adaptive risk scoring based on user history
73
+
74
+ Example:
75
+ >>> risk_node = RiskAssessmentNode(
76
+ ... risk_factors=["ip_reputation", "device_trust", "location", "behavior"],
77
+ ... threshold_low=0.3,
78
+ ... threshold_medium=0.6,
79
+ ... threshold_high=0.8,
80
+ ... ml_enabled=True
81
+ ... )
82
+ >>>
83
+ >>> context = {
84
+ ... "user_id": "user123",
85
+ ... "ip_address": "203.0.113.100",
86
+ ... "device_info": {"device_id": "device_456", "recognized": False},
87
+ ... "timestamp": datetime.now(UTC).isoformat()
88
+ ... }
89
+ >>>
90
+ >>> result = risk_node.run(action="assess", context=context)
91
+ >>> print(f"Risk level: {result['risk_level']}")
92
+ """
93
+
94
+ def __init__(
95
+ self,
96
+ name: str = "risk_assessment",
97
+ risk_factors: Optional[List[str]] = None,
98
+ threshold_low: float = 0.3,
99
+ threshold_medium: float = 0.6,
100
+ threshold_high: float = 0.8,
101
+ ml_enabled: bool = True,
102
+ geoip_enabled: bool = True,
103
+ velocity_check_enabled: bool = True,
104
+ behavioral_analysis: bool = True,
105
+ **kwargs,
106
+ ):
107
+ """Initialize risk assessment node.
108
+
109
+ Args:
110
+ name: Node name
111
+ risk_factors: List of risk factors to evaluate
112
+ threshold_low: Threshold for low risk classification
113
+ threshold_medium: Threshold for medium risk classification
114
+ threshold_high: Threshold for high risk classification
115
+ ml_enabled: Enable machine learning-based analysis
116
+ geoip_enabled: Enable GeoIP location analysis
117
+ velocity_check_enabled: Enable velocity/travel time checking
118
+ behavioral_analysis: Enable behavioral pattern analysis
119
+ **kwargs: Additional node parameters
120
+ """
121
+ # Set attributes before calling super().__init__()
122
+ self.risk_factors = risk_factors or [
123
+ "ip_reputation",
124
+ "device_trust",
125
+ "location",
126
+ "behavior",
127
+ "time_pattern",
128
+ ]
129
+ self.threshold_low = threshold_low
130
+ self.threshold_medium = threshold_medium
131
+ self.threshold_high = threshold_high
132
+ self.ml_enabled = ml_enabled
133
+ self.geoip_enabled = geoip_enabled
134
+ self.velocity_check_enabled = velocity_check_enabled
135
+ self.behavioral_analysis = behavioral_analysis
136
+
137
+ # Initialize parent classes
138
+ super().__init__(name=name, **kwargs)
139
+
140
+ # User history storage
141
+ self.user_history: Dict[str, List[Dict[str, Any]]] = {}
142
+ self.successful_auths: Dict[str, List[Dict[str, Any]]] = {}
143
+
144
+ # Risk assessment statistics
145
+ self.assessment_stats = {
146
+ "total_assessments": 0,
147
+ "high_risk_count": 0,
148
+ "low_risk_count": 0,
149
+ "blocked_attempts": 0,
150
+ "avg_assessment_time_ms": 0,
151
+ }
152
+
153
+ def get_parameters(self) -> Dict[str, NodeParameter]:
154
+ """Get node parameters for validation and documentation.
155
+
156
+ Returns:
157
+ Dictionary mapping parameter names to NodeParameter objects
158
+ """
159
+ return {
160
+ "action": NodeParameter(
161
+ name="action",
162
+ type=str,
163
+ description="Risk assessment action to perform",
164
+ required=True,
165
+ ),
166
+ "context": NodeParameter(
167
+ name="context",
168
+ type=dict,
169
+ description="Risk context for assessment",
170
+ required=True,
171
+ ),
172
+ "include_mitigation": NodeParameter(
173
+ name="include_mitigation",
174
+ type=bool,
175
+ description="Include mitigation recommendations",
176
+ required=False,
177
+ default=False,
178
+ ),
179
+ }
180
+
181
+ def run(
182
+ self,
183
+ action: str,
184
+ context: Dict[str, Any],
185
+ include_mitigation: bool = False,
186
+ **kwargs,
187
+ ) -> Dict[str, Any]:
188
+ """Run risk assessment.
189
+
190
+ Args:
191
+ action: Assessment action to perform
192
+ context: Risk context
193
+ include_mitigation: Include mitigation recommendations
194
+ **kwargs: Additional parameters
195
+
196
+ Returns:
197
+ Dictionary containing risk assessment results
198
+ """
199
+ start_time = datetime.now(UTC)
200
+
201
+ try:
202
+ # Basic validation without deep sanitization to preserve context structure
203
+ if not isinstance(action, str):
204
+ raise ValueError("Action must be a string")
205
+ if not isinstance(context, dict):
206
+ raise ValueError("Context must be a dictionary")
207
+ if not isinstance(include_mitigation, bool):
208
+ include_mitigation = bool(include_mitigation)
209
+
210
+ self.log_node_execution("risk_assessment_start", action=action)
211
+
212
+ # Route to appropriate action handler
213
+ if action == "assess":
214
+ result = self._assess_risk(context, include_mitigation)
215
+ elif action == "record_successful_auth":
216
+ result = self._record_successful_auth(context)
217
+ else:
218
+ result = {"success": False, "error": f"Unknown action: {action}"}
219
+
220
+ # Update statistics
221
+ processing_time = (datetime.now(UTC) - start_time).total_seconds() * 1000
222
+ self._update_stats(processing_time, result.get("risk_level", "unknown"))
223
+
224
+ # Add timing information
225
+ result["processing_time_ms"] = processing_time
226
+ result["timestamp"] = start_time.isoformat()
227
+
228
+ self.log_node_execution(
229
+ "risk_assessment_complete",
230
+ action=action,
231
+ risk_level=result.get("risk_level", "unknown"),
232
+ processing_time_ms=processing_time,
233
+ )
234
+
235
+ return result
236
+
237
+ except Exception as e:
238
+ self.log_error_with_traceback(e, "risk_assessment")
239
+ raise
240
+
241
+ async def execute_async(self, **inputs) -> Dict[str, Any]:
242
+ """Async execution method for enterprise integration."""
243
+ return self.run(**inputs)
244
+
245
+ def _assess_risk(
246
+ self, context: Dict[str, Any], include_mitigation: bool = False
247
+ ) -> Dict[str, Any]:
248
+ """Assess authentication risk based on context.
249
+
250
+ Args:
251
+ context: Risk context
252
+ include_mitigation: Include mitigation recommendations
253
+
254
+ Returns:
255
+ Risk assessment results
256
+ """
257
+ risk_context = self._parse_risk_context(context)
258
+
259
+ # Record this assessment attempt in history for velocity checks
260
+ self._record_assessment_attempt(risk_context)
261
+
262
+ # Initialize risk factors
263
+ risk_factors = []
264
+ trust_factors = []
265
+ risk_score = 0.0
266
+
267
+ # Evaluate each risk factor
268
+ for factor in self.risk_factors:
269
+ factor_result = self._evaluate_risk_factor(factor, risk_context)
270
+ risk_score += factor_result["score"]
271
+
272
+ if factor_result["risk_indicators"]:
273
+ risk_factors.extend(factor_result["risk_indicators"])
274
+
275
+ if factor_result["trust_indicators"]:
276
+ trust_factors.extend(factor_result["trust_indicators"])
277
+
278
+ # Normalize risk score - don't divide by number of factors
279
+ risk_score = min(1.0, risk_score)
280
+
281
+ # Determine risk level
282
+ risk_level = self._determine_risk_level(risk_score)
283
+
284
+ # Check for adaptive adjustments based on history
285
+ if self.behavioral_analysis:
286
+ adjusted_score, adjustment_factors = self._apply_behavioral_adjustments(
287
+ risk_context, risk_score
288
+ )
289
+ risk_score = adjusted_score
290
+ risk_level = self._determine_risk_level(risk_score)
291
+
292
+ if adjustment_factors["trust"]:
293
+ trust_factors.extend(adjustment_factors["trust"])
294
+ if adjustment_factors["risk"]:
295
+ risk_factors.extend(adjustment_factors["risk"])
296
+
297
+ # Prepare result
298
+ result = {
299
+ "success": True,
300
+ "risk_score": round(risk_score, 3),
301
+ "risk_level": risk_level.value,
302
+ "risk_factors": list(set(risk_factors)),
303
+ "trust_factors": list(set(trust_factors)) if trust_factors else [],
304
+ "confidence": self._calculate_confidence(risk_context, risk_factors),
305
+ }
306
+
307
+ # Add mitigation recommendations if requested
308
+ if include_mitigation:
309
+ mitigation = self._generate_mitigation_recommendations(
310
+ risk_level, risk_factors
311
+ )
312
+ result["mitigation_required"] = mitigation["required"]
313
+ result["additional_checks"] = mitigation["additional_checks"]
314
+
315
+ # Add location details if available
316
+ if hasattr(risk_context, "location") and risk_context.location:
317
+ result["location_details"] = risk_context.location
318
+
319
+ return result
320
+
321
+ def _record_assessment_attempt(self, context: RiskContext) -> None:
322
+ """Record assessment attempt in history for velocity checks.
323
+
324
+ Args:
325
+ context: Risk context to record
326
+ """
327
+ user_id = context.user_id
328
+
329
+ # Initialize user history if needed
330
+ if user_id not in self.user_history:
331
+ self.user_history[user_id] = []
332
+
333
+ # Record in history
334
+ auth_record = {
335
+ "timestamp": context.timestamp,
336
+ "ip_address": context.ip_address,
337
+ "device_id": context.device_info.get("device_id"),
338
+ "location": context.location,
339
+ }
340
+
341
+ self.user_history[user_id].append(auth_record)
342
+
343
+ # Limit history size
344
+ if len(self.user_history[user_id]) > 100:
345
+ self.user_history[user_id] = self.user_history[user_id][-100:]
346
+
347
+ def _parse_risk_context(self, context: Dict[str, Any]) -> RiskContext:
348
+ """Parse risk context from input.
349
+
350
+ Args:
351
+ context: Raw context dictionary
352
+
353
+ Returns:
354
+ Parsed risk context
355
+ """
356
+ return RiskContext(
357
+ user_id=context["user_id"],
358
+ ip_address=context["ip_address"],
359
+ device_info=context.get("device_info", {}),
360
+ timestamp=context["timestamp"],
361
+ location=context.get("location"),
362
+ user_timezone=context.get("user_timezone"),
363
+ usual_hours=context.get("usual_hours"),
364
+ usual_locations=context.get("usual_locations", []),
365
+ )
366
+
367
+ def _evaluate_risk_factor(
368
+ self, factor: str, context: RiskContext
369
+ ) -> Dict[str, Any]:
370
+ """Evaluate a specific risk factor.
371
+
372
+ Args:
373
+ factor: Risk factor to evaluate
374
+ context: Risk context
375
+
376
+ Returns:
377
+ Factor evaluation result
378
+ """
379
+ if factor == "ip_reputation":
380
+ return self._evaluate_ip_reputation(context)
381
+ elif factor == "device_trust":
382
+ return self._evaluate_device_trust(context)
383
+ elif factor == "location":
384
+ return self._evaluate_location_risk(context)
385
+ elif factor == "behavior":
386
+ return self._evaluate_behavioral_risk(context)
387
+ elif factor == "time_pattern":
388
+ return self._evaluate_time_pattern_risk(context)
389
+ else:
390
+ return {"score": 0.0, "risk_indicators": [], "trust_indicators": []}
391
+
392
+ def _evaluate_ip_reputation(self, context: RiskContext) -> Dict[str, Any]:
393
+ """Evaluate IP address reputation risk.
394
+
395
+ Args:
396
+ context: Risk context
397
+
398
+ Returns:
399
+ IP reputation evaluation
400
+ """
401
+ ip = context.ip_address
402
+ risk_indicators = []
403
+ trust_indicators = []
404
+ score = 0.0
405
+
406
+ # Check if IP is internal/corporate
407
+ if ip.startswith(("10.", "172.", "192.168.")):
408
+ trust_indicators.append("corporate_network")
409
+ score = 0.05 # Very low risk for internal networks
410
+ elif ip.startswith("127."):
411
+ trust_indicators.append("localhost")
412
+ score = 0.0
413
+ else:
414
+ # External IP - moderate risk
415
+ risk_indicators.append("external_ip")
416
+ score = 0.37
417
+
418
+ # Check for known suspicious IP patterns
419
+ if self._is_suspicious_ip(ip):
420
+ risk_indicators.append("suspicious_ip")
421
+ score = 0.8
422
+
423
+ return {
424
+ "score": score,
425
+ "risk_indicators": risk_indicators,
426
+ "trust_indicators": trust_indicators,
427
+ }
428
+
429
+ def _is_suspicious_ip(self, ip: str) -> bool:
430
+ """Check if IP is suspicious (simplified check).
431
+
432
+ Args:
433
+ ip: IP address to check
434
+
435
+ Returns:
436
+ True if IP appears suspicious
437
+ """
438
+ # Simplified suspicious IP detection
439
+ # In production, this would check against threat intelligence feeds
440
+ suspicious_patterns = [
441
+ "185.220.", # Known Tor exits
442
+ "198.96.", # Known proxy services
443
+ "198.98.", # Known proxy services
444
+ ]
445
+
446
+ return any(ip.startswith(pattern) for pattern in suspicious_patterns)
447
+
448
+ def _evaluate_device_trust(self, context: RiskContext) -> Dict[str, Any]:
449
+ """Evaluate device trust level.
450
+
451
+ Args:
452
+ context: Risk context
453
+
454
+ Returns:
455
+ Device trust evaluation
456
+ """
457
+ device_info = context.device_info
458
+ risk_indicators = []
459
+ trust_indicators = []
460
+ score = 0.0
461
+
462
+ # Check if device is recognized
463
+ if device_info.get("recognized", False):
464
+ trust_indicators.append("recognized_device")
465
+ score = 0.02 # Very low score for recognized devices
466
+ else:
467
+ risk_indicators.append("unrecognized_device")
468
+ score = 0.4 # Moderate risk for unrecognized devices
469
+
470
+ # Check device type
471
+ device_type = device_info.get("device_type", "unknown")
472
+ if device_type in ["desktop", "laptop"]:
473
+ trust_indicators.append("managed_device_type")
474
+ # No additional score for good device types
475
+ elif device_type == "mobile":
476
+ score += 0.1 # Slight risk for mobile
477
+ risk_indicators.append("mobile_device")
478
+ elif device_type == "unknown":
479
+ risk_indicators.append("unknown_device_type")
480
+ score += 0.2 # Moderate risk for unknown devices
481
+
482
+ return {
483
+ "score": score,
484
+ "risk_indicators": risk_indicators,
485
+ "trust_indicators": trust_indicators,
486
+ }
487
+
488
+ def _evaluate_location_risk(self, context: RiskContext) -> Dict[str, Any]:
489
+ """Evaluate location-based risk.
490
+
491
+ Args:
492
+ context: Risk context
493
+
494
+ Returns:
495
+ Location risk evaluation
496
+ """
497
+ risk_indicators = []
498
+ trust_indicators = []
499
+ score = 0.0
500
+
501
+ # If we have location information
502
+ if context.location:
503
+ country = context.location.get("country")
504
+ city = context.location.get("city")
505
+
506
+ # Check against usual locations
507
+ if context.usual_locations and country:
508
+ if country in context.usual_locations:
509
+ trust_indicators.append("usual_location")
510
+ score = 0.1
511
+ else:
512
+ risk_indicators.append("unusual_location")
513
+ score = 0.6
514
+
515
+ # Check for velocity (impossible travel)
516
+ if self.velocity_check_enabled:
517
+ velocity_risk = self._check_velocity(context)
518
+ if velocity_risk["impossible_travel"]:
519
+ risk_indicators.append("impossible_travel")
520
+ score = max(score, 0.9)
521
+ else:
522
+ # No location info available - low risk since it's common
523
+ risk_indicators.append("no_location_data")
524
+ score = 0.05
525
+
526
+ return {
527
+ "score": score,
528
+ "risk_indicators": risk_indicators,
529
+ "trust_indicators": trust_indicators,
530
+ }
531
+
532
+ def _check_velocity(self, context: RiskContext) -> Dict[str, Any]:
533
+ """Check for impossible travel velocity.
534
+
535
+ Args:
536
+ context: Risk context
537
+
538
+ Returns:
539
+ Velocity check result
540
+ """
541
+ user_id = context.user_id
542
+ current_time = datetime.fromisoformat(context.timestamp.replace("Z", "+00:00"))
543
+
544
+ # Get recent authentication history
545
+ if user_id in self.user_history:
546
+ recent_auths = [
547
+ auth
548
+ for auth in self.user_history[user_id]
549
+ if (
550
+ current_time
551
+ - datetime.fromisoformat(auth["timestamp"].replace("Z", "+00:00"))
552
+ ).total_seconds()
553
+ < 3600
554
+ ]
555
+
556
+ # Get previous auths (excluding current timestamp to avoid comparing against self)
557
+ previous_auths = [
558
+ auth for auth in recent_auths if auth["timestamp"] != context.timestamp
559
+ ]
560
+
561
+ if previous_auths:
562
+ # Check last auth location vs current
563
+ last_auth = previous_auths[-1]
564
+ if "location" in last_auth and context.location:
565
+ last_location = last_auth["location"]
566
+ current_location = context.location
567
+
568
+ # Check if locations are different
569
+ if last_location.get("city") != current_location.get(
570
+ "city"
571
+ ) or last_location.get("country") != current_location.get(
572
+ "country"
573
+ ):
574
+
575
+ # Check time difference - less than 1 hour for different cities/countries = impossible
576
+ time_diff = (
577
+ current_time
578
+ - datetime.fromisoformat(
579
+ last_auth["timestamp"].replace("Z", "+00:00")
580
+ )
581
+ ).total_seconds()
582
+ if time_diff < 3600: # Less than 1 hour
583
+ return {"impossible_travel": True}
584
+
585
+ return {"impossible_travel": False}
586
+
587
+ def _evaluate_behavioral_risk(self, context: RiskContext) -> Dict[str, Any]:
588
+ """Evaluate behavioral risk patterns.
589
+
590
+ Args:
591
+ context: Risk context
592
+
593
+ Returns:
594
+ Behavioral risk evaluation
595
+ """
596
+ risk_indicators = []
597
+ trust_indicators = []
598
+ score = 0.0
599
+
600
+ # This would integrate with behavioral analysis in production
601
+ # For now, basic heuristics
602
+
603
+ user_id = context.user_id
604
+ if user_id in self.successful_auths:
605
+ recent_successes = len(self.successful_auths[user_id])
606
+ if recent_successes > 10:
607
+ trust_indicators.append("established_pattern")
608
+ score = 0.05 # Very low risk for established users
609
+ elif recent_successes < 3:
610
+ risk_indicators.append("new_user_pattern")
611
+ score = 0.2 # Lower risk for new users
612
+ else:
613
+ risk_indicators.append("no_auth_history")
614
+ score = 0.15 # Lower risk for unknown users (first time is normal)
615
+
616
+ return {
617
+ "score": score,
618
+ "risk_indicators": risk_indicators,
619
+ "trust_indicators": trust_indicators,
620
+ }
621
+
622
+ def _evaluate_time_pattern_risk(self, context: RiskContext) -> Dict[str, Any]:
623
+ """Evaluate time-based access pattern risk.
624
+
625
+ Args:
626
+ context: Risk context
627
+
628
+ Returns:
629
+ Time pattern risk evaluation
630
+ """
631
+ risk_indicators = []
632
+ trust_indicators = []
633
+ score = 0.0
634
+
635
+ # Parse current time
636
+ try:
637
+ # Handle different timestamp formats
638
+ timestamp = context.timestamp
639
+ if timestamp.endswith("Z"):
640
+ timestamp = timestamp.replace("Z", "+00:00")
641
+ elif not timestamp.endswith("+00:00") and "+" not in timestamp[-6:]:
642
+ # Add UTC timezone if none provided
643
+ timestamp = timestamp + "+00:00"
644
+
645
+ current_time = datetime.fromisoformat(timestamp)
646
+ hour = current_time.hour
647
+
648
+ # Check against usual hours
649
+ if context.usual_hours:
650
+ start_hour = context.usual_hours.get("start", 9)
651
+ end_hour = context.usual_hours.get("end", 17)
652
+
653
+ # Debug output
654
+ # print(f"Time check: hour={hour}, start={start_hour}, end={end_hour}, usual_hours={context.usual_hours}")
655
+
656
+ if start_hour <= hour <= end_hour:
657
+ trust_indicators.append("normal_hours")
658
+ score = 0.1
659
+ else:
660
+ risk_indicators.append("unusual_time")
661
+ score = 0.5 # Higher risk for off-hours
662
+ else:
663
+ # No usual hours defined, check for very unusual times only
664
+ if hour < 3 or hour > 23:
665
+ risk_indicators.append("unusual_time")
666
+ score = 0.3
667
+ except Exception as e:
668
+ # Could not parse time
669
+ risk_indicators.append("invalid_timestamp")
670
+ score = 0.2
671
+
672
+ return {
673
+ "score": score,
674
+ "risk_indicators": risk_indicators,
675
+ "trust_indicators": trust_indicators,
676
+ }
677
+
678
+ def _apply_behavioral_adjustments(
679
+ self, context: RiskContext, base_score: float
680
+ ) -> tuple[float, Dict[str, List[str]]]:
681
+ """Apply behavioral adjustments to risk score.
682
+
683
+ Args:
684
+ context: Risk context
685
+ base_score: Base risk score
686
+
687
+ Returns:
688
+ Tuple of (adjusted_score, adjustment_factors)
689
+ """
690
+ adjustment_factors = {"trust": [], "risk": []}
691
+ adjusted_score = base_score
692
+
693
+ user_id = context.user_id
694
+
695
+ # Check for consistent pattern
696
+ if user_id in self.successful_auths:
697
+ successes = self.successful_auths[user_id]
698
+
699
+ # Look for consistent IP/device patterns
700
+ if len(successes) >= 5:
701
+ recent_ips = [auth.get("ip_address") for auth in successes[-5:]]
702
+ recent_devices = [auth.get("device_id") for auth in successes[-5:]]
703
+
704
+ if (
705
+ context.ip_address in recent_ips
706
+ and context.device_info.get("device_id") in recent_devices
707
+ ):
708
+ adjustment_factors["trust"].append("consistent_pattern")
709
+ adjusted_score *= (
710
+ 0.3 # Significantly reduce risk for consistent patterns
711
+ )
712
+
713
+ return adjusted_score, adjustment_factors
714
+
715
+ def _determine_risk_level(self, risk_score: float) -> RiskLevel:
716
+ """Determine risk level from score.
717
+
718
+ Args:
719
+ risk_score: Calculated risk score (0-1)
720
+
721
+ Returns:
722
+ Risk level enum
723
+ """
724
+ if risk_score >= self.threshold_high:
725
+ return RiskLevel.HIGH
726
+ elif risk_score >= self.threshold_medium:
727
+ return RiskLevel.HIGH # This should be HIGH for >= 0.6
728
+ elif risk_score >= self.threshold_low:
729
+ return RiskLevel.MEDIUM # This should be MEDIUM for >= 0.3
730
+ else:
731
+ return RiskLevel.LOW
732
+
733
+ def _calculate_confidence(
734
+ self, context: RiskContext, risk_factors: List[str]
735
+ ) -> float:
736
+ """Calculate confidence in risk assessment.
737
+
738
+ Args:
739
+ context: Risk context
740
+ risk_factors: Identified risk factors
741
+
742
+ Returns:
743
+ Confidence score (0-1)
744
+ """
745
+ # Base confidence
746
+ confidence = 0.7
747
+
748
+ # Increase confidence with more data points
749
+ if context.location:
750
+ confidence += 0.1
751
+ if context.device_info:
752
+ confidence += 0.1
753
+ if context.usual_locations:
754
+ confidence += 0.1
755
+
756
+ # Decrease confidence for ambiguous factors
757
+ if "no_location_data" in risk_factors:
758
+ confidence -= 0.2
759
+ if "unknown_device_type" in risk_factors:
760
+ confidence -= 0.1
761
+
762
+ return min(1.0, max(0.3, confidence))
763
+
764
+ def _generate_mitigation_recommendations(
765
+ self, risk_level: RiskLevel, risk_factors: List[str]
766
+ ) -> Dict[str, List[str]]:
767
+ """Generate mitigation recommendations based on risk.
768
+
769
+ Args:
770
+ risk_level: Assessed risk level
771
+ risk_factors: Identified risk factors
772
+
773
+ Returns:
774
+ Mitigation recommendations
775
+ """
776
+ required = []
777
+ additional_checks = []
778
+
779
+ if risk_level == RiskLevel.HIGH:
780
+ required.extend(["mfa"])
781
+ additional_checks.extend(
782
+ ["email_verification", "security_questions", "device_verification"]
783
+ )
784
+ elif risk_level == RiskLevel.MEDIUM:
785
+ required.append("mfa")
786
+ additional_checks.append("email_verification")
787
+
788
+ # Factor-specific recommendations
789
+ if "unrecognized_device" in risk_factors:
790
+ additional_checks.append("device_registration")
791
+ if "unusual_location" in risk_factors:
792
+ additional_checks.append("location_verification")
793
+ if "suspicious_ip" in risk_factors:
794
+ required.append("admin_approval")
795
+
796
+ return {
797
+ "required": list(set(required)),
798
+ "additional_checks": list(set(additional_checks)),
799
+ }
800
+
801
+ def _record_successful_auth(self, context: Dict[str, Any]) -> Dict[str, Any]:
802
+ """Record successful authentication for behavioral learning.
803
+
804
+ Args:
805
+ context: Authentication context
806
+
807
+ Returns:
808
+ Recording result
809
+ """
810
+ user_id = context["user_id"]
811
+
812
+ # Initialize user history if needed
813
+ if user_id not in self.user_history:
814
+ self.user_history[user_id] = []
815
+ if user_id not in self.successful_auths:
816
+ self.successful_auths[user_id] = []
817
+
818
+ # Record in history
819
+ auth_record = {
820
+ "timestamp": context["timestamp"],
821
+ "ip_address": context["ip_address"],
822
+ "device_id": context.get("device_info", {}).get("device_id"),
823
+ "location": context.get("location"),
824
+ }
825
+
826
+ self.user_history[user_id].append(auth_record)
827
+ self.successful_auths[user_id].append(auth_record)
828
+
829
+ # Limit history size
830
+ if len(self.user_history[user_id]) > 100:
831
+ self.user_history[user_id] = self.user_history[user_id][-100:]
832
+ if len(self.successful_auths[user_id]) > 50:
833
+ self.successful_auths[user_id] = self.successful_auths[user_id][-50:]
834
+
835
+ return {"success": True, "recorded": True, "user_id": user_id}
836
+
837
+ def _update_stats(self, processing_time_ms: float, risk_level: str) -> None:
838
+ """Update assessment statistics.
839
+
840
+ Args:
841
+ processing_time_ms: Processing time in milliseconds
842
+ risk_level: Assessed risk level
843
+ """
844
+ self.assessment_stats["total_assessments"] += 1
845
+
846
+ if risk_level == "high":
847
+ self.assessment_stats["high_risk_count"] += 1
848
+ elif risk_level == "low":
849
+ self.assessment_stats["low_risk_count"] += 1
850
+
851
+ # Update average processing time
852
+ if self.assessment_stats["avg_assessment_time_ms"] == 0:
853
+ self.assessment_stats["avg_assessment_time_ms"] = processing_time_ms
854
+ else:
855
+ self.assessment_stats["avg_assessment_time_ms"] = (
856
+ self.assessment_stats["avg_assessment_time_ms"] * 0.9
857
+ + processing_time_ms * 0.1
858
+ )
859
+
860
+ def get_assessment_stats(self) -> Dict[str, Any]:
861
+ """Get risk assessment statistics.
862
+
863
+ Returns:
864
+ Dictionary with assessment statistics
865
+ """
866
+ return {
867
+ **self.assessment_stats,
868
+ "risk_factors_enabled": self.risk_factors,
869
+ "ml_enabled": self.ml_enabled,
870
+ "velocity_check_enabled": self.velocity_check_enabled,
871
+ "behavioral_analysis": self.behavioral_analysis,
872
+ }