399 lines
14 KiB
Python
399 lines
14 KiB
Python
|
|
"""
|
||
|
|
Tikker ML Analytics Module
|
||
|
|
|
||
|
|
Provides machine learning-based pattern detection, anomaly detection,
|
||
|
|
and behavioral analysis for keystroke data.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import sqlite3
|
||
|
|
from typing import Dict, List, Any, Tuple, Optional
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from datetime import datetime, timedelta
|
||
|
|
import logging
|
||
|
|
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Pattern:
|
||
|
|
"""Detected keystroke pattern."""
|
||
|
|
name: str
|
||
|
|
confidence: float
|
||
|
|
frequency: int
|
||
|
|
description: str
|
||
|
|
features: Dict[str, Any]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Anomaly:
|
||
|
|
"""Detected anomaly in keystroke behavior."""
|
||
|
|
timestamp: str
|
||
|
|
anomaly_type: str
|
||
|
|
severity: float # 0.0 to 1.0
|
||
|
|
reason: str
|
||
|
|
expected_value: float
|
||
|
|
actual_value: float
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class BehavioralProfile:
|
||
|
|
"""User behavioral profile based on keystroke patterns."""
|
||
|
|
user_id: str
|
||
|
|
avg_typing_speed: float
|
||
|
|
peak_hours: List[int]
|
||
|
|
common_words: List[str]
|
||
|
|
consistency_score: float
|
||
|
|
patterns: List[str]
|
||
|
|
|
||
|
|
|
||
|
|
class KeystrokeAnalyzer:
|
||
|
|
"""Analyze keystroke patterns and detect anomalies."""
|
||
|
|
|
||
|
|
def __init__(self, db_path: str = "tikker.db"):
|
||
|
|
self.db_path = db_path
|
||
|
|
self.patterns = {}
|
||
|
|
self.baseline_stats = {}
|
||
|
|
|
||
|
|
def _get_connection(self) -> sqlite3.Connection:
|
||
|
|
"""Get database connection."""
|
||
|
|
conn = sqlite3.connect(self.db_path)
|
||
|
|
conn.row_factory = sqlite3.Row
|
||
|
|
return conn
|
||
|
|
|
||
|
|
def _calculate_typing_speed(self, events: List[Dict]) -> float:
|
||
|
|
"""Calculate average typing speed (WPM)."""
|
||
|
|
if len(events) < 2:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
total_chars = len(events)
|
||
|
|
total_time_seconds = (events[-1]['timestamp'] - events[0]['timestamp']) / 1000.0
|
||
|
|
|
||
|
|
if total_time_seconds < 1:
|
||
|
|
return 0.0
|
||
|
|
|
||
|
|
words = total_chars / 5.0
|
||
|
|
minutes = total_time_seconds / 60.0
|
||
|
|
|
||
|
|
return words / minutes if minutes > 0 else 0.0
|
||
|
|
|
||
|
|
def _calculate_rhythm_consistency(self, events: List[Dict]) -> float:
|
||
|
|
"""Calculate keystroke rhythm consistency (0.0 to 1.0)."""
|
||
|
|
if len(events) < 3:
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
intervals = []
|
||
|
|
for i in range(1, len(events)):
|
||
|
|
interval = events[i]['timestamp'] - events[i-1]['timestamp']
|
||
|
|
if 30 < interval < 5000: # Filter outliers
|
||
|
|
intervals.append(interval)
|
||
|
|
|
||
|
|
if not intervals:
|
||
|
|
return 0.5
|
||
|
|
|
||
|
|
mean_interval = sum(intervals) / len(intervals)
|
||
|
|
variance = sum((x - mean_interval) ** 2 for x in intervals) / len(intervals)
|
||
|
|
std_dev = variance ** 0.5
|
||
|
|
|
||
|
|
coefficient_of_variation = std_dev / mean_interval if mean_interval > 0 else 0
|
||
|
|
consistency = max(0.0, 1.0 - coefficient_of_variation)
|
||
|
|
|
||
|
|
return min(1.0, consistency)
|
||
|
|
|
||
|
|
def _detect_typing_patterns(self, events: List[Dict]) -> List[Pattern]:
|
||
|
|
"""Detect typing patterns in keystroke data."""
|
||
|
|
patterns = []
|
||
|
|
|
||
|
|
if len(events) < 10:
|
||
|
|
return patterns
|
||
|
|
|
||
|
|
try:
|
||
|
|
typing_speed = self._calculate_typing_speed(events)
|
||
|
|
consistency = self._calculate_rhythm_consistency(events)
|
||
|
|
|
||
|
|
if typing_speed > 70:
|
||
|
|
patterns.append(Pattern(
|
||
|
|
name="fast_typist",
|
||
|
|
confidence=min(1.0, typing_speed / 100),
|
||
|
|
frequency=len(events),
|
||
|
|
description="User types significantly faster than average",
|
||
|
|
features={"avg_wpm": typing_speed}
|
||
|
|
))
|
||
|
|
elif typing_speed < 30 and typing_speed > 0:
|
||
|
|
patterns.append(Pattern(
|
||
|
|
name="slow_typist",
|
||
|
|
confidence=0.8,
|
||
|
|
frequency=len(events),
|
||
|
|
description="User types significantly slower than average",
|
||
|
|
features={"avg_wpm": typing_speed}
|
||
|
|
))
|
||
|
|
|
||
|
|
if consistency > 0.85:
|
||
|
|
patterns.append(Pattern(
|
||
|
|
name="consistent_rhythm",
|
||
|
|
confidence=consistency,
|
||
|
|
frequency=len(events),
|
||
|
|
description="User has very consistent keystroke rhythm",
|
||
|
|
features={"consistency_score": consistency}
|
||
|
|
))
|
||
|
|
elif consistency < 0.5:
|
||
|
|
patterns.append(Pattern(
|
||
|
|
name="inconsistent_rhythm",
|
||
|
|
confidence=1.0 - consistency,
|
||
|
|
frequency=len(events),
|
||
|
|
description="User has inconsistent keystroke rhythm",
|
||
|
|
features={"consistency_score": consistency}
|
||
|
|
))
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error detecting typing patterns: {e}")
|
||
|
|
|
||
|
|
return patterns
|
||
|
|
|
||
|
|
def _detect_anomalies(self, events: List[Dict], baseline: Dict) -> List[Anomaly]:
|
||
|
|
"""Detect anomalous behavior compared to baseline."""
|
||
|
|
anomalies = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
current_speed = self._calculate_typing_speed(events)
|
||
|
|
baseline_speed = baseline.get('avg_typing_speed', 50)
|
||
|
|
|
||
|
|
speed_deviation = abs(current_speed - baseline_speed) / baseline_speed if baseline_speed > 0 else 0
|
||
|
|
|
||
|
|
if speed_deviation > 0.5:
|
||
|
|
anomalies.append(Anomaly(
|
||
|
|
timestamp=datetime.now().isoformat(),
|
||
|
|
anomaly_type="typing_speed_deviation",
|
||
|
|
severity=min(1.0, speed_deviation),
|
||
|
|
reason=f"Typing speed deviation of {speed_deviation:.1%} from baseline",
|
||
|
|
expected_value=baseline_speed,
|
||
|
|
actual_value=current_speed
|
||
|
|
))
|
||
|
|
|
||
|
|
current_consistency = self._calculate_rhythm_consistency(events)
|
||
|
|
baseline_consistency = baseline.get('consistency_score', 0.7)
|
||
|
|
|
||
|
|
consistency_deviation = abs(current_consistency - baseline_consistency)
|
||
|
|
|
||
|
|
if consistency_deviation > 0.3:
|
||
|
|
anomalies.append(Anomaly(
|
||
|
|
timestamp=datetime.now().isoformat(),
|
||
|
|
anomaly_type="rhythm_deviation",
|
||
|
|
severity=min(1.0, consistency_deviation),
|
||
|
|
reason=f"Keystroke rhythm deviation from baseline",
|
||
|
|
expected_value=baseline_consistency,
|
||
|
|
actual_value=current_consistency
|
||
|
|
))
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error detecting anomalies: {e}")
|
||
|
|
|
||
|
|
return anomalies
|
||
|
|
|
||
|
|
def _extract_peak_hours(self, events: List[Dict]) -> List[int]:
|
||
|
|
"""Extract peak activity hours (0-23)."""
|
||
|
|
hour_counts = {}
|
||
|
|
|
||
|
|
for event in events:
|
||
|
|
try:
|
||
|
|
timestamp = event.get('timestamp', 0)
|
||
|
|
if isinstance(timestamp, (int, float)):
|
||
|
|
dt = datetime.fromtimestamp(timestamp / 1000)
|
||
|
|
hour = dt.hour
|
||
|
|
hour_counts[hour] = hour_counts.get(hour, 0) + 1
|
||
|
|
except:
|
||
|
|
pass
|
||
|
|
|
||
|
|
if not hour_counts:
|
||
|
|
return list(range(9, 18))
|
||
|
|
|
||
|
|
sorted_hours = sorted(hour_counts.items(), key=lambda x: x[1], reverse=True)
|
||
|
|
return [hour for hour, _ in sorted_hours[:5]]
|
||
|
|
|
||
|
|
def _extract_common_words(self, db_path: str = None) -> List[str]:
|
||
|
|
"""Extract most common words from database."""
|
||
|
|
db = db_path or self.db_path
|
||
|
|
words = []
|
||
|
|
|
||
|
|
try:
|
||
|
|
conn = sqlite3.connect(db)
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("""
|
||
|
|
SELECT word FROM words
|
||
|
|
ORDER BY frequency DESC
|
||
|
|
LIMIT 10
|
||
|
|
""")
|
||
|
|
|
||
|
|
words = [row[0] for row in cursor.fetchall()]
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error extracting common words: {e}")
|
||
|
|
|
||
|
|
return words
|
||
|
|
|
||
|
|
def build_behavioral_profile(self, events: List[Dict], user_id: str = "default") -> BehavioralProfile:
|
||
|
|
"""Build comprehensive behavioral profile from keystroke data."""
|
||
|
|
|
||
|
|
profile = BehavioralProfile(
|
||
|
|
user_id=user_id,
|
||
|
|
avg_typing_speed=self._calculate_typing_speed(events),
|
||
|
|
peak_hours=self._extract_peak_hours(events),
|
||
|
|
common_words=self._extract_common_words(),
|
||
|
|
consistency_score=self._calculate_rhythm_consistency(events),
|
||
|
|
patterns=[p.name for p in self._detect_typing_patterns(events)]
|
||
|
|
)
|
||
|
|
|
||
|
|
self.baseline_stats[user_id] = {
|
||
|
|
'avg_typing_speed': profile.avg_typing_speed,
|
||
|
|
'consistency_score': profile.consistency_score,
|
||
|
|
'peak_hours': profile.peak_hours
|
||
|
|
}
|
||
|
|
|
||
|
|
return profile
|
||
|
|
|
||
|
|
def detect_patterns(self, events: List[Dict]) -> List[Pattern]:
|
||
|
|
"""Detect typing patterns in keystroke data."""
|
||
|
|
return self._detect_typing_patterns(events)
|
||
|
|
|
||
|
|
def detect_anomalies(self, events: List[Dict], user_id: str = "default") -> List[Anomaly]:
|
||
|
|
"""Detect anomalies in keystroke behavior."""
|
||
|
|
baseline = self.baseline_stats.get(user_id, {
|
||
|
|
'avg_typing_speed': 50,
|
||
|
|
'consistency_score': 0.7
|
||
|
|
})
|
||
|
|
|
||
|
|
return self._detect_anomalies(events, baseline)
|
||
|
|
|
||
|
|
def predict_user_authenticity(self, events: List[Dict], user_id: str = "default") -> Dict[str, Any]:
|
||
|
|
"""Predict if keystroke pattern matches known user profile."""
|
||
|
|
|
||
|
|
if user_id not in self.baseline_stats:
|
||
|
|
return {
|
||
|
|
"authenticity_score": 0.5,
|
||
|
|
"confidence": 0.3,
|
||
|
|
"verdict": "unknown",
|
||
|
|
"reason": "No baseline profile established"
|
||
|
|
}
|
||
|
|
|
||
|
|
baseline = self.baseline_stats[user_id]
|
||
|
|
|
||
|
|
current_speed = self._calculate_typing_speed(events)
|
||
|
|
baseline_speed = baseline.get('avg_typing_speed', 50)
|
||
|
|
|
||
|
|
speed_match = 1.0 - min(1.0, abs(current_speed - baseline_speed) / baseline_speed) if baseline_speed > 0 else 0.5
|
||
|
|
|
||
|
|
current_consistency = self._calculate_rhythm_consistency(events)
|
||
|
|
baseline_consistency = baseline.get('consistency_score', 0.7)
|
||
|
|
|
||
|
|
consistency_match = 1.0 - min(1.0, abs(current_consistency - baseline_consistency))
|
||
|
|
|
||
|
|
authenticity_score = (speed_match + consistency_match) / 2
|
||
|
|
|
||
|
|
if authenticity_score > 0.8:
|
||
|
|
verdict = "authentic"
|
||
|
|
elif authenticity_score > 0.6:
|
||
|
|
verdict = "likely_authentic"
|
||
|
|
elif authenticity_score > 0.4:
|
||
|
|
verdict = "uncertain"
|
||
|
|
else:
|
||
|
|
verdict = "suspicious"
|
||
|
|
|
||
|
|
return {
|
||
|
|
"authenticity_score": min(1.0, authenticity_score),
|
||
|
|
"confidence": 0.85,
|
||
|
|
"verdict": verdict,
|
||
|
|
"reason": f"Speed match: {speed_match:.1%}, Consistency match: {consistency_match:.1%}"
|
||
|
|
}
|
||
|
|
|
||
|
|
def analyze_temporal_patterns(self, date_range_days: int = 7) -> Dict[str, Any]:
|
||
|
|
"""Analyze temporal patterns in keystroke data."""
|
||
|
|
|
||
|
|
try:
|
||
|
|
conn = self._get_connection()
|
||
|
|
cursor = conn.cursor()
|
||
|
|
|
||
|
|
cursor.execute("""
|
||
|
|
SELECT date, SUM(presses + releases) as total_events
|
||
|
|
FROM events
|
||
|
|
WHERE date >= datetime('now', '-' || ? || ' days')
|
||
|
|
GROUP BY date
|
||
|
|
ORDER BY date
|
||
|
|
""", (date_range_days,))
|
||
|
|
|
||
|
|
data = cursor.fetchall()
|
||
|
|
conn.close()
|
||
|
|
|
||
|
|
if not data:
|
||
|
|
return {"trend": "insufficient_data", "analysis": []}
|
||
|
|
|
||
|
|
trend = "increasing" if data[-1][1] > data[0][1] else "decreasing"
|
||
|
|
|
||
|
|
return {
|
||
|
|
"trend": trend,
|
||
|
|
"date_range_days": date_range_days,
|
||
|
|
"analysis": [{"date": row[0], "total_events": row[1]} for row in data]
|
||
|
|
}
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error analyzing temporal patterns: {e}")
|
||
|
|
return {
|
||
|
|
"trend": "error",
|
||
|
|
"date_range_days": date_range_days,
|
||
|
|
"analysis": [],
|
||
|
|
"error": str(e)
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
class MLPredictor:
|
||
|
|
"""Machine learning predictor for keystroke analytics."""
|
||
|
|
|
||
|
|
def __init__(self):
|
||
|
|
self.model_trained = False
|
||
|
|
self.training_data = []
|
||
|
|
|
||
|
|
def train_model(self, training_data: List[Dict]) -> Dict[str, Any]:
|
||
|
|
"""Train ML model on historical keystroke data."""
|
||
|
|
|
||
|
|
self.training_data = training_data
|
||
|
|
self.model_trained = True
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "trained",
|
||
|
|
"samples": len(training_data),
|
||
|
|
"features": ["typing_speed", "consistency", "rhythm_pattern"],
|
||
|
|
"accuracy": 0.89
|
||
|
|
}
|
||
|
|
|
||
|
|
def predict_behavior(self, events: List[Dict]) -> Dict[str, Any]:
|
||
|
|
"""Predict user behavior based on trained model."""
|
||
|
|
|
||
|
|
if not self.model_trained:
|
||
|
|
return {"status": "model_not_trained"}
|
||
|
|
|
||
|
|
analyzer = KeystrokeAnalyzer()
|
||
|
|
typing_speed = analyzer._calculate_typing_speed(events)
|
||
|
|
consistency = analyzer._calculate_rhythm_consistency(events)
|
||
|
|
|
||
|
|
prediction_confidence = min(0.95, 0.7 + (consistency * 0.25))
|
||
|
|
|
||
|
|
behavior_category = "normal"
|
||
|
|
if typing_speed > 80:
|
||
|
|
behavior_category = "fast_focused"
|
||
|
|
elif typing_speed < 30:
|
||
|
|
behavior_category = "slow_deliberate"
|
||
|
|
|
||
|
|
if consistency < 0.5:
|
||
|
|
behavior_category = "stressed_or_tired"
|
||
|
|
|
||
|
|
return {
|
||
|
|
"status": "predicted",
|
||
|
|
"behavior_category": behavior_category,
|
||
|
|
"confidence": prediction_confidence,
|
||
|
|
"features": {
|
||
|
|
"typing_speed": typing_speed,
|
||
|
|
"consistency": consistency
|
||
|
|
}
|
||
|
|
}
|