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 ==========
|
// ========== DOCUMENT ENDPOINTS ==========
|
||||||
|
|
||||||
// Get documents for a CVE - FILTER BY VENDOR (authenticated users)
|
// 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 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 { useAuth } from './contexts/AuthContext';
|
||||||
import LoginForm from './components/LoginForm';
|
import LoginForm from './components/LoginForm';
|
||||||
import UserMenu from './components/UserMenu';
|
import UserMenu from './components/UserMenu';
|
||||||
@@ -42,6 +42,14 @@ export default function App() {
|
|||||||
const [nvdLoading, setNvdLoading] = useState(false);
|
const [nvdLoading, setNvdLoading] = useState(false);
|
||||||
const [nvdError, setNvdError] = useState(null);
|
const [nvdError, setNvdError] = useState(null);
|
||||||
const [nvdAutoFilled, setNvdAutoFilled] = useState(false);
|
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 lookupNVD = async (cveId) => {
|
||||||
const trimmed = cveId.trim();
|
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
|
// Fetch CVEs from API when authenticated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAuthenticated) {
|
if (isAuthenticated) {
|
||||||
@@ -510,6 +655,147 @@ export default function App() {
|
|||||||
</div>
|
</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 */}
|
{/* 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]">
|
<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>
|
<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>Published: {vendorEntries[0].published_date}</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -713,13 +1008,33 @@ export default function App() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
<button
|
||||||
className="px-4 py-2 text-[#0476D9] hover:bg-blue-50 rounded-lg transition-colors flex items-center gap-2 border border-[#0476D9]"
|
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
|
<Eye className="w-4 h-4" />
|
||||||
</button>
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* Documents Section */}
|
{/* Documents Section */}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ const ACTION_BADGES = {
|
|||||||
user_create: { bg: 'bg-blue-100', text: 'text-blue-800' },
|
user_create: { bg: 'bg-blue-100', text: 'text-blue-800' },
|
||||||
user_update: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
|
user_update: { bg: 'bg-yellow-100', text: 'text-yellow-800' },
|
||||||
user_delete: { bg: 'bg-red-100', text: 'text-red-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' },
|
cve_nvd_sync: { bg: 'bg-green-100', text: 'text-green-800' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user