feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交
核心功能: - 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制 - FastAPI + PostgreSQL + Redis + Docker Compose 架构 - 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性 - 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页) - 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min) 并发与稳定性修复: - 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行) - JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写 - 移除 SQLite fallback, 统一 PostgreSQL - API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
This commit is contained in:
323
app/templates/test_center.html
Normal file
323
app/templates/test_center.html
Normal file
@@ -0,0 +1,323 @@
|
||||
<!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 => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user