diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f60672a..6dcf13b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,13 +1,30 @@ # CVM Supply Chain Security — CODEOWNERS -# Changes to CI/CD, security config, and dependency policy require security team review. +# Changes to CI/CD, security config, and dependency policy require security review. +# +# OWNER CORRECTION (2026-09-12): every rule in this file previously named +# @grcengineering/security, a team that does not exist. GitHub's own +# `GET /repos/grcengineering/cvm/codeowners/errors` returned "Unknown owner" for +# all ten lines, which means NO rule matched anyone and the file was inert — +# including the branch-protection "require review from Code Owners" path. +# Rules now name @p4gs, who has admin on this repository, so they actually bind. +# If a real @grcengineering/security team is created (publicly visible, with +# write access here), swap the owner back and re-check the errors endpoint. -.github/workflows/ @grcengineering/security -.github/actions/ @grcengineering/security -.github/CODEOWNERS @grcengineering/security -.github/dependabot.yml @grcengineering/security -renovate.json @grcengineering/security -deny.toml @grcengineering/security -rust-toolchain.toml @grcengineering/security -Dockerfile @grcengineering/security -SECURITY.md @grcengineering/security -supply-chain/ @grcengineering/security +.github/ @p4gs +.github/workflows/ @p4gs +.github/actions/ @p4gs +.github/CODEOWNERS @p4gs +.github/dependabot.yml @p4gs +renovate.json5 @p4gs +deny.toml @p4gs +rust-toolchain.toml @p4gs +Dockerfile @p4gs +SECURITY.md @p4gs +supply-chain/ @p4gs + +# sscs-bootstrapper policy surface — the files that decide which controls run, +# who may sign, and which packages are approved. Treat as security config. +.sscsb/ @p4gs +security-insights.yml @p4gs +.trufflehog.yaml @p4gs +Cargo.lock @p4gs diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..751f853 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ + +## Summary + + + +## AI Provenance Declaration + + + +- [ ] AI generated or assisted with **code** in this PR +- [ ] AI generated or assisted with **tests** in this PR +- [ ] AI introduced or suggested **new dependencies** in this PR +- [ ] AI generated or assisted with **documentation** in this PR + +**AI tool(s)/model(s) used (if any):** + +**Human review performed on AI-generated parts (what/how):** + +## Dependency Changes + + + +- [ ] No new dependencies +- [ ] New dependencies validated (`sscsb deps check`) and approved + +## Merge Policy Reminder + +Merges to protected branches must be signed by an approved **human** hardware-backed +key. When AI involvement is declared above, the merge commit needs review +evidence (`Reviewed-by:` trailer). See `docs/signing.md`. diff --git a/.github/chainguard/sscsb-automation.sts.yaml b/.github/chainguard/sscsb-automation.sts.yaml new file mode 100644 index 0000000..a47a0ef --- /dev/null +++ b/.github/chainguard/sscsb-automation.sts.yaml @@ -0,0 +1,26 @@ +# sscsb Octo STS trust policy — lives at .github/chainguard/sscsb-automation.sts.yaml +# in the repo that GRANTS access. Workflows in `subject_pattern` below can +# exchange their OIDC identity for a short-lived token with EXACTLY these +# permissions on THIS repo. No PAT, nothing stored, expires in ~1 hour. +# +# Docs: https://github.com/octo-sts/app +issuer: https://token.actions.githubusercontent.com + +# Which workflow identities may federate. GitHub's OIDC `sub` claim is +# ID-DECORATED — `repo:OWNER@/REPO@:ref:refs/heads/main` — +# so a pattern spelled from names alone never matches (Octo STS refuses with +# `subject "repo:OWNER@123/REPO@456:ref:…" did not match "repo:OWNER/REPO:ref:…"`). +# The `(@)?` groups accept both the bare and the decorated form; the ids +# are what survive a rename and what a re-created repository of the same name +# does NOT share, so pin them (sscsb fills them in from the GitHub API when +# `gh` is available; otherwise `[0-9]+` accepts any id until you replace it): +# gh api repos/grcengineering/cvm --jq .id → repo id +# gh api users/grcengineering --jq .id → owner id +# `.` in the repository name is escaped: this is a regular expression. +subject_pattern: 'repo:grcengineering(@155832502)?/cvm(@1201601113)?:ref:refs/heads/main' + +permissions: + # Least privilege: grant only what the automation needs. + contents: read + # issues: write + # pull_requests: write diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..4b8f22a --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,52 @@ +# sscsb: CodeQL — deep interprocedural analysis on PRs and the default branch. +# +# sscsb's template ships `actions` only and tells you to add your own languages, +# because CodeQL that does not read your source code is not analysing your +# source code. This workspace is Rust (9 crates under crates/), so `rust` is +# added alongside `actions`. +# +# build-mode: none for both. Rust extraction is source-based and does not build +# the crate graph; `actions` supports no other mode. +name: CodeQL +on: + push: + branches: ["main"] + pull_request: + schedule: + - cron: "45 3 * * 2" + +permissions: + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: actions + build-mode: none + - language: rust + build-mode: none + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Initialize CodeQL + uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + languages: ${{ matrix.language }} + build-mode: ${{ matrix.build-mode }} + - name: Analyze + uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/deploy-gate.yml b/.github/workflows/deploy-gate.yml new file mode 100644 index 0000000..54a87ed --- /dev/null +++ b/.github/workflows/deploy-gate.yml @@ -0,0 +1,293 @@ +# sscsb: verification gate before publish — the repository's single +# provenance-verify implementation. release.yml calls this as a reusable +# workflow between its provenance and publish jobs (`needs:` enforces the +# order), so a release set is published only after: +# +# 1. checksum gate — every *.sha256 sidecar matches its file; +# 2. signature gate — every Cosign bundle verifies against the identity of +# THIS repository's release workflow at THIS tag +# (Fulcio cert, GitHub OIDC issuer, Rekor inclusion), +# and every file that should carry a bundle does; +# 3. subject set — the attested subjects, derived from the assets alone: +# everything shipped EXCEPT the files that describe a +# subject rather than being one (the *.sha256 sidecars, +# the *.sigstore.json bundles, the *.intoto.jsonl +# envelope and sbom.cdx.json). Gates 4-6 run over THIS +# list, so an extra asset a release stages — an agent +# skill, a policy document, a schema — is verified, +# never signed-but-unattested; +# 4. provenance gate — GitHub's attestation store holds a SLSA v1 build +# provenance for each subject's digest, signed by the +# same workflow at the same tag, on a GitHub-hosted +# runner (`gh attestation verify`); +# 5. SBOM gate — the store also holds a CycloneDX SBOM attestation +# bound to each subject's digest; +# 6. SLSA L3 gate — the *.intoto.jsonl the slsa-github-generator emitted +# verifies with slsa-verifier for every subject, +# against this source repository, this tag, and the +# pinned trusted builder. +# +# Each gate fails EXPLICITLY when there is nothing to verify. An unmatched glob +# silently becoming a literal string is not a verification result, and "we +# found no artifacts, so nothing failed" must never be a way to reach publish. +# +# Two ways in, one set of checks: +# workflow_call — from release.yml, over the run's artifacts (the exact +# bytes `publish` then uploads to the draft); +# workflow_dispatch — a manual re-verify of an already-PUBLISHED release, +# downloading its assets by tag. Same gates; a release +# that stops verifying is a finding, not a mystery. +name: Deploy Gate — Verify before Publish +on: + workflow_call: + inputs: + artifact-name: + description: "Run artifact holding the release set (tarballs, .sha256, SBOM, *.sigstore.json)" + required: true + type: string + provenance-artifact: + description: "Run artifact holding the SLSA provenance (*.intoto.jsonl) the generator emitted" + required: true + type: string + tag: + description: "Release tag the set was built from (vX.Y.Z); bundles, attestations and provenance must bind to it" + required: true + type: string + signer-workflow: + description: "Repo-relative path of the workflow whose identity signed and attested the set" + required: false + type: string + default: .github/workflows/release.yml + workflow_dispatch: + inputs: + tag: + description: "Published release tag to re-verify (vX.Y.Z)" + required: true + type: string + +permissions: + contents: read + +jobs: + verify: + runs-on: ubuntu-latest + permissions: + contents: read # run artifacts, published release assets, the attestation store + # Inputs reach the shell through the ENVIRONMENT, never interpolated into a + # `run:` — `gh release download "${{ inputs.tag }}"` would splice a caller- + # influenced string straight into bash, the script-injection shape + # `sscsb verify workflow-audit-extended` flags. + # + # --builder-id is OPTIONAL to slsa-verifier, and omitting it makes this gate + # accept provenance from ANY builder slsa-verifier trusts for this repository + # — not the one that actually builds these releases. BUILDER_ID must track + # the generator pinned in release.yml's `provenance` job — change one, change + # the other. + env: + TAG: ${{ inputs.tag }} + SIGNER_WORKFLOW: ${{ inputs.signer-workflow || '.github/workflows/release.yml' }} + BUILDER_ID: https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.1.0 + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Download the release set from this run + if: ${{ inputs.artifact-name != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.artifact-name }} + path: assets + - name: Download the SLSA provenance from this run + if: ${{ inputs.artifact-name != '' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ inputs.provenance-artifact }} + path: assets + - name: Download the published release's assets (manual re-verify) + if: ${{ inputs.artifact-name == '' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + mkdir -p assets + gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --dir assets \ + || { echo "::error::could not download release assets for ${TAG}"; exit 1; } + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install slsa-verifier + uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # v2.7.1 + - name: Verify checksums (integrity gate) + run: | + set -uo pipefail + cd assets + shopt -s nullglob + sums=(*.sha256) + if [ ${#sums[@]} -eq 0 ]; then + echo "::error::no *.sha256 sidecars found — refusing to certify an unchecksummed set" + exit 1 + fi + sha256sum -c "${sums[@]}" + # Identity regexp is ANCHORED (^ … $) with every regex metacharacter in + # the identity escaped — the host's dots, the tag's dots — and it names + # the signer workflow at THIS tag, so it cannot match a look-alike host, a + # repo whose name merely starts with ours, another workflow in this repo, + # or the same workflow signing a different tag. + - name: Verify Cosign bundles (signature gate) + run: | + set -uo pipefail + cd assets + shopt -s nullglob + bundles=(*.sigstore.json) + if [ ${#bundles[@]} -eq 0 ]; then + echo "::error::no *.sigstore.json bundles found — refusing to certify unsigned artifacts" + exit 1 + fi + identity="https://github.com/${GITHUB_REPOSITORY}/${SIGNER_WORKFLOW}@refs/tags/${TAG}" + regexp="^$(printf '%s' "$identity" | sed 's/[][\.*^$+?(){}|]/\\&/g')\$" + fail=0 + for bundle in "${bundles[@]}"; do + artifact="${bundle%.sigstore.json}" + if [ ! -f "$artifact" ]; then + echo "::error::${bundle} has no matching artifact — an orphan bundle certifies nothing" + fail=1 + continue + fi + cosign verify-blob "$artifact" \ + --bundle "$bundle" \ + --certificate-identity-regexp "$regexp" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com \ + || { echo "::error::signature verification failed for ${artifact}"; fail=1; } + done + # Every file must have been covered — an unsigned file in the set is + # as disqualifying as a bad signature. The one exception is the + # *.intoto.jsonl: a DSSE envelope the generator itself signed, which + # the SLSA gate below verifies with slsa-verifier instead. + for f in *; do + case "$f" in *.sigstore.json|*.intoto.jsonl) continue ;; esac + [ -f "$f.sigstore.json" ] || { echo "::error::${f} is unsigned"; fail=1; } + done + exit $fail + # The attested subject set, derived from the published assets the same + # way release.yml derived it from the staged ones: everything shipped + # EXCEPT the files that DESCRIBE a subject rather than being one — the + # `.sha256` sidecars, the Cosign bundles, the SLSA envelope, and the + # SBOM. Deriving it here rather than globbing `*.tar.gz` is what makes + # every extra asset a release stages actually gate-checked; a `*.tar.gz` + # glob would verify the tarballs and wave the rest through unattested. + # Computed ONCE and read by all three gates below, so they cannot drift + # into verifying different sets. + - name: Determine the attested subject set + run: | + set -uo pipefail + cd assets + shopt -s nullglob + : > ../subjects.txt + for f in *; do + case "$f" in *.sha256 | *.sigstore.json | *.intoto.jsonl | sbom.cdx.json) continue ;; esac + [ -f "$f" ] || continue + printf '%s\n' "$f" >> ../subjects.txt + done + if [ ! -s ../subjects.txt ]; then + echo "::error::no attestable assets found — nothing would be verified" + exit 1 + fi + cat ../subjects.txt + # Both store lookups pin: this repository, the signer workflow, the tag's + # ref, GitHub-hosted runners only, and the exact predicate type. A short + # retry absorbs store propagation right after the release job wrote them — + # it never relaxes a check. + - name: Verify build provenance in the attestation store (provenance gate) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + cd assets + artifacts=() + while IFS= read -r subject; do artifacts+=("$subject"); done < ../subjects.txt + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::no attested subjects found — nothing was verified" + exit 1 + fi + fail=0 + for f in "${artifacts[@]}"; do + ok=0 + for attempt in 1 2 3; do + if gh attestation verify "$f" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "${GITHUB_REPOSITORY}/${SIGNER_WORKFLOW}" \ + --source-ref "refs/tags/${TAG}" \ + --predicate-type https://slsa.dev/provenance/v1 \ + --deny-self-hosted-runners; then + ok=1; break + fi + echo "attempt ${attempt}: provenance for ${f} not verifiable yet"; sleep 10 + done + [ "$ok" -eq 1 ] || { echo "::error::no verifiable build provenance for ${f}"; fail=1; } + done + exit $fail + # --predicate-type is REQUIRED here: `gh attestation verify` defaults to + # the build-provenance predicate, so without it the SBOM attestation is + # invisible and this gate would be re-running the previous one. + - name: Verify SBOM attestation in the attestation store (SBOM gate) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + cd assets + artifacts=() + while IFS= read -r subject; do artifacts+=("$subject"); done < ../subjects.txt + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::no attested subjects found — nothing was verified" + exit 1 + fi + [ -s sbom.cdx.json ] || { echo "::error::sbom.cdx.json missing from the set"; exit 1; } + fail=0 + for f in "${artifacts[@]}"; do + ok=0 + for attempt in 1 2 3; do + if gh attestation verify "$f" \ + --repo "$GITHUB_REPOSITORY" \ + --signer-workflow "${GITHUB_REPOSITORY}/${SIGNER_WORKFLOW}" \ + --source-ref "refs/tags/${TAG}" \ + --predicate-type https://cyclonedx.org/bom \ + --deny-self-hosted-runners; then + ok=1; break + fi + echo "attempt ${attempt}: SBOM attestation for ${f} not verifiable yet"; sleep 10 + done + [ "$ok" -eq 1 ] || { echo "::error::no verifiable SBOM attestation for ${f}"; fail=1; } + done + exit $fail + # slsa-verifier checks that each subject's digest is a subject of the + # provenance, that the provenance was produced by the PINNED trusted + # builder, and that it came from this source repository at this tag. + # `verify-artifact` hashes whatever file it is given and looks that + # digest up among the provenance's subjects — it is indifferent to the + # file's type, so a staged document is covered exactly as a tarball is. + - name: Verify SLSA provenance with slsa-verifier (SLSA L3 gate) + run: | + set -uo pipefail + cd assets + shopt -s nullglob + provenance=(*.intoto.jsonl) + artifacts=() + while IFS= read -r subject; do artifacts+=("$subject"); done < ../subjects.txt + if [ ${#provenance[@]} -ne 1 ]; then + echo "::error::expected exactly one *.intoto.jsonl provenance, found ${#provenance[@]} — refusing to publish unprovenanced artifacts" + exit 1 + fi + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::no attested subjects found — nothing was verified" + exit 1 + fi + fail=0 + for f in "${artifacts[@]}"; do + slsa-verifier verify-artifact "$f" \ + --provenance-path "${provenance[0]}" \ + --source-uri "github.com/${GITHUB_REPOSITORY}" \ + --source-tag "$TAG" \ + --builder-id "$BUILDER_ID" \ + || { echo "::error::SLSA provenance verification failed for ${f}"; fail=1; } + done + exit $fail diff --git a/.github/workflows/octo-sts-example.yml b/.github/workflows/octo-sts-example.yml new file mode 100644 index 0000000..5cec267 --- /dev/null +++ b/.github/workflows/octo-sts-example.yml @@ -0,0 +1,35 @@ +# sscsb: Octo STS — short-lived, repo-scoped GitHub credentials via OIDC +# federation. Replaces long-lived PATs and static App credentials. +# +# Requires: the Octo STS GitHub App installed on the TARGET repo/org, and a +# trust policy at .github/chainguard/.sts.yaml in the target repo +# (sscsb installs a template — see .github/chainguard/sscsb-automation.sts.yaml). +name: Octo STS Example +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + federated-call: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # exchange OIDC identity for a scoped token + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + # Upstream README shows @main; sscsb pins the release SHA (v1.1.1). + - name: Exchange OIDC for short-lived token + id: octo-sts + uses: octo-sts/action@f603d3be9d8dd9871a265776e625a27b00effe05 # v1.1.1 + with: + scope: grcengineering/cvm + identity: sscsb-automation + - name: Use the scoped token (expires in ~1h, never stored) + env: + GH_TOKEN: ${{ steps.octo-sts.outputs.token }} + run: gh api "repos/grcengineering/cvm" --jq .full_name diff --git a/.github/workflows/release-attest-sbom.yml b/.github/workflows/release-attest-sbom.yml new file mode 100644 index 0000000..57ff01f --- /dev/null +++ b/.github/workflows/release-attest-sbom.yml @@ -0,0 +1,132 @@ +# sscsb: GitHub-native SBOM attestation (actions/attest with sbom-path). +# ADDITIVE to release-attest.yml (build provenance), release-sign.yml (Cosign +# keyless), and release-slsa.yml (SLSA L3 generator) — a fourth, independent +# provenance trail, NOT a replacement: it binds the artifact's SBOM to the +# artifact's digest and lands the attestation in GitHub's own attestation store, +# verifiable anywhere with (no cosign or slsa-verifier install needed): +# gh attestation verify --repo grcengineering/cvm \ +# --predicate-type https://cyclonedx.org/bom +# The --predicate-type is REQUIRED: `gh attestation verify` defaults to the +# build-provenance predicate, so an SBOM attestation is invisible without it. +# +# Uses actions/attest in SBOM mode (sbom-path). actions/attest-sbom is +# DEPRECATED in favour of actions/attest; this pins the SAME attest engine +# version (v4.1.1) that release-attest.yml's attest-build-provenance wrapper +# uses internally — bump both together. +# +# Availability: public repositories on all plans; private repositories require +# GitHub Enterprise Cloud (see docs.github.com → artifact attestations). +name: Release — SBOM Attestation +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + attest-sbom: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # sign via GitHub's Sigstore instance (OIDC) + attestations: write # write to the repo's attestation store + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # CUSTOMIZE: replace with your real build (same artifact set as + # release-attest.yml / release-sign.yml / release-slsa.yml so every trail + # covers identical digests). + - name: Build artifact + run: | + mkdir -p dist + git archive --format=tar.gz -o "dist/${GITHUB_REPOSITORY#*/}-${GITHUB_REF_NAME}.tar.gz" HEAD + # Fails EXPLICITLY on an empty dist/: "there was nothing to attest" must + # never be indistinguishable from "everything was attested". + - name: Refuse to attest nothing + run: | + set -uo pipefail + shopt -s nullglob + artifacts=(dist/*) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::dist/ is empty — nothing to attest (did the build step run?)" + exit 1 + fi + # CUSTOMIZE: in a real pipeline the SBOM should describe the ACTUAL + # released artifact and its dependencies. The stub scans the repo tree, + # which matches the git-archive build above. + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + format: cyclonedx-json + output-file: sbom.cdx.json + # Fail EXPLICITLY if the SBOM did not materialise — an absent SBOM must + # never be silently attested-over (same invariant as the empty-dist gate). + - name: Refuse to attest a missing SBOM + run: | + set -uo pipefail + if [ ! -s sbom.cdx.json ]; then + echo "::error::sbom.cdx.json is missing or empty — nothing to attest" + exit 1 + fi + - name: Attest the SBOM to the artifact digest + uses: actions/attest@a1948c3f048ba23858d222213b7c278aabede763 # v4.1.1 + with: + subject-path: dist/* + sbom-path: sbom.cdx.json + - name: Upload artifacts for the verify job + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-attested-artifacts + path: dist/ + + verify: + needs: [attest-sbom] + runs-on: ubuntu-latest + permissions: + contents: read + attestations: read # fetch attestations via the API (required for private repos; public repos merely tolerate its absence) + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: sbom-attested-artifacts + path: dist/ + # Identity is pinned to BOTH the repository and the signer workflow path — + # this proves *this repo's release-attest-sbom workflow* produced the SBOM + # attestation, not merely that some SBOM attestation exists. Values reach + # the shell through the ENVIRONMENT, never `${{ }}` interpolation into the + # script body — the same rule sscsb enforces on your workflows. + # Verification also fails EXPLICITLY when there is nothing to verify. + - name: Verify SBOM attestation for every artifact + env: + GH_TOKEN: ${{ github.token }} + run: | + set -uo pipefail + shopt -s nullglob + artifacts=(dist/*) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::no artifacts in dist/ — nothing was verified" + exit 1 + fi + for f in "${artifacts[@]}"; do + # --predicate-type is REQUIRED for SBOM attestations: `gh attestation + # verify` defaults to the build-provenance predicate + # (https://slsa.dev/provenance/v1), so without this it would look for + # provenance and never match the SBOM attestation. CycloneDX = the + # URI below; switch to https://spdx.dev/Document/v2.3 for SPDX. + gh attestation verify "$f" \ + --repo "$GITHUB_REPOSITORY" \ + --predicate-type https://cyclonedx.org/bom \ + --signer-workflow "$GITHUB_REPOSITORY/.github/workflows/release-attest-sbom.yml" + done diff --git a/.github/workflows/release-sign.yml b/.github/workflows/release-sign.yml new file mode 100644 index 0000000..15909f3 --- /dev/null +++ b/.github/workflows/release-sign.yml @@ -0,0 +1,59 @@ +# sscsb: Sigstore keyless release signing (Cosign + Fulcio + Rekor) with SBOM +# attached. Artifacts are signed as blobs with offline-verifiable bundles; +# certificates bind to this workflow's OIDC identity. +name: Release — Sign & SBOM +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + sign: + runs-on: ubuntu-latest + permissions: + contents: write # upload assets to the release + id-token: write # keyless signing (Fulcio OIDC) + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # CUSTOMIZE: replace with your real build. The default packs the source + # tree so the pipeline works end-to-end on any repo from day one. + - name: Build artifact + run: | + mkdir -p dist + git archive --format=tar.gz -o "dist/${GITHUB_REPOSITORY#*/}-${GITHUB_REF_NAME}.tar.gz" HEAD + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Generate SBOM for the artifact tree + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + format: cyclonedx-json + output-file: dist/sbom.cdx.json + upload-release-assets: false + # Fails EXPLICITLY on an empty dist/: "there was nothing to sign" must never + # be indistinguishable from "everything was signed". + - name: Keyless sign all artifacts (bundle = cert + sig + Rekor proof) + run: | + set -uo pipefail + shopt -s nullglob + artifacts=(dist/*) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::dist/ is empty — nothing to sign (did the build step run?)" + exit 1 + fi + for f in "${artifacts[@]}"; do + [ "${f%.sigstore.json}" != "$f" ] && continue + cosign sign-blob "$f" --bundle "$f.sigstore.json" --yes + done + - name: Upload artifacts + signatures + SBOM to the release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "$GITHUB_REF_NAME" dist/* --clobber diff --git a/.github/workflows/release-slsa.yml b/.github/workflows/release-slsa.yml new file mode 100644 index 0000000..f0d090d --- /dev/null +++ b/.github/workflows/release-slsa.yml @@ -0,0 +1,113 @@ +# sscsb: SLSA Build L3 provenance via the slsa-github-generator reusable +# workflow, verified in-pipeline with slsa-verifier. +# +# This is the MUTABLE-release shape: it runs on `release: published` and the +# generator ATTACHES the provenance to that release (`upload-assets: true`), +# i.e. it writes to a release after publish. On the immutable draft-then- +# publish path (release-immutability), use release.yml instead — it calls the +# SAME generator with `upload-assets: false` and uploads the provenance to the +# DRAFT before the single publish. The generator is compatible with immutable +# releases; only this after-publish attachment is not. Install one or the +# other, not both. +# +# PINNING EXCEPTION (deliberate, audited): slsa-github-generator MUST be +# referenced by @vX.Y.Z tag — slsa-verifier validates the trusted builder's +# ref and the build fails on SHA or short-tag refs (upstream README; +# slsa-verifier issue #12). Every other action here is SHA-pinned. +name: Release — SLSA Provenance +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + # CUSTOMIZE: replace with your real build (same artifact set as release-sign.yml). + - name: Build artifact + run: | + mkdir -p dist + git archive --format=tar.gz -o "dist/${GITHUB_REPOSITORY#*/}-${GITHUB_REF_NAME}.tar.gz" HEAD + - name: Compute subject hashes + id: hash + run: | + cd dist + echo "hashes=$(sha256sum -- * | base64 -w0)" >> "$GITHUB_OUTPUT" + - name: Upload artifacts + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: release-artifacts + path: dist/ + + provenance: + needs: [build] + permissions: + actions: read # workflow context for provenance + id-token: write # sign provenance (Fulcio) + contents: write # attach provenance to the release + # Tag-pinned BY DESIGN — see header comment. + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: "${{ needs.build.outputs.hashes }}" + upload-assets: true + + verify: + needs: [build, provenance] + runs-on: ubuntu-latest + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-artifacts + path: dist/ + - name: Download provenance + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.provenance.outputs.provenance-name }} + - name: Install slsa-verifier + uses: slsa-framework/slsa-verifier/actions/installer@ea584f4502babc6f60d9bc799dbbb13c1caa9ee6 # v2.7.1 + # The provenance filename reaches the shell through the ENVIRONMENT, never by + # `${{ }}` interpolation into the script body — the same rule sscsb enforces + # on your workflows. Verification also fails EXPLICITLY when there is nothing + # to verify: an empty dist/ must never read as "everything passed". + # --builder-id is OPTIONAL to slsa-verifier, and omitting it makes the + # verdict "some builder slsa-verifier trusts produced this, for this + # source URI" rather than "the builder this workflow actually uses". + # BUILDER_ID must track the generator pinned in the `provenance` job + # above — change one, change the other. + - name: Verify provenance for every artifact + env: + PROVENANCE: ${{ needs.provenance.outputs.provenance-name }} + BUILDER_ID: https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.1.0 + run: | + set -uo pipefail + shopt -s nullglob + artifacts=(dist/*) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "::error::no artifacts in dist/ — nothing was verified" + exit 1 + fi + for f in "${artifacts[@]}"; do + slsa-verifier verify-artifact "$f" \ + --provenance-path "$PROVENANCE" \ + --source-uri "github.com/${GITHUB_REPOSITORY}" \ + --source-tag "${GITHUB_REF_NAME}" \ + --builder-id "$BUILDER_ID" + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9a1186c..b7aca2e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,13 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - name: Build release binary run: | cargo build --release --target x86_64-unknown-linux-musl \ @@ -35,7 +41,13 @@ jobs: id-token: write attestations: write steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4 with: name: release-binary @@ -55,7 +67,13 @@ jobs: IMAGE_NAME: ghcr.io/${{ github.repository }} IMAGE_TAG: ${{ github.ref_name }} steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: @@ -84,8 +102,14 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - run: cargo install cargo-cyclonedx + with: + persist-credentials: false + - run: cargo install cargo-cyclonedx --locked - run: cargo cyclonedx --all --format json - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 with: diff --git a/.github/workflows/sast-opengrep.yml b/.github/workflows/sast-opengrep.yml new file mode 100644 index 0000000..d9f03a7 --- /dev/null +++ b/.github/workflows/sast-opengrep.yml @@ -0,0 +1,70 @@ +# sscsb: OpenGrep SAST (default engine — Semgrep-compatible, open rules). +# +# This is the repository's single rule-driven SAST engine. It replaced the +# `semgrep` job formerly in security.yml, which used the archived +# semgrep/semgrep-action@v1. OpenGrep is Semgrep's open fork and resolves the +# same registry rulesets, so `p/rust` and `p/security-audit` below carry over +# that job's exact coverage with no loss — on a pinned, cosign-verified binary +# and with SARIF uploaded to code scanning, which the old job did not do. +# CodeQL (codeql.yml) remains the second, interprocedural SAST engine. +# No first-party OpenGrep action exists, so the pinned release binary is +# downloaded directly and its checksum verified before execution. +name: SAST (OpenGrep) +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +env: + OPENGREP_VERSION: v1.25.0 + +jobs: + opengrep: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Install cosign (verifies the OpenGrep release below) + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + - name: Install pinned OpenGrep and verify signature (cosign keyless) + run: | + curl -fsSL -o opengrep \ + "https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_manylinux_x86" + curl -fsSL -o opengrep.sig \ + "https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_manylinux_x86.sig" + curl -fsSL -o opengrep.cert \ + "https://github.com/opengrep/opengrep/releases/download/${OPENGREP_VERSION}/opengrep_manylinux_x86.cert" + cosign verify-blob opengrep \ + --signature opengrep.sig \ + --certificate opengrep.cert \ + --certificate-identity-regexp "^https://github\.com/opengrep/opengrep/" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + chmod +x opengrep && sudo mv opengrep /usr/local/bin/ + # --exclude .sscsb/rules: a scanner must not scan its own rule definitions. + # A rule file necessarily contains the pattern text it matches on, so any + # finding there is false by construction. + - name: Run OpenGrep + run: | + opengrep scan \ + --config .sscsb/rules \ + --config p/rust \ + --config p/security-audit \ + --exclude .sscsb/rules \ + --sarif-output=opengrep.sarif --error . + - name: Upload SARIF + if: always() + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + sarif_file: opengrep.sarif diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml new file mode 100644 index 0000000..042796d --- /dev/null +++ b/.github/workflows/sbom.yml @@ -0,0 +1,37 @@ +# sscsb: SBOM generation (Syft via anchore/sbom-action, CycloneDX JSON). +name: SBOM +on: + push: + branches: ["main"] + release: + types: [published] + +permissions: + contents: read + +jobs: + sbom: + runs-on: ubuntu-latest + permissions: + contents: write # attach SBOM to releases + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Generate CycloneDX SBOM + uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 + with: + format: cyclonedx-json + output-file: sbom.cdx.json + artifact-name: sbom.cdx.json + upload-release-assets: true + - name: Upload SBOM artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: sbom-cyclonedx + path: sbom.cdx.json diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..15b4a61 --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,43 @@ +# sscsb: OpenSSF Scorecard — repository security-posture scoring with +# published results (enables the REST API + badge). +name: Scorecard +on: + branch_protection_rule: + schedule: + - cron: "30 2 * * 1" + push: + branches: ["main"] + +permissions: read-all + +jobs: + analysis: + runs-on: ubuntu-latest + permissions: + security-events: write # upload SARIF + id-token: write # publish results (OIDC) + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Run Scorecard + uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 + with: + results_file: scorecard.sarif + results_format: sarif + publish_results: true + - name: Upload artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: scorecard-sarif + path: scorecard.sarif + retention-days: 5 + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + sarif_file: scorecard.sarif diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml new file mode 100644 index 0000000..c142601 --- /dev/null +++ b/.github/workflows/secrets-scan.yml @@ -0,0 +1,40 @@ +# sscsb: CI secret-scanning (TruffleHog only). +# Local hooks block first; this catches anything that slips past (e.g. web UI commits). +# +# STRONGEST-TOOL-ONLY POLICY: `.sscsb/config.toml` sets +# `controls.secrets.gitleaks = false`. TruffleHog verifies candidate secrets +# against the issuing provider, so a finding is a live credential rather than a +# regex hit; Gitleaks pattern-matches only, and gitleaks-action additionally +# requires a paid license for organization-owned repositories (this repo is +# owned by the `grcengineering` org). The Gitleaks job sscsb's template emits +# unconditionally is therefore removed here — `sscsb init` regenerates it and it +# must be removed again after any regeneration (upstream template does not read +# the `gitleaks` sub-key). +# All actions pinned to full commit SHAs (sscsb actions-audit enforces this). +name: Secret Scan +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + trufflehog: + runs-on: ubuntu-latest + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout (full history for diff scanning) + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + # Upstream README suggests @main; sscsb pins the release SHA instead (v3.95.9). + - name: TruffleHog scan + uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9 + with: + extra_args: --results=verified,unknown diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9a0d092..e43ad69 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -15,7 +15,13 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -26,7 +32,13 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: EmbarkStudios/cargo-deny-action@3fd3802e88374d3fe9159b834c7714ec57d6c979 # v2 clippy: @@ -35,21 +47,14 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - run: cargo clippy --workspace --all-targets -- -D warnings - - semgrep: - name: Semgrep SAST - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: semgrep/semgrep-action@713efdd345f3035192eaa63f56867b88e63e4e5d # v1 with: - config: >- - p/rust - p/security-audit + persist-credentials: false + - run: cargo clippy --workspace --all-targets -- -D warnings dependency-review: name: Dependency Review @@ -59,7 +64,13 @@ jobs: contents: read pull-requests: write steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4 with: fail-on-severity: high @@ -71,8 +82,14 @@ jobs: permissions: contents: read steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - run: cargo install cargo-cyclonedx + with: + persist-credentials: false + - run: cargo install cargo-cyclonedx --locked - run: cargo cyclonedx --all --format json - uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4 with: diff --git a/.github/workflows/vuln-scan.yml b/.github/workflows/vuln-scan.yml new file mode 100644 index 0000000..84207f6 --- /dev/null +++ b/.github/workflows/vuln-scan.yml @@ -0,0 +1,52 @@ +# sscsb: vulnerability scanning — Trivy (fs: vuln+secret+misconfig → SARIF) +# and OSV-Scanner V2 via Google's reusable workflow (lockfile-exact). +name: Vulnerability Scan +on: + push: + branches: ["main"] + pull_request: + schedule: + - cron: "17 4 * * 1" + +permissions: + contents: read + +jobs: + trivy: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - name: Trivy filesystem scan + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scanners: vuln,secret,misconfig + format: sarif + output: trivy.sarif + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0 + with: + sarif_file: trivy.sarif + + # Google's recommended consumption of OSV-Scanner is this reusable workflow. + # Reusable-workflow jobs carry their own steps; permissions below are what it documents. + osv-scanner: + permissions: + actions: read + contents: read + security-events: write + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + with: + scan-args: |- + --recursive + ./ diff --git a/.gitignore b/.gitignore index 9a097eb..d821ca2 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ *.swo *~ .DS_Store + +# sscsb: generated output (SBOMs, receipts, VEX), not policy +.sscsb/out/ diff --git a/.sscsb/best-practices-badge.md b/.sscsb/best-practices-badge.md new file mode 100644 index 0000000..d411e9d --- /dev/null +++ b/.sscsb/best-practices-badge.md @@ -0,0 +1,36 @@ +# OpenSSF Best Practices Badge — worksheet for grcengineering/cvm + +`sscsb` can't register the badge for you (it needs your GitHub OAuth), but it +CAN make earning the **passing** badge a copy-paste exercise: most passing +criteria are already satisfied by the controls sscsb installs. Register, then +paste the evidence below. + +**Register:** https://www.bestpractices.dev/ → "Get Your Project Badge" → +sign in with GitHub → add `https://github.com/grcengineering/cvm`. + +**Scorecard link:** OpenSSF Scorecard's `CII-Best-Practices` check reads the +badge API for this repo, so earning the badge also lifts your Scorecard score. + +## Passing criteria → sscsb evidence + +| Criterion (passing level) | Satisfied by | Status | +|---|---|---| +| Project has an OSS license (`LICENSE`) | your LICENSE file | ☐ confirm | +| Public version-controlled source repo | GitHub `grcengineering/cvm` | ☑ | +| Automated test suite runs in CI | `.github/workflows/ci.yml` | ☐ confirm | +| Static analysis (SAST) applied | `sast` control (OpenGrep/CodeQL) | ☑ | +| Vulnerability scanning of dependencies | `vuln-scan` (Trivy + OSV-Scanner) | ☑ | +| No unpatched medium+/high vulns | `vuln-scan` gate (`fail_on = high`) | ☑ | +| Secrets not committed; scanned | `secrets` (TruffleHog + Gitleaks) | ☑ | +| Vulnerability reporting process documented | `SECURITY.md` | ☐ confirm | +| Cryptographic signing of releases | `sigstore-signing` / `github-attestations` | ☑ | +| Delivery over TLS / pinned deps | `renovate` + pinned Actions (`actions-audit`) | ☑ | +| Change control (PRs, protected branch) | `branch-protection` | ☐ confirm | +| SBOM published | `sbom` (Syft, CycloneDX) | ☑ | + +Boxes marked ☑ are installed/enforced by sscsb; ☐ need a one-time confirmation +(e.g. a `LICENSE`/`SECURITY.md` exists, CI runs tests, branch protection is on — +all checkable with `sscsb verify` and `sscsb verify scorecard`). + +_Generated by sscs-bootstrapper. Re-run `sscsb init` after deleting this file to +regenerate._ diff --git a/.sscsb/config.toml b/.sscsb/config.toml new file mode 100644 index 0000000..a643e45 --- /dev/null +++ b/.sscsb/config.toml @@ -0,0 +1,242 @@ +# SSCS Bootstrapper configuration — the single source of truth for which +# controls are enabled. Every control is independently toggleable here; +# no code changes required. Secure defaults are ON; optional integrations +# that need external services or extra tooling are OFF. +# +# Toggle: sscsb enable | sscsb disable +# Inspect: sscsb status | sscsb verify | sscsb report + +[general] +# Branches where human-only signing and merge policy are enforced. +protected_branches = ["main", "master"] +# fail_open = true would let hooks pass when scanners are missing. Keep false. +fail_open = false +github_repo = "grcengineering/cvm" + +# ── Phase 1 — Local source integrity ── + +# Secret scanning hooks: TruffleHog + Gitleaks block secrets at pre-commit and pre-push +[controls.secrets] +enabled = true +trufflehog = true +# Strongest-tool-only policy: TruffleHog is the sole credential scanner here. +# It verifies candidate secrets against the issuing provider (live-credential +# proof), where Gitleaks only pattern-matches. Two engines on the same surface +# buys redundancy of *detection* but not of *verification*, and the weaker one +# supplies the false positives. Additionally, gitleaks-action requires a paid +# license for organization-owned repositories, which this repo is. +gitleaks = false +pre_push_range_scan = true + +# CommitSigningGuard: Hardware-backed, human-only signing enforced on protected branches at pre-push +[controls.commit-signing] +enabled = true +require_hardware_backed = true +require_review_evidence_for_ai_merges = true + +# AI agent commit signing: Verifiable AI-agent signatures (distinct identity, never valid on protected branches); off by default +[controls.agent-signing] +enabled = false +require_agent_signatures = false +allowed_backends = ["github-app", "tpm", "fido2", "kms", "piv", "software"] +max_key_age_days = 90 + +# Five-environment signing model: Machine-wide signing posture: human enclave lane, distinct agent identity, cloud/web/Codespaces guidance +[controls.signing-model] +enabled = true + +# Branch protection verification: Verify GitHub protected-branch rules (PRs, no force-push, signatures, checks) +[controls.branch-protection] +enabled = true + +# Actions pinning & permissions audit: Flag mutable action refs and missing/over-broad workflow permissions +[controls.actions-audit] +enabled = true + +# gittuf ref protection: Signed, forge-independent policy over who may change which git refs, verified in CI; off by default (advanced) +[controls.gittuf] +enabled = false + +# AI commit trailers: Validate AI-Assisted / AI-Tool / AI-Model / AI-Role commit trailers +[controls.ai-trailers] +enabled = true + +# AI dependency & command gate: Extra gating when AI-assisted commits add dependencies or shell commands +[controls.ai-dep-gate] +enabled = true + +# AI-provenance PR template: PR template asking whether AI generated code/tests/dependencies/docs +[controls.pr-template] +enabled = true + +# AI provenance receipts: Optional cryptographic receipts linking commits to AI tool/model/role +[controls.ai-receipts] +enabled = false +sign_with_cosign = false +cosign_identity = "" +cosign_issuer = "https://token.actions.githubusercontent.com" + +# Committed binary artifacts: No compiled programs or code-carrying archives in the tracked tree — bytes, not names +[controls.binary-artifacts] +enabled = true + +# Webhook secrets: Every active repository webhook carries a shared secret and verifies TLS +[controls.webhooks] +enabled = true + +# ── Phase 2 — Dependency & vulnerability visibility ── + +# SBOM generation: Syft SBOM in CycloneDX (default) or SPDX JSON +[controls.sbom] +enabled = true +format = "cyclonedx-json" + +# Vulnerability scanning: Trivy (vuln+secret+misconfig) and OSV-Scanner V2 (lockfile-exact) +[controls.vuln-scan] +enabled = true +fail_on = "high" + +# Dependency pinning: Digest-pinned base images, verified downloads, pinned installs, committed lockfiles +[controls.dependency-pinning] +enabled = true + +# OpenSSF Scorecard: Scorecard workflow scoring repository security posture +[controls.scorecard] +enabled = true + +# Renovate onboarding: Automated dependency updates with digest pinning + lockfile maintenance +[controls.renovate] +enabled = true + +# Package trust gate: Existence validation, human approval for new packages, typosquat heuristics, lockfile-exact installs +[controls.package-trust] +enabled = true +registry_check = true +typosquat_check = true + +# Bumblebee endpoint exposure scan: Inventory installed packages, MCP servers, editor/browser extensions and agent skills; match against known-compromise catalogs +[controls.bumblebee] +enabled = false +profile = "baseline" +catalog = "" + +# Grype (optional): SBOM-first vulnerability scanning where Syft+Grype is preferred +[controls.grype] +enabled = false + +# Socket Firewall (optional): Malicious-package detection/blocking at install time +[controls.socket-firewall] +enabled = false + +# ── Phase 3 — Provenance, signing & credential federation ── + +# Sigstore keyless signing: Cosign keyless signing + SBOM/provenance attestations bound to digests +[controls.sigstore-signing] +enabled = true + +# SLSA Build L3 provenance: slsa-github-generator reusable workflow (tag-pinned per its trust model) +[controls.slsa-provenance] +enabled = true + +# GitHub artifact attestations: GitHub-native build provenance (attest-build-provenance) — additive to Cosign/SLSA, verified with `gh attestation verify` +[controls.github-attestations] +enabled = true + +# GitHub SBOM attestation: GitHub-native SBOM attestation bound to the artifact digest (actions/attest, sbom-path) — additive, verified with `gh attestation verify` +[controls.sbom-attestation] +enabled = true + +# OpenSSF Model Signing: Sign & verify ML model artifacts with Sigstore keyless signing; applies when models are present (off by default) +[controls.model-signing] +enabled = false + +# Provenance verification gates: slsa-verifier + cosign verification required before promote/deploy/publish +[controls.provenance-verify] +enabled = true +builder_id = "" + +# Immutable releases (draft-then-publish): Draft-then-publish release.yml so assets attach before publish — compatible with GitHub release immutability (Settings -> Releases); opt-in, supersedes the modular release-sign/slsa flow +[controls.release-immutability] +enabled = false + +# Octo STS federation: Short-lived repo-scoped GitHub credentials replacing long-lived PATs +[controls.octo-sts] +enabled = true + +# Harden-Runner: StepSecurity Harden-Runner egress/tamper monitoring in every workflow +[controls.harden-runner] +enabled = true + +# Witness (optional): Richer in-toto attestation capture and policy around build steps +[controls.witness] +enabled = false + +# ── Phase 4 — Deeper code security & CI hardening ── + +# SAST (OpenGrep default): OpenGrep rule-driven SAST in pre-commit and CI; Semgrep selectable +[controls.sast] +enabled = true +engine = "opengrep" +pre_commit = false +rules = ".sscsb/rules" + +# Sighthound (optional): Ultra-fast local pre-commit SAST layer +[controls.sighthound] +enabled = false + +# CodeQL: Deep interprocedural analysis on PRs and default branch +[controls.codeql] +enabled = true + +# ClusterFuzzLite fuzzing: Continuous fuzzing on PRs (cargo-fuzz + ClusterFuzzLite) — the OpenSSF-Scorecard Rust fuzzing probe; opt-in (needs project fuzz targets) +[controls.fuzzing] +enabled = false + +# Extended workflow audit: pull_request_target misuse, script injection, credential persistence, secret echo, risky actions +[controls.workflow-audit-extended] +enabled = true + +# StepSecurity secure-repo: Onboarding accelerator via app.stepsecurity.io (web service, not an action) +[controls.secure-repo] +enabled = true + +# wait-for-secrets (optional): Human-in-the-loop secret injection for high-sensitivity workflows +[controls.wait-for-secrets] +enabled = false + +# ── Phase 5 — Observability & governance ── + +# Dependency-Track: Continuous SBOM management platform (self-hosted); sscsb uploads BOMs +[controls.dependency-track] +enabled = false +url = "" +project_name = "" + +# GUAC ingestion: Supply-chain knowledge graph over SBOMs, attestations, and VEX +[controls.guac] +enabled = false + +# OpenVEX: Generate and ingest VEX for exploitability-aware triage +[controls.openvex] +enabled = true + +# ORAS OCI storage (optional): Push SBOMs/attestations to an OCI registry as reference artifacts +[controls.oras] +enabled = false + +# OpenSSF Security Insights: Machine-readable security-insights.yml declaring the project's security practices and reporting channels +[controls.security-insights] +enabled = true + +# OpenSSF Best Practices Badge helper: Worksheet pre-filling the passing-badge criteria from installed controls (lifts Scorecard's CII check) +[controls.best-practices-badge] +enabled = true + +# OSPS Baseline assessment: Maps enabled controls to OpenSSF Project Security Baseline families and adds an OSPS column to `sscsb report` +[controls.osps-baseline] +enabled = true + +# Compliance map & report: Machine-readable control → SLSA/SSDF/CRA/OSPS/Badge map behind `sscsb report` +[controls.compliance-map] +enabled = true + diff --git a/.sscsb/hooks/commit-msg b/.sscsb/hooks/commit-msg new file mode 100755 index 0000000..dce9c70 --- /dev/null +++ b/.sscsb/hooks/commit-msg @@ -0,0 +1,13 @@ +#!/bin/sh +# Installed by sscsb (SSCS Bootstrapper). DO NOT EDIT — regenerate with `sscsb init`. +# This shim only delegates; policy logic lives in the sscsb CLI (Rust). +if command -v sscsb >/dev/null 2>&1; then + exec sscsb hook commit-msg "$@" +fi +if [ -n "${SSCSB_BIN:-}" ] && [ -x "${SSCSB_BIN}" ]; then + exec "${SSCSB_BIN}" hook commit-msg "$@" +fi +echo "sscsb: CLI not found on PATH — blocking commit-msg (fail-closed) because" >&2 +echo "sscsb: enabled supply-chain controls cannot run without it." >&2 +echo "sscsb: install sscsb (cargo install --path . / release binary) or set SSCSB_BIN." >&2 +exit 1 diff --git a/.sscsb/hooks/pre-commit b/.sscsb/hooks/pre-commit new file mode 100755 index 0000000..3a0d6a5 --- /dev/null +++ b/.sscsb/hooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh +# Installed by sscsb (SSCS Bootstrapper). DO NOT EDIT — regenerate with `sscsb init`. +# This shim only delegates; policy logic lives in the sscsb CLI (Rust). +if command -v sscsb >/dev/null 2>&1; then + exec sscsb hook pre-commit "$@" +fi +if [ -n "${SSCSB_BIN:-}" ] && [ -x "${SSCSB_BIN}" ]; then + exec "${SSCSB_BIN}" hook pre-commit "$@" +fi +echo "sscsb: CLI not found on PATH — blocking pre-commit (fail-closed) because" >&2 +echo "sscsb: enabled supply-chain controls cannot run without it." >&2 +echo "sscsb: install sscsb (cargo install --path . / release binary) or set SSCSB_BIN." >&2 +exit 1 diff --git a/.sscsb/hooks/pre-push b/.sscsb/hooks/pre-push new file mode 100755 index 0000000..425e5a3 --- /dev/null +++ b/.sscsb/hooks/pre-push @@ -0,0 +1,13 @@ +#!/bin/sh +# Installed by sscsb (SSCS Bootstrapper). DO NOT EDIT — regenerate with `sscsb init`. +# This shim only delegates; policy logic lives in the sscsb CLI (Rust). +if command -v sscsb >/dev/null 2>&1; then + exec sscsb hook pre-push "$@" +fi +if [ -n "${SSCSB_BIN:-}" ] && [ -x "${SSCSB_BIN}" ]; then + exec "${SSCSB_BIN}" hook pre-push "$@" +fi +echo "sscsb: CLI not found on PATH — blocking pre-push (fail-closed) because" >&2 +echo "sscsb: enabled supply-chain controls cannot run without it." >&2 +echo "sscsb: install sscsb (cargo install --path . / release binary) or set SSCSB_BIN." >&2 +exit 1 diff --git a/.sscsb/osps-baseline.md b/.sscsb/osps-baseline.md new file mode 100644 index 0000000..dae3607 --- /dev/null +++ b/.sscsb/osps-baseline.md @@ -0,0 +1,30 @@ +# OSPS Baseline assessment — grcengineering/cvm + +The [OpenSSF Project Security Baseline](https://baseline.openssf.org/) is a set of +minimum, maturity-tiered security requirements. sscsb maps the controls it installs +to the Baseline's requirement families so you can see, and attest to, which the repo +already satisfies. Run `sscsb report` for the live, machine-readable version (the +OSPS column) and `sscsb report --format json` for evidence. + +## Coverage by requirement family + +| OSPS family | Intent | Covered by sscsb controls | +|---|---|---| +| **OSPS-AC** Access Control | protect the primary branch; least-privilege access | `branch-protection`, `commit-signing`, `gittuf` | +| **OSPS-BR** Build & Release | integrity/provenance of source and released assets | `sigstore-signing`, `slsa-provenance`, `github-attestations`, `sbom-attestation`, `provenance-verify`, `model-signing`, `actions-audit` | +| **OSPS-QA** Quality | dependency transparency, review, well-documented code | `sbom`, `renovate`, `package-trust`, `sast` | +| **OSPS-SA** Security Assessment | architecture/security documentation | `security-insights`, `scorecard` | +| **OSPS-VM** Vulnerability Management | reporting process + finding/fixing defects | `vuln-scan`, `codeql`, `fuzzing`, `openvex`, `security-insights` | +| **OSPS-DO** Documentation | user & security docs | `SECURITY.md` (installed), `security-insights` | +| **OSPS-LE** Legal | license present, DCO/CLA | `LICENSE` (confirm) | +| **OSPS-GV** Governance | roles, decision-making | maintainer-authored (`security-insights` roles) | + +## Maturity note + +sscsb's controls target the Baseline's **Level 1–2** technical requirements (the +machine-enforceable ones). Level-3 and the Documentation/Legal/Governance families +include human attestations sscsb cannot auto-satisfy — fill those in your +`security-insights.yml` and project docs. + +_Generated by sscs-bootstrapper. Mappings use real OSPS family IDs from +github.com/ossf/security-baseline. Delete + re-run `sscsb init` to regenerate._ diff --git a/.sscsb/policy/allowed_signers b/.sscsb/policy/allowed_signers new file mode 100644 index 0000000..eaba490 --- /dev/null +++ b/.sscsb/policy/allowed_signers @@ -0,0 +1,2 @@ +# Generated by sscsb from .sscsb/policy/signers.toml — do not edit. +10093271+p4gs@users.noreply.github.com namespaces="git,sscsb-scan-record" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxEAReKc/G/sUxY/UbXprOccUKaxhd+sr/7Gbb40u9w github-signing@1password.p4gs diff --git a/.sscsb/policy/packages.toml b/.sscsb/policy/packages.toml new file mode 100644 index 0000000..9b19770 --- /dev/null +++ b/.sscsb/policy/packages.toml @@ -0,0 +1,14 @@ +# sscsb approved-packages baseline. +# +# A dependency not in this baseline (and not already in the previous manifest +# revision) blocks at commit time until a human approves it: +# sscsb deps check # validate existence + typosquat heuristics +# sscsb deps approve : +# sscsb deps baseline # approve everything currently in manifests +# +# [approved] +# cargo = ["serde"] +# npm = [] +# pypi = [] +# go = [] +# rubygems = [] diff --git a/.sscsb/policy/signers.toml b/.sscsb/policy/signers.toml new file mode 100644 index 0000000..cd8a48a --- /dev/null +++ b/.sscsb/policy/signers.toml @@ -0,0 +1,34 @@ +# sscsb approved-signers policy. +# +# Humans, CI, and AI agents must NEVER share keys or identities. Only signers +# listed here can push to protected branches, and only `class = "human"` +# signers satisfy the human-only protected-branch signing policy. AI agents +# draft changes; they never sign, so no `class = "ai"` entry should ever carry +# a key that is used for signing — the class exists so an AI-associated +# identity can be explicitly DENIED signing rights. +# +# [[signer]] +# principal = "you@example.com" # matches allowed_signers principal +# class = "human" # human | ci | ai +# hardware_backed = true # asserted when the key lives on a YubiKey/secure element +# ssh_public_key = "ssh-ed25519 AAAA... you@example.com" +# # gpg_fingerprint = "ABCD1234..." # for gpg.format=openpgp signers +# +# AI agents may sign ONLY when the `agent-signing` control is enabled, and their +# signatures are ALWAYS rejected on protected branches regardless of any other +# field — humans, CI, and AI never share identities. When agent-signing is on: +# +# [[signer]] +# principal = "agent@ci.example.com" # a DISTINCT identity, never a human's +# class = "ai" # only emitted into allowed_signers when agent-signing is on +# backend = "github-app" # tpm | fido2 | kms | github-app | piv | software +# hardware_backed = true # self-asserted; see attestation_file to back it up +# # attestation_file = ".sscsb/policy/attestations/agent.bin" # out-of-band hardware proof +# # expires = "2027-01-01" # reported by `sscsb verify`; rotate before this +# ssh_public_key = "ssh-ed25519 AAAA... agent@ci.example.com" + +[[signer]] +principal = "10093271+p4gs@users.noreply.github.com" +class = "human" +hardware_backed = false +ssh_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxEAReKc/G/sUxY/UbXprOccUKaxhd+sr/7Gbb40u9w github-signing@1password.p4gs" diff --git a/.sscsb/policy/signing-model.toml b/.sscsb/policy/signing-model.toml new file mode 100644 index 0000000..45b1010 --- /dev/null +++ b/.sscsb/policy/signing-model.toml @@ -0,0 +1,19 @@ +# sscsb signing-model attestations. +# +# These lanes (cloud / web / Codespaces) live behind forge web UIs and App +# installs with NO read API, so sscsb cannot prove them — it records the DATE +# you confirmed each, and `sscsb signing verify` warns when a confirmation goes +# stale (default 180 days). Record with: +# sscsb signing setup --confirm +# after you've done that lane's guided steps. + +# [github-web] +# vigilant_mode = "YYYY-MM-DD" # you enabled "Flag unsigned commits as unverified" +# phishing_resistant_mfa = "YYYY-MM-DD" # you enrolled a passkey / hardware security key + +# [codespaces] +# gpg_verification = "YYYY-MM-DD" # enabled for a SELECTED trusted-repo list (never "all") + +# [cloud-claude] +# github_app_installed = "YYYY-MM-DD" # authorized the Claude GitHub App (not a personal token) +# signing_mode = "app-signed" # app-signed | unsigned-drafts diff --git a/.sscsb/rules/sscsb-default.yaml b/.sscsb/rules/sscsb-default.yaml new file mode 100644 index 0000000..897160f --- /dev/null +++ b/.sscsb/rules/sscsb-default.yaml @@ -0,0 +1,43 @@ +# sscsb default SAST ruleset (Semgrep-compatible - runs under OpenGrep and +# Semgrep identically). Deliberately small: high-signal supply-chain patterns +# that make sense for EVERY repo. Point `controls.sast.rules` at "auto" to use +# the full Semgrep registry instead. +rules: + - id: sscsb.curl-pipe-shell + languages: [generic] + severity: ERROR + message: >- + Piping a remote script directly into a shell executes unverified code - + download, checksum/signature-verify, then execute. + patterns: + - pattern-either: + - pattern-regex: 'curl[^|;\n]*\|\s*(sudo\s+)?(ba|z|da)?sh' + - pattern-regex: 'wget[^|;\n]*\|\s*(sudo\s+)?(ba|z|da)?sh' + paths: + include: ["*.sh", "*.bash", "*.zsh", "Makefile", "*.yml", "*.yaml"] + + - id: sscsb.git-protocol-insecure + languages: [generic] + severity: WARNING + message: >- + Insecure git:// or http:// remote - use https:// or ssh:// so the + transport is authenticated. + pattern-regex: '(git|http)://[a-zA-Z0-9.-]+/[^\s"]+\.git' + + - id: sscsb.npm-install-ignore-scripts-missing + languages: [generic] + severity: WARNING + message: >- + CI npm installs should use `npm ci --ignore-scripts` unless install + scripts are specifically required (install-script malware vector). + pattern-regex: 'npm install(?! .*--ignore-scripts)[^\n]*' + paths: + include: ["*.yml", "*.yaml"] + + - id: sscsb.hardcoded-home-path + languages: [generic] + severity: WARNING + message: Hardcoded user home path reduces portability and can leak usernames. + pattern-regex: '/(Users|home)/[a-z0-9_-]+/' + paths: + include: ["*.sh", "*.yml", "*.yaml", "*.toml"] diff --git a/.sscsb/scan-record.local.json b/.sscsb/scan-record.local.json new file mode 100644 index 0000000..12ac4c1 --- /dev/null +++ b/.sscsb/scan-record.local.json @@ -0,0 +1,734 @@ +{ + "schema_version": 1, + "methodology_version": 2, + "repo": { + "owner": "grcengineering", + "name": "cvm", + "url": "https://github.com/grcengineering/cvm", + "default_branch": "main", + "commit": "12094c5397f09ebac53d43c7203e5ed1e29c0277", + "description": "" + }, + "scanned_at": "2026-09-12T15:59:10Z", + "scanner": { + "sscsb_version": "0.4.0", + "workflow_run_id": 0, + "workflow_run_url": "" + }, + "request_issue": null, + "controls": [ + { + "id": "secrets", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "pre-commit + commit-msg + pre-push shims installed, executable, and unmodified (core.hooksPath=.sscsb/hooks)", + "trufflehog: 3.97.4 (/opt/homebrew/bin/trufflehog)", + "gitleaks: disabled in config" + ] + }, + { + "id": "commit-signing", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "pre-commit + commit-msg + pre-push shims installed, executable, and unmodified (core.hooksPath=.sscsb/hooks)", + "1 approved signer(s), 1 human", + "git config gpg.format = ssh", + "git config user.signingkey = /Users/p4gs/.ssh/github_signing_key.pub", + "signing key does not look hardware-backed (no `-sk`) — spec recommends YubiKey ed25519-sk; software keys weaken the human-accountability model", + "git config commit.gpgSign = true", + "policy: hardware-backed keys required on protected branches" + ] + }, + { + "id": "agent-signing", + "phase": 1, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "signing-model", + "phase": 1, + "in_scope": true, + "raw_outcome": "degraded", + "scan_outcome": "unverified", + "reclassified": true, + "reason": "the control could not be performed on this machine — an unperformed check is never a verdict", + "messages": [ + "human-local: configured", + "agent-claude-code: incomplete — run `sscsb signing setup agent-claude-code`", + "cloud-claude: repo-side attribution is probeable and is not in place — an attestation cannot stand in for it", + "cloud-claude: github_app_installed: not attested — `sscsb signing setup cloud-claude --confirm`", + "github-web: vigilant_mode: not attested — `sscsb signing setup github-web --confirm`", + "github-web: phishing_resistant_mfa: not attested — `sscsb signing setup github-web --confirm`", + "codespaces: gpg_verification: not attested — `sscsb signing setup codespaces --confirm`" + ] + }, + { + "id": "branch-protection", + "phase": 1, + "in_scope": true, + "raw_outcome": "fail", + "scan_outcome": "fail", + "reclassified": false, + "reason": null, + "messages": [ + "main: no rulesets — read via classic branch protection", + "main: required pull requests ✓", + "main: force-push blocking ✓", + "main: required status checks ✓", + "main: deletion protection ✓", + "main: Scorecard — stale-review dismissal ✓", + "main: Scorecard — ≥1 required approving review ✓", + "main: Scorecard gap — code-owner review off (needs a 2nd reviewer; a solo maintainer cannot self-approve — opt in with `sscsb harden branch-protection --require-reviews` once you have one)", + "main: Scorecard gap — last-push approval off (needs a 2nd reviewer; a solo maintainer cannot self-approve — opt in with `sscsb harden branch-protection --require-reviews` once you have one)", + "main: Scorecard — branch-up-to-date (strict) ✓", + "main: MISSING required signed commits — enable 'Require signed commits'", + "master: not found on the remote (the branch record answered for `main` instead) — nothing verified for this name" + ] + }, + { + "id": "actions-audit", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "[info] .github/workflows/release-slsa.yml: `slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0` is tag-pinned by design: slsa-github-generator must be referenced by @vX.Y.Z for slsa-verifier to verify the trusted builder" + ] + }, + { + "id": "gittuf", + "phase": 1, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "ai-trailers", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "enforced by the commit-msg hook", + "pre-commit + commit-msg + pre-push shims installed, executable, and unmodified (core.hooksPath=.sscsb/hooks)" + ] + }, + { + "id": "ai-dep-gate", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "enforced by the commit-msg hook", + "pre-commit + commit-msg + pre-push shims installed, executable, and unmodified (core.hooksPath=.sscsb/hooks)" + ] + }, + { + "id": "pr-template", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "AI-provenance PR template installed (code/tests/deps/docs questions)" + ] + }, + { + "id": "ai-receipts", + "phase": 1, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "binary-artifacts", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "59 tracked file(s), none a compiled program or a code-carrying archive" + ] + }, + { + "id": "webhooks", + "phase": 1, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "grcengineering/cvm: no webhooks configured" + ] + }, + { + "id": "sbom", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "syft 1.51.1: .sscsb/out/sbom.cdx.json validated, 85 component(s) catalogued" + ] + }, + { + "id": "vuln-scan", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "trivy: 0.74.0", + "osv-scanner: 2.5.1", + "1 finding(s) from trivy + osv-scanner; 0 at or above `high` (undetermined severity counts as above)" + ] + }, + { + "id": "dependency-pinning", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "base images digest-pinned, downloads verified, installs pinned, lockfiles committed" + ] + }, + { + "id": "scorecard", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/scorecard.yml installed", + "live scan: no open Scorecard findings 🎉" + ] + }, + { + "id": "renovate", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "renovate.json5 installed (9 key(s))" + ] + }, + { + "id": "package-trust", + "phase": 2, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "new-package approval gate enforced in commit-msg hook", + "pre-commit + commit-msg + pre-push shims installed, executable, and unmodified (core.hooksPath=.sscsb/hooks)", + "approved baseline present (0 package(s))", + "registry existence validation ON for `sscsb deps check` and approvals (anti-slopsquat)", + "typosquat proximity heuristic ON for `sscsb deps check`, approvals, and the commit gate" + ] + }, + { + "id": "bumblebee", + "phase": 2, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "grype", + "phase": 2, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "socket-firewall", + "phase": 2, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "sigstore-signing", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/release-sign.yml installed (1 job(s))" + ] + }, + { + "id": "slsa-provenance", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/release-slsa.yml installed (3 job(s))" + ] + }, + { + "id": "github-attestations", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/release-attest.yml not installed — verified by consolidated evidence in .github/workflows/release.yml instead", + ".github/workflows/release.yml job `provenance`: attests build provenance to GitHub's attestation store with `actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be` under `attestations: write` + `id-token: write`; fires on `push` (tags filter not evaluated)" + ] + }, + { + "id": "sbom-attestation", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/release-attest-sbom.yml installed (2 job(s))" + ] + }, + { + "id": "model-signing", + "phase": 3, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "provenance-verify", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "slsa-verifier: 2.7.1", + "cosign: 3.1.3", + "gate: `sscsb provenance verify --artifact --provenance .intoto.jsonl --source-uri github.com// --builder-id [--source-tag vX.Y.Z]`", + "no builder_id pinned under [controls.provenance-verify] — `provenance verify` will require --builder-id on the command line rather than trusting any builder", + "deploy-gate workflow present (verification before publish)" + ] + }, + { + "id": "release-immutability", + "phase": 3, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "octo-sts", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/octo-sts-example.yml installed (1 job(s))", + ".github/chainguard/sscsb-automation.sts.yaml installed (3 key(s))" + ] + }, + { + "id": "harden-runner", + "phase": 3, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "codeql.yml: harden-runner present in job `analyze`", + "deploy-gate.yml: harden-runner present in job `verify`", + "octo-sts-example.yml: harden-runner present in job `federated-call`", + "release-attest-sbom.yml: harden-runner present in job `attest-sbom`", + "release-attest-sbom.yml: harden-runner present in job `verify`", + "release-sign.yml: harden-runner present in job `sign`", + "release-slsa.yml: harden-runner present in job `build`", + "release-slsa.yml: reusable-workflow only — job `provenance` calls `slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0`, where harden-runner is the called workflow's concern", + "release-slsa.yml: harden-runner present in job `verify`", + "release.yml: harden-runner present in job `build`", + "release.yml: harden-runner present in job `provenance`", + "release.yml: harden-runner present in job `container`", + "release.yml: harden-runner present in job `sbom`", + "sast-opengrep.yml: harden-runner present in job `opengrep`", + "sbom.yml: harden-runner present in job `sbom`", + "scorecard.yml: harden-runner present in job `analysis`", + "secrets-scan.yml: harden-runner present in job `trufflehog`", + "security.yml: harden-runner present in job `audit`", + "security.yml: harden-runner present in job `deny`", + "security.yml: harden-runner present in job `clippy`", + "security.yml: harden-runner present in job `dependency-review`", + "security.yml: harden-runner present in job `sbom`", + "vuln-scan.yml: harden-runner present in job `trivy`", + "vuln-scan.yml: reusable-workflow only — job `osv-scanner` calls `google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2`, where harden-runner is the called workflow's concern" + ] + }, + { + "id": "witness", + "phase": 3, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "sast", + "phase": 4, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "engine: opengrep (rules: /Users/p4gs/Code/grcengineering/cvm/.sscsb/rules)", + "local ruleset present (1 file(s))", + "opengrep: 1.25.0" + ] + }, + { + "id": "sighthound", + "phase": 4, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "codeql", + "phase": 4, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".github/workflows/codeql.yml installed (1 job(s))" + ] + }, + { + "id": "fuzzing", + "phase": 4, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "workflow-audit-extended", + "phase": 4, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "[info] .github/workflows/release-slsa.yml: `slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0` is tag-pinned by design: slsa-github-generator must be referenced by @vX.Y.Z for slsa-verifier to verify the trusted builder" + ] + }, + { + "id": "secure-repo", + "phase": 4, + "in_scope": true, + "raw_outcome": "info", + "scan_outcome": "info", + "reclassified": false, + "reason": null, + "messages": [ + "StepSecurity secure-repo is a web service (app.stepsecurity.io), not an action; run it against this repo to auto-generate hardening PRs. See docs/phase-4.md." + ] + }, + { + "id": "wait-for-secrets", + "phase": 4, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "dependency-track", + "phase": 5, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "guac", + "phase": 5, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "openvex", + "phase": 5, + "in_scope": true, + "raw_outcome": "info", + "scan_outcome": "info", + "reclassified": false, + "reason": null, + "messages": [ + "no OpenVEX documents in .sscsb/out — N/A for this repo until one is generated", + "generate: `sscsb vex create --vuln CVE-… --product pkg:… --status not_affected --justification …`", + "ingest: `sscsb scan --vex ` suppresses not_affected/fixed findings visibly" + ] + }, + { + "id": "oras", + "phase": 5, + "in_scope": false, + "raw_outcome": "disabled", + "scan_outcome": "info", + "reclassified": false, + "reason": "optional control not enabled by this repository", + "messages": [ + "disabled in .sscsb/config.toml" + ] + }, + { + "id": "security-insights", + "phase": 5, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "structurally valid — run `si validate` for full schema conformance" + ] + }, + { + "id": "best-practices-badge", + "phase": 5, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".sscsb/best-practices-badge.md installed (present and non-empty; no machine-checkable structure — its substance is a human judgement sscsb does not assert)" + ] + }, + { + "id": "osps-baseline", + "phase": 5, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + ".sscsb/osps-baseline.md installed (present and non-empty; no machine-checkable structure — its substance is a human judgement sscsb does not assert)" + ] + }, + { + "id": "compliance-map", + "phase": 5, + "in_scope": true, + "raw_outcome": "pass", + "scan_outcome": "pass", + "reclassified": false, + "reason": null, + "messages": [ + "map covers all 47 controls across SLSA/SSDF/CRA/Badge" + ] + } + ], + "score": { + "grade": "A", + "provisional": false, + "overall_percent": 96.6, + "evidence_coverage_percent": 90.6, + "phases": [ + { + "phase": 1, + "pass": 8, + "fail": 1, + "gap": 0, + "unverified": 1, + "info": 0, + "percent": 88.9 + }, + { + "phase": 2, + "pass": 6, + "fail": 0, + "gap": 0, + "unverified": 0, + "info": 0, + "percent": 100.0 + }, + { + "phase": 3, + "pass": 7, + "fail": 0, + "gap": 0, + "unverified": 0, + "info": 0, + "percent": 100.0 + }, + { + "phase": 4, + "pass": 3, + "fail": 0, + "gap": 0, + "unverified": 0, + "info": 1, + "percent": 100.0 + }, + { + "phase": 5, + "pass": 4, + "fail": 0, + "gap": 0, + "unverified": 0, + "info": 1, + "percent": 100.0 + } + ] + }, + "local": { + "record_version": 1, + "lane": "local", + "namespace": "sscsb-scan-record", + "generated_at": "2026-09-12T15:59:10Z", + "sscsb_version": "0.4.0", + "repo": { + "owner": "grcengineering", + "name": "cvm", + "url": "https://github.com/grcengineering/cvm", + "default_branch": "main", + "branch": "chore/sscsb-bootstrap", + "commit": "12094c5397f09ebac53d43c7203e5ed1e29c0277" + }, + "worktree": { + "clean": true, + "tracked_changes": [] + }, + "signer": { + "principal": "10093271+p4gs@users.noreply.github.com", + "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBxEAReKc/G/sUxY/UbXprOccUKaxhd+sr/7Gbb40u9w", + "fingerprint": "SHA256:prXatGO56nl8Or4JdDSzIIcj8hZE1jBxnFaXZOnAPDQ", + "program": "op-ssh-sign" + }, + "allowed_signers": { + "path": ".sscsb/policy/allowed_signers", + "sha256": "9c08731db65f44491bb6ec657e5eea801eb4776441a79690482a69c156e946e1" + } + } +} \ No newline at end of file diff --git a/.sscsb/scan-record.local.json.sig b/.sscsb/scan-record.local.json.sig new file mode 100644 index 0000000..307ff25 --- /dev/null +++ b/.sscsb/scan-record.local.json.sig @@ -0,0 +1,6 @@ +-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgHEQBF4pz8b+xTFj9Rtems5xxQp +rGF36yv/sZtvjS73AAAAARc3Njc2Itc2Nhbi1yZWNvcmQAAAAAAAAABnNoYTUxMgAAAFMA +AAALc3NoLWVkMjU1MTkAAABAhtJ1Fcp5UCJWCy05esJD/RPU5ia9aIU5O+WfwAsYY+jz+N +5qehQvsXxm942Uz1Frq95op1fqvRU3Csrl4SOdDA== +-----END SSH SIGNATURE----- diff --git a/.trufflehog.yaml b/.trufflehog.yaml new file mode 100644 index 0000000..95c5c95 --- /dev/null +++ b/.trufflehog.yaml @@ -0,0 +1,7 @@ +# sscsb sample TruffleHog configuration (.trufflehog.yaml). +# sscsb invokes trufflehog with --results=verified,unknown --fail; this file +# holds project-specific detector tuning. Most repos need nothing here — +# an empty custom-detector list keeps behavior identical to stock trufflehog. +# +# Docs: https://github.com/trufflesecurity/trufflehog#regex-detector-example +detectors: [] diff --git a/Dockerfile b/Dockerfile index a3c4d80..4a9722f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,12 @@ # SSCS: Pin base images by digest, not tag (How-to-Harden control 2.3). -# Renovate will auto-update digests via PR. Run `docker pull` + `docker inspect` -# to get current digests if updating manually. +# Renovate will auto-update digests via PR. To refresh a digest manually: +# docker buildx imagetools inspect : --format '{{.Manifest.Digest}}' +# Digests below resolved 2026-09-12. The tag is kept alongside the digest so +# Renovate can still track the tag; the digest is what actually gets pulled. # Stage 1: Build # renovate: datasource=docker depName=cgr.dev/chainguard/rust -FROM cgr.dev/chainguard/rust:latest-dev AS builder +FROM cgr.dev/chainguard/rust:latest-dev@sha256:2797424582bc3fab467e27cfc7b1a66d31ac57a6b5b4bd8f2d90059a6a1ec9d8 AS builder WORKDIR /app COPY . . RUN cargo build --release --target x86_64-unknown-linux-musl \ @@ -12,7 +14,7 @@ RUN cargo build --release --target x86_64-unknown-linux-musl \ # Stage 2: Runtime (distroless, ~2-3 MB, nonroot by default) # renovate: datasource=docker depName=cgr.dev/chainguard/static -FROM cgr.dev/chainguard/static:latest +FROM cgr.dev/chainguard/static:latest@sha256:207a5673ab31ed83332e54ae33d0f1de4adb5984bd93b8309789889e7bf30ba6 COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/cvm /cvm USER nonroot ENTRYPOINT ["/cvm"] diff --git a/deny.toml b/deny.toml index cbea94f..afbd6ee 100644 --- a/deny.toml +++ b/deny.toml @@ -1,12 +1,22 @@ +# cargo-deny policy. Schema is cargo-deny v2 (0.18+): the v1 keys this file +# used to carry — `advisories.vulnerability`, `licenses.unlicensed`, +# `licenses.copyleft` — were removed upstream, and `advisories.unmaintained` +# became a SCOPE, not a severity. cargo-deny 0.19.4 refuses to deserialize the +# old shape at all, so the `deny` job in .github/workflows/security.yml could +# never have run. Same intent, expressed in the schema the tool reads: +# * vulnerabilities are denied by default in v2 (no key needed); +# * anything absent from `licenses.allow` is denied, so copyleft and +# unlicensed crates are refused by construction. + [advisories] -vulnerability = "deny" -unmaintained = "warn" +# "all" is the strongest scope: an unmaintained advisory anywhere in the graph, +# direct or transitive, is a finding. (The old `= "warn"` had no v2 equivalent; +# v2 has no warn level, so this tightens rather than relaxes.) +unmaintained = "all" yanked = "deny" [licenses] -unlicensed = "deny" allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-3.0", "Zlib"] -copyleft = "deny" [bans] wildcards = "deny" diff --git a/renovate.json b/renovate.json deleted file mode 100644 index 37c2955..0000000 --- a/renovate.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended"], - "minimumReleaseAge": "3 days", - "packageRules": [ - { - "description": "Production dependencies: 3-day cool-down", - "matchDepTypes": ["dependencies"], - "minimumReleaseAge": "3 days" - }, - { - "description": "Dev/build dependencies: 1-day cool-down", - "matchDepTypes": ["devDependencies", "build-dependencies"], - "minimumReleaseAge": "1 day" - }, - { - "description": "Security patches: no cool-down (apply immediately)", - "matchUpdateTypes": ["patch"], - "matchDepTypes": ["dependencies"], - "isVulnerabilityAlert": true, - "minimumReleaseAge": "0 days" - }, - { - "description": "GitHub Actions: 3-day cool-down + SHA pinning", - "matchManagers": ["github-actions"], - "minimumReleaseAge": "3 days", - "pinDigests": true - } - ] -} diff --git a/renovate.json5 b/renovate.json5 new file mode 100644 index 0000000..bd86076 --- /dev/null +++ b/renovate.json5 @@ -0,0 +1,57 @@ +// Renovate configuration — SINGLE source of truth. +// +// `sscsb init` wrote this file next to the repo's pre-existing `renovate.json`. +// Renovate errors out ("Found multiple config file names") when more than one +// config file is present, which would have silently disabled dependency +// updating altogether — so the two are merged here and `renovate.json` is +// deleted. Everything below that is not from sscsb's baseline came from the +// old `renovate.json` and is preserved. +// +// No trailing commas: sscsb's `renovate` control parses this as comment- +// stripped JSON, and a trailing comma degrades the check even though Renovate +// itself would accept it. +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigestsToSemver", + "security:openssf-scorecard" + ], + "osvVulnerabilityAlerts": true, + "dependencyDashboardOSVVulnerabilitySummary": "all", + "lockFileMaintenance": { + "enabled": true, + "schedule": ["before 3am on monday"] + }, + // Cool-down window: a freshly published version is not proposed until it has + // survived in the wild. Primary defence against a compromised release + // (Shai-Hulud, chalk/debug, tj-actions) reaching us on day zero. + "minimumReleaseAge": "3 days", + "packageRules": [ + { + "description": "Production dependencies: 3-day cool-down", + "matchDepTypes": ["dependencies"], + "minimumReleaseAge": "3 days" + }, + { + "description": "Dev/build dependencies: 1-day cool-down", + "matchDepTypes": ["devDependencies", "build-dependencies"], + "minimumReleaseAge": "1 day" + }, + { + "description": "GitHub Actions: 3-day cool-down + SHA pinning", + "matchManagers": ["github-actions"], + "minimumReleaseAge": "3 days", + "pinDigests": true + } + ], + // Known-vulnerability fixes bypass the cool-down entirely: minimumReleaseAge + // null here overrides the 3-day default above for alert PRs only. Replaces + // the old config's `isVulnerabilityAlert` packageRule, which Renovate + // deprecated in favour of this block. + "vulnerabilityAlerts": { + "labels": ["security"], + "minimumReleaseAge": null + }, + "prConcurrentLimit": 5 +} diff --git a/security-insights.yml b/security-insights.yml new file mode 100644 index 0000000..d2abf2c --- /dev/null +++ b/security-insights.yml @@ -0,0 +1,41 @@ +# OpenSSF Security Insights — machine-readable security posture for this project. +# Spec: https://github.com/ossf/security-insights-spec (schema v2). +# sscsb generated this valid starter. Any placeholder values sscsb leaves behind +# need real information filled in — then validate with si-tooling (`si validate`). +# `sscsb verify security-insights` reports Info until every placeholder is gone. +# +# Only the `header` + `project` blocks are emitted (both validate on their own). +# Add the optional `repository:` block (status, core-team, license, security +# posture) when you're ready to attest repository-level details. +header: + schema-version: "2.0.0" + last-updated: "2026-09-12" + last-reviewed: "2026-09-12" + url: "https://github.com/grcengineering/cvm/blob/main/security-insights.yml" + comment: | + Generated by sscs-bootstrapper. Supply-chain controls (secret scanning, + signing, SBOM, vulnerability scanning, SLSA provenance) are enforced via the + .sscsb config and .github/workflows installed by `sscsb init`. +project: + name: "grcengineering/cvm" + homepage: "https://github.com/grcengineering/cvm" + administrators: + - name: "Justin Pagano" + affiliation: "GRC Engineering" + email: "security@grcengineering.com" + primary: true + repositories: + - name: "grcengineering/cvm" + comment: "Primary repository this Security Insights file is published in." + url: "https://github.com/grcengineering/cvm" + vulnerability-reporting: + reports-accepted: true + bug-bounty-available: false + # sscsb installs SECURITY.md; adjust if you relocate the policy. + security-policy: "https://github.com/grcengineering/cvm/blob/main/SECURITY.md" + comment: | + Email security@grcengineering.com (see SECURITY.md for the disclosure + timeline and the supply-chain-compromise path). GitHub private + security advisories on this repository are also accepted. + documentation: + detailed-guide: "https://github.com/grcengineering/cvm/blob/main/SECURITY.md"