# ADR-0006: 测试用例对齐 OWASP 框架（owasp_refs）

**状态**: 已采纳
**日期**: 2026-05-08
**参与者**: roger

## 背景

`AgentSec` 已有 91 条测试用例，按攻击面（prompt_injection / tool_abuse / SSRF
等）分类。但下游读者（评审、外部用户、CI 报告消费者）期望按 OWASP 框架编号
查阅覆盖度 ——「LLM01:2025 我有几条？LLM10:2025 有没有？」。当前没有这种视
角。`threat_refs` 字段已经在用，但语义是「外部 TI ID」而非「框架编号」。

下一步还要陆续接入 OWASP Agentic（v1.1，17 条）和 OWASP MCP（待研究）；如
果每个框架都新加一个字段（`agentic_refs` / `mcp_refs`）会让 schema 膨胀。

## 决策

在 `TestCase` 上新增一个**单一**字段 `owasp_refs: list[str]`，承载所有 OWASP
框架的引用。条目按前缀区分框架：

- `LLM01:2025` … `LLM10:2025` —— OWASP Top 10 for LLM Applications 2025 版
- `AGENTIC-T1` … `AGENTIC-T17` —— OWASP Agentic AI Threats v1.1
- `MCP\d+` —— OWASP MCP（保留前缀，Phase 7c 启用）

由 Pydantic field_validator 用一个编译好的正则强校验，未识别的前缀直接拒绝。
报告渲染器与 CI 脚本都从 `owasp_refs` 派生 coverage 视图，不再扫 `threat_refs`。

## 理由

- **单一字段**：避免每框架一个字段的 schema 膨胀；下游聚合代码只需识别前缀。
- **强校验**：拼写错误（如 `LLM5:2025` / `Agentic-T1`）在 case 加载阶段即报错，
  避免「沉默丢失覆盖」。
- **与 `threat_refs` 解耦**：`threat_refs` 仍然指向具体 CVE / TI 行；
  `owasp_refs` 指向抽象框架编号。两者职责不重叠。

## 备选方案

1. **复用 `threat_refs`**：在 `threat_intel.yaml` 里新增 `LLM01:2025` 这样的
   行，case 用 `threat_refs: [LLM01:2025]` 引用。
   *为何放弃*：把 OWASP 框架塞进 TI 数据库语义不正 —— TI 是「具体威胁实例」，
   框架编号是「分类标签」，混在一起会让 IOC 流水线（`ioc-update` / NVD 拉取）
   逻辑变复杂。
2. **每框架一个字段**：`llm_top10_refs` / `agentic_refs` / `mcp_refs`。
   *为何放弃*：schema 膨胀；要在多个地方维护同一类聚合逻辑。
3. **`tags: list[str]`**：复用现有 `tags` 字段。
   *为何放弃*：`tags` 是自由字符串，没有强校验，与「精确框架编号」的需求不
   匹配 —— 容易写成 `tags: [llm01]` / `[OWASP-LLM-01]` 等不一致的形态。

## 后果

- **加载契约变更**：YAML case 以前没有 `owasp_refs`，加载仍然成功（默认 `[]`）；
  本次会一次性给所有 91 条 case 补 tag。
- **CI 多了一道闸**：`scripts/check_owasp_coverage.py` 失败时阻塞合入，
  避免「新加 case 忘了打 owasp_refs」或「LLM 某分类被退化为 0」。
- **报告体积略增**：每份 remote-report.md / combined-report.md 多一段
  「OWASP 框架覆盖」表（约 25 行 markdown）。
- **MCP 留白**：MCP 前缀保留，但 Phase 7a 不消费；外部读者会看到「MCP 暂无
  case」，这是有意为之的留白，对应 Phase 7c。
