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:
2026-02-11 16:41:39 -07:00
parent bf3d01becf
commit 0d67a99c7e
9 changed files with 1020 additions and 0 deletions

View File

@@ -0,0 +1,261 @@
const express = require('express');
const path = require('path');
const fs = require('fs');
const { requireAuth, requireRole } = require('../middleware/auth');
const logAudit = require('../helpers/auditLog');
const { processVulnerabilityReport } = require('../helpers/excelProcessor');
function createWeeklyReportsRouter(db, upload) {
const router = express.Router();
// Helper to sanitize filename
function sanitizePathSegment(segment) {
if (!segment || typeof segment !== 'string') return '';
return segment
.replace(/\0/g, '')
.replace(/\.\./g, '')
.replace(/[\/\\]/g, '')
.trim();
}
// Helper to generate week label
function getWeekLabel(date) {
const now = new Date();
const uploadDate = new Date(date);
const daysDiff = Math.floor((now - uploadDate) / (1000 * 60 * 60 * 24));
if (daysDiff < 7) {
return "This week's report";
} else if (daysDiff < 14) {
return "Last week's report";
} else {
const month = uploadDate.getMonth() + 1;
const day = uploadDate.getDate();
const year = uploadDate.getFullYear();
return `Week of ${month.toString().padStart(2, '0')}/${day.toString().padStart(2, '0')}/${year}`;
}
}
// POST /api/weekly-reports/upload - Upload and process vulnerability report
router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), upload.single('file'), async (req, res) => {
const uploadedFile = req.file;
if (!uploadedFile) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file extension
const ext = path.extname(uploadedFile.originalname).toLowerCase();
if (ext !== '.xlsx') {
fs.unlinkSync(uploadedFile.path); // Clean up temp file
return res.status(400).json({ error: 'Only .xlsx files are allowed' });
}
const timestamp = Date.now();
const sanitizedName = sanitizePathSegment(uploadedFile.originalname);
const reportsDir = path.join(__dirname, '..', 'uploads', 'weekly_reports');
// Create directory if it doesn't exist
if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}
const originalFilename = `${timestamp}_original_${sanitizedName}`;
const processedFilename = `${timestamp}_processed_${sanitizedName}`;
const originalPath = path.join(reportsDir, originalFilename);
const processedPath = path.join(reportsDir, processedFilename);
try {
// Move uploaded file to permanent location
fs.renameSync(uploadedFile.path, originalPath);
// Process the file with Python script
const result = await processVulnerabilityReport(originalPath, processedPath);
const uploadDate = new Date().toISOString().split('T')[0];
// Update previous current reports to not current
db.run('UPDATE weekly_reports SET is_current = 0 WHERE is_current = 1', (err) => {
if (err) {
console.error('Error updating previous current reports:', err);
}
});
// Insert new report record
const insertSql = `
INSERT INTO weekly_reports (
upload_date, week_label, original_filename, processed_filename,
original_file_path, processed_file_path, row_count_original,
row_count_processed, uploaded_by, is_current
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`;
const weekLabel = getWeekLabel(uploadDate);
db.run(
insertSql,
[
uploadDate,
weekLabel,
sanitizedName,
processedFilename,
originalPath,
processedPath,
result.original_rows,
result.processed_rows,
req.user.id
],
function (err) {
if (err) {
console.error('Error inserting weekly report:', err);
return res.status(500).json({ error: 'Failed to save report metadata' });
}
// Log audit entry
logAudit(
db,
req.user.id,
req.user.username,
'UPLOAD_WEEKLY_REPORT',
'weekly_reports',
this.lastID,
JSON.stringify({ filename: sanitizedName, rows: result.processed_rows }),
req.ip
);
res.json({
success: true,
id: this.lastID,
original_rows: result.original_rows,
processed_rows: result.processed_rows,
week_label: weekLabel
});
}
);
} catch (error) {
// Clean up files on error
if (fs.existsSync(originalPath)) fs.unlinkSync(originalPath);
if (fs.existsSync(processedPath)) fs.unlinkSync(processedPath);
console.error('Error processing vulnerability report:', error);
res.status(500).json({ error: error.message || 'Failed to process report' });
}
});
// GET /api/weekly-reports - List all reports
router.get('/', requireAuth(db), (req, res) => {
const sql = `
SELECT id, upload_date, week_label, original_filename, processed_filename,
row_count_original, row_count_processed, is_current, uploaded_at
FROM weekly_reports
ORDER BY upload_date DESC, uploaded_at DESC
`;
db.all(sql, [], (err, rows) => {
if (err) {
console.error('Error fetching weekly reports:', err);
return res.status(500).json({ error: 'Failed to fetch reports' });
}
res.json(rows);
});
});
// GET /api/weekly-reports/:id/download/:type - Download report file
router.get('/:id/download/:type', requireAuth(db), (req, res) => {
const { id, type } = req.params;
if (type !== 'original' && type !== 'processed') {
return res.status(400).json({ error: 'Invalid download type. Use "original" or "processed"' });
}
const sql = `SELECT original_file_path, processed_file_path, original_filename FROM weekly_reports WHERE id = ?`;
db.get(sql, [id], (err, row) => {
if (err) {
console.error('Error fetching report:', err);
return res.status(500).json({ error: 'Failed to fetch report' });
}
if (!row) {
return res.status(404).json({ error: 'Report not found' });
}
const filePath = type === 'original' ? row.original_file_path : row.processed_file_path;
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found on disk' });
}
// Log audit entry
logAudit(
db,
req.user.id,
req.user.username,
'DOWNLOAD_WEEKLY_REPORT',
'weekly_reports',
id,
JSON.stringify({ type }),
req.ip
);
const downloadName = type === 'original' ? row.original_filename : row.original_filename.replace('.xlsx', '_processed.xlsx');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename="${downloadName}"`);
res.sendFile(filePath);
});
});
// DELETE /api/weekly-reports/:id - Delete report (admin only)
router.delete('/:id', requireAuth(db), requireRole(db, 'admin'), (req, res) => {
const { id } = req.params;
const sql = 'SELECT original_file_path, processed_file_path FROM weekly_reports WHERE id = ?';
db.get(sql, [id], (err, row) => {
if (err) {
console.error('Error fetching report for deletion:', err);
return res.status(500).json({ error: 'Failed to fetch report' });
}
if (!row) {
return res.status(404).json({ error: 'Report not found' });
}
// Delete database record
db.run('DELETE FROM weekly_reports WHERE id = ?', [id], (err) => {
if (err) {
console.error('Error deleting report:', err);
return res.status(500).json({ error: 'Failed to delete report' });
}
// Delete files
if (fs.existsSync(row.original_file_path)) {
fs.unlinkSync(row.original_file_path);
}
if (fs.existsSync(row.processed_file_path)) {
fs.unlinkSync(row.processed_file_path);
}
// Log audit entry
logAudit(
db,
req.user.id,
req.user.username,
'DELETE_WEEKLY_REPORT',
'weekly_reports',
id,
null,
req.ip
);
res.json({ success: true });
});
});
});
return router;
}
module.exports = createWeeklyReportsRouter;