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:
232
app/templates/tasks.html
Normal file
232
app/templates/tasks.html
Normal file
@@ -0,0 +1,232 @@
|
||||
<!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: #f4f6fa; color: #1e293b; }
|
||||
{% include "_nav_style.html" %}
|
||||
main { padding: 24px; max-width: 1500px; margin: 0 auto; }
|
||||
|
||||
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 18px; }
|
||||
.page-head h1 { margin: 0 0 6px; font-size: 21px; font-weight: 700; }
|
||||
.page-head .sub { font-size: 13px; color: #64748b; }
|
||||
.page-head .count-chip {
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 8px;
|
||||
padding: 8px 14px; font-size: 12.5px; color: #475569;
|
||||
}
|
||||
.page-head .count-chip b { color: #0f172a; }
|
||||
|
||||
.panel { background: #fff; border-radius: 12px; padding: 18px 20px;
|
||||
box-shadow: 0 1px 3px rgba(15,23,42,.07), 0 6px 20px rgba(15,23,42,.04);
|
||||
border: 1px solid #eef2f7; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #f8fafc; padding: 10px 12px; text-align: left; font-size: 11.5px;
|
||||
color: #64748b; font-weight: 600; border-bottom: 1px solid #e8edf4; white-space: nowrap; }
|
||||
td { padding: 10px 12px; font-size: 12.5px; border-bottom: 1px solid #f4f7fb; color: #334155; }
|
||||
tr:last-child td { border-bottom: none; }
|
||||
tbody tr:hover { background: #fafbfd; }
|
||||
td.mono { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11.5px; color: #64748b; }
|
||||
td.fname { font-weight: 500; color: #1e293b; max-width: 230px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.verdict-cell { display: flex; align-items: center; gap: 6px; }
|
||||
|
||||
.mode-badge { display: inline-block; padding: 2px 8px; border-radius: 5px; font-size: 11px; }
|
||||
.mode-sync { background: #e0f2fe; color: #0369a1; }
|
||||
.mode-async { background: #f3e5f5; color: #7b1fa2; }
|
||||
|
||||
details > summary { cursor: pointer; color: #2563eb; user-select: none; font-size: 12px; }
|
||||
details > summary:hover { text-decoration: underline; }
|
||||
/* 详情列:限制宽度,内层内容横向滚动,不撑开外层表格 */
|
||||
tbody td:last-child { min-width: 220px; max-width: 360px; vertical-align: top; }
|
||||
details { display: block; width: 100%; }
|
||||
details .findings { padding: 12px; font-size: 12px; background: #f8fafc;
|
||||
border-radius: 8px; margin-top: 6px;
|
||||
max-width: 100%; overflow-x: auto; overflow-y: visible; display: block; }
|
||||
details .findings table { border: 1px solid #e8edf4; border-radius: 6px; overflow: hidden; min-width: 600px; }
|
||||
details .findings th { font-size: 11px; padding: 6px 10px; white-space: nowrap; }
|
||||
details .findings td { font-size: 11.5px; padding: 6px 10px; white-space: nowrap; }
|
||||
.empty { text-align: center; padding: 50px; color: #94a3b8; font-size: 13px; }
|
||||
|
||||
/* ---------- 分页 ---------- */
|
||||
.pager { display: flex; align-items: center; justify-content: space-between; margin-top: 16px;
|
||||
padding-top: 14px; border-top: 1px solid #eef2f7; }
|
||||
.pager .p-info { font-size: 12.5px; color: #64748b; }
|
||||
.pager .p-info b { color: #0f172a; }
|
||||
.pager .p-nav { display: flex; gap: 5px; align-items: center; }
|
||||
.pager .p-nav a, .pager .p-nav span {
|
||||
min-width: 32px; height: 30px; padding: 0 10px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 7px; font-size: 12.5px; text-decoration: none;
|
||||
border: 1px solid #e2e8f0; color: #475569; background: #fff;
|
||||
transition: all .15s;
|
||||
}
|
||||
.pager .p-nav a:hover { border-color: #93c5fd; color: #2563eb; background: #eff6ff; }
|
||||
.pager .p-nav .cur { background: #1e3a5f; border-color: #1e3a5f; color: #fff; font-weight: 600; }
|
||||
.pager .p-nav .disabled { opacity: .4; pointer-events: none; }
|
||||
.pager .p-nav .dots { border: none; background: none; min-width: 20px; padding: 0; color: #94a3b8; }
|
||||
|
||||
/* ---------- 风险体系说明 ---------- */
|
||||
.risk-legend { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 14px; padding-top: 14px;
|
||||
border-top: 1px dashed #e8edf4; }
|
||||
.risk-legend .rl { display: flex; align-items: flex-start; gap: 7px; font-size: 11.5px; color: #94a3b8; }
|
||||
.risk-legend .rl .rl-name { color: #334155; font-weight: 600; font-size: 12px; white-space: nowrap; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{% set nav_active = 'tasks' %}
|
||||
{% include "_nav.html" %}
|
||||
<main>
|
||||
|
||||
<div class="page-head">
|
||||
<div>
|
||||
<h1>任务历史</h1>
|
||||
<div class="sub">全部扫描任务记录 · 每页 {{ page_size }} 条</div>
|
||||
</div>
|
||||
<div class="count-chip">
|
||||
共 <b>{{ total }}</b> 条记录 · 第 <b>{{ page }}</b> / {{ total_pages }} 页
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
{% if tasks %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>任务 ID</th>
|
||||
<th>文件名</th>
|
||||
<th>来源渠道</th>
|
||||
<th>类型</th>
|
||||
<th>大小</th>
|
||||
<th>模式</th>
|
||||
<th>检测结论</th>
|
||||
<th>处置动作</th>
|
||||
<th>风险分</th>
|
||||
<th>耗时</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in tasks %}
|
||||
<tr id="task-{{ t.id }}">
|
||||
<td class="mono">{{ t.created_at|bj_time("%m-%d %H:%M:%S") if t.created_at else '' }}</td>
|
||||
<td class="mono" title="{{ t.id }}">{{ t.id[0:10] }}...</td>
|
||||
<td class="fname" title="{{ t.filename }}">{{ t.filename }}</td>
|
||||
<td class="mono" title="{{ t.client_id }}">{{ t.channel|cn_channel }}</td>
|
||||
<td class="mono" title="{{ t.real_type }}">{{ t.real_type[:16] if t.real_type else t.declared_ext }}</td>
|
||||
<td class="mono">
|
||||
{% if t.size > 1048576 %}{{ (t.size / 1048576)|round(1) }} MB
|
||||
{% elif t.size > 1024 %}{{ (t.size / 1024)|round(1) }} KB
|
||||
{% else %}{{ t.size }} B{% endif %}
|
||||
</td>
|
||||
<td><span class="mode-badge mode-{{ t.mode }}">{{ t.mode|cn_mode }}</span></td>
|
||||
<td>
|
||||
<div class="verdict-cell">
|
||||
<span class="tag {{ t.verdict|cn_verdict_badge }}">{{ t.verdict|cn_verdict }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><span class="tag plain">{{ t.action|cn_action }}</span></td>
|
||||
<td class="mono">{{ "%.0f"|format(t.score) }}</td>
|
||||
<td class="mono">{{ t.duration_ms }} ms</td>
|
||||
<td>
|
||||
<details>
|
||||
<summary>详情 ({{ t.findings|length }})</summary>
|
||||
<div class="findings">
|
||||
{% if t.findings %}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>检测引擎</th><th>严重度</th><th>命中规则</th><th>权重</th><th>细节</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for f in t.findings %}
|
||||
<tr>
|
||||
<td><span class="tag plain">{{ f.engine|cn_engine }}</span></td>
|
||||
<td><span class="tag {{ f.severity|cn_severity_badge }}">{{ f.severity|cn_severity }}</span></td>
|
||||
<td class="mono">{{ f.rule }}</td>
|
||||
<td class="mono">{{ f.weight }}</td>
|
||||
<td>{{ f.detail }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<div style="color:#94a3b8;font-size:12px;">未检出任何风险信号</div>
|
||||
{% endif %}
|
||||
{% if t.sha256 %}
|
||||
<div style="margin-top:10px;font-size:11px;color:#94a3b8;font-family:monospace;line-height:1.8;">
|
||||
SHA256:{{ t.sha256 }}
|
||||
{% if t.md5 %}<br>MD5:{{ t.md5 }}{% endif %}
|
||||
<br>策略版本:{{ t.policy_version }}
|
||||
{% if t.reason %}<br>裁决依据:{{ t.reason }}{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<!-- 风险体系说明 -->
|
||||
<div class="risk-legend">
|
||||
{% for r in RISK_MATRIX %}
|
||||
<div class="rl">
|
||||
<span class="tag risk-{{ r.key }}">{{ r.label }}</span>
|
||||
<span>{{ r.desc }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pager">
|
||||
<div class="p-info">
|
||||
显示第 <b>{{ (page - 1) * page_size + 1 }}</b> - <b>{{ [page * page_size, total]|min }}</b> 条,共 <b>{{ total }}</b> 条
|
||||
</div>
|
||||
<div class="p-nav">
|
||||
{% if has_prev %}
|
||||
<a href="/console/tasks?page={{ prev_page }}">上一页</a>
|
||||
{% else %}
|
||||
<span class="disabled">上一页</span>
|
||||
{% endif %}
|
||||
|
||||
{% if page_nums and page_nums[0] > 1 %}
|
||||
<a href="/console/tasks?page=1">1</a>
|
||||
{% if page_nums[0] > 2 %}<span class="dots">···</span>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for p in page_nums %}
|
||||
{% if p == page %}
|
||||
<span class="cur">{{ p }}</span>
|
||||
{% else %}
|
||||
<a href="/console/tasks?page={{ p }}">{{ p }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_nums and page_nums[-1] < total_pages %}
|
||||
{% if page_nums[-1] < total_pages - 1 %}<span class="dots">···</span>{% endif %}
|
||||
<a href="/console/tasks?page={{ total_pages }}">{{ total_pages }}</a>
|
||||
{% endif %}
|
||||
|
||||
{% if has_next %}
|
||||
<a href="/console/tasks?page={{ next_page }}">下一页</a>
|
||||
{% else %}
|
||||
<span class="disabled">下一页</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="empty">
|
||||
<div style="font-size:28px;margin-bottom:12px;">📭</div>
|
||||
暂无扫描记录。调用 <code>/api/v1/scans</code> 提交文件后,结果会自动出现在这里。
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user