From 7a561a7d092f0827b8de8ce3ac02dbe837695d45 Mon Sep 17 00:00:00 2001 From: ms280690 Date: Thu, 16 Jul 2026 10:38:04 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20dependency=20scan=20reusable=20workflow?= =?UTF-8?q?=20=E2=80=94=20osv-scanner=20(issue=20#5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Application-source dependency vulnerability scan, first sub-issue of #8. - .github/actions/osv-scanner: composite that installs a pinned, checksum-verified osv-scanner v2.4.0 binary and scans all lockfiles against the OSV database (language-agnostic). Produces SARIF (optionally uploaded to the Security tab) and emits job-level annotations per affected package. Gates on fail-on-severity by mapping it to a CVSS threshold (critical>=9.0, high>=7.0, medium>=4.0, low>=0.1) parsed from groups[].max_severity. - .github/workflows/dep-scan-app.yml: reusable gate; inputs directory, fail-on-severity, sarif-upload, actions-ref. Immutable self-reference pattern; no harden-runner; inherits caller permissions. - ci.yml: osv-scanner dogfood (smoke test — no lockfiles in this repo). - README + approved-actions: usage, CVSS threshold mapping, osv.dev egress note. osv-scanner has no native fail-on-severity flag (nor does the official reusable workflow — only fail-on-vuln), so the gate is implemented by parsing the JSON report's max_severity. Kept the checksum-verified binary pattern (consistent with gitleaks/trufflehog/tflint/kubeconform; no new org-allowlist publisher). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/actions/osv-scanner/action.yml | 98 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 20 ++++++ .github/workflows/dep-scan-app.yml | 63 +++++++++++++++++ README.md | 27 +++++++ docs/approved-actions.md | 1 + 5 files changed, 209 insertions(+) create mode 100644 .github/actions/osv-scanner/action.yml create mode 100644 .github/workflows/dep-scan-app.yml diff --git a/.github/actions/osv-scanner/action.yml b/.github/actions/osv-scanner/action.yml new file mode 100644 index 0000000..cc4a86e --- /dev/null +++ b/.github/actions/osv-scanner/action.yml @@ -0,0 +1,98 @@ +name: "OSV-Scanner Dependency Scan" +description: | + Scans all lockfiles in the repo against the OSV vulnerability database + (https://osv.dev). Language-agnostic — one invocation covers Python, JS/TS, + Go, Ruby, Rust, Java, etc. + + Installs a pinned, checksum-verified osv-scanner binary directly. Produces + SARIF (optionally uploaded to the Security tab) and emits each finding as a + job-level annotation. The step fails when any vulnerability's CVSS score is at + or above the fail-on-severity threshold. + + Required permissions on calling job: contents: read (plus security-events: + write when sarif-upload is true). +inputs: + version: + description: "osv-scanner version to install (no leading 'v')." + default: "2.4.0" + directory: + description: "Directory to scan (recursively)." + default: "." + fail-on-severity: + description: "Lowest CVSS severity that fails the build: critical, high, medium, or low." + default: "critical" + sarif-upload: + description: "Upload SARIF results to the GitHub Security tab (needs security-events: write)." + default: "true" +runs: + using: "composite" + steps: + - name: Install osv-scanner + shell: bash + env: + OSV_VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + TMP="$(mktemp -d)" + BASE="https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}" + curl -sSfL -o "${TMP}/osv-scanner_linux_amd64" "${BASE}/osv-scanner_linux_amd64" + curl -sSfL -o "${TMP}/osv-scanner_SHA256SUMS" "${BASE}/osv-scanner_SHA256SUMS" + # Verify the downloaded binary against the published checksum before use. + ( cd "${TMP}" && grep " osv-scanner_linux_amd64\$" osv-scanner_SHA256SUMS | sha256sum -c - ) + install "${TMP}/osv-scanner_linux_amd64" "${RUNNER_TEMP}/osv-scanner" + + - name: Scan dependencies + id: scan + shell: bash + env: + DIRECTORY: ${{ inputs.directory }} + run: | + set -euo pipefail + OSV="${RUNNER_TEMP}/osv-scanner" + SARIF="${RUNNER_TEMP}/osv.sarif" + JSON="${RUNNER_TEMP}/osv.json" + # osv-scanner exits non-zero when vulnerabilities are found; we gate on + # severity ourselves below, so tolerate that exit here. + "${OSV}" scan source -r --format sarif --output-file "${SARIF}" "${DIRECTORY}" || true + "${OSV}" scan source -r --format json --output-file "${JSON}" "${DIRECTORY}" || true + echo "sarif=${SARIF}" >> "${GITHUB_OUTPUT}" + echo "json=${JSON}" >> "${GITHUB_OUTPUT}" + + - name: Upload SARIF to GitHub Security tab + if: ${{ inputs.sarif-upload != 'false' && hashFiles(steps.scan.outputs.sarif) != '' }} + uses: github/codeql-action/upload-sarif@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5 + with: + sarif_file: ${{ steps.scan.outputs.sarif }} + category: osv-scanner + + - name: Annotate and enforce gate + shell: bash + env: + JSON: ${{ steps.scan.outputs.json }} + FAIL_ON: ${{ inputs.fail-on-severity }} + run: | + set -euo pipefail + case "${FAIL_ON}" in + critical) THRESH=9.0 ;; + high) THRESH=7.0 ;; + medium) THRESH=4.0 ;; + low) THRESH=0.1 ;; + *) echo "::error::invalid fail-on-severity '${FAIL_ON}'"; exit 1 ;; + esac + + # Emit one annotation per affected package (job-level: lockfiles have no + # reliable source-line mapping). max_severity is a numeric CVSS score. + jq -r '.results[]?.packages[]? + | .package.name as $p + | ([.groups[]?.max_severity | select(. != null) | tonumber] | max // 0) as $s + | "\($s)\t\($p)\t" + ([.vulnerabilities[]?.id] | join(","))' "${JSON}" \ + | while IFS=$'\t' read -r sev pkg ids; do + echo "::warning title=osv-scanner::${pkg}: ${ids} (max CVSS ${sev})" + done + + MAX="$(jq '[.results[]?.packages[]?.groups[]?.max_severity | select(. != null) | tonumber] | max // 0' "${JSON}")" + echo "Highest CVSS severity found: ${MAX} (threshold ${THRESH} for '${FAIL_ON}')" + if awk "BEGIN{exit !(${MAX} >= ${THRESH})}"; then + echo "::error::Dependency vulnerabilities at or above '${FAIL_ON}' severity (CVSS ${MAX})." + exit 1 + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d76cb0e..b988e33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,26 @@ jobs: - uses: ./.github/actions/tflint + # Scan dependency lockfiles with osv-scanner. This repo has no lockfiles, so + # this is a smoke test that the composite installs and runs (exits 0). + osv-scanner: + name: OSV-Scanner + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # SARIF upload to the Security tab + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - uses: ./.github/actions/osv-scanner + # Validate Helm/Kustomize with kubeconform. This repo has no charts/, so this # is a smoke test that the composite installs and runs (exits 0, nothing to # validate). diff --git a/.github/workflows/dep-scan-app.yml b/.github/workflows/dep-scan-app.yml new file mode 100644 index 0000000..8ad861a --- /dev/null +++ b/.github/workflows/dep-scan-app.yml @@ -0,0 +1,63 @@ +name: Dependency Scan (App) + +# Reusable PR gate that scans application dependency lockfiles for known +# vulnerabilities with osv-scanner (https://osv.dev). Language-agnostic. +# Automated update PRs are handled separately by the central Renovate runner. +# +# Deliberately does NOT use step-security/harden-runner: this workflow runs in +# the caller's context and may be consumed by private repos where third-party +# CI telemetry is not acceptable. See docs/approved-actions.md. + +on: + workflow_call: + inputs: + directory: + description: 'Directory to scan (recursively)' + default: '.' + type: string + fail-on-severity: + description: 'Lowest CVSS severity that fails the build: critical, high, medium, low' + default: 'critical' + type: string + sarif-upload: + description: 'Upload SARIF results to the GitHub Security tab' + default: true + type: boolean + actions-ref: + description: >- + Ref or commit SHA of sparkgeo/github-actions to load the osv-scanner + composite from. Pin this to the SAME SHA you pinned dep-scan-app.yml + to, so the whole workflow is immutable. + default: 'main' + type: string + +# No permissions block: a reusable workflow inherits the caller job's granted +# permissions. The caller grants `contents: read` (always) and adds +# `security-events: write` only when it wants SARIF upload. See README. + +jobs: + osv-scanner: + name: OSV-Scanner + runs-on: ubuntu-latest + steps: + - name: Checkout repository under test + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + # Load the composite action at the caller-pinned ref into a fixed path, + # then invoke it locally. Keeps the action reference immutable (no mutable + # cross-repo @main) while honouring the repo's SHA-pin policy. + - name: Checkout osv-scanner composite action + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: sparkgeo/github-actions + ref: ${{ inputs.actions-ref }} + path: .sparkgeo-github-actions + persist-credentials: false + + - uses: ./.sparkgeo-github-actions/.github/actions/osv-scanner + with: + directory: ${{ inputs.directory }} + fail-on-severity: ${{ inputs.fail-on-severity }} + sarif-upload: ${{ inputs.sarif-upload }} diff --git a/README.md b/README.md index a6abfcb..6a80b71 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ All action references in this repo are pinned to full commit SHAs. See [CONTRIBU | Lint App | [`lint-app.yml`](.github/workflows/lint-app.yml) | `workflow_call` | Reusable MegaLinter gate — auto-detects all languages, blocks on any linter error, uploads SARIF to the Security tab | | Lint IaC | [`lint-iac.yml`](.github/workflows/lint-iac.yml) | `workflow_call` | Reusable tflint gate — recursive Terraform/OpenTofu lint, provider-agnostic via `.tflint.hcl`, inline PR annotations, plugin caching | | Lint Helm | [`lint-helm.yml`](.github/workflows/lint-helm.yml) | `workflow_call` | Reusable kubeconform gate — renders Helm charts / Kustomize overlays and validates against Kubernetes API schemas; blocks on schema errors | +| Dependency Scan (App) | [`dep-scan-app.yml`](.github/workflows/dep-scan-app.yml) | `workflow_call` | Reusable osv-scanner gate — scans all lockfiles against the OSV database; blocks on `fail-on-severity`; SARIF to Security tab | ## Composite Actions @@ -41,6 +42,7 @@ gh api repos/sparkgeo/github-actions/commits/main --jq '.sha' | Pre-commit | [`pre-commit`](.github/actions/pre-commit/action.yml) | Runs the consuming repo's `.pre-commit-config.yaml` hooks; changed files on PRs, all files otherwise. Language-agnostic | `version` (default: `4.6.0`), `config-path` (default: `.pre-commit-config.yaml`), `from-ref`/`to-ref` (default: PR base/head) | | TFLint | [`tflint`](.github/actions/tflint/action.yml) | Recursive Terraform/OpenTofu lint; provider rule sets via consuming-repo `.tflint.hcl`; inline PR annotations. Installs a checksum-verified tflint binary | `version` (default: `0.63.1`), `directory` (default: `.`), `minimum-failure-severity` (default: `error`) | | Kubeconform | [`kubeconform`](.github/actions/kubeconform/action.yml) | Renders Helm charts (`helm template`) and Kustomize overlays (`kustomize build`) and validates output against Kubernetes API schemas. Installs a checksum-verified kubeconform binary | `version` (default: `0.8.0`), `charts-dir` (default: `charts`), `kustomize-dir` (default: `""`), `kubernetes-version` (default: `1.32.0`), `ignore-missing-schemas` (default: `false`) | +| OSV-Scanner | [`osv-scanner`](.github/actions/osv-scanner/action.yml) | Scans lockfiles against the OSV database; job-level annotations + SARIF; fails at/above the CVSS threshold. Installs a checksum-verified osv-scanner binary | `version` (default: `2.4.0`), `directory` (default: `.`), `fail-on-severity` (default: `critical`), `sarif-upload` (default: `true`) | ### GitHub Actionlint @@ -369,6 +371,31 @@ Findings are emitted as job-level error annotations naming the failing chart/ove For the local fast-feedback stage, copy [`examples/helm.pre-commit-config.yaml`](examples/helm.pre-commit-config.yaml) to your repo root as `.pre-commit-config.yaml` (`helm lint`) and gate it in CI with `lint-precommit.yml`. +## Dependency scanning + +PR gates that check dependencies for known vulnerabilities. Automated update PRs are handled separately by the central Renovate runner (see issue #8), not these workflows. + +### Dependency Scan — App (osv-scanner) + +Scans every lockfile in the repo (Python, JS/TS, Go, Ruby, Rust, Java, …) against the [OSV database](https://osv.dev). Language-agnostic — no per-ecosystem config. Findings are emitted as job-level annotations and uploaded as SARIF; the job fails when any vulnerability's CVSS score is at or above `fail-on-severity`. + +```yaml +# .github/workflows/dep-scan.yml +name: Dependency Scan +on: [pull_request] +jobs: + app: + uses: sparkgeo/github-actions/.github/workflows/dep-scan-app.yml@ + permissions: + contents: read + security-events: write # required for SARIF upload + with: + actions-ref: + fail-on-severity: critical # default; 'high' for a stricter gate +``` + +`fail-on-severity` maps to CVSS: `critical` ≥ 9.0, `high` ≥ 7.0, `medium` ≥ 4.0, `low` ≥ 0.1. On a private repo with no GitHub Code Security license, set `sarif-upload: false` (findings still appear as job annotations and in the log). + ## Consuming repo CI setup Public and private repos use different subsets of these actions. The key differences are `harden-runner` (sends egress telemetry to StepSecurity — omit on private repos) and `scorecard` (requires public visibility to produce meaningful scores). diff --git a/docs/approved-actions.md b/docs/approved-actions.md index 07fd6ef..fd8d050 100644 --- a/docs/approved-actions.md +++ b/docs/approved-actions.md @@ -75,6 +75,7 @@ These are not GitHub Actions (no `uses:` reference) so the org allowlist does no | `pre-commit` | `pre-commit` composite action, `lint-precommit.yml` | `4.6.0` | Run via `pipx run --spec pre-commit==4.6.0` (pipx is preinstalled on GitHub runners) — pinned version, ephemeral env, no PATH write. Hook versions themselves are pinned (to commit SHAs) in each consuming repo's `.pre-commit-config.yaml`. | 2026-06-10 | | `tflint` | `tflint` composite action, `lint-iac.yml` | `v0.63.1` | Binary downloaded from the GitHub release and **verified against the published SHA-256 checksum** before use. The `terraform-linters/setup-tflint` Action is avoided to keep the supply chain to a single checksum-verified download. Plugin rule sets are pinned in each consuming repo's `.tflint.hcl`. | 2026-06-23 | | `kubeconform` | `kubeconform` composite action, `lint-helm.yml` | `v0.8.0` | Binary downloaded from the GitHub release and **verified against the published SHA-256 checksum** before use. helm and kustomize are preinstalled on GitHub-hosted runners. | 2026-06-23 | +| `osv-scanner` | `osv-scanner` composite action, `dep-scan-app.yml` | `v2.4.0` | Binary downloaded from the GitHub release and **verified against the published SHA-256 checksum** before use. Queries `osv.dev` / `deps.dev` at scan time — sends package names, versions, and file hashes (no source code). Use `--offline` mode if that egress is unacceptable. | 2026-07-16 | When bumping a version, update it in all locations listed above and re-confirm the checksum download path.