security-audit #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: security-audit | |
| # Audits this repo against SECURITY.md. Reusable: runs nightly via the | |
| # schedule trigger, on-demand via workflow_dispatch, and is called from | |
| # release.yml as a precondition to publishing. | |
| on: | |
| schedule: | |
| - cron: "21 4 * * *" | |
| workflow_dispatch: | |
| workflow_call: | |
| permissions: | |
| contents: read | |
| actions: read | |
| issues: write | |
| id-token: write | |
| jobs: | |
| audit: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| # The AUDIT_PAT (read-only Administration + Secrets + Environments) | |
| # lives in this environment, whose deployment-branch-policy admits | |
| # only `main` and `v*` tags — refs that the §3 rulesets reserve to | |
| # admins. A bot-pushed feature branch cannot reach this job at all | |
| # (GitHub rejects the run before any step starts), so the PAT | |
| # cannot be exfiltrated through a hand-authored workflow on a | |
| # non-admin-gated ref. | |
| environment: | |
| name: security-audit | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 1 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: 22 | |
| - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 | |
| with: | |
| version: 11.0.6 | |
| - name: Install workspace dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Verify AUDIT_PAT is provisioned | |
| env: | |
| AUDIT_PAT: ${{ secrets.AUDIT_PAT }} | |
| run: | | |
| [ -n "$AUDIT_PAT" ] && exit 0 | |
| echo "FAIL" > audit-status.txt | |
| echo "**FAIL** — \`AUDIT_PAT\` is not present in the \`security-audit\` environment. See [SECURITY.md > CI Validation Contract](https://github.com/$GITHUB_REPOSITORY/blob/main/SECURITY.md#ci-validation-contract) for provisioning." > audit-report.md | |
| echo "::error::AUDIT_PAT secret is not set." | |
| exit 1 | |
| - name: Audit against SECURITY.md | |
| uses: anthropics/claude-code-action@4481e6d3c7bbb88db2a928ca3444c536f589c7c1 # v1 | |
| env: | |
| # AUDIT_PAT is fine-grained, read-only Administration + | |
| # Secrets + Environments. Required (the previous step | |
| # refuses to proceed without it), so no fallback to | |
| # github.token. | |
| GH_TOKEN: ${{ secrets.AUDIT_PAT }} | |
| with: | |
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | |
| # Without an explicit allowlist the action defaults to a | |
| # restrictive set that excludes Bash and Write, so an | |
| # auditing prompt that wants to run `gh api` and produce a | |
| # report file racks up permission denials and exits without | |
| # writing audit-status.txt. | |
| claude_args: '--allowed-tools "Read,Write,Edit,Bash,Grep,Glob"' | |
| prompt: | | |
| Audit this repository against SECURITY.md. The `FAIL IF` | |
| lines are concrete checks; the spec also says the list is | |
| not exhaustive, so flag any other security hole you find. | |
| For each `FAIL IF`, run the mechanical check (`gh api`, | |
| grep, file presence, or a script) and record PASS or FAIL | |
| with concrete evidence — file path and line number, API | |
| response excerpt, or command output. Then do a qualitative | |
| pass over `.github/workflows/`, `.config/tend.yaml`, | |
| `.github/dependabot.yml`, `scripts/`, and any code that | |
| touches secrets. | |
| `$GH_TOKEN` is the `AUDIT_PAT` — a fine-grained, read-only | |
| PAT covering Administration + Secrets + Environments, | |
| guaranteed present by the previous step. If `gh api` | |
| returns 403 on any check, record FAIL with the note "PAT | |
| scope drifted from SECURITY.md"; reserve `UNVERIFIABLE` | |
| only for genuinely indeterminable cases (e.g. a transient | |
| network error) and explain why. | |
| Write `audit-report.md` with three sections: | |
| - `## FAIL IF results` — one line per check with PASS/FAIL | |
| and evidence | |
| - `## Qualitative findings` — severity BLOCKER / WARNING / INFO | |
| - `## Summary` — overall PASS or FAIL with a one-paragraph | |
| rationale | |
| Write `PASS` or `FAIL` (no other text) to `audit-status.txt`. | |
| Status is FAIL if any `FAIL IF` is violated or any | |
| qualitative finding is BLOCKER. Do not call `exit` — the | |
| workflow inspects the status file. | |
| - name: Surface result, file or close issue | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| set -eo pipefail | |
| STATUS=$(tr -d '[:space:]' < audit-status.txt 2>/dev/null || echo "FAIL") | |
| DATE=$(date -u +%Y-%m-%dT%H:%MZ) | |
| RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | |
| # Idempotent label creation; ignore "already exists" errors. | |
| gh label create security-audit-failure \ | |
| --color B60205 --description "Security audit failure" 2>/dev/null || true | |
| if [ "$STATUS" = "PASS" ]; then | |
| # Auto-close any open audit-failure issues so the issue | |
| # tracker reflects the live state. | |
| for n in $(gh issue list --label security-audit-failure \ | |
| --state open --json number --jq '.[].number'); do | |
| gh issue close "$n" --comment "Audit passed at $DATE. [Run]($RUN_URL)" | |
| done | |
| echo "Audit passed." | |
| exit 0 | |
| fi | |
| if [ ! -s audit-report.md ]; then | |
| printf '%s\n' \ | |
| "Audit step produced no \`audit-report.md\`. See workflow run logs." \ | |
| > audit-report.md | |
| fi | |
| { | |
| echo "Audit failed at $DATE. [Run]($RUN_URL)" | |
| echo | |
| cat audit-report.md | |
| } > audit-comment.md | |
| EXISTING=$(gh issue list --label security-audit-failure \ | |
| --state open --json number --jq '.[0].number' || true) | |
| if [ -n "$EXISTING" ]; then | |
| gh issue comment "$EXISTING" --body-file audit-comment.md | |
| echo "Appended re-audit failure to issue #$EXISTING" | |
| else | |
| gh issue create \ | |
| --title "[security-audit] FAIL on $(date -u +%Y-%m-%d)" \ | |
| --label security-audit-failure \ | |
| --body-file audit-comment.md | |
| fi | |
| exit 1 |