← 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;