Sonofg0tham/ward
GitHub: Sonofg0tham/ward
Ward 是一款前置元数据扫描器,专门用于在分支名、提交信息和 PR 标题等不可信字符串到达 AI 代理之前检测并拦截其中的提示词注入攻击。
Stars: 0 | Forks: 0
# Ward
Ward 是一个 CLI 和 GitHub Action。它会在任何基于 LLM 的审查者、SAST 代理或 IaC 扫描器读取数据之前,对 AI 代理摄入的元数据进行筛查。其目标是:捕捉隐藏在传统安全工具忽视之处的提示词注入企图。
**最新基准测试 (v0.1.3):**
- **Smoke**(内置 50 行样本,离线):范围内召回率 75.2%,误报率 0.0%。
- **完整语料库**(`ward bench --download`,1,391 行真实数据):在 Lakera、deepset 和 Spikee 中**范围内召回率 53.5%,误报率 0.0%**。AdvBench 是刻意的上限测试,召回率为 0%。
在 271 行良性的 deepset 数据上取得 0.0% 的误报率是目前最强的信号。完整报告见 [`benchmark/v0.1.3-smoke.md`](benchmark/v0.1.3-smoke.md) 和 [`benchmark/v0.1.3-full.md`](benchmark/v0.1.3-full.md)。每个 PR 都会通过 CI 工作流收到自己的基准差异评论。
## 为什么会有这个项目
在整个 2026 年初,AI 代码审查代理多次通过被传统安全工具视为惰性的元数据受到攻击。此类攻击记录在:
- **环境代码 / CLAUDE.md 提示词注入**披露(2026 年 2 月),攻击者替换了 `CLAUDE.md` 以指示审查代理破坏仓库并发布虚假的批准。被 Claude 发现。
- **Claude Code GitHub Action CVE**(2026 年 6 月披露,在 Claude Code 2.1.128 中修复),精心构造的 issue 内容诱导代理执行了泄漏环境变量的命令。
- Snyk 的 **"Clinejection"** 文章,仅仅一个包含提示词注入 payload 的 GitHub issue 标题就触发了 AI 审查者 (Cline) 发布恶意的 npm 包。
- **"hackerbot-claw GitHub Actions 供应链攻击"**(2026 年 2 月),通过分支名注入攻破了微软的 `ai-discovery-agent`,并通过文件名注入攻破了 DataDog 的 `iac-scanner`。这些是针对工作流的 bash 脚本攻击而不是提示词注入,但它们证明了元数据作为攻击面的趋势。
所有这些事件的共同模式是:payload 往往落在 SAST、密钥扫描器和提示词防火墙都不会检查的地方。
现有的安全栈在这里帮不上忙:
- **SAST 扫描器**忽略分支名和提交信息。这些以前从未被视为攻击面。
- **密钥扫描器**寻找的是凭据,而不是指令。
- **提示词防火墙**(Lakera、LlamaFirewall、BoltClaw)位于代理内部的 LLM 边界。当它们看到文本时,文本已经在上下文窗口中了。
- **OWASP ASI Top 10** 命名了这个模式(ASI01,通过不可信输入进行目标劫持),但并未提供工具。
Ward 位于更早的位置。它在任何 LLM 有机会采取行动之前,针对攻击者实际使用的攻击面运行。
## Ward 的适用位置
| 工具 | 层级 | 捕获内容 |
|------|-------|---------|
| **Ward** | 在代理读取输入之前 | 分支名、文件名、提交信息、PR 标题、PR 描述、代码注释、README 文件中的提示词注入 |
| **Lakera Guard** | LLM 边界 | 提示词本身中的提示词注入、越狱、偏离主题的查询 |
| **LlamaFirewall** | LLM 边界 | 提示词注入、对齐违规、输出策略执行 |
| **BoltClaw** | 代理配置 | 篡改代理系统提示词、工具白名单、MCP 配置 |
| **SAST / 密钥扫描器** | 源代码 | 代码本身的漏洞和凭据 |
Ward 只是其中一层。它不能替代其他工具。纵深防御仍然适用。
## Ward 能捕获什么
六种检测器类别,内置 25+ 条规则:
- **指令覆盖**("ignore previous instructions"、"your new task is..."、伪造的 `[SYSTEM]` 块)。
- **角色操纵**(分词器标签如 `<|im_start|>system`、"developer mode"、DAN 风格的激活方式)。
- **混淆**(零宽 unicode、RTL 覆盖、不常见字段中的 base64 块、十六进制块、HTML 注释)。
- **工具调用注入**(伪造的 `` 包装器、JSON 工具调用对象、`mcp://` URI、名称中的 shell 元字符)。
- **数据外发提示**(指示将发现 POST 到 URL、包含密钥、将数据编码在 DNS 查询中的指令)。
- **AI 工具特定的怪癖**(Anthropic 的 Human / Assistant 标签、Cursor 命令面板、Antigravity 工具 schema、Copilot 斜杠命令)。
## 安装
```
pipx install ward-scanner
```
验证安装:
```
ward version
```
## 使用
### 通过引用扫描 PR
```
export GITHUB_TOKEN=ghp_...
ward scan-pr sonofg0tham/ward#42
```
通过 GitHub API 读取 PR 标题、正文、head 分支名、提交信息和更改的文件路径。从不读取文件内容。
### 扫描本地 git 状态
```
ward scan-local
```
遍历工作树,扫描当前分支名、最近 20 条提交信息、标签名称、每个被追踪文件的路径,以及任何 `.md`、`.txt`、`.rst` 和源文件的文件顶部内容。
### 扫描单个字符串
```
echo "feat/ignore-previous-instructions" | ward scan-stdin --surface branch_name
```
所有其他的 Ward 命令都是基于此构建的。你可以通过管道将任何想要扫描的字符串传给它。
### 其他命令
```
ward scan-branch feat/ignore-previous-instructions
ward scan-commit HEAD
ward explain io.ignore_previous
```
### 输出格式
```
ward scan-local --format pretty # default, terminal table
ward scan-local --format json # machine-readable
ward scan-local --format sarif # GitHub Code Scanning compatible
```
### 严重性阈值
```
# 丢弃任何低于 MEDIUM 的内容,仅在 CRITICAL 时 FAIL。
ward scan-local --severity-threshold medium --fail-on critical
```
退出代码:
- `0` PASS,没有超过阈值的发现。
- `1` WARN,存在发现,但没有达到失败严重性级别。
- `2` FAIL,至少有一个发现达到或超过失败级别。
## 针对公开语料库的基准测试
`ward bench` 使用四个内置的公开对抗性语料库(Lakera ignore-instructions、deepset prompt-injections、Spikee jailbreaks、AdvBench harmful-behaviors)对 Ward 进行评分。样本打包在 wheel 包中,遵循各上游的 MIT 或 Apache 2.0 许可证。
```
ward bench
# 已写入 benchmark 报告:ward-bench-report.md
# In-scope recall:75.2% FPR:0.0%
```
默认输出为 Markdown,使用 `--format json` 以便 CI 摄取。
标志:`--corpus `(可重复)、`--output `、`--no-write`、`--list`。
内置的基准测试历史记录位于 [`benchmark/`](benchmark/)。每个版本都会提交自己的报告,以便可以跨版本审计检测范围。AdvBench 作为*上限测试*包含在内:该语料库包含单纯的恶意意图字符串,没有任何注入措辞,因此 Ward 在这里会被设计为得分为 0% ——这是诚实的表述,而不是倒退。
## 运行对抗性实验室
Ward 内置了一个实验室,它通过两个管道(未受保护和受 Ward 保护)运行每个脚本化的攻击场景,并生成一份可以粘贴到博客文章或 PR 评论中的 Markdown 报告:
```
ward lab attack
# 已写入 lab 报告:ward-lab-report.md
# 被 Ward 阻止:5/5 场景。
```
模拟审查代理不调用 LLM。该实验室旨在证明不可信的指令是否会到达代理的上下文窗口,而不是 LLM 会对其进行何种处理。连接真实的审查者是下一步的计划。
标志:`--output `、`--no-write`(打印到标准输出)、`--fail-on `。
## Pre-commit 钩子
如果你使用 [pre-commit](https://pre-commit.com/) 框架,请将其放入你的 `.pre-commit-config.yaml` 中:
```
- repo: https://github.com/sonofg0tham/ward
rev: v0.1.0
hooks:
- id: ward-scan-local
args: [--fail-on, high]
```
然后 Ward 会在每次 `git commit` 和 `git push` 时运行,筛查你的分支名、提交信息和被追踪的文档文件中的注入模式。防止你提交被投毒的 PR,使其永远不会到达 GitHub。
其他钩子 ID:`ward-scan-stdin`(专为 `commit-msg` 阶段设计,筛查你正在输入的信息)、`ward-selftest`(手动运行,可用作 CI 门禁)。
## GitHub Action
只需三行即可将其添加到工作流中:
```
- uses: sonofg0tham/ward@v0.1.3
with:
fail-on: high
```
将 SARIF 上传到 GitHub Security 标签页的更完整示例:
```
name: Ward
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
ward:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: sonofg0tham/ward@v0.1.3
with:
fail-on: high
format: sarif
upload-sarif: true
```
## 将 Ward 用作 Python SDK
如果你正在构建一个代理系统(CrewAI、AutoGen、LangGraph 或你自己的循环),并希望在文本到达模型之前对其进行筛查,请直接导入 Ward:
```
from ward import build_input, scan_inputs, load_rule_pack, Verdict
# 在启动时加载一次捆绑的 rule pack。
pack = load_rule_pack()
def safe_ingest(untrusted_text: str) -> str:
inputs = [build_input("pr_body", untrusted_text, location="user-input")]
report = scan_inputs(inputs, pack, target="my-agent")
if report.verdict is not Verdict.PASS:
flagged = [f.rule_id for f in report.findings]
raise ValueError(f"Refusing to ingest untrusted text: {flagged}")
return untrusted_text
```
13 种支持的表面类型(`branch_name`、`commit_message`、`pr_body`、`file_content`……)让你可以调整适用哪些规则。一个摄入网络搜索结果的 LangGraph 工具会使用 `pr_body` 或 `file_content`;一个读取文件名的 CrewAI 代理会使用 `file_name`。
### 在 LangGraph 节点内部
```
from ward import build_input, scan_inputs, load_rule_pack, Verdict
_pack = load_rule_pack()
def web_search_node(state):
text = state["search_result"]
report = scan_inputs(
[build_input("file_content", text, location="search")],
_pack,
target="search_result",
)
if report.verdict is not Verdict.PASS:
state["search_result"] = "(blocked by Ward)"
state["ward_findings"] = [f.rule_id for f in report.findings]
return state
```
### 在 CrewAI 工具内部
```
from crewai.tools import BaseTool
from ward import build_input, scan_inputs, load_rule_pack, Verdict
class GuardedFileReader(BaseTool):
name = "read_file"
description = "Read a file, screened by Ward."
_pack = load_rule_pack()
def _run(self, path: str) -> str:
text = open(path).read()
report = scan_inputs(
[build_input("file_content", text, location=path)],
self._pack,
target=path,
)
if report.verdict is not Verdict.PASS:
return f"(refused: Ward flagged {[f.rule_id for f in report.findings]})"
return text
```
## 自定义规则包
在你的仓库旁放置一个包含 YAML 文件的目录,并让 Ward 指向它:
```
ward scan-local --rule-pack ./security/ward-rules
```
每个 YAML 文件都是一个规则列表。Schema 记录在 [`src/ward/rules/instruction_overrides.yaml`](src/ward/rules/instruction_overrides.yaml)。
## 使用 `.wardignore` 忽略整个路径
有些目录——测试固定数据、安全研究笔记、规则包本身——本身包含对抗性内容,因此不应该被扫描。在仓库根目录下放置一个使用 fnmatch 风格全局匹配的 `.wardignore` 文件:
```
# .wardignore
tests/fixtures/**/* # adversarial by design
security/research/* # writeup of past attacks
docs/threat-models/*
```
被忽略路径中的文件名仍然会被扫描(即使是位于被忽略的目录中,恶意文件名依然可疑)。只有内容扫描会被抑制。Ward 自己的仓库就利用了这一点,将其自身的源码树排除在自我扫描之外。
## 在文档中抑制规则
安全研究文档(包括 Ward 自己的 README)需要在不触发扫描器的情况下*讨论*攻击字符串。在任何文档文件的顶部附近放置此指令:
```
```
该指令接受规则 ID 或 fnmatch 风格的全局匹配,以逗号分隔。它仅在 `file_content` 表面(由 `scan-local` 完整读取的文档文件)上生效,绝不在 `code_comment`、分支名、提交信息、PR 标题或 PR 正文中生效。这种不对称性是故意的:能够提交 PR 的攻击者无法通过发布一个在其顶部注释中使检测静默的新源文件。
**重要的威胁模型说明:** 此指令对于能够在 PR 中修改现有文档文件的攻击者不提供保护。这种更改在 PR 审查中是可见的,但 Ward 不会自动标记该修改。如果需要完全不通过扫描内容生效的基于路径的抑制,请在仓库根目录使用 `.wardignore`。基于来源感知的模式(即该指令仅对自合并基准以来未更改的文件生效)已列入 v0.2 路线图。
该指令支持的注释样式(仅限 file_content 表面):
```
# ward-allow-file:io.* # ReST / .txt / .adoc
/* ward-allow-file: io.* */ /* if you wrap docs in C comments */
```
## 抗规避能力
Ward 向检测器提供文本的规范化视图,以及旨在击败常见规避技巧的几种替代形式:
- **Leetspeak** —— `1gn0r3 4ll pr3v10us` 会变成 `ignore all previous`。
- **词内分隔符** —— `i.g.n.o.r.e` 和 `i-g-n-o-r-e` 会被折叠为 `ignore`。
- **重复字母** —— `ignooooore` 和 `previousssss` 会折叠为 `ignore` 和 `previous`。我们会尝试两种折叠变体(折叠为 1 个字母和折叠为 2 个字母),以便像 `all`、`free`、`see` 这样自然双写的英语单词能够保留下来。
- **零宽 unicode** —— 在正则表达式匹配前被剥离。
- **NFKC** —— 全角和兼容性字符会被折叠为 ASCII。
- **Base64 / 十六进制块** —— 解码后重新扫描。
- **标识符分隔符** —— 分支和文件名中的 `-`、`_`、`/`、`.` 会被规范为空格。
**已知限制:** 全单空格情况(`i g n o r e p r e v i
o u s`)无法处理,因为无法可靠地从分散的单例中恢复原始单词边界。词间的多空格分隔符(`i g n o r e p r e v i o u s`)仍然存在歧义,不在 v0.1 的处理范围内。
## 威胁模型
Ward 是一个模式匹配工具。它捕获 OWASP ASI Top 10 (ASI01) 和上述 2026 年初事件中记录的攻击类别。
它**无法**捕获:
- 不匹配任何规则的新型零日注入技术。
- 嵌入在非文本格式(图像、PDF、音频)中的攻击。
- 上下文建立后对模型本身的攻击。那是提示防火墙的工作。
- 正在被审查的代码中的漏洞。那是 SAST 的工作。
完整的威胁模型和漏洞披露流程请参见 [SECURITY.md](SECURITY.md)。
## 遥测
Ward 不发送任何内容。没有回传,没有匿名统计,没有指标收集。Ward 发出的唯一外部网络调用是你通过 `ward scan-pr` 显式触发的 GitHub API 请求。
## 开发
```
git clone https://github.com/sonofg0tham/ward
cd ward
python -m venv .venv && source .venv/bin/activate # or .venv\Scripts\activate on Windows
pip install -e ".[dev]"
pytest
```
覆盖率目标为 75%,当前主干运行为 83%。
## 许可证
MIT。详见 [LICENSE](LICENSE)。
标签:AI代码审计, Blue Team, DevSecOps, GitHub Action, Google AI, 上游代理, 图数据库, 大语言模型安全, 提示词注入检测, 机密管理, 软件开发工具包, 逆向工具