← 返回 Day 165 正文

Day 165 · 图解

Day 165 — 图解:报告渲染管线与容器化交付

本文件用 ASCII 图与 Mermaid 图讲清 Day 165 的三条主线: 报告管线、确定性来源、容器交付边界。


图 1:报告渲染管线(数据流,单向)

  ┌────────────────────────────────────────────────────────────────────────┐
  │ 阶段 0 · 收集(三个面汇入)                                             │
  │                                                                        │
  │   Day 162 资产面        Day 163 漏洞面        Day 164 流量面            │
  │   ┌────────────┐       ┌────────────┐       ┌────────────┐            │
  │   │ 端口开放    │       │ SQLi 错误回显│       │ 明文凭证    │            │
  │   │ 目录列表    │       │ 反射型 XSS  │       │ 可疑 UA     │            │
  │   └─────┬──────┘       └─────┬──────┘       └─────┬──────┘            │
  │         │                    │                    │                   │
  │         └────────────────────┼────────────────────┘                   │
  │                              ▼                                        │
  │                    ┌──────────────────┐                               │
  │                    │ Finding 列表      │  ← 结构化数据,不是文本        │
  │                    └────────┬─────────┘                               │
  └─────────────────────────────┼──────────────────────────────────────────┘
                                ▼
  ┌────────────────────────────────────────────────────────────────────────┐
  │ 阶段 1 · 归一化(**唯一**允许做计算的地方)                             │
  │                                                                        │
  │   去重  · 同 id 只保留 risk 最高的一条                                  │
  │   排序  · (severity ↑, risk ↓, target ↑, rule_id ↑)  ← 四元组保证全序   │
  │   计数  · 含 0 值(表格形状稳定,便于 diff)                            │
  │   脱敏  · 出口兜底,幂等                                                │
  └─────────────────────────────┬──────────────────────────────────────────┘
                                ▼
  ┌────────────────────────────────────────────────────────────────────────┐
  │ 阶段 2 · 中间表示 IR(与格式无关)                                      │
  │                                                                        │
  │   { schema, title, scope, generated_at, tool_version,                  │
  │     total_risk, counts{}, coverage_gaps[], notes[], findings[] }       │
  └─────────────────────────────┬──────────────────────────────────────────┘
                                ▼
  ┌──────────────┬──────────────┬──────────────┬──────────────────────────┐
  │ render_json  │ render_md    │ render_html  │ render_sarif             │
  │              │              │              │                          │
  │ json.dumps   │ 拼字符串      │ 拼字符串      │ 结构映射                  │
  │ **零计算**    │ **零计算**    │ + escape()   │ severity→level(3档)      │
  │              │              │              │ 原始值留在 properties     │
  └──────┬───────┴──────┬───────┴──────┬───────┴────────────┬─────────────┘
         ▼              ▼              ▼                    ▼
   机器 / 归档      评审 / 工单      汇报 / 截图         CI / IDE
                                             │
                                             ▼
                                    ┌──────────────────┐
                                    │ 退出码 0/1/2/3/4 │ ← 流程消费者
                                    └──────────────────┘

图 2:确定性的三个杀手与对策

   不确定来源                    症状                        对策
  ────────────────────────────────────────────────────────────────────────
   ① datetime.now()     每次运行时间戳都不同       generated_at 由调用方传入
                                                   或 SOURCE_DATE_EPOCH 固定
  ────────────────────────────────────────────────────────────────────────
   ② 迭代顺序           多线程/集合/dict 顺序不定   排序键写死四元组
                                                   (末位 rule_id 保证全序)
  ────────────────────────────────────────────────────────────────────────
   ③ uuid4() / 自增号   同一条问题每次换 id        内容哈希 stable_id(
                                                    target, rule_id, severity)
  ────────────────────────────────────────────────────────────────────────

   结果:同一输入 → 字节相同的输出 → 可 diff、可哈希、可缓存、可复现

Mermaid 版(支持渲染的环境下):

flowchart LR A[原始观测] --> B{含时间戳?} B -- 是 --> C[替换为外部注入的<br/>generated_at] B -- 否 --> D{顺序稳定?} C --> D D -- 否 --> E[统一排序<br/>severity/risk/target/rule_id] D -- 是 --> F{id 稳定?} E --> F F -- 否 --> G[改用内容哈希] F -- 是 --> H[字节确定的报告] G --> H H --> I[git diff 可用] H --> J[产物哈希可信] H --> K[回归对比无噪声]
flowchart LR
    A[原始观测] --> B{含时间戳?}
    B -- 是 --> C[替换为外部注入的<br/>generated_at]
    B -- 否 --> D{顺序稳定?}
    C --> D
    D -- 否 --> E[统一排序<br/>severity/risk/target/rule_id]
    D -- 是 --> F{id 稳定?}
    E --> F
    F -- 否 --> G[改用内容哈希]
    F -- 是 --> H[字节确定的报告]
    G --> H
    H --> I[git diff 可用]
    H --> J[产物哈希可信]
    H --> K[回归对比无噪声]

图 3:退出码决策树(CLI 对外契约)

                       ┌──────────────┐
                       │  启动 CLI    │
                       └──────┬───────┘
                              ▼
                  ┌────────────────────────┐
                  │ 输入可读且 JSON 合法?  │──否──► 退出码 2
                  └───────────┬────────────┘        (用法/数据错误)
                              │是
                              ▼
                  ┌────────────────────────┐
                  │ severity 等字段合法?   │──否──► 退出码 2
                  └───────────┬────────────┘   (宁可不跑,也不静默丢条目)
                              │是
                              ▼
                  ┌────────────────────────┐
                  │ 渲染 + 写盘成功?       │──否──► 退出码 3
                  └───────────┬────────────┘   (磁盘/权限)
                              │是
                              ▼
                  ┌────────────────────────┐
                  │ 有 >= 阈值的发现?      │──是──► 退出码 1
                  └───────────┬────────────┘     (去修)
                              │否
                              ▼
                  ┌────────────────────────┐
                  │ 存在覆盖缺口?          │──是──► 退出码 4
                  └───────────┬────────────┘     (补测 或 显式接受盲区)
                              │否
                              ▼
                         ┌─────────┐
                         │ 退出码 0 │
                         └─────────┘

为什么 1 的优先级高于 4? 因为有高危时行动项明确且紧急(去修), 而覆盖缺口是"需要判断"的状态。多返回一个码不会让人更快行动, 反而会稀释"这个码必须马上处理"的信号强度。


图 4:容器交付边界(信任分层)

  ┌───────────────────────────── 宿主(信任边界) ──────────────────────────────┐
  │                                                                            │
  │    ./findings.example.json  ──ro──┐        ./out/ (需 chown 10001)          │
  │                                   │              ▲                         │
  │   ┌───────────────────────────────┼──────────────┼──────────────────────┐  │
  │   │ 容器 audit-toolbox:1.0        ▼              │                      │  │
  │   │                                                                    │  │
  │   │   uid=10001 (非 root)    read_only rootfs    cap_drop: ALL          │  │
  │   │   network: none          no-new-privileges   /tmp: 16m, noexec      │  │
  │   │   mem: 256M              cpus: 1.0                                  │  │
  │   │                                                                    │  │
  │   │   /app/report_core.py    ← 只读(改不了自己)                        │  │
  │   │   /app/03-toolbox-cli.py                                            │  │
  │   │   /data/findings.json    ← 只读输入                                  │  │
  │   │   /out/report.*          ← 唯一可写出口                              │  │
  │   │                                                                    │  │
  │   │   PID 1 = python(exec 形式 ENTRYPOINT)                             │  │
  │   │        └─ 退出码直接冒泡给 docker / CI  ──► 门禁成立                 │  │
  │   └────────────────────────────────────────────────────────────────────┘  │
  │                                                                            │
  │   ❌ 不烤进镜像:findings 数据、凭证、客户信息                              │
  │      (镜像会被推送/缓存/拉取,进层就删不干净)                             │
  └────────────────────────────────────────────────────────────────────────────┘

图 5:EACCES 排查路径(本课实测踩坑)

   ❌ Permission denied: '/out/day165-docker.md'

        ┌──────────────────────────────────────────────┐
        │ 第一步:看容器里是谁                          │
        │   docker run --rm --entrypoint id <image>     │
        │   → uid=10001(app) gid=10001(app)             │
        └───────────────────┬──────────────────────────┘
                            ▼
        ┌──────────────────────────────────────────────┐
        │ 第二步:看宿主目录属主                        │
        │   ls -ld out/                                 │
        │   → drwxr-xr-x root root   ← 10001 写不进去   │
        └───────────────────┬──────────────────────────┘
                            ▼
        ┌──────────────────────────────────────────────┐
        │ 修法 A(推荐)chown 10001:10001 out/          │
        │ 修法 B(调试)--user $(id -u):$(id -g)        │
        │ 修法 C(生产)用命名卷 -v audit-out:/out       │
        │                                              │
        │ ✗ 不推荐:把容器改回 root                     │
        │   —— 用整个沙箱换一个目录权限,本末倒置        │
        └──────────────────────────────────────────────┘

这次翻车的正面价值:程序没有崩溃抛堆栈,而是输出人类可读的错误 并返回退出码 3。这个行为本身验证了第 3.4 节的退出码设计—— "渲染/写盘失败"和"有高危发现"是可区分的两类故障。


图 6:四种格式的信息损失地图

                  信息量(全部字段 + 排序 + 精度)
                              ▲
                              │
        JSON ●────────────────┤  无损失(机器最优)
                              │
        SARIF ●───────────────┤  severity 5 档 → level 3 档
                              │  (原始值保留在 properties)
        HTML ●────────────────┤  需 escape,否则自己变 XSS 载体
        Markdown ●────────────┤  无样式、无机器定位
                              │
                              └────────────────────────────────►
                                 人类可读性(排版 / 层级 / 直观)

结论:不存在"一个格式打天下"。JSON 给机器、Markdown/HTML 给人、 SARIF 给流程,各自承担自己的那一段。压缩可以,但必须可还原。