← back to Handbag Auth Nextjs

src/lib/security-logger.ts

277 lines

/**
 * Security Logger Module
 * Implements secure logging for security events and anomaly detection
 * OWASP: Logging and Monitoring (https://owasp.org/www-project-top-ten/2017/A10_2017-Insufficient_Logging%26Monitoring)
 */

import fs from 'fs'
import path from 'path'

export enum SecurityEventType {
  AUTH_FAILURE = 'AUTH_FAILURE',
  AUTH_SUCCESS = 'AUTH_SUCCESS',
  RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
  INVALID_INPUT = 'INVALID_INPUT',
  SQL_INJECTION_ATTEMPT = 'SQL_INJECTION_ATTEMPT',
  XSS_ATTEMPT = 'XSS_ATTEMPT',
  UNAUTHORIZED_ACCESS = 'UNAUTHORIZED_ACCESS',
  ADMIN_ACTION = 'ADMIN_ACTION',
  DATA_BREACH_ATTEMPT = 'DATA_BREACH_ATTEMPT',
  SUSPICIOUS_ACTIVITY = 'SUSPICIOUS_ACTIVITY',
}

export interface SecurityEvent {
  timestamp: string
  type: SecurityEventType
  ip: string
  userId?: string
  endpoint: string
  method: string
  details: Record<string, any>
  severity: 'low' | 'medium' | 'high' | 'critical'
}

class SecurityLogger {
  private logFile: string
  private alertThresholds = {
    AUTH_FAILURE: 5, // Alert after 5 failed auth attempts
    RATE_LIMIT_EXCEEDED: 10, // Alert after 10 rate limit hits
    INVALID_INPUT: 20, // Alert after 20 invalid inputs
  }
  private eventCounts = new Map<string, Map<SecurityEventType, number>>()

  constructor() {
    const logDir = path.join(process.cwd(), 'logs')
    if (!fs.existsSync(logDir)) {
      fs.mkdirSync(logDir, { recursive: true })
    }
    this.logFile = path.join(logDir, `security-${new Date().toISOString().split('T')[0]}.log`)
  }

  /**
   * Log a security event
   */
  log(event: Omit<SecurityEvent, 'timestamp'>): void {
    const fullEvent: SecurityEvent = {
      ...event,
      timestamp: new Date().toISOString(),
    }

    // Write to file (append mode)
    const logLine = JSON.stringify(fullEvent) + '\n'
    fs.appendFileSync(this.logFile, logLine)

    // Track event counts for anomaly detection
    this.trackEventCount(event.ip, event.type)

    // Check for anomalies
    this.checkForAnomalies(fullEvent)

    // Log to console in development
    if (process.env.NODE_ENV === 'development') {
      console.log(`[SECURITY] ${event.type}:`, event)
    }
  }

  /**
   * Track event counts per IP
   */
  private trackEventCount(ip: string, type: SecurityEventType): void {
    if (!this.eventCounts.has(ip)) {
      this.eventCounts.set(ip, new Map())
    }

    const ipEvents = this.eventCounts.get(ip)!
    const currentCount = ipEvents.get(type) || 0
    ipEvents.set(type, currentCount + 1)

    // Clean up old entries periodically
    if (Math.random() < 0.01) {
      this.cleanupOldEntries()
    }
  }

  /**
   * Check for security anomalies
   */
  private checkForAnomalies(event: SecurityEvent): void {
    const ipEvents = this.eventCounts.get(event.ip)

    if (!ipEvents) return

    // Check if IP has exceeded thresholds
    for (const [eventType, threshold] of Object.entries(this.alertThresholds)) {
      const count = ipEvents.get(eventType as SecurityEventType) || 0
      if (count >= threshold) {
        this.raiseAlert({
          type: 'THRESHOLD_EXCEEDED',
          ip: event.ip,
          eventType,
          count,
          threshold,
        })
      }
    }

    // Check for suspicious patterns
    this.detectSuspiciousPatterns(event, ipEvents)
  }

  /**
   * Detect suspicious activity patterns
   */
  private detectSuspiciousPatterns(
    event: SecurityEvent,
    ipEvents: Map<SecurityEventType, number>
  ): void {
    const totalEvents = Array.from(ipEvents.values()).reduce((sum, count) => sum + count, 0)

    // Pattern 1: Multiple different attack types from same IP
    const attackTypes = [
      SecurityEventType.SQL_INJECTION_ATTEMPT,
      SecurityEventType.XSS_ATTEMPT,
      SecurityEventType.DATA_BREACH_ATTEMPT,
    ]
    const attackCount = attackTypes.reduce(
      (sum, type) => sum + (ipEvents.get(type) || 0),
      0
    )

    if (attackCount >= 3) {
      this.raiseAlert({
        type: 'MULTI_VECTOR_ATTACK',
        ip: event.ip,
        attackCount,
        details: 'Multiple attack vectors detected from same IP',
      })
    }

    // Pattern 2: Rapid-fire requests
    if (totalEvents > 100) {
      this.raiseAlert({
        type: 'EXCESSIVE_REQUESTS',
        ip: event.ip,
        totalEvents,
        details: 'Abnormally high request volume',
      })
    }
  }

  /**
   * Raise security alert
   */
  private raiseAlert(alert: Record<string, any>): void {
    const alertEvent: SecurityEvent = {
      timestamp: new Date().toISOString(),
      type: SecurityEventType.SUSPICIOUS_ACTIVITY,
      ip: alert.ip || 'system',
      endpoint: 'security-monitor',
      method: 'ALERT',
      details: alert,
      severity: 'high',
    }

    // Write to alert log
    const alertLogFile = path.join(
      process.cwd(),
      'logs',
      `security-alerts-${new Date().toISOString().split('T')[0]}.log`
    )
    fs.appendFileSync(alertLogFile, JSON.stringify(alertEvent) + '\n')

    // In production, you would send this to a monitoring service
    console.error('[SECURITY ALERT]:', alert)

    // TODO: Integrate with Slack webhook or monitoring service
    if (process.env.SLACK_WEBHOOK_URL) {
      // Send alert to Slack
    }
  }

  /**
   * Clean up old event counts (prevent memory leak)
   */
  private cleanupOldEntries(): void {
    // Remove IPs with low event counts (likely legitimate users)
    for (const [ip, events] of this.eventCounts.entries()) {
      const total = Array.from(events.values()).reduce((sum, count) => sum + count, 0)
      if (total < 3) {
        this.eventCounts.delete(ip)
      }
    }

    // Reset counts for IPs that haven't had recent activity
    // This is simplified - in production, you'd track timestamps
    if (this.eventCounts.size > 1000) {
      this.eventCounts.clear()
    }
  }

  /**
   * Get security stats for monitoring
   */
  getStats(): Record<string, any> {
    const stats: Record<string, any> = {
      totalIPs: this.eventCounts.size,
      topOffenders: [],
      eventTypeCounts: {},
    }

    // Calculate top offenders
    const ipScores = new Map<string, number>()
    for (const [ip, events] of this.eventCounts.entries()) {
      const score = Array.from(events.values()).reduce((sum, count) => sum + count, 0)
      ipScores.set(ip, score)
    }

    // Sort and get top 10
    stats.topOffenders = Array.from(ipScores.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 10)
      .map(([ip, score]) => ({ ip, score }))

    // Count events by type
    for (const events of this.eventCounts.values()) {
      for (const [type, count] of events.entries()) {
        stats.eventTypeCounts[type] = (stats.eventTypeCounts[type] || 0) + count
      }
    }

    return stats
  }
}

// Export singleton instance
export const securityLogger = new SecurityLogger()

/**
 * Helper function to detect SQL injection attempts
 */
export function detectSQLInjection(input: string): boolean {
  const sqlPatterns = [
    /(\b(SELECT|INSERT|UPDATE|DELETE|DROP|UNION|ALTER|CREATE)\b)/gi,
    /(\b(OR|AND)\b\s*\d+\s*=\s*\d+)/gi,
    /(--|\||;|\/\*|\*\/|@@|@|xp_|exec|execute|sp_|0x)/gi,
    /(\b(CAST|CONVERT|CHAR|NCHAR|VARCHAR|NVARCHAR)\b\s*\()/gi,
  ]

  return sqlPatterns.some(pattern => pattern.test(input))
}

/**
 * Helper function to detect XSS attempts
 */
export function detectXSS(input: string): boolean {
  const xssPatterns = [
    /<script[^>]*>.*?<\/script>/gi,
    /<iframe[^>]*>.*?<\/iframe>/gi,
    /javascript:/gi,
    /on\w+\s*=/gi, // Event handlers like onclick=
    /<img[^>]*onerror=/gi,
    /<svg[^>]*onload=/gi,
    /document\.(cookie|write|location)/gi,
    /window\.(location|open)/gi,
  ]

  return xssPatterns.some(pattern => pattern.test(input))
}