import React, { useState, useEffect } from 'react'; import { Search, FileText, AlertCircle, AlertTriangle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown, Shield, Activity, Menu } 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'; import NavDrawer from './components/NavDrawer'; import AdminScopeToggle from './components/AdminScopeToggle'; import CalendarWidget from './components/CalendarWidget'; import ConfirmModal from './components/ConfirmModal'; import VulnerabilityTriagePage from './components/pages/ReportingPage'; import KnowledgeBasePage from './components/pages/KnowledgeBasePage'; import ExportsPage from './components/pages/ExportsPage'; import CompliancePage from './components/pages/CompliancePage'; import CCPMetricsPage from './components/pages/CCPMetricsPage'; import JiraPage from './components/pages/JiraPage'; import AdminPage from './components/pages/AdminPage'; import ArchiveSummaryBar from './components/pages/ArchiveSummaryBar'; import FeedbackModal from './components/FeedbackModal'; import NotificationBell from './components/NotificationBell'; import './App.css'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; // ============================================ // INLINE STYLES - NUCLEAR OPTION FOR VISIBILITY // ============================================ const STYLES = { // Main container with visible background mainContainer: { minHeight: '100vh', background: 'linear-gradient(135deg, #0F172A 0%, #1E293B 50%, #0F172A 100%)', padding: '1.5rem', position: 'relative', overflow: 'hidden', }, // Stat cards with refined borders statCard: { background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 100%)', border: '2px solid #0EA5E9', borderRadius: '0.5rem', padding: '1rem', boxShadow: '0 4px 16px rgba(0, 0, 0, 0.5), 0 0 20px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.15)', position: 'relative', overflow: 'hidden', }, // Intel card with refined glowing border intelCard: { background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)', border: '2px solid rgba(14, 165, 233, 0.4)', borderRadius: '0.5rem', boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15), inset 0 1px 0 rgba(14, 165, 233, 0.12)', position: 'relative', overflow: 'hidden', }, // Vendor card with depth vendorCard: { background: 'linear-gradient(135deg, rgba(15, 23, 42, 0.95) 0%, rgba(30, 41, 59, 0.9) 100%)', border: '1.5px solid rgba(14, 165, 233, 0.3)', borderRadius: '0.5rem', padding: '1rem', boxShadow: '0 4px 12px rgba(0, 0, 0, 0.4), inset 0 1px 0 rgba(14, 165, 233, 0.08)', marginBottom: '0.75rem', }, // CRITICAL severity badge - Modern red with refined glow badgeCritical: { display: 'inline-flex', alignItems: 'center', gap: '0.5rem', background: 'linear-gradient(135deg, rgba(239, 68, 68, 0.25) 0%, rgba(239, 68, 68, 0.2) 100%)', border: '2px solid #EF4444', borderRadius: '0.375rem', padding: '0.375rem 0.875rem', color: '#FCA5A5', fontWeight: '700', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.5px', textShadow: '0 0 8px rgba(239, 68, 68, 0.5)', boxShadow: '0 0 16px rgba(239, 68, 68, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)', }, // HIGH severity badge - Amber with refined glow badgeHigh: { display: 'inline-flex', alignItems: 'center', gap: '0.5rem', background: 'linear-gradient(135deg, rgba(245, 158, 11, 0.25) 0%, rgba(245, 158, 11, 0.2) 100%)', border: '2px solid #F59E0B', borderRadius: '0.375rem', padding: '0.375rem 0.875rem', color: '#FCD34D', fontWeight: '700', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.5px', textShadow: '0 0 8px rgba(245, 158, 11, 0.5)', boxShadow: '0 0 16px rgba(245, 158, 11, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)', }, // MEDIUM severity badge - Sky blue with refined glow badgeMedium: { display: 'inline-flex', alignItems: 'center', gap: '0.5rem', background: 'linear-gradient(135deg, rgba(14, 165, 233, 0.25) 0%, rgba(14, 165, 233, 0.2) 100%)', border: '2px solid #0EA5E9', borderRadius: '0.375rem', padding: '0.375rem 0.875rem', color: '#7DD3FC', fontWeight: '700', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.5px', textShadow: '0 0 8px rgba(14, 165, 233, 0.5)', boxShadow: '0 0 16px rgba(14, 165, 233, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)', }, // LOW severity badge - Emerald with refined glow badgeLow: { display: 'inline-flex', alignItems: 'center', gap: '0.5rem', background: 'linear-gradient(135deg, rgba(16, 185, 129, 0.25) 0%, rgba(16, 185, 129, 0.2) 100%)', border: '2px solid #10B981', borderRadius: '0.375rem', padding: '0.375rem 0.875rem', color: '#6EE7B7', fontWeight: '700', fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.5px', textShadow: '0 0 8px rgba(16, 185, 129, 0.5)', boxShadow: '0 0 16px rgba(16, 185, 129, 0.3), 0 4px 8px rgba(0, 0, 0, 0.4)', }, // Glowing dot for badges glowDot: (color) => ({ width: '8px', height: '8px', borderRadius: '50%', background: color, boxShadow: `0 0 12px ${color}, 0 0 6px ${color}`, animation: 'pulse 2s ease-in-out infinite', }), }; // Helper function to get severity badge style const getSeverityBadgeStyle = (severity) => { switch (severity?.toLowerCase()) { case 'critical': return STYLES.badgeCritical; case 'high': return STYLES.badgeHigh; case 'medium': return STYLES.badgeMedium; case 'low': return STYLES.badgeLow; default: return STYLES.badgeMedium; } }; // Helper function to get severity dot color const getSeverityDotColor = (severity) => { switch (severity?.toLowerCase()) { case 'critical': return '#EF4444'; case 'high': return '#F59E0B'; case 'medium': return '#0EA5E9'; case 'low': return '#10B981'; default: return '#0EA5E9'; } }; 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, canDelete, canExport, isAdmin, getActiveTeamsParam, adminScope } = 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 VALID_PAGES = new Set(['home', 'triage', 'compliance', 'knowledge-base', 'exports', 'jira', 'admin']); const [currentPage, setCurrentPageRaw] = useState(() => { try { const saved = localStorage.getItem('cve-dashboard-page'); return saved && VALID_PAGES.has(saved) ? saved : 'home'; } catch { return 'home'; } }); const setCurrentPage = (page) => { setCurrentPageRaw(page); try { localStorage.setItem('cve-dashboard-page', page); } catch {} }; const [navOpen, setNavOpen] = useState(false); const [calendarFilter, setCalendarFilter] = useState(null); const [reportingExcFilter, setReportingExcFilter] = useState(null); const [showAddCVE, setShowAddCVE] = useState(false); const [showUserManagement, setShowUserManagement] = useState(false); const [showAuditLog, setShowAuditLog] = useState(false); const [showFeedback, setShowFeedback] = useState(false); const [feedbackType, setFeedbackType] = useState('bug'); 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 [expandedCVEs, setExpandedCVEs] = useState({}); const [visibleCount, setVisibleCount] = useState(5); const [jiraTickets, setJiraTickets] = useState([]); const [showAddTicket, setShowAddTicket] = useState(false); const [showEditTicket, setShowEditTicket] = useState(false); const [editingTicket, setEditingTicket] = useState(null); const [ticketForm, setTicketForm] = useState({ cve_id: '', vendor: '', ticket_key: '', url: '', summary: '', status: 'Open' }); // For adding ticket from within a CVE card const [addTicketContext, setAddTicketContext] = useState(null); // { cve_id, vendor } // Archer tickets state const [archerTickets, setArcherTickets] = useState([]); const [showAddArcherTicket, setShowAddArcherTicket] = useState(false); const [showEditArcherTicket, setShowEditArcherTicket] = useState(false); const [editingArcherTicket, setEditingArcherTicket] = useState(null); const [archerTicketForm, setArcherTicketForm] = useState({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' }); const [addArcherTicketContext, setAddArcherTicketContext] = useState(null); // { cve_id, vendor } // Ivanti workflows state const [ivantiTotal, setIvantiTotal] = useState(null); const [ivantiWorkflows, setIvantiWorkflows] = useState([]); const [ivantiSyncedAt, setIvantiSyncedAt] = useState(null); const [ivantiSyncStatus, setIvantiSyncStatus] = useState(null); const [ivantiSyncError, setIvantiSyncError] = useState(null); const [ivantiLoading, setIvantiLoading] = useState(false); const [ivantiSyncing, setIvantiSyncing] = useState(false); // Archive filter state const [archiveFilter, setArchiveFilter] = useState(null); const [archiveRefreshKey, setArchiveRefreshKey] = useState(0); const [archiveList, setArchiveList] = useState([]); const [archiveListLoading, setArchiveListLoading] = useState(false); // Confirmation modal state — replaces window.confirm() const [pendingConfirm, setPendingConfirm] = useState(null); const toggleCVEExpand = (cveId) => { setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] })); }; 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 fetchJiraTickets = async () => { try { const response = await fetch(`${API_BASE}/jira-tickets`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch JIRA tickets'); const data = await response.json(); setJiraTickets(data); } catch (err) { console.error('Error fetching JIRA tickets:', err); } }; const fetchArcherTickets = async () => { try { const response = await fetch(`${API_BASE}/archer-tickets`, { credentials: 'include' }); if (!response.ok) throw new Error('Failed to fetch Archer tickets'); const data = await response.json(); setArcherTickets(data); } catch (err) { console.error('Error fetching Archer tickets:', err); } }; const applyIvantiState = (data) => { setIvantiTotal(data.total ?? 0); setIvantiWorkflows(data.workflows || []); setIvantiSyncedAt(data.synced_at || null); setIvantiSyncStatus(data.sync_status || null); setIvantiSyncError(data.error_message || null); }; const fetchIvantiWorkflows = async () => { setIvantiLoading(true); try { const response = await fetch(`${API_BASE}/ivanti/workflows`, { credentials: 'include' }); const data = await response.json(); if (response.ok) applyIvantiState(data); } catch (err) { console.error('Error loading Ivanti workflows:', err); } finally { setIvantiLoading(false); } }; const syncIvantiWorkflows = async () => { setIvantiSyncing(true); try { const response = await fetch(`${API_BASE}/ivanti/workflows/sync`, { method: 'POST', credentials: 'include' }); const data = await response.json(); if (response.ok) applyIvantiState(data); } catch (err) { console.error('Error syncing Ivanti workflows:', err); } finally { setIvantiSyncing(false); setArchiveRefreshKey(k => k + 1); } }; const handleArchiveStateClick = (state) => { const newFilter = archiveFilter === state ? null : state; setArchiveFilter(newFilter); if (newFilter) { setArchiveListLoading(true); const teamsParam = getActiveTeamsParam(); const url = teamsParam ? `${API_BASE}/ivanti/archive?state=${newFilter}&teams=${encodeURIComponent(teamsParam)}` : `${API_BASE}/ivanti/archive?state=${newFilter}`; fetch(url, { credentials: 'include' }) .then(res => res.ok ? res.json() : Promise.reject()) .then(data => setArchiveList(data.archives || [])) .catch(() => setArchiveList([])) .finally(() => setArchiveListLoading(false)); } else { setArchiveList([]); } }; 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 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) => { setPendingConfirm({ title: 'Delete Document', message: 'Are you sure you want to delete this document?', confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); 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) => { setPendingConfirm({ title: 'Delete Vendor Entry', message: `Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`, confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); try { const url = `${API_BASE}/cves/${cve.id}`; console.log('DELETE request to:', url); const response = await fetch(url, { method: 'DELETE', credentials: 'include' }); if (!response.ok) { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const data = await response.json(); throw new Error(data.error || 'Failed to delete CVE entry'); } else { throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); } } alert(`Deleted ${cve.vendor} entry for ${cve.cve_id}`); fetchCVEs(); fetchVendors(); } catch (err) { alert(`Error: ${err.message}`); } }, }); }; const handleDeleteEntireCVE = async (cveId, vendorCount) => { setPendingConfirm({ title: 'Delete Entire CVE', message: `Are you sure you want to delete ALL ${vendorCount} vendor entries for ${cveId}? This will permanently remove all associated documents and files.`, confirmText: 'Delete All', onConfirm: async () => { setPendingConfirm(null); try { const url = `${API_BASE}/cves/by-cve-id/${encodeURIComponent(cveId)}`; console.log('DELETE request to:', url); const response = await fetch(url, { method: 'DELETE', credentials: 'include' }); if (!response.ok) { const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { const data = await response.json(); throw new Error(data.error || 'Failed to delete CVE'); } else { throw new Error(`Server returned ${response.status} ${response.statusText}. Check API_BASE configuration.`); } } alert(`Deleted all entries for ${cveId}`); fetchCVEs(); fetchVendors(); } catch (err) { alert(`Error: ${err.message}`); } }, }); }; const handleAddTicket = async (e) => { e.preventDefault(); try { const response = await fetch(`${API_BASE}/jira-tickets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(ticketForm) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to create ticket'); } alert('JIRA ticket added successfully!'); setShowAddTicket(false); setAddTicketContext(null); setTicketForm({ cve_id: '', vendor: '', ticket_key: '', url: '', summary: '', status: 'Open' }); fetchJiraTickets(); } catch (err) { alert(`Error: ${err.message}`); } }; const handleEditTicket = (ticket) => { setEditingTicket(ticket); setTicketForm({ cve_id: ticket.cve_id, vendor: ticket.vendor, ticket_key: ticket.ticket_key, url: ticket.url || '', summary: ticket.summary || '', status: ticket.status }); setShowEditTicket(true); }; const handleUpdateTicket = async (e) => { e.preventDefault(); try { const response = await fetch(`${API_BASE}/jira-tickets/${editingTicket.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ ticket_key: ticketForm.ticket_key, url: ticketForm.url, summary: ticketForm.summary, status: ticketForm.status }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to update ticket'); } alert('JIRA ticket updated!'); setShowEditTicket(false); setEditingTicket(null); setTicketForm({ cve_id: '', vendor: '', ticket_key: '', url: '', summary: '', status: 'Open' }); fetchJiraTickets(); } catch (err) { alert(`Error: ${err.message}`); } }; const handleDeleteTicket = async (ticket) => { setPendingConfirm({ title: 'Delete Ticket', message: `Delete ticket ${ticket.ticket_key}?`, confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); try { const response = await fetch(`${API_BASE}/jira-tickets/${ticket.id}`, { method: 'DELETE', credentials: 'include' }); if (!response.ok) throw new Error('Failed to delete ticket'); alert('Ticket deleted'); fetchJiraTickets(); } catch (err) { alert(`Error: ${err.message}`); } }, }); }; const openAddTicketForCVE = (cve_id, vendor) => { setAddTicketContext({ cve_id, vendor }); setTicketForm({ cve_id, vendor, ticket_key: '', url: '', summary: '', status: 'Open' }); setShowAddTicket(true); }; // ========== ARCHER TICKET HANDLERS ========== const handleAddArcherTicket = async (e) => { e.preventDefault(); try { const response = await fetch(`${API_BASE}/archer-tickets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify(archerTicketForm) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to create Archer ticket'); } alert('Archer ticket added successfully!'); setShowAddArcherTicket(false); setAddArcherTicketContext(null); setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' }); fetchArcherTickets(); } catch (err) { alert(`Error: ${err.message}`); } }; const handleEditArcherTicket = (ticket) => { setEditingArcherTicket(ticket); setArcherTicketForm({ exc_number: ticket.exc_number, archer_url: ticket.archer_url || '', status: ticket.status, cve_id: ticket.cve_id, vendor: ticket.vendor }); setShowEditArcherTicket(true); }; const handleUpdateArcherTicket = async (e) => { e.preventDefault(); try { const response = await fetch(`${API_BASE}/archer-tickets/${editingArcherTicket.id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ exc_number: archerTicketForm.exc_number, archer_url: archerTicketForm.archer_url, status: archerTicketForm.status }) }); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to update Archer ticket'); } alert('Archer ticket updated!'); setShowEditArcherTicket(false); setEditingArcherTicket(null); setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id: '', vendor: '' }); fetchArcherTickets(); } catch (err) { alert(`Error: ${err.message}`); } }; const handleDeleteArcherTicket = async (ticket) => { setPendingConfirm({ title: 'Delete Archer Ticket', message: `Delete Archer ticket ${ticket.exc_number}?`, confirmText: 'Delete', onConfirm: async () => { setPendingConfirm(null); try { const response = await fetch(`${API_BASE}/archer-tickets/${ticket.id}`, { method: 'DELETE', credentials: 'include' }); if (!response.ok) throw new Error('Failed to delete Archer ticket'); alert('Archer ticket deleted'); fetchArcherTickets(); } catch (err) { alert(`Error: ${err.message}`); } }, }); }; const _openAddArcherTicketForCVE = (cve_id, vendor) => { setAddArcherTicketContext({ cve_id, vendor }); setArcherTicketForm({ exc_number: '', archer_url: '', status: 'Draft', cve_id, vendor }); setShowAddArcherTicket(true); }; // Fetch CVEs from API when authenticated useEffect(() => { if (isAuthenticated) { fetchCVEs(); fetchVendors(); fetchJiraTickets(); fetchArcherTickets(); fetchIvantiWorkflows(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated]); // Refetch when filters change useEffect(() => { if (isAuthenticated) { fetchCVEs(); setVisibleCount(5); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [searchQuery, selectedVendor, selectedSeverity]); // Show loading while checking auth if (authLoading) { return (

Loading...

); } // Show login if not authenticated if (!isAuthenticated) { return ; } // 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; return (
setNavOpen(false)} currentPage={currentPage} onNavigate={(page) => { // Clear contextual filters when navigating directly via the nav drawer if (page === 'triage') { setCalendarFilter(null); setReportingExcFilter(null); } setCurrentPage(page); }} /> {/* Scanning line effect */}
{/* Header */}

STEAM Security Dashboard

NTS Threat Intelligence and Metric Aggregation

{canWrite() && ( )} {canWrite() && ( )} setShowUserManagement(true)} onAuditLog={() => setShowAuditLog(true)} onFeatureRequest={() => { setFeedbackType('feature'); setShowFeedback(true); }} />
{/* Stats Bar - only shown on Home page */} {currentPage === 'home' &&
Total CVEs
{Object.keys(filteredGroupedCVEs).length}
Vendor Entries
{cves.length}
Open Tickets
{jiraTickets.filter(t => t.status !== 'Closed').length}
Critical
{cves.filter(c => c.severity === 'Critical').length}
}
{/* Page content */} {currentPage === 'triage' && } {currentPage === 'compliance' && } {currentPage === 'ccp-metrics' && } {currentPage === 'knowledge-base' && } {currentPage === 'exports' && } {currentPage === 'jira' && } {currentPage === 'admin' && isAdmin() && } {currentPage === 'admin' && !isAdmin() && (() => { setCurrentPage('home'); return null; })()} {/* User Management Modal */} {showUserManagement && ( setShowUserManagement(false)} /> )} {/* Audit Log Modal */} {showAuditLog && ( setShowAuditLog(false)} /> )} {/* NVD Sync Modal */} {showNvdSync && ( setShowNvdSync(false)} onSyncComplete={() => fetchCVEs()} /> )} {/* Feedback Modal (Bug Report / Feature Request) */} setShowFeedback(false)} defaultType={feedbackType} currentPage={currentPage} /> {/* Add CVE Modal */} {showAddCVE && (

Add CVE Entry

Tip: You can add the same CVE-ID multiple times with different vendors. Each vendor will have its own documents folder.

{ setNewCVE({...newCVE, cve_id: e.target.value.toUpperCase()}); setNvdAutoFilled(false); setNvdError(null); }} onBlur={(e) => lookupNVD(e.target.value)} className="intel-input w-full" /> {nvdLoading && ( )}

Can be the same as existing CVE if adding another vendor

{nvdAutoFilled && (

Auto-filled from NVD

)} {nvdError && (

{nvdError}

)}
setNewCVE({...newCVE, vendor: e.target.value})} className="intel-input w-full" />

Must be unique for this CVE-ID