electricapp/hasp

GitHub: electricapp/hasp

一款面向 GitHub Actions 的偏执安全扫描器与沙箱化步骤运行器,通过内核级隔离和代理介导的 secret 分发,从静态审计与运行时两个层面加固 CI/CD 供应链安全。

Stars: 7 | Forks: 0

# hasp 一个偏执的安全扫描器以及用于 GitHub Actions 的沙箱化步骤运行器。 它会验证每个 `uses:` 指令是否被固定到不可变的 commit SHA, 确认该 SHA 确实存在于上游仓库中,检查 commit 的来源信息,映射哪些 secrets 对哪些 actions 可见, 并审查工作流是否存在注入漏洞、权限过大、隐藏的执行路径以及供应链风险。 `hasp exec` 将任何可执行子进程(例如 CI 步骤)封装在内核沙箱中, 在这里 secrets 成为了一种权能——由带有声明式域名白名单的、每个 secret 独有的 localhost 代理进行托管, 因此被破坏的依赖项永远无法将凭证渗出到未经授权的域。 ``` $ hasp --paranoid hasp: scanning .github/workflows/ hasp: found 14 action reference(s) PASS actions/checkout@11bd71901bbe (commit verified) FAIL actions/cache@6849a6489940 (comment says v3.0.0 (-> 6673cd0) but pinned to 6849a64 (v4.1.2)) WARN actions/setup-node@v4 (mutable ref -- pin to SHA 49933ea5288c) FAIL my-org/phantom@deadbeef0000 (SHA not found -- phantom or typo'd commit) [CRIT] Script injection via ${{ github.event.pull_request.title }} [HIGH] Commit abc123 is diverged from actions/checkout default branch [MED ] Unsigned commit abc123 in actions/checkout ``` ## 架构 hasp 二进制文件将自身拆分为相互隔离的子进程,每个子进程只具有完成其 工作所需的最低权限。在 fork 之前,启动器会执行预扫描完整性检查(git blob SHA-1)以检测检出后的篡改。 在 Linux 上,扫描器受 Landlock 和 seccomp-BPF 限制,而验证器路径则获得一个额外的 cgroup-BPF 出站白名单。`GITHUB_TOKEN` 永远不会接触到解析不受信任的 YAML 的进程。`.hasp.yml` 策略文件可以针对每个 action 扩展或替换内置的审计规则。 ``` hasp ┌──────────────────────┐ │ LAUNCHER │ │ │ │ - git integrity │ │ check (pre-fork) │ │ - load .hasp.yml │ │ - parse CLI flags │ │ - orchestration │ │ - report printing │ │ - exit code logic │ └──┬──────────┬────────┘ │ │ ┌─────────────┘ └──────────────┐ │ fork+exec fork+exec │ │ (no GITHUB_TOKEN) (has token)│ ▼ ▼ ┌───────────────────┐ ┌──────────────────────┐ │ SCANNER │ │ TOKEN PROXY │ │ │ │ │ │ Landlock: │ │ Holds GITHUB_TOKEN │ │ - deny writes │ │ Holds ureq client │ │ - deny reads │ │ PinnedResolver: │ │ (after parse) │ │ api.github.com │ │ Seccomp: │ │ only │ │ - deny execve │ │ cgroup-BPF: │ │ - deny network │ │ - GitHub IPs only │ │ - deny ptrace │ │ Loopback TCP server │ │ │ │ + per-run auth │ │ │ │ │ │ Reads YAML files │ │ Serves: │ │ Parses workflows │ │ VERIFY owner/repo │ │ Extracts refs │ │ RESOLVE tag->SHA │ │ Static audit │ │ FIND_TAG SHA->tag │ │ │ │ DEFAULT_BRANCH │ │ Outputs: │ │ REACHABLE (compare)│ │ ScanPayload │ │ SIGNED (gpg check) │ │ via stdout IPC │ └──────────┬───────────┘ └───────────────────┘ │ loopback TCP only ┌─────────────────────────────────────┘ │ ▼ ┌────────────────────┐ │ VERIFIER │ │ │ │ Landlock: │ │ - deny writes │ │ - deny reads │ │ (after init) │ │ Seccomp: │ │ - deny execve │ │ - deny ptrace │ │ cgroup-BPF: │ │ - proxy only │ │ │ │ NO GITHUB_TOKEN │ │ NO ureq client │ │ │ │ Talks to proxy │ │ via authenticated │ │ localhost TCP │ │ │ │ Runs: │ │ SHA verification │ │ provenance check │ │ │ │ Outputs: │ │ VerifyPayload │ │ via stdout IPC │ └────────────────────┘ ``` ### Exec 模式架构 (`hasp exec`) ``` hasp exec --manifest .hasp/publish.yml -- npm publish │ ├─ parse manifest, pre-resolve DNS, capture secrets │ ├─ [sudo hasp --internal-bpf-helper] (short-lived, root) │ └─ create cgroup + load BPF → chown to caller → exit │ ├─ spawn FORWARD PROXY per secret (each in own BPF cgroup) │ ┌───────────────────────────────────┐ │ │ FORWARD PROXY (NPM_TOKEN) │ │ │ BPF: only registry.npmjs.org IPs │ │ │ Loopback-only, ephemeral port │ │ │ Validates Host header │ │ │ Injects Bearer token │ │ │ Plain HTTP in → HTTPS out │ │ └───────────────────────────────────┘ │ ├─ spawn CHILD in BPF cgroup (only proxy ports allowed) │ ┌─────────────────────────────┐ │ │ npm publish │ │ │ env: scrubbed (no secrets) │ │ │ HASP_PROXY_NPM_TOKEN= │ │ │ http://127.0.0.1:{port} │ │ │ Landlock: read-only fs │ │ │ (except ./dist) │ │ │ Seccomp: deny ptrace │ │ │ BPF: only 127.0.0.1:{port} │ │ └─────────────────────────────┘ │ └─ wait for child → kill proxies → exit with child's code ``` ### 数据流 ``` .github/workflows/*.yml .hasp.yml │ │ ├─────────────┬─────────┘ │ │ │ (git blob check first) │ │ ▼ ▼ ┌───────────────┐ stdout pipe ┌──────────────┐ │ SCANNER │ ──────────────────► │ LAUNCHER │ │ │ ScanPayload: │ │ │ parse YAML │ action_refs[] │ correlate │ │ extract refs │ skipped_refs[] │ results │ │ static audit │ container_refs[] │ print │ │ │ audit_findings[] │ report │ └───────────────┘ └──────┬───────┘ │ stdin pipe │ stdout pipe ┌──────────────────┐ │ ┌──────────────────┐ │ VERIFIER │◄────────┘ │ VERIFIER │ │ │ action_refs │ │──────► LAUNCHER │ verify SHAs │ │ VerifyPayload: │ │ check provenance│ │ results[] │ │ │ │ provenance[] │ └────────┬─────────┘ └──────────────────┘ │ loopback TCP │ ┌────────▼─────────┐ │ TOKEN PROXY │──────► api.github.com:443 │ │ (TLS, SPKI-pinned) │ GitHub IP allow │ │ list on Linux │ │ GITHUB_TOKEN │ │ ureq + rustls │ └──────────────────┘ ``` ### 沙箱阶段 (Linux) ``` Process start │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Phase 1: Landlock V5 deny writes + Seccomp BPF │ │ │ │ Filesystem: read-only (no write, mkdir, symlink, truncate) │ │ Syscalls: deny execve, execveat, ptrace, process_vm_* │ │ Network: deny socket/connect/bind/sendmsg (scanner only) │ │ verifier/proxy egress narrowed by cgroup-BPF │ └──────────────────────────────────────────────────────────────┘ │ │ ... read YAML, parse, build payloads ... │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Phase 2: Landlock V5 deny reads │ │ │ │ Filesystem: no read, no readdir, no execute │ │ Process is now fully jailed — can only write to stdout │ └──────────────────────────────────────────────────────────────┘ │ │ ... serialize results to stdout IPC ... │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Phase 3: Launcher self-sandboxing (after children exit) │ │ │ │ Launcher applies seccomp: deny execve, ptrace, network │ │ Final report-printing phase cannot execute code │ └──────────────────────────────────────────────────────────────┘ │ ▼ Process exit ``` ### 威胁模型 ``` ┌─────────────────────────────────────────────────────────────────────┐ │ ATTACK SURFACE │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Malicious YAML ──► SCANNER (sandboxed, no token, no network) │ │ │ │ │ │ Even if yaml-rust2 has a bug and the attacker gets code │ │ │ execution in the scanner: │ │ │ - Cannot write to disk (Landlock) │ │ │ - Cannot exec malware (seccomp) │ │ │ - Cannot open sockets (seccomp) │ │ │ - Cannot read files (Landlock Phase 2) │ │ │ - Cannot access GITHUB_TOKEN (env scrubbed before fork) │ │ │ - Cannot ptrace other procs (seccomp) │ │ │ │ │ GitHub API ──► TOKEN PROXY (holds token, pinned TLS) │ │ │ │ │ │ The proxy only talks to api.github.com through a pinned │ │ │ resolver, SPKI-pinned TLS, and Linux cgroup-BPF IP │ │ │ allowlists. The verifier can only reach the loopback │ │ │ proxy. │ │ │ Proxy env vars (HTTP_PROXY etc.) are stripped on startup. │ │ │ Token is XOR-masked at rest, unmasked only during API │ │ │ calls (~50ms), then volatile-write scrubbed on drop. │ │ │ Token scope verified on startup (warns if overprivileged). │ │ │ Auth uses constant-time comparison (no timing leak). │ │ │ Proxy shuts down after 5 auth failures (rate limited). │ │ │ API calls capped at 300/run (token exhaustion prevention). │ │ │ │ │ Orphaned fork commits ──► PROVENANCE CHECKER │ │ │ │ │ │ GitHub's shared object store lets fork commits be │ │ │ addressed by SHA from the parent repo. We detect this │ │ │ via the compare API (diverged = suspicious). │ │ │ Unsigned commits also flagged. │ │ │ └─────────────────────────────────────────────────────────────────────┘ ``` ## 检查内容 ### 预扫描完整性 - **工作流完整性检查** (`src/integrity.rs`): 计算每个工作流文件的 git blob SHA-1;检测先前 CI 步骤在检出后的篡改 ### 固定验证 - 每个 `uses:` 都固定到完整的 40 字符 SHA(而不是可变的 tag/branch) - 该 SHA 确实存在于上游仓库中(捕获虚假的 commit) - 内联的 `# vX.Y.Z` 注释与该 SHA 实际所属的 tag 匹配 - 为可变引用提供已解析 SHA 的建议固定替换方案 ### Commit 来源 (`--paranoid`) - Commit 可从仓库的默认分支到达(捕获孤立的 fork commit) - Commit 具有已验证的签名(捕获未签名/未归属的代码) - 针对新推送 SHA 的策略驱动冷却期 (`--min-sha-age 48h`) - 针对安全/认证/部署/发布 actions 的更严格冷却期 (`--security-action-min-sha-age 30d`) - 标记来自非受信任发布者的非常新的固定 commit - **Tag 可变性检测**:标记在旧 commit 上追溯创建的 tag(tagger.date 与 commit date 对比) - 标记最近创建/信誉较低的 action 仓库 ### 静态审计 (`--paranoid`) - `run:` 块 (CRIT) 和 `with:` 输入 (HIGH) 中的 `${{ }}` 表达式注入 - `pull_request_target` / `workflow_run` + 攻击者控制的检出检测 - 危险的 `GITHUB_ENV` / `GITHUB_PATH` 写入(注入时为 CRIT,否则为 MED) - 在可重用工作流调用上的 `secrets: inherit`(暴露所有 secrets) - 可绕过的对攻击者控制上下文的 `contains()` 检查 - 没有 `persist-credentials: false` 的 `actions/checkout`(token 留在磁盘上) - 第三方 actions 的 Secret-to-action 可见性映射 - 过大的 `GITHUB_TOKEN` 权限(`contents: write`、`packages: write` 等) - 缺少顶层 `permissions: {}` 块 - 未验证的 action 来源(非 GitHub 官方发布者) - 流行的 action 的域名抢注相似物(`action/checkout` 对比 `actions/checkout`) ### 容器镜像 - `docker://` 步骤镜像、作业容器和服务容器 - Digest 固定(`@sha256:...`)与可变 tag 检测 ### 不可审计的引用 - 远程可重用工作流(不进行传递扫描) - 本地复合 actions(在可解析时进行传递扫描) - 远程复合 actions(在可获取元数据时进行传递审计;默认深度为 3,可通过 `--max-transitive-depth` 或 `.hasp.yml` 中的 `max-transitive-depth` 配置)。当达到深度限制时,该分支的扫描会静默停止——不会发出警告或错误。如果需要更深入地了解嵌套依赖链,请增加该限制。 ### 隐藏执行审计 (`--paranoid`) - 标记 Action 元数据的 `pre` / `post` 钩子 - 标记带有内部 shell `run:` 步骤的复合 actions - 公开固定 action 元数据中的嵌套执行以供审查 ## 用法 ``` # 基础扫描(检查 pinning,使用 token 验证 SHA) export GITHUB_TOKEN=$(gh auth token) hasp # 完全偏执审计 hasp --paranoid # 对已固定的 SHA 强制执行 48 小时的冷却期 hasp --min-sha-age 48h # 要求 security / auth / deploy / publish actions 具有 30 天的时效性 hasp --security-action-min-sha-age 30d # Strict 模式(可变引用 = 失败,需要 token) hasp --strict # Offline 模式(跳过 GitHub API 验证) hasp --no-verify # 自定义 workflow 目录 hasp --dir path/to/workflows # 使用特定的 policy 文件 hasp --policy path/to/custom.yml # 即使存在也忽略 .hasp.yml hasp --no-policy --paranoid # 增加 transitive dependency 扫描深度(默认:3,最大:10) hasp --paranoid --max-transitive-depth 5 # 对照已发布的 release 验证二进制完整性 hasp --self-check # 在带有代理中介 secrets 的沙箱环境中运行命令 hasp exec --manifest .hasp/publish.yml -- npm publish # 使用显式可写目录运行(可重复执行) hasp exec --manifest .hasp/deploy.yml --writable ./dist --writable /tmp -- deploy.sh ``` ### 沙箱化步骤运行器 (`hasp exec`) `hasp exec` 在沙箱环境中运行任何命令,在该环境中 secrets 作为 权能——由带有声明式域名白名单的、每个 secret 独有的 localhost 代理进行托管。子进程获得零直接网络访问权限和 其环境中的零 secrets。 ``` # 使用代理中介的 NPM_TOKEN 运行 npm publish export NPM_TOKEN=npm_abc123 hasp exec --manifest .hasp/publish.yml -- npm publish # Dry run:零 secrets,零网络,只读文件系统 hasp exec --allow-unsandboxed -- echo hello ``` 步骤清单 (YAML) 声明了每个步骤的 secret 授权、网络白名单 和可写目录: ``` # .hasp/publish.yml secrets: NPM_TOKEN: domains: [registry.npmjs.org] inject: header # header | basic | none header_prefix: "Bearer " # default network: allow: [registry.npmjs.org] # union with secret domains filesystem: writable: [./dist] # Landlock write grants ``` **工作原理:** 1. 清单被解析并验证 2. 预先解析所有允许域名的 DNS 3. 从环境中捕获 secrets 并进行清除 4. 为每个 secret 生成一个 TLS 终止正向代理(每个都在自己的 BPF cgroup 中) 5. 子进程的 BPF cgroup 只允许连接到代理 localhost 端口 6. 子进程的环境被清除(只保留 `PATH`、`HOME`、`USER`、`LANG`、`TERM` + 代理 URL) 7. Landlock 拒绝除声明的可写目录之外的所有写入;seccomp 拒绝 ptrace 8. 子进程运行,hasp 以子进程的退出代码退出 子进程通过设置特定于工具的环境变量来使用代理(例如, `NPM_CONFIG_REGISTRY=http://127.0.0.1:{port}`)。代理根据域名白名单验证 `Host` 头,将凭证作为 HTTP 头注入,并通过 HTTPS 转发到上游。 ### 策略文件 (`.hasp.yml`) 在仓库根目录提交一个 `.hasp.yml`,以配置针对每个 action 的检查, 扩展或替换内置信任列表,并抑制已知的误报, 而无需禁用整个检查类别。当存在该文件时,即使没有 `--paranoid`, 策略也会启用其配置的检查。 ``` version: 1 # ── 全局默认值 ────────────────────────────────────────── pin: deny # deny | warn | off min-sha-age: 48h security-action-min-sha-age: 30d max-transitive-depth: 3 # 1-10, how deep to scan composite action dependencies # ── 检查级别 ───────────────────────────────────────────── # deny = 发现问题即失败,warn = 打印警告但不阻止,off = 跳过 checks: expression-injection: deny permissions: deny secret-exposure: deny privileged-triggers: deny github-env-writes: deny secrets-inherit: deny contains-bypass: deny persist-credentials: warn typosquatting: deny untrusted-sources: warn provenance: reachability: deny signatures: warn fresh-commit: warn tag-age-gap: deny repo-reputation: warn recent-repo: deny transitive: deny hidden-execution: deny # ── 信任列表 ────────────────────────────────────────────── # mode:extend(添加到内置列表)或 replace(仅使用这些列表) trust: owners: mode: extend list: [my-org] privileged-actions: mode: extend list: [my-org/deploy-action] high-impact-secrets: mode: extend list: [MY_CUSTOM_TOKEN] # ── 每项 action 的覆盖 ───────────────────────────────────── # 首次匹配生效。Glob * 在单个片段内匹配。 actions: - match: "my-org/*" pin: warn min-sha-age: 0s checks: untrusted-sources: off - match: "actions/checkout" checks: persist-credentials: off # ── 抑制项 ───────────────────────────────────────────── # 应急通道。必须提供原因。被抑制的发现项将被排除。 ignore: - check: persist-credentials match: "actions/checkout" reason: "v4 cleans up in post-step" - check: expression-injection match: "*" file: ".github/workflows/label-sync.yml" reason: "Schedule-only trigger" ``` **优先级**:全局默认值 < 每个 action 的覆盖(首次匹配生效)< 抑制(事后过滤)。冲突时 CLI 标志始终优先(`--strict` 强制使用 `pin: deny`,`--paranoid` 强制所有检查为 `deny`)。一般规则:以最严格的为准。 **保护策略文件**:`.hasp.yml` 本身是一个攻击面——修改了它的恶意 PR 可能会抑制发现结果或削弱检查。使用以下方法保护它: - **CODEOWNERS**:要求安全团队审查 `.hasp.yml` 的更改(`/.hasp.yml @your-org/security`) - **分支保护**:在合并对策略文件的更改之前要求 PR 批准 - **在 CI 中使用 `--paranoid`**:CLI 标志会覆盖策略文件,因此 CI 工作流中的 `--paranoid` 可确保所有检查都以 `deny` 运行,无论 `.hasp.yml` 指定了什么 - 当策略禁用所有检查或使用广泛的抑制模式时,hasp 会向 stderr 发出警告 ### GitHub Action 该 action 在运行之前验证二进制文件。`verify` 输入控制 执行多少级别的验证(每个级别包括其下的所有级别): | 级别 | `verify` 值 | 检查内容 | | ----- | -------------- | -------------------------------------------------------------------- | | 1 | `sha256` | SHA256 校验和 + 与发布的 `.sha256` 文件交叉检查 | | 2 | `sigstore` | + Sigstore cosign 签名 (证明二进制文件来自 CI) | | 3 | `slsa` | + SLSA 构建来源证明 (证明确切的 commit + workflow) | ``` permissions: {} jobs: scan: runs-on: ubuntu-latest permissions: contents: read # Required for checkout + hasp id-token: write # Required for SLSA verification (omit if verify: sha256) steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: OWNER/hasp@REPLACE_WITH_FULL_40_CHAR_SHA # pin to a SHA, never @v1 with: mode: paranoid # default | paranoid | strict verify: slsa # sha256 | sigstore | slsa env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` **安全使用规则:** 1. **固定到完整的 SHA。** 切勿使用 `@v1` 或 `@main`。如果这样做,hasp 本身会标记你。 2. **仅授予 `contents: read`。** hasp 不需要其他权限。仅在使用 `verify: slsa` 时才添加 `id-token: write`。该 action 不执行写入、推送、评论或调用任何 API(GitHub 的只读 commit/tag 端点除外)。 3. **首次使用前审查固定 SHA 处的 `action.yml`。** 这是一个带有 shell 步骤的复合 action——没有 Node.js,没有构建产物,完全可以在一个文件中审计。 4. **验证 `expected-hash` 输入。** 该 action 附带其默认版本的默认哈希值。如果更改 `version`,请更新 `expected-hash` 以匹配(从发布的 `.sha256` 文件中获取)。 5. **托管运行器上的完整 OS 级别隔离。** GitHub 托管的 Ubuntu 运行器 (22.04+,内核 6.8+) 支持 Landlock、seccomp-BPF 和 cgroup v2。当非特权 BPF 不可用时(Ubuntu 上的默认情况),hasp 使用 `sudo` BPF 辅助程序加载 cgroup-BPF 程序。不需要 `--allow-unsandboxed`。 6. **cosign 也经过了 SHA256 验证。** 该 action 下载 cosign 进行 Sigstore 验证,并在运行前验证其哈希值。固定的 cosign 版本和哈希值是您可以审计和覆盖的 action 输入。 Action 输入: | 输入 | 默认值 | 描述 | | ---------------- | ------------------- | ------------------------------------------- | | `version` | `v0.1.0` | 要下载的发布版本 | | `expected-hash` | *(release hash)* | 二进制文件的 SHA256;如果不匹配则失败 | | `verify` | `slsa` | 验证级别:`sha256`、`sigstore` 或 `slsa` | | `mode` | `paranoid` | `default`、`paranoid` 或 `strict` | | `policy` | *(auto-detect)* | `.hasp.yml` 的路径,或 `"none"` 以禁用 | | `dir` | `.github/workflows` | 要扫描的目录 | | `args` | | 额外的 CLI 标志 (例如 `"--min-sha-age 48h"`) | | `cosign-version` | `v2.4.3` | 用于 Sigstore 验证的 Cosign 版本 | | `cosign-hash` | *(pinned hash)* | cosign 二进制文件的 SHA256 | ### 版本 `hasp --version` 打印 `hasp 0.1.0 (abc123def456) [rustc 1.94.0]`——git 哈希和确切的 Rust 编译器版本在构建时嵌入,用于可重现性追踪。 ### 退出代码 | 代码 | 含义 | | ---- | ----------------------------------------------------- | | `0` | 所有检查通过(或在非严格模式下仅有警告) | | `1` | 检测到一个或多个失败 | | `2` | 使用错误或内部故障 | ## 验证与信任 每个版本都附带多个可独立验证的信任锚: - **SHA256 校验和** — 完整性检查 - **Sigstore cosign 签名** — 关于哪个 CI workflow 构建了二进制文件的无密钥 OIDC 证明 - **SLSA 构建来源** — commit、workflow 和运行器的签名证明 - **SPDX SBOM** — 机器可读格式的完整依赖清单 - **可重现构建** — `Dockerfile.reproduce` 附带固定的 Rust 版本、`SOURCE_DATE_EPOCH=0` 和 `RUSTFLAGS --remap-path-prefix` 用于确定性输出 `--self-check` 根据已发布的哈希(TLS 固定获取)验证正在运行的二进制文件,显示 Sigstore 签名者身份,并打印可直接运行的 `cosign verify-blob` 和 `gh attestation verify` 命令。 发布 CI 流水线针对自身运行 hasp(`security.yml` 自扫描作业)。 有关分步构建、验证和 CI 集成说明,请参见 [REPRODUCE.md](docs/REPRODUCE.md)。有关完整的 5 级验证阶梯,请参见 [TRUST.md](docs/TRUST.md),有关漏洞报告,请参见 [SECURITY.md](docs/SECURITY.md)。 ## 构建 ``` cargo build --release ``` ### 可重现构建 ``` docker build -f Dockerfile.reproduce --output=. . # 生成内容:./hasp(静态链接的 musl 二进制文件) # 与 GitHub release artifacts 对比 SHA256 ``` ### 依赖项 10 个直接的 crate 依赖项(8 个跨平台 + 2 个仅限 Linux)。一个有意的 C 依赖项(`mimalloc` 安全模式)。零个 proc macros。零个异步运行时。 | Crate | 用途 | | -------------- | ------------------------------------------------------------- | | `yaml-rust2` | YAML 解析 (纯 Rust) | | `mimalloc` | 具有保护页和空闲内存擦除的强化分配器 | | `rustls` | 用于 GitHub SPKI 固定的自定义 TLS 验证器 | | `webpki-roots` | 为 rustls 捆绑的 Mozilla 根存储 | | `ureq` | 阻塞式 HTTP 客户端 (rustls TLS) | | `sha1` | 用于工作流完整性检查的 Git blob 哈希 | | `sha2` | 用于 `--self-check` 的 SHA-256 | | `base64` | `--self-check` 中的 Sigstore 证书解析 | | `landlock` | 文件系统沙箱化 (Linux) | | `libc` | Seccomp-BPF + cgroup-BPF 系统调用 (Linux) | ## IPC 协议 子进程通过 stdin/stdout 管道使用换行符分隔、制表符分隔且带百分号编码字段的记录进行通信。token 代理使用具有相同编码的单独经过身份验证的环回 TCP 协议。 ``` Scanner -> Launcher: HASP_SCAN_V1 magic header ACTION, SKIPPED, CONTAINER, AUDIT records Launcher -> Verifier: HASP_ACTION_REFS_V1 magic header REF records (action refs to verify) Verifier -> Launcher: HASP_VERIFY_V1 magic header VERIFY records (verification results) PROVENANCE records (audit findings) Verifier <-> Proxy: Loopback TCP, one request per connection shared-secret authenticated VERIFY, RESOLVE, FIND_TAG, REPO_INFO, REACHABLE, SIGNED, COMMIT_DATE, TAG_DATE, GET_ACTION_YML commands ``` ## 平台支持 | 平台 | 沙箱 | 状态 | | ------------- | -------------------------------------------------- | ------------------------------ | | Linux x86_64 | Landlock + seccomp-BPF + cgroup-BPF 出站沙箱 | 完全支持 | | Linux aarch64 | Landlock + seccomp-BPF + cgroup-BPF 出站沙箱 | 完全支持 | | macOS | 无 | 需要 `--allow-unsandboxed` | | Windows | 无 | 未测试 | ## 比较 `hasp exec` 与其他 CI/CD 安全工具的比较: | 特性 | hasp exec | [Harden-Runner][hr] | [Iron-Proxy][ip] | [Dagger][dg] | GitHub 2026 路线图 | | ------------------------ | ------------------------------ | ------------------------------ | ------------------- | -------------------- | ------------------- | | 每步内核沙箱 | Landlock + seccomp + BPF | 否 | 否 | 容器 | 否 | | 每步网络策略 | 每个进程的 BPF cgroup | 作业范围 | DNS/nftables | Container net | Runner 范围 | | 子进程环境中不存在 Secret| 是 (代理注入) | 否 | 是 (代理替换) | 是 (tmpfs mount) | 否 | | 现有 GHA 的直接替换方案 | `hasp exec -- cmd` | 步骤 1 代理 | 需要重写 | 需要重写 | 原生 | | 默认关闭失败 (Fail-closed) | 没有沙箱时拒绝 | 审计模式默认 | 可配置 | 容器保证 | 待定 | | Secret 范围 | 每命令,每域 | 无 | 每工作负载 | 每模块 | 每环境 | | 执行机制 | 内核 (Landlock/BPF/seccomp) | 用户空间代理 | DNS + nftables | Docker/BuildKit | Runner 级别的策略 | **Harden-Runner** 是一个类似于 EDR 的监控代理——它观察并可选地 在作业级别阻止出站流量,但不会沙箱化单个步骤或 代理 secrets。 **Iron-Proxy** 在概念上是最接近的(代理介导的 secret 替换 + 网络强制执行),但针对的是 AI 代理,并且不对工作负载本身使用内核沙箱。 **Dagger** 通过容器实现了真正的每步 secret 隔离,但 需要在 Dagger 的 SDK 中重写您的构建。 **GitHub 的 2026 路线图** 计划推出运行器级别的出站策略和分支范围的 secrets,但没有每步的内核沙箱或代理介导的注入。 ## 延期工作 **仓库身份连续性。** 下一步的强化措施是通过稳定的 GitHub 所有者/仓库 ID 固定 受信任的上游,并在熟悉的 `owner/repo` 名称解析为不同的数字身份时发出警报。这 需要一个本地缓存或明确的基线文件,因此 hasp 尚未声称 实施了它。 ## 许可证 MIT
标签:API接口, cgroup, CI/CD 安全, CISA项目, Commit SHA 验证, DevSecOps, DNS 解析, GitHub Actions, GraphQL安全矩阵, JSONLines, Landlock, seccomp-BPF, StruQ, YAML 解析, 上游代理, 云安全监控, 代理网关, 代码固化, 供应链风险管控, 内核沙盒, 可视化界面, 安全合规, 安全扫描器, 密钥保护, 工作流审计, 数据投毒防御, 权限控制, 沙盒, 网络代理, 自动笔记, 软件开发工具包, 进程隔离, 通知系统, 防数据渗出, 零信任, 静态分析