- Create Economist SubmissionTracking package correctly: * mainArticle = full blog post content * coverLetter = 216-word SIR— letter * Links to blog post via blogPostId - Archive 'Letter to The Economist' from blog posts (it's the cover letter) - Fix date display on article cards (use published_at) - Target publication already displaying via blue badge Database changes: - Make blogPostId optional in SubmissionTracking model - Economist package ID: 68fa85ae49d4900e7f2ecd83 - Le Monde package ID: 68fa2abd2e6acd5691932150 Next: Enhanced modal with tabs, validation, export 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
476 lines
18 KiB
JavaScript
Executable file
476 lines
18 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
/**
|
|
* Security Audit Script
|
|
* Checks for common security vulnerabilities and best practices
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { execSync } = require('child_process');
|
|
|
|
// ANSI colors for output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
red: '\x1b[31m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
magenta: '\x1b[35m',
|
|
cyan: '\x1b[36m'
|
|
};
|
|
|
|
const issues = {
|
|
critical: [],
|
|
high: [],
|
|
medium: [],
|
|
low: [],
|
|
info: []
|
|
};
|
|
|
|
function log(level, message, detail = '') {
|
|
const levelColors = {
|
|
CRITICAL: colors.red,
|
|
HIGH: colors.red,
|
|
MEDIUM: colors.yellow,
|
|
LOW: colors.cyan,
|
|
INFO: colors.blue,
|
|
PASS: colors.green
|
|
};
|
|
|
|
const color = levelColors[level] || colors.reset;
|
|
console.log(`${color}[${level}]${colors.reset} ${message}`);
|
|
if (detail) {
|
|
console.log(` ${detail}`);
|
|
}
|
|
}
|
|
|
|
function addIssue(severity, title, description, remediation) {
|
|
const issue = { title, description, remediation };
|
|
issues[severity].push(issue);
|
|
}
|
|
|
|
// ============================================================================
|
|
// 1. CHECK ENVIRONMENT VARIABLES
|
|
// ============================================================================
|
|
function checkEnvironmentVariables() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '1. Environment Variables Security' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
const requiredSecrets = [
|
|
'JWT_SECRET',
|
|
'SESSION_SECRET',
|
|
'MONGODB_URI'
|
|
];
|
|
|
|
const envExamplePath = path.join(__dirname, '../.env.example');
|
|
const envPath = path.join(__dirname, '../.env');
|
|
|
|
// Check if .env.example exists
|
|
if (!fs.existsSync(envExamplePath)) {
|
|
addIssue('medium', 'Missing .env.example',
|
|
'.env.example file not found',
|
|
'Create .env.example with placeholder values for all required environment variables');
|
|
log('MEDIUM', 'Missing .env.example file');
|
|
} else {
|
|
log('PASS', '.env.example file exists');
|
|
}
|
|
|
|
// Check if .env is in .gitignore
|
|
const gitignorePath = path.join(__dirname, '../.gitignore');
|
|
if (fs.existsSync(gitignorePath)) {
|
|
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
if (!gitignoreContent.includes('.env')) {
|
|
addIssue('critical', '.env not in .gitignore',
|
|
'.env file might be committed to git',
|
|
'Add .env to .gitignore immediately');
|
|
log('CRITICAL', '.env not found in .gitignore');
|
|
} else {
|
|
log('PASS', '.env is in .gitignore');
|
|
}
|
|
}
|
|
|
|
// Check for hardcoded secrets in code
|
|
const srcDir = path.join(__dirname, '../src');
|
|
try {
|
|
const grepCmd = `grep -r -i "password\\s*=\\s*['\"]\\|secret\\s*=\\s*['\"]\\|api[_-]key\\s*=\\s*['\"]" ${srcDir} || true`;
|
|
const result = execSync(grepCmd, { encoding: 'utf8' });
|
|
if (result.trim()) {
|
|
addIssue('critical', 'Hardcoded secrets detected',
|
|
`Found potential hardcoded secrets:\n${result}`,
|
|
'Remove hardcoded secrets and use environment variables');
|
|
log('CRITICAL', 'Potential hardcoded secrets found');
|
|
console.log(result);
|
|
} else {
|
|
log('PASS', 'No hardcoded secrets detected in src/');
|
|
}
|
|
} catch (err) {
|
|
// grep returns non-zero if no matches, which is good
|
|
log('PASS', 'No hardcoded secrets detected in src/');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 2. CHECK DEPENDENCIES FOR VULNERABILITIES
|
|
// ============================================================================
|
|
function checkDependencies() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '2. Dependency Vulnerabilities' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
try {
|
|
log('INFO', 'Running npm audit...');
|
|
const auditResult = execSync('npm audit --json', { encoding: 'utf8' });
|
|
const audit = JSON.parse(auditResult);
|
|
|
|
if (audit.vulnerabilities) {
|
|
const vulns = audit.vulnerabilities;
|
|
const critical = Object.values(vulns).filter(v => v.severity === 'critical').length;
|
|
const high = Object.values(vulns).filter(v => v.severity === 'high').length;
|
|
const moderate = Object.values(vulns).filter(v => v.severity === 'moderate').length;
|
|
const low = Object.values(vulns).filter(v => v.severity === 'low').length;
|
|
|
|
if (critical > 0) {
|
|
addIssue('critical', 'Critical dependency vulnerabilities',
|
|
`Found ${critical} critical vulnerabilities`,
|
|
'Run npm audit fix or update vulnerable dependencies');
|
|
log('CRITICAL', `${critical} critical vulnerabilities`);
|
|
}
|
|
if (high > 0) {
|
|
addIssue('high', 'High severity dependency vulnerabilities',
|
|
`Found ${high} high severity vulnerabilities`,
|
|
'Run npm audit fix or update vulnerable dependencies');
|
|
log('HIGH', `${high} high severity vulnerabilities`);
|
|
}
|
|
if (moderate > 0) {
|
|
log('MEDIUM', `${moderate} moderate severity vulnerabilities`);
|
|
}
|
|
if (low > 0) {
|
|
log('LOW', `${low} low severity vulnerabilities`);
|
|
}
|
|
|
|
if (critical === 0 && high === 0 && moderate === 0 && low === 0) {
|
|
log('PASS', 'No known vulnerabilities in dependencies');
|
|
}
|
|
}
|
|
} catch (err) {
|
|
log('INFO', 'npm audit completed with findings (check above)');
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 3. CHECK AUTHENTICATION & AUTHORIZATION
|
|
// ============================================================================
|
|
function checkAuthSecurity() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '3. Authentication & Authorization' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
// Check JWT secret strength
|
|
const jwtUtilPath = path.join(__dirname, '../src/utils/jwt.util.js');
|
|
if (fs.existsSync(jwtUtilPath)) {
|
|
const jwtContent = fs.readFileSync(jwtUtilPath, 'utf8');
|
|
|
|
// Check if JWT_SECRET is required
|
|
if (!jwtContent.includes('JWT_SECRET')) {
|
|
addIssue('critical', 'JWT secret not configured',
|
|
'JWT_SECRET environment variable not used',
|
|
'Configure JWT_SECRET in environment variables');
|
|
log('CRITICAL', 'JWT_SECRET not found in jwt.util.js');
|
|
} else {
|
|
log('PASS', 'JWT uses environment variable for secret');
|
|
}
|
|
|
|
// Check for secure JWT options
|
|
if (!jwtContent.includes('expiresIn')) {
|
|
addIssue('medium', 'JWT expiration not set',
|
|
'Tokens may not expire',
|
|
'Set expiresIn option for JWT tokens');
|
|
log('MEDIUM', 'JWT expiration not configured');
|
|
} else {
|
|
log('PASS', 'JWT expiration configured');
|
|
}
|
|
}
|
|
|
|
// Check password hashing
|
|
const userModelPath = path.join(__dirname, '../src/models/User.model.js');
|
|
if (fs.existsSync(userModelPath)) {
|
|
const userContent = fs.readFileSync(userModelPath, 'utf8');
|
|
|
|
if (!userContent.includes('bcrypt')) {
|
|
addIssue('critical', 'Passwords not hashed',
|
|
'bcrypt not found in User model',
|
|
'Use bcrypt to hash passwords with salt rounds >= 10');
|
|
log('CRITICAL', 'Password hashing (bcrypt) not found');
|
|
} else {
|
|
log('PASS', 'Passwords are hashed with bcrypt');
|
|
|
|
// Check salt rounds
|
|
const saltRoundsMatch = userContent.match(/bcrypt\.hash\([^,]+,\s*(\d+)/);
|
|
if (saltRoundsMatch) {
|
|
const rounds = parseInt(saltRoundsMatch[1]);
|
|
if (rounds < 10) {
|
|
addIssue('medium', 'Weak bcrypt salt rounds',
|
|
`Salt rounds set to ${rounds}, should be >= 10`,
|
|
'Increase bcrypt salt rounds to at least 10');
|
|
log('MEDIUM', `Bcrypt salt rounds: ${rounds} (should be >= 10)`);
|
|
} else {
|
|
log('PASS', `Bcrypt salt rounds: ${rounds}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check rate limiting
|
|
const serverPath = path.join(__dirname, '../src/server.js');
|
|
if (fs.existsSync(serverPath)) {
|
|
const serverContent = fs.readFileSync(serverPath, 'utf8');
|
|
|
|
if (!serverContent.includes('rateLimit') && !serverContent.includes('express-rate-limit')) {
|
|
addIssue('high', 'No rate limiting',
|
|
'Rate limiting not implemented',
|
|
'Add express-rate-limit to prevent brute force attacks');
|
|
log('HIGH', 'Rate limiting not found');
|
|
} else {
|
|
log('PASS', 'Rate limiting implemented');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 4. CHECK INPUT VALIDATION
|
|
// ============================================================================
|
|
function checkInputValidation() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '4. Input Validation & Sanitization' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
// Check for validation middleware
|
|
const validationPath = path.join(__dirname, '../src/middleware/validation.middleware.js');
|
|
if (!fs.existsSync(validationPath)) {
|
|
addIssue('high', 'No validation middleware',
|
|
'Input validation middleware not found',
|
|
'Create validation middleware to sanitize user inputs');
|
|
log('HIGH', 'Validation middleware not found');
|
|
} else {
|
|
log('PASS', 'Validation middleware exists');
|
|
|
|
const validationContent = fs.readFileSync(validationPath, 'utf8');
|
|
|
|
// Check for common validation functions
|
|
const validations = ['validateEmail', 'validateRequired', 'validateObjectId'];
|
|
validations.forEach(fn => {
|
|
if (validationContent.includes(fn)) {
|
|
log('PASS', `${fn} validation implemented`);
|
|
} else {
|
|
log('LOW', `${fn} validation not found`);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check for NoSQL injection protection
|
|
const controllersDir = path.join(__dirname, '../src/controllers');
|
|
if (fs.existsSync(controllersDir)) {
|
|
try {
|
|
const grepCmd = `grep -r "\\$where\\|\\$ne\\|\\$gt" ${controllersDir} || true`;
|
|
const result = execSync(grepCmd, { encoding: 'utf8' });
|
|
if (result.trim()) {
|
|
addIssue('medium', 'Potential NoSQL injection vectors',
|
|
'Direct use of MongoDB operators in controllers',
|
|
'Sanitize user inputs before using in database queries');
|
|
log('MEDIUM', 'Potential NoSQL injection vectors found');
|
|
} else {
|
|
log('PASS', 'No obvious NoSQL injection vectors');
|
|
}
|
|
} catch (err) {
|
|
log('PASS', 'No obvious NoSQL injection vectors');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 5. CHECK SECURITY HEADERS
|
|
// ============================================================================
|
|
function checkSecurityHeaders() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '5. Security Headers' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
const serverPath = path.join(__dirname, '../src/server.js');
|
|
if (fs.existsSync(serverPath)) {
|
|
const serverContent = fs.readFileSync(serverPath, 'utf8');
|
|
|
|
if (!serverContent.includes('helmet')) {
|
|
addIssue('high', 'helmet middleware not used',
|
|
'Security headers not configured',
|
|
'Add helmet middleware to set security headers');
|
|
log('HIGH', 'helmet middleware not found');
|
|
} else {
|
|
log('PASS', 'helmet middleware configured');
|
|
}
|
|
|
|
if (!serverContent.includes('cors')) {
|
|
addIssue('medium', 'CORS not configured',
|
|
'CORS middleware not found',
|
|
'Configure CORS to restrict cross-origin requests');
|
|
log('MEDIUM', 'CORS not configured');
|
|
} else {
|
|
log('PASS', 'CORS configured');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// 6. CHECK FILE PERMISSIONS
|
|
// ============================================================================
|
|
function checkFilePermissions() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '6. File Permissions' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
const sensitiveFiles = [
|
|
'.env',
|
|
'package.json',
|
|
'src/config/app.config.js'
|
|
];
|
|
|
|
sensitiveFiles.forEach(file => {
|
|
const filePath = path.join(__dirname, '..', file);
|
|
if (fs.existsSync(filePath)) {
|
|
const stats = fs.statSync(filePath);
|
|
const mode = (stats.mode & parseInt('777', 8)).toString(8);
|
|
|
|
if (file === '.env' && mode !== '600') {
|
|
addIssue('medium', `.env file permissions too permissive`,
|
|
`File permissions: ${mode} (should be 600)`,
|
|
`chmod 600 ${file}`);
|
|
log('MEDIUM', `.env permissions: ${mode} (should be 600)`);
|
|
} else {
|
|
log('PASS', `${file} permissions: ${mode}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// ============================================================================
|
|
// 7. CHECK LOGGING & ERROR HANDLING
|
|
// ============================================================================
|
|
function checkLoggingAndErrors() {
|
|
console.log('\n' + colors.cyan + '='.repeat(80) + colors.reset);
|
|
console.log(colors.cyan + '7. Logging & Error Handling' + colors.reset);
|
|
console.log(colors.cyan + '='.repeat(80) + colors.reset);
|
|
|
|
const errorMiddlewarePath = path.join(__dirname, '../src/middleware/error.middleware.js');
|
|
if (!fs.existsSync(errorMiddlewarePath)) {
|
|
addIssue('medium', 'No error handling middleware',
|
|
'Error middleware not found',
|
|
'Create error handling middleware to sanitize error messages');
|
|
log('MEDIUM', 'Error handling middleware not found');
|
|
} else {
|
|
const errorContent = fs.readFileSync(errorMiddlewarePath, 'utf8');
|
|
|
|
// Check that stack traces are not exposed in production
|
|
if (errorContent.includes('stack') && !errorContent.includes('NODE_ENV')) {
|
|
addIssue('medium', 'Stack traces may be exposed',
|
|
'Error handler may expose stack traces in production',
|
|
'Only show stack traces in development environment');
|
|
log('MEDIUM', 'Stack traces may be exposed in production');
|
|
} else {
|
|
log('PASS', 'Error handling configured properly');
|
|
}
|
|
}
|
|
|
|
// Check logger configuration
|
|
const loggerPath = path.join(__dirname, '../src/utils/logger.util.js');
|
|
if (fs.existsSync(loggerPath)) {
|
|
const loggerContent = fs.readFileSync(loggerPath, 'utf8');
|
|
|
|
if (loggerContent.includes('password') || loggerContent.includes('token')) {
|
|
log('LOW', 'Logger may log sensitive data - review logger.util.js');
|
|
} else {
|
|
log('PASS', 'Logger configuration looks safe');
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// GENERATE REPORT
|
|
// ============================================================================
|
|
function generateReport() {
|
|
console.log('\n' + colors.magenta + '='.repeat(80) + colors.reset);
|
|
console.log(colors.magenta + 'SECURITY AUDIT SUMMARY' + colors.reset);
|
|
console.log(colors.magenta + '='.repeat(80) + colors.reset);
|
|
|
|
const totalIssues = issues.critical.length + issues.high.length +
|
|
issues.medium.length + issues.low.length;
|
|
|
|
console.log(`\n${colors.cyan}Total Issues Found: ${totalIssues}${colors.reset}`);
|
|
console.log(` ${colors.red}Critical: ${issues.critical.length}${colors.reset}`);
|
|
console.log(` ${colors.red}High: ${issues.high.length}${colors.reset}`);
|
|
console.log(` ${colors.yellow}Medium: ${issues.medium.length}${colors.reset}`);
|
|
console.log(` ${colors.cyan}Low: ${issues.low.length}${colors.reset}`);
|
|
|
|
// Print critical issues
|
|
if (issues.critical.length > 0) {
|
|
console.log('\n' + colors.red + 'CRITICAL ISSUES:' + colors.reset);
|
|
issues.critical.forEach((issue, i) => {
|
|
console.log(`\n${i + 1}. ${colors.red}${issue.title}${colors.reset}`);
|
|
console.log(` ${issue.description}`);
|
|
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
|
});
|
|
}
|
|
|
|
// Print high issues
|
|
if (issues.high.length > 0) {
|
|
console.log('\n' + colors.red + 'HIGH SEVERITY ISSUES:' + colors.reset);
|
|
issues.high.forEach((issue, i) => {
|
|
console.log(`\n${i + 1}. ${colors.red}${issue.title}${colors.reset}`);
|
|
console.log(` ${issue.description}`);
|
|
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
|
});
|
|
}
|
|
|
|
// Print medium issues
|
|
if (issues.medium.length > 0) {
|
|
console.log('\n' + colors.yellow + 'MEDIUM SEVERITY ISSUES:' + colors.reset);
|
|
issues.medium.forEach((issue, i) => {
|
|
console.log(`\n${i + 1}. ${colors.yellow}${issue.title}${colors.reset}`);
|
|
console.log(` ${issue.description}`);
|
|
console.log(` ${colors.green}→ ${issue.remediation}${colors.reset}`);
|
|
});
|
|
}
|
|
|
|
// Overall status
|
|
console.log('\n' + colors.magenta + '='.repeat(80) + colors.reset);
|
|
if (issues.critical.length === 0 && issues.high.length === 0) {
|
|
console.log(colors.green + '✓ No critical or high severity issues found' + colors.reset);
|
|
} else {
|
|
console.log(colors.red + '✗ Critical or high severity issues require immediate attention' + colors.reset);
|
|
}
|
|
console.log(colors.magenta + '='.repeat(80) + colors.reset + '\n');
|
|
|
|
// Exit with error code if critical/high issues found
|
|
process.exit((issues.critical.length + issues.high.length) > 0 ? 1 : 0);
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN
|
|
// ============================================================================
|
|
function main() {
|
|
console.log(colors.magenta + '\n' + '='.repeat(80));
|
|
console.log('TRACTATUS SECURITY AUDIT');
|
|
console.log('Checking for common security vulnerabilities and best practices');
|
|
console.log('='.repeat(80) + colors.reset);
|
|
|
|
checkEnvironmentVariables();
|
|
checkDependencies();
|
|
checkAuthSecurity();
|
|
checkInputValidation();
|
|
checkSecurityHeaders();
|
|
checkFilePermissions();
|
|
checkLoggingAndErrors();
|
|
|
|
generateReport();
|
|
}
|
|
|
|
main();
|