Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
41 changes: 41 additions & 0 deletions docs/doctoring/osv-scanner-version-comment-scorecard.md
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions docs/scorecard-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
61 changes: 61 additions & 0 deletions tests/test_osv_scanner_action_version_comment.py
Original file line number Diff line number Diff line change
@@ -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 == []
1 change: 1 addition & 0 deletions tests/test_repository_branch_coverage_pr743_cleanup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)


Expand Down
Loading