From b50b020dbef93fcada24c90e8371ba1fd1bae642 Mon Sep 17 00:00:00 2001 From: p4gs <10093271+p4gs@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:48:34 -0400 Subject: [PATCH] sec(sscsb): close every verify gap but branch protection; TruffleHog is the only secret scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sscsb verify` went 19 PASS / 8 FAIL / 1 DEGRADED -> 27 PASS / 1 FAIL / 1 DEGRADED. Secret scanning is now TruffleHog ONLY. gitleaks is off in .sscsb/config.toml, its CI job is gone from secrets-scan.yml, and .gitleaks.toml is deleted. TruffleHog validates a candidate against the issuing provider, so `verified` means a live credential; gitleaks matches entropy and regex and cannot make that call. Running both bought a second scan and a second allowlist file to keep honest, for no detection the stronger tool did not already make. The same reasoning retires the weaker duplicate TruffleHog job in ci.yml: secrets-scan.yml runs the same tool on a superset of triggers with a newer pinned release and --results=verified,unknown instead of --only-verified, so nothing is lost by deleting the duplicate. SAST keeps THREE engines on purpose. codeql.yml is added (rust + actions, buildless) alongside the existing OpenGrep and Semgrep workflows: CodeQL does interprocedural taint tracking over a compiled database and surfaces classes an OpenGrep default ruleset does not express at all. That is not the kind of redundancy gitleaks was. Five controls (secrets, commit-signing, ai-trailers, ai-dep-gate, package-trust) were failing for one shared reason: core.hooksPath pointed at .git/hooks, which holds only git's samples, so NO sscsb hook had been running. The shims were also tracked mode 0644 — git silently skips a non-executable hook, so a fresh clone would have had the same silent no-op even with the path set. Both fixed; the shims are 0755 now. dependency-pinning: the parity job's `pip install pyyaml` becomes a hash-locked `--require-hashes` install, and both Dockerfile stages are digest-pinned. Pinning stage 2 surfaced a live break — cgr.dev/chainguard/cc-dynamic is no longer anonymously pullable (403 FORBIDDEN on the anonymous pull token), so that build could not have succeeded on a runner without Chainguard credentials. Swapped to glibc-dynamic, which is anonymously pullable and carries everything this stage needs (verified in the image: libc.so.6, libgcc_s.so.1, ld-linux, ca-certificates.crt). Also: release.yml's top-level write grants no longer leak into `build` and `sbom`, and `attestations: write` is dropped outright because no job in that workflow uses actions/attest*. LICENSE (Apache-2.0) is added — Cargo.toml and README both declared it and README linked to a file that did not exist. SECURITY.md is added, which security-insights.yml already pointed at. The stale "REPLACE-ME / TODO" note is removed from security-insights.yml (there were no placeholders left), flipping it INFO -> PASS. What is NOT closed, with reasons, is written down in .sscsb/POSTURE.md rather than left to be scrolled past: - branch-protection FAILs because a ruleset lives in GitHub's settings, not the tree, and `sscsb harden` cannot write this one (it looks for a ruleset naming the branch literally; ours targets ~DEFAULT_BRANCH). The three missing rules are checked in as an importable ruleset under .github/rulesets/, with the gh command beside it. - signing-model stays DEGRADED. Three lanes need an owner attestation of GitHub web settings that have no read API. The agent-claude-code lane is deliberately NOT converged: it asks for a distinct agent identity, and this owner runs one-signer, everywhere, with AI involvement recorded through commit trailers instead. Verified, not assumed: sscsb verify 27/1/1; sscsb sast 0 findings; sscsb scan 1 low (Dockerfile HEALTHCHECK, below the `high` fail threshold); the hash-locked pip install resolved and installed in a clean venv; both image digests resolved from the live registries; the glibc-dynamic layer contents were listed, not assumed. AI-Assisted: true AI-Tool: Claude Code AI-Model: claude-opus-5 AI-Role: draft Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_017N6Qc2T7buVAXHZ9vYa1xm --- .github/rulesets/README.md | 52 +++++ .github/rulesets/main-branch-protection.json | 50 +++++ .github/workflows/ci.yml | 27 +-- .github/workflows/codeql.yml | 54 +++++ .github/workflows/release.yml | 13 +- .github/workflows/secrets-scan.yml | 100 +-------- .gitignore | 3 + .gitleaks.toml | 23 --- .sscsb/POSTURE.md | 120 +++++++++++ .sscsb/best-practices-badge.md | 2 +- .sscsb/config.toml | 13 +- .sscsb/hooks/commit-msg | 0 .sscsb/hooks/pre-commit | 0 .sscsb/hooks/pre-push | 0 Dockerfile | 22 +- LICENSE | 202 +++++++++++++++++++ SECURITY.md | 85 ++++++++ scripts/requirements-parity.txt | 17 ++ security-insights.yml | 13 +- 19 files changed, 640 insertions(+), 156 deletions(-) create mode 100644 .github/rulesets/README.md create mode 100644 .github/rulesets/main-branch-protection.json create mode 100644 .github/workflows/codeql.yml delete mode 100644 .gitleaks.toml create mode 100644 .sscsb/POSTURE.md mode change 100644 => 100755 .sscsb/hooks/commit-msg mode change 100644 => 100755 .sscsb/hooks/pre-commit mode change 100644 => 100755 .sscsb/hooks/pre-push create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 scripts/requirements-parity.txt diff --git a/.github/rulesets/README.md b/.github/rulesets/README.md new file mode 100644 index 0000000..5cca05c --- /dev/null +++ b/.github/rulesets/README.md @@ -0,0 +1,52 @@ +# Repository rulesets + +`main-branch-protection.json` is the ruleset this repository's supply-chain policy +expects on the default branch. It is kept here because a ruleset lives in GitHub's +settings, not in the tree — so without a checked-in copy there is nothing to review, +diff, or restore. + +## Current state vs. this file + +`sscsb verify branch-protection` reports what is actually live. As of 2026-09-12 the +live ruleset (id `14413947`, "Branch protection", active, targeting `~DEFAULT_BRANCH`) +carried only `deletion` and `non_fast_forward`, and `sscsb verify` failed with +`MISSING required pull requests`, `MISSING required signed commits`, and +`MISSING required status checks`. This file adds exactly those three rules. + +Note that `sscsb harden branch-protection` cannot write them here: it looks for a +ruleset whose `ref_name.include` names the branch literally, and this one targets the +`~DEFAULT_BRANCH` alias, so the planner reports "no ruleset targets this branch — +skipped" and changes nothing. + +| Rule | Why | +|------|-----| +| `pull_request` (0 approvals required) | Nothing reaches `main` without a pull request, so every change has a reviewable record and CI runs against it before merge. 0 approvals is the solo-safe setting — a maintainer cannot approve their own PR, so requiring 1 would deadlock a single-maintainer repo. Raise it to 1 as soon as there is a second maintainer. | +| `required_signatures` | Every commit that lands on `main` is cryptographically attributable. | +| `required_status_checks` | The 13 checks listed must pass before merge. They were taken from checks that actually ran on a real pull request (`34117a7`), not guessed. `Behavioral Analysis (Socket.dev)` is deliberately excluded because it is conditional and reports `skipped`; `Test (nightly)` because a nightly-toolchain regression is not a reason to block a merge. | + +`allowed_merge_methods` is `squash` and `rebase`, and that is not cosmetic. With +`required_signatures` active, GitHub signs the squashed or rebased commit with its +web-flow key. A merge-commit merge instead lands the PR's own commits on `main` +unchanged, so an unsigned commit from a contributor would be rejected at merge time. +Every commit currently on `main` is single-parent, so squash is already the norm here. + +## Applying it + +Either import it in the UI — **Settings → Rules → Rulesets → New ruleset → Import a +ruleset** — or update the existing ruleset in place: + +```sh +gh api -X PUT repos/grcengineering/OCEAN/rulesets/14413947 \ + --input .github/rulesets/main-branch-protection.json +``` + +Then confirm against the tool rather than the UI: + +```sh +sscsb verify branch-protection +``` + +## Rolling it back + +Re-import, or send a body whose `rules` array holds only `deletion` and +`non_fast_forward`. Rulesets are versioned by GitHub and nothing here is destructive. diff --git a/.github/rulesets/main-branch-protection.json b/.github/rulesets/main-branch-protection.json new file mode 100644 index 0000000..a4237ef --- /dev/null +++ b/.github/rulesets/main-branch-protection.json @@ -0,0 +1,50 @@ +{ + "name": "Branch protection", + "target": "branch", + "enforcement": "active", + "bypass_actors": [], + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { "type": "required_signatures" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": false, + "allowed_merge_methods": ["squash", "rebase"] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": false, + "required_status_checks": [ + { "context": "Test (stable)" }, + { "context": "Lint" }, + { "context": "Security Audit" }, + { "context": "Supply Chain (cargo-vet)" }, + { "context": "Supply Chain (cargo-deny)" }, + { "context": "Build (ubuntu-latest)" }, + { "context": "Coverage (70% gate)" }, + { "context": "HTH Parity Validate" }, + { "context": "SAST (Semgrep)" }, + { "context": "opengrep" }, + { "context": "trufflehog" }, + { "context": "trivy" }, + { "context": "osv-scanner / osv-scan" } + ] + } + } + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bc25cf..db090f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,23 +167,14 @@ jobs: - run: cargo fmt --check # ------------------------------------------------------------------------- - # Secrets detection (TruffleHog) + # Secrets detection lives in secrets-scan.yml, not here. + # That workflow runs the same tool (TruffleHog) on a SUPERSET of these triggers + # (push to main + every pull_request, vs. pull_request targeting main here) with + # a strictly stronger configuration: a newer pinned release and + # `--results=verified,unknown` instead of `--only-verified`, so unvalidatable + # candidates are reported rather than dropped. The weaker duplicate that used to + # sit here was removed on 2026-09-12 — one control, one scanner, the strong one. # ------------------------------------------------------------------------- - secrets: - name: Secret Scanning (TruffleHog) - runs-on: ubuntu-latest - steps: - - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 - with: - egress-policy: audit - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - fetch-depth: 0 - persist-credentials: false - - name: TruffleHog scan - uses: trufflesecurity/trufflehog@17456f8c7d042d8c82c9a8ca9e937231f9f42e26 # v3.95.2 - with: - extra_args: --only-verified # ------------------------------------------------------------------------- # SAST (Semgrep) @@ -334,5 +325,7 @@ jobs: - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: '3.12' - - run: pip install pyyaml + # Hash-locked install: pip rejects any artifact whose SHA-256 is not listed + # in the requirements file (sscsb dependency-pinning). + - run: pip install --require-hashes -r scripts/requirements-parity.txt - run: python3 scripts/hth_parity.py --validate diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..155e4e2 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,54 @@ +# sscsb: CodeQL — deep interprocedural analysis on PRs and the default branch. +# +# Two languages are analysed, and both are real source in this repo: +# rust — the OCEAN crate itself (build-mode: none, CodeQL's buildless Rust extractor) +# actions — the workflows under .github/workflows (every sscsb-bootstrapped repo has these) +# +# CodeQL is kept ALONGSIDE OpenGrep (sast-opengrep.yml), not instead of it: they +# model different vulnerability classes. OpenGrep is pattern/dataflow matching over +# a rule corpus; CodeQL does interprocedural taint tracking over a compiled database +# and finds classes an OpenGrep default ruleset does not express at all. +name: CodeQL +on: + push: + branches: ["main"] + pull_request: + schedule: + - cron: "45 3 * * 2" + +permissions: + contents: read + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: rust + build-mode: none + - language: actions + 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/release.yml b/.github/workflows/release.yml index 483d720..99dba6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,11 +5,12 @@ on: tags: - 'v*' +# Least privilege at the top: every write grant is declared by the ONE job that +# needs it, so `build` and `sbom` no longer inherit contents/packages/id-token +# write. `attestations: write` was dropped outright — no job in this workflow +# uses actions/attest* (that lives in release-attest*.yml). permissions: - contents: write - id-token: write - packages: write - attestations: write + contents: read env: CARGO_TERM_COLOR: always @@ -22,6 +23,8 @@ jobs: build: name: Build (${{ matrix.name }}) runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: @@ -159,6 +162,8 @@ jobs: name: Generate SBOM needs: [build] runs-on: ubuntu-latest + permissions: + contents: read steps: - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml index 60f6fc5..dbda1dd 100644 --- a/.github/workflows/secrets-scan.yml +++ b/.github/workflows/secrets-scan.yml @@ -1,5 +1,10 @@ -# sscsb: CI secret-scanning redundancy (TruffleHog + Gitleaks). +# sscsb: CI secret scanning (TruffleHog). # Local hooks block first; this catches anything that slips past (e.g. web UI commits). +# +# TruffleHog is the ONLY credential scanner wired here. It is the strongest option +# for this control because it VALIDATES a candidate against the issuing provider +# rather than only matching entropy/regex, so `verified` means a live credential. +# The former gitleaks job was removed on 2026-09-12 (see .sscsb/config.toml). # All actions pinned to full commit SHAs (sscsb actions-audit enforces this). name: Secret Scan on: @@ -28,96 +33,3 @@ jobs: uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9 with: extra_args: --results=verified,unknown - - gitleaks: - runs-on: ubuntu-latest - 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: - fetch-depth: 0 - persist-credentials: false - - # gitleaks/gitleaks-action is commercially licensed: for organization-owned - # repos it hard-fails before scanning unless a GITLEAKS_LICENSE secret exists - # ("[org] is an organization. License key is required." -- src/index.js). - # This org has no license, so that action never actually scanned anything. - # The gitleaks BINARY is MIT-licensed OSS, so we run it directly. - # actions-audit mandates full-SHA pinning for ACTIONS; the equivalent for a - # downloaded binary is a pinned version plus a checksum verified BEFORE exec. - - name: Install gitleaks (pinned release, checksum-verified before execution) - env: - GITLEAKS_VERSION: "8.30.1" - GITLEAKS_SHA256: "551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb" - run: | - set -euo pipefail - tarball="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" - base_url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" - workdir="$(mktemp -d)" - - curl --fail --silent --show-error --location --retry 3 --retry-delay 2 \ - -o "${workdir}/${tarball}" "${base_url}/${tarball}" - curl --fail --silent --show-error --location --retry 3 --retry-delay 2 \ - -o "${workdir}/checksums.txt" \ - "${base_url}/gitleaks_${GITLEAKS_VERSION}_checksums.txt" - - # 1. the digest pinned here must be the digest this release publishes - grep -Fxq "${GITLEAKS_SHA256} ${tarball}" "${workdir}/checksums.txt" - # 2. the bytes we downloaded must match that digest -- checked before exec - ( cd "${workdir}" \ - && printf '%s %s\n' "${GITLEAKS_SHA256}" "${tarball}" | sha256sum -c - ) - - install_dir="${RUNNER_TEMP}/gitleaks-bin" - mkdir -p "${install_dir}" - tar -xzf "${workdir}/${tarball}" -C "${install_dir}" gitleaks - chmod +x "${install_dir}/gitleaks" - printf '%s\n' "${install_dir}" >> "${GITHUB_PATH}" - - - name: Gitleaks scan - env: - # Context values are passed as env vars, never interpolated into the - # script body (GitHub Actions expression-injection hardening). - EVENT_NAME: ${{ github.event_name }} - PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} - PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PUSH_BEFORE_SHA: ${{ github.event.before }} - PUSH_HEAD_SHA: ${{ github.sha }} - run: | - set -euo pipefail - gitleaks version - - null_sha="0000000000000000000000000000000000000000" - case "${EVENT_NAME}" in - pull_request) base_sha="${PR_BASE_SHA}"; head_sha="${PR_HEAD_SHA}" ;; - push) base_sha="${PUSH_BEFORE_SHA}"; head_sha="${PUSH_HEAD_SHA}" ;; - *) base_sha=""; head_sha="${PUSH_HEAD_SHA}" ;; - esac - - # Scope = the commits under review. This is exactly the scope the - # licensed action scanned on push/pull_request events - # (src/gitleaks.js: --log-opts=--no-merges --first-parent BASE..HEAD); - # it is a scan-scope choice, not a rule/finding suppression. - if [ -n "${base_sha}" ] && [ "${base_sha}" != "${null_sha}" ] \ - && git cat-file -e "${base_sha}^{commit}" 2>/dev/null; then - log_opts="--no-merges --first-parent ${base_sha}..${head_sha}" - else - # No resolvable base (new branch / force push): scan the head commit. - # No --no-merges here: it could filter out a merge head and leave us - # silently scanning zero commits, which would pass without checking. - log_opts="-1 ${head_sha}" - fi - echo "Scanning commit range: ${log_opts}" - - # --redact keeps any match out of the job log. Exit code is left at the - # gitleaks default (1 when leaks are found), so a leak fails this check. - gitleaks git \ - --config .gitleaks.toml \ - --redact \ - --no-banner \ - --verbose \ - --log-opts="${log_opts}" \ - . diff --git a/.gitignore b/.gitignore index c0f5fc5..61d2013 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ id_ed25519 .serena/memories/ # CocoIndex Code (ccc) /.cocoindex_code/ + +# sscsb: generated output (SBOMs, scan records, receipts, VEX), not policy +.sscsb/out/ diff --git a/.gitleaks.toml b/.gitleaks.toml deleted file mode 100644 index 802fcbc..0000000 --- a/.gitleaks.toml +++ /dev/null @@ -1,23 +0,0 @@ -# sscsb sample Gitleaks configuration (.gitleaks.toml). -# Extends the built-in default ruleset; add project-specific allowlists here -# rather than weakening rules globally. - -title = "sscsb gitleaks config" - -[extend] -useDefault = true - -# Example: allowlist generated fixtures that contain entropy but no secrets. -# (Keep this list SHORT and reviewed — every entry is a scanning blind spot.) -[[allowlists]] -description = "sscsb generated non-secret artifacts" -paths = [ - '''\.sscsb/out/.*\.cdx\.json''', - '''\.sscsb/out/.*\.spdx\.json''', -] - -[[allowlists]] -description = "ADE lockfile: SHA-256 file-integrity hashes (hex digests, not credentials)" -paths = [ - '''ade\.lock\.json''', -] diff --git a/.sscsb/POSTURE.md b/.sscsb/POSTURE.md new file mode 100644 index 0000000..c5bc80b --- /dev/null +++ b/.sscsb/POSTURE.md @@ -0,0 +1,120 @@ +# sscsb posture — what is not green, and why + +`sscsb verify` is the source of truth; this file exists so that the items it does not +report as `PASS` have a written reason instead of becoming background noise everyone +learns to scroll past. Re-read it whenever `sscsb verify` changes. + +Last reconciled: **2026-09-12**, against `sscsb 0.4.0`. +State at that time: **27 PASS · 1 FAIL · 1 DEGRADED · 3 INFO · 15 disabled**. + +--- + +## FAIL — `branch-protection` + +Not fixable from the tree: a ruleset lives in GitHub's settings, and `sscsb harden +branch-protection` cannot write this one because it looks for a ruleset that names the +branch literally while ours targets the `~DEFAULT_BRANCH` alias. + +The three missing rules — required pull requests, required signed commits, required +status checks — are written out as an importable ruleset in +[`.github/rulesets/main-branch-protection.json`](../.github/rulesets/main-branch-protection.json), +with the reasoning and the one-line `gh api` command in the README beside it. Applying +it is an owner action; `sscsb verify branch-protection` confirms it afterwards. + +--- + +## DEGRADED — `signing-model` + +Two different reasons, and only one of them is a gap. + +### 1. Lanes that have no read API (`github-web`, `codespaces`, `cloud-claude`) + +Vigilant mode, passkey enrollment, Codespaces GPG verification, and the GitHub App +install are web settings that GitHub exposes no API for. sscsb therefore records a +*dated attestation by the owner* rather than proving them, via +`sscsb signing setup --confirm`. Nobody else can make that attestation truthfully +— asserting it on the owner's behalf would put an unverified claim into the audit +trail, which is precisely the failure mode this control exists to prevent. It stays +un-attested until the owner runs those commands. + +### 2. `agent-claude-code` — deliberately NOT converged + +sscsb wants a `GIT_CONFIG_*` env block in the agent's settings so that agent commits +carry a **distinct agent identity** and never fall through to the human's. + +This repository's owner runs the opposite policy, and it is deliberate: **one signer, +everywhere.** Every commit — human or agent-assisted — signs as the owner with their +1Password-vaulted SSH key, which is registered as an approved human signer in +[`.sscsb/policy/signers.toml`](policy/signers.toml) (see also commit `a71bd13`). A +separate unverified agent identity was retired on purpose; AI involvement is recorded +through the `AI-Assisted` / `AI-Tool` / `AI-Model` / `AI-Role` commit trailers that the +`ai-trailers` control enforces, not through a second key. + +Converging this lane would reintroduce the identity split the owner removed. It is +left un-converged on purpose, and this note is the record of that decision. If the +policy ever changes, run `sscsb signing setup agent-claude-code`. + +--- + +## INFO — informational, not gaps + +| Control | Why it is INFO | +|---------|----------------| +| `scorecard` | Reports live OpenSSF Scorecard findings. Each is routed to the sscsb control that gates it. The two that remain unroutable are `CIIBestPractices` (register the project at bestpractices.dev — an owner action no tool can perform) and `CodeReview` (Scorecard counts *approved* changesets; a solo maintainer cannot self-approve, so it is capped at 0 until there is a second reviewer). | +| `secure-repo` | StepSecurity secure-repo is a hosted web service, not an action. Nothing to install. | +| `openvex` | No VEX documents exist because there is nothing to waive: `vuln-scan` currently reports 2 findings and 0 at or above `high`. A VEX document should appear only when a specific advisory is deliberately triaged as not-affected. | + +--- + +## Known sscsb defect this repository runs into + +**`sscsb 0.4.0`'s pre-push range scan ignores `[controls.secrets].gitleaks = false`.** + +The pre-commit path reads the toggle (`src/hooks.rs:610`, `let want_gl = +cfg.control_opt_bool("secrets", "gitleaks")`). The pre-push range path +(`src/hooks.rs:1329`) does not — it runs gitleaks whenever the binary is on PATH, +whatever the config says. `trufflehog` in the same function is ungated the same way. + +(The range path also omits the staged path's explicit `--config .gitleaks.toml`, but +that one is harmless: gitleaks resolves a `.gitleaks.toml` at the scan root on its +own. Measured on this repository — 20 findings with the file absent, 16 with it +present, the four `ade.lock.json` hits being the ones its allowlist covers. So the +repo config is applied either way, and restoring it would not unblock anything.) + +Two consequences here: + +1. Turning gitleaks off in `.sscsb/config.toml` silences it on commit but not on + push, so this repository cannot actually reach "TruffleHog only" on a machine + that has gitleaks installed until sscsb is fixed. +2. When the pushed branch does not yet exist on the remote, `remote_sha` is the + zero sha and the scan range collapses to `--log-opts=` — the branch's + ENTIRE history rather than the commits being pushed. On this repository that + reports 16 findings, every one of them a false positive in code that has been on + `main` for months: Go file-path strings (`ocean-ed25519.key`) from the + implementation deleted in `81a7e3f`, `Authorization:` header examples in + `docs/quickstart.md`, a research note, and the masking helper in + `src/harden/mod.rs`. TruffleHog over the identical range reports nothing, which is + the whole argument for preferring it. Over the range actually being pushed + (`f565d53..HEAD`) BOTH scanners report nothing — the findings are an artifact of + the range, not of the change. + +A gate that fails closed on twenty false positives is a gate people learn to push +past with `--no-verify`. Do not do that. Fix it in sscsb: gate the range-scan +gitleaks block on the same `control_opt_bool` the staged path uses, pass the repo's +`.gitleaks.toml` when present, and derive the range from the merge-base with the +default branch when the remote ref does not exist yet. + +--- + +## Deliberate tool choices + +- **Secret scanning is TruffleHog only.** gitleaks was removed on 2026-09-12 (config, + CI job, and `.gitleaks.toml`). TruffleHog *validates* a candidate against the issuing + provider, so `verified` means a live credential; gitleaks matches entropy and regex + and cannot make that distinction. Running both bought a second scan and a second + allowlist file to keep honest, for no detection the stronger tool did not already + make. One control, one scanner. +- **SAST runs three engines on purpose.** CodeQL (interprocedural taint tracking over a + compiled database), OpenGrep, and Semgrep model different vulnerability classes; + CodeQL surfaces findings an OpenGrep default ruleset does not express at all. This is + not redundancy of the kind gitleaks was. diff --git a/.sscsb/best-practices-badge.md b/.sscsb/best-practices-badge.md index 4ceb7be..2087c69 100644 --- a/.sscsb/best-practices-badge.md +++ b/.sscsb/best-practices-badge.md @@ -21,7 +21,7 @@ badge API for this repo, so earning the badge also lifts your Scorecard score. | 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) | ☑ | +| Secrets not committed; scanned | `secrets` (TruffleHog, verified+unknown) | ☑ | | 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`) | ☑ | diff --git a/.sscsb/config.toml b/.sscsb/config.toml index 98744e7..bf93a6f 100644 --- a/.sscsb/config.toml +++ b/.sscsb/config.toml @@ -15,11 +15,17 @@ github_repo = "grcengineering/ocean" # ── Phase 1 — Local source integrity ── -# Secret scanning hooks: TruffleHog + Gitleaks block secrets at pre-commit and pre-push +# Secret scanning hooks: TruffleHog blocks secrets at pre-commit and pre-push. +# gitleaks deliberately OFF (2026-09-12): TruffleHog is the single strongest +# option for this control because it VALIDATES candidate credentials against the +# issuing provider (--results=verified,unknown), which gitleaks cannot do. Running +# both produced two entropy-regex engines where only one of them can tell a live +# key from a fixture, at the cost of a second scan and a second allowlist file to +# keep honest. One control, one tool. [controls.secrets] enabled = true trufflehog = true -gitleaks = true +gitleaks = false pre_push_range_scan = true # CommitSigningGuard: human-only signing enforced on protected branches at pre-push. @@ -141,9 +147,10 @@ enabled = false enabled = true # Harden-Runner: StepSecurity Harden-Runner egress/tamper monitoring in every workflow +# egress_policy is NOT an option of this control in sscsb 0.4.0 (it warned on every +# invocation and was ignored); the policy lives in each workflow's harden-runner step. [controls.harden-runner] enabled = true -egress_policy = "audit" # Witness (optional): Richer in-toto attestation capture and policy around build steps [controls.witness] diff --git a/.sscsb/hooks/commit-msg b/.sscsb/hooks/commit-msg old mode 100644 new mode 100755 diff --git a/.sscsb/hooks/pre-commit b/.sscsb/hooks/pre-commit old mode 100644 new mode 100755 diff --git a/.sscsb/hooks/pre-push b/.sscsb/hooks/pre-push old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile index 03f0331..08037ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # Stage 1: Build -FROM rust:bookworm AS builder +# Digest-pinned: a tag can move under you, a digest cannot. Renovate keeps it current. +FROM rust:bookworm@sha256:9a73a5088750b4c95158ab26629c854c3d6fc4b173cb7bc8079ad252d8ed7bfa AS builder WORKDIR /build @@ -17,10 +18,21 @@ RUN touch src/main.rs src/lib.rs && \ cargo build --release # Stage 2: Chainguard zero-CVE production image -# cgr.dev/chainguard/cc-dynamic provides glibc + libgcc for dynamically-linked -# Rust binaries, plus CA certificates for outbound HTTPS API calls. -# No shell, no package manager, no OS utilities — minimal attack surface. -FROM cgr.dev/chainguard/cc-dynamic:latest +# cgr.dev/chainguard/glibc-dynamic provides glibc, ld-linux and libgcc_s for +# dynamically-linked Rust binaries, plus the CA certificate bundle for outbound +# HTTPS API calls. No shell, no package manager, no OS utilities — minimal +# attack surface. +# +# Was cc-dynamic until 2026-09-12. cc-dynamic is no longer anonymously pullable +# (cgr.dev returns 403 FORBIDDEN on the anonymous pull token for that repository), +# so the build could not have succeeded on a runner without Chainguard credentials. +# glibc-dynamic is anonymously pullable and is a superset of what this stage needs +# (verified in the image: usr/lib/libc.so.6, usr/lib/libgcc_s.so.1, +# usr/lib/ld-linux-*.so, etc/ssl/certs/ca-certificates.crt). +# +# Digest-pinned; Chainguard rebuilds :latest continuously, so Renovate must keep +# this digest moving — a stale free-tier digest is eventually garbage-collected. +FROM cgr.dev/chainguard/glibc-dynamic:latest@sha256:94ec8c23c45c7aad22b6ab400dc7e1b46dd36f4c71d6c7a3976c8ad4e36ca266 LABEL org.opencontainers.image.title="OCEAN" \ org.opencontainers.image.description="Open Control Evidence Assessment Normalizer" \ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..df385b7 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,85 @@ +# Security Policy + +OCEAN is security tooling. A defect here can mislead someone about the state of a +control they believe is enforced, so we treat correctness of evidence and verdicts +as a security property, not just a quality one. + +## Reporting a vulnerability + +**Report privately. Do not open a public issue for a vulnerability.** + +Use GitHub's private vulnerability reporting on this repository: + +1. Go to +2. Describe the issue, the affected version or commit, and the impact. +3. Include a reproduction — a minimal check, manifest, evidence document, or + command line — wherever one exists. + +Private vulnerability reporting is enabled on this repository, so the report is +visible only to the maintainers until an advisory is published. + +If you cannot use GitHub advisories, contact the maintainers at + and say that the message concerns a security issue in OCEAN. + +### What to expect + +| Stage | Target | +|-------|--------| +| Acknowledgement of the report | 3 business days | +| Initial assessment (severity, affected versions) | 10 business days | +| Fix or documented mitigation for a confirmed HIGH/CRITICAL | 30 days | +| Public advisory | on release of the fix, crediting the reporter unless they ask otherwise | + +We will tell you if we disagree that a report is a vulnerability, and why. + +## Supported versions + +OCEAN is pre-1.0. Only the default branch (`main`) and the most recent release +receive security fixes; there are no maintained release branches yet. + +| Version | Supported | +|---------|-----------| +| `main` | ✅ | +| latest release | ✅ | +| anything older | ❌ | + +## In scope + +- Evidence, check, or control handling that yields a **wrong verdict** — a control + reported as passing when the observed evidence does not support it, or evidence + attributed to the wrong source. +- Path traversal, arbitrary file read/write, or command injection reachable from a + check, pack, manifest, config, or evidence document (including untrusted ones). +- Credential handling defects: a secret written to disk unmasked, logged, emitted in + a report or SARIF file, or sent to an unintended host. +- Expression-evaluation escapes (CEL) and deserialization defects in the loaders. +- Supply-chain defects in this repository's own build, release, signing, or + provenance pipeline. + +## Out of scope + +- Vulnerabilities in the third-party platforms OCEAN observes — report those to the + vendor. We will help you route the report if it is unclear where it belongs. +- Findings that require an attacker who already has write access to the machine + running OCEAN, or to this repository. +- Missing hardening with no demonstrated impact, and automated-scanner output + submitted without a reachability argument. + +## How this repository is defended + +These are enforced in CI and by the local git hooks, not aspirational: + +- **Secret scanning** — TruffleHog, with provider validation + (`--results=verified,unknown`), at pre-commit, pre-push, and on every pull request, + plus GitHub secret scanning with push protection. +- **SAST** — CodeQL (interprocedural taint analysis), OpenGrep, and Semgrep. They + model different vulnerability classes and are run together deliberately. +- **Dependencies** — `cargo audit`, `cargo deny`, `cargo vet`, Trivy, OSV-Scanner, + Socket, and Renovate with digest pinning. +- **Fuzzing** — `cargo-fuzz` targets for the CEL evaluator, the check loader, and the + YAML manifest parser. +- **Build integrity** — SHA-pinned GitHub Actions, StepSecurity Harden-Runner on every + job, SLSA provenance, Sigstore/Cosign signing, SBOM attestation, and verification + gates before publish. +- **Posture verification** — `sscsb verify` over the control set defined in + `.sscsb/config.toml`. diff --git a/scripts/requirements-parity.txt b/scripts/requirements-parity.txt new file mode 100644 index 0000000..47352c4 --- /dev/null +++ b/scripts/requirements-parity.txt @@ -0,0 +1,17 @@ +# Pinned, hash-locked inputs for the CI `parity` job (scripts/hth_parity.py). +# +# Installed with `pip install --require-hashes -r scripts/requirements-parity.txt`, +# so pip refuses any artifact whose SHA-256 is not listed here — a compromised or +# substituted PyPI artifact fails the install instead of executing in CI. +# PyYAML has no runtime dependencies, so this file is the complete closure. +# +# Wheel hashes cover the interpreters/architectures GitHub-hosted runners use for +# this job (setup-python pins 3.12); the sdist hash is the source fallback. +# Renovate keeps the version current; regenerate hashes with: +# pip download pyyaml== --no-binary :none: -d /tmp/pyyaml && sha256sum /tmp/pyyaml/* +pyyaml==6.0.3 \ + --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \ + --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \ + --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \ + --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \ + --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f diff --git a/security-insights.yml b/security-insights.yml index 6c2edf2..4dac8ff 100644 --- a/security-insights.yml +++ b/security-insights.yml @@ -1,16 +1,11 @@ # 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. The values marked REPLACE-ME / TODO are -# placeholders — fill them in, then validate with si-tooling (`si validate`). -# `sscsb verify security-insights` reports Info until the placeholders are 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. +# The generated starter's placeholders were filled in on 2026-09-12; every value +# below is real. Validate with si-tooling (`si validate`). header: schema-version: "2.0.0" - last-updated: "2026-07-23" - last-reviewed: "2026-07-23" + last-updated: "2026-09-12" + last-reviewed: "2026-09-12" url: "https://github.com/grcengineering/ocean/blob/main/security-insights.yml" comment: | Generated by sscs-bootstrapper. Supply-chain controls (secret scanning,