From d7f3208b04d16e9e40e58e9776cfca82175d69fb Mon Sep 17 00:00:00 2001 From: Cameron Brooks <95648474+CameronBrooks11@users.noreply.github.com> Date: Tue, 20 Oct 2026 14:55:00 -0400 Subject: [PATCH] fix(gitleaks): scan merge commits' own diffs git log -p shows no diff for a merge commit, so a secret introduced only in a merge resolution (in neither parent) was never scanned by the Scan step. Pass --log-opts with gitleaks' defaults restated (--all --full-history --diff-filter=tuxdb, as its debug log shows) plus --diff-merges=first-parent. Across every FlowMatrix-AI repo (433 merge commits) this changes no finding counts. tests/gitleaks-scan.test.sh runs the step's exact run: block; its merge case fails against the previous workflow. --- .github/workflows/gitleaks.yml | 7 ++++- tests/gitleaks-scan.test.sh | 54 ++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/gitleaks-scan.test.sh diff --git a/.github/workflows/gitleaks.yml b/.github/workflows/gitleaks.yml index 8c7c8dc..98263ae 100644 --- a/.github/workflows/gitleaks.yml +++ b/.github/workflows/gitleaks.yml @@ -127,4 +127,9 @@ jobs: cfg="" [ -f .gitleaks.toml ] && cfg="--config .gitleaks.toml" # --redact: never print secret values to the (persisted) Actions log - gitleaks git --redact -v $cfg + # --log-opts replaces gitleaks' defaults (--all --full-history + # --diff-filter=tuxdb), so restate them. --diff-merges=first-parent + # adds what git log -p otherwise omits: a merge's own diff, so a + # secret introduced only in a merge resolution is scanned. + gitleaks git --redact -v $cfg \ + --log-opts="--all --full-history --diff-merges=first-parent --diff-filter=tuxdb" diff --git a/tests/gitleaks-scan.test.sh b/tests/gitleaks-scan.test.sh new file mode 100644 index 0000000..fc3b6f7 --- /dev/null +++ b/tests/gitleaks-scan.test.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# Runs the exact "Scan" run: block from gitleaks.yml against throwaway git +# repos. Needs python3 with PyYAML and gitleaks on PATH. +# Usage: bash tests/gitleaks-scan.test.sh [workflow] [org-config] +# +# Token-shaped strings are assembled at runtime and never written to this file. +set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +WF="${1:-$HERE/../.github/workflows/gitleaks.yml}" +ORG="${2:-$HERE/../.gitleaks.toml}" +T="$(mktemp -d)" +trap 'rm -rf "$T"' EXIT +python3 -c " +import yaml,sys +steps=yaml.safe_load(open(sys.argv[1]))['jobs']['scan']['steps'] +print(next(s['run'] for s in steps if s.get('name')=='Scan'))" "$WF" > "$T/step.sh" || exit 1 + +rnd() { python3 -c "import secrets,string;print(''.join(secrets.choice(string.ascii_letters+string.digits) for _ in range($1)))"; } +PAT="ghp""_$(rnd 36)" + +g() { git -C "$1" -c user.email=t@example.com -c user.name=t "${@:2}"; } +mk() { # dir + git init -q -b main "$1" + printf 'base\n' > "$1/m.md"; g "$1" add .; g "$1" commit -q -m base + cp "$ORG" "$1/.gitleaks.toml" +} +run() { # name dir expect + local out rc ok=1 + out=$(cd "$2" && bash --noprofile --norc -eo pipefail "$T/step.sh" 2>&1); rc=$? + { [ "$3" = pass ] && [ $rc -eq 0 ]; } || { [ "$3" = fail ] && [ $rc -ne 0 ]; } || ok=0 + printf '%s' "$out" | grep -qF -- "${PAT#ghp_}" && { ok=0; echo " secret value reached the log"; } + if [ $ok = 1 ]; then echo "ok - $1 (exit $rc)"; else echo "FAIL - $1 (exit $rc, wanted $3)"; echo "$out" | tail -15; FAILED=1; fi +} +FAILED=0 + +mk "$T/a" +run "a clean repo passes" "$T/a" pass + +mk "$T/b"; printf 'PAT: %s\n' "$PAT" >> "$T/b/m.md"; g "$T/b" add m.md; g "$T/b" commit -q -m add +run "a token in an ordinary commit fails" "$T/b" fail + +# In neither parent: only the merge's own diff shows it. +mk "$T/c" +g "$T/c" checkout -q -b side; printf 's\n' > "$T/c/s.md"; g "$T/c" add s.md; g "$T/c" commit -q -m side +g "$T/c" checkout -q main; printf 'n\n' > "$T/c/n.md"; g "$T/c" add n.md; g "$T/c" commit -q -m main +g "$T/c" merge -q --no-commit side >/dev/null 2>&1 +printf 'PAT: %s\n' "$PAT" >> "$T/c/m.md"; g "$T/c" add m.md; g "$T/c" commit -q -m "evil merge" +run "a token introduced only in a merge fails" "$T/c" fail + +mk "$T/d"; g "$T/d" checkout -q -b side; printf 'PAT: %s\n' "$PAT" > "$T/d/x.md" +g "$T/d" add x.md; g "$T/d" commit -q -m x; g "$T/d" checkout -q main +run "a token on a non-HEAD branch fails" "$T/d" fail + +exit $FAILED