Add weekly vulnerability report upload feature
Implements a comprehensive system for uploading and processing weekly vulnerability reports that automatically splits multiple CVE IDs in a single cell into separate rows for easier filtering and analysis. Backend Changes: - Add weekly_reports table with migration - Create Excel processor helper using Python child_process - Implement API routes for upload, list, download, delete - Mount routes in server.js after multer initialization - Move split_cve_report.py to backend/scripts/ Frontend Changes: - Add WeeklyReportModal component with phase-based UI - Add "Weekly Report" button next to NVD Sync - Integrate modal into App.js with state management - Display existing reports with current report indicator - Download buttons for original and processed files Features: - Upload .xlsx files (editor/admin only) - Automatic CVE ID splitting via Python script - Store metadata in database + files on filesystem - Auto-archive previous reports (mark one as current) - Download both original and processed versions - Audit logging for all operations - Security: file validation, auth checks, path sanitization Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
93
backend/helpers/excelProcessor.js
Normal file
93
backend/helpers/excelProcessor.js
Normal file
@@ -0,0 +1,93 @@
|
||||
const { spawn } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
/**
|
||||
* Process vulnerability report Excel file by splitting CVE IDs into separate rows
|
||||
* @param {string} inputPath - Path to original Excel file
|
||||
* @param {string} outputPath - Path for processed Excel file
|
||||
* @returns {Promise<{original_rows: number, processed_rows: number, output_path: string}>}
|
||||
*/
|
||||
function processVulnerabilityReport(inputPath, outputPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const scriptPath = path.join(__dirname, '..', 'scripts', 'split_cve_report.py');
|
||||
|
||||
// Verify script exists
|
||||
if (!fs.existsSync(scriptPath)) {
|
||||
return reject(new Error(`Python script not found: ${scriptPath}`));
|
||||
}
|
||||
|
||||
// Verify input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
return reject(new Error(`Input file not found: ${inputPath}`));
|
||||
}
|
||||
|
||||
const python = spawn('python3', [scriptPath, inputPath, outputPath]);
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let timedOut = false;
|
||||
|
||||
// 30 second timeout
|
||||
const timeout = setTimeout(() => {
|
||||
timedOut = true;
|
||||
python.kill();
|
||||
reject(new Error('Processing timed out. File may be too large or corrupted.'));
|
||||
}, 30000);
|
||||
|
||||
python.stdout.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
python.stderr.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
python.on('close', (code) => {
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (timedOut) return;
|
||||
|
||||
if (code !== 0) {
|
||||
// Parse Python error messages
|
||||
if (stderr.includes('Sheet') && stderr.includes('not found')) {
|
||||
return reject(new Error('Invalid Excel file. Expected "Vulnerabilities" sheet with "CVE ID" column.'));
|
||||
}
|
||||
if (stderr.includes('pandas') || stderr.includes('openpyxl')) {
|
||||
return reject(new Error('Python dependencies missing. Run: pip3 install pandas openpyxl'));
|
||||
}
|
||||
return reject(new Error(`Python script failed: ${stderr || 'Unknown error'}`));
|
||||
}
|
||||
|
||||
// Parse output for row counts
|
||||
const originalMatch = stdout.match(/Original rows:\s*(\d+)/);
|
||||
const newMatch = stdout.match(/New rows:\s*(\d+)/);
|
||||
|
||||
if (!originalMatch || !newMatch) {
|
||||
return reject(new Error('Failed to parse row counts from Python output'));
|
||||
}
|
||||
|
||||
// Verify output file was created
|
||||
if (!fs.existsSync(outputPath)) {
|
||||
return reject(new Error('Processed file was not created'));
|
||||
}
|
||||
|
||||
resolve({
|
||||
original_rows: parseInt(originalMatch[1]),
|
||||
processed_rows: parseInt(newMatch[1]),
|
||||
output_path: outputPath
|
||||
});
|
||||
});
|
||||
|
||||
python.on('error', (err) => {
|
||||
clearTimeout(timeout);
|
||||
if (err.code === 'ENOENT') {
|
||||
reject(new Error('Python 3 is required but not found. Please install Python.'));
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { processVulnerabilityReport };
|
||||
Reference in New Issue
Block a user