From d520c4ae411f03dc847993b7faddbc376120da40 Mon Sep 17 00:00:00 2001 From: jramos Date: Mon, 2 Feb 2026 11:33:44 -0700 Subject: [PATCH] Added tweaks to allow edits/deletes of cve and vendors or to fix typos --- backend/server.js | 218 ++++++++++++++++++ frontend/cve_database.db | 0 frontend/src/App.js | 331 +++++++++++++++++++++++++++- frontend/src/components/AuditLog.js | 2 + 4 files changed, 543 insertions(+), 8 deletions(-) create mode 100644 frontend/cve_database.db diff --git a/backend/server.js b/backend/server.js index d2ede73..48b0536 100644 --- a/backend/server.js +++ b/backend/server.js @@ -341,6 +341,224 @@ app.post('/api/cves/nvd-sync', requireAuth(db), requireRole('editor', 'admin'), } }); +// ========== 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) diff --git a/frontend/cve_database.db b/frontend/cve_database.db new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/App.js b/frontend/src/App.js index 1e0c907..f12a882 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw } from 'lucide-react'; +import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2 } from 'lucide-react'; import { useAuth } from './contexts/AuthContext'; import LoginForm from './components/LoginForm'; import UserMenu from './components/UserMenu'; @@ -42,6 +42,14 @@ export default function App() { const [nvdLoading, setNvdLoading] = useState(false); const [nvdError, setNvdError] = useState(null); const [nvdAutoFilled, setNvdAutoFilled] = useState(false); + const [showEditCVE, setShowEditCVE] = useState(false); + const [editingCVE, setEditingCVE] = useState(null); + const [editForm, setEditForm] = useState({ + cve_id: '', vendor: '', severity: 'Medium', description: '', published_date: '', status: 'Open' + }); + const [editNvdLoading, setEditNvdLoading] = useState(false); + const [editNvdError, setEditNvdError] = useState(null); + const [editNvdAutoFilled, setEditNvdAutoFilled] = useState(false); const lookupNVD = async (cveId) => { const trimmed = cveId.trim(); @@ -283,6 +291,143 @@ export default function App() { } }; + const handleEditCVE = (cve) => { + setEditingCVE(cve); + setEditForm({ + cve_id: cve.cve_id, + vendor: cve.vendor, + severity: cve.severity, + description: cve.description || '', + published_date: cve.published_date || '', + status: cve.status || 'Open' + }); + setEditNvdLoading(false); + setEditNvdError(null); + setEditNvdAutoFilled(false); + setShowEditCVE(true); + }; + + const handleEditCVESubmit = async (e) => { + e.preventDefault(); + if (!editingCVE) return; + + try { + const body = {}; + if (editForm.cve_id !== editingCVE.cve_id) body.cve_id = editForm.cve_id; + if (editForm.vendor !== editingCVE.vendor) body.vendor = editForm.vendor; + if (editForm.severity !== editingCVE.severity) body.severity = editForm.severity; + if (editForm.description !== (editingCVE.description || '')) body.description = editForm.description; + if (editForm.published_date !== (editingCVE.published_date || '')) body.published_date = editForm.published_date; + if (editForm.status !== (editingCVE.status || 'Open')) body.status = editForm.status; + + if (Object.keys(body).length === 0) { + alert('No changes detected.'); + return; + } + + const response = await fetch(`${API_BASE}/cves/${editingCVE.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(body) + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to update CVE'); + } + + alert('CVE updated successfully!'); + setShowEditCVE(false); + setEditingCVE(null); + fetchCVEs(); + fetchVendors(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }; + + const lookupNVDForEdit = async (cveId) => { + const trimmed = cveId.trim(); + if (!/^CVE-\d{4}-\d{4,}$/.test(trimmed)) return; + + setEditNvdLoading(true); + setEditNvdError(null); + setEditNvdAutoFilled(false); + + try { + const response = await fetch(`${API_BASE}/nvd/lookup/${encodeURIComponent(trimmed)}`, { + credentials: 'include' + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'NVD lookup failed'); + } + + const data = await response.json(); + setEditForm(prev => ({ + ...prev, + description: data.description || prev.description, + severity: data.severity || prev.severity, + published_date: data.published_date || prev.published_date + })); + setEditNvdAutoFilled(true); + } catch (err) { + setEditNvdError(err.message); + } finally { + setEditNvdLoading(false); + } + }; + + const handleDeleteCVEEntry = async (cve) => { + if (!window.confirm(`Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`)) { + return; + } + + try { + const response = await fetch(`${API_BASE}/cves/${cve.id}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete CVE entry'); + } + + alert(`Deleted ${cve.vendor} entry for ${cve.cve_id}`); + fetchCVEs(); + fetchVendors(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }; + + const handleDeleteEntireCVE = async (cveId, vendorCount) => { + if (!window.confirm(`Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`)) { + return; + } + + try { + const response = await fetch(`${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || 'Failed to delete CVE'); + } + + alert(`Deleted all entries for ${cveId}`); + fetchCVEs(); + fetchVendors(); + } catch (err) { + alert(`Error: ${err.message}`); + } + }; + // Fetch CVEs from API when authenticated useEffect(() => { if (isAuthenticated) { @@ -510,6 +655,147 @@ export default function App() { )} + {/* Edit CVE Modal */} + {showEditCVE && editingCVE && ( +
+
+
+
+

Edit CVE Entry

+ +
+ +
+

+ Note: Changing CVE ID or Vendor will move associated documents to the new path. +

+
+ +
+
+ +
+ { setEditForm({...editForm, cve_id: e.target.value.toUpperCase()}); setEditNvdAutoFilled(false); setEditNvdError(null); }} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + /> + {editNvdLoading && ( + + )} +
+ {editNvdAutoFilled && ( +

+ + Updated from NVD +

+ )} + {editNvdError && ( +

+ + {editNvdError} +

+ )} +
+ +
+ + setEditForm({...editForm, vendor: e.target.value})} + className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent" + /> +
+ +
+ + +
+ +
+ +