Repository navigation
ci: read-only top-level workflow tokens; never re-attempt research-v1 (Scorecard #173-#178) - #197
Merged
Merged
Conversation
Code-scanning alert #178 (OpenSSF Scorecard Token-Permissions, topLevel): scholarly-release-v1.yml granted contents, id-token and attestations write at the workflow top level. A top-level grant applies to every job, including any job added later, so it is never least privilege. kernel-freshness.yml had the same defect with issues: write; Scorecard does not report that one, but it is the same root cause. Both workflows have exactly one job. The write grants move onto that job (publish / freshness), and the top level becomes contents: read. Each job's effective permissions are unchanged; only the default for any future job is narrowed. Add scripts/check-workflow-permissions.sh, run in CI by scripts/check-workflow-permissions_test.sh: - Every workflow must declare read-only top-level permissions. - write-all is rejected. - Every job-level write must be listed, with a justification, in scripts/workflow-write-permissions.allow. - Stale allowlist entries fail, so the list cannot become a blanket exemption. The suite fails against the workflows on main (802d515) and passes after this change. It covers flow mappings, quoted values, an omitted block, shorthands, an unlisted job grant and a grant moved to another job. Signed-off-by: ErenAri <erenari27@gmail.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 11
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
scholarly-release-v1 runs on every push to main that changes its own file. research-v1 was published once, from 141c491 on 2026-09-19 (run 35456618576). Any later edit, including the permission fix in this PR, re-ran the publish job with a contents:write token. Its own refusal step stopped it, but the run went red. A read-only `state` job (contents/attestations read) now runs scripts/research/research-v1-release-state.sh before anything else, and the write-scoped `publish` job runs only on `needs.state.outputs.publish == 'true'`: - publish=true only when neither the tag nor the release exists AND the run is the pinned commit 141c491. That is the original first publication, which can no longer happen. - publish=false only when the published release is verified intact. The annotated tag (direct and peeled refs) peels to 141c491. The release is non-draft and non-prerelease with exactly the four assets. archive-lock.json is byte-identical to the reviewed lock. The manifest and payload match the lock. RELEASE-CHECKSUMS.txt covers exactly the other three files and verifies. All four assets carry a scholarly-release-v1.yml attestation for 141c491 on refs/heads/main. The run ends green with no write token issued. - Every other state fails closed and never silently skips: a missing release run from another commit (which would create a different research-v1), a partial publication, a mismatch, or a lookup error. scripts/check-release-consistency.sh now requires this gate: the pinned commit, the state script, `needs: state`, the exact `if:` and a read-only state job. It rejects the ungated workflow from cf7a20a. Tests: - scripts/research/test-release-state-v1.sh (real git remote, stubbed gh) has 30 checks, each asserting the failure reason. - Every guard mutation is caught except dropping the payload size check. That one is equivalent, since the payload SHA-256 binding implies size. - Against the live release, the script returns publish=false from 802d515 and fails for a wrong pinned commit. docs/workflow-token-permissions.md records the Scorecard v5.5.0 scoring rules, reproduces alerts #173-#178 locally on 802d515 (0 SARIF results on this tree, score 10), and documents the five required job-level write grants with the step that uses each and its gate. Signed-off-by: ErenAri <erenari27@gmail.com>
research/archive/v1/archive-plan.json includes all of scripts/research/ in the immutable research-v1 payload. Adding the state guard and its test there grew the rebuilt archive from 668 to 670 files. The pinned archive-lock.json then no longer matched, and "Build and verify archival payload" (research-archive-v1.yml) failed on 1daeb15, as it should. Move them to scripts/research-v1-release-state.sh and scripts/research-v1-release-state_test.sh and update every reference: the scholarly workflow, ci.yml, check-release-consistency.sh and the docs. A local rebuild from the pinned source artifacts again yields the committed lock exactly: 668 files, manifest 6ed6d38d, payload 143a8e8a. test-archive-v1.sh passes. Signed-off-by: ErenAri <erenari27@gmail.com>
pkg/scorecard/sarif.go (shouldAddLocation) emits a warning as a SARIF result only when the check is below the policy minimum, details are enabled, and the warning has a file location; an inconclusive check is skipped too. Found by independent review of #197. Signed-off-by: ErenAri <erenari27@gmail.com>
ErenAri
marked this pull request as ready for review
October 14, 2026 18:39
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.
DRAFT — security remediation. Not authorized to merge; needs operator approval of the exact head SHA.
Head:
1c6926b2651bc21551658fa17c13b939a7d1da6b(4 commits on main802d515; the 4th is a docs-only precision fix)Alerts
All 6 open code-scanning alerts on
maincome from OpenSSF Scorecard v5.5.0, ruleTokenPermissionsID(high,supply-chain/local), not CodeQL. The Dependabot and secret-scanning queues are empty. No alert touches the production Go boundary enforced byscripts/check-production-code-scanning.sh, which is unchanged.scholarly-release-v1.yml:10topLevelcontents: writepromote-release.ymlpromote: jobLevelcontents/packageswriterelease-artifacts.ymlstage-draft-release: jobLevelcontentswriterelease-candidate-canary.ymlexternal-consumers: jobLevelactionswriterollback-drill.ymlrollback: jobLevelpackageswriteDoes moving #178 to job level close it, or just move it?
It closes the finding. In Scorecard v5.5.0 (
checks/evaluation/permissions.go):contents/packages/actionswrite dropsToken-Permissionsto 0;pkg/scorecard/sarif.goemits no results for a check that meets the policy minimum. Below the minimum, every warning becomes an alert stamped with the check score. That is why #178 also turned the five required job-level grants into "score is 0" alerts.Verified with the official v5.5.0 binary (checksum-verified) in local mode, Token-Permissions, policy minimum 10:
802d515The job-level warnings still appear in
--show-details(including the newscholarly-release-v1.ymlpublishgrant), but they produce no alerts. Expected after merge: the next Scorecard upload on main stops reporting all six, so GitHub should mark #173–#178 fixed. That is only confirmed once the post-merge scan has run.docs/workflow-token-permissions.mddocuments the five required grants separately: the step that uses each, and its gate. None are dismissed or suppressed.Fixes
deps: bump the go-deps group across 1 directory with 2 updates #178 (commit
cf7a20a). Inscholarly-release-v1.ymlandkernel-freshness.yml(same root cause, unreported), the top level becomescontents: readand the grants move onto the single job.scripts/check-workflow-permissions.shrequires read-only top levels and a justified allowlist (scripts/workflow-write-permissions.allow, 27 entries) for every job-level write.write-all, stale entries and duplicate keys, scans.yml/.yamlcase-insensitively, and parses with a SafeLoader.No re-attempt of research-v1 (commits
1daeb15+acff42c).scholarly-release-v1runs on every edit to itself, so this PR alone would have re-run the publish job with acontents: writetoken. Its own refusal step would have stopped it, with a red run. Now a read-onlystatejob (contents/attestations: read,persist-credentials: false) runsscripts/research-v1-release-state.shfirst. The write-scopedpublishjob needsneeds.state.outputs.publish == 'true'.publish=trueonly when neither tag nor release exists and the run is the pinned commit141c491. That is the original first publication, which can no longer happen.publish=false(green, no write token) only when every check passes:141c491;archive-lock.jsonis byte-identical to the reviewed lock;RELEASE-CHECKSUMS.txtlists exactly the other 3 files and verifies;scholarly-release-v1.ymlattestation for141c491onrefs/heads/main.check-release-consistency.shnow requires this gate (pinned commit, state script,needs: state, exactif:, read-only state job).publish=falsefrom802d515; fails with a wrong pinned commit.Frozen-archive catch (commit
acff42c).1daeb15, the guard and its test lived inscripts/research/.archive-plan.jsonputs that whole directory into the immutable research-v1 payload, so the rebuilt archive grew from 668 to 670 files andresearch-archive-v1("Build and verify archival payload") correctly failed.scripts/. A local rebuild from the pinned source artifacts again matches the committedarchive-lock.jsonexactly (668 files, manifest6ed6d38d…, payload143a8e8a…), andtest-archive-v1.shpasses.scripts/research/**and similar paths, and the net PR diff no longer touches them, so it may not re-run on this head. It is not a required check.Tests
main's workflows.cf7a20a.check-workflow-permissions_test.sh: 20 fixtures, each failing for its stated reason.research-v1-release-state_test.sh: 30 checks, real git remote, stubbed gh.check-release-consistency_test.sh: 5 gate mutations, each rejected for the gate.go vet,go test ./...,go test -race ./...,govulncheck,make check-release-consistency,production-code-scanning_test, runner-pin tests, strictcheck-canary-pins,make production-tech-checkall PASS.golangci-lintcan't run because the installed binary is built with go1.25, below the repo's 1.26.9. No Go files changed; the CIgolangci-lintcheck covers it.Independent security review
Two passes, no blockers.
gh release viewprints exactlyrelease not foundfor a missing release;publish=line reaches$GITHUB_OUTPUT;success()is implied on the publishif:;*/./checksum cases.attestations: readletsgh attestation verifywork withGITHUB_TOKEN. Today'spromote-releaserun 38604911768 verified attestations with exactlyactions: read, attestations: read. If it were insufficient, the state job fails closed (red), it does not publish.Remaining risks
scholarly-release-v1run on the merge commit should be green: the state job verifies, publish is skipped. If that verification ever fails, the run is red by design and nothing is written.check-release-consistency.shchecks that the state script is invoked, not its contents. Weakening the script relies on review and CI tests.release-candidate-canary.yml(release: v0.3.8 stable metadata and maintenance readiness evidence from rc.2 (DRAFT, NOT READY) #176) and all release, promotion and drill workflows are unchanged, as are environments and protections.Signed-off-by: ErenAri erenari27@gmail.com