141 lines
5.9 KiB
HTML
141 lines
5.9 KiB
HTML
|
|
{% extends "base.html" %}
|
||
|
|
{% block title %}Service Detection{% endblock %}
|
||
|
|
{% block content %}
|
||
|
|
<h1>[?] Service Detection</h1>
|
||
|
|
|
||
|
|
<div class="toolbar">
|
||
|
|
<button class="btn" onclick="runScan()">Scan Server</button>
|
||
|
|
<button class="btn" onclick="browseAll()">Browse All 200+ Services</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div id="scan-results"></div>
|
||
|
|
|
||
|
|
<div class="card" id="browse-panel" style="display:none">
|
||
|
|
<div class="card-title">Service Database ({{count}} services)</div>
|
||
|
|
<input type="text" id="svc-search" placeholder="Filter services..." style="width:100%;margin-bottom:10px" oninput="filterServices()">
|
||
|
|
<div id="all-services" style="max-height:600px;overflow-y:auto"></div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="card">
|
||
|
|
<div class="card-title">Output</div>
|
||
|
|
<div class="output" id="output"><span class="info">Click "Scan Server" to detect installed services</span></div>
|
||
|
|
</div>
|
||
|
|
{% endblock %}
|
||
|
|
|
||
|
|
{% block scripts %}
|
||
|
|
<script>
|
||
|
|
let allServices = {};
|
||
|
|
|
||
|
|
async function runScan() {
|
||
|
|
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="card-title">Scanning...</div><div class="output"><span class="info">Checking processes, ports, packages, configs, docker, systemd...</span></div></div>';
|
||
|
|
const res = await apiGet('/api/detect/scan');
|
||
|
|
if (!res.ok) {
|
||
|
|
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="err">'+res.error+'</span></div></div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const services = res.data;
|
||
|
|
if (!services.length) {
|
||
|
|
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="info">No services detected (or SSH failed)</span></div></div>';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Group by category
|
||
|
|
const groups = {};
|
||
|
|
services.forEach(s => {
|
||
|
|
const cat = s.category;
|
||
|
|
if (!groups[cat]) groups[cat] = [];
|
||
|
|
groups[cat].push(s);
|
||
|
|
});
|
||
|
|
|
||
|
|
let html = '<div class="card"><div class="card-title">Detected Services (' + services.length + ' found)</div></div>';
|
||
|
|
|
||
|
|
for (const [cat, svcs] of Object.entries(groups)) {
|
||
|
|
html += '<div class="card"><div class="card-title">' + escHtml(cat) + ' (' + svcs.length + ')</div>';
|
||
|
|
html += '<table><thead><tr><th>Service</th><th>Confidence</th><th>Evidence</th><th>Configs</th><th>Actions</th></tr></thead><tbody>';
|
||
|
|
for (const s of svcs) {
|
||
|
|
const conf = s.score >= 5 ? 'status-ok' : s.score >= 3 ? 'status-warn' : 'status-err';
|
||
|
|
const confLabel = s.score >= 5 ? 'HIGH' : s.score >= 3 ? 'MED' : 'LOW';
|
||
|
|
const configLinks = s.configs.filter(c => c && !c.endsWith('/')).map(c =>
|
||
|
|
`<a href="#" onclick="editConfig('${c}');return false" style="font-size:11px">${c.split('/').pop()}</a>`
|
||
|
|
).join(', ');
|
||
|
|
const configDirs = s.configs.filter(c => c && c.endsWith('/')).map(c =>
|
||
|
|
`<a href="#" onclick="browseConfig('${c}');return false" style="font-size:11px;color:#888">${c}</a>`
|
||
|
|
).join(', ');
|
||
|
|
|
||
|
|
html += '<tr>';
|
||
|
|
html += '<td><strong>' + escHtml(s.name) + '</strong></td>';
|
||
|
|
html += '<td class="' + conf + '">' + confLabel + ' (' + s.score + ')</td>';
|
||
|
|
html += '<td style="font-size:11px;color:#888">' + s.evidence.map(escHtml).join(', ') + '</td>';
|
||
|
|
html += '<td>' + configLinks + (configDirs ? '<br>' + configDirs : '') + '</td>';
|
||
|
|
html += '<td>';
|
||
|
|
if (s.ports.length) html += '<span style="color:#555">ports: ' + s.ports.join(',') + '</span> ';
|
||
|
|
html += '</td>';
|
||
|
|
html += '</tr>';
|
||
|
|
}
|
||
|
|
html += '</tbody></table></div>';
|
||
|
|
}
|
||
|
|
|
||
|
|
document.getElementById('scan-results').innerHTML = html;
|
||
|
|
}
|
||
|
|
|
||
|
|
function editConfig(path) {
|
||
|
|
window.location.href = '/configs?file=' + encodeURIComponent(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
function browseConfig(path) {
|
||
|
|
window.location.href = '/files?path=' + encodeURIComponent(path);
|
||
|
|
}
|
||
|
|
|
||
|
|
async function browseAll() {
|
||
|
|
const panel = document.getElementById('browse-panel');
|
||
|
|
panel.style.display = '';
|
||
|
|
if (Object.keys(allServices).length) return;
|
||
|
|
|
||
|
|
const res = await apiGet('/api/detect/all-services');
|
||
|
|
if (!res.ok) return;
|
||
|
|
allServices = res.data;
|
||
|
|
renderAllServices(allServices);
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderAllServices(data) {
|
||
|
|
let html = '';
|
||
|
|
let total = 0;
|
||
|
|
for (const [cat, svcs] of Object.entries(data)) {
|
||
|
|
total += svcs.length;
|
||
|
|
html += '<h2 style="margin-top:10px">' + escHtml(cat) + ' (' + svcs.length + ')</h2>';
|
||
|
|
html += '<table><thead><tr><th>Service</th><th>Ports</th><th>Packages</th><th>Config Files</th></tr></thead><tbody>';
|
||
|
|
for (const s of svcs) {
|
||
|
|
html += '<tr>';
|
||
|
|
html += '<td>' + escHtml(s.name) + '</td>';
|
||
|
|
html += '<td style="color:#888">' + (s.ports.length ? s.ports.join(', ') : '-') + '</td>';
|
||
|
|
html += '<td style="font-size:11px;color:#888">' + (s.packages.length ? s.packages.join(', ') : '-') + '</td>';
|
||
|
|
html += '<td style="font-size:11px">' + (s.configs.length ? s.configs.map(c =>
|
||
|
|
'<a href="#" onclick="editConfig(\'' + c + '\');return false">' + c + '</a>'
|
||
|
|
).join('<br>') : '-') + '</td>';
|
||
|
|
html += '</tr>';
|
||
|
|
}
|
||
|
|
html += '</tbody></table>';
|
||
|
|
}
|
||
|
|
document.getElementById('all-services').innerHTML = html;
|
||
|
|
document.querySelector('#browse-panel .card-title').textContent = 'Service Database (' + total + ' services)';
|
||
|
|
}
|
||
|
|
|
||
|
|
function filterServices() {
|
||
|
|
const q = document.getElementById('svc-search').value.toLowerCase();
|
||
|
|
if (!q) { renderAllServices(allServices); return; }
|
||
|
|
const filtered = {};
|
||
|
|
for (const [cat, svcs] of Object.entries(allServices)) {
|
||
|
|
const matches = svcs.filter(s =>
|
||
|
|
s.name.toLowerCase().includes(q) ||
|
||
|
|
cat.toLowerCase().includes(q) ||
|
||
|
|
s.packages.some(p => p.includes(q)) ||
|
||
|
|
s.configs.some(c => c.includes(q))
|
||
|
|
);
|
||
|
|
if (matches.length) filtered[cat] = matches;
|
||
|
|
}
|
||
|
|
renderAllServices(filtered);
|
||
|
|
}
|
||
|
|
</script>
|
||
|
|
{% endblock %}
|