From 5bc6d99d6c99b372765251a40cfc035633a451db Mon Sep 17 00:00:00 2001 From: tebrihk Date: Sat, 28 Mar 2026 15:59:51 +0100 Subject: [PATCH] Implement IP_Intelligence_and_VPN_Detection --- .env.example | 38 + IP_INTELLIGENCE_README.md | 546 ++++++++++++ index.js | 117 ++- ipIntelligence.test.js | 687 +++++++++++++++ routes/ipIntelligence.js | 797 +++++++++++++++++ src/config.js | 54 ++ src/middleware/ipIntelligenceMiddleware.js | 650 ++++++++++++++ src/services/ipBlockingService.js | 928 ++++++++++++++++++++ src/services/ipIntelligenceService.js | 953 +++++++++++++++++++++ src/services/ipMonitoringService.js | 844 ++++++++++++++++++ 10 files changed, 5601 insertions(+), 13 deletions(-) create mode 100644 IP_INTELLIGENCE_README.md create mode 100644 ipIntelligence.test.js create mode 100644 routes/ipIntelligence.js create mode 100644 src/middleware/ipIntelligenceMiddleware.js create mode 100644 src/services/ipBlockingService.js create mode 100644 src/services/ipIntelligenceService.js create mode 100644 src/services/ipMonitoringService.js diff --git a/.env.example b/.env.example index b62c663..9005d0a 100644 --- a/.env.example +++ b/.env.example @@ -100,3 +100,41 @@ GLOBAL_STATS_INITIAL_DELAY=5000 # Predictive Churn Analysis Configuration CHURN_ANALYSIS_INTERVAL=3600000 + +# IP Intelligence and VPN Detection Configuration +IP_INTELLIGENCE_ENABLED=false + +# IP Intelligence Providers (enable at least one for production) +IPINFO_ENABLED=false +IPINFO_API_KEY=your-ipinfo-api-key +IPINFO_TIMEOUT=5000 + +MAXMIND_ENABLED=false +MAXMIND_API_KEY=your-maxmind-api-key +MAXMIND_TIMEOUT=5000 + +ABUSEIPDB_ENABLED=false +ABUSEIPDB_API_KEY=your-abuseipdb-api-key +ABUSEIPDB_TIMEOUT=5000 + +IPQUALITYSCORE_ENABLED=false +IPQUALITYSCORE_API_KEY=your-ipqualityscore-api-key +IPQUALITYSCORE_TIMEOUT=5000 + +# IP Risk Thresholds (0-100 scale) +IP_RISK_THRESHOLD_LOW=30 +IP_RISK_THRESHOLD_MEDIUM=60 +IP_RISK_THRESHOLD_HIGH=80 +IP_RISK_THRESHOLD_CRITICAL=90 + +# IP Intelligence Cache Configuration +IP_CACHE_ENABLED=true +IP_CACHE_TTL_MS=3600000 +IP_CACHE_MAX_SIZE=10000 + +# IP Intelligence Rate Limiting +IP_RATE_LIMIT_PER_MINUTE=100 +IP_RATE_LIMIT_BURST=20 + +# Security Alert Configuration +SECURITY_ALERT_EMAIL=security@yourdomain.com diff --git a/IP_INTELLIGENCE_README.md b/IP_INTELLIGENCE_README.md new file mode 100644 index 0000000..c269b5c --- /dev/null +++ b/IP_INTELLIGENCE_README.md @@ -0,0 +1,546 @@ +# IP Intelligence and VPN Detection System + +## Overview + +The IP Intelligence and VPN Detection system provides active defense against fraudsters who hide behind VPNs, Tor, and other high-risk IP addresses. This system addresses issues #78 and #43 by integrating multiple IP intelligence providers to assess risk scores for every connecting IP during the Sign-In With Stellar (SIWS) flow and other critical operations. + +## ๐ŸŽฏ Key Features + +### **Multi-Provider Intelligence** +- **IPInfo.io** - Comprehensive IP geolocation and VPN detection +- **MaxMind** - Advanced geolocation and risk assessment +- **AbuseIPDB** - Community-driven abuse reporting and confidence scores +- **IPQualityScore** - Fraud detection, bot detection, and proxy identification + +### **Real-Time Risk Assessment** +- **Risk Scoring** (0-100 scale): Minimal, Low, Medium, High, Critical +- **Pattern Recognition**: Tor exit nodes, VPN providers, data centers, proxy servers +- **Behavioral Analysis**: IP reputation tracking and violation history +- **Geographic Intelligence**: Country-based risk assessment and concentration analysis + +### **Active Defense Mechanisms** +- **Automatic Blocking**: Temporary and permanent IP blocking for high-risk addresses +- **Access Restrictions**: Limit critical actions based on IP risk level +- **Enhanced Rate Limiting**: Dynamic rate limiting based on IP reputation +- **SIWS Protection**: Real-time blocking during authentication flow + +### **Comprehensive Monitoring** +- **Real-time Alerts**: Email notifications for security events +- **Analytics Dashboard**: Detailed risk analytics and trend analysis +- **Audit Trail**: Complete logging of all IP intelligence actions +- **Performance Metrics**: System health and performance monitoring + +## ๐Ÿ—๏ธ Architecture + +### **Core Services** + +#### 1. IPIntelligenceService +```javascript +// Primary service for IP risk assessment +const riskAssessment = await ipIntelligenceService.assessIPRisk('192.168.1.1'); +``` + +**Key Capabilities:** +- Multi-provider data aggregation +- Intelligent caching (1-hour TTL) +- Rate limiting and error handling +- Risk score calculation and normalization + +#### 2. IPIntelligenceMiddleware +```javascript +// Middleware for real-time protection +app.use('/api/creator', ipMiddleware.createCreatorMiddleware()); +app.use('/api/subscription', ipMiddleware.createSIWSMiddleware()); +``` + +**Protection Points:** +- SIWS authentication flow +- Creator channel creation +- High-value withdrawals +- Content uploads +- CDN access + +#### 3. IPBlockingService +```javascript +// Automatic IP blocking and restriction +const blockDecision = await ipBlockingService.evaluateIP(ipAddress, riskAssessment); +``` + +**Blocking Features:** +- Temporary blocks (configurable duration) +- Permanent blocks for critical threats +- Automatic escalation based on violation history +- Manual override capabilities + +#### 4. IPMonitoringService +```javascript +// Continuous monitoring and analytics +const analytics = await ipMonitoringService.getAnalytics({ period: '24h' }); +``` + +**Monitoring Capabilities:** +- Real-time event tracking +- Risk trend analysis +- Geographic distribution analysis +- Automated alerting system + +## ๐Ÿ”ง Configuration + +### **Environment Variables** + +```bash +# Enable IP Intelligence System +IP_INTELLIGENCE_ENABLED=true + +# Provider Configuration +IPINFO_ENABLED=true +IPINFO_API_KEY=your-ipinfo-api-key + +MAXMIND_ENABLED=true +MAXMIND_API_KEY=your-maxmind-api-key + +ABUSEIPDB_ENABLED=true +ABUSEIPDB_API_KEY=your-abuseipdb-api-key + +IPQUALITYSCORE_ENABLED=true +IPQUALITYSCORE_API_KEY=your-ipqualityscore-api-key + +# Risk Thresholds +IP_RISK_THRESHOLD_LOW=30 +IP_RISK_THRESHOLD_MEDIUM=60 +IP_RISK_THRESHOLD_HIGH=80 +IP_RISK_THRESHOLD_CRITICAL=90 + +# Cache Configuration +IP_CACHE_ENABLED=true +IP_CACHE_TTL_MS=3600000 +IP_CACHE_MAX_SIZE=10000 + +# Security Alerts +SECURITY_ALERT_EMAIL=security@yourdomain.com +``` + +### **Risk Level Configuration** + +| Risk Level | Score Range | Actions Allowed | Typical Response | +|------------|-------------|----------------|-----------------| +| Minimal | 0-29 | All actions | Standard processing | +| Low | 30-59 | Most actions | Basic monitoring | +| Medium | 60-79 | Limited actions | Enhanced monitoring | +| High | 80-89 | Restricted actions | Additional verification | +| Critical | 90-100 | No actions | Automatic blocking | + +## ๐Ÿ“Š API Endpoints + +### **Management API** (`/api/ip-intelligence`) + +#### Get Service Statistics +```http +GET /api/ip-intelligence/stats +``` + +#### Assess IP Risk +```http +POST /api/ip-intelligence/assess +Content-Type: application/json + +{ + "ipAddress": "192.168.1.1", + "options": { + "context": "siws_auth", + "userAgent": "Mozilla/5.0..." + } +} +``` + +#### Check IP Block Status +```http +GET /api/ip-intelligence/is-blocked/:ipAddress +``` + +#### Manual IP Blocking +```http +POST /api/ip-intelligence/block +Content-Type: application/json + +{ + "ipAddress": "192.168.1.1", + "type": "temporary", + "duration": 3600000, + "reason": "Manual block by administrator" +} +``` + +#### Get Analytics Dashboard +```http +GET /api/ip-intelligence/dashboard +``` + +#### Get Detailed Analytics +```http +GET /api/ip-intelligence/analytics?period=24h&includeDetails=true +``` + +#### Export Data +```http +GET /api/ip-intelligence/export?period=7d&format=csv&type=all +``` + +### **Response Examples** + +#### Risk Assessment Response +```json +{ + "success": true, + "data": { + "ipAddress": "192.168.1.1", + "riskScore": 75, + "riskLevel": "medium", + "providerScores": { + "ipinfo": { "score": 70, "weight": 0.25 }, + "abuseipdb": { "score": 85, "weight": 0.35 }, + "ipqualityscore": { "score": 72, "weight": 0.25 } + }, + "riskFactors": [ + "VPN detected via IPInfo", + "Recent abuse detected" + ], + "recommendations": [ + "Enhanced monitoring required", + "Rate limiting recommended" + ], + "assessedAt": "2024-01-15T10:30:00.000Z" + } +} +``` + +#### Blocking Decision Response +```json +{ + "success": true, + "data": { + "ipAddress": "192.168.1.1", + "action": "restrict", + "reason": "Elevated risk score (75)", + "duration": 86400000, + "metadata": { + "riskScore": 75, + "riskLevel": "medium", + "blockId": "block_1642248600000_abc123" + } + } +} +``` + +## ๐Ÿ›ก๏ธ Security Features + +### **Real-Time Protection** + +#### SIWS Flow Protection +- Blocks high-risk IPs during authentication +- Requires additional verification for medium-risk IPs +- Maintains audit trail of all authentication attempts + +#### Critical Action Protection +- **Creator Channel Creation**: Blocks high-risk IPs +- **High-Value Withdrawals**: Additional verification required +- **Content Uploads**: Enhanced monitoring for suspicious IPs +- **CDN Access**: Rate limiting based on IP reputation + +### **Automatic Blocking** + +#### Blocking Triggers +- **Risk Score โ‰ฅ 90**: Automatic permanent block +- **Risk Score โ‰ฅ 80**: Temporary block (24 hours default) +- **Multiple Violations**: Escalated blocking +- **Critical Risk Factors**: Immediate blocking + +#### Block Types +- **Temporary**: Configurable duration (1 hour to 7 days) +- **Permanent**: Manual review required for unblocking +- **Restriction**: Limited actions allowed + +### **Enhanced Rate Limiting** + +#### Dynamic Rate Limiting +```javascript +// Rate limits adjusted based on IP risk +const riskMultiplier = getRiskMultiplier(riskScore); +// High risk: 25% of normal rate limit +// Medium risk: 50% of normal rate limit +// Low risk: 75% of normal rate limit +// Minimal risk: 100% of normal rate limit +``` + +## ๐Ÿ“ˆ Analytics and Monitoring + +### **Risk Analytics** + +#### Risk Distribution +- Real-time risk level distribution +- Geographic risk concentration analysis +- Provider accuracy comparison +- Trend analysis over time + +#### IP Reputation Tracking +- Historical behavior analysis +- Violation history tracking +- Reputation score calculation +- Pattern recognition + +### **Performance Metrics** + +#### System Performance +- API response times by provider +- Cache hit rates +- Error rates and failures +- Resource utilization + +#### Security Metrics +- Blocks applied per hour +- False positive rates +- Threat detection accuracy +- Alert response times + +### **Alerting System** + +#### Alert Types +- **High Risk Spike**: Unusual increase in high-risk IPs +- **Geographic Concentration**: Traffic concentration from single country +- **Provider Failures**: API provider connectivity issues +- **System Health**: Performance degradation alerts + +#### Alert Delivery +- Email notifications to security team +- Dashboard alerts and indicators +- Integration with monitoring systems +- Historical alert tracking + +## ๐Ÿงช Testing + +### **Unit Tests** +```bash +# Run IP intelligence tests +npm test ipIntelligence.test.js + +# Test coverage includes: +# - IP validation and private IP detection +# - Risk assessment and scoring +# - Provider integration and error handling +# - Cache management and rate limiting +# - Middleware functionality +# - Blocking service operations +# - Monitoring and analytics +``` + +### **Integration Tests** +```bash +# Test complete workflow +npm run test:integration + +# Tests include: +# - End-to-end IP assessment +# - SIWS flow protection +# - Blocking and unblocking +# - Analytics generation +# - Alert delivery +``` + +### **Performance Tests** +```bash +# Load testing for IP intelligence +npm run test:performance + +# Tests include: +# - Concurrent IP assessments +# - Cache performance +# - Rate limiting effectiveness +# - Memory usage optimization +``` + +## ๐Ÿ” Troubleshooting + +### **Common Issues** + +#### Provider API Failures +```bash +# Check API key configuration +echo $IPINFO_API_KEY +echo $ABUSEIPDB_API_KEY + +# Test provider connectivity +curl -H "Authorization: Bearer $IPINFO_API_KEY" \ + https://ipinfo.io/8.8.8.8/json +``` + +#### High Memory Usage +```bash +# Check cache size +curl http://localhost:3000/api/ip-intelligence/stats + +# Reduce cache size if needed +IP_CACHE_MAX_SIZE=5000 npm start +``` + +#### Rate Limiting Issues +```bash +# Check rate limit configuration +curl http://localhost:3000/api/ip-intelligence/monitoring-status + +# Adjust rate limits +IP_RATE_LIMIT_PER_MINUTE=200 npm start +``` + +### **Debugging Tools** + +#### Enable Debug Logging +```bash +# Enable detailed logging +DEBUG=ip-intelligence:* npm start +``` + +#### Manual IP Assessment +```bash +# Test specific IP addresses +curl -X POST http://localhost:3000/api/ip-intelligence/assess \ + -H "Content-Type: application/json" \ + -d '{"ipAddress":"8.8.8.8"}' +``` + +#### Check Block Status +```bash +# Verify IP blocking +curl http://localhost:3000/api/ip-intelligence/is-blocked/192.168.1.1 +``` + +## ๐Ÿš€ Production Deployment + +### **Pre-Deployment Checklist** + +1. **API Key Configuration** + - Obtain production API keys from all providers + - Test API connectivity and rate limits + - Configure secure key storage + +2. **Risk Threshold Tuning** + - Adjust thresholds based on traffic patterns + - Monitor false positive rates + - Fine-tune blocking policies + +3. **Monitoring Setup** + - Configure security alert emails + - Set up dashboard monitoring + - Integrate with existing monitoring systems + +4. **Performance Optimization** + - Configure appropriate cache sizes + - Set up database indexes + - Monitor resource utilization + +### **Security Considerations** + +#### API Key Security +- Store API keys in environment variables only +- Use different keys for development and production +- Regularly rotate API keys +- Monitor API usage for anomalies + +#### Privacy Compliance +- Comply with GDPR and data protection laws +- Implement data retention policies +- Provide user access to their data +- Document data processing activities + +#### Access Control +- Protect IP intelligence endpoints with authentication +- Implement role-based access control +- Audit all administrative actions +- Use secure communication channels + +### **Scaling Considerations** + +#### Horizontal Scaling +- Deploy multiple instances behind load balancer +- Use Redis for distributed caching +- Implement database connection pooling +- Monitor and optimize resource usage + +#### Provider Management +- Implement provider failover logic +- Monitor provider API health +- Configure timeout and retry policies +- Balance load across providers + +## ๐Ÿ“š API References + +### **Provider APIs** + +#### IPInfo.io +- **Documentation**: https://ipinfo.io/developers +- **Rate Limits**: 1,000 requests/day (free), 50,000+/day (paid) +- **Features**: Geolocation, VPN detection, carrier info + +#### MaxMind +- **Documentation**: https://dev.maxmind.com/geoip/docs +- **Rate Limits**: Based on subscription tier +- **Features**: Geolocation, risk assessment, connection type + +#### AbuseIPDB +- **Documentation**: https://www.abuseipdb.com/api +- **Rate Limits**: 1,000 requests/day (free), 100,000+/day (paid) +- **Features**: Abuse confidence score, country blocking + +#### IPQualityScore +- **Documentation**: https://www.ipqualityscore.com/documentation +- **Rate Limits**: 5,000 requests/month (free), 100,000+/month (paid) +- **Features**: Fraud detection, bot detection, proxy detection + +### **Integration Examples** + +#### JavaScript/Node.js +```javascript +const { IPIntelligenceService } = require('./src/services/ipIntelligenceService'); + +const ipService = new IPIntelligenceService({ + providers: { + ipinfo: { enabled: true, apiKey: 'your-key' }, + abuseipdb: { enabled: true, apiKey: 'your-key' } + } +}); + +const assessment = await ipService.assessIPRisk('192.168.1.1'); +``` + +#### Python +```python +import requests + +def assess_ip_risk(ip_address): + response = requests.post('http://localhost:3000/api/ip-intelligence/assess', + json={'ipAddress': ip_address}) + return response.json() +``` + +#### curl +```bash +curl -X POST http://localhost:3000/api/ip-intelligence/assess \ + -H "Content-Type: application/json" \ + -d '{"ipAddress":"192.168.1.1"}' +``` + +## ๐Ÿ“ž Support + +### **Getting Help** +- **Documentation**: Review this comprehensive guide +- **Issue Tracking**: Create issues in the repository +- **Security Issues**: Report to security team immediately +- **Performance Issues**: Contact technical support + +### **Community Resources** +- **GitHub Discussions**: Ask questions and share experiences +- **Security Forums**: Discuss threat intelligence and best practices +- **Provider Support**: Contact individual provider support teams + +--- + +**โš ๏ธ Important**: This is a critical security system. Ensure proper testing, monitoring, and review before deploying to production. Regular updates and maintenance are essential for continued effectiveness. diff --git a/index.js b/index.js index f3c7556..b85f39a 100644 --- a/index.js +++ b/index.js @@ -22,12 +22,18 @@ const { SubscriptionService } = require('./src/services/subscriptionService'); const { SubscriptionExpiryChecker } = require('./src/services/subscriptionExpiryChecker'); const VideoProcessingWorker = require('./src/services/videoProcessingWorker'); const { BackgroundWorkerService } = require('./src/services/backgroundWorkerService'); -const GlobalStatsService = require('./src/services/globalStatsService'); +const { GlobalStatsService } = require('./src/services/globalStatsService'); const GlobalStatsWorker = require('./src/services/globalStatsWorker'); +const { AMLScannerWorker } = require('./src/services/amlScannerWorker'); +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 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 { buildAuditLogCsv } = require('./src/utils/export/auditLogCsv'); const { buildAuditLogPdf } = require('./src/utils/export/auditLogPdf'); const { getRequestIp } = require('./src/utils/requestIp'); @@ -66,7 +72,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({ @@ -82,11 +88,43 @@ function createApp(dependencies = {}) { app.set('subscriptionExpiryChecker', subscriptionExpiryChecker); app.set('backgroundWorker', backgroundWorker); - // Start background worker if RabbitMQ is configured - if (config.rabbitmq && (config.rabbitmq.url || config.rabbitmq.host)) { - backgroundWorker.start().catch(error => { - console.error('Failed to start background worker:', error); + // Initialize and start AML scanner if enabled + let amlScannerWorker = null; + if (config.aml && config.aml.enabled) { + amlScannerWorker = dependencies.amlScannerWorker || new AMLScannerWorker(database, config.aml); + app.set('amlScannerWorker', amlScannerWorker); + + amlScannerWorker.start().catch(error => { + console.error('Failed to start AML scanner worker:', error); }); + + console.log('AML Scanner Worker initialized'); + } + + // Initialize IP intelligence services if enabled + let ipIntelligenceService = null; + let ipBlockingService = null; + let ipMonitoringService = null; + let ipMiddleware = null; + + if (config.ipIntelligence && config.ipIntelligence.enabled) { + ipIntelligenceService = dependencies.ipIntelligenceService || new IPIntelligenceService(config.ipIntelligence); + ipBlockingService = dependencies.ipBlockingService || new IPBlockingService(database, config.ipIntelligence); + ipMonitoringService = dependencies.ipMonitoringService || new IPMonitoringService(database, config.ipIntelligence); + ipMiddleware = new IPIntelligenceMiddleware(ipIntelligenceService, config.ipIntelligence); + + // Expose services on the express app + app.set('ipIntelligenceService', ipIntelligenceService); + app.set('ipBlockingService', ipBlockingService); + app.set('ipMonitoringService', ipMonitoringService); + app.set('ipIntelligenceMiddleware', ipMiddleware); + + // Start IP monitoring service + ipMonitoringService.start().catch(error => { + console.error('Failed to start IP monitoring service:', error); + }); + + console.log('IP Intelligence services initialized'); } const dayInMs = 24 * 60 * 60 * 1000; @@ -141,14 +179,29 @@ function createApp(dependencies = {}) { app.use(cors()); app.use(express.json()); - + // Add request tracing middleware for structured logging app.use(requestTracingMiddleware); + + // Add IP intelligence middleware if enabled + if (ipMiddleware) { + // Apply IP intelligence checks to sensitive endpoints + app.use('/api/cdn', ipMiddleware.createGeneralMiddleware('cdn_access')); + app.use('/api/creator', ipMiddleware.createCreatorMiddleware()); + app.use('/api/creator/videos', ipMiddleware.createContentMiddleware()); + app.use('/api/payouts', ipMiddleware.createWithdrawalMiddleware()); + + // SIWS flow protection + app.use('/api/subscription', ipMiddleware.createSIWSMiddleware()); + + logger.info('IP Intelligence middleware applied to sensitive endpoints'); + } + // Subscription events webhook app.use('/api/subscription', require('./routes/subscription')); // Payouts API app.use('/api/payouts', require('./routes/payouts')); - + // Global stats endpoints app.use('/api/global-stats', createGlobalStatsRouter({ database, globalStatsService })); @@ -403,6 +456,15 @@ 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 + })); + } + // Health check endpoint app.get('/health', async (req, res) => { const health = { @@ -414,6 +476,8 @@ function createApp(dependencies = {}) { redis: 'Unknown', rabbitmq: 'Unknown', stellar: 'Unknown', + aml: 'Unknown', + ipIntelligence: 'Unknown' }, }; @@ -470,6 +534,33 @@ function createApp(dependencies = {}) { isDegraded = true; } + // Check AML Scanner + try { + if (amlScannerWorker) { + const stats = amlScannerWorker.getScanStats(); + health.services.aml = stats.isRunning ? 'Running' : 'Stopped'; + if (!stats.isRunning) isDegraded = true; + } else { + health.services.aml = 'Not Configured'; + } + } catch (error) { + health.services.aml = 'Error'; + 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; + } + if (isDegraded) { health.status = 'Degraded'; } @@ -478,7 +569,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 +580,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/ipIntelligence.test.js b/ipIntelligence.test.js new file mode 100644 index 0000000..af273c8 --- /dev/null +++ b/ipIntelligence.test.js @@ -0,0 +1,687 @@ +const { IPIntelligenceService } = require('../src/services/ipIntelligenceService'); +const { IPIntelligenceMiddleware } = require('../src/middleware/ipIntelligenceMiddleware'); +const { IPBlockingService } = require('../src/services/ipBlockingService'); +const { IPMonitoringService } = require('../src/services/ipMonitoringService'); + +describe('IP Intelligence System', () => { + let ipIntelligenceService; + let ipMiddleware; + let ipBlockingService; + let ipMonitoringService; + let mockDatabase; + + beforeEach(() => { + // Mock database + mockDatabase = { + db: { + prepare: jest.fn(), + exec: jest.fn() + } + }; + + // Mock configuration + const mockConfig = { + providers: { + ipinfo: { enabled: false }, + maxmind: { enabled: false }, + abuseipdb: { enabled: false }, + ipqualityscore: { enabled: false } + }, + riskThresholds: { + low: 30, + medium: 60, + high: 80, + critical: 90 + }, + cache: { + enabled: true, + ttl: 3600000, + maxSize: 1000 + } + }; + + // Initialize services + ipIntelligenceService = new IPIntelligenceService(mockConfig); + ipMiddleware = new IPIntelligenceMiddleware(ipIntelligenceService, {}); + ipBlockingService = new IPBlockingService(mockDatabase, {}); + ipMonitoringService = new IPMonitoringService(mockDatabase, {}); + + // Mock database methods + mockDatabase.db.prepare.mockReturnValue({ + get: jest.fn(), + all: jest.fn(), + run: jest.fn() + }); + }); + + describe('IPIntelligenceService', () => { + describe('IP Validation', () => { + test('should validate valid IPv4 addresses', () => { + expect(ipIntelligenceService.isValidIP('192.168.1.1')).toBe(true); + expect(ipIntelligenceService.isValidIP('8.8.8.8')).toBe(true); + expect(ipIntelligenceService.isValidIP('255.255.255.255')).toBe(true); + }); + + test('should validate valid IPv6 addresses', () => { + expect(ipIntelligenceService.isValidIP('::1')).toBe(true); + expect(ipIntelligenceService.isValidIP('2001:db8::1')).toBe(true); + }); + + test('should reject invalid IP addresses', () => { + expect(ipIntelligenceService.isValidIP('invalid')).toBe(false); + expect(ipIntelligenceService.isValidIP('999.999.999.999')).toBe(false); + expect(ipIntelligenceService.isValidIP('')).toBe(false); + }); + }); + + describe('Private IP Detection', () => { + test('should detect private IPv4 addresses', () => { + expect(ipIntelligenceService.isPrivateIP('192.168.1.1')).toBe(true); + expect(ipIntelligenceService.isPrivateIP('10.0.0.1')).toBe(true); + expect(ipIntelligenceService.isPrivateIP('172.16.0.1')).toBe(true); + expect(ipIntelligenceService.isPrivateIP('127.0.0.1')).toBe(true); + }); + + test('should detect private IPv6 addresses', () => { + expect(ipIntelligenceService.isPrivateIP('::1')).toBe(true); + expect(ipIntelligenceService.isPrivateIP('fc00::')).toBe(true); + expect(ipIntelligenceService.isPrivateIP('fe80::')).toBe(true); + }); + + test('should not detect public IPs as private', () => { + expect(ipIntelligenceService.isPrivateIP('8.8.8.8')).toBe(false); + expect(ipIntelligenceService.isPrivateIP('1.1.1.1')).toBe(false); + }); + }); + + describe('Risk Assessment', () => { + test('should create invalid IP result for invalid addresses', async () => { + const result = await ipIntelligenceService.assessIPRisk('invalid'); + + expect(result.riskScore).toBe(100); + expect(result.riskLevel).toBe('critical'); + expect(result.riskFactors).toContain('Invalid IP address format'); + }); + + test('should create private IP result for private addresses', async () => { + const result = await ipIntelligenceService.assessIPRisk('192.168.1.1'); + + expect(result.riskScore).toBe(0); + expect(result.riskLevel).toBe('minimal'); + expect(result.riskFactors).toContain('Private/internal IP address'); + }); + + test('should handle API errors gracefully', async () => { + // Mock all providers to fail + jest.spyOn(ipIntelligenceService, 'collectProviderData').mockRejectedValue(new Error('API Error')); + + const result = await ipIntelligenceService.assessIPRisk('8.8.8.8'); + + expect(result.riskScore).toBe(50); + expect(result.riskLevel).toBe('medium'); + expect(result.riskFactors).toContain('Assessment failed - using default risk'); + }); + + test('should use cached results when available', async () => { + const ipAddress = '8.8.8.8'; + + // First call + const result1 = await ipIntelligenceService.assessIPRisk(ipAddress); + + // Second call should use cache + const result2 = await ipIntelligenceService.assessIPRisk(ipAddress); + + expect(result1).toEqual(result2); + expect(ipIntelligenceService.getCachedResult(ipAddress)).toBeDefined(); + }); + }); + + describe('Risk Level Calculation', () => { + test('should calculate correct risk levels', () => { + expect(ipIntelligenceService.getRiskLevel(95)).toBe('critical'); + expect(ipIntelligenceService.getRiskLevel(85)).toBe('high'); + expect(ipIntelligenceService.getRiskLevel(70)).toBe('medium'); + expect(ipIntelligenceService.getRiskLevel(40)).toBe('low'); + expect(ipIntelligenceService.getRiskLevel(20)).toBe('minimal'); + }); + }); + + describe('Rate Limiting', () => { + test('should enforce rate limits', () => { + // Fill rate limit bucket + for (let i = 0; i < 150; i++) { + ipIntelligenceService.checkRateLimit(); + } + + // Should be rate limited now + expect(ipIntelligenceService.checkRateLimit()).toBe(false); + }); + + test('should reset rate limit after time passes', () => { + // Fill rate limit bucket + for (let i = 0; i < 150; i++) { + ipIntelligenceService.checkRateLimit(); + } + + // Mock time passing + const originalTimestamps = ipIntelligenceService.requestTimestamps; + ipIntelligenceService.requestTimestamps = originalTimestamps.map(ts => ts - 70000); // 70 seconds ago + + // Should be allowed now + expect(ipIntelligenceService.checkRateLimit()).toBe(true); + }); + }); + + describe('Cache Management', () => { + test('should cache results', async () => { + const ipAddress = '8.8.8.8'; + const result = await ipIntelligenceService.assessIPRisk(ipAddress); + + expect(ipIntelligenceService.getCachedResult(ipAddress)).toEqual(result); + }); + + test('should clean up expired cache entries', async () => { + // Add expired cache entry + const ipAddress = '8.8.8.8'; + ipIntelligenceService.cacheResult(ipAddress, { riskScore: 50 }); + + // Mock expired timestamp + ipIntelligenceService.cacheTimestamps.set(ipAddress, Date.now() - 4000000); // 70 minutes ago + + // Clean up should remove expired entry + ipIntelligenceService.cleanupCache(); + + expect(ipIntelligenceService.getCachedResult(ipAddress)).toBeNull(); + }); + + test('should limit cache size', async () => { + // Fill cache beyond limit + for (let i = 0; i < 150; i++) { + ipIntelligenceService.cacheResult(`192.168.1.${i}`, { riskScore: i }); + } + + const stats = ipIntelligenceService.getCacheStats(); + expect(stats.size).toBeLessThanOrEqual(1000); + }); + }); + }); + + describe('IPIntelligenceMiddleware', () => { + let mockReq, mockRes, mockNext; + + beforeEach(() => { + mockReq = { + ip: '192.168.1.1', + get: jest.fn(), + logger: { fields: { traceId: 'test-trace-123' } } + }; + mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis() + }; + mockNext = jest.fn(); + }); + + describe('SIWS Middleware', () => { + test('should allow private IPs in SIWS flow', async () => { + const middleware = ipMiddleware.createSIWSMiddleware(); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockReq.ipIntelligence).toBeDefined(); + expect(mockReq.ipIntelligence.riskLevel).toBe('minimal'); + }); + + test('should block high risk IPs in SIWS flow', async () => { + mockReq.ip = '8.8.8.8'; + + // Mock high risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 95, + riskLevel: 'critical', + riskFactors: ['Tor exit node'] + }); + + const middleware = ipMiddleware.createSIWSMiddleware(); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: 'Access denied due to security restrictions', + code: 'IP_BLOCKED', + metadata: { + riskLevel: 'critical', + reason: 'High risk IP detected during authentication' + } + }); + }); + + test('should require additional verification for medium risk IPs', async () => { + mockReq.ip = '8.8.8.8'; + + // Mock medium risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 70, + riskLevel: 'medium', + riskFactors: ['VPN detected'] + }); + + const middleware = ipMiddleware.createSIWSMiddleware(); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(429); + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: 'Additional verification required', + code: 'IP_RESTRICTED', + metadata: { + riskLevel: 'medium', + requiresAdditionalVerification: true, + allowedActions: expect.any(Array) + } + }); + }); + + test('should handle assessment errors gracefully', async () => { + mockReq.ip = '8.8.8.8'; + + // Mock assessment error + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockRejectedValue(new Error('API Error')); + + const middleware = ipMiddleware.createSIWSMiddleware(); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); // Fail safe - allow + expect(mockReq.ipIntelligence.error).toBeDefined(); + }); + }); + + describe('General Middleware', () => { + test('should allow low risk actions', async () => { + mockReq.ip = '8.8.8.8'; + + // Mock low risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 25, + riskLevel: 'low', + riskFactors: [] + }); + + const middleware = ipMiddleware.createGeneralMiddleware('create_subscription'); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockReq.ipIntelligence.riskLevel).toBe('low'); + }); + + test('should block high risk actions', async () => { + mockReq.ip = '8.8.8.8'; + + // Mock high risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 85, + riskLevel: 'high', + riskFactors: ['Tor exit node'] + }); + + const middleware = ipMiddleware.createGeneralMiddleware('create_creator'); + + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(403); + }); + }); + + describe('Risk Level Comparison', () => { + test('should compare risk levels correctly', () => { + expect(ipMiddleware.compareRiskLevels('high', 'medium')).toBeGreaterThan(0); + expect(ipMiddleware.compareRiskLevels('low', 'high')).toBeLessThan(0); + expect(ipMiddleware.compareRiskLevels('medium', 'medium')).toBe(0); + }); + }); + + describe('Action Permission', () => { + test('should allow appropriate actions for risk levels', () => { + expect(ipMiddleware.isActionAllowed('view_content', 'low')).toBe(true); + expect(ipMiddleware.isActionAllowed('view_content', 'high')).toBe(true); + expect(ipMiddleware.isActionAllowed('create_creator', 'high')).toBe(false); + expect(ipMiddleware.isActionAllowed('create_creator', 'critical')).toBe(false); + }); + }); + }); + + describe('IPBlockingService', () => { + describe('Block Evaluation', () => { + test('should block IPs exceeding threshold', async () => { + const riskAssessment = { + riskScore: 95, + riskLevel: 'critical', + riskFactors: ['Tor exit node'] + }; + + const decision = await ipBlockingService.evaluateIP('8.8.8.8', riskAssessment, { + actionType: 'create_creator' + }); + + expect(decision.action).toBe('temporary'); // or 'permanent' + expect(decision.reason).toContain('High risk score'); + }); + + test('should restrict medium risk IPs', async () => { + const riskAssessment = { + riskScore: 70, + riskLevel: 'medium', + riskFactors: ['VPN detected'] + }; + + const decision = await ipBlockingService.evaluateIP('8.8.8.8', riskAssessment); + + expect(decision.action).toBe('restrict'); + expect(decision.reason).toContain('Elevated risk score'); + }); + + test('should allow low risk IPs', async () => { + const riskAssessment = { + riskScore: 25, + riskLevel: 'low', + riskFactors: [] + }; + + const decision = await ipBlockingService.evaluateIP('8.8.8.8', riskAssessment); + + expect(decision.action).toBe('allow'); + }); + }); + + describe('Manual Blocking', () => { + test('should manually block IP', async () => { + const result = await ipBlockingService.manualBlockIP('8.8.8.8', { + type: 'temporary', + duration: 3600000, // 1 hour + reason: 'Test block' + }); + + expect(result.success).toBe(true); + expect(result.blockId).toBeDefined(); + expect(result.type).toBe('temporary'); + }); + + test('should manually unblock IP', async () => { + // First block + await ipBlockingService.manualBlockIP('8.8.8.8'); + + // Then unblock + const result = await ipBlockingService.manualUnblockIP('8.8.8.8', 'Test unblock'); + + expect(result.success).toBe(true); + expect(result.reason).toBe('Test unblock'); + }); + }); + + describe('Block Status', () => { + test('should check if IP is blocked', () => { + // Block an IP first + ipBlockingService.activeBlocks.set('8.8.8.8', { + block_type: 'temporary', + reason: 'Test block', + expires_at: new Date(Date.now() + 3600000).toISOString() + }); + + const blockInfo = ipBlockingService.isIPBlocked('8.8.8.8'); + + expect(blockInfo).toBeDefined(); + expect(blockInfo.blockType).toBe('temporary'); + expect(blockInfo.reason).toBe('Test block'); + }); + + test('should return null for unblocked IPs', () => { + const blockInfo = ipBlockingService.isIPBlocked('1.1.1.1'); + + expect(blockInfo).toBeNull(); + }); + }); + + describe('Statistics', () => { + test('should provide blocking statistics', () => { + // Add some blocks + ipBlockingService.activeBlocks.set('8.8.8.8', { + block_type: 'temporary', + risk_level: 'high' + }); + ipBlockingService.activeBlocks.set('1.1.1.1', { + block_type: 'permanent', + risk_level: 'critical' + }); + + const stats = ipBlockingService.getBlockingStats(); + + expect(stats.activeBlocks).toBe(2); + expect(stats.blockTypes.temporary).toBe(1); + expect(stats.blockTypes.permanent).toBe(1); + expect(stats.riskLevels.high).toBe(1); + expect(stats.riskLevels.critical).toBe(1); + }); + }); + }); + + describe('IPMonitoringService', () => { + describe('Event Recording', () => { + test('should record monitoring events', async () => { + await ipMonitoringService.recordEvent('8.8.8.8', 'risk_assessment', { + riskScore: 75, + riskLevel: 'medium' + }); + + expect(mockDatabase.db.prepare).toHaveBeenCalledWith( + expect.stringContaining('INSERT INTO ip_monitoring_events'), + expect.any(Array) + ); + }); + }); + + describe('Analytics Generation', () => { + test('should generate analytics data', async () => { + // Mock database responses + mockDatabase.db.prepare.mockReturnValue({ + get: jest.fn().mockReturnValue({ + total_events: 100, + unique_ips: 50, + avg_risk_score: 45.5, + max_risk_score: 95, + high_risk_events: 10, + blocks: 5, + violations: 3 + }), + all: jest.fn().mockReturnValue([ + { risk_level: 'medium', count: 50 }, + { risk_level: 'high', count: 30 }, + { risk_level: 'low', count: 20 } + ]) + }); + + const analytics = await ipMonitoringService.getAnalytics({ period: '24h' }); + + expect(analytics).toBeDefined(); + expect(analytics.summary).toBeDefined(); + expect(analytics.riskDistribution).toBeDefined(); + }); + }); + + describe('Monitoring Status', () => { + test('should provide monitoring status', () => { + const status = ipMonitoringService.getMonitoringStatus(); + + expect(status).toBeDefined(); + expect(status.isRunning).toBe(false); // Not started + expect(status.config).toBeDefined(); + }); + }); + + describe('Service Lifecycle', () => { + test('should start monitoring service', async () => { + await ipMonitoringService.start(); + + expect(ipMonitoringService.isRunning).toBe(true); + expect(ipMonitoringService.monitoringTimer).toBeDefined(); + }); + + test('should stop monitoring service', async () => { + await ipMonitoringService.start(); + await ipMonitoringService.stop(); + + expect(ipMonitoringService.isRunning).toBe(false); + expect(ipMonitoringService.monitoringTimer).toBeNull(); + }); + }); + }); + + describe('Integration Tests', () => { + test('should handle complete IP intelligence workflow', async () => { + const ipAddress = '8.8.8.8'; + + // 1. Assess IP risk + const riskAssessment = await ipIntelligenceService.assessIPRisk(ipAddress); + expect(riskAssessment).toBeDefined(); + + // 2. Evaluate for blocking + const blockDecision = await ipBlockingService.evaluateIP(ipAddress, riskAssessment); + expect(blockDecision).toBeDefined(); + + // 3. Record monitoring event + await ipMonitoringService.recordEvent(ipAddress, 'risk_assessment', riskAssessment); + + // 4. Check if blocked + const isBlocked = ipBlockingService.isIPBlocked(ipAddress); + expect(typeof isBlocked).toBe('boolean'); + }); + + test('should handle SIWS flow with IP intelligence', async () => { + const mockReq = { + ip: '8.8.8.8', + get: jest.fn(), + logger: { fields: { traceId: 'test-trace-456' } } + }; + const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis() + }; + const mockNext = jest.fn(); + + // Mock low risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 25, + riskLevel: 'low', + riskFactors: [] + }); + + const middleware = ipMiddleware.createSIWSMiddleware(); + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).toHaveBeenCalled(); + expect(mockReq.ipIntelligence).toBeDefined(); + expect(mockReq.ipIntelligence.riskLevel).toBe('low'); + }); + + test('should handle high risk IP in SIWS flow', async () => { + const mockReq = { + ip: '8.8.8.8', + get: jest.fn(), + logger: { fields: { traceId: 'test-trace-789' } } + }; + const mockRes = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis() + }; + const mockNext = jest.fn(); + + // Mock high risk assessment + jest.spyOn(ipIntelligenceService, 'assessIPRisk').mockResolvedValue({ + riskScore: 95, + riskLevel: 'critical', + riskFactors: ['Tor exit node'] + }); + + const middleware = ipMiddleware.createSIWSMiddleware(); + await middleware(mockReq, mockRes, mockNext); + + expect(mockNext).not.toHaveBeenCalled(); + expect(mockRes.status).toHaveBeenCalledWith(403); + expect(mockRes.json).toHaveBeenCalledWith({ + success: false, + error: 'Access denied due to security restrictions', + code: 'IP_BLOCKED', + metadata: { + riskLevel: 'critical', + reason: 'High risk IP detected during authentication' + } + }); + }); + }); + + describe('Error Handling', () => { + test('should handle database errors gracefully', async () => { + mockDatabase.db.prepare.mockImplementation(() => { + throw new Error('Database error'); + }); + + await expect(ipMonitoringService.recordEvent('8.8.8.8', 'test', {})) + .rejects.toThrow('Database error'); + }); + + test('should handle network errors gracefully', async () => { + // Mock network error + jest.spyOn(ipIntelligenceService, 'collectProviderData').mockRejectedValue(new Error('Network error')); + + const result = await ipIntelligenceService.assessIPRisk('8.8.8.8'); + + expect(result.riskScore).toBe(50); + expect(result.riskLevel).toBe('medium'); + expect(result.riskFactors).toContain('Assessment failed - using default risk'); + }); + }); + + describe('Performance', () => { + test('should handle concurrent requests', async () => { + const promises = []; + + // Create multiple concurrent requests + for (let i = 0; i < 10; i++) { + promises.push(ipIntelligenceService.assessIPRisk(`192.168.1.${i}`)); + } + + const results = await Promise.all(promises); + + expect(results).toHaveLength(10); + results.forEach(result => { + expect(result).toBeDefined(); + expect(result.ipAddress).toMatch(/^192\.168\.1\.\d+$/); + }); + }); + + test('should use cache efficiently', async () => { + const ipAddress = '8.8.8.8'; + + // First request + const start1 = Date.now(); + await ipIntelligenceService.assessIPRisk(ipAddress); + const duration1 = Date.now() - start1; + + // Second request (should use cache) + const start2 = Date.now(); + await ipIntelligenceService.assessIPRisk(ipAddress); + const duration2 = Date.now() - start2; + + // Cached request should be much faster + expect(duration2).toBeLessThan(duration1 / 2); + }); + }); +}); diff --git a/routes/ipIntelligence.js b/routes/ipIntelligence.js new file mode 100644 index 0000000..17ee7bb --- /dev/null +++ b/routes/ipIntelligence.js @@ -0,0 +1,797 @@ +const express = require('express'); +const { logger } = require('../src/utils/logger'); + +/** + * Create IP Intelligence management routes + * @param {object} dependencies - Service dependencies + * @returns {express.Router} + */ +function createIPIntelligenceRoutes(dependencies = {}) { + const router = express.Router(); + const ipIntelligenceService = dependencies.ipIntelligenceService; + const ipBlockingService = dependencies.ipBlockingService; + const ipMonitoringService = dependencies.ipMonitoringService; + + /** + * Get IP intelligence service statistics + * GET /api/ip-intelligence/stats + */ + router.get('/stats', async (req, res) => { + try { + if (!ipIntelligenceService) { + return res.status(503).json({ + success: false, + error: 'IP intelligence service not available' + }); + } + + const stats = ipIntelligenceService.getServiceStats(); + + return res.status(200).json({ + success: true, + data: stats + }); + + } catch (error) { + logger.error('Error fetching IP intelligence stats', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP intelligence statistics' + }); + } + }); + + /** + * Assess IP risk + * POST /api/ip-intelligence/assess + */ + router.post('/assess', async (req, res) => { + try { + const { ipAddress, options = {} } = req.body; + + if (!ipAddress) { + return res.status(400).json({ + success: false, + error: 'IP address is required' + }); + } + + if (!ipIntelligenceService) { + return res.status(503).json({ + success: false, + error: 'IP intelligence service not available' + }); + } + + const assessment = await ipIntelligenceService.assessIPRisk(ipAddress, options); + + return res.status(200).json({ + success: true, + data: assessment + }); + + } catch (error) { + logger.error('Error assessing IP risk', { + error: error.message, + ipAddress: req.body?.ipAddress, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to assess IP risk' + }); + } + }); + + /** + * Get IP reputation data + * GET /api/ip-intelligence/reputation/:ipAddress + */ + router.get('/reputation/:ipAddress', async (req, res) => { + try { + const { ipAddress } = req.params; + + if (!ipIntelligenceService) { + return res.status(503).json({ + success: false, + error: 'IP intelligence service not available' + }); + } + + const reputation = ipIntelligenceService.getIPReputation(ipAddress); + + return res.status(200).json({ + success: true, + data: reputation || null + }); + + } catch (error) { + logger.error('Error fetching IP reputation', { + error: error.message, + ipAddress: req.params.ipAddress, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP reputation' + }); + } + }); + + /** + * Get IP reputation statistics + * GET /api/ip-intelligence/reputation-stats + */ + router.get('/reputation-stats', async (req, res) => { + try { + if (!ipIntelligenceService) { + return res.status(503).json({ + success: false, + error: 'IP intelligence service not available' + }); + } + + const stats = ipIntelligenceService.getReputationStats(); + + return res.status(200).json({ + success: true, + data: stats + }); + + } catch (error) { + logger.error('Error fetching IP reputation stats', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP reputation statistics' + }); + } + }); + + /** + * Get blocking statistics + * GET /api/ip-intelligence/blocking-stats + */ + router.get('/blocking-stats', async (req, res) => { + try { + if (!ipBlockingService) { + return res.status(503).json({ + success: false, + error: 'IP blocking service not available' + }); + } + + const stats = ipBlockingService.getBlockingStats(); + + return res.status(200).json({ + success: true, + data: stats + }); + + } catch (error) { + logger.error('Error fetching blocking stats', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch blocking statistics' + }); + } + }); + + /** + * Check if IP is blocked + * GET /api/ip-intelligence/is-blocked/:ipAddress + */ + router.get('/is-blocked/:ipAddress', async (req, res) => { + try { + const { ipAddress } = req.params; + + if (!ipBlockingService) { + return res.status(503).json({ + success: false, + error: 'IP blocking service not available' + }); + } + + const blockInfo = ipBlockingService.isIPBlocked(ipAddress); + + return res.status(200).json({ + success: true, + data: { + ipAddress, + isBlocked: !!blockInfo, + blockInfo + } + }); + + } catch (error) { + logger.error('Error checking IP block status', { + error: error.message, + ipAddress: req.params.ipAddress, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to check IP block status' + }); + } + }); + + /** + * Manually block an IP + * POST /api/ip-intelligence/block + */ + router.post('/block', async (req, res) => { + try { + const { ipAddress, type = 'temporary', duration, reason } = req.body; + + if (!ipAddress) { + return res.status(400).json({ + success: false, + error: 'IP address is required' + }); + } + + if (!ipBlockingService) { + return res.status(503).json({ + success: false, + error: 'IP blocking service not available' + }); + } + + const result = await ipBlockingService.manualBlockIP(ipAddress, { + type, + duration, + reason: reason || 'Manual block via API' + }); + + if (result.success) { + // Record monitoring event + await ipMonitoringService?.recordEvent(ipAddress, 'block', { + manual: true, + type, + duration, + reason + }); + } + + return res.status(result.success ? 200 : 400).json({ + success: result.success, + data: result + }); + + } catch (error) { + logger.error('Error manually blocking IP', { + error: error.message, + ipAddress: req.body?.ipAddress, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to block IP' + }); + } + }); + + /** + * Manually unblock an IP + * POST /api/ip-intelligence/unblock + */ + router.post('/unblock', async (req, res) => { + try { + const { ipAddress, reason } = req.body; + + if (!ipAddress) { + return res.status(400).json({ + success: false, + error: 'IP address is required' + }); + } + + if (!ipBlockingService) { + return res.status(503).json({ + success: false, + error: 'IP blocking service not available' + }); + } + + const result = await ipBlockingService.manualUnblockIP(ipAddress, reason || 'Manual unblock via API'); + + if (result.success) { + // Record monitoring event + await ipMonitoringService?.recordEvent(ipAddress, 'unblock', { + manual: true, + reason + }); + } + + return res.status(result.success ? 200 : 400).json({ + success: result.success, + data: result + }); + + } catch (error) { + logger.error('Error manually unblocking IP', { + error: error.message, + ipAddress: req.body?.ipAddress, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to unblock IP' + }); + } + }); + + /** + * Get IP analytics + * GET /api/ip-intelligence/analytics + */ + router.get('/analytics', async (req, res) => { + try { + const { period = '24h', includeDetails = 'false', topN = 10 } = req.query; + + if (!ipMonitoringService) { + return res.status(503).json({ + success: false, + error: 'IP monitoring service not available' + }); + } + + const analytics = await ipMonitoringService.getAnalytics({ + period, + includeDetails: includeDetails === 'true', + topN: parseInt(topN) || 10 + }); + + return res.status(200).json({ + success: true, + data: analytics + }); + + } catch (error) { + logger.error('Error fetching IP analytics', { + error: error.message, + period: req.query.period, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP analytics' + }); + } + }); + + /** + * Get monitoring status + * GET /api/ip-intelligence/monitoring-status + */ + router.get('/monitoring-status', async (req, res) => { + try { + if (!ipMonitoringService) { + return res.status(503).json({ + success: false, + error: 'IP monitoring service not available' + }); + } + + const status = ipMonitoringService.getMonitoringStatus(); + + return res.status(200).json({ + success: true, + data: status + }); + + } catch (error) { + logger.error('Error fetching monitoring status', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch monitoring status' + }); + } + }); + + /** + * Get recent IP events + * GET /api/ip-intelligence/events + */ + router.get('/events', async (req, res) => { + try { + const { limit = 50, eventType } = req.query; + + if (!ipMonitoringService) { + return res.status(503).json({ + success: false, + error: 'IP monitoring service not available' + }); + } + + // Query recent events from database + let query = ` + SELECT * FROM ip_monitoring_events + ORDER BY timestamp DESC + LIMIT ? + `; + + const params = [parseInt(limit) || 50]; + + if (eventType) { + query = ` + SELECT * FROM ip_monitoring_events + WHERE event_type = ? + ORDER BY timestamp DESC + LIMIT ? + `; + params.unshift(eventType); + } + + const events = ipMonitoringService.database.db.prepare(query).all(...params); + + return res.status(200).json({ + success: true, + data: { + events: events.map(event => ({ + ...event, + metadata: JSON.parse(event.metadata_json || '{}') + })), + count: events.length + } + }); + + } catch (error) { + logger.error('Error fetching IP events', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP events' + }); + } + }); + + /** + * Get IP intelligence dashboard overview + * GET /api/ip-intelligence/dashboard + */ + router.get('/dashboard', async (req, res) => { + try { + if (!ipIntelligenceService || !ipBlockingService || !ipMonitoringService) { + return res.status(503).json({ + success: false, + error: 'IP intelligence services not available' + }); + } + + // Get overview data + const [ + serviceStats, + blockingStats, + analytics, + monitoringStatus + ] = await Promise.all([ + Promise.resolve(ipIntelligenceService.getServiceStats()), + Promise.resolve(ipBlockingService.getBlockingStats()), + ipMonitoringService.getAnalytics({ period: '24h', topN: 5 }), + Promise.resolve(ipMonitoringService.getMonitoringStatus()) + ]); + + const dashboard = { + timestamp: new Date().toISOString(), + services: { + intelligence: serviceStats, + blocking: blockingStats, + monitoring: monitoringStatus + }, + overview: { + activeBlocks: blockingStats.activeBlocks, + recentEvents: analytics.summary?.totalEvents || 0, + uniqueIPs: analytics.summary?.uniqueIPs || 0, + averageRiskScore: analytics.summary?.averageRiskScore || 0, + highRiskEvents: analytics.summary?.highRiskEvents || 0 + }, + topRiskIPs: analytics.topRiskIPs?.slice(0, 5) || [], + riskDistribution: analytics.riskDistribution || [], + recentActivity: analytics.eventTypeDistribution || [], + alerts: { + monitoring: monitoringStatus.isRunning, + alertCooldowns: monitoringStatus.alertCooldowns + } + }; + + return res.status(200).json({ + success: true, + data: dashboard + }); + + } catch (error) { + logger.error('Error fetching IP intelligence dashboard', { + error: error.message, + traceId: req.logger?.fields?.traceId + }); + + return res.status(500).json({ + success: false, + error: 'Failed to fetch IP intelligence dashboard' + }); + } + }); + + /** + * Export IP intelligence data + * GET /api/ip-intelligence/export + */ + router.get('/export', async (req, res) => { + try { + const { + period = '24h', + format = 'json', + includeDetails = 'false', + type = 'all' + } = req.query; + + if (!ipMonitoringService) { + return res.status(503).json({ + success: false, + error: 'IP monitoring service not available' + }); + } + + let data; + let filename; + let contentType; + + switch (type) { + case 'events': + data = await this.exportEvents(period); + filename = `ip_events_${period}.${format}`; + break; + case 'analytics': + data = await ipMonitoringService.getAnalytics({ + period, + includeDetails: includeDetails === 'true' + }); + filename = `ip_analytics_${period}.${format}`; + break; + case 'blocks': + data = await this.exportBlocks(period); + filename = `ip_blocks_${period}.${format}`; + break; + case 'all': + default: + data = await this.exportAllData(period); + filename = `ip_intelligence_${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 IP intelligence 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 IP intelligence data' + }); + } + }); + + /** + * Export events data + * @param {string} period + * @returns {object} Events data + */ + async exportEvents(period) { + const startDate = this.getStartDate(period); + + const events = ipMonitoringService.database.db.prepare(` + SELECT * FROM ip_monitoring_events + WHERE timestamp > ? + ORDER BY timestamp DESC + `).all(startDate.toISOString()); + + return events.map(event => ({ + ...event, + metadata: JSON.parse(event.metadata_json || '{}') + })); + } + + /** + * Export blocks data + * @param {string} period + * @returns {object} Blocks data + */ + async exportBlocks(period) { + const startDate = this.getStartDate(period); + + const blocks = ipMonitoringService.database.db.prepare(` + SELECT * FROM ip_blocks + WHERE created_at > ? AND is_active = 1 + ORDER BY created_at DESC + `).all(startDate.toISOString()); + + return blocks.map(block => ({ + ...block, + metadata: JSON.parse(block.metadata_json || '{}') + })); + } + + /** + * Export all data + * @param {string} period + * @returns {object} All data + */ + async exportAllData(period) { + const [events, blocks, analytics] = await Promise.all([ + this.exportEvents(period), + this.exportBlocks(period), + ipMonitoringService.getAnalytics({ period, includeDetails: true }) + ]); + + return { + events, + blocks, + analytics, + exportedAt: new Date().toISOString(), + period + }; + } + + /** + * Convert data to CSV + * @param {object} data + * @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 in obj) { + if (obj.hasOwnProperty(key)) { + const value = obj[key]; + const newKey = prefix ? `${prefix}.${key}` : key; + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + Object.assign(flattened, flattenObject(value, newKey)); + } else { + flattened[newKey] = 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 + * @param {object} data + * @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 + * @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 = { createIPIntelligenceRoutes }; diff --git a/src/config.js b/src/config.js index d52e650..e2abeb2 100644 --- a/src/config.js +++ b/src/config.js @@ -50,6 +50,60 @@ function loadConfig(env = process.env) { password: env.REDIS_PASSWORD || '', db: Number(env.REDIS_DB || 0), }, + aml: { + enabled: env.AML_ENABLED === 'true', + scanInterval: Number(env.AML_SCAN_INTERVAL_MS || 24 * 60 * 60 * 1000), // Daily + batchSize: Number(env.AML_BATCH_SIZE || 50), + maxRetries: Number(env.AML_MAX_RETRIES || 3), + complianceOfficerEmail: env.COMPLIANCE_OFFICER_EMAIL || '', + sanctions: { + ofacApiKey: env.OFAC_API_KEY || '', + euSanctionsApiKey: env.EU_SANCTIONS_API_KEY || '', + unSanctionsApiKey: env.UN_SANCTIONS_API_KEY || '', + ukSanctionsApiKey: env.UK_SANCTIONS_API_KEY || '', + cacheTimeout: Number(env.SANCTIONS_CACHE_TIMEOUT_MS || 60 * 60 * 1000), // 1 hour + } + }, + 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) + } + }, s3: env.S3_BUCKET ? { bucket: env.S3_BUCKET, region: env.S3_REGION || 'us-east-1', diff --git a/src/middleware/ipIntelligenceMiddleware.js b/src/middleware/ipIntelligenceMiddleware.js new file mode 100644 index 0000000..9eb38f7 --- /dev/null +++ b/src/middleware/ipIntelligenceMiddleware.js @@ -0,0 +1,650 @@ +const { logger } = require('../utils/logger'); +const { getRequestIp } = require('../utils/requestIp'); + +/** + * IP Intelligence Middleware for SIWS flow and fraud prevention + * Provides active defense against VPNs, Tor, and high-risk IP addresses + */ +class IPIntelligenceMiddleware { + constructor(ipIntelligenceService, config = {}) { + this.ipService = ipIntelligenceService; + this.config = { + // Risk level restrictions + restrictions: { + // Actions that require different risk levels + critical: { + maxRiskLevel: config.critical?.maxRiskLevel || 'low', + actions: ['create_creator', 'high_value_withdrawal', 'bulk_operations'] + }, + high: { + maxRiskLevel: config.high?.maxRiskLevel || 'medium', + actions: ['create_subscription', 'upload_content', 'update_settings'] + }, + medium: { + maxRiskLevel: config.medium?.maxRiskLevel || 'high', + actions: ['view_content', 'basic_operations'] + } + }, + // SIWS specific settings + siws: { + enabled: config.siws?.enabled !== false, + maxRiskLevel: config.siws?.maxRiskLevel || 'medium', + requireAdditionalVerification: config.siws?.requireAdditionalVerification || true, + blockHighRisk: config.siws?.blockHighRisk !== false + }, + // Response configuration + responses: { + blocked: { + status: 403, + message: 'Access denied due to security restrictions', + code: 'IP_BLOCKED' + }, + restricted: { + status: 429, + message: 'Additional verification required', + code: 'IP_RESTRICTED' + }, + monitored: { + status: 200, + message: 'Request allowed with enhanced monitoring', + code: 'IP_MONITORED' + } + }, + // Monitoring settings + monitoring: { + logAllRequests: config.monitoring?.logAllRequests || false, + logHighRiskOnly: config.monitoring?.logHighRiskOnly !== false, + alertThreshold: config.monitoring?.alertThreshold || 80, + trackReputation: config.monitoring?.trackReputation !== false + }, + ...config + }; + + // IP reputation tracking + this.ipReputation = new Map(); + this.loadReputationData(); + } + + /** + * Create middleware for SIWS authentication flow + * @returns {Function} Express middleware + */ + createSIWSMiddleware() { + return async (req, res, next) => { + try { + if (!this.config.siws.enabled) { + return next(); + } + + const ipAddress = getRequestIp(req); + + if (!ipAddress) { + logger.warn('Unable to determine IP address for SIWS request', { + userAgent: req.get('User-Agent'), + traceId: req.logger?.fields?.traceId + }); + return next(); // Allow but log + } + + // Assess IP risk + const riskAssessment = await this.ipService.assessIPRisk(ipAddress, { + context: 'siws_auth', + userAgent: req.get('User-Agent') + }); + + // Update IP reputation + this.updateIPReputation(ipAddress, 'siws_attempt', riskAssessment.riskScore); + + // Check if IP should be blocked + if (this.shouldBlockIP(riskAssessment, 'siws')) { + logger.warn('SIWS request blocked - high risk IP', { + ipAddress, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + riskFactors: riskAssessment.riskFactors, + userAgent: req.get('User-Agent'), + traceId: req.logger?.fields?.traceId + }); + + return res.status(this.config.responses.blocked.status).json({ + success: false, + error: this.config.responses.blocked.message, + code: this.config.responses.blocked.code, + metadata: { + riskLevel: riskAssessment.riskLevel, + reason: 'High risk IP detected during authentication' + } + }); + } + + // Check if additional verification is required + if (this.requiresAdditionalVerification(riskAssessment, 'siws')) { + logger.info('SIWS request requires additional verification', { + ipAddress, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + traceId: req.logger?.fields?.traceId + }); + + // Add verification requirement to request + req.ipIntelligence = { + ...riskAssessment, + requiresAdditionalVerification: true, + allowedActions: this.getAllowedActions(riskAssessment, 'siws') + }; + + return res.status(this.config.responses.restricted.status).json({ + success: false, + error: this.config.responses.restricted.message, + code: this.config.responses.restricted.code, + metadata: { + riskLevel: riskAssessment.riskLevel, + requiresAdditionalVerification: true, + allowedActions: req.ipIntelligence.allowedActions + } + }); + } + + // Add IP intelligence to request for monitoring + req.ipIntelligence = { + ...riskAssessment, + allowedActions: this.getAllowedActions(riskAssessment, 'siws'), + requiresAdditionalVerification: false + }; + + // Log if monitoring is enabled + if (this.config.monitoring.logHighRiskOnly && riskAssessment.riskScore >= this.config.monitoring.alertThreshold) { + logger.warn('High risk IP allowed in SIWS flow', { + ipAddress, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + riskFactors: riskAssessment.riskFactors, + traceId: req.logger?.fields?.traceId + }); + } + + next(); + + } catch (error) { + logger.error('IP intelligence middleware error in SIWS flow', { + error: error.message, + ipAddress: getRequestIp(req), + traceId: req.logger?.fields?.traceId + }); + + // Fail safe - allow but monitor + req.ipIntelligence = { + error: error.message, + riskScore: 50, + riskLevel: 'medium' + }; + next(); + } + }; + } + + /** + * Create middleware for general IP intelligence checks + * @param {string} actionType - Type of action being performed + * @returns {Function} Express middleware + */ + createGeneralMiddleware(actionType) { + return async (req, res, next) => { + try { + const ipAddress = getRequestIp(req); + + if (!ipAddress) { + return next(); // Allow but log + } + + // Assess IP risk + const riskAssessment = await this.ipService.assessIPRisk(ipAddress, { + context: actionType, + userAgent: req.get('User-Agent'), + endpoint: req.path, + method: req.method + }); + + // Update IP reputation + this.updateIPReputation(ipAddress, actionType, riskAssessment.riskScore); + + // Check if action is allowed for this risk level + if (!this.isActionAllowed(actionType, riskAssessment.riskLevel)) { + logger.warn('Action blocked due to IP risk level', { + ipAddress, + actionType, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + endpoint: req.path, + method: req.method, + traceId: req.logger?.fields?.traceId + }); + + return res.status(this.config.responses.blocked.status).json({ + success: false, + error: this.config.responses.blocked.message, + code: this.config.responses.blocked.code, + metadata: { + actionType, + riskLevel: riskAssessment.riskLevel, + reason: 'Action not allowed for this IP risk level' + } + }); + } + + // Add IP intelligence to request + req.ipIntelligence = riskAssessment; + + // Log high-risk requests + if (riskAssessment.riskScore >= this.config.monitoring.alertThreshold) { + logger.warn('High risk IP request allowed', { + ipAddress, + actionType, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + endpoint: req.path, + traceId: req.logger?.fields?.traceId + }); + } + + next(); + + } catch (error) { + logger.error('IP intelligence middleware error', { + error: error.message, + actionType, + ipAddress: getRequestIp(req), + traceId: req.logger?.fields?.traceId + }); + + // Fail safe - allow but monitor + req.ipIntelligence = { + error: error.message, + riskScore: 50, + riskLevel: 'medium' + }; + next(); + } + }; + } + + /** + * Create middleware for creator-specific actions + * @returns {Function} Express middleware + */ + createCreatorMiddleware() { + return this.createGeneralMiddleware('create_creator'); + } + + /** + * Create middleware for high-value withdrawals + * @returns {Function} Express middleware + */ + createWithdrawalMiddleware() { + return this.createGeneralMiddleware('high_value_withdrawal'); + } + + /** + * Create middleware for content uploads + * @returns {Function} Express middleware + */ + createContentMiddleware() { + return this.createGeneralMiddleware('upload_content'); + } + + /** + * Check if IP should be blocked based on risk assessment + * @param {object} riskAssessment + * @param {string} context + * @returns {boolean} + */ + shouldBlockIP(riskAssessment, context) { + const riskLevel = riskAssessment.riskLevel; + + switch (context) { + case 'siws': + // Block critical and high risk IPs in SIWS + return ['critical', 'high'].includes(riskLevel); + default: + // Block critical risk IPs for other contexts + return riskLevel === 'critical'; + } + } + + /** + * Check if additional verification is required + * @param {object} riskAssessment + * @param {string} context + * @returns {boolean} + */ + requiresAdditionalVerification(riskAssessment, context) { + const riskLevel = riskAssessment.riskLevel; + + switch (context) { + case 'siws': + // Require verification for medium and high risk IPs + return ['medium', 'high'].includes(riskLevel); + default: + // Require verification for high risk IPs + return riskLevel === 'high'; + } + } + + /** + * Check if action is allowed for risk level + * @param {string} actionType + * @param {string} riskLevel + * @returns {boolean} + */ + isActionAllowed(actionType, riskLevel) { + // Find the restriction category for this action + for (const [category, config] of Object.entries(this.config.restrictions)) { + if (config.actions.includes(actionType)) { + return this.compareRiskLevels(riskLevel, config.maxRiskLevel) <= 0; + } + } + + // Default to allowing medium risk actions + return this.compareRiskLevels(riskLevel, 'medium') <= 0; + } + + /** + * Compare risk levels (returns -1, 0, or 1) + * @param {string} level1 + * @param {string} level2 + * @returns {number} + */ + compareRiskLevels(level1, level2) { + const levels = ['minimal', 'low', 'medium', 'high', 'critical']; + const index1 = levels.indexOf(level1); + const index2 = levels.indexOf(level2); + + return index1 - index2; + } + + /** + * Get allowed actions for risk level and context + * @param {object} riskAssessment + * @param {string} context + * @returns {array} Allowed actions + */ + getAllowedActions(riskAssessment, context) { + const riskLevel = riskAssessment.riskLevel; + const allowedActions = []; + + // Define action hierarchy by risk level + const actionHierarchy = { + minimal: ['view_content', 'basic_operations'], + low: ['view_content', 'basic_operations', 'create_subscription'], + medium: ['view_content', 'basic_operations', 'create_subscription', 'upload_content'], + high: ['view_content', 'basic_operations'], + critical: [] // No actions allowed for critical risk + }; + + if (context === 'siws') { + // Special handling for SIWS + if (this.compareRiskLevels(riskLevel, 'low') <= 0) { + allowedActions.push('siws_auth'); + } + } else { + // General actions based on risk level + const actions = actionHierarchy[riskLevel] || []; + allowedActions.push(...actions); + } + + return allowedActions; + } + + /** + * Update IP reputation tracking + * @param {string} ipAddress + * @param {string} actionType + * @param {number} riskScore + */ + updateIPReputation(ipAddress, actionType, riskScore) { + if (!this.config.monitoring.trackReputation) return; + + const now = Date.now(); + let reputation = this.ipReputation.get(ipAddress); + + if (!reputation) { + reputation = { + ipAddress, + firstSeen: now, + lastSeen: now, + actionCount: 0, + riskScores: [], + averageRiskScore: riskScore, + reputationScore: 100 - riskScore, // Start with good reputation + actions: new Map() + }; + } + + // Update reputation data + reputation.lastSeen = now; + reputation.actionCount++; + reputation.riskScores.push(riskScore); + + // Keep only last 10 risk scores for average + if (reputation.riskScores.length > 10) { + reputation.riskScores.shift(); + } + + // Calculate new average risk score + reputation.averageRiskScore = reputation.riskScores.reduce((a, b) => a + b, 0) / reputation.riskScores.length; + + // Update action-specific tracking + if (!reputation.actions.has(actionType)) { + reputation.actions.set(actionType, { count: 0, firstSeen: now }); + } + const actionData = reputation.actions.get(actionType); + actionData.count++; + actionData.lastSeen = now; + + // Calculate reputation score (0-100, higher is better) + const recentActions = reputation.riskScores.slice(-5); // Last 5 actions + const recentAverage = recentActions.reduce((a, b) => a + b, 0) / recentActions.length; + reputation.reputationScore = Math.max(0, Math.min(100, 100 - recentAverage)); + + this.ipReputation.set(ipAddress, reputation); + + // Clean up old reputation data periodically + if (this.ipReputation.size > 10000) { + this.cleanupReputationData(); + } + } + + /** + * Load existing reputation data + */ + loadReputationData() { + // In production, this would load from database or file + // For now, start with empty reputation + logger.info('IP reputation tracking initialized', { + maxEntries: 10000 + }); + } + + /** + * Clean up old reputation data + */ + cleanupReputationData() { + const now = Date.now(); + const thirtyDaysAgo = now - (30 * 24 * 60 * 60 * 1000); + + for (const [ip, reputation] of this.ipReputation.entries()) { + if (reputation.lastSeen < thirtyDaysAgo) { + this.ipReputation.delete(ip); + } + } + + logger.debug('IP reputation data cleaned up', { + remainingEntries: this.ipReputation.size + }); + } + + /** + * Get IP reputation data + * @param {string} ipAddress + * @returns {object|null} Reputation data + */ + getIPReputation(ipAddress) { + return this.ipReputation.get(ipAddress) || null; + } + + /** + * Get reputation statistics + * @returns {object} Reputation stats + */ + getReputationStats() { + const stats = { + totalIPs: this.ipReputation.size, + averageReputationScore: 0, + riskDistribution: { + minimal: 0, + low: 0, + medium: 0, + high: 0, + critical: 0 + }, + topRiskIPs: [], + actionStats: {} + }; + + if (this.ipReputation.size === 0) { + return stats; + } + + let totalReputationScore = 0; + const ipScores = []; + + for (const reputation of this.ipReputation.values()) { + totalReputationScore += reputation.reputationScore; + ipScores.push({ + ip: reputation.ipAddress, + score: reputation.reputationScore, + riskScore: reputation.averageRiskScore + }); + + // Count risk distribution + const riskLevel = this.getRiskLevelFromScore(reputation.averageRiskScore); + stats.riskDistribution[riskLevel]++; + + // Count actions + for (const [action, data] of reputation.actions.entries()) { + if (!stats.actionStats[action]) { + stats.actionStats[action] = 0; + } + stats.actionStats[action] += data.count; + } + } + + stats.averageReputationScore = totalReputationScore / this.ipReputation.size; + + // Get top risk IPs (lowest reputation scores) + stats.topRiskIPs = ipScores + .sort((a, b) => a.score - b.score) + .slice(0, 10); + + return stats; + } + + /** + * Get risk level from score + * @param {number} score + * @returns {string} Risk level + */ + getRiskLevelFromScore(score) { + if (score >= 90) return 'critical'; + if (score >= 80) return 'high'; + if (score >= 60) return 'medium'; + if (score >= 30) return 'low'; + return 'minimal'; + } + + /** + * Create middleware for enhanced rate limiting based on IP risk + * @param {object} baseRateLimiter + * @returns {Function} Enhanced rate limiting middleware + */ + createEnhancedRateLimitMiddleware(baseRateLimiter) { + return async (req, res, next) => { + try { + const ipAddress = getRequestIp(req); + + if (!ipAddress) { + return baseRateLimiter(req, res, next); + } + + // Get IP reputation + const reputation = this.getIPReputation(ipAddress); + + if (!reputation) { + return baseRateLimiter(req, res, next); + } + + // Adjust rate limits based on reputation + const riskMultiplier = this.getRiskMultiplier(reputation.averageRiskScore); + const adjustedLimits = this.adjustRateLimits(baseRateLimiter, riskMultiplier); + + // Apply adjusted rate limiting + // This would integrate with the existing rate limiter + // For now, proceed with normal rate limiting + baseRateLimiter(req, res, next); + + } catch (error) { + logger.error('Enhanced rate limiting middleware error', { + error: error.message, + ipAddress: getRequestIp(req), + traceId: req.logger?.fields?.traceId + }); + baseRateLimiter(req, res, next); + } + }; + } + + /** + * Get risk multiplier for rate limiting + * @param {number} riskScore + * @returns {number} Multiplier + */ + getRiskMultiplier(riskScore) { + if (riskScore >= 80) return 0.25; // 75% reduction for high risk + if (riskScore >= 60) return 0.5; // 50% reduction for medium risk + if (riskScore >= 30) return 0.75; // 25% reduction for low risk + return 1; // No reduction for minimal risk + } + + /** + * Adjust rate limits based on risk multiplier + * @param {object} rateLimiter + * @param {number} multiplier + * @returns {object} Adjusted rate limiter + */ + adjustRateLimits(rateLimiter, multiplier) { + // This would adjust the rate limiter configuration + // Implementation depends on the specific rate limiter being used + return { + ...rateLimiter, + bucketCapacity: Math.floor(rateLimiter.bucketCapacity * multiplier), + leakRatePerSecond: rateLimiter.leakRatePerSecond * multiplier + }; + } + + /** + * Get middleware statistics + * @returns {object} Middleware stats + */ + getMiddlewareStats() { + return { + reputationStats: this.getReputationStats(), + config: { + siwsEnabled: this.config.siws.enabled, + monitoringEnabled: this.config.monitoring.trackReputation, + alertThreshold: this.config.monitoring.alertThreshold + } + }; + } +} + +module.exports = { IPIntelligenceMiddleware }; diff --git a/src/services/ipBlockingService.js b/src/services/ipBlockingService.js new file mode 100644 index 0000000..b08be05 --- /dev/null +++ b/src/services/ipBlockingService.js @@ -0,0 +1,928 @@ +const { logger } = require('../utils/logger'); +const { IPIntelligenceService } = require('./ipIntelligenceService'); +const { IPIntelligenceMiddleware } = require('../middleware/ipIntelligenceMiddleware'); + +/** + * IP Blocking and Restriction Service + * Provides active defense mechanisms for high-risk IP addresses + */ +class IPBlockingService { + constructor(database, config = {}) { + this.database = database; + this.config = { + // Blocking configuration + blocking: { + enabled: config.blocking?.enabled !== false, + autoBlock: config.blocking?.autoBlock !== false, + blockDuration: config.blocking?.blockDuration || 24 * 60 * 60 * 1000, // 24 hours + maxBlockDuration: config.blocking?.maxBlockDuration || 7 * 24 * 60 * 60 * 1000, // 7 days + blockThreshold: config.blocking?.blockThreshold || 90, // Risk score threshold + escalationThreshold: config.blocking?.escalationThreshold || 95, // Escalation threshold + maxViolations: config.blocking?.maxViolations || 5 // Max violations before permanent block + }, + // Restriction configuration + restrictions: { + enabled: config.restrictions?.enabled !== false, + rateLimitReduction: config.restrictions?.rateLimitReduction || 0.5, // 50% reduction + requireVerification: config.restrictions?.requireVerification !== false, + limitActions: config.restrictions?.limitActions || [ + 'create_creator', + 'high_value_withdrawal', + 'bulk_operations' + ] + }, + // Monitoring configuration + monitoring: { + enabled: config.monitoring?.enabled !== false, + alertThreshold: config.monitoring?.alertThreshold || 80, + trackPatterns: config.monitoring?.trackPatterns !== false, + analyzeBehavior: config.monitoring?.analyzeBehavior !== false + }, + ...config + }; + + // Initialize blocking database table + this.initializeBlockingTable(); + + // IP violation tracking + this.ipViolations = new Map(); + + // Active blocks + this.activeBlocks = new Map(); + + // Load existing blocks + this.loadActiveBlocks(); + } + + /** + * Initialize database table for IP blocking + */ + initializeBlockingTable() { + try { + this.database.db.exec(` + CREATE TABLE IF NOT EXISTS ip_blocks ( + id TEXT PRIMARY KEY, + ip_address TEXT NOT NULL, + block_type TEXT NOT NULL, -- 'temporary', 'permanent', 'restriction' + risk_score INTEGER NOT NULL, + risk_level TEXT NOT NULL, + reason TEXT NOT NULL, + metadata_json TEXT, + created_at TEXT NOT NULL, + expires_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1, + violation_count INTEGER DEFAULT 1, + last_violation_at TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_ip_blocks_ip_address ON ip_blocks(ip_address); + CREATE INDEX IF NOT EXISTS idx_ip_blocks_expires_at ON ip_blocks(expires_at); + CREATE INDEX IF NOT EXISTS idx_ip_blocks_is_active ON ip_blocks(is_active); + `); + + logger.info('IP blocking database table initialized'); + } catch (error) { + logger.error('Failed to initialize IP blocking table', { + error: error.message + }); + } + } + + /** + * Load active blocks from database + */ + async loadActiveBlocks() { + try { + const now = new Date().toISOString(); + const blocks = this.database.db.prepare(` + SELECT * FROM ip_blocks + WHERE is_active = 1 AND (expires_at IS NULL OR expires_at > ?) + `).all(now); + + for (const block of blocks) { + this.activeBlocks.set(block.ip_address, { + ...block, + metadata: JSON.parse(block.metadata_json || '{}') + }); + } + + logger.info('Loaded active IP blocks', { + count: blocks.length + }); + + } catch (error) { + logger.error('Failed to load active blocks', { + error: error.message + }); + } + } + + /** + * Evaluate IP for blocking or restriction + * @param {string} ipAddress + * @param {object} riskAssessment + * @param {object} context + * @returns {Promise} Blocking decision + */ + async evaluateIP(ipAddress, riskAssessment, context = {}) { + try { + const decision = { + ipAddress, + action: 'allow', // 'allow', 'restrict', 'block' + reason: '', + duration: null, + metadata: { + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + riskFactors: riskAssessment.riskFactors, + context, + evaluatedAt: new Date().toISOString() + } + }; + + // Check if IP is already blocked + const existingBlock = this.activeBlocks.get(ipAddress); + if (existingBlock) { + return this.handleExistingBlock(existingBlock, decision); + } + + // Check violation history + const violations = this.getIPViolations(ipAddress); + decision.metadata.violationCount = violations.length; + + // Evaluate for blocking + if (this.shouldBlockIP(riskAssessment, violations, context)) { + return this.applyBlocking(ipAddress, riskAssessment, violations, decision); + } + + // Evaluate for restriction + if (this.shouldRestrictIP(riskAssessment, violations, context)) { + return this.applyRestriction(ipAddress, riskAssessment, violations, decision); + } + + // Log high-risk IPs that are allowed + if (riskAssessment.riskScore >= this.config.monitoring.alertThreshold) { + logger.warn('High risk IP allowed but monitored', { + ipAddress, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + violationCount: violations.length, + context + }); + } + + return decision; + + } catch (error) { + logger.error('IP evaluation failed', { + ipAddress, + error: error.message + }); + + // Fail safe - allow but monitor + return { + ipAddress, + action: 'allow', + reason: 'Evaluation failed - fail safe allow', + error: error.message + }; + } + } + + /** + * Handle existing block + * @param {object} existingBlock + * @param {object} decision + * @returns {object} Updated decision + */ + handleExistingBlock(existingBlock, decision) { + decision.action = existingBlock.block_type; + decision.reason = existingBlock.reason; + decision.duration = existingBlock.expires_at ? + new Date(existingBlock.expires_at) - new Date() : null; + decision.metadata.existingBlock = true; + + // Check if block should be updated + if (this.shouldEscalateBlock(existingBlock, decision.metadata.riskScore)) { + return this.escalateBlock(existingBlock, decision); + } + + return decision; + } + + /** + * Check if IP should be blocked + * @param {object} riskAssessment + * @param {array} violations + * @param {object} context + * @returns {boolean} + */ + shouldBlockIP(riskAssessment, violations, context) { + if (!this.config.blocking.enabled) return false; + if (!this.config.blocking.autoBlock) return false; + + const riskScore = riskAssessment.riskScore; + const riskLevel = riskAssessment.riskLevel; + + // Block based on risk score + if (riskScore >= this.config.blocking.blockThreshold) { + return true; + } + + // Block based on risk level + if (riskLevel === 'critical') { + return true; + } + + // Block based on violation count + if (violations.length >= this.config.blocking.maxViolations) { + return true; + } + + // Block based on specific risk factors + const criticalFactors = ['Tor exit node', 'High abuse confidence', 'Bot activity detected']; + const hasCriticalFactors = riskAssessment.riskFactors.some(factor => + criticalFactors.some(critical => factor.toLowerCase().includes(critical.toLowerCase())) + ); + + if (hasCriticalFactors) { + return true; + } + + // Block based on context + if (context.actionType === 'create_creator' && riskLevel === 'high') { + return true; + } + + return false; + } + + /** + * Check if IP should be restricted + * @param {object} riskAssessment + * @param {array} violations + * @param {object} context + * @returns {boolean} + */ + shouldRestrictIP(riskAssessment, violations, context) { + if (!this.config.restrictions.enabled) return false; + + const riskScore = riskAssessment.riskScore; + const riskLevel = riskAssessment.riskLevel; + + // Restrict based on risk score + if (riskScore >= this.config.monitoring.alertThreshold) { + return true; + } + + // Restrict based on risk level + if (riskLevel === 'high') { + return true; + } + + // Restrict based on violation history + if (violations.length >= 2) { + return true; + } + + // Restrict based on context + if (this.config.restrictions.limitActions.includes(context.actionType)) { + return riskLevel === 'medium'; + } + + return false; + } + + /** + * Apply blocking to IP + * @param {string} ipAddress + * @param {object} riskAssessment + * @param {array} violations + * @param {object} decision + * @returns {object} Updated decision + */ + async applyBlocking(ipAddress, riskAssessment, violations, decision) { + try { + const blockType = this.determineBlockType(riskAssessment, violations); + const duration = this.calculateBlockDuration(riskAssessment, violations); + const reason = this.generateBlockReason(riskAssessment, violations); + + // Create block record + const blockId = this.createBlockRecord(ipAddress, { + type: blockType, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + reason, + duration, + violations: violations.length + }); + + // Add to active blocks + this.activeBlocks.set(ipAddress, { + id: blockId, + ip_address: ipAddress, + block_type: blockType, + risk_score: riskAssessment.riskScore, + risk_level: riskAssessment.riskLevel, + reason, + created_at: new Date().toISOString(), + expires_at: duration ? new Date(Date.now() + duration).toISOString() : null, + is_active: 1, + violation_count: violations.length + 1, + last_violation_at: new Date().toISOString() + }); + + // Record violation + this.recordViolation(ipAddress, riskAssessment, 'block'); + + decision.action = blockType; + decision.reason = reason; + decision.duration = duration; + decision.metadata.blockId = blockId; + + logger.warn('IP blocked', { + ipAddress, + blockType, + duration, + reason, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + violationCount: violations.length + 1 + }); + + return decision; + + } catch (error) { + logger.error('Failed to apply IP block', { + ipAddress, + error: error.message + }); + + decision.action = 'allow'; + decision.reason = 'Block application failed - fail safe allow'; + decision.error = error.message; + + return decision; + } + } + + /** + * Apply restriction to IP + * @param {string} ipAddress + * @param {object} riskAssessment + * @param {array} violations + * @param {object} decision + * @returns {object} Updated decision + */ + async applyRestriction(ipAddress, riskAssessment, violations, decision) { + try { + const reason = this.generateRestrictionReason(riskAssessment, violations); + + // Create restriction record + const blockId = this.createBlockRecord(ipAddress, { + type: 'restriction', + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + reason, + duration: this.config.blocking.blockDuration, + violations: violations.length + }); + + // Add to active blocks + this.activeBlocks.set(ipAddress, { + id: blockId, + ip_address: ipAddress, + block_type: 'restriction', + risk_score: riskAssessment.riskScore, + risk_level: riskAssessment.riskLevel, + reason, + created_at: new Date().toISOString(), + expires_at: new Date(Date.now() + this.config.blocking.blockDuration).toISOString(), + is_active: 1, + violation_count: violations.length + 1, + last_violation_at: new Date().toISOString() + }); + + // Record violation + this.recordViolation(ipAddress, riskAssessment, 'restriction'); + + decision.action = 'restrict'; + decision.reason = reason; + decision.duration = this.config.blocking.blockDuration; + decision.metadata.blockId = blockId; + + logger.info('IP restricted', { + ipAddress, + reason, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + violationCount: violations.length + 1 + }); + + return decision; + + } catch (error) { + logger.error('Failed to apply IP restriction', { + ipAddress, + error: error.message + }); + + decision.action = 'allow'; + decision.reason = 'Restriction application failed - fail safe allow'; + decision.error = error.message; + + return decision; + } + } + + /** + * Determine block type based on risk assessment + * @param {object} riskAssessment + * @param {array} violations + * @returns {string} Block type + */ + determineBlockType(riskAssessment, violations) { + const riskScore = riskAssessment.riskScore; + const riskLevel = riskAssessment.riskLevel; + + // Permanent block for critical risk + if (riskLevel === 'critical' || riskScore >= this.config.blocking.escalationThreshold) { + return 'permanent'; + } + + // Temporary block for high risk + if (riskLevel === 'high' || violations.length >= this.config.blocking.maxViolations) { + return 'temporary'; + } + + // Default to temporary + return 'temporary'; + } + + /** + * Calculate block duration + * @param {object} riskAssessment + * @param {array} violations + * @returns {number|null} Duration in milliseconds + */ + calculateBlockDuration(riskAssessment, violations) { + const blockType = this.determineBlockType(riskAssessment, violations); + + if (blockType === 'permanent') { + return null; + } + + // Scale duration based on risk and violations + let baseDuration = this.config.blocking.blockDuration; + + // Increase duration for high risk + if (riskAssessment.riskLevel === 'high') { + baseDuration *= 2; + } + + // Increase duration for repeat violations + if (violations.length > 0) { + baseDuration *= (1 + violations.length * 0.5); + } + + // Cap at maximum duration + return Math.min(baseDuration, this.config.blocking.maxBlockDuration); + } + + /** + * Generate block reason + * @param {object} riskAssessment + * @param {array} violations + * @returns {string} Reason + */ + generateBlockReason(riskAssessment, violations) { + const reasons = []; + + if (riskAssessment.riskLevel === 'critical') { + reasons.push('Critical risk level detected'); + } + + if (riskAssessment.riskScore >= this.config.blocking.blockThreshold) { + reasons.push(`High risk score (${riskAssessment.riskScore})`); + } + + if (violations.length >= this.config.blocking.maxViolations) { + reasons.push(`Too many violations (${violations.length})`); + } + + // Add specific risk factors + const criticalFactors = riskAssessment.riskFactors.filter(factor => + factor.toLowerCase().includes('tor') || + factor.toLowerCase().includes('abuse') || + factor.toLowerCase().includes('bot') + ); + + if (criticalFactors.length > 0) { + reasons.push(`Critical factors: ${criticalFactors.join(', ')}`); + } + + return reasons.join('; ') || 'Security policy violation'; + } + + /** + * Generate restriction reason + * @param {object} riskAssessment + * @param {array} violations + * @returns {string} Reason + */ + generateRestrictionReason(riskAssessment, violations) { + const reasons = []; + + if (riskAssessment.riskScore >= this.config.monitoring.alertThreshold) { + reasons.push(`Elevated risk score (${riskAssessment.riskScore})`); + } + + if (riskAssessment.riskLevel === 'high') { + reasons.push('High risk level detected'); + } + + if (violations.length > 0) { + reasons.push(`Previous violations (${violations.length})`); + } + + return reasons.join('; ') || 'Security precaution'; + } + + /** + * Create block record in database + * @param {string} ipAddress + * @param {object} blockData + * @returns {string} Block ID + */ + createBlockRecord(ipAddress, blockData) { + const blockId = `block_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const now = new Date().toISOString(); + + this.database.db.prepare(` + INSERT INTO ip_blocks ( + id, ip_address, block_type, risk_score, risk_level, reason, + metadata_json, created_at, expires_at, violation_count, last_violation_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + blockId, + ipAddress, + blockData.type, + blockData.riskScore, + blockData.riskLevel, + blockData.reason, + JSON.stringify({ + violations: blockData.violations, + duration: blockData.duration, + assessedAt: now + }), + now, + blockData.duration ? new Date(Date.now() + blockData.duration).toISOString() : null, + blockData.violations, + now + ); + + return blockId; + } + + /** + * Record IP violation + * @param {string} ipAddress + * @param {object} riskAssessment + * @param {string} action + */ + recordViolation(ipAddress, riskAssessment, action) { + const now = Date.now(); + let violations = this.ipViolations.get(ipAddress); + + if (!violations) { + violations = []; + } + + violations.push({ + timestamp: now, + action, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + riskFactors: riskAssessment.riskFactors + }); + + // Keep only last 10 violations + if (violations.length > 10) { + violations.shift(); + } + + this.ipViolations.set(ipAddress, violations); + } + + /** + * Get IP violations + * @param {string} ipAddress + * @returns {array} Violations + */ + getIPViolations(ipAddress) { + return this.ipViolations.get(ipAddress) || []; + } + + /** + * Check if IP should escalate block + * @param {object} existingBlock + * @param {number} newRiskScore + * @returns {boolean} + */ + shouldEscalateBlock(existingBlock, newRiskScore) { + // Escalate if new risk score is significantly higher + if (newRiskScore > existingBlock.risk_score + 20) { + return true; + } + + // Escalate if block type is temporary but risk is critical + if (existingBlock.block_type === 'temporary' && newRiskScore >= this.config.blocking.escalationThreshold) { + return true; + } + + return false; + } + + /** + * Escalate existing block + * @param {object} existingBlock + * @param {object} decision + * @returns {object} Updated decision + */ + async escalateBlock(existingBlock, decision) { + try { + // Update block to permanent + this.database.db.prepare(` + UPDATE ip_blocks + SET block_type = 'permanent', expires_at = NULL, updated_at = ? + WHERE id = ? + `).run(new Date().toISOString(), existingBlock.id); + + // Update active blocks + existingBlock.block_type = 'permanent'; + existingBlock.expires_at = null; + existingBlock.updated_at = new Date().toISOString(); + + logger.warn('IP block escalated', { + ipAddress: existingBlock.ip_address, + previousType: 'temporary', + newType: 'permanent', + riskScore: decision.metadata.riskScore + }); + + decision.action = 'permanent'; + decision.reason = 'Block escalated due to increased risk'; + decision.duration = null; + + return decision; + + } catch (error) { + logger.error('Failed to escalate IP block', { + ipAddress: existingBlock.ip_address, + error: error.message + }); + + return decision; + } + } + + /** + * Check if IP is blocked + * @param {string} ipAddress + * @returns {object|null} Block information + */ + isIPBlocked(ipAddress) { + const block = this.activeBlocks.get(ipAddress); + + if (!block) { + return null; + } + + // Check if block has expired + if (block.expires_at && new Date(block.expires_at) < new Date()) { + this.removeExpiredBlock(ipAddress); + return null; + } + + return { + blockType: block.block_type, + reason: block.reason, + expiresAt: block.expires_at, + riskScore: block.risk_score, + riskLevel: block.risk_level + }; + } + + /** + * Remove expired block + * @param {string} ipAddress + */ + removeExpiredBlock(ipAddress) { + try { + // Update database + this.database.db.prepare(` + UPDATE ip_blocks SET is_active = 0 WHERE ip_address = ? + `).run(ipAddress); + + // Remove from active blocks + this.activeBlocks.delete(ipAddress); + + logger.info('Expired IP block removed', { + ipAddress + }); + + } catch (error) { + logger.error('Failed to remove expired block', { + ipAddress, + error: error.message + }); + } + } + + /** + * Manually block IP + * @param {string} ipAddress + * @param {object} options + * @returns {Promise} Block result + */ + async manualBlockIP(ipAddress, options = {}) { + const { + type = 'temporary', + duration = this.config.blocking.blockDuration, + reason = 'Manual block by administrator', + riskScore = 100, + riskLevel = 'critical' + } = options; + + try { + // Create block record + const blockId = this.createBlockRecord(ipAddress, { + type, + riskScore, + riskLevel, + reason, + duration, + violations: 0 + }); + + // Add to active blocks + this.activeBlocks.set(ipAddress, { + id: blockId, + ip_address: ipAddress, + block_type: type, + risk_score: riskScore, + risk_level: riskLevel, + reason, + created_at: new Date().toISOString(), + expires_at: type === 'permanent' ? null : new Date(Date.now() + duration).toISOString(), + is_active: 1, + violation_count: 0, + last_violation_at: new Date().toISOString() + }); + + logger.warn('Manual IP block created', { + ipAddress, + type, + duration, + reason, + blockId + }); + + return { + success: true, + blockId, + ipAddress, + type, + duration, + reason + }; + + } catch (error) { + logger.error('Failed to create manual block', { + ipAddress, + error: error.message + }); + + return { + success: false, + error: error.message + }; + } + } + + /** + * Manually unblock IP + * @param {string} ipAddress + * @param {string} reason + * @returns {Promise} Unblock result + */ + async manualUnblockIP(ipAddress, reason = 'Manual unblock by administrator') { + try { + // Update database + this.database.db.prepare(` + UPDATE ip_blocks + SET is_active = 0, updated_at = ? + WHERE ip_address = ? AND is_active = 1 + `).run(new Date().toISOString(), ipAddress); + + // Remove from active blocks + const block = this.activeBlocks.get(ipAddress); + this.activeBlocks.delete(ipAddress); + + logger.info('Manual IP unblock', { + ipAddress, + reason, + previousBlock: block ? block.block_type : null + }); + + return { + success: true, + ipAddress, + reason, + previousBlock: block ? block.block_type : null + }; + + } catch (error) { + logger.error('Failed to create manual unblock', { + ipAddress, + error: error.message + }); + + return { + success: false, + error: error.message + }; + } + } + + /** + * Get blocking statistics + * @returns {object} Blocking stats + */ + getBlockingStats() { + const stats = { + activeBlocks: this.activeBlocks.size, + blockTypes: { + temporary: 0, + permanent: 0, + restriction: 0 + }, + riskLevels: { + minimal: 0, + low: 0, + medium: 0, + high: 0, + critical: 0 + }, + totalViolations: 0, + recentBlocks: [] + }; + + // Count block types and risk levels + for (const block of this.activeBlocks.values()) { + stats.blockTypes[block.block_type]++; + stats.riskLevels[block.risk_level]++; + stats.totalViolations += block.violation_count; + } + + // Get recent blocks + const recentBlocks = Array.from(this.activeBlocks.values()) + .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) + .slice(0, 10); + + stats.recentBlocks = recentBlocks.map(block => ({ + ipAddress: block.ip_address, + blockType: block.block_type, + riskLevel: block.risk_level, + reason: block.reason, + createdAt: block.created_at, + expiresAt: block.expires_at + })); + + return stats; + } + + /** + * Clean up expired blocks + */ + cleanupExpiredBlocks() { + const now = new Date(); + const expiredIPs = []; + + for (const [ip, block] of this.activeBlocks.entries()) { + if (block.expires_at && new Date(block.expires_at) < now) { + expiredIPs.push(ip); + } + } + + expiredIPs.forEach(ip => this.removeExpiredBlock(ip)); + + if (expiredIPs.length > 0) { + logger.info('Cleaned up expired blocks', { + count: expiredIPs.length + }); + } + } +} + +module.exports = { IPBlockingService }; diff --git a/src/services/ipIntelligenceService.js b/src/services/ipIntelligenceService.js new file mode 100644 index 0000000..9ea0f4d --- /dev/null +++ b/src/services/ipIntelligenceService.js @@ -0,0 +1,953 @@ +const axios = require('axios'); +const { logger } = require('../utils/logger'); + +/** + * IP Intelligence Service for risk assessment and fraud detection + * Integrates with multiple IP intelligence providers to calculate risk scores + */ +class IPIntelligenceService { + constructor(config) { + this.config = { + // Multiple providers for redundancy and accuracy + providers: { + ipinfo: { + enabled: config.ipinfo?.enabled || false, + apiKey: config.ipinfo?.apiKey || process.env.IPINFO_API_KEY || '', + baseUrl: 'https://ipinfo.io', + timeout: config.ipinfo?.timeout || 5000 + }, + maxmind: { + enabled: config.maxmind?.enabled || false, + apiKey: config.maxmind?.apiKey || process.env.MAXMIND_API_KEY || '', + baseUrl: 'https://geoip.maxmind.com/geoip/v2.1', + timeout: config.maxmind?.timeout || 5000 + }, + abuseipdb: { + enabled: config.abuseipdb?.enabled || false, + apiKey: config.abuseipdb?.apiKey || process.env.ABUSEIPDB_API_KEY || '', + baseUrl: 'https://api.abuseipdb.com/api/v2', + timeout: config.abuseipdb?.timeout || 5000 + }, + ipqualityscore: { + enabled: config.ipqualityscore?.enabled || false, + apiKey: config.ipqualityscore?.apiKey || process.env.IPQUALITYSCORE_API_KEY || '', + baseUrl: 'https://ipqualityscore.com/api/json', + timeout: config.ipqualityscore?.timeout || 5000 + } + }, + // Risk scoring configuration + riskThresholds: { + low: config.riskThresholds?.low || 30, + medium: config.riskThresholds?.medium || 60, + high: config.riskThresholds?.high || 80, + critical: config.riskThresholds?.critical || 90 + }, + // Caching configuration + cache: { + enabled: config.cache?.enabled !== false, + ttl: config.cache?.ttl || 3600000, // 1 hour + maxSize: config.cache?.maxSize || 10000 + }, + // Rate limiting + rateLimit: { + requestsPerMinute: config.rateLimit?.requestsPerMinute || 100, + burstLimit: config.rateLimit?.burstLimit || 20 + } + }; + + // Initialize cache + this.cache = new Map(); + this.cacheTimestamps = new Map(); + + // Rate limiting + this.requestTimestamps = []; + + // Known malicious patterns + this.maliciousPatterns = { + torExitNodes: new Set(), + vpnProviders: new Set(), + proxyServers: new Set(), + dataCenters: new Set() + }; + + // Initialize known threats + this.initializeKnownThreats(); + } + + /** + * Initialize known threat patterns + */ + initializeKnownThreats() { + // Tor exit node patterns (simplified - in production, use real-time data) + this.maliciousPatterns.torExitNodes = new Set([ + '185.220.101.', '185.220.102.', '185.220.103.', + '185.220.104.', '185.220.105.', '185.220.106.' + ]); + + // Known VPN provider ranges (simplified) + this.maliciousPatterns.vpnProviders = new Set([ + '1.1.1.', '8.8.8.', '208.67.222.', '208.67.220.' + ]); + + // Known proxy server patterns + this.maliciousPatterns.proxyServers = new Set([ + '192.168.', '10.', '172.16.', '172.17.', '172.18.', '172.19.', + '172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', + '172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.' + ]); + + // Known data center ranges + this.maliciousPatterns.dataCenters = new Set([ + '52.', '54.', '107.', '172.', '174.', '175.', '204.' + ]); + } + + /** + * Assess IP risk using multiple intelligence providers + * @param {string} ipAddress - IP address to assess + * @param {object} options - Assessment options + * @returns {Promise} Risk assessment result + */ + async assessIPRisk(ipAddress, options = {}) { + try { + // Check cache first + const cached = this.getCachedResult(ipAddress); + if (cached && !options.bypassCache) { + logger.debug('IP risk assessment served from cache', { + ipAddress, + riskScore: cached.riskScore, + traceId: logger.defaultMeta?.traceId + }); + return cached; + } + + // Rate limiting check + if (!this.checkRateLimit()) { + throw new Error('Rate limit exceeded for IP intelligence requests'); + } + + // Validate IP address + if (!this.isValidIP(ipAddress)) { + return this.createInvalidIPResult(ipAddress); + } + + // Skip private/internal IPs + if (this.isPrivateIP(ipAddress)) { + return this.createPrivateIPResult(ipAddress); + } + + // Collect intelligence from all enabled providers + const providerResults = await this.collectProviderData(ipAddress); + + // Calculate comprehensive risk score + const riskAssessment = this.calculateRiskScore(ipAddress, providerResults); + + // Cache the result + this.cacheResult(ipAddress, riskAssessment); + + logger.info('IP risk assessment completed', { + ipAddress, + riskScore: riskAssessment.riskScore, + riskLevel: riskAssessment.riskLevel, + providers: Object.keys(providerResults), + traceId: logger.defaultMeta?.traceId + }); + + return riskAssessment; + + } catch (error) { + logger.error('IP risk assessment failed', { + ipAddress, + error: error.message, + traceId: logger.defaultMeta?.traceId + }); + + // Fail safe - return medium risk on errors + return this.createErrorResult(ipAddress, error); + } + } + + /** + * Collect data from all enabled providers + * @param {string} ipAddress + * @returns {Promise} Provider results + */ + async collectProviderData(ipAddress) { + const results = {}; + const providerPromises = []; + + // IPInfo provider + if (this.config.providers.ipinfo.enabled) { + providerPromises.push( + this.queryIPInfo(ipAddress) + .then(data => { results.ipinfo = data; }) + .catch(error => { + logger.warn('IPInfo provider failed', { ipAddress, error: error.message }); + results.ipinfo = { error: error.message }; + }) + ); + } + + // MaxMind provider + if (this.config.providers.maxmind.enabled) { + providerPromises.push( + this.queryMaxMind(ipAddress) + .then(data => { results.maxmind = data; }) + .catch(error => { + logger.warn('MaxMind provider failed', { ipAddress, error: error.message }); + results.maxmind = { error: error.message }; + }) + ); + } + + // AbuseIPDB provider + if (this.config.providers.abuseipdb.enabled) { + providerPromises.push( + this.queryAbuseIPDB(ipAddress) + .then(data => { results.abuseipdb = data; }) + .catch(error => { + logger.warn('AbuseIPDB provider failed', { ipAddress, error: error.message }); + results.abuseipdb = { error: error.message }; + }) + ); + } + + // IPQualityScore provider + if (this.config.providers.ipqualityscore.enabled) { + providerPromises.push( + this.queryIPQualityScore(ipAddress) + .then(data => { results.ipqualityscore = data; }) + .catch(error => { + logger.warn('IPQualityScore provider failed', { ipAddress, error: error.message }); + results.ipqualityscore = { error: error.message }; + }) + ); + } + + // Wait for all provider requests + await Promise.allSettled(providerPromises); + + return results; + } + + /** + * Query IPInfo provider + * @param {string} ipAddress + * @returns {Promise} IPInfo data + */ + async queryIPInfo(ipAddress) { + const url = `${this.config.providers.ipinfo.baseUrl}/${ipAddress}/json`; + const headers = this.config.providers.ipinfo.apiKey ? + { Authorization: `Bearer ${this.config.providers.ipinfo.apiKey}` } : {}; + + const response = await axios.get(url, { + headers, + timeout: this.config.providers.ipinfo.timeout + }); + + const data = response.data; + + return { + provider: 'ipinfo', + ip: data.ip, + country: data.country, + region: data.region, + city: data.city, + org: data.org, + postal: data.postal, + timezone: data.timezone, + hostname: data.hostname, + isVPN: this.isVPNIndicator(data.org, data.hostname), + isHosting: this.isHostingProvider(data.org), + riskFactors: this.extractIPInfoRiskFactors(data) + }; + } + + /** + * Query MaxMind provider + * @param {string} ipAddress + * @returns {Promise} MaxMind data + */ + async queryMaxMind(ipAddress) { + const url = `${this.config.providers.maxmind.baseUrl}/country/${ipAddress}`; + const headers = { + 'Authorization': `Bearer ${this.config.providers.maxmind.apiKey}` + }; + + const response = await axios.get(url, { + headers, + timeout: this.config.providers.maxmind.timeout + }); + + const data = response.data; + + return { + provider: 'maxmind', + country: data.country?.iso_code, + riskFactors: this.extractMaxMindRiskFactors(data) + }; + } + + /** + * Query AbuseIPDB provider + * @param {string} ipAddress + * @returns {Promise} AbuseIPDB data + */ + async queryAbuseIPDB(ipAddress) { + const url = this.config.providers.abuseipdb.baseUrl; + const params = { + ipAddress, + maxAgeInDays: 90, + verbose: '' + }; + const headers = { + 'Key': this.config.providers.abuseipdb.apiKey, + 'Accept': 'application/json' + }; + + const response = await axios.get(url, { + params, + headers, + timeout: this.config.providers.abuseipdb.timeout + }); + + const data = response.data; + + return { + provider: 'abuseipdb', + abuseConfidenceScore: data.data.abuseConfidenceScore, + countryCode: data.data.countryCode, + usageType: data.data.usageType, + isTor: data.data.isTor, + isPublicProxy: data.data.isPublicProxy, + reports: data.data.totalReports, + lastReportedAt: data.data.lastReportedAt, + riskFactors: this.extractAbuseIPDBRiskFactors(data.data) + }; + } + + /** + * Query IPQualityScore provider + * @param {string} ipAddress + * @returns {Promise} IPQualityScore data + */ + async queryIPQualityScore(ipAddress) { + const url = this.config.providers.ipqualityscore.baseUrl; + const params = { + IP: ipAddress, + key: this.config.providers.ipqualityscore.apiKey, + strictness: 1, + allow_public_access_points: 'true', + fast: 'false', + lighter_penalties: 'false' + }; + + const response = await axios.get(url, { + params, + timeout: this.config.providers.ipqualityscore.timeout + }); + + const data = response.data; + + return { + provider: 'ipqualityscore', + fraudScore: data.fraud_score, + vpn: data.vpn, + tor: data.tor, + proxy: data.proxy, + activeVPN: data.active_vpn, + activeTor: data.active_tor, + activeProxy: data.active_proxy, + recentAbuse: data.recent_abuse, + botStatus: data.bot_status, + riskFactors: this.extractIPQualityScoreRiskFactors(data) + }; + } + + /** + * Calculate comprehensive risk score + * @param {string} ipAddress + * @param {object} providerResults + * @returns {object} Risk assessment + */ + calculateRiskScore(ipAddress, providerResults) { + let riskScore = 0; + const riskFactors = []; + const providerScores = {}; + + // Process each provider's data + Object.entries(providerResults).forEach(([provider, data]) => { + if (data.error) { + providerScores[provider] = { score: 0, error: data.error }; + return; + } + + const providerScore = this.calculateProviderScore(provider, data); + providerScores[provider] = providerScore; + riskScore += providerScore.score * providerScore.weight; + + // Collect risk factors + if (providerScore.riskFactors && providerScore.riskFactors.length > 0) { + riskFactors.push(...providerScore.riskFactors); + } + }); + + // Normalize score (0-100) + riskScore = Math.min(Math.max(riskScore, 0), 100); + + // Determine risk level + const riskLevel = this.getRiskLevel(riskScore); + + // Add pattern-based risk factors + const patternFactors = this.checkMaliciousPatterns(ipAddress); + riskFactors.push(...patternFactors); + + return { + ipAddress, + riskScore: Math.round(riskScore), + riskLevel, + providerScores, + riskFactors: [...new Set(riskFactors)], // Remove duplicates + assessedAt: new Date().toISOString(), + recommendations: this.generateRecommendations(riskLevel, riskFactors), + metadata: { + providers: Object.keys(providerResults), + assessmentTime: Date.now() + } + }; + } + + /** + * Calculate score for individual provider + * @param {string} provider + * @param {object} data + * @returns {object} Provider score + */ + calculateProviderScore(provider, data) { + let score = 0; + const riskFactors = []; + let weight = 0.25; // Default weight + + switch (provider) { + case 'ipinfo': + weight = 0.25; + if (data.isVPN) { + score += 40; + riskFactors.push('VPN detected via IPInfo'); + } + if (data.isHosting) { + score += 20; + riskFactors.push('Hosting provider detected'); + } + if (data.riskFactors && data.riskFactors.length > 0) { + score += 15; + riskFactors.push(...data.riskFactors); + } + break; + + case 'maxmind': + weight = 0.15; + if (data.riskFactors && data.riskFactors.length > 0) { + score += 20; + riskFactors.push(...data.riskFactors); + } + break; + + case 'abuseipdb': + weight = 0.35; + if (data.abuseConfidenceScore > 75) { + score += 60; + riskFactors.push('High abuse confidence'); + } else if (data.abuseConfidenceScore > 50) { + score += 40; + riskFactors.push('Medium abuse confidence'); + } else if (data.abuseConfidenceScore > 25) { + score += 20; + riskFactors.push('Low abuse confidence'); + } + if (data.isTor) { + score += 50; + riskFactors.push('Tor exit node detected'); + } + if (data.isPublicProxy) { + score += 30; + riskFactors.push('Public proxy detected'); + } + if (data.reports > 10) { + score += 25; + riskFactors.push('High abuse reports'); + } + if (data.riskFactors && data.riskFactors.length > 0) { + riskFactors.push(...data.riskFactors); + } + break; + + case 'ipqualityscore': + weight = 0.25; + if (data.fraudScore > 75) { + score += 50; + riskFactors.push('High fraud score'); + } else if (data.fraudScore > 50) { + score += 30; + riskFactors.push('Medium fraud score'); + } + if (data.vpn || data.activeVPN) { + score += 35; + riskFactors.push('VPN detected via IPQualityScore'); + } + if (data.tor || data.activeTor) { + score += 45; + riskFactors.push('Tor detected via IPQualityScore'); + } + if (data.proxy || data.activeProxy) { + score += 25; + riskFactors.push('Proxy detected via IPQualityScore'); + } + if (data.recentAbuse) { + score += 30; + riskFactors.push('Recent abuse detected'); + } + if (data.botStatus === 'bad') { + score += 40; + riskFactors.push('Bot activity detected'); + } + if (data.riskFactors && data.riskFactors.length > 0) { + riskFactors.push(...data.riskFactors); + } + break; + } + + return { + score, + weight, + riskFactors + }; + } + + /** + * Check for malicious patterns + * @param {string} ipAddress + * @returns {array} Risk factors + */ + checkMaliciousPatterns(ipAddress) { + const riskFactors = []; + + // Check Tor exit nodes + for (const pattern of this.maliciousPatterns.torExitNodes) { + if (ipAddress.startsWith(pattern)) { + riskFactors.push('Known Tor exit node pattern'); + break; + } + } + + // Check VPN providers + for (const pattern of this.maliciousPatterns.vpnProviders) { + if (ipAddress.startsWith(pattern)) { + riskFactors.push('Known VPN provider pattern'); + break; + } + } + + // Check proxy servers + for (const pattern of this.maliciousPatterns.proxyServers) { + if (ipAddress.startsWith(pattern)) { + riskFactors.push('Proxy server pattern'); + break; + } + } + + // Check data centers + for (const pattern of this.maliciousPatterns.dataCenters) { + if (ipAddress.startsWith(pattern)) { + riskFactors.push('Data center IP range'); + break; + } + } + + return riskFactors; + } + + /** + * Extract risk factors from IPInfo data + * @param {object} data + * @returns {array} Risk factors + */ + extractIPInfoRiskFactors(data) { + const factors = []; + + if (data.org && data.org.toLowerCase().includes('vpn')) { + factors.push('VPN organization detected'); + } + if (data.org && data.org.toLowerCase().includes('hosting')) { + factors.push('Hosting organization detected'); + } + if (data.hostname && data.hostname.toLowerCase().includes('tor')) { + factors.push('Tor hostname detected'); + } + + return factors; + } + + /** + * Extract risk factors from MaxMind data + * @param {object} data + * @returns {array} Risk factors + */ + extractMaxMindRiskFactors(data) { + const factors = []; + + // Add MaxMind-specific risk factors + if (data.country) { + const highRiskCountries = ['CN', 'RU', 'IR', 'KP']; + if (highRiskCountries.includes(data.country.iso_code)) { + factors.push(`High-risk country: ${data.country.iso_code}`); + } + } + + return factors; + } + + /** + * Extract risk factors from AbuseIPDB data + * @param {object} data + * @returns {array} Risk factors + */ + extractAbuseIPDBRiskFactors(data) { + const factors = []; + + if (data.usageType) { + const highRiskUsageTypes = ['commercial', 'search engine spider', 'scraper']; + if (highRiskUsageTypes.includes(data.usageType.toLowerCase())) { + factors.push(`High-risk usage type: ${data.usageType}`); + } + } + + return factors; + } + + /** + * Extract risk factors from IPQualityScore data + * @param {object} data + * @returns {array} Risk factors + */ + extractIPQualityScoreRiskFactors(data) { + const factors = []; + + if (data.mobile && data.mobile === false) { + factors.push('Non-mobile connection (potentially automated)'); + } + + return factors; + } + + /** + * Check if IP is a VPN indicator + * @param {string} org + * @param {string} hostname + * @returns {boolean} + */ + isVPNIndicator(org, hostname) { + if (!org && !hostname) return false; + + const vpnKeywords = ['vpn', 'private', 'anonymous', 'hide', 'proxy']; + const checkString = `${org} ${hostname}`.toLowerCase(); + + return vpnKeywords.some(keyword => checkString.includes(keyword)); + } + + /** + * Check if IP is a hosting provider + * @param {string} org + * @returns {boolean} + */ + isHostingProvider(org) { + if (!org) return false; + + const hostingKeywords = ['hosting', 'server', 'datacenter', 'cloud', 'aws', 'azure', 'gcp']; + return hostingKeywords.some(keyword => org.toLowerCase().includes(keyword)); + } + + /** + * Get risk level based on score + * @param {number} score + * @returns {string} Risk level + */ + getRiskLevel(score) { + if (score >= this.config.riskThresholds.critical) return 'critical'; + if (score >= this.config.riskThresholds.high) return 'high'; + if (score >= this.config.riskThresholds.medium) return 'medium'; + if (score >= this.config.riskThresholds.low) return 'low'; + return 'minimal'; + } + + /** + * Generate recommendations based on risk level and factors + * @param {string} riskLevel + * @param {array} riskFactors + * @returns {array} Recommendations + */ + generateRecommendations(riskLevel, riskFactors) { + const recommendations = []; + + switch (riskLevel) { + case 'critical': + recommendations.push('BLOCK - Immediate blocking recommended'); + recommendations.push('Manual review required'); + recommendations.push('Consider legal action if abuse detected'); + break; + case 'high': + recommendations.push('RESTRICT - Limit critical actions'); + recommendations.push('Enhanced monitoring required'); + recommendations.push('Additional verification steps'); + break; + case 'medium': + recommendations.push('MONITOR - Increased monitoring'); + recommendations.push('Rate limiting recommended'); + recommendations.push('Periodic review advised'); + break; + case 'low': + recommendations.push('STANDARD - Normal processing'); + recommendations.push('Basic monitoring sufficient'); + break; + case 'minimal': + recommendations.push('TRUSTED - Minimal restrictions'); + break; + } + + // Add factor-specific recommendations + if (riskFactors.includes('VPN detected')) { + recommendations.push('VPN users may require additional verification'); + } + if (riskFactors.includes('Tor exit node')) { + recommendations.push('Tor users should be carefully monitored'); + } + if (riskFactors.includes('High abuse confidence')) { + recommendations.push('High abuse history - consider blocking'); + } + + return recommendations; + } + + /** + * Validate IP address format + * @param {string} ipAddress + * @returns {boolean} + */ + isValidIP(ipAddress) { + const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; + + return ipv4Regex.test(ipAddress) || ipv6Regex.test(ipAddress); + } + + /** + * Check if IP is private/internal + * @param {string} ipAddress + * @returns {boolean} + */ + isPrivateIP(ipAddress) { + const privateRanges = [ + /^10\./, + /^172\.(1[6-9]|2[0-9]|3[0-1])\./, + /^192\.168\./, + /^127\./, + /^169\.254\./, + /^::1$/, + /^fc00:/, + /^fe80:/ + ]; + + return privateRanges.some(range => range.test(ipAddress)); + } + + /** + * Create result for invalid IP + * @param {string} ipAddress + * @returns {object} Invalid IP result + */ + createInvalidIPResult(ipAddress) { + return { + ipAddress, + riskScore: 100, + riskLevel: 'critical', + providerScores: {}, + riskFactors: ['Invalid IP address format'], + assessedAt: new Date().toISOString(), + recommendations: ['BLOCK - Invalid IP address'], + metadata: { + providers: [], + assessmentTime: Date.now(), + error: 'Invalid IP address format' + } + }; + } + + /** + * Create result for private IP + * @param {string} ipAddress + * @returns {object} Private IP result + */ + createPrivateIPResult(ipAddress) { + return { + ipAddress, + riskScore: 0, + riskLevel: 'minimal', + providerScores: {}, + riskFactors: ['Private/internal IP address'], + assessedAt: new Date().toISOString(), + recommendations: ['TRUSTED - Internal network address'], + metadata: { + providers: [], + assessmentTime: Date.now(), + isPrivate: true + } + }; + } + + /** + * Create result for errors + * @param {string} ipAddress + * @param {Error} error + * @returns {object} Error result + */ + createErrorResult(ipAddress, error) { + return { + ipAddress, + riskScore: 50, + riskLevel: 'medium', + providerScores: {}, + riskFactors: ['Assessment failed - using default risk'], + assessedAt: new Date().toISOString(), + recommendations: ['MONITOR - Assessment service unavailable'], + metadata: { + providers: [], + assessmentTime: Date.now(), + error: error.message + } + }; + } + + /** + * Check rate limiting + * @returns {boolean} True if request is allowed + */ + checkRateLimit() { + const now = Date.now(); + const oneMinuteAgo = now - 60000; + + // Remove old timestamps + this.requestTimestamps = this.requestTimestamps.filter(timestamp => timestamp > oneMinuteAgo); + + // Check if under rate limit + if (this.requestTimestamps.length >= this.config.rateLimit.requestsPerMinute) { + return false; + } + + // Add current timestamp + this.requestTimestamps.push(now); + return true; + } + + /** + * Get cached result + * @param {string} ipAddress + * @returns {object|null} Cached result + */ + getCachedResult(ipAddress) { + if (!this.config.cache.enabled) return null; + + const cached = this.cache.get(ipAddress); + const timestamp = this.cacheTimestamps.get(ipAddress); + + if (cached && timestamp && (Date.now() - timestamp) < this.config.cache.ttl) { + return cached; + } + + // Remove expired cache entry + if (cached) { + this.cache.delete(ipAddress); + this.cacheTimestamps.delete(ipAddress); + } + + return null; + } + + /** + * Cache assessment result + * @param {string} ipAddress + * @param {object} result + */ + cacheResult(ipAddress, result) { + if (!this.config.cache.enabled) return; + + // Clean up cache if over size limit + if (this.cache.size >= this.config.cache.maxSize) { + this.cleanupCache(); + } + + this.cache.set(ipAddress, result); + this.cacheTimestamps.set(ipAddress, Date.now()); + } + + /** + * Clean up expired cache entries + */ + cleanupCache() { + const now = Date.now(); + const expiredKeys = []; + + for (const [ip, timestamp] of this.cacheTimestamps.entries()) { + if (now - timestamp > this.config.cache.ttl) { + expiredKeys.push(ip); + } + } + + expiredKeys.forEach(ip => { + this.cache.delete(ip); + this.cacheTimestamps.delete(ip); + }); + + // If still over limit, remove oldest entries + if (this.cache.size >= this.config.cache.maxSize) { + const entries = Array.from(this.cacheTimestamps.entries()) + .sort((a, b) => a[1] - b[1]); + + const toRemove = entries.slice(0, Math.floor(this.config.cache.maxSize * 0.2)); + toRemove.forEach(([ip]) => { + this.cache.delete(ip); + this.cacheTimestamps.delete(ip); + }); + } + } + + /** + * Get cache statistics + * @returns {object} Cache stats + */ + getCacheStats() { + return { + size: this.cache.size, + maxSize: this.config.cache.maxSize, + ttl: this.config.cache.ttl, + enabled: this.config.cache.enabled + }; + } + + /** + * Get service statistics + * @returns {object} Service stats + */ + getServiceStats() { + return { + providers: Object.keys(this.config.providers).filter(key => this.config.providers[key].enabled), + cacheStats: this.getCacheStats(), + rateLimit: { + requestsPerMinute: this.config.rateLimit.requestsPerMinute, + currentUsage: this.requestTimestamps.length + }, + riskThresholds: this.config.riskThresholds + }; + } +} + +module.exports = { IPIntelligenceService }; diff --git a/src/services/ipMonitoringService.js b/src/services/ipMonitoringService.js new file mode 100644 index 0000000..d285ec1 --- /dev/null +++ b/src/services/ipMonitoringService.js @@ -0,0 +1,844 @@ +const { logger } = require('../utils/logger'); +const { sendEmail } = require('../utils/email'); + +/** + * IP Intelligence Monitoring and Alerting Service + * Provides comprehensive monitoring, analytics, and alerting for IP intelligence + */ +class IPMonitoringService { + constructor(database, config = {}) { + this.database = database; + this.config = { + // Monitoring configuration + monitoring: { + enabled: config.monitoring?.enabled !== false, + interval: config.monitoring?.interval || 5 * 60 * 1000, // 5 minutes + retentionDays: config.monitoring?.retentionDays || 30, + batchSize: config.monitoring?.batchSize || 100 + }, + // Alerting configuration + alerting: { + enabled: config.alerting?.enabled !== false, + email: config.alerting?.email || process.env.SECURITY_ALERT_EMAIL, + thresholds: { + highRiskIPs: config.alerting?.thresholds?.highRiskIPs || 10, + blockedIPs: config.alerting?.thresholds?.blockedIPs || 5, + unusualActivity: config.alerting?.thresholds?.unusualActivity || 20, + reputationDrop: config.alerting?.thresholds?.reputationDrop || 30 + }, + cooldown: config.alerting?.cooldown || 15 * 60 * 1000 // 15 minutes + }, + // Analytics configuration + analytics: { + enabled: config.analytics?.enabled !== false, + aggregationIntervals: config.analytics?.aggregationIntervals || ['hourly', 'daily'], + topN: config.analytics?.topN || 10, + includeDetails: config.analytics?.includeDetails || false + }, + ...config + }; + + // Monitoring state + this.isRunning = false; + this.monitoringTimer = null; + this.alertCooldowns = new Map(); + + // Analytics cache + this.analyticsCache = new Map(); + this.lastAnalyticsUpdate = null; + + // Initialize monitoring tables + this.initializeMonitoringTables(); + } + + /** + * Initialize monitoring database tables + */ + initializeMonitoringTables() { + try { + this.database.db.exec(` + CREATE TABLE IF NOT EXISTS ip_monitoring_events ( + id TEXT PRIMARY KEY, + ip_address TEXT NOT NULL, + event_type TEXT NOT NULL, -- 'risk_assessment', 'block', 'unblock', 'violation' + risk_score INTEGER, + risk_level TEXT, + metadata_json TEXT, + timestamp TEXT NOT NULL, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS ip_analytics_hourly ( + id TEXT PRIMARY KEY, + ip_address TEXT NOT NULL, + hour_timestamp TEXT NOT NULL, + total_requests INTEGER DEFAULT 0, + avg_risk_score REAL DEFAULT 0, + max_risk_score INTEGER DEFAULT 0, + violations INTEGER DEFAULT 0, + blocks INTEGER DEFAULT 0, + unique_actions TEXT, + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS ip_analytics_daily ( + id TEXT PRIMARY KEY, + date_timestamp TEXT NOT NULL, + total_unique_ips INTEGER DEFAULT 0, + total_requests INTEGER DEFAULT 0, + avg_risk_score REAL DEFAULT 0, + high_risk_requests INTEGER DEFAULT 0, + blocks_applied INTEGER DEFAULT 0, + violations_detected INTEGER DEFAULT 0, + top_risk_ips TEXT, + top_countries TEXT, + created_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_ip_monitoring_events_ip ON ip_monitoring_events(ip_address); + CREATE INDEX IF NOT EXISTS idx_ip_monitoring_events_timestamp ON ip_monitoring_events(timestamp); + CREATE INDEX IF NOT EXISTS idx_ip_monitoring_events_type ON ip_monitoring_events(event_type); + + CREATE INDEX IF NOT EXISTS idx_ip_analytics_hourly_ip ON ip_analytics_hourly(ip_address); + CREATE INDEX IF NOT EXISTS idx_ip_analytics_hourly_timestamp ON ip_analytics_hourly(hour_timestamp); + + CREATE INDEX IF NOT EXISTS idx_ip_analytics_daily_date ON ip_analytics_daily(date_timestamp); + `); + + logger.info('IP monitoring database tables initialized'); + } catch (error) { + logger.error('Failed to initialize monitoring tables', { + error: error.message + }); + } + } + + /** + * Start monitoring service + */ + async start() { + if (this.isRunning) { + logger.warn('IP monitoring service is already running'); + return; + } + + this.isRunning = true; + logger.info('Starting IP monitoring service', { + interval: this.config.monitoring.interval + }); + + // Start periodic monitoring + this.monitoringTimer = setInterval(async () => { + await this.performMonitoringCycle(); + }, this.config.monitoring.interval); + + // Perform initial monitoring cycle + await this.performMonitoringCycle(); + + logger.info('IP monitoring service started successfully'); + } + + /** + * Stop monitoring service + */ + async stop() { + this.isRunning = false; + + if (this.monitoringTimer) { + clearInterval(this.monitoringTimer); + this.monitoringTimer = null; + } + + logger.info('IP monitoring service stopped'); + } + + /** + * Perform monitoring cycle + */ + async performMonitoringCycle() { + try { + const cycleStart = Date.now(); + + // Collect monitoring metrics + const metrics = await this.collectMetrics(); + + // Analyze for alerts + await this.analyzeAndAlert(metrics); + + // Update analytics + await this.updateAnalytics(metrics); + + // Clean up old data + await this.cleanupOldData(); + + const cycleDuration = Date.now() - cycleStart; + + logger.debug('IP monitoring cycle completed', { + duration: cycleDuration, + metrics: { + totalEvents: metrics.totalEvents, + highRiskIPs: metrics.highRiskIPs, + blockedIPs: metrics.blockedIPs + } + }); + + } catch (error) { + logger.error('IP monitoring cycle failed', { + error: error.message + }); + } + } + + /** + * Collect monitoring metrics + * @returns {object} Monitoring metrics + */ + async collectMetrics() { + const now = new Date(); + const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000); + const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + // Recent events + const recentEvents = this.database.db.prepare(` + SELECT * FROM ip_monitoring_events + WHERE timestamp > ? + ORDER BY timestamp DESC + LIMIT 1000 + `).all(oneHourAgo.toISOString()); + + // High risk IPs + const highRiskIPs = this.database.db.prepare(` + SELECT DISTINCT ip_address, risk_score, risk_level + FROM ip_monitoring_events + WHERE timestamp > ? AND risk_score >= 80 + ORDER BY risk_score DESC + LIMIT 50 + `).all(oneHourAgo.toISOString()); + + // Blocked IPs + const blockedIPs = this.database.db.prepare(` + SELECT DISTINCT ip_address, COUNT(*) as block_count + FROM ip_monitoring_events + WHERE timestamp > ? AND event_type = 'block' + GROUP BY ip_address + ORDER BY block_count DESC + LIMIT 50 + `).all(oneDayAgo.toISOString()); + + // Risk distribution + const riskDistribution = this.database.db.prepare(` + SELECT risk_level, COUNT(*) as count + FROM ip_monitoring_events + WHERE timestamp > ? + GROUP BY risk_level + `).all(oneHourAgo.toISOString()); + + // Top countries + const topCountries = this.database.db.prepare(` + SELECT json_extract(metadata_json, '$.country') as country, COUNT(*) as count + FROM ip_monitoring_events + WHERE timestamp > ? AND json_extract(metadata_json, '$.country') IS NOT NULL + GROUP BY json_extract(metadata_json, '$.country') + ORDER BY count DESC + LIMIT 10 + `).all(oneHourAgo.toISOString()); + + return { + timestamp: now.toISOString(), + totalEvents: recentEvents.length, + highRiskIPs: highRiskIPs.length, + blockedIPs: blockedIPs.length, + riskDistribution: riskDistribution.reduce((acc, row) => { + acc[row.risk_level] = row.count; + return acc; + }, {}), + topCountries: topCountries, + recentEvents: recentEvents.slice(0, 100), + topRiskIPs: highRiskIPs.slice(0, 10), + topBlockedIPs: blockedIPs.slice(0, 10) + }; + } + + /** + * Analyze metrics and send alerts + * @param {object} metrics + */ + async analyzeAndAlert(metrics) { + if (!this.config.alerting.enabled) return; + + const alerts = []; + + // Check high risk IP threshold + if (metrics.highRiskIPs >= this.config.alerting.thresholds.highRiskIPs) { + alerts.push({ + type: 'HIGH_RISK_IPS', + severity: 'warning', + message: `High number of high-risk IPs detected: ${metrics.highRiskIPs}`, + data: { + count: metrics.highRiskIPs, + threshold: this.config.alerting.thresholds.highRiskIPs, + topIPs: metrics.topRiskIPs + } + }); + } + + // Check blocked IP threshold + if (metrics.blockedIPs >= this.config.alerting.thresholds.blockedIPs) { + alerts.push({ + type: 'BLOCKED_IPS', + severity: 'warning', + message: `High number of blocked IPs: ${metrics.blockedIPs}`, + data: { + count: metrics.blockedIPs, + threshold: this.config.alerting.thresholds.blockedIPs, + topIPs: metrics.topBlockedIPs + } + }); + } + + // Check for unusual patterns + const unusualPatterns = this.detectUnusualPatterns(metrics); + if (unusualPatterns.length > 0) { + alerts.push({ + type: 'UNUSUAL_ACTIVITY', + severity: 'info', + message: `Unusual activity patterns detected`, + data: { + patterns: unusualPatterns + } + }); + } + + // Send alerts + for (const alert of alerts) { + await this.sendAlert(alert); + } + } + + /** + * Detect unusual patterns in metrics + * @param {object} metrics + * @returns {array} Unusual patterns + */ + detectUnusualPatterns(metrics) { + const patterns = []; + + // Check for spike in high-risk IPs + const previousHour = this.getPreviousHourMetrics(); + if (previousHour && previousHour.highRiskIPs > 0) { + const increase = (metrics.highRiskIPs - previousHour.highRiskIPs) / previousHour.highRiskIPs; + if (increase > 2.0) { // 200% increase + patterns.push({ + type: 'SPIKE_IN_HIGH_RISK', + description: `High-risk IPs increased by ${(increase * 100).toFixed(1)}%`, + current: metrics.highRiskIPs, + previous: previousHour.highRiskIPs + }); + } + } + + // Check for concentration from specific countries + const totalIPs = Object.values(metrics.riskDistribution).reduce((a, b) => a + b, 0); + for (const country of metrics.topCountries) { + const percentage = (country.count / totalIPs) * 100; + if (percentage > 50) { // More than 50% from one country + patterns.push({ + type: 'GEOGRAPHIC_CONCENTRATION', + description: `${(percentage).toFixed(1)}% of traffic from ${country.country}`, + country: country.country, + percentage + }); + } + } + + return patterns; + } + + /** + * Get previous hour metrics for comparison + * @returns {object|null} Previous metrics + */ + getPreviousHourMetrics() { + // This would be implemented to store historical metrics + // For now, return null + return null; + } + + /** + * Send alert + * @param {object} alert + */ + async sendAlert(alert) { + try { + // Check cooldown + const cooldownKey = `${alert.type}_${alert.data.count || 0}`; + const lastAlert = this.alertCooldowns.get(cooldownKey); + + if (lastAlert && (Date.now() - lastAlert) < this.config.alerting.cooldown) { + return; // Still in cooldown + } + + // Update cooldown + this.alertCooldowns.set(cooldownKey, Date.now()); + + // Send email alert + if (this.config.alerting.email) { + await this.sendEmailAlert(alert); + } + + // Log alert + logger.warn('IP intelligence alert triggered', { + type: alert.type, + severity: alert.severity, + message: alert.message, + data: alert.data + }); + + } catch (error) { + logger.error('Failed to send IP intelligence alert', { + error: error.message, + alertType: alert.type + }); + } + } + + /** + * Send email alert + * @param {object} alert + */ + async sendEmailAlert(alert) { + const emailData = { + to: this.config.alerting.email, + subject: `๐Ÿšจ IP Intelligence Alert - ${alert.type}`, + template: 'ip_intelligence_alert', + data: { + alert, + timestamp: new Date().toISOString(), + severity: alert.severity.toUpperCase() + } + }; + + await sendEmail(emailData); + } + + /** + * Update analytics + * @param {object} metrics + */ + async updateAnalytics(metrics) { + if (!this.config.analytics.enabled) return; + + try { + // Update hourly analytics + await this.updateHourlyAnalytics(metrics); + + // Update daily analytics + await this.updateDailyAnalytics(metrics); + + // Cache analytics + this.analyticsCache.set('current', metrics); + this.lastAnalyticsUpdate = Date.now(); + + } catch (error) { + logger.error('Failed to update analytics', { + error: error.message + }); + } + } + + /** + * Update hourly analytics + * @param {object} metrics + */ + async updateHourlyAnalytics(metrics) { + const now = new Date(); + const hourKey = now.toISOString().slice(0, 13); // YYYY-MM-DDTHH + + // Update existing or create new hourly record + const existing = this.database.db.prepare(` + SELECT * FROM ip_analytics_hourly WHERE hour_timestamp = ? + `).get(hourKey); + + if (existing) { + this.database.db.prepare(` + UPDATE ip_analytics_hourly + SET total_requests = total_requests + ?, + avg_risk_score = ?, + max_risk_score = MAX(max_risk_score, ?), + violations = violations + ?, + blocks = blocks + ? + WHERE id = ? + `).run( + metrics.totalEvents, + (existing.avg_risk_score * existing.total_requests + this.calculateAverageRiskScore(metrics)) / (existing.total_requests + metrics.totalEvents), + this.calculateMaxRiskScore(metrics), + this.calculateViolations(metrics), + this.calculateBlocks(metrics), + existing.id + ); + } else { + const id = `hourly_${hourKey}_${Math.random().toString(36).substr(2, 9)}`; + this.database.db.prepare(` + INSERT INTO ip_analytics_hourly ( + id, hour_timestamp, total_requests, avg_risk_score, max_risk_score, + violations, blocks, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run( + id, + hourKey, + metrics.totalEvents, + this.calculateAverageRiskScore(metrics), + this.calculateMaxRiskScore(metrics), + this.calculateViolations(metrics), + this.calculateBlocks(metrics), + now.toISOString() + ); + } + } + + /** + * Update daily analytics + * @param {object} metrics + */ + async updateDailyAnalytics(metrics) { + const now = new Date(); + const dateKey = now.toISOString().slice(0, 10); // YYYY-MM-DD + + // Update existing or create new daily record + const existing = this.database.db.prepare(` + SELECT * FROM ip_analytics_daily WHERE date_timestamp = ? + `).get(dateKey); + + if (existing) { + this.database.db.prepare(` + UPDATE ip_analytics_daily + SET total_requests = total_requests + ?, + avg_risk_score = ?, + high_risk_requests = high_risk_requests + ?, + blocks_applied = blocks_applied + ?, + violations_detected = violations_detected + ? + WHERE id = ? + `).run( + metrics.totalEvents, + (existing.avg_risk_score * existing.total_requests + this.calculateAverageRiskScore(metrics)) / (existing.total_requests + metrics.totalEvents), + metrics.highRiskIPs, + this.calculateBlocks(metrics), + this.calculateViolations(metrics), + existing.id + ); + } else { + const id = `daily_${dateKey}_${Math.random().toString(36).substr(2, 9)}`; + this.database.db.prepare(` + INSERT INTO ip_analytics_daily ( + id, date_timestamp, total_unique_ips, total_requests, avg_risk_score, + high_risk_requests, blocks_applied, violations_detected, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + id, + dateKey, + this.calculateUniqueIPs(metrics), + metrics.totalEvents, + this.calculateAverageRiskScore(metrics), + metrics.highRiskIPs, + this.calculateBlocks(metrics), + this.calculateViolations(metrics), + now.toISOString() + ); + } + } + + /** + * Calculate average risk score from metrics + * @param {object} metrics + * @returns {number} Average risk score + */ + calculateAverageRiskScore(metrics) { + if (!metrics.recentEvents || metrics.recentEvents.length === 0) return 0; + + const totalScore = metrics.recentEvents.reduce((sum, event) => sum + (event.risk_score || 0), 0); + return totalScore / metrics.recentEvents.length; + } + + /** + * Calculate max risk score from metrics + * @param {object} metrics + * @returns {number} Max risk score + */ + calculateMaxRiskScore(metrics) { + if (!metrics.recentEvents || metrics.recentEvents.length === 0) return 0; + + return Math.max(...metrics.recentEvents.map(event => event.risk_score || 0)); + } + + /** + * Calculate violations from metrics + * @param {object} metrics + * @returns {number} Violation count + */ + calculateViolations(metrics) { + if (!metrics.recentEvents) return 0; + + return metrics.recentEvents.filter(event => event.event_type === 'violation').length; + } + + /** + * Calculate blocks from metrics + * @param {object} metrics + * @returns {number} Block count + */ + calculateBlocks(metrics) { + if (!metrics.recentEvents) return 0; + + return metrics.recentEvents.filter(event => event.event_type === 'block').length; + } + + /** + * Calculate unique IPs from metrics + * @param {object} metrics + * @returns {number} Unique IP count + */ + calculateUniqueIPs(metrics) { + if (!metrics.recentEvents) return 0; + + const uniqueIPs = new Set(metrics.recentEvents.map(event => event.ip_address)); + return uniqueIPs.size; + } + + /** + * Clean up old monitoring data + */ + async cleanupOldData() { + try { + const cutoffDate = new Date(Date.now() - (this.config.monitoring.retentionDays * 24 * 60 * 60 * 1000)); + + // Clean up old events + const deletedEvents = this.database.db.prepare(` + DELETE FROM ip_monitoring_events WHERE timestamp < ? + `).run(cutoffDate.toISOString()); + + // Clean up old hourly analytics + const deletedHourly = this.database.db.prepare(` + DELETE FROM ip_analytics_hourly WHERE hour_timestamp < ? + `).run(cutoffDate.toISOString()); + + if (deletedEvents.changes > 0 || deletedHourly.changes > 0) { + logger.info('Cleaned up old monitoring data', { + deletedEvents: deletedEvents.changes, + deletedHourly: deletedHourly.changes, + cutoffDate: cutoffDate.toISOString() + }); + } + + } catch (error) { + logger.error('Failed to cleanup old monitoring data', { + error: error.message + }); + } + } + + /** + * Record IP monitoring event + * @param {string} ipAddress + * @param {string} eventType + * @param {object} data + */ + async recordEvent(ipAddress, eventType, data = {}) { + try { + const eventId = `event_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const now = new Date().toISOString(); + + this.database.db.prepare(` + INSERT INTO ip_monitoring_events ( + id, ip_address, event_type, risk_score, risk_level, metadata_json, timestamp, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run( + eventId, + ipAddress, + eventType, + data.riskScore || null, + data.riskLevel || null, + JSON.stringify(data), + now, + now + ); + + } catch (error) { + logger.error('Failed to record IP monitoring event', { + ipAddress, + eventType, + error: error.message + }); + } + } + + /** + * Get analytics data + * @param {object} options + * @returns {object} Analytics data + */ + async getAnalytics(options = {}) { + const { + period = '24h', + includeDetails = false, + topN = 10 + } = options; + + try { + // Check cache first + const cacheKey = `analytics_${period}_${includeDetails}`; + const cached = this.analyticsCache.get(cacheKey); + + if (cached && (Date.now() - this.lastAnalyticsUpdate) < 5 * 60 * 1000) { // 5 minutes cache + return cached; + } + + const analytics = await this.generateAnalytics(period, includeDetails, topN); + + // Cache result + this.analyticsCache.set(cacheKey, analytics); + + return analytics; + + } catch (error) { + logger.error('Failed to get IP analytics', { + error: error.message, + period + }); + + return { + error: error.message, + timestamp: new Date().toISOString() + }; + } + } + + /** + * Generate analytics data + * @param {string} period + * @param {boolean} includeDetails + * @param {number} topN + * @returns {object} Analytics data + */ + async generateAnalytics(period, includeDetails, topN) { + const now = new Date(); + let startDate; + + switch (period) { + case '1h': + startDate = new Date(now.getTime() - 60 * 60 * 1000); + break; + case '24h': + startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); + break; + case '7d': + startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + break; + case '30d': + startDate = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + break; + default: + startDate = new Date(now.getTime() - 24 * 60 * 60 * 1000); + } + + // Get summary data + const summary = this.database.db.prepare(` + SELECT + COUNT(*) as total_events, + COUNT(DISTINCT ip_address) as unique_ips, + AVG(risk_score) as avg_risk_score, + MAX(risk_score) as max_risk_score, + COUNT(CASE WHEN risk_score >= 80 THEN 1 END) as high_risk_events, + COUNT(CASE WHEN event_type = 'block' THEN 1 END) as blocks, + COUNT(CASE WHEN event_type = 'violation' THEN 1 END) as violations + FROM ip_monitoring_events + WHERE timestamp > ? + `).get(startDate.toISOString()); + + // Get risk distribution + const riskDistribution = this.database.db.prepare(` + SELECT risk_level, COUNT(*) as count + FROM ip_monitoring_events + WHERE timestamp > ? + GROUP BY risk_level + ORDER BY count DESC + `).all(startDate.toISOString()); + + // Get top risk IPs + const topRiskIPs = this.database.db.prepare(` + SELECT + ip_address, + AVG(risk_score) as avg_risk_score, + MAX(risk_score) as max_risk_score, + COUNT(*) as event_count, + GROUP_CONCAT(DISTINCT event_type) as event_types + FROM ip_monitoring_events + WHERE timestamp > ? + GROUP BY ip_address + ORDER BY avg_risk_score DESC + LIMIT ? + `).all(startDate.toISOString(), topN); + + // Get event type distribution + const eventTypeDistribution = this.database.db.prepare(` + SELECT event_type, COUNT(*) as count + FROM ip_monitoring_events + WHERE timestamp > ? + GROUP BY event_type + ORDER BY count DESC + `).all(startDate.toISOString()); + + // Get hourly trends + const hourlyTrends = this.database.db.prepare(` + SELECT + strftime('%H', timestamp) as hour, + COUNT(*) as events, + AVG(risk_score) as avg_risk_score + FROM ip_monitoring_events + WHERE timestamp > ? + GROUP BY strftime('%H', timestamp) + ORDER BY hour + `).all(startDate.toISOString()); + + return { + period, + timestamp: now.toISOString(), + summary: { + totalEvents: summary.total_events || 0, + uniqueIPs: summary.unique_ips || 0, + averageRiskScore: Math.round(summary.avg_risk_score || 0), + maxRiskScore: summary.max_risk_score || 0, + highRiskEvents: summary.high_risk_events || 0, + blocks: summary.blocks || 0, + violations: summary.violations || 0 + }, + riskDistribution, + topRiskIPs: topRiskIPs.map(ip => ({ + ...ip, + eventTypes: ip.event_types.split(',') + })), + eventTypeDistribution, + hourlyTrends, + metadata: { + includeDetails, + topN, + generatedAt: now.toISOString() + } + }; + } + + /** + * Get monitoring status + * @returns {object} Monitoring status + */ + getMonitoringStatus() { + return { + isRunning: this.isRunning, + config: this.config, + lastAnalyticsUpdate: this.lastAnalyticsUpdate, + alertCooldowns: this.alertCooldowns.size, + cacheSize: this.analyticsCache.size + }; + } +} + +module.exports = { IPMonitoringService };