Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/actions/osv-scanner/action.yml
Original file line number Diff line number Diff line change
@@ -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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
63 changes: 63 additions & 0 deletions .github/workflows/dep-scan-app.yml
Original file line number Diff line number Diff line change
@@ -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 }}
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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@<SHA>
permissions:
contents: read
security-events: write # required for SARIF upload
with:
actions-ref: <SHA>
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).
Expand Down
1 change: 1 addition & 0 deletions docs/approved-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
Loading