From b7d665833c8d1e8cb48548c0c1eefc7a2b533489 Mon Sep 17 00:00:00 2001 From: Wajid Zahoor Date: Thu, 2 Oct 2025 14:19:57 +0100 Subject: [PATCH 1/5] chore(pre-commit): add gitleaks hook and YAML-only sealed-secrets allowlist --- .gitleaks.toml | 15 +++++++++++++++ .pre-commit-config.yaml | 5 +++++ 2 files changed, 20 insertions(+) create mode 100644 .gitleaks.toml diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..8cb012b9 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,15 @@ +[extend] +useDefault = true + +[[rules]] +id = "generic-api-key" + +# Pattern-only allowlist for long Ag… tokens in YAML +[[rules.allowlists]] +condition = "AND" +regexes = [ + # Boundary-safe Ag… token without lookarounds (RE2-safe) + '''(?:^|[^A-Za-z0-9+/=])(Ag[A-Za-z0-9+/]{500,}={0,2})(?:[^A-Za-z0-9+/=]|$)''' +] +# Limit to YAML only for now. Comment this out if you want it to apply everywhere. +paths = ['''(?i).*\.ya?ml$'''] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ef75b79c..5b01b5a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,3 +30,8 @@ repos: language: system entry: uv sync files: ^(uv\.lock|pyproject\.toml)$ + + - repo: https://github.com/gitleaks/gitleaks + rev: v8.28.0 + hooks: + - id: gitleaks From bfaaa5b80ee7e234f4c93efcaf54e67a0f406725 Mon Sep 17 00:00:00 2001 From: Wajid Zahoor Date: Thu, 2 Oct 2025 14:19:57 +0100 Subject: [PATCH 2/5] tests(gitleaks): add staged-leak checks and YAML allowlist; symlink template/.gitleaks.toml --- template/.gitleaks.toml | 1 + tests/test_gitleaks_precommit.py | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 120000 template/.gitleaks.toml create mode 100644 tests/test_gitleaks_precommit.py diff --git a/template/.gitleaks.toml b/template/.gitleaks.toml new file mode 120000 index 00000000..3a6d50ec --- /dev/null +++ b/template/.gitleaks.toml @@ -0,0 +1 @@ +../.gitleaks.toml \ No newline at end of file diff --git a/tests/test_gitleaks_precommit.py b/tests/test_gitleaks_precommit.py new file mode 100644 index 00000000..f14e9942 --- /dev/null +++ b/tests/test_gitleaks_precommit.py @@ -0,0 +1,85 @@ +from pathlib import Path + +import pytest + +from test_example import copy_project, make_venv + +# --- Stable patterns gitleaks flags out-of-the-box (should FAIL) --- +STABLE_LEAK_CASES = [ + ("github_token.txt", "ghp_1234567890abcdefghijklmnopqrstuvwx12AB"), + ( + "slack_webhook.txt", + "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX", + ), + ("stripe_secret.txt", "sk_test_4eC39HqLyjWDarjtT1zdp7dcFAKE"), +] + + +@pytest.mark.parametrize("fname, content", STABLE_LEAK_CASES) +def test_gitleaks_stable_patterns_fail(tmp_path: Path, fname: str, content: str): + """ + Generate a project, add a known-leaky pattern, stage it, + and verify tox -e pre-commit (gitleaks) fails. + """ + copy_project(tmp_path) + run = make_venv(tmp_path) + + (tmp_path / fname).write_text(content) + run("git add -A") # pre-commit's gitleaks scans the staged index + + with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"): + run("./venv/bin/tox -e pre-commit") + + +# --- Sealed-secrets: YAML/YML allowlisted; non-YAML should be flagged --- +def _fake_sealed_secret_blob(n: int = 800) -> str: + body = ("Qw9+/" * ((n // 4) + 1))[:n] + return "Ag" + body + "==" + + +def test_gitleaks_yaml_allowlist_for_sealed_secrets(tmp_path: Path): + """ + Keep .gitleaks.toml as-is (realistic behavior). + - In .yaml/.yml: blob under spec.encryptedData -> allowlisted -> hook PASS + - In non-YAML: same blob in code -> not allowlisted -> hook FAIL + """ + blob = _fake_sealed_secret_blob() + + sealed_yaml = f"""\ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: demo + namespace: default +spec: + encryptedData: + token: "{blob}" +""" + + # Case 1: .yaml (allowlisted => PASS) + proj_yaml = tmp_path / "proj_yaml" + proj_yaml.mkdir() + copy_project(proj_yaml) + run_yaml = make_venv(proj_yaml) + (proj_yaml / "secret.yaml").write_text(sealed_yaml) + run_yaml("git add -A") + run_yaml("./venv/bin/tox -e pre-commit") + + # Case 2: .yml (allowlisted => PASS) + proj_yml = tmp_path / "proj_yml" + proj_yml.mkdir() + copy_project(proj_yml) + run_yml = make_venv(proj_yml) + (proj_yml / "secret.yml").write_text(sealed_yaml) + run_yml("git add -A") + run_yml("./venv/bin/tox -e pre-commit") + + # Case 3: non-YAML (should be flagged => FAIL) + proj_code = tmp_path / "proj_code" + proj_code.mkdir() + copy_project(proj_code) + run_code = make_venv(proj_code) + (proj_code / "leaky.py").write_text(f'api_key = "{blob}"\n') + run_code("git add -A") + with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"): + run_code("./venv/bin/tox -e pre-commit") From f6e2aa815378e1f15521698fe33142141e422e0c Mon Sep 17 00:00:00 2001 From: Wajid Zahoor Date: Thu, 2 Oct 2025 14:19:58 +0100 Subject: [PATCH 3/5] test(gitleaks): split sealed-secrets tests; use realistic-looking sealed-secret blob --- tests/test_gitleaks_precommit.py | 64 +++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/tests/test_gitleaks_precommit.py b/tests/test_gitleaks_precommit.py index f14e9942..768c0f61 100644 --- a/tests/test_gitleaks_precommit.py +++ b/tests/test_gitleaks_precommit.py @@ -32,16 +32,38 @@ def test_gitleaks_stable_patterns_fail(tmp_path: Path, fname: str, content: str) # --- Sealed-secrets: YAML/YML allowlisted; non-YAML should be flagged --- -def _fake_sealed_secret_blob(n: int = 800) -> str: - body = ("Qw9+/" * ((n // 4) + 1))[:n] - return "Ag" + body + "==" +def _fake_sealed_secret_blob(n: int = 800, seed: str = "sealed-secrets-test") -> str: + """ + Generate a deterministic, base64-looking ciphertext that resembles a SealedSecret. + - Always starts with 'Ag' + - Uses a realistic base64 alphabet mix (via sha256-derived bytes) + - Adds '=' padding only if required by base64 length + - Deterministic for stable tests (change `seed` to vary appearance) + """ + import base64 + import hashlib + + # Build a deterministic byte stream from the seed, not random + chunk = hashlib.sha256(seed.encode("utf-8")).digest() # 32 bytes + raw = (chunk * ((n // len(chunk)) + 4))[: n + 64] # extra slack, then trim + + # Base64-encode -> realistic distribution of A–Z a–z 0–9 + / + b64 = base64.b64encode(raw).decode("ascii") + # Compose with 'Ag' prefix and keep length near n + body = b64.replace("=", "") # remove padding from the body + s = "Ag" + body[:n] # ensure 'Ag' at start -def test_gitleaks_yaml_allowlist_for_sealed_secrets(tmp_path: Path): + # Fix padding so total length is a multiple of 4 (valid base64-looking) + rem = len(s) % 4 + if rem: + s += "=" * (4 - rem) + return s + + +def test_gitleaks_yaml_allowlist_for_sealed_secrets_yaml(tmp_path: Path): """ - Keep .gitleaks.toml as-is (realistic behavior). - - In .yaml/.yml: blob under spec.encryptedData -> allowlisted -> hook PASS - - In non-YAML: same blob in code -> not allowlisted -> hook FAIL + Case 1: .yaml (allowlisted => PASS) """ blob = _fake_sealed_secret_blob() @@ -56,7 +78,6 @@ def test_gitleaks_yaml_allowlist_for_sealed_secrets(tmp_path: Path): token: "{blob}" """ - # Case 1: .yaml (allowlisted => PASS) proj_yaml = tmp_path / "proj_yaml" proj_yaml.mkdir() copy_project(proj_yaml) @@ -65,7 +86,24 @@ def test_gitleaks_yaml_allowlist_for_sealed_secrets(tmp_path: Path): run_yaml("git add -A") run_yaml("./venv/bin/tox -e pre-commit") - # Case 2: .yml (allowlisted => PASS) + +def test_gitleaks_yaml_allowlist_for_sealed_secrets_yml(tmp_path: Path): + """ + Case 2: .yml (allowlisted => PASS) + """ + blob = _fake_sealed_secret_blob() + + sealed_yaml = f"""\ +apiVersion: bitnami.com/v1alpha1 +kind: SealedSecret +metadata: + name: demo + namespace: default +spec: + encryptedData: + token: "{blob}" +""" + proj_yml = tmp_path / "proj_yml" proj_yml.mkdir() copy_project(proj_yml) @@ -74,7 +112,13 @@ def test_gitleaks_yaml_allowlist_for_sealed_secrets(tmp_path: Path): run_yml("git add -A") run_yml("./venv/bin/tox -e pre-commit") - # Case 3: non-YAML (should be flagged => FAIL) + +def test_leaky_code_fails_gitleaks(tmp_path: Path): + """ + Case 3: non-YAML (should be flagged => FAIL) + """ + blob = _fake_sealed_secret_blob() + proj_code = tmp_path / "proj_code" proj_code.mkdir() copy_project(proj_code) From b683927c03bf542c6248bc9fd4823b33e2cd1413 Mon Sep 17 00:00:00 2001 From: Wajid Zahoor Date: Thu, 2 Oct 2025 14:19:58 +0100 Subject: [PATCH 4/5] docs: improve comments in .gitleaks.toml for clarity --- .gitleaks.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitleaks.toml b/.gitleaks.toml index 8cb012b9..9ecf1248 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -1,3 +1,7 @@ +# This allow-list is limited to YAML/YML files to cut down SealedSecrets false positives. +# All gitleaks default rules still apply everywhere (useDefault = true). +# To broaden this allow-list to all files, comment out the 'paths' line below. + [extend] useDefault = true From f1c6b2a44078cff14ddd46bf8fd871b6a88ac787 Mon Sep 17 00:00:00 2001 From: Wajid Zahoor Date: Thu, 2 Oct 2025 15:50:31 +0100 Subject: [PATCH 5/5] test(gitleaks): use .venv/bin/tox (uv creates .venv) --- tests/test_gitleaks_precommit.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_gitleaks_precommit.py b/tests/test_gitleaks_precommit.py index 768c0f61..43f9ce85 100644 --- a/tests/test_gitleaks_precommit.py +++ b/tests/test_gitleaks_precommit.py @@ -28,7 +28,7 @@ def test_gitleaks_stable_patterns_fail(tmp_path: Path, fname: str, content: str) run("git add -A") # pre-commit's gitleaks scans the staged index with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"): - run("./venv/bin/tox -e pre-commit") + run(".venv/bin/tox -e pre-commit") # --- Sealed-secrets: YAML/YML allowlisted; non-YAML should be flagged --- @@ -84,7 +84,7 @@ def test_gitleaks_yaml_allowlist_for_sealed_secrets_yaml(tmp_path: Path): run_yaml = make_venv(proj_yaml) (proj_yaml / "secret.yaml").write_text(sealed_yaml) run_yaml("git add -A") - run_yaml("./venv/bin/tox -e pre-commit") + run_yaml(".venv/bin/tox -e pre-commit") def test_gitleaks_yaml_allowlist_for_sealed_secrets_yml(tmp_path: Path): @@ -110,7 +110,7 @@ def test_gitleaks_yaml_allowlist_for_sealed_secrets_yml(tmp_path: Path): run_yml = make_venv(proj_yml) (proj_yml / "secret.yml").write_text(sealed_yaml) run_yml("git add -A") - run_yml("./venv/bin/tox -e pre-commit") + run_yml(".venv/bin/tox -e pre-commit") def test_leaky_code_fails_gitleaks(tmp_path: Path): @@ -126,4 +126,4 @@ def test_leaky_code_fails_gitleaks(tmp_path: Path): (proj_code / "leaky.py").write_text(f'api_key = "{blob}"\n') run_code("git add -A") with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"): - run_code("./venv/bin/tox -e pre-commit") + run_code(".venv/bin/tox -e pre-commit")