ci: check fixture identifiers by allowlist instead of denylist - #1
Conversation
The previous check grepped for known-bad strings, which meant the workflow file itself had to spell out the customer and internal identifiers it was guarding against — publishing them in a public repo to prevent them being published. Replaces it with scripts/check-fixture-identifiers.mjs, which asserts that tenant/agent/kid values match neutral patterns. An allowlist cannot leak what it forbids, and it also catches identifiers nobody thought to enumerate. Inspects both JSON fields and HTTP header values, plus the keyid inside Signature-Input; a field-only version missed a name planted in the header set. Fails loudly if no fixtures or no matching fields are found, so it cannot pass vacuously.
The gitleaks GitHub Action requires a paid licence for organisation repositories and failed in 4s with 'License key is required', so the job reported failure without scanning anything. The CLI it wraps has no such requirement. Scans the working tree and git history separately: 'gitleaks detect' only walks commit history, so a secret present in the tree but not yet committed passes it silently -- verified by planting a token that the working-tree scan catches and the history scan does not.
CodeQL flagged the kid regex as high severity: with '-' present in both the group body and its separator class, an input like 'agent-' + '--' * n can be partitioned ambiguously and the match backtracks exponentially. Reproduced locally -- a 50 character input did not finish within 8 seconds. Rewritten as a single character class, so there is one quantifier and no ambiguity. A 50,000 character adversarial input now matches in under a millisecond. All real kid shapes are still accepted, including the dotted/underscored variant that pins kid quoting, and real identifiers are still rejected.
boot-coco
left a comment
There was a problem hiding this comment.
Code Review — Boot (审 sha 24b4263)
Verdict: APPROVE
scripts/check-fixture-identifiers.mjs
Whitelist approach: correct. Blacklist would name the forbidden identifiers in the workflow file (self-defeating). Whitelist catches unexpected identifiers including ones never listed.
Coverage: complete. 5 field rules cover:
- JSON fields:
tenant_id,agent_id,kid - HTTP header values:
X-Openmax-Tenant,X-Openmax-Agent Signature-Inputkeyid extracted via/keyid="([^"]+)"/g
Anti-vacuous protection: two layers.
- No fixture files in testdata/ → exit 1 ("check would pass vacuously")
- Per-field existence check — every FIELD_RULES entry must have at least one value seen across all fixtures, else exit 1 ("not actually inspecting anything")
Seed control verification (3/3 independently replicated):
tenant_id: "ajj"→ caught ✓X-Openmax-Agent: "finops-core"→ caught ✓Signature-Input keyid="finops-core.prod_2026a-v2"→ caught ✓- Clean tree → passes ✓
ReDoS Fix
KID_OK = /^agent-[a-z0-9._-]+$/ — single character class, one quantifier, no nested groups. Verified: 50,000-char adversarial input (agent- + --×25000) completes in 0.1ms. No backtracking possible.
The original ([._-][a-z0-9-]+)* had - in both the group body and separator class → ambiguous partitioning → exponential backtracking. Fix is correct.
.github/workflows/ci.yml Secret Scan
gitleaks dir .(work tree) +gitleaks git .(history) — two separate steps, correct.detectalone only scans history.- CLI pinned to
8.28.0(no paid Action license dependency). --redactflag prevents leaked values from appearing in CI logs.
Overall CI Pipeline
6 jobs: Unit tests / Lint / Golden vector integrity / Fixture identifier guard / SAST (CodeQL) / Secret scan. All appropriate for a public repo handling signing material.
No issues found.
The identifier guard added in the initial commit grepped for known-bad strings — which meant the workflow file had to spell out the customer and internal identifiers it was guarding against, publishing them in a public repo in order to prevent them being published. A fresh-clone scan of the new repo found exactly four hits, all of them on that one grep line.
Replaced with
scripts/check-fixture-identifiers.mjs, which asserts thattenant_id/agent_id/kidvalues match neutral patterns. An allowlist cannot leak what it forbids, and it catches identifiers nobody thought to enumerate — a denylist only ever catches the names someone remembered to add.Coverage note: it inspects both JSON fields and HTTP header values (
X-Openmax-Tenant,X-Openmax-Agent) plus thekeyidinsideSignature-Input. A field-only first version passed while a real agent name sat planted in the header set, so the header path is there because a must-fire control caught its absence.It also fails loudly when no fixture files or no matching fields are found, so it cannot pass vacuously if fixtures move or get emptied.
Must-fire controls, all four now caught, clean tree passes:
tenant_id-> customer name (vectors.json)X-Openmax-Agent-> real agent namekeyidinsideSignature-Input-> real kidX-Openmax-Tenant-> customer nameOne of those controls initially read as "not caught" and was itself broken — the plant was applied with a plain string replace against JSON-escaped quotes, so nothing actually changed and the check was correctly reporting a clean file. Re-run JSON-aware, it is caught. Worth stating because "the mutation didn't fire" and "the mutation never applied" look identical in a log.
npm test103/103 locally.