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:
2026-10-28 19:07:11 +08:00
commit 16f134e328
113 changed files with 24604 additions and 0 deletions

1679
docs/00-总体方案.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,494 @@
# MeshSafe Diagnostics Kit v1.0 方案
> 版本: v1.0
> 日期: 2026-10-28
> 状态: **待审核**
> 作者: MeshSafe Team
> 优先级: 高(P0)
---
## 一、定位
> **目标**:让 MeshSafe 运维人员在产品内部就能看到"系统健康状况",遇到问题能一键打包带出来给开发方分析。
>
> **边界**:
> - ❌ 不联网、不依赖外部服务、不做 LLM、不自动上传
> - ✅ 所有分析跑在客户容器里(在线定时)
> - ✅ 导出诊断包(手动触发,U 盘带出)
> - ✅ Docker 能力自动检测 + 三级降级
> - ✅ 全部定时器周期 + 规则阈值配置化
> - ✅ 总开关可关闭整个诊断套件(只留手动导出)
---
## 二、核心设计决策
| # | 决策 | 理由 |
|---|------|------|
| 1 | **在线层:只跑 grep 模式匹配,不收集完整日志** | 光闸内网,日志可能带客户敏感信息;完整日志只在**用户手动导出**时打包 |
| 2 | **三层定时器**(错误类 30s / DB 聚合 60s / 容器类 300s)| 拦截日志要快出快分析;容器/配置类变化慢 |
| 3 | **页面请求时必跑轻量规则**(DB 聚合 + 引擎健康 + 文件 stat) | ~430ms 用户无感,确保页面显示的是最新数据 |
| 4 | **日志匹配 + docker inspect 跑后台定时器** | 涉及 subprocess + IO,耗时 ~2.5s 不阻塞前台 |
| 5 | **Docker 能力三档降级**(full / partial / none) | 客户环境可能挂不了 docker.sock;降级后页面明确告诉用户哪些规则没跑 |
| 6 | **总开关 enabled** | 有的客户不需要/不想跑诊断,一键关掉只留手动导出 |
| 7 | **全部定时器周期 + 规则阈值配置化** | 光闸环境差异大,硬编码不合适 |
---
## 三、功能清单
### 3.1 控制台新页面:诊断面板
| # | 模块 | 展示内容 | 数据源 |
|---|------|---------|--------|
| A1 | 实时健康概览 | 4 张卡片:总扫描量、恶意拦截、**错误比例**(高就红)、引擎在线 | DB ScanTask + scanner.health() |
| A2 | 引擎健康详情 | 每个引擎状态、ClamAV 病毒库日期、YARA 规则数、最近检测耗时 | scanner.health() + 配置快照 |
| A3 | 容器运行状态 | 5 个容器 status / restart_count / Docker 能力检测结果 | docker inspect(或降级跳过)|
| A4 | 异常检测面板 | 规则引擎跑出来的问题列表(每条带严重度、消息、证据、检测时间戳)| 实时计算 + Redis 缓存 |
| A5 | 最近 20 条错误 | 从 DB 查 verdict='error' 的 ScanTask | DB |
| A6 | **一键导出按钮** | 生成 `diagnostics.tar.gz` → 浏览器下载 | 后端打包 |
**页面顶部信息栏**:
```
最后刷新: 10:29:15 🧩 Docker 能力: ✅ 完整(日志检测 + 容器状态)
或 ⚠️ 部分 / ℹ️ 未启用
或 ⛔ 诊断套件未启用
```
**每条规则结果带时间戳**(不同规则可不同,因来自不同运行通道):
```
🔴 R009 ClamAV 被 OOM Kill (检测于 10:29:15) ← 页面请求时跑
🟡 R002 ClamAV 病毒库 14 天未更新 (检测于 10:29:15) ← 页面请求时跑
🟢 R004 所有引擎健康 (检测于 10:28:02) ← 定时器跑
```
**未启用 Docker 时**:跳过 R009-R015,对应规则行显示 `ℹ️ Docker 能力不可用,本条规则未运行`。
**诊断套件总开关关闭时**:页面显示大横幅 `⛔ 诊断套件未启用`,下面规则列表隐藏,只保留手动导出按钮。
### 3.2 规则清单(15 条)
#### 页面请求必跑(轻量,~430ms,每次打开都算)
| 规则 | 类型 | 数据源 | 耗时 |
|------|------|--------|------|
| R001 error 比例 | DB SQL | ScanTask GROUP BY verdict | ~20ms |
| R005 24h 无扫描 | DB SQL | ScanTask MAX(created_at) | ~20ms |
| R002 ClamAV 病毒库日期 | 引擎协议 | clamd SIGINFO | ~100ms |
| R004 引擎健康 | 引擎协议 | scanner.health() | ~300ms |
| R003 YARA 规则 mtime | 文件 stat | os.path.getmtime | <1ms |
#### 定时器组 A:错误/拦截类(30s 周期)
| 规则 | 依赖 | 异常信号 | 匹配模式 |
|------|------|---------|---------|
| R009 ClamAV OOM/crash | docker logs clamd | clamd 被杀/崩 | `Killed` / `exit 137` / `segfault` / `oom-killed` |
| R010 Postgres FATAL | docker logs postgres | 连接池爆 | `FATAL` / `too many connections` / `PANIC` |
| R011 Redis OOM | docker logs redis | 内存满 | `OOM command not allowed` / `out of memory` |
| R012 磁盘满 | 5 个容器 docker logs | 磁盘爆 | `No space left on device` / `ENOSPC` |
| R013 Nginx upstream 失败 | docker logs nginx | api 挂了 | `Connection refused` / `upstream timed out` |
| R014 API 连接异常 | docker logs api | 依赖断了 | `Redis connection refused` / `Postgres connection refused` |
#### 定时器组 B:DB 聚合类(60s 周期)
| 规则 | 数据源 |
|------|--------|
| R006 suspicious > malicious | ScanTask GROUP BY verdict |
| R007 reject 比例 | ScanTask COUNT + verdict |
| R008 同 SHA256 重复 | ScanTask GROUP BY sha256 HAVING COUNT > 1 |
#### 定时器组 C:容器/引擎类(300s 周期)
| 规则 | 依赖 |
|------|------|
| R015 容器重启次数 | docker inspect × 5(任一 restart_count > 3)|
### 3.3 每条规则输出格式
```json
{
"id": "R009",
"severity": "high", // high / medium / low / info
"message": "ClamAV 被 OOM Kill(近 24h 重启 3 次)",
"hint": "给 clamav 容器加内存限制 512m → 1g",
"evidence": [
"meshsafe-clamav 最近日志:Killed (exit 137)",
"meshsafe-clamav restart_count = 3"
],
"run_at": "2026-10-28T10:29:15+08:00",
"run_by": "page_request", // page_request / timer:error_scan / export
"skipped": false,
"skip_reason": null
}
```
### 3.4 一键导出
**端点**:`GET /api/v1/telemetry/dump`(返回文件下载)
**导出包结构**:
```
diagnostics-20261028-103045.tar.gz
├── rules_report.json ← 导出这一刻跑一遍规则引擎的完整结果(带证据 + 时间戳)
├── containers/
│ ├── meshsafe-api.log ← docker logs --tail 2000(导出时抓完整)
│ ├── meshsafe-api.meta.json ← restart_count / started_at / 镜像 tag
│ ├── meshsafe-clamav.log
│ ├── meshsafe-clamav.meta.json
│ ├── meshsafe-postgres.log
│ ├── meshsafe-postgres.meta.json
│ ├── meshsafe-redis.log
│ ├── meshsafe-redis.meta.json
│ ├── meshsafe-nginx.log
│ └── meshsafe-nginx.meta.json
├── scan_tasks.json ← 最近 500 条 ScanTask(脱敏)
├── scan_findings.json ← 对应 ScanFinding(rule name / severity / engine)
├── engine_health.json ← scanner.health() 快照
├── policy_snapshot.json ← 策略版本号 + YARA 规则文件列表(**不含规则原文**)
├── docker_detect.json ← 启动时 Docker 能力检测结果
├── compose_state.json ← docker compose ps 输出(容器运行状态表)
└── meta.json ← 打包时间 / 生成人 / MeshSafe 版本 / 规则版本
```
**数据脱敏策略**:
| 字段 | 处理方式 |
|------|---------|
| 原始文件名 | 保留扩展名 + SHA256 前 8 位 |
| 文件路径 | 截断为相对路径 |
| 用户名 | `u***n` |
| IP | `10.0.1.*`(保留最后一段)|
| 策略 YAML | 默认只传版本号 + 规则列表;加开关可选全量 |
### 3.5 Docker 能力检测
启动时自动运行(`@app.on_event("startup")`),三档降级:
| 检测结果 | 条件 | 影响 | 页面显示 |
|---------|------|------|---------|
| **full** | docker.sock 挂载 + docker CLI 可用 + `docker logs/inspect meshsafe-api` 成功 | R009-R015 全部正常运行 | ✅ Docker 能力完整 |
| **partial** | docker CLI 有但 socket 权限不够 | R009-R015 可能部分失败 | ⚠️ Docker 访问受限,部分容器日志规则未运行 |
| **none** | socket 未挂 / CLI 未装 | R009-R015 **自动跳过** | ℹ️ 未启用 Docker 日志检测 |
检测结果写 `docker_detect.json`(导出包),同时存 Redis 供页面查询。
---
## 四、定时器运行架构
### 4.1 三层定时器 + 页面必跑
```
MeshSafe API 进程
│
├── FastAPI app
│ └── startup event
│ ├── ① DIAG_ENABLED? → false 直接 return(全部定时器不启动)
│ ├── ② Docker 能力检测 → 存内存 dict + Redis
│ ├── ③ asyncio.create_task(_timer_loop("error_scan", 30s, R001/R005/R009-R014))
│ ├── ④ asyncio.create_task(_timer_loop("db_aggregate", 60s, R006/R007/R008))
│ └── ⑤ asyncio.create_task(_timer_loop("container", 300s, R002/R003/R004/R015))
│
├── 页面请求 GET /console/diagnostics
│ ├── 同步跑 R001/R002/R003/R004/R005(~430ms,用户无感)
│ ├── 读 Redis 缓存拿 R006-R015
│ └── 合并 + 渲染(每条带时间戳)
│
└── 手动导出 GET /api/v1/telemetry/dump
├── 阻塞跑完整规则引擎(含实时 + 日志 + 容器)
├── 抓 docker logs --tail 2000(全容器)
├── 打包 tar.gz
└── 用后台线程跑,超时 60 秒
```
### 4.2 单次运行耗时预期
| 通道 | 规则 | 耗时 | 周期 | 并发安全 |
|------|------|------|------|---------|
| 页面必跑 | R001/R002/R003/R004/R005 | ~430ms | 每次页面加载 | 同步 await,不阻塞 worker |
| 定时器 A | R001/R005/R009-R014 | ~2.5s | 30s | asyncio.create_subprocess_exec |
| 定时器 B | R006/R007/R008 | ~60ms | 60s | 单次 SQL 查询 |
| 定时器 C | R002/R003/R004/R015 | ~1.5s | 300s | clamd TCP + docker inspect × 5 |
| 导出 | 全部 15 条 + docker logs 全量 | ~5-8s | 用户手动 | 后台线程,60s 超时 |
### 4.3 Redis 缓存结构
```
ms:diag:latest ← Hash: { rules: [...], generated_at: 1730406555, docker_status: "full", enabled: true }
ms:diag:cache:R001 ← String: { "result": {...}, "ts": 1730406555, "run_by": "timer:error_scan" }
ms:diag:cache:R009 ← 同上
...
ms:diag:docker_status ← String: "full" / "partial" / "none"
ms:diag:enabled ← String: "true" / "false"
```
**TTL**:单条规则缓存 6 分钟;`latest` 整个包 10 分钟。
### 4.4 定时器异常保护
```python
async def _timer_loop(name, interval, rules, docker_status):
"""通用定时器循环。"""
while True:
try:
await asyncio.sleep(interval)
results = await _run_rule_group(rules, docker_status)
await _write_redis(results, name)
except asyncio.CancelledError:
logger.info(f"定时器 {name} 被取消")
break
except Exception as e:
logger.exception(f"定时器 {name} 异常,{interval}s 后重试: {e}")
# 继续 while True,下次 sleep 后重试
```
---
## 五、后端接口清单
| 方法 | 路径 | 说明 | 响应 |
|------|------|------|------|
| GET | `/console/diagnostics` | 诊断面板 HTML | 页面(含 DIAG_ENABLED=false 横幅)|
| GET | `/api/v1/telemetry/summary` | 实时面板数据(页面调用)| JSON |
| GET | `/api/v1/telemetry/engine-health` | 引擎健康快照 | JSON |
| GET | `/api/v1/telemetry/errors?limit=20` | 最近错误列表 | JSON |
| GET | `/api/v1/telemetry/docker-status` | Docker 能力检测结果 | JSON |
| GET | `/api/v1/telemetry/rules` | **所有规则最新结果**(带时间戳 + 运行来源)| JSON |
| GET | `/api/v1/telemetry/dump` | **一键导出 tar.gz** | 文件下载 |
| GET | `/api/v1/telemetry/config` | 当前配置快照(周期、阈值、Docker 模式)| JSON |
---
## 六、后端代码结构
```
app/
├── diagnostic/ ← 新增目录
│ ├── __init__.py
│ ├── config.py ← 读取 diagnostics.yaml + 环境变量覆盖
│ ├── engine.py ← DiagnosticEngine(跑规则集)
│ ├── rules_db.py ← DB 聚合类规则实现(R001/R005-R008)
│ ├── rules_engine.py ← 引擎健康类规则实现(R002-R004)
│ ├── rules_log.py ← 日志匹配类规则实现(R009-R014)
│ ├── rules_container.py ← 容器状态类规则实现(R015)
│ ├── docker_probe.py ← 启动时 Docker 能力检测
│ ├── collector.py ← DB + scanner.health() 原始数据拉取
│ ├── dumper.py ← 打包 tar.gz + 脱敏
│ └── scheduler.py ← 三层定时器循环
├── console.py
│ └── 新增 1 个页面路由 + 7 个 API 路由
├── main.py
│ └── startup event 加 DIAG_ENABLED 检查 + scheduler 启动
├── config/
│ └── diagnostics.yaml ← 新增配置文件
├── templates/
│ ├── diagnostics.html ← 新增诊断面板模板
│ └── _nav.html ← nav_active='diagnostics' 入口
└── Dockerfile
└── 加 `RUN apt-get install docker.io-cli`
```
---
## 七、配置文件完整内容
**文件**:`app/config/diagnostics.yaml`
```yaml
# ===============================================================
# MeshSafe Diagnostics Kit v1.0 配置
# ===============================================================
# 环境变量可覆盖:
# MESHSAFE_DIAG_ENABLED=true|false
# MESHSAFE_DIAG_DOCKER=auto|true|false
# MESHSAFE_DIAG_ERROR_INTERVAL=30
# MESHSAFE_DIAG_CONTAINER_INTERVAL=300
# MESHSAFE_DIAG_DB_INTERVAL=60
# ===============================================================
diagnostics:
# ---------- 总开关 ----------
# true = 启动定时器、检测 Docker、运行规则引擎
# false = 只保留手动导出功能,所有自动诊断关闭
enabled: true
# ---------- Docker 日志/容器状态能力 ----------
# auto = 启动时自动检测 docker.sock 挂载
# true = 强制启用,连不上启动报错
# false = 强制关闭
docker:
enabled: auto
# ---------- 后台定时器周期(秒,0 = 不跑)----------
timers:
error_scan_interval: 30 # 错误/拦截类日志(R001/R005/R009-R014)
container_check_interval: 300 # 容器/引擎类(R002/R003/R004/R015)
db_aggregate_interval: 60 # DB 聚合类(R006/R007/R008)
# ---------- 日志行数控制 ----------
docker_log_tail: 500 # 定时器 grep 最近多少行
export_log_tail: 2000 # 导出时完整抓多少行
# ---------- 规则阈值 ----------
rules:
error_ratio_threshold: 0.05 # R001: error > 5% 告警
clamav_db_stale_days: 7 # R002: 病毒库 > 7 天告警
reject_ratio_threshold: 0.20 # R007: reject > 20% 告警
suspicious_exceeds_malicious: true # R006 开关
```
---
## 八、页面设计
### 8.1 诊断面板布局
```
┌────────────────────────────────────────────────────────────────────┐
│ Nav: 概览 | 策略配置 | 任务历史 | 测试靶场 | 🔧 诊断 ← active │
│ 最后刷新: 10:29:15 🧩 Docker: ✅ 完整 │
├────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─ 实时健康概览 ────────────────────────────────────────────────┐ │
│ │ 扫描总量 恶意拦截 可疑待核 错误比例 引擎在线 │ │
│ │ 12,472 117 47 10.0% 🔴 5/5 ✅ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ ⚠️ 异常检测(本地规则引擎)───────────────────────────────────┐ │
│ │ 🔴 R001 错误比例偏高(10.0% > 阈值 5%) (10:29:15) │ │
│ │ 近 100 次扫描 12 次 error │ │
│ │ 💡 建议检查 ClamAV / Redis / Postgres 引擎 │ │
│ │ │ │
│ │ 🔴 R009 ClamAV 被 OOM Kill(重启 3 次) (10:29:15) │ │
│ │ 证据: meshsafe-clamav stderr: Killed (exit 137) │ │
│ │ 💡 建议给 clamav 容器加内存 512m → 1g │ │
│ │ │ │
│ │ 🟡 R002 ClamAV 病毒库 14 天未更新 (10:29:15) │ │
│ │ 💡 在线更新失败 → U 盘离线导入 │ │
│ │ │ │
│ │ 🟢 所有其他规则通过 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 🧩 容器运行状态 ──────────────────────────────────────────────┐ │
│ │ API ✅ healthy restart: 0 启动: 2h 前 │ │
│ │ Postgres ✅ healthy restart: 0 │ │
│ │ Redis ✅ healthy restart: 0 │ │
│ │ ClamAV ✅ running restart: 3 ⚠️ 近 24h 不稳定 │ │
│ │ Nginx ✅ healthy restart: 0 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ 🧪 引擎状态 ─────────────────────────────────────────────────┐ │
│ │ 🦠 ClamAV ✅ 病毒库: 2026-10-14 (14天前) │ │
│ │ 🎯 YARA ✅ 规则: 205 条 │ │
│ │ 📄 Document ✅ │ │
│ │ 🔑 Keywords ✅ │ │
│ │ ⚙️ Binary ✅ │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─ ❌ 最近 20 条错误 ────────────────────────────────────────────┐ │
│ │ [table: 时间 | 文件名 | 错误原因 | 引擎 | 分数] │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ [📦 导出诊断包] 按钮 │
└────────────────────────────────────────────────────────────────────┘
```
### 8.2 总开关关闭时
```
┌────────────────────────────────────────────────────────────────────┐
│ ⛔ 诊断套件未启用 │
│ diagnostics.yaml: diagnostics.enabled = false │
│ │
│ 诊断套件提供实时健康检测、容器运行状态监控、一键诊断包导出。 │
│ 关闭后仅保留手动导出功能,所有自动规则引擎将停止运行。 │
│ │
│ 启用方式: │
│ 1. 修改 app/config/diagnostics.yaml: enabled → true │
│ 2. 或设置环境变量:MESHSAFE_DIAG_ENABLED=true │
│ 3. 重启 api 容器 │
│ │
│ [📦 手动导出诊断包(离线收集)] │
└────────────────────────────────────────────────────────────────────┘
```
---
## 九、实施计划
```
Phase 1(3-4 天)— 核心框架
├── app/diagnostic/ 目录结构 + 配置读取
├── DiagnosticEngine 框架 + 规则基类
├── DB 聚合类规则(R001/R005-R008)
├── 引擎协议类规则(R002/R003/R004)
└── Docker 能力检测
Phase 2(2-3 天)— 定时器 + 页面
├── 三层定时器 + Redis 缓存
├── 页面必跑规则通道
├── 诊断面板 HTML 模板(含 Docker 状态 + 总开关横幅)
├── 异常列表 + 时间戳显示
└── docker-compose.yml api 服务加 docker.sock 挂载
Phase 3(2 天)— 日志匹配 + 导出
├── 日志匹配类规则(R009-R014)
├── docker inspect 类规则(R015)
├── 一键导出 tar.gz + 脱敏
├── Dockerfile 加 docker CLI
└── 导出包结构完成
Phase 4(1 天)— 测试 + 验证
├── docker build + 重启 + 功能验证
├── 模拟无 Docker 环境(不挂 socket)→ 验证降级
├── 关闭总开关 → 验证横幅 + 手动导出仍可用
└── 全接口自测
```
---
## 十、后续演进(不在 v1.0)
| 版本 | 功能 |
|------|------|
| v1.1 | 导出包可选"含完整策略 YAML";规则阈值页面可配(不用改 yaml)|
| v1.1 | 导出包可选"上传到指定 S3 桶"(如果客户有内网 S3)|
| v2.0 | 云端收集服务(MeshSafe Pulse):多客户聚合 + dashboard + 规则引擎云端版 |
| v2.0 | LLM 辅助分析:把 diagnostics.tar.gz 喂给 LLM 生成结构化报告 |
| v3.0 | AI 辅助修复闭环(仅在你自己的 sandbox 里做,不碰客户环境)|
---
## 附录 A:Dockerfile 改动
```dockerfile
# ===== 新增:装 docker CLI =====
# api 容器需要 docker CLI 才能通过 docker.sock 访问宿主机 Docker
RUN apt-get update && apt-get install -y --no-install-recommends \
docker.io-cli \
&& rm -rf /var/lib/apt/lists/*
```
## 附录 B:docker-compose.yml 改动
```yaml
api:
volumes:
# ... 原有 volume ...
- /var/run/docker.sock:/var/run/docker.sock:ro # ← 新增:只读挂载
```
## 附录 C:快速降级指引
| 问题 | 处理 |
|------|------|
| 启动报 `ModuleNotFoundError: docker` | 忘装 docker CLI,跑 `apt-get install docker.io-cli` |
| 启动后页面显示 `ℹ️ Docker 能力未启用` | 没挂 docker.sock;compose api volumes 加一行 + rebuild |
| 想完全关掉诊断 | `diagnostics.enabled: false` 或环境变量 |
| 定时器吃 CPU | 三个定时器周期都可以调大;或设为 0 不跑某个 |
| 导出包太大 | `export_log_tail` 调小;或只导出 `scan_tasks.json` |
---
**文档结束** — 待审核 → 确认后开始编码

1322
docs/接口文档.md Normal file

File diff suppressed because it is too large Load Diff

459
docs/策略配置说明.md Normal file
View File

@@ -0,0 +1,459 @@
# MeshSafe 策略配置说明
| 项目 | 内容 |
|---|---|
| 适用对象 | 安全管理员、系统管理员 |
| 配置文件 | `config/policy.yaml` |
| 生效方式 | 修改后调用 `POST /api/v1/policies/reload` 或重启服务 |
| 文档版本 | v1.0 |
---
## 目录
1. [配置总览](#1-配置总览)
2. [字段详解](#2-字段详解)
3. [裁决流程](#3-裁决流程)
4. [调整场景手册](#4-调整场景手册)
5. [校验与错误处理](#5-校验与错误处理)
6. [变更管理](#6-变更管理)
---
## 1. 配置总览
策略文件采用 YAML 格式,路径由环境变量 `MESHSAFE_POLICY_FILE` 指定(默认 `/app/config/policy.yaml`)。
**核心原则:策略与代码分离。** 调整判定规则、阈值、白名单只需修改本文件,不需要重新构建或发布镜像。
```yaml
version: "2026.10.1"
description: "MeshSafe 默认全局策略(法院文书摆渡场景)"
global:
block_score: 70
review_score: [30, 70]
engine_failure_mode: fail_closed
max_file_size: 52428800
sync_max_size: 52428800
allow_encrypted_archive: false
allow_encrypted_document: false
allowed_extensions: [ ... ]
sandbox_required_extensions: [ ... ]
rule_overrides: { ... }
notification: { ... }
audit: { ... }
```
### 顶层结构
| 键 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `version` | string | 是 | 策略版本号,建议用日期格式。写入每条扫描结果 |
| `description` | string | 否 | 策略说明 |
| `global` | map | 是 | 全局阈值与行为开关 |
| `allowed_extensions` | list | 是 | 允许传输的扩展名白名单 |
| `sandbox_required_extensions` | list | 否 | 强制走沙箱深检的类型 |
| `rule_overrides` | map | 否 | 特定规则的强制处置 |
| `notification` | map | 否 | 判定理由的输出控制 |
| `audit` | map | 否 | 审计记录内容控制 |
---
## 2. 字段详解
### 2.1 global
#### 2.1.1 `block_score`
| 项 | 说明 |
|---|---|
| 类型 | 整数,0~100 |
| 默认 | `70` |
| 含义 | 风险评分 **≥ 此值** 时判定为 `malicious`,处置动作为 `reject` |
调整方向:调低 → 更严格(更多文件被拒),调高 → 更宽松(需积累更多信号才拒绝)。
> 注意:单条 `critical` 级信号通常权重已达 50~90,因此 `block_score` 低于 50 时,大量正常文档会因多条低级信号叠加被拒。
#### 2.1.2 `review_score`
| 项 | 说明 |
|---|---|
| 类型 | 数组 `[下界, 上界]` |
| 默认 | `[30, 70]` |
| 含义 | 评分落在该区间时判定为 `suspicious`,处置动作为 `manual_review`(转人工复核) |
**这是误报缓冲的关键参数。** 调整方向:调低下界 → 更多文件进入人工复核(更安全但人工压力大);调高下界 → 更少人工复核(但风险文件可能被直接放行)。
#### 2.1.3 `engine_failure_mode`
| 项 | 说明 |
|---|---|
| 类型 | 字符串 `fail_closed` / `fail_open` |
| 默认 | `fail_closed` |
| 含义 | 检测引擎不可用时的整体行为 |
| 取值 | 行为 | 适用 |
|---|---|---|
| `fail_closed` | 直接拒绝,并说明引擎不可用 | **生产环境必须使用此项** |
| `fail_open` | 放行但标记 | 仅限测试环境 |
> 等保三级要求"安全优先",生产环境使用 `fail_open` 会在测评时被判定为不符合项。
#### 2.1.4 `max_file_size`
| 项 | 说明 |
|---|---|
| 类型 | 整数(字节) |
| 默认 | `52428800`(50 MB) |
| 含义 | 单文件大小上限。超限直接拒绝 |
#### 2.1.5 `sync_max_size`
| 项 | 说明 |
|---|---|
| 类型 | 整数(字节) |
| 默认 | `52428800`(50 MB) |
| 含义 | 同步模式处理的文件大小上限。超限转异步 |
必须 ≤ `max_file_size`。若大于,则会出现"文件本身允许但无法同步处理"的中间态。
#### 2.1.6 `allow_encrypted_archive` / `allow_encrypted_document`
| 项 | 说明 |
|---|---|
| 类型 | 布尔 |
| 默认 | `false` |
| 含义 | 是否允许加密的压缩包 / 加密的文档 |
加密文件无法被静态分析,**在无法确认内容安全的前提下不应放行**。确需放行时,应配合人工复核流程。
### 2.2 allowed_extensions
允许传输的扩展名白名单。
```yaml
allowed_extensions:
- .docx
- .pdf
- .xlsx
- .jpg
- .png
- .zip
```
规则:
- 必须以 `.` 开头(小写),否则加载校验失败
- 比较时统一转小写,配置中的大写会被拒绝
- **列表为空表示不限制**,生产环境强烈不建议
- 只校验**顶层文件**的扩展名;归档内的子文件由各自规则判定(受 `rule_overrides` 控制)
> 白名单是第一道也是最粗的一道防线。它挡不住"扩展名正确但内容是恶意"的攻击——那是 L1/L2 层的职责。
### 2.3 sandbox_required_extensions
强制走沙箱异步深检的扩展名。
```yaml
sandbox_required_extensions:
- .exe
- .msi
- .js
- .vbs
- .iso
```
**当前版本(P3 一期)沙箱未实现**,配置此项的效果是:命中的文件会走异步模式,并在 `meta` 中标注需要深检。P4 阶段接入 CAPE 后无需修改本项。
### 2.4 rule_overrides
特定规则的强制处置。**这是绕过评分直接裁决的能力**,用于把"一票否决"型规则固化下来。
```yaml
rule_overrides:
MRaptorAutoExecSuspicious:
action: reject
reason: "文档宏具备自动执行且包含可疑行为,按策略直接拒绝"
DDEAutoExecution:
action: reject
reason: "检测到 DDE/DDEAUTO 自动执行字段,按策略直接拒绝"
```
| 字段 | 说明 |
|---|---|
| `action` | 处置动作,见下表 |
| `reason` | 判定理由,可直接展示给用户(支持中文) |
**action 取值**
| 取值 | 行为 |
|---|---|
| `pass` | 放行 |
| `pass_with_flag` | 放行并标记 |
| `manual_review` | 转人工复核(同时把评分提升至 65,确保落入复核区间) |
| `reject` | 拒绝 |
| `reject_and_notify` | 拒绝并通知(当前实现等同 `reject`) |
**生效逻辑**
1. 命中 `rule_overrides` 的规则会**优先于评分**处理
2. 若 `action` 为 `reject` 类 → 立即返回拒绝结论,不再累加评分
3. 若 `action` 为 `manual_review` → 评分提升至 65 并继续后续裁决
**为什么用覆盖而不是纯评分**:像 `PathTraversal`、`DDEAutoExecution` 这类信号,一旦出现就足以判定不可放行,不应依赖其他信号累加来"凑够"阈值。
> 规则名必须与《接口文档》§6 中的 `rule` 字段完全一致,包括大小写。写错不会报错,只是永不生效——因此**每次修改后应实际测试验证**。
### 2.5 notification
```yaml
notification:
include_reason: true
max_reason_length: 500
```
| 字段 | 说明 |
|---|---|
| `include_reason` | 是否在结果中返回判定理由。关闭后 `reason` 为空字符串 |
| `max_reason_length` | 理由最大长度,超出截断(末尾加 `...`) |
> `include_reason` 只影响**返回给调用方的内容**,不影响审计日志。审计日志始终记录完整信息。
### 2.6 audit
```yaml
audit:
log_findings: true
log_extracted_paths: true
```
| 字段 | 说明 |
|---|---|
| `log_findings` | 审计日志中是否记录命中的规则明细 |
| `log_extracted_paths` | 是否记录归档内提取出的文件路径 |
关闭 `log_findings` 可显著降低审计日志体积,代价是事后无法还原"当初为什么判定为恶意"。**建议保持开启**。
---
## 3. 裁决流程
```
┌─────────────────────────┐
检测信号 ────────▶│ 步骤 1:引擎可用性检查 │──▶ 引擎不可用 → fail_closed 拒绝
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 2:文件大小检查 │──▶ 超限 → 拒绝
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 3:扩展名白名单 │──▶ 不在清单 → 拒绝
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 4:加密文件检查 │──▶ 加密且不允许 → 拒绝
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 5:规则覆盖匹配 │──▶ 命中 reject 类 → 立即拒绝
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 6:权重累加评分 │
└─────────────────────────┘
│
▼
┌─────────────────────────┐
│ 步骤 7:阈值映射处置 │
└─────────────────────────┘
```
### 3.1 评分计算
```
score = min(Σ(命中信号的 weight), 100)
```
已被 `rule_overrides` 处理的信号不重复累加。
### 3.2 阈值映射
| 评分区间 | verdict | action |
|---|---|---|
| `score == 0` | `clean` | `pass` |
| `0 < score < review_low` | `suspicious` | `pass_with_flag` |
| `review_low ≤ score < block_score` | `suspicious` | `manual_review` |
| `score ≥ block_score` | `malicious` | `reject` |
### 3.3 权重来源
| 层级 | 典型权重 |
|---|---|
| L0 结构层 | 10 ~ 50 |
| L1 ClamAV | 25 ~ 90 |
| L2 文档深度 | 15 ~ 80 |
| L2B YARA / 二进制 | 10 ~ 85 |
权重定义在各检测引擎内部,调整权重要改代码——**因此优先用 `rule_overrides` 表达策略意图**,而不是频繁改权重。
---
## 4. 调整场景手册
### 4.1 场景:误报过多,业务被阻塞
**症状**:大量正常文档被判为 `manual_review` 或 `reject`。
**排查步骤**
1. 查审计日志,找出被拒文件的命中规则
2. 判断是哪条规则在误报
**处理方式**
| 误报来源 | 处理 |
|---|---|
| 某条规则过于敏感 | 用 `rule_overrides` 将该规则降级为 `pass_with_flag`,并改写 `reason` 说明 |
| 阈值过低 | 提高 `review_score` 下界,或提高 `block_score` |
| 白名单缺少某类型 | 在 `allowed_extensions` 补充 |
| 正常文件确实含宏(如模板文件) | 用 `rule_overrides` 对特定规则放行,**但需评估该文件是否会成为攻击载体** |
> 建议不要直接调高 `block_score`——那会让真正的恶意文件更容易通过。优先降级具体规则。
### 4.2 场景:漏报,需要提升严格度
| 措施 | 配置 |
|---|---|
| 降低复核门槛 | 调低 `review_score` 下界 |
| 直接提高拒绝门槛 | 调低 `block_score` |
| 某类风险必须拦截 | 在 `rule_overrides` 中加 `reject` |
| 禁用某类可执行文件 | 确认 `allowed_extensions` 中不含该类型 |
| 加密文件也拦截 | 保持 `allow_encrypted_*` 为 `false` |
### 4.3 场景:新增业务通道,需要独立策略
**当前版本为全局单策略。** 如需按通道区分,两种做法:
**做法 A(不推荐,需改代码)**:在策略中引入 `channels` 段,裁决时按 `channel` 取对应子策略。
**做法 B(推荐,零代码改动)**:在摆渡系统侧做分流。摆渡系统按通道自行维护白名单,检测服务只做统一的恶意性判定。
> 本期方案已确认采用全局策略。通道字段贯穿全链路(数据库、审计日志、API),后续扩展无需重构。
### 4.4 场景:允许某类加密文件
```yaml
global:
allow_encrypted_document: true
```
**同时必须配套**:加密文件无法静态检测,风险由人工复核承担。建议同时把该类文件的处置设为 `manual_review`:
```yaml
rule_overrides:
PdfEncrypted:
action: manual_review
reason: "PDF 已加密,无法自动检测,转人工复核"
```
### 4.5 场景:新增一种文件类型要放行
1. 在 `allowed_extensions` 补充扩展名
2. 确认该类型没有对应的深度检测引擎(否则即使放行也只是"未检测",不是"已检测通过")
3. 若该类型有特殊风险(如脚本类),评估是否需要新增检测规则
---
## 5. 校验与错误处理
### 5.1 加载时校验项
策略文件在加载时执行严格校验,**任一项不通过则拒绝加载并保留原有策略**:
| 校验项 | 规则 |
|---|---|
| 根节点类型 | 必须是映射结构 |
| `review_score` | 下界必须小于上界 |
| `block_score` | 不应低于 `review_score` 上界;须在 0~100 之间 |
| `rule_overrides[].action` | 必须是合法动作枚举 |
| `allowed_extensions` | 每项必须以 `.` 开头 |
| `sync_max_size` | 须 ≤ `max_file_size` |
> **为什么校验失败要保留旧策略**:如果加载失败时回退到"默认策略"或"空策略",等于把生产环境的判定逻辑悄悄改掉了,比直接报错危险得多。
### 5.2 重载失败的表现
```json
{
"detail": "策略校验失败: block_score (50) 不应低于 review_score 上界 (70)"
}
```
原策略继续生效。修正后重新调用重载接口。
### 5.3 常见配置错误
| 错误 | 后果 | 修正 |
|---|---|---|
| 扩展名写成 `docx`(漏点) | 加载校验失败 | 改为 `.docx` |
| 扩展名大写 `.DOCX` | 加载校验失败 | 改为小写 |
| `rule_overrides` 规则名拼错 | **静默失效** | 对照接口文档 §6 核对,并实际测试 |
| `block_score` 调得极低(如 10) | 正常文件大量被拒 | 回调至 60~80 |
| `allowed_extensions` 留空 | 所有类型都被拒 | 补充清单 |
> **规则名拼错是唯一会静默失效的配置错误**,务必实测。
---
## 6. 变更管理
### 6.1 变更流程
```
提出变更 → 安全管理员评审 → 备份当前策略 → 修改配置
→ 语法校验(本地) → 重载 → 用样本文件验证 → 记录变更
```
### 6.2 变更记录要求
每次策略变更应记录:
| 项 | 说明 |
|---|---|
| 变更时间 | 精确到分钟 |
| 变更人 | 姓名与账号 |
| 变更内容 | 具体字段的前后值 |
| 变更原因 | 业务或安全需求说明 |
| 影响评估 | 是否影响在途文件、是否需要通知对接方 |
| 验证方式 | 用哪些样本文件验证、结果如何 |
### 6.3 版本追溯
每次扫描结果携带 `meta.policy_version` 与 `meta.policy_sha256`。数据库中保留策略版本快照,可用于**按历史策略复现**某一历史文件的判定结果。
### 6.4 灰度建议
策略变更影响面大时,建议:
1. 先在测试环境用真实样本文件验证
2. 对比新旧策略的判定差异(可通过 `rescan` 接口批量重跑)
3. 差异符合预期后再在生产重载
---
## 附录:完整默认策略
见 `config/policy.yaml`。本文档说明各字段含义,实际以配置文件为准。
---
**文档结束** · 配套文档:《总体方案》《接口文档》《等保三级对照表》《运维手册》