Skip to content

Add the nightly security audit against SECURITY.md - #10

Draft
nedtwigg wants to merge 2 commits into
security-specfrom
security-audit
Draft

nedtwigg wants to merge 2 commits into
security-specfrom
security-audit

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Stacked on #9 (security-spec), which is on #8.

What

A single-domain port of Dormouse's nightly security audit, executing SECURITY.md:

  • .github/workflows/security-audit.yml — job and check run both named security-audit; runs nightly, on workflow_dispatch, and on every push to main, so every main commit carries a check run a consumer can read (gh api repos/diffplug/pgstencil/commits/<sha>/check-runs). That is the contract dormouse#745 verifies for the commit it vendors.
  • One Opus agent (.github/audit/_preamble.md + .github/audit/security.md) runs every FAIL IF mechanically (db:verify, unit, the Better Auth integration suites under Docker, packages:verify), then an adversarial pass, appending to audit-report.md as it goes. Verdict grammar: exact VERDICT: PASS plus the <!-- END OF REPORT --> sentinel is the only pass; a VERDICT: FAIL prefix is a finding even when cut off; anything else is INCONCLUSIVE. FAIL and INCONCLUSIVE file or append to a security-audit-failure issue (retitled upward only); PASS closes it.
  • Secrets redacted from the transcript and report before publishing, fail-closed; transcript artifact kept 14 days.
  • scripts/security-audit-local.sh runs the same prompt files locally; scripts/security-audit.test.mjs (28 tests, pnpm test:scripts, now in check.yml) pins the reporting grammar, the retitle rule, the redactor, and the local runner, mutation-checked.
  • SECURITY.md gains the FAIL IF for this workflow and a "How this file is checked" section.

One design difference from Dormouse

anthropics/claude-code-action throws Unsupported event type: push, and a push trigger is what gives each commit its check run. So this job installs a pinned Claude Code (2.1.278, the same installer the action uses) and runs claude -p with the same model and tool flags. No GitHub App install, no id-token: write; CI and the local runner now execute the same binary over the same prompts. The headless flag set has not been exercised in a fresh CI HOME yet; a misbehaving run leaves no sentinel and reports INCONCLUSIVE, never a false PASS.

Operator step

Add CLAUDE_CODE_OAUTH_TOKEN as a repository secret. Until then every run fails at the verify step with a report naming the missing secret.

Verified

pnpm format:check, typecheck, test:scripts (28), test:unit, actionlint on both workflows, bash -n on every run: block. The audit itself was not run.

🤖 Generated with Claude Code

nedtwigg and others added 2 commits September 21, 2026 17:43
SECURITY.md states its guarantees as FAIL IF conditions so a machine can
execute them, but nothing executed them. A single Opus agent now runs
every one as a mechanical check with evidence, then reads the auth and
infrastructure code adversarially, and publishes one report.

The port of Dormouse's nightly audit drops everything that only exists
because that repo has three domains: no orchestrator, no subagents, no
fragment merge, no separate status file. The verdict is the report's own
first line, and a `<!-- END OF REPORT -->` sentinel is what says the
report is finished — so an agent cut off after rewriting that line reads
as inconclusive rather than as a pass. Three outcomes, not two: an audit
that never decided files under the same label as a finding and holds the
check run red, but says in its own issue that it is not a security
finding.

The push-to-main trigger is load-bearing rather than convenience. A
consumer that vendors a packed tarball recovers the commit from
dist/provenance.json; it can now confirm that commit was audited by
reading the security-audit check run on it, which is why the job id and
its name are both that string.

Claude Code is invoked directly instead of through claude-code-action,
which throws `Unsupported event type: push` — its context parser accepts
only workflow_dispatch, repository_dispatch, schedule and workflow_run,
and a step-level GITHUB_EVENT_NAME cannot stand in because the runner
writes the real GITHUB_* values over a step's env. The CLI keeps the
per-commit check run and makes CI and the local runner the same command
over the same prompt files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The reporting step is the only thing that decides whether a commit's
security-audit check run goes green, and it was enforced by nobody: a
`VERDICT: PASS` widened to a prefix match, a dropped sentinel test, or a
sink removed from the redactor's argument list would all have passed
review and shipped.

`pnpm test:scripts` extracts the shipped shell out of the workflow by
indentation and runs it — a renamed step or a moved block scalar throws
rather than quietly handing the test the rest of the file. The sinks the
redactor must cover are read from the archive step's own path list, so a
file that starts being published without starting to be redacted fails
here instead. The local runner is covered against the same grammar, and
`.github/audit/_preamble.md` is pinned as the producer of the sentinel
every reader waits for, since nothing else writes that literal.

Verified by mutation: widening the PASS match, dropping the sentinel
requirement, and removing a sink from the redactor each turn the suite
red. `clamp-issue-body.mjs` and its self-test come from Dormouse
unchanged but for their pointers; GitHub rejects an over-long issue body
outright, which loses the whole finding rather than truncating it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant