Files
MeshSate/app/templates/test_center.html

324 lines
14 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<link rel="icon" href="/static/logo.png">
<title>MeshSafe 控制台 — 测试靶场</title>
<style>
* { box-sizing: border-box; }
body { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
margin: 0; background: #f5f6fa; color: #333; }
{% include "_nav_style.html" %}
main { padding: 24px; max-width: 1400px; margin: 0 auto; }
h2 { margin: 0 0 16px; font-size: 18px; }
.summary { display: flex; gap: 16px; margin-bottom: 20px; }
.summary .card { flex: 1; background: white; border-radius: 10px; padding: 16px 20px;
box-shadow: 0 2px 8px rgba(0,0,0,.06); text-align: center; }
.summary .num { font-size: 32px; font-weight: 700; }
.summary .lbl { font-size: 12px; color: #888; margin-top: 4px; }
.summary .total .num { color: #1a2a6c; }
.summary .pass .num { color: #2e7d32; }
.summary .fail .num { color: #c62828; }
table { width: 100%; border-collapse: collapse; background: white;
border-radius: 10px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,.06); }
th, td { padding: 10px 14px; text-align: left; font-size: 13px; border-bottom: 1px solid #f0f0f0; }
th { background: #f8f9fd; font-weight: 500; color: #555; }
tr:hover { background: #fafbff; }
.chip { display: inline-block; padding: 1px 8px; border-radius: 10px;
font-size: 11px; font-weight: 500; white-space: nowrap; }
.chip-zt { background: #ffebee; color: #c62828; }
.chip-warn { background: #fff3e0; color: #ef6c00; }
.chip-ok { background: #e8f5e9; color: #2e7d32; }
.chip-info { background: #e3f2fd; color: #1565c0; }
.chip-gray { background: #f0f0f0; color: #666; }
.pass-mark { color: #2e7d32; font-size: 16px; }
.fail-mark { color: #c62828; font-size: 16px; }
details { background: #fafbff; }
details summary { cursor: pointer; padding: 6px 12px; font-size: 12px; color: #1565c0; }
details[open] summary { border-bottom: 1px solid #e3f2fd; }
.findings-table { margin: 8px 12px; }
.findings-table th { background: #eef2fb; font-size: 12px; padding: 6px 10px; }
.findings-table td { padding: 5px 10px; font-size: 12px; }
.refresh-btn { display: inline-block; padding: 8px 20px; background: #1a2a6c;
color: white; border-radius: 6px; text-decoration: none; font-size: 13px; }
.refresh-btn:hover { background: #0f1d52; }
.clamav-note { background: #fff8e1; border: 1px solid #ffe082; border-radius: 8px;
padding: 10px 16px; font-size: 13px; color: #5d4037; margin-bottom: 16px; }
</style>
</head>
<body>
{% set nav_active = 'test' %}{% include "_nav.html" %}
<main>
<input type="file" id="uploadInput" multiple style="display:none">
{% if results %}
<div class="summary">
<div class="card total"><div class="num">{{ total }}</div><div class="lbl">样本总数</div></div>
<div class="card pass"><div class="num">{{ pass_count }}</div><div class="lbl">检测通过 ✅</div></div>
<div class="card fail"><div class="num">{{ fail_count }}</div><div class="lbl">检测未通过 ❌</div></div>
<div class="card" style="text-align:center;">
<a class="refresh-btn" href="/console/test?rescan=1">🔄 重新扫描</a>
<button id="uploadBtn" class="refresh-btn" style="background:#2e7d32; margin-left:8px;">📤 上传样本</button>
</div>
</div>
<div style="display:flex; gap:16px; margin-bottom:14px; font-size:12px; color:#64748b; align-items:center;">
<span>📦 当前策略:<b style="color:#1e293b;">{{ policy_version or '-' }}</b></span>
{% if last_run_at %}
<span>🕐 上次运行:<b style="color:#1e293b;">{{ last_run_at|bj_time("%m-%d %H:%M:%S") }}</b></span>
{% else %}
<span style="color:#f59e0b;">⚠️ 从未运行过 —— 点「重新扫描」开始检测</span>
{% endif %}
{% if rescan %}<span style="color:#0369a1;">✅ 本次为实时全量扫描结果</span>{% endif %}
</div>
{% if results[0].clamav_error %}
<div class="clamav-note">
⚠️ ClamAV 引擎不可用(本地未起 clamd)—— 部分 verdict 显示为 <code>error (ClamAV 不可用)</code>,
但 YARA / structure / documents / keywords 引擎已正常工作。生产环境 docker compose 起 ClamAV 后会自动加载。
</div>
{% endif %}
<table>
<thead>
<tr>
<th style="width:40px;">#</th>
<th style="width:50px;">结果</th>
<th>样本文件</th>
<th style="width:80px;">分类</th>
<th style="width:180px;">期望裁决 · 规则</th>
<th style="width:200px;">检测结论 · 规则</th>
<th style="width:60px;">方向</th>
<th style="width:60px;">规则</th>
<th style="width:60px;">Score</th>
<th style="width:70px;">耗时</th>
<th style="width:80px;">Findings</th>
</tr>
</thead>
<tbody>
{% for r in results %}
<tr>
<td style="color:#999;font-size:12px;">{{ loop.index }}</td>
<td class="{% if r.overall_ok %}pass-mark{% else %}fail-mark{% endif %}">
{% if r.overall_ok %}✅{% else %}❌{% endif %}
</td>
<td>
<div style="font-family:monospace;font-size:12px;">{{ r.filename }}</div>
<div style="color:#888;font-size:11px;">{{ r.description }}</div>
</td>
<td><span class="chip chip-info">{{ r.category }}</span></td>
<td>
<!-- 期望裁决:完整显示 expected_verdict -->
{% set exp_v = r.expected_verdict or '' %}
{% if exp_v in ('reject', 'malicious') %}
<span class="tag bad">拦截 {{ exp_v }}</span>
{% elif exp_v == 'suspicious' %}
<span class="chip chip-warn">可疑</span>
{% elif exp_v == 'pass' or exp_v == 'clean' %}
<span class="tag ok">放行 {{ exp_v }}</span>
{% else %}
<span class="chip chip-gray">{{ exp_v or '未设' }}</span>
{% endif %}
<!-- 期望规则 -->
{% if r.expected_rules %}
<div style="margin-top:4px;">
{% for rule in r.expected_rules %}
<span class="chip chip-gray" style="font-family:monospace;font-size:10px;">{{ rule }}</span>
{% endfor %}
</div>
{% endif %}
</td>
<td>
<!-- 实际裁决 -->
<span class="tag {{ r.actual_verdict|cn_verdict_badge }}" title="{{ r.actual_verdict }}">{{ r.actual_verdict|cn_verdict }}</span>
{% if r.clamav_error %}<div style="font-size:10px;color:#ef6c00;">ClamAV 不可用</div>{% endif %}
<!-- 实际规则 -->
{% if r.actual_rules %}
<div style="margin-top:4px;">
{% for rule in r.actual_rules %}
<span class="chip chip-{% if rule in r.expected_rules %}ok{% else %}info{% endif %}" style="font-family:monospace;font-size:10px;">{{ rule }}</span>
{% endfor %}
</div>
{% elif r.actual_verdict in ('pass', 'clean') %}
<div style="margin-top:4px;color:#aaa;font-size:11px;">(无规则命中)</div>
{% else %}
<div style="margin-top:4px;color:#f59e0b;font-size:11px;">⚠️ 无规则</div>
{% endif %}
</td>
<td style="text-align:center;">
{% if r.verdict_match %}
<span style="color:#2e7d32;font-size:16px;" title="判定方向匹配">✅</span>
{% else %}
<span style="color:#c62828;font-size:16px;" title="判定方向偏离">❌</span>
{% endif %}
</td>
<td style="text-align:center;">
{% if r.rule_hit %}
<span style="color:#2e7d32;font-size:16px;" title="规则命中">✅</span>
{% else %}
<span style="color:#c62828;font-size:16px;" title="规则未命中">❌</span>
{% endif %}
</td>
<td><b>{{ "%.0f"|format(r.score) }}</b></td>
<td style="color:#888;">{{ r.duration_ms }}ms</td>
<td>
<details>
<summary>{{ r.findings_count }} 条</summary>
{% if r.findings %}
<table class="findings-table">
<thead><tr><th>引擎</th><th>规则</th><th>严重度</th><th>权重</th><th>细节</th></tr></thead>
<tbody>
{% for f in r.findings %}
<tr>
<td><span class="chip chip-info">{{ f.engine }}</span></td>
<td style="font-family:monospace;">{{ f.rule }}</td>
<td>{{ f.severity }}</td>
<td>{{ f.weight }}</td>
<td style="color:#888;font-size:11px;">{{ f.detail }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div style="text-align:center;padding:60px;color:#999;">
测试样本目录不存在:<code>{{ samples_dir }}</code><br>
请运行:<code>python scripts/generate_test_samples.py</code>
</div>
{% endif %}
<!-- 用户自定义上传扫描结果区 -->
<div id="uploadResultArea" style="display:none; margin-top:24px;">
<h2 style="display:flex;align-items:center;gap:10px;">
📤 我的上传扫描结果
<span id="uploadSummary" style="font-size:13px;color:#888;font-weight:normal;"></span>
</h2>
<table id="uploadTable" style="margin-bottom:16px;">
<thead>
<tr>
<th style="width:40px;">#</th>
<th style="width:30px;">结果</th>
<th>文件</th>
<th style="width:90px;">大小</th>
<th style="width:110px;">verdict</th>
<th style="width:70px;">Score</th>
<th style="width:80px;">耗时</th>
<th>Findings</th>
</tr>
</thead>
<tbody id="uploadTableBody"></tbody>
</table>
</div>
</main>
<script>
(function() {
const btn = document.getElementById('uploadBtn');
const input = document.getElementById('uploadInput');
btn.addEventListener('click', function() { input.click(); });
input.addEventListener('change', async function() {
const files = Array.from(input.files);
if (!files.length) return;
btn.disabled = true;
btn.textContent = '⏳ 扫描中 ' + files.length + ' 个文件...';
input.value = ''; // reset 允许重复选同文件
const fd = new FormData();
files.forEach(f => fd.append('files', f));
try {
const resp = await fetch('/console/test/upload', {
method: 'POST',
body: fd,
credentials: 'same-origin',
});
const data = await resp.json();
renderUploadResults(data.results || []);
} catch (err) {
alert('上传扫描失败: ' + err.message);
} finally {
btn.disabled = false;
btn.textContent = '📤 上传样本';
}
});
function renderUploadResults(results) {
const area = document.getElementById('uploadResultArea');
const tbody = document.getElementById('uploadTableBody');
const summary = document.getElementById('uploadSummary');
area.style.display = 'block';
tbody.innerHTML = '';
let pass = 0, fail = 0;
results.forEach((r, i) => {
const icon = r.verdict === 'reject' || r.verdict === 'malicious' ? '❌' :
r.verdict === 'error' ? '⚠️' : '✅';
if (r.verdict === 'reject' || r.verdict === 'malicious' || r.verdict === 'error') fail++;
else pass++;
const sizeKB = r.size < 1024 ? r.size + ' B' : (r.size / 1024).toFixed(1) + ' KB';
const findingsHtml = r.findings && r.findings.length
? '<details><summary>' + r.findings.length + ' 条 findings</summary>' +
'<table class="findings-table"><thead><tr><th>引擎</th><th>规则</th><th>严重度</th><th>权重</th><th>细节</th></tr></thead><tbody>' +
r.findings.map(f =>
'<tr><td><span class="chip chip-info">' + escapeHtml(f.engine) + '</span></td>' +
'<td style="font-family:monospace;">' + escapeHtml(f.rule) + '</td>' +
'<td>' + escapeHtml(f.severity) + '</td>' +
'<td>' + escapeHtml(String(f.weight)) + '</td>' +
'<td style="color:#888;font-size:11px;">' + escapeHtml(f.detail) + '</td></tr>'
).join('') +
'</tbody></table></details>'
: '<span style="color:#aaa;">(无)</span>';
const clamavNote = r.clamav_error
? '<div style="font-size:10px;color:#ef6c00;">ClamAV 不可用</div>'
: '';
tbody.insertAdjacentHTML('beforeend',
'<tr>' +
'<td style="color:#999;font-size:12px;">' + (i + 1) + '</td>' +
'<td>' + icon + '</td>' +
'<td>' +
'<div style="font-family:monospace;font-size:12px;">' + escapeHtml(r.filename) + '</div>' +
(r.reason ? '<div style="color:#c62828;font-size:11px;max-width:260px;overflow:hidden;text-overflow:ellipsis;">' + escapeHtml(r.reason) + '</div>' : '') +
'</td>' +
'<td style="color:#888;font-size:12px;">' + sizeKB + '</td>' +
'<td><span class="tag ' + (r.verdict_badge || 'muted') + '" title="' + escapeHtml(r.verdict) + '">' + escapeHtml(r.verdict_cn || r.verdict) + '</span>' + clamavNote + '</td>' +
'<td><b>' + (r.score || 0).toFixed(0) + '</b></td>' +
'<td style="color:#888;">' + (r.duration_ms || 0) + 'ms</td>' +
'<td>' + findingsHtml + '</td>' +
'</tr>'
);
});
summary.textContent = '✅ ' + pass + ' 通过 / ❌ ' + fail + ' 未通过 / 共 ' + results.length + ' 个文件';
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
}
})();
</script>
</body>
</html>