first commit

This commit is contained in:
Mateusz Gruszczyński
2026-02-13 12:42:53 +01:00
commit bc1b4279de
21 changed files with 3835 additions and 0 deletions

264
static/css/style.css Normal file
View File

@@ -0,0 +1,264 @@
/* CVE Monitor - Custom Styles */
:root {
--sidebar-width: 250px;
--navbar-height: 56px;
}
body {
font-size: 0.95rem;
min-height: 100vh;
}
/* Sidebar */
.sidebar {
position: fixed;
top: var(--navbar-height);
bottom: 0;
left: 0;
z-index: 100;
padding: 0;
box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1);
overflow-y: auto;
}
.sidebar .nav-link {
font-weight: 500;
color: #333;
padding: 0.75rem 1rem;
border-left: 3px solid transparent;
transition: all 0.2s;
}
.sidebar .nav-link:hover {
color: #0d6efd;
background-color: rgba(0, 0, 0, 0.05);
}
.sidebar .nav-link.active {
color: #0d6efd;
border-left-color: #0d6efd;
background-color: rgba(13, 110, 253, 0.1);
}
.sidebar .nav-link i {
margin-right: 0.5rem;
width: 20px;
text-align: center;
}
.sidebar-heading {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.05em;
}
/* Dark mode */
[data-bs-theme="dark"] .sidebar {
box-shadow: inset -1px 0 0 rgba(255, 255, 255, 0.1);
}
[data-bs-theme="dark"] .sidebar .nav-link {
color: #dee2e6;
}
[data-bs-theme="dark"] .sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.1);
}
[data-bs-theme="dark"] .sidebar .nav-link.active {
background-color: rgba(13, 110, 253, 0.2);
}
/* Main content */
main {
margin-left: var(--sidebar-width);
padding-top: 1rem;
}
@media (max-width: 767.98px) {
.sidebar {
position: static;
height: auto;
}
main {
margin-left: 0;
}
}
/* Cards */
.card {
border-radius: 0.5rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
margin-bottom: 1rem;
}
.card-body {
padding: 1.25rem;
}
/* Stats cards */
.card .fs-1 {
opacity: 0.2;
}
.card:hover .fs-1 {
opacity: 0.3;
transition: opacity 0.3s;
}
/* Table */
.table {
font-size: 0.9rem;
}
.table thead th {
border-bottom: 2px solid #dee2e6;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 0.05em;
color: #6c757d;
}
.table tbody tr {
cursor: pointer;
transition: background-color 0.2s;
}
.table tbody tr:hover {
background-color: rgba(0, 0, 0, 0.02);
}
[data-bs-theme="dark"] .table tbody tr:hover {
background-color: rgba(255, 255, 255, 0.05);
}
/* Severity badges */
.badge-critical {
background-color: #dc3545;
color: white;
}
.badge-high {
background-color: #fd7e14;
color: white;
}
.badge-medium {
background-color: #0dcaf0;
color: black;
}
.badge-low {
background-color: #6c757d;
color: white;
}
/* CVE ID column */
.cve-id {
font-family: 'Courier New', monospace;
font-weight: 600;
color: #0d6efd;
}
/* Loading spinner overlay */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
[data-bs-theme="dark"] .loading-overlay {
background: rgba(0, 0, 0, 0.8);
}
/* Pagination */
.pagination {
margin-top: 1rem;
}
/* Search results */
#searchResults .list-group-item {
cursor: pointer;
transition: background-color 0.2s;
}
#searchResults .list-group-item:hover {
background-color: rgba(0, 0, 0, 0.05);
}
/* Charts */
canvas {
max-height: 300px;
}
/* Responsive */
@media (max-width: 575.98px) {
.btn-toolbar {
flex-direction: column;
align-items: flex-start !important;
}
.btn-group {
margin-bottom: 0.5rem;
}
}
/* Scrollbar customization */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
[data-bs-theme="dark"] ::-webkit-scrollbar-track {
background: #2d2d2d;
}
[data-bs-theme="dark"] ::-webkit-scrollbar-thumb {
background: #666;
}
/* Animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.3s ease-in;
}
/* Vendor badge counter */
.vendor-badge {
font-size: 0.7rem;
padding: 0.25rem 0.5rem;
border-radius: 10px;
}

425
static/js/app.js Normal file
View 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');
}