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>
262 lines
8.3 KiB
JavaScript
262 lines
8.3 KiB
JavaScript
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;
|