2026-01-27 04:08:35 +00:00
import React , { useState , useEffect } from 'react' ;
2026-02-02 11:33:44 -07:00
import { Search , FileText , AlertCircle , Download , Upload , Eye , Filter , CheckCircle , XCircle , Loader , Trash2 , Plus , RefreshCw , Edit2 } 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-29 15:10:29 -07:00
import AuditLog from './components/AuditLog' ;
2026-02-02 10:50:38 -07:00
import NvdSyncModal from './components/NvdSyncModal' ;
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-29 15:10:29 -07:00
const [ showAuditLog , setShowAuditLog ] = useState ( false ) ;
2026-02-02 10:50:38 -07:00
const [ showNvdSync , setShowNvdSync ] = 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 ) ;
2026-02-02 10:50:38 -07:00
const [ nvdLoading , setNvdLoading ] = useState ( false ) ;
const [ nvdError , setNvdError ] = useState ( null ) ;
const [ nvdAutoFilled , setNvdAutoFilled ] = useState ( false ) ;
2026-02-02 11:33:44 -07:00
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 ) ;
2026-02-02 10:50:38 -07:00
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 ) ;
}
} ;
2026-01-27 04:08:35 +00:00
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 ]
} ) ;
2026-02-02 10:50:38 -07:00
setNvdLoading ( false ) ;
setNvdError ( null ) ;
setNvdAutoFilled ( false ) ;
2026-01-27 04:08:35 +00:00
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-02-02 11:33:44 -07:00
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 } ` ) ;
}
} ;
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 >
< / d i v >
< / d i v >
) ;
}
// 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 < / h 1 >
< p className = "text-gray-600" > Query vulnerabilities , manage vendors , and attach documentation < / p >
< / d i v >
2026-01-28 14:36:33 -07:00
< div className = "flex items-center gap-4" >
2026-02-02 10:50:38 -07:00
{ canWrite ( ) && (
< button
onClick = { ( ) => setShowNvdSync ( true ) }
className = "px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors flex items-center gap-2 shadow-md"
>
< RefreshCw className = "w-5 h-5" / >
Sync with NVD
< / b u t t o n >
) }
2026-01-28 14:36:33 -07:00
{ 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
< / b u t t o n >
) }
2026-01-29 15:10:29 -07:00
< UserMenu onManageUsers = { ( ) => setShowUserManagement ( true ) } onAuditLog = { ( ) => setShowAuditLog ( true ) } / >
2026-01-28 14:36:33 -07:00
< / d i v >
2026-01-27 04:08:35 +00:00
< / d i v >
2026-01-28 14:36:33 -07:00
{ /* User Management Modal */ }
{ showUserManagement && (
< UserManagement onClose = { ( ) => setShowUserManagement ( false ) } / >
) }
2026-01-29 15:10:29 -07:00
{ /* Audit Log Modal */ }
{ showAuditLog && (
< AuditLog onClose = { ( ) => setShowAuditLog ( false ) } / >
) }
2026-02-02 10:50:38 -07:00
{ /* NVD Sync Modal */ }
{ showNvdSync && (
< NvdSyncModal onClose = { ( ) => setShowNvdSync ( false ) } onSyncComplete = { ( ) => fetchCVEs ( ) } / >
) }
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 < / h 2 >
2026-01-27 04:08:35 +00:00
< button
2026-02-02 10:50:38 -07:00
onClick = { ( ) => { setShowAddCVE ( false ) ; setNvdLoading ( false ) ; setNvdError ( null ) ; setNvdAutoFilled ( false ) ; } }
2026-01-27 04:08:35 +00:00
className = "text-gray-400 hover:text-gray-600"
>
< XCircle className = "w-6 h-6" / >
< / b u t t o n >
< / d i v >
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 : < / s t r o n g > Y o u c a n a d d t h e s a m e C V E - I D m u l t i p l e t i m e s w i t h d i f f e r e n t v e n d o r s .
Each vendor will have its own documents folder .
< / p >
< / d i v >
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 *
< / l a b e l >
2026-02-02 10:50:38 -07:00
< div className = "relative" >
< input
type = "text"
required
placeholder = "CVE-2024-1234"
value = { newCVE . cve _id }
onChange = { ( e ) => { setNewCVE ( { ... newCVE , cve _id : e . target . value . toUpperCase ( ) } ) ; setNvdAutoFilled ( false ) ; setNvdError ( null ) ; } }
onBlur = { ( e ) => lookupNVD ( 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"
/ >
{ nvdLoading && (
< Loader className = "absolute right-3 top-2.5 w-5 h-5 text-[#0476D9] animate-spin" / >
) }
< / d i v >
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-02-02 10:50:38 -07:00
{ nvdAutoFilled && (
< p className = "text-xs text-green-600 mt-1 flex items-center gap-1" >
< CheckCircle className = "w-3 h-3" / >
Auto - filled from NVD
< / p >
) }
{ nvdError && (
< p className = "text-xs text-amber-600 mt-1 flex items-center gap-1" >
< AlertCircle className = "w-3 h-3" / >
{ nvdError }
< / p >
) }
2026-01-27 04:08:35 +00:00
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Vendor *
< / l a b e l >
< 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
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Severity *
< / l a b e l >
< 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 < / o p t i o n >
< option value = "High" > High < / o p t i o n >
< option value = "Medium" > Medium < / o p t i o n >
< option value = "Low" > Low < / o p t i o n >
< / s e l e c t >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Description *
< / l a b e l >
< 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"
/ >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" >
Published Date *
< / l a b e l >
< 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"
/ >
< / d i v >
< 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
< / b u t t o n >
< button
type = "button"
2026-02-02 10:50:38 -07:00
onClick = { ( ) => { setShowAddCVE ( false ) ; setNvdLoading ( false ) ; setNvdError ( null ) ; setNvdAutoFilled ( false ) ; } }
2026-01-27 04:08:35 +00:00
className = "px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors"
>
Cancel
< / b u t t o n >
< / d i v >
< / f o r m >
< / d i v >
< / d i v >
< / d i v >
) }
2026-02-02 11:33:44 -07:00
{ /* 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 < / h 2 >
< button
onClick = { ( ) => { setShowEditCVE ( false ) ; setEditingCVE ( null ) ; } }
className = "text-gray-400 hover:text-gray-600"
>
< XCircle className = "w-6 h-6" / >
< / b u t t o n >
< / d i v >
< div className = "mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg" >
< p className = "text-sm text-amber-800" >
< strong > Note : < / s t r o n g > C h a n g i n g C V E I D o r V e n d o r w i l l m o v e a s s o c i a t e d d o c u m e n t s t o t h e n e w p a t h .
< / p >
< / d i v >
< form onSubmit = { handleEditCVESubmit } className = "space-y-4" >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > CVE ID * < / l a b e l >
< 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" / >
) }
< / d i v >
{ 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 >
) }
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > Vendor * < / l a b e l >
< 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"
/ >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > Severity * < / l a b e l >
< 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 < / o p t i o n >
< option value = "High" > High < / o p t i o n >
< option value = "Medium" > Medium < / o p t i o n >
< option value = "Low" > Low < / o p t i o n >
< / s e l e c t >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > Description * < / l a b e l >
< 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"
/ >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > Published Date * < / l a b e l >
< 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"
/ >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-1" > Status * < / l a b e l >
< 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 < / o p t i o n >
< option value = "Addressed" > Addressed < / o p t i o n >
< option value = "In Progress" > In Progress < / o p t i o n >
< option value = "Resolved" > Resolved < / o p t i o n >
< / s e l e c t >
< / d i v >
< 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
< / b u t t o n >
< 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
< / b u t t o n >
< 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
< / b u t t o n >
< / d i v >
< / f o r m >
< / d i v >
< / d i v >
< / d i v >
) }
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 < / h 2 >
< 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
< / b u t t o n >
< / d i v >
{ 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 >
< / d i v >
< / d i v >
) : 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 : < / s t r o n g > { v e n d o r I n f o . s e v e r i t y } < / p >
< p > < strong > Status : < / s t r o n g > { v e n d o r I n f o . s t a t u s } < / p >
< p > < strong > Documents : < / s t r o n g > { v e n d o r I n f o . t o t a l _ d o c u m e n t s } a t t a c h e d < / p >
< / d i v >
< 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
< / s p a n >
< 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
< / s p a n >
< 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
< / s p a n >
< / d i v >
< / d i v >
) ) }
2026-01-27 04:08:35 +00:00
< / d i v >
< / d i v >
< / d i v >
) : (
< 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 >
< / d i v >
< / d i v >
) }
< / d i v >
) }
< / d i v >
{ /* 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
< / l a b e l >
< 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"
/ >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-2" >
< Filter className = "inline w-4 h-4 mr-1" / >
Vendor
< / l a b e l >
< 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 } < / o p t i o n >
) ) }
< / s e l e c t >
< / d i v >
< div >
< label className = "block text-sm font-medium text-gray-700 mb-2" >
< AlertCircle className = "inline w-4 h-4 mr-1" / >
Severity
< / l a b e l >
< 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 } < / o p t i o n >
) ) }
< / s e l e c t >
< / d i v >
< / d i v >
< / d i v >
{ /* 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
< / b u t t o n >
) }
< / d i v >
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 >
< / d i v >
) : 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 < / h 3 >
< 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
< / b u t t o n >
< / d i v >
) : (
< 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 } < / h 3 >
< 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 } < / s p a n >
< span > • < / s p a n >
< span > { vendorEntries . length } affected vendor { vendorEntries . length > 1 ? 's' : '' } < / s p a n >
2026-02-02 11:33:44 -07:00
{ 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
< / b u t t o n >
) }
2026-01-27 04:08:35 +00:00
< / d i v >
2026-01-27 23:00:12 +00:00
< / d i v >
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 } < / h 4 >
< span className = { ` px-3 py-1 rounded-full text-sm font-medium ${ getSeverityColor ( cve . severity ) } ` } >
{ cve . severity }
< / s p a n >
< 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' }
< / s p a n >
< / d i v >
< div className = "flex items-center gap-4 text-sm text-gray-500" >
< span > Status : < span className = "font-medium text-gray-700" > { cve . status } < / s p a n > < / s p a n >
< span className = "flex items-center gap-1" >
< FileText className = "w-4 h-4" / >
{ cve . document _count } document { cve . document _count !== 1 ? 's' : '' }
< / s p a n >
2026-01-27 04:08:35 +00:00
< / d i v >
2026-01-27 23:00:12 +00:00
< / d i v >
2026-02-02 11:33:44 -07:00
< 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
< / b u t t o n >
{ 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" / >
< / b u t t o n >
) }
{ 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" / >
< / b u t t o n >
) }
< / d i v >
2026-01-27 04:08:35 +00:00
< / d i v >
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 } )
< / h 5 >
{ 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 >
< / d i v >
< / d i v >
< 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
< / b u t t o n >
2026-01-28 14:36:33 -07:00
) }
2026-01-27 23:00:12 +00:00
< / d i v >
< / d i v >
) ) }
< / d i v >
) : (
< 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' }
< / b u t t o n >
) }
2026-01-27 23:00:12 +00:00
< / d i v >
) }
< / d i v >
) ;
} ) }
2026-01-27 04:08:35 +00:00
< / d i v >
< / d i v >
2026-01-27 23:00:12 +00:00
< / d i v >
) ) }
2026-01-27 04:08:35 +00:00
< / d i v >
) }
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 < / h 3 >
< p className = "text-gray-600" > Try adjusting your search criteria or filters < / p >
< / d i v >
) }
< / d i v >
< / d i v >
) ;
}