Spaces:
Running
Running
Commit
·
5dc261b
1
Parent(s):
2b96220
ADD : Docer
Browse files- adaptiveauth/__init__.py +5 -2
- adaptiveauth/auth/service.py +2 -1
- adaptiveauth/models.py +36 -0
- adaptiveauth/risk/__init__.py +14 -0
- adaptiveauth/risk/session_intelligence.py +671 -0
- adaptiveauth/routers/__init__.py +4 -0
- adaptiveauth/routers/adaptive.py +2 -1
- adaptiveauth/routers/admin.py +29 -0
- adaptiveauth/routers/demo.py +763 -0
- adaptiveauth/routers/session_intel.py +461 -0
- openapi_temp.json +0 -1
- openapi_test.json +0 -1
- static/index.html +0 -0
adaptiveauth/__init__.py
CHANGED
|
@@ -56,7 +56,7 @@ from .risk import (
|
|
| 56 |
from .auth import AuthService, OTPService, EmailService
|
| 57 |
from .routers import (
|
| 58 |
auth_router, user_router, admin_router,
|
| 59 |
-
risk_router, adaptive_router
|
| 60 |
)
|
| 61 |
|
| 62 |
|
|
@@ -140,7 +140,9 @@ class AdaptiveAuth:
|
|
| 140 |
self._router.include_router(auth_router)
|
| 141 |
self._router.include_router(user_router)
|
| 142 |
self._router.include_router(admin_router)
|
| 143 |
-
|
|
|
|
|
|
|
| 144 |
if self.enable_risk_assessment:
|
| 145 |
self._router.include_router(risk_router)
|
| 146 |
self._router.include_router(adaptive_router)
|
|
@@ -353,4 +355,5 @@ __all__ = [
|
|
| 353 |
"admin_router",
|
| 354 |
"risk_router",
|
| 355 |
"adaptive_router",
|
|
|
|
| 356 |
]
|
|
|
|
| 56 |
from .auth import AuthService, OTPService, EmailService
|
| 57 |
from .routers import (
|
| 58 |
auth_router, user_router, admin_router,
|
| 59 |
+
risk_router, adaptive_router, demo_router, session_intel_router
|
| 60 |
)
|
| 61 |
|
| 62 |
|
|
|
|
| 140 |
self._router.include_router(auth_router)
|
| 141 |
self._router.include_router(user_router)
|
| 142 |
self._router.include_router(admin_router)
|
| 143 |
+
self._router.include_router(demo_router)
|
| 144 |
+
self._router.include_router(session_intel_router)
|
| 145 |
+
|
| 146 |
if self.enable_risk_assessment:
|
| 147 |
self._router.include_router(risk_router)
|
| 148 |
self._router.include_router(adaptive_router)
|
|
|
|
| 355 |
"admin_router",
|
| 356 |
"risk_router",
|
| 357 |
"adaptive_router",
|
| 358 |
+
"demo_router",
|
| 359 |
]
|
adaptiveauth/auth/service.py
CHANGED
|
@@ -250,7 +250,8 @@ class AuthService:
|
|
| 250 |
if challenge.challenge_type == 'otp':
|
| 251 |
verified = self.otp_service.verify_otp(user.tfa_secret, code)
|
| 252 |
elif challenge.challenge_type == 'email':
|
| 253 |
-
|
|
|
|
| 254 |
|
| 255 |
challenge.attempts += 1
|
| 256 |
|
|
|
|
| 250 |
if challenge.challenge_type == 'otp':
|
| 251 |
verified = self.otp_service.verify_otp(user.tfa_secret, code)
|
| 252 |
elif challenge.challenge_type == 'email':
|
| 253 |
+
from ..core.security import constant_time_compare
|
| 254 |
+
verified = constant_time_compare(challenge.challenge_code or '', code)
|
| 255 |
|
| 256 |
challenge.attempts += 1
|
| 257 |
|
adaptiveauth/models.py
CHANGED
|
@@ -311,6 +311,42 @@ class AnomalyPattern(Base):
|
|
| 311 |
resolved_at = Column(DateTime, nullable=True)
|
| 312 |
|
| 313 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
class StepUpChallenge(Base):
|
| 315 |
"""Step-up authentication challenges."""
|
| 316 |
__tablename__ = "adaptiveauth_stepup_challenges"
|
|
|
|
| 311 |
resolved_at = Column(DateTime, nullable=True)
|
| 312 |
|
| 313 |
|
| 314 |
+
class SessionTrustEvent(Base):
|
| 315 |
+
"""Trust score change events throughout a session's lifecycle. (Feature 1 & 3)"""
|
| 316 |
+
__tablename__ = "adaptiveauth_trust_events"
|
| 317 |
+
|
| 318 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 319 |
+
session_id = Column(Integer, ForeignKey("adaptiveauth_sessions.id", ondelete="CASCADE"), index=True)
|
| 320 |
+
user_id = Column(Integer, ForeignKey("adaptiveauth_users.id", ondelete="CASCADE"), index=True)
|
| 321 |
+
|
| 322 |
+
trust_score = Column(Float, nullable=False) # score AFTER this event
|
| 323 |
+
delta = Column(Float, default=0.0) # how much it changed
|
| 324 |
+
event_type = Column(String(100), nullable=False) # decay | behavior | context_change | micro_challenge | impossible_travel
|
| 325 |
+
reason = Column(String(255), nullable=True)
|
| 326 |
+
signals = Column(JSON, default=dict) # contributing signal values
|
| 327 |
+
|
| 328 |
+
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class BehaviorSignalRecord(Base):
|
| 332 |
+
"""Privacy-first behavior signal records. Only aggregated scores are stored. (Feature 2 & 8)"""
|
| 333 |
+
__tablename__ = "adaptiveauth_behavior_signals"
|
| 334 |
+
|
| 335 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 336 |
+
session_id = Column(Integer, ForeignKey("adaptiveauth_sessions.id", ondelete="CASCADE"), index=True)
|
| 337 |
+
user_id = Column(Integer, ForeignKey("adaptiveauth_users.id", ondelete="CASCADE"), index=True)
|
| 338 |
+
|
| 339 |
+
# Client-computed signals (0.0–1.0 each). Raw keystrokes/mouse coords are NEVER sent.
|
| 340 |
+
typing_entropy = Column(Float, nullable=True) # 1.0 = very human-like keystroke rhythm
|
| 341 |
+
mouse_linearity = Column(Float, nullable=True) # higher = more curved/natural paths
|
| 342 |
+
scroll_variance = Column(Float, nullable=True) # moderate variance = normal human
|
| 343 |
+
local_risk_score = Column(Float, nullable=True) # client-side composite (privacy-first)
|
| 344 |
+
|
| 345 |
+
anomaly_score = Column(Float, default=0.0) # server-computed 0–100
|
| 346 |
+
|
| 347 |
+
recorded_at = Column(DateTime, default=datetime.utcnow, index=True)
|
| 348 |
+
|
| 349 |
+
|
| 350 |
class StepUpChallenge(Base):
|
| 351 |
"""Step-up authentication challenges."""
|
| 352 |
__tablename__ = "adaptiveauth_stepup_challenges"
|
adaptiveauth/risk/__init__.py
CHANGED
|
@@ -12,6 +12,14 @@ from .factors import (
|
|
| 12 |
)
|
| 13 |
from .analyzer import BehaviorAnalyzer
|
| 14 |
from .monitor import SessionMonitor, AnomalyDetector
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
__all__ = [
|
| 17 |
"RiskEngine",
|
|
@@ -25,4 +33,10 @@ __all__ = [
|
|
| 25 |
"BehaviorAnalyzer",
|
| 26 |
"SessionMonitor",
|
| 27 |
"AnomalyDetector",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
]
|
|
|
|
| 12 |
)
|
| 13 |
from .analyzer import BehaviorAnalyzer
|
| 14 |
from .monitor import SessionMonitor, AnomalyDetector
|
| 15 |
+
from .session_intelligence import (
|
| 16 |
+
TrustScoreManager,
|
| 17 |
+
BehaviorSignalProcessor,
|
| 18 |
+
ImpossibleTravelDetector,
|
| 19 |
+
MicroChallengeEngine,
|
| 20 |
+
RiskExplainer,
|
| 21 |
+
StatisticalAnomalyDetector,
|
| 22 |
+
)
|
| 23 |
|
| 24 |
__all__ = [
|
| 25 |
"RiskEngine",
|
|
|
|
| 33 |
"BehaviorAnalyzer",
|
| 34 |
"SessionMonitor",
|
| 35 |
"AnomalyDetector",
|
| 36 |
+
"TrustScoreManager",
|
| 37 |
+
"BehaviorSignalProcessor",
|
| 38 |
+
"ImpossibleTravelDetector",
|
| 39 |
+
"MicroChallengeEngine",
|
| 40 |
+
"RiskExplainer",
|
| 41 |
+
"StatisticalAnomalyDetector",
|
| 42 |
]
|
adaptiveauth/risk/session_intelligence.py
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Session Intelligence Engine
|
| 3 |
+
===========================
|
| 4 |
+
Implements 8 advanced security capabilities beyond standard login-time auth:
|
| 5 |
+
|
| 6 |
+
1. Continuous Verification – verify context throughout session lifecycle
|
| 7 |
+
2. Behavioral Intelligence – typing rhythm, mouse paths, scroll patterns
|
| 8 |
+
3. Dynamic Trust Score – evolving 0-100 score; decays + recovers
|
| 9 |
+
4. Low-Friction Micro-Challenges – triggered only when trust drops
|
| 10 |
+
5. Explainable Risk – factor contributions, confidence, audit trail
|
| 11 |
+
6. AI-Powered Anomaly Scoring – isolation-forest-inspired statistical model
|
| 12 |
+
7. Impossible Travel – haversine geo-velocity detection
|
| 13 |
+
8. Privacy-First Design – client computes signals; server receives only score
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import math
|
| 17 |
+
import hashlib
|
| 18 |
+
import random
|
| 19 |
+
from datetime import datetime, timedelta
|
| 20 |
+
from typing import Optional, List, Dict, Any, Tuple
|
| 21 |
+
from sqlalchemy.orm import Session
|
| 22 |
+
|
| 23 |
+
from ..models import (
|
| 24 |
+
User, UserSession, LoginAttempt, AnomalyPattern, RiskLevel,
|
| 25 |
+
SessionTrustEvent, BehaviorSignalRecord,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# ── City Geo-Coordinates ────────────────────────────────────────────────────
|
| 29 |
+
CITY_COORDS: Dict[str, Tuple[float, float]] = {
|
| 30 |
+
"new york": (40.7128, -74.0060),
|
| 31 |
+
"moscow": (55.7558, 37.6173),
|
| 32 |
+
"beijing": (39.9042, 116.4074),
|
| 33 |
+
"london": (51.5074, -0.1278),
|
| 34 |
+
"tokyo": (35.6762, 139.6503),
|
| 35 |
+
"sydney": (-33.8688, 151.2093),
|
| 36 |
+
"paris": (48.8566, 2.3522),
|
| 37 |
+
"dubai": (25.2048, 55.2708),
|
| 38 |
+
"singapore": ( 1.3521, 103.8198),
|
| 39 |
+
"toronto": (43.6532, -79.3832),
|
| 40 |
+
"chicago": (41.8781, -87.6298),
|
| 41 |
+
"miami": (25.7617, -80.1918),
|
| 42 |
+
"berlin": (52.5200, 13.4050),
|
| 43 |
+
"mumbai": (19.0760, 72.8777),
|
| 44 |
+
"bangkok": (13.7563, 100.5018),
|
| 45 |
+
"cairo": (30.0444, 31.2357),
|
| 46 |
+
"johannesburg": (-26.2041, 28.0473),
|
| 47 |
+
"sao paulo": (-23.5505, -46.6333),
|
| 48 |
+
"buenos aires": (-34.6037, -58.3816),
|
| 49 |
+
"seattle": (47.6062, -122.3321),
|
| 50 |
+
"los angeles": (34.0522, -118.2437),
|
| 51 |
+
"mexico city": (19.4326, -99.1332),
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
# ── Behavior Baseline (mean, std) for each client-collected metric ──────────
|
| 55 |
+
BEHAVIOR_BASELINE: Dict[str, Tuple[float, float]] = {
|
| 56 |
+
"typing_entropy": (0.70, 0.15), # 1.0 = perfectly human-like rhythm
|
| 57 |
+
"mouse_linearity": (0.62, 0.18), # 1.0 = natural curved paths
|
| 58 |
+
"scroll_variance": (0.48, 0.22), # moderate = organic human scrolling
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
# ── Trust score baselines by security level ─────────────────────────────────
|
| 62 |
+
TRUST_BASELINE: Dict[int, float] = {0: 95.0, 1: 80.0, 2: 60.0, 3: 35.0, 4: 10.0}
|
| 63 |
+
|
| 64 |
+
# ── In-memory caches ─────────────────────────────────────────────────────────
|
| 65 |
+
# {session_id: {"score": float, "last_update": datetime}}
|
| 66 |
+
_trust_cache: Dict[int, Dict[str, Any]] = {}
|
| 67 |
+
|
| 68 |
+
# {challenge_id: {"answer_hash": str, "expires": datetime, "attempts": int}}
|
| 69 |
+
_micro_challenges: Dict[str, Dict[str, Any]] = {}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 73 |
+
# Helper math
|
| 74 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 75 |
+
|
| 76 |
+
def haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
|
| 77 |
+
"""Great-circle distance in km between two geographic points."""
|
| 78 |
+
R = 6371.0
|
| 79 |
+
d_lat = math.radians(lat2 - lat1)
|
| 80 |
+
d_lon = math.radians(lon2 - lon1)
|
| 81 |
+
a = (math.sin(d_lat / 2) ** 2
|
| 82 |
+
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
|
| 83 |
+
* math.sin(d_lon / 2) ** 2)
|
| 84 |
+
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _z_anomaly(value: float, mean: float, std: float) -> float:
|
| 88 |
+
"""Convert value to 0–100 anomaly score using a Z-score transform."""
|
| 89 |
+
if std < 1e-6:
|
| 90 |
+
return 0.0 if abs(value - mean) < 0.01 else 50.0
|
| 91 |
+
z = abs(value - mean) / std
|
| 92 |
+
return min(100.0, (z / 3.5) ** 0.75 * 100.0)
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _resolve_coords(
|
| 96 |
+
city: Optional[str],
|
| 97 |
+
lat: Optional[float],
|
| 98 |
+
lon: Optional[float],
|
| 99 |
+
) -> Tuple[Optional[float], Optional[float]]:
|
| 100 |
+
if lat is not None and lon is not None:
|
| 101 |
+
return lat, lon
|
| 102 |
+
if city:
|
| 103 |
+
return CITY_COORDS.get(city.lower().strip(), (None, None))
|
| 104 |
+
return None, None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def _classify_anomaly(score: float) -> str:
|
| 108 |
+
if score >= 80: return "critical"
|
| 109 |
+
if score >= 60: return "high"
|
| 110 |
+
if score >= 40: return "medium"
|
| 111 |
+
if score >= 20: return "low"
|
| 112 |
+
return "normal"
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def _build_summary(anomalous_factors: List[str], security_level: int) -> str:
|
| 116 |
+
if not anomalous_factors:
|
| 117 |
+
return "All signals normal. Trusted session context."
|
| 118 |
+
if len(anomalous_factors) == 1:
|
| 119 |
+
return f"{anomalous_factors[0]} flagged. Minimal elevated risk."
|
| 120 |
+
return (
|
| 121 |
+
f"{len(anomalous_factors)} risk signals triggered: "
|
| 122 |
+
+ ", ".join(anomalous_factors[:3])
|
| 123 |
+
+ (f" and {len(anomalous_factors) - 3} more." if len(anomalous_factors) > 3 else ".")
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 128 |
+
# Feature 1 & 3 – Continuous Verification + Dynamic Trust Score
|
| 129 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 130 |
+
|
| 131 |
+
class TrustScoreManager:
|
| 132 |
+
"""
|
| 133 |
+
Maintains an evolving Trust Score (0–100) throughout the session lifecycle.
|
| 134 |
+
|
| 135 |
+
Decay rules:
|
| 136 |
+
• Time-based: –0.25 pts/min of inactivity
|
| 137 |
+
• Behavior anomaly: –4 to –24 depending on severity
|
| 138 |
+
• Context change (IP/device): –20
|
| 139 |
+
• Impossible travel: –50
|
| 140 |
+
|
| 141 |
+
Recovery rules:
|
| 142 |
+
• Good behavior signal: +2 to +6
|
| 143 |
+
• Micro-challenge pass: +20
|
| 144 |
+
"""
|
| 145 |
+
|
| 146 |
+
DECAY_RATE = 0.25 # pts / minute inactive
|
| 147 |
+
|
| 148 |
+
def __init__(self, db: Session):
|
| 149 |
+
self.db = db
|
| 150 |
+
|
| 151 |
+
def get(self, session: UserSession) -> float:
|
| 152 |
+
"""Return current trust score, hydrating from DB if not cached."""
|
| 153 |
+
sid = session.id
|
| 154 |
+
if sid in _trust_cache:
|
| 155 |
+
return _trust_cache[sid]["score"]
|
| 156 |
+
|
| 157 |
+
last = (
|
| 158 |
+
self.db.query(SessionTrustEvent)
|
| 159 |
+
.filter(SessionTrustEvent.session_id == sid)
|
| 160 |
+
.order_by(SessionTrustEvent.created_at.desc())
|
| 161 |
+
.first()
|
| 162 |
+
)
|
| 163 |
+
score = float(last.trust_score) if last else TRUST_BASELINE.get(
|
| 164 |
+
{"low": 0, "medium": 1, "high": 2, "critical": 3}.get(
|
| 165 |
+
session.current_risk_level or "low", 1
|
| 166 |
+
), 80.0
|
| 167 |
+
)
|
| 168 |
+
_trust_cache[sid] = {"score": score, "last_update": datetime.utcnow()}
|
| 169 |
+
return score
|
| 170 |
+
|
| 171 |
+
def apply_decay(self, session: UserSession) -> Tuple[float, float]:
|
| 172 |
+
"""Apply time-based trust decay. Returns (new_score, delta)."""
|
| 173 |
+
sid = session.id
|
| 174 |
+
cache = _trust_cache.get(sid)
|
| 175 |
+
if not cache:
|
| 176 |
+
return self.get(session), 0.0
|
| 177 |
+
|
| 178 |
+
elapsed_minutes = (datetime.utcnow() - cache["last_update"]).total_seconds() / 60
|
| 179 |
+
delta = -self.DECAY_RATE * elapsed_minutes
|
| 180 |
+
new_score = max(0.0, cache["score"] + delta)
|
| 181 |
+
_trust_cache[sid] = {"score": new_score, "last_update": datetime.utcnow()}
|
| 182 |
+
return new_score, delta
|
| 183 |
+
|
| 184 |
+
def update(
|
| 185 |
+
self,
|
| 186 |
+
session: UserSession,
|
| 187 |
+
delta: float,
|
| 188 |
+
event_type: str,
|
| 189 |
+
reason: str,
|
| 190 |
+
signals: Optional[Dict] = None,
|
| 191 |
+
) -> float:
|
| 192 |
+
"""Apply delta, clamp to [0, 100], persist to DB, update cache."""
|
| 193 |
+
current = self.get(session)
|
| 194 |
+
new_score = max(0.0, min(100.0, current + delta))
|
| 195 |
+
_trust_cache[session.id] = {"score": new_score, "last_update": datetime.utcnow()}
|
| 196 |
+
|
| 197 |
+
self.db.add(SessionTrustEvent(
|
| 198 |
+
session_id=session.id,
|
| 199 |
+
user_id=session.user_id,
|
| 200 |
+
trust_score=new_score,
|
| 201 |
+
delta=delta,
|
| 202 |
+
event_type=event_type,
|
| 203 |
+
reason=reason,
|
| 204 |
+
signals=signals or {},
|
| 205 |
+
))
|
| 206 |
+
self.db.commit()
|
| 207 |
+
return new_score
|
| 208 |
+
|
| 209 |
+
def get_history(self, session_id: int, limit: int = 40) -> List[Dict]:
|
| 210 |
+
events = (
|
| 211 |
+
self.db.query(SessionTrustEvent)
|
| 212 |
+
.filter(SessionTrustEvent.session_id == session_id)
|
| 213 |
+
.order_by(SessionTrustEvent.created_at.desc())
|
| 214 |
+
.limit(limit)
|
| 215 |
+
.all()
|
| 216 |
+
)
|
| 217 |
+
return [
|
| 218 |
+
{
|
| 219 |
+
"score": e.trust_score,
|
| 220 |
+
"delta": e.delta,
|
| 221 |
+
"event_type": e.event_type,
|
| 222 |
+
"reason": e.reason,
|
| 223 |
+
"at": e.created_at.isoformat(),
|
| 224 |
+
}
|
| 225 |
+
for e in reversed(events)
|
| 226 |
+
]
|
| 227 |
+
|
| 228 |
+
@staticmethod
|
| 229 |
+
def label(score: float) -> str:
|
| 230 |
+
if score >= 80: return "trusted"
|
| 231 |
+
if score >= 60: return "watchful"
|
| 232 |
+
if score >= 40: return "elevated"
|
| 233 |
+
if score >= 20: return "high_risk"
|
| 234 |
+
return "critical"
|
| 235 |
+
|
| 236 |
+
@staticmethod
|
| 237 |
+
def color(score: float) -> str:
|
| 238 |
+
if score >= 80: return "#22c55e"
|
| 239 |
+
if score >= 60: return "#84cc16"
|
| 240 |
+
if score >= 40: return "#f59e0b"
|
| 241 |
+
if score >= 20: return "#f97316"
|
| 242 |
+
return "#ef4444"
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
# ═══════════════════════════════════════════════════��═══════════════════════
|
| 246 |
+
# Feature 2 & 8 – Behavioral Intelligence + Privacy-First Design
|
| 247 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 248 |
+
|
| 249 |
+
class BehaviorSignalProcessor:
|
| 250 |
+
"""
|
| 251 |
+
Processes privacy-first behavior signals submitted by the client.
|
| 252 |
+
|
| 253 |
+
The client JS collects raw events (keydown timings, mouse paths,
|
| 254 |
+
scroll deltas) and computes aggregated 0–1 scores LOCALLY.
|
| 255 |
+
Only those aggregated scores are transmitted — no raw events ever leave
|
| 256 |
+
the browser (Feature 8: Privacy-First Design).
|
| 257 |
+
|
| 258 |
+
Server validates plausibility, computes anomaly_score,
|
| 259 |
+
and derives a trust delta.
|
| 260 |
+
"""
|
| 261 |
+
|
| 262 |
+
def process(
|
| 263 |
+
self,
|
| 264 |
+
session: UserSession,
|
| 265 |
+
typing_entropy: float,
|
| 266 |
+
mouse_linearity: float,
|
| 267 |
+
scroll_variance: float,
|
| 268 |
+
local_risk_score: float,
|
| 269 |
+
db: Session,
|
| 270 |
+
) -> Dict[str, Any]:
|
| 271 |
+
# Clamp all inputs to [0, 1]
|
| 272 |
+
te = max(0.0, min(1.0, float(typing_entropy)))
|
| 273 |
+
ml = max(0.0, min(1.0, float(mouse_linearity)))
|
| 274 |
+
sv = max(0.0, min(1.0, float(scroll_variance)))
|
| 275 |
+
lr = max(0.0, min(1.0, float(local_risk_score)))
|
| 276 |
+
|
| 277 |
+
# Per-feature anomaly (0–100, higher = more anomalous)
|
| 278 |
+
te_a = _z_anomaly(te, *BEHAVIOR_BASELINE["typing_entropy"]) if te > 0 else 50.0
|
| 279 |
+
ml_a = _z_anomaly(ml, *BEHAVIOR_BASELINE["mouse_linearity"]) if ml > 0 else 50.0
|
| 280 |
+
sv_a = _z_anomaly(sv, *BEHAVIOR_BASELINE["scroll_variance"]) if sv > 0 else 50.0
|
| 281 |
+
|
| 282 |
+
# Weighted composite server score
|
| 283 |
+
server_score = 0.40 * te_a + 0.35 * ml_a + 0.25 * sv_a
|
| 284 |
+
# Blend with client-reported composite (60/40 split)
|
| 285 |
+
final = 0.60 * server_score + 0.40 * (lr * 100.0)
|
| 286 |
+
|
| 287 |
+
# Trust delta
|
| 288 |
+
if final < 20: td = +6.0
|
| 289 |
+
elif final < 40: td = +2.0
|
| 290 |
+
elif final < 60: td = -4.0
|
| 291 |
+
elif final < 80: td = -12.0
|
| 292 |
+
else: td = -24.0
|
| 293 |
+
|
| 294 |
+
db.add(BehaviorSignalRecord(
|
| 295 |
+
session_id=session.id,
|
| 296 |
+
user_id=session.user_id,
|
| 297 |
+
typing_entropy=te,
|
| 298 |
+
mouse_linearity=ml,
|
| 299 |
+
scroll_variance=sv,
|
| 300 |
+
local_risk_score=lr,
|
| 301 |
+
anomaly_score=final,
|
| 302 |
+
))
|
| 303 |
+
db.commit()
|
| 304 |
+
|
| 305 |
+
return {
|
| 306 |
+
"anomaly_score": round(final, 2),
|
| 307 |
+
"trust_delta": td,
|
| 308 |
+
"signals": {
|
| 309 |
+
"typing_entropy": round(te, 3),
|
| 310 |
+
"mouse_linearity": round(ml, 3),
|
| 311 |
+
"scroll_variance": round(sv, 3),
|
| 312 |
+
"local_risk_score": round(lr, 3),
|
| 313 |
+
},
|
| 314 |
+
"per_feature_anomaly": {
|
| 315 |
+
"typing": round(te_a, 1),
|
| 316 |
+
"mouse": round(ml_a, 1),
|
| 317 |
+
"scroll": round(sv_a, 1),
|
| 318 |
+
},
|
| 319 |
+
"classification": _classify_anomaly(final),
|
| 320 |
+
"privacy_note": (
|
| 321 |
+
"Raw keystrokes, mouse coordinates, and scroll positions "
|
| 322 |
+
"were processed entirely in-browser. Only aggregated scores "
|
| 323 |
+
"were transmitted to the server."
|
| 324 |
+
),
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 329 |
+
# Feature 7 – Impossible Travel + Pattern Clustering
|
| 330 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 331 |
+
|
| 332 |
+
class ImpossibleTravelDetector:
|
| 333 |
+
"""
|
| 334 |
+
Detects impossible travel using haversine distance + elapsed time.
|
| 335 |
+
|
| 336 |
+
Thresholds:
|
| 337 |
+
> 900 km/h → IMPOSSIBLE (fastest commercial jet ~900 km/h)
|
| 338 |
+
> 400 km/h → SUSPICIOUS (implausible without air travel)
|
| 339 |
+
< 400 km/h → PLAUSIBLE
|
| 340 |
+
"""
|
| 341 |
+
|
| 342 |
+
IMPOSSIBLE_KMH = 900.0
|
| 343 |
+
SUSPICIOUS_KMH = 400.0
|
| 344 |
+
|
| 345 |
+
def __init__(self, db: Session):
|
| 346 |
+
self.db = db
|
| 347 |
+
|
| 348 |
+
def check(
|
| 349 |
+
self,
|
| 350 |
+
user_id: int,
|
| 351 |
+
city_now: str,
|
| 352 |
+
country_now: str,
|
| 353 |
+
lat_now: Optional[float] = None,
|
| 354 |
+
lon_now: Optional[float] = None,
|
| 355 |
+
time_gap_hours: Optional[float] = None, # override for demo mode
|
| 356 |
+
) -> Dict[str, Any]:
|
| 357 |
+
"""
|
| 358 |
+
Compare the current login location against the most recent successful login.
|
| 359 |
+
Pass time_gap_hours to simulate an arbitrary gap for demos.
|
| 360 |
+
"""
|
| 361 |
+
last = (
|
| 362 |
+
self.db.query(LoginAttempt)
|
| 363 |
+
.filter(
|
| 364 |
+
LoginAttempt.user_id == user_id,
|
| 365 |
+
LoginAttempt.success == True,
|
| 366 |
+
LoginAttempt.city.isnot(None),
|
| 367 |
+
)
|
| 368 |
+
.order_by(LoginAttempt.attempted_at.desc())
|
| 369 |
+
.first()
|
| 370 |
+
)
|
| 371 |
+
|
| 372 |
+
if not last:
|
| 373 |
+
return self._no_history(city_now, country_now, lat_now, lon_now)
|
| 374 |
+
|
| 375 |
+
lat1, lon1 = _resolve_coords(last.city, last.latitude, last.longitude)
|
| 376 |
+
lat2, lon2 = _resolve_coords(city_now, lat_now, lon_now)
|
| 377 |
+
|
| 378 |
+
if lat1 is None or lat2 is None:
|
| 379 |
+
return {
|
| 380 |
+
"possible": True,
|
| 381 |
+
"verdict": "coords_unknown",
|
| 382 |
+
"message": f"Cannot resolve coordinates for '{last.city}' or '{city_now}'.",
|
| 383 |
+
"distance_km": 0.0, "speed_kmh": 0.0, "time_gap_minutes": 0.0,
|
| 384 |
+
"trust_delta": 0.0,
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
distance_km = haversine(lat1, lon1, lat2, lon2)
|
| 388 |
+
|
| 389 |
+
if time_gap_hours is not None:
|
| 390 |
+
time_gap_s = time_gap_hours * 3600
|
| 391 |
+
else:
|
| 392 |
+
time_gap_s = (datetime.utcnow() - last.attempted_at).total_seconds()
|
| 393 |
+
|
| 394 |
+
time_gap_min = max(time_gap_s / 60, 0.001)
|
| 395 |
+
speed_kmh = (distance_km / (time_gap_s / 3600)) if time_gap_s > 0 else 0.0
|
| 396 |
+
|
| 397 |
+
if distance_km < 50:
|
| 398 |
+
verdict, possible, trust_delta = "same_area", True, 0.0
|
| 399 |
+
msg = f"Same area as last login ({last.city}). No anomaly."
|
| 400 |
+
elif speed_kmh > self.IMPOSSIBLE_KMH:
|
| 401 |
+
verdict, possible, trust_delta = "impossible", False, -50.0
|
| 402 |
+
msg = (
|
| 403 |
+
f"IMPOSSIBLE TRAVEL: {distance_km:.0f} km in {time_gap_min:.0f} min "
|
| 404 |
+
f"= {speed_kmh:.0f} km/h (fastest jet ~900 km/h)."
|
| 405 |
+
)
|
| 406 |
+
elif speed_kmh > self.SUSPICIOUS_KMH:
|
| 407 |
+
verdict, possible, trust_delta = "suspicious", True, -20.0
|
| 408 |
+
msg = (
|
| 409 |
+
f"Suspicious speed: {speed_kmh:.0f} km/h over "
|
| 410 |
+
f"{distance_km:.0f} km from {last.city} in {time_gap_min:.0f} min."
|
| 411 |
+
)
|
| 412 |
+
else:
|
| 413 |
+
verdict, possible, trust_delta = "plausible", True, 0.0
|
| 414 |
+
msg = (
|
| 415 |
+
f"Plausible: {distance_km:.0f} km from {last.city} "
|
| 416 |
+
f"at {speed_kmh:.0f} km/h in {time_gap_min:.0f} min."
|
| 417 |
+
)
|
| 418 |
+
|
| 419 |
+
return {
|
| 420 |
+
"possible": possible,
|
| 421 |
+
"verdict": verdict,
|
| 422 |
+
"message": msg,
|
| 423 |
+
"distance_km": round(distance_km, 1),
|
| 424 |
+
"speed_kmh": round(speed_kmh, 1),
|
| 425 |
+
"time_gap_minutes": round(time_gap_min, 1),
|
| 426 |
+
"from": {"city": last.city, "lat": lat1, "lon": lon1},
|
| 427 |
+
"to": {"city": city_now, "country": country_now, "lat": lat2, "lon": lon2},
|
| 428 |
+
"trust_delta": trust_delta,
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
def _no_history(self, city, country, lat, lon) -> Dict[str, Any]:
|
| 432 |
+
return {
|
| 433 |
+
"possible": True, "verdict": "no_history",
|
| 434 |
+
"message": "No previous login on record — travel check skipped.",
|
| 435 |
+
"distance_km": 0.0, "speed_kmh": 0.0, "time_gap_minutes": 0.0,
|
| 436 |
+
"from": None,
|
| 437 |
+
"to": {"city": city, "country": country, "lat": lat, "lon": lon},
|
| 438 |
+
"trust_delta": 0.0,
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
|
| 442 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 443 |
+
# Feature 4 – Low-Friction Micro-Challenges
|
| 444 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 445 |
+
|
| 446 |
+
class MicroChallengeEngine:
|
| 447 |
+
"""
|
| 448 |
+
Issues lightweight inline challenges ONLY when trust drops below a threshold.
|
| 449 |
+
Inspired by CAPTCHA but far less intrusive — a single arithmetic question.
|
| 450 |
+
|
| 451 |
+
Trust restored: +20 on pass, –15 on fail.
|
| 452 |
+
Threshold: trust < 40 → challenge recommended.
|
| 453 |
+
"""
|
| 454 |
+
|
| 455 |
+
THRESHOLD = 40.0
|
| 456 |
+
|
| 457 |
+
def should_challenge(self, trust_score: float) -> bool:
|
| 458 |
+
return trust_score < self.THRESHOLD
|
| 459 |
+
|
| 460 |
+
def generate(self) -> Dict[str, Any]:
|
| 461 |
+
ops = [('+', lambda a, b: a + b), ('-', lambda a, b: a - b),
|
| 462 |
+
('×', lambda a, b: a * b)]
|
| 463 |
+
sym, fn = random.choice(ops)
|
| 464 |
+
a = random.randint(2, 9) if sym == '×' else random.randint(10, 50)
|
| 465 |
+
b = random.randint(2, 9) if sym == '×' else random.randint(1, 20)
|
| 466 |
+
answer = fn(a, b)
|
| 467 |
+
|
| 468 |
+
cid = hashlib.sha256(
|
| 469 |
+
f"{datetime.utcnow().isoformat()}{random.random()}".encode()
|
| 470 |
+
).hexdigest()[:16]
|
| 471 |
+
|
| 472 |
+
_micro_challenges[cid] = {
|
| 473 |
+
"answer_hash": hashlib.sha256(str(answer).encode()).hexdigest(),
|
| 474 |
+
"expires": datetime.utcnow() + timedelta(minutes=5),
|
| 475 |
+
"attempts": 0,
|
| 476 |
+
"type": "math",
|
| 477 |
+
}
|
| 478 |
+
return {
|
| 479 |
+
"challenge_id": cid,
|
| 480 |
+
"type": "math",
|
| 481 |
+
"question": f"What is {a} {sym} {b} ?",
|
| 482 |
+
"hint": "Answer is an integer.",
|
| 483 |
+
"expires_in_seconds": 300,
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
def verify(self, challenge_id: str, response: str) -> Dict[str, Any]:
|
| 487 |
+
ch = _micro_challenges.get(challenge_id)
|
| 488 |
+
if not ch:
|
| 489 |
+
return {"correct": False, "reason": "Challenge not found or already used.", "trust_delta": -5.0}
|
| 490 |
+
if ch["expires"] < datetime.utcnow():
|
| 491 |
+
_micro_challenges.pop(challenge_id, None)
|
| 492 |
+
return {"correct": False, "reason": "Challenge expired.", "trust_delta": -5.0}
|
| 493 |
+
|
| 494 |
+
given_hash = hashlib.sha256(response.strip().encode()).hexdigest()
|
| 495 |
+
correct = given_hash == ch["answer_hash"]
|
| 496 |
+
ch["attempts"] = ch.get("attempts", 0) + 1
|
| 497 |
+
|
| 498 |
+
if correct or ch["attempts"] >= 3:
|
| 499 |
+
_micro_challenges.pop(challenge_id, None)
|
| 500 |
+
|
| 501 |
+
return {
|
| 502 |
+
"correct": correct,
|
| 503 |
+
"trust_delta": +20.0 if correct else -15.0,
|
| 504 |
+
"reason": (
|
| 505 |
+
"✅ Correct – trust score restored." if correct
|
| 506 |
+
else f"❌ Incorrect. {'Max attempts reached.' if ch['attempts'] >= 3 else 'Try again.'}"
|
| 507 |
+
),
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
|
| 511 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 512 |
+
# Feature 5 – Explainable Risk Transparency
|
| 513 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 514 |
+
|
| 515 |
+
class RiskExplainer:
|
| 516 |
+
"""
|
| 517 |
+
Generates audit-ready, human-readable risk explanations.
|
| 518 |
+
Shows exactly: which signals contributed, their magnitude, and confidence.
|
| 519 |
+
"""
|
| 520 |
+
|
| 521 |
+
_FACTOR_META = {
|
| 522 |
+
"location": {"icon": "🌍", "label": "Location", "weight": "97.68%"},
|
| 523 |
+
"device": {"icon": "💻", "label": "Device", "weight": "0.21%"},
|
| 524 |
+
"time": {"icon": "🕐", "label": "Time Pattern", "weight": "0.02%"},
|
| 525 |
+
"velocity": {"icon": "⚡", "label": "Velocity", "weight": "2.08%"},
|
| 526 |
+
"behavior": {"icon": "🧠", "label": "Behavior", "weight": "0.01%"},
|
| 527 |
+
}
|
| 528 |
+
_LEVEL_LABEL = {
|
| 529 |
+
0: "No step-up – trusted context",
|
| 530 |
+
1: "Standard password auth",
|
| 531 |
+
2: "Email verification required (new IP)",
|
| 532 |
+
3: "2FA required (unknown device)",
|
| 533 |
+
4: "Access BLOCKED – critical risk",
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
def explain_login(
|
| 537 |
+
self,
|
| 538 |
+
risk_factors: Dict[str, float],
|
| 539 |
+
risk_level: str,
|
| 540 |
+
security_level: int,
|
| 541 |
+
) -> Dict[str, Any]:
|
| 542 |
+
factors = []
|
| 543 |
+
for key, score in risk_factors.items():
|
| 544 |
+
meta = self._FACTOR_META.get(key, {"icon": "📌", "label": key.title(), "weight": "N/A"})
|
| 545 |
+
anomalous = score > 30.0
|
| 546 |
+
factors.append({
|
| 547 |
+
"factor": meta["label"],
|
| 548 |
+
"score": round(score, 1),
|
| 549 |
+
"icon": meta["icon"],
|
| 550 |
+
"model_weight": meta["weight"],
|
| 551 |
+
"status": "anomalous" if anomalous else "normal",
|
| 552 |
+
"detail": self._detail(key, anomalous),
|
| 553 |
+
"contribution": round(-score * 0.4 if anomalous else (30 - score) * 0.1, 1),
|
| 554 |
+
})
|
| 555 |
+
factors.sort(key=lambda x: abs(x["contribution"]), reverse=True)
|
| 556 |
+
|
| 557 |
+
anomalous_names = [f["factor"] for f in factors if f["status"] == "anomalous"]
|
| 558 |
+
confidence = max(0.50, min(0.99, 1.0 - len(anomalous_names) * 0.08))
|
| 559 |
+
|
| 560 |
+
return {
|
| 561 |
+
"audit_id": hashlib.sha256(
|
| 562 |
+
f"{datetime.utcnow().isoformat()}{str(risk_factors)}".encode()
|
| 563 |
+
).hexdigest()[:12],
|
| 564 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 565 |
+
"risk_level": risk_level,
|
| 566 |
+
"security_level": security_level,
|
| 567 |
+
"action": self._LEVEL_LABEL.get(security_level, "Unknown"),
|
| 568 |
+
"confidence": round(confidence, 2),
|
| 569 |
+
"factors": factors,
|
| 570 |
+
"summary": _build_summary(anomalous_names, security_level),
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
def explain_trust_event(self, event_type: str, delta: float, signals: Dict) -> str:
|
| 574 |
+
msgs = {
|
| 575 |
+
"behavior_good": f"Human-like behavior signals (+{abs(delta):.0f} trust)",
|
| 576 |
+
"behavior_anomaly": f"Unusual behavior pattern (–{abs(delta):.0f} trust)",
|
| 577 |
+
"decay": f"Inactivity decay (–{abs(delta):.1f} trust)",
|
| 578 |
+
"context_change": f"IP or device changed (–{abs(delta):.0f} trust)",
|
| 579 |
+
"micro_challenge_pass": "Micro-challenge passed (+20 trust)",
|
| 580 |
+
"micro_challenge_fail": "Micro-challenge failed (–15 trust)",
|
| 581 |
+
"impossible_travel": "Impossible travel detected (–50 trust)",
|
| 582 |
+
"init": "Session initialised",
|
| 583 |
+
}
|
| 584 |
+
return msgs.get(event_type, f"Trust updated by {delta:+.1f}")
|
| 585 |
+
|
| 586 |
+
@staticmethod
|
| 587 |
+
def _detail(key: str, anomalous: bool) -> str:
|
| 588 |
+
details = {
|
| 589 |
+
"location": ("Location matches behavioral profile.", "New or unexpected country/city."),
|
| 590 |
+
"device": ("Known device fingerprint.", "Unknown device fingerprint."),
|
| 591 |
+
"time": ("Login within typical hours.", "Login outside typical hours."),
|
| 592 |
+
"velocity": ("Normal login frequency.", "Rapid or repeated attempts detected."),
|
| 593 |
+
"behavior": ("Behavior matches past patterns.", "Behavioral deviation detected."),
|
| 594 |
+
}
|
| 595 |
+
pair = details.get(key, ("Normal.", "Anomalous."))
|
| 596 |
+
return pair[1] if anomalous else pair[0]
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 600 |
+
# Feature 6 – AI-Powered Anomaly Detection (Statistical Isolation Forest)
|
| 601 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 602 |
+
|
| 603 |
+
class StatisticalAnomalyDetector:
|
| 604 |
+
"""
|
| 605 |
+
Isolation-Forest–inspired anomaly scorer.
|
| 606 |
+
|
| 607 |
+
Scores a multi-dimensional feature vector against a learned baseline.
|
| 608 |
+
Each feature's anomaly contribution is computed via Z-score transform,
|
| 609 |
+
then weighted into a composite 0–100 anomaly score.
|
| 610 |
+
|
| 611 |
+
In production this would be replaced with a trained sklearn IsolationForest
|
| 612 |
+
or an LSTM sequence model; the interface is kept identical for easy swap-in.
|
| 613 |
+
"""
|
| 614 |
+
|
| 615 |
+
BASELINE: Dict[str, Tuple[float, float]] = {
|
| 616 |
+
# feature → (mean, std) derived from research on legitimate user behavior
|
| 617 |
+
"typing_entropy": (0.70, 0.15),
|
| 618 |
+
"mouse_linearity": (0.62, 0.18),
|
| 619 |
+
"scroll_variance": (0.48, 0.22),
|
| 620 |
+
"hour_normalized": (0.55, 0.28), # 0 = midnight, 1 = noon normalised to [0,1]
|
| 621 |
+
"failed_attempts_norm": (0.03, 0.10), # recent failed attempts / 20
|
| 622 |
+
}
|
| 623 |
+
WEIGHTS: Dict[str, float] = {
|
| 624 |
+
"typing_entropy": 0.28,
|
| 625 |
+
"mouse_linearity": 0.24,
|
| 626 |
+
"scroll_variance": 0.14,
|
| 627 |
+
"hour_normalized": 0.18,
|
| 628 |
+
"failed_attempts_norm": 0.16,
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
def score(self, features: Dict[str, float]) -> Dict[str, Any]:
|
| 632 |
+
"""
|
| 633 |
+
Score a feature vector. Returns anomaly_score ∈ [0, 100].
|
| 634 |
+
Higher means more anomalous.
|
| 635 |
+
"""
|
| 636 |
+
per_feature: Dict[str, float] = {}
|
| 637 |
+
total = 0.0
|
| 638 |
+
for feat, (mean, std) in self.BASELINE.items():
|
| 639 |
+
val = features.get(feat, mean)
|
| 640 |
+
a = _z_anomaly(float(val), mean, std)
|
| 641 |
+
per_feature[feat] = round(a, 1)
|
| 642 |
+
total += a * self.WEIGHTS.get(feat, 0.10)
|
| 643 |
+
|
| 644 |
+
label, color = self._classify(total)
|
| 645 |
+
confidence = round(min(0.99, 0.50 + total / 200.0), 2)
|
| 646 |
+
|
| 647 |
+
return {
|
| 648 |
+
"anomaly_score": round(total, 2),
|
| 649 |
+
"classification": label,
|
| 650 |
+
"color": color,
|
| 651 |
+
"confidence": confidence,
|
| 652 |
+
"per_feature": per_feature,
|
| 653 |
+
"baseline_comparison": {
|
| 654 |
+
feat: {
|
| 655 |
+
"your_value": round(features.get(feat, mean), 3),
|
| 656 |
+
"typical_mean": mean,
|
| 657 |
+
"typical_std": std,
|
| 658 |
+
"z_score": round(abs(features.get(feat, mean) - mean) / max(std, 1e-6), 2),
|
| 659 |
+
}
|
| 660 |
+
for feat, (mean, std) in self.BASELINE.items()
|
| 661 |
+
},
|
| 662 |
+
"method": "statistical_isolation_forest_analogy",
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
@staticmethod
|
| 666 |
+
def _classify(score: float) -> Tuple[str, str]:
|
| 667 |
+
if score >= 80: return "CRITICAL", "#ef4444"
|
| 668 |
+
if score >= 60: return "HIGH", "#f97316"
|
| 669 |
+
if score >= 40: return "MEDIUM", "#f59e0b"
|
| 670 |
+
if score >= 20: return "LOW", "#84cc16"
|
| 671 |
+
return "NORMAL", "#22c55e"
|
adaptiveauth/routers/__init__.py
CHANGED
|
@@ -6,6 +6,8 @@ from .user import router as user_router
|
|
| 6 |
from .admin import router as admin_router
|
| 7 |
from .risk import router as risk_router
|
| 8 |
from .adaptive import router as adaptive_router
|
|
|
|
|
|
|
| 9 |
|
| 10 |
__all__ = [
|
| 11 |
"auth_router",
|
|
@@ -13,4 +15,6 @@ __all__ = [
|
|
| 13 |
"admin_router",
|
| 14 |
"risk_router",
|
| 15 |
"adaptive_router",
|
|
|
|
|
|
|
| 16 |
]
|
|
|
|
| 6 |
from .admin import router as admin_router
|
| 7 |
from .risk import router as risk_router
|
| 8 |
from .adaptive import router as adaptive_router
|
| 9 |
+
from .demo import router as demo_router
|
| 10 |
+
from .session_intel import router as session_intel_router
|
| 11 |
|
| 12 |
__all__ = [
|
| 13 |
"auth_router",
|
|
|
|
| 15 |
"admin_router",
|
| 16 |
"risk_router",
|
| 17 |
"adaptive_router",
|
| 18 |
+
"demo_router",
|
| 19 |
+
"session_intel_router",
|
| 20 |
]
|
adaptiveauth/routers/adaptive.py
CHANGED
|
@@ -179,7 +179,8 @@ async def verify_challenge(
|
|
| 179 |
current_user.tfa_secret, request.code
|
| 180 |
)
|
| 181 |
elif challenge.challenge_type == 'email':
|
| 182 |
-
|
|
|
|
| 183 |
|
| 184 |
challenge.attempts += 1
|
| 185 |
|
|
|
|
| 179 |
current_user.tfa_secret, request.code
|
| 180 |
)
|
| 181 |
elif challenge.challenge_type == 'email':
|
| 182 |
+
from ..core.security import constant_time_compare
|
| 183 |
+
verified = constant_time_compare(challenge.challenge_code or '', request.code)
|
| 184 |
|
| 185 |
challenge.attempts += 1
|
| 186 |
|
adaptiveauth/routers/admin.py
CHANGED
|
@@ -20,6 +20,35 @@ from .. import schemas
|
|
| 20 |
router = APIRouter(prefix="/admin", tags=["Admin"])
|
| 21 |
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
@router.get("/users", response_model=schemas.AdminUserList)
|
| 24 |
async def list_users(
|
| 25 |
page: int = Query(1, ge=1),
|
|
|
|
| 20 |
router = APIRouter(prefix="/admin", tags=["Admin"])
|
| 21 |
|
| 22 |
|
| 23 |
+
@router.get("/email-status")
|
| 24 |
+
async def email_status(
|
| 25 |
+
current_user: User = Depends(require_admin()),
|
| 26 |
+
):
|
| 27 |
+
"""Check email service configuration status (admin only)."""
|
| 28 |
+
from ..auth.email import get_email_service
|
| 29 |
+
from ..config import get_settings
|
| 30 |
+
svc = get_email_service()
|
| 31 |
+
s = get_settings()
|
| 32 |
+
return {
|
| 33 |
+
"configured": svc.is_configured,
|
| 34 |
+
"fields": {
|
| 35 |
+
"MAIL_USERNAME": bool(s.MAIL_USERNAME),
|
| 36 |
+
"MAIL_PASSWORD": bool(s.MAIL_PASSWORD),
|
| 37 |
+
"MAIL_SERVER": bool(s.MAIL_SERVER),
|
| 38 |
+
"MAIL_FROM": bool(s.MAIL_FROM),
|
| 39 |
+
},
|
| 40 |
+
"mail_port": s.MAIL_PORT,
|
| 41 |
+
"starttls": s.MAIL_STARTTLS,
|
| 42 |
+
"env_prefix": "ADAPTIVEAUTH_",
|
| 43 |
+
"setup_instructions": (
|
| 44 |
+
"Create a .env file in the project root with: "
|
| 45 |
+
"ADAPTIVEAUTH_MAIL_USERNAME, ADAPTIVEAUTH_MAIL_PASSWORD, "
|
| 46 |
+
"ADAPTIVEAUTH_MAIL_SERVER, ADAPTIVEAUTH_MAIL_FROM, "
|
| 47 |
+
"ADAPTIVEAUTH_MAIL_PORT=587, ADAPTIVEAUTH_MAIL_STARTTLS=True"
|
| 48 |
+
)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
@router.get("/users", response_model=schemas.AdminUserList)
|
| 53 |
async def list_users(
|
| 54 |
page: int = Query(1, ge=1),
|
adaptiveauth/routers/demo.py
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AdaptiveAuth Demo Router
|
| 3 |
+
Specialized endpoints for demonstrating the two core framework scenarios.
|
| 4 |
+
|
| 5 |
+
Scenario 1: User Behavior Anomaly Detection
|
| 6 |
+
- User has an established behavioral profile (normal IP, device, hours)
|
| 7 |
+
- When they log in from a suspicious context (new country, new device)
|
| 8 |
+
- The framework detects the behavior change and escalates security
|
| 9 |
+
|
| 10 |
+
Scenario 2: Anomaly (Attack) Detection
|
| 11 |
+
- An attacker makes repeated failed login attempts (brute force)
|
| 12 |
+
- The AnomalyDetector fires and flags the attacker's IP
|
| 13 |
+
- Future logins from that IP are blocked/escalated
|
| 14 |
+
- Demonstrates how the framework protects the entire application
|
| 15 |
+
|
| 16 |
+
FOR DEMONSTRATION PURPOSES ONLY - Do not expose in production without auth.
|
| 17 |
+
"""
|
| 18 |
+
from fastapi import APIRouter, Depends, HTTPException, status
|
| 19 |
+
from sqlalchemy.orm import Session
|
| 20 |
+
from datetime import datetime, timedelta
|
| 21 |
+
from typing import Optional
|
| 22 |
+
|
| 23 |
+
from ..core.database import get_db
|
| 24 |
+
from ..core.security import hash_password, constant_time_compare
|
| 25 |
+
from ..auth.service import AuthService
|
| 26 |
+
from ..models import (
|
| 27 |
+
User, UserProfile, LoginAttempt, AnomalyPattern,
|
| 28 |
+
StepUpChallenge, RiskLevel, RiskEvent
|
| 29 |
+
)
|
| 30 |
+
from ..risk.analyzer import BehaviorAnalyzer
|
| 31 |
+
from ..risk.monitor import AnomalyDetector
|
| 32 |
+
|
| 33 |
+
router = APIRouter(prefix="/demo", tags=["Demo Scenarios"])
|
| 34 |
+
|
| 35 |
+
# ─────────────────────────── Demo Constants ────────────────────────────────
|
| 36 |
+
|
| 37 |
+
DEMO_USER_EMAIL = "demo.normal@adaptive.demo"
|
| 38 |
+
DEMO_USER_PASSWORD = "Demo@Normal123!"
|
| 39 |
+
DEMO_ADMIN_EMAIL = "demo.admin@adaptive.demo"
|
| 40 |
+
DEMO_ADMIN_PASSWORD = "Admin@Demo456!"
|
| 41 |
+
|
| 42 |
+
# The user's established "normal" context (30 days of history)
|
| 43 |
+
NORMAL_CONTEXT = {
|
| 44 |
+
"ip_address": "203.0.113.10", # RFC-5737 documentation IP
|
| 45 |
+
"user_agent": (
|
| 46 |
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
| 47 |
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
| 48 |
+
"Chrome/121.0.0.0 Safari/537.36"
|
| 49 |
+
),
|
| 50 |
+
"device_fingerprint": "trusted-win-chrome-abc123",
|
| 51 |
+
"country": "US",
|
| 52 |
+
"city": "New York",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
# A completely different context – new country, new mobile device, unusual hour
|
| 56 |
+
SUSPICIOUS_CONTEXT = {
|
| 57 |
+
"ip_address": "198.51.100.55", # RFC-5737 documentation IP
|
| 58 |
+
"user_agent": (
|
| 59 |
+
"Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) "
|
| 60 |
+
"AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.2 Mobile/15E148 Safari/604.1"
|
| 61 |
+
),
|
| 62 |
+
"device_fingerprint": "unknown-iphone-device-xyz987",
|
| 63 |
+
"country": "RU",
|
| 64 |
+
"city": "Moscow",
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
# Attacker context – automated script from a different country
|
| 68 |
+
ATTACKER_CONTEXT = {
|
| 69 |
+
"ip_address": "192.0.2.100", # RFC-5737 documentation IP
|
| 70 |
+
"user_agent": "python-requests/2.31.0",
|
| 71 |
+
"device_fingerprint": "attacker-bot-device-001",
|
| 72 |
+
"country": "CN",
|
| 73 |
+
"city": "Beijing",
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# ─────────────────────────── Setup ─────────────────────────────────────────
|
| 78 |
+
|
| 79 |
+
@router.post("/setup")
|
| 80 |
+
async def setup_demo(
|
| 81 |
+
reset: bool = False,
|
| 82 |
+
db: Session = Depends(get_db)
|
| 83 |
+
):
|
| 84 |
+
"""
|
| 85 |
+
Initialise demo environment.
|
| 86 |
+
|
| 87 |
+
Creates two users:
|
| 88 |
+
- demo.normal@adaptive.demo → regular user with 30 days of clean history
|
| 89 |
+
- demo.admin@adaptive.demo → admin for the dashboard
|
| 90 |
+
|
| 91 |
+
Pass ?reset=true to wipe and rebuild the demo user's behavioral profile.
|
| 92 |
+
"""
|
| 93 |
+
created = []
|
| 94 |
+
messages = []
|
| 95 |
+
|
| 96 |
+
# ── Normal demo user ──────────────────────────────────────────────────
|
| 97 |
+
demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
|
| 98 |
+
|
| 99 |
+
if demo_user and reset:
|
| 100 |
+
# Wipe behavioral data so the demo starts fresh
|
| 101 |
+
db.query(UserProfile).filter(UserProfile.user_id == demo_user.id).delete()
|
| 102 |
+
db.query(LoginAttempt).filter(LoginAttempt.user_id == demo_user.id).delete()
|
| 103 |
+
db.commit()
|
| 104 |
+
messages.append("Behavioral profile reset for demo user.")
|
| 105 |
+
|
| 106 |
+
if not demo_user:
|
| 107 |
+
demo_user = User(
|
| 108 |
+
email=DEMO_USER_EMAIL,
|
| 109 |
+
password_hash=hash_password(DEMO_USER_PASSWORD),
|
| 110 |
+
full_name="Demo User",
|
| 111 |
+
is_active=True,
|
| 112 |
+
is_verified=True,
|
| 113 |
+
role="user",
|
| 114 |
+
created_at=datetime.utcnow() - timedelta(days=30),
|
| 115 |
+
)
|
| 116 |
+
db.add(demo_user)
|
| 117 |
+
db.commit()
|
| 118 |
+
db.refresh(demo_user)
|
| 119 |
+
created.append("demo_user")
|
| 120 |
+
|
| 121 |
+
# ── Behavioural history ───────────────────────────────────────────────
|
| 122 |
+
profile = db.query(UserProfile).filter(
|
| 123 |
+
UserProfile.user_id == demo_user.id
|
| 124 |
+
).first()
|
| 125 |
+
|
| 126 |
+
if not profile:
|
| 127 |
+
analyzer = BehaviorAnalyzer(db)
|
| 128 |
+
profile = analyzer.get_or_create_profile(demo_user)
|
| 129 |
+
|
| 130 |
+
# Simulate 15 successful logins over the past 30 days
|
| 131 |
+
for i in range(15):
|
| 132 |
+
login_time = datetime.utcnow() - timedelta(days=i * 2, hours=(i % 9) + 8)
|
| 133 |
+
db.add(LoginAttempt(
|
| 134 |
+
user_id=demo_user.id,
|
| 135 |
+
email=DEMO_USER_EMAIL,
|
| 136 |
+
ip_address=NORMAL_CONTEXT["ip_address"],
|
| 137 |
+
user_agent=NORMAL_CONTEXT["user_agent"],
|
| 138 |
+
device_fingerprint=NORMAL_CONTEXT["device_fingerprint"],
|
| 139 |
+
country="US",
|
| 140 |
+
city="New York",
|
| 141 |
+
risk_score=7.5,
|
| 142 |
+
risk_level=RiskLevel.LOW.value,
|
| 143 |
+
security_level=0,
|
| 144 |
+
risk_factors={
|
| 145 |
+
"device": 0.0, "location": 0.0,
|
| 146 |
+
"time": 5.0, "velocity": 0.0, "behavior": 0.0,
|
| 147 |
+
},
|
| 148 |
+
success=True,
|
| 149 |
+
attempted_at=login_time,
|
| 150 |
+
))
|
| 151 |
+
|
| 152 |
+
db.commit()
|
| 153 |
+
|
| 154 |
+
# Build known profile data from those logins
|
| 155 |
+
now_iso = datetime.utcnow().isoformat()
|
| 156 |
+
profile.known_ips = [{
|
| 157 |
+
"ip": NORMAL_CONTEXT["ip_address"],
|
| 158 |
+
"country": "US", "city": "New York",
|
| 159 |
+
"first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
|
| 160 |
+
"last_seen": now_iso, "count": 15,
|
| 161 |
+
}]
|
| 162 |
+
profile.known_browsers = [{
|
| 163 |
+
"user_agent": NORMAL_CONTEXT["user_agent"],
|
| 164 |
+
"browser_name": "Chrome",
|
| 165 |
+
"first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
|
| 166 |
+
"last_seen": now_iso, "count": 15,
|
| 167 |
+
}]
|
| 168 |
+
profile.known_devices = [{
|
| 169 |
+
"fingerprint": NORMAL_CONTEXT["device_fingerprint"],
|
| 170 |
+
"name": "Windows - Chrome",
|
| 171 |
+
"first_seen": (datetime.utcnow() - timedelta(days=30)).isoformat(),
|
| 172 |
+
"last_seen": now_iso, "count": 15,
|
| 173 |
+
}]
|
| 174 |
+
profile.typical_login_hours = [8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
|
| 175 |
+
profile.typical_login_days = [0, 1, 2, 3, 4] # Mon–Fri
|
| 176 |
+
profile.total_logins = 15
|
| 177 |
+
profile.successful_logins = 15
|
| 178 |
+
profile.failed_logins = 0
|
| 179 |
+
db.commit()
|
| 180 |
+
created.append("behavioral_profile (15 logins)")
|
| 181 |
+
|
| 182 |
+
# ── Admin user ────────────────────────────────────────────────────────
|
| 183 |
+
admin_user = db.query(User).filter(User.email == DEMO_ADMIN_EMAIL).first()
|
| 184 |
+
if not admin_user:
|
| 185 |
+
admin_user = User(
|
| 186 |
+
email=DEMO_ADMIN_EMAIL,
|
| 187 |
+
password_hash=hash_password(DEMO_ADMIN_PASSWORD),
|
| 188 |
+
full_name="Demo Admin",
|
| 189 |
+
is_active=True,
|
| 190 |
+
is_verified=True,
|
| 191 |
+
role="admin",
|
| 192 |
+
created_at=datetime.utcnow() - timedelta(days=30),
|
| 193 |
+
)
|
| 194 |
+
db.add(admin_user)
|
| 195 |
+
db.commit()
|
| 196 |
+
created.append("admin_user")
|
| 197 |
+
|
| 198 |
+
return {
|
| 199 |
+
"status": "ready",
|
| 200 |
+
"created": created,
|
| 201 |
+
"messages": messages,
|
| 202 |
+
"demo_credentials": {
|
| 203 |
+
"normal_user": {
|
| 204 |
+
"email": DEMO_USER_EMAIL,
|
| 205 |
+
"password": DEMO_USER_PASSWORD,
|
| 206 |
+
"description": (
|
| 207 |
+
"Regular user – 30 days of login history from "
|
| 208 |
+
"New York (US) on Windows Chrome, Mon–Fri 8AM–5PM"
|
| 209 |
+
),
|
| 210 |
+
},
|
| 211 |
+
"admin_user": {
|
| 212 |
+
"email": DEMO_ADMIN_EMAIL,
|
| 213 |
+
"password": DEMO_ADMIN_PASSWORD,
|
| 214 |
+
"description": "Admin user – access the full dashboard",
|
| 215 |
+
},
|
| 216 |
+
},
|
| 217 |
+
"established_normal_behavior": {
|
| 218 |
+
"ip": NORMAL_CONTEXT["ip_address"],
|
| 219 |
+
"location": "New York, US",
|
| 220 |
+
"device": "Windows – Chrome",
|
| 221 |
+
"typical_hours": "8 AM – 5 PM (UTC)",
|
| 222 |
+
"typical_days": "Monday – Friday",
|
| 223 |
+
"login_count": 15,
|
| 224 |
+
},
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# ─────────────────────── Scenario 1: Behavior Change ───────────────────────
|
| 229 |
+
|
| 230 |
+
@router.post("/scenario1/normal-login")
|
| 231 |
+
async def scenario1_normal_login(db: Session = Depends(get_db)):
|
| 232 |
+
"""
|
| 233 |
+
SCENARIO 1 – Step 1: Login from the user's known trusted context.
|
| 234 |
+
|
| 235 |
+
Expected result: LOW RISK, Security Level 0–1, immediate access.
|
| 236 |
+
The framework recognises the familiar IP, device, and browser.
|
| 237 |
+
"""
|
| 238 |
+
demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
|
| 239 |
+
if not demo_user:
|
| 240 |
+
raise HTTPException(
|
| 241 |
+
status_code=400,
|
| 242 |
+
detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
|
| 243 |
+
)
|
| 244 |
+
|
| 245 |
+
auth_service = AuthService(db)
|
| 246 |
+
result = await auth_service.adaptive_login(
|
| 247 |
+
email=DEMO_USER_EMAIL,
|
| 248 |
+
password=DEMO_USER_PASSWORD,
|
| 249 |
+
context=NORMAL_CONTEXT,
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
return {
|
| 253 |
+
"scenario": "1 – User Behaviour Anomaly Detection",
|
| 254 |
+
"step": "Step 1 of 3: Normal login (trusted context)",
|
| 255 |
+
"context": {
|
| 256 |
+
"ip": NORMAL_CONTEXT["ip_address"],
|
| 257 |
+
"location": "New York, US ✓ Known",
|
| 258 |
+
"device": "Windows Chrome ✓ Known",
|
| 259 |
+
"time_utc": f"{datetime.utcnow().strftime('%H:%M')} UTC",
|
| 260 |
+
},
|
| 261 |
+
"framework_decision": result,
|
| 262 |
+
"explanation": _explain(result),
|
| 263 |
+
"what_the_framework_checked": [
|
| 264 |
+
"IP 203.0.113.10 → found in behavioral profile (15 previous logins)",
|
| 265 |
+
"Device fingerprint → matches known trusted device",
|
| 266 |
+
"Browser (Chrome/Win) → matches known browser",
|
| 267 |
+
"Login time → within typical hours (8 AM–5 PM)",
|
| 268 |
+
],
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@router.post("/scenario1/suspicious-login")
|
| 273 |
+
async def scenario1_suspicious_login(db: Session = Depends(get_db)):
|
| 274 |
+
"""
|
| 275 |
+
SCENARIO 1 – Step 2: Same credentials, completely different context.
|
| 276 |
+
|
| 277 |
+
Expected result: HIGH RISK, Security Level 2–3, challenge required.
|
| 278 |
+
The framework detects multiple behavioral anomalies simultaneously.
|
| 279 |
+
"""
|
| 280 |
+
demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
|
| 281 |
+
if not demo_user:
|
| 282 |
+
raise HTTPException(
|
| 283 |
+
status_code=400,
|
| 284 |
+
detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
|
| 285 |
+
)
|
| 286 |
+
|
| 287 |
+
auth_service = AuthService(db)
|
| 288 |
+
result = await auth_service.adaptive_login(
|
| 289 |
+
email=DEMO_USER_EMAIL,
|
| 290 |
+
password=DEMO_USER_PASSWORD,
|
| 291 |
+
context=SUSPICIOUS_CONTEXT,
|
| 292 |
+
)
|
| 293 |
+
|
| 294 |
+
return {
|
| 295 |
+
"scenario": "1 – User Behaviour Anomaly Detection",
|
| 296 |
+
"step": "Step 2 of 3: Suspicious login (unknown context)",
|
| 297 |
+
"context": {
|
| 298 |
+
"ip": SUSPICIOUS_CONTEXT["ip_address"],
|
| 299 |
+
"location": "Moscow, Russia ✗ NEVER SEEN BEFORE",
|
| 300 |
+
"device": "iPhone ✗ UNKNOWN DEVICE",
|
| 301 |
+
"time_utc": f"{datetime.utcnow().strftime('%H:%M')} UTC",
|
| 302 |
+
},
|
| 303 |
+
"framework_decision": result,
|
| 304 |
+
"explanation": _explain(result),
|
| 305 |
+
"anomalies_triggered": [
|
| 306 |
+
"IP 198.51.100.55 NOT in behavioral profile",
|
| 307 |
+
"Country changed: US → Russia",
|
| 308 |
+
"Device fingerprint unknown (mobile vs usual desktop)",
|
| 309 |
+
"User agent changed: Windows Chrome → iPhone Safari",
|
| 310 |
+
],
|
| 311 |
+
"note_on_challenge": (
|
| 312 |
+
"If challenge_type is 'email', use code '000000' in the demo "
|
| 313 |
+
"(real deployment sends a live email)."
|
| 314 |
+
if result.get("status") == "challenge_required" else None
|
| 315 |
+
),
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
|
| 319 |
+
@router.post("/scenario1/complete-challenge")
|
| 320 |
+
async def scenario1_complete_challenge(
|
| 321 |
+
challenge_id: str,
|
| 322 |
+
code: str,
|
| 323 |
+
db: Session = Depends(get_db),
|
| 324 |
+
):
|
| 325 |
+
"""
|
| 326 |
+
SCENARIO 1 – Step 3: User passes the step-up challenge.
|
| 327 |
+
|
| 328 |
+
The framework grants access after identity is verified,
|
| 329 |
+
and updates the behavioral profile with the new context.
|
| 330 |
+
|
| 331 |
+
In the live demo, the email OTP is patched to accept '000000'.
|
| 332 |
+
"""
|
| 333 |
+
# Patch the stored code so the demo works without a real email
|
| 334 |
+
challenge = db.query(StepUpChallenge).filter(
|
| 335 |
+
StepUpChallenge.id == int(challenge_id)
|
| 336 |
+
).first()
|
| 337 |
+
|
| 338 |
+
if not challenge:
|
| 339 |
+
raise HTTPException(status_code=404, detail="Challenge not found.")
|
| 340 |
+
|
| 341 |
+
if challenge.challenge_type == "email" and not challenge.is_completed:
|
| 342 |
+
# Allow '000000' as a universal demo OTP
|
| 343 |
+
if code == "000000":
|
| 344 |
+
challenge.challenge_code = "000000"
|
| 345 |
+
db.commit()
|
| 346 |
+
|
| 347 |
+
auth_service = AuthService(db)
|
| 348 |
+
result = await auth_service.verify_step_up(
|
| 349 |
+
challenge_id=challenge_id,
|
| 350 |
+
code=code,
|
| 351 |
+
context=SUSPICIOUS_CONTEXT,
|
| 352 |
+
)
|
| 353 |
+
|
| 354 |
+
if result.get("status") == "error":
|
| 355 |
+
raise HTTPException(
|
| 356 |
+
status_code=400,
|
| 357 |
+
detail=result.get("message", "Verification failed"),
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
return {
|
| 361 |
+
"scenario": "1 – User Behaviour Anomaly Detection",
|
| 362 |
+
"step": "Step 3 of 3: Step-up challenge completed",
|
| 363 |
+
"result": result,
|
| 364 |
+
"explanation": (
|
| 365 |
+
"Identity verified. Framework grants access and begins learning "
|
| 366 |
+
"the new context as a potential future trusted location/device."
|
| 367 |
+
),
|
| 368 |
+
"framework_actions_taken": [
|
| 369 |
+
"Step-up challenge marked complete",
|
| 370 |
+
"JWT access token issued",
|
| 371 |
+
"New session created with elevated security awareness",
|
| 372 |
+
"Risk event logged for audit trail",
|
| 373 |
+
],
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
# ─────────────────────── Scenario 2: Attack Detection ──────────────────────
|
| 378 |
+
|
| 379 |
+
@router.post("/scenario2/simulate-attack")
|
| 380 |
+
async def scenario2_simulate_attack(
|
| 381 |
+
num_attempts: int = 12,
|
| 382 |
+
db: Session = Depends(get_db),
|
| 383 |
+
):
|
| 384 |
+
"""
|
| 385 |
+
SCENARIO 2 – Step 1: Simulate a brute-force attack.
|
| 386 |
+
|
| 387 |
+
Injects `num_attempts` failed login records from the attacker IP,
|
| 388 |
+
then runs the AnomalyDetector. Returns detected anomaly patterns.
|
| 389 |
+
"""
|
| 390 |
+
if num_attempts < 1:
|
| 391 |
+
num_attempts = 1
|
| 392 |
+
if num_attempts > 25:
|
| 393 |
+
num_attempts = 25
|
| 394 |
+
|
| 395 |
+
attacker_ip = ATTACKER_CONTEXT["ip_address"]
|
| 396 |
+
|
| 397 |
+
# Inject failed attempts directly into the DB (no real password check)
|
| 398 |
+
for i in range(num_attempts):
|
| 399 |
+
db.add(LoginAttempt(
|
| 400 |
+
user_id=None,
|
| 401 |
+
email=DEMO_USER_EMAIL,
|
| 402 |
+
ip_address=attacker_ip,
|
| 403 |
+
user_agent=ATTACKER_CONTEXT["user_agent"],
|
| 404 |
+
device_fingerprint=ATTACKER_CONTEXT["device_fingerprint"],
|
| 405 |
+
country=ATTACKER_CONTEXT["country"],
|
| 406 |
+
city=ATTACKER_CONTEXT["city"],
|
| 407 |
+
risk_score=97.0,
|
| 408 |
+
risk_level=RiskLevel.CRITICAL.value,
|
| 409 |
+
security_level=4,
|
| 410 |
+
risk_factors={
|
| 411 |
+
"velocity": 100.0, "location": 90.0,
|
| 412 |
+
"device": 80.0, "time": 30.0, "behavior": 50.0,
|
| 413 |
+
},
|
| 414 |
+
success=False,
|
| 415 |
+
failure_reason="invalid_password",
|
| 416 |
+
attempted_at=datetime.utcnow() - timedelta(
|
| 417 |
+
seconds=(num_attempts - i) * 4
|
| 418 |
+
),
|
| 419 |
+
))
|
| 420 |
+
|
| 421 |
+
db.commit()
|
| 422 |
+
|
| 423 |
+
# Run anomaly detection
|
| 424 |
+
detector = AnomalyDetector(db)
|
| 425 |
+
brute_force_pattern = detector.detect_brute_force(
|
| 426 |
+
attacker_ip, window_minutes=60, threshold=5
|
| 427 |
+
)
|
| 428 |
+
stuffing_pattern = detector.detect_credential_stuffing(
|
| 429 |
+
attacker_ip, window_minutes=60, unique_users_threshold=3
|
| 430 |
+
)
|
| 431 |
+
|
| 432 |
+
anomalies = []
|
| 433 |
+
if brute_force_pattern:
|
| 434 |
+
anomalies.append({
|
| 435 |
+
"id": brute_force_pattern.id,
|
| 436 |
+
"type": "BRUTE FORCE",
|
| 437 |
+
"severity": "CRITICAL",
|
| 438 |
+
"attacker_ip": attacker_ip,
|
| 439 |
+
"failed_attempts": num_attempts,
|
| 440 |
+
"confidence": f"{brute_force_pattern.confidence * 100:.0f}%",
|
| 441 |
+
"detected_at": brute_force_pattern.first_detected.isoformat(),
|
| 442 |
+
})
|
| 443 |
+
if stuffing_pattern:
|
| 444 |
+
anomalies.append({
|
| 445 |
+
"id": stuffing_pattern.id,
|
| 446 |
+
"type": "CREDENTIAL STUFFING",
|
| 447 |
+
"severity": "CRITICAL",
|
| 448 |
+
"attacker_ip": attacker_ip,
|
| 449 |
+
"confidence": f"{stuffing_pattern.confidence * 100:.0f}%",
|
| 450 |
+
})
|
| 451 |
+
|
| 452 |
+
return {
|
| 453 |
+
"scenario": "2 – Anomaly (Attack) Detection",
|
| 454 |
+
"step": "Step 1 of 3: Attack simulated",
|
| 455 |
+
"attack_details": {
|
| 456 |
+
"attacker_ip": attacker_ip,
|
| 457 |
+
"attacker_location": "Beijing, China",
|
| 458 |
+
"tool_signature": ATTACKER_CONTEXT["user_agent"],
|
| 459 |
+
"attempts_injected": num_attempts,
|
| 460 |
+
"target": DEMO_USER_EMAIL,
|
| 461 |
+
"all_attempts_failed": True,
|
| 462 |
+
},
|
| 463 |
+
"anomalies_detected": anomalies,
|
| 464 |
+
"framework_response": (
|
| 465 |
+
f"AnomalyDetector flagged {attacker_ip} as CRITICAL. "
|
| 466 |
+
"All future login attempts from this IP will be blocked (Security Level 4)."
|
| 467 |
+
),
|
| 468 |
+
"next_step": (
|
| 469 |
+
"Now call POST /demo/scenario2/legitimate-user "
|
| 470 |
+
"to see how a real user is treated compared to the attacker."
|
| 471 |
+
),
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
|
| 475 |
+
@router.post("/scenario2/legitimate-user")
|
| 476 |
+
async def scenario2_legitimate_user(db: Session = Depends(get_db)):
|
| 477 |
+
"""
|
| 478 |
+
SCENARIO 2 – Step 2: Legitimate user logs in while the attack is ongoing.
|
| 479 |
+
|
| 480 |
+
Uses the demo user's trusted context (New York IP, known device).
|
| 481 |
+
Demonstrates that the framework distinguishes real users from attackers.
|
| 482 |
+
"""
|
| 483 |
+
demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
|
| 484 |
+
if not demo_user:
|
| 485 |
+
raise HTTPException(
|
| 486 |
+
status_code=400,
|
| 487 |
+
detail="Demo not initialised. Call POST /api/v1/demo/setup first.",
|
| 488 |
+
)
|
| 489 |
+
|
| 490 |
+
auth_service = AuthService(db)
|
| 491 |
+
result = await auth_service.adaptive_login(
|
| 492 |
+
email=DEMO_USER_EMAIL,
|
| 493 |
+
password=DEMO_USER_PASSWORD,
|
| 494 |
+
context=NORMAL_CONTEXT,
|
| 495 |
+
)
|
| 496 |
+
|
| 497 |
+
active_anomalies = db.query(AnomalyPattern).filter(
|
| 498 |
+
AnomalyPattern.is_active == True
|
| 499 |
+
).count()
|
| 500 |
+
|
| 501 |
+
return {
|
| 502 |
+
"scenario": "2 – Anomaly (Attack) Detection",
|
| 503 |
+
"step": "Step 2 of 3: Legitimate user logs in during attack",
|
| 504 |
+
"user_context": {
|
| 505 |
+
"ip": NORMAL_CONTEXT["ip_address"],
|
| 506 |
+
"location": "New York, US ✓ Trusted",
|
| 507 |
+
"device": "Windows Chrome ✓ Trusted",
|
| 508 |
+
},
|
| 509 |
+
"framework_decision": result,
|
| 510 |
+
"explanation": _explain(result),
|
| 511 |
+
"active_threats_right_now": active_anomalies,
|
| 512 |
+
"key_insight": (
|
| 513 |
+
"Even though an attack is in progress against this account, "
|
| 514 |
+
"the legitimate user is recognised by their full behavioral profile "
|
| 515 |
+
"(trusted IP + trusted device + known browser + time pattern). "
|
| 516 |
+
"The framework can differentiate them from the attacker."
|
| 517 |
+
),
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
|
| 521 |
+
@router.post("/scenario2/attacker-login-attempt")
|
| 522 |
+
async def scenario2_attacker_attempt(db: Session = Depends(get_db)):
|
| 523 |
+
"""
|
| 524 |
+
SCENARIO 2 – Step 3: The attacker now tries a valid password.
|
| 525 |
+
|
| 526 |
+
Even with correct credentials, the attacker is blocked because their
|
| 527 |
+
IP is flagged, their user-agent is suspicious, and velocity rules fire.
|
| 528 |
+
"""
|
| 529 |
+
auth_service = AuthService(db)
|
| 530 |
+
result = await auth_service.adaptive_login(
|
| 531 |
+
email=DEMO_USER_EMAIL,
|
| 532 |
+
password=DEMO_USER_PASSWORD, # correct password!
|
| 533 |
+
context=ATTACKER_CONTEXT,
|
| 534 |
+
)
|
| 535 |
+
|
| 536 |
+
return {
|
| 537 |
+
"scenario": "2 – Anomaly (Attack) Detection",
|
| 538 |
+
"step": "Step 3 of 3: Attacker uses correct password – still blocked",
|
| 539 |
+
"attacker_context": {
|
| 540 |
+
"ip": ATTACKER_CONTEXT["ip_address"],
|
| 541 |
+
"location": "Beijing, China ✗ Flagged IP",
|
| 542 |
+
"tool": ATTACKER_CONTEXT["user_agent"] + " ✗ Suspicious",
|
| 543 |
+
},
|
| 544 |
+
"framework_decision": result,
|
| 545 |
+
"explanation": _explain(result),
|
| 546 |
+
"key_insight": (
|
| 547 |
+
"The attacker has the correct password but is BLOCKED because: "
|
| 548 |
+
"(1) IP is flagged as CRITICAL in the anomaly database, "
|
| 549 |
+
"(2) velocity rules detect rapid prior attempts, "
|
| 550 |
+
"(3) suspicious user-agent (python-requests). "
|
| 551 |
+
"Correct password alone is not enough."
|
| 552 |
+
),
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
|
| 556 |
+
@router.get("/scenario2/anomalies")
|
| 557 |
+
async def get_scenario2_anomalies(db: Session = Depends(get_db)):
|
| 558 |
+
"""Get all active anomaly patterns (used by the live anomaly feed in the UI)."""
|
| 559 |
+
detector = AnomalyDetector(db)
|
| 560 |
+
anomalies = detector.get_active_anomalies()
|
| 561 |
+
|
| 562 |
+
return {
|
| 563 |
+
"active_anomalies": [
|
| 564 |
+
{
|
| 565 |
+
"id": a.id,
|
| 566 |
+
"type": a.pattern_type.upper().replace("_", " "),
|
| 567 |
+
"severity": a.severity.upper(),
|
| 568 |
+
"ip": a.ip_address,
|
| 569 |
+
"user_id": a.user_id,
|
| 570 |
+
"confidence": f"{a.confidence * 100:.0f}%",
|
| 571 |
+
"first_detected": a.first_detected.isoformat(),
|
| 572 |
+
"last_detected": a.last_detected.isoformat(),
|
| 573 |
+
"data": a.pattern_data,
|
| 574 |
+
}
|
| 575 |
+
for a in anomalies
|
| 576 |
+
],
|
| 577 |
+
"total": len(anomalies),
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
@router.delete("/scenario2/clear-anomalies")
|
| 582 |
+
async def clear_demo_anomalies(db: Session = Depends(get_db)):
|
| 583 |
+
"""Resolve all anomalies (clean-up after demo)."""
|
| 584 |
+
anomalies = db.query(AnomalyPattern).filter(
|
| 585 |
+
AnomalyPattern.is_active == True
|
| 586 |
+
).all()
|
| 587 |
+
for a in anomalies:
|
| 588 |
+
a.is_active = False
|
| 589 |
+
a.resolved_at = datetime.utcnow()
|
| 590 |
+
db.commit()
|
| 591 |
+
return {"message": f"Resolved {len(anomalies)} anomaly pattern(s)."}
|
| 592 |
+
|
| 593 |
+
|
| 594 |
+
@router.post("/scenario2/run-monitoring-cycle")
|
| 595 |
+
async def run_monitoring_cycle(db: Session = Depends(get_db)):
|
| 596 |
+
"""
|
| 597 |
+
Run one complete continuous-monitoring cycle:
|
| 598 |
+
- Clean expired sessions
|
| 599 |
+
- Re-scan all known attack IPs for brute-force / credential-stuffing patterns
|
| 600 |
+
- Collect session statistics
|
| 601 |
+
- Return a full monitoring report
|
| 602 |
+
|
| 603 |
+
The UI calls this endpoint every 2 seconds while monitoring is active.
|
| 604 |
+
"""
|
| 605 |
+
from ..risk.monitor import SessionMonitor
|
| 606 |
+
|
| 607 |
+
monitor = SessionMonitor(db)
|
| 608 |
+
detector = AnomalyDetector(db)
|
| 609 |
+
attacker_ip = ATTACKER_CONTEXT["ip_address"]
|
| 610 |
+
|
| 611 |
+
# 1. Clean expired sessions
|
| 612 |
+
expired_cleaned = monitor.cleanup_expired_sessions()
|
| 613 |
+
|
| 614 |
+
# 2. Re-scan for active attack patterns
|
| 615 |
+
brute_force = detector.detect_brute_force(
|
| 616 |
+
attacker_ip, window_minutes=60, threshold=5
|
| 617 |
+
)
|
| 618 |
+
stuffing = detector.detect_credential_stuffing(
|
| 619 |
+
attacker_ip, window_minutes=60, unique_users_threshold=3
|
| 620 |
+
)
|
| 621 |
+
|
| 622 |
+
# 3. Session statistics
|
| 623 |
+
session_stats = monitor.get_session_statistics()
|
| 624 |
+
|
| 625 |
+
# 4. Failed login count in last hour
|
| 626 |
+
recent_failed = db.query(LoginAttempt).filter(
|
| 627 |
+
LoginAttempt.success == False,
|
| 628 |
+
LoginAttempt.attempted_at >= datetime.utcnow() - timedelta(minutes=60),
|
| 629 |
+
).count()
|
| 630 |
+
|
| 631 |
+
# 5. All active anomalies
|
| 632 |
+
active = detector.get_active_anomalies()
|
| 633 |
+
|
| 634 |
+
threat_level = "NORMAL"
|
| 635 |
+
if active:
|
| 636 |
+
sev_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
| 637 |
+
max_sev = max(active, key=lambda a: sev_order.get(a.severity, 0)).severity
|
| 638 |
+
threat_level = max_sev.upper()
|
| 639 |
+
|
| 640 |
+
return {
|
| 641 |
+
"cycle_at": datetime.utcnow().isoformat(),
|
| 642 |
+
"sessions": {
|
| 643 |
+
"expired_cleaned": expired_cleaned,
|
| 644 |
+
"active": session_stats["active"],
|
| 645 |
+
"suspicious": session_stats["suspicious"],
|
| 646 |
+
"total": session_stats["total"],
|
| 647 |
+
},
|
| 648 |
+
"scan": {
|
| 649 |
+
"brute_force_active": brute_force is not None,
|
| 650 |
+
"credential_stuffing_active": stuffing is not None,
|
| 651 |
+
"total_active_anomalies": len(active),
|
| 652 |
+
"recent_failed_logins_1h": recent_failed,
|
| 653 |
+
},
|
| 654 |
+
"active_anomalies": [
|
| 655 |
+
{
|
| 656 |
+
"type": a.pattern_type.upper().replace("_", " "),
|
| 657 |
+
"severity": a.severity.upper(),
|
| 658 |
+
"ip": a.ip_address,
|
| 659 |
+
"confidence": f"{a.confidence * 100:.0f}%",
|
| 660 |
+
"last_detected": a.last_detected.isoformat(),
|
| 661 |
+
}
|
| 662 |
+
for a in active
|
| 663 |
+
],
|
| 664 |
+
"threat_level": threat_level,
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
|
| 668 |
+
# ─────────────────────── Demo State ────────────────────────────────────────
|
| 669 |
+
|
| 670 |
+
@router.get("/state")
|
| 671 |
+
async def get_demo_state(db: Session = Depends(get_db)):
|
| 672 |
+
"""Return current demo environment state (used by the UI to show setup status)."""
|
| 673 |
+
demo_user = db.query(User).filter(User.email == DEMO_USER_EMAIL).first()
|
| 674 |
+
admin_user = db.query(User).filter(User.email == DEMO_ADMIN_EMAIL).first()
|
| 675 |
+
|
| 676 |
+
profile = None
|
| 677 |
+
login_history_count = 0
|
| 678 |
+
known_devices = 0
|
| 679 |
+
known_ips = 0
|
| 680 |
+
|
| 681 |
+
if demo_user:
|
| 682 |
+
profile = db.query(UserProfile).filter(
|
| 683 |
+
UserProfile.user_id == demo_user.id
|
| 684 |
+
).first()
|
| 685 |
+
login_history_count = db.query(LoginAttempt).filter(
|
| 686 |
+
LoginAttempt.user_id == demo_user.id
|
| 687 |
+
).count()
|
| 688 |
+
if profile:
|
| 689 |
+
known_devices = len(profile.known_devices or [])
|
| 690 |
+
known_ips = len(profile.known_ips or [])
|
| 691 |
+
|
| 692 |
+
active_anomalies = db.query(AnomalyPattern).filter(
|
| 693 |
+
AnomalyPattern.is_active == True
|
| 694 |
+
).count()
|
| 695 |
+
|
| 696 |
+
is_ready = (
|
| 697 |
+
demo_user is not None
|
| 698 |
+
and profile is not None
|
| 699 |
+
and login_history_count >= 5
|
| 700 |
+
)
|
| 701 |
+
|
| 702 |
+
return {
|
| 703 |
+
"is_ready": is_ready,
|
| 704 |
+
"demo_user_exists": demo_user is not None,
|
| 705 |
+
"admin_user_exists": admin_user is not None,
|
| 706 |
+
"behavioral_profile": {
|
| 707 |
+
"exists": profile is not None,
|
| 708 |
+
"login_history_count": login_history_count,
|
| 709 |
+
"known_devices": known_devices,
|
| 710 |
+
"known_ips": known_ips,
|
| 711 |
+
"typical_hours": profile.typical_login_hours if profile else [],
|
| 712 |
+
"typical_days": profile.typical_login_days if profile else [],
|
| 713 |
+
},
|
| 714 |
+
"active_anomalies": active_anomalies,
|
| 715 |
+
"credentials": {
|
| 716 |
+
"normal_user": {
|
| 717 |
+
"email": DEMO_USER_EMAIL,
|
| 718 |
+
"password": DEMO_USER_PASSWORD,
|
| 719 |
+
},
|
| 720 |
+
"admin": {
|
| 721 |
+
"email": DEMO_ADMIN_EMAIL,
|
| 722 |
+
"password": DEMO_ADMIN_PASSWORD,
|
| 723 |
+
},
|
| 724 |
+
} if demo_user else None,
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
# ─────────────────────── Helpers ───────────────────────────────────────────
|
| 729 |
+
|
| 730 |
+
def _explain(result: dict) -> str:
|
| 731 |
+
"""Human-readable explanation of the framework's decision."""
|
| 732 |
+
s = result.get("status", "")
|
| 733 |
+
lvl = result.get("security_level", 0)
|
| 734 |
+
|
| 735 |
+
if s == "success":
|
| 736 |
+
if lvl == 0:
|
| 737 |
+
return (
|
| 738 |
+
"Framework recognised the trusted device, IP, and time pattern. "
|
| 739 |
+
"No additional verification needed – access granted immediately."
|
| 740 |
+
)
|
| 741 |
+
return "Access granted after challenge completion."
|
| 742 |
+
|
| 743 |
+
if s == "challenge_required":
|
| 744 |
+
challenge = result.get("challenge_type", "verification")
|
| 745 |
+
if lvl == 2:
|
| 746 |
+
return (
|
| 747 |
+
f"Unknown IP detected. {challenge.upper()} challenge sent. "
|
| 748 |
+
"Medium-risk: new location but no other anomalies."
|
| 749 |
+
)
|
| 750 |
+
if lvl == 3:
|
| 751 |
+
return (
|
| 752 |
+
f"Unknown device AND new IP. {challenge.upper()} challenge required. "
|
| 753 |
+
"High-risk: full identity verification needed."
|
| 754 |
+
)
|
| 755 |
+
return f"Elevated risk. {challenge.upper()} challenge triggered."
|
| 756 |
+
|
| 757 |
+
if s == "blocked":
|
| 758 |
+
return (
|
| 759 |
+
"BLOCKED. Velocity rules, anomaly patterns, or critical risk score "
|
| 760 |
+
"exceeded the threshold. Access denied."
|
| 761 |
+
)
|
| 762 |
+
|
| 763 |
+
return "Framework evaluated the request and made a security decision."
|
adaptiveauth/routers/session_intel.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Session Intelligence Router
|
| 3 |
+
============================
|
| 4 |
+
HTTP endpoints for all 8 advanced security features.
|
| 5 |
+
|
| 6 |
+
Protected endpoints require a valid Bearer JWT token.
|
| 7 |
+
Demo endpoints are unauthenticated for demo purposes.
|
| 8 |
+
"""
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Optional, Dict, Any
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
| 13 |
+
from pydantic import BaseModel, Field, field_validator
|
| 14 |
+
from sqlalchemy.orm import Session
|
| 15 |
+
|
| 16 |
+
from ..core.database import get_db
|
| 17 |
+
from ..core.dependencies import get_current_user, get_current_session, oauth2_scheme
|
| 18 |
+
from ..core.security import decode_token
|
| 19 |
+
from ..models import User, UserSession
|
| 20 |
+
from ..risk.session_intelligence import (
|
| 21 |
+
TrustScoreManager,
|
| 22 |
+
BehaviorSignalProcessor,
|
| 23 |
+
ImpossibleTravelDetector,
|
| 24 |
+
MicroChallengeEngine,
|
| 25 |
+
RiskExplainer,
|
| 26 |
+
StatisticalAnomalyDetector,
|
| 27 |
+
CITY_COORDS,
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
router = APIRouter(prefix="/session-intel", tags=["Session Intelligence"])
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# ── Request / Response schemas ───────────────────────────────────────────────
|
| 34 |
+
|
| 35 |
+
class BehaviorSignalInput(BaseModel):
|
| 36 |
+
"""Privacy-first: client sends only aggregated scores, never raw events."""
|
| 37 |
+
typing_entropy: float = Field(..., ge=0, le=1, description="1.0 = very human-like")
|
| 38 |
+
mouse_linearity: float = Field(..., ge=0, le=1, description="1.0 = natural curved paths")
|
| 39 |
+
scroll_variance: float = Field(..., ge=0, le=1, description="1.0 = organic scroll rhythm")
|
| 40 |
+
local_risk_score: float = Field(..., ge=0, le=1, description="Client-computed composite (0 = safe)")
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class TravelCheckInput(BaseModel):
|
| 44 |
+
from_city: str = Field(..., description="Origin city name")
|
| 45 |
+
from_country: str = Field("", description="Origin country")
|
| 46 |
+
to_city: str = Field(..., description="Destination city name")
|
| 47 |
+
to_country: str = Field("", description="Destination country")
|
| 48 |
+
time_gap_hours: float = Field(..., gt=0, description="Hours between the two events")
|
| 49 |
+
from_lat: Optional[float] = None
|
| 50 |
+
from_lon: Optional[float] = None
|
| 51 |
+
to_lat: Optional[float] = None
|
| 52 |
+
to_lon: Optional[float] = None
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class AnomalyScoreInput(BaseModel):
|
| 56 |
+
typing_entropy: float = Field(0.70, ge=0, le=1)
|
| 57 |
+
mouse_linearity: float = Field(0.62, ge=0, le=1)
|
| 58 |
+
scroll_variance: float = Field(0.48, ge=0, le=1)
|
| 59 |
+
hour_normalized: float = Field(0.55, ge=0, le=1,
|
| 60 |
+
description="Current hour / 24 (0 = midnight)")
|
| 61 |
+
failed_attempts_norm: float = Field(0.00, ge=0, le=1,
|
| 62 |
+
description="Recent failed logins / 20")
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class ChallengeVerifyInput(BaseModel):
|
| 66 |
+
challenge_id: str
|
| 67 |
+
response: str = Field(..., description="User's answer as a string")
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class SimulateTrustDropInput(BaseModel):
|
| 71 |
+
target_score: float = Field(25.0, ge=0, le=100)
|
| 72 |
+
reason: str = Field("Simulated trust drop for demo")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class ExplainInput(BaseModel):
|
| 76 |
+
"""Standalone explainability — submit raw factor scores for a report."""
|
| 77 |
+
location_score: float = Field(0.0, ge=0, le=100)
|
| 78 |
+
device_score: float = Field(0.0, ge=0, le=100)
|
| 79 |
+
time_score: float = Field(0.0, ge=0, le=100)
|
| 80 |
+
velocity_score: float = Field(0.0, ge=0, le=100)
|
| 81 |
+
behavior_score: float = Field(0.0, ge=0, le=100)
|
| 82 |
+
security_level: int = Field(0, ge=0, le=4)
|
| 83 |
+
risk_level: str = Field("low")
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# ── Helpers ──────────────────────────────────────────────────────────────────
|
| 87 |
+
|
| 88 |
+
def _get_or_create_demo_session(user: User, db: Session) -> UserSession:
|
| 89 |
+
"""Return the user's most recent active session, or a lightweight synthetic one."""
|
| 90 |
+
session = (
|
| 91 |
+
db.query(UserSession)
|
| 92 |
+
.filter(UserSession.user_id == user.id, UserSession.status == "active")
|
| 93 |
+
.order_by(UserSession.created_at.desc())
|
| 94 |
+
.first()
|
| 95 |
+
)
|
| 96 |
+
if session:
|
| 97 |
+
return session
|
| 98 |
+
|
| 99 |
+
# Create a minimal demo session so we can track trust events
|
| 100 |
+
session = UserSession(
|
| 101 |
+
user_id=user.id,
|
| 102 |
+
session_token=f"demo-{user.id}-{datetime.utcnow().timestamp():.0f}",
|
| 103 |
+
current_risk_level="low",
|
| 104 |
+
status="active",
|
| 105 |
+
created_at=datetime.utcnow(),
|
| 106 |
+
)
|
| 107 |
+
db.add(session)
|
| 108 |
+
db.commit()
|
| 109 |
+
db.refresh(session)
|
| 110 |
+
return session
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def _resolve_demo_travel(
|
| 114 |
+
input_data: TravelCheckInput,
|
| 115 |
+
) -> Dict[str, Any]:
|
| 116 |
+
"""
|
| 117 |
+
Pure coordinate-based travel check for the demo endpoint.
|
| 118 |
+
This does not require a registered user — it just calculates distance + speed.
|
| 119 |
+
"""
|
| 120 |
+
from ..risk.session_intelligence import haversine, _resolve_coords, CITY_COORDS
|
| 121 |
+
|
| 122 |
+
lat1, lon1 = _resolve_coords(input_data.from_city, input_data.from_lat, input_data.from_lon)
|
| 123 |
+
lat2, lon2 = _resolve_coords(input_data.to_city, input_data.to_lat, input_data.to_lon)
|
| 124 |
+
|
| 125 |
+
if lat1 is None or lat2 is None:
|
| 126 |
+
known = sorted(CITY_COORDS.keys())
|
| 127 |
+
missing = input_data.from_city if lat1 is None else input_data.to_city
|
| 128 |
+
return {
|
| 129 |
+
"possible": None,
|
| 130 |
+
"verdict": "coords_unknown",
|
| 131 |
+
"message": f"Unknown city: '{missing}'. Known cities: {', '.join(known)}.",
|
| 132 |
+
"distance_km": 0.0, "speed_kmh": 0.0,
|
| 133 |
+
"time_gap_minutes": input_data.time_gap_hours * 60,
|
| 134 |
+
"trust_delta": 0.0,
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
distance_km = haversine(lat1, lon1, lat2, lon2)
|
| 138 |
+
time_gap_s = input_data.time_gap_hours * 3600
|
| 139 |
+
time_gap_min = time_gap_s / 60
|
| 140 |
+
speed_kmh = distance_km / input_data.time_gap_hours
|
| 141 |
+
|
| 142 |
+
IMPOSSIBLE = 900.0
|
| 143 |
+
SUSPICIOUS = 400.0
|
| 144 |
+
|
| 145 |
+
if distance_km < 50:
|
| 146 |
+
verdict, possible, trust_delta = "same_area", True, 0.0
|
| 147 |
+
msg = "Same geographic area. No anomaly detected."
|
| 148 |
+
elif speed_kmh > IMPOSSIBLE:
|
| 149 |
+
verdict, possible, trust_delta = "impossible", False, -50.0
|
| 150 |
+
msg = (
|
| 151 |
+
f"🚨 IMPOSSIBLE TRAVEL: {distance_km:.0f} km in {time_gap_min:.0f} min "
|
| 152 |
+
f"= {speed_kmh:.0f} km/h — faster than any commercial aircraft (~900 km/h)."
|
| 153 |
+
)
|
| 154 |
+
elif speed_kmh > SUSPICIOUS:
|
| 155 |
+
verdict, possible, trust_delta = "suspicious", True, -20.0
|
| 156 |
+
msg = (
|
| 157 |
+
f"⚠ Suspicious speed: {speed_kmh:.0f} km/h over {distance_km:.0f} km "
|
| 158 |
+
f"in {time_gap_min:.0f} min. Requires air travel justification."
|
| 159 |
+
)
|
| 160 |
+
else:
|
| 161 |
+
verdict, possible, trust_delta = "plausible", True, 0.0
|
| 162 |
+
msg = (
|
| 163 |
+
f"✓ Plausible: {distance_km:.0f} km at {speed_kmh:.0f} km/h "
|
| 164 |
+
f"in {time_gap_min:.0f} min."
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
return {
|
| 168 |
+
"possible": possible,
|
| 169 |
+
"verdict": verdict,
|
| 170 |
+
"message": msg,
|
| 171 |
+
"distance_km": round(distance_km, 1),
|
| 172 |
+
"speed_kmh": round(speed_kmh, 1),
|
| 173 |
+
"time_gap_minutes": round(time_gap_min, 1),
|
| 174 |
+
"from": {
|
| 175 |
+
"city": input_data.from_city, "country": input_data.from_country,
|
| 176 |
+
"lat": lat1, "lon": lon1,
|
| 177 |
+
},
|
| 178 |
+
"to": {
|
| 179 |
+
"city": input_data.to_city, "country": input_data.to_country,
|
| 180 |
+
"lat": lat2, "lon": lon2,
|
| 181 |
+
},
|
| 182 |
+
"trust_delta": trust_delta,
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 187 |
+
# Protected endpoints (require JWT Bearer token)
|
| 188 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 189 |
+
|
| 190 |
+
@router.post("/behavior-signal")
|
| 191 |
+
async def receive_behavior_signal(
|
| 192 |
+
signal: BehaviorSignalInput,
|
| 193 |
+
current_user: User = Depends(get_current_user),
|
| 194 |
+
db: Session = Depends(get_db),
|
| 195 |
+
):
|
| 196 |
+
"""
|
| 197 |
+
Feature 2 & 8 – Submit privacy-first behavior signal.
|
| 198 |
+
|
| 199 |
+
Client computes typing_entropy, mouse_linearity, scroll_variance locally.
|
| 200 |
+
Only aggregated 0–1 scores are transmitted — no raw events leave the browser.
|
| 201 |
+
"""
|
| 202 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 203 |
+
processor = BehaviorSignalProcessor()
|
| 204 |
+
result = processor.process(
|
| 205 |
+
session=session,
|
| 206 |
+
typing_entropy=signal.typing_entropy,
|
| 207 |
+
mouse_linearity=signal.mouse_linearity,
|
| 208 |
+
scroll_variance=signal.scroll_variance,
|
| 209 |
+
local_risk_score=signal.local_risk_score,
|
| 210 |
+
db=db,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# Update trust score
|
| 214 |
+
tsm = TrustScoreManager(db)
|
| 215 |
+
event_type = "behavior_good" if result["trust_delta"] > 0 else "behavior_anomaly"
|
| 216 |
+
new_trust = tsm.update(
|
| 217 |
+
session=session,
|
| 218 |
+
delta=result["trust_delta"],
|
| 219 |
+
event_type=event_type,
|
| 220 |
+
reason=f"Behavior signal processed — anomaly score: {result['anomaly_score']:.1f}",
|
| 221 |
+
signals=result["signals"],
|
| 222 |
+
)
|
| 223 |
+
|
| 224 |
+
mce = MicroChallengeEngine()
|
| 225 |
+
return {
|
| 226 |
+
"status": "processed",
|
| 227 |
+
"behavior": result,
|
| 228 |
+
"trust": {
|
| 229 |
+
"score": round(new_trust, 2),
|
| 230 |
+
"label": TrustScoreManager.label(new_trust),
|
| 231 |
+
"color": TrustScoreManager.color(new_trust),
|
| 232 |
+
"delta": result["trust_delta"],
|
| 233 |
+
},
|
| 234 |
+
"micro_challenge_recommended": mce.should_challenge(new_trust),
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
|
| 238 |
+
@router.get("/trust-score")
|
| 239 |
+
async def get_trust_score(
|
| 240 |
+
current_user: User = Depends(get_current_user),
|
| 241 |
+
db: Session = Depends(get_db),
|
| 242 |
+
):
|
| 243 |
+
"""Feature 1 & 3 – Get current trust score + decay-adjusted value + history."""
|
| 244 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 245 |
+
tsm = TrustScoreManager(db)
|
| 246 |
+
|
| 247 |
+
score, decay_delta = tsm.apply_decay(session)
|
| 248 |
+
if abs(decay_delta) > 0.1:
|
| 249 |
+
tsm.update(session, decay_delta, "decay",
|
| 250 |
+
f"Time-based decay: {decay_delta:.2f} pts", {})
|
| 251 |
+
score = tsm.get(session)
|
| 252 |
+
|
| 253 |
+
history = tsm.get_history(session.id)
|
| 254 |
+
mce = MicroChallengeEngine()
|
| 255 |
+
|
| 256 |
+
return {
|
| 257 |
+
"trust_score": round(score, 2),
|
| 258 |
+
"label": TrustScoreManager.label(score),
|
| 259 |
+
"color": TrustScoreManager.color(score),
|
| 260 |
+
"session_id": session.id,
|
| 261 |
+
"history": history,
|
| 262 |
+
"micro_challenge_recommended": mce.should_challenge(score),
|
| 263 |
+
"thresholds": {
|
| 264 |
+
"trusted": 80,
|
| 265 |
+
"watchful": 60,
|
| 266 |
+
"elevated": 40,
|
| 267 |
+
"high_risk": 20,
|
| 268 |
+
},
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
@router.post("/continuous-verify")
|
| 273 |
+
async def continuous_verify(
|
| 274 |
+
request: Request,
|
| 275 |
+
current_user: User = Depends(get_current_user),
|
| 276 |
+
db: Session = Depends(get_db),
|
| 277 |
+
):
|
| 278 |
+
"""
|
| 279 |
+
Feature 1 – Continuous Verification cycle.
|
| 280 |
+
Re-evaluates the session context; applies decay; returns updated trust.
|
| 281 |
+
"""
|
| 282 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 283 |
+
tsm = TrustScoreManager(db)
|
| 284 |
+
|
| 285 |
+
# Apply time-based decay since last check
|
| 286 |
+
score, decay_delta = tsm.apply_decay(session)
|
| 287 |
+
if abs(decay_delta) > 0.05:
|
| 288 |
+
tsm.update(session, decay_delta, "decay",
|
| 289 |
+
f"Continuous verify — inactivity decay {decay_delta:.2f} pts", {})
|
| 290 |
+
score = tsm.get(session)
|
| 291 |
+
|
| 292 |
+
mce = MicroChallengeEngine()
|
| 293 |
+
return {
|
| 294 |
+
"verified": True,
|
| 295 |
+
"user_email": current_user.email,
|
| 296 |
+
"session_id": session.id,
|
| 297 |
+
"trust_score": round(score, 2),
|
| 298 |
+
"label": TrustScoreManager.label(score),
|
| 299 |
+
"color": TrustScoreManager.color(score),
|
| 300 |
+
"decay_applied": round(decay_delta, 3),
|
| 301 |
+
"micro_challenge_recommended": mce.should_challenge(score),
|
| 302 |
+
"verified_at": datetime.utcnow().isoformat(),
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
@router.get("/explain")
|
| 307 |
+
async def explain_session(
|
| 308 |
+
current_user: User = Depends(get_current_user),
|
| 309 |
+
db: Session = Depends(get_db),
|
| 310 |
+
):
|
| 311 |
+
"""
|
| 312 |
+
Feature 5 – Explainability: return history-driven trust explanation
|
| 313 |
+
and the last known login risk breakdown.
|
| 314 |
+
"""
|
| 315 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 316 |
+
tsm = TrustScoreManager(db)
|
| 317 |
+
score = tsm.get(session)
|
| 318 |
+
history = tsm.get_history(session.id, limit=10)
|
| 319 |
+
|
| 320 |
+
explainer = RiskExplainer()
|
| 321 |
+
trust_events_explained = [
|
| 322 |
+
{
|
| 323 |
+
"at": e["at"],
|
| 324 |
+
"score": e["score"],
|
| 325 |
+
"delta": e["delta"],
|
| 326 |
+
"explanation": explainer.explain_trust_event(e["event_type"], e["delta"], {}),
|
| 327 |
+
}
|
| 328 |
+
for e in history
|
| 329 |
+
]
|
| 330 |
+
|
| 331 |
+
return {
|
| 332 |
+
"user_email": current_user.email,
|
| 333 |
+
"current_trust": round(score, 2),
|
| 334 |
+
"trust_label": TrustScoreManager.label(score),
|
| 335 |
+
"recent_events": trust_events_explained,
|
| 336 |
+
"summary": (
|
| 337 |
+
f"Trust score is {score:.0f}/100 ({TrustScoreManager.label(score)}). "
|
| 338 |
+
f"{len(history)} events recorded this session."
|
| 339 |
+
),
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
@router.post("/micro-challenge/generate")
|
| 344 |
+
async def generate_challenge(
|
| 345 |
+
current_user: User = Depends(get_current_user),
|
| 346 |
+
db: Session = Depends(get_db),
|
| 347 |
+
):
|
| 348 |
+
"""Feature 4 – Generate an inline math micro-challenge."""
|
| 349 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 350 |
+
tsm = TrustScoreManager(db)
|
| 351 |
+
score = tsm.get(session)
|
| 352 |
+
|
| 353 |
+
mce = MicroChallengeEngine()
|
| 354 |
+
challenge = mce.generate()
|
| 355 |
+
return {
|
| 356 |
+
"current_trust": round(score, 2),
|
| 357 |
+
"challenge_needed": mce.should_challenge(score),
|
| 358 |
+
"challenge": challenge,
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
|
| 362 |
+
@router.post("/micro-challenge/verify")
|
| 363 |
+
async def verify_challenge(
|
| 364 |
+
body: ChallengeVerifyInput,
|
| 365 |
+
current_user: User = Depends(get_current_user),
|
| 366 |
+
db: Session = Depends(get_db),
|
| 367 |
+
):
|
| 368 |
+
"""Feature 4 – Verify the answer to a micro-challenge and update trust."""
|
| 369 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 370 |
+
mce = MicroChallengeEngine()
|
| 371 |
+
result = mce.verify(body.challenge_id, body.response)
|
| 372 |
+
|
| 373 |
+
tsm = TrustScoreManager(db)
|
| 374 |
+
event = "micro_challenge_pass" if result["correct"] else "micro_challenge_fail"
|
| 375 |
+
new_trust = tsm.update(session, result["trust_delta"], event, result["reason"], {})
|
| 376 |
+
|
| 377 |
+
return {
|
| 378 |
+
**result,
|
| 379 |
+
"new_trust": round(new_trust, 2),
|
| 380 |
+
"trust_label": TrustScoreManager.label(new_trust),
|
| 381 |
+
"trust_color": TrustScoreManager.color(new_trust),
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
@router.post("/simulate-trust-drop")
|
| 386 |
+
async def simulate_trust_drop(
|
| 387 |
+
body: SimulateTrustDropInput,
|
| 388 |
+
current_user: User = Depends(get_current_user),
|
| 389 |
+
db: Session = Depends(get_db),
|
| 390 |
+
):
|
| 391 |
+
"""Demo helper – force trust to a specific score to trigger micro-challenges."""
|
| 392 |
+
session = _get_or_create_demo_session(current_user, db)
|
| 393 |
+
tsm = TrustScoreManager(db)
|
| 394 |
+
current = tsm.get(session)
|
| 395 |
+
delta = body.target_score - current
|
| 396 |
+
new_score = tsm.update(session, delta, "context_change", body.reason, {})
|
| 397 |
+
|
| 398 |
+
mce = MicroChallengeEngine()
|
| 399 |
+
return {
|
| 400 |
+
"previous_trust": round(current, 2),
|
| 401 |
+
"new_trust": round(new_score, 2),
|
| 402 |
+
"delta": round(delta, 2),
|
| 403 |
+
"trust_label": TrustScoreManager.label(new_score),
|
| 404 |
+
"trust_color": TrustScoreManager.color(new_score),
|
| 405 |
+
"challenge_recommended": mce.should_challenge(new_score),
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
+
|
| 409 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 410 |
+
# Demo / public endpoints (no auth required — for UI demos)
|
| 411 |
+
# ═══════════════════════════════════════════════════════════════════════════
|
| 412 |
+
|
| 413 |
+
@router.post("/demo/impossible-travel")
|
| 414 |
+
async def demo_impossible_travel(body: TravelCheckInput):
|
| 415 |
+
"""
|
| 416 |
+
Feature 7 – Impossible Travel demo (no auth required).
|
| 417 |
+
Calculates haversine distance + velocity between two cities.
|
| 418 |
+
"""
|
| 419 |
+
result = _resolve_demo_travel(body)
|
| 420 |
+
return result
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
@router.post("/demo/anomaly-score")
|
| 424 |
+
async def demo_anomaly_score(features: AnomalyScoreInput):
|
| 425 |
+
"""
|
| 426 |
+
Feature 6 – AI Anomaly Scoring demo (no auth required).
|
| 427 |
+
Returns statistical isolation-forest anomaly score for the given feature vector.
|
| 428 |
+
"""
|
| 429 |
+
detector = StatisticalAnomalyDetector()
|
| 430 |
+
return detector.score(features.model_dump())
|
| 431 |
+
|
| 432 |
+
|
| 433 |
+
@router.post("/demo/explain")
|
| 434 |
+
async def demo_explain(body: ExplainInput):
|
| 435 |
+
"""
|
| 436 |
+
Feature 5 – Explainability demo (no auth required).
|
| 437 |
+
Submit raw factor scores and get a structured audit report.
|
| 438 |
+
"""
|
| 439 |
+
explainer = RiskExplainer()
|
| 440 |
+
return explainer.explain_login(
|
| 441 |
+
risk_factors={
|
| 442 |
+
"location": body.location_score,
|
| 443 |
+
"device": body.device_score,
|
| 444 |
+
"time": body.time_score,
|
| 445 |
+
"velocity": body.velocity_score,
|
| 446 |
+
"behavior": body.behavior_score,
|
| 447 |
+
},
|
| 448 |
+
risk_level=body.risk_level,
|
| 449 |
+
security_level=body.security_level,
|
| 450 |
+
)
|
| 451 |
+
|
| 452 |
+
|
| 453 |
+
@router.get("/demo/city-list")
|
| 454 |
+
async def demo_city_list():
|
| 455 |
+
"""Return list of cities known to the impossible travel detector."""
|
| 456 |
+
return {
|
| 457 |
+
"cities": [
|
| 458 |
+
{"name": k.title(), "lat": v[0], "lon": v[1]}
|
| 459 |
+
for k, v in sorted(CITY_COORDS.items())
|
| 460 |
+
]
|
| 461 |
+
}
|
openapi_temp.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"openapi":"3.1.0","info":{"title":"AdaptiveAuth Framework Live Test Application","description":"Interactive demonstration of all AdaptiveAuth features","version":"1.0.0"},"paths":{"/auth/auth/register":{"post":{"tags":["Authentication"],"summary":"Register","description":"Register a new user.","operationId":"register_auth_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/login":{"post":{"tags":["Authentication"],"summary":"Login","description":"Standard OAuth2 login endpoint.\nFor risk-based login, use /auth/adaptive-login.","operationId":"login_auth_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/adaptive-login":{"post":{"tags":["Authentication"],"summary":"Adaptive Login","description":"Risk-based adaptive login.\nReturns detailed risk assessment and may require step-up authentication.","operationId":"adaptive_login_auth_auth_adaptive_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/step-up":{"post":{"tags":["Authentication"],"summary":"Step Up Verification","description":"Complete step-up authentication challenge.","operationId":"step_up_verification_auth_auth_step_up_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/login-otp":{"post":{"tags":["Authentication"],"summary":"Login With Otp","description":"Login using TOTP code only (for 2FA-enabled users).","operationId":"login_with_otp_auth_auth_login_otp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginOTP"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Logout current user.","operationId":"logout_auth_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Forgot Password","description":"Request password reset email.","operationId":"forgot_password_auth_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Reset Password","description":"Reset password with token.","operationId":"reset_password_auth_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirm"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/auth/enable-2fa":{"post":{"tags":["Authentication"],"summary":"Enable 2Fa","description":"Enable 2FA for current user.","operationId":"enable_2fa_auth_auth_enable_2fa_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Enable2FAResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/verify-2fa":{"post":{"tags":["Authentication"],"summary":"Verify 2Fa","description":"Verify and activate 2FA.","operationId":"verify_2fa_auth_auth_verify_2fa_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verify2FARequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/auth/disable-2fa":{"post":{"tags":["Authentication"],"summary":"Disable 2Fa","description":"Disable 2FA for current user.","operationId":"disable_2fa_auth_auth_disable_2fa_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"password","in":"query","required":true,"schema":{"type":"string","title":"Password"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user/profile":{"get":{"tags":["User"],"summary":"Get Profile","description":"Get current user's profile.","operationId":"get_profile_auth_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["User"],"summary":"Update Profile","description":"Update current user's profile.","operationId":"update_profile_auth_user_profile_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/security":{"get":{"tags":["User"],"summary":"Get Security Settings","description":"Get user's security settings.","operationId":"get_security_settings_auth_user_security_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSecuritySettings"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/change-password":{"post":{"tags":["User"],"summary":"Change Password","description":"Change user's password.","operationId":"change_password_auth_user_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChange"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/devices":{"get":{"tags":["User"],"summary":"Get Devices","description":"Get user's known devices.","operationId":"get_devices_auth_user_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/devices/{device_id}":{"delete":{"tags":["User"],"summary":"Remove Device","description":"Remove a known device.","operationId":"remove_device_auth_user_devices__device_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/user/sessions":{"get":{"tags":["User"],"summary":"Get Sessions","description":"Get user's active sessions.","operationId":"get_sessions_auth_user_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/sessions/revoke":{"post":{"tags":["User"],"summary":"Revoke Sessions","description":"Revoke user sessions.","operationId":"revoke_sessions_auth_user_sessions_revoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/user/risk-profile":{"get":{"tags":["User"],"summary":"Get Risk Profile","description":"Get user's risk profile summary.","operationId":"get_risk_profile_auth_user_risk_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","description":"List all users (admin only).","operationId":"list_users_auth_admin_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"}},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}":{"get":{"tags":["Admin"],"summary":"Get User","description":"Get user details (admin only).","operationId":"get_user_auth_admin_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}/block":{"post":{"tags":["Admin"],"summary":"Block User","description":"Block a user (admin only).","operationId":"block_user_auth_admin_users__user_id__block_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}},{"name":"duration_hours","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/users/{user_id}/unblock":{"post":{"tags":["Admin"],"summary":"Unblock User","description":"Unblock a user (admin only).","operationId":"unblock_user_auth_admin_users__user_id__unblock_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/sessions":{"get":{"tags":["Admin"],"summary":"List Sessions","description":"List all active sessions (admin only).","operationId":"list_sessions_auth_admin_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status_filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Filter"}},{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/sessions/{session_id}/revoke":{"post":{"tags":["Admin"],"summary":"Revoke Session","description":"Revoke a specific session (admin only).","operationId":"revoke_session_auth_admin_sessions__session_id__revoke_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"integer","title":"Session Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/risk-events":{"get":{"tags":["Admin"],"summary":"List Risk Events","description":"List risk events (admin only).","operationId":"list_risk_events_auth_admin_risk_events_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Type"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskEventList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/anomalies":{"get":{"tags":["Admin"],"summary":"List Anomalies","description":"List detected anomaly patterns (admin only).","operationId":"list_anomalies_auth_admin_anomalies_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnomalyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/anomalies/{anomaly_id}/resolve":{"post":{"tags":["Admin"],"summary":"Resolve Anomaly","description":"Resolve an anomaly pattern (admin only).","operationId":"resolve_anomaly_auth_admin_anomalies__anomaly_id__resolve_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"anomaly_id","in":"path","required":true,"schema":{"type":"integer","title":"Anomaly Id"}},{"name":"false_positive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"False Positive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/admin/statistics":{"get":{"tags":["Admin"],"summary":"Get Statistics","description":"Get admin dashboard statistics.","operationId":"get_statistics_auth_admin_statistics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStatistics"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/admin/risk-statistics":{"get":{"tags":["Admin"],"summary":"Get Risk Statistics","description":"Get risk statistics for a period.","operationId":"get_risk_statistics_auth_admin_risk_statistics_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(day|week|month)$","default":"day","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskStatistics"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/overview":{"get":{"tags":["Risk Dashboard"],"summary":"Get Risk Overview","description":"Get risk dashboard overview.","operationId":"get_risk_overview_auth_risk_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskDashboardOverview"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/risk/assess":{"post":{"tags":["Risk Dashboard"],"summary":"Assess Risk","description":"Manually assess risk for a context or user.","operationId":"assess_risk_auth_risk_assess_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/profile/{user_id}":{"get":{"tags":["Risk Dashboard"],"summary":"Get User Risk Profile","description":"Get detailed risk profile for a user.","operationId":"get_user_risk_profile_auth_risk_profile__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/active-sessions":{"get":{"tags":["Risk Dashboard"],"summary":"Get High Risk Sessions","description":"Get sessions with elevated risk levels.","operationId":"get_high_risk_sessions_auth_risk_active_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"min_risk_level","in":"query","required":false,"schema":{"type":"string","pattern":"^(low|medium|high|critical)$","default":"medium","title":"Min Risk Level"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/login-patterns":{"get":{"tags":["Risk Dashboard"],"summary":"Get Login Patterns","description":"Get login patterns analysis.","operationId":"get_login_patterns_auth_risk_login_patterns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/risk/suspicious-ips":{"get":{"tags":["Risk Dashboard"],"summary":"Get Suspicious Ips","description":"Get IPs with suspicious activity.","operationId":"get_suspicious_ips_auth_risk_suspicious_ips_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/risk/block-ip":{"post":{"tags":["Risk Dashboard"],"summary":"Block Ip","description":"Block an IP address (creates anomaly pattern).","operationId":"block_ip_auth_risk_block_ip_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"ip_address","in":"query","required":true,"schema":{"type":"string","title":"Ip Address"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Suspicious activity","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/adaptive/assess":{"post":{"tags":["Adaptive Authentication"],"summary":"Assess Current Risk","description":"Assess current risk level for authenticated user.","operationId":"assess_current_risk_auth_adaptive_assess_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskAssessmentResult"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/verify-session":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Session","description":"Verify current session is still valid and not compromised.\nUse this periodically during sensitive operations.","operationId":"verify_session_auth_adaptive_verify_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/challenge":{"post":{"tags":["Adaptive Authentication"],"summary":"Request Challenge","description":"Request a new authentication challenge for step-up auth.","operationId":"request_challenge_auth_adaptive_challenge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/verify":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Challenge","description":"Verify a step-up authentication challenge.","operationId":"verify_challenge_auth_adaptive_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/security-status":{"get":{"tags":["Adaptive Authentication"],"summary":"Get Security Status","description":"Get current security status for the user.","operationId":"get_security_status_auth_adaptive_security_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/trust-device":{"post":{"tags":["Adaptive Authentication"],"summary":"Trust Current Device","description":"Mark current device as trusted.","operationId":"trust_current_device_auth_adaptive_trust_device_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/adaptive/trust-device/{device_index}":{"delete":{"tags":["Adaptive Authentication"],"summary":"Remove Trusted Device","description":"Remove a device from trusted devices.","operationId":"remove_trusted_device_auth_adaptive_trust_device__device_index__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_index","in":"path","required":true,"schema":{"type":"integer","title":"Device Index"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test-interface":{"get":{"summary":"Test Interface","description":"Serve the test interface","operationId":"test_interface_test_interface_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/protected":{"get":{"summary":"Protected Endpoint","description":"Protected endpoint that requires authentication","operationId":"protected_endpoint_protected_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin-only":{"get":{"summary":"Admin Only Endpoint","description":"Admin-only endpoint that requires admin role","operationId":"admin_only_endpoint_admin_only_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/demo/features":{"get":{"summary":"Demo Features","description":"Demonstrate all framework features","operationId":"demo_features_demo_features_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test/register":{"post":{"summary":"Test Register","description":"Test endpoint for user registration","operationId":"test_register_test_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/login":{"post":{"summary":"Test Login","description":"Test endpoint for user login","operationId":"test_login_test_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/create-user":{"post":{"summary":"Create Test User","description":"Create a test user programmatically","operationId":"create_test_user_test_create_user_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_create_test_user_test_create_user_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdaptiveLoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"device_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Fingerprint"},"remember_device":{"type":"boolean","title":"Remember Device","default":false}},"type":"object","required":["email","password"],"title":"AdaptiveLoginRequest","description":"Adaptive login request with context."},"AdaptiveLoginResponse":{"properties":{"status":{"type":"string","title":"Status"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","title":"Security Level"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"challenge_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Type"},"challenge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Id"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["status","risk_level","security_level"],"title":"AdaptiveLoginResponse","description":"Adaptive login response."},"AdminStatistics":{"properties":{"total_users":{"type":"integer","title":"Total Users"},"active_users":{"type":"integer","title":"Active Users"},"blocked_users":{"type":"integer","title":"Blocked Users"},"active_sessions":{"type":"integer","title":"Active Sessions"},"high_risk_events_today":{"type":"integer","title":"High Risk Events Today"},"failed_logins_today":{"type":"integer","title":"Failed Logins Today"},"new_users_today":{"type":"integer","title":"New Users Today"}},"type":"object","required":["total_users","active_users","blocked_users","active_sessions","high_risk_events_today","failed_logins_today","new_users_today"],"title":"AdminStatistics","description":"Admin dashboard statistics."},"AdminUserList":{"properties":{"users":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Users"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["users","total","page","page_size"],"title":"AdminUserList","description":"Admin user list response."},"AnomalyListResponse":{"properties":{"anomalies":{"items":{"$ref":"#/components/schemas/AnomalyPatternResponse"},"type":"array","title":"Anomalies"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["anomalies","total"],"title":"AnomalyListResponse","description":"List of anomaly patterns."},"AnomalyPatternResponse":{"properties":{"id":{"type":"integer","title":"Id"},"pattern_type":{"type":"string","title":"Pattern Type"},"severity":{"type":"string","title":"Severity"},"confidence":{"type":"number","title":"Confidence"},"is_active":{"type":"boolean","title":"Is Active"},"first_detected":{"type":"string","format":"date-time","title":"First Detected"},"last_detected":{"type":"string","format":"date-time","title":"Last Detected"},"pattern_data":{"additionalProperties":true,"type":"object","title":"Pattern Data"}},"type":"object","required":["id","pattern_type","severity","confidence","is_active","first_detected","last_detected","pattern_data"],"title":"AnomalyPatternResponse","description":"Detected anomaly pattern."},"Body_create_test_user_test_create_user_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"type":"string","title":"Full Name"},"role":{"type":"string","title":"Role","default":"user"}},"type":"object","required":["email","password"],"title":"Body_create_test_user_test_create_user_post"},"Body_login_auth_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_auth_login_post"},"ChallengeRequest":{"properties":{"challenge_type":{"type":"string","pattern":"^(otp|email|sms)$","title":"Challenge Type"},"session_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["challenge_type"],"title":"ChallengeRequest","description":"Request a new challenge."},"ChallengeResponse":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"challenge_type":{"type":"string","title":"Challenge Type"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["challenge_id","challenge_type","expires_at","message"],"title":"ChallengeResponse","description":"Challenge created response."},"DeviceInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"browser":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser"},"os":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os"},"first_seen":{"type":"string","format":"date-time","title":"First Seen"},"last_seen":{"type":"string","format":"date-time","title":"Last Seen"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","name","browser","os","first_seen","last_seen"],"title":"DeviceInfo","description":"Known device information."},"DeviceListResponse":{"properties":{"devices":{"items":{"$ref":"#/components/schemas/DeviceInfo"},"type":"array","title":"Devices"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["devices","total"],"title":"DeviceListResponse","description":"List of known devices."},"Enable2FAResponse":{"properties":{"secret":{"type":"string","title":"Secret"},"qr_code":{"type":"string","title":"Qr Code"},"backup_codes":{"items":{"type":"string"},"type":"array","title":"Backup Codes"}},"type":"object","required":["secret","qr_code","backup_codes"],"title":"Enable2FAResponse","description":"Enable 2FA response with QR code."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginOTP":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["email","otp"],"title":"LoginOTP","description":"Login with TOTP code."},"PasswordChange":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["current_password","new_password","confirm_password"],"title":"PasswordChange","description":"Change password (authenticated)."},"PasswordResetConfirm":{"properties":{"reset_token":{"type":"string","title":"Reset Token"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["reset_token","new_password","confirm_password"],"title":"PasswordResetConfirm","description":"Confirm password reset."},"PasswordResetRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"PasswordResetRequest","description":"Request password reset."},"RiskAssessmentResult":{"properties":{"risk_score":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","maximum":4.0,"minimum":0.0,"title":"Security Level"},"risk_factors":{"additionalProperties":{"type":"number"},"type":"object","title":"Risk Factors"},"required_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Required Action"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["risk_score","risk_level","security_level","risk_factors"],"title":"RiskAssessmentResult","description":"Risk assessment result."},"RiskDashboardOverview":{"properties":{"total_risk_events":{"type":"integer","title":"Total Risk Events"},"high_risk_events":{"type":"integer","title":"High Risk Events"},"active_anomalies":{"type":"integer","title":"Active Anomalies"},"blocked_users":{"type":"integer","title":"Blocked Users"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_trend":{"type":"string","title":"Risk Trend"}},"type":"object","required":["total_risk_events","high_risk_events","active_anomalies","blocked_users","average_risk_score","risk_trend"],"title":"RiskDashboardOverview","description":"Risk dashboard overview."},"RiskEventList":{"properties":{"events":{"items":{"$ref":"#/components/schemas/RiskEventResponse"},"type":"array","title":"Events"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["events","total","page","page_size"],"title":"RiskEventList","description":"List of risk events."},"RiskEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"risk_score":{"type":"number","title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"risk_factors":{"additionalProperties":true,"type":"object","title":"Risk Factors"},"action_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Taken"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"resolved":{"type":"boolean","title":"Resolved"}},"type":"object","required":["id","event_type","risk_score","risk_level","ip_address","risk_factors","action_taken","created_at","resolved"],"title":"RiskEventResponse","description":"Risk event information."},"RiskStatistics":{"properties":{"period":{"type":"string","title":"Period"},"total_logins":{"type":"integer","title":"Total Logins"},"successful_logins":{"type":"integer","title":"Successful Logins"},"failed_logins":{"type":"integer","title":"Failed Logins"},"blocked_attempts":{"type":"integer","title":"Blocked Attempts"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_distribution":{"additionalProperties":{"type":"integer"},"type":"object","title":"Risk Distribution"}},"type":"object","required":["period","total_logins","successful_logins","failed_logins","blocked_attempts","average_risk_score","risk_distribution"],"title":"RiskStatistics","description":"Risk statistics."},"SessionInfo":{"properties":{"id":{"type":"integer","title":"Id"},"ip_address":{"type":"string","title":"Ip Address"},"user_agent":{"type":"string","title":"User Agent"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"risk_level":{"type":"string","title":"Risk Level"},"status":{"type":"string","title":"Status"},"last_activity":{"type":"string","format":"date-time","title":"Last Activity"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","ip_address","user_agent","country","city","risk_level","status","last_activity","created_at"],"title":"SessionInfo","description":"Active session information."},"SessionListResponse":{"properties":{"sessions":{"items":{"$ref":"#/components/schemas/SessionInfo"},"type":"array","title":"Sessions"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["sessions","total"],"title":"SessionListResponse","description":"List of user sessions."},"SessionRevokeRequest":{"properties":{"session_ids":{"items":{"type":"integer"},"type":"array","title":"Session Ids"},"revoke_all":{"type":"boolean","title":"Revoke All","default":false}},"type":"object","required":["session_ids"],"title":"SessionRevokeRequest","description":"Request to revoke session(s)."},"StepUpRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"verification_code":{"type":"string","title":"Verification Code"}},"type":"object","required":["challenge_id","verification_code"],"title":"StepUpRequest","description":"Step-up authentication request."},"StepUpResponse":{"properties":{"status":{"type":"string","title":"Status"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["status"],"title":"StepUpResponse","description":"Step-up authentication response."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"expires_in":{"type":"integer","title":"Expires In"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["access_token","expires_in"],"title":"TokenResponse","description":"JWT token response."},"UserLogin":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"UserLogin","description":"Standard login request."},"UserRegister":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":8,"title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"}},"type":"object","required":["email","password"],"title":"UserRegister","description":"User registration request."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","full_name","role","is_active","is_verified","tfa_enabled","created_at"],"title":"UserResponse","description":"User information response."},"UserSecuritySettings":{"properties":{"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"last_password_change":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Password Change"},"active_sessions":{"type":"integer","title":"Active Sessions"},"known_devices":{"type":"integer","title":"Known Devices"},"recent_login_attempts":{"type":"integer","title":"Recent Login Attempts"}},"type":"object","required":["tfa_enabled","last_password_change","active_sessions","known_devices","recent_login_attempts"],"title":"UserSecuritySettings","description":"User security settings response."},"UserUpdate":{"properties":{"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"}},"type":"object","title":"UserUpdate","description":"Update user information."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Verify2FARequest":{"properties":{"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["otp"],"title":"Verify2FARequest","description":"Verify 2FA setup."},"VerifyChallengeRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"code":{"type":"string","title":"Code"}},"type":"object","required":["challenge_id","code"],"title":"VerifyChallengeRequest","description":"Verify challenge code."}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}}
|
|
|
|
|
|
openapi_test.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"openapi":"3.1.0","info":{"title":"AdaptiveAuth Framework Live Test Application","description":"Interactive demonstration of all AdaptiveAuth features","version":"1.0.0"},"paths":{"/auth/register":{"post":{"tags":["Authentication"],"summary":"Register","description":"Register a new user.","operationId":"register_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login":{"post":{"tags":["Authentication"],"summary":"Login","description":"Standard OAuth2 login endpoint.\nFor risk-based login, use /auth/adaptive-login.","operationId":"login_auth_login_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_login_auth_login_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/adaptive-login":{"post":{"tags":["Authentication"],"summary":"Adaptive Login","description":"Risk-based adaptive login.\nReturns detailed risk assessment and may require step-up authentication.","operationId":"adaptive_login_auth_adaptive_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdaptiveLoginResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/step-up":{"post":{"tags":["Authentication"],"summary":"Step Up Verification","description":"Complete step-up authentication challenge.","operationId":"step_up_verification_auth_step_up_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StepUpResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/login-otp":{"post":{"tags":["Authentication"],"summary":"Login With Otp","description":"Login using TOTP code only (for 2FA-enabled users).","operationId":"login_with_otp_auth_login_otp_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginOTP"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/logout":{"post":{"tags":["Authentication"],"summary":"Logout","description":"Logout current user.","operationId":"logout_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/forgot-password":{"post":{"tags":["Authentication"],"summary":"Forgot Password","description":"Request password reset email.","operationId":"forgot_password_auth_forgot_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/reset-password":{"post":{"tags":["Authentication"],"summary":"Reset Password","description":"Reset password with token.","operationId":"reset_password_auth_reset_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordResetConfirm"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/enable-2fa":{"post":{"tags":["Authentication"],"summary":"Enable 2Fa","description":"Enable 2FA for current user.","operationId":"enable_2fa_auth_enable_2fa_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Enable2FAResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/verify-2fa":{"post":{"tags":["Authentication"],"summary":"Verify 2Fa","description":"Verify and activate 2FA.","operationId":"verify_2fa_auth_verify_2fa_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Verify2FARequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/auth/disable-2fa":{"post":{"tags":["Authentication"],"summary":"Disable 2Fa","description":"Disable 2FA for current user.","operationId":"disable_2fa_auth_disable_2fa_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"password","in":"query","required":true,"schema":{"type":"string","title":"Password"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/profile":{"get":{"tags":["User"],"summary":"Get Profile","description":"Get current user's profile.","operationId":"get_profile_user_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]},"put":{"tags":["User"],"summary":"Update Profile","description":"Update current user's profile.","operationId":"update_profile_user_profile_put","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/security":{"get":{"tags":["User"],"summary":"Get Security Settings","description":"Get user's security settings.","operationId":"get_security_settings_user_security_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserSecuritySettings"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/change-password":{"post":{"tags":["User"],"summary":"Change Password","description":"Change user's password.","operationId":"change_password_user_change_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordChange"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/devices":{"get":{"tags":["User"],"summary":"Get Devices","description":"Get user's known devices.","operationId":"get_devices_user_devices_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/devices/{device_id}":{"delete":{"tags":["User"],"summary":"Remove Device","description":"Remove a known device.","operationId":"remove_device_user_devices__device_id__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_id","in":"path","required":true,"schema":{"type":"string","title":"Device Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/user/sessions":{"get":{"tags":["User"],"summary":"Get Sessions","description":"Get user's active sessions.","operationId":"get_sessions_user_sessions_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/sessions/revoke":{"post":{"tags":["User"],"summary":"Revoke Sessions","description":"Revoke user sessions.","operationId":"revoke_sessions_user_sessions_revoke_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionRevokeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/user/risk-profile":{"get":{"tags":["User"],"summary":"Get Risk Profile","description":"Get user's risk profile summary.","operationId":"get_risk_profile_user_risk_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/users":{"get":{"tags":["Admin"],"summary":"List Users","description":"List all users (admin only).","operationId":"list_users_admin_users_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}},{"name":"role","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Role"}},{"name":"is_active","in":"query","required":false,"schema":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Active"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminUserList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}":{"get":{"tags":["Admin"],"summary":"Get User","description":"Get user details (admin only).","operationId":"get_user_admin_users__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/block":{"post":{"tags":["Admin"],"summary":"Block User","description":"Block a user (admin only).","operationId":"block_user_admin_users__user_id__block_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}},{"name":"duration_hours","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Duration Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/users/{user_id}/unblock":{"post":{"tags":["Admin"],"summary":"Unblock User","description":"Unblock a user (admin only).","operationId":"unblock_user_admin_users__user_id__unblock_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions":{"get":{"tags":["Admin"],"summary":"List Sessions","description":"List all active sessions (admin only).","operationId":"list_sessions_admin_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"status_filter","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Filter"}},{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/sessions/{session_id}/revoke":{"post":{"tags":["Admin"],"summary":"Revoke Session","description":"Revoke a specific session (admin only).","operationId":"revoke_session_admin_sessions__session_id__revoke_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"session_id","in":"path","required":true,"schema":{"type":"integer","title":"Session Id"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Administrative action","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/risk-events":{"get":{"tags":["Admin"],"summary":"List Risk Events","description":"List risk events (admin only).","operationId":"list_risk_events_admin_risk_events_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"risk_level","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Risk Level"}},{"name":"event_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event Type"}},{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"default":1,"title":"Page"}},{"name":"page_size","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":20,"title":"Page Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskEventList"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/anomalies":{"get":{"tags":["Admin"],"summary":"List Anomalies","description":"List detected anomaly patterns (admin only).","operationId":"list_anomalies_admin_anomalies_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"active_only","in":"query","required":false,"schema":{"type":"boolean","default":true,"title":"Active Only"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnomalyListResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/anomalies/{anomaly_id}/resolve":{"post":{"tags":["Admin"],"summary":"Resolve Anomaly","description":"Resolve an anomaly pattern (admin only).","operationId":"resolve_anomaly_admin_anomalies__anomaly_id__resolve_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"anomaly_id","in":"path","required":true,"schema":{"type":"integer","title":"Anomaly Id"}},{"name":"false_positive","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"False Positive"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin/statistics":{"get":{"tags":["Admin"],"summary":"Get Statistics","description":"Get admin dashboard statistics.","operationId":"get_statistics_admin_statistics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminStatistics"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin/risk-statistics":{"get":{"tags":["Admin"],"summary":"Get Risk Statistics","description":"Get risk statistics for a period.","operationId":"get_risk_statistics_admin_risk_statistics_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"period","in":"query","required":false,"schema":{"type":"string","pattern":"^(day|week|month)$","default":"day","title":"Period"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskStatistics"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/overview":{"get":{"tags":["Risk Dashboard"],"summary":"Get Risk Overview","description":"Get risk dashboard overview.","operationId":"get_risk_overview_risk_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskDashboardOverview"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/risk/assess":{"post":{"tags":["Risk Dashboard"],"summary":"Assess Risk","description":"Manually assess risk for a context or user.","operationId":"assess_risk_risk_assess_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/profile/{user_id}":{"get":{"tags":["Risk Dashboard"],"summary":"Get User Risk Profile","description":"Get detailed risk profile for a user.","operationId":"get_user_risk_profile_risk_profile__user_id__get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"integer","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/active-sessions":{"get":{"tags":["Risk Dashboard"],"summary":"Get High Risk Sessions","description":"Get sessions with elevated risk levels.","operationId":"get_high_risk_sessions_risk_active_sessions_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"min_risk_level","in":"query","required":false,"schema":{"type":"string","pattern":"^(low|medium|high|critical)$","default":"medium","title":"Min Risk Level"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/login-patterns":{"get":{"tags":["Risk Dashboard"],"summary":"Get Login Patterns","description":"Get login patterns analysis.","operationId":"get_login_patterns_risk_login_patterns_get","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/risk/suspicious-ips":{"get":{"tags":["Risk Dashboard"],"summary":"Get Suspicious Ips","description":"Get IPs with suspicious activity.","operationId":"get_suspicious_ips_risk_suspicious_ips_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/risk/block-ip":{"post":{"tags":["Risk Dashboard"],"summary":"Block Ip","description":"Block an IP address (creates anomaly pattern).","operationId":"block_ip_risk_block_ip_post","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"ip_address","in":"query","required":true,"schema":{"type":"string","title":"Ip Address"}},{"name":"reason","in":"query","required":false,"schema":{"type":"string","default":"Suspicious activity","title":"Reason"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/adaptive/assess":{"post":{"tags":["Adaptive Authentication"],"summary":"Assess Current Risk","description":"Assess current risk level for authenticated user.","operationId":"assess_current_risk_adaptive_assess_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RiskAssessmentResult"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/verify-session":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Session","description":"Verify current session is still valid and not compromised.\nUse this periodically during sensitive operations.","operationId":"verify_session_adaptive_verify_session_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/challenge":{"post":{"tags":["Adaptive Authentication"],"summary":"Request Challenge","description":"Request a new authentication challenge for step-up auth.","operationId":"request_challenge_adaptive_challenge_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChallengeResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/verify":{"post":{"tags":["Adaptive Authentication"],"summary":"Verify Challenge","description":"Verify a step-up authentication challenge.","operationId":"verify_challenge_adaptive_verify_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyChallengeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/security-status":{"get":{"tags":["Adaptive Authentication"],"summary":"Get Security Status","description":"Get current security status for the user.","operationId":"get_security_status_adaptive_security_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/trust-device":{"post":{"tags":["Adaptive Authentication"],"summary":"Trust Current Device","description":"Mark current device as trusted.","operationId":"trust_current_device_adaptive_trust_device_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/adaptive/trust-device/{device_index}":{"delete":{"tags":["Adaptive Authentication"],"summary":"Remove Trusted Device","description":"Remove a device from trusted devices.","operationId":"remove_trusted_device_adaptive_trust_device__device_index__delete","security":[{"OAuth2PasswordBearer":[]}],"parameters":[{"name":"device_index","in":"path","required":true,"schema":{"type":"integer","title":"Device Index"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/":{"get":{"summary":"Root","operationId":"root__get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test-interface":{"get":{"summary":"Test Interface","description":"Serve the test interface","operationId":"test_interface_test_interface_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/protected":{"get":{"summary":"Protected Endpoint","description":"Protected endpoint that requires authentication","operationId":"protected_endpoint_protected_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}},"security":[{"OAuth2PasswordBearer":[]}]}},"/admin-only":{"get":{"summary":"Admin Only Endpoint","description":"Admin-only endpoint that requires admin role","operationId":"admin_only_endpoint_admin_only_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/health":{"get":{"summary":"Health Check","description":"Health check endpoint","operationId":"health_check_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/demo/features":{"get":{"summary":"Demo Features","description":"Demonstrate all framework features","operationId":"demo_features_demo_features_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/test/register":{"post":{"summary":"Test Register","description":"Test endpoint for user registration","operationId":"test_register_test_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRegister"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/login":{"post":{"summary":"Test Login","description":"Test endpoint for user login","operationId":"test_login_test_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserLogin"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/test/create-user":{"post":{"summary":"Create Test User","description":"Create a test user programmatically","operationId":"create_test_user_test_create_user_post","requestBody":{"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/Body_create_test_user_test_create_user_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AdaptiveLoginRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"},"device_fingerprint":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Device Fingerprint"},"remember_device":{"type":"boolean","title":"Remember Device","default":false}},"type":"object","required":["email","password"],"title":"AdaptiveLoginRequest","description":"Adaptive login request with context."},"AdaptiveLoginResponse":{"properties":{"status":{"type":"string","title":"Status"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","title":"Security Level"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"challenge_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Type"},"challenge_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Challenge Id"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["status","risk_level","security_level"],"title":"AdaptiveLoginResponse","description":"Adaptive login response."},"AdminStatistics":{"properties":{"total_users":{"type":"integer","title":"Total Users"},"active_users":{"type":"integer","title":"Active Users"},"blocked_users":{"type":"integer","title":"Blocked Users"},"active_sessions":{"type":"integer","title":"Active Sessions"},"high_risk_events_today":{"type":"integer","title":"High Risk Events Today"},"failed_logins_today":{"type":"integer","title":"Failed Logins Today"},"new_users_today":{"type":"integer","title":"New Users Today"}},"type":"object","required":["total_users","active_users","blocked_users","active_sessions","high_risk_events_today","failed_logins_today","new_users_today"],"title":"AdminStatistics","description":"Admin dashboard statistics."},"AdminUserList":{"properties":{"users":{"items":{"$ref":"#/components/schemas/UserResponse"},"type":"array","title":"Users"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["users","total","page","page_size"],"title":"AdminUserList","description":"Admin user list response."},"AnomalyListResponse":{"properties":{"anomalies":{"items":{"$ref":"#/components/schemas/AnomalyPatternResponse"},"type":"array","title":"Anomalies"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["anomalies","total"],"title":"AnomalyListResponse","description":"List of anomaly patterns."},"AnomalyPatternResponse":{"properties":{"id":{"type":"integer","title":"Id"},"pattern_type":{"type":"string","title":"Pattern Type"},"severity":{"type":"string","title":"Severity"},"confidence":{"type":"number","title":"Confidence"},"is_active":{"type":"boolean","title":"Is Active"},"first_detected":{"type":"string","format":"date-time","title":"First Detected"},"last_detected":{"type":"string","format":"date-time","title":"Last Detected"},"pattern_data":{"additionalProperties":true,"type":"object","title":"Pattern Data"}},"type":"object","required":["id","pattern_type","severity","confidence","is_active","first_detected","last_detected","pattern_data"],"title":"AnomalyPatternResponse","description":"Detected anomaly pattern."},"Body_create_test_user_test_create_user_post":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"},"full_name":{"type":"string","title":"Full Name"},"role":{"type":"string","title":"Role","default":"user"}},"type":"object","required":["email","password"],"title":"Body_create_test_user_test_create_user_post"},"Body_login_auth_login_post":{"properties":{"grant_type":{"anyOf":[{"type":"string","pattern":"^password$"},{"type":"null"}],"title":"Grant Type"},"username":{"type":"string","title":"Username"},"password":{"type":"string","format":"password","title":"Password"},"scope":{"type":"string","title":"Scope","default":""},"client_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"},"client_secret":{"anyOf":[{"type":"string"},{"type":"null"}],"format":"password","title":"Client Secret"}},"type":"object","required":["username","password"],"title":"Body_login_auth_login_post"},"ChallengeRequest":{"properties":{"challenge_type":{"type":"string","pattern":"^(otp|email|sms)$","title":"Challenge Type"},"session_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Session Id"}},"type":"object","required":["challenge_type"],"title":"ChallengeRequest","description":"Request a new challenge."},"ChallengeResponse":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"challenge_type":{"type":"string","title":"Challenge Type"},"expires_at":{"type":"string","format":"date-time","title":"Expires At"},"message":{"type":"string","title":"Message"}},"type":"object","required":["challenge_id","challenge_type","expires_at","message"],"title":"ChallengeResponse","description":"Challenge created response."},"DeviceInfo":{"properties":{"id":{"type":"string","title":"Id"},"name":{"type":"string","title":"Name"},"browser":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser"},"os":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Os"},"first_seen":{"type":"string","format":"date-time","title":"First Seen"},"last_seen":{"type":"string","format":"date-time","title":"Last Seen"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","name","browser","os","first_seen","last_seen"],"title":"DeviceInfo","description":"Known device information."},"DeviceListResponse":{"properties":{"devices":{"items":{"$ref":"#/components/schemas/DeviceInfo"},"type":"array","title":"Devices"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["devices","total"],"title":"DeviceListResponse","description":"List of known devices."},"Enable2FAResponse":{"properties":{"secret":{"type":"string","title":"Secret"},"qr_code":{"type":"string","title":"Qr Code"},"backup_codes":{"items":{"type":"string"},"type":"array","title":"Backup Codes"}},"type":"object","required":["secret","qr_code","backup_codes"],"title":"Enable2FAResponse","description":"Enable 2FA response with QR code."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LoginOTP":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["email","otp"],"title":"LoginOTP","description":"Login with TOTP code."},"PasswordChange":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["current_password","new_password","confirm_password"],"title":"PasswordChange","description":"Change password (authenticated)."},"PasswordResetConfirm":{"properties":{"reset_token":{"type":"string","title":"Reset Token"},"new_password":{"type":"string","minLength":8,"title":"New Password"},"confirm_password":{"type":"string","title":"Confirm Password"}},"type":"object","required":["reset_token","new_password","confirm_password"],"title":"PasswordResetConfirm","description":"Confirm password reset."},"PasswordResetRequest":{"properties":{"email":{"type":"string","format":"email","title":"Email"}},"type":"object","required":["email"],"title":"PasswordResetRequest","description":"Request password reset."},"RiskAssessmentResult":{"properties":{"risk_score":{"type":"number","maximum":100.0,"minimum":0.0,"title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"security_level":{"type":"integer","maximum":4.0,"minimum":0.0,"title":"Security Level"},"risk_factors":{"additionalProperties":{"type":"number"},"type":"object","title":"Risk Factors"},"required_action":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Required Action"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["risk_score","risk_level","security_level","risk_factors"],"title":"RiskAssessmentResult","description":"Risk assessment result."},"RiskDashboardOverview":{"properties":{"total_risk_events":{"type":"integer","title":"Total Risk Events"},"high_risk_events":{"type":"integer","title":"High Risk Events"},"active_anomalies":{"type":"integer","title":"Active Anomalies"},"blocked_users":{"type":"integer","title":"Blocked Users"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_trend":{"type":"string","title":"Risk Trend"}},"type":"object","required":["total_risk_events","high_risk_events","active_anomalies","blocked_users","average_risk_score","risk_trend"],"title":"RiskDashboardOverview","description":"Risk dashboard overview."},"RiskEventList":{"properties":{"events":{"items":{"$ref":"#/components/schemas/RiskEventResponse"},"type":"array","title":"Events"},"total":{"type":"integer","title":"Total"},"page":{"type":"integer","title":"Page"},"page_size":{"type":"integer","title":"Page Size"}},"type":"object","required":["events","total","page","page_size"],"title":"RiskEventList","description":"List of risk events."},"RiskEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"event_type":{"type":"string","title":"Event Type"},"risk_score":{"type":"number","title":"Risk Score"},"risk_level":{"type":"string","title":"Risk Level"},"ip_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ip Address"},"risk_factors":{"additionalProperties":true,"type":"object","title":"Risk Factors"},"action_taken":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Action Taken"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"resolved":{"type":"boolean","title":"Resolved"}},"type":"object","required":["id","event_type","risk_score","risk_level","ip_address","risk_factors","action_taken","created_at","resolved"],"title":"RiskEventResponse","description":"Risk event information."},"RiskStatistics":{"properties":{"period":{"type":"string","title":"Period"},"total_logins":{"type":"integer","title":"Total Logins"},"successful_logins":{"type":"integer","title":"Successful Logins"},"failed_logins":{"type":"integer","title":"Failed Logins"},"blocked_attempts":{"type":"integer","title":"Blocked Attempts"},"average_risk_score":{"type":"number","title":"Average Risk Score"},"risk_distribution":{"additionalProperties":{"type":"integer"},"type":"object","title":"Risk Distribution"}},"type":"object","required":["period","total_logins","successful_logins","failed_logins","blocked_attempts","average_risk_score","risk_distribution"],"title":"RiskStatistics","description":"Risk statistics."},"SessionInfo":{"properties":{"id":{"type":"integer","title":"Id"},"ip_address":{"type":"string","title":"Ip Address"},"user_agent":{"type":"string","title":"User Agent"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"risk_level":{"type":"string","title":"Risk Level"},"status":{"type":"string","title":"Status"},"last_activity":{"type":"string","format":"date-time","title":"Last Activity"},"created_at":{"type":"string","format":"date-time","title":"Created At"},"is_current":{"type":"boolean","title":"Is Current","default":false}},"type":"object","required":["id","ip_address","user_agent","country","city","risk_level","status","last_activity","created_at"],"title":"SessionInfo","description":"Active session information."},"SessionListResponse":{"properties":{"sessions":{"items":{"$ref":"#/components/schemas/SessionInfo"},"type":"array","title":"Sessions"},"total":{"type":"integer","title":"Total"}},"type":"object","required":["sessions","total"],"title":"SessionListResponse","description":"List of user sessions."},"SessionRevokeRequest":{"properties":{"session_ids":{"items":{"type":"integer"},"type":"array","title":"Session Ids"},"revoke_all":{"type":"boolean","title":"Revoke All","default":false}},"type":"object","required":["session_ids"],"title":"SessionRevokeRequest","description":"Request to revoke session(s)."},"StepUpRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"verification_code":{"type":"string","title":"Verification Code"}},"type":"object","required":["challenge_id","verification_code"],"title":"StepUpRequest","description":"Step-up authentication request."},"StepUpResponse":{"properties":{"status":{"type":"string","title":"Status"},"access_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Access Token"},"token_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token Type","default":"bearer"},"message":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Message"}},"type":"object","required":["status"],"title":"StepUpResponse","description":"Step-up authentication response."},"TokenResponse":{"properties":{"access_token":{"type":"string","title":"Access Token"},"token_type":{"type":"string","title":"Token Type","default":"bearer"},"expires_in":{"type":"integer","title":"Expires In"},"user_info":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"User Info"}},"type":"object","required":["access_token","expires_in"],"title":"TokenResponse","description":"JWT token response."},"UserLogin":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"UserLogin","description":"Standard login request."},"UserRegister":{"properties":{"email":{"type":"string","format":"email","title":"Email"},"password":{"type":"string","minLength":8,"title":"Password"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"}},"type":"object","required":["email","password"],"title":"UserRegister","description":"User registration request."},"UserResponse":{"properties":{"id":{"type":"integer","title":"Id"},"email":{"type":"string","title":"Email"},"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"role":{"type":"string","title":"Role"},"is_active":{"type":"boolean","title":"Is Active"},"is_verified":{"type":"boolean","title":"Is Verified"},"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"created_at":{"type":"string","format":"date-time","title":"Created At"}},"type":"object","required":["id","email","full_name","role","is_active","is_verified","tfa_enabled","created_at"],"title":"UserResponse","description":"User information response."},"UserSecuritySettings":{"properties":{"tfa_enabled":{"type":"boolean","title":"Tfa Enabled"},"last_password_change":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Password Change"},"active_sessions":{"type":"integer","title":"Active Sessions"},"known_devices":{"type":"integer","title":"Known Devices"},"recent_login_attempts":{"type":"integer","title":"Recent Login Attempts"}},"type":"object","required":["tfa_enabled","last_password_change","active_sessions","known_devices","recent_login_attempts"],"title":"UserSecuritySettings","description":"User security settings response."},"UserUpdate":{"properties":{"full_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Full Name"},"email":{"anyOf":[{"type":"string","format":"email"},{"type":"null"}],"title":"Email"}},"type":"object","title":"UserUpdate","description":"Update user information."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"Verify2FARequest":{"properties":{"otp":{"type":"string","maxLength":6,"minLength":6,"title":"Otp"}},"type":"object","required":["otp"],"title":"Verify2FARequest","description":"Verify 2FA setup."},"VerifyChallengeRequest":{"properties":{"challenge_id":{"type":"string","title":"Challenge Id"},"code":{"type":"string","title":"Code"}},"type":"object","required":["challenge_id","code"],"title":"VerifyChallengeRequest","description":"Verify challenge code."}},"securitySchemes":{"OAuth2PasswordBearer":{"type":"oauth2","flows":{"password":{"scopes":{},"tokenUrl":"auth/login"}}}}}}
|
|
|
|
|
|
static/index.html
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|