Skip to content

fix(ci): the hardening gate was blind to the commonest way to write a step - #129

Merged
marcohost33-maker merged 4 commits into
mainfrom
fix/workflow-hardening-gate
Aug 29, 2026
Merged

marcohost33-maker merged 4 commits into
mainfrom
fix/workflow-hardening-gate

Conversation

@marcohost33-maker

Copy link
Copy Markdown
Owner

Found by an adversarial audit of this repository's own guards, then reproduced as BLIND -> CAUGHT pairs before anything was changed.

Four ways an unsafe workflow passed the gate

.github/scripts/check_workflow_hardening.py enforces AGENTS.md section 4 on every workflow here. It reported green while all four of these walked through:

The hole Why it passed
1 Unpinned action in list form (- uses:) USES_RE was ^\s*uses:, which does not match a list item
2 docker://org/img:latest docker:// was exempt outright, mutable tag included
3 permissions: write-all Only the presence of a permissions block was checked
4 pull_request_target waived in a # comment Substring test over the raw file text

Hole 1 is the serious one: 7 of 27 refs in this tree were invisible to the gate, every actions/checkout among them, in ci-python-local.yml, ci-qutip.yml, ci.yml, encoding-guard.yml and pypi.yml. They are correctly pinned today — by discipline, not by this check. A PR moving one of them to @main passed.

Hole 3 is the one worth pausing on: a declaration of total access counted as evidence of least privilege, because the check asked whether the word appeared, not what it granted.

Also removed: the github.com/ exemption. uses: does not accept that prefix and a GitHub owner name cannot contain a dot, so no such org can exist — dead code. It was the subject of a CodeQL incomplete-substring alert, which is the only reason this file came under scrutiny. CodeQL flagged the harmless line and missed the regex two lines above it that actually broke the gate.

Two zizmor suppressions whose reasons no longer hold

  • impostor-commit was disabled because the audit crashed on the private cross-repo pin coworkerz-ci. That pin is gone — the only two occurrences of the name left in the tree are the comments explaining the suppression. It is the one audit that verifies a 40-character SHA actually exists upstream, so the entire pinning strategy was unverified without it. Counter-checked before re-enabling, with a token so the online audits really ran: No findings to report, all 25 pins authentic.
  • advanced-security: false was justified with "private repo without GHAS". This repository is public — measured, not assumed. While that flag was false, zizmor findings were never uploaded to Code Scanning: absent for want of an upload, not for want of findings.

The fix needed a second pass

Closing hole 2 initially broke digest-pinned containers: _check_uses_pin splits the ref at @ before consulting _is_third_party_uses, so a digest test there ran on the truncated string, and the digest then failed the 40-char git-SHA rule. Container digests and git SHAs are different pin shapes and are now checked separately.

That regression was caught by an over-correction control, not by review — which is the argument for the tests below.

Tests

The gate had none. It was the guard nobody guarded, which is why it could report green for four classes of unsafe workflow.

Added as pairs: each unsafe workflow next to a control that must still pass. Rejection alone would be satisfied by a gate that fails everything, which is exactly as useless as one that passes everything. Plus a digest-pinned container that must remain allowed, and a check that the real tree passes its own gate.

Evidence: 7 passed. Four BLIND -> CAUGHT pairs measured against the pre-fix script, with a clean workflow green under both versions.

Not fixed here

quality contract is not a required status check on main. Only test (3.10-3.14) and qutip-cross-check are. Five security workflows run and none of them block a merge, so the comment in zizmor.yml claiming the step "GATET die CI (rot = blockt merge)" is factually wrong. Making it required is the right next step — but after this fix lands, not before, or a blind gate becomes mandatory.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

marcohost33-maker and others added 2 commits August 29, 2026 10:06
… step

Found by an adversarial audit of this repo's own guards, then reproduced as
BLIND -> CAUGHT pairs. Four ways an unsafe workflow passed the gate:

1. ``USES_RE`` was ``^\s*uses:``, which does not match the list form
   ``- uses:``. Seven of 27 refs in this tree were invisible to the gate,
   every ``actions/checkout`` among them. They are pinned today by
   discipline, not by this check: a PR moving one to ``@main`` passed.
2. ``docker://`` was exempt outright, so a mutable third-party tag
   (``docker://org/img:latest``) was waved through.
3. ``permissions`` was checked for PRESENCE only, so ``permissions:
   write-all`` -- a declaration of total access -- counted as evidence of
   least privilege.
4. The ``pull_request_target`` rationale was a substring test over the raw
   file, so writing the waiver's NAME inside a ``#`` comment satisfied it.

Also removes the ``github.com/`` exemption: ``uses:`` does not accept that
prefix and a GitHub owner name cannot contain a dot, so no such org can
exist. It was dead code, and it was the subject of a CodeQL
incomplete-substring alert -- which is how the file came under scrutiny at
all. CodeQL flagged the harmless line and missed the regex two lines above
that actually broke the gate.

Fixing (2) needed a second pass: ``_check_uses_pin`` splits the ref at ``@``
before consulting ``_is_third_party_uses``, so a digest test there ran on the
truncated string and a digest-pinned container was then rejected by the
40-char git-SHA rule. Container digests and git SHAs are different pin
shapes and are now checked separately. That regression was caught by an
over-correction control, not by review.

The gate had no test at all -- it was the guard nobody guarded. Added, as
pairs: each unsafe workflow next to a control that must still pass, plus a
digest-pinned container that must remain allowed, plus a check that the real
tree passes its own gate.

Evidence: 7 passed. Four BLIND -> CAUGHT pairs against the pre-fix script,
with a clean workflow green under both versions (so the gate did not simply
become stricter about everything).

Not addressed here, tracked separately: ``quality contract`` is not a
required status check, so this gate does not block a merge even when red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
Both suppressions were justified when written and are now justified by
nothing measurable.

`impostor-commit` was disabled because the audit crashed on the private
cross-repo pin `coworkerz-ci`. That pin is gone: the only two occurrences of
that name left in the tree are the comments explaining the suppression. It is
the one audit that checks whether a 40-character SHA actually exists
upstream, so with it off the entire pinning strategy was unverified.
Counter-check before re-enabling, with online audits active and a token so
they really ran: `No findings to report`, all 25 pins authentic.

`advanced-security: false` was justified with "private repo without GHAS".
The repository is public -- measured, not assumed. While that flag was false,
zizmor findings were never uploaded to Code Scanning: they were absent there
for want of an upload, not for want of findings.

Neither change alters what zizmor checks; they stop discarding what it
already found.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

Two mistakes of mine, both caught by CI rather than locally, both from
running a piece of the chain instead of the chain.

1. The zizmor action refuses to start with both `advanced-security: true`
   and `annotations: true`. I switched the first and left the second. Running
   zizmor directly does not exercise the action's own option validation, so
   the local check could not have caught it -- the action wrapper is part of
   the contract and was never run.
   `annotations: false` is the right resolution rather than reverting:
   inline annotations vanish with the run, a Code Scanning alert persists and
   can be triaged. Uploading them is the point of enabling advanced-security.

2. An extraneous `f` prefix on a string literal with no placeholder
   (`tests/test_workflow_hardening_gate.py`). Ruff catches it; I had run only
   pytest locally, not `ruff check src tests benchmarks`.

Local chain now run as the job runs it: Lint `All checks passed!`,
7 passed, and the gate green on the real tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

AGENTS.md Definition of Done item 7 requires a CHANGELOG entry or an
explicit rationale for omitting one. Recording the four fail-open holes,
the two re-enabled zizmor audits, the over-correction that closing the
docker:// hole first caused, and the known gap that quality contract is
still not a required status check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
@marcohost33-maker
marcohost33-maker merged commit c18a48d into main Aug 29, 2026
16 checks passed
@marcohost33-maker
marcohost33-maker deleted the fix/workflow-hardening-gate branch August 29, 2026 09:47
marcohost33-maker added a commit that referenced this pull request Aug 29, 2026
…del vocabulary

CHANGELOG entries appended to the existing [Unreleased] Fixed section (which
already carries both #127 and #129 items -- added, not reordered), including
the explicit correction of my earlier report that the Prony seed change was
not a regression: the reviewer's counter-example holds as a class.

layers-and-taxonomy.md still documented residual_model as a two-value field.
That line was found by turning the very defect class of finding 2 -- a label
asserting what should have happened rather than what did -- against my own
result, one layer up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsDFcKo97HuF5ZLjxa3rRa
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.

2 participants