diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index c3b8fa5db..d63740f37 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -77,7 +77,7 @@ jobs: id: osv_base continue-on-error: true timeout-minutes: 8 - uses: google/osv-scanner-action/osv-scanner-action@a82132c0bd6c7261ffcb78e754c46c70ab57ad9a # v2.3.8 + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.5.0 with: scan-args: | --format=json @@ -95,7 +95,7 @@ jobs: if: steps.osv_base.outcome == 'failure' continue-on-error: true timeout-minutes: 4 - uses: google/osv-scanner-action/osv-scanner-action@a82132c0bd6c7261ffcb78e754c46c70ab57ad9a # v2.3.8 + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.5.0 with: scan-args: | --format=json @@ -116,7 +116,7 @@ jobs: id: osv_head continue-on-error: true timeout-minutes: 8 - uses: google/osv-scanner-action/osv-scanner-action@a82132c0bd6c7261ffcb78e754c46c70ab57ad9a # v2.3.8 + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.5.0 with: scan-args: | --format=json @@ -134,7 +134,7 @@ jobs: if: steps.osv_head.outcome == 'failure' continue-on-error: true timeout-minutes: 4 - uses: google/osv-scanner-action/osv-scanner-action@a82132c0bd6c7261ffcb78e754c46c70ab57ad9a # v2.3.8 + uses: google/osv-scanner-action/osv-scanner-action@f4cfcc01edc9c8b756a9b873b7a623ca674da51e # v2.5.0 with: scan-args: | --format=json diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d2f9f24d..446aa1dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Aligned the four pinned `google/osv-scanner-action` comments with embedded release v2.5.0 and removed the `contents: write` one-shot repair workflow that Scorecard Token-Permissions scored 0 on ContextualWisdomLab/.github#921. - Parsed `opencode.jsonc` as JSONC (stripping `//` and `/* */` comments outside string literals) in the reasoning-effort guard and its contract tests, instead of raw `json.loads`, which rejected the file the moment it carried its first explanatory comment (added for the `contextual-orchestrator` provider block) with `Expecting property name enclosed in double quotes`. Comment markers inside string values, such as the `$schema` URL, are left untouched. - Download the pinned `uv` 0.12.1 exporter from the official GitHub Releases URL instead of `releases.astral.sh`, which now returns HTTP 403 and blocks org-wide OpenCode `coverage-evidence`. The SHA-256 pin is unchanged. The opener may follow one hop onto `release-assets.githubusercontent.com` or `objects.githubusercontent.com` and still rejects every other host, userinfo, non-HTTPS scheme, and nondefault port (ContextualWisdomLab/.github#1109). - Compared the trusted `uv` executable's post-install `--version` output against the real GitHub Releases build's full string, `uv 0.12.1 (x86_64-unknown-linux-gnu)`, instead of the bare `uv 0.12.1` the prior check required; the genuine release binary always prints the target triple, so every installation was failing the pin check immediately after the archive download itself was fixed (ContextualWisdomLab/.github#1109). diff --git a/docs/doctoring/osv-scanner-version-comment-scorecard.md b/docs/doctoring/osv-scanner-version-comment-scorecard.md new file mode 100644 index 000000000..67e36c4f2 --- /dev/null +++ b/docs/doctoring/osv-scanner-version-comment-scorecard.md @@ -0,0 +1,41 @@ +# OSV scanner version comments and Scorecard Token-Permissions + +## Incident and buyer impact + +Dependabot PR ContextualWisdomLab/.github#921 bumped +`google/osv-scanner-action/osv-scanner-action` to +`f4cfcc01edc9c8b756a9b873b7a623ca674da51e`, which embeds +`ghcr.io/google/osv-scanner-action:v2.5.0`. The four `uses:` comments +still said `# v2.3.8`. A one-shot repair workflow then granted +top-level `contents: write` so GitHub Actions could rewrite those +comments. OpenSSF Scorecard Token-Permissions scored that workflow 0, +and the repair job itself failed on an unterminated Python string. +A commercial buyer reading the security dashboard saw a red Scorecard +gate and could not tell which OSV scanner release actually ran. + +## Decision + +Correct the four trailing comments in-tree to `# v2.5.0`. Delete the +one-shot writer. Do not grant `contents: write` to repair a comment. +Keep the exact full SHA, scan arguments, reporter pin, timeouts, and +permissions unchanged. Temporary branch-writer workflows are not +merge evidence. + +This is version-comment honesty and least-privilege token scope, not +operational-PII masking. + +## References + +National Institute of Standards and Technology. (2022). *Secure +software development framework (SSDF) version 1.1: Recommendations +for mitigating the risk of software vulnerabilities* (NIST Special +Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 + +OpenSSF Scorecard. (2024). *Check: Token-Permissions*. +https://github.com/ossf/scorecard/blob/main/docs/checks.md#token-permissions + +Supply-chain Levels for Software Artifacts. (2023). *SLSA v1.0 +specification*. https://slsa.dev/spec/v1.0/ + +GitHub. (2025). *Automatic token authentication*. +https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication diff --git a/docs/scorecard-governance.md b/docs/scorecard-governance.md index 80f6cc130..26e9270cd 100644 --- a/docs/scorecard-governance.md +++ b/docs/scorecard-governance.md @@ -49,6 +49,18 @@ review-thread resolution, and latest-head required checks. Historical approved-changeset ratios are monitored but not used to waive current-head review gates. +## Token-Permissions + +Top-level or job-level `contents: write` on a one-shot repair workflow +is a Scorecard Token-Permissions score-0 finding. Temporary branch +writers that only rewrite comments or version strings are not merge +evidence. Apply those edits in the pull-request tree and delete the +writer. Keep `contents: write` only on the documented dispatch and +scheduler exception paths in +[`docs/automation/review-agent-comment-invocation.md`](automation/review-agent-comment-invocation.md). +Pinned action comments must name the release the SHA embeds so a +buyer can audit which scanner ran. + ## Failure Evidence Every central workflow failure must print the actionable reason in its logs. diff --git a/tests/test_osv_scanner_action_version_comment.py b/tests/test_osv_scanner_action_version_comment.py new file mode 100644 index 000000000..2b7072656 --- /dev/null +++ b/tests/test_osv_scanner_action_version_comment.py @@ -0,0 +1,61 @@ +"""Contract tests for OSV scanner action version traceability. + +A commercial buyer reading ``security-scan.yml`` must see the same +release the pinned SHA actually embeds. The failed one-shot repair on +ContextualWisdomLab/.github#921 left four ``# v2.3.8`` comments on SHA +``f4cfcc01edc9c8b756a9b873b7a623ca674da51e`` (upstream v2.5.0) and a +``contents: write`` workflow that Scorecard Token-Permissions scored 0. +""" + +from pathlib import Path + + +_REPOSITORY_ROOT = Path(__file__).resolve().parents[1] +_WORKFLOW_DIRECTORY = _REPOSITORY_ROOT / ".github" / "workflows" +_WORKFLOW_PATH = _WORKFLOW_DIRECTORY / "security-scan.yml" +_ONE_SHOT_PATH = _WORKFLOW_DIRECTORY / "one-shot-pr921-osv-version-comments.yml" +_ACTION_SHA = "f4cfcc01edc9c8b756a9b873b7a623ca674da51e" +_EMBEDDED_RELEASE = "v2.5.0" +_STALE_RELEASE = "v2.3.8" +_PIN_PREFIX = f"google/osv-scanner-action/osv-scanner-action@{_ACTION_SHA}" + + +def _pinned_osv_lines(workflow: str) -> list[str]: + """Return stripped ``uses:`` lines that pin the current OSV scanner SHA.""" + + return [ + line.strip() + for line in workflow.splitlines() + if _PIN_PREFIX in line + ] + + +def test_osv_scanner_action_pin_reports_embedded_release() -> None: + """Every pinned scanner invocation identifies upstream release v2.5.0.""" + + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + pinned_lines = _pinned_osv_lines(workflow) + + assert len(pinned_lines) == 4 + assert all(line.endswith(f"# {_EMBEDDED_RELEASE}") for line in pinned_lines) + assert f"@{_ACTION_SHA} # {_STALE_RELEASE}" not in workflow + + +def test_osv_version_comment_matches_real_workflow_layout() -> None: + """The four production scanner steps keep SHA, args, and comment aligned.""" + + workflow = _WORKFLOW_PATH.read_text(encoding="utf-8") + expected = f"uses: {_PIN_PREFIX} # {_EMBEDDED_RELEASE}" + + assert workflow.count(expected) == 4 + assert "scan-args:" in workflow + assert workflow.count("continue-on-error: true") >= 4 + assert "contents: write" not in workflow + + +def test_pr921_one_shot_write_workflow_is_absent() -> None: + """Completed Scorecard-failing one-shot writers must not remain mergeable.""" + + assert not _ONE_SHOT_PATH.exists() + leftover = list(_WORKFLOW_DIRECTORY.glob("one-shot-pr921-*.yml")) + assert leftover == [] diff --git a/tests/test_repository_branch_coverage_pr743_cleanup.py b/tests/test_repository_branch_coverage_pr743_cleanup.py index 0b3d56367..396e4ae2a 100644 --- a/tests/test_repository_branch_coverage_pr743_cleanup.py +++ b/tests/test_repository_branch_coverage_pr743_cleanup.py @@ -10,6 +10,7 @@ WORKFLOW_DIRECTORY / "repair-pr743-git-config-red-test.yml", WORKFLOW_DIRECTORY / "one-shot-repair-uv-strix-ci.yml", WORKFLOW_DIRECTORY / "one-shot-pr743-apply-git-isolation.yml", + WORKFLOW_DIRECTORY / "one-shot-pr921-osv-version-comments.yml", )