# 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 on: workflow_call: # Callers must grant actions: read, contents: read, security-events: write and # id-token: write ON THE CALLING JOB. A job-level `permissions:` block REPLACES # the workflow-level one and is never merged with it, so the declarations below # cannot elevate a caller that omitted them. `actions: read` is required by # Scorecard's Packaging check (Actions.ListWorkflowRunsByFileName); without it # the check errors. Audited 2026-09-14: 11 of 13 callers in the estate omitted it. # Only default-branch publication receives OIDC; PR analysis cannot publish # unmerged results to the public Scorecard dataset or badge. permissions: contents: read jobs: scorecard: if: github.event_name != 'pull_request' name: Run Scorecard runs-on: ubuntu-latest timeout-minutes: 15 permissions: actions: read contents: read security-events: write id-token: write steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Run Scorecard Analysis uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 with: results_file: results.sarif results_format: sarif # 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: ${{ 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 # as collateral of an unrelated gitleaks refactor. Without a SARIF upload # nothing feeds GitHub code scanning, so every Scorecard alert in the # estate froze: measured in this repo on 2026-08-25, all 12 Scorecard # alerts were stuck at 2026-06-03 while Hypatia's updated 2026-08-24. # The badge kept working, which is why it went unnoticed. - name: Checkout native-lock reconciliation helper uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: metadatastician/standards ref: 5a0891e9e58b785181a92e141505ca96ca987b42 # per-file reconciliation (#1168) path: .standards-scorecard persist-credentials: false sparse-checkout: scripts/reconcile-scorecard-actions-lock.rb sparse-checkout-cone-mode: false - name: Reconcile native action pins id: reconcile # Fail open on the ARTEFACT, never on the outcome. Previously this step # ran under `set -euo pipefail` with no `continue-on-error`, and the # upload below consumed results.reconciled.sarif. Any reconciler failure # therefore skipped the upload entirely, and code scanning silently kept # serving the PREVIOUS scan's alerts behind a green badge. That is the # same shape as the ~2-month freeze recorded above (PR #393). # The upload is now unconditional; the run still fails, at the end of # the job, once the results are safely published. continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail gh extension install github/gh-actions-lock --pin v0.2.0 # Exit 3: the reconciled SARIF was written, but some workflows failed # native lock verification and keep their findings. Exit 2: usage or # SARIF-structure error, nothing written. The code is passed on so # the final step can say which one happened. rc=0 ruby .standards-scorecard/scripts/reconcile-scorecard-actions-lock.rb \ results.sarif results.reconciled.sarif actions-lock-audit.json || rc=$? printf 'rc=%s\n' "$rc" >> "$GITHUB_OUTPUT" exit "$rc" - name: Select SARIF to upload id: select-sarif run: | set -euo pipefail if [ -s results.reconciled.sarif ]; then printf 'file=%s\n' results.reconciled.sarif >> "$GITHUB_OUTPUT" printf 'reconciled=true\n' >> "$GITHUB_OUTPUT" else echo "::warning title=Scorecard reconciliation produced no SARIF::Uploading raw results.sarif. Native action pins will appear UNRECONCILED in this upload." printf 'file=%s\n' results.sarif >> "$GITHUB_OUTPUT" printf 'reconciled=false\n' >> "$GITHUB_OUTPUT" fi - name: Upload SARIF to code scanning uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) with: sarif_file: ${{ steps.select-sarif.outputs.file }} - name: Upload results artifact uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: scorecard-results path: | results.sarif results.reconciled.sarif actions-lock-audit.json retention-days: 90 # The upload above can no longer be skipped, so a reconciler outage cannot # freeze this repo's Security tab. A failed reconciliation is still a # failure, and is surfaced here rather than swallowed. - name: Fail if reconciliation did not succeed if: "!cancelled() && (steps.reconcile.outcome != 'success' || steps.select-sarif.outputs.reconciled != 'true')" run: | if [ "${{ steps.reconcile.outputs.rc }}" = "3" ] && [ "${{ steps.select-sarif.outputs.reconciled }}" = "true" ]; then echo "::error title=Scorecard reconciliation incomplete::Reconciled SARIF was uploaded. Workflows that failed native action-lock verification keep their findings; the reconcile step log names each one. This run fails by design." else echo "::error title=Scorecard reconciliation failed::Reconcile step outcome was '${{ steps.reconcile.outcome }}' (exit '${{ steps.reconcile.outputs.rc }}'). Raw SARIF was uploaded so results are not lost, but this run fails by design." fi exit 1 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 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 uses: ossf/scorecard-action@fa7117db83c52d787ae5a89f350999701505d78e # v2.5.0 with: 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 --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 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 --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}" \ --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: Checkout native-lock reconciliation helper uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: metadatastician/standards ref: 5a0891e9e58b785181a92e141505ca96ca987b42 # per-file reconciliation (#1168) path: .standards-scorecard persist-credentials: false sparse-checkout: scripts/reconcile-scorecard-actions-lock.rb sparse-checkout-cone-mode: false - name: Reconcile native action pins id: reconcile # Fail open on the ARTEFACT, never on the outcome. Previously this step # ran under `set -euo pipefail` with no `continue-on-error`, and the # upload below consumed results.reconciled.sarif. Any reconciler failure # therefore skipped the upload entirely, and code scanning silently kept # serving the PREVIOUS scan's alerts behind a green badge. That is the # same shape as the ~2-month freeze recorded above (PR #393). # The upload is now unconditional; the run still fails, at the end of # the job, once the results are safely published. continue-on-error: true env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail gh extension install github/gh-actions-lock --pin v0.2.0 # Exit 3: the reconciled SARIF was written, but some workflows failed # native lock verification and keep their findings. Exit 2: usage or # SARIF-structure error, nothing written. The code is passed on so # the final step can say which one happened. rc=0 ruby .standards-scorecard/scripts/reconcile-scorecard-actions-lock.rb \ results.sarif results.reconciled.sarif actions-lock-audit.json || rc=$? printf 'rc=%s\n' "$rc" >> "$GITHUB_OUTPUT" exit "$rc" - name: Select SARIF to upload id: select-sarif run: | set -euo pipefail if [ -s results.reconciled.sarif ]; then printf 'file=%s\n' results.reconciled.sarif >> "$GITHUB_OUTPUT" printf 'reconciled=true\n' >> "$GITHUB_OUTPUT" else echo "::warning title=Scorecard reconciliation produced no SARIF::Uploading raw results.sarif. Native action pins will appear UNRECONCILED in this upload." printf 'file=%s\n' results.sarif >> "$GITHUB_OUTPUT" printf 'reconciled=false\n' >> "$GITHUB_OUTPUT" fi - name: Upload SARIF to code scanning uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0 (4.38.1 blocked estate-wide; nexia-list#100) with: sarif_file: ${{ steps.select-sarif.outputs.file }} - name: Retain scan evidence uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: scorecard-pr-results path: | results.sarif results.reconciled.sarif actions-lock-audit.json if-no-files-found: error retention-days: 14 # The upload above can no longer be skipped, so a reconciler outage cannot # freeze this repo's Security tab. A failed reconciliation is still a # failure, and is surfaced here rather than swallowed. - name: Fail if reconciliation did not succeed if: "!cancelled() && (steps.reconcile.outcome != 'success' || steps.select-sarif.outputs.reconciled != 'true')" run: | if [ "${{ steps.reconcile.outputs.rc }}" = "3" ] && [ "${{ steps.select-sarif.outputs.reconciled }}" = "true" ]; then echo "::error title=Scorecard reconciliation incomplete::Reconciled SARIF was uploaded. Workflows that failed native action-lock verification keep their findings; the reconcile step log names each one. This run fails by design." else echo "::error title=Scorecard reconciliation failed::Reconcile step outcome was '${{ steps.reconcile.outcome }}' (exit '${{ steps.reconcile.outputs.rc }}'). Raw SARIF was uploaded so results are not lost, but this run fails by design." fi exit 1