2026-01-27 04:08:35 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-01-27 23:00:12 +00:00
|
|
|
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus } from 'lucide-react';
|
2026-01-28 14:36:33 -07:00
|
|
|
import { useAuth } from './contexts/AuthContext';
|
|
|
|
|
import LoginForm from './components/LoginForm';
|
|
|
|
|
import UserMenu from './components/UserMenu';
|
|
|
|
|
import UserManagement from './components/UserManagement';
|
2026-01-27 04:08:35 +00:00
|
|
|
|
2026-01-28 09:23:30 -07:00
|
|
|
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';
|
2026-01-27 04:08:35 +00:00
|
|
|
|
|
|
|
|
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
|
|
|
|
|
|
|
|
|
export default function App() {
|
2026-01-28 14:36:33 -07:00
|
|
|
const { isAuthenticated, loading: authLoading, canWrite, isAdmin } = useAuth();
|
2026-01-27 04:08:35 +00:00
|
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
|
|
|
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
|
|
|
|
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
|
|
|
|
const [selectedCVE, setSelectedCVE] = useState(null);
|
2026-01-27 23:00:12 +00:00
|
|
|
const [selectedVendorView, setSelectedVendorView] = useState(null);
|
2026-01-27 04:08:35 +00:00
|
|
|
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);
|
2026-01-28 14:36:33 -07:00
|
|
|
const [showUserManagement, setShowUserManagement] = useState(false);
|
2026-01-27 04:08:35 +00:00
|
|
|
const [newCVE, setNewCVE] = useState({
|
|
|
|
|
cve_id: '',
|
|
|
|
|
vendor: '',
|
|
|
|
|
severity: 'Medium',
|
|
|
|
|
description: '',
|
|
|
|
|
published_date: new Date().toISOString().split('T')[0]
|
|
|
|
|
});
|
|
|
|
|
const [uploadingFile, setUploadingFile] = useState(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);
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
const response = await fetch(`${API_BASE}/cves?${params}`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
2026-01-27 04:08:35 +00:00
|
|
|
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 {
|
2026-01-28 14:36:33 -07:00
|
|
|
const response = await fetch(`${API_BASE}/vendors`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
2026-01-27 04:08:35 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
const fetchDocuments = async (cveId, vendor) => {
|
|
|
|
|
const key = `${cveId}-${vendor}`;
|
|
|
|
|
if (cveDocuments[key]) return;
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
try {
|
2026-01-28 14:36:33 -07:00
|
|
|
const response = await fetch(`${API_BASE}/cves/${cveId}/documents?vendor=${vendor}`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
2026-01-27 04:08:35 +00:00
|
|
|
if (!response.ok) throw new Error('Failed to fetch documents');
|
|
|
|
|
const data = await response.json();
|
2026-01-27 23:00:12 +00:00
|
|
|
setCveDocuments(prev => ({ ...prev, [key]: data }));
|
2026-01-27 04:08:35 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Error fetching documents:', err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const quickCheckCVEStatus = async () => {
|
|
|
|
|
if (!quickCheckCVE.trim()) return;
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
try {
|
2026-01-28 14:36:33 -07:00
|
|
|
const response = await fetch(`${API_BASE}/cves/check/${quickCheckCVE.trim()}`, {
|
|
|
|
|
credentials: 'include'
|
|
|
|
|
});
|
2026-01-27 04:08:35 +00:00
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
const handleViewDocuments = async (cveId, vendor) => {
|
|
|
|
|
if (selectedCVE === cveId && selectedVendorView === vendor) {
|
2026-01-27 04:08:35 +00:00
|
|
|
setSelectedCVE(null);
|
2026-01-27 23:00:12 +00:00
|
|
|
setSelectedVendorView(null);
|
2026-01-27 04:08:35 +00:00
|
|
|
} else {
|
|
|
|
|
setSelectedCVE(cveId);
|
2026-01-27 23:00:12 +00:00
|
|
|
setSelectedVendorView(vendor);
|
|
|
|
|
await fetchDocuments(cveId, vendor);
|
2026-01-27 04:08:35 +00:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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' },
|
2026-01-28 14:36:33 -07:00
|
|
|
credentials: 'include',
|
2026-01-27 04:08:35 +00:00
|
|
|
body: JSON.stringify(newCVE)
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
if (!response.ok) {
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
throw new Error(data.error || 'Failed to add CVE');
|
|
|
|
|
}
|
2026-01-27 04:08:35 +00:00
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
alert(`CVE ${newCVE.cve_id} added successfully for vendor: ${newCVE.vendor}!`);
|
2026-01-27 04:08:35 +00:00
|
|
|
setShowAddCVE(false);
|
|
|
|
|
setNewCVE({
|
|
|
|
|
cve_id: '',
|
|
|
|
|
vendor: '',
|
|
|
|
|
severity: 'Medium',
|
|
|
|
|
description: '',
|
|
|
|
|
published_date: new Date().toISOString().split('T')[0]
|
|
|
|
|
});
|
|
|
|
|
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,.txt,.doc,.docx';
|
|
|
|
|
|
|
|
|
|
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',
|
2026-01-28 14:36:33 -07:00
|
|
|
credentials: 'include',
|
2026-01-27 04:08:35 +00:00
|
|
|
body: formData
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) throw new Error('Failed to upload document');
|
|
|
|
|
|
|
|
|
|
alert(`Document uploaded successfully!`);
|
2026-01-27 23:00:12 +00:00
|
|
|
const key = `${cveId}-${vendor}`;
|
|
|
|
|
delete cveDocuments[key];
|
|
|
|
|
await fetchDocuments(cveId, vendor);
|
2026-01-27 04:08:35 +00:00
|
|
|
fetchCVEs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
} finally {
|
|
|
|
|
setUploadingFile(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
fileInput.click();
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
const handleDeleteDocument = async (docId, cveId, vendor) => {
|
2026-01-27 04:08:35 +00:00
|
|
|
if (!window.confirm('Are you sure you want to delete this document?')) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
try {
|
|
|
|
|
const response = await fetch(`${API_BASE}/documents/${docId}`, {
|
2026-01-28 14:36:33 -07:00
|
|
|
method: 'DELETE',
|
|
|
|
|
credentials: 'include'
|
2026-01-27 04:08:35 +00:00
|
|
|
});
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
if (!response.ok) throw new Error('Failed to delete document');
|
2026-01-28 14:36:33 -07:00
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
alert('Document deleted successfully!');
|
2026-01-27 23:00:12 +00:00
|
|
|
const key = `${cveId}-${vendor}`;
|
|
|
|
|
delete cveDocuments[key];
|
|
|
|
|
await fetchDocuments(cveId, vendor);
|
2026-01-27 04:08:35 +00:00
|
|
|
fetchCVEs();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
alert(`Error: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
// 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 (
|
|
|
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<Loader className="w-12 h-12 text-[#0476D9] mx-auto animate-spin" />
|
|
|
|
|
<p className="text-gray-600 mt-4">Loading...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show login if not authenticated
|
|
|
|
|
if (!isAuthenticated) {
|
|
|
|
|
return <LoginForm />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
// Group CVEs by CVE ID
|
|
|
|
|
const groupedCVEs = cves.reduce((acc, cve) => {
|
|
|
|
|
if (!acc[cve.cve_id]) {
|
|
|
|
|
acc[cve.cve_id] = [];
|
|
|
|
|
}
|
|
|
|
|
acc[cve.cve_id].push(cve);
|
|
|
|
|
return acc;
|
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
|
|
const filteredGroupedCVEs = groupedCVEs;
|
2026-01-27 04:08:35 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-gray-100 p-6">
|
|
|
|
|
<div className="max-w-7xl mx-auto">
|
2026-01-27 23:00:12 +00:00
|
|
|
{/* Header */}
|
2026-01-27 04:08:35 +00:00
|
|
|
<div className="mb-8 flex justify-between items-center">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">CVE Dashboard</h1>
|
|
|
|
|
<p className="text-gray-600">Query vulnerabilities, manage vendors, and attach documentation</p>
|
|
|
|
|
</div>
|
2026-01-28 14:36:33 -07:00
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
{canWrite() && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setShowAddCVE(true)}
|
|
|
|
|
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors flex items-center gap-2 shadow-md"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-5 h-5" />
|
|
|
|
|
Add CVE/Vendor
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
<UserMenu onManageUsers={() => setShowUserManagement(true)} />
|
|
|
|
|
</div>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
|
2026-01-28 14:36:33 -07:00
|
|
|
{/* User Management Modal */}
|
|
|
|
|
{showUserManagement && (
|
|
|
|
|
<UserManagement onClose={() => setShowUserManagement(false)} />
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
{/* Add CVE Modal */}
|
|
|
|
|
{showAddCVE && (
|
|
|
|
|
<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">
|
2026-01-27 23:00:12 +00:00
|
|
|
<h2 className="text-2xl font-bold text-gray-900">Add CVE Entry</h2>
|
2026-01-27 04:08:35 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => setShowAddCVE(false)}
|
|
|
|
|
className="text-gray-400 hover:text-gray-600"
|
|
|
|
|
>
|
|
|
|
|
<XCircle className="w-6 h-6" />
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
<div className="mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
|
|
|
<p className="text-sm text-blue-800">
|
|
|
|
|
<strong>Tip:</strong> You can add the same CVE-ID multiple times with different vendors.
|
|
|
|
|
Each vendor will have its own documents folder.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-27 04:08:35 +00:00
|
|
|
<form onSubmit={handleAddCVE} className="space-y-4">
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
CVE ID *
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
required
|
|
|
|
|
placeholder="CVE-2024-1234"
|
|
|
|
|
value={newCVE.cve_id}
|
|
|
|
|
onChange={(e) => setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()})}
|
|
|
|
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
|
|
|
|
/>
|
2026-01-27 23:00:12 +00:00
|
|
|
<p className="text-xs text-gray-500 mt-1">Can be the same as existing CVE if adding another vendor</p>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
Vendor *
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
required
|
|
|
|
|
placeholder="Microsoft, Cisco, Oracle, etc."
|
|
|
|
|
value={newCVE.vendor}
|
|
|
|
|
onChange={(e) => setNewCVE({...newCVE, 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"
|
|
|
|
|
/>
|
2026-01-27 23:00:12 +00:00
|
|
|
<p className="text-xs text-gray-500 mt-1">Must be unique for this CVE-ID</p>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
|
|
|
Severity *
|
|
|
|
|
</label>
|
|
|
|
|
<select
|
|
|
|
|
value={newCVE.severity}
|
|
|
|
|
onChange={(e) => setNewCVE({...newCVE, 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
|
|
|
|
|
placeholder="Brief description of the vulnerability"
|
|
|
|
|
value={newCVE.description}
|
|
|
|
|
onChange={(e) => setNewCVE({...newCVE, 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={newCVE.published_date}
|
|
|
|
|
onChange={(e) => setNewCVE({...newCVE, 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 className="flex gap-3 pt-4">
|
|
|
|
|
<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"
|
|
|
|
|
>
|
2026-01-27 23:00:12 +00:00
|
|
|
Add CVE Entry
|
2026-01-27 04:08:35 +00:00
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={() => setShowAddCVE(false)}
|
|
|
|
|
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>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
{/* Quick Check */}
|
2026-01-27 04:08:35 +00:00
|
|
|
<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>
|
|
|
|
|
<div className="flex gap-3">
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Enter CVE ID (e.g., CVE-2024-1234)"
|
|
|
|
|
value={quickCheckCVE}
|
|
|
|
|
onChange={(e) => setQuickCheckCVE(e.target.value)}
|
|
|
|
|
onKeyPress={(e) => e.key === 'Enter' && quickCheckCVEStatus()}
|
|
|
|
|
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0476D9] focus:border-transparent"
|
|
|
|
|
/>
|
|
|
|
|
<button
|
|
|
|
|
onClick={quickCheckCVEStatus}
|
|
|
|
|
className="px-6 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors font-medium shadow-md"
|
|
|
|
|
>
|
|
|
|
|
Check Status
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{quickCheckResult && (
|
|
|
|
|
<div className={`mt-4 p-4 rounded-lg ${quickCheckResult.exists ? 'bg-green-50 border border-green-200' : 'bg-yellow-50 border border-yellow-200'}`}>
|
|
|
|
|
{quickCheckResult.error ? (
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
<XCircle className="w-5 h-5 text-red-600 mt-0.5" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-red-900">Error</p>
|
|
|
|
|
<p className="text-sm text-red-700">{quickCheckResult.error}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : quickCheckResult.exists ? (
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5" />
|
|
|
|
|
<div className="flex-1">
|
2026-01-27 23:00:12 +00:00
|
|
|
<p className="font-medium text-green-900">✓ CVE Addressed ({quickCheckResult.vendors.length} vendor{quickCheckResult.vendors.length > 1 ? 's' : ''})</p>
|
|
|
|
|
<div className="mt-3 space-y-3">
|
|
|
|
|
{quickCheckResult.vendors.map((vendorInfo, idx) => (
|
|
|
|
|
<div key={idx} className="p-3 bg-white rounded border border-green-200">
|
|
|
|
|
<p className="font-semibold text-gray-900 mb-2">{vendorInfo.vendor}</p>
|
|
|
|
|
<div className="grid grid-cols-2 gap-2 text-sm text-gray-700 mb-2">
|
|
|
|
|
<p><strong>Severity:</strong> {vendorInfo.severity}</p>
|
|
|
|
|
<p><strong>Status:</strong> {vendorInfo.status}</p>
|
|
|
|
|
<p><strong>Documents:</strong> {vendorInfo.total_documents} attached</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
|
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.advisory ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}`}>
|
|
|
|
|
{vendorInfo.compliance.advisory ? '✓' : '✗'} Advisory
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.email ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
|
|
|
|
{vendorInfo.compliance.email ? '✓' : '○'} Email
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${vendorInfo.compliance.screenshot ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-600'}`}>
|
|
|
|
|
{vendorInfo.compliance.screenshot ? '✓' : '○'} Screenshot
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
<AlertCircle className="w-5 h-5 text-yellow-600 mt-0.5" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium text-yellow-900">Not Found</p>
|
|
|
|
|
<p className="text-sm text-yellow-700">This CVE has not been addressed yet. No entry exists in the database.</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search and Filters */}
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
|
<div className="md:col-span-1">
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
<Search className="inline w-4 h-4 mr-1" />
|
|
|
|
|
Search CVEs
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="CVE ID or description..."
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={(e) => setSearchQuery(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-2">
|
|
|
|
|
<Filter className="inline w-4 h-4 mr-1" />
|
|
|
|
|
Vendor
|
|
|
|
|
</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedVendor}
|
|
|
|
|
onChange={(e) => setSelectedVendor(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"
|
|
|
|
|
>
|
|
|
|
|
{vendors.map(vendor => (
|
|
|
|
|
<option key={vendor} value={vendor}>{vendor}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
|
|
|
<AlertCircle className="inline w-4 h-4 mr-1" />
|
|
|
|
|
Severity
|
|
|
|
|
</label>
|
|
|
|
|
<select
|
|
|
|
|
value={selectedSeverity}
|
|
|
|
|
onChange={(e) => setSelectedSeverity(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"
|
|
|
|
|
>
|
|
|
|
|
{severityLevels.map(level => (
|
|
|
|
|
<option key={level} value={level}>{level}</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Results Summary */}
|
|
|
|
|
<div className="mb-4 flex justify-between items-center">
|
|
|
|
|
<p className="text-gray-600">
|
2026-01-27 23:00:12 +00:00
|
|
|
Found {Object.keys(filteredGroupedCVEs).length} CVE{Object.keys(filteredGroupedCVEs).length !== 1 ? 's' : ''}
|
|
|
|
|
({cves.length} vendor entr{cves.length !== 1 ? 'ies' : 'y'})
|
2026-01-27 04:08:35 +00:00
|
|
|
</p>
|
|
|
|
|
{selectedDocuments.length > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={exportSelectedDocuments}
|
|
|
|
|
className="flex items-center gap-2 px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] transition-colors shadow-md"
|
|
|
|
|
>
|
|
|
|
|
<Download className="w-4 h-4" />
|
|
|
|
|
Export {selectedDocuments.length} Document{selectedDocuments.length !== 1 ? 's' : ''} for Report
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
{/* CVE List - Grouped by CVE ID */}
|
2026-01-27 04:08:35 +00:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
|
|
|
|
<Loader className="w-12 h-12 text-[#0476D9] mx-auto mb-4 animate-spin" />
|
|
|
|
|
<p className="text-gray-600">Loading CVEs...</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : error ? (
|
|
|
|
|
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
|
|
|
|
<XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">Error Loading CVEs</h3>
|
|
|
|
|
<p className="text-gray-600 mb-4">{error}</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={fetchCVEs}
|
|
|
|
|
className="px-4 py-2 bg-[#0476D9] text-white rounded-lg hover:bg-[#0360B8] shadow-md"
|
|
|
|
|
>
|
|
|
|
|
Retry
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-4">
|
2026-01-27 23:00:12 +00:00
|
|
|
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => (
|
|
|
|
|
<div key={cveId} className="bg-white rounded-lg shadow-md border-2 border-gray-200">
|
|
|
|
|
<div className="p-6">
|
|
|
|
|
{/* CVE Header */}
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
<h3 className="text-2xl font-bold text-gray-900 mb-2">{cveId}</h3>
|
|
|
|
|
<p className="text-gray-600 mb-3">{vendorEntries[0].description}</p>
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-gray-500">
|
|
|
|
|
<span>Published: {vendorEntries[0].published_date}</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
2026-01-27 23:00:12 +00:00
|
|
|
</div>
|
2026-01-27 04:08:35 +00:00
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
{/* Vendor Entries */}
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{vendorEntries.map((cve) => {
|
|
|
|
|
const key = `${cve.cve_id}-${cve.vendor}`;
|
|
|
|
|
const documents = cveDocuments[key] || [];
|
|
|
|
|
const isExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div key={cve.id} className="border border-gray-200 rounded-lg p-4 bg-gray-50">
|
|
|
|
|
<div className="flex justify-between items-start">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="flex items-center gap-3 mb-2">
|
|
|
|
|
<h4 className="text-lg font-semibold text-gray-900">{cve.vendor}</h4>
|
|
|
|
|
<span className={`px-3 py-1 rounded-full text-sm font-medium ${getSeverityColor(cve.severity)}`}>
|
|
|
|
|
{cve.severity}
|
|
|
|
|
</span>
|
|
|
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${cve.doc_status === 'Complete' ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}`}>
|
|
|
|
|
{cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4 text-sm text-gray-500">
|
|
|
|
|
<span>Status: <span className="font-medium text-gray-700">{cve.status}</span></span>
|
|
|
|
|
<span className="flex items-center gap-1">
|
|
|
|
|
<FileText className="w-4 h-4" />
|
|
|
|
|
{cve.document_count} document{cve.document_count !== 1 ? 's' : ''}
|
|
|
|
|
</span>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
2026-01-27 23:00:12 +00:00
|
|
|
</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>
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
2026-01-27 23:00:12 +00:00
|
|
|
|
|
|
|
|
{/* Documents Section */}
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="mt-4 pt-4 border-t border-gray-300">
|
|
|
|
|
<h5 className="text-sm font-semibold text-gray-700 mb-3 flex items-center gap-2">
|
|
|
|
|
<FileText className="w-4 h-4" />
|
|
|
|
|
Documents for {cve.vendor} ({documents.length})
|
|
|
|
|
</h5>
|
|
|
|
|
{documents.length > 0 ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{documents.map(doc => (
|
|
|
|
|
<div
|
|
|
|
|
key={doc.id}
|
|
|
|
|
className="flex items-center justify-between p-3 bg-white rounded-lg hover:bg-gray-50 transition-colors border border-gray-200"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center gap-3 flex-1">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked={selectedDocuments.includes(doc.id)}
|
|
|
|
|
onChange={() => toggleDocumentSelection(doc.id)}
|
|
|
|
|
className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]"
|
|
|
|
|
/>
|
|
|
|
|
<FileText className="w-5 h-5 text-gray-400" />
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<p className="text-sm font-medium text-gray-900">{doc.name}</p>
|
|
|
|
|
<p className="text-xs text-gray-500 capitalize">
|
|
|
|
|
{doc.type} • {doc.file_size}
|
|
|
|
|
{doc.notes && ` • ${doc.notes}`}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex gap-2">
|
2026-01-28 09:23:30 -07:00
|
|
|
<a
|
|
|
|
|
href={`${API_HOST}/${doc.file_path}`}
|
2026-01-27 23:00:12 +00:00
|
|
|
target="_blank"
|
|
|
|
|
rel="noopener noreferrer"
|
|
|
|
|
className="px-3 py-1 text-sm text-[#0476D9] hover:bg-blue-50 rounded transition-colors border border-[#0476D9]"
|
|
|
|
|
>
|
|
|
|
|
View
|
|
|
|
|
</a>
|
2026-01-28 14:36:33 -07:00
|
|
|
{isAdmin() && (
|
2026-01-27 23:00:12 +00:00
|
|
|
<button
|
|
|
|
|
onClick={() => handleDeleteDocument(doc.id, cve.cve_id, cve.vendor)}
|
|
|
|
|
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors border border-red-600 flex items-center gap-1"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="w-3 h-3" />
|
|
|
|
|
Delete
|
|
|
|
|
</button>
|
2026-01-28 14:36:33 -07:00
|
|
|
)}
|
2026-01-27 23:00:12 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-gray-500 italic">No documents attached yet</p>
|
|
|
|
|
)}
|
2026-01-28 14:36:33 -07:00
|
|
|
{canWrite() && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
|
|
|
|
|
disabled={uploadingFile}
|
|
|
|
|
className="mt-3 px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors flex items-center gap-2 disabled:opacity-50 border border-gray-300"
|
|
|
|
|
>
|
|
|
|
|
<Upload className="w-4 h-4" />
|
|
|
|
|
{uploadingFile ? 'Uploading...' : 'Upload Document'}
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
2026-01-27 23:00:12 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-01-27 23:00:12 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
2026-01-27 04:08:35 +00:00
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-27 23:00:12 +00:00
|
|
|
{Object.keys(filteredGroupedCVEs).length === 0 && !loading && (
|
2026-01-27 04:08:35 +00:00
|
|
|
<div className="bg-white rounded-lg shadow-md p-12 text-center">
|
|
|
|
|
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-gray-900 mb-2">No CVEs Found</h3>
|
|
|
|
|
<p className="text-gray-600">Try adjusting your search criteria or filters</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|