Newer
Older
express-blog / src / utils / securityForensics.js
@Jason Jason on 26 Jul 5 KB modified: package.json
// // src/utils/securityForensics.js
const crypto = require("crypto");
const fs = require("fs").promises;
const path = require("path");
const HttpError = require("../utils/HttpError");

const { winstonLogger } = require("../utils/logging");

// Threat detection patterns
const THREAT_PATTERNS = {
  // Common phishing/spam indicators
  suspiciousKeywords: [
    "verify account",
    "urgent action",
    "suspended account",
    "click here",
    "limited time",
    "act now",
    "confirm identity",
    "update payment",
    "security alert",
    "unusual activity",
  ],

  // Suspicious domains (add known bad actor domains)
  suspiciousDomains: [
    "tempmail.org",
    "10minutemail.com",
    "guerrillamail.com",
    "throwaway.email",
    "temp-mail.org",
  ],

  // Suspicious patterns
  suspiciousPatterns: [
    /https?:\/\/[^\s]+/gi, // URLs in messages
    /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, // Email addresses
    /\b(?:\d{4}[-\s]?){3}\d{4}\b/g, // Credit card patterns
    /\b\d{3}-\d{2}-\d{4}\b/g, // SSN patterns
  ],
};

// Enhanced forensic data collection (focused on threat detection)
function captureSecurityData(req, additionalData = {}) {
  const timestamp = new Date().toISOString();
  const requestId = crypto.randomUUID();

  // Connection and network data
  const connectionData = {
    ip: req.ip,
    ips: req.ips || [],
    remoteAddress: req.socket?.remoteAddress,
    protocol: req.protocol,
    secure: req.secure,
    hostname: req.hostname,
    originalUrl: req.originalUrl,
    encrypted: req.socket?.encrypted || false,
  };

  // Security-relevant headers
  const securityHeaders = {
    userAgent: req.headers["user-agent"],
    acceptLanguage: req.headers["accept-language"],
    referer: req.headers["referer"],
    origin: req.headers["origin"],
    xForwardedFor: req.headers["x-forwarded-for"],
    xRealIp: req.headers["x-real-ip"],
    host: req.headers["host"],
    // Check for proxy/VPN indicators
    via: req.headers["via"],
    xForwardedProto: req.headers["x-forwarded-proto"],
    cfConnectingIp: req.headers["cf-connecting-ip"], // Cloudflare
    cfIpCountry: req.headers["cf-ipcountry"],
    cfRay: req.headers["cf-ray"],
  };

  // Request timing and patterns
  const requestData = {
    method: req.method,
    url: req.url,
    path: req.path,
    query: req.query,
    timestamp: timestamp,
    requestStart: req._startTime || Date.now(),
    processingTime: Date.now() - (req._startTime || Date.now()),
  };

  // TLS/Security info
  let tlsData = null;
  if (req.socket && req.socket.encrypted) {
    try {
      const cipher = req.socket.getCipher ? req.socket.getCipher() : null;
      tlsData = {
        cipher: cipher,
        tlsVersion: req.socket.getProtocol ? req.socket.getProtocol() : null,
        authorized: req.socket.authorized,
      };
    } catch (err) {
      tlsData = { error: "TLS data unavailable" };
    }
  }

  return {
    requestId,
    timestamp,
    connection: connectionData,
    security: securityHeaders,
    request: requestData,
    tls: tlsData,
    additional: additionalData,
  };
}

// Threat analysis function
function analyzeThreatLevel(formData, securityData) {
  let threatScore = 0;
  const indicators = [];

  // Check message content for suspicious patterns
  const message = formData.message?.toLowerCase() || "";
  const name = formData.name?.toLowerCase() || "";

  // Suspicious keywords in message
  THREAT_PATTERNS.suspiciousKeywords.forEach((keyword) => {
    if (message.includes(keyword.toLowerCase())) {
      threatScore += 3;
      indicators.push(`suspicious_keyword: ${keyword}`);
    }
  });

  const email = formData.email?.toLowerCase() || "";
  // Check for suspicious email domains
  const emailDomain = email.split("@")[1];
  if (emailDomain && THREAT_PATTERNS.suspiciousDomains.includes(emailDomain)) {
    threatScore += 5;
    indicators.push(`suspicious_email_domain: ${emailDomain}`);
  }

  // Check for suspicious patterns in content
  THREAT_PATTERNS.suspiciousPatterns.forEach((pattern, index) => {
    if (pattern.test(message)) {
      threatScore += 2;
      indicators.push(`suspicious_pattern_${index}`);
    }
  });

  // Check for rapid form submission (potential automation)
  if (securityData.additional.clientData?.formTime < 5000) {
    // Less than 5 seconds
    threatScore += 2;
    indicators.push("rapid_submission");
  }

  // Check for suspicious user agent
  const userAgent = securityData.security.userAgent || "";
  if (!userAgent || userAgent.includes("bot") || userAgent.includes("crawl")) {
    threatScore += 3;
    indicators.push("suspicious_user_agent");
  }

  // Check for missing referer (direct access)
  if (!securityData.security.referer) {
    threatScore += 1;
    indicators.push("no_referer");
  }

  // Determine threat level
  let threatLevel = "low";
  if (threatScore >= 8) threatLevel = "high";
  else if (threatScore >= 4) threatLevel = "medium";

  return {
    score: threatScore,
    level: threatLevel,
    indicators: indicators,
    requiresReview: threatScore >= 4,
  };
}

async function logSecurityEvent(data, eventType = "contact_submission") {
  try {
    const date = new Date().toISOString().split("T")[0];
    const logEntry = {
      eventType,
      ...data,
      loggedAt: new Date().toISOString(),
    };

    // Log security event at custom 'security' level
    winstonLogger.security(logEntry);

    // Separate high-threat log file
    if (data.threatAnalysis?.level === "high") {
      const logDir = path.join(__dirname, "..", "..", "logs", "security");
      await fs.mkdir(logDir, { recursive: true });

      const alertFile = path.join(logDir, `high_threat_${date}.log`);
      await fs.appendFile(alertFile, message + "\n");
    }
  } catch (err) {
    // Fail silently or log to error log, depending on requirements
    winstonLogger.error(`Failed to log security event: ${err.message}`);
  }
}

module.exports = {
  captureSecurityData,
  analyzeThreatLevel,
  logSecurityEvent,
};