const state = {
currentVendor: null,
currentPage: 1,
itemsPerPage: 50,
filters: { severity: '', year: '' },
charts: { trend: null, severity: null }
};
function updateThemeIcon(theme) {
const icon = document.querySelector('#darkModeToggle i');
if (icon) icon.className = theme === 'dark' ? 'fas fa-sun' : 'fas fa-moon';
}
function toggleDarkMode() {
const currentTheme = document.documentElement.getAttribute('data-bs-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-bs-theme', newTheme);
localStorage.setItem('theme', newTheme);
updateThemeIcon(newTheme);
if (state.currentVendor) {
fetch(`/api/stats/${state.currentVendor}`)
.then(r => r.json())
.then(data => updateCharts(data.stats))
.catch(console.error);
}
}
function getSeverityIcon(severity) {
const icons = {
'CRITICAL': '',
'HIGH': '',
'MEDIUM': '',
'LOW': ''
};
return icons[severity] || '';
}
function formatDate(dateString) {
if (!dateString) return 'N/A';
return new Date(dateString).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
}
function formatMonth(monthString) {
const [year, month] = monthString.split('-');
return new Date(year, month - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'short' });
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function updateLastUpdate() {
const elem = document.getElementById('lastUpdate');
if (elem) elem.innerHTML = `Last update: ${new Date().toLocaleTimeString()}`;
}
function showLoading() {
const tbody = document.getElementById('cveTableBody');
if (tbody) tbody.innerHTML = `
Loading... |
`;
}
function showError(message) {
console.error(message);
alert(message);
}
document.addEventListener('DOMContentLoaded', () => {
initializeApp();
setupEventListeners();
loadVendors();
});
function initializeApp() {
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-bs-theme', savedTheme);
updateThemeIcon(savedTheme);
}
function setupEventListeners() {
const handlers = {
'darkModeToggle': toggleDarkMode,
'refreshBtn': () => state.currentVendor && loadVendorData(state.currentVendor, true),
'clearFilters': clearFiltersHandler,
'exportJSON': () => exportData('json'),
'exportCSV': () => exportData('csv'),
'searchBtn': performSearch
};
Object.entries(handlers).forEach(([id, handler]) => {
const elem = document.getElementById(id);
if (elem) elem.addEventListener('click', handler);
});
const filterForm = document.getElementById('filterForm');
if (filterForm) filterForm.addEventListener('submit', (e) => { e.preventDefault(); applyFilters(); });
const searchInput = document.getElementById('searchInput');
if (searchInput) searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') performSearch(); });
const prevPage = document.getElementById('prevPage');
const nextPage = document.getElementById('nextPage');
if (prevPage) prevPage.querySelector('a')?.addEventListener('click', (e) => { e.preventDefault(); changePage(-1); });
if (nextPage) nextPage.querySelector('a')?.addEventListener('click', (e) => { e.preventDefault(); changePage(1); });
}
async function loadVendors() {
try {
const response = await fetch('/api/vendors');
const data = await response.json();
if (data.vendors) {
renderVendorList(data.vendors);
renderVendorDropdown(data.vendors);
if (data.vendors.length > 0) loadVendorData(data.vendors[0].code);
}
} catch (error) {
console.error('Error loading vendors:', error);
showError('Failed to load vendors list');
}
}
function renderVendorList(vendors) {
const vendorList = document.getElementById('vendorList');
if (!vendorList) return;
vendorList.innerHTML = vendors.map(vendor => `
${vendor.name}
${vendor.total || 0}
`).join('');
vendorList.querySelectorAll('a[data-vendor]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
loadVendorData(link.getAttribute('data-vendor'));
vendorList.querySelectorAll('.nav-link').forEach(l => l.classList.remove('active'));
link.classList.add('active');
});
});
}
function renderVendorDropdown(vendors) {
const dropdown = document.getElementById('vendorDropdown');
if (!dropdown) return;
dropdown.innerHTML = vendors.map(vendor => `
${vendor.name}
`).join('');
dropdown.querySelectorAll('a[data-vendor]').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
loadVendorData(link.getAttribute('data-vendor'));
});
});
}
async function loadVendorData(vendorCode, forceRefresh = false) {
state.currentVendor = vendorCode;
state.currentPage = 1;
showLoading();
try {
const [cvesResponse, statsResponse] = await Promise.all([
fetch(`/api/cve/${vendorCode}?limit=${state.itemsPerPage}&offset=0`),
fetch(`/api/stats/${vendorCode}`)
]);
const cvesData = await cvesResponse.json();
const statsData = await statsResponse.json();
updateTitle(vendorCode);
updateStats(statsData.stats);
renderCVETable(cvesData.cves);
updateCharts(statsData.stats);
updateLastUpdate();
} catch (error) {
console.error('Error loading vendor data:', error);
showError('Failed to load CVE data');
}
}
function updateTitle(vendorCode) {
const vendors = {
'microsoft': 'Microsoft', 'apple': 'Apple', 'fortinet': 'Fortinet',
'cisco': 'Cisco', 'adobe': 'Adobe', 'oracle': 'Oracle',
'google': 'Google', 'linux': 'Linux Kernel', 'vmware': 'VMware',
'paloalto': 'Palo Alto Networks', 'docker': 'Docker', 'kubernetes': 'Kubernetes'
};
const mainTitle = document.getElementById('mainTitle');
if (mainTitle) mainTitle.innerHTML = `${vendors[vendorCode] || vendorCode} CVEs`;
}
function updateStats(stats) {
const elements = {
statTotal: stats.total || 0,
statCritical: stats.severity?.CRITICAL || 0,
statHigh: stats.severity?.HIGH || 0,
statMonth: stats.this_month || 0
};
Object.entries(elements).forEach(([id, value]) => {
const elem = document.getElementById(id);
if (elem) elem.textContent = value;
});
}
function renderCVETable(cves) {
const tbody = document.getElementById('cveTableBody');
if (!tbody) return;
if (!cves || cves.length === 0) {
tbody.innerHTML = `No CVEs found |
`;
return;
}
tbody.innerHTML = cves.map(cve => `
| ${cve.cve_id} |
${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'} |
${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'} |
${escapeHtml(cve.description || 'No description available')} |
${formatDate(cve.published_date)} |
`).join('');
if (cves.length >= state.itemsPerPage) {
const paginationNav = document.getElementById('paginationNav');
if (paginationNav) paginationNav.classList.remove('d-none');
}
}
async function showCVEDetails(cveId) {
const modalElement = document.getElementById('cveModal');
if (!modalElement) return;
const modal = new bootstrap.Modal(modalElement);
const modalTitle = document.getElementById('cveModalTitle');
const modalBody = document.getElementById('cveModalBody');
if (modalTitle) modalTitle.textContent = cveId;
if (modalBody) modalBody.innerHTML = '';
modal.show();
try {
const response = await fetch(`/api/cve/${state.currentVendor}`);
const data = await response.json();
const cve = data.cves.find(c => c.cve_id === cveId);
if (cve && modalBody) {
const references = cve.references ? JSON.parse(cve.references) : [];
const cweIds = cve.cwe_ids ? JSON.parse(cve.cwe_ids) : [];
modalBody.innerHTML = `
Severity
${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'}
CVSS Score
${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}
${cve.cvss_vector ? `
${cve.cvss_vector}` : ''}
Description
${escapeHtml(cve.description || 'N/A')}
Published
${formatDate(cve.published_date)}
Modified
${formatDate(cve.last_modified)}
${cweIds.length > 0 ? `CWE IDs
${cweIds.map(cwe => `${cwe}`).join('')}
` : ''}
${references.length > 0 ? `References
${references.slice(0, 5).map(ref => `- ${ref}
`).join('')}
` : ''}
`;
}
} catch (error) {
console.error('Error loading CVE details:', error);
if (modalBody) modalBody.innerHTML = 'Error loading CVE details
';
}
}
function updateCharts(stats) {
updateTrendChart(stats.monthly || {});
updateSeverityChart(stats.severity || {});
}
function updateTrendChart(monthlyData) {
const ctx = document.getElementById('trendChart');
if (!ctx) return;
if (state.charts.trend) state.charts.trend.destroy();
const months = Object.keys(monthlyData).sort();
state.charts.trend = new Chart(ctx, {
type: 'bar',
data: {
labels: months.map(m => formatMonth(m)),
datasets: [{
label: 'CVE Count',
data: months.map(m => monthlyData[m]),
backgroundColor: 'rgba(13, 110, 253, 0.5)',
borderColor: 'rgba(13, 110, 253, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true } }
}
});
}
function updateSeverityChart(severityData) {
const ctx = document.getElementById('severityChart');
if (!ctx) return;
if (state.charts.severity) state.charts.severity.destroy();
state.charts.severity = new Chart(ctx, {
type: 'pie',
data: {
labels: ['Critical', 'High', 'Medium', 'Low'],
datasets: [{
data: [severityData.CRITICAL || 0, severityData.HIGH || 0, severityData.MEDIUM || 0, severityData.LOW || 0],
backgroundColor: ['rgba(220, 53, 69, 0.8)', 'rgba(253, 126, 20, 0.8)', 'rgba(13, 202, 240, 0.8)', 'rgba(108, 117, 125, 0.8)']
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'bottom' } }
}
});
}
async function applyFilters() {
state.filters.severity = document.getElementById('filterSeverity')?.value || '';
state.filters.year = document.getElementById('filterYear')?.value || '';
state.currentPage = 1;
showLoading();
try {
let url = `/api/cve/${state.currentVendor}/filter?limit=${state.itemsPerPage}&offset=0`;
if (state.filters.severity) url += `&severity=${state.filters.severity}`;
if (state.filters.year) url += `&year=${state.filters.year}`;
const response = await fetch(url);
const data = await response.json();
renderCVETable(data.cves);
} catch (error) {
console.error('Error applying filters:', error);
showError('Failed to apply filters');
}
}
function clearFiltersHandler() {
const severityFilter = document.getElementById('filterSeverity');
const yearFilter = document.getElementById('filterYear');
if (severityFilter) severityFilter.value = '';
if (yearFilter) yearFilter.value = '';
state.filters = { severity: '', year: '' };
if (state.currentVendor) loadVendorData(state.currentVendor);
}
function changePage(delta) {
state.currentPage += delta;
if (state.currentPage < 1) state.currentPage = 1;
const offset = (state.currentPage - 1) * state.itemsPerPage;
fetch(`/api/cve/${state.currentVendor}?limit=${state.itemsPerPage}&offset=${offset}`)
.then(r => r.json())
.then(data => {
renderCVETable(data.cves);
const currentPage = document.getElementById('currentPage');
if (currentPage) currentPage.textContent = state.currentPage;
})
.catch(console.error);
}
async function performSearch() {
const searchInput = document.getElementById('searchInput');
if (!searchInput) return;
const query = searchInput.value.trim();
const resultsDiv = document.getElementById('searchResults');
if (!resultsDiv) return;
if (query.length < 3) {
resultsDiv.innerHTML = 'Please enter at least 3 characters
';
return;
}
resultsDiv.innerHTML = '';
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
resultsDiv.innerHTML = ``;
} else {
resultsDiv.innerHTML = 'No results found
';
}
} catch (error) {
console.error('Error searching:', error);
resultsDiv.innerHTML = 'Search failed
';
}
}
function exportData(format) {
if (!state.currentVendor) return;
window.open(`/api/export/${state.currentVendor}/${format}`, '_blank');
}