Skip to content

Commit fe26961

Browse files
committed
Make SECURITY.md self-documenting, and AUDIT_PAT required.
2 parents 26f5c30 + 7f87e0f commit fe26961

2 files changed

Lines changed: 70 additions & 53 deletions

File tree

.github/workflows/security-audit.yaml

Lines changed: 51 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ jobs:
2020
audit:
2121
runs-on: ubuntu-latest
2222
timeout-minutes: 20
23+
# The AUDIT_PAT (read-only Administration + Secrets + Environments)
24+
# lives in this environment, whose deployment-branch-policy admits
25+
# only `main` and `v*` tags — refs that the §3 rulesets reserve to
26+
# admins. A bot-pushed feature branch cannot reach this job at all
27+
# (GitHub rejects the run before any step starts), so the PAT
28+
# cannot be exfiltrated through a hand-authored workflow on a
29+
# non-admin-gated ref.
30+
environment:
31+
name: security-audit
2332
steps:
2433
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2534
with:
@@ -36,10 +45,24 @@ jobs:
3645
- name: Install workspace dependencies
3746
run: pnpm install --frozen-lockfile
3847

48+
- name: Verify AUDIT_PAT is provisioned
49+
env:
50+
AUDIT_PAT: ${{ secrets.AUDIT_PAT }}
51+
run: |
52+
[ -n "$AUDIT_PAT" ] && exit 0
53+
echo "FAIL" > audit-status.txt
54+
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
55+
echo "::error::AUDIT_PAT secret is not set."
56+
exit 1
57+
3958
- name: Audit against SECURITY.md
4059
uses: anthropics/claude-code-action@4481e6d3c7bbb88db2a928ca3444c536f589c7c1 # v1
4160
env:
42-
GH_TOKEN: ${{ github.token }}
61+
# AUDIT_PAT is fine-grained, read-only Administration +
62+
# Secrets + Environments. Required (the previous step
63+
# refuses to proceed without it), so no fallback to
64+
# github.token.
65+
GH_TOKEN: ${{ secrets.AUDIT_PAT }}
4366
with:
4467
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
4568
# Without an explicit allowlist the action defaults to a
@@ -49,54 +72,37 @@ jobs:
4972
# writing audit-status.txt.
5073
claude_args: '--allowed-tools "Read,Write,Edit,Bash,Grep,Glob"'
5174
prompt: |
52-
You are auditing this repository against SECURITY.md. The
53-
specifications are concrete `FAIL IF` lines plus the
54-
explicit clause that the list is not exhaustive — any code
55-
change that creates a security hole or reveals an existing
56-
one should fail this job.
57-
58-
Process:
59-
1. Read SECURITY.md.
60-
2. For each `FAIL IF` line, identify the mechanical check
61-
(gh api, grep, file presence, running a script) and
62-
execute it. Record PASS or FAIL with concrete evidence
63-
— file path and line number, API response excerpt, or
64-
command output.
65-
3. After the FAIL IF list is exhausted, do a qualitative
66-
pass. Inspect `.github/workflows/`, `.config/tend.yaml`,
67-
`.github/dependabot.yml`, `scripts/`, and any code that
68-
references secrets, for security holes the specs don't
69-
cover.
70-
71-
The workflow's `$GH_TOKEN` is a default `GITHUB_TOKEN` with
72-
limited scopes — it cannot read repository administration
73-
endpoints. If `gh api` returns 403 / "Resource not
74-
accessible by integration" on a check (typically `/rulesets`,
75-
`/environments/*`, or `/actions/secrets`), record the check
76-
as `UNVERIFIABLE` (not `FAIL`) with a note that the audit
77-
needs an `AUDIT_PAT` secret with `Administration: read` to
78-
cover it. `UNVERIFIABLE` checks do not flip the overall
79-
status to FAIL on their own.
80-
81-
Produce a Markdown report with three sections:
75+
Audit this repository against SECURITY.md. The `FAIL IF`
76+
lines are concrete checks; the spec also says the list is
77+
not exhaustive, so flag any other security hole you find.
78+
79+
For each `FAIL IF`, run the mechanical check (`gh api`,
80+
grep, file presence, or a script) and record PASS or FAIL
81+
with concrete evidence — file path and line number, API
82+
response excerpt, or command output. Then do a qualitative
83+
pass over `.github/workflows/`, `.config/tend.yaml`,
84+
`.github/dependabot.yml`, `scripts/`, and any code that
85+
touches secrets.
86+
87+
`$GH_TOKEN` is the `AUDIT_PAT` — a fine-grained, read-only
88+
PAT covering Administration + Secrets + Environments,
89+
guaranteed present by the previous step. If `gh api`
90+
returns 403 on any check, record FAIL with the note "PAT
91+
scope drifted from SECURITY.md"; reserve `UNVERIFIABLE`
92+
only for genuinely indeterminable cases (e.g. a transient
93+
network error) and explain why.
94+
95+
Write `audit-report.md` with three sections:
8296
- `## FAIL IF results` — one line per check with PASS/FAIL
83-
and concrete evidence
84-
- `## Qualitative findings` — free-form findings with
85-
severity (BLOCKER / WARNING / INFO)
97+
and evidence
98+
- `## Qualitative findings` — severity BLOCKER / WARNING / INFO
8699
- `## Summary` — overall PASS or FAIL with a one-paragraph
87100
rationale
88101
89-
Write the report to `audit-report.md` in the workspace.
90-
Write `PASS` or `FAIL` (no other text, no newline required)
91-
to `audit-status.txt`. Status is FAIL if any `FAIL IF` is
92-
violated or any qualitative finding is BLOCKER severity.
93-
Do not call `exit`; the next workflow step inspects the
94-
status file and surfaces the result.
95-
96-
Available environment: `$GH_TOKEN` is the workflow's
97-
GitHub token (read repo, write issues), `$GITHUB_REPOSITORY`
98-
is `owner/name`. Use `gh api` for GitHub configuration
99-
queries (rulesets, secrets, environments, collaborators).
102+
Write `PASS` or `FAIL` (no other text) to `audit-status.txt`.
103+
Status is FAIL if any `FAIL IF` is violated or any
104+
qualitative finding is BLOCKER. Do not call `exit` — the
105+
workflow inspects the status file.
100106
101107
- name: Surface result, file or close issue
102108
if: always()

SECURITY.md

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Security
22

3-
Dormouse is a terminal, so users trust it with shells, source trees, credentials, and local files. The dependency graph and release pipeline is part of the product's security boundary.
3+
> **Audited automatically.** This spec is checked against the repository by [`security-audit.yaml`](.github/workflows/security-audit.yaml) on a 24-hour schedule (04:21 UTC) and as a required gate before every VS Code release. Each failure is filed as an issue labeled [`security-audit-failure`](https://github.com/diffplug/dormouse/issues?q=is%3Aissue+label%3Asecurity-audit-failure) — open ones are live, closed ones are the historical record of what tripped past audits and what changed to clear them.
44
5-
The policies described in this document are enforced on every PR.
5+
Dormouse is a terminal, so users trust it with shells, source trees, credentials, and local files. The dependency graph and release pipeline is part of the product's security boundary.
66

77
## Dependency Supply Chain
88

@@ -23,7 +23,7 @@ New npm package versions are not adopted immediately. The workspace uses pnpm's
2323

2424
GitHub Actions are always pinned by commit hash, not version tag. Dependabot will update the hashes as necessary.
2525

26-
The agent-managed workflows (`tend-*.yaml`, `workflow-audit.yaml`, and `security-audit.yaml`) are exempt from the two rules below because they run Claude-powered automation that requires modifying issues, PRs, or code, or fetching an OIDC token. Their scope is bounded separately in the "Automated Maintainer" section.
26+
**Agent-managed workflows** are `tend-*.yaml`, `workflow-audit.yaml`, and `security-audit.yaml`. They implement the repo's automation and self-audit infrastructure, and are exempt from the two rules below because they need to modify issues, PRs, or code, or fetch an OIDC token. Their bounded scope is defined in the "Automated Maintainer" section.
2727

2828
- FAIL IF `pull_request_target` appears in any `.github/workflows/**` file other than `tend-*.yaml`.
2929
- FAIL IF a non-agent-managed workflow grants write permissions other than the explicitly scoped release provenance permissions `id-token: write` and `attestations: write`.
@@ -40,17 +40,18 @@ This repository runs the [tend](https://github.com/max-sixty/tend) agent harness
4040

4141
**Upstream compromise.** Tend's action is pinned by commit SHA (`max-sixty/tend@<sha>`) in every generated workflow, so silent updates to the running setup are not possible. `uvx tend@latest` runs only at install and during nightly regen; a compromise of that path would affect the next re-run, not the in-flight workflows.
4242

43-
**Audit visibility.** `workflow-audit.yaml` is a nightly job that walks every commit touching `.github/workflows/` since its previous successful run (using the GitHub API's timestamp as the lower bound, so a failed run pushes the window forward rather than dropping commits). It opens an issue summarizing each commit's author, refs, and changed files. A bot push that adds a new workflow file is visible in the next successful audit even if the bot tries to silently modify the audit workflow — the modification itself appears in the audit.
43+
**Audit visibility.** `workflow-audit.yaml` is a nightly job that walks every commit touching `.github/workflows/` since its previous successful run, opening an issue summarizing each. A bot push that disables or modifies the audit itself is caught in the next successful run's diff window.
4444

4545
- FAIL IF the repository ruleset named `Merge access` is missing, doesn't target `~DEFAULT_BRANCH`, blocks anything other than `update`, or doesn't have admin (`RepositoryRole` actor `5`) as its sole bypass actor.
4646
- FAIL IF the repository ruleset named `Tag operations` is missing, doesn't target `~ALL` tags, doesn't block both `creation` and `update`, or doesn't have admin-only bypass.
4747
- FAIL IF `dormouse-bot` holds a permission higher than `push` on this repository.
4848
- FAIL IF `OVSX_PAT` or `VSCE_PAT` appears as a repo-level secret. They must live only in the `vscode-extension-publish` environment.
49-
- FAIL IF the `vscode-extension-publish` environment's deployment-branch-policies allow any ref pattern that is not admin-gated by the `Tag operations` or `Merge access` rulesets.
49+
- FAIL IF any GitHub environment's deployment-branch-policies admit a ref that is not admin-gated by the `Tag operations` or `Merge access` rulesets. Today this covers `vscode-extension-publish` (`v*` tag, admin-only via `Tag operations`) and `security-audit` (`main` admin-only via `Merge access`, plus `v*` tag).
50+
- FAIL IF `AUDIT_PAT` is missing from the `security-audit` environment, or is present at the repo level instead. The audit refuses to run without it, and it must be env-scoped so a bot-pushed feature branch cannot reach it.
5051
- FAIL IF `CHROMATIC_PROJECT_TOKEN` is missing from `secrets.allowed` in `.config/tend.yaml`. The allowlist entry is an explicit acknowledgment that the bot can read this token.
5152
- FAIL IF `.github/workflows/workflow-audit.yaml` is missing, disabled, or has not produced a successful run in the last 48 hours.
52-
- FAIL IF any `tend-*.yaml` workflow uses an unpinned action reference (e.g. `@main`, no version). Inside `tend-*.yaml`, both tag pins (`@v6`, `@0.0.25`) and SHA pins are accepted because the file is owned by the upstream generator (`max-sixty/tend`), which currently uses tag pins. All actions in every other workflow — including `workflow-audit.yaml` and `security-audit.yaml` — must follow the SHA-pin rule in "GitHub Actions Policies".
53-
- FAIL IF any agent-managed workflow (`tend-*.yaml`, `workflow-audit.yaml`, `security-audit.yaml`) grants a permission beyond `contents: write`, `pull-requests: write`, `issues: write`, `id-token: write`, `actions: read`, or any `read` permission.
53+
- FAIL IF any `tend-*.yaml` workflow uses an unpinned action reference (e.g. `@main`, no version). Tag pins are accepted inside `tend-*.yaml` because the file is owned by the upstream generator; every other workflow — agent-managed or not — must SHA-pin per the rule above.
54+
- FAIL IF any agent-managed workflow grants a permission beyond `contents: write`, `pull-requests: write`, `issues: write`, `id-token: write`, `actions: read`, or any `read` permission.
5455

5556
## VS Code Extension Releases
5657

@@ -73,5 +74,15 @@ Desktop releases are not fully automated. GitHub Actions builds unsigned artifac
7374

7475
The `security-audit` workflow at `.github/workflows/security-audit.yaml` enforces this document. It runs nightly and is a required dependency of the VS Code publish job in `release.yml`, so no release ships without a passing audit. The audit reads SECURITY.md, executes each `FAIL IF` as a mechanical check, and also does a qualitative pass for security holes the specs don't cover. On any `FAIL IF` violation or BLOCKER-severity finding, the workflow opens (or updates) an issue labeled `security-audit-failure` with the full audit report, and exits non-zero. When a subsequent audit passes, the open failure issue is auto-closed so the tracker matches the live state.
7576

77+
The audit job declares `environment: security-audit`, whose deployment-branch-policy admits only `main` and `v*` tags. Both ref classes are admin-only by §3's rulesets, so a write-scoped bot cannot reach the env's secrets (most importantly `AUDIT_PAT`, when provisioned) by pushing a workflow file to a feature branch.
78+
79+
As a consequence of that env-gating, audit changes are iterated on `main` directly. A `workflow_dispatch` from any other ref is rejected by the environment's deployment-policy before any step runs. To experiment on a branch, widen the env's policy temporarily and revert after.
80+
81+
`AUDIT_PAT` is **required**. The audit's first step verifies the secret is present and refuses to run otherwise — without it the audit cannot read the administration endpoints needed to verify ruleset bypass actors, repo-level secret listing, and environment policies, so the spec it claims to enforce would be unenforceable in its key sections. Mint a fine-grained PAT on an admin's account with read-only `Administration` + `Secrets` + `Environments` scoped to `diffplug/dormouse` only, then store it env-scoped:
82+
83+
```bash
84+
gh secret set AUDIT_PAT --env security-audit --repo diffplug/dormouse --body 'github_pat_…'
85+
```
86+
7687
- FAIL IF `.github/workflows/security-audit.yaml` is missing, disabled, or no longer invoked from `release.yml`'s publish path.
77-
- FAIL IF the audit has been weakened — e.g. the prompt no longer requires the qualitative pass, a `FAIL IF` can be ignored, or the failure-reporting step that opens a `security-audit-failure` issue and exits non-zero has been removed.
88+
- FAIL IF the audit has been weakened — e.g. the prompt no longer requires the qualitative pass, a `FAIL IF` can be ignored, the failure-reporting step that opens a `security-audit-failure` issue and exits non-zero has been removed, or the `AUDIT_PAT` pre-check is removed or bypassed.

0 commit comments

Comments
 (0)