first commit
This commit is contained in:
425
static/js/app.js
Normal file
425
static/js/app.js
Normal file
@@ -0,0 +1,425 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user