diff --git a/.env.example b/.env.example
index 9005d0a..07e7e7d 100644
--- a/.env.example
+++ b/.env.example
@@ -44,6 +44,39 @@ WEB3STORAGE_API_KEY=your-web3-storage-api-key
# Infura IPFS Configuration
INFURA_API_KEY=your-infura-api-key
+# Security Alert Configuration
+SECURITY_ALERT_EMAIL=security@yourdomain.com
+
+# Behavioral Biometric Fraud Detection Configuration
+BEHAVIORAL_BIOMETRIC_ENABLED=false
+
+# Behavioral Collection Settings
+BEHAVIORAL_COLLECTION_ENABLED=true
+BEHAVIORAL_SAMPLE_RATE=1.0
+BEHAVIORAL_MAX_EVENTS_PER_SESSION=1000
+BEHAVIORAL_SESSION_TIMEOUT=1800000
+BEHAVIORAL_ANONYMIZE_IP=true
+BEHAVIORAL_HASH_SALT=your-behavioral-hash-salt
+
+# Behavioral Classifier Settings
+BEHAVIORAL_CLASSIFIER_ENABLED=true
+BEHAVIORAL_MODEL_TYPE=rule_based
+BEHAVIORAL_CONFIDENCE_THRESHOLD=0.7
+BEHAVIORAL_TRAINING_THRESHOLD=100
+BEHAVIORAL_RETRAIN_INTERVAL=604800000
+
+# Behavioral Risk Thresholds (0-1 scale)
+BEHAVIORAL_BOT_SCORE_THRESHOLD=0.8
+BEHAVIORAL_THROTTLING_THRESHOLD=0.6
+BEHAVIORAL_WATCH_LIST_THRESHOLD=0.9
+BEHAVIORAL_ANOMALY_THRESHOLD=0.75
+
+# Behavioral Privacy Settings
+BEHAVIORAL_DATA_RETENTION_DAYS=30
+BEHAVIORAL_HASH_PERSONAL_DATA=true
+BEHAVIORAL_EXCLUDE_PII=true
+BEHAVIORAL_GDPR_COMPLIANT=true
+
# Database Configuration (for production)
# DATABASE_URL=postgresql://username:password@localhost:5432/substream
diff --git a/behavioralBiometric.test.js b/behavioralBiometric.test.js
new file mode 100644
index 0000000..8698dc7
--- /dev/null
+++ b/behavioralBiometric.test.js
@@ -0,0 +1,835 @@
+const { BehavioralBiometricService } = require('../src/services/behavioralBiometricService');
+const { BotDetectionClassifier } = require('../src/services/botDetectionClassifier');
+
+describe('Behavioral Biometric System', () => {
+ let behavioralService;
+ let classifier;
+ let mockDatabase;
+
+ beforeEach(() => {
+ // Mock database
+ mockDatabase = {
+ db: {
+ prepare: jest.fn(),
+ exec: jest.fn(),
+ run: jest.fn(),
+ get: jest.fn(),
+ all: jest.fn()
+ }
+ };
+
+ // Initialize services
+ behavioralService = new BehavioralBiometricService(mockDatabase, {
+ collection: { enabled: true, maxEventsPerSession: 100 },
+ classifier: { enabled: true, confidenceThreshold: 0.7 },
+ thresholds: { botScoreThreshold: 0.8, throttlingThreshold: 0.6 },
+ privacy: { dataRetentionDays: 30 }
+ });
+
+ classifier = new BotDetectionClassifier({
+ confidenceThreshold: 0.7,
+ thresholds: {
+ highEventsPerMinute: 100,
+ lowMouseSpeedVariance: 0.1,
+ highTypingConsistency: 0.95,
+ highRapidClicks: 0.5
+ }
+ });
+ });
+
+ afterEach(() => {
+ // Clean up
+ behavioralService.stop();
+ });
+
+ describe('BehavioralBiometricService', () => {
+ describe('Session Management', () => {
+ test('should start tracking session', async () => {
+ const sessionId = 'test-session-123';
+ const sessionData = {
+ userAgent: 'Mozilla/5.0',
+ viewport: { width: 1920, height: 1080 }
+ };
+
+ const result = behavioralService.startSession(sessionId, sessionData);
+
+ expect(result.tracking).toBe(true);
+ expect(result.sessionId).toBe(sessionId);
+ expect(result.fingerprint).toBeDefined();
+ expect(behaviorService.activeSessions.has(sessionId)).toBe(true);
+ });
+
+ test('should record behavioral events', async () => {
+ const sessionId = 'test-session-456';
+ behavioralService.startSession(sessionId);
+
+ const eventData = {
+ type: 'click',
+ coordinates: { x: 100, y: 200 },
+ targetElement: 'button'
+ };
+
+ const result = behavioralService.recordEvent(sessionId, eventData);
+
+ expect(result.recorded).toBe(true);
+ expect(result.eventId).toBeDefined();
+ });
+
+ test('should analyze session for bot detection', async () => {
+ const sessionId = 'test-session-789';
+ behavioralService.startSession(sessionId);
+
+ // Simulate some events
+ behavioralService.recordEvent(sessionId, { type: 'mousemove', coordinates: { x: 50, y: 50 } });
+ behavioralService.recordEvent(sessionId, { type: 'click', coordinates: { x: 100, y: 100 } });
+ behavioralService.recordEvent(sessionId, { type: 'keydown', key: 'a' });
+
+ const result = behavioralService.analyzeSession(sessionId);
+
+ expect(result.sessionId).toBe(sessionId);
+ expect(result.botScore).toBeDefined();
+ expect(result.riskLevel).toBeDefined();
+ expect(result.confidence).toBeDefined();
+ expect(result.features).toBeDefined();
+ });
+
+ test('should end session tracking', async () => {
+ const sessionId = 'test-session-end-123';
+ behavioralService.startSession(sessionId);
+
+ const result = behavioralService.endSession(sessionId);
+
+ expect(result.sessionId).toBe(sessionId);
+ expect(result.duration).toBeGreaterThan(0);
+ expect(result.totalEvents).toBeGreaterThanOrEqual(0);
+ expect(behavioralService.activeSessions.has(sessionId)).toBe(false);
+ });
+
+ test('should handle session timeout', async () => {
+ const sessionId = 'test-session-timeout-123';
+
+ // Mock session that has timed out
+ const expiredSession = {
+ id: 'expired-session',
+ sessionId,
+ startTime: new Date(Date.now() - (35 * 60 * 1000)).toISOString(), // 35 minutes ago
+ };
+
+ behavioralService.activeSessions.set(sessionId, expiredSession);
+ behavioralService.cleanupExpiredSessions();
+
+ expect(behavioralService.activeSessions.has(sessionId)).toBe(false);
+ });
+ });
+
+ describe('Behavioral Feature Extraction', () => {
+ test('should extract features from session data', () => {
+ const session = {
+ startTime: Date.now() - 60000, // 1 minute ago
+ events: [
+ { type: 'mousemove', timestamp: Date.now() - 59000, coordinates: { x: 10, y: 20 }, movementSpeed: 0.5 },
+ { type: 'mousemove', timestamp: Date.now() - 58000, coordinates: { x: 15, y: 25 }, movementSpeed: 0.8 },
+ { type: 'click', timestamp: Date.now() - 57000, clickPattern: 'normal_click' },
+ { type: 'click', timestamp: Date.now() - 56000, clickPattern: 'normal_click' },
+ { type: 'keydown', timestamp: Date.now() - 55000, keystrokeTiming: { pattern: 'normal_typing', interval: 100 } }
+ ]
+ };
+
+ const features = behavioralService.extractBehavioralFeatures(session);
+
+ expect(features.sessionDuration).toBe(60000);
+ expect(features.eventsPerMinute).toBe(10);
+ expect(features.totalEvents).toBe(6);
+ expect(features.mouseEvents).toBe(2);
+ expect(features.clickEvents).toBe(2);
+ expect(features.keyEvents).toBe(1);
+ expect(features.avgMouseSpeed).toBe(0.65);
+ expect(features.rapidClicks).toBe(0);
+ expect(features.delayedClicks).toBe(0);
+ });
+
+ test('should calculate typing consistency', () => {
+ const keyEvents = [
+ { timestamp: Date.now() - 3000, keystrokeTiming: { interval: 100 } },
+ { timestamp: Date.now() - 2900, keystrokeTiming: { interval: 100 } },
+ { timestamp: Date.now() - 2800, keystrokeTiming: { interval: 100 } },
+ { timestamp: Date.now() - 2700, keystrokeTiming: { interval: 100 } },
+ { timestamp: Date.now() - 2600, keystrokeTiming: { interval: 100 } }
+ ];
+
+ const consistency = behavioralService.calculateTypingConsistency(keyEvents);
+
+ expect(consistency).toBe(1); // Perfect consistency
+ });
+
+ test('should calculate scroll smoothness', () => {
+ const scrollEvents = [
+ { timestamp: Date.now() - 1000, metadata: { scrollDelta: 10 } },
+ { timestamp: Date.now() - 900, metadata: { scrollDelta: 12 } },
+ { timestamp: Date.now() - 800, metadata: { scrollDelta: 11 } },
+ { timestamp: Date.now() - 700, metadata: { scrollDelta: 10 } },
+ { timestamp: Date.now() - 600, metadata: { scrollDelta: 9 } }
+ ];
+
+ const smoothness = behavioralService.calculateScrollSmoothness(scrollEvents);
+
+ expect(smoothness).toBeGreaterThan(0.8); // High smoothness
+ });
+
+ test('should analyze click patterns', () => {
+ const previousEvents = [
+ { type: 'click', timestamp: Date.now() - 2000 },
+ { type: 'click', timestamp: Date.now() - 1000 }
+ ];
+
+ const pattern = behavioralService.analyzeClickPattern(previousEvents, {
+ timestamp: Date.now()
+ });
+
+ expect(pattern).toBe('normal_click');
+ });
+
+ test('should analyze keystroke timing', () => {
+ const previousEvents = [
+ { type: 'keydown', timestamp: Date.now() - 2000 },
+ { type: 'keydown', timestamp: Date.now() - 1900 }
+ ];
+
+ const timing = behavioralService.analyzeKeystrokeTiming(previousEvents, {
+ timestamp: Date.now()
+ });
+
+ expect(timing.pattern).toBe('normal_typing');
+ expect(timing.interval).toBe(100);
+ expect(timing.consistency).toBeDefined();
+ });
+ });
+
+ describe('Hash Generation', () => {
+ test('should generate behavioral hash', () => {
+ const features = {
+ eventsPerMinute: 10,
+ avgMouseSpeed: 0.5,
+ typingConsistency: 0.8,
+ rapidClicks: 0,
+ totalEvents: 50
+ };
+
+ const hash1 = behavioralService.generateBehavioralHash(features);
+ const hash2 = behavioralService.generateBehavioralHash(features);
+
+ expect(hash1).toBe(hash2); // Same input should produce same hash
+ expect(hash1).toMatch(/^[a-f0-9]{64}$/); // Should be a hex string
+ });
+
+ test('should generate user fingerprint', () => {
+ const sessionData = {
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
+ viewport: { width: 1920, height: 1080 },
+ platform: 'Win32',
+ language: 'en-US'
+ };
+
+ const fingerprint = behavioralService.generateUserFingerprint(sessionData);
+
+ expect(fingerprint).toMatch(/^[a-f0-9]{64}$/); // Should be a hex string
+ expect(fingerprint).not.toBe(sessionData.userAgent); // Should not contain raw data
+ });
+
+ test('should hash sensitive data', () => {
+ const data = 'sensitive-information';
+ const hashed = behavioralService.hashData(data);
+
+ expect(hashed).toMatch(/^[a-f0-9]{64}$/); // Should be a hex string
+ expect(hashed).not.toBe(data); // Should not contain raw data
+ });
+ });
+
+ describe('Risk Level Calculation', () => {
+ test('should calculate minimal risk level', () => {
+ const riskLevel = behavioralService.calculateRiskLevel(0.1);
+ expect(riskLevel).toBe('minimal');
+ });
+
+ test('should calculate low risk level', () => {
+ const riskLevel = behavioralService.calculateRiskLevel(0.3);
+ expect(riskLevel).toBe('low');
+ });
+
+ test('should calculate medium risk level', () => {
+ const riskLevel = behavioralService.calculateRiskLevel(0.6);
+ expect(riskLevel).toBe('medium');
+ });
+
+ test('should calculate high risk level', () => {
+ const riskLevel = behavioralService.calculateRiskLevel(0.8);
+ expect(riskLevel).toBe('high');
+ });
+
+ test('should calculate critical risk level', () => {
+ const riskLevel = behavioralService.calculateRiskLevel(0.95);
+ expect(riskLevel).toBe('critical');
+ });
+ });
+
+ describe('Bot Detection', () => {
+ test('should use rule-based detection when not trained', () => {
+ const features = {
+ eventsPerMinute: 150, // High frequency
+ mouseSpeedVariance: 0.05, // Very low variance
+ typingConsistency: 0.98, // Very consistent
+ rapidClicks: 8, // Many rapid clicks
+ totalEvents: 100
+ };
+
+ const prediction = behavioralService.analyzeSession('test-session');
+
+ expect(prediction.botScore).toBeGreaterThan(0.8); // Should detect as bot
+ expect(prediction.riskLevel).toBe('high');
+ });
+
+ test('should detect human-like behavior', () => {
+ const features = {
+ eventsPerMinute: 5, // Low frequency
+ mouseSpeedVariance: 0.5, // Normal variance
+ typingConsistency: 0.7, // Normal consistency
+ rapidClicks: 1, // Few rapid clicks
+ totalEvents: 10
+ };
+
+ const prediction = behavioralService.analyzeSession('test-session');
+
+ expect(prediction.botScore).toBeLessThan(0.5); // Should detect as human
+ expect(prediction.riskLevel).toBe('low');
+ });
+ });
+
+ describe('Watch List Management', () => {
+ test('should add address to watch list', async () => {
+ const sessionId = 'test-session-watchlist';
+ const stellarAddress = 'GD5DJQDKEZCHR3BVVXZB4H5QGQDQZQZQZQZQZQZQ';
+ const riskScore = 0.95;
+ const reason = 'High bot score detected';
+
+ behavioralService.startSession(sessionId);
+ behavioralService.addToWatchList(sessionId, riskScore, reason);
+
+ const watchListStatus = behavioralService.checkWatchList(stellarAddress);
+
+ expect(watchListStatus.onWatchList).toBe(true);
+ expect(watchListStatus.riskScore).toBe(riskScore);
+ expect(watchListStatus.reason).toBe(reason);
+ });
+
+ test('should check if address is not on watch list', async () => {
+ const stellarAddress = 'GD5DJQDKEZCHR3BVVXZB4H5QGQDQZQZQZQZQZQZQ';
+
+ const watchListStatus = behavioralService.checkWatchList(stellarAddress);
+
+ expect(watchListStatus.onWatchList).toBe(false);
+ });
+ });
+
+ describe('Throttling', () => {
+ test('should apply throttling to high-risk sessions', async () => {
+ const sessionId = 'test-session-throttle';
+
+ // Mock high bot score session
+ const session = {
+ sessionId,
+ botScore: 0.9,
+ riskLevel: 'critical'
+ };
+ behavioralService.activeSessions.set(sessionId, session);
+
+ behavioralService.applySessionThrottling(sessionId);
+
+ const updatedSession = behavioralService.activeSessions.get(sessionId);
+
+ expect(updatedSession.throttledAt).toBeDefined();
+ expect(updatedSession.throttlingLevel).toBeLessThan(1);
+ });
+
+ test('should calculate throttling level based on bot score', () => {
+ expect(behavioralService.calculateThrottlingLevel(0.9)).toBe(0.1); // 90% throttling
+ expect(behavioralService.calculateThrottlingLevel(0.7)).toBe(0.3); // 70% throttling
+ expect(behavioral.calculateThrottling(0.5)).toBe(0.7); // 30% throttling
+ expect(behavioral.calculateThrottling(0.3)).toBe(1); // No throttling
+ });
+ });
+
+ describe('Analytics', () => {
+ test('should generate behavioral analytics', () => {
+ const analytics = behavioralService.getBehavioralAnalytics({
+ period: '24h',
+ includeDetails: false
+ });
+
+ expect(analytics).toBeDefined();
+ expect(analytics.period).toBe('24h');
+ expect(analytics.sessionStats).toBeDefined();
+ expect(analytics.riskDistribution).toBeDefined();
+ expect(analytics.activeSessions).toBeDefined();
+ });
+
+ test('should include session statistics', () => {
+ const analytics = behavioralService.getBehavioralAnalytics({
+ period: '24h'
+ });
+
+ expect(analytics.sessionStats.totalSessions).toBeGreaterThanOrEqual(0);
+ expect(analytics.sessionStats.avgBotScore).toBeGreaterThanOrEqual(0);
+ expect(analytics.sessionStats.flaggedSessions).toBeGreaterThanOrEqual(0);
+ expect(analytics.sessionStats.throttledSessions).toBeGreaterThanOrEqual(0);
+ });
+ });
+
+ describe('Privacy Protection', () => {
+ test('should anonymize IP addresses when enabled', () => {
+ const config = { collection: { anonymizeIP: true, hashSalt: 'test-salt' } };
+ const service = new BehavioralBiometricService(mockDatabase, config);
+
+ const metadata = { ip: '192.168.1.1' };
+ const processedEvent = service.processEvent({ metadata }, { metadata });
+
+ expect(processedEvent.metadata.ip).toMatch(/^[a-f0-9]{64}$/); // Should be hashed
+ expect(processedEvent.metadata.ip).not.toBe('192.168.1.1'); // Should not contain raw IP
+ });
+
+ test('should exclude PII when enabled', () => {
+ const config = { privacy: { excludePII: true } };
+ const service = new BehavioralBiometricService(mockDatabase, config);
+
+ const metadata = {
+ email: 'user@example.com',
+ name: 'John Doe',
+ phone: '+1234567890'
+ };
+ const processedEvent = service.processEvent({ metadata }, { metadata });
+
+ expect(processedEvent.metadata.email).toBeUndefined();
+ expect(processedEvent.metadata.name).toBeUndefined();
+ expect(processedEvent.metadata.phone).toBeUndefined();
+ });
+ });
+
+ describe('Performance', () => {
+ test('should get service statistics', () => {
+ const stats = behavioralService.getServiceStats();
+
+ expect(stats.config).toBeDefined();
+ expect(stats.activeSessions).toBe(0);
+ expect(stats.databaseStats).toBeDefined();
+ expect(stats.modelStats).toBeNull(); // ML model not trained by default
+ });
+
+ test('should track performance metrics', async () => {
+ const sessionId = 'test-performance';
+ behavioralService.startSession(sessionId);
+
+ // Simulate some events
+ for (let i = 0; i < 10; i++) {
+ behavioralService.recordEvent(sessionId, { type: 'click' });
+ }
+
+ const metrics = behavioralService.getServiceStats();
+
+ expect(metrics.databaseStats.totalSessions).toBe(1);
+ expect(metrics.databaseStats.totalEvents).toBe(10);
+ expect(metrics.performance.errors).toBe(0);
+ });
+ });
+
+ describe('Data Cleanup', () => {
+ test('should clean up old data', () => {
+ const initialCount = mockDatabase.db.prepare.mock.results.length;
+
+ behavioralService.cleanupOldData();
+
+ expect(mockDatabase.db.run).toHaveBeenCalledWith(
+ expect.stringContaining('DELETE FROM behavioral_sessions'),
+ expect.any(Array)
+ );
+ });
+ });
+
+ describe('Error Handling', () => {
+ test('should handle invalid session gracefully', () => {
+ const result = behavioralService.startSession('', {});
+
+ expect(result.tracking).toBe(false);
+ expect(result.error).toBeDefined();
+ });
+
+ event('should handle event recording errors gracefully', () => {
+ const result = behavioralService.recordEvent('invalid-session', {});
+
+ expect(result.recorded).toBe(false);
+ expect(result.error).toBeDefined();
+ });
+
+ test('should handle analysis errors gracefully', () => {
+ const result = behavioralService.analyzeSession('non-existent-session');
+
+ expect(result.error).toBeDefined();
+ });
+ });
+ });
+
+ describe('BotDetectionClassifier', () => {
+ describe('Training', () => {
+ test('should train with labeled data', () => {
+ const trainingData = [
+ {
+ features: {
+ eventsPerMinute: 150,
+ mouseSpeedVariance: 0.05,
+ typingConsistency: 0.98,
+ rapidClicks: 8,
+ totalEvents: 100
+ },
+ label: 'bot'
+ },
+ {
+ features: {
+ eventsPerMinute: 5,
+ mouseSpeedVariance: 0.5,
+ typingConsistency: 0.7,
+ rapidClicks: 1,
+ totalEvents: 10
+ },
+ label: 'human'
+ }
+ ];
+
+ classifier.train(trainingData);
+
+ expect(classifier.isTrained).toBe(true);
+ expect(classifier.featureStats).toBeDefined();
+ expect(classifier.performance.lastTrained).toBeDefined();
+ });
+
+ test('should calculate feature statistics', () => {
+ const trainingData = [
+ {
+ features: { eventsPerMinute: 100 },
+ label: 'bot'
+ },
+ {
+ features: { eventsPerMinute: 50 },
+ label: 'human'
+ },
+ {
+ features: { eventsPerMinute: 25 },
+ label: 'human'
+ }
+ ];
+
+ classifier.train(trainingData);
+
+ const stats = classifier.featureStats;
+ expect(stats.eventsPerMinute.mean).toBe((100 + 50 + 25) / 3);
+ expect(stats.eventsPerMinute.min).toBe(25);
+ expect(stats.eventsPerMinute.max).toBe(100);
+ });
+ });
+
+ describe('Prediction', () => {
+ test('should predict bot behavior with trained model', () => {
+ const trainingData = [
+ {
+ features: { eventsPerMinute: 150, mouseSpeedVariance: 0.05, typingConsistency: 0.98 },
+ label: 'bot'
+ },
+ {
+ features: { eventsPerMinute: 5, mouseSpeedVariance: 0.5, typingConsistency: 0.7 },
+ label: 'human'
+ }
+ ];
+
+ classifier.train(trainingData);
+
+ // Test bot prediction
+ const botFeatures = {
+ eventsPerMinute: 120,
+ mouseSpeedVariance: 0.08,
+ typingConsistency: 0.95,
+ rapidClicks: 6,
+ totalEvents: 80
+ };
+
+ const botPrediction = classifier.predict(botFeatures);
+
+ expect(botPrediction.isBot).toBe(true);
+ expect(botPrediction.botScore).toBeGreaterThan(0.8);
+ expect(botPrediction.confidence).toBeGreaterThan(0.5);
+
+ // Test human prediction
+ const humanFeatures = {
+ eventsPerMinute: 8,
+ mouseSpeedVariance: 0.6,
+ typingConsistency: 0.6,
+ rapidClicks: 0,
+ totalEvents: 12
+ };
+
+ const humanPrediction = classifier.predict(humanFeatures);
+
+ expect(humanPrediction.isBot).toBe(false);
+ expect(humanPrediction.botScore).toBeLessThan(0.5);
+ });
+
+ test('should use rule-based prediction when not trained', () => {
+ const features = {
+ eventsPerMinute: 200,
+ mouseSpeedVariance: 0.02,
+ typingConsistency: 0.99,
+ rapidClicks: 10,
+ totalEvents: 150
+ };
+
+ const prediction = classifier.predict(features);
+
+ expect(prediction.isBot).toBe(true);
+ expect(prediction.botScore).toBeGreaterThan(0.8);
+ expect(prediction.confidence).toBeLessThan(1);
+ expect(prediction.method).toBe('rule_based');
+ });
+
+ test('should normalize features', () => {
+ const features = {
+ eventsPerMinute: 150,
+ mouseSpeedVariance: 0.05,
+ typingConsistency: 0.98,
+ rapidClicks: 8,
+ totalEvents: 100
+ };
+
+ classifier.train([
+ { features: { eventsPerMinute: 100 }, label: 'bot' },
+ { features: { eventsPerMinute: 50 }, label: 'human' },
+ { features: { eventsPerMinute: 25 }, label: 'human' }
+ ]);
+
+ const normalized = classifier.normalizeFeatures(features);
+
+ expect(normalized.eventsPerMinute).toBeGreaterThanOrEqual(0);
+ expect(normalized.eventsPerMinute).toBeLessThanOrEqual(1);
+ expect(normalized.mouseSpeedVariance).toBeGreaterThanOrEqual(0);
+ expect(normalized.typingConsistency).toBeGreaterThanOrEqual(0);
+ });
+
+ test('should calculate prediction confidence', () => {
+ const features = {
+ eventsPerMinute: 100,
+ mouseSpeedVariance: 0.5,
+ typingConsistency: 0.7,
+ rapidClicks: 4,
+ totalEvents: 50
+ };
+
+ classifier.train([
+ { features: { eventsPerMinute: 100 }, label: 'bot' },
+ { features: { eventsPerMinute: 50 }, label: 'human' }
+ ]);
+
+ const prediction = classifier.predict(features);
+
+ expect(prediction.confidence).toBeGreaterThanOrEqual(0);
+ expect(prediction.confidence).toBeLessThanOrEqual(1);
+ });
+
+ test('should detect anomalies', () => {
+ const features = {
+ eventsPerMinute: 300, // Extremely high
+ mouseSpeedVariance: 0, // Perfectly consistent
+ typingConsistency: 1, // Perfectly consistent
+ rapidClicks: 15, // All clicks rapid
+ totalEvents: 200
+ };
+
+ const prediction = classifier.predict(features);
+
+ expect(prediction.isBot).toBe(true);
+ expect(prediction.botScore).toBeCloseTo(1, 1);
+ });
+
+ test('should update model with new data', () => {
+ const initialData = [
+ { features: { eventsPerMinute: 50 }, label: 'human' }
+ ];
+
+ classifier.train(initialData);
+ expect(classifier.isTrained).toBe(true);
+
+ // Add new training data
+ const newData = [
+ { features: { eventsPerMinute: 150 }, label: 'bot' },
+ { features: { eventsPerMinute: 25 }, label: 'human' }
+ ];
+
+ classifier.updateModel({ eventsPerMinute: 120 }, false);
+
+ expect(classifier.trainingData.length).toBe(3);
+ });
+
+ describe('Performance Tracking', () => {
+ test('should track prediction performance', () => {
+ classifier.resetPerformanceStats();
+
+ // Simulate some predictions
+ classifier.predict({ eventsPerMinute: 100 }, true); // Correct prediction
+ classifier.predict({ eventsPerMinute: 50 }, false); // False positive
+ classifier.predict({ eventsPerMinute: 80 }, true); // Correct prediction
+ classifier.predict({ eventsPerMinute: 30 }, false); // False negative
+
+ const stats = classifier.getPerformanceStats();
+
+ expect(stats.totalPredictions).toBe(4);
+ expect(stats.correctPredictions).toBe(2);
+ expect(stats.falsePositives).toBe(1);
+ expect(stats.falseNegatives).toBe(1);
+ expect(stats.accuracy).toBe(0.5);
+ expect(stats.precision).toBe(0.67);
+ expect(stats.recall).toBe(0.67);
+ expect(stats.f1Score).toBe(0.67);
+ });
+
+ test('should export and import model', () => {
+ const exportedModel = classifier.exportModel();
+
+ expect(exportedModel.config).toBeDefined();
+ expect(exportedModel.featureStats).toBeDefined();
+ expect(exportedModel.isTrained).toBe(true);
+ expect(exportedModel.trainingDataSize).toBeGreaterThanOrEqual(0);
+
+ // Create new classifier and import model
+ const newClassifier = new BotDetectionClassifier();
+ newClassifier.importModel(exportedModel);
+
+ expect(newClassifier.isTrained).toBe(true);
+ expect(newClassifier.config.confidenceThreshold).toBe(0.7);
+ });
+ });
+ });
+
+ describe('Integration Tests', () => {
+ test('should handle complete session lifecycle', async () => {
+ const sessionId = 'integration-test-session';
+
+ // Start session
+ const startResult = behavioralService.startSession(sessionId, {
+ userAgent: 'Test Agent',
+ viewport: { width: 1024, height: 768 }
+ });
+ expect(startResult.tracking).toBe(true);
+
+ // Record various events
+ behavioralService.recordEvent(sessionId, { type: 'mousemove', coordinates: { x: 100, y: 100 } });
+ behavioralService.recordEvent(sessionId, { type: 'click', coordinates: { x: 200, y: 200 } });
+ behavioralService.recordEvent(sessionId, { type: 'keydown', key: 'a' });
+ behavioralService.recordEvent(sessionId, { type: 'scroll', scrollY: 100 });
+
+ // Analyze session
+ const analysis = behavioralService.analyzeSession(sessionId);
+ expect(analysis.sessionId).toBe(sessionId);
+ expect(analysis.botScore).toBeDefined();
+ expect(analysis.riskLevel).toBeDefined();
+
+ // End session
+ const endResult = behavioralService.endSession(sessionId);
+ expect(endResult.sessionId).toBe(sessionId);
+ expect(endResult.duration).toBeGreaterThan(0);
+ });
+
+ test('should handle high-risk session workflow', async () => {
+ const sessionId = 'high-risk-session';
+ const stellarAddress = 'GD5DJQDKEZCHR3BVVXZB4H5QGQDQZQZQZQZQZQ';
+
+ // Start session
+ behavioralService.startSession(sessionId, {
+ userAgent: 'Bot Agent 3000',
+ viewport: { width: 1920, height: 1080 }
+ });
+
+ // Simulate bot-like behavior
+ for (let i = 0; i < 50; i++) {
+ behavioralService.recordEvent(sessionId, { type: 'mousemove', coordinates: { x: Math.random() * 1000, y: Math.random() * 1000 } });
+ }
+
+ // Analyze session (should detect as bot)
+ const analysis = behavioralService.analyzeSession(sessionId);
+ expect(analysis.isBot).toBe(true);
+ expect(analysis.botScore).toBeGreaterThan(0.8);
+
+ // Should be added to watch list
+ const watchListStatus = behavioralService.checkWatchList(stellarAddress);
+ expect(watchListStatus.onWatchList).toBe(true);
+
+ // End session
+ behavioralService.endSession(sessionId);
+
+ // Verify watch list entry
+ const watchListEntry = behavioralService.database.db.prepare(
+ 'SELECT * FROM high_risk_watch_list WHERE stellar_address = ? AND is_active = 1'
+ ).get(stellarAddress);
+
+ expect(watchListEntry).toBeDefined();
+ expect(watchEntry.reason).toContain('High bot score detected');
+ });
+
+ test('should handle throttling for flagged sessions', async () => {
+ const sessionId = 'throttle-test-session';
+
+ behavioralService.startSession(sessionId);
+
+ // Simulate behavior that triggers throttling
+ for (let i = 0; i < 100; i++) {
+ behavioralService.recordEvent(sessionId, { type: 'mousemove' });
+ }
+
+ // Analyze session (should trigger throttling)
+ const analysis = behavioralService.analyzeSession(sessionId);
+ expect(analysis.isThrottled).toBe(true);
+ expect(analysis.throttlingLevel).toBeLessThan(1);
+
+ // Verify throttling level
+ const session = behavioralService.activeSessions.get(sessionId);
+ expect(session.throttlingLevel).toBeLessThan(1);
+ expect(session.throttledAt).toBeDefined();
+ });
+
+ test('should handle privacy requirements', async () => {
+ const sessionId = 'privacy-test-session';
+
+ // Start session with PII
+ const sessionData = {
+ userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
+ viewport: { width: 1920, height: 1080 },
+ stellarAddress: 'GD5DJQDKEZCHR3BVVXZB4H5QGQDQZQZQZQZQZQ'
+ };
+
+ behavioralService.startSession(sessionId, sessionData);
+
+ // Record event with PII
+ behavioralService.recordEvent(sessionId, {
+ type: 'click',
+ metadata: {
+ email: 'user@example.com',
+ name: 'John Doe',
+ stellarAddress: 'GD5DJQDKEZCHR3BVVXZB4H5QGQDQZQZQZQZQZQ'
+ }
+ });
+
+ // Check if PII is excluded
+ const session = behavioralService.activeSessions.get(sessionId);
+ const event = session.events[session.events.length - 1];
+
+ expect(event.metadata.email).toBeUndefined();
+ expect(event.metadata.name).toBeUndefined();
+ expect(event.metadata.stellarAddress).toBeUndefined();
+ });
+ });
+});
+
+module.exports = {};
diff --git a/index.js b/index.js
index 60d2550..187b352 100644
--- a/index.js
+++ b/index.js
@@ -20,6 +20,12 @@ const { CreatorAuthService } = require('./src/services/creatorAuthService');
const { SorobanSubscriptionVerifier } = require('./src/services/sorobanSubscriptionVerifier');
const { SubscriptionService } = require('./src/services/subscriptionService');
const { SubscriptionExpiryChecker } = require('./src/services/subscriptionExpiryChecker');
+const { IPIntelligenceService } = require('./src/services/ipIntelligenceService');
+const { IPBlockingService } = require('./src/services/ipBlockingService');
+const { IPMonitoringService } = require('./src/services/ipMonitoringService');
+const { IPIntelligenceMiddleware } = require('./src/middleware/ipIntelligenceMiddleware');
+const { BehavioralBiometricService } = require('./src/services/behavioralBiometricService');
+const { BotDetectionClassifier } = require('./src/services/botDetectionClassifier');
const VideoProcessingWorker = require('./src/services/videoProcessingWorker');
const { BackgroundWorkerService } = require('./src/services/backgroundWorkerService');
const { GlobalStatsService } = require('./src/services/globalStatsService');
@@ -34,6 +40,7 @@ const createGlobalStatsRouter = require('./routes/globalStats');
const createDeviceRoutes = require('./routes/device');
const createSwaggerRoutes = require('./routes/swagger');
const { createIPIntelligenceRoutes } = require('./routes/ipIntelligence');
+const { createBehavioralBiometricRoutes } = require('./routes/behavioralBiometric');
const { buildAuditLogCsv } = require('./src/utils/export/auditLogCsv');
const { buildAuditLogPdf } = require('./src/utils/export/auditLogPdf');
const { getRequestIp } = require('./src/utils/requestIp');
@@ -189,20 +196,7 @@ function createApp(dependencies = {}) {
// Add request tracing middleware for structured logging
app.use(requestTracingMiddleware);
-
- // Add IP intelligence middleware if enabled
- if (ipMiddleware) {
- // Apply IP intelligence checks to sensitive endpoints
- app.use('/api/cdn', ipMiddleware.createGeneralMiddleware('cdn_access'));
- app.use('/api/creator', ipMiddleware.createCreatorMiddleware());
- app.use('/api/creator/videos', ipMiddleware.createContentMiddleware());
- app.use('/api/payouts', ipMiddleware.createWithdrawalMiddleware());
-
- // SIWS flow protection
- app.use('/api/subscription', ipMiddleware.createSIWSMiddleware());
-
- logger.info('IP Intelligence middleware applied to sensitive endpoints');
- }
+
// Subscription events webhook
app.use('/api/subscription', require('./routes/subscription'));
@@ -475,6 +469,13 @@ function createApp(dependencies = {}) {
}));
}
+ // Behavioral biometric management routes
+ if (behavioralService) {
+ app.use('/api/behavioral', createBehavioralBiometricRoutes({
+ behavioralService
+ }));
+ }
+
// Health check endpoint
app.get('/health', async (req, res) => {
const health = {
@@ -486,8 +487,8 @@ function createApp(dependencies = {}) {
redis: 'Unknown',
rabbitmq: 'Unknown',
stellar: 'Unknown',
- aml: 'Unknown',
- ipIntelligence: 'Unknown'
+ ipIntelligence: 'Unknown',
+ behavioralBiometric: 'Unknown'
},
};
@@ -544,30 +545,29 @@ function createApp(dependencies = {}) {
isDegraded = true;
}
- // Check AML Scanner
+ // Check IP Intelligence
try {
- if (amlScannerWorker) {
- const stats = amlScannerWorker.getScanStats();
- health.services.aml = stats.isRunning ? 'Running' : 'Stopped';
- if (!stats.isRunning) isDegraded = true;
+ if (ipIntelligenceService) {
+ const stats = ipIntelligenceService.getServiceStats();
+ health.services.ipIntelligence = 'Running';
} else {
- health.services.aml = 'Not Configured';
+ health.services.ipIntelligence = 'Not Configured';
}
} catch (error) {
- health.services.aml = 'Error';
+ health.services.ipIntelligence = 'Error';
isDegraded = true;
}
- // Check IP Intelligence
+ // Check Behavioral Biometric
try {
- if (ipIntelligenceService) {
- const stats = ipIntelligenceService.getServiceStats();
- health.services.ipIntelligence = 'Running';
+ if (behavioralService) {
+ const stats = behavioralService.getServiceStats();
+ health.services.behavioralBiometric = 'Running';
} else {
- health.services.ipIntelligence = 'Not Configured';
+ health.services.behavioralBiometric = 'Not Configured';
}
} catch (error) {
- health.services.ipIntelligence = 'Error';
+ health.services.behavioralBiometric = 'Error';
isDegraded = true;
}
diff --git a/public/js/behavioralTracker.js b/public/js/behavioralTracker.js
new file mode 100644
index 0000000..0518c1b
--- /dev/null
+++ b/public/js/behavioralTracker.js
@@ -0,0 +1,870 @@
+/**
+ * Behavioral Biometric Frontend Tracking Library
+ * Silently tracks user interaction patterns for bot detection
+ */
+class BehavioralTracker {
+ constructor(config = {}) {
+ this.config = {
+ enabled: config.enabled !== false,
+ apiEndpoint: config.apiEndpoint || '/api/behavioral',
+ sampleRate: config.sampleRate || 1.0,
+ batchSize: config.batchSize || 10,
+ flushInterval: config.flushInterval || 5000, // 5 seconds
+ maxEventsPerSession: config.maxEventsPerSession || 1000,
+ sessionTimeout: config.sessionTimeout || 30 * 60 * 1000, // 30 minutes
+ debug: config.debug || false
+ };
+
+ // Session management
+ this.sessionId = null;
+ this.userId = null;
+ this.sessionStartTime = null;
+ this.eventQueue = [];
+ this.isTracking = false;
+
+ // Event tracking
+ this.eventListeners = new Map();
+ this.lastEventTime = null;
+ this.eventCount = 0;
+
+ // Performance monitoring
+ this.performanceMetrics = {
+ eventsCollected: 0,
+ eventsSent: 0,
+ errors: 0,
+ averageLatency: 0
+ };
+
+ // Initialize if enabled
+ if (this.config.enabled) {
+ this.initialize();
+ }
+ }
+
+ /**
+ * Initialize behavioral tracking
+ */
+ initialize() {
+ try {
+ // Generate or retrieve session ID
+ this.sessionId = this.getOrCreateSessionId();
+ this.sessionStartTime = Date.now();
+ this.isTracking = true;
+
+ // Start session
+ this.startSession();
+
+ // Set up event listeners
+ this.setupEventListeners();
+
+ // Start periodic flush
+ this.startPeriodicFlush();
+
+ // Handle page unload
+ this.setupPageUnloadHandler();
+
+ this.log('Behavioral tracking initialized', {
+ sessionId: this.sessionId
+ });
+
+ } catch (error) {
+ this.logError('Failed to initialize behavioral tracking', error);
+ }
+ }
+
+ /**
+ * Get or create session ID
+ * @returns {string} Session ID
+ */
+ getOrCreateSessionId() {
+ // Try to get existing session ID from storage
+ let sessionId = sessionStorage.getItem('behavioral_session_id');
+
+ if (!sessionId) {
+ // Generate new session ID
+ sessionId = 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
+ sessionStorage.setItem('behavioral_session_id', sessionId);
+ }
+
+ return sessionId;
+ }
+
+ /**
+ * Start session with backend
+ */
+ async startSession() {
+ try {
+ const sessionData = {
+ userAgent: navigator.userAgent,
+ viewport: {
+ width: window.innerWidth,
+ height: window.innerHeight
+ },
+ platform: navigator.platform,
+ language: navigator.language,
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+ screen: {
+ width: screen.width,
+ height: screen.height
+ },
+ colorDepth: screen.colorDepth,
+ pixelRatio: window.devicePixelRatio,
+ hardwareConcurrency: navigator.hardwareConcurrency,
+ deviceMemory: navigator.deviceMemory,
+ connection: this.getConnectionInfo()
+ };
+
+ const response = await this.sendRequest('POST', '/session/start', {
+ sessionId: this.sessionId,
+ sessionData
+ });
+
+ if (response.tracking) {
+ this.userId = response.fingerprint;
+ this.log('Session started successfully', {
+ sessionId: this.sessionId,
+ fingerprint: response.fingerprint
+ });
+ }
+
+ } catch (error) {
+ this.logError('Failed to start session', error);
+ }
+ }
+
+ /**
+ * Get connection information
+ * @returns {object} Connection info
+ */
+ getConnectionInfo() {
+ const connection = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
+
+ if (!connection) {
+ return {};
+ }
+
+ return {
+ effectiveType: connection.effectiveType,
+ downlink: connection.downlink,
+ rtt: connection.rtt,
+ saveData: connection.saveData
+ };
+ }
+
+ /**
+ * Setup event listeners for behavioral tracking
+ */
+ setupEventListeners() {
+ // Mouse events
+ this.addEventListener(document, 'mousemove', this.handleMouseMove.bind(this));
+ this.addEventListener(document, 'mousedown', this.handleMouseDown.bind(this));
+ this.addEventListener(document, 'mouseup', this.handleMouseUp.bind(this));
+ this.addEventListener(document, 'click', this.handleClick.bind(this));
+ this.addEventListener(document, 'dblclick', this.handleDoubleClick.bind(this));
+ this.addEventListener(document, 'contextmenu', this.handleContextMenu.bind(this));
+
+ // Keyboard events
+ this.addEventListener(document, 'keydown', this.handleKeyDown.bind(this));
+ this.addEventListener(document, 'keyup', this.handleKeyUp.bind(this));
+ this.addEventListener(document, 'keypress', this.handleKeyPress.bind(this));
+
+ // Scroll events
+ this.addEventListener(window, 'scroll', this.handleScroll.bind(this));
+ this.addEventListener(window, 'wheel', this.handleWheel.bind(this));
+
+ // Touch events (mobile)
+ if ('ontouchstart' in window) {
+ this.addEventListener(document, 'touchstart', this.handleTouchStart.bind(this));
+ this.addEventListener(document, 'touchmove', this.handleTouchMove.bind(this));
+ this.addEventListener(document, 'touchend', this.handleTouchEnd.bind(this));
+ }
+
+ // Focus events
+ this.addEventListener(window, 'focus', this.handleFocus.bind(this));
+ this.addEventListener(window, 'blur', this.handleBlur.bind(this));
+
+ // Visibility change
+ this.addEventListener(document, 'visibilitychange', this.handleVisibilityChange.bind(this));
+
+ // Page navigation
+ this.addEventListener(window, 'beforeunload', this.handleBeforeUnload.bind(this));
+ }
+
+ /**
+ * Add event listener with error handling
+ * @param {Element} element - Target element
+ * @param {string} eventType - Event type
+ * @param {Function} handler - Event handler
+ */
+ addEventListener(element, eventType, handler) {
+ try {
+ const wrappedHandler = (event) => {
+ try {
+ handler(event);
+ } catch (error) {
+ this.logError(`Error in ${eventType} handler`, error);
+ }
+ };
+
+ element.addEventListener(eventType, wrappedHandler, { passive: true });
+
+ // Store reference for cleanup
+ if (!this.eventListeners.has(element)) {
+ this.eventListeners.set(element, []);
+ }
+ this.eventListeners.get(element).push({ eventType, handler: wrappedHandler });
+
+ } catch (error) {
+ this.logError(`Failed to add ${eventType} listener`, error);
+ }
+ }
+
+ /**
+ * Handle mouse move events
+ * @param {MouseEvent} event - Mouse event
+ */
+ handleMouseMove(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('mousemove', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ viewport: {
+ width: window.innerWidth,
+ height: window.innerHeight
+ },
+ movementSpeed: this.calculateMovementSpeed(event),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle mouse down events
+ * @param {MouseEvent} event - Mouse event
+ */
+ handleMouseDown(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('mousedown', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ button: event.button,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle mouse up events
+ * @param {MouseEvent} event - Mouse event
+ */
+ handleMouseUp(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('mouseup', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ button: event.button,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle click events
+ * @param {MouseEvent} event - Click event
+ */
+ handleClick(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('click', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ button: event.button,
+ clickCount: event.detail || 1,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle double click events
+ * @param {MouseEvent} event - Double click event
+ */
+ handleDoubleClick(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('dblclick', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle context menu events
+ * @param {MouseEvent} event - Context menu event
+ */
+ handleContextMenu(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('contextmenu', {
+ coordinates: {
+ x: event.clientX,
+ y: event.clientY
+ },
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle key down events
+ * @param {KeyboardEvent} event - Keyboard event
+ */
+ handleKeyDown(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('keydown', {
+ key: event.key,
+ code: event.code,
+ location: event.location,
+ ctrlKey: event.ctrlKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ metaKey: event.metaKey,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle key up events
+ * @param {KeyboardEvent} event - Keyboard event
+ */
+ handleKeyUp(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('keyup', {
+ key: event.key,
+ code: event.code,
+ location: event.location,
+ ctrlKey: event.ctrlKey,
+ shiftKey: event.shiftKey,
+ altKey: event.altKey,
+ metaKey: event.metaKey,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle key press events
+ * @param {KeyboardEvent} event - Keyboard event
+ */
+ handleKeyPress(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('keypress', {
+ key: event.key,
+ code: event.code,
+ charCode: event.charCode,
+ location: event.location,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle scroll events
+ * @param {Event} event - Scroll event
+ */
+ handleScroll(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('scroll', {
+ scrollX: window.scrollX,
+ scrollY: window.scrollY,
+ viewport: {
+ width: window.innerWidth,
+ height: window.innerHeight
+ },
+ scrollDelta: this.calculateScrollDelta(event),
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle wheel events
+ * @param {WheelEvent} event - Wheel event
+ */
+ handleWheel(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('wheel', {
+ deltaX: event.deltaX,
+ deltaY: event.deltaY,
+ deltaZ: event.deltaZ,
+ deltaMode: event.deltaMode,
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle touch start events (mobile)
+ * @param {TouchEvent} event - Touch event
+ */
+ handleTouchStart(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ const touches = Array.from(event.touches).map(touch => ({
+ identifier: touch.identifier,
+ coordinates: {
+ x: touch.clientX,
+ y: touch.clientY
+ },
+ force: touch.force,
+ radiusX: touch.radiusX,
+ radiusY: touch.radiusY
+ }));
+
+ this.recordEvent('touchstart', {
+ touches,
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle touch move events (mobile)
+ * @param {TouchEvent} event - Touch event
+ */
+ handleTouchMove(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ const touches = Array.from(event.touches).map(touch => ({
+ identifier: touch.identifier,
+ coordinates: {
+ x: touch.clientX,
+ y: touch.clientY
+ },
+ force: touch.force,
+ radiusX: touch.radiusX,
+ radiusY: touch.radiusY
+ }));
+
+ this.recordEvent('touchmove', {
+ touches,
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle touch end events (mobile)
+ * @param {TouchEvent} event - Touch event
+ */
+ handleTouchEnd(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ const touches = Array.from(event.touches).map(touch => ({
+ identifier: touch.identifier,
+ coordinates: {
+ x: touch.clientX,
+ y: touch.clientY
+ },
+ force: touch.force,
+ radiusX: touch.radiusX,
+ radiusY: touch.radiusY
+ }));
+
+ this.recordEvent('touchend', {
+ touches,
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle focus events
+ * @param {FocusEvent} event - Focus event
+ */
+ handleFocus(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('focus', {
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle blur events
+ * @param {FocusEvent} event - Blur event
+ */
+ handleBlur(event) {
+ if (!this.shouldTrackEvent()) return;
+
+ this.recordEvent('blur', {
+ targetElement: this.getTargetElement(event.target),
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle visibility change events
+ * @param {Event} event - Visibility change event
+ */
+ handleVisibilityChange(event) {
+ this.recordEvent('visibilitychange', {
+ hidden: document.hidden,
+ visibilityState: document.visibilityState,
+ timestamp: event.timeStamp || Date.now()
+ });
+ }
+
+ /**
+ * Handle before unload events
+ * @param {Event} event - Before unload event
+ */
+ handleBeforeUnload(event) {
+ // End session when user leaves
+ this.endSession();
+ }
+
+ /**
+ * Setup page unload handler
+ */
+ setupPageUnloadHandler() {
+ // Multiple approaches for different browsers
+ window.addEventListener('beforeunload', () => {
+ this.endSession();
+ });
+
+ window.addEventListener('pagehide', () => {
+ this.endSession();
+ });
+
+ // For older browsers
+ window.addEventListener('unload', () => {
+ this.endSession();
+ });
+ }
+
+ /**
+ * Check if event should be tracked
+ * @returns {boolean} Whether to track the event
+ */
+ shouldTrackEvent() {
+ // Check if tracking is enabled
+ if (!this.isTracking) return false;
+
+ // Check sample rate
+ if (Math.random() > this.config.sampleRate) return false;
+
+ // Check event limit
+ if (this.eventCount >= this.config.maxEventsPerSession) return false;
+
+ // Check session timeout
+ if (Date.now() - this.sessionStartTime > this.config.sessionTimeout) {
+ this.endSession();
+ this.initialize(); // Restart session
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Record behavioral event
+ * @param {string} eventType - Event type
+ * @param {object} eventData - Event data
+ */
+ recordEvent(eventType, eventData) {
+ try {
+ const event = {
+ type: eventType,
+ timestamp: eventData.timestamp || Date.now(),
+ sessionId: this.sessionId,
+ ...eventData
+ };
+
+ // Add to queue
+ this.eventQueue.push(event);
+ this.eventCount++;
+ this.lastEventTime = Date.now();
+
+ // Flush queue if batch size reached
+ if (this.eventQueue.length >= this.config.batchSize) {
+ this.flushEvents();
+ }
+
+ this.performanceMetrics.eventsCollected++;
+
+ } catch (error) {
+ this.logError('Failed to record event', error);
+ }
+ }
+
+ /**
+ * Start periodic flush of events
+ */
+ startPeriodicFlush() {
+ this.flushInterval = setInterval(() => {
+ if (this.eventQueue.length > 0) {
+ this.flushEvents();
+ }
+ }, this.config.flushInterval);
+ }
+
+ /**
+ * Flush events to backend
+ */
+ async flushEvents() {
+ if (this.eventQueue.length === 0) return;
+
+ const events = this.eventQueue.splice(0, this.config.batchSize);
+ const startTime = Date.now();
+
+ try {
+ const response = await this.sendRequest('POST', '/events/batch', {
+ sessionId: this.sessionId,
+ events: events
+ });
+
+ if (response.recorded) {
+ this.performanceMetrics.eventsSent += events.length;
+ const latency = Date.now() - startTime;
+
+ // Update average latency
+ const totalLatency = this.performanceMetrics.averageLatency * this.performanceMetrics.eventsSent + latency;
+ this.performanceMetrics.averageLatency = totalLatency / (this.performanceMetrics.eventsSent + events.length);
+
+ this.log('Events flushed successfully', {
+ eventCount: events.length,
+ latency,
+ totalEvents: this.performanceMetrics.eventsSent
+ });
+ }
+
+ } catch (error) {
+ this.logError('Failed to flush events', error);
+ this.performanceMetrics.errors++;
+
+ // Put events back in queue for retry
+ this.eventQueue.unshift(...events);
+ }
+ }
+
+ /**
+ * Send request to backend API
+ * @param {string} method - HTTP method
+ * @param {string} endpoint - API endpoint
+ * @param {object} data - Request data
+ * @returns {Promise} Response
+ */
+ async sendRequest(method, endpoint, data) {
+ const url = this.config.apiEndpoint + endpoint;
+ const options = {
+ method,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'X-Behavioral-Session-Id': this.sessionId
+ }
+ };
+
+ if (method === 'POST' || method === 'PUT') {
+ options.body = JSON.stringify(data);
+ }
+
+ const response = await fetch(url, options);
+
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
+ }
+
+ return response.json();
+ }
+
+ /**
+ * Calculate movement speed
+ * @param {MouseEvent} event - Mouse event
+ * @returns {number} Movement speed
+ */
+ calculateMovementSpeed(event) {
+ if (!this.lastMouseMoveEvent) {
+ this.lastMouseMoveEvent = event;
+ return 0;
+ }
+
+ const dx = event.clientX - this.lastMouseMoveEvent.clientX;
+ const dy = event.clientY - this.lastMouseMoveEvent.clientY;
+ const dt = event.timeStamp - this.lastMouseMoveEvent.timeStamp;
+
+ const speed = dt > 0 ? Math.sqrt(dx * dx + dy * dy) / dt : 0;
+
+ this.lastMouseMoveEvent = event;
+ return speed;
+ }
+
+ /**
+ * Calculate scroll delta
+ * @param {Event} event - Scroll event
+ * @returns {number} Scroll delta
+ */
+ calculateScrollDelta(event) {
+ // This is a simplified calculation
+ return Math.abs(window.scrollY - (this.lastScrollY || 0));
+ }
+
+ /**
+ * Get target element information
+ * @param {Element} element - Target element
+ * @returns {object} Element information
+ */
+ getTargetElement(element) {
+ if (!element) return null;
+
+ const elementInfo = {
+ tagName: element.tagName,
+ id: element.id,
+ className: element.className,
+ textContent: element.textContent ? element.textContent.slice(0, 100) : null, // Limit text length
+ attributes: {}
+ };
+
+ // Add important attributes
+ const importantAttrs = ['type', 'name', 'role', 'aria-label', 'title', 'href', 'src', 'alt'];
+ importantAttrs.forEach(attr => {
+ if (element.hasAttribute(attr)) {
+ elementInfo.attributes[attr] = element.getAttribute(attr);
+ }
+ });
+
+ return elementInfo;
+ }
+
+ /**
+ * End session tracking
+ */
+ async endSession() {
+ if (!this.isTracking) return;
+
+ try {
+ // Flush remaining events
+ if (this.eventQueue.length > 0) {
+ await this.flushEvents();
+ }
+
+ // End session with backend
+ const response = await this.sendRequest('POST', '/session/end', {
+ sessionId: this.sessionId,
+ endTime: Date.now(),
+ totalEvents: this.eventCount
+ });
+
+ this.log('Session ended', {
+ sessionId: this.sessionId,
+ duration: Date.now() - this.sessionStartTime,
+ totalEvents: this.eventCount,
+ performance: this.performanceMetrics
+ });
+
+ } catch (error) {
+ this.logError('Failed to end session', error);
+ } finally {
+ // Cleanup
+ this.isTracking = false;
+ this.eventQueue = [];
+ this.eventCount = 0;
+
+ if (this.flushInterval) {
+ clearInterval(this.flushInterval);
+ }
+
+ // Remove event listeners
+ this.cleanup();
+ }
+ }
+
+ /**
+ * Clean up event listeners
+ */
+ cleanup() {
+ for (const [element, listeners] of this.eventListeners.entries()) {
+ listeners.forEach(({ eventType, handler }) => {
+ element.removeEventListener(eventType, handler);
+ });
+ }
+ this.eventListeners.clear();
+ }
+
+ /**
+ * Get performance metrics
+ * @returns {object} Performance metrics
+ */
+ getPerformanceMetrics() {
+ return {
+ ...this.performanceMetrics,
+ queueSize: this.eventQueue.length,
+ isTracking: this.isTracking,
+ sessionDuration: this.isTracking ? Date.now() - this.sessionStartTime : 0
+ };
+ }
+
+ /**
+ * Log message (debug mode only)
+ * @param {string} message - Log message
+ * @param {object} data - Additional data
+ */
+ log(message, data = {}) {
+ if (this.config.debug) {
+ console.log(`[BehavioralTracker] ${message}`, data);
+ }
+ }
+
+ /**
+ * Log error
+ * @param {string} message - Error message
+ * @param {Error} error - Error object
+ */
+ logError(message, error) {
+ console.error(`[BehavioralTracker] ${message}`, error);
+ this.performanceMetrics.errors++;
+ }
+
+ /**
+ * Destroy tracker
+ */
+ destroy() {
+ this.endSession();
+ }
+}
+
+// Auto-initialize if script is loaded
+if (typeof window !== 'undefined') {
+ // Wait for DOM to be ready
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => {
+ window.behavioralTracker = new BehavioralTracker();
+ });
+ } else {
+ window.behavioralTracker = new BehavioralTracker();
+ }
+}
+
+// Export for manual initialization
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = BehavioralTracker;
+}
diff --git a/routes/behavioralBiometric.js b/routes/behavioralBiometric.js
new file mode 100644
index 0000000..69449a0
--- /dev/null
+++ b/routes/behavioralBiometric.js
@@ -0,0 +1,648 @@
+const express = require('express');
+const { logger } = require('../src/utils/logger');
+
+/**
+ * Create Behavioral Biometric management routes
+ * @param {object} dependencies - Service dependencies
+ * @returns {express.Router}
+ */
+function createBehavioralBiometricRoutes(dependencies = {}) {
+ const router = express.Router();
+ const behavioralService = dependencies.behavioralService;
+
+ if (!behavioralService) {
+ return router.status(503).json({
+ success: false,
+ error: 'Behavioral biometric service not available'
+ });
+ }
+
+ /**
+ * Start behavioral tracking session
+ * POST /api/behavioral/session/start
+ */
+ router.post('/session/start', async (req, res) => {
+ try {
+ const { sessionId, sessionData } = req.body;
+
+ if (!sessionId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Session ID is required'
+ });
+ }
+
+ const result = behavioralService.startSession(sessionId, sessionData);
+
+ return res.status(200).json({
+ success: true,
+ data: result
+ });
+
+ } catch (error) {
+ logger.error('Error starting behavioral session', {
+ error: error.message,
+ sessionId: req.body?.sessionId,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to start behavioral session'
+ });
+ }
+ });
+
+ /**
+ * Record behavioral events
+ * POST /api/behavioral/events/batch
+ */
+ router.post('/events/batch', async (req, res) => {
+ try {
+ const { sessionId, events } = req.body;
+
+ if (!sessionId || !Array.isArray(events)) {
+ return res.status(400).json({
+ success: false,
+ error: 'Session ID and events array are required'
+ });
+ }
+
+ const results = [];
+
+ for (const event of events) {
+ const result = behavioralService.recordEvent(sessionId, event);
+ results.push(result);
+ }
+
+ const recordedCount = results.filter(r => r.recorded).length;
+
+ return res.status(200).json({
+ success: true,
+ data: {
+ recorded: recordedCount,
+ total: events.length,
+ results
+ }
+ });
+
+ } catch (error) {
+ logger.error('Error recording behavioral events', {
+ error: error.message,
+ sessionId: req.body?.sessionId,
+ eventCount: req.body?.events?.length,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to record behavioral events'
+ });
+ }
+ });
+
+ /**
+ * End behavioral tracking session
+ * POST /api/behavioral/session/end
+ */
+ router.post('/session/end', async (req, res) => {
+ try {
+ const { sessionId, endTime, totalEvents } = req.body;
+
+ if (!sessionId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Session ID is required'
+ });
+ }
+
+ const result = behavioralService.endSession(sessionId);
+
+ return res.status(200).json({
+ success: true,
+ data: result
+ });
+
+ } catch (error) {
+ logger.error('Error ending behavioral session', {
+ error: error.message,
+ sessionId: req.body?.sessionId,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to end behavioral session'
+ });
+ }
+ });
+
+ /**
+ * Analyze session for bot detection
+ * POST /api/behavioral/session/analyze
+ */
+ router.post('/session/analyze', async (req, res) => {
+ try {
+ const { sessionId } = req.body;
+
+ if (!sessionId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Session ID is required'
+ });
+ }
+
+ const result = behavioralService.analyzeSession(sessionId);
+
+ return res.status(200).json({
+ success: true,
+ data: result
+ });
+
+ } catch (error) {
+ logger.error('Error analyzing behavioral session', {
+ error: error.message,
+ sessionId: req.body?.sessionId,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to analyze behavioral session'
+ });
+ }
+ });
+
+ /**
+ * Get behavioral analytics
+ * GET /api/behavioral/analytics
+ */
+ router.get('/analytics', async (req, res) => {
+ try {
+ const { period = '24h', includeDetails = 'false' } = req.query;
+
+ const analytics = behavioralService.getBehavioralAnalytics({
+ period,
+ includeDetails: includeDetails === 'true'
+ });
+
+ return res.status(200).json({
+ success: true,
+ data: analytics
+ });
+
+ } catch (error) {
+ logger.error('Error getting behavioral analytics', {
+ error: error.message,
+ period: req.query.period,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to get behavioral analytics'
+ });
+ }
+ });
+
+ /**
+ * Check if address is on high-risk watch list
+ * GET /api/behavioral/watchlist/:stellarAddress
+ */
+ router.get('/watchlist/:stellarAddress', async (req, res) => {
+ try {
+ const { stellarAddress } = req.params;
+
+ if (!stellarAddress) {
+ return res.status(400).json({
+ success: false,
+ error: 'Stellar address is required'
+ });
+ }
+
+ const result = behavioralService.checkWatchList(stellarAddress);
+
+ return res.status(200).json({
+ success: true,
+ data: result
+ });
+
+ } catch (error) {
+ logger.error('Error checking watch list', {
+ error: error.message,
+ stellarAddress: req.params.stellarAddress,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to check watch list'
+ });
+ }
+ });
+
+ /**
+ * Get service statistics
+ * GET /api/behavioral/stats
+ */
+ router.get('/stats', async (req, res) => {
+ try {
+ const stats = behavioralService.getServiceStats();
+
+ return res.status(200).json({
+ success: true,
+ data: stats
+ });
+
+ } catch (error) {
+ logger.error('Error getting behavioral stats', {
+ error: error.message,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to get behavioral stats'
+ });
+ }
+ });
+
+ /**
+ * Clean up old data
+ * POST /api/behavioral/cleanup
+ */
+ router.post('/cleanup', async (req, res) => {
+ try {
+ behavioralService.cleanupOldData();
+
+ return res.status(200).json({
+ success: true,
+ message: 'Old behavioral data cleaned up successfully'
+ });
+
+ } catch (error) {
+ logger.error('Error cleaning up behavioral data', {
+ error: error.message,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to clean up behavioral data'
+ });
+ }
+ });
+
+ /**
+ * Get session details
+ * GET /api/behavioral/session/:sessionId
+ */
+ router.get('/session/:sessionId', async (req, res) => {
+ try {
+ const { sessionId } = req.params;
+
+ if (!sessionId) {
+ return res.status(400).json({
+ success: false,
+ error: 'Session ID is required'
+ });
+ }
+
+ const session = behavioralService.activeSessions.get(sessionId);
+
+ if (!session) {
+ return res.status(404).json({
+ success: false,
+ error: 'Session not found'
+ });
+ }
+
+ return res.status(200).json({
+ success: true,
+ data: {
+ sessionId,
+ startTime: session.startTime,
+ totalEvents: session.totalEvents,
+ botScore: session.botScore,
+ riskLevel: session.riskLevel,
+ isFlagged: session.isFlagged,
+ isThrottled: session.isThrottled,
+ duration: Date.now() - new Date(session.startTime).getTime()
+ }
+ });
+
+ } catch (error) {
+ logger.error('Error getting session details', {
+ error: error.message,
+ sessionId: req.params.sessionId,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to get session details'
+ });
+ }
+ });
+
+ /**
+ * Get active sessions
+ * GET /api/behavioral/sessions/active
+ */
+ router.get('/sessions/active', async (req, res) => {
+ try {
+ const activeSessions = Array.from(behavioralService.activeSessions.entries()).map(([sessionId, session]) => ({
+ sessionId,
+ startTime: session.startTime,
+ totalEvents: session.totalEvents,
+ botScore: session.botScore,
+ riskLevel: session.riskLevel,
+ isFlagged: session.isFlagged,
+ isThrottled: session.isThrottled,
+ duration: Date.now() - new Date(session.startTime).getTime()
+ }));
+
+ return res.status(200).json({
+ success: true,
+ data: {
+ activeSessions,
+ count: activeSessions.length
+ }
+ });
+
+ } catch (error) {
+ logger.error('Error getting active sessions', {
+ error: error.message,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to get active sessions'
+ });
+ }
+ });
+
+ /**
+ * Export behavioral data
+ * GET /api/behavioral/export
+ */
+ router.get('/export', async (req, res) => {
+ try {
+ const {
+ period = '24h',
+ format = 'json',
+ includeDetails = 'false',
+ type = 'all'
+ } = req.query;
+
+ let data;
+ let filename;
+ let contentType;
+
+ switch (type) {
+ case 'sessions':
+ data = await this.exportSessions(period, includeDetails === 'true');
+ filename = `behavioral_sessions_${period}.${format}`;
+ break;
+ case 'events':
+ data = await this.exportEvents(period);
+ filename = `behavioral_events_${period}.${format}`;
+ break;
+ case 'watchlist':
+ data = await this.exportWatchList();
+ filename = `behavioral_watchlist.${format}`;
+ break;
+ case 'all':
+ default:
+ data = await this.exportAllData(period, includeDetails === 'true');
+ filename = `behavioral_export_${period}.${format}`;
+ break;
+ }
+
+ // Format response
+ if (format === 'csv') {
+ contentType = 'text/csv';
+ data = this.convertToCSV(data);
+ } else if (format === 'xml') {
+ contentType = 'application/xml';
+ data = this.convertToXML(data);
+ } else {
+ contentType = 'application/json';
+ data = JSON.stringify(data, null, 2);
+ }
+
+ res.setHeader('Content-Type', contentType);
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
+ return res.status(200).send(data);
+
+ } catch (error) {
+ logger.error('Error exporting behavioral data', {
+ error: error.message,
+ period: req.query.period,
+ format: req.query.format,
+ type: req.query.type,
+ traceId: req.logger?.fields?.traceId
+ });
+
+ return res.status(500).json({
+ success: false,
+ error: 'Failed to export behavioral data'
+ });
+ }
+ });
+
+ /**
+ * Export sessions data
+ * @param {string} period - Period string
+ * @param {boolean} includeDetails - Include detailed data
+ * @returns {object} Sessions data
+ */
+ async exportSessions(period, includeDetails) {
+ const startDate = this.getStartDate(period);
+
+ const sessions = behavioralService.database.db.prepare(`
+ SELECT * FROM behavioral_sessions
+ WHERE start_time > ?
+ ORDER BY start_time DESC
+ `).all(startDate.toISOString());
+
+ if (includeDetails) {
+ // Add events for each session
+ for (const session of sessions) {
+ session.events = behavioralService.database.db.prepare(`
+ SELECT * FROM behavioral_events
+ WHERE session_id = ?
+ ORDER BY timestamp ASC
+ `).all(session.session_id);
+ }
+ }
+
+ return sessions;
+ }
+
+ /**
+ * Export events data
+ * @param {string} period - Period string
+ * @returns {object} Events data
+ */
+ async exportEvents(period) {
+ const startDate = this.getStartDate(period);
+
+ return behavioralService.database.db.prepare(`
+ SELECT * FROM behavioral_events
+ WHERE created_at > ?
+ ORDER BY timestamp DESC
+ `).all(startDate.toISOString());
+ }
+
+ /**
+ * Export watch list data
+ * @returns {object} Watch list data
+ */
+ async exportWatchList() {
+ return behavioralService.database.db.prepare(`
+ SELECT * FROM high_risk_watch_list
+ WHERE is_active = 1
+ ORDER BY added_at DESC
+ `).all();
+ }
+
+ /**
+ * Export all data
+ * @param {string} period - Period string
+ * @param {boolean} includeDetails - Include detailed data
+ * @returns {object} All data
+ */
+ async exportAllData(period, includeDetails) {
+ const [sessions, events, watchList] = await Promise.all([
+ this.exportSessions(period, includeDetails),
+ this.exportEvents(period),
+ this.exportWatchList()
+ ]);
+
+ return {
+ sessions,
+ events,
+ watchList,
+ exportedAt: new Date().toISOString(),
+ period,
+ includeDetails
+ };
+ }
+
+ /**
+ * Convert data to CSV format
+ * @param {object} data - Data to convert
+ * @returns {string} CSV string
+ */
+ convertToCSV(data) {
+ if (Array.isArray(data)) {
+ if (data.length === 0) return '';
+
+ const headers = Object.keys(data[0]);
+ const csvRows = [headers.join(',')];
+
+ for (const row of data) {
+ const values = headers.map(header => {
+ let value = row[header];
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'object') value = JSON.stringify(value);
+ return `"${String(value).replace(/"/g, '""')}"`;
+ });
+ csvRows.push(values.join(','));
+ }
+
+ return csvRows.join('\n');
+ } else {
+ // Convert object to CSV
+ const flattenObject = (obj, prefix = '') => {
+ const flattened = {};
+ for (const [key, value] of Object.entries(obj)) {
+ if (value === null || value === undefined) continue;
+
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
+ Object.assign(flattened, flattenObject(value, prefix ? `${prefix}.${key}` : key));
+ } else {
+ flattened[prefix ? `${prefix}.${key}` : key] = value;
+ }
+ }
+ return flattened;
+ };
+
+ const flattened = flattenObject(data);
+ const headers = Object.keys(flattened);
+ const values = headers.map(header => {
+ const value = flattened[header];
+ if (value === null || value === undefined) return '';
+ if (typeof value === 'object') value = JSON.stringify(value);
+ return `"${String(value).replace(/"/g, '""')}"`;
+ });
+
+ return [headers.join(','), values.join(',')].join('\n');
+ }
+ }
+
+ /**
+ * Convert data to XML format
+ * @param {object} data - Data to convert
+ * @returns {string} XML string
+ */
+ convertToXML(data) {
+ const objectToXML = (obj, indent = 0) => {
+ const spaces = ' '.repeat(indent);
+ let xml = '';
+
+ for (const [key, value] of Object.entries(obj)) {
+ if (value === null || value === undefined) continue;
+
+ if (Array.isArray(value)) {
+ xml += `${spaces}<${key}>\n`;
+ value.forEach(item => {
+ if (typeof item === 'object') {
+ xml += objectToXML(item, indent + 1);
+ } else {
+ xml += `${spaces} - ${item}
\n`;
+ }
+ });
+ xml += `${spaces}${key}>\n`;
+ } else if (typeof value === 'object') {
+ xml += `${spaces}<${key}>\n`;
+ xml += objectToXML(value, indent + 1);
+ xml += `${spaces}${key}>\n`;
+ } else {
+ xml += `${spaces}<${key}>${value}${key}>\n`;
+ }
+ }
+
+ return xml;
+ };
+
+ return `\n\n${objectToXML(data, 1)}`;
+ }
+
+ /**
+ * Get start date for period
+ * @param {string} period - Period string
+ * @returns {Date} Start date
+ */
+ getStartDate(period) {
+ const now = new Date();
+ switch (period) {
+ case '1h':
+ return new Date(now.getTime() - 60 * 60 * 1000);
+ case '24h':
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ case '7d':
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ case '30d':
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ default:
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ }
+ }
+
+ return router;
+}
+
+module.exports = { createBehavioralBiometricRoutes };
diff --git a/src/config.js b/src/config.js
index bf718cd..941fbb6 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,4 +1,5 @@
const path = require('path');
+const crypto = require('crypto');
const { Networks } = require('@stellar/stellar-sdk');
@@ -117,6 +118,76 @@ function loadConfig(env = process.env) {
port: Number(env.IPFS_PORT || 5001),
protocol: env.IPFS_PROTOCOL || 'http',
} : null,
+ ipIntelligence: {
+ enabled: env.IP_INTELLIGENCE_ENABLED === 'true',
+ providers: {
+ ipinfo: {
+ enabled: env.IPINFO_ENABLED === 'true',
+ apiKey: env.IPINFO_API_KEY || '',
+ timeout: Number(env.IPINFO_TIMEOUT || 5000)
+ },
+ maxmind: {
+ enabled: env.MAXMIND_ENABLED === 'true',
+ apiKey: env.MAXMIND_API_KEY || '',
+ timeout: Number(env.MAXMIND_TIMEOUT || 5000)
+ },
+ abuseipdb: {
+ enabled: env.ABUSEIPDB_ENABLED === 'true',
+ apiKey: env.ABUSEIPDB_API_KEY || '',
+ timeout: Number(env.ABUSEIPDB_TIMEOUT || 5000)
+ },
+ ipqualityscore: {
+ enabled: env.IPQUALITYSCORE_ENABLED === 'true',
+ apiKey: env.IPQUALITYSCORE_API_KEY || '',
+ timeout: Number(env.IPQUALITYSCORE_TIMEOUT || 5000)
+ }
+ },
+ riskThresholds: {
+ low: Number(env.IP_RISK_THRESHOLD_LOW || 30),
+ medium: Number(env.IP_RISK_THRESHOLD_MEDIUM || 60),
+ high: Number(env.IP_RISK_THRESHOLD_HIGH || 80),
+ critical: Number(env.IP_RISK_THRESHOLD_CRITICAL || 90)
+ },
+ cache: {
+ enabled: env.IP_CACHE_ENABLED !== 'false',
+ ttl: Number(env.IP_CACHE_TTL_MS || 3600000), // 1 hour
+ maxSize: Number(env.IP_CACHE_MAX_SIZE || 10000)
+ },
+ rateLimit: {
+ requestsPerMinute: Number(env.IP_RATE_LIMIT_PER_MINUTE || 100),
+ burstLimit: Number(env.IP_RATE_LIMIT_BURST || 20)
+ }
+ },
+ behavioralBiometric: {
+ enabled: env.BEHAVIORAL_BIOMETRIC_ENABLED === 'true',
+ collection: {
+ enabled: env.BEHAVIORAL_COLLECTION_ENABLED !== 'false',
+ sampleRate: Number(env.BEHAVIORAL_SAMPLE_RATE || 1.0),
+ maxEventsPerSession: Number(env.BEHAVIORAL_MAX_EVENTS_PER_SESSION || 1000),
+ sessionTimeout: Number(env.BEHAVIORAL_SESSION_TIMEOUT || 30 * 60 * 1000), // 30 minutes
+ anonymizeIP: env.BEHAVIORAL_ANONYMIZE_IP !== 'false',
+ hashSalt: env.BEHAVIORAL_HASH_SALT || crypto.randomBytes(32).toString('hex')
+ },
+ classifier: {
+ enabled: env.BEHAVIORAL_CLASSIFIER_ENABLED !== 'false',
+ modelType: env.BEHAVIORAL_MODEL_TYPE || 'rule_based',
+ confidenceThreshold: Number(env.BEHAVIORAL_CONFIDENCE_THRESHOLD || 0.7),
+ trainingThreshold: Number(env.BEHAVIORAL_TRAINING_THRESHOLD || 100),
+ retrainInterval: Number(env.BEHAVIORAL_RETRAIN_INTERVAL || 7 * 24 * 60 * 60 * 1000) // 7 days
+ },
+ thresholds: {
+ botScoreThreshold: Number(env.BEHAVIORAL_BOT_SCORE_THRESHOLD || 0.8),
+ throttlingThreshold: env.BEHAVIORAL_THROTTLING_THRESHOLD || 0.6,
+ watchListThreshold: env.BEHAVIORAL_WATCH_LIST_THRESHOLD || 0.9,
+ anomalyThreshold: env.BEHAVIORAL_ANOMALY_THRESHOLD || 0.75
+ },
+ privacy: {
+ dataRetentionDays: Number(env.BEHAVIORAL_DATA_RETENTION_DAYS || 30),
+ hashPersonalData: env.BEHAVIORAL_HASH_PERSONAL_DATA !== 'false',
+ excludePII: env.BEHAVIORAL_EXCLUDE_PII !== 'false',
+ gdprCompliant: env.BEHAVIORAL_GDPR_COMPLIANT !== 'false'
+ }
+ },
rabbitmq: {
url: env.RABBITMQ_URL || '',
host: env.RABBITMQ_HOST || 'localhost',
@@ -129,6 +200,7 @@ function loadConfig(env = process.env) {
notificationQueue: env.RABBITMQ_NOTIFICATION_QUEUE || 'substream_notifications_queue',
emailQueue: env.RABBITMQ_EMAIL_QUEUE || 'substream_emails_queue',
leaderboardQueue: env.RABBITMQ_LEADERBOARD_QUEUE || 'substream_leaderboard_queue',
+ }
},
substream: {
baseDomain: env.SUBSTREAM_BASE_DOMAIN || 'substream.app',
@@ -151,9 +223,8 @@ function loadConfig(env = process.env) {
useApi: env.CADDY_USE_API === 'true',
},
};
-}
-module.exports = {
- DEFAULT_CONTRACT_ID,
- loadConfig,
-};
+ module.exports = {
+ DEFAULT_CONTRACT_ID,
+ loadConfig,
+ };
diff --git a/src/services/behavioralBiometricService.js b/src/services/behavioralBiometricService.js
new file mode 100644
index 0000000..683a575
--- /dev/null
+++ b/src/services/behavioralBiometricService.js
@@ -0,0 +1,1135 @@
+const crypto = require('crypto');
+const { logger } = require('../utils/logger');
+
+/**
+ * Behavioral Biometric Data Collection and Analysis Service
+ * Tracks user interaction patterns to detect automated/bot behavior
+ */
+class BehavioralBiometricService {
+ constructor(database, config = {}) {
+ this.database = database;
+ this.config = {
+ // Data collection settings
+ collection: {
+ enabled: config.collection?.enabled !== false,
+ sampleRate: config.collection?.sampleRate || 1.0, // 100% sampling
+ maxEventsPerSession: config.collection?.maxEventsPerSession || 1000,
+ sessionTimeout: config.collection?.sessionTimeout || 30 * 60 * 1000, // 30 minutes
+ anonymizeIP: config.collection?.anonymizeIP !== false,
+ hashSalt: config.collection?.hashSalt || crypto.randomBytes(32).toString('hex')
+ },
+ // ML model settings
+ classifier: {
+ enabled: config.classifier?.enabled !== false,
+ modelType: config.classifier?.modelType || 'random_forest',
+ trainingThreshold: config.classifier?.trainingThreshold || 100,
+ confidenceThreshold: config.classifier?.confidenceThreshold || 0.7,
+ retrainInterval: config.classifier?.retrainInterval || 7 * 24 * 60 * 60 * 1000 // 7 days
+ },
+ // Detection thresholds
+ thresholds: {
+ botScoreThreshold: config.thresholds?.botScoreThreshold || 0.8,
+ throttlingThreshold: config.thresholds?.throttlingThreshold || 0.6,
+ watchListThreshold: config.thresholds?.watchListThreshold || 0.9,
+ anomalyThreshold: config.thresholds?.anomalyThreshold || 0.75
+ },
+ // Privacy settings
+ privacy: {
+ dataRetentionDays: config.privacy?.dataRetentionDays || 30,
+ hashPersonalData: config.privacy?.hashPersonalData !== false,
+ excludePII: config.privacy?.excludePII !== false,
+ gdprCompliant: config.privacy?.gdprCompliant !== false
+ },
+ ...config
+ };
+
+ // Initialize behavioral tracking
+ this.initializeBehavioralTracking();
+
+ // ML model state
+ this.mlModel = null;
+ this.modelStats = {
+ totalPredictions: 0,
+ correctPredictions: 0,
+ falsePositives: 0,
+ falseNegatives: 0,
+ lastTrained: null
+ };
+
+ // Session tracking
+ this.activeSessions = new Map();
+ this.sessionCleanupInterval = setInterval(() => {
+ this.cleanupExpiredSessions();
+ }, 5 * 60 * 1000); // Every 5 minutes
+ }
+
+ /**
+ * Initialize behavioral tracking database tables
+ */
+ initializeBehavioralTracking() {
+ try {
+ this.database.db.exec(`
+ CREATE TABLE IF NOT EXISTS behavioral_sessions (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ user_fingerprint TEXT,
+ start_time TEXT NOT NULL,
+ end_time TEXT,
+ total_events INTEGER DEFAULT 0,
+ bot_score REAL DEFAULT 0,
+ is_flagged INTEGER DEFAULT 0,
+ is_throttled INTEGER DEFAULT 0,
+ risk_level TEXT DEFAULT 'unknown',
+ behavioral_hash TEXT,
+ metadata_json TEXT,
+ created_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS behavioral_events (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ event_type TEXT NOT NULL,
+ timestamp TEXT NOT NULL,
+ coordinates_x REAL,
+ coordinates_y REAL,
+ target_element TEXT,
+ viewport_width INTEGER,
+ viewport_height INTEGER,
+ user_agent TEXT,
+ movement_speed REAL,
+ click_pattern TEXT,
+ keystroke_timing TEXT,
+ scroll_pattern TEXT,
+ metadata_json TEXT,
+ created_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS behavioral_patterns (
+ id TEXT PRIMARY KEY,
+ pattern_hash TEXT NOT NULL,
+ pattern_type TEXT NOT NULL,
+ confidence REAL DEFAULT 0,
+ frequency INTEGER DEFAULT 1,
+ last_seen TEXT NOT NULL,
+ is_bot_pattern INTEGER DEFAULT 0,
+ created_at TEXT NOT NULL
+ );
+
+ CREATE TABLE IF NOT EXISTS high_risk_watch_list (
+ id TEXT PRIMARY KEY,
+ stellar_address TEXT NOT NULL,
+ reason TEXT NOT NULL,
+ risk_score REAL DEFAULT 0,
+ session_id TEXT,
+ added_at TEXT NOT NULL,
+ expires_at TEXT,
+ is_active INTEGER DEFAULT 1,
+ metadata_json TEXT
+ );
+
+ CREATE INDEX IF NOT EXISTS idx_behavioral_sessions_session_id ON behavioral_sessions(session_id);
+ CREATE INDEX IF NOT EXISTS idx_behavioral_sessions_start_time ON behavioral_sessions(start_time);
+ CREATE INDEX IF NOT EXISTS idx_behavioral_sessions_bot_score ON behavioral_sessions(bot_score);
+ CREATE INDEX IF NOT EXISTS idx_behavioral_events_session_id ON behavioral_events(session_id);
+ CREATE INDEX IF NOT EXISTS idx_behavioral_events_timestamp ON behavioral_events(timestamp);
+ CREATE INDEX IF NOT EXISTS idx_behavioral_patterns_hash ON behavioral_patterns(pattern_hash);
+ CREATE INDEX IF NOT EXISTS idx_high_risk_watch_list_address ON high_risk_watch_list(stellar_address);
+ CREATE INDEX IF NOT EXISTS idx_high_risk_watch_list_active ON high_risk_watch_list(is_active);
+ `);
+
+ logger.info('Behavioral biometric database tables initialized');
+ } catch (error) {
+ logger.error('Failed to initialize behavioral tracking tables', {
+ error: error.message
+ });
+ }
+ }
+
+ /**
+ * Start tracking a new session
+ * @param {string} sessionId - Unique session identifier
+ * @param {object} sessionData - Initial session data
+ * @returns {object} Session tracking result
+ */
+ startSession(sessionId, sessionData = {}) {
+ try {
+ if (!this.config.collection.enabled) {
+ return { tracking: false, reason: 'Behavioral tracking disabled' };
+ }
+
+ const sessionRecord = {
+ id: `session_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`,
+ sessionId,
+ userFingerprint: this.generateUserFingerprint(sessionData),
+ startTime: new Date().toISOString(),
+ endTime: null,
+ totalEvents: 0,
+ botScore: 0,
+ isFlagged: false,
+ isThrottled: false,
+ riskLevel: 'unknown',
+ behavioralHash: null,
+ metadata: {
+ userAgent: sessionData.userAgent,
+ viewport: sessionData.viewport,
+ platform: sessionData.platform,
+ language: sessionData.language
+ },
+ events: [],
+ createdAt: new Date().toISOString()
+ };
+
+ // Store in active sessions
+ this.activeSessions.set(sessionId, sessionRecord);
+
+ // Store in database
+ this.database.db.prepare(`
+ INSERT INTO behavioral_sessions (
+ id, session_id, user_fingerprint, start_time, metadata_json, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?)
+ `).run(
+ sessionRecord.id,
+ sessionId,
+ sessionRecord.userFingerprint,
+ sessionRecord.startTime,
+ JSON.stringify(sessionRecord.metadata),
+ sessionRecord.createdAt
+ );
+
+ logger.debug('Started behavioral tracking session', {
+ sessionId,
+ fingerprint: sessionRecord.userFingerprint
+ });
+
+ return {
+ tracking: true,
+ sessionId,
+ fingerprint: sessionRecord.userFingerprint
+ };
+
+ } catch (error) {
+ logger.error('Failed to start behavioral session', {
+ sessionId,
+ error: error.message
+ });
+
+ return {
+ tracking: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Record a behavioral event
+ * @param {string} sessionId - Session identifier
+ * @param {object} eventData - Event data
+ * @returns {object} Recording result
+ */
+ recordEvent(sessionId, eventData) {
+ try {
+ if (!this.config.collection.enabled) {
+ return { recorded: false, reason: 'Behavioral tracking disabled' };
+ }
+
+ const session = this.activeSessions.get(sessionId);
+ if (!session) {
+ return { recorded: false, reason: 'Session not found' };
+ }
+
+ // Check event limit
+ if (session.totalEvents >= this.config.collection.maxEventsPerSession) {
+ return { recorded: false, reason: 'Event limit reached' };
+ }
+
+ // Process event data
+ const processedEvent = this.processEvent(eventData, session);
+
+ // Store event
+ const eventId = `event_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
+
+ this.database.db.prepare(`
+ INSERT INTO behavioral_events (
+ id, session_id, event_type, timestamp, coordinates_x, coordinates_y,
+ target_element, viewport_width, viewport_height, user_agent,
+ movement_speed, click_pattern, keystroke_timing, scroll_pattern,
+ metadata_json, created_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ eventId,
+ sessionId,
+ processedEvent.eventType,
+ processedEvent.timestamp,
+ processedEvent.coordinates?.x || null,
+ processedEvent.coordinates?.y || null,
+ processedEvent.targetElement || null,
+ processedEvent.viewport?.width || null,
+ processedEvent.viewport?.height || null,
+ processedEvent.userAgent || null,
+ processedEvent.movementSpeed || null,
+ processedEvent.clickPattern || null,
+ processedEvent.keystrokeTiming || null,
+ processedEvent.scrollPattern || null,
+ JSON.stringify(processedEvent.metadata || {}),
+ processedEvent.timestamp
+ );
+
+ // Update session
+ session.totalEvents++;
+ session.events.push(processedEvent);
+
+ // Analyze session periodically
+ if (session.totalEvents % 10 === 0) {
+ this.analyzeSession(sessionId);
+ }
+
+ return {
+ recorded: true,
+ eventId,
+ eventCount: session.totalEvents
+ };
+
+ } catch (error) {
+ logger.error('Failed to record behavioral event', {
+ sessionId,
+ error: error.message
+ });
+
+ return {
+ recorded: false,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Process and normalize event data
+ * @param {object} eventData - Raw event data
+ * @param {object} session - Session context
+ * @returns {object} Processed event data
+ */
+ processEvent(eventData, session) {
+ const processedEvent = {
+ eventType: eventData.type || 'unknown',
+ timestamp: eventData.timestamp || new Date().toISOString(),
+ coordinates: eventData.coordinates || null,
+ targetElement: eventData.targetElement || null,
+ viewport: eventData.viewport || session.metadata?.viewport || null,
+ userAgent: eventData.userAgent || session.metadata?.userAgent || null,
+ metadata: {}
+ };
+
+ // Calculate movement speed for mouse events
+ if (processedEvent.eventType === 'mousemove' && session.events.length > 0) {
+ const lastEvent = session.events[session.events.length - 1];
+ if (lastEvent.coordinates && processedEvent.coordinates) {
+ const distance = Math.sqrt(
+ Math.pow(processedEvent.coordinates.x - lastEvent.coordinates.x, 2) +
+ Math.pow(processedEvent.coordinates.y - lastEvent.coordinates.y, 2)
+ );
+ const timeDiff = new Date(processedEvent.timestamp) - new Date(lastEvent.timestamp);
+ processedEvent.movementSpeed = timeDiff > 0 ? distance / timeDiff : 0;
+ }
+ }
+
+ // Analyze click patterns
+ if (processedEvent.eventType === 'click') {
+ processedEvent.clickPattern = this.analyzeClickPattern(session.events, processedEvent);
+ }
+
+ // Analyze keystroke timing
+ if (processedEvent.eventType === 'keydown') {
+ processedEvent.keystrokeTiming = this.analyzeKeystrokeTiming(session.events, processedEvent);
+ }
+
+ // Analyze scroll patterns
+ if (processedEvent.eventType === 'scroll') {
+ processedEvent.scrollPattern = this.analyzeScrollPattern(session.events, processedEvent);
+ }
+
+ // Anonymize sensitive data if enabled
+ if (this.config.collection.anonymizeIP && processedEvent.metadata?.ip) {
+ processedEvent.metadata.ip = this.hashData(processedEvent.metadata.ip);
+ }
+
+ return processedEvent;
+ }
+
+ /**
+ * Analyze click patterns
+ * @param {array} previousEvents - Previous session events
+ * @param {object} currentEvent - Current click event
+ * @returns {string} Click pattern description
+ */
+ analyzeClickPattern(previousEvents, currentEvent) {
+ const clickEvents = previousEvents.filter(e => e.eventType === 'click');
+
+ if (clickEvents.length === 0) {
+ return 'first_click';
+ }
+
+ const lastClick = clickEvents[clickEvents.length - 1];
+ const timeDiff = new Date(currentEvent.timestamp) - new Date(lastClick.timestamp);
+
+ // Analyze timing patterns
+ if (timeDiff < 50) {
+ return 'rapid_click';
+ } else if (timeDiff > 5000) {
+ return 'delayed_click';
+ } else {
+ return 'normal_click';
+ }
+ }
+
+ /**
+ * Analyze keystroke timing patterns
+ * @param {array} previousEvents - Previous session events
+ * @param {object} currentEvent - Current keystroke event
+ * @returns {object} Keystroke timing analysis
+ */
+ analyzeKeystrokeTiming(previousEvents, currentEvent) {
+ const keyEvents = previousEvents.filter(e => e.eventType === 'keydown');
+
+ if (keyEvents.length === 0) {
+ return { pattern: 'first_keystroke', interval: null };
+ }
+
+ const lastKey = keyEvents[keyEvents.length - 1];
+ const interval = new Date(currentEvent.timestamp) - new Date(lastKey.timestamp);
+
+ return {
+ pattern: interval < 50 ? 'rapid_typing' : interval > 1000 ? 'slow_typing' : 'normal_typing',
+ interval,
+ consistency: this.calculateTypingConsistency(keyEvents)
+ };
+ }
+
+ /**
+ * Analyze scroll patterns
+ * @param {array} previousEvents - Previous session events
+ * @param {object} currentEvent - Current scroll event
+ * @returns {object} Scroll pattern analysis
+ */
+ analyzeScrollPattern(previousEvents, currentEvent) {
+ const scrollEvents = previousEvents.filter(e => e.eventType === 'scroll');
+
+ if (scrollEvents.length === 0) {
+ return { pattern: 'first_scroll', velocity: null };
+ }
+
+ const lastScroll = scrollEvents[scrollEvents.length - 1];
+ const timeDiff = new Date(currentEvent.timestamp) - new Date(lastScroll.timestamp);
+ const velocity = timeDiff > 0 ? Math.abs(currentEvent.metadata?.scrollDelta || 0) / timeDiff : 0;
+
+ return {
+ pattern: velocity > 10 ? 'fast_scroll' : velocity < 1 ? 'slow_scroll' : 'normal_scroll',
+ velocity,
+ smoothness: this.calculateScrollSmoothness(scrollEvents)
+ };
+ }
+
+ /**
+ * Calculate typing consistency
+ * @param {array} keyEvents - Keyboard events
+ * @returns {number} Consistency score (0-1)
+ */
+ calculateTypingConsistency(keyEvents) {
+ if (keyEvents.length < 3) return 0.5;
+
+ const intervals = [];
+ for (let i = 1; i < keyEvents.length; i++) {
+ const interval = new Date(keyEvents[i].timestamp) - new Date(keyEvents[i-1].timestamp);
+ intervals.push(interval);
+ }
+
+ const mean = intervals.reduce((a, b) => a + b, 0) / intervals.length;
+ const variance = intervals.reduce((sum, interval) => sum + Math.pow(interval - mean, 2), 0) / intervals.length;
+ const stdDev = Math.sqrt(variance);
+
+ // Consistency is inverse of standard deviation (normalized)
+ return Math.max(0, 1 - (stdDev / mean));
+ }
+
+ /**
+ * Calculate scroll smoothness
+ * @param {array} scrollEvents - Scroll events
+ * @returns {number} Smoothness score (0-1)
+ */
+ calculateScrollSmoothness(scrollEvents) {
+ if (scrollEvents.length < 3) return 0.5;
+
+ const velocities = [];
+ for (let i = 1; i < scrollEvents.length; i++) {
+ const timeDiff = new Date(scrollEvents[i].timestamp) - new Date(scrollEvents[i-1].timestamp);
+ if (timeDiff > 0) {
+ const velocity = Math.abs(scrollEvents[i].metadata?.scrollDelta || 0) / timeDiff;
+ velocities.push(velocity);
+ }
+ }
+
+ if (velocities.length === 0) return 0.5;
+
+ const mean = velocities.reduce((a, b) => a + b, 0) / velocities.length;
+ const variance = velocities.reduce((sum, velocity) => sum + Math.pow(velocity - mean, 2), 0) / velocities.length;
+
+ // Smoothness is inverse of velocity variance
+ return Math.max(0, 1 - (variance / (mean * mean)));
+ }
+
+ /**
+ * Analyze session for bot-like behavior
+ * @param {string} sessionId - Session identifier
+ * @returns {object} Analysis results
+ */
+ analyzeSession(sessionId) {
+ try {
+ const session = this.activeSessions.get(sessionId);
+ if (!session) {
+ return { error: 'Session not found' };
+ }
+
+ // Calculate behavioral features
+ const features = this.extractBehavioralFeatures(session);
+
+ // Generate behavioral hash
+ const behavioralHash = this.generateBehavioralHash(features);
+ session.behavioralHash = behavioralHash;
+
+ // Run ML classifier if available
+ let botScore = 0.5; // Default neutral score
+ let confidence = 0;
+
+ if (this.mlModel && this.config.classifier.enabled) {
+ const prediction = this.mlModel.predict(features);
+ botScore = prediction.score;
+ confidence = prediction.confidence;
+ } else {
+ // Fallback to rule-based analysis
+ botScore = this.ruleBasedBotDetection(features);
+ }
+
+ // Update session
+ session.botScore = botScore;
+ session.riskLevel = this.calculateRiskLevel(botScore);
+
+ // Apply thresholds
+ const flagged = botScore >= this.config.thresholds.botScoreThreshold;
+ const throttled = botScore >= this.config.thresholds.throttlingThreshold;
+
+ session.isFlagged = flagged;
+ session.isThrottled = throttled;
+
+ // Update database
+ this.database.db.prepare(`
+ UPDATE behavioral_sessions
+ SET bot_score = ?, risk_level = ?, is_flagged = ?, is_throttled = ?,
+ behavioral_hash = ?, total_events = ?
+ WHERE session_id = ?
+ `).run(
+ botScore,
+ session.riskLevel,
+ flagged ? 1 : 0,
+ throttled ? 1 : 0,
+ behavioralHash,
+ session.totalEvents,
+ sessionId
+ );
+
+ // Apply throttling if needed
+ if (throttled) {
+ this.applySessionThrottling(sessionId);
+ }
+
+ // Add to watch list if high risk
+ if (botScore >= this.config.thresholds.watchListThreshold) {
+ this.addToWatchList(sessionId, botScore, 'High bot score detected');
+ }
+
+ logger.info('Session analysis completed', {
+ sessionId,
+ botScore,
+ riskLevel: session.riskLevel,
+ flagged,
+ throttled,
+ eventCount: session.totalEvents
+ });
+
+ return {
+ sessionId,
+ botScore,
+ riskLevel: session.riskLevel,
+ flagged,
+ throttled,
+ confidence,
+ features,
+ behavioralHash
+ };
+
+ } catch (error) {
+ logger.error('Failed to analyze session', {
+ sessionId,
+ error: error.message
+ });
+
+ return {
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Extract behavioral features from session data
+ * @param {object} session - Session data
+ * @returns {object} Behavioral features
+ */
+ extractBehavioralFeatures(session) {
+ const events = session.events || [];
+
+ // Basic session metrics
+ const sessionDuration = new Date() - new Date(session.startTime);
+ const eventsPerMinute = events.length / (sessionDuration / 60000);
+
+ // Mouse movement features
+ const mouseEvents = events.filter(e => e.eventType === 'mousemove');
+ const mouseSpeeds = mouseEvents.map(e => e.movementSpeed).filter(s => s !== null);
+ const avgMouseSpeed = mouseSpeeds.length > 0 ? mouseSpeeds.reduce((a, b) => a + b, 0) / mouseSpeeds.length : 0;
+ const mouseSpeedVariance = this.calculateVariance(mouseSpeeds);
+
+ // Click features
+ const clickEvents = events.filter(e => e.eventType === 'click');
+ const clickIntervals = this.calculateClickIntervals(clickEvents);
+ const avgClickInterval = clickIntervals.length > 0 ? clickIntervals.reduce((a, b) => a + b, 0) / clickIntervals.length : 0;
+
+ // Typing features
+ const keyEvents = events.filter(e => e.eventType === 'keydown');
+ const typingConsistency = this.calculateTypingConsistency(keyEvents);
+
+ // Scroll features
+ const scrollEvents = events.filter(e => e.eventType === 'scroll');
+ const scrollSmoothness = this.calculateScrollSmoothness(scrollEvents);
+
+ // Pattern features
+ const clickPatterns = clickEvents.map(e => e.clickPattern);
+ const rapidClicks = clickPatterns.filter(p => p === 'rapid_click').length;
+ const delayedClicks = clickPatterns.filter(p => p === 'delayed_click').length;
+
+ return {
+ sessionDuration,
+ eventsPerMinute,
+ avgMouseSpeed,
+ mouseSpeedVariance,
+ avgClickInterval,
+ typingConsistency,
+ scrollSmoothness,
+ rapidClicks,
+ delayedClicks,
+ totalEvents: events.length,
+ mouseEvents: mouseEvents.length,
+ clickEvents: clickEvents.length,
+ keyEvents: keyEvents.length,
+ scrollEvents: scrollEvents.length
+ };
+ }
+
+ /**
+ * Rule-based bot detection fallback
+ * @param {object} features - Behavioral features
+ * @returns {number} Bot score (0-1)
+ */
+ ruleBasedBotDetection(features) {
+ let score = 0;
+ let factors = 0;
+
+ // High events per minute
+ if (features.eventsPerMinute > 100) {
+ score += 0.3;
+ factors++;
+ }
+
+ // Low mouse speed variance (robotic movement)
+ if (features.mouseSpeedVariance < 0.1 && features.avgMouseSpeed > 0) {
+ score += 0.2;
+ factors++;
+ }
+
+ // Very consistent typing (robotic)
+ if (features.typingConsistency > 0.95 && features.keyEvents > 10) {
+ score += 0.2;
+ factors++;
+ }
+
+ // Rapid clicking patterns
+ if (features.rapidClicks > features.clickEvents * 0.5) {
+ score += 0.2;
+ factors++;
+ }
+
+ // No natural delays
+ if (features.delayedClicks === 0 && features.clickEvents > 5) {
+ score += 0.1;
+ factors++;
+ }
+
+ // Normalize score
+ return factors > 0 ? score / factors : 0.5;
+ }
+
+ /**
+ * Calculate variance of an array
+ * @param {array} values - Array of numbers
+ * @returns {number} Variance
+ */
+ calculateVariance(values) {
+ if (values.length === 0) return 0;
+
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ return values.reduce((sum, value) => sum + Math.pow(value - mean, 2), 0) / values.length;
+ }
+
+ /**
+ * Calculate click intervals
+ * @param {array} clickEvents - Click events
+ * @returns {array} Click intervals in milliseconds
+ */
+ calculateClickIntervals(clickEvents) {
+ const intervals = [];
+ for (let i = 1; i < clickEvents.length; i++) {
+ const interval = new Date(clickEvents[i].timestamp) - new Date(clickEvents[i-1].timestamp);
+ intervals.push(interval);
+ }
+ return intervals;
+ }
+
+ /**
+ * Calculate risk level from bot score
+ * @param {number} botScore - Bot score (0-1)
+ * @returns {string} Risk level
+ */
+ calculateRiskLevel(botScore) {
+ if (botScore >= 0.9) return 'critical';
+ if (botScore >= 0.75) return 'high';
+ if (botScore >= 0.5) return 'medium';
+ if (botScore >= 0.25) return 'low';
+ return 'minimal';
+ }
+
+ /**
+ * Generate behavioral hash
+ * @param {object} features - Behavioral features
+ * @returns {string} Behavioral hash
+ */
+ generateBehavioralHash(features) {
+ const hashData = {
+ eventsPerMinute: Math.round(features.eventsPerMinute),
+ avgMouseSpeed: Math.round(features.avgMouseSpeed * 100) / 100,
+ typingConsistency: Math.round(features.typingConsistency * 100) / 100,
+ scrollSmoothness: Math.round(features.scrollSmoothness * 100) / 100,
+ rapidClicks: features.rapidClicks,
+ totalEvents: features.totalEvents
+ };
+
+ return this.hashData(JSON.stringify(hashData));
+ }
+
+ /**
+ * Generate user fingerprint
+ * @param {object} sessionData - Session data
+ * @returns {string} User fingerprint
+ */
+ generateUserFingerprint(sessionData) {
+ const fingerprintData = {
+ userAgent: sessionData.userAgent || '',
+ viewport: sessionData.viewport || '',
+ platform: sessionData.platform || '',
+ language: sessionData.language || '',
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
+ };
+
+ return this.hashData(JSON.stringify(fingerprintData));
+ }
+
+ /**
+ * Hash data with salt
+ * @param {string} data - Data to hash
+ * @returns {string} Hashed data
+ */
+ hashData(data) {
+ return crypto
+ .createHash('sha256')
+ .update(data + this.config.collection.hashSalt)
+ .digest('hex');
+ }
+
+ /**
+ * Apply session throttling
+ * @param {string} sessionId - Session identifier
+ */
+ applySessionThrottling(sessionId) {
+ const session = this.activeSessions.get(sessionId);
+ if (!session) return;
+
+ // Add throttling metadata
+ session.throttledAt = new Date().toISOString();
+ session.throttlingLevel = this.calculateThrottlingLevel(session.botScore);
+
+ logger.warn('Session throttling applied', {
+ sessionId,
+ botScore: session.botScore,
+ throttlingLevel: session.throttlingLevel
+ });
+
+ // Update database
+ this.database.db.prepare(`
+ UPDATE behavioral_sessions
+ SET is_throttled = 1, metadata_json = json_patch(metadata_json, ?)
+ WHERE session_id = ?
+ `).run(
+ JSON.stringify({ throttledAt: session.throttledAt, throttlingLevel: session.throttlingLevel }),
+ sessionId
+ );
+ }
+
+ /**
+ * Calculate throttling level
+ * @param {number} botScore - Bot score
+ * @returns {number} Throttling level (0.1-1.0)
+ */
+ calculateThrottlingLevel(botScore) {
+ if (botScore >= 0.9) return 0.1; // 90% throttling
+ if (botScore >= 0.8) return 0.3; // 70% throttling
+ if (botScore >= 0.7) return 0.5; // 50% throttling
+ if (botScore >= 0.6) return 0.7; // 30% throttling
+ return 1.0; // No throttling
+ }
+
+ /**
+ * Add session to high-risk watch list
+ * @param {string} sessionId - Session identifier
+ * @param {number} riskScore - Risk score
+ * @param {string} reason - Reason for addition
+ */
+ addToWatchList(sessionId, riskScore, reason) {
+ const session = this.activeSessions.get(sessionId);
+ if (!session) return;
+
+ // Get stellar address from session metadata
+ const stellarAddress = session.metadata?.stellarAddress;
+ if (!stellarAddress) {
+ logger.warn('No stellar address found for watch list addition', {
+ sessionId,
+ riskScore,
+ reason
+ });
+ return;
+ }
+
+ const watchListId = `watch_${Date.now()}_${crypto.randomBytes(8).toString('hex')}`;
+ const expiresAt = new Date(Date.now() + (30 * 24 * 60 * 60 * 1000)).toISOString(); // 30 days
+
+ this.database.db.prepare(`
+ INSERT INTO high_risk_watch_list (
+ id, stellar_address, reason, risk_score, session_id, added_at, expires_at, metadata_json
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ `).run(
+ watchListId,
+ stellarAddress,
+ reason,
+ riskScore,
+ sessionId,
+ new Date().toISOString(),
+ expiresAt,
+ JSON.stringify({
+ sessionId,
+ botScore: session.botScore,
+ riskLevel: session.riskLevel,
+ fingerprint: session.userFingerprint
+ })
+ );
+
+ logger.warn('Added to high-risk watch list', {
+ watchListId,
+ stellarAddress: this.hashData(stellarAddress), // Hash for privacy
+ riskScore,
+ reason,
+ sessionId
+ });
+ }
+
+ /**
+ * Check if address is on watch list
+ * @param {string} stellarAddress - Stellar address to check
+ * @returns {object} Watch list status
+ */
+ checkWatchList(stellarAddress) {
+ const record = this.database.db.prepare(`
+ SELECT * FROM high_risk_watch_list
+ WHERE stellar_address = ? AND is_active = 1 AND (expires_at IS NULL OR expires_at > ?)
+ `).get(stellarAddress, new Date().toISOString());
+
+ if (!record) {
+ return { onWatchList: false };
+ }
+
+ return {
+ onWatchList: true,
+ watchListId: record.id,
+ riskScore: record.risk_score,
+ reason: record.reason,
+ addedAt: record.added_at,
+ expiresAt: record.expires_at,
+ metadata: JSON.parse(record.metadata_json || '{}')
+ };
+ }
+
+ /**
+ * End session tracking
+ * @param {string} sessionId - Session identifier
+ * @returns {object} Session summary
+ */
+ endSession(sessionId) {
+ try {
+ const session = this.activeSessions.get(sessionId);
+ if (!session) {
+ return { error: 'Session not found' };
+ }
+
+ // Final analysis
+ const analysis = this.analyzeSession(sessionId);
+
+ // Update session end time
+ session.endTime = new Date().toISOString();
+
+ // Update database
+ this.database.db.prepare(`
+ UPDATE behavioral_sessions
+ SET end_time = ?, bot_score = ?, risk_level = ?, is_flagged = ?, is_throttled = ?
+ WHERE session_id = ?
+ `).run(
+ session.endTime,
+ session.botScore,
+ session.riskLevel,
+ session.isFlagged ? 1 : 0,
+ session.isThrottled ? 1 : 0,
+ sessionId
+ );
+
+ // Remove from active sessions
+ this.activeSessions.delete(sessionId);
+
+ logger.info('Session ended', {
+ sessionId,
+ duration: new Date(session.endTime) - new Date(session.startTime),
+ botScore: session.botScore,
+ riskLevel: session.riskLevel,
+ totalEvents: session.totalEvents
+ });
+
+ return {
+ sessionId,
+ duration: new Date(session.endTime) - new Date(session.startTime),
+ botScore: session.botScore,
+ riskLevel: session.riskLevel,
+ totalEvents: session.totalEvents,
+ flagged: session.isFlagged,
+ throttled: session.isThrottled,
+ analysis
+ };
+
+ } catch (error) {
+ logger.error('Failed to end session', {
+ sessionId,
+ error: error.message
+ });
+
+ return {
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Clean up expired sessions
+ */
+ cleanupExpiredSessions() {
+ const now = Date.now();
+ const expiredSessions = [];
+
+ for (const [sessionId, session] of this.activeSessions.entries()) {
+ const sessionAge = now - new Date(session.startTime).getTime();
+ if (sessionAge > this.config.collection.sessionTimeout) {
+ expiredSessions.push(sessionId);
+ }
+ }
+
+ expiredSessions.forEach(sessionId => {
+ this.endSession(sessionId);
+ });
+
+ if (expiredSessions.length > 0) {
+ logger.debug('Cleaned up expired sessions', {
+ count: expiredSessions.length
+ });
+ }
+ }
+
+ /**
+ * Get behavioral analytics
+ * @param {object} options - Analytics options
+ * @returns {object} Analytics data
+ */
+ getBehavioralAnalytics(options = {}) {
+ const {
+ period = '24h',
+ includeDetails = false
+ } = options;
+
+ const startDate = this.getStartDate(period);
+
+ // Session analytics
+ const sessionStats = this.database.db.prepare(`
+ SELECT
+ COUNT(*) as total_sessions,
+ AVG(bot_score) as avg_bot_score,
+ MAX(bot_score) as max_bot_score,
+ COUNT(CASE WHEN is_flagged = 1 THEN 1 END) as flagged_sessions,
+ COUNT(CASE WHEN is_throttled = 1 THEN 1 END) as throttled_sessions,
+ risk_level,
+ COUNT(*) as count
+ FROM behavioral_sessions
+ WHERE start_time > ?
+ GROUP BY risk_level
+ `).all(startDate.toISOString());
+
+ // Risk distribution
+ const riskDistribution = sessionStats.reduce((acc, row) => {
+ acc[row.risk_level] = row.count;
+ return acc;
+ }, {});
+
+ // Top flagged sessions
+ const topFlaggedSessions = this.database.db.prepare(`
+ SELECT session_id, bot_score, risk_level, total_events, start_time
+ FROM behavioral_sessions
+ WHERE start_time > ? AND is_flagged = 1
+ ORDER BY bot_score DESC
+ LIMIT 10
+ `).all(startDate.toISOString());
+
+ // Watch list analytics
+ const watchListStats = this.database.db.prepare(`
+ SELECT
+ COUNT(*) as total_entries,
+ AVG(risk_score) as avg_risk_score,
+ COUNT(CASE WHERE expires_at > ? THEN 1 END) as active_entries
+ FROM high_risk_watch_list
+ `).get(startDate.toISOString(), new Date().toISOString());
+
+ return {
+ period,
+ timestamp: new Date().toISOString(),
+ sessionStats: {
+ totalSessions: sessionStats.reduce((sum, row) => sum + row.count, 0),
+ avgBotScore: sessionStats.reduce((sum, row) => sum + (row.avg_bot_score * row.count), 0) / sessionStats.reduce((sum, row) => sum + row.count, 0) || 0,
+ maxBotScore: Math.max(...sessionStats.map(row => row.max_bot_score)),
+ flaggedSessions: sessionStats.reduce((sum, row) => sum + row.flagged_sessions, 0),
+ throttledSessions: sessionStats.reduce((sum, row) => sum + row.throttled_sessions, 0)
+ },
+ riskDistribution,
+ topFlaggedSessions,
+ watchList: watchListStats,
+ activeSessions: this.activeSessions.size
+ };
+ }
+
+ /**
+ * Get start date for period
+ * @param {string} period - Period string
+ * @returns {Date} Start date
+ */
+ getStartDate(period) {
+ const now = new Date();
+ switch (period) {
+ case '1h':
+ return new Date(now.getTime() - 60 * 60 * 1000);
+ case '24h':
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ case '7d':
+ return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
+ case '30d':
+ return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
+ default:
+ return new Date(now.getTime() - 24 * 60 * 60 * 1000);
+ }
+ }
+
+ /**
+ * Get service statistics
+ * @returns {object} Service statistics
+ */
+ getServiceStats() {
+ return {
+ config: this.config,
+ activeSessions: this.activeSessions.size,
+ modelStats: this.mlModel ? this.modelStats : null,
+ databaseStats: {
+ totalSessions: this.database.db.prepare('SELECT COUNT(*) FROM behavioral_sessions').get()['COUNT(*)'],
+ totalEvents: this.database.db.prepare('SELECT COUNT(*) FROM behavioral_events').get()['COUNT(*)'],
+ watchListEntries: this.database.db.prepare('SELECT COUNT(*) FROM high_risk_watch_list WHERE is_active = 1').get()['COUNT(*)']
+ }
+ };
+ }
+
+ /**
+ * Clean up old data for privacy compliance
+ */
+ cleanupOldData() {
+ try {
+ const cutoffDate = new Date(Date.now() - (this.config.privacy.dataRetentionDays * 24 * 60 * 60 * 1000));
+
+ // Clean up old sessions
+ const deletedSessions = this.database.db.prepare(`
+ DELETE FROM behavioral_sessions WHERE start_time < ?
+ `).run(cutoffDate.toISOString());
+
+ // Clean up old events
+ const deletedEvents = this.database.db.prepare(`
+ DELETE FROM behavioral_events WHERE created_at < ?
+ `).run(cutoffDate.toISOString());
+
+ // Clean up expired watch list entries
+ const deletedWatchList = this.database.db.prepare(`
+ DELETE FROM high_risk_watch_list WHERE expires_at < ?
+ `).run(cutoffDate.toISOString());
+
+ logger.info('Cleaned up old behavioral data', {
+ deletedSessions: deletedSessions.changes,
+ deletedEvents: deletedEvents.changes,
+ deletedWatchList: deletedWatchList.changes,
+ cutoffDate: cutoffDate.toISOString()
+ });
+
+ } catch (error) {
+ logger.error('Failed to cleanup old behavioral data', {
+ error: error.message
+ });
+ }
+ }
+
+ /**
+ * Stop the service
+ */
+ stop() {
+ if (this.sessionCleanupInterval) {
+ clearInterval(this.sessionCleanupInterval);
+ }
+
+ // End all active sessions
+ for (const sessionId of this.activeSessions.keys()) {
+ this.endSession(sessionId);
+ }
+
+ logger.info('Behavioral biometric service stopped');
+ }
+}
+
+module.exports = { BehavioralBiometricService };
diff --git a/src/services/botDetectionClassifier.js b/src/services/botDetectionClassifier.js
new file mode 100644
index 0000000..1420322
--- /dev/null
+++ b/src/services/botDetectionClassifier.js
@@ -0,0 +1,569 @@
+const { logger } = require('../utils/logger');
+
+/**
+ * Simple Machine Learning Classifier for Bot Detection
+ * Uses rule-based logic and basic statistical analysis for bot detection
+ */
+class BotDetectionClassifier {
+ constructor(config = {}) {
+ this.config = {
+ // Model configuration
+ modelType: config.modelType || 'rule_based',
+ confidenceThreshold: config.confidenceThreshold || 0.7,
+ trainingThreshold: config.trainingThreshold || 100,
+
+ // Feature weights (tuned based on training data)
+ weights: {
+ eventsPerMinute: config.weights?.eventsPerMinute || 0.3,
+ mouseSpeedVariance: config.weights?.mouseSpeedVariance || 0.25,
+ typingConsistency: config.weights?.typingConsistency || 0.2,
+ rapidClicks: config.weights?.rapidClicks || 0.15,
+ scrollSmoothness: config.weights?.scrollSmoothness || 0.1
+ },
+
+ // Thresholds for individual features
+ thresholds: {
+ highEventsPerMinute: config.thresholds?.highEventsPerMinute || 100,
+ lowMouseSpeedVariance: config.thresholds?.lowMouseSpeedVariance || 0.1,
+ highTypingConsistency: config.thresholds?.highTypingConsistency || 0.95,
+ highRapidClicks: config.thresholds?.highRapidClicks || 0.5,
+ lowScrollSmoothness: config.thresholds?.lowScrollSmoothness || 0.2
+ },
+
+ // Anomaly detection
+ anomalyDetection: {
+ enabled: config.anomalyDetection?.enabled !== false,
+ sensitivity: config.anomalyDetection?.sensitivity || 0.8,
+ windowSize: config.anomalyDetection?.windowSize || 50
+ },
+
+ ...config
+ };
+
+ // Training data storage
+ this.trainingData = [];
+ this.featureStats = this.initializeFeatureStats();
+ this.isTrained = false;
+
+ // Performance tracking
+ this.performance = {
+ totalPredictions: 0,
+ correctPredictions: 0,
+ falsePositives: 0,
+ falseNegatives: 0,
+ lastTrained: null
+ };
+ }
+
+ /**
+ * Initialize feature statistics for normalization
+ * @returns {object} Initial feature statistics
+ */
+ initializeFeatureStats() {
+ return {
+ eventsPerMinute: { mean: 0, std: 0, min: 0, max: 0 },
+ avgMouseSpeed: { mean: 0, std: 0, min: 0, max: 0 },
+ mouseSpeedVariance: { mean: 0, std: 0, min: 0, max: 0 },
+ avgClickInterval: { mean: 0, std: 0, min: 0, max: 0 },
+ typingConsistency: { mean: 0, std: 0, min: 0, max: 0 },
+ scrollSmoothness: { mean: 0, std: 0, min: 0, max: 0 },
+ rapidClicks: { mean: 0, std: 0, min: 0, max: 0 },
+ delayedClicks: { mean: 0, std: 0, min: 0, max: 0 }
+ };
+ }
+
+ /**
+ * Train the classifier with labeled data
+ * @param {Array} data - Training data with features and labels
+ */
+ train(data) {
+ try {
+ logger.info('Training bot detection classifier', {
+ dataSize: data.length,
+ modelType: this.config.modelType
+ });
+
+ // Store training data
+ this.trainingData = data;
+
+ // Calculate feature statistics
+ this.calculateFeatureStats(data);
+
+ // Train the model
+ if (this.config.modelType === 'rule_based') {
+ this.trainRuleBasedModel(data);
+ } else if (this.config.modelType === 'statistical') {
+ this.trainStatisticalModel(data);
+ }
+
+ this.isTrained = true;
+ this.performance.lastTrained = new Date().toISOString();
+
+ logger.info('Classifier training completed', {
+ trained: this.isTrained,
+ featureStats: this.featureStats
+ });
+
+ } catch (error) {
+ logger.error('Failed to train classifier', {
+ error: error.message
+ });
+ throw error;
+ }
+ }
+
+ /**
+ * Calculate feature statistics for normalization
+ * @param {Array} data - Training data
+ */
+ calculateFeatureStats(data) {
+ const features = ['eventsPerMinute', 'avgMouseSpeed', 'mouseSpeedVariance',
+ 'avgClickInterval', 'typingConsistency', 'scrollSmoothness',
+ 'rapidClicks', 'delayedClicks'];
+
+ features.forEach(feature => {
+ const values = data.map(d => d.features[feature] || 0).filter(v => !isNaN(v));
+
+ if (values.length > 0) {
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
+ const std = Math.sqrt(variance);
+
+ this.featureStats[feature] = {
+ mean,
+ std,
+ min: Math.min(...values),
+ max: Math.max(...values)
+ };
+ }
+ });
+ }
+
+ /**
+ * Train rule-based model
+ * @param {Array} data - Training data
+ */
+ trainRuleBasedModel(data) {
+ // Rule-based model uses predefined thresholds
+ // Training helps optimize thresholds based on data
+ this.optimizeThresholds(data);
+ }
+
+ /**
+ * Train statistical model
+ * @param {Array} data - Training data
+ */
+ trainStatisticalModel(data) {
+ // For statistical model, we could implement simple logistic regression
+ // For now, we'll use the rule-based approach with learned thresholds
+ this.optimizeThresholds(data);
+ }
+
+ /**
+ * Optimize thresholds based on training data
+ * @param {Array} data - Training data
+ */
+ optimizeThresholds(data) {
+ // Analyze feature distributions for bot vs human patterns
+ const botData = data.filter(d => d.label === 'bot');
+ const humanData = data.filter(d => d.label === 'human');
+
+ if (botData.length === 0 || humanData.length === 0) {
+ logger.warn('Insufficient labeled data for threshold optimization');
+ return;
+ }
+
+ // Calculate optimal thresholds for each feature
+ const features = ['eventsPerMinute', 'mouseSpeedVariance', 'typingConsistency', 'rapidClicks'];
+
+ features.forEach(feature => {
+ const botValues = botData.map(d => d.features[feature] || 0);
+ const humanValues = humanData.map(d => d.features[feature] || 0);
+
+ // Find threshold that maximizes separation
+ const optimalThreshold = this.findOptimalThreshold(botValues, humanValues);
+
+ if (optimalThreshold !== null) {
+ this.config.thresholds[`high${feature.charAt(0).toUpperCase() + feature.slice(1)}`] = optimalThreshold;
+ }
+ });
+
+ logger.info('Thresholds optimized', {
+ thresholds: this.config.thresholds
+ });
+ }
+
+ /**
+ * Find optimal threshold for feature separation
+ * @param {Array} botValues - Bot feature values
+ * @param {Array} humanValues - Human feature values
+ * @returns {number|null} Optimal threshold
+ */
+ findOptimalThreshold(botValues, humanValues) {
+ const allValues = [...botValues, ...humanValues].sort((a, b) => a - b);
+ let bestThreshold = null;
+ let bestScore = 0;
+
+ for (let i = 0; i < allValues.length - 1; i++) {
+ const threshold = allValues[i];
+
+ // Calculate classification accuracy at this threshold
+ const tp = botValues.filter(v => v >= threshold).length; // True positives
+ const fp = humanValues.filter(v => v >= threshold).length; // False positives
+ const tn = humanValues.filter(v => v < threshold).length; // True negatives
+ const fn = botValues.filter(v => v < threshold).length; // False negatives
+
+ const accuracy = (tp + tn) / (tp + fp + tn + fn);
+
+ if (accuracy > bestScore) {
+ bestScore = accuracy;
+ bestThreshold = threshold;
+ }
+ }
+
+ return bestThreshold;
+ }
+
+ /**
+ * Predict if session is bot-like
+ * @param {object} features - Behavioral features
+ * @returns {object} Prediction result
+ */
+ predict(features) {
+ try {
+ if (!this.isTrained) {
+ // Use default rule-based prediction if not trained
+ return this.ruleBasedPrediction(features);
+ }
+
+ // Normalize features
+ const normalizedFeatures = this.normalizeFeatures(features);
+
+ // Calculate bot score
+ let botScore = 0;
+ let confidence = 0;
+ const factors = [];
+
+ // Events per minute
+ if (normalizedFeatures.eventsPerMinute !== null) {
+ const score = Math.min(normalizedFeatures.eventsPerMinute / 2, 1);
+ botScore += score * this.config.weights.eventsPerMinute;
+ factors.push({ feature: 'eventsPerMinute', score, weight: this.config.weights.eventsPerMinute });
+ }
+
+ // Mouse speed variance (lower variance = more robotic)
+ if (normalizedFeatures.mouseSpeedVariance !== null) {
+ const score = 1 - normalizedFeatures.mouseSpeedVariance; // Invert: lower variance = higher bot score
+ botScore += score * this.config.weights.mouseSpeedVariance;
+ factors.push({ feature: 'mouseSpeedVariance', score, weight: this.config.weights.mouseSpeedVariance });
+ }
+
+ // Typing consistency (higher consistency = more robotic)
+ if (normalizedFeatures.typingConsistency !== null) {
+ const score = normalizedFeatures.typingConsistency;
+ botScore += score * this.config.weights.typingConsistency;
+ factors.push({ feature: 'typingConsistency', score, weight: this.config.weights.typingConsistency });
+ }
+
+ // Rapid clicks
+ if (normalizedFeatures.rapidClicks !== null) {
+ const score = normalizedFeatures.rapidClicks;
+ botScore += score * this.config.weights.rapidClicks;
+ factors.push({ feature: 'rapidClicks', score, weight: this.config.weights.rapidClicks });
+ }
+
+ // Scroll smoothness (lower smoothness = more robotic)
+ if (normalizedFeatures.scrollSmoothness !== null) {
+ const score = 1 - normalizedFeatures.scrollSmoothness; // Invert: lower smoothness = higher bot score
+ botScore += score * this.config.weights.scrollSmoothness;
+ factors.push({ feature: 'scrollSmoothness', score, weight: this.config.weights.scrollSmoothness });
+ }
+
+ // Calculate confidence based on factor agreement
+ confidence = this.calculateConfidence(factors);
+
+ // Apply anomaly detection if enabled
+ if (this.config.anomalyDetection.enabled) {
+ const anomalyScore = this.detectAnomalies(features);
+ if (anomalyScore > this.config.anomalyDetection.sensitivity) {
+ botScore = Math.min(botScore + anomalyScore, 1);
+ confidence = Math.max(confidence, anomalyScore);
+ }
+ }
+
+ // Update performance tracking
+ this.performance.totalPredictions++;
+
+ const prediction = {
+ isBot: botScore >= this.config.confidenceThreshold,
+ botScore: Math.round(botScore * 100) / 100,
+ confidence: Math.round(confidence * 100) / 100,
+ factors,
+ features: normalizedFeatures
+ };
+
+ logger.debug('Bot detection prediction', {
+ botScore: prediction.botScore,
+ confidence: prediction.confidence,
+ isBot: prediction.isBot
+ });
+
+ return prediction;
+
+ } catch (error) {
+ logger.error('Failed to predict bot score', {
+ error: error.message
+ });
+
+ // Fail safe prediction
+ return {
+ isBot: false,
+ botScore: 0.5,
+ confidence: 0,
+ error: error.message
+ };
+ }
+ }
+
+ /**
+ * Rule-based prediction fallback
+ * @param {object} features - Behavioral features
+ * @returns {object} Prediction result
+ */
+ ruleBasedPrediction(features) {
+ let botScore = 0;
+ let factors = [];
+
+ // High events per minute
+ if (features.eventsPerMinute > this.config.thresholds.highEventsPerMinute) {
+ botScore += 0.3;
+ factors.push({ feature: 'eventsPerMinute', score: 0.3, reason: 'High event frequency' });
+ }
+
+ // Low mouse speed variance (robotic movement)
+ if (features.mouseSpeedVariance < this.config.thresholds.lowMouseSpeedVariance && features.avgMouseSpeed > 0) {
+ botScore += 0.25;
+ factors.push({ feature: 'mouseSpeedVariance', score: 0.25, reason: 'Low movement variance' });
+ }
+
+ // Very consistent typing (robotic)
+ if (features.typingConsistency > this.config.thresholds.highTypingConsistency && features.keyEvents > 10) {
+ botScore += 0.2;
+ factors.push({ feature: 'typingConsistency', score: 0.2, reason: 'Too consistent typing' });
+ }
+
+ // Many rapid clicks
+ if (features.rapidClicks > features.clickEvents * 0.5) {
+ botScore += 0.15;
+ factors.push({ feature: 'rapidClicks', score: 0.15, reason: 'Many rapid clicks' });
+ }
+
+ // No natural delays
+ if (features.delayedClicks === 0 && features.clickEvents > 5) {
+ botScore += 0.1;
+ factors.push({ feature: 'delayedClicks', score: 0.1, reason: 'No natural delays' });
+ }
+
+ const confidence = factors.length > 0 ? 0.7 : 0.3; // Moderate confidence for rule-based
+
+ return {
+ isBot: botScore >= this.config.confidenceThreshold,
+ botScore,
+ confidence,
+ factors,
+ method: 'rule_based'
+ };
+ }
+
+ /**
+ * Normalize features using z-score normalization
+ * @param {object} features - Raw features
+ * @returns {object} Normalized features
+ */
+ normalizeFeatures(features) {
+ const normalized = {};
+
+ Object.keys(this.featureStats).forEach(feature => {
+ const stats = this.featureStats[feature];
+ const value = features[feature];
+
+ if (value !== undefined && value !== null && !isNaN(value) && stats.std > 0) {
+ // Z-score normalization
+ normalized[feature] = (value - stats.mean) / stats.std;
+ // Clamp to reasonable range (-3 to 3)
+ normalized[feature] = Math.max(-3, Math.min(3, normalized[feature]));
+ // Convert to 0-1 scale
+ normalized[feature] = (normalized[feature] + 3) / 6;
+ } else {
+ normalized[feature] = null;
+ }
+ });
+
+ return normalized;
+ }
+
+ /**
+ * Calculate prediction confidence
+ * @param {Array} factors - Feature factors
+ * @returns {number} Confidence score (0-1)
+ */
+ calculateConfidence(factors) {
+ if (factors.length === 0) return 0;
+
+ // Calculate agreement between factors
+ const scores = factors.map(f => f.score);
+ const mean = scores.reduce((a, b) => a + b, 0) / scores.length;
+ const variance = scores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / scores.length;
+
+ // Higher confidence when factors agree (low variance)
+ const agreement = 1 - Math.min(variance, 1);
+
+ // Adjust confidence based on number of factors
+ const factorBonus = Math.min(factors.length / 5, 1); // Bonus for more factors
+
+ return Math.min(agreement + factorBonus, 1);
+ }
+
+ /**
+ * Detect anomalies in behavioral patterns
+ * @param {object} features - Behavioral features
+ * @returns {number} Anomaly score (0-1)
+ */
+ detectAnomalies(features) {
+ let anomalyScore = 0;
+ let anomalies = [];
+
+ // Check for unusual patterns
+ if (features.eventsPerMinute > 200) {
+ anomalyScore += 0.3;
+ anomalies.push('Extremely high event frequency');
+ }
+
+ if (features.mouseSpeedVariance === 0 && features.avgMouseSpeed > 0) {
+ anomalyScore += 0.2;
+ anomalies.push('Perfectly consistent mouse movement');
+ }
+
+ if (features.typingConsistency === 1 && features.keyEvents > 20) {
+ anomalyScore += 0.2;
+ anomalies.push('Perfectly consistent typing');
+ }
+
+ if (features.rapidClicks === features.clickEvents && features.clickEvents > 10) {
+ anomalyScore += 0.2;
+ anomalies.push('All clicks are rapid');
+ }
+
+ if (features.scrollSmoothness === 0 && features.scrollEvents > 5) {
+ anomalyScore += 0.1;
+ anomalies.push('Perfectly linear scrolling');
+ }
+
+ // Log anomalies for debugging
+ if (anomalies.length > 0) {
+ logger.debug('Behavioral anomalies detected', {
+ anomalies,
+ anomalyScore,
+ features
+ });
+ }
+
+ return anomalyScore;
+ }
+
+ /**
+ * Update model with new training data
+ * @param {object} features - Behavioral features
+ * @param {boolean} isBot - True if this is bot behavior
+ */
+ updateModel(features, isBot) {
+ const trainingExample = {
+ features,
+ label: isBot ? 'bot' : 'human',
+ timestamp: new Date().toISOString()
+ };
+
+ this.trainingData.push(trainingExample);
+
+ // Retrain if we have enough new data
+ if (this.trainingData.length >= this.config.trainingThreshold) {
+ this.train(this.trainingData);
+ }
+ }
+
+ /**
+ * Get model performance statistics
+ * @returns {object} Performance stats
+ */
+ getPerformanceStats() {
+ const accuracy = this.performance.totalPredictions > 0 ?
+ this.performance.correctPredictions / this.performance.totalPredictions : 0;
+
+ const precision = (this.performance.correctPredictions - this.performance.falsePositives) > 0 ?
+ (this.performance.correctPredictions - this.performance.falsePositives) / this.performance.correctPredictions : 0;
+
+ const recall = (this.performance.correctPredictions - this.performance.falseNegatives) > 0 ?
+ (this.performance.correctPredictions - this.performance.falseNegatives) / this.performance.correctPredictions : 0;
+
+ return {
+ ...this.performance,
+ accuracy: Math.round(accuracy * 100) / 100,
+ precision: Math.round(precision * 100) / 100,
+ recall: Math.round(recall * 100) / 100,
+ f1Score: precision > 0 && recall > 0 ? Math.round(2 * (precision * recall) / (precision + recall) * 100) / 100 : 0
+ };
+ }
+
+ /**
+ * Reset model performance tracking
+ */
+ resetPerformanceStats() {
+ this.performance = {
+ totalPredictions: 0,
+ correctPredictions: 0,
+ falsePositives: 0,
+ falseNegatives: 0,
+ lastTrained: this.performance.lastTrained
+ };
+ }
+
+ /**
+ * Export model configuration
+ * @returns {object} Model configuration
+ */
+ exportModel() {
+ return {
+ config: this.config,
+ featureStats: this.featureStats,
+ isTrained: this.isTrained,
+ performance: this.getPerformanceStats(),
+ trainingDataSize: this.trainingData.length
+ };
+ }
+
+ /**
+ * Import model configuration
+ * @param {object} modelData - Model data to import
+ */
+ importModel(modelData) {
+ try {
+ this.config = { ...this.config, ...modelData.config };
+ this.featureStats = modelData.featureStats || this.featureStats;
+ this.isTrained = modelData.isTrained || false;
+ this.performance = modelData.performance || this.performance;
+
+ logger.info('Model imported successfully', {
+ isTrained: this.isTrained,
+ configType: this.config.modelType
+ });
+
+ } catch (error) {
+ logger.error('Failed to import model', {
+ error: error.message
+ });
+ throw error;
+ }
+ }
+}
+
+module.exports = { BotDetectionClassifier };