import React, { useState, useEffect } from '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'; import UserManagement from './components/UserManagement'; import AuditLog from './components/AuditLog'; import NvdSyncModal from './components/NvdSyncModal'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001'; const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low']; export default function App() { const { isAuthenticated, loading: authLoading, canWrite, isAdmin } = useAuth(); const [searchQuery, setSearchQuery] = useState(''); const [selectedVendor, setSelectedVendor] = useState('All Vendors'); const [selectedSeverity, setSelectedSeverity] = useState('All Severities'); const [selectedCVE, setSelectedCVE] = useState(null); const [selectedVendorView, setSelectedVendorView] = useState(null); const [selectedDocuments, setSelectedDocuments] = useState([]); const [cves, setCves] = useState([]); const [vendors, setVendors] = useState(['All Vendors']); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [cveDocuments, setCveDocuments] = useState({}); const [quickCheckCVE, setQuickCheckCVE] = useState(''); const [quickCheckResult, setQuickCheckResult] = useState(null); const [showAddCVE, setShowAddCVE] = useState(false); const [showUserManagement, setShowUserManagement] = useState(false); const [showAuditLog, setShowAuditLog] = useState(false); const [showNvdSync, setShowNvdSync] = useState(false); const [newCVE, setNewCVE] = useState({ cve_id: '', vendor: '', severity: 'Medium', description: '', published_date: new Date().toISOString().split('T')[0] }); const [uploadingFile, setUploadingFile] = useState(false); 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(); if (!/^CVE-\d{4}-\d{4,}$/.test(trimmed)) return; setNvdLoading(true); setNvdError(null); setNvdAutoFilled(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(); setNewCVE(prev => ({ ...prev, description: prev.description || data.description, severity: data.severity, published_date: data.published_date || prev.published_date })); setNvdAutoFilled(true); } catch (err) { setNvdError(err.message); } finally { setNvdLoading(false); } }; const fetchCVEs = async () => { setLoading(true); setError(null); try { const params = new URLSearchParams(); if (searchQuery) params.append('search', searchQuery); if (selectedVendor !== 'All Vendors') params.append('vendor', selectedVendor); if (selectedSeverity !== 'All Severities') params.append('severity', selectedSeverity); const response = await fetch(`${API_BASE}/cves?${params}`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch CVEs'); const data = await response.json(); setCves(data); } catch (err) { setError(err.message); console.error('Error fetching CVEs:', err); } finally { setLoading(false); } }; const fetchVendors = async () => { try { const response = await fetch(`${API_BASE}/vendors`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch vendors'); const data = await response.json(); setVendors(['All Vendors', ...data]); } catch (err) { console.error('Error fetching vendors:', err); } }; const fetchDocuments = async (cveId, vendor) => { const key = `${cveId}-${vendor}`; if (cveDocuments[key]) return; try { const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch documents'); const data = await response.json(); setCveDocuments(prev => ({ ...prev, [key]: data })); } catch (err) { console.error('Error fetching documents:', err); } }; const quickCheckCVEStatus = async () => { if (!quickCheckCVE.trim()) return; try { const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to check CVE'); const data = await response.json(); setQuickCheckResult(data); } catch (err) { console.error('Error checking CVE:', err); setQuickCheckResult({ error: err.message }); } }; const handleViewDocuments = async (cveId, vendor) => { if (selectedCVE === cveId && selectedVendorView === vendor) { setSelectedCVE(null); setSelectedVendorView(null); } else { setSelectedCVE(cveId); setSelectedVendorView(vendor); await fetchDocuments(cveId, vendor); } }; const getSeverityColor = (severity) => { const colors = { 'Critical': 'bg-red-100 text-red-800', 'High': 'bg-orange-100 text-orange-800', 'Medium': 'bg-yellow-100 text-yellow-800', 'Low': 'bg-blue-100 text-blue-800' }; return colors[severity] || 'bg-gray-100 text-gray-800'; }; const toggleDocumentSelection = (docId) => { setSelectedDocuments(prev => prev.includes(docId) ? prev.filter(id => id !== docId) : [...prev, docId] ); }; const exportSelectedDocuments = () => { alert(`Exporting ${selectedDocuments.length} documents for report attachment`); }; const handleAddCVE = async (e) => { e.preventDefault(); try { const response = await fetch(`${API_BASE}/cves`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(newCVE) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to add CVE'); } alert(`CVE ${newCVE.cve_id} added successfully for vendor: ${newCVE.vendor}!`); setShowAddCVE(false); setNewCVE({ cve_id: '', vendor: '', severity: 'Medium', description: '', published_date: new Date().toISOString().split('T')[0] }); setNvdLoading(false); setNvdError(null); setNvdAutoFilled(false); fetchCVEs(); } catch (err) { alert(`Error: ${err.message}`); } }; const handleFileUpload = async (cveId, vendor) => { const fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = '.pdf,.png,.jpg,.jpeg,.gif,.bmp,.tiff,.tif,.txt,.csv,.log,.msg,.eml,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.odt,.ods,.odp,.rtf,.html,.htm,.xml,.json,.yaml,.yml,.zip,.gz,.tar,.7z'; fileInput.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const docType = prompt( 'Document type (advisory, email, screenshot, patch, other):', 'advisory' ); if (!docType) return; const notes = prompt('Notes (optional):'); setUploadingFile(true); const formData = new FormData(); formData.append('file', file); formData.append('cveId', cveId); formData.append('vendor', vendor); formData.append('type', docType); if (notes) formData.append('notes', notes); try { const response = await fetch(`${API_BASE}/cves/${cveId}/documents`, { method: 'POST', credentials: 'include', body: formData }); if (!response.ok) throw new Error('Failed to upload document'); alert(`Document uploaded successfully!`); const key = `${cveId}-${vendor}`; delete cveDocuments[key]; await fetchDocuments(cveId, vendor); fetchCVEs(); } catch (err) { alert(`Error: ${err.message}`); } finally { setUploadingFile(false); } }; fileInput.click(); }; const handleDeleteDocument = async (docId, cveId, vendor) => { if (!window.confirm('Are you sure you want to delete this document?')) { return; } try { const response = await fetch(`${API_BASE}/documents/${docId}`, { method: 'DELETE', credentials: 'include' }); if (!response.ok) throw new Error('Failed to delete document'); alert('Document deleted successfully!'); const key = `${cveId}-${vendor}`; delete cveDocuments[key]; await fetchDocuments(cveId, vendor); fetchCVEs(); } catch (err) { alert(`Error: ${err.message}`); } }; 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) { fetchCVEs(); fetchVendors(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated]); // Refetch when filters change useEffect(() => { if (isAuthenticated) { fetchCVEs(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery, selectedVendor, selectedSeverity]); // Show loading while checking auth if (authLoading) { return (
Loading...
Query vulnerabilities, manage vendors, and attach documentation
Tip: You can add the same CVE-ID multiple times with different vendors. Each vendor will have its own documents folder.
Note: Changing CVE ID or Vendor will move associated documents to the new path.
Error
{quickCheckResult.error}
✓ CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})
{vendorInfo.vendor}
Severity: {vendorInfo.severity}
Status: {vendorInfo.status}
Documents: {vendorInfo.total_documents} attached
Not Found
This CVE has not been addressed yet. No entry exists in the database.
Found {Object.keys(filteredGroupedCVEs).length} CVE{Object.keys(filteredGroupedCVEs).length !== 1 ? 's' : ''} ({cves.length} vendor entr{cves.length !== 1 ? 'ies' : 'y'})
{selectedDocuments.length > 0 && ( )}Loading CVEs...
{error}
{vendorEntries[0].description}
{doc.name}
{doc.type} • {doc.file_size} {doc.notes && ` • ${doc.notes}`}
No documents attached yet
)} {canWrite() && ( )}Try adjusting your search criteria or filters