核心功能: - 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
224 lines
11 KiB
HTML
224 lines
11 KiB
HTML
<!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; }
|
||
.tabs { display: flex; border-bottom: 2px solid #e0e0e0; margin-bottom: 16px; }
|
||
.tab { padding: 10px 20px; cursor: pointer; font-size: 14px; color: #666; border-bottom: 2px solid transparent; margin-bottom: -2px; }
|
||
.tab.active { color: #1a2a6c; border-bottom-color: #1a2a6c; font-weight: 500; }
|
||
.panel { display: none; }
|
||
.panel.active { display: block; }
|
||
.card { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 8px rgba(0,0,0,.06); margin-bottom: 16px; }
|
||
.card h3 { margin: 0 0 12px; font-size: 14px; color: #555; font-weight: 600; }
|
||
textarea { width: 100%; min-height: 480px; font-family: "JetBrains Mono", Consolas, monospace;
|
||
font-size: 13px; padding: 14px; border: 1px solid #ddd; border-radius: 6px;
|
||
resize: vertical; line-height: 1.5; }
|
||
textarea:focus { border-color: #1a2a6c; outline: none; }
|
||
.toolbar { display: flex; gap: 10px; margin-top: 12px; align-items: center; }
|
||
button { padding: 9px 20px; border: none; border-radius: 6px; font-size: 14px; cursor: pointer; }
|
||
.btn-primary { background: #1a2a6c; color: white; font-weight: 500; }
|
||
.btn-primary:hover { background: #243a8c; }
|
||
.btn-ghost { background: #f0f0f0; color: #555; }
|
||
.btn-ghost:hover { background: #e4e4e4; }
|
||
.btn-danger { background: #c62828; color: white; }
|
||
.btn-danger:hover { background: #d32f2f; }
|
||
.versions { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||
.versions th { background: #f8f9fa; padding: 8px 12px; text-align: left; color: #666;
|
||
font-weight: 500; border-bottom: 1px solid #eee; }
|
||
.versions td { padding: 8px 12px; border-bottom: 1px solid #f0f0f0; font-family: monospace; }
|
||
.chip { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px;
|
||
margin: 2px; }
|
||
.chip-zt { background: #ffebee; color: #c62828; }
|
||
.chip-warn { background: #fff3e0; color: #ef6c00; }
|
||
.chip-info { background: #e3f2fd; color: #1565c0; }
|
||
.chip-ok { background: #e8f5e9; color: #2e7d32; }
|
||
.form-row { display: flex; gap: 12px; align-items: center; margin-bottom: 12px; }
|
||
.form-row label { width: 160px; font-size: 13px; color: #555; }
|
||
.form-row input[type=number], .form-row input[type=text] { padding: 8px 10px; border: 1px solid #ddd;
|
||
border-radius: 5px; font-size: 13px; width: 120px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
{% set nav_active = 'policy' %}{% include "_nav.html" %}
|
||
<main>
|
||
|
||
<div class="tabs">
|
||
<div class="tab active" data-tab="yaml">YAML 模式(直接编辑)</div>
|
||
<div class="tab" data-tab="rules">规则清单(只读预览)</div>
|
||
<div class="tab" data-tab="yara">YARA 规则库({{ yara_loaded_count }} 条已加载)</div>
|
||
<div class="tab" data-tab="versions">版本历史</div>
|
||
</div>
|
||
|
||
<!-- ============ YAML 模式 ============ -->
|
||
<div id="panel-yaml" class="panel active">
|
||
<form method="post" action="/console/policy/save">
|
||
<input type="hidden" name="mode" value="yaml">
|
||
<div class="card">
|
||
<h3>策略内容(数据库为准,YAML 作为导出备份)</h3>
|
||
<textarea name="yaml_content" spellcheck="false">{{ yaml_text }}</textarea>
|
||
<div class="toolbar">
|
||
<button type="submit" class="btn-primary" {% if user.role != 'admin' %}disabled title="admin 权限才能保存"{% endif %}>保存并热重载</button>
|
||
<button type="button" class="btn-ghost" onclick="document.querySelector('#panel-yaml textarea').value = document.querySelector('#panel-yaml textarea').value;">刷新</button>
|
||
<span style="color:#888;font-size:12px;">当前版本:<b>{{ policy.version }}</b> · SHA256:{{ policy.sha256[:12] }}...</span>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- ============ 规则清单 ============ -->
|
||
<div id="panel-rules" class="panel">
|
||
<div class="card">
|
||
<h3>零容忍规则集({{ policy.zero_tolerance_rules|length }} 条 · 命中即拒绝)</h3>
|
||
<div>
|
||
{% if zt_rules_cn %}
|
||
{% for r in zt_rules_cn %}
|
||
<span class="chip chip-zt" title="{{ r.rule }} — {{ r.cn_desc }}">{{ r.cn_name }}</span>
|
||
{% endfor %}
|
||
{% else %}
|
||
{% for r in policy.zero_tolerance_rules %}
|
||
<span class="chip chip-zt">{{ r }}</span>
|
||
{% endfor %}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h3>策略全局参数</h3>
|
||
<table class="versions">
|
||
<tr><th>参数名</th><th>说明</th><th>当前值</th></tr>
|
||
<tr><td>block_score</td><td>单引擎累计得分 ≥ 此阈值 → 判定为 malicious 直接拒绝</td><td><b>{{ policy.block_score }}</b></td></tr>
|
||
<tr><td>review_score</td><td>累计得分落在这个区间 → 判定为 suspicious 转人工复核</td><td>{{ policy.review_score[0] }} ~ {{ policy.review_score[1] }}</td></tr>
|
||
<tr><td>engine_failure_mode</td><td>检测引擎异常时的处理策略(fail_closed=拒绝 fail_open=放行)</td><td><b style="color:#c62828;">{{ policy.engine_failure_mode }}</b></td></tr>
|
||
<tr><td>max_file_size</td><td>允许上传的单文件最大体积(超过直接拒绝)</td><td>{% set kb = policy.max_file_size / 1024 %}{% set mb = kb / 1024 %}{% if mb >= 1 %}{{ "%.1f"|format(mb) }} MB{% else %}{{ "%.0f"|format(kb) }} KB{% endif %}({{ policy.max_file_size }} 字节)</td></tr>
|
||
<tr><td>allow_encrypted_archive</td><td>是否允许带密码的加密 ZIP/RAR/7z 压缩包通过</td><td>{% if policy.allow_encrypted_archive %}<span class="chip chip-ok">允许</span>{% else %}<span class="chip chip-zt">禁止</span>{% endif %}</td></tr>
|
||
<tr><td>allow_encrypted_document</td><td>是否允许带密码的加密 Office 文档(.docx/.xlsx/.pptx)通过</td><td>{% if policy.allow_encrypted_document %}<span class="chip chip-ok">允许</span>{% else %}<span class="chip chip-zt">禁止</span>{% endif %}</td></tr>
|
||
<tr><td>白名单扩展名</td><td>允许上传的文件扩展名(不在此列表的直接拒绝)</td><td style="font-family:sans-serif;">
|
||
{% for e in policy.allowed_extensions %}
|
||
<span class="chip chip-info">{{ e }}</span>
|
||
{% endfor %}
|
||
</td></tr>
|
||
</table>
|
||
</div>
|
||
{% if policy.keyword_rules %}
|
||
<div class="card">
|
||
<h3>词表规则({{ policy.keyword_rules|length }} 条)</h3>
|
||
{% for kw in policy.keyword_rules %}
|
||
<div class="chip chip-info">{{ kw.name }}</div>
|
||
{% endfor %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- ============ YARA 规则库 ============ -->
|
||
<div id="panel-yara" class="panel">
|
||
<div class="card">
|
||
<h3>YARA 规则库({{ yara_files_count }} 个文件 · {{ yara_loaded_count }} 条已编译)</h3>
|
||
{% if yara_rules %}
|
||
<table class="versions">
|
||
<thead>
|
||
<tr>
|
||
<th>规则名</th>
|
||
<th>分类</th>
|
||
<th>风险等级</th>
|
||
<th>误报率</th>
|
||
<th>权重</th>
|
||
<th>触发次数</th>
|
||
<th>参考</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{% for r in yara_rules %}
|
||
<tr>
|
||
<td style="font-family:monospace;font-size:12px;" title="{{ r.name }}">{{ r.name }}</td>
|
||
<td>
|
||
<span class="chip chip-info" title="分类代码 {{ r.category }}">{{ r.category_cn }}</span>
|
||
</td>
|
||
<td>
|
||
{% if r.risk_level_cn %}<span class="chip {{ r.risk_level_chip }}">{{ r.risk_level_cn }}</span>
|
||
{% else %}<span class="chip chip-ok">{{ r.risk_level }}</span>{% endif %}
|
||
</td>
|
||
<td style="color:#888;font-size:12px;">{{ r.false_positive_rate }}</td>
|
||
<td><b>{{ r.weight }}</b></td>
|
||
<td>
|
||
{% if r.trigger_count > 0 %}<span class="chip chip-zt">{{ r.trigger_count }}</span>
|
||
{% else %}<span style="color:#bbb;">0</span>{% endif %}
|
||
</td>
|
||
<td style="font-size:11px;color:#888;">{{ r.reference[:50] }}{% if r.reference|length > 50 %}...{% endif %}</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
{% else %}
|
||
<div style="color:#999;text-align:center;padding:30px;">
|
||
{% if yara_rules_dir %}
|
||
YARA 规则库目录:<code>{{ yara_rules_dir }}</code><br>
|
||
目录内 {{ yara_files_count }} 个 .yar 文件
|
||
{% if yara_files_count == 0 %}—— 请检查规则文件是否存在{% endif %}
|
||
{% else %}
|
||
YARA 规则库未加载(可能 yara-python 未安装或规则目录不存在)
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
<div class="card" style="margin-top:12px;">
|
||
<h3>权重分档说明(来自 policy.yaml → yara_rule_weights)</h3>
|
||
<div style="font-size:13px;color:#555;line-height:1.8;">
|
||
<span class="chip chip-zt">critical → weight=85</span>
|
||
<span class="chip chip-warn">high → weight=65</span>
|
||
<span class="chip chip-info">medium → weight=45</span>
|
||
<span class="chip chip-ok">low → weight=25</span>
|
||
<span style="color:#888;margin-left:12px;">未配置的规则默认 weight=50</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ 版本历史 ============ -->
|
||
<div id="panel-versions" class="panel">
|
||
<div class="card">
|
||
<h3>策略版本历史({{ versions|length }} 条)</h3>
|
||
<table class="versions">
|
||
<thead><tr><th>版本</th><th>SHA256</th><th>创建时间</th><th>操作</th></tr></thead>
|
||
<tbody>
|
||
{% for v in versions %}
|
||
<tr>
|
||
<td>{{ v.version }}</td>
|
||
<td>{{ v.sha256 }}</td>
|
||
<td>{{ v.created_at|bj_time if v.created_at else '' }}</td>
|
||
<td>
|
||
{% if user.role == 'admin' %}
|
||
<form method="post" action="/console/policy/rollback/{{ v.version }}" style="display:inline;">
|
||
<button type="submit" class="btn-danger" style="padding:4px 12px;font-size:12px;">回滚到此版本</button>
|
||
</form>
|
||
{% endif %}
|
||
</td>
|
||
</tr>
|
||
{% else %}
|
||
<tr><td colspan="4" style="text-align:center;color:#999;">暂无版本记录(首次部署执行 scripts.init_db)</td></tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
<script>
|
||
document.querySelectorAll('.tab').forEach(tab => {
|
||
tab.onclick = () => {
|
||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
||
tab.classList.add('active');
|
||
document.getElementById('panel-' + tab.dataset.tab).classList.add('active');
|
||
};
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|