diff --git a/.circleci/config.yml b/.circleci/config.yml index 2317533eb..8167c1671 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -227,12 +227,21 @@ jobs: - run: name: Install Gitleaks command: | + set -euo pipefail GITLEAKS_VERSION="8.21.2" - curl -sSfL \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - | tar -xz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks version + GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba" + GITLEAKS_TEMP="$(mktemp -d)" + GITLEAKS_PATH="${GITLEAKS_TEMP}/${GITLEAKS_ARCHIVE}" + trap 'rm -rf -- "${GITLEAKS_TEMP}"' EXIT + + curl --fail --location --silent --show-error --retry 3 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \ + --output "${GITLEAKS_PATH}" + printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict + tar -xzf "${GITLEAKS_PATH}" -C "${GITLEAKS_TEMP}" gitleaks + sudo install -m 0755 "${GITLEAKS_TEMP}/gitleaks" /usr/local/bin/gitleaks + test "$(gitleaks version)" = "${GITLEAKS_VERSION}" - run: name: Run Gitleaks — detect committed secrets command: | diff --git a/.github/README.md b/.github/README.md index 9892f60b0..1a4233b5b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -55,19 +55,17 @@ See also: [product surfaces](https://github.com/szl-holdings/platform/tree/main/ | Workflow | Trigger | Required Check | Purpose | |----------|---------|----------------|---------| -| `ci.yml` | PR + push to `main`/`master` | `CI Gate` | Aggregate gate: lint, typecheck, test, build, integration tests, secret scan, readiness smoke, proof-chain, route security | +| `ci.yml` | PR + push to `main` | Individual checks | Clean-clone validation on Linux and Windows, lint, and TypeScript typecheck | | `ci.yml` | PR + push to `main`/`master` | `Readiness Gate (smoke:product-mode)` | Product-mode API smoke test surfaced separately for fast PR visibility | | `e2e.yml` | PR + push to `main`/`master` | `E2E Gate` | Full Playwright matrix across all artifact surfaces + axe-core a11y | | `dependency-review.yml` | PR only | `dependency-review` | OSS vulnerability scan on changed dependencies | | `codeql.yml` | PR + push + weekly schedule | `analyze` | GitHub CodeQL static analysis (JavaScript/TypeScript) | -### Security (advisory / scheduled) +### Security | Workflow | Trigger | Purpose | |----------|---------|---------| -| `security.yml` | PR + push | Dependency vulnerability scan + SBOM generation | -| `secret-scan-scheduled.yml` | Daily 06:17 UTC + `.gitleaks.toml` changes | Full-history Gitleaks scan against `main`; uploads SARIF to Security tab and opens triage issue on findings | -| `secret-scan.yml` | PR only | PR-diff Gitleaks scan using `.gitleaks.toml` | +| `security.yml` | PR to `main` + push to `main` + manual + Mondays 03:00 UTC | Dependency/SBOM checks, Gitleaks and project-specific secret scans, lockfile integrity, license report, and the fan-in `Security Gate (blocking)` job | ### Build & Quality (advisory) @@ -131,10 +129,11 @@ All Dependabot PRs must pass the same required CI checks as any other PR. ## Secret Scanning -Two complementary layers: +Three complementary layers: -1. **PR-time gate** (`ci.yml` → `secret-scan` job + `secret-scan.yml`): Gitleaks scans the PR diff using `.gitleaks.toml`. Blocks merge on any finding. -2. **Scheduled sweep** (`secret-scan-scheduled.yml`): Full-history Gitleaks scan of `main` every day at 06:17 UTC. Uploads SARIF to the Security tab; opens a triage issue on findings. +1. **GitHub-native scanning and push protection:** provider-known patterns are checked by GitHub, including before accepted pushes when push protection matches. +2. **PR-time scan** (`security.yml` → `secret-scan`): Gitleaks scans the PR's base-to-head commit range, then the project-specific scanner checks the current tree. A finding fails the `Security Gate (blocking)` fan-in job. +3. **Default-branch and scheduled scan** (`security.yml` → `secret-scan`): pushes to `main`, manual dispatches, and the Monday 03:00 UTC schedule scan reachable repository history with Gitleaks and check the current tree with the project-specific scanner. Config lives in `.gitleaks.toml`. If you need to add an allowlist entry, document the reason inline and keep patterns as narrow as possible. diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c527fad15..143b71763 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -72,12 +72,19 @@ jobs: - name: Install Gitleaks run: | + set -euo pipefail GITLEAKS_VERSION="8.21.2" - curl -sSfL \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ - | tar -xz gitleaks - sudo mv gitleaks /usr/local/bin/gitleaks - gitleaks version + GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba" + GITLEAKS_PATH="${RUNNER_TEMP}/${GITLEAKS_ARCHIVE}" + + curl --fail --location --silent --show-error --retry 3 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \ + --output "${GITLEAKS_PATH}" + printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict + tar -xzf "${GITLEAKS_PATH}" -C "${RUNNER_TEMP}" gitleaks + sudo install -m 0755 "${RUNNER_TEMP}/gitleaks" /usr/local/bin/gitleaks + test "$(gitleaks version)" = "${GITLEAKS_VERSION}" - name: Run Gitleaks — detect committed secrets @@ -85,8 +92,8 @@ jobs: PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - # On a pull_request event scan only the commits in the PR; on push/schedule - # scan the entire current HEAD tree. + # On a pull_request event scan only the commits in the PR. Push, scheduled, + # and manual runs scan the repository history reachable from the checkout. if [[ "${{ github.event_name }}" == "pull_request" ]]; then gitleaks detect \ --source . \ diff --git a/ops/github/secret-scanning-runbook.md b/ops/github/secret-scanning-runbook.md index 075f22e22..54e1675d3 100644 --- a/ops/github/secret-scanning-runbook.md +++ b/ops/github/secret-scanning-runbook.md @@ -1,13 +1,13 @@ # Runbook — Secret Scan Hit Triage -Last updated: 2026-04-19 +Last updated: 2026-08-13 Owner: SZL Holdings (repo owner) This runbook covers what to do when **any** of the following fires on the -public `szl-holdings/szl-holdings-platform` repo: +public `szl-holdings/platform` repo: - The PR-time Gitleaks gate (`Security Audit & SBOM / Secret Scan (Gitleaks)` in `.github/workflows/security.yml`) -- The daily scheduled Gitleaks sweep (`Secret Scan (Scheduled — main)` in `.github/workflows/secret-scan-scheduled.yml`) +- The scheduled or manual Gitleaks sweep (`Security Audit & SBOM` in `.github/workflows/security.yml`) - GitHub-native secret scanning alert (Security → Secret scanning) - Push protection bounced a `git push` with a "Secret detected" message @@ -17,14 +17,14 @@ public `szl-holdings/szl-holdings-platform` repo: |---|---|---|---| | GitHub native secret scanning | Repo Settings → Code security → "Secret scanning" | Partner-known token patterns (AWS, GCP, GitHub, Stripe, Slack, …) | On every push to any branch, continuously over history | | GitHub push protection | Repo Settings → Code security → "Push protection" | Same partner-known patterns, **before** the push lands | At `git push` time (blocking) | -| Gitleaks PR gate | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset + repo overrides in `.gitleaks.toml` | Every PR (blocking — required check) | -| Gitleaks scheduled sweep | `.github/workflows/secret-scan-scheduled.yml` | Full Gitleaks ruleset over full `main` history; SARIF → Security tab | Daily 06:17 UTC + manual dispatch | -| Project-specific scanner | `scripts/qa/scan-secrets.js` | Internal naming patterns (belt-and-suspenders) | Inside the PR gate | +| Gitleaks PR gate | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset + repo overrides in `.gitleaks.toml` over the PR base-to-head range | Every PR; a finding fails the fan-in `Security Gate (blocking)` job | +| Gitleaks history sweep | `.github/workflows/security.yml` → `secret-scan` job | Full Gitleaks ruleset over history reachable from the checked-out `main` ref | Pushes to `main`, Mondays 03:00 UTC, and manual dispatch | +| Project-specific scanner | `scripts/qa/scan-secrets.js` | Internal naming patterns (belt-and-suspenders) in the current tree | Every `security.yml` PR, push, scheduled, and manual run | ## One-time GitHub setup (owner-only, click-path) Both of the following must be **enabled** on -`szl-holdings/szl-holdings-platform`. They are free on public repos. +`szl-holdings/platform`. They are free on public repos. 1. **Settings → Code security** 2. Under **Secret scanning** click **Enable**. @@ -46,7 +46,7 @@ scanning: Enabled** and **Push protection: Enabled** with a green check. ### Step 1 — Confirm it's a real secret (not a fixture) -1. Open the alert / SARIF finding / failed run and read the matched line **in the redacted form GitHub shows you**. +1. Open the native alert or failed workflow run and read the matched line **only in the redacted form GitHub shows you**. 2. Check the file path against the known-safe list: - `tests/**`, `**/__fixtures__/**`, `**/*.test.*`, `**/*.spec.*` - `packages/demo-seed/**`, `packages/simulation/**`, `packages/replay-core/**` @@ -67,7 +67,7 @@ The credential is already public — rotation is the only mitigation. - Production runtime: follow `infra/runbooks/RUNBOOK_SECRETS.md`. 3. **Verify rotation** by triggering a workflow run (or `workflow_dispatch` of `Security Audit & SBOM`) that uses the secret and confirming it succeeds. 4. **Record the rotation** as a row in the table at the bottom of this file. -5. **Close the GitHub alert** with the resolution **"Revoked"** (Security → Secret scanning → click the alert → "Close as → Revoked"). For a Gitleaks-triggered triage issue, close it with a comment linking the rotation row. +5. **Close the GitHub alert** with the resolution **"Revoked"** (Security → Secret scanning → click the alert → "Close as → Revoked"). 6. **Sweep history** with `gitleaks detect --log-opts="--all"` locally to confirm no other instance of the same value exists elsewhere in history; if it does, rotate any other affected credential too. ### Step 3 — Prevent the recurrence @@ -75,14 +75,14 @@ The credential is already public — rotation is the only mitigation. For every confirmed leak, add a follow-up in the same PR / issue: - If a developer workflow allowed the leak (e.g., copy-pasting an env value into source), document the safer pattern in `infra/runbooks/RUNBOOK_SECRETS.md`. -- If Gitleaks did not catch the pattern but should have, add a `[[rules]]` entry to `.gitleaks.toml` and re-run the scheduled workflow via **Actions → Secret Scan (Scheduled — main) → Run workflow**. +- If Gitleaks did not catch the pattern but should have, add a `[[rules]]` entry to `.gitleaks.toml` and re-run **Actions → Security Audit & SBOM → Run workflow**. ## Manual on-demand scan -Anyone with `read` on the repo can trigger the scheduled scan ad-hoc: +An authorized maintainer can trigger the scan ad-hoc: ```text -Actions → Secret Scan (Scheduled — main) → Run workflow → main → Run workflow +Actions → Security Audit & SBOM → Run workflow → main → Run workflow ``` Locally: diff --git a/services/customer-portal/.github/workflows/gitleaks.yml b/services/customer-portal/.github/workflows/gitleaks.yml index 6cc925329..208983b26 100644 --- a/services/customer-portal/.github/workflows/gitleaks.yml +++ b/services/customer-portal/.github/workflows/gitleaks.yml @@ -26,13 +26,21 @@ jobs: fetch-depth: 0 - name: Install gitleaks run: | - VER="8.21.2" - curl -sSL "https://github.com/gitleaks/gitleaks/releases/download/v${VER}/gitleaks_${VER}_linux_x64.tar.gz" -o /tmp/gitleaks.tgz - tar -xzf /tmp/gitleaks.tgz -C /tmp gitleaks - sudo install /tmp/gitleaks /usr/local/bin/gitleaks - gitleaks version + set -euo pipefail + GITLEAKS_VERSION="8.21.2" + GITLEAKS_ARCHIVE="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" + GITLEAKS_SHA256="5bc41815076e6ed6ef8fbecc9d9b75bcae31f39029ceb55da08086315316e3ba" + GITLEAKS_PATH="${RUNNER_TEMP}/${GITLEAKS_ARCHIVE}" + + curl --fail --location --silent --show-error --retry 3 \ + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${GITLEAKS_ARCHIVE}" \ + --output "${GITLEAKS_PATH}" + printf '%s %s\n' "${GITLEAKS_SHA256}" "${GITLEAKS_PATH}" | sha256sum --check --strict + tar -xzf "${GITLEAKS_PATH}" -C "${RUNNER_TEMP}" gitleaks + sudo install -m 0755 "${RUNNER_TEMP}/gitleaks" /usr/local/bin/gitleaks + test "$(gitleaks version)" = "${GITLEAKS_VERSION}" - name: Gitleaks scan run: | - CFG="" - [ -f .gitleaks.toml ] && CFG="-c .gitleaks.toml" - gitleaks detect --no-banner --redact ${CFG} --exit-code 1 + GITLEAKS_CONFIG=() + [ -f .gitleaks.toml ] && GITLEAKS_CONFIG=(-c .gitleaks.toml) + gitleaks detect --no-banner --redact "${GITLEAKS_CONFIG[@]}" --exit-code 1