From cbf69c1163fb50cd05779ca399198158b0c2d065 Mon Sep 17 00:00:00 2001 From: Chris Shuttlesworth Date: Sat, 4 Jul 2026 21:27:35 -0400 Subject: [PATCH] Add reusable ggshield secret-scan workflow + Lefthook hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GitGuardian is currently GitHub-App-only (retroactive dashboard alerts). Add ggshield as a pre-merge gate so leaks are caught before they land, backed by the same GitGuardian dashboard/policy: - ggshield-scan.yml: reusable workflow_call, GitGuardian/ggshield-action over the pushed/PR commit range; requires org secret GITGUARDIAN_API_KEY (callers pass secrets: inherit). - lefthook/base.yml: ggshield pre-commit command alongside gitleaks; self-skips when ggshield isn't installed/authenticated so it never wedges a dev without a token (advisory — gitleaks stays the offline net). - README: document both. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ggshield-scan.yml | 51 +++++++++++++++++++++++++++++ README.md | 32 ++++++++++++++++-- lefthook/base.yml | 14 +++++++- 3 files changed, 93 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ggshield-scan.yml diff --git a/.github/workflows/ggshield-scan.yml b/.github/workflows/ggshield-scan.yml new file mode 100644 index 0000000..d09e32b --- /dev/null +++ b/.github/workflows/ggshield-scan.yml @@ -0,0 +1,51 @@ +# Reusable GitGuardian (ggshield) secret scan. +# +# Runs `ggshield secret scan ci` over the caller's pushed/PR commit range so a +# leaked credential is caught *before* it merges — the pre-merge twin of the +# GitGuardian GitHub App, which only flags secrets retroactively after a push. +# Findings also land in the shared GitGuardian dashboard (policy + false-positive +# management live there — e.g. bws UUIDs are resolved as FPs in the dashboard, +# not ignored in code). +# +# Callers (one per repo) need the org Actions secret GITGUARDIAN_API_KEY and: +# +# jobs: +# ggshield: +# uses: cshuttle/workflows/.github/workflows/ggshield-scan.yml@main +# secrets: inherit +# +# `secrets: inherit` passes the org-level GITGUARDIAN_API_KEY through without +# re-declaring it per caller. +name: ggshield-scan + +# Least privilege: the scan only reads the checkout + calls the GitGuardian API. +permissions: + contents: read + +on: + workflow_call: + secrets: + GITGUARDIAN_API_KEY: + description: GitGuardian API key (scope `scan`); source of truth in bws Infrastructure. + required: true + +jobs: + scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + # ggshield diffs the commit range; it needs full history, not a shallow + # clone, to resolve the base of a push or PR. + fetch-depth: 0 + + - name: ggshield secret scan + uses: GitGuardian/ggshield-action@v1 + env: + # ggshield-action reads these to compute the exact push/PR commit range + # to scan (see GitGuardian's GitHub Actions docs). + GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }} + GITHUB_PUSH_BASE_SHA: ${{ github.event.base }} + GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }} + GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }} diff --git a/README.md b/README.md index da76ae4..f0f8a7b 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,36 @@ jobs: Optional input `paths` (space-separated roots to scan; default `.`). +### `ggshield-scan.yml` + +Runs a GitGuardian [`ggshield`](https://github.com/GitGuardian/ggshield) secret +scan over the caller's pushed/PR commit range — a **pre-merge** gate, unlike the +GitGuardian GitHub App which only flags leaks retroactively. Findings also appear +in the shared GitGuardian dashboard (where policy and false-positives — e.g. bws +UUIDs — are managed; don't obfuscate them in code). + +```yaml +# .github/workflows/ggshield.yml in any repo +name: ggshield +on: + push: + pull_request: +jobs: + ggshield: + uses: cshuttle/workflows/.github/workflows/ggshield-scan.yml@main + secrets: inherit +``` + +Requires the org Actions secret **`GITGUARDIAN_API_KEY`** (scope `scan`; source +of truth in bws Infrastructure). `secrets: inherit` passes it through — no +per-repo secret needed. + ## Git hooks ### `lefthook/base.yml` -Shared advisory pre-commit hooks (shellcheck, gitleaks, yamllint, whitespace / -merge-conflict). Consume from any repo with a tiny `lefthook.yml`: +Shared advisory pre-commit hooks (shellcheck, gitleaks, ggshield, yamllint, +whitespace / merge-conflict). Consume from any repo with a tiny `lefthook.yml`: ```yaml remotes: @@ -47,4 +71,6 @@ remotes: ``` Then `lefthook install` per clone. Tools expected on PATH: `lefthook`, -`shellcheck`, `gitleaks`, `yamllint`. +`shellcheck`, `gitleaks`, `ggshield`, `yamllint`. `ggshield` also needs a +GitGuardian token (`ggshield auth login` or `GITGUARDIAN_API_KEY`); without one +its hook self-skips (advisory) — `gitleaks` still runs offline. diff --git a/lefthook/base.yml b/lefthook/base.yml index 4dc85ac..bca8eb8 100644 --- a/lefthook/base.yml +++ b/lefthook/base.yml @@ -5,8 +5,10 @@ # CI (server-side branch protection isn't available on the Free GitHub plan). # # Per clone: lefthook install (re-run after a pre-push change to wire the hook) -# Tools on PATH: shellcheck, gitleaks, yamllint (pre-commit); +# Tools on PATH: shellcheck, gitleaks, yamllint, ggshield (pre-commit); # kustomize, helm, kubeconform (pre-push, GitOps repos only). +# ggshield additionally needs a GitGuardian token — `ggshield auth login` or +# GITGUARDIAN_API_KEY in env; without it the ggshield hook self-skips (advisory). pre-commit: parallel: true commands: @@ -15,6 +17,16 @@ pre-commit: run: shellcheck {staged_files} gitleaks: run: gitleaks protect --staged --redact --no-banner + ggshield: + # GitGuardian secret scan of the staged changes — the local twin of the + # ggshield-scan CI gate, backed by the same dashboard/policy. Advisory: if + # ggshield isn't installed or has no token it self-skips (never wedges a + # dev who hasn't set up auth); gitleaks above is the always-offline net and + # CI is the hard gate. Auth: `ggshield auth login` or GITGUARDIAN_API_KEY. + run: | + command -v ggshield >/dev/null 2>&1 || { echo "ggshield not installed — skip"; exit 0; } + ggshield api-status >/dev/null 2>&1 || { echo "ggshield not authenticated — skip"; exit 0; } + ggshield secret scan pre-commit yaml-lint: glob: "*.{yml,yaml}" # SOPS-encrypted / Helm-templated files aren't plain YAML