diff --git a/.github/workflows/osv-scanner-pr.yml b/.github/workflows/osv-scanner-pr.yml index 00bbf2c81..ac7fe7123 100644 --- a/.github/workflows/osv-scanner-pr.yml +++ b/.github/workflows/osv-scanner-pr.yml @@ -34,12 +34,12 @@ jobs: osv-scan: if: github.event.action != 'closed' # ponytail: use upstream reusable PR workflow, don't hand-roll the diff scan - # Pinned to v2.3.8 + 1 commit (3a7550f) which gates the JSON job outputs - # behind the new `export-results` input (default false). v2.3.8 dumped the - # full old/new osv-scanner JSON into job outputs unconditionally, tripping - # GitHub's 1,048,576-byte job-outputs cap and failing the run. Same nested - # action pins as v2.3.8; only the Export step is now conditional. - uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@3a7550f43ba5b58905a821ce3a0ed24c4858b3f4 # v2.3.8 + export-results gate + # Pinned to the official v2.5.1 commit, whose reusable workflow exposes + # `export-results` with a false default. Leaving that input false keeps the + # full old/new osv-scanner JSON out of job outputs and avoids GitHub's + # 1,048,576-byte job-output cap; v2.5.1 makes its Export results step + # conditional on that input. + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 + export-results gate permissions: actions: read contents: read diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index 29376269d..19e07fcd4 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -78,7 +78,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@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 with: scan-args: | --format=json @@ -96,7 +96,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@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 with: scan-args: | --format=json @@ -117,7 +117,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@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 with: scan-args: | --format=json @@ -135,7 +135,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@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 with: scan-args: | --format=json @@ -189,7 +189,7 @@ jobs: if len(findings) > 50: print(f"... {len(findings) - 50} additional {label} OSV finding(s) omitted from the log summary.") - name: Report PR-introduced OSV findings - uses: google/osv-scanner-action/osv-reporter-action@8dc09193bb540e09b23da07ad7e30bd33bf87018 # v2.3.8 + uses: google/osv-scanner-action/osv-reporter-action@6e4298ebc4db23e847df9b2e2de2939d6f066c67 # v2.5.1 with: scan-args: | --output=results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index 1630c32d4..c6d1c12b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,10 @@ Semantic Versioning where the repository publishes a release. ### Fixed +- Aligned every central OSV-Scanner Action scanner, reporter, and reusable + workflow use to the official v2.5.1 commit, + preserving exact-base/head policy while applying the current namespace, + local-cache, and offline-vulnerability matching fixes. - Publish only the sanitized cumulative Strix report tree, avoiding a later copy of relative scanner output that could reintroduce known internal warning text into uploaded security evidence. diff --git a/docs/doctoring/osv-scanner-action-single-version.md b/docs/doctoring/osv-scanner-action-single-version.md new file mode 100644 index 000000000..accbc7636 --- /dev/null +++ b/docs/doctoring/osv-scanner-action-single-version.md @@ -0,0 +1,33 @@ +# OSV-Scanner Action single-version boundary + +## Incident boundary + +The central dependency scans invoke `google/osv-scanner-action` six times: +four scanner steps, their reporter gate, and the reusable pull-request scan. +All six remained on Action v2.3.8, while the stale dependency pull request +targeted v2.5.0 after v2.5.1 had become the current official release. + +## Decision + +Pin every central scanner, reporter, and reusable workflow use to +`6e4298ebc4db23e847df9b2e2de2939d6f066c67`, the commit referenced by the +official v2.5.1 tag. That release preserves package namespaces, restores the +local database cache environment variable, and fixes offline vulnerability +matching (Google, 2026). + +GitHub documents that a full commit SHA is unique and immutable and should be +verified against the action repository (GitHub, n.d.). A repository-wide +contract therefore parses every central workflow occurrence, checks the exact +component inventory, rejects malformed pins or mismatched comments, and admits +only the reviewed v2.5.1 SHA and tag. Scan arguments, timeouts, permissions, +exact-base/head comparison, reporter gates, and fail-closed dependency policy +are unchanged. + +## References + +GitHub. (n.d.). *Using pre-written building blocks in your workflow*. +Retrieved August 24, 2026, from +https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/find-and-customize-actions + +Google. (2026, August 17). *OSV-Scanner Action v2.5.1* [Software release]. +https://github.com/google/osv-scanner-action/releases/tag/v2.5.1 diff --git a/tests/test_osv_scanner_action_pin_contract.py b/tests/test_osv_scanner_action_pin_contract.py new file mode 100644 index 000000000..26bfcf062 --- /dev/null +++ b/tests/test_osv_scanner_action_pin_contract.py @@ -0,0 +1,45 @@ +"""Keep every central OSV-Scanner Action use on one reviewed release.""" + +from __future__ import annotations + +import re +from collections import Counter +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +OSV_ACTION_SHA = "6e4298ebc4db23e847df9b2e2de2939d6f066c67" +OSV_ACTION_TAG = "v2.5.1" +_PIN = re.compile( + r"google/osv-scanner-action/(?P" + r"osv-scanner-action|osv-reporter-action|" + r"\.github/workflows/osv-scanner-reusable-pr\.yml)@" + r"(?P[^\s]+)\s+#\s+(?Pv[^\s]+)" +) +_EXPECTED_COMPONENTS = Counter( + { + "osv-scanner-action": 4, + "osv-reporter-action": 1, + ".github/workflows/osv-scanner-reusable-pr.yml": 1, + } +) + + +def test_all_osv_scanner_actions_share_the_reviewed_current_release() -> None: + """Reject partial bumps, malformed refs, and stale OSV Action comments.""" + observed: set[tuple[str, str]] = set() + components: Counter[str] = Counter() + + for path in sorted((REPO_ROOT / ".github/workflows").glob("*.y*ml")): + for line_number, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + if "uses:" not in line or "google/osv-scanner-action/" not in line: + continue + match = _PIN.search(line) + assert match is not None, f"malformed OSV Action pin: {path}:{line_number}" + observed.add((match.group("sha"), match.group("tag"))) + components[match.group("component")] += 1 + + assert observed == {(OSV_ACTION_SHA, OSV_ACTION_TAG)} + assert components == _EXPECTED_COMPONENTS