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

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` |
---
**文档结束** — 待审核 → 确认后开始编码