← back to Watches

security/security-scanner.js

742 lines

/**
 * SECURITY SCANNER
 * Automated vulnerability scanning and reporting
 *
 * Features:
 * - Dependency vulnerability scanning
 * - Configuration security audit
 * - SSL/TLS testing
 * - Header security analysis
 * - OWASP Top 10 checks
 * - Automated remediation suggestions
 */

import { execSync } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
import https from 'https';
import http from 'http';

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

class SecurityScanner {
  constructor() {
    this.scanResults = {
      timestamp: new Date().toISOString(),
      score: 0,
      vulnerabilities: [],
      warnings: [],
      recommendations: []
    };
  }

  /**
   * Run complete security scan
   */
  async runFullScan(options = {}) {
    const {
      checkDependencies = true,
      checkHeaders = true,
      checkConfig = true,
      checkSSL = false,
      targetUrl = 'http://localhost:7600'
    } = options;

    console.log('Starting comprehensive security scan...\n');

    this.scanResults.timestamp = new Date().toISOString();
    this.scanResults.vulnerabilities = [];
    this.scanResults.warnings = [];
    this.scanResults.recommendations = [];

    if (checkDependencies) {
      await this.scanDependencies();
    }

    if (checkHeaders) {
      await this.scanSecurityHeaders(targetUrl);
    }

    if (checkConfig) {
      await this.scanConfiguration();
    }

    if (checkSSL && targetUrl.startsWith('https')) {
      await this.scanSSLConfiguration(targetUrl);
    }

    // Check OWASP Top 10
    await this.checkOWASPTop10();

    // Calculate security score
    this.calculateSecurityScore();

    return this.scanResults;
  }

  /**
   * Scan npm dependencies for vulnerabilities
   */
  async scanDependencies() {
    console.log('Scanning dependencies for vulnerabilities...');

    try {
      const auditOutput = execSync('npm audit --json', {
        encoding: 'utf8',
        cwd: path.join(__dirname, '..'),
        stdio: ['pipe', 'pipe', 'ignore']
      });

      const audit = JSON.parse(auditOutput);

      if (audit.metadata) {
        const { vulnerabilities } = audit.metadata;

        Object.entries(vulnerabilities).forEach(([severity, count]) => {
          if (count > 0 && severity !== 'info') {
            const item = {
              category: 'DEPENDENCIES',
              severity: severity.toUpperCase(),
              title: `${count} ${severity} vulnerability(ies) in dependencies`,
              description: `Found ${count} ${severity} severity vulnerabilities in npm packages`,
              remediation: 'Run `npm audit fix` to automatically fix vulnerabilities',
              owaspRef: 'A06:2021 - Vulnerable and Outdated Components'
            };

            if (severity === 'critical' || severity === 'high') {
              this.scanResults.vulnerabilities.push(item);
            } else {
              this.scanResults.warnings.push(item);
            }
          }
        });

        if (vulnerabilities.total === 0) {
          console.log('✓ No dependency vulnerabilities found');
        }
      }
    } catch (error) {
      console.error('Error scanning dependencies:', error.message);
      this.scanResults.warnings.push({
        category: 'DEPENDENCIES',
        severity: 'WARNING',
        title: 'Could not scan dependencies',
        description: error.message
      });
    }
  }

  /**
   * Scan security headers
   */
  async scanSecurityHeaders(url) {
    console.log(`Scanning security headers for ${url}...`);

    try {
      const headers = await this.fetchHeaders(url);

      // Required security headers
      const requiredHeaders = {
        'strict-transport-security': {
          name: 'HSTS',
          severity: 'HIGH',
          owaspRef: 'A05:2021 - Security Misconfiguration'
        },
        'x-frame-options': {
          name: 'X-Frame-Options',
          severity: 'MEDIUM',
          owaspRef: 'A01:2021 - Broken Access Control'
        },
        'x-content-type-options': {
          name: 'X-Content-Type-Options',
          severity: 'MEDIUM',
          owaspRef: 'A05:2021 - Security Misconfiguration'
        },
        'content-security-policy': {
          name: 'Content-Security-Policy',
          severity: 'HIGH',
          owaspRef: 'A03:2021 - Injection'
        },
        'x-xss-protection': {
          name: 'X-XSS-Protection',
          severity: 'MEDIUM',
          owaspRef: 'A03:2021 - Injection'
        },
        'referrer-policy': {
          name: 'Referrer-Policy',
          severity: 'LOW',
          owaspRef: 'A05:2021 - Security Misconfiguration'
        }
      };

      Object.entries(requiredHeaders).forEach(([header, info]) => {
        if (!headers[header]) {
          const item = {
            category: 'HEADERS',
            severity: info.severity,
            title: `Missing ${info.name} header`,
            description: `The ${info.name} security header is not set`,
            remediation: `Add ${info.name} header to HTTP responses`,
            owaspRef: info.owaspRef
          };

          if (info.severity === 'HIGH') {
            this.scanResults.vulnerabilities.push(item);
          } else {
            this.scanResults.warnings.push(item);
          }
        } else {
          console.log(`✓ ${info.name} header present:`, headers[header]);
        }
      });

      // Check for information disclosure
      if (headers['x-powered-by']) {
        this.scanResults.warnings.push({
          category: 'HEADERS',
          severity: 'LOW',
          title: 'Information disclosure via X-Powered-By header',
          description: `X-Powered-By header reveals: ${headers['x-powered-by']}`,
          remediation: 'Remove X-Powered-By header',
          owaspRef: 'A05:2021 - Security Misconfiguration'
        });
      }

      if (headers['server']) {
        this.scanResults.recommendations.push({
          category: 'HEADERS',
          title: 'Server header reveals server information',
          description: `Server header: ${headers['server']}`,
          recommendation: 'Consider removing or obscuring server header'
        });
      }
    } catch (error) {
      console.error('Error scanning headers:', error.message);
    }
  }

  /**
   * Scan configuration files for security issues
   */
  async scanConfiguration() {
    console.log('Scanning configuration files...');

    try {
      const projectRoot = path.join(__dirname, '..');

      // Check for exposed secrets
      const envExample = path.join(projectRoot, '.env.example');
      const envFile = path.join(projectRoot, '.env');

      try {
        const envContent = await fs.readFile(envFile, 'utf8');

        // Check for weak/default secrets
        if (envContent.includes('JWT_SECRET=your_jwt_secret_here')) {
          this.scanResults.vulnerabilities.push({
            category: 'CONFIGURATION',
            severity: 'CRITICAL',
            title: 'Default JWT secret detected',
            description: 'JWT_SECRET is using default value from .env.example',
            remediation: 'Generate a strong random JWT secret: openssl rand -base64 32',
            owaspRef: 'A02:2021 - Cryptographic Failures'
          });
        }

        // Check for weak admin credentials
        if (envContent.match(/ADMIN_PASSWORD=(admin|password|123456)/i)) {
          this.scanResults.vulnerabilities.push({
            category: 'CONFIGURATION',
            severity: 'CRITICAL',
            title: 'Weak admin password detected',
            description: 'Admin password is weak or uses common value',
            remediation: 'Set a strong admin password (minimum 16 characters, mixed case, numbers, symbols)',
            owaspRef: 'A07:2021 - Identification and Authentication Failures'
          });
        }

        console.log('✓ Configuration files scanned');
      } catch (error) {
        // .env file might not exist, which is OK
        console.log('Note: .env file not found (using environment variables)');
      }

      // Check package.json for security issues
      const packageJson = JSON.parse(
        await fs.readFile(path.join(projectRoot, 'package.json'), 'utf8')
      );

      if (!packageJson.scripts || !packageJson.scripts.start) {
        this.scanResults.recommendations.push({
          category: 'CONFIGURATION',
          title: 'No start script defined',
          recommendation: 'Add a start script to package.json'
        });
      }
    } catch (error) {
      console.error('Error scanning configuration:', error.message);
    }
  }

  /**
   * Scan SSL/TLS configuration
   */
  async scanSSLConfiguration(url) {
    console.log('Scanning SSL/TLS configuration...');

    try {
      const urlObj = new URL(url);

      await new Promise((resolve, reject) => {
        const req = https.get(url, (res) => {
          const cert = res.socket.getPeerCertificate();

          if (cert) {
            // Check certificate expiration
            const validTo = new Date(cert.valid_to);
            const daysUntilExpiry = Math.floor((validTo - new Date()) / (1000 * 60 * 60 * 24));

            if (daysUntilExpiry < 0) {
              this.scanResults.vulnerabilities.push({
                category: 'SSL',
                severity: 'CRITICAL',
                title: 'SSL certificate expired',
                description: `Certificate expired on ${cert.valid_to}`,
                remediation: 'Renew SSL certificate immediately',
                owaspRef: 'A02:2021 - Cryptographic Failures'
              });
            } else if (daysUntilExpiry < 30) {
              this.scanResults.warnings.push({
                category: 'SSL',
                severity: 'MEDIUM',
                title: 'SSL certificate expiring soon',
                description: `Certificate expires in ${daysUntilExpiry} days`,
                remediation: 'Renew SSL certificate'
              });
            } else {
              console.log(`✓ SSL certificate valid until ${cert.valid_to}`);
            }
          }

          resolve();
        });

        req.on('error', reject);
        req.end();
      });
    } catch (error) {
      console.error('Error scanning SSL:', error.message);
    }
  }

  /**
   * Check OWASP Top 10 compliance
   */
  async checkOWASPTop10() {
    console.log('Checking OWASP Top 10 compliance...');

    const checks = [
      {
        id: 'A01',
        name: 'Broken Access Control',
        check: () => this.checkAccessControl(),
        owaspRef: 'A01:2021'
      },
      {
        id: 'A02',
        name: 'Cryptographic Failures',
        check: () => this.checkCryptography(),
        owaspRef: 'A02:2021'
      },
      {
        id: 'A03',
        name: 'Injection',
        check: () => this.checkInjectionProtection(),
        owaspRef: 'A03:2021'
      },
      {
        id: 'A05',
        name: 'Security Misconfiguration',
        check: () => this.checkSecurityConfiguration(),
        owaspRef: 'A05:2021'
      },
      {
        id: 'A06',
        name: 'Vulnerable Components',
        check: () => this.checkVulnerableComponents(),
        owaspRef: 'A06:2021'
      },
      {
        id: 'A07',
        name: 'Authentication Failures',
        check: () => this.checkAuthenticationMechanisms(),
        owaspRef: 'A07:2021'
      },
      {
        id: 'A09',
        name: 'Security Logging Failures',
        check: () => this.checkLoggingMechanisms(),
        owaspRef: 'A09:2021'
      }
    ];

    for (const check of checks) {
      try {
        await check.check();
        console.log(`✓ ${check.id}: ${check.name}`);
      } catch (error) {
        console.error(`✗ ${check.id}: ${check.name} -`, error.message);
      }
    }
  }

  /**
   * OWASP check implementations
   */
  async checkAccessControl() {
    // Check if JWT middleware is present
    const projectRoot = path.join(__dirname, '..');
    const securityMiddleware = path.join(projectRoot, 'middleware', 'security.js');

    try {
      const content = await fs.readFile(securityMiddleware, 'utf8');

      if (!content.includes('authenticateJWT')) {
        throw new Error('JWT authentication middleware not found');
      }
    } catch (error) {
      this.scanResults.warnings.push({
        category: 'ACCESS_CONTROL',
        severity: 'MEDIUM',
        title: 'Authentication middleware check failed',
        description: error.message,
        owaspRef: 'A01:2021 - Broken Access Control'
      });
    }
  }

  async checkCryptography() {
    // Already checked in configuration scan
    return true;
  }

  async checkInjectionProtection() {
    const projectRoot = path.join(__dirname, '..');
    const securityMiddleware = path.join(projectRoot, 'middleware', 'security.js');

    try {
      const content = await fs.readFile(securityMiddleware, 'utf8');

      if (!content.includes('sanitize') && !content.includes('validator')) {
        this.scanResults.warnings.push({
          category: 'INJECTION',
          severity: 'HIGH',
          title: 'Input validation middleware may be missing',
          description: 'No evidence of input sanitization found',
          remediation: 'Implement input validation and sanitization',
          owaspRef: 'A03:2021 - Injection'
        });
      }
    } catch (error) {
      // Ignore if file doesn't exist
    }
  }

  async checkSecurityConfiguration() {
    // Already checked in header scan
    return true;
  }

  async checkVulnerableComponents() {
    // Already checked in dependency scan
    return true;
  }

  async checkAuthenticationMechanisms() {
    // Check for rate limiting on auth endpoints
    this.scanResults.recommendations.push({
      category: 'AUTHENTICATION',
      title: 'Ensure rate limiting on authentication endpoints',
      recommendation: 'Verify rate limiting is configured for /api/admin/login'
    });
  }

  async checkLoggingMechanisms() {
    const projectRoot = path.join(__dirname, '..');
    const logsDir = path.join(projectRoot, 'logs');

    try {
      await fs.access(logsDir);
      console.log('✓ Logging directory exists');
    } catch (error) {
      this.scanResults.recommendations.push({
        category: 'LOGGING',
        title: 'Create logs directory',
        recommendation: 'Create /logs directory for security event logging'
      });
    }
  }

  /**
   * Fetch HTTP headers
   */
  async fetchHeaders(url) {
    return new Promise((resolve, reject) => {
      const client = url.startsWith('https') ? https : http;

      const req = client.get(url, (res) => {
        resolve(res.headers);
      });

      req.on('error', reject);
      req.setTimeout(5000, () => {
        req.destroy();
        reject(new Error('Request timeout'));
      });
      req.end();
    });
  }

  /**
   * Calculate overall security score
   */
  calculateSecurityScore() {
    let score = 100;

    // Deduct points for vulnerabilities
    this.scanResults.vulnerabilities.forEach(vuln => {
      switch (vuln.severity) {
        case 'CRITICAL':
          score -= 20;
          break;
        case 'HIGH':
          score -= 10;
          break;
        case 'MEDIUM':
          score -= 5;
          break;
        case 'LOW':
          score -= 2;
          break;
      }
    });

    // Deduct points for warnings
    this.scanResults.warnings.forEach(warning => {
      switch (warning.severity) {
        case 'HIGH':
          score -= 5;
          break;
        case 'MEDIUM':
          score -= 3;
          break;
        case 'LOW':
          score -= 1;
          break;
      }
    });

    this.scanResults.score = Math.max(0, score);
    this.scanResults.grade = this.calculateGrade(this.scanResults.score);

    return this.scanResults.score;
  }

  /**
   * Calculate letter grade
   */
  calculateGrade(score) {
    if (score >= 90) return 'A';
    if (score >= 80) return 'B';
    if (score >= 70) return 'C';
    if (score >= 60) return 'D';
    return 'F';
  }

  /**
   * Generate security report
   */
  async generateReport(format = 'json') {
    const reportData = {
      ...this.scanResults,
      summary: {
        totalVulnerabilities: this.scanResults.vulnerabilities.length,
        criticalCount: this.scanResults.vulnerabilities.filter(v => v.severity === 'CRITICAL').length,
        highCount: this.scanResults.vulnerabilities.filter(v => v.severity === 'HIGH').length,
        mediumCount: this.scanResults.vulnerabilities.filter(v => v.severity === 'MEDIUM').length,
        lowCount: this.scanResults.vulnerabilities.filter(v => v.severity === 'LOW').length,
        warningCount: this.scanResults.warnings.length,
        recommendationCount: this.scanResults.recommendations.length
      }
    };

    if (format === 'json') {
      return JSON.stringify(reportData, null, 2);
    }

    if (format === 'markdown') {
      return this.generateMarkdownReport(reportData);
    }

    if (format === 'html') {
      return this.generateHTMLReport(reportData);
    }

    return reportData;
  }

  /**
   * Generate markdown report
   */
  generateMarkdownReport(data) {
    let md = `# Security Scan Report\n\n`;
    md += `**Scan Date:** ${data.timestamp}\n`;
    md += `**Security Score:** ${data.score}/100 (Grade: ${data.grade})\n\n`;

    md += `## Summary\n\n`;
    md += `- **Vulnerabilities:** ${data.summary.totalVulnerabilities}\n`;
    md += `  - Critical: ${data.summary.criticalCount}\n`;
    md += `  - High: ${data.summary.highCount}\n`;
    md += `  - Medium: ${data.summary.mediumCount}\n`;
    md += `  - Low: ${data.summary.lowCount}\n`;
    md += `- **Warnings:** ${data.summary.warningCount}\n`;
    md += `- **Recommendations:** ${data.summary.recommendationCount}\n\n`;

    if (data.vulnerabilities.length > 0) {
      md += `## Vulnerabilities\n\n`;
      data.vulnerabilities.forEach((vuln, i) => {
        md += `### ${i + 1}. ${vuln.title} [${vuln.severity}]\n\n`;
        md += `- **Category:** ${vuln.category}\n`;
        md += `- **Description:** ${vuln.description}\n`;
        if (vuln.remediation) md += `- **Remediation:** ${vuln.remediation}\n`;
        if (vuln.owaspRef) md += `- **OWASP Reference:** ${vuln.owaspRef}\n`;
        md += `\n`;
      });
    }

    if (data.warnings.length > 0) {
      md += `## Warnings\n\n`;
      data.warnings.forEach((warning, i) => {
        md += `### ${i + 1}. ${warning.title} [${warning.severity}]\n\n`;
        md += `- **Category:** ${warning.category}\n`;
        md += `- **Description:** ${warning.description}\n`;
        if (warning.remediation) md += `- **Remediation:** ${warning.remediation}\n`;
        md += `\n`;
      });
    }

    if (data.recommendations.length > 0) {
      md += `## Recommendations\n\n`;
      data.recommendations.forEach((rec, i) => {
        md += `${i + 1}. **${rec.title}**\n`;
        if (rec.description) md += `   - ${rec.description}\n`;
        if (rec.recommendation) md += `   - ${rec.recommendation}\n`;
        md += `\n`;
      });
    }

    return md;
  }

  /**
   * Generate HTML report
   */
  generateHTMLReport(data) {
    // Basic HTML report template
    return `<!DOCTYPE html>
<html>
<head>
  <title>Security Scan Report</title>
  <style>
    body { font-family: Arial, sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
    .header { background: #2c3e50; color: white; padding: 20px; border-radius: 5px; }
    .score { font-size: 48px; font-weight: bold; }
    .grade-A { color: #2ecc71; }
    .grade-B { color: #f39c12; }
    .grade-C { color: #e67e22; }
    .grade-D, .grade-F { color: #e74c3c; }
    .vulnerability { background: #ffebee; padding: 15px; margin: 10px 0; border-left: 4px solid #f44336; }
    .warning { background: #fff3e0; padding: 15px; margin: 10px 0; border-left: 4px solid #ff9800; }
    .recommendation { background: #e3f2fd; padding: 15px; margin: 10px 0; border-left: 4px solid #2196f3; }
    .severity-CRITICAL { color: #d32f2f; font-weight: bold; }
    .severity-HIGH { color: #f57c00; font-weight: bold; }
    .severity-MEDIUM { color: #fbc02d; font-weight: bold; }
    .severity-LOW { color: #689f38; }
  </style>
</head>
<body>
  <div class="header">
    <h1>Security Scan Report</h1>
    <p>${data.timestamp}</p>
    <div class="score grade-${data.grade}">${data.score}/100 (Grade: ${data.grade})</div>
  </div>

  <h2>Summary</h2>
  <ul>
    <li>Total Vulnerabilities: ${data.summary.totalVulnerabilities}</li>
    <li>Critical: ${data.summary.criticalCount}</li>
    <li>High: ${data.summary.highCount}</li>
    <li>Medium: ${data.summary.mediumCount}</li>
    <li>Low: ${data.summary.lowCount}</li>
    <li>Warnings: ${data.summary.warningCount}</li>
    <li>Recommendations: ${data.summary.recommendationCount}</li>
  </ul>

  ${data.vulnerabilities.length > 0 ? `
    <h2>Vulnerabilities</h2>
    ${data.vulnerabilities.map((v, i) => `
      <div class="vulnerability">
        <h3>${i + 1}. ${v.title} <span class="severity-${v.severity}">[${v.severity}]</span></h3>
        <p><strong>Category:</strong> ${v.category}</p>
        <p><strong>Description:</strong> ${v.description}</p>
        ${v.remediation ? `<p><strong>Remediation:</strong> ${v.remediation}</p>` : ''}
        ${v.owaspRef ? `<p><strong>OWASP:</strong> ${v.owaspRef}</p>` : ''}
      </div>
    `).join('')}
  ` : ''}

  ${data.warnings.length > 0 ? `
    <h2>Warnings</h2>
    ${data.warnings.map((w, i) => `
      <div class="warning">
        <h3>${i + 1}. ${w.title} <span class="severity-${w.severity}">[${w.severity}]</span></h3>
        <p><strong>Category:</strong> ${w.category}</p>
        <p><strong>Description:</strong> ${w.description}</p>
        ${w.remediation ? `<p><strong>Remediation:</strong> ${w.remediation}</p>` : ''}
      </div>
    `).join('')}
  ` : ''}

  ${data.recommendations.length > 0 ? `
    <h2>Recommendations</h2>
    ${data.recommendations.map((r, i) => `
      <div class="recommendation">
        <h3>${i + 1}. ${r.title}</h3>
        ${r.description ? `<p>${r.description}</p>` : ''}
        ${r.recommendation ? `<p><strong>Recommendation:</strong> ${r.recommendation}</p>` : ''}
      </div>
    `).join('')}
  ` : ''}
</body>
</html>`;
  }

  /**
   * Save report to file
   */
  async saveReport(format = 'json', filename = null) {
    const report = await this.generateReport(format);
    const ext = format === 'html' ? 'html' : format === 'markdown' ? 'md' : 'json';
    const filepath = filename || path.join(__dirname, '..', 'logs', `security-scan-${Date.now()}.${ext}`);

    await fs.mkdir(path.dirname(filepath), { recursive: true });
    await fs.writeFile(filepath, report);

    return filepath;
  }
}

export default SecurityScanner;