diff --git a/frontend/src/App.css b/frontend/src/App.css index b76c0eb..e410620 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -17,6 +17,18 @@ } } +/* Toast notification slide-in */ +@keyframes toast-slide-in { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + :root { /* Base Colors - Modern Slate Foundation */ --intel-darkest: #0F172A; diff --git a/frontend/src/App.js b/frontend/src/App.js index 66bc9bb..79b8af6 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,5 @@ -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 React, { useState } from 'react'; +import { Plus, RefreshCw, Menu, Loader } from 'lucide-react'; import { useAuth } from './contexts/AuthContext'; import LoginForm from './components/LoginForm'; import UserMenu from './components/UserMenu'; @@ -8,8 +8,6 @@ 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'; @@ -17,190 +15,17 @@ 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 ArcherPage from './components/pages/ArcherPage'; import ArcherTemplatePage from './components/pages/ArcherTemplatePage'; +import HomePage from './components/pages/HomePage'; 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'; - -// Determine if a Jira status represents a "closed/done" state -function isClosedStatus(status) { - if (!status) return false; - const lower = status.toLowerCase(); - return ['closed', 'done', 'resolved', 'complete', 'completed', 'cancelled', 'canceled', "won't do", 'declined'].some(s => lower.includes(s)); -} - -function getTicketStatusColor(status) { - if (!status) return '#F59E0B'; - if (isClosedStatus(status)) return '#10B981'; - const lower = status.toLowerCase(); - if (['open', 'to do', 'backlog', 'new'].some(s => lower === s)) return '#F59E0B'; - // Everything else (in progress, approval, prioritizing, etc.) gets blue/purple - return '#0EA5E9'; -} - -// ============================================ -// 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 severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low']; +const VALID_PAGES = new Set(['home', 'triage', 'compliance', 'knowledge-base', 'exports', 'jira', 'admin', 'archer-templates']); export default function App() { - const { isAuthenticated, loading: authLoading, canWrite, canDelete, canExport, isAdmin, isInGroup, 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', 'archer-templates']); + const { isAuthenticated, loading: authLoading, canWrite, isAdmin, isInGroup } = useAuth(); + const [currentPage, setCurrentPageRaw] = useState(() => { try { const saved = localStorage.getItem('cve-dashboard-page'); @@ -211,6 +36,7 @@ export default function App() { 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); @@ -220,750 +46,16 @@ export default function App() { 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); + // Navigation handler that accepts optional context (filters) + const handleNavigate = (page, context) => { + if (page === 'triage') { + setCalendarFilter(context?.calendarFilter || null); + setReportingExcFilter(context?.reportingExcFilter || null); } + setCurrentPage(page); }; - 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 ( @@ -981,17 +73,6 @@ export default function App() { 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); }} @@ -1043,7 +123,7 @@ export default function App() { NVD Sync )} - {canWrite() && ( + {canWrite() && currentPage === 'home' && (
- - {/* Stats Bar - only shown on Home page */} - {currentPage === 'home' &&
-
-
-
Total CVEs
-
{Object.keys(filteredGroupedCVEs).length}
-
-
-
-
Vendor Entries
-
{cves.length}
-
-
-
-
Open Tickets
-
{jiraTickets.filter(t => !isClosedStatus(t.status)).length}
-
-
-
-
Critical
-
{cves.filter(c => c.severity === 'Critical').length}
-
-
} {/* Page content */} - {currentPage === 'triage' && } - {currentPage === 'compliance' && } - {currentPage === 'ccp-metrics' && isInGroup('Admin', 'Leadership') && } - {currentPage === 'ccp-metrics' && !isInGroup('Admin', 'Leadership') && (() => { setCurrentPage('home'); return null; })()} - {currentPage === 'knowledge-base' && } - {currentPage === 'exports' && } - {currentPage === 'jira' && } + {currentPage === 'home' && } + {currentPage === 'triage' && } + {currentPage === 'compliance' && } + {currentPage === 'ccp-metrics' && isInGroup('Admin', 'Leadership') && } + {currentPage === 'ccp-metrics' && !isInGroup('Admin', 'Leadership') && (() => { setCurrentPage('home'); return null; })()} + {currentPage === 'knowledge-base' && } + {currentPage === 'exports' && } + {currentPage === 'jira' && } {currentPage === 'archer-templates' && } {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) */} + {/* Global Modals */} + {showUserManagement && setShowUserManagement(false)} />} + {showAuditLog && setShowAuditLog(false)} />} + {showNvdSync && setShowNvdSync(false)} onSyncComplete={() => {}} />} 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

-
- -
- - -
- -
- -