628 lines
29 KiB
HTML
628 lines
29 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}AUTARCH — Vulnerability Scanner{% endblock %}
|
||
|
|
{% block content %}
|
||
|
|
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||
|
|
<div>
|
||
|
|
<h1>Vulnerability Scanner</h1>
|
||
|
|
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||
|
|
Port scanning, CVE matching, default credential checking, header & SSL analysis, Nuclei integration.
|
||
|
|
</p>
|
||
|
|
</div>
|
||
|
|
<a href="{{ url_for('offense.index') }}" class="btn btn-sm" style="margin-left:auto">← Offense</a>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Tab Bar -->
|
||
|
|
<div class="tab-bar">
|
||
|
|
<button class="tab active" data-tab-group="vs" data-tab="scan" onclick="showTab('vs','scan')">Scan</button>
|
||
|
|
<button class="tab" data-tab-group="vs" data-tab="templates" onclick="showTab('vs','templates');vsLoadTemplates()">Templates</button>
|
||
|
|
<button class="tab" data-tab-group="vs" data-tab="results" onclick="showTab('vs','results');vsLoadScans()">Results</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== SCAN TAB ==================== -->
|
||
|
|
<div class="tab-content active" data-tab-group="vs" data-tab="scan">
|
||
|
|
<div class="section">
|
||
|
|
<h2>New Scan</h2>
|
||
|
|
<div style="display:grid;grid-template-columns:1fr auto;gap:1rem;align-items:end;max-width:900px">
|
||
|
|
<div>
|
||
|
|
<div class="form-group" style="margin-bottom:0.75rem">
|
||
|
|
<label>Target (IP / hostname / URL)</label>
|
||
|
|
<input type="text" id="vs-target" class="form-control" placeholder="192.168.1.1 or example.com" onkeypress="if(event.key==='Enter')vsStartScan()">
|
||
|
|
</div>
|
||
|
|
<div style="display:flex;gap:0.75rem;align-items:end;flex-wrap:wrap">
|
||
|
|
<div class="form-group" style="margin:0;min-width:140px">
|
||
|
|
<label>Profile</label>
|
||
|
|
<select id="vs-profile" class="form-control" onchange="vsProfileChanged()">
|
||
|
|
<option value="quick">Quick</option>
|
||
|
|
<option value="standard" selected>Standard</option>
|
||
|
|
<option value="full">Full</option>
|
||
|
|
<option value="custom">Custom</option>
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div class="form-group" style="margin:0;flex:1;min-width:180px">
|
||
|
|
<label>Ports <span style="color:var(--text-muted);font-size:0.75rem">(optional, overrides profile)</span></label>
|
||
|
|
<input type="text" id="vs-ports" class="form-control" placeholder="e.g. 1-1024,8080,8443">
|
||
|
|
</div>
|
||
|
|
<button id="vs-start-btn" class="btn btn-primary" onclick="vsStartScan()">Start Scan</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div id="vs-profile-desc" style="font-size:0.8rem;color:var(--text-muted);max-width:220px;padding-bottom:4px">
|
||
|
|
Port scan + service detection + CVE matching + headers + SSL
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Active Scan Progress -->
|
||
|
|
<div id="vs-active-scan" class="section" style="display:none">
|
||
|
|
<h2>Active Scan</h2>
|
||
|
|
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:0.75rem">
|
||
|
|
<div style="flex:1;background:var(--bg-input);border-radius:6px;height:24px;overflow:hidden">
|
||
|
|
<div id="vs-progress-bar" style="height:100%;background:var(--accent);transition:width 0.3s;width:0%"></div>
|
||
|
|
</div>
|
||
|
|
<span id="vs-progress-pct" style="font-size:0.85rem;font-weight:600;min-width:40px;text-align:right">0%</span>
|
||
|
|
</div>
|
||
|
|
<div id="vs-progress-msg" style="font-size:0.8rem;color:var(--text-muted);margin-bottom:0.75rem"></div>
|
||
|
|
|
||
|
|
<!-- Severity Summary Badges -->
|
||
|
|
<div id="vs-severity-badges" style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1rem">
|
||
|
|
<span class="vs-sev-badge" style="background:rgba(239,68,68,0.2);color:var(--danger)">Critical: <strong id="vs-cnt-critical">0</strong></span>
|
||
|
|
<span class="vs-sev-badge" style="background:rgba(230,126,34,0.2);color:#e67e22">High: <strong id="vs-cnt-high">0</strong></span>
|
||
|
|
<span class="vs-sev-badge" style="background:rgba(245,158,11,0.2);color:var(--warning)">Medium: <strong id="vs-cnt-medium">0</strong></span>
|
||
|
|
<span class="vs-sev-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">Low: <strong id="vs-cnt-low">0</strong></span>
|
||
|
|
<span class="vs-sev-badge" style="background:rgba(139,143,168,0.15);color:var(--text-muted)">Info: <strong id="vs-cnt-info">0</strong></span>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Findings Table -->
|
||
|
|
<div style="overflow-x:auto">
|
||
|
|
<table class="data-table" style="font-size:0.82rem">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th style="width:90px">Severity</th>
|
||
|
|
<th>Finding</th>
|
||
|
|
<th style="width:140px">Service</th>
|
||
|
|
<th style="width:60px">Port</th>
|
||
|
|
<th>Details</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="vs-findings-body"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div id="vs-no-findings" style="display:none;color:var(--text-muted);font-size:0.85rem;padding:1rem 0">
|
||
|
|
No findings yet...
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Standalone Tools -->
|
||
|
|
<div class="section" style="margin-top:1.5rem">
|
||
|
|
<h2>Standalone Checks</h2>
|
||
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem;max-width:900px">
|
||
|
|
<!-- Headers Check -->
|
||
|
|
<div class="card">
|
||
|
|
<h4>Security Headers</h4>
|
||
|
|
<div class="form-group" style="margin-bottom:0.5rem">
|
||
|
|
<input type="text" id="vs-hdr-url" class="form-control" placeholder="https://example.com" onkeypress="if(event.key==='Enter')vsCheckHeaders()">
|
||
|
|
</div>
|
||
|
|
<button class="btn btn-primary btn-small" onclick="vsCheckHeaders()">Check Headers</button>
|
||
|
|
<div id="vs-hdr-results" style="margin-top:0.75rem;font-size:0.82rem"></div>
|
||
|
|
</div>
|
||
|
|
<!-- SSL Check -->
|
||
|
|
<div class="card">
|
||
|
|
<h4>SSL/TLS Analysis</h4>
|
||
|
|
<div style="display:flex;gap:0.5rem">
|
||
|
|
<div class="form-group" style="flex:1;margin-bottom:0.5rem">
|
||
|
|
<input type="text" id="vs-ssl-host" class="form-control" placeholder="example.com" onkeypress="if(event.key==='Enter')vsCheckSSL()">
|
||
|
|
</div>
|
||
|
|
<div class="form-group" style="width:70px;margin-bottom:0.5rem">
|
||
|
|
<input type="number" id="vs-ssl-port" class="form-control" value="443" min="1" max="65535">
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<button class="btn btn-primary btn-small" onclick="vsCheckSSL()">Check SSL</button>
|
||
|
|
<div id="vs-ssl-results" style="margin-top:0.75rem;font-size:0.82rem"></div>
|
||
|
|
</div>
|
||
|
|
<!-- Creds Check -->
|
||
|
|
<div class="card">
|
||
|
|
<h4>Default Credentials</h4>
|
||
|
|
<div class="form-group" style="margin-bottom:0.5rem">
|
||
|
|
<input type="text" id="vs-cred-target" class="form-control" placeholder="192.168.1.1" onkeypress="if(event.key==='Enter')vsCheckCreds()">
|
||
|
|
</div>
|
||
|
|
<button class="btn btn-primary btn-small" onclick="vsCheckCreds()">Check Creds</button>
|
||
|
|
<div id="vs-cred-results" style="margin-top:0.75rem;font-size:0.82rem"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== TEMPLATES TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="vs" data-tab="templates">
|
||
|
|
<div class="section">
|
||
|
|
<h2>Nuclei Templates</h2>
|
||
|
|
<div id="vs-nuclei-status" style="margin-bottom:1rem;font-size:0.85rem"></div>
|
||
|
|
<div class="form-group" style="max-width:400px;margin-bottom:1rem">
|
||
|
|
<input type="text" id="vs-tmpl-search" class="form-control" placeholder="Search templates..." oninput="vsFilterTemplates()">
|
||
|
|
</div>
|
||
|
|
<div id="vs-templates-list" style="max-height:500px;overflow-y:auto;font-size:0.82rem"></div>
|
||
|
|
<div id="vs-templates-loading" style="color:var(--text-muted);font-size:0.85rem">
|
||
|
|
<div class="spinner-inline"></div> Loading templates...
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- ==================== RESULTS TAB ==================== -->
|
||
|
|
<div class="tab-content" data-tab-group="vs" data-tab="results">
|
||
|
|
<div class="section">
|
||
|
|
<h2>Scan History</h2>
|
||
|
|
<div class="tool-actions" style="margin-bottom:1rem">
|
||
|
|
<button class="btn btn-primary btn-small" onclick="vsLoadScans()">Refresh</button>
|
||
|
|
</div>
|
||
|
|
<div style="overflow-x:auto">
|
||
|
|
<table class="data-table" style="font-size:0.82rem">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Target</th>
|
||
|
|
<th>Date</th>
|
||
|
|
<th>Profile</th>
|
||
|
|
<th>Status</th>
|
||
|
|
<th>Findings</th>
|
||
|
|
<th>Severity Breakdown</th>
|
||
|
|
<th style="width:140px">Actions</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="vs-history-body"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div id="vs-no-history" style="display:none;color:var(--text-muted);font-size:0.85rem;padding:1rem 0">
|
||
|
|
No scans recorded yet.
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Detailed Results Panel (shown when clicking a scan row) -->
|
||
|
|
<div id="vs-detail-panel" class="section" style="display:none">
|
||
|
|
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem">
|
||
|
|
<h2 id="vs-detail-title" style="margin:0">Scan Details</h2>
|
||
|
|
<button class="btn btn-sm" onclick="document.getElementById('vs-detail-panel').style.display='none'">× Close</button>
|
||
|
|
</div>
|
||
|
|
<div id="vs-detail-summary" style="margin-bottom:1rem;font-size:0.85rem"></div>
|
||
|
|
<div style="overflow-x:auto">
|
||
|
|
<table class="data-table" style="font-size:0.82rem">
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th style="width:90px">Severity</th>
|
||
|
|
<th>Finding</th>
|
||
|
|
<th style="width:100px">Type</th>
|
||
|
|
<th style="width:140px">Service</th>
|
||
|
|
<th style="width:60px">Port</th>
|
||
|
|
<th>Details</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody id="vs-detail-findings"></tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
<div id="vs-detail-services" style="margin-top:1rem"></div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<style>
|
||
|
|
.vs-sev-badge {
|
||
|
|
display: inline-flex;
|
||
|
|
align-items: center;
|
||
|
|
gap: 4px;
|
||
|
|
padding: 4px 12px;
|
||
|
|
border-radius: 20px;
|
||
|
|
font-size: 0.78rem;
|
||
|
|
font-weight: 500;
|
||
|
|
}
|
||
|
|
.vs-sev-critical { background: var(--danger); color: #fff; }
|
||
|
|
.vs-sev-high { background: #e67e22; color: #fff; }
|
||
|
|
.vs-sev-medium { background: var(--warning); color: #000; }
|
||
|
|
.vs-sev-low { background: #3b82f6; color: #fff; }
|
||
|
|
.vs-sev-info { background: var(--bg-input); color: var(--text-muted); }
|
||
|
|
.hdr-good { color: #22c55e; }
|
||
|
|
.hdr-weak { color: #f59e0b; }
|
||
|
|
.hdr-missing { color: var(--danger); }
|
||
|
|
.spinner-inline {
|
||
|
|
display: inline-block; width: 14px; height: 14px;
|
||
|
|
border: 2px solid var(--border); border-top-color: var(--accent);
|
||
|
|
border-radius: 50%; animation: spin 0.8s linear infinite;
|
||
|
|
vertical-align: middle; margin-right: 6px;
|
||
|
|
}
|
||
|
|
@keyframes spin { to { transform: rotate(360deg) } }
|
||
|
|
.vs-tmpl-item {
|
||
|
|
padding: 4px 8px;
|
||
|
|
border-bottom: 1px solid var(--border);
|
||
|
|
font-family: monospace;
|
||
|
|
}
|
||
|
|
.vs-tmpl-item:hover {
|
||
|
|
background: var(--bg-input);
|
||
|
|
}
|
||
|
|
.vs-detail-row:hover {
|
||
|
|
background: var(--bg-input);
|
||
|
|
cursor: pointer;
|
||
|
|
}
|
||
|
|
</style>
|
||
|
|
|
||
|
|
<script>
|
||
|
|
/* ── State ── */
|
||
|
|
let vsActivePoll = null;
|
||
|
|
let vsAllTemplates = [];
|
||
|
|
|
||
|
|
/* ── Severity Badge Helper ── */
|
||
|
|
function vsSevBadge(sev) {
|
||
|
|
sev = (sev || 'info').toLowerCase();
|
||
|
|
const colors = {
|
||
|
|
critical: 'background:var(--danger);color:#fff',
|
||
|
|
high: 'background:#e67e22;color:#fff',
|
||
|
|
medium: 'background:var(--warning);color:#000',
|
||
|
|
low: 'background:#3b82f6;color:#fff',
|
||
|
|
info: 'background:var(--bg-input);color:var(--text-muted)'
|
||
|
|
};
|
||
|
|
return '<span style="display:inline-block;padding:2px 10px;border-radius:12px;font-size:0.75rem;font-weight:600;' +
|
||
|
|
(colors[sev] || colors.info) + '">' + sev.toUpperCase() + '</span>';
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Profile description ── */
|
||
|
|
const profileDescs = {
|
||
|
|
quick: 'Fast port scan + top service CVEs',
|
||
|
|
standard: 'Port scan + service detection + CVE matching + headers + SSL',
|
||
|
|
full: 'All ports + full CVE + default creds + headers + SSL + nuclei',
|
||
|
|
custom: 'User-defined parameters'
|
||
|
|
};
|
||
|
|
function vsProfileChanged() {
|
||
|
|
const p = document.getElementById('vs-profile').value;
|
||
|
|
document.getElementById('vs-profile-desc').textContent = profileDescs[p] || '';
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Start Scan ── */
|
||
|
|
function vsStartScan() {
|
||
|
|
const target = document.getElementById('vs-target').value.trim();
|
||
|
|
if (!target) return;
|
||
|
|
const profile = document.getElementById('vs-profile').value;
|
||
|
|
const ports = document.getElementById('vs-ports').value.trim();
|
||
|
|
const btn = document.getElementById('vs-start-btn');
|
||
|
|
|
||
|
|
setLoading(btn, true);
|
||
|
|
postJSON('/vuln-scanner/scan', { target, profile, ports })
|
||
|
|
.then(d => {
|
||
|
|
setLoading(btn, false);
|
||
|
|
if (!d.ok) { alert('Error: ' + (d.error || 'Unknown')); return; }
|
||
|
|
document.getElementById('vs-active-scan').style.display = '';
|
||
|
|
document.getElementById('vs-findings-body').innerHTML = '';
|
||
|
|
document.getElementById('vs-no-findings').style.display = '';
|
||
|
|
vsResetCounts();
|
||
|
|
vsCheckScan(d.job_id);
|
||
|
|
})
|
||
|
|
.catch(e => { setLoading(btn, false); alert('Error: ' + e.message); });
|
||
|
|
}
|
||
|
|
|
||
|
|
function vsResetCounts() {
|
||
|
|
['critical','high','medium','low','info'].forEach(s => {
|
||
|
|
document.getElementById('vs-cnt-' + s).textContent = '0';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Poll Scan ── */
|
||
|
|
function vsCheckScan(jobId) {
|
||
|
|
if (vsActivePoll) clearInterval(vsActivePoll);
|
||
|
|
vsActivePoll = setInterval(() => {
|
||
|
|
fetchJSON('/vuln-scanner/scan/' + jobId)
|
||
|
|
.then(d => {
|
||
|
|
if (!d.ok) { clearInterval(vsActivePoll); vsActivePoll = null; return; }
|
||
|
|
// Progress
|
||
|
|
const pct = d.progress || 0;
|
||
|
|
document.getElementById('vs-progress-bar').style.width = pct + '%';
|
||
|
|
document.getElementById('vs-progress-pct').textContent = pct + '%';
|
||
|
|
document.getElementById('vs-progress-msg').textContent = d.progress_message || '';
|
||
|
|
|
||
|
|
// Summary counts
|
||
|
|
const sum = d.summary || {};
|
||
|
|
['critical','high','medium','low','info'].forEach(s => {
|
||
|
|
document.getElementById('vs-cnt-' + s).textContent = sum[s] || 0;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Findings
|
||
|
|
const findings = d.findings || [];
|
||
|
|
if (findings.length > 0) {
|
||
|
|
document.getElementById('vs-no-findings').style.display = 'none';
|
||
|
|
let html = '';
|
||
|
|
for (const f of findings) {
|
||
|
|
html += '<tr>' +
|
||
|
|
'<td>' + vsSevBadge(f.severity) + '</td>' +
|
||
|
|
'<td>' + esc(f.title || '') + '</td>' +
|
||
|
|
'<td>' + esc(f.service || '') + '</td>' +
|
||
|
|
'<td>' + (f.port || '') + '</td>' +
|
||
|
|
'<td style="font-size:0.78rem;color:var(--text-secondary);max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' +
|
||
|
|
esc((f.description || '').substring(0, 150)) + '</td>' +
|
||
|
|
'</tr>';
|
||
|
|
}
|
||
|
|
document.getElementById('vs-findings-body').innerHTML = html;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Done?
|
||
|
|
if (d.status === 'complete' || d.status === 'error') {
|
||
|
|
clearInterval(vsActivePoll);
|
||
|
|
vsActivePoll = null;
|
||
|
|
if (d.status === 'error') {
|
||
|
|
document.getElementById('vs-progress-msg').innerHTML =
|
||
|
|
'<span style="color:var(--danger)">Error: ' + esc(d.error || 'Unknown') + '</span>';
|
||
|
|
} else {
|
||
|
|
document.getElementById('vs-progress-msg').innerHTML =
|
||
|
|
'<span style="color:#22c55e">Scan complete</span>';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.catch(() => { clearInterval(vsActivePoll); vsActivePoll = null; });
|
||
|
|
}, 2000);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Load Scans ── */
|
||
|
|
function vsLoadScans() {
|
||
|
|
fetchJSON('/vuln-scanner/scans')
|
||
|
|
.then(d => {
|
||
|
|
const scans = d.scans || [];
|
||
|
|
const noHist = document.getElementById('vs-no-history');
|
||
|
|
const body = document.getElementById('vs-history-body');
|
||
|
|
if (!scans.length) {
|
||
|
|
noHist.style.display = '';
|
||
|
|
body.innerHTML = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
noHist.style.display = 'none';
|
||
|
|
let html = '';
|
||
|
|
for (const s of scans) {
|
||
|
|
const sum = s.summary || {};
|
||
|
|
const dateStr = s.started ? new Date(s.started).toLocaleString() : '';
|
||
|
|
const statusColor = s.status === 'complete' ? '#22c55e' : (s.status === 'error' ? 'var(--danger)' : 'var(--warning)');
|
||
|
|
const sevBreak =
|
||
|
|
(sum.critical ? '<span style="color:var(--danger)">' + sum.critical + 'C</span> ' : '') +
|
||
|
|
(sum.high ? '<span style="color:#e67e22">' + sum.high + 'H</span> ' : '') +
|
||
|
|
(sum.medium ? '<span style="color:var(--warning)">' + sum.medium + 'M</span> ' : '') +
|
||
|
|
(sum.low ? '<span style="color:#3b82f6">' + sum.low + 'L</span> ' : '') +
|
||
|
|
(sum.info ? '<span style="color:var(--text-muted)">' + sum.info + 'I</span>' : '');
|
||
|
|
html += '<tr class="vs-detail-row" onclick="vsViewScan(\'' + s.job_id + '\')">' +
|
||
|
|
'<td>' + esc(s.target) + '</td>' +
|
||
|
|
'<td style="font-size:0.78rem">' + esc(dateStr) + '</td>' +
|
||
|
|
'<td>' + esc(s.profile) + '</td>' +
|
||
|
|
'<td style="color:' + statusColor + '">' + esc(s.status) + '</td>' +
|
||
|
|
'<td><strong>' + (s.findings_count || 0) + '</strong></td>' +
|
||
|
|
'<td>' + (sevBreak || '<span style="color:var(--text-muted)">none</span>') + '</td>' +
|
||
|
|
'<td style="white-space:nowrap">' +
|
||
|
|
'<button class="btn btn-sm" onclick="event.stopPropagation();vsViewScan(\'' + s.job_id + '\')">View</button> ' +
|
||
|
|
'<button class="btn btn-sm" onclick="event.stopPropagation();vsExportScan(\'' + s.job_id + '\')">Export</button> ' +
|
||
|
|
'<button class="btn btn-sm btn-danger" onclick="event.stopPropagation();vsDeleteScan(\'' + s.job_id + '\')">Del</button>' +
|
||
|
|
'</td></tr>';
|
||
|
|
}
|
||
|
|
body.innerHTML = html;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── View Scan Details ── */
|
||
|
|
function vsViewScan(jobId) {
|
||
|
|
fetchJSON('/vuln-scanner/scan/' + jobId)
|
||
|
|
.then(d => {
|
||
|
|
if (!d.ok) return;
|
||
|
|
const panel = document.getElementById('vs-detail-panel');
|
||
|
|
panel.style.display = '';
|
||
|
|
document.getElementById('vs-detail-title').textContent = 'Scan: ' + (d.target || jobId);
|
||
|
|
|
||
|
|
const sum = d.summary || {};
|
||
|
|
let sumHtml = '<div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:0.5rem">' +
|
||
|
|
'<span class="vs-sev-badge" style="background:rgba(239,68,68,0.2);color:var(--danger)">Critical: <strong>' + (sum.critical || 0) + '</strong></span>' +
|
||
|
|
'<span class="vs-sev-badge" style="background:rgba(230,126,34,0.2);color:#e67e22">High: <strong>' + (sum.high || 0) + '</strong></span>' +
|
||
|
|
'<span class="vs-sev-badge" style="background:rgba(245,158,11,0.2);color:var(--warning)">Medium: <strong>' + (sum.medium || 0) + '</strong></span>' +
|
||
|
|
'<span class="vs-sev-badge" style="background:rgba(59,130,246,0.2);color:#3b82f6">Low: <strong>' + (sum.low || 0) + '</strong></span>' +
|
||
|
|
'<span class="vs-sev-badge" style="background:rgba(139,143,168,0.15);color:var(--text-muted)">Info: <strong>' + (sum.info || 0) + '</strong></span>' +
|
||
|
|
'</div>';
|
||
|
|
sumHtml += '<div>Status: <strong>' + esc(d.status || '') + '</strong> | Profile: <strong>' + esc(d.profile || '') + '</strong>';
|
||
|
|
if (d.completed) sumHtml += ' | Completed: ' + new Date(d.completed).toLocaleString();
|
||
|
|
sumHtml += '</div>';
|
||
|
|
document.getElementById('vs-detail-summary').innerHTML = sumHtml;
|
||
|
|
|
||
|
|
const findings = d.findings || [];
|
||
|
|
if (findings.length) {
|
||
|
|
let fhtml = '';
|
||
|
|
for (const f of findings) {
|
||
|
|
fhtml += '<tr>' +
|
||
|
|
'<td>' + vsSevBadge(f.severity) + '</td>' +
|
||
|
|
'<td>' + esc(f.title || '') + '</td>' +
|
||
|
|
'<td>' + esc(f.type || '') + '</td>' +
|
||
|
|
'<td>' + esc(f.service || '') + '</td>' +
|
||
|
|
'<td>' + (f.port || '') + '</td>' +
|
||
|
|
'<td style="font-size:0.78rem;color:var(--text-secondary)">' + esc((f.description || '').substring(0, 200));
|
||
|
|
if (f.reference) fhtml += ' <a href="' + esc(f.reference) + '" target="_blank" style="font-size:0.75rem">[ref]</a>';
|
||
|
|
if (f.cvss) fhtml += ' <span style="color:var(--accent);font-size:0.75rem">CVSS:' + esc(f.cvss) + '</span>';
|
||
|
|
fhtml += '</td></tr>';
|
||
|
|
}
|
||
|
|
document.getElementById('vs-detail-findings').innerHTML = fhtml;
|
||
|
|
} else {
|
||
|
|
document.getElementById('vs-detail-findings').innerHTML =
|
||
|
|
'<tr><td colspan="6" style="color:var(--text-muted);text-align:center">No findings</td></tr>';
|
||
|
|
}
|
||
|
|
|
||
|
|
// Services discovered
|
||
|
|
const services = d.services || [];
|
||
|
|
if (services.length) {
|
||
|
|
let shtml = '<h3 style="margin-top:0.5rem">Discovered Services</h3>' +
|
||
|
|
'<table class="data-table" style="font-size:0.82rem"><thead><tr><th>Port</th><th>Protocol</th><th>Service</th><th>Version</th></tr></thead><tbody>';
|
||
|
|
for (const s of services) {
|
||
|
|
shtml += '<tr><td>' + s.port + '</td><td>' + esc(s.protocol || 'tcp') + '</td>' +
|
||
|
|
'<td>' + esc(s.service || '') + '</td><td>' + esc(s.version || '') + '</td></tr>';
|
||
|
|
}
|
||
|
|
shtml += '</tbody></table>';
|
||
|
|
document.getElementById('vs-detail-services').innerHTML = shtml;
|
||
|
|
} else {
|
||
|
|
document.getElementById('vs-detail-services').innerHTML = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
panel.scrollIntoView({ behavior: 'smooth' });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Delete Scan ── */
|
||
|
|
function vsDeleteScan(jobId) {
|
||
|
|
if (!confirm('Delete this scan?')) return;
|
||
|
|
fetch('/vuln-scanner/scan/' + jobId, { method: 'DELETE' })
|
||
|
|
.then(r => r.json())
|
||
|
|
.then(() => {
|
||
|
|
vsLoadScans();
|
||
|
|
document.getElementById('vs-detail-panel').style.display = 'none';
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Export Scan ── */
|
||
|
|
function vsExportScan(jobId) {
|
||
|
|
window.open('/vuln-scanner/scan/' + jobId + '/export?format=json', '_blank');
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Check Headers ── */
|
||
|
|
function vsCheckHeaders() {
|
||
|
|
const url = document.getElementById('vs-hdr-url').value.trim();
|
||
|
|
if (!url) return;
|
||
|
|
const div = document.getElementById('vs-hdr-results');
|
||
|
|
div.innerHTML = '<div class="spinner-inline"></div> Checking...';
|
||
|
|
|
||
|
|
postJSON('/vuln-scanner/headers', { url })
|
||
|
|
.then(d => {
|
||
|
|
if (!d.ok) { div.innerHTML = '<span style="color:var(--danger)">Error: ' + esc(d.error || 'Unknown') + '</span>'; return; }
|
||
|
|
let html = '<div style="margin-bottom:0.5rem">Score: <strong>' + (d.score || 0) + '%</strong>';
|
||
|
|
if (d.server) html += ' | Server: <strong>' + esc(d.server) + '</strong>';
|
||
|
|
html += '</div>';
|
||
|
|
const headers = d.headers || {};
|
||
|
|
for (const [hdr, info] of Object.entries(headers)) {
|
||
|
|
const cls = 'hdr-' + info.rating;
|
||
|
|
const icon = info.present ? '✓' : '✗';
|
||
|
|
html += '<div style="padding:2px 0"><span class="' + cls + '">' + icon + '</span> ' + esc(hdr);
|
||
|
|
if (info.value) html += ' <span style="color:var(--text-muted);font-size:0.75rem">' + esc(info.value).substring(0, 60) + '</span>';
|
||
|
|
html += '</div>';
|
||
|
|
}
|
||
|
|
div.innerHTML = html;
|
||
|
|
})
|
||
|
|
.catch(e => { div.innerHTML = '<span style="color:var(--danger)">' + e.message + '</span>'; });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Check SSL ── */
|
||
|
|
function vsCheckSSL() {
|
||
|
|
const host = document.getElementById('vs-ssl-host').value.trim();
|
||
|
|
if (!host) return;
|
||
|
|
const port = parseInt(document.getElementById('vs-ssl-port').value) || 443;
|
||
|
|
const div = document.getElementById('vs-ssl-results');
|
||
|
|
div.innerHTML = '<div class="spinner-inline"></div> Checking...';
|
||
|
|
|
||
|
|
postJSON('/vuln-scanner/ssl', { host, port })
|
||
|
|
.then(d => {
|
||
|
|
if (!d.ok || d.error) {
|
||
|
|
div.innerHTML = '<span style="color:var(--danger)">Error: ' + esc(d.error || 'Unknown') + '</span>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
let html = '<div>Valid: <strong class="' + (d.valid ? 'hdr-good' : 'hdr-missing') + '">' + (d.valid ? 'Yes' : 'No') + '</strong></div>';
|
||
|
|
html += '<div>Protocol: ' + esc(d.protocol || '?') + '</div>';
|
||
|
|
html += '<div>Cipher: ' + esc(d.cipher || '?') + '</div>';
|
||
|
|
if (d.key_size) html += '<div>Key size: ' + d.key_size + ' bits</div>';
|
||
|
|
if (d.expires) html += '<div>Expires: ' + esc(d.expires) + '</div>';
|
||
|
|
if (d.issuer && typeof d.issuer === 'object') {
|
||
|
|
html += '<div>Issuer: ' + esc(d.issuer.organizationName || d.issuer.commonName || JSON.stringify(d.issuer)) + '</div>';
|
||
|
|
}
|
||
|
|
const issues = d.issues || [];
|
||
|
|
for (const issue of issues) {
|
||
|
|
html += '<div class="hdr-missing">[!] ' + esc(issue) + '</div>';
|
||
|
|
}
|
||
|
|
const weak = d.weak_ciphers || [];
|
||
|
|
for (const wc of weak) {
|
||
|
|
html += '<div class="hdr-missing">[!] Weak cipher: ' + esc(wc) + '</div>';
|
||
|
|
}
|
||
|
|
if (!issues.length && !weak.length && d.valid) {
|
||
|
|
html += '<div class="hdr-good" style="margin-top:4px">No issues detected</div>';
|
||
|
|
}
|
||
|
|
div.innerHTML = html;
|
||
|
|
})
|
||
|
|
.catch(e => { div.innerHTML = '<span style="color:var(--danger)">' + e.message + '</span>'; });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Check Creds ── */
|
||
|
|
function vsCheckCreds() {
|
||
|
|
const target = document.getElementById('vs-cred-target').value.trim();
|
||
|
|
if (!target) return;
|
||
|
|
const div = document.getElementById('vs-cred-results');
|
||
|
|
div.innerHTML = '<div class="spinner-inline"></div> Scanning ports & testing credentials...';
|
||
|
|
|
||
|
|
postJSON('/vuln-scanner/creds', { target })
|
||
|
|
.then(d => {
|
||
|
|
if (!d.ok) { div.innerHTML = '<span style="color:var(--danger)">Error: ' + esc(d.error || 'Unknown') + '</span>'; return; }
|
||
|
|
const found = d.found || [];
|
||
|
|
let html = '<div style="margin-bottom:0.5rem">' + (d.services_checked || 0) + ' services checked</div>';
|
||
|
|
if (found.length) {
|
||
|
|
for (const c of found) {
|
||
|
|
html += '<div style="color:var(--danger);padding:2px 0">[!] ' + esc(c.service) + ': <strong>' +
|
||
|
|
esc(c.username) + ':' + esc(c.password) + '</strong></div>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
html += '<div class="hdr-good">No default credentials found</div>';
|
||
|
|
}
|
||
|
|
div.innerHTML = html;
|
||
|
|
})
|
||
|
|
.catch(e => { div.innerHTML = '<span style="color:var(--danger)">' + e.message + '</span>'; });
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Load Templates ── */
|
||
|
|
function vsLoadTemplates() {
|
||
|
|
const list = document.getElementById('vs-templates-list');
|
||
|
|
const loading = document.getElementById('vs-templates-loading');
|
||
|
|
const status = document.getElementById('vs-nuclei-status');
|
||
|
|
loading.style.display = '';
|
||
|
|
list.innerHTML = '';
|
||
|
|
|
||
|
|
fetchJSON('/vuln-scanner/templates')
|
||
|
|
.then(d => {
|
||
|
|
loading.style.display = 'none';
|
||
|
|
if (d.installed) {
|
||
|
|
status.innerHTML = '<span class="hdr-good">✓ Nuclei installed</span>' +
|
||
|
|
' <span style="color:var(--text-muted)">(' + esc(d.nuclei_path) + ')</span>';
|
||
|
|
} else {
|
||
|
|
status.innerHTML = '<span class="hdr-missing">✗ Nuclei not installed</span>' +
|
||
|
|
' <span style="color:var(--text-muted)">Install: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest</span>';
|
||
|
|
}
|
||
|
|
|
||
|
|
vsAllTemplates = d.templates || [];
|
||
|
|
const cats = d.categories || [];
|
||
|
|
|
||
|
|
if (cats.length) {
|
||
|
|
let catHtml = '<div style="margin-bottom:0.75rem;display:flex;gap:0.5rem;flex-wrap:wrap">';
|
||
|
|
for (const cat of cats) {
|
||
|
|
const count = vsAllTemplates.filter(t => t.startsWith(cat + '/')).length;
|
||
|
|
catHtml += '<span class="vs-sev-badge" style="background:var(--bg-input);color:var(--accent);cursor:pointer" ' +
|
||
|
|
'onclick="document.getElementById(\'vs-tmpl-search\').value=\'' + cat + '/\';vsFilterTemplates()">' +
|
||
|
|
esc(cat) + ' (' + count + ')</span>';
|
||
|
|
}
|
||
|
|
catHtml += '</div>';
|
||
|
|
list.innerHTML = catHtml;
|
||
|
|
}
|
||
|
|
|
||
|
|
vsRenderTemplates(vsAllTemplates.slice(0, 200));
|
||
|
|
})
|
||
|
|
.catch(() => { loading.style.display = 'none'; });
|
||
|
|
}
|
||
|
|
|
||
|
|
function vsFilterTemplates() {
|
||
|
|
const q = document.getElementById('vs-tmpl-search').value.trim().toLowerCase();
|
||
|
|
const filtered = q ? vsAllTemplates.filter(t => t.toLowerCase().includes(q)) : vsAllTemplates;
|
||
|
|
vsRenderTemplates(filtered.slice(0, 200));
|
||
|
|
}
|
||
|
|
|
||
|
|
function vsRenderTemplates(templates) {
|
||
|
|
const container = document.getElementById('vs-templates-list');
|
||
|
|
// Preserve category badges if they exist
|
||
|
|
const badges = container.querySelector('div');
|
||
|
|
let html = badges ? badges.outerHTML : '';
|
||
|
|
if (templates.length) {
|
||
|
|
html += '<div style="margin-bottom:0.5rem;color:var(--text-muted)">Showing ' + templates.length +
|
||
|
|
(templates.length >= 200 ? '+ (use search to filter)' : '') + ' templates</div>';
|
||
|
|
for (const t of templates) {
|
||
|
|
html += '<div class="vs-tmpl-item">' + esc(t) + '</div>';
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
html += '<div style="color:var(--text-muted)">No templates found</div>';
|
||
|
|
}
|
||
|
|
container.innerHTML = html;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* ── Helpers (use global esc, postJSON, fetchJSON, setLoading, showTab) ── */
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|