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, 上游代理, 图数据库, 大语言模型安全, 提示词注入检测, 机密管理, 软件开发工具包, 逆向工具