← back to Watches

security/intrusion-detection.js

420 lines

/**
 * INTRUSION DETECTION SYSTEM (IDS)
 * Real-time threat detection and automated response
 *
 * Features:
 * - Behavioral analysis
 * - Pattern recognition
 * - Automated blocking
 * - Alert generation
 * - Forensic logging
 */

import { securityLogger } from '../middleware/security.js';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

class IntrusionDetectionSystem {
  constructor() {
    this.suspiciousIPs = new Map();
    this.blockedIPs = new Set();
    this.attackPatterns = this.initializeAttackPatterns();
    this.thresholds = {
      maxFailedAuth: 5,
      maxRequestsPerMinute: 60,
      maxValidationErrors: 10,
      suspiciousScore: 50
    };
    this.alertCallbacks = [];
  }

  /**
   * Initialize known attack patterns
   */
  initializeAttackPatterns() {
    return {
      sqlInjection: [
        /(\bunion\b.*\bselect\b)/i,
        /(\bdrop\b.*\btable\b)/i,
        /(\bselect\b.*\bfrom\b.*\bwhere\b)/i,
        /(--|;|\/\*|\*\/)/,
        /(\bexec\b|\bexecute\b)/i,
        /(\binsert\b.*\binto\b)/i,
        /(\bdelete\b.*\bfrom\b)/i
      ],
      xss: [
        /<script\b[^>]*>/i,
        /javascript:/i,
        /on\w+\s*=/i,
        /<iframe\b[^>]*>/i,
        /onerror\s*=/i,
        /onload\s*=/i
      ],
      pathTraversal: [
        /\.\.\//,
        /\.\.\\\/,
        /%2e%2e%2f/i,
        /%252e%252e%252f/i
      ],
      commandInjection: [
        /[;&|`$()]/,
        /\$\{.*\}/,
        /\$\(.*\)/
      ],
      enumeration: [
        /\/admin/i,
        /\/backup/i,
        /\/config/i,
        /\.env/i,
        /\/\.git/i,
        /\/\.svn/i
      ]
    };
  }

  /**
   * Analyze incoming request for threats
   */
  analyzeRequest(req) {
    const ip = this.getClientIP(req);
    const suspiciousScore = this.calculateSuspiciousScore(req);

    // Track IP behavior
    this.trackIPBehavior(ip, req);

    // Check if already blocked
    if (this.blockedIPs.has(ip)) {
      this.logThreat('BLOCKED_IP_ATTEMPT', ip, req, 'CRITICAL');
      return { blocked: true, reason: 'IP is blocked' };
    }

    // Check suspicious score
    if (suspiciousScore >= this.thresholds.suspiciousScore) {
      this.handleSuspiciousActivity(ip, req, suspiciousScore);
      return { blocked: true, reason: 'Suspicious activity detected' };
    }

    return { blocked: false, score: suspiciousScore };
  }

  /**
   * Calculate suspicious score for request
   */
  calculateSuspiciousScore(req) {
    let score = 0;

    // Check for attack patterns in URL
    const fullUrl = req.originalUrl || req.url;
    score += this.checkAttackPatterns(fullUrl) * 20;

    // Check for attack patterns in query parameters
    if (req.query) {
      Object.values(req.query).forEach(value => {
        if (typeof value === 'string') {
          score += this.checkAttackPatterns(value) * 15;
        }
      });
    }

    // Check for attack patterns in body
    if (req.body) {
      const bodyStr = JSON.stringify(req.body);
      score += this.checkAttackPatterns(bodyStr) * 15;
    }

    // Check user agent
    const userAgent = req.get('user-agent') || '';
    if (this.isSuspiciousUserAgent(userAgent)) {
      score += 10;
    }

    // Check for missing common headers
    if (!req.get('accept') || !req.get('accept-language')) {
      score += 5;
    }

    // Check request method patterns
    if (['TRACE', 'TRACK', 'OPTIONS'].includes(req.method)) {
      score += 5;
    }

    return Math.min(score, 100);
  }

  /**
   * Check text against attack patterns
   */
  checkAttackPatterns(text) {
    let matches = 0;

    Object.values(this.attackPatterns).forEach(patterns => {
      patterns.forEach(pattern => {
        if (pattern.test(text)) {
          matches++;
        }
      });
    });

    return matches;
  }

  /**
   * Check if user agent is suspicious
   */
  isSuspiciousUserAgent(userAgent) {
    const suspiciousPatterns = [
      /bot|crawler|spider|scraper/i,
      /nikto|nmap|sqlmap|havij/i,
      /masscan|zap|burp/i,
      /python-requests|curl|wget/i
    ];

    return suspiciousPatterns.some(pattern => pattern.test(userAgent));
  }

  /**
   * Track IP behavior over time
   */
  trackIPBehavior(ip, req) {
    if (!this.suspiciousIPs.has(ip)) {
      this.suspiciousIPs.set(ip, {
        requests: [],
        failedAuth: 0,
        validationErrors: 0,
        firstSeen: Date.now(),
        score: 0
      });
    }

    const ipData = this.suspiciousIPs.get(ip);
    const now = Date.now();

    // Track request
    ipData.requests.push(now);

    // Clean old requests (older than 1 minute)
    ipData.requests = ipData.requests.filter(time => now - time < 60000);

    // Check request rate
    if (ipData.requests.length > this.thresholds.maxRequestsPerMinute) {
      ipData.score += 20;
      this.logThreat('RATE_ANOMALY', ip, req, 'WARNING');
    }

    // Clean up old IP data (older than 1 hour)
    if (now - ipData.firstSeen > 3600000) {
      this.suspiciousIPs.delete(ip);
    }
  }

  /**
   * Handle suspicious activity
   */
  handleSuspiciousActivity(ip, req, score) {
    const ipData = this.suspiciousIPs.get(ip);

    if (ipData) {
      ipData.score = score;

      // Auto-block if score exceeds threshold
      if (score >= 80) {
        this.blockIP(ip, 'Automated block - high threat score', 3600000); // 1 hour
        this.logThreat('AUTO_BLOCKED', ip, req, 'CRITICAL');
        this.triggerAlert('CRITICAL', `IP ${ip} auto-blocked - threat score: ${score}`);
      } else if (score >= 50) {
        this.logThreat('SUSPICIOUS_ACTIVITY', ip, req, 'HIGH');
        this.triggerAlert('HIGH', `Suspicious activity from IP ${ip} - score: ${score}`);
      }
    }
  }

  /**
   * Block an IP address
   */
  blockIP(ip, reason, duration = 3600000) {
    this.blockedIPs.add(ip);

    securityLogger.log({
      type: 'IP_BLOCKED',
      severity: 'CRITICAL',
      ip: ip,
      reason: reason,
      duration: duration
    });

    // Auto-unblock after duration
    if (duration > 0) {
      setTimeout(() => {
        this.unblockIP(ip);
      }, duration);
    }
  }

  /**
   * Unblock an IP address
   */
  unblockIP(ip) {
    this.blockedIPs.delete(ip);

    securityLogger.log({
      type: 'IP_UNBLOCKED',
      severity: 'INFO',
      ip: ip
    });
  }

  /**
   * Record failed authentication attempt
   */
  recordFailedAuth(ip, req) {
    if (!this.suspiciousIPs.has(ip)) {
      this.trackIPBehavior(ip, req);
    }

    const ipData = this.suspiciousIPs.get(ip);
    ipData.failedAuth++;
    ipData.score += 15;

    if (ipData.failedAuth >= this.thresholds.maxFailedAuth) {
      this.blockIP(ip, 'Too many failed authentication attempts', 900000); // 15 minutes
      this.logThreat('BRUTE_FORCE_DETECTED', ip, req, 'CRITICAL');
      this.triggerAlert('CRITICAL', `Brute force attack detected from IP ${ip}`);
    }
  }

  /**
   * Record validation error
   */
  recordValidationError(ip, req) {
    if (!this.suspiciousIPs.has(ip)) {
      this.trackIPBehavior(ip, req);
    }

    const ipData = this.suspiciousIPs.get(ip);
    ipData.validationErrors++;
    ipData.score += 5;

    if (ipData.validationErrors >= this.thresholds.maxValidationErrors) {
      this.logThreat('VALIDATION_ATTACK', ip, req, 'HIGH');
    }
  }

  /**
   * Log security threat
   */
  logThreat(type, ip, req, severity) {
    securityLogger.log({
      type: type,
      severity: severity,
      ip: ip,
      path: req.path,
      method: req.method,
      userAgent: req.get('user-agent'),
      timestamp: new Date().toISOString()
    });
  }

  /**
   * Get client IP address
   */
  getClientIP(req) {
    return req.ip ||
           req.headers['x-forwarded-for']?.split(',')[0] ||
           req.connection?.remoteAddress ||
           'unknown';
  }

  /**
   * Register alert callback
   */
  onAlert(callback) {
    this.alertCallbacks.push(callback);
  }

  /**
   * Trigger alert
   */
  triggerAlert(severity, message) {
    this.alertCallbacks.forEach(callback => {
      try {
        callback({ severity, message, timestamp: new Date().toISOString() });
      } catch (error) {
        console.error('Alert callback error:', error);
      }
    });
  }

  /**
   * Get system status
   */
  getStatus() {
    const now = Date.now();
    const recentThreats = Array.from(this.suspiciousIPs.entries())
      .filter(([ip, data]) => data.score > 30)
      .map(([ip, data]) => ({
        ip,
        score: data.score,
        requests: data.requests.length,
        failedAuth: data.failedAuth,
        validationErrors: data.validationErrors
      }));

    return {
      blockedIPs: Array.from(this.blockedIPs),
      monitoredIPs: this.suspiciousIPs.size,
      recentThreats: recentThreats,
      totalBlocked: this.blockedIPs.size,
      timestamp: new Date().toISOString()
    };
  }

  /**
   * Export forensic data
   */
  async exportForensicData() {
    const data = {
      timestamp: new Date().toISOString(),
      blockedIPs: Array.from(this.blockedIPs),
      suspiciousActivity: Array.from(this.suspiciousIPs.entries()).map(([ip, data]) => ({
        ip,
        ...data,
        requests: data.requests.length
      })),
      securityLogs: securityLogger.getRecentEvents(1000)
    };

    const filename = `forensic-${Date.now()}.json`;
    const filepath = path.join(__dirname, '..', 'logs', filename);

    await fs.mkdir(path.join(__dirname, '..', 'logs'), { recursive: true });
    await fs.writeFile(filepath, JSON.stringify(data, null, 2));

    return filepath;
  }
}

// Express middleware
export const idsMiddleware = (ids) => {
  return (req, res, next) => {
    const analysis = ids.analyzeRequest(req);

    if (analysis.blocked) {
      return res.status(403).json({
        error: 'Forbidden',
        message: 'Request blocked by intrusion detection system',
        reason: analysis.reason
      });
    }

    // Attach IDS analysis to request for logging
    req.idsAnalysis = analysis;
    next();
  };
};

export default IntrusionDetectionSystem;