Skip to content

Commit 32c6c7b

Browse files
ci(gitleaks): add pre-commit hook, config with sealed-secrets allowlist, and tests (#287)
## Summary * Enable **gitleaks** as a pre-commit hook to help prevent committing secrets. * Add an **allowlist** for sealed-secrets (long `Ag…` patterns), scoped to `*.yml` / `*.yaml`. * To extend the allowlist all files repo-wide, **comment the entire** `paths` line in `.gitleaks.toml` file. --- ## How to test I verified this change with: ``` pre-commit run --all-files ``` This should include output from **gitleaks**, scanning for potential hardcoded secrets. For local testing, I used: ``` python3 -m venv .venv source .venv/bin/activate pip install pre-commit pre-commit install --hook-type pre-commit pre-commit run --all-files ``` Depending on your environment, testing may happen differently: * **Devcontainer**: pre-commit hooks (including gitleaks) bootstrapped automatically * **CI**: pre-commit hooks run as part of the lint stage (e.g., `tox -e pre-commit`) --- ## Manual spot checks 1. **Secret detection** * Try committing a file with a fake API key/private key → commit should be blocked by gitleaks. 2. **Allowlist works (YAML only)** * Place a sealed-secret-like long `Ag…` token in a `.yaml`/`.yml` file → should be allowlisted. * Place the same token in a non-YAML file → should be flagged. > The allowlist is regex-based and may occasionally cause false positives or false negatives, but it provides better protection than having no scanning in place. --- ## Notes * gitleaks may produce false positives (flagging non-secrets) or false negatives (missing real secrets) * To extend the allowlist beyond YAML, **comment out the entire** `paths` line in `.gitleaks.toml` * For more information, including configuration options for customizing detection rules, refer to the official documentation: https://github.com/gitleaks/gitleaks
1 parent 73c31e3 commit 32c6c7b

File tree

4 files changed

+154
-0
lines changed

4 files changed

+154
-0
lines changed

.gitleaks.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This allow-list is limited to YAML/YML files to cut down SealedSecrets false positives.
2+
# All gitleaks default rules still apply everywhere (useDefault = true).
3+
# To broaden this allow-list to all files, comment out the 'paths' line below.
4+
5+
[extend]
6+
useDefault = true
7+
8+
[[rules]]
9+
id = "generic-api-key"
10+
11+
# Pattern-only allowlist for long Ag… tokens in YAML
12+
[[rules.allowlists]]
13+
condition = "AND"
14+
regexes = [
15+
# Boundary-safe Ag… token without lookarounds (RE2-safe)
16+
'''(?:^|[^A-Za-z0-9+/=])(Ag[A-Za-z0-9+/]{500,}={0,2})(?:[^A-Za-z0-9+/=]|$)'''
17+
]
18+
# Limit to YAML only for now. Comment this out if you want it to apply everywhere.
19+
paths = ['''(?i).*\.ya?ml$''']

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,8 @@ repos:
3030
language: system
3131
entry: uv sync
3232
files: ^(uv\.lock|pyproject\.toml)$
33+
34+
- repo: https://github.com/gitleaks/gitleaks
35+
rev: v8.28.0
36+
hooks:
37+
- id: gitleaks

template/.gitleaks.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../.gitleaks.toml

tests/test_gitleaks_precommit.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
from pathlib import Path
2+
3+
import pytest
4+
5+
from test_example import copy_project, make_venv
6+
7+
# --- Stable patterns gitleaks flags out-of-the-box (should FAIL) ---
8+
STABLE_LEAK_CASES = [
9+
("github_token.txt", "ghp_1234567890abcdefghijklmnopqrstuvwx12AB"),
10+
(
11+
"slack_webhook.txt",
12+
"https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
13+
),
14+
("stripe_secret.txt", "sk_test_4eC39HqLyjWDarjtT1zdp7dcFAKE"),
15+
]
16+
17+
18+
@pytest.mark.parametrize("fname, content", STABLE_LEAK_CASES)
19+
def test_gitleaks_stable_patterns_fail(tmp_path: Path, fname: str, content: str):
20+
"""
21+
Generate a project, add a known-leaky pattern, stage it,
22+
and verify tox -e pre-commit (gitleaks) fails.
23+
"""
24+
copy_project(tmp_path)
25+
run = make_venv(tmp_path)
26+
27+
(tmp_path / fname).write_text(content)
28+
run("git add -A") # pre-commit's gitleaks scans the staged index
29+
30+
with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"):
31+
run(".venv/bin/tox -e pre-commit")
32+
33+
34+
# --- Sealed-secrets: YAML/YML allowlisted; non-YAML should be flagged ---
35+
def _fake_sealed_secret_blob(n: int = 800, seed: str = "sealed-secrets-test") -> str:
36+
"""
37+
Generate a deterministic, base64-looking ciphertext that resembles a SealedSecret.
38+
- Always starts with 'Ag'
39+
- Uses a realistic base64 alphabet mix (via sha256-derived bytes)
40+
- Adds '=' padding only if required by base64 length
41+
- Deterministic for stable tests (change `seed` to vary appearance)
42+
"""
43+
import base64
44+
import hashlib
45+
46+
# Build a deterministic byte stream from the seed, not random
47+
chunk = hashlib.sha256(seed.encode("utf-8")).digest() # 32 bytes
48+
raw = (chunk * ((n // len(chunk)) + 4))[: n + 64] # extra slack, then trim
49+
50+
# Base64-encode -> realistic distribution of A–Z a–z 0–9 + /
51+
b64 = base64.b64encode(raw).decode("ascii")
52+
53+
# Compose with 'Ag' prefix and keep length near n
54+
body = b64.replace("=", "") # remove padding from the body
55+
s = "Ag" + body[:n] # ensure 'Ag' at start
56+
57+
# Fix padding so total length is a multiple of 4 (valid base64-looking)
58+
rem = len(s) % 4
59+
if rem:
60+
s += "=" * (4 - rem)
61+
return s
62+
63+
64+
def test_gitleaks_yaml_allowlist_for_sealed_secrets_yaml(tmp_path: Path):
65+
"""
66+
Case 1: .yaml (allowlisted => PASS)
67+
"""
68+
blob = _fake_sealed_secret_blob()
69+
70+
sealed_yaml = f"""\
71+
apiVersion: bitnami.com/v1alpha1
72+
kind: SealedSecret
73+
metadata:
74+
name: demo
75+
namespace: default
76+
spec:
77+
encryptedData:
78+
token: "{blob}"
79+
"""
80+
81+
proj_yaml = tmp_path / "proj_yaml"
82+
proj_yaml.mkdir()
83+
copy_project(proj_yaml)
84+
run_yaml = make_venv(proj_yaml)
85+
(proj_yaml / "secret.yaml").write_text(sealed_yaml)
86+
run_yaml("git add -A")
87+
run_yaml(".venv/bin/tox -e pre-commit")
88+
89+
90+
def test_gitleaks_yaml_allowlist_for_sealed_secrets_yml(tmp_path: Path):
91+
"""
92+
Case 2: .yml (allowlisted => PASS)
93+
"""
94+
blob = _fake_sealed_secret_blob()
95+
96+
sealed_yaml = f"""\
97+
apiVersion: bitnami.com/v1alpha1
98+
kind: SealedSecret
99+
metadata:
100+
name: demo
101+
namespace: default
102+
spec:
103+
encryptedData:
104+
token: "{blob}"
105+
"""
106+
107+
proj_yml = tmp_path / "proj_yml"
108+
proj_yml.mkdir()
109+
copy_project(proj_yml)
110+
run_yml = make_venv(proj_yml)
111+
(proj_yml / "secret.yml").write_text(sealed_yaml)
112+
run_yml("git add -A")
113+
run_yml(".venv/bin/tox -e pre-commit")
114+
115+
116+
def test_leaky_code_fails_gitleaks(tmp_path: Path):
117+
"""
118+
Case 3: non-YAML (should be flagged => FAIL)
119+
"""
120+
blob = _fake_sealed_secret_blob()
121+
122+
proj_code = tmp_path / "proj_code"
123+
proj_code.mkdir()
124+
copy_project(proj_code)
125+
run_code = make_venv(proj_code)
126+
(proj_code / "leaky.py").write_text(f'api_key = "{blob}"\n')
127+
run_code("git add -A")
128+
with pytest.raises(AssertionError, match=r"(?i)(leak|gitleaks|secret)"):
129+
run_code(".venv/bin/tox -e pre-commit")

0 commit comments

Comments
 (0)