1883 lines
91 KiB
JavaScript
1883 lines
91 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown } 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 './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, #0A0E27 0%, #131937 50%, #0A0E27 100%)',
|
|
padding: '1.5rem',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
},
|
|
// Stat cards with BRIGHT CYAN borders
|
|
statCard: {
|
|
background: 'linear-gradient(135deg, rgba(19, 25, 55, 1) 0%, rgba(30, 39, 73, 0.95) 100%)',
|
|
border: '3px solid #00D9FF',
|
|
borderRadius: '0.5rem',
|
|
padding: '1rem',
|
|
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 30px rgba(0, 217, 255, 0.3), inset 0 2px 0 rgba(0, 217, 255, 0.2)',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
},
|
|
// Intel card with thick glowing border
|
|
intelCard: {
|
|
background: 'linear-gradient(135deg, rgba(19, 25, 55, 1) 0%, rgba(30, 39, 73, 0.95) 50%, rgba(19, 25, 55, 1) 100%)',
|
|
border: '3px solid rgba(0, 217, 255, 0.6)',
|
|
borderRadius: '0.5rem',
|
|
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.7), 0 0 40px rgba(0, 217, 255, 0.25), inset 0 2px 0 rgba(0, 217, 255, 0.15)',
|
|
position: 'relative',
|
|
overflow: 'hidden',
|
|
},
|
|
// Vendor card with depth
|
|
vendorCard: {
|
|
background: 'linear-gradient(135deg, rgba(10, 14, 39, 0.95) 0%, rgba(19, 25, 55, 0.9) 100%)',
|
|
border: '2px solid rgba(0, 217, 255, 0.4)',
|
|
borderRadius: '0.5rem',
|
|
padding: '1rem',
|
|
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(0, 217, 255, 0.1)',
|
|
marginBottom: '0.75rem',
|
|
},
|
|
// CRITICAL severity badge - BRIGHT RED with WHITE text
|
|
badgeCritical: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
background: 'linear-gradient(135deg, rgba(255, 51, 102, 0.4) 0%, rgba(255, 51, 102, 0.3) 100%)',
|
|
border: '2px solid #FF3366',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
color: '#FFFFFF',
|
|
fontWeight: '700',
|
|
fontSize: '0.75rem',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.5px',
|
|
textShadow: '0 0 10px rgba(255, 51, 102, 0.9)',
|
|
boxShadow: '0 0 20px rgba(255, 51, 102, 0.5), 0 4px 8px rgba(0, 0, 0, 0.4)',
|
|
},
|
|
// HIGH severity badge - BRIGHT ORANGE/YELLOW with WHITE text
|
|
badgeHigh: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
background: 'linear-gradient(135deg, rgba(255, 184, 0, 0.4) 0%, rgba(255, 184, 0, 0.3) 100%)',
|
|
border: '2px solid #FFB800',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
color: '#FFFFFF',
|
|
fontWeight: '700',
|
|
fontSize: '0.75rem',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.5px',
|
|
textShadow: '0 0 10px rgba(255, 184, 0, 0.9)',
|
|
boxShadow: '0 0 20px rgba(255, 184, 0, 0.5), 0 4px 8px rgba(0, 0, 0, 0.4)',
|
|
},
|
|
// MEDIUM severity badge - BRIGHT CYAN with WHITE text
|
|
badgeMedium: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
background: 'linear-gradient(135deg, rgba(0, 217, 255, 0.4) 0%, rgba(0, 217, 255, 0.3) 100%)',
|
|
border: '2px solid #00D9FF',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
color: '#FFFFFF',
|
|
fontWeight: '700',
|
|
fontSize: '0.75rem',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.5px',
|
|
textShadow: '0 0 10px rgba(0, 217, 255, 0.9)',
|
|
boxShadow: '0 0 20px rgba(0, 217, 255, 0.5), 0 4px 8px rgba(0, 0, 0, 0.4)',
|
|
},
|
|
// LOW severity badge - BRIGHT GREEN with WHITE text
|
|
badgeLow: {
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: '0.5rem',
|
|
background: 'linear-gradient(135deg, rgba(0, 255, 136, 0.4) 0%, rgba(0, 255, 136, 0.3) 100%)',
|
|
border: '2px solid #00FF88',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
color: '#FFFFFF',
|
|
fontWeight: '700',
|
|
fontSize: '0.75rem',
|
|
textTransform: 'uppercase',
|
|
letterSpacing: '0.5px',
|
|
textShadow: '0 0 10px rgba(0, 255, 136, 0.9)',
|
|
boxShadow: '0 0 20px rgba(0, 255, 136, 0.5), 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 '#FF3366';
|
|
case 'high': return '#FFB800';
|
|
case 'medium': return '#00D9FF';
|
|
case 'low': return '#00FF88';
|
|
default: return '#00D9FF';
|
|
}
|
|
};
|
|
const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001';
|
|
|
|
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
|
|
|
|
export default function App() {
|
|
const { isAuthenticated, loading: authLoading, canWrite, isAdmin } = useAuth();
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
|
|
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
|
|
const [selectedCVE, setSelectedCVE] = useState(null);
|
|
const [selectedVendorView, setSelectedVendorView] = useState(null);
|
|
const [selectedDocuments, setSelectedDocuments] = useState([]);
|
|
const [cves, setCves] = useState([]);
|
|
const [vendors, setVendors] = useState(['All Vendors']);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState(null);
|
|
const [cveDocuments, setCveDocuments] = useState({});
|
|
const [quickCheckCVE, setQuickCheckCVE] = useState('');
|
|
const [quickCheckResult, setQuickCheckResult] = useState(null);
|
|
const [showAddCVE, setShowAddCVE] = useState(false);
|
|
const [showUserManagement, setShowUserManagement] = useState(false);
|
|
const [showAuditLog, setShowAuditLog] = useState(false);
|
|
const [showNvdSync, setShowNvdSync] = useState(false);
|
|
const [newCVE, setNewCVE] = useState({
|
|
cve_id: '',
|
|
vendor: '',
|
|
severity: 'Medium',
|
|
description: '',
|
|
published_date: new Date().toISOString().split('T')[0]
|
|
});
|
|
const [uploadingFile, setUploadingFile] = useState(false);
|
|
const [nvdLoading, setNvdLoading] = useState(false);
|
|
const [nvdError, setNvdError] = useState(null);
|
|
const [nvdAutoFilled, setNvdAutoFilled] = useState(false);
|
|
const [showEditCVE, setShowEditCVE] = useState(false);
|
|
const [editingCVE, setEditingCVE] = useState(null);
|
|
const [editForm, setEditForm] = useState({
|
|
cve_id: '', vendor: '', severity: 'Medium', description: '', published_date: '', status: 'Open'
|
|
});
|
|
const [editNvdLoading, setEditNvdLoading] = useState(false);
|
|
const [editNvdError, setEditNvdError] = useState(null);
|
|
const [editNvdAutoFilled, setEditNvdAutoFilled] = useState(false);
|
|
const [expandedCVEs, setExpandedCVEs] = useState({});
|
|
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 }
|
|
|
|
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 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) => {
|
|
if (!window.confirm('Are you sure you want to delete this document?')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/documents/${docId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) throw new Error('Failed to delete document');
|
|
|
|
alert('Document deleted successfully!');
|
|
const key = `${cveId}-${vendor}`;
|
|
delete cveDocuments[key];
|
|
await fetchDocuments(cveId, vendor);
|
|
fetchCVEs();
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const handleEditCVE = (cve) => {
|
|
setEditingCVE(cve);
|
|
setEditForm({
|
|
cve_id: cve.cve_id,
|
|
vendor: cve.vendor,
|
|
severity: cve.severity,
|
|
description: cve.description || '',
|
|
published_date: cve.published_date || '',
|
|
status: cve.status || 'Open'
|
|
});
|
|
setEditNvdLoading(false);
|
|
setEditNvdError(null);
|
|
setEditNvdAutoFilled(false);
|
|
setShowEditCVE(true);
|
|
};
|
|
|
|
const handleEditCVESubmit = async (e) => {
|
|
e.preventDefault();
|
|
if (!editingCVE) return;
|
|
|
|
try {
|
|
const body = {};
|
|
if (editForm.cve_id !== editingCVE.cve_id) body.cve_id = editForm.cve_id;
|
|
if (editForm.vendor !== editingCVE.vendor) body.vendor = editForm.vendor;
|
|
if (editForm.severity !== editingCVE.severity) body.severity = editForm.severity;
|
|
if (editForm.description !== (editingCVE.description || '')) body.description = editForm.description;
|
|
if (editForm.published_date !== (editingCVE.published_date || '')) body.published_date = editForm.published_date;
|
|
if (editForm.status !== (editingCVE.status || 'Open')) body.status = editForm.status;
|
|
|
|
if (Object.keys(body).length === 0) {
|
|
alert('No changes detected.');
|
|
return;
|
|
}
|
|
|
|
const response = await fetch(`${API_BASE}/cves/${editingCVE.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(body)
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'Failed to update CVE');
|
|
}
|
|
|
|
alert('CVE updated successfully!');
|
|
setShowEditCVE(false);
|
|
setEditingCVE(null);
|
|
fetchCVEs();
|
|
fetchVendors();
|
|
} catch (err) {
|
|
alert(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const lookupNVDForEdit = async (cveId) => {
|
|
const trimmed = cveId.trim();
|
|
if (!/^CVE-\d{4}-\d{4,}$/.test(trimmed)) return;
|
|
|
|
setEditNvdLoading(true);
|
|
setEditNvdError(null);
|
|
setEditNvdAutoFilled(false);
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE}/nvd/lookup/${encodeURIComponent(trimmed)}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || 'NVD lookup failed');
|
|
}
|
|
|
|
const data = await response.json();
|
|
setEditForm(prev => ({
|
|
...prev,
|
|
description: data.description || prev.description,
|
|
severity: data.severity || prev.severity,
|
|
published_date: data.published_date || prev.published_date
|
|
}));
|
|
setEditNvdAutoFilled(true);
|
|
} catch (err) {
|
|
setEditNvdError(err.message);
|
|
} finally {
|
|
setEditNvdLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteCVEEntry = async (cve) => {
|
|
if (!window.confirm(`Are you sure you want to delete the "${cve.vendor}" entry for ${cve.cve_id}? This will also delete all associated documents.`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const 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) => {
|
|
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 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) => {
|
|
if (!window.confirm(`Delete ticket ${ticket.ticket_key}?`)) return;
|
|
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);
|
|
};
|
|
|
|
// Fetch CVEs from API when authenticated
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
fetchCVEs();
|
|
fetchVendors();
|
|
fetchJiraTickets();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [isAuthenticated]);
|
|
|
|
// Refetch when filters change
|
|
useEffect(() => {
|
|
if (isAuthenticated) {
|
|
fetchCVEs();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [searchQuery, selectedVendor, selectedSeverity]);
|
|
|
|
// Show loading while checking auth
|
|
if (authLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<Loader className="w-12 h-12 text-[#0476D9] mx-auto animate-spin" />
|
|
<p className="text-gray-600 mt-4">Loading...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Show login if not authenticated
|
|
if (!isAuthenticated) {
|
|
return <LoginForm />;
|
|
}
|
|
|
|
// 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 (
|
|
<div className="min-h-screen bg-intel-darkest grid-bg p-6 relative overflow-hidden fade-in">
|
|
{/* Scanning line effect */}
|
|
<div className="scan-line"></div>
|
|
|
|
<div className="max-w-7xl mx-auto relative z-10">
|
|
{/* Header */}
|
|
<div className="mb-8">
|
|
<div className="flex justify-between items-start mb-6">
|
|
<div className="flex-1">
|
|
<h1 className="text-4xl font-bold text-intel-accent mb-1 font-mono tracking-tight">
|
|
CVE INTEL
|
|
</h1>
|
|
<p className="text-gray-400 text-sm font-sans">Threat Intelligence & Vulnerability Command Center</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => setShowNvdSync(true)}
|
|
className="intel-button intel-button-success relative z-10 flex items-center gap-2"
|
|
>
|
|
<RefreshCw className="w-4 h-4" />
|
|
NVD Sync
|
|
</button>
|
|
)}
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => setShowAddCVE(true)}
|
|
className="intel-button intel-button-primary relative z-10 flex items-center gap-2"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
Add Entry
|
|
</button>
|
|
)}
|
|
<UserMenu onManageUsers={() => setShowUserManagement(true)} onAuditLog={() => setShowAuditLog(true)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Stats Bar - INLINE STYLES FOR GUARANTEED VISIBILITY */}
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
<div style={STYLES.statCard}>
|
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '3px', background: 'linear-gradient(90deg, transparent, #00D9FF, transparent)', boxShadow: '0 0 12px rgba(0, 217, 255, 0.7)' }}></div>
|
|
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#B8C5D9', marginBottom: '0.25rem' }}>Total CVEs</div>
|
|
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#00D9FF', textShadow: '0 0 20px rgba(0, 217, 255, 0.5)' }}>{Object.keys(filteredGroupedCVEs).length}</div>
|
|
</div>
|
|
<div style={STYLES.statCard}>
|
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '3px', background: 'linear-gradient(90deg, transparent, #00D9FF, transparent)', boxShadow: '0 0 12px rgba(0, 217, 255, 0.7)' }}></div>
|
|
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#B8C5D9', marginBottom: '0.25rem' }}>Vendor Entries</div>
|
|
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#E4E8F1' }}>{cves.length}</div>
|
|
</div>
|
|
<div style={{...STYLES.statCard, border: '3px solid #FFB800', boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 30px rgba(255, 184, 0, 0.3), inset 0 2px 0 rgba(255, 184, 0, 0.2)'}}>
|
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '3px', background: 'linear-gradient(90deg, transparent, #FFB800, transparent)', boxShadow: '0 0 12px rgba(255, 184, 0, 0.7)' }}></div>
|
|
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#B8C5D9', marginBottom: '0.25rem' }}>Open Tickets</div>
|
|
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#FFB800', textShadow: '0 0 20px rgba(255, 184, 0, 0.5)' }}>{jiraTickets.filter(t => t.status !== 'Closed').length}</div>
|
|
</div>
|
|
<div style={{...STYLES.statCard, border: '3px solid #FF3366', boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 30px rgba(255, 51, 102, 0.3), inset 0 2px 0 rgba(255, 51, 102, 0.2)'}}>
|
|
<div style={{ position: 'absolute', top: 0, left: 0, right: 0, height: '3px', background: 'linear-gradient(90deg, transparent, #FF3366, transparent)', boxShadow: '0 0 12px rgba(255, 51, 102, 0.7)' }}></div>
|
|
<div style={{ fontSize: '0.75rem', textTransform: 'uppercase', letterSpacing: '0.1em', color: '#B8C5D9', marginBottom: '0.25rem' }}>Critical</div>
|
|
<div style={{ fontSize: '1.5rem', fontWeight: '700', fontFamily: 'monospace', color: '#FF3366', textShadow: '0 0 20px rgba(255, 51, 102, 0.5)' }}>{cves.filter(c => c.severity === 'Critical').length}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* User Management Modal */}
|
|
{showUserManagement && (
|
|
<UserManagement onClose={() => setShowUserManagement(false)} />
|
|
)}
|
|
|
|
{/* Audit Log Modal */}
|
|
{showAuditLog && (
|
|
<AuditLog onClose={() => setShowAuditLog(false)} />
|
|
)}
|
|
|
|
{/* NVD Sync Modal */}
|
|
{showNvdSync && (
|
|
<NvdSyncModal onClose={() => setShowNvdSync(false)} onSyncComplete={() => fetchCVEs()} />
|
|
)}
|
|
|
|
{/* Add CVE Modal */}
|
|
{showAddCVE && (
|
|
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
|
|
<div className="intel-card rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto border-intel-accent">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-2xl font-bold text-intel-accent font-mono">Add CVE Entry</h2>
|
|
<button
|
|
onClick={() => { setShowAddCVE(false); setNvdLoading(false); setNvdError(null); setNvdAutoFilled(false); }}
|
|
className="text-gray-400 hover:text-intel-accent transition-colors"
|
|
>
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mb-4 p-3 bg-intel-medium border border-intel-accent/30 rounded">
|
|
<p className="text-sm text-white">
|
|
<strong className="text-intel-accent">Tip:</strong> You can add the same CVE-ID multiple times with different vendors.
|
|
Each vendor will have its own documents folder.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleAddCVE} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
CVE ID *
|
|
</label>
|
|
<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="intel-input w-full"
|
|
/>
|
|
{nvdLoading && (
|
|
<Loader className="absolute right-3 top-2.5 w-5 h-5 text-intel-accent animate-spin" />
|
|
)}
|
|
</div>
|
|
<p className="text-xs text-gray-500 mt-1">Can be the same as existing CVE if adding another vendor</p>
|
|
{nvdAutoFilled && (
|
|
<p className="text-xs text-intel-success mt-1 flex items-center gap-1">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Auto-filled from NVD
|
|
</p>
|
|
)}
|
|
{nvdError && (
|
|
<p className="text-xs text-intel-warning mt-1 flex items-center gap-1">
|
|
<AlertCircle className="w-3 h-3" />
|
|
{nvdError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
Vendor *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Microsoft, Cisco, Oracle, etc."
|
|
value={newCVE.vendor}
|
|
onChange={(e) => setNewCVE({...newCVE, vendor: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
<p className="text-xs text-gray-500 mt-1">Must be unique for this CVE-ID</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
Severity *
|
|
</label>
|
|
<select
|
|
value={newCVE.severity}
|
|
onChange={(e) => setNewCVE({...newCVE, severity: e.target.value})}
|
|
className="intel-input w-full"
|
|
>
|
|
<option value="Critical">Critical</option>
|
|
<option value="High">High</option>
|
|
<option value="Medium">Medium</option>
|
|
<option value="Low">Low</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
Description *
|
|
</label>
|
|
<textarea
|
|
required
|
|
placeholder="Brief description of the vulnerability"
|
|
value={newCVE.description}
|
|
onChange={(e) => setNewCVE({...newCVE, description: e.target.value})}
|
|
rows={3}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
Published Date *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
value={newCVE.published_date}
|
|
onChange={(e) => setNewCVE({...newCVE, published_date: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
type="submit"
|
|
className="flex-1 intel-button intel-button-primary"
|
|
>
|
|
Add CVE Entry
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setShowAddCVE(false); setNvdLoading(false); setNvdError(null); setNvdAutoFilled(false); }}
|
|
className="px-4 py-2 bg-intel-dark text-gray-400 rounded border border-gray-600 hover:bg-intel-medium transition-colors font-mono text-sm uppercase tracking-wider"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit CVE Modal */}
|
|
{showEditCVE && editingCVE && (
|
|
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
|
|
<div className="intel-card rounded-lg shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto border-intel-accent">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-2xl font-bold text-intel-accent font-mono">Edit CVE Entry</h2>
|
|
<button
|
|
onClick={() => { setShowEditCVE(false); setEditingCVE(null); }}
|
|
className="text-gray-400 hover:text-intel-accent transition-colors"
|
|
>
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mb-4 p-3 bg-intel-medium border border-intel-warning/30 rounded">
|
|
<p className="text-sm text-white">
|
|
<strong className="text-intel-warning">Note:</strong> Changing CVE ID or Vendor will move associated documents to the new path.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleEditCVESubmit} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">CVE ID *</label>
|
|
<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="intel-input w-full"
|
|
/>
|
|
{editNvdLoading && (
|
|
<Loader className="absolute right-3 top-2.5 w-5 h-5 text-intel-accent animate-spin" />
|
|
)}
|
|
</div>
|
|
{editNvdAutoFilled && (
|
|
<p className="text-xs text-intel-success mt-1 flex items-center gap-1">
|
|
<CheckCircle className="w-3 h-3" />
|
|
Updated from NVD
|
|
</p>
|
|
)}
|
|
{editNvdError && (
|
|
<p className="text-xs text-intel-warning mt-1 flex items-center gap-1">
|
|
<AlertCircle className="w-3 h-3" />
|
|
{editNvdError}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Vendor *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={editForm.vendor}
|
|
onChange={(e) => setEditForm({...editForm, vendor: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Severity *</label>
|
|
<select
|
|
value={editForm.severity}
|
|
onChange={(e) => setEditForm({...editForm, severity: e.target.value})}
|
|
className="intel-input w-full"
|
|
>
|
|
<option value="Critical">Critical</option>
|
|
<option value="High">High</option>
|
|
<option value="Medium">Medium</option>
|
|
<option value="Low">Low</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Description *</label>
|
|
<textarea
|
|
required
|
|
value={editForm.description}
|
|
onChange={(e) => setEditForm({...editForm, description: e.target.value})}
|
|
rows={3}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Published Date *</label>
|
|
<input
|
|
type="date"
|
|
required
|
|
value={editForm.published_date}
|
|
onChange={(e) => setEditForm({...editForm, published_date: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status *</label>
|
|
<select
|
|
value={editForm.status}
|
|
onChange={(e) => setEditForm({...editForm, status: e.target.value})}
|
|
className="intel-input w-full"
|
|
>
|
|
<option value="Open">Open</option>
|
|
<option value="Addressed">Addressed</option>
|
|
<option value="In Progress">In Progress</option>
|
|
<option value="Resolved">Resolved</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex gap-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => lookupNVDForEdit(editForm.cve_id)}
|
|
disabled={editNvdLoading}
|
|
className="intel-button intel-button-success flex items-center gap-2 disabled:opacity-50"
|
|
>
|
|
<RefreshCw className={`w-4 h-4 ${editNvdLoading ? 'animate-spin' : ''}`} />
|
|
NVD Update
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="flex-1 intel-button intel-button-primary"
|
|
>
|
|
Save Changes
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setShowEditCVE(false); setEditingCVE(null); }}
|
|
className="px-4 py-2 bg-intel-dark text-gray-400 rounded border border-gray-600 hover:bg-intel-medium transition-colors font-mono text-sm uppercase tracking-wider"
|
|
>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Add JIRA Ticket Modal */}
|
|
{showAddTicket && (
|
|
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
|
|
<div className="intel-card rounded-lg shadow-2xl max-w-md w-full border-intel-warning">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-bold text-intel-warning font-mono">Add JIRA Ticket</h2>
|
|
<button onClick={() => { setShowAddTicket(false); setAddTicketContext(null); }} className="text-gray-400 hover:text-intel-accent transition-colors">
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
<form onSubmit={handleAddTicket} className="space-y-4">
|
|
{!addTicketContext && (
|
|
<>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">CVE ID *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="CVE-2024-1234"
|
|
value={ticketForm.cve_id}
|
|
onChange={(e) => setTicketForm({...ticketForm, cve_id: e.target.value.toUpperCase()})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Vendor *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="Cisco"
|
|
value={ticketForm.vendor}
|
|
onChange={(e) => setTicketForm({...ticketForm, vendor: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
{addTicketContext && (
|
|
<div className="p-3 bg-intel-medium border border-intel-warning/30 rounded text-sm text-white">
|
|
Adding ticket for <strong className="text-intel-warning">{addTicketContext.cve_id}</strong> / <strong className="text-intel-warning">{addTicketContext.vendor}</strong>
|
|
</div>
|
|
)}
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Ticket Key *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
placeholder="VULN-1234"
|
|
value={ticketForm.ticket_key}
|
|
onChange={(e) => setTicketForm({...ticketForm, ticket_key: e.target.value.toUpperCase()})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">JIRA URL</label>
|
|
<input
|
|
type="url"
|
|
placeholder="https://jira.company.com/browse/VULN-1234"
|
|
value={ticketForm.url}
|
|
onChange={(e) => setTicketForm({...ticketForm, url: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Summary</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Brief description"
|
|
value={ticketForm.summary}
|
|
onChange={(e) => setTicketForm({...ticketForm, summary: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status</label>
|
|
<select
|
|
value={ticketForm.status}
|
|
onChange={(e) => setTicketForm({...ticketForm, status: e.target.value})}
|
|
className="intel-input w-full"
|
|
>
|
|
<option value="Open">Open</option>
|
|
<option value="In Progress">In Progress</option>
|
|
<option value="Closed">Closed</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex gap-3 pt-4">
|
|
<button type="submit" className="flex-1 intel-button intel-button-primary">
|
|
Add Ticket
|
|
</button>
|
|
<button type="button" onClick={() => { setShowAddTicket(false); setAddTicketContext(null); }} className="px-4 py-2 bg-intel-dark text-gray-400 rounded border border-gray-600 hover:bg-intel-medium transition-colors font-mono text-sm uppercase tracking-wider">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Edit JIRA Ticket Modal */}
|
|
{showEditTicket && editingTicket && (
|
|
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
|
|
<div className="intel-card rounded-lg shadow-2xl max-w-md w-full border-intel-warning">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 className="text-xl font-bold text-intel-warning font-mono">Edit JIRA Ticket</h2>
|
|
<button onClick={() => { setShowEditTicket(false); setEditingTicket(null); }} className="text-gray-400 hover:text-intel-accent transition-colors">
|
|
<XCircle className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
<div className="p-3 bg-intel-medium rounded text-sm text-white mb-4 font-mono">
|
|
{editingTicket.cve_id} / {editingTicket.vendor}
|
|
</div>
|
|
<form onSubmit={handleUpdateTicket} className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Ticket Key *</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={ticketForm.ticket_key}
|
|
onChange={(e) => setTicketForm({...ticketForm, ticket_key: e.target.value.toUpperCase()})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">JIRA URL</label>
|
|
<input
|
|
type="url"
|
|
value={ticketForm.url}
|
|
onChange={(e) => setTicketForm({...ticketForm, url: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Summary</label>
|
|
<input
|
|
type="text"
|
|
value={ticketForm.summary}
|
|
onChange={(e) => setTicketForm({...ticketForm, summary: e.target.value})}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">Status</label>
|
|
<select
|
|
value={ticketForm.status}
|
|
onChange={(e) => setTicketForm({...ticketForm, status: e.target.value})}
|
|
className="intel-input w-full"
|
|
>
|
|
<option value="Open">Open</option>
|
|
<option value="In Progress">In Progress</option>
|
|
<option value="Closed">Closed</option>
|
|
</select>
|
|
</div>
|
|
<div className="flex gap-3 pt-4">
|
|
<button type="submit" className="flex-1 intel-button intel-button-primary">
|
|
Save Changes
|
|
</button>
|
|
<button type="button" onClick={() => { setShowEditTicket(false); setEditingTicket(null); }} className="px-4 py-2 bg-intel-dark text-gray-400 rounded border border-gray-600 hover:bg-intel-medium transition-colors font-mono text-sm uppercase tracking-wider">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Three Column Layout */}
|
|
<div className="grid grid-cols-12 gap-6">
|
|
{/* LEFT PANEL - Wiki/Knowledge Base */}
|
|
<div className="col-span-12 lg:col-span-3 space-y-4">
|
|
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '4px solid #00FF88'}} className="rounded-lg">
|
|
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#00FF88', marginBottom: '1rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 15px rgba(0, 255, 136, 0.5)' }}>
|
|
Knowledge Base
|
|
</h2>
|
|
|
|
{/* Wiki/Blog Style Entries */}
|
|
<div className="space-y-3">
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(0, 255, 136, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} className="hover:border-intel-success">
|
|
<h3 className="text-white font-semibold text-sm mb-1 font-mono">CVE Response Procedures</h3>
|
|
<p className="text-gray-400 text-xs mb-2">Standard operating procedures for vulnerability response and escalation...</p>
|
|
<span className="text-xs text-intel-success font-mono">Last updated: 2024-02-08</span>
|
|
</div>
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(0, 255, 136, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} className="hover:border-intel-success">
|
|
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Vendor Contact Matrix</h3>
|
|
<p className="text-gray-400 text-xs mb-2">Emergency contacts and escalation paths for security vendors...</p>
|
|
<span className="text-xs text-intel-success font-mono">Last updated: 2024-02-05</span>
|
|
</div>
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(0, 255, 136, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} className="hover:border-intel-success">
|
|
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Severity Classification Guide</h3>
|
|
<p className="text-gray-400 text-xs mb-2">Guidelines for assessing and classifying vulnerability severity levels...</p>
|
|
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-28</span>
|
|
</div>
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(0, 255, 136, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} className="hover:border-intel-success">
|
|
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Patching Policy</h3>
|
|
<p className="text-gray-400 text-xs mb-2">Enterprise patch management timelines and approval workflow...</p>
|
|
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-15</span>
|
|
</div>
|
|
|
|
<div style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(0, 255, 136, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} className="hover:border-intel-success">
|
|
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Documentation Standards</h3>
|
|
<p className="text-gray-400 text-xs mb-2">Required documentation for vulnerability tracking and audit compliance...</p>
|
|
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-10</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* CENTER PANEL - Main Content */}
|
|
<div className="col-span-12 lg:col-span-6 space-y-4">
|
|
{/* Quick Check */}
|
|
<div style={{...STYLES.intelCard, padding: '1.5rem'}} className="rounded-lg">
|
|
<div className="scan-line"></div>
|
|
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#00D9FF', marginBottom: '0.75rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 20px rgba(0, 217, 255, 0.5)' }}>Quick CVE Lookup</h2>
|
|
<div className="flex gap-3">
|
|
<input
|
|
type="text"
|
|
placeholder="Enter CVE ID (e.g., CVE-2024-1234)"
|
|
value={quickCheckCVE}
|
|
onChange={(e) => setQuickCheckCVE(e.target.value)}
|
|
onKeyPress={(e) => e.key === 'Enter' && quickCheckCVEStatus()}
|
|
className="flex-1 intel-input"
|
|
/>
|
|
<button
|
|
onClick={quickCheckCVEStatus}
|
|
className="intel-button intel-button-primary"
|
|
>
|
|
Scan
|
|
</button>
|
|
</div>
|
|
|
|
{quickCheckResult && (
|
|
<div className={`mt-4 p-4 rounded border ${quickCheckResult.exists ? 'bg-intel-success/10 border-intel-success/30' : 'bg-intel-warning/10 border-intel-warning/30'}`}>
|
|
{quickCheckResult.error ? (
|
|
<div className="flex items-start gap-3">
|
|
<XCircle className="w-5 h-5 text-intel-danger mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-intel-danger font-mono">Error</p>
|
|
<p className="text-sm text-gray-300">{quickCheckResult.error}</p>
|
|
</div>
|
|
</div>
|
|
) : quickCheckResult.exists ? (
|
|
<div className="flex items-start gap-3">
|
|
<CheckCircle className="w-5 h-5 text-intel-success mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="font-medium text-intel-success font-mono">✓ 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-intel-dark/70 rounded border border-intel-accent/30 shadow-lg">
|
|
<p className="font-semibold text-white mb-2 font-sans">{vendorInfo.vendor}</p>
|
|
<div className="grid grid-cols-2 gap-2 text-sm text-gray-300 mb-2 font-mono">
|
|
<p><strong className="text-white">Severity:</strong> {vendorInfo.severity}</p>
|
|
<p><strong className="text-white">Status:</strong> {vendorInfo.status}</p>
|
|
<p><strong className="text-white">Documents:</strong> {vendorInfo.total_documents} attached</p>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-start gap-3">
|
|
<AlertCircle className="w-5 h-5 text-intel-warning mt-0.5" />
|
|
<div>
|
|
<p className="font-medium text-intel-warning font-mono">Not Found</p>
|
|
<p className="text-sm text-gray-300">This CVE has not been addressed yet. No entry exists in the database.</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Search and Filters */}
|
|
<div style={{...STYLES.intelCard, padding: '1.5rem'}} className="rounded-lg">
|
|
<div className="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
<Search className="inline w-4 h-4 mr-1" />
|
|
Search CVEs
|
|
</label>
|
|
<input
|
|
type="text"
|
|
placeholder="CVE ID or description..."
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="intel-input w-full"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
<Filter className="inline w-4 h-4 mr-1" />
|
|
Vendor
|
|
</label>
|
|
<select
|
|
value={selectedVendor}
|
|
onChange={(e) => setSelectedVendor(e.target.value)}
|
|
className="intel-input w-full"
|
|
>
|
|
{vendors.map(vendor => (
|
|
<option key={vendor} value={vendor}>{vendor}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-300 mb-2 uppercase tracking-wider">
|
|
<AlertCircle className="inline w-4 h-4 mr-1" />
|
|
Severity
|
|
</label>
|
|
<select
|
|
value={selectedSeverity}
|
|
onChange={(e) => setSelectedSeverity(e.target.value)}
|
|
className="intel-input w-full"
|
|
>
|
|
{severityLevels.map(level => (
|
|
<option key={level} value={level}>{level}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Results Summary */}
|
|
<div className="flex justify-between items-center">
|
|
<p className="text-gray-400 font-mono text-sm">
|
|
<span className="text-intel-accent font-bold">{Object.keys(filteredGroupedCVEs).length}</span> CVE{Object.keys(filteredGroupedCVEs).length !== 1 ? 's' : ''}
|
|
<span className="text-gray-500 mx-2">•</span>
|
|
<span className="text-gray-300">{cves.length}</span> vendor entr{cves.length !== 1 ? 'ies' : 'y'}
|
|
</p>
|
|
{selectedDocuments.length > 0 && (
|
|
<button
|
|
onClick={exportSelectedDocuments}
|
|
className="intel-button intel-button-primary flex items-center gap-2"
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
Export {selectedDocuments.length} Doc{selectedDocuments.length !== 1 ? 's' : ''}
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* CVE List - Grouped by CVE ID */}
|
|
{loading ? (
|
|
<div className="intel-card rounded-lg p-12 text-center">
|
|
<div className="loading-spinner w-12 h-12 mx-auto mb-4"></div>
|
|
<p className="text-gray-400 font-mono text-sm uppercase tracking-wider">Scanning Vulnerabilities...</p>
|
|
</div>
|
|
) : error ? (
|
|
<div className="intel-card rounded-lg p-12 text-center border-intel-danger">
|
|
<XCircle className="w-12 h-12 text-intel-danger mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-gray-200 mb-2 font-mono">Error Loading CVEs</h3>
|
|
<p className="text-gray-400 mb-4">{error}</p>
|
|
<button
|
|
onClick={fetchCVEs}
|
|
className="intel-button intel-button-primary"
|
|
>
|
|
Retry
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => {
|
|
const isCVEExpanded = expandedCVEs[cveId];
|
|
const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3 };
|
|
const highestSeverity = vendorEntries.reduce((highest, entry) => {
|
|
const currentOrder = severityOrder[entry.severity] ?? 4;
|
|
const highestOrder = severityOrder[highest] ?? 4;
|
|
return currentOrder < highestOrder ? entry.severity : highest;
|
|
}, vendorEntries[0].severity);
|
|
const totalDocCount = vendorEntries.reduce((sum, entry) => sum + (entry.document_count || 0), 0);
|
|
const overallStatuses = [...new Set(vendorEntries.map(e => e.status))];
|
|
|
|
return (
|
|
<div key={cveId} style={STYLES.intelCard} className="rounded-lg">
|
|
{/* Clickable CVE Header */}
|
|
<div
|
|
style={{ padding: '1.5rem', cursor: 'pointer', transition: 'all 0.2s', userSelect: 'none' }}
|
|
onClick={() => toggleCVEExpand(cveId)}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<ChevronDown
|
|
className={`w-5 h-5 text-intel-accent transition-transform duration-200 flex-shrink-0 ${isCVEExpanded ? 'rotate-0' : '-rotate-90'}`}
|
|
/>
|
|
<h3 className="text-2xl font-bold text-intel-accent font-mono tracking-tight">{cveId}</h3>
|
|
</div>
|
|
|
|
{/* Collapsed: truncated description + summary row */}
|
|
{!isCVEExpanded && (
|
|
<div className="ml-8">
|
|
<p style={{ color: '#E4E8F1', fontSize: '0.875rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginBottom: '0.5rem' }}>{vendorEntries[0].description}</p>
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<span style={getSeverityBadgeStyle(highestSeverity)}>
|
|
<span style={STYLES.glowDot(getSeverityDotColor(highestSeverity))}></span>
|
|
{highestSeverity}
|
|
</span>
|
|
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>{vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
|
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace', display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
|
<FileText className="w-3 h-3" />
|
|
{totalDocCount} doc{totalDocCount !== 1 ? 's' : ''}
|
|
</span>
|
|
<span style={{ fontSize: '0.75rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
|
|
{overallStatuses.join(', ')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Expanded: full description + metadata */}
|
|
{isCVEExpanded && (
|
|
<div className="ml-8">
|
|
<p className="text-white mb-3">{vendorEntries[0].description}</p>
|
|
<div className="flex items-center gap-2 text-sm text-gray-300 font-mono">
|
|
<span>Published: {vendorEntries[0].published_date}</span>
|
|
<span className="text-intel-accent">•</span>
|
|
<span>{vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''}</span>
|
|
{canWrite() && vendorEntries.length >= 2 && (
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); handleDeleteEntireCVE(cveId, vendorEntries.length); }}
|
|
className="ml-2 px-3 py-1 text-xs intel-button intel-button-danger flex items-center gap-1"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
Delete All
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded: Vendor Entries */}
|
|
{isCVEExpanded && (
|
|
<div className="px-6 pb-6">
|
|
<div className="space-y-3">
|
|
{vendorEntries.map((cve) => {
|
|
const key = `${cve.cve_id}-${cve.vendor}`;
|
|
const documents = cveDocuments[key] || [];
|
|
const isDocExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor;
|
|
|
|
return (
|
|
<div key={cve.id} style={STYLES.vendorCard}>
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<h4 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#FFFFFF' }}>{cve.vendor}</h4>
|
|
<span style={getSeverityBadgeStyle(cve.severity)}>
|
|
<span style={STYLES.glowDot(getSeverityDotColor(cve.severity))}></span>
|
|
{cve.severity}
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '1rem', fontSize: '0.875rem', color: '#E4E8F1', fontFamily: 'monospace' }}>
|
|
<span>Status: <span style={{ fontWeight: '500', color: '#FFFFFF' }}>{cve.status}</span></span>
|
|
<span style={{ display: 'flex', alignItems: 'center', gap: '0.25rem' }}>
|
|
<FileText className="w-4 h-4" />
|
|
{cve.document_count} doc{cve.document_count !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => handleViewDocuments(cve.cve_id, cve.vendor)}
|
|
className="px-4 py-2 text-intel-accent hover:bg-intel-medium rounded border border-intel-accent/50 transition-all flex items-center gap-2 font-mono text-xs uppercase tracking-wider"
|
|
>
|
|
<Eye className="w-4 h-4" />
|
|
{isDocExpanded ? 'Hide' : 'View'}
|
|
</button>
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => handleEditCVE(cve)}
|
|
className="px-3 py-2 text-intel-warning hover:bg-intel-medium rounded border border-intel-warning/50 transition-all flex items-center gap-1"
|
|
title="Edit CVE entry"
|
|
>
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => handleDeleteCVEEntry(cve)}
|
|
className="px-3 py-2 text-intel-danger hover:bg-intel-medium rounded border border-intel-danger/50 transition-all flex items-center gap-1"
|
|
title="Delete this vendor entry"
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Documents Section */}
|
|
{isDocExpanded && (
|
|
<div className="mt-4 pt-4 border-t border-intel-accent/30">
|
|
<h5 className="text-sm font-semibold text-white mb-3 flex items-center gap-2 font-mono uppercase tracking-wider">
|
|
<FileText className="w-4 h-4 text-intel-accent" />
|
|
Documents ({documents.length})
|
|
</h5>
|
|
{documents.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{documents.map(doc => (
|
|
<div
|
|
key={doc.id}
|
|
className="document-item flex items-center justify-between"
|
|
>
|
|
<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-intel-accent rounded focus:ring-2 focus:ring-intel-accent bg-intel-dark border-intel-accent/50"
|
|
/>
|
|
<FileText className="w-5 h-5 text-intel-accent" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium text-white font-mono">{doc.name}</p>
|
|
<p className="text-xs text-gray-300 capitalize font-mono">
|
|
{doc.type} <span className="text-intel-accent">•</span> {doc.file_size}
|
|
{doc.notes && <span> <span className="text-intel-accent">•</span> {doc.notes}</span>}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<a
|
|
href={`${API_HOST}/${doc.file_path}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-3 py-1 text-sm text-intel-accent hover:bg-intel-medium rounded transition-all border border-intel-accent/50 font-mono uppercase tracking-wider"
|
|
>
|
|
View
|
|
</a>
|
|
{isAdmin() && (
|
|
<button
|
|
onClick={() => handleDeleteDocument(doc.id, cve.cve_id, cve.vendor)}
|
|
className="px-3 py-1 text-sm text-intel-danger hover:bg-intel-medium rounded transition-all border border-intel-danger/50 flex items-center gap-1 font-mono uppercase tracking-wider"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
Del
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-400 italic font-mono">No documents attached</p>
|
|
)}
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => handleFileUpload(cve.cve_id, cve.vendor)}
|
|
disabled={uploadingFile}
|
|
className="mt-3 px-4 py-2 text-sm text-gray-400 hover:text-intel-accent hover:bg-intel-medium rounded transition-all flex items-center gap-2 disabled:opacity-50 border border-gray-600 font-mono uppercase tracking-wider"
|
|
>
|
|
<Upload className="w-4 h-4" />
|
|
{uploadingFile ? 'Uploading...' : 'Upload Doc'}
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* JIRA Tickets for this vendor */}
|
|
{(() => {
|
|
const vendorTickets = jiraTickets.filter(t => t.cve_id === cve.cve_id && t.vendor === cve.vendor);
|
|
return vendorTickets.length > 0 || canWrite() ? (
|
|
<div className="mt-4 pt-4 border-t border-intel-warning/30">
|
|
<div className="flex justify-between items-center mb-3">
|
|
<h5 className="text-sm font-semibold text-white flex items-center gap-2 font-mono uppercase tracking-wider">
|
|
<AlertCircle className="w-4 h-4 text-intel-warning" />
|
|
JIRA Tickets ({vendorTickets.length})
|
|
</h5>
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => openAddTicketForCVE(cve.cve_id, cve.vendor)}
|
|
className="text-xs px-3 py-1 intel-button intel-button-primary flex items-center gap-1"
|
|
>
|
|
<Plus className="w-3 h-3" />
|
|
Add Ticket
|
|
</button>
|
|
)}
|
|
</div>
|
|
{vendorTickets.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{vendorTickets.map(ticket => (
|
|
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(255, 184, 0, 0.3)', borderRadius: '0.375rem', padding: '0.75rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.04)' }} className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3 flex-1">
|
|
<a
|
|
href={ticket.url || '#'}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-mono text-sm font-semibold text-intel-accent hover:text-intel-warning transition-colors"
|
|
>
|
|
{ticket.ticket_key}
|
|
</a>
|
|
{ticket.summary && <span style={{ fontSize: '0.875rem', color: '#E4E8F1', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '20rem' }}>{ticket.summary}</span>}
|
|
<span style={
|
|
ticket.status === 'Open' ? STYLES.badgeCritical :
|
|
ticket.status === 'In Progress' ? STYLES.badgeHigh :
|
|
STYLES.badgeLow
|
|
}>
|
|
<span style={STYLES.glowDot(
|
|
ticket.status === 'Open' ? '#FF3366' :
|
|
ticket.status === 'In Progress' ? '#FFB800' :
|
|
'#00FF88'
|
|
)}></span>
|
|
{ticket.status}
|
|
</span>
|
|
</div>
|
|
{canWrite() && (
|
|
<div className="flex gap-2">
|
|
<button onClick={() => handleEditTicket(ticket)} className="p-1 text-gray-400 hover:text-intel-warning transition-colors">
|
|
<Edit2 className="w-4 h-4" />
|
|
</button>
|
|
<button onClick={() => handleDeleteTicket(ticket)} className="p-1 text-gray-400 hover:text-intel-danger transition-colors">
|
|
<Trash2 className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-gray-400 italic font-mono">No JIRA tickets linked</p>
|
|
)}
|
|
</div>
|
|
) : null;
|
|
})()}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{Object.keys(filteredGroupedCVEs).length === 0 && !loading && (
|
|
<div className="intel-card rounded-lg p-12 text-center">
|
|
<AlertCircle className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
|
<h3 className="text-lg font-medium text-white mb-2 font-mono">No CVEs Found</h3>
|
|
<p className="text-gray-300">Try adjusting your search criteria or filters</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* End Center Panel */}
|
|
|
|
{/* RIGHT PANEL - Calendar & Open Tickets */}
|
|
<div className="col-span-12 lg:col-span-3 space-y-4">
|
|
{/* Calendar Widget */}
|
|
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '4px solid #00D9FF'}} className="rounded-lg">
|
|
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#00D9FF', marginBottom: '1rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 15px rgba(0, 217, 255, 0.5)' }}>
|
|
Calendar
|
|
</h2>
|
|
|
|
{/* Simple Calendar Grid */}
|
|
<div className="mb-2">
|
|
<div className="text-center mb-3">
|
|
<span className="text-white font-semibold font-mono">February 2024</span>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-1 text-center text-xs mb-2">
|
|
<div className="text-gray-400 font-mono">Su</div>
|
|
<div className="text-gray-400 font-mono">Mo</div>
|
|
<div className="text-gray-400 font-mono">Tu</div>
|
|
<div className="text-gray-400 font-mono">We</div>
|
|
<div className="text-gray-400 font-mono">Th</div>
|
|
<div className="text-gray-400 font-mono">Fr</div>
|
|
<div className="text-gray-400 font-mono">Sa</div>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-1 text-center">
|
|
{/* Week 1 */}
|
|
<div className="text-gray-600 font-mono text-xs p-1">28</div>
|
|
<div className="text-gray-600 font-mono text-xs p-1">29</div>
|
|
<div className="text-gray-600 font-mono text-xs p-1">30</div>
|
|
<div className="text-gray-600 font-mono text-xs p-1">31</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">1</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">2</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">3</div>
|
|
{/* Week 2 */}
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">4</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">5</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">6</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">7</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">8</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">9</div>
|
|
<div className="bg-intel-accent/30 text-white font-mono text-xs p-1 rounded font-bold border border-intel-accent">10</div>
|
|
{/* Week 3 */}
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">11</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">12</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">13</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">14</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">15</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">16</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">17</div>
|
|
{/* Week 4 */}
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">18</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">19</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">20</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">21</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">22</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">23</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">24</div>
|
|
{/* Week 5 */}
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">25</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">26</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">27</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">28</div>
|
|
<div className="text-white font-mono text-xs p-1 hover:bg-intel-accent/20 rounded cursor-pointer">29</div>
|
|
<div className="text-gray-600 font-mono text-xs p-1">1</div>
|
|
<div className="text-gray-600 font-mono text-xs p-1">2</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Open Vendor Tickets */}
|
|
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '4px solid #FFB800'}} className="rounded-lg">
|
|
<div className="flex justify-between items-center mb-4">
|
|
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#FFB800', display: 'flex', alignItems: 'center', gap: '0.5rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 15px rgba(255, 184, 0, 0.5)' }}>
|
|
<AlertCircle className="w-5 h-5" />
|
|
Open Tickets
|
|
</h2>
|
|
{canWrite() && (
|
|
<button
|
|
onClick={() => { setAddTicketContext(null); setTicketForm({ cve_id: '', vendor: '', ticket_key: '', url: '', summary: '', status: 'Open' }); setShowAddTicket(true); }}
|
|
className="intel-button intel-button-primary flex items-center gap-1 text-xs px-2 py-1"
|
|
>
|
|
<Plus className="w-3 h-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<div className="text-center mb-3">
|
|
<div style={{ fontSize: '2rem', fontWeight: '700', fontFamily: 'monospace', color: '#FFB800', textShadow: '0 0 20px rgba(255, 184, 0, 0.5)' }}>
|
|
{jiraTickets.filter(t => t.status !== 'Closed').length}
|
|
</div>
|
|
<div className="text-xs text-gray-400 uppercase tracking-wider">Active</div>
|
|
</div>
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{jiraTickets.filter(t => t.status !== 'Closed').slice(0, 10).map(ticket => (
|
|
<div key={ticket.id} style={{ background: 'linear-gradient(135deg, rgba(19, 25, 55, 0.85) 0%, rgba(30, 39, 73, 0.75) 100%)', border: '1px solid rgba(255, 184, 0, 0.3)', borderRadius: '0.375rem', padding: '0.5rem', boxShadow: '0 2px 6px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.04)' }}>
|
|
<div className="flex items-start justify-between gap-2 mb-1">
|
|
<a
|
|
href={ticket.url || '#'}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="font-mono text-xs font-semibold text-intel-accent hover:text-intel-warning transition-colors"
|
|
>
|
|
{ticket.ticket_key}
|
|
</a>
|
|
{canWrite() && (
|
|
<div className="flex gap-1">
|
|
<button onClick={() => handleEditTicket(ticket)} className="text-gray-400 hover:text-intel-warning transition-colors">
|
|
<Edit2 className="w-3 h-3" />
|
|
</button>
|
|
<button onClick={() => handleDeleteTicket(ticket)} className="text-gray-400 hover:text-intel-danger transition-colors">
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-white font-mono mb-1">{ticket.cve_id}</div>
|
|
<div className="text-xs text-gray-400">{ticket.vendor}</div>
|
|
{ticket.summary && <div className="text-xs text-gray-300 mt-1 truncate">{ticket.summary}</div>}
|
|
<div className="mt-2">
|
|
<span style={{ ...STYLES.badgeHigh, fontSize: '0.65rem', padding: '0.25rem 0.5rem' }}>
|
|
<span style={{...STYLES.glowDot('#FFB800'), width: '6px', height: '6px'}}></span>
|
|
{ticket.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{jiraTickets.filter(t => t.status !== 'Closed').length === 0 && (
|
|
<div className="text-center py-8">
|
|
<CheckCircle className="w-8 h-8 text-intel-success mx-auto mb-2" />
|
|
<p className="text-sm text-gray-400 italic font-mono">No open tickets</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* End Right Panel */}
|
|
|
|
</div>
|
|
{/* End Three Column Layout */}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|