Files
cve_monitor/static/js/app.js
Mateusz Gruszczyński bc1b4279de first commit
2026-02-13 12:42:53 +01:00

426 lines
16 KiB
JavaScript

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': '<i class="fas fa-skull-crossbones"></i>',
'HIGH': '<i class="fas fa-exclamation-triangle"></i>',
'MEDIUM': '<i class="fas fa-exclamation-circle"></i>',
'LOW': '<i class="fas fa-info-circle"></i>'
};
return icons[severity] || '<i class="fas fa-question"></i>';
}
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 = `<i class="fas fa-clock me-1"></i>Last update: ${new Date().toLocaleTimeString()}`;
}
function showLoading() {
const tbody = document.getElementById('cveTableBody');
if (tbody) tbody.innerHTML = `<tr><td colspan="5" class="text-center py-4"><div class="spinner-border text-primary"></div><p class="mt-2 text-muted">Loading...</p></td></tr>`;
}
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 => `
<li class="nav-item">
<a class="nav-link" href="#" data-vendor="${vendor.code}">
${vendor.name}
<span class="badge bg-primary float-end">${vendor.total || 0}</span>
</a>
</li>
`).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 => `
<li><a class="dropdown-item" href="#" data-vendor="${vendor.code}">${vendor.name}</a></li>
`).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 = `<i class="fas fa-shield-alt text-primary me-2"></i>${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 = `<tr><td colspan="5" class="text-center py-4 text-muted"><i class="fas fa-inbox fa-3x mb-3"></i><p>No CVEs found</p></td></tr>`;
return;
}
tbody.innerHTML = cves.map(cve => `
<tr onclick="showCVEDetails('${cve.cve_id}')" style="cursor: pointer;">
<td><span class="cve-id">${cve.cve_id}</span></td>
<td><span class="badge badge-${(cve.severity || 'unknown').toLowerCase()}">${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'}</span></td>
<td><strong>${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}</strong></td>
<td><div class="text-truncate" style="max-width: 400px;">${escapeHtml(cve.description || 'No description available')}</div></td>
<td><small class="text-muted">${formatDate(cve.published_date)}</small></td>
</tr>
`).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 = '<div class="text-center"><div class="spinner-border"></div></div>';
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 = `
<div class="row">
<div class="col-md-6 mb-3">
<h6>Severity</h6>
<span class="badge badge-${(cve.severity || 'unknown').toLowerCase()} fs-6">${getSeverityIcon(cve.severity)} ${cve.severity || 'UNKNOWN'}</span>
</div>
<div class="col-md-6 mb-3">
<h6>CVSS Score</h6>
<p class="fs-4 mb-0"><strong>${cve.cvss_score ? cve.cvss_score.toFixed(1) : 'N/A'}</strong></p>
${cve.cvss_vector ? `<small class="text-muted">${cve.cvss_vector}</small>` : ''}
</div>
</div>
<div class="mb-3"><h6>Description</h6><p>${escapeHtml(cve.description || 'N/A')}</p></div>
<div class="row">
<div class="col-md-6 mb-3"><h6>Published</h6><p>${formatDate(cve.published_date)}</p></div>
<div class="col-md-6 mb-3"><h6>Modified</h6><p>${formatDate(cve.last_modified)}</p></div>
</div>
${cweIds.length > 0 ? `<div class="mb-3"><h6>CWE IDs</h6><p>${cweIds.map(cwe => `<span class="badge bg-secondary me-1">${cwe}</span>`).join('')}</p></div>` : ''}
${references.length > 0 ? `<div class="mb-3"><h6>References</h6><ul class="list-unstyled">${references.slice(0, 5).map(ref => `<li><a href="${ref}" target="_blank"><i class="fas fa-external-link-alt me-1"></i>${ref}</a></li>`).join('')}</ul></div>` : ''}
`;
}
} catch (error) {
console.error('Error loading CVE details:', error);
if (modalBody) modalBody.innerHTML = '<p class="text-danger">Error loading CVE details</p>';
}
}
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 = '<p class="text-muted">Please enter at least 3 characters</p>';
return;
}
resultsDiv.innerHTML = '<div class="text-center"><div class="spinner-border"></div></div>';
try {
const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.results && data.results.length > 0) {
resultsDiv.innerHTML = `<div class="list-group">${data.results.map(cve => `
<a href="#" class="list-group-item list-group-item-action" onclick="showCVEDetails('${cve.cve_id}'); return false;">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1 cve-id">${cve.cve_id}</h6>
<span class="badge badge-${(cve.severity || 'unknown').toLowerCase()}">${cve.severity}</span>
</div>
<p class="mb-1 small">${escapeHtml(cve.description.substring(0, 150))}...</p>
<small class="text-muted">${formatDate(cve.published_date)}</small>
</a>`).join('')}</div>`;
} else {
resultsDiv.innerHTML = '<p class="text-muted">No results found</p>';
}
} catch (error) {
console.error('Error searching:', error);
resultsDiv.innerHTML = '<p class="text-danger">Search failed</p>';
}
}
function exportData(format) {
if (!state.currentVendor) return;
window.open(`/api/export/${state.currentVendor}/${format}`, '_blank');
}