Implemented comprehensive scan comparison functionality with historical analysis and improved user experience for scan triggering. Features Added: - Scan comparison engine with ports, services, and certificates analysis - Drift score calculation (0.0-1.0 scale) for infrastructure changes - Side-by-side comparison UI with color-coded changes (added/removed/changed) - Historical trend charts showing port counts over time - "Compare with Previous" button on scan detail pages - Scan history API endpoint for trending data API Endpoints: - GET /api/scans/<id1>/compare/<id2> - Compare two scans - GET /api/stats/scan-history/<id> - Historical scan data for charts UI Improvements: - Replaced config file text inputs with dropdown selectors - Added config file selection to dashboard and scans pages - Improved delete scan confirmation with proper async handling - Enhanced error messages with detailed validation feedback - Added 2-second delay before redirect to ensure deletion completes Comparison Features: - Port changes: tracks added, removed, and unchanged ports - Service changes: detects version updates and service modifications - Certificate changes: monitors SSL/TLS certificate updates - Interactive historical charts with clickable data points - Automatic detection of previous scan for comparison Bug Fixes: - Fixed scan deletion UI alert appearing on successful deletion - Prevented config file path duplication (configs/configs/...) - Improved error handling for failed API responses - Added proper JSON response parsing with fallback handling Testing: - Created comprehensive test suite for comparison functionality - Tests cover comparison API, service methods, and drift scoring - Added edge case tests for identical scans and missing data
527 lines
24 KiB
HTML
527 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Compare Scans - SneakyScanner{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mt-4">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<a href="{{ url_for('main.scans') }}" class="text-muted text-decoration-none mb-2 d-inline-block">
|
|
← Back to All Scans
|
|
</a>
|
|
<h1 style="color: #60a5fa;">Scan Comparison</h1>
|
|
<p class="text-muted">Comparing Scan #{{ scan_id1 }} vs Scan #{{ scan_id2 }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div id="comparison-loading" class="text-center py-5">
|
|
<div class="spinner-border" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted">Loading comparison...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div id="comparison-error" class="alert alert-danger" style="display: none;"></div>
|
|
|
|
<!-- Comparison Content -->
|
|
<div id="comparison-content" style="display: none;">
|
|
<!-- Drift Score Card -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0" style="color: #60a5fa;">Infrastructure Drift Analysis</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-3">
|
|
<div class="text-center">
|
|
<div class="display-4 mb-2" id="drift-score" style="color: #60a5fa;">-</div>
|
|
<div class="text-muted">Drift Score</div>
|
|
<small class="text-muted d-block mt-1">(0.0 = identical, 1.0 = completely different)</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-9">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Older Scan (#<span id="scan1-id"></span>)</label>
|
|
<div id="scan1-title" class="fw-bold">-</div>
|
|
<small class="text-muted" id="scan1-timestamp">-</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Newer Scan (#<span id="scan2-id"></span>)</label>
|
|
<div id="scan2-title" class="fw-bold">-</div>
|
|
<small class="text-muted" id="scan2-timestamp">-</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Quick Actions</label>
|
|
<div>
|
|
<a href="/scans/{{ scan_id1 }}" class="btn btn-sm btn-secondary">View Scan #{{ scan_id1 }}</a>
|
|
<a href="/scans/{{ scan_id2 }}" class="btn btn-sm btn-secondary">View Scan #{{ scan_id2 }}</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ports Comparison -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0" style="color: #60a5fa;">
|
|
<i class="bi bi-hdd-network"></i> Port Changes
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #065f46; border-color: #6ee7b7;">
|
|
<div class="stat-value" id="ports-added-count">0</div>
|
|
<div class="stat-label">Ports Added</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #7f1d1d; border-color: #fca5a5;">
|
|
<div class="stat-value" id="ports-removed-count">0</div>
|
|
<div class="stat-label">Ports Removed</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="ports-unchanged-count">0</div>
|
|
<div class="stat-label">Ports Unchanged</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Added Ports -->
|
|
<div id="ports-added-section" style="display: none;">
|
|
<h6 class="text-success mb-2"><i class="bi bi-plus-circle"></i> Added Ports</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Port</th>
|
|
<th>Protocol</th>
|
|
<th>State</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ports-added-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Removed Ports -->
|
|
<div id="ports-removed-section" style="display: none;">
|
|
<h6 class="text-danger mb-2"><i class="bi bi-dash-circle"></i> Removed Ports</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Port</th>
|
|
<th>Protocol</th>
|
|
<th>State</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="ports-removed-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Services Comparison -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0" style="color: #60a5fa;">
|
|
<i class="bi bi-gear"></i> Service Changes
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #065f46; border-color: #6ee7b7;">
|
|
<div class="stat-value" id="services-added-count">0</div>
|
|
<div class="stat-label">Services Added</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #7f1d1d; border-color: #fca5a5;">
|
|
<div class="stat-value" id="services-removed-count">0</div>
|
|
<div class="stat-label">Services Removed</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #78350f; border-color: #fcd34d;">
|
|
<div class="stat-value" id="services-changed-count">0</div>
|
|
<div class="stat-label">Services Changed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Changed Services -->
|
|
<div id="services-changed-section" style="display: none;">
|
|
<h6 class="text-warning mb-2"><i class="bi bi-arrow-left-right"></i> Changed Services</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP:Port</th>
|
|
<th>Old Service</th>
|
|
<th>New Service</th>
|
|
<th>Old Version</th>
|
|
<th>New Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="services-changed-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Added Services -->
|
|
<div id="services-added-section" style="display: none;">
|
|
<h6 class="text-success mb-2"><i class="bi bi-plus-circle"></i> Added Services</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Port</th>
|
|
<th>Service</th>
|
|
<th>Product</th>
|
|
<th>Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="services-added-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Removed Services -->
|
|
<div id="services-removed-section" style="display: none;">
|
|
<h6 class="text-danger mb-2"><i class="bi bi-dash-circle"></i> Removed Services</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP Address</th>
|
|
<th>Port</th>
|
|
<th>Service</th>
|
|
<th>Product</th>
|
|
<th>Version</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="services-removed-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Certificates Comparison -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0" style="color: #60a5fa;">
|
|
<i class="bi bi-shield-lock"></i> Certificate Changes
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #065f46; border-color: #6ee7b7;">
|
|
<div class="stat-value" id="certs-added-count">0</div>
|
|
<div class="stat-label">Certificates Added</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #7f1d1d; border-color: #fca5a5;">
|
|
<div class="stat-value" id="certs-removed-count">0</div>
|
|
<div class="stat-label">Certificates Removed</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stat-card" style="background-color: #78350f; border-color: #fcd34d;">
|
|
<div class="stat-value" id="certs-changed-count">0</div>
|
|
<div class="stat-label">Certificates Changed</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Changed Certificates -->
|
|
<div id="certs-changed-section" style="display: none;">
|
|
<h6 class="text-warning mb-2"><i class="bi bi-arrow-left-right"></i> Changed Certificates</h6>
|
|
<div class="table-responsive mb-3">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>IP:Port</th>
|
|
<th>Old Subject</th>
|
|
<th>New Subject</th>
|
|
<th>Old Expiry</th>
|
|
<th>New Expiry</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="certs-changed-tbody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Added/Removed Certificates (shown if any) -->
|
|
<div id="certs-added-removed-info" style="display: none;">
|
|
<p class="text-muted mb-0">
|
|
<i class="bi bi-info-circle"></i>
|
|
Additional certificate additions and removals correspond to the port changes shown above.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const scanId1 = {{ scan_id1 }};
|
|
const scanId2 = {{ scan_id2 }};
|
|
|
|
// Load comparison data
|
|
async function loadComparison() {
|
|
const loadingDiv = document.getElementById('comparison-loading');
|
|
const errorDiv = document.getElementById('comparison-error');
|
|
const contentDiv = document.getElementById('comparison-content');
|
|
|
|
try {
|
|
const response = await fetch(`/api/scans/${scanId1}/compare/${scanId2}`);
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load comparison');
|
|
}
|
|
|
|
const data = await response.json();
|
|
|
|
// Hide loading, show content
|
|
loadingDiv.style.display = 'none';
|
|
contentDiv.style.display = 'block';
|
|
|
|
// Populate comparison UI
|
|
populateComparison(data);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading comparison:', error);
|
|
loadingDiv.style.display = 'none';
|
|
errorDiv.textContent = `Error: ${error.message}`;
|
|
errorDiv.style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function populateComparison(data) {
|
|
// Drift score
|
|
const driftScore = data.drift_score || 0;
|
|
document.getElementById('drift-score').textContent = driftScore.toFixed(3);
|
|
|
|
// Color code drift score
|
|
const driftElement = document.getElementById('drift-score');
|
|
if (driftScore < 0.1) {
|
|
driftElement.style.color = '#6ee7b7'; // Green - minimal drift
|
|
} else if (driftScore < 0.3) {
|
|
driftElement.style.color = '#fcd34d'; // Yellow - moderate drift
|
|
} else {
|
|
driftElement.style.color = '#fca5a5'; // Red - significant drift
|
|
}
|
|
|
|
// Scan metadata
|
|
document.getElementById('scan1-id').textContent = data.scan1.id;
|
|
document.getElementById('scan1-title').textContent = data.scan1.title || 'Untitled Scan';
|
|
document.getElementById('scan1-timestamp').textContent = new Date(data.scan1.timestamp).toLocaleString();
|
|
|
|
document.getElementById('scan2-id').textContent = data.scan2.id;
|
|
document.getElementById('scan2-title').textContent = data.scan2.title || 'Untitled Scan';
|
|
document.getElementById('scan2-timestamp').textContent = new Date(data.scan2.timestamp).toLocaleString();
|
|
|
|
// Ports comparison
|
|
populatePortsComparison(data.ports);
|
|
|
|
// Services comparison
|
|
populateServicesComparison(data.services);
|
|
|
|
// Certificates comparison
|
|
populateCertificatesComparison(data.certificates);
|
|
}
|
|
|
|
function populatePortsComparison(ports) {
|
|
const addedCount = ports.added.length;
|
|
const removedCount = ports.removed.length;
|
|
const unchangedCount = ports.unchanged.length;
|
|
|
|
document.getElementById('ports-added-count').textContent = addedCount;
|
|
document.getElementById('ports-removed-count').textContent = removedCount;
|
|
document.getElementById('ports-unchanged-count').textContent = unchangedCount;
|
|
|
|
// Show added ports
|
|
if (addedCount > 0) {
|
|
document.getElementById('ports-added-section').style.display = 'block';
|
|
const tbody = document.getElementById('ports-added-tbody');
|
|
tbody.innerHTML = '';
|
|
ports.added.forEach(port => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td>${port.ip}</td>
|
|
<td class="mono">${port.port}</td>
|
|
<td>${port.protocol.toUpperCase()}</td>
|
|
<td>${port.state}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Show removed ports
|
|
if (removedCount > 0) {
|
|
document.getElementById('ports-removed-section').style.display = 'block';
|
|
const tbody = document.getElementById('ports-removed-tbody');
|
|
tbody.innerHTML = '';
|
|
ports.removed.forEach(port => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td>${port.ip}</td>
|
|
<td class="mono">${port.port}</td>
|
|
<td>${port.protocol.toUpperCase()}</td>
|
|
<td>${port.state}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
}
|
|
|
|
function populateServicesComparison(services) {
|
|
const addedCount = services.added.length;
|
|
const removedCount = services.removed.length;
|
|
const changedCount = services.changed.length;
|
|
|
|
document.getElementById('services-added-count').textContent = addedCount;
|
|
document.getElementById('services-removed-count').textContent = removedCount;
|
|
document.getElementById('services-changed-count').textContent = changedCount;
|
|
|
|
// Show changed services
|
|
if (changedCount > 0) {
|
|
document.getElementById('services-changed-section').style.display = 'block';
|
|
const tbody = document.getElementById('services-changed-tbody');
|
|
tbody.innerHTML = '';
|
|
services.changed.forEach(svc => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td class="mono">${svc.ip}:${svc.port}</td>
|
|
<td>${svc.old.service_name || '-'}</td>
|
|
<td class="text-warning">${svc.new.service_name || '-'}</td>
|
|
<td>${svc.old.version || '-'}</td>
|
|
<td class="text-warning">${svc.new.version || '-'}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Show added services
|
|
if (addedCount > 0) {
|
|
document.getElementById('services-added-section').style.display = 'block';
|
|
const tbody = document.getElementById('services-added-tbody');
|
|
tbody.innerHTML = '';
|
|
services.added.forEach(svc => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td>${svc.ip}</td>
|
|
<td class="mono">${svc.port}</td>
|
|
<td>${svc.service_name || '-'}</td>
|
|
<td>${svc.product || '-'}</td>
|
|
<td>${svc.version || '-'}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Show removed services
|
|
if (removedCount > 0) {
|
|
document.getElementById('services-removed-section').style.display = 'block';
|
|
const tbody = document.getElementById('services-removed-tbody');
|
|
tbody.innerHTML = '';
|
|
services.removed.forEach(svc => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td>${svc.ip}</td>
|
|
<td class="mono">${svc.port}</td>
|
|
<td>${svc.service_name || '-'}</td>
|
|
<td>${svc.product || '-'}</td>
|
|
<td>${svc.version || '-'}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
}
|
|
|
|
function populateCertificatesComparison(certs) {
|
|
const addedCount = certs.added.length;
|
|
const removedCount = certs.removed.length;
|
|
const changedCount = certs.changed.length;
|
|
|
|
document.getElementById('certs-added-count').textContent = addedCount;
|
|
document.getElementById('certs-removed-count').textContent = removedCount;
|
|
document.getElementById('certs-changed-count').textContent = changedCount;
|
|
|
|
// Show changed certificates
|
|
if (changedCount > 0) {
|
|
document.getElementById('certs-changed-section').style.display = 'block';
|
|
const tbody = document.getElementById('certs-changed-tbody');
|
|
tbody.innerHTML = '';
|
|
certs.changed.forEach(cert => {
|
|
const row = document.createElement('tr');
|
|
row.classList.add('scan-row');
|
|
row.innerHTML = `
|
|
<td class="mono">${cert.ip}:${cert.port}</td>
|
|
<td>${cert.old.subject || '-'}</td>
|
|
<td class="text-warning">${cert.new.subject || '-'}</td>
|
|
<td>${cert.old.not_valid_after ? new Date(cert.old.not_valid_after).toLocaleDateString() : '-'}</td>
|
|
<td class="text-warning">${cert.new.not_valid_after ? new Date(cert.new.not_valid_after).toLocaleDateString() : '-'}</td>
|
|
`;
|
|
tbody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Show info if there are added/removed certs
|
|
if (addedCount > 0 || removedCount > 0) {
|
|
document.getElementById('certs-added-removed-info').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
// Load comparison on page load
|
|
loadComparison();
|
|
</script>
|
|
{% endblock %}
|