Skip to content

fix(selftest): parse the workflow audits, and sweep the last .yml-only discovery sites - #719

Merged
d-morrison merged 14 commits into
mainfrom
fix/yaml-workflow-discovery-716
Aug 28, 2026
Merged

fix(selftest): parse the workflow audits, and sweep the last .yml-only discovery sites#719
d-morrison merged 14 commits into
mainfrom
fix/yaml-workflow-discovery-716

Conversation

@d-morrison

@d-morrison d-morrison commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Closes #716. Closes #720.

Three sites still globbed *.yml only, so a .yaml workflow bypassed each
silently: run-permissions-docs-tests.py's read-only classification, and
_selftest.yml's SUBMODULES_TOKEN and SHA-pin audits. The sibling of the
job-guard gap #712 fixed.

What changed

Both shell audits moved out of _selftest.yml into Python scripts that walk
parsed YAML rather than matching workflow text, and all four consumers of
the discovery rule -- these two, the permissions-docs suite, and the job-guard
suite #712 fixed separately -- now share one workflow_discovery module.

Parsing rather than grepping was not gold-plating; it is what closes #720. A
line-anchored ^\s*uses: sees the continuation form and not - uses: ...,
which left three real references exempt from the pin audit (all in
altdoc-multiversion-docs.yml, measured 2026-08-28 against main at
7719d04). Widening the anchor is not the fix either: two of the five lines it
newly matches are _selftest.yml's heredoc-written fixture workflows, which
are text inside a run: block. A parsed walk sees both spellings and cannot
reach heredoc content.

Review history

Eight adversarial review rounds ran before this was pushed. Each found real
defects; all are addressed, each with a test and a confirmed mutation:

Round Findings
1 grep exit 2 read as "clean"; the audits themselves untested; a test ignoring exit status
2 the - uses: blind spot; mapfile needs bash 4, macOS ships 3.2
3 malformed jobs/steps/with shapes skipped; startswith("Morrison-Lab/gha") exempting gha-evil; SUBMODULES_TOKEN matching NOT_SUBMODULES_TOKEN
4 non-string step-level uses: skipped; the token audit's message overstating its scope
5 eager PyYAML import; docker://...@sha256: read as unpinned; stale docstrings; "five real references" was wrong -- it is three
6 job-level with:/secrets: unwalked (a coverage regression against the old grep); crossed pin forms accepted
7 every scalar block skipped, not just secrets: inherit
8 input keys matched case-sensitively, so Token: bypassed the audit

One finding was rebutted rather than addressed: round 4 asked to scope the
token audit to actions/checkout. Narrowing a security audit to one action
name would miss a fork, a wrapper composite, or a rename, and the errors are
asymmetric -- a false negative ships a broken checkout to a consumer, a false
positive costs a PR comment. The misleading message was corrected instead, and
a test now pins the breadth so narrowing it later is a decision rather than a
silent regression.

Tests

run-workflow-audit-tests.py, 48 cases. The ones to keep if it is ever
trimmed are the .yaml-carried violations -- this repo's tree is all .yml,
so a discovery regression leaves every other check green -- and the refusals:
an unparsable or structurally malformed workflow is an error, not a clean
file.

Eighteen mutations were confirmed to turn the suites red rather than assumed
to.

…HA-pin audits

Closes #716.

Three sites still globbed `*.yml` only, so a `.yaml` workflow bypassed each
silently: `run-permissions-docs-tests.py`'s `read_only_workflows` discovery,
and `_selftest.yml`'s SUBMODULES_TOKEN and SHA-pin audits.

Discovery for the two shell audits is factored into a shared
`list-workflow-files.sh` rather than duplicated, and it fails closed on an
empty or missing directory --- a caller that received no paths would grep no
input and pass having examined nothing.
CLAUDE.md's Tests section gains the `run-list-workflow-files-tests.sh`
paragraph, the command-substitution constraint the two audits depend on, and
the two new permissions-docs cases. Changelog fragment added.
Addresses all three findings from the pre-push adversarial review (codex).

1. Both audits read grep's exit status as three values, not two. The inline
   forms used grep as an `if` condition and ended a pipeline in `|| true`,
   so an unreadable or vanished file reported clean --- contradicting the
   fail-closed guarantee this PR adds.
2. The audits themselves are now exercised offline, each against a fixture
   whose violation lives in a `.yaml` file. Testing only the discovery
   helper left both consumers free to revert to a `*.yml` glob with every
   check still green, since this repo's real tree carries no .yaml workflow.
3. `expect_output` asserts the helper's exit status, not only its text.

Files gha#720 for the pre-existing `- uses:` blind spot the pins fixtures
surfaced; pinned as current behaviour rather than widened, since the
one-character widening false-positives on a heredoc fixture.
Round 2 of the pre-push adversarial review raised two findings; both dissolve
by walking parsed YAML rather than matching workflow text.

1. The SHA-pin audit was blind to the `- uses:` list form, so five real
   references were exempt (gha#720). Deferring that was wrong for a check
   whose success line asserts every action is pinned. A parsed walk sees both
   spellings, and cannot see a `uses:` written inside a `run:` heredoc --
   which is why widening the regex was not the fix.
2. `mapfile` needs bash 4, and macOS ships 3.2 at /bin/bash, so both shell
   audits exited 127 for a maintainer running them directly.

Discovery is now one `workflow_discovery` module shared by both audits and by
the permissions-docs suite, rather than a glob repeated three times. An
unparsable workflow is an error rather than a clean file, which is the same
distinction the shell version drew from grep's exit 2.
CLAUDE.md's Tests section now describes the parsed-YAML shape, why a line
anchor could see neither the `- uses:` list form nor the difference between a
real reference and heredoc text, and the seven confirmed mutations. Changelog
gains the #720 entry alongside #716's.
…tions

Round 3 of the pre-push adversarial review raised three findings.

1. The parsed walk skipped an absent or wrongly-typed `jobs`, `steps`, `with`,
   or step entry, so an empty or structurally invalid workflow reported clean
   having been walked not at all -- the parsed-walk version of the grep exit-2
   conflation round 2 fixed. Each shape is now an error.
2. `startswith("Morrison-Lab/gha")` exempted `Morrison-Lab/gha-evil` from
   SHA-pinning: a different repository, under an exemption meaning "ours".
3. `SUBMODULES_TOKEN in value` matched `NOT_SUBMODULES_TOKEN`, blocking a
   valid workflow. The test is now the identifier on word boundaries.

Eleven mutations confirmed red across the suite, four of them new here.
… count

Round 5 of the pre-push adversarial review raised four findings, all valid.

1. Moving the YAML import to module load broke `--help` on a machine without
   PyYAML, undoing the lazy-import guarantee the permissions-docs suite had.
   It is imported inside `load_workflow` again, so the discovery half stays
   importable without it.
2. The pin test accepted only a 40-character Git commit, so
   `docker://image@sha256:<digest>` -- a form GitHub supports, and as
   immutable as a commit -- read as unpinned. A digest-pinned docker ref now
   passes; a tag-pinned one still fails.
3. The token audit's module docstring and its selftest step name still
   described a checkout-only check after round 4 deliberately kept it broad.
4. "five real references" was wrong. Five `- uses:` LINES were invisible to
   the old anchor; two of them sit inside `_selftest.yml` heredocs and are
   not references. The real count is three, all in
   altdoc-multiversion-docs.yml. Corrected in the script, CLAUDE.md, and the
   changelog; gha#720 carries the same error and is corrected there too.

Thirteen mutations confirmed red.
Round 6 of the pre-push adversarial review raised two findings, both valid.

1. The parsed token audit visited only `steps[].with.token`, while the regex
   it replaced matched any line-leading `token:` and so also covered a
   reusable-workflow caller's job-level `with:` and `secrets:` blocks. That is
   a coverage regression wearing a refactor's clothes -- the text scan's reach
   was never written down, so nothing flagged the loss. Both blocks are walked
   now; `secrets: inherit` is a string and is skipped, not refused.
2. The pin test accepted a commit SHA or an image digest for every reference,
   so `actions/checkout@sha256:...` and `docker://alpine@<40hex>` both read as
   pinned though neither resolves. The reference is classified first.

Fifteen mutations confirmed red.
…herit

Round 7's single finding: skipping any string-valued job-level `with:` or
`secrets:` was too broad. `secrets: inherit` is the one legitimate scalar
there; a scalar `with:`, or a `secrets:` naming anything else, is malformed,
and skipping it left a block the audit never examined reported as clean.

A `token:` whose value is a list or mapping is refused on the same reasoning,
while a number or boolean -- which an input may legitimately be -- is not.
Round 8 raised two findings, both valid.

1. GitHub's runner resolves an action's inputs case-insensitively, so a
   `Token:` was the same input while reading as a different key here -- a
   one-keystroke bypass of the whole audit. Both the job-level and step-level
   lookups normalize case now. `Submodules-Token` is still a different key,
   pinned by its own case.
2. CLAUDE.md still said widening the old regex produces "one new hit" two
   paragraphs after correcting the count to five lines / three references.
…e shared module

Four copies of the same glob existed, and #712 and #716 each fixed the drift in
one of them. The job-guard suite now imports workflow_discovery like the other
three, so there is one copy left to keep right.
@d-morrison d-morrison changed the title fix(selftest): discover .yaml workflows in the permissions-docs and SHA-pin audits fix(selftest): parse the workflow audits, and sweep the last .yml-only discovery sites Aug 28, 2026
@d-morrison
d-morrison marked this pull request as ready for review August 28, 2026 19:23
lint-markdown's list-item splice detector flags a new `- ` item that follows
a continuation line with no blank line between them.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

…ing "checkout"

Addresses both findings from the review of 4a0e9df.

1. The prose said "Eighteen" over a 16-item list, and the real number is 19.
   It is now a numbered enumeration so the count is checkable against the list
   rather than asserted over it -- which is what this file's own rule about
   completeness claims asks for, and which the previous wording failed.
2. The token audit's closing message still said the token "must not gate a
   checkout", after the audit was deliberately kept broad. It now says no
   action may take the secret as a general-purpose token, and names the
   checkout remedy only for the checkout case.
@github-actions

Copy link
Copy Markdown
Contributor

Claude finished reviewView run


All 48 tests pass at the PR head, and the closing message now correctly reads "Where the step is a checkout, use..." rather than unconditionally describing the flagged step as a checkout — confirming both prior-round findings are fixed.

Review of #719 (round 2)

This round's diff (4a0e9df2..6cb9567d) touches only three files: audit_workflow_token_usage.py (the closing error message), CLAUDE.md (the mutation-count paragraph), and the changelog fragment (blank-line separation between list items) — exactly the two findings from the prior round (posted 2026-08-28T19:32:30Z, linked here) plus a docs formatting touch-up.

Verification performed

  • Extracted the PR-head blobs via git cat-file/git show (the working tree's .github/workflows/ is restored to the default branch per this session's gha#598 policy) and ran run-workflow-audit-tests.py directly: all 48 cases pass.
  • Programmatically counted the enumerated mutation list in CLAUDE.md: it now runs 1. through 19. with no gaps or duplicates, matching the prose's "Nineteen mutations" — this fixes finding 1 from the prior round (previously "Eighteen" was claimed against a 16-item list).
  • Traced each of the 19 enumerated mutations against a specific test case in run-workflow-audit-tests.py/run-permissions-docs-tests.py (e.g. claude-code-review: degrade gracefully when inline-comment tool is absent #16 secrets: inherit refusal ↔ the inherit fixture, claude-code-review: allowlist inline-comment tool so review findings anchor to lines #17 "skip of every scalar block" ↔ bad_scalars, claude-code-review: fall back to summary on inline-comment permission errors #18 container-valued token:container_token, ci: harden Claude caller stubs (@claude guard, dispatch input type) #19 case-sensitive key match ↔ the Token/TOKEN/cased_job fixtures). All map cleanly to a distinct, real test.
  • Built a throwaway fixture exercising audit_workflow_token_usage.py's non-checkout violation path and confirmed the closing message now reads "Where the step is a checkout, use the checkout-submodules composite's 'submodules-token:' input instead" — no longer unconditionally implying the flagged step is a checkout. This fixes finding 2 from the prior round.
  • Spot-checked the changelog fragment's diff: it adds blank lines between the fragment's bullet list items, matching the commit's stated intent (fix(changelog): blank-line-separate the fragment's list items) and this repo's own changelog conventions.

Findings

None. Both issues raised in the prior round are resolved by this diff, and I found no new issues in the changed lines — the code logic (audit_workflow_action_pins.py, audit_workflow_token_usage.py, workflow_discovery.py, _selftest.yml) is unchanged from the previously-reviewed commit; only the message string, the CLAUDE.md prose, and the changelog fragment's formatting changed in this round.

Verdict

Ready for merge — the prior round's "Needs minor work" status is resolved: the mutation-count documentation defect is fixed (19 claimed, 19 enumerated, all verified against real tests) and the token-audit error message no longer overstates its scope to non-checkout violations. No new issues found in this round's diff.

Reviewed commit: 575cf86

@github-actions

Copy link
Copy Markdown
Contributor

💰 Cost: $0.7991 (review) — run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant