triarchsecurity/stpa
GitHub: triarchsecurity/stpa
基于 STPA 控制论方法论的软件威胁建模工具,通过分析控制结构中的不安全控制行为来发现组件级扫描无法覆盖的系统性安全缺陷。
Stars: 0 | Forks: 0
# stpa — 面向软件的基于控制论的威胁建模
将它指向某个代码仓库或设计文档。你将获得一个控制结构、一个可量化的不安全控制行为网格,以及一个**已排序的工程待办列表**,其中每一项都包含文件位置、成本估算,以及一个用于证明修复已落地的命令。
基于 **STPA**(System-Theoretic Process Analysis — Leveson & Thomas, *STPA Handbook*, 2018)和 **STPA-Sec** 的安全框架(Young & Leveson, *CACM* 57(2), 2014)构建。
完全在离线环境下运行。无需 API 密钥,无需网络调用,无需遥测,无需账号。
## 为什么不用 STRIDE
STRIDE、攻击树及其衍生方法都是**组件与技术枚举**:分解系统,然后针对每个部分逐一核对攻击者技术列表。这种方法对于其设计初衷所针对的失效情况是有效的——即组件做了它不该做的事。
但它在结构上无法发现现代主流漏洞类别:**每个组件的行为都完全符合规范,但系统组合起来却仍然不安全。**
损坏的对象级授权。租户隔离失败。TOCTOU。过期的权限窗口。混淆代理。批量赋值。工作流状态滥用。双重支付竞态。通过管理工具绕过路径。在上述每一个问题中,*没有任何东西是损坏的*。授权中间件正确地授权了它被要求做的事。数据库正确地返回了它被要求查询的行。损失源于**正确组件之间的控制关系**。
还有第二种较少被讨论的失效情况:**检查清单只会与检查清单本身一起老化。** 在编写清单时不存在的攻击技术不会出现在清单上。STPA 的发现集只有在*设计*发生变化时才会改变,因为它枚举的是危险的**上下文**,而不是技术。
两者都使用。这并不是 STRIDE 的替代品——STRIDE 恰好在 STPA 的薄弱环节(组件级、协议级、已知技术覆盖)表现出色。已发表的对比文献(Kavallieratos 等人,*Computers & Security*,2023)明确指出了这一点,`METHOD.md` 也对此进行了引用。
## 安装
需要 [Bun](https://bun.sh)(或者为带有 TS 加载器的 Node ≥20 适配 shebang)。
```
git clone https://github.com/triarchsecurity/stpa.git && cd stpa
chmod +x stpa
./stpa --help
```
可选:将其添加到你的 PATH 中:
```
ln -s "$PWD/stpa" /usr/local/bin/stpa
```
## 战术操作 —— 运行一次
```
stpa scan ./my-repo --json > candidates.json # 1. orientation (candidates, NOT findings)
stpa new .stpa # 2. scaffold
# ...写入 .stpa/01-scope.md — losses 和 hazards
# ...写入 .stpa/model.json — control 结构 ← 这是实际的工作
stpa init .stpa/model.json -o .stpa/grid.json # 3. the 4 x N grid
# ...解析 grid.json 中的每个 cell
stpa status .stpa/grid.json # 4. coverage check
# ...写入 .stpa/remediation.json — cost、location、fix、probe
stpa run .stpa # 5. plan + REPORT.html
```
在任意浏览器中打开 `.stpa/REPORT.html`。它是一个独立的单文件——没有 CDN,没有脚本,没有字体。你可以将其作为电子邮件发送、提交到代码库或打印出来。
### 每个命令的作用
| 命令 | 作用 |
|---|---|
| `stpa new [dir]` | 以正确的结构生成 `model.json` + `01-scope.md` |
| `stpa scan ` | 枚举入口点、守卫、数据访问和外部影响点——**需要确认的候选项,绝非最终结论**。`--focus` 选择视角,`--depth` 决定可见的内容量,`--list-focus` 显示可用的选项 |
| `stpa init ` | 构建网格:恰好包含 `4 × (控制动作)` 个单元格 |
| `stpa status ` | 覆盖率、绑定分布、未处理的单元格。如果发现异常集中,将退出并返回状态码 **3** |
| `stpa grid ` | 以 Markdown 格式输出网格 |
| `stpa plan [dir]` | 发现项 → 根本原因 → 波次 → 指标。如果任何发现项没有计划,`--check` 将退出并返回状态码 1 |
| `stpa report [dir]` | 生成独立的 HTML 交付物 |
| `stpa run [dir]` | 执行 `plan` + `report` |
### 选择深度和焦点
扫描器是一个视角,而不是清单。将它指向你实际关注的危险源:
```
stpa scan ./repo --list-focus # every available lens
stpa scan ./repo --focus authz,tenancy # authorization + multi-tenancy boundaries
stpa scan ./repo --focus api,webhook # externally reachable entry points
stpa scan ./repo --focus auth,authn,crypto # the authentication plane
stpa scan ./repo --focus effects,payments # irreversible and money-moving actions
stpa scan ./repo --focus cron,queue,async # the unattended plane
stpa scan ./repo --depth deep --focus authz # no cap, tests included, every hit
stpa scan ./repo --depth survey # counts only — orientation on a huge repo
stpa scan ./repo --include src/api --exclude legacy
```
`--depth standard` 会限制每个类别的匹配数量,并**告诉你丢弃了多少**。在大型代码库中,这个数字通常数以千计。受限的扫描只是一个样本;将其视为完整清单,往往是导致某个控制行为在无人决定放弃它的情况下脱离分析的原因。
### 两个覆盖率指标,必须兼顾
- **网格覆盖率** —— 你是否完成了你界定范围内的分析? `(已绑定的发现项 + 有理有据的死项) / 单元格总数`
- **表面覆盖率** —— 你界定了多少系统范围? `已建模的控制动作 / 候选控制动作`
只报告前者而不报告后者,正是“100% 覆盖率”沦为虚假保证的原因。在 `model.json` 中声明 `scope.candidateControlActions`;`stpa status` 和报告会并排打印这两个指标,而且未声明的范围会被标记出来,而不是被默认视为已完成。
### 你需要手工编写的三个产物
工具负责算术。**分析由你完成。** 这种分工是刻意为之——如果工具去猜测你的损失或控制器,只会产出一个自信但错误的威胁模型,这比没有模型还要糟糕。
1. **`01-scope.md`** —— 损失(利益相关者层面)和危险(**系统状态**,绝不是攻击者行为)。
2. **`model.json`** —— 控制器、控制行为、反馈以及**过程模型**:每个控制器*相信*什么,这种信念从何而来,它可能会变得多么陈旧。这才是产出收益所在。
3. **`remediation.json`** —— 针对每个发现项:严重程度、可达性、工作量、文件位置、修复方案、探针。
### 时间预算
| 目标 | 现实预期 |
|---|---|
| 单个 PR 或功能 | ~20 分钟(`Workflows/QuickTriage.md`) |
| 单个服务,<10 个控制行为 | 1–2 小时 |
| 多服务系统,10–40 个 | 一天;按控制器并行处理 |
| 整个平台,40+ 个 | 按信任边界切分——覆盖全平台的网格意味着永远也做不完 |
## 战略价值 —— 它如何体现自身价值
**1. 它可以在还没有任何可测试内容时运行。** SAST 需要代码;DAST 需要部署环境;渗透测试需要目标。而 STPA 只需要一份设计文档。发现控制结构缺陷成本最低的时刻,是在该控制结构存在之前——而且手册引用了标准估计,即 70–90% 与安全相关的设计决策是在概念开发阶段做出的。
**2. 它产出的是需求,而不是一份报告。** 每一个发现项都会机械地反转为一个约束,而每个约束都带有一个探针。将这些探针落地到你的测试套件中,威胁模型就会变成 CI 强制执行的内容。一个不以可执行断言结束的威胁模型在不到一个季度内就会过时——这才是威胁建模被视为“走过场”的根本原因。
**3. 它为你提供一个站得住脚的完整性指标。** 分析是一个有限的网格—— `4 × N` 个单元格——覆盖率是 `(已绑定的发现项 + 有理有据的死项) / 单元格`。“我们分析了 84% 的网格;这是我们未触及的 16%”是一句你可以对审计师、客户或董事会说的话。而“我们做了一个威胁模型”则不是。
**4. 它揭示了那些没人去建模的控制器。** 每一个真实的系统都有不留代码痕迹的权限实体:拥有生产数据库访问权的待命工程师、CI 流水线、具有模拟功能的支持工具、处理你数据的第三方、你的构建所信任的包仓库。这种方法会强制将它们画到架构图上。在实践中,这正是那些令人不安的发现项隐藏的地方。
**5. 重新分析成本极低,因此它可以成为一种习惯而非一次性事件。** `stpa init model-v2.json --merge grid.json` 会将已处理的单元格结转,并仅列出真正新增的内容。当控制结构发生变化时——例如新的控制器、新的集成、新的操作员角色——就重新运行一次。这与年度评估在经济学上有着根本的不同。
### 如何将其融入 SDLC
| 时机 | 用法 |
|---|---|
| 设计评审 / RFC | 从文档出发进行全面分析(模式 B)。假设日志*本身就是*评审议程。 |
| 架构变更 | 合并模式下的重新分析。只有新增的控制动作才会增加单元格。 |
| 涉及身份验证、多租户、计费或任务的 PR | `QuickTriage` —— 20 分钟,相同的结构 |
| 预审计 / 客户安全审查 | HTML 报告加上覆盖率指标 |
| 事件发生后 | 将事件的控制行为反馈到四种类型中;通常能揭示出同类问题 |
### 它不会做什么
- **它不是扫描器。** 没有 CVE,没有依赖审计,没有污点分析。应将其与 SAST/DAST/SCA *结合*使用,而不是取而代之。
- **它不会发现内存安全漏洞、注入或加密滥用** —— 这些属于组件缺陷,是 STRIDE 和 SAST 的主场。
- **它不产生概率。** 这里没有任何可与 CVSS 相提并论的内容。优先级排序基于严重性 × 可达性,这是一种明确的主观判断,并会以此方式记录在案。
- **它依赖于分析师。** 两个人针对同一系统会产生不同的控制结构。该方法构建的是判断框架;它不能替代判断本身。`METHOD.md` 列出了已发表的批评意见,而不是将其隐藏起来。
## 一页纸概述方法论
1. **损失** —— 从利益相关者的角度来看,什么是不可接受的。
2. **危险** —— 导致损失的**系统状态**。*攻击者窃取 token* 不是危险。*系统接受了不再反映持有者权限的 token* 才是危险。
3. **控制结构** —— 控制器(包括人类和流水线)、控制行为、反馈,以及每个控制器的**过程模型**。
4. **不安全控制行为** —— 每一个控制行为 × 四种类型:
| 类型 | 问题 | 捕获内容 |
|---|---|---|
| 未提供 | 跳过它会导致危险吗? | 缺失授权检查、永远无法传播的撤销、从未写入的审计事件 |
| 已提供 | 在某些上下文中执行它是否危险? | IDOR、跨租户读取、重复退款、部署未经审查的代码 |
| 错误的时机/顺序 | 太早、太晚、顺序颠倒? | TOCTOU、先检查后使用竞态、迁移前部署 |
| 持续过长/停止过早 | 持续时间错误? | 永不过期的会话、未释放的权限、TTL 无限的凭证 |
5. **损失场景** —— 为什么会发生 UCA,以及为什么一个*正确*的行为可能无法达到预期效果。
6. **约束** —— 反转每个发现项;附加一个探针。
**核心视角:** *过程模型不一致是授权漏洞的一般形态。* 控制器基于其**相信**的内容采取行动;当信念与现实出现分歧,而它依然采取行动时,就会产生漏洞。IDOR、租户数据泄漏、过期的权限、混淆代理、JWT 混淆、webhook 伪造和 TOCTOU 其实都是同一种漏洞穿上了不同的外衣。询问*“这个控制器相信什么,以及谁能够影响这种信念?”* 比查阅任何技术清单都能更快地发现它们。
**攻击者是一个上下文选择器**,而不是一个新的执行者。攻击者不会增加控制动作——他们只选择上下文,使得原本合法的操作变得危险。Young & Leveson 将对手放在了第 4 步,这个工具包也是如此。
## 并行化大型分析
大型系统通常由几个人或 Agent 同时逐层进行分析。有两条规则,都在代码中强制执行,因为它们都是经过试错总结出来的:
1. **在派发任务之前写下预期集合。** `manifest.json` 列出了每一个层面及其控制动作。覆盖率是根据*实际返回*的单元格计算的,而不是*预期*的单元格,因此直接合并“返回的任何内容”会导致虽然只覆盖了系统的一小部分,却得出一个看似健康的数值。
2. **在合并之前进行核对。**
stpa merge .stpa --expect .stpa/manifest.json
如果任何一个层面未产出内容,或存在无人声明的单元格缺口,将退出并返回状态码 **4**。在 `scope.deferred` 中声明缺口,以便表面覆盖率与实际情况相符——绝不带着隐藏的漏洞进行合并。
交付物应保存为**文件**,绝不能作为消息 payload 返回:文件要么存在要么不存在,而一个执行完毕却未提供任何交付物的 Delegate,与一个执行成功的 Delegate 是无法区分的。
## 刻意设计的反博弈机制
如果一个覆盖率指标计算任何文本就算作有效,那它就会将浅尝辄止的工作认证为已完成。这里有两个防线,都在代码中强制执行:
- **绑定。** 只有当一个发现项引用了**为该特定系统声明**的过程模型变量或反馈通道时,它才会被计入覆盖率。通用的漏洞类别文本不会绑定到任何东西,因此得分也为零。已验证:16 个通用的发现项填满 16 个单元格网格 → **0.0%**。
- **集中度。** 在每个发现项中引用*相同*的元素可以通过绑定检查,但它仍然是套话,因此 `stpa status` 会报告绑定分布情况,在覆盖率行本身盖上 `** CONCENTRATED — NOT A CLEAN 100 **` 的印章,并退出返回状态码 **3**。
已知局限性,在此声明而非隐瞒:在*两个*声明的元素之间以 50/50 的比例交替使用相同的文本可以通过这两道防线。要消除这种情况需要对文本进行语义判断,这是算术运算做不到的。**这些防线只能抓住偷懒的博弈,抓不住处心积虑的博弈——最终的后盾是人类去阅读这些陈述。**
## 仓库布局
```
stpa CLI entry point
README.md this file
METHOD.md STPA/STPA-Sec theory, Handbook definitions, limitations, sources
SoftwareMapping.md STPA vocabulary → software; UCA-type → vulnerability-class tables;
loss & hazard catalogues; per-stack entry-point recipes
AdversarialContext.md adversary-as-context-selector; R0–R4 reachability; 12 context patterns
Workflows/ the four steps + constraints, full analysis, and PR-scale triage
Tools/ the four executables the CLI dispatches to
Examples/ledgerline/ a complete worked run against a design document with no code
Fixtures/ a minimal model for smoke-testing
```
## 从 Claude Code 中使用它
这个仓库还可以作为 Claude Code 的 skill 使用。将其克隆到你的 skills 目录中,并添加一个 `SKILL.md` 来描述 `Workflows/` 中的工作流路由——`Workflows/Start.md` 被编写为一个交互式录入过程,它会询问代码仓库链接、范围、深度以及一系列损失,然后运行整个流水线并返回 HTML 报告。
该工具包本身不依赖于 Claude Code 或任何 Agent。`Tools/` 下的所有内容都是普通的 Bun 可执行文件,并且 `stpa` CLI 从 shell 驱动所有这些内容。
## 致谢与许可
MIT 许可证 —— 详见 `LICENSE`。
该方法属于 Nancy Leveson 和 John Thomas;安全框架属于 William Young 和 Nancy Leveson。完整的引用和客观真实的已发表局限性列表请参见 `METHOD.md`。本工具包是一个独立的实现,未经 MIT 或 STPA 作者认可。
标签:MITM代理, 云安全监控, 多模态安全, 威胁建模, 系统安全工程, 自动化攻击, 静态分析