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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 44 additions & 28 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
name: Secret scan (gitleaks)
name: Secret scan (betterleaks)

# Installs the OSS gitleaks binary (no license key required, unlike the
# official gitleaks-action which is paid for org use) and scans either
# the PR diff or the full history depending on the trigger.
# Installs the OSS betterleaks binary and scans either the PR diff or the
# full history depending on the trigger.
#
# betterleaks is the successor to gitleaks, written by gitleaks' original
# author after he lost admin control of that project. MIT, no license key
# required for org use (unlike the official gitleaks-action, which is paid).
# It reads .gitleaks.toml / .gitleaksignore / `gitleaks:allow` comments as
# fallbacks, so an existing gitleaks config keeps working unchanged.

on:
workflow_call:
Expand All @@ -11,12 +16,21 @@ on:
description: "Runner label(s) as a JSON array string (parsed with fromJSON). Pass '[\"self-hosted\", \"linux\", \"x64\"]' to use the self-hosted pool."
type: string
default: '["ubuntu-latest"]'
gitleaks-version:
# renovate: datasource=github-releases depName=gitleaks/gitleaks
betterleaks-version:
# renovate: datasource=github-releases depName=betterleaks/betterleaks
type: string
default: "8.30.1"
default: "1.8.1"
config-path:
description: "Path to .gitleaks.toml. Empty = use gitleaks defaults."
description: "Path to .betterleaks.toml (or a legacy .gitleaks.toml). Empty = betterleaks auto-discovers one in the repo root, else uses defaults."
type: string
default: ""
confidence:
description: |
Minimum confidence to report: low, medium or high. Empty (default)
reports every finding. betterleaks detects considerably more than
gitleaks did, so a repo with documentation placeholders that used to
scan clean may surface low-confidence findings; set 'medium' to
filter those, or pin exact fingerprints in .gitleaksignore.
type: string
default: ""
scan-mode:
Expand All @@ -31,7 +45,7 @@ on:
type: boolean
default: true
upload-artifacts:
description: "Upload the gitleaks SARIF report to GitHub Actions storage. Set false to conserve the account-wide Actions storage quota; the scan still gates the job either way."
description: "Upload the betterleaks SARIF report to GitHub Actions storage. Set false to conserve the account-wide Actions storage quota; the scan still gates the job either way."
type: boolean
default: true
artifact-retention-days:
Expand All @@ -48,37 +62,39 @@ jobs:
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
# Full history is required for `gitleaks detect` to traverse commits.
# Full history is required for `betterleaks git` to traverse commits.
fetch-depth: 0

- name: Install gitleaks
- name: Install betterleaks
env:
VERSION: ${{ inputs.gitleaks-version }}
VERSION: ${{ inputs.betterleaks-version }}
run: |
set -euo pipefail
arch="$(uname -m)"
case "$arch" in
x86_64) gl_arch=x64 ;;
aarch64|arm64) gl_arch=arm64 ;;
x86_64) bl_arch=x64 ;;
aarch64|arm64) bl_arch=arm64 ;;
*) echo "unsupported arch: $arch" >&2; exit 1 ;;
esac
url="https://github.com/gitleaks/gitleaks/releases/download/v${VERSION}/gitleaks_${VERSION}_linux_${gl_arch}.tar.gz"
curl -sSfL -o /tmp/gitleaks.tar.gz "$url"
tar -xzf /tmp/gitleaks.tar.gz -C /tmp gitleaks
sudo install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks
gitleaks version
url="https://github.com/betterleaks/betterleaks/releases/download/v${VERSION}/betterleaks_${VERSION}_linux_${bl_arch}.tar.gz"
curl -sSfL -o /tmp/betterleaks.tar.gz "$url"
tar -xzf /tmp/betterleaks.tar.gz -C /tmp betterleaks
sudo install -m 0755 /tmp/betterleaks /usr/local/bin/betterleaks
betterleaks version

- name: Run gitleaks
- name: Run betterleaks
env:
MODE: ${{ inputs.scan-mode }}
CONFIG: ${{ inputs.config-path }}
CONFIDENCE: ${{ inputs.confidence }}
FAIL: ${{ inputs.fail-on-finding }}
run: |
set -euo pipefail
# No --verbose: findings still land in the SARIF report and still
# fail the job; verbose only floods the log with every scanned commit.
args=(--no-banner --redact --report-format=sarif --report-path=gitleaks.sarif)
args=(--no-banner --redact --report-format=sarif --report-path=betterleaks.sarif)
if [ -n "$CONFIG" ]; then args+=(--config="$CONFIG"); fi
if [ -n "$CONFIDENCE" ]; then args+=(--confidence="$CONFIDENCE"); fi
[ "$FAIL" = "false" ] && args+=(--exit-code=0) || args+=(--exit-code=1)

effective_mode="$MODE"
Expand All @@ -88,30 +104,30 @@ jobs:

case "$effective_mode" in
full)
gitleaks detect "${args[@]}"
betterleaks git "${args[@]}"
;;
pr-diff)
if [ "${{ github.event_name }}" != "pull_request" ]; then
echo "scan-mode=pr-diff but event is ${{ github.event_name }} — nothing to scan"
exit 0
fi
gitleaks detect "${args[@]}" \
betterleaks git "${args[@]}" \
--log-opts="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}"
;;
*)
echo "unknown scan-mode: $effective_mode" >&2; exit 1 ;;
esac

# The SARIF report is a debugging convenience, not the gate — the
# gitleaks step above is what fails the scan on a finding. Don't let an
# artifact-upload failure (e.g. the account-wide Actions storage quota
# betterleaks step above is what fails the scan on a finding. Don't let
# an artifact-upload failure (e.g. the account-wide Actions storage quota
# being hit, which recalculates every 6-12h) turn a clean scan red.
- name: Upload SARIF
if: always() && inputs.upload-artifacts && hashFiles('gitleaks.sarif') != ''
if: always() && inputs.upload-artifacts && hashFiles('betterleaks.sarif') != ''
continue-on-error: true
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: gitleaks-sarif
path: gitleaks.sarif
name: betterleaks-sarif
path: betterleaks.sarif
if-no-files-found: ignore
retention-days: ${{ inputs.artifact-retention-days }}
12 changes: 12 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Suppressed findings (betterleaks reads .gitleaksignore as a fallback for
# .betterleaksignore, so this file works unchanged across the migration).
#
# Format: <commit>:<path>:<rule-id>:<line>
#
# examples/README.md documents a compose-validate caller that passes
# `GRAFANA_ADMIN_PASSWORD=ci-validate` as an env var — a literal placeholder
# for a throwaway CI value, not a credential. betterleaks' generic-password
# rule flags it at low confidence. The match lives in committed history, so a
# `betterleaks:allow` comment on the current line can't clear it; the
# fingerprint has to be pinned here.
526db3a8224309be3e54b652a71b6ff9f97df8d0:examples/README.md:generic-password:159
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,77 @@ project uses [SemVer](https://semver.org/) for the `vMAJOR.MINOR.PATCH` tags.

## [Unreleased]

### Changed

- **BREAKING (consumers): `secret-scan.yml` now scans with
[betterleaks](https://github.com/betterleaks/betterleaks) instead of
gitleaks, and the `gitleaks-version` input is replaced by
`betterleaks-version`** (default `1.8.1`). A caller still passing
`gitleaks-version` will fail validation — drop it, or rename it.

betterleaks is gitleaks' successor, written from scratch by gitleaks'
original author after he lost admin control of that repository and its
name. It is MIT, he retains ownership, and it carries no license key
requirement for org use — the same reason this workflow installed the OSS
gitleaks binary rather than the paid official action.

Compatibility is close to total, which is what made the swap cheap: the
flags this workflow uses (`--no-banner`, `--redact`,
`--report-format=sarif`, `--report-path`, `--config`, `--exit-code`,
`--log-opts`) all exist, release tarballs are named identically in shape,
and betterleaks still reads `.gitleaks.toml`, `GITLEAKS_CONFIG`,
`.gitleaksignore` (same fingerprint format) and `gitleaks:allow` comments as
fallbacks. An existing gitleaks config needs no changes.

**Expect new findings.** betterleaks reports ~98.6% recall against CredData
where gitleaks reports ~70.4%, so a repo that scanned clean may not any
more. This repo was exactly that case: a full-history scan went from *no
leaks* under gitleaks to *1 leak* under betterleaks — a `generic-password`
match, at low confidence, on the `GRAFANA_ADMIN_PASSWORD=ci-validate`
documentation placeholder in `examples/README.md`. Because such matches live
in committed history, a `betterleaks:allow` comment on the current line
cannot clear them; the fingerprint has to be pinned (see Added). PR-diff
mode is unaffected unless the offending line is touched, so the surprise
lands on push/scheduled full-history runs.

- Other reusables are unchanged. The major bump is repo-wide because the tags
are: `@v3` now tracks this line, and the docs are updated to match.

### Added

- `secret-scan.yml` — `confidence` input (`low` / `medium` / `high`, default
empty = report everything). Filters findings below the given confidence,
which is the blunt escape hatch for the recall increase above when pinning
individual fingerprints isn't practical.

- `.gitleaksignore` — pins the one low-confidence false positive in this
repo's own history so the full-history scan and the `pre-push` hook stay
green. Documents the fingerprint format for consumers hitting the same
thing.

### Migration

Callers that never set `gitleaks-version` only need the tag bump:

```yaml
uses: nkg/github-actions/.github/workflows/secret-scan.yml@v3
```

Callers that pinned a version rename the input:

```yaml
with:
betterleaks-version: "1.8.1" # was: gitleaks-version: "8.30.1"
```

If the first run surfaces false positives, either pin fingerprints in
`.gitleaksignore` (preferred — keeps low-confidence detection on) or set
`confidence: medium`. The SARIF artifact is now named `betterleaks-sarif`.

Local tooling moved too: `mise.toml` pins `betterleaks` and `lefthook.yml`'s
pre-commit/pre-push secret scans invoke it, so `mise install` on an existing
clone swaps the binary.

## [2.17.0] - 2026-08-25

### Added
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ doesn't false-positive.
Workflows are consumed by tag. Tags follow `vMAJOR.MINOR.PATCH` and a
floating `vMAJOR` tag tracks the latest non-breaking release on that line.

- Pin to `@v2` for low-friction updates within a major version.
- Pin to `@v2.4.0` (or a SHA) when you need bit-for-bit reproducibility.
- Pin to `@v3` for low-friction updates within a major version.
- Pin to `@v3.0.0` (or a SHA) when you need bit-for-bit reproducibility.

Breaking changes bump the major. See `CHANGELOG.md`.

Expand All @@ -92,7 +92,7 @@ on:

jobs:
test:
uses: nkg/github-actions/.github/workflows/python-uv.yml@v2
uses: nkg/github-actions/.github/workflows/python-uv.yml@v3
with:
python-version: "3.14"
secrets: inherit
Expand Down Expand Up @@ -123,7 +123,7 @@ which default to the self-hosted pool.
```yaml
steps:
- uses: actions/checkout@v6
- uses: nkg/github-actions/.github/actions/setup-mise@v2
- uses: nkg/github-actions/.github/actions/setup-mise@v3
- run: mise run build
```

Expand Down Expand Up @@ -154,7 +154,7 @@ steps:

| Workflow | Purpose |
|-------------------------|------------------------------------------------------------------|
| `secret-scan.yml` | OSS gitleaks; PR-diff or full-history scan, SARIF artifact |
| `secret-scan.yml` | OSS betterleaks; PR-diff or full-history scan, SARIF artifact |
| `container-security.yml`| Trivy scan of every image in a compose file or explicit list |
| `trivy-repo.yml` | Trivy filesystem (lockfiles) or config (IaC) scan of the repo |
| `sops-audit.yml` | Verify SOPS encryption + plaintext-secret scan + shellcheck |
Expand Down Expand Up @@ -202,7 +202,7 @@ are single-job.
## Local development

Git hooks (lefthook) run the same checks as `self-test.yml` — actionlint,
yamllint, composite-action syntax, gitleaks — before commit/push, plus a
yamllint, composite-action syntax, betterleaks — before commit/push, plus a
guard against direct pushes to `main` (bypass: `ALLOW_MAIN_PUSH=1 git push`).
This dogfoods the pattern documented in [examples/pre-push-hook.md](examples/pre-push-hook.md).
One-time setup per clone:
Expand Down
Loading
Loading