feat(controls): ship checkoutMustNotPersistCredentials (ISSUE-307) - #293
feat(controls): ship checkoutMustNotPersistCredentials (ISSUE-307)#293stephrobert wants to merge 2 commits into
Conversation
The pin-refs job was the last checkout in the repo without persist-credentials: false — the app token landed in .git/config and survived for the rest of the job, which the incoming checkoutMustNotPersistCredentials control (ISSUE-307, PR #293) rightly flags in our own self-scan (94.7% < 100% threshold). Supply the token at push time through an env-based credential helper instead, the same pattern the homebrew job already uses: .git/config only stores the helper script, never the token. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
## [0.3.87](v0.3.86...v0.3.87) (2026-07-06) ### 🐛 Bug Fixes * **collect:** contain local CI-config reads and skip symlinked Dockerfiles ([ef9c275](ef9c275)) * **gitlab:** harden local include resolution ([729e259](729e259)) ### ♻️ Refactoring * **render:** sanitize repo-derived text and bound resource use ([17ed05e](17ed05e)) * **score:** resolve the score endpoint from CLI/env only ([0a89351](0a89351)) ### 👷 CI/CD * **grype:** install pinned grype by checksum, drop scan-action ([a275cdf](a275cdf)), closes [#294](#294) * pin runtime tool installs to immutable versions ([a203b7f](a203b7f)) * **release:** pin v0.3.86 refs [skip ci] ([101cd21](101cd21)) * **release:** stop persisting credentials in the pin-refs checkout ([8f0f261](8f0f261)), closes [#293](#293) * **scorecard:** document the action's mutable-image gap ([61c8cdd](61c8cdd))
Promote the artipacked control off the GitHub bench. It flags any `actions/checkout` step that omits `persist-credentials: false`, leaving the GITHUB_TOKEN in .git/config where a later step can exfiltrate it. The control's rule was complete but benched for lack of fixtures and, as it turned out, config plumbing: `enabled: false` was silently ignored and the control was absent from `config view`. Completed the wiring to match its 20 shipping peers: - remove from benchedControls (registry.go) so the Rego file loads; - add the EnabledOnlyControlConfig struct field, valid-key entry, catalog entry and DisabledControlNames gate so the toggle works; - document the control in .plumber.yaml (regenerates default.yaml); - add it to the config init wizard menu and starter defaults; - extend the ISSUE-307 testdata with the explicit `persist-credentials: true` violation and the quoted `"false"` clean form, and cover all four cases in TestIssue307_Artipacked. Verified: fires 12 findings on the dirty GitHub fixture, honours `enabled: false`, and leaves GitLab analysis byte-identical.
3610586 to
478587c
Compare
…SSUE-310 high) A persisted checkout credential is latent hygiene until `.git` is actually packed into a downloadable artifact. Split the single high ISSUE-307 into two graded tiers under the same control: - ISSUE-307 (low) — checkout persists the token but nothing packs `.git`. A heads-up to add `persist-credentials: false`; the fork-controlled-code route stays owned by ISSUE-802 / ISSUE-804. - ISSUE-310 (high) — the same persisted credential AND a later upload-artifact of a `.git`-inclusive path (`.`, workspace root, or a path naming `.git`). The token is exfiltrable — the demonstrable ArtiPACKED leak. Scoring reads severity from the code registry, so two severities need two codes; both map to checkoutMustNotPersistCredentials (no new config key). Fixtures cover the A∧B high case, the A-only low case, an upload scoped to a non-.git path (stays low), and credentials-disabled (clean). Rationale: on 46 real repos the broad high rule fired on 35; the A∧B-only shape fired on 0. Grading keeps the recall while reserving high for the leak that actually reproduces.
Update: graded by exploitability (ISSUE-307 low + ISSUE-310 high)Reworked so severity tracks whether the token is actually exfiltrated, rather than flagging every persisted-credential checkout at high. A persisted checkout credential is latent hygiene until
Scoring reads severity from the code registry per code, so two severities need two codes; both map to the same control (no new config key). Why grade instead of narrowing to A∧B onlyI measured both models against 46 real repos (grafana, n8n, strapi, awx, amazon.aws, python-gitlab, posting, …):
Uploading Validated: a real dirty repo → 6 × ISSUE-307 (low); a synthetic checkout-then- |
Closes #187.
Summary
Promotes the
artipackedcontrol (ISSUE-307, severity high) off theGitHub bench. It flags any
actions/checkoutstep that does not setpersist-credentials: false, leaving theGITHUB_TOKENin.git/configwhere a later step can exfiltrate it.
While shipping it I found the control was benched for more than missing
fixtures: it had no config plumbing.
enabled: falsewas silentlyignored and the control never appeared in
config view. This PR completesthe wiring so it behaves exactly like its 20 shipping GitHub peers.
Changes
checkoutMustNotPersistCredentialsfrombenchedControls[ProviderGitHub]so the Rego file loads.EnabledOnlyControlConfigstruct field, valid-key entry,control/catalog.goentry andDisabledControlNamesgate — soenabled: falseactually disables it and it shows inconfig view..plumber.yaml(regenerates the embedded default) and add it to theconfig initwizard menu + starter defaults.ISSUE-307fixtures with the explicitpersist-credentials: trueviolation and the quoted"false"clean form;TestIssue307_Artipackednow covers all four cases.Acceptance criteria (#187)
benchedControls[ProviderGitHub].policies/testdata/fixtures (4 cases: default checkout, explicittrue, booleanfalse, quoted"false").controls:block of.plumber.yamlwithenabled: true.ISSUE-307entry present indocs/GITHUB_ISSUES.md.make build && make test && make lintgreen.Verification
Verified on real GitHub Actions repositories: fires as expected (e.g. 88
findings on a large workflow set), honours
enabled: false, and thedetection agrees 1:1 with the reference
artipackedimplementation acrossn8n, crowdsec, bunkerweb and centreon. No false positive observed: the
control never flags a checkout that already sets
persist-credentials: false. GitLab analysis is byte-identical before/after (GitHub-only change).