Added tweaks to allow edits/deletes of cve and vendors or to fix typos
This commit is contained in:
@@ -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)
|
||||
|
||||
0
frontend/cve_database.db
Normal file
0
frontend/cve_database.db
Normal file
@@ -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() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit CVE Modal */}
|
||||
{showEditCVE && editingCVE && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-2xl font-bold text-gray-900">Edit CVE Entry</h2>
|
||||
<button
|
||||
onClick={() => { setShowEditCVE(false); setEditingCVE(null); }}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<XCircle className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg">
|
||||
<p className="text-sm text-amber-800">
|
||||
<strong>Note:</strong> Changing CVE ID or Vendor will move associated documents to the new path.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleEditCVESubmit} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">CVE ID *</label>
|
||||
<div className="relative">
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editForm.cve_id}
|
||||
onChange={(e) => { 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 && (
|
||||
<Loader className="absolute right-3 top-2.5 w-5 h-5 text-[#0476D9] animate-spin" />
|
||||
)}
|
||||
</div>
|
||||
{editNvdAutoFilled && (
|
||||
<p className="text-xs text-green-600 mt-1 flex items-center gap-1">
|
||||
<CheckCircle className="w-3 h-3" />
|
||||
Updated from NVD
|
||||
</p>
|
||||
)}
|
||||
{editNvdError && (
|
||||
<p className="text-xs text-amber-600 mt-1 flex items-center gap-1">
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{editNvdError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Vendor *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={editForm.vendor}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Severity *</label>
|
||||
<select
|
||||
value={editForm.severity}
|
||||
onChange={(e) => setEditForm({...editForm, severity: 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"
|
||||
>
|
||||
<option value="Critical">Critical</option>
|
||||
<option value="High">High</option>
|
||||
<option value="Medium">Medium</option>
|
||||
<option value="Low">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Description *</label>
|
||||
<textarea
|
||||
required
|
||||
value={editForm.description}
|
||||
onChange={(e) => setEditForm({...editForm, description: e.target.value})}
|
||||
rows={3}
|
||||
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Published Date *</label>
|
||||
<input
|
||||
type="date"
|
||||
required
|
||||
value={editForm.published_date}
|
||||
onChange={(e) => setEditForm({...editForm, published_date: 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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Status *</label>
|
||||
<select
|
||||
value={editForm.status}
|
||||
onChange={(e) => setEditForm({...editForm, status: 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"
|
||||
>
|
||||
<option value="Open">Open</option>
|
||||
<option value="Addressed">Addressed</option>
|
||||
<option value="In Progress">In Progress</option>
|
||||
<option value="Resolved">Resolved</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => lookupNVDForEdit(editForm.cve_id)}
|
||||
disabled={editNvdLoading}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors font-medium flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${editNvdLoading ? 'animate-spin' : ''}`} />
|
||||
Update from NVD
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="flex-1 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setShowEditCVE(false); setEditingCVE(null); }}
|
||||
className="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Check */}
|
||||
<div className="bg-gradient-to-r from-blue-50 to-blue-100 rounded-lg shadow-md p-6 mb-6 border-2 border-[#0476D9]">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Quick CVE Status Check</h2>
|
||||
@@ -682,6 +968,15 @@ export default function App() {
|
||||
<span>Published: {vendorEntries[0].published_date}</span>
|
||||
<span>•</span>
|
||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
||||
{canWrite() && vendorEntries.length >= 2 && (
|
||||
<button
|
||||
onClick={() => handleDeleteEntireCVE(cveId, vendorEntries.length)}
|
||||
className="ml-2 px-3 py-1 text-xs text-red-600 hover:bg-red-50 rounded transition-colors border border-red-300 flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
Delete All Vendors
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -713,13 +1008,33 @@ export default function App() {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{isExpanded ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
{isExpanded ? 'Hide' : 'View'} Documents
|
||||
</button>
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleEditCVE(cve)}
|
||||
className="px-3 py-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors flex items-center gap-1 border border-orange-300"
|
||||
title="Edit CVE entry"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{canWrite() && (
|
||||
<button
|
||||
onClick={() => handleDeleteCVEEntry(cve)}
|
||||
className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors flex items-center gap-1 border border-red-300"
|
||||
title="Delete this vendor entry"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Documents Section */}
|
||||
|
||||
@@ -14,6 +14,8 @@ const ACTION_BADGES = {
|
||||
user_create: { bg: 'bg-blue-100', text: 'text-blue-800' },
|
||||
user_update: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
|
||||
user_delete: { bg: 'bg-red-100', text: 'text-red-800' },
|
||||
cve_edit: { bg: 'bg-orange-100', text: 'text-orange-800' },
|
||||
cve_delete: { bg: 'bg-red-100', text: 'text-red-800' },
|
||||
cve_nvd_sync: { bg: 'bg-green-100', text: 'text-green-800' },
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user