Added tweaks to allow edits/deletes of cve and vendors or to fix typos
This commit is contained in:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user