// CVE Management Backend API // Install: npm install express sqlite3 multer cors dotenv bcryptjs cookie-parser require('dotenv').config(); const express = require('express'); const sqlite3 = require('sqlite3').verbose(); const multer = require('multer'); const cors = require('cors'); const cookieParser = require('cookie-parser'); const path = require('path'); const fs = require('fs'); // Auth imports const { requireAuth, requireRole } = require('./middleware/auth'); const createAuthRouter = require('./routes/auth'); const createUsersRouter = require('./routes/users'); const createAuditLogRouter = require('./routes/auditLog'); const logAudit = require('./helpers/auditLog'); const createNvdLookupRouter = require('./routes/nvdLookup'); const app = express(); const PORT = process.env.PORT || 3001; const API_HOST = process.env.API_HOST || 'localhost'; const SESSION_SECRET = process.env.SESSION_SECRET || 'default-secret-change-me'; const CORS_ORIGINS = process.env.CORS_ORIGINS ? process.env.CORS_ORIGINS.split(',') : ['http://localhost:3000']; // Log all incoming requests app.use((req, res, next) => { console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`); next(); }); // Middleware app.use(cors({ origin: CORS_ORIGINS, credentials: true })); app.use(express.json()); app.use(cookieParser()); app.use('/uploads', express.static('uploads')); // Database connection const db = new sqlite3.Database('./cve_database.db', (err) => { if (err) console.error('Database connection error:', err); else console.log('Connected to CVE database'); }); // Auth routes (public) app.use('/api/auth', createAuthRouter(db, logAudit)); // User management routes (admin only) app.use('/api/users', createUsersRouter(db, requireAuth, requireRole, logAudit)); // Audit log routes (admin only) app.use('/api/audit-logs', createAuditLogRouter(db, requireAuth, requireRole)); // NVD lookup routes (authenticated users) app.use('/api/nvd', createNvdLookupRouter(db, requireAuth)); // Simple storage - upload to temp directory first const storage = multer.diskStorage({ destination: (req, file, cb) => { const tempDir = 'uploads/temp'; if (!fs.existsSync(tempDir)) { fs.mkdirSync(tempDir, { recursive: true }); } cb(null, tempDir); }, filename: (req, file, cb) => { const timestamp = Date.now(); cb(null, `${timestamp}-${file.originalname}`); } }); const upload = multer({ storage: storage, limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit }); // ========== CVE ENDPOINTS ========== // Get all CVEs with optional filters (authenticated users) app.get('/api/cves', requireAuth(db), (req, res) => { const { search, vendor, severity, status } = req.query; let query = ` SELECT c.*, COUNT(d.id) as document_count, CASE WHEN COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) > 0 THEN 'Complete' ELSE 'Incomplete' END as doc_status FROM cves c LEFT JOIN documents d ON c.cve_id = d.cve_id WHERE 1=1 `; const params = []; if (search) { query += ` AND (c.cve_id LIKE ? OR c.description LIKE ?)`; params.push(`%${search}%`, `%${search}%`); } if (vendor && vendor !== 'All Vendors') { query += ` AND c.vendor = ?`; params.push(vendor); } if (severity && severity !== 'All Severities') { query += ` AND c.severity = ?`; params.push(severity); } if (status) { query += ` AND c.status = ?`; params.push(status); } query += ` GROUP BY c.id ORDER BY c.published_date DESC`; db.all(query, params, (err, rows) => { if (err) { console.error('Error fetching CVEs:', err); return res.status(500).json({ error: err.message }); } res.json(rows); }); }); // Get distinct CVE IDs for NVD sync (authenticated users) app.get('/api/cves/distinct-ids', requireAuth(db), (req, res) => { db.all('SELECT DISTINCT cve_id FROM cves ORDER BY cve_id', [], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); res.json(rows.map(r => r.cve_id)); }); }); // Check if CVE exists and get its status - UPDATED FOR MULTI-VENDOR (authenticated users) app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { const { cveId } = req.params; const query = ` SELECT c.*, COUNT(d.id) as total_documents, COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) as has_advisory, COUNT(CASE WHEN d.type = 'email' THEN 1 END) as has_email, COUNT(CASE WHEN d.type = 'screenshot' THEN 1 END) as has_screenshot FROM cves c LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor WHERE c.cve_id = ? GROUP BY c.id `; db.all(query, [cveId], (err, rows) => { if (err) { return res.status(500).json({ error: err.message }); } if (!rows || rows.length === 0) { return res.json({ exists: false, message: 'CVE not found - not yet addressed' }); } // Return all vendor entries for this CVE res.json({ exists: true, vendors: rows.map(row => ({ vendor: row.vendor, severity: row.severity, status: row.status, total_documents: row.total_documents, compliance: { advisory: row.has_advisory > 0, email: row.has_email > 0, screenshot: row.has_screenshot > 0 } })), addressed: true, has_required_docs: rows.some(row => row.has_advisory > 0) }); }); }); // NEW ENDPOINT: Get all vendors for a specific CVE (authenticated users) app.get('/api/cves/:cveId/vendors', requireAuth(db), (req, res) => { const { cveId } = req.params; const query = ` SELECT vendor, severity, status, description, published_date FROM cves WHERE cve_id = ? ORDER BY vendor `; db.all(query, [cveId], (err, rows) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(rows); }); }); // Create new CVE entry - ALLOW MULTIPLE VENDORS (editor or admin) app.post('/api/cves', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { console.log('=== ADD CVE REQUEST ==='); console.log('Body:', req.body); console.log('======================='); const { cve_id, vendor, severity, description, published_date } = req.body; const query = ` INSERT INTO cves (cve_id, vendor, severity, description, published_date) VALUES (?, ?, ?, ?, ?) `; console.log('Query:', query); console.log('Values:', [cve_id, vendor, severity, description, published_date]); db.run(query, [cve_id, vendor, severity, description, published_date], function(err) { if (err) { console.error('DATABASE ERROR:', err); // Make sure this is here // ... rest of error handling // Check if it's a duplicate CVE_ID + Vendor combination if (err.message.includes('UNIQUE constraint failed')) { return res.status(409).json({ error: 'This CVE already exists for this vendor. Choose a different vendor or update the existing entry.' }); } return res.status(500).json({ error: err.message }); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_create', entityType: 'cve', entityId: cve_id, details: { vendor, severity }, ipAddress: req.ip }); res.json({ id: this.lastID, cve_id, message: `CVE created successfully for vendor: ${vendor}` }); }); }); // Update CVE status (editor or admin) app.patch('/api/cves/:cveId/status', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { cveId } = req.params; const { status } = req.body; const query = `UPDATE cves SET status = ?, updated_at = CURRENT_TIMESTAMP WHERE cve_id = ?`; db.run(query, [status, cveId], function(err) { if (err) { return res.status(500).json({ error: err.message }); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_update_status', entityType: 'cve', entityId: cveId, details: { status }, ipAddress: req.ip }); res.json({ message: 'Status updated successfully', changes: this.changes }); }); }); // Bulk sync CVE data from NVD (editor or admin) app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { updates } = req.body; if (!Array.isArray(updates) || updates.length === 0) { return res.status(400).json({ error: 'No updates provided' }); } let updated = 0; const errors = []; let completed = 0; db.serialize(() => { updates.forEach((entry) => { const fields = []; const values = []; if (entry.description !== null && entry.description !== undefined) { fields.push('description = ?'); values.push(entry.description); } if (entry.severity !== null && entry.severity !== undefined) { fields.push('severity = ?'); values.push(entry.severity); } if (entry.published_date !== null && entry.published_date !== undefined) { fields.push('published_date = ?'); values.push(entry.published_date); } if (fields.length === 0) { completed++; if (completed === updates.length) sendResponse(); return; } fields.push('updated_at = CURRENT_TIMESTAMP'); values.push(entry.cve_id); db.run( `UPDATE cves SET ${fields.join(', ')} WHERE cve_id = ?`, values, function(err) { if (err) { errors.push({ cve_id: entry.cve_id, error: err.message }); } else { updated += this.changes; } completed++; if (completed === updates.length) sendResponse(); } ); }); }); function sendResponse() { logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_nvd_sync', entityType: 'cve', entityId: null, details: { count: updated, cve_ids: updates.map(u => u.cve_id) }, ipAddress: req.ip }); const result = { message: 'NVD sync completed', updated }; if (errors.length > 0) result.errors = errors; res.json(result); } }); // ========== CVE EDIT & DELETE ENDPOINTS ========== // Edit single CVE entry (editor or admin) app.put('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { id } = req.params; const { cve_id, vendor, severity, description, published_date, status } = req.body; // Fetch existing row first db.get('SELECT * FROM cves WHERE id = ?', [id], (err, existing) => { if (err) return res.status(500).json({ error: err.message }); if (!existing) return res.status(404).json({ error: 'CVE entry not found' }); const before = { cve_id: existing.cve_id, vendor: existing.vendor, severity: existing.severity, description: existing.description, published_date: existing.published_date, status: existing.status }; const newCveId = cve_id !== undefined ? cve_id : existing.cve_id; const newVendor = vendor !== undefined ? vendor : existing.vendor; const cveIdChanged = newCveId !== existing.cve_id; const vendorChanged = newVendor !== existing.vendor; const doUpdate = () => { // Build dynamic SET clause const fields = []; const values = []; if (cve_id !== undefined) { fields.push('cve_id = ?'); values.push(cve_id); } if (vendor !== undefined) { fields.push('vendor = ?'); values.push(vendor); } if (severity !== undefined) { fields.push('severity = ?'); values.push(severity); } if (description !== undefined) { fields.push('description = ?'); values.push(description); } if (published_date !== undefined) { fields.push('published_date = ?'); values.push(published_date); } if (status !== undefined) { fields.push('status = ?'); values.push(status); } if (fields.length === 0) return res.status(400).json({ error: 'No fields to update' }); fields.push('updated_at = CURRENT_TIMESTAMP'); values.push(id); db.run(`UPDATE cves SET ${fields.join(', ')} WHERE id = ?`, values, function(updateErr) { if (updateErr) return res.status(500).json({ error: updateErr.message }); const after = { cve_id: newCveId, vendor: newVendor, severity: severity !== undefined ? severity : existing.severity, description: description !== undefined ? description : existing.description, published_date: published_date !== undefined ? published_date : existing.published_date, status: status !== undefined ? status : existing.status }; logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_edit', entityType: 'cve', entityId: newCveId, details: { before, after }, ipAddress: req.ip }); res.json({ message: 'CVE updated successfully', changes: this.changes }); }); }; if (cveIdChanged || vendorChanged) { // Check UNIQUE constraint db.get('SELECT id FROM cves WHERE cve_id = ? AND vendor = ? AND id != ?', [newCveId, newVendor, id], (checkErr, conflict) => { if (checkErr) return res.status(500).json({ error: checkErr.message }); if (conflict) return res.status(409).json({ error: 'A CVE entry with this CVE ID and vendor already exists.' }); // Rename document directory const oldDir = path.join('uploads', existing.cve_id, existing.vendor); const newDir = path.join('uploads', newCveId, newVendor); if (fs.existsSync(oldDir)) { const newParent = path.join('uploads', newCveId); if (!fs.existsSync(newParent)) { fs.mkdirSync(newParent, { recursive: true }); } fs.renameSync(oldDir, newDir); // Clean up old cve_id directory if empty const oldParent = path.join('uploads', existing.cve_id); if (fs.existsSync(oldParent)) { const remaining = fs.readdirSync(oldParent); if (remaining.length === 0) fs.rmdirSync(oldParent); } } // Update documents table - file paths db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [existing.cve_id, existing.vendor], (docErr, docs) => { if (docErr) return res.status(500).json({ error: docErr.message }); const oldPrefix = path.join('uploads', existing.cve_id, existing.vendor); const newPrefix = path.join('uploads', newCveId, newVendor); let docUpdated = 0; const totalDocs = docs.length; const finishDocUpdate = () => { if (docUpdated >= totalDocs) doUpdate(); }; if (totalDocs === 0) { doUpdate(); } else { docs.forEach((doc) => { const newFilePath = doc.file_path.replace(oldPrefix, newPrefix); db.run('UPDATE documents SET cve_id = ?, vendor = ?, file_path = ? WHERE id = ?', [newCveId, newVendor, newFilePath, doc.id], (docUpdateErr) => { if (docUpdateErr) console.error('Error updating document:', docUpdateErr); docUpdated++; finishDocUpdate(); } ); }); } }); }); } else { doUpdate(); } }); }); // Delete entire CVE - all vendors (editor or admin) - MUST be before /:id route app.delete('/api/cves/by-cve-id/:cveId', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { cveId } = req.params; // Get all rows for this CVE ID to know what we're deleting db.all('SELECT * FROM cves WHERE cve_id = ?', [cveId], (err, rows) => { if (err) return res.status(500).json({ error: err.message }); if (!rows || rows.length === 0) return res.status(404).json({ error: 'No CVE entries found for this CVE ID' }); // Delete all documents from DB db.run('DELETE FROM documents WHERE cve_id = ?', [cveId], (docErr) => { if (docErr) console.error('Error deleting documents:', docErr); // Delete all CVE rows db.run('DELETE FROM cves WHERE cve_id = ?', [cveId], function(cveErr) { if (cveErr) return res.status(500).json({ error: cveErr.message }); // Remove upload directory const cveDir = path.join('uploads', cveId); if (fs.existsSync(cveDir)) { fs.rmSync(cveDir, { recursive: true, force: true }); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_delete', entityType: 'cve', entityId: cveId, details: { type: 'all_vendors', vendors: rows.map(r => r.vendor), count: rows.length }, ipAddress: req.ip }); res.json({ message: `Deleted CVE ${cveId} and all ${rows.length} vendor entries`, deleted: this.changes }); }); }); }); }); // Delete single CVE vendor entry (editor or admin) app.delete('/api/cves/:id', requireAuth(db), requireRole('editor', 'admin'), (req, res) => { const { id } = req.params; db.get('SELECT * FROM cves WHERE id = ?', [id], (err, cve) => { if (err) return res.status(500).json({ error: err.message }); if (!cve) return res.status(404).json({ error: 'CVE entry not found' }); // Delete associated documents from DB db.all('SELECT id, file_path FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (docErr, docs) => { if (docErr) console.error('Error fetching documents:', docErr); // Delete document files from disk if (docs && docs.length > 0) { docs.forEach(doc => { if (doc.file_path && fs.existsSync(doc.file_path)) { fs.unlinkSync(doc.file_path); } }); } // Delete documents from DB db.run('DELETE FROM documents WHERE cve_id = ? AND vendor = ?', [cve.cve_id, cve.vendor], (delDocErr) => { if (delDocErr) console.error('Error deleting documents from DB:', delDocErr); // Delete CVE row db.run('DELETE FROM cves WHERE id = ?', [id], function(delErr) { if (delErr) return res.status(500).json({ error: delErr.message }); // Clean up directories const vendorDir = path.join('uploads', cve.cve_id, cve.vendor); if (fs.existsSync(vendorDir)) { fs.rmSync(vendorDir, { recursive: true, force: true }); } const cveDir = path.join('uploads', cve.cve_id); if (fs.existsSync(cveDir)) { const remaining = fs.readdirSync(cveDir); if (remaining.length === 0) fs.rmdirSync(cveDir); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'cve_delete', entityType: 'cve', entityId: cve.cve_id, details: { type: 'single_vendor', vendor: cve.vendor, severity: cve.severity }, ipAddress: req.ip }); res.json({ message: `Deleted ${cve.vendor} entry for ${cve.cve_id}` }); }); }); }); }); }); // ========== DOCUMENT ENDPOINTS ========== // Get documents for a CVE - FILTER BY VENDOR (authenticated users) app.get('/api/cves/:cveId/documents', requireAuth(db), (req, res) => { const { cveId } = req.params; const { vendor } = req.query; // NEW: Optional vendor filter let query = `SELECT * FROM documents WHERE cve_id = ?`; let params = [cveId]; if (vendor) { query += ` AND vendor = ?`; params.push(vendor); } query += ` ORDER BY uploaded_at DESC`; db.all(query, params, (err, rows) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(rows); }); }); // Upload document - ADD ERROR HANDLING FOR MULTER (editor or admin) app.post('/api/cves/:cveId/documents', requireAuth(db), requireRole('editor', 'admin'), (req, res, next) => { upload.single('file')(req, res, (err) => { if (err) { console.error('MULTER ERROR:', err); return res.status(500).json({ error: 'File upload failed: ' + err.message }); } console.log('=== UPLOAD REQUEST RECEIVED ==='); console.log('CVE ID:', req.params.cveId); console.log('Body:', req.body); console.log('File:', req.file); console.log('================================'); const { cveId } = req.params; const { type, notes, vendor } = req.body; const file = req.file; if (!file) { console.error('ERROR: No file uploaded'); return res.status(400).json({ error: 'No file uploaded' }); } if (!vendor) { console.error('ERROR: Vendor is required'); return res.status(400).json({ error: 'Vendor is required' }); } // Move file from temp to proper location const finalDir = path.join('uploads', cveId, vendor); if (!fs.existsSync(finalDir)) { fs.mkdirSync(finalDir, { recursive: true }); } const finalPath = path.join(finalDir, file.filename); // Move file from temp to final location fs.renameSync(file.path, finalPath); const query = ` INSERT INTO documents (cve_id, vendor, name, type, file_path, file_size, mime_type, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `; const fileSizeKB = (file.size / 1024).toFixed(2) + ' KB'; db.run(query, [ cveId, vendor, file.originalname, type, finalPath, fileSizeKB, file.mimetype, notes ], function(err) { if (err) { console.error('DATABASE ERROR:', err); // If database insert fails, delete the file if (fs.existsSync(finalPath)) { fs.unlinkSync(finalPath); } return res.status(500).json({ error: err.message }); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'document_upload', entityType: 'document', entityId: cveId, details: { vendor, type, filename: file.originalname }, ipAddress: req.ip }); res.json({ id: this.lastID, message: 'Document uploaded successfully', file: { name: file.originalname, path: finalPath, size: fileSizeKB } }); }); }); }); // Delete document (admin only) app.delete('/api/documents/:id', requireAuth(db), requireRole('admin'), (req, res) => { const { id } = req.params; // First get the file path to delete the actual file db.get('SELECT file_path FROM documents WHERE id = ?', [id], (err, row) => { if (err) { return res.status(500).json({ error: err.message }); } if (row && fs.existsSync(row.file_path)) { fs.unlinkSync(row.file_path); } db.run('DELETE FROM documents WHERE id = ?', [id], function(err) { if (err) { return res.status(500).json({ error: err.message }); } logAudit(db, { userId: req.user.id, username: req.user.username, action: 'document_delete', entityType: 'document', entityId: id, details: { file_path: row ? row.file_path : null }, ipAddress: req.ip }); res.json({ message: 'Document deleted successfully' }); }); }); }); // ========== UTILITY ENDPOINTS ========== // Get all vendors (authenticated users) app.get('/api/vendors', requireAuth(db), (req, res) => { const query = `SELECT DISTINCT vendor FROM cves ORDER BY vendor`; db.all(query, [], (err, rows) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(rows.map(r => r.vendor)); }); }); // Get statistics (authenticated users) app.get('/api/stats', requireAuth(db), (req, res) => { const query = ` SELECT COUNT(DISTINCT c.id) as total_cves, COUNT(DISTINCT CASE WHEN c.severity = 'Critical' THEN c.id END) as critical_count, COUNT(DISTINCT CASE WHEN c.status = 'Addressed' THEN c.id END) as addressed_count, COUNT(DISTINCT d.id) as total_documents, COUNT(DISTINCT CASE WHEN cd.compliance_status = 'Complete' THEN c.id END) as compliant_count FROM cves c LEFT JOIN documents d ON c.cve_id = d.cve_id LEFT JOIN cve_document_status cd ON c.cve_id = cd.cve_id `; db.get(query, [], (err, row) => { if (err) { return res.status(500).json({ error: err.message }); } res.json(row); }); }); // Start server app.listen(PORT, () => { console.log(`CVE API server running on http://${API_HOST}:${PORT}`); console.log(`CORS origins: ${CORS_ORIGINS.join(', ')}`); });