## 为什么选择 ghactor
GitHub Actions 是一个供应链雷区。浮动的 `@main` 标签会在你不知情时发生偏移。缺失的 `permissions:` 块默认为 *write-all*。一个粗心的 `pull_request_target` 加上对 PR head 的 checkout 就会导致完整的仓库接管。大多数团队会在 review 时发现这些问题——如果他们进行了 review 的话。
ghactor 会在 workflow 发布前将其拦截:
- **`lint`** 封装了 `actionlint`,并包含 31 条专注于实际被利用场景的安全规则(pwn-request、注入、过期的 pin、cache 投毒、env 走私、tj-actions 式的污点传播、弃用的 runner、跨组织的 `secrets: inherit`)。
- **`pin`** 将每个 `uses:` 重写为 40 字符的 SHA,并将原始标签作为注释保留,因此升级时仍然可以进行干净的 diff。
- **`fix`** 可一次性自动添加 `permissions:`、job 超时、shell pinning 以及可选的 SHA pinning。
- **`gen`** 脚手架可生成开箱即用、固定到 SHA 的强化版 workflow YAML(CI、CodeQL、release + attest-build-provenance、Dependabot)。
- **`audit`** 将固定的 action 与 GitHub Advisory Database 进行交叉检查,标记已归档的仓库,并捕获 404 的 action。
- **`doctor`** 为整个仓库提供 0-100 的安全态势评分。
一个二进制文件,零 Node runtime,可作为 composite Action 或 CLI 提供。
## 安装
```
go install github.com/kdairatchi/ghactor/cmd/ghactor@latest
```
`pin`、`update` 和 `audit` 需要 `gh` (GitHub CLI)。`trial` 需要 [`act`](https://github.com/nektos/act)。`trail` 直接使用 GitHub REST(读取 `GITHUB_TOKEN` / `GHACTOR_GITHUB_TOKEN`;未设置 token 时回退到 `gh`)。通过 `GITHUB_API_URL` 支持 GHES。
## 它是如何协同工作的
```
flowchart LR
W["workflows/*.yml"] --> L["lint"]
W --> F["fix"]
W --> P["pin"]
W --> T["trial"]
W --> A["audit"]
W --> SC["secrets"]
L --> S{"SARIF?"}
S -->|yes| GH["GitHub Security tab"]
S -->|no| C["CLI output"]
F -->|--pin| P
P --> W
T --> ACT["act local runner"]
classDef cmd fill:#0d0d15,stroke:#f0c040,color:#f0c040
classDef sink fill:#0d0d15,stroke:#06b6d4,color:#06b6d4
classDef src fill:#111119,stroke:#9ca3af,color:#fff
class L,F,P,T,A,SC cmd
class GH,C,ACT sink
class W,S src
```
## 命令
| 命令 | 功能描述 |
|-------------|---------------------------------------------------------------------------------|
| `lint` | 运行 rhysd/actionlint **以及** ghactor 的安全规则 (GHA001–GHA032) |
| `pin` | 通过 `gh api` 将 `uses: owner/repo@tag` 重写为 `@<40-char SHA> # tag` |
| `fix` | 添加缺失的 `permissions:`,注入 `timeout-minutes:`,固定 shells/containers |
| `update` | 显示哪些 action 有更新的版本 (通过 `gh api .../releases/latest`) |
| `audit` | 将固定的 action 与 GHSA 进行交叉检查;标记已归档和 404 的仓库 |
| `secrets` | Gitleaks 风格的 workflow YAML 扫描,查找泄露的 API key、token 和私钥 |
| `baseline` | 为当前发现生成快照,以便 CI 仅在出现*新*问题时失败(遗留代码采纳) |
| `trial` | Shell 到 `act` 以在本地运行 workflow |
| `trail` | 美化打印最近的运行记录,包含成功/失败/平均持续时间 |
| `doctor` | 生成带有 0–100 分数的全仓库健康报告 |
| `rules` | 列出所有 ghactor 规则 |
| `explain` | 打印单条规则的卡片(描述、修复方案、修复示例) |
| `gen` | 生成强化版的 workflow(ci-go, ci-node, codeql, release-goreleaser, …) |
| `watch` | 在 YAML 更改时重新进行 lint(基于 fsnotify,支持 debounce) |
| `sarifdiff` | 对比两个 SARIF 报告 — 呈现 baseline 和 head 之间的新发现 |
## 规则
这 31 条规则分为四个类别——**注入**、**偏移 / 供应链**、**最小权限**和**发布完整性**。`lint` 默认运行所有这些规则。
```
flowchart TB
subgraph INJ["injection"]
direction LR
GHA003["GHA003
pwn-request
error"]
GHA004["GHA004
expression injection
error"]
GHA018["GHA018
env smuggling
error"]
GHA026["GHA026
tj-actions taint
error"]
GHA031["GHA031
obfuscated run
warn"]
GHA032["GHA032
spoofable actor
warn"]
end
subgraph PIN["drift / supply-chain"]
direction LR
GHA001["GHA001
tag not SHA
warn"]
GHA006["GHA006
floating ref
warn"]
GHA008["GHA008
stale SHA
warn"]
GHA009["GHA009
reusable wf ref
error / warn"]
GHA023["GHA023
container tag
warn"]
GHA024["GHA024
deprecated version
warn / error"]
end
subgraph LP["least-privilege"]
direction LR
GHA002["GHA002
no permissions
warn"]
GHA011["GHA011
persist-creds prt
error"]
GHA015["GHA015
prt artifact upload
error"]
GHA019["GHA019
id-token scope
warn"]
GHA020["GHA020
write-all
warn"]
GHA029["GHA029
cross-org inherit
warn"]
end
subgraph REL["release integrity"]
direction LR
GHA017["GHA017
no concurrency
info"]
GHA025["GHA025
deprecated runner
warn / error"]
GHA027["GHA027
publish cache
warn"]
GHA028["GHA028
no provenance
info"]
end
classDef err fill:#1a0d0d,stroke:#ef4444,color:#ef4444
classDef warn fill:#14110a,stroke:#f0c040,color:#f0c040
classDef info fill:#0a1419,stroke:#06b6d4,color:#06b6d4
class GHA003,GHA004,GHA009,GHA011,GHA015,GHA018,GHA026 err
class GHA001,GHA002,GHA006,GHA008,GHA019,GHA020,GHA023,GHA024,GHA025,GHA027,GHA029,GHA031,GHA032 warn
class GHA017,GHA028 info
```
完整详情:`ghactor rules --verbose` 或 `ghactor explain GHA004`。
| ID | 严重性 | 检查内容 |
|---------|------------|------------------------------------------------------------------------------------------------------|
| GHA001 | warn | Action 使用标签而不是 40 字符的 SHA 进行固定 |
| GHA002 | warn | 没有 `permissions:` 块 — 默认为 write-all |
| GHA003 | error | `pull_request_target` + checkout 到 PR head ref (pwn-request 模式) |
| GHA004 | error | 不受信任的 `${{ github.event.* }}` 被插入到 `run:` 中(注入) |
| GHA005 | info | Job 没有 `timeout-minutes:`(默认 360) |
| GHA006 | warn | Action 固定到浮动 ref (`@main`, `@master`, `@latest`, `@HEAD`) |
| GHA007 | warn | `uses:` 完全没有 `@ref` |
| GHA008 | warn | 固定的 SHA 已过期 — `# tag` 注释解析为不同的 SHA;在 opts 中需要 `Resolver` |
| GHA009 | error/warn | 可复用 workflow 的 ref 是浮动分支 (error) 或 semver 标签而不是 SHA (warning) |
| GHA010 | error | Action 匹配 `.ghactor.yml` 中的 `deny_actions` glob 模式 |
| GHA011 | error | 在 `pull_request_target` 下使用带有 `persist-credentials: true` 的 `actions/checkout`(token 窃取) |
| GHA012 | warn | `run:` 包含 `curl \| sh` / `wget \| bash` 模式(远程代码执行) |
| GHA013 | warn | `actions/cache` key 派生自不受信任的 `github.event.*` / `inputs.*`(cache 投毒) |
| GHA014 | error | 遗留的 `::set-env` / `::add-path` / `ACTIONS_ALLOW_UNSECURE_COMMANDS=true`(自 2020 年 11 月起已禁用) |
| GHA015 | error | 在 `pull_request_target` 下的 `actions/upload-artifact`(artifact 提取机密) |
| GHA016 | warn | 没有限制性标签的 `self-hosted` runner — fork PR 可以将其作为目标 |
| GHA017 | info | 部署/发布 workflow 没有 `concurrency:` 块(重复部署竞争) |
| GHA018 | error | 不受信任的表达式被写入 `$GITHUB_ENV` / `$GITHUB_OUTPUT`(env 走私) |
| GHA019 | warn | Job 请求 `id-token: write` — 验证 OIDC 信任策略是否限制了 `sub`/`aud` |
| GHA020 | warn | `permissions:` 授予 `write-all` / 广泛的 `write` 权限 — 缩小到最小权限 |
| GHA021 | warn | 可复用 workflow 的 `workflow_call` 输入未指定类型 — 固定 `type:` 以防止类型强制转换 |
| GHA022 | info | Step 的 `run:` 块没有 `shell:` — 默认设置在不同 runner 间会发生偏移 (bash vs. pwsh) |
| GHA023 | warn | `container:` / `services.*.image:` 通过标签而不是 digest 固定 — 重写为 `image@sha256:…` |
| GHA024 | warn/error | Action 固定到已弃用的主版本 (例如 `actions/upload-artifact@v3` — 自 2025-01-30 起强制失败) |
| GHA025 | warn/error | Job 使用已弃用/已移除的 runner 镜像 (`ubuntu-20.04`, `macos-12`, `windows-2019`, …) |
| GHA026 | error | `GHA004` 提升 — 来自已知受污染 action 的不可信 `steps.
.outputs.*` (CVE-2023-27529) |
| GHA027 | warn | 发布/发布 workflow 恢复了构建缓存 — 特性分支缓存对 release 造成毒害 |
| GHA028 | info | 发布/发布 workflow 没有 `actions/attest-build-provenance` step |
| GHA029 | warn | 来自外部组织且带有 `secrets: inherit` 的可复用 workflow — 转发所有仓库机密 |
| GHA030 | error | Action 不在 `.ghactor.yml` 的 `allow_actions:` 白名单中(可选择加入 — 仅在设置了列表时触发) |
| GHA031 | warn | `run:` 块混淆 — base64 解码通过管道传输给 shell,`eval` 了 `${{ }}`,`curl \| bash` 链 |
| GHA032 | warn | Job 的 `if:` 门控基于可伪造的 `github.actor` / `[bot]` 身份匹配 |
## 示例
```
# Lint 所有内容
ghactor lint
# 仅保留 ghactor 规则,跳过 timeouts 噪音
ghactor lint --only-ghactor --disable GHA005
# 通过管道传递给 jq 以用于 CI 仪表板
ghactor lint --json | jq '.[] | select(.severity=="error")'
ghactor lint --baseline .ghactor/baseline.json # suppress known findings; report NEW only
ghactor lint --resolve-drift # online: GHA008 stale-SHA check via gh api
ghactor lint --junit ghactor.junit.xml # JUnit XML (Jenkins/GitLab CI)
ghactor lint --github # GH Actions workflow annotations on stdout
ghactor lint --actions . # also lint composite action.yml files
ghactor lint --since origin/main # only lint files changed in this PR
ghactor explain GHA004 # print full card for a rule
ghactor explain GHA020 --json # machine-readable rule metadata
ghactor gen # list available templates
ghactor gen ci-go -o .github/workflows/ci.yml # scaffold a hardened Go CI workflow
ghactor gen attest-release --force # SLSA build-provenance release
ghactor gen dependabot -o .github/dependabot.yml
ghactor watch # auto-re-lint on YAML change
ghactor watch --clear --debounce 150ms # fast dev loop
# 预览 / 应用 SHA pinning(缓存在 .ghactor/cache.json,30天 TTL)
ghactor pin --dry-run
ghactor pin
# 一次性强化:perms + 15分钟 timeouts + pin 每个 action + shells + containers
ghactor fix --all --timeout 15
ghactor audit # check pinned actions against GHSA
ghactor audit --offline --json # static deny list only, machine-readable
ghactor audit --no-archival --no-missing # skip archived/404 repo checks
ghactor secrets --entropy # scan + high-entropy string detection
ghactor baseline create # snapshot current findings
ghactor baseline status --fail-on error # CI-mode: fail only on NEW findings
ghactor baseline prune # drop fingerprints that no longer match
ghactor doctor # scored health report
ghactor trail -n 50 # last 50 runs
ghactor trial -e pull_request # run locally via act
```
## 退出代码
当发现的问题达到或超过 `--fail-on` 指定的级别(默认为 `warning`)时,`lint` 将以 `1` 退出。如果你只想在 CI 中拦截真正的安全问题,请使用 `--fail-on error`——其他所有问题都只作为信号保留,而不会成为阻塞。
## 在 CI 中使用
```
flowchart LR
PR["Pull request"] --> CK["actions/checkout"]
CK --> GA["kdairatchi/ghactor@v1"]
GA --> SARIF["ghactor.sarif"]
SARIF --> UP["upload-sarif"]
UP --> TAB["Security tab"]
GA -->|"fail-on: error"| FAIL{"Blocking finding?"}
FAIL -->|yes| BLK["PR blocked"]
FAIL -->|no| OK["Check passes"]
classDef cmd fill:#0d0d15,stroke:#f0c040,color:#f0c040
classDef sink fill:#0d0d15,stroke:#06b6d4,color:#06b6d4
classDef node fill:#111119,stroke:#9ca3af,color:#fff
classDef bad fill:#1a0d0d,stroke:#ef4444,color:#ef4444
classDef good fill:#0d1a10,stroke:#22c55e,color:#22c55e
class GA cmd
class TAB,UP,SARIF sink
class PR,CK,FAIL node
class BLK bad
class OK good
```
**作为 composite action** — 发现的问题将通过 SARIF 显示在 Security 选项卡中:
```
name: ghactor
on: [push, pull_request]
permissions:
contents: read
security-events: write
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- uses: kdairatchi/ghactor@v1
with:
fail-on: error
disable: GHA005
```
**或者直接调用 CLI:**
```
- uses: actions/setup-go@11bd71901bbe5b1630ceea73d27597364c9af683 # v5
with: { go-version: stable }
- run: go install github.com/kdairatchi/ghactor/cmd/ghactor@latest
- run: ghactor lint --sarif ghactor.sarif --fail-on error
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: ghactor.sarif
category: ghactor
```
## 配置
在仓库根目录下可选的 `.ghactor.yml`:
```
fail_on: warning # error | warning | info
disable: # skip specific rules
- GHA005
deny_actions: # glob patterns → GHA010 error
- "some-untrusted-org/*"
- "*/shady-action"
allow_actions: # glob patterns → GHA030 error for anything else
- "actions/*"
- "myorg/*"
severity: # per-rule severity override
GHA028: off # disable an info rule entirely
GHA005: warning # escalate an info rule
```
## 安全
私下报告疑似漏洞 — 参见 [SECURITY.md](SECURITY.md)。
由 [@kdairatchi](https://github.com/kdairatchi) 构建 · [ProwlrBot](https://github.com/ProwlrBot) 生态系统的一部分 · 采用 MIT 许可