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 解析, 上游代理, 云安全监控, 代理网关, 代码固化, 供应链风险管控, 内核沙盒, 可视化界面, 安全合规, 安全扫描器, 密钥保护, 工作流审计, 数据投毒防御, 权限控制, 沙盒, 网络代理, 自动笔记, 软件开发工具包, 进程隔离, 通知系统, 防数据渗出, 零信任, 静态分析