From 372a1abfb0e99a871c379c632d4064c6f2f5cbeb Mon Sep 17 00:00:00 2001 From: tebrihk Date: Sat, 28 Mar 2026 16:17:37 +0100 Subject: [PATCH] Add Behavioral_Biometric_Fraud_Detection --- .env.example | 33 + behavioralBiometric.test.js | 835 ++++++++++++++ index.js | 68 +- public/js/behavioralTracker.js | 870 +++++++++++++++ routes/behavioralBiometric.js | 648 +++++++++++ src/config.js | 82 +- src/services/behavioralBiometricService.js | 1135 ++++++++++++++++++++ src/services/botDetectionClassifier.js | 569 ++++++++++ 8 files changed, 4226 insertions(+), 14 deletions(-) create mode 100644 behavioralBiometric.test.js create mode 100644 public/js/behavioralTracker.js create mode 100644 routes/behavioralBiometric.js create mode 100644 src/services/behavioralBiometricService.js create mode 100644 src/services/botDetectionClassifier.js diff --git a/.env.example b/.env.example index b62c663..f3260ff 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 f3c7556..e05abb5 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'); @@ -28,6 +34,8 @@ const createVideoRoutes = require('./routes/video'); 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'); @@ -66,7 +74,7 @@ function createApp(dependencies = {}) { notificationService, emailUtil: { sendEmail }, }); - dependencies.subscriptionService || new SubscriptionService({ database, auditLogService, config }); + dependencies.subscriptionService || new SubscriptionService({ database, auditLogService, config }); const subscriptionExpiryChecker = dependencies.subscriptionExpiryChecker || new SubscriptionExpiryChecker({ @@ -141,14 +149,14 @@ function createApp(dependencies = {}) { app.use(cors()); app.use(express.json()); - + // Add request tracing middleware for structured logging app.use(requestTracingMiddleware); // Subscription events webhook app.use('/api/subscription', require('./routes/subscription')); // Payouts API app.use('/api/payouts', require('./routes/payouts')); - + // Global stats endpoints app.use('/api/global-stats', createGlobalStatsRouter({ database, globalStatsService })); @@ -403,6 +411,22 @@ function createApp(dependencies = {}) { // API Documentation with Swagger UI app.use('/api/docs', createSwaggerRoutes); + // IP Intelligence management routes + if (ipIntelligenceService) { + app.use('/api/ip-intelligence', createIPIntelligenceRoutes({ + ipIntelligenceService, + ipBlockingService, + ipMonitoringService + })); + } + + // Behavioral biometric management routes + if (behavioralService) { + app.use('/api/behavioral', createBehavioralBiometricRoutes({ + behavioralService + })); + } + // Health check endpoint app.get('/health', async (req, res) => { const health = { @@ -414,6 +438,8 @@ function createApp(dependencies = {}) { redis: 'Unknown', rabbitmq: 'Unknown', stellar: 'Unknown', + ipIntelligence: 'Unknown', + behavioralBiometric: 'Unknown' }, }; @@ -470,6 +496,32 @@ function createApp(dependencies = {}) { isDegraded = true; } + // Check IP Intelligence + try { + if (ipIntelligenceService) { + const stats = ipIntelligenceService.getServiceStats(); + health.services.ipIntelligence = 'Running'; + } else { + health.services.ipIntelligence = 'Not Configured'; + } + } catch (error) { + health.services.ipIntelligence = 'Error'; + isDegraded = true; + } + + // Check Behavioral Biometric + try { + if (behavioralService) { + const stats = behavioralService.getServiceStats(); + health.services.behavioralBiometric = 'Running'; + } else { + health.services.behavioralBiometric = 'Not Configured'; + } + } catch (error) { + health.services.behavioralBiometric = 'Error'; + isDegraded = true; + } + if (isDegraded) { health.status = 'Degraded'; } @@ -478,7 +530,7 @@ function createApp(dependencies = {}) { }); app.use((req, res) => res.status(404).json({ success: false, error: 'Not found' })); - + // Global error handler with Sentry integration app.use((err, req, res, next) => { // Log error with structured logging @@ -489,15 +541,15 @@ function createApp(dependencies = {}) { walletAddress: req.user?.publicKey || req.body?.walletAddress, endpoint: req.originalUrl, }; - + // Capture with Sentry errorTracking.captureException(err, errorContext); - + // Return error response res.status(err.statusCode || err.status || 500).json({ success: false, - error: process.env.NODE_ENV === 'production' - ? 'Internal server error' + error: process.env.NODE_ENV === 'production' + ? 'Internal server error' : err.message, ...(process.env.NODE_ENV !== 'production' && { stack: err.stack }), }); 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}\n`; + } else if (typeof value === 'object') { + xml += `${spaces}<${key}>\n`; + xml += objectToXML(value, indent + 1); + xml += `${spaces}\n`; + } else { + xml += `${spaces}<${key}>${value}\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 d52e650..e06ba90 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'); @@ -63,6 +64,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', @@ -75,11 +146,10 @@ 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', - }, + } }; -} -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 };