Files
MeshSate/app/templates/test_center.html
zhaolei d05537e1fb feat: 靶场对齐 CSV 126 条用例
- policy.yaml: 新增 9 条恶意行为关键词规则(ReverseShell/CredentialStealer/Mimikatz/TScon 等)
- binary.py: 新增 4 条 PE 检测(UPXPackerDetected/RWXSectionDetected/PEArchInfo/DebugInformationPresent)
- console.py + test_center.html: 靶场表格最前加 case_id 列,与 CSV 用例编号一致
- docker-compose.yml: 挂载 tests/samples 为只读 volume,样本更新免 rebuild
- scripts/generate_csv_samples.py: 一键从用例.csv 生成样本 + index.json
- tests/samples/: 128 个 CSV 对齐样本文件 + index.json(93 条元数据)

引擎覆盖度: L3 12%→60%, L5 20%→55%, 总计 54%→75%
2026-10-29 11:33:58 +08:00

332 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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:70px;">用例编号</th>
<th style="width:30px;">#</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>
{% if r.case_id %}
<span class="chip chip-info" style="font-family:monospace;font-size:11px;">{{ r.case_id }}</span>
{% else %}
<span style="color:#ccc;">—</span>
{% endif %}
</td>
<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>