From 25f35d66714b42ab3487d99d8c08a000c271b7bb Mon Sep 17 00:00:00 2001 From: ehaldane-digicatapult <204178965+ehaldane-digicatapult@users.noreply.github.com> Date: Mon, 13 Jul 2026 11:26:01 +0100 Subject: [PATCH 1/4] Add OpenSSF scorecard workflows --- .../workflows/generate-security-scorecard.yml | 125 ++++++++++++++++++ README.md | 48 +++++++ examples/generate-security-scorecard.md | 43 ++++++ 3 files changed, 216 insertions(+) create mode 100644 .github/workflows/generate-security-scorecard.yml create mode 100644 examples/generate-security-scorecard.md diff --git a/.github/workflows/generate-security-scorecard.yml b/.github/workflows/generate-security-scorecard.yml new file mode 100644 index 0000000..77fafdf --- /dev/null +++ b/.github/workflows/generate-security-scorecard.yml @@ -0,0 +1,125 @@ +name: Scorecard analysis workflow + +# Only use on public/private repositories where Code Scanning has been enabled. +on: + workflow_call: + inputs: + results_file: + required: true + type: string + default: "results.sarif" + results_format: + required: true + type: string + default: "sarif" # sarif or json only + upload_type: + required: false + type: string + default: "artifact" # art(i|e)fact or dashboard only + publish_results: + required: false + type: boolean + default: false + file_mode: + required: false + type: string + default: "archive" # archive or git only + repo_token: + required: false + type: string + custom_scan: + required: false + type: boolean + default: false + description: >- + When true, run the Scorecard CLI directly (via Docker) restricted to the checks in + `custom_checks`, instead of the full `ossf/scorecard-action` suite. Note that + `publish_results` has no effect in this mode - see the workflow's README section. + custom_checks: + required: false + type: string + default: "" + description: >- + Comma-separated list of Scorecard check names to run when `custom_scan` is true, + e.g. "Branch-Protection,Code-Review". Required (non-empty) when `custom_scan` is true. + scorecard_version: + required: false + type: string + default: "v5.5.0" + description: >- + Pinned tag of the `ghcr.io/ossf/scorecard` CLI image used only when `custom_scan` is + true. Does not affect the version of `ossf/scorecard-action` used for the default scan. + +# Use workflow-level read-all in caller workflows. +# permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - name: "Checkout code" + uses: actions/checkout@v6.0.2 + with: + persist-credentials: false + + - name: "Validate custom scan inputs" + if: ${{ inputs.custom_scan == true && inputs.custom_checks == '' }} + run: | + echo "::error::custom_checks must be a non-empty, comma-separated list of check names when custom_scan is true (e.g. 'Branch-Protection,Code-Review')." + exit 1 + + - name: "Run analysis" + if: ${{ inputs.custom_scan != true }} + uses: ossf/scorecard-action@v2.4.3 + with: + file_mode: ${{inputs.file_mode}} + publish_results: ${{inputs.publish_results}} + results_file: ${{inputs.results_file}} + results_format: ${{inputs.results_format}} + repo_token: ${{inputs.repo_token}} + + # Runs the Scorecard CLI directly so that `--checks` can restrict the run to + # `custom_checks`. `ossf/scorecard-action` has no equivalent input - see the + # workflow's README section for why `publish_results` is not supported here. + - name: "Run custom analysis" + if: ${{ inputs.custom_scan == true }} + env: + GITHUB_AUTH_TOKEN: ${{ inputs.repo_token }} + ENABLE_SARIF: ${{ inputs.results_format == 'sarif' && '1' || '' }} + run: | + docker run --rm \ + -e GITHUB_AUTH_TOKEN \ + -e ENABLE_SARIF \ + -v "${{ github.workspace }}:/workspace" \ + -w /workspace \ + "ghcr.io/ossf/scorecard:${{ inputs.scorecard_version }}" \ + --repo="github.com/${{ github.repository }}" \ + --checks="${{ inputs.custom_checks }}" \ + --format="${{ inputs.results_format }}" \ + --file-mode="${{ inputs.file_mode }}" \ + --show-details \ + --output="${{ inputs.results_file }}" + + # Upload the results as artifacts. + # Commenting out will disable uploads of run results in SARIF format to the repository Actions tab. + # https://docs.github.com/en/actions/advanced-guides/storing-workflow-data-as-artifacts + - name: "Upload artifact" + uses: actions/upload-artifact@v7.0.1 + if: ${{inputs.upload_type == 'artifact' || inputs.upload_type == 'artefact'}} + with: + name: SARIF file + path: ${{inputs.results_file}} + retention-days: 5 + + # Upload the results to GitHub's code scanning dashboard. + # Commenting out will disable upload of results to your repo's Code Scanning dashboard. + - name: "Upload to code-scanning" + uses: github/codeql-action/upload-sarif@v4.35.3 + if: ${{inputs.upload_type == 'dashboard'}} + with: + sarif_file: ${{inputs.results_file}} diff --git a/README.md b/README.md index 555b651..7cc55fd 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Shared github workflows created by the `digicatapult` organisation. **Security & analysis** - [Generate SBOM](#generate-sbom-examples) +- [Generate Security Scorecard](#generate-security-scorecard-examples) - [Scan Secrets](#scan-secrets-examples) - [Scan Vulnerabilities](#scan-vulnerabilities-examples) - [ZAP Scan](#zap-scan-examples) @@ -367,6 +368,53 @@ This GitHub Actions workflow generates an SBOM for a project. It allows flexibil 6. **Upload Artifact**: Optionally uploads the generated SBOM file as a workflow artifact. 7. **Upload SBOM to Dependency Track**: Optionally uploads the CycloneDX SBOM to a DT server. Docker Scout SBOMs are currently incompatible with DT due to inaccuracies in the CycloneDX spec implementation; CycloneDX-NPM is a more faithful implementation. To upload successfully, the step must have a DT hostname via the `DTRACK_HOSTNAME` secret, a parent project GUID (`DTRACK_PARENT_GUID`), and an API key (`DTRACK_APIKEY`) with both the `BOM_UPLOAD` and `PROJECT_CREATION_UPLOAD` permissions. +### [Generate Security Scorecard](.github/workflows/generate-security-scorecard.yml) ([examples](examples/generate-security-scorecard.md)) + +Runs an [OpenSSF Scorecard](https://github.com/ossf/scorecard) analysis against the callee repository. By default this runs the full `ossf/scorecard-action` check suite. When `custom_scan` is `true`, it instead runs the Scorecard CLI directly (via Docker), restricted to the checks listed in `custom_checks` (e.g. `Branch-Protection`), since `ossf/scorecard-action` has no input for selecting individual checks. + +#### Inputs + +| Input | Type | Description | Default | Required | +| ----------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | -------- | +| results_file | string | File path to store the Scorecard results | `results.sarif` | true | +| results_format | string | Format of the results. Options: `sarif`, `json` | `sarif` | true | +| upload_type | string | How results are published. Options: `artifact`/`artefact` uploads a workflow artifact, `dashboard` uploads to GitHub Code Scanning | `artifact` | false | +| publish_results | boolean | Publishes results to the public [Scorecard REST API](https://api.scorecard.dev) and enables the Scorecard badge. Only supported by the default path - see the note below | `false` | false | +| file_mode | string | Method used to fetch repository files. Options: `archive`, `git` | `archive` | false | +| repo_token | string | GitHub token with read access, used to authenticate Scorecard's GitHub API calls | `""` | false | +| custom_scan | boolean | Runs the Scorecard CLI directly (via Docker), restricted to `custom_checks`, instead of the full `ossf/scorecard-action` suite | `false` | false | +| custom_checks | string | Comma-separated list of Scorecard check names to run when `custom_scan` is `true`, e.g. `Branch-Protection,Code-Review`. Required (non-empty) when `custom_scan` is `true` | `""` | false | +| scorecard_version | string | Pinned tag of the `ghcr.io/ossf/scorecard` CLI image, used only when `custom_scan` is `true` | `v5.5.0` | false | + +#### Permissions + +| Access | Jobs used | Level | Reason | Conditions | +| ------------------------ | ---------- | -------- | ----------------------------------------------------------------- | ---------- | +| `security-events: write` | `analysis` | Workflow | To POST new code scanning alerts based on the SARIF report | N/A | +| `id-token: write` | `analysis` | Workflow | Required by `ossf/scorecard-action` to publish results/OIDC flows | N/A | + +#### Workflow Description + +This GitHub Actions workflow runs an OpenSSF Scorecard analysis and publishes the results. + +1. **Checkout code**: Checks out the callee repository. +2. **Validate custom scan inputs**: Fails fast with a clear error if `custom_scan` is `true` but `custom_checks` is empty, rather than letting the CLI silently run all checks. +3. **Run analysis** _(default path, `custom_scan: false`)_: Runs `ossf/scorecard-action`, which always executes the complete set of Scorecard checks. +4. **Run custom analysis** _(`custom_scan: true`)_: Runs the pinned `ghcr.io/ossf/scorecard` CLI image directly via `docker run`, passing `--checks` for the `custom_checks` list, `--format`/`--file-mode` matching the other inputs, and `--output` to the shared `results_file` path so the upload steps below work unchanged. `ENABLE_SARIF=1` is set automatically when `results_format` is `sarif`, since SARIF output is feature-flagged in the CLI. +5. **Upload artifact** / **Upload to code-scanning**: Uploads `results_file` as a workflow artifact or to GitHub's Code Scanning dashboard, depending on `upload_type`. + +#### `custom_scan` limitations + +> [!NOTE] +> `publish_results` and the Scorecard badge/REST API integration are **not available** when `custom_scan` is `true`. +> +> `ossf/scorecard-action`'s `publish_results` input does two things that cannot be reimplemented with the Scorecard CLI: +> +> 1. It submits results for public repositories to the [Scorecard REST API](https://api.scorecard.dev), which OSSF ingests for cross-project analysis at scale. +> 2. It's a prerequisite for displaying a [Scorecard badge](https://openssf.org/blog/2022/09/08/show-off-your-security-score-announcing-scorecards-badges/) that gets updated in real time, thanks to the above REST API. +> +> Both integrations are tied to the full check suite produced by `ossf/scorecard-action`; a partial, custom-checks run isn't a valid input for either. If you need `publish_results`, use the default (`custom_scan: false`) path instead. + ### [Poetry Static checks](.github/workflows/static-checks-poetry.yml) ([examples](examples/static-checks-poetry.md)) Runs static analysis for Poetry projects (default matrix includes `pylint`, `black`, `ruff`, `mypy`, and `bandit`). Each command runs as a separate matrix job so a single failing check does not interrupt the others. Optional TruffleHog (secrets) and Semgrep CE (vulnerabilities) scans can be enabled alongside the static analysis matrix. diff --git a/examples/generate-security-scorecard.md b/examples/generate-security-scorecard.md new file mode 100644 index 0000000..479583e --- /dev/null +++ b/examples/generate-security-scorecard.md @@ -0,0 +1,43 @@ +# Security Scorecard analysis + +## Using [generate-security-scorecard.yml](../.github/workflows/generate-security-scorecard.yml) in callers + +`security-events: write` and `id-token: write` permissions are used. They are invoked at the workflow level for the `analysis` job. + +### Default scan (full check suite, results published) + +Runs the complete `ossf/scorecard-action` check suite and uploads results to GitHub Code Scanning. `publish_results: true` also submits results to the public [Scorecard REST API](https://api.scorecard.dev) and enables the Scorecard badge. These benefits are only available in this default mod, using `ossf/scorecard-action`, and cannot be achieved with the CLI. + +```yaml +jobs: + generate-security-scorecard: + uses: digicatapult/shared-workflows/.github/workflows/generate-security-scorecard.yml@main + permissions: + security-events: write + id-token: write + with: + publish_results: true + upload_type: dashboard + secrets: inherit +``` + +### Custom scan (restricted check set) + +Runs only the listed checks via the Scorecard CLI directly, instead of the full suite. Useful when only a subset of checks (e.g. branch protection) is relevant to a repository, without the noise of unrelated findings. + +```yaml +jobs: + generate-security-scorecard: + uses: digicatapult/shared-workflows/.github/workflows/generate-security-scorecard.yml@main + permissions: + security-events: write + id-token: write + with: + custom_scan: true + custom_checks: "Branch-Protection,Code-Review" + upload_type: dashboard + secrets: inherit +``` + +> [!IMPORTANT] +> `publish_results` has no effect when `custom_scan` is `true`. Public transparency (submission to the Scorecard REST API) and the Scorecard README badge are both tied to the full `ossf/scorecard-action` check suite and can't be reimplemented for a partial, custom-checks run. If you need either of those, use the default (`custom_scan: false`) path instead. From 63995d633730297ca10f3b7871bea1618fc39c42 Mon Sep 17 00:00:00 2001 From: ehaldane-digicatapult <204178965+ehaldane-digicatapult@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:12:54 +0100 Subject: [PATCH 2/4] Update default values; fix secret pattern --- .github/workflows/generate-security-scorecard.yml | 7 +++++-- examples/generate-security-scorecard.md | 14 ++++++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.github/workflows/generate-security-scorecard.yml b/.github/workflows/generate-security-scorecard.yml index 77fafdf..ce6caec 100644 --- a/.github/workflows/generate-security-scorecard.yml +++ b/.github/workflows/generate-security-scorecard.yml @@ -3,6 +3,9 @@ name: Scorecard analysis workflow # Only use on public/private repositories where Code Scanning has been enabled. on: workflow_call: + secrets: + repo_token: + required: false inputs: results_file: required: true @@ -81,7 +84,7 @@ jobs: publish_results: ${{inputs.publish_results}} results_file: ${{inputs.results_file}} results_format: ${{inputs.results_format}} - repo_token: ${{inputs.repo_token}} + repo_token: ${{secrets.repo_token}} # Runs the Scorecard CLI directly so that `--checks` can restrict the run to # `custom_checks`. `ossf/scorecard-action` has no equivalent input - see the @@ -89,7 +92,7 @@ jobs: - name: "Run custom analysis" if: ${{ inputs.custom_scan == true }} env: - GITHUB_AUTH_TOKEN: ${{ inputs.repo_token }} + GITHUB_AUTH_TOKEN: ${{ secrets.repo_token }} ENABLE_SARIF: ${{ inputs.results_format == 'sarif' && '1' || '' }} run: | docker run --rm \ diff --git a/examples/generate-security-scorecard.md b/examples/generate-security-scorecard.md index 479583e..0b844c2 100644 --- a/examples/generate-security-scorecard.md +++ b/examples/generate-security-scorecard.md @@ -17,8 +17,11 @@ jobs: id-token: write with: publish_results: true - upload_type: dashboard - secrets: inherit + upload_type: artifact + results_format: sarif + results_file: ${{ github.event.repository.name }}-scorecard.sarif + secrets: + repo_token: ${{ secrets.GITHUB_TOKEN }} ``` ### Custom scan (restricted check set) @@ -35,8 +38,11 @@ jobs: with: custom_scan: true custom_checks: "Branch-Protection,Code-Review" - upload_type: dashboard - secrets: inherit + upload_type: artifact + results_format: sarif + results_file: ${{ github.event.repository.name }}-scorecard.sarif + secrets: + repo_token: ${{ secrets.GITHUB_TOKEN }} ``` > [!IMPORTANT] From 9d17a657cf56dca2a850ac54a6c1495735298045 Mon Sep 17 00:00:00 2001 From: ehaldane-digicatapult <204178965+ehaldane-digicatapult@users.noreply.github.com> Date: Mon, 13 Jul 2026 12:33:00 +0100 Subject: [PATCH 3/4] Match user to host --- .github/workflows/generate-security-scorecard.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/generate-security-scorecard.yml b/.github/workflows/generate-security-scorecard.yml index ce6caec..25a2a77 100644 --- a/.github/workflows/generate-security-scorecard.yml +++ b/.github/workflows/generate-security-scorecard.yml @@ -96,6 +96,7 @@ jobs: ENABLE_SARIF: ${{ inputs.results_format == 'sarif' && '1' || '' }} run: | docker run --rm \ + --user "$(id -u):$(id -g)" \ -e GITHUB_AUTH_TOKEN \ -e ENABLE_SARIF \ -v "${{ github.workspace }}:/workspace" \ From a0a0f2f787e975416632bb8112ceb39314b68386 Mon Sep 17 00:00:00 2001 From: ehaldane-digicatapult <204178965+ehaldane-digicatapult@users.noreply.github.com> Date: Mon, 13 Jul 2026 13:19:03 +0100 Subject: [PATCH 4/4] Add dynamic policy document --- .../workflows/generate-security-scorecard.yml | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/generate-security-scorecard.yml b/.github/workflows/generate-security-scorecard.yml index 25a2a77..e0f4a35 100644 --- a/.github/workflows/generate-security-scorecard.yml +++ b/.github/workflows/generate-security-scorecard.yml @@ -95,6 +95,35 @@ jobs: GITHUB_AUTH_TOKEN: ${{ secrets.repo_token }} ENABLE_SARIF: ${{ inputs.results_format == 'sarif' && '1' || '' }} run: | + # Generate a Scorecard policy file that enables all checks specified in `custom_checks`. + ALL_CHECKS="Binary-Artifacts Branch-Protection CI-Tests CII-Best-Practices Code-Review \ + Contributors Dangerous-Workflow Dependency-Update-Tool Fuzzing License Maintained \ + Packaging Pinned-Dependencies SAST Security-Policy Signed-Releases Token-Permissions \ + Vulnerabilities Webhooks" + + IFS=',' read -ra ENABLED <<< "${{ inputs.custom_checks }}" + + { + echo "version: 1" + echo "policies:" + for check in $ALL_CHECKS; do + trimmed_check="$check" + enabled=false + for e in "${ENABLED[@]}"; do + if [ "$(echo "$e" | xargs)" = "$trimmed_check" ]; then + enabled=true + fi + done + echo " $trimmed_check:" + if $enabled; then + echo " mode: enforced" + echo " score: 1" + else + echo " mode: disabled" + fi + done + } > scorecard-policy.yml + docker run --rm \ --user "$(id -u):$(id -g)" \ -e GITHUB_AUTH_TOKEN \ @@ -104,6 +133,7 @@ jobs: "ghcr.io/ossf/scorecard:${{ inputs.scorecard_version }}" \ --repo="github.com/${{ github.repository }}" \ --checks="${{ inputs.custom_checks }}" \ + --policy="scorecard-policy.yml" \ --format="${{ inputs.results_format }}" \ --file-mode="${{ inputs.file_mode }}" \ --show-details \