Repository navigation
fix(audit-required-checks): read a malformed rules answer as an API error, not as no required checks - #305
Merged
Conversation
…error (RED) Case j: the rules answer's shape check asked only for an array of objects, and the contexts filter skips a rule with no string type and reads a required_status_checks rule's checks with `[]?`. So a rule with no type, a required_status_checks rule with no list of checks, or a check with an empty context read as "no required_status_checks rule", exit 0, and the fleet audit filed the repo as clean and opened no issue. A check with no context read as a phantom named `null`, a check that is not an object crashed the filter (exit 4), and a context that was an object was printed, the answer's own bytes, into the public digest. Each of 14 malformed answers must exit 3 with stdout and stderr byte-equal to the header and one API error line naming the endpoint. A fleet run must file them as ERRORs that open the issue; a full answer and an empty list of checks must still exit 0. Four existing assertions take dotclaude's wording, "did not answer a valid list of rules". Against origin/main's validator (67e3588): 21 of 87 checks fail, the same under /bin/bash 3.2.57 and bash 5.3.15. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rror, never no required checks The rules answer's shape check asked only for an array of objects. The contexts filter keeps the rules whose type is required_status_checks and reads their checks with `[]?`, so a malformed rule read as no required checks: exit 0, filed by the fleet audit as clean, no issue opened, and the check the rule required was never validated. Port dotclaude's check (topcoder1/dotclaude#521) verbatim: every rule needs a string type, and every required_status_checks rule a list of objects with a non-empty string context. Anything else exits 3 naming the endpoint. Unlike dotclaude's copy, the error quotes none of the answer: the fleet digest is a public issue (#299). The message takes dotclaude's wording, "did not answer a valid list of rules", since "an array of rules" was wrong for an array holding a bad rule. Case j (d1df485) now passes: 87 of 87 under /bin/bash 3.2.57 and bash 5.3.15. All 13 fleet repos' live rules answers pass the new check, and a full run on this repo gives byte-identical output to the old validator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The analysis is complete. The jq validation filter has been correctly tightened on all dimensions:
No issues found. The fix closes a real silent-pass vulnerability and the new test suite (14 malformed cases + fleet integration test + 2 valid-shape acceptance tests) covers all branches. No issues found. Comprehensive tests for a well-targeted fix that closes a silent-pass bug where malformed rule responses were treated as "no required checks." Claude Review · workflow run |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This copy of
scripts/validate-required-checks.shchecked the rules answer (GET /repos/<repo>/rules/branches/<branch>, merged across pages since #303) only for being an array of objects. The weekly "Audit required checks" workflow runs this copy across the fleet.The contexts filter keeps only the rules whose
typeisrequired_status_checks, and reads their checks with[]?. So a malformed rule read as no required checks: exit 0 withno required_status_checks rule on main. The fleet audit then filed the repo as clean and opened no issue, so whatever check that rule required was never validated: a silent pass.topcoder1/dotclaude#521 (merged 2026-10-16) closed the same gap in dotclaude's copy. This PR ports that check.
What changed
The shape check (
scripts/validate-required-checks.sh) is now dotclaude's jq program, byte for byte:type;required_status_checksrule lists its checks as an array of objects, each with a non-empty stringcontext.Anything else exits 3 and names the endpoint as it was sent.
[], when no rule applies, still exits 0.fix(audit-required-checks): fail loudly on API errors, file findings apart #299's contract is unchanged:
${RULES_ANSWER:0:200}to this error; this one names only the endpoint.The message is now dotclaude's:
did not answer a valid list of rules. "an array of rules" was wrong for an array that holds a bad rule. Four existing assertions take the new wording.The CONTEXTS filter is unchanged. Once the check holds, its
[]?cannot drop anything.selftest/README.md: one added passage.Tests
All tests are in
selftest/test_audit_required_checks.sh, new case j. CI runs that file throughselftest/test_workflow_guards.py, and every expected value is hardcoded.The fixtures. Each malformed answer is clean-a's rules answer with one defect, and every
ruleset_sourcein it is a canary. There are 14:type, and a number for it;required_status_checksrule whose checks are missing (parameters: {}), a string, or an empty object, and one whoseparametersis a string;The assertions:
cmp) to the header and the one API error line. So no verdict line and no byte of the answer can appear, whichever field it comes from.ERROR: not audited (exit 3: GitHub API error)blocks naming the endpoint, and no canary. The workflow's own digest step must then open the issue.pull_requestandnon_fast_forwardrules, a check withintegration_id), and a rule with an empty list of checks.RED first. Commit 83da44c is the tests alone. Against origin/main's validator (5b880ca; the validator is unchanged since 67e3588), 21 of 87 checks fail, the same under
/bin/bash3.2.57 and bash 5.3.15:All 3 repos clean.;nulland5, a bad check before a good one (null), and an object context, which printed the answer's own bytes into the output;parameters.GREEN. 87 of 87 pass under both bashes, with identical verdict lines.
uv run pytest -q) gave787 passed, 1 skipped. It ran before the rebase onto fix(claude-review): skip the review only while the PR is still a draft #304, which adds only fix(claude-review): skip the review only while the PR is still a draft #304's own files. This test's CI wrapper passes on the rebased head.SC2016disable for the jq variable$c.Mutation check. Each of these, applied to a scratch copy, makes a named case fail:
type, array, string-context or non-empty check;parameters, which hold no canary;The one survivor, dropping the per-check
type == "object", is equivalent: a non-object check makes jq error or evaluate to false, and both take the same exit-3 branch. The independent review confirmed this over 14 check values.Measured live (read-only)
--strictmode.Codex pre-review
codex review --base. No findings.codex exec -s read-only. Each round listed the findings already declined, so that only new ones counted.CONTEXTStransport and Python's.strip()." test "validates astest, and"\n"reads as no rule, exit 0. Both copies behave the same, and the gap predates this change. The right fix is to carry contexts exactly and report a misconfigured context as a finding, not to reject it here as an API error. That is a follow-up, below.$(cat ...)drops trailing newlines, so a newline-only leak would pass. FIXED withcmp."\u0000"reads as no rule, exit 0. Declined here and folded into the same follow-up.No new findings.Independent review
An opus
code-reviewer, separate from the authoring context, reviewed the first draft. Verdict: APPROVE WITH NITS.parametersnow does.{}read as no checks.Re-check of the final tests: APPROVE. It reviewed 8857416, whose files are byte-identical to this branch's.
env -i,LANG=CandC.UTF-8), and through CI's entry point. That ispytest selftest/test_workflow_guards.py -k audit_required_checks: 1 passed, and 1 failed on the old validator.type, the body length, and a trailing space.Preflight
bb-preflight.sh topcoder1/ci-workflows claude/validator-strict-rules-shapewas run before the push.selftest/README.mdoverlap with fix(claude-review): skip the review only while the PR is still a draft #304, in another entry. A 3-way merge from fix(claude-review): skip the review only while the PR is still a draft #304's real merge base gave 0 conflicts in either order.Known limitations, not changed here
ruleset_source_typeon arequired_status_checksrule makes the filter'sascii_downcaseerror, so the run exits 4 ("validator crashed") instead of 3. That is loud, not silent. dotclaude's copy behaves the same.Codex rounds: 6, the cap. Rounds 1 and 2 were native reviews; rounds 3 to 6 were adversarial. Round 6 found nothing new; round 5's only finding was a variant of round 3's declined transport gap. So the loop stopped at the cap, not on two clean rounds in a row. Of the 6 findings, 2 were fixed (both test gaps, each now killing a mutant) and 4 were declined into the follow-up: the transport gap and its test coverage.
Auto-merge rationale: manual click-merge. PRs to topcoder1/ci-workflows are always manual merge, and this validator decides the fleet audit's verdicts, which are posted publicly.
🤖 Generated with Claude Code