From ca43509dfc54e42b43e15ccb797c8ad28d58d664 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 09:45:11 +0100 Subject: [PATCH 1/5] ci: support pull-request Scorecard analysis in the shared workflow --- .github/workflows/actions.lock | 4 +- .github/workflows/scorecard-reusable.yml | 51 ++++++++++++++++++++---- .github/workflows/scorecard.yml | 12 ++++-- 3 files changed, 54 insertions(+), 13 deletions(-) diff --git a/.github/workflows/actions.lock b/.github/workflows/actions.lock index fb983aa9c..bce8bbfd9 100644 --- a/.github/workflows/actions.lock +++ b/.github/workflows/actions.lock @@ -182,7 +182,7 @@ dependencies: owner_id: 47606891 repo_id: 331103973 'github/codeql-action@cdf488f595d80d6e07e03d4674febd5ab45fa938': - ref: 'v4.37.9' + ref: 'cdf488f595d80d6e07e03d4674febd5ab45fa938' commit: 'sha1-cdf488f595d80d6e07e03d4674febd5ab45fa938' owner_id: 9919 repo_id: 259445878 @@ -202,7 +202,7 @@ dependencies: owner_id: 1841483 repo_id: 220799100 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc': - ref: 'v2.4.4' + ref: '2d1146689b8cda280b9bc96326124645441f03bc' commit: 'sha1-2d1146689b8cda280b9bc96326124645441f03bc' owner_id: 67707773 repo_id: 421101922 diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index c09abc2c6..c6cddaa97 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -1,3 +1,4 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. name: OSSF Scorecard Reusable Workflow @@ -5,21 +6,22 @@ name: OSSF Scorecard Reusable Workflow on: workflow_call: -# NOTE: This workflow requires security-events: write and id-token: write -# to publish scorecard results. Callers MUST grant these permissions. -# The previous attempt to add only `actions: read` at top-level (0ced540e) failed -# because it exceeded caller grants. The fix is to declare ALL required permissions -# at the top-level so callers know what to grant. +# Callers must grant contents: read, security-events: write and id-token: write. +# Only default-branch publication receives OIDC; PR analysis cannot publish +# unmerged results to the public Scorecard dataset or badge. permissions: contents: read - security-events: write - id-token: write jobs: scorecard: + if: github.event_name != 'pull_request' name: Run Scorecard runs-on: ubuntu-latest timeout-minutes: 15 + permissions: + contents: read + security-events: write + id-token: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -34,7 +36,7 @@ jobs: # publish_results feeds the public OpenSSF API and the badge, and is # INDEPENDENT of results_format -- so emitting SARIF does not cost us # the badge. Both outputs are live with this shape. - publish_results: true + publish_results: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} # RESTORED 2026-08-25. Commit 5a93d9d5 (PR #393, "Ci/gitleaks self hosted # fix", 2026-06-21) deleted this step and switched results_format to json @@ -54,3 +56,36 @@ jobs: name: scorecard-results path: results.sarif retention-days: 90 + + pull-request: + if: github.event_name == 'pull_request' + name: Run Scorecard PR + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + steps: + - name: Checkout pull request + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + # OpenSSF labels pull_request support experimental. Upload actual results; + # a failed scan or upload must fail the workflow, not become a clean gate. + - name: Analyze pull request + uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4 + with: + results_file: results.sarif + results_format: sarif + publish_results: false + - name: Upload SARIF to code scanning + uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 + with: + sarif_file: results.sarif + - name: Retain scan evidence + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scorecard-pr-results + path: results.sarif + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0f4b09e49..a0d96c27f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,8 +1,12 @@ +# This workflow is managed by gh actions-lock. # SPDX-License-Identifier: MPL-2.0 # This workflow is managed by gh actions-lock. name: Scorecard on: + push: + branches: [main] + pull_request: schedule: - cron: '0 0 * * 0' workflow_dispatch: @@ -15,11 +19,13 @@ concurrency: # id-token: write is the OIDC token Scorecard uses to publish results; # security-events: write uploads the SARIF. permissions: - actions: read contents: read - security-events: write - id-token: write jobs: scorecard: + permissions: + actions: read + contents: read + security-events: write + id-token: write uses: ./.github/workflows/scorecard-reusable.yml From 0eda9f2570c1834ea8bb62ccf074c0c0087b5cfe Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:52:37 +0100 Subject: [PATCH 2/5] ci(scorecard): check out the PR head SHA so the analysis satisfies the ruleset The "Shared scanner content safety" ruleset requires a Scorecard code-scanning analysis on the PR's head or merge commit. actions/checkout on a pull_request event checks out refs/pull/N/merge, which GitHub regenerates whenever main moves, so the analysis binds to a merge commit that stops matching before the PR can merge. Checking out github.event.pull_request.head.sha instead binds the upload to the head commit, which is stable for the life of the push. codeql-action's getRef() rewrites refs/pull/N/merge to refs/pull/N/head when HEAD differs from GITHUB_SHA, so the upload lands on the head SHA without any further change. --- .github/workflows/scorecard-reusable.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index c6cddaa97..aa4b92e5e 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -70,6 +70,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false + ref: ${{ github.event.pull_request.head.sha }} # OpenSSF labels pull_request support experimental. Upload actual results; # a failed scan or upload must fail the workflow, not become a clean gate. - name: Analyze pull request From 703b610b8cb37d82e12caa2f4170c39fa7c008fe Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 20:53:56 +0100 Subject: [PATCH 3/5] ci(scorecard): fix indentation of the checkout ref key --- .github/workflows/scorecard-reusable.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index aa4b92e5e..016fd8cd3 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -70,7 +70,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha }} # OpenSSF labels pull_request support experimental. Upload actual results; # a failed scan or upload must fail the workflow, not become a clean gate. - name: Analyze pull request From 7d33e8f45ae5018244295e4271cac3bca2320d8d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:28:45 +0100 Subject: [PATCH 4/5] ci(scorecard): give PR scans all three Scorecard categories The "Shared scanner content safety" ruleset evaluates code scanning per (tool, category). scorecard-action forces Local mode on pull_request events, so a PR upload carries only supply-chain/local while the scheduled main run carries branch-protection and online-scm as well. Every PR therefore fails with "expecting 2 results from Scorecard". Local proof with the v5.5.0 CLI: a repo-mode scan at the PR head SHA skips the repo-state checks (2 categories, no branch-protection); only a HEAD scan yields all 3. So the PR job now installs the pinned scorecard CLI (release tarball, sha256-verified), runs exactly the seven repo-level checks at repository HEAD with the policy file taken from scorecard-action's pinned SHA, and appends those two runs to the scorecard-action SARIF before the existing upload. The local category remains the scorecard-action measurement of the checked-out PR tree. The step fails unless exactly three categories result. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/scorecard-reusable.yml | 47 ++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index 016fd8cd3..7f5a28cef 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -79,6 +79,53 @@ jobs: results_file: results.sarif results_format: sarif publish_results: false + # scorecard-action forces Local (filesystem) mode on pull_request events, + # so the PR run only ever produces the supply-chain/local category. The + # code-scanning ruleset evaluates Scorecard per category and main's + # scheduled run also carries supply-chain/branch-protection and + # supply-chain/online-scm, so a PR is "expecting 2 results" forever. + # Those two categories are repo-state properties (branch protection, CI, + # code review, maintenance), not properties of a commit, and scorecard + # refuses to compute them for a non-HEAD --commit. So measure them at + # repository HEAD with the pinned scorecard CLI, restricted to exactly the + # repo-level checks, and merge the runs into the same SARIF. The local + # category stays the scorecard-action measurement of the PR tree. + - name: Install scorecard CLI (pinned + checksum-verified) + run: | + set -euo pipefail + SCORECARD_VERSION=5.5.0 + SHA256=83b90a05c1540ef1390db1cd5711e5fd04be9c1d8537fb84d39d02092d6a8dff + cd "$RUNNER_TEMP" + curl -fsSL -o scorecard.tar.gz \ + "https://github.com/ossf/scorecard/releases/download/v${SCORECARD_VERSION}/scorecard_${SCORECARD_VERSION}_linux_amd64.tar.gz" + echo "${SHA256} scorecard.tar.gz" | sha256sum -c - + tar -xzf scorecard.tar.gz scorecard + chmod +x scorecard + ./scorecard version + - name: Measure repo-level checks at HEAD + env: + ENABLE_SARIF: "1" + GITHUB_AUTH_TOKEN: ${{ github.token }} + # Policy file from scorecard-action at the same pinned SHA as above. + POLICY_URL: https://raw.githubusercontent.com/ossf/scorecard-action/2d1146689b8cda280b9bc96326124645441f03bc/policies/template.yml + POLICY_SHA256: 6e75dcc0df989d333492c0e1f6e484fc5b4d26ab768333ddcd0e3788539689b0 + run: | + set -euo pipefail + curl -fsSL -o "$RUNNER_TEMP/policy.yml" "$POLICY_URL" + echo "${POLICY_SHA256} $RUNNER_TEMP/policy.yml" | sha256sum -c - + "$RUNNER_TEMP/scorecard" \ + --repo="github.com/${GITHUB_REPOSITORY}" \ + --checks=Branch-Protection,CI-Tests,CII-Best-Practices,Code-Review,Contributors,Maintained,Signed-Releases \ + --format=sarif --policy="$RUNNER_TEMP/policy.yml" --show-details \ + > "$RUNNER_TEMP/repo-state.sarif" + # Merge: keep every run from the PR-tree scan, append the repo-level + # runs. Fail if the result is not exactly the three categories. + jq -s '.[0] as $pr | .[1] as $head | $pr | .runs += $head.runs' \ + results.sarif "$RUNNER_TEMP/repo-state.sarif" > merged.sarif + mv merged.sarif results.sarif + jq -r '.runs[].automationDetails.id' results.sarif + n=$(jq '[.runs[].automationDetails.id | split("/")[0:2] | join("/")] | unique | length' results.sarif) + test "$n" -eq 3 || { echo "expected 3 Scorecard categories, got $n" >&2; exit 1; } - name: Upload SARIF to code scanning uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9 with: From fccb5cf5d5c78754a663597c82ba53ccdc91eaa5 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Mon, 7 Sep 2026 21:36:20 +0100 Subject: [PATCH 5/5] ci(scorecard): enforce HTTPS on the two pinned downloads SonarCloud (githubactions:S6506) flags curl -L without a protocol restriction: a redirect could in principle land on plain HTTP. Both downloads are checksum-verified afterwards, so a downgrade could not smuggle content in, but there is no reason to permit one at all. Restrict the initial request and any redirect to HTTPS. Verified locally with curl 8.14: both URLs still fetch and both sha256 checks still pass under the restricted protocol set. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/scorecard-reusable.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/scorecard-reusable.yml b/.github/workflows/scorecard-reusable.yml index 7f5a28cef..8750b94ac 100644 --- a/.github/workflows/scorecard-reusable.yml +++ b/.github/workflows/scorecard-reusable.yml @@ -96,7 +96,7 @@ jobs: SCORECARD_VERSION=5.5.0 SHA256=83b90a05c1540ef1390db1cd5711e5fd04be9c1d8537fb84d39d02092d6a8dff cd "$RUNNER_TEMP" - curl -fsSL -o scorecard.tar.gz \ + curl -fsSL --proto "=https" --proto-redir "=https" -o scorecard.tar.gz \ "https://github.com/ossf/scorecard/releases/download/v${SCORECARD_VERSION}/scorecard_${SCORECARD_VERSION}_linux_amd64.tar.gz" echo "${SHA256} scorecard.tar.gz" | sha256sum -c - tar -xzf scorecard.tar.gz scorecard @@ -111,7 +111,7 @@ jobs: POLICY_SHA256: 6e75dcc0df989d333492c0e1f6e484fc5b4d26ab768333ddcd0e3788539689b0 run: | set -euo pipefail - curl -fsSL -o "$RUNNER_TEMP/policy.yml" "$POLICY_URL" + curl -fsSL --proto "=https" --proto-redir "=https" -o "$RUNNER_TEMP/policy.yml" "$POLICY_URL" echo "${POLICY_SHA256} $RUNNER_TEMP/policy.yml" | sha256sum -c - "$RUNNER_TEMP/scorecard" \ --repo="github.com/${GITHUB_REPOSITORY}" \