From 28c05e707be422a54f25603b88205c71a7d3a731 Mon Sep 17 00:00:00 2001 From: Dan Barker <1181564+danbarker@users.noreply.github.com> Date: Sat, 14 Nov 2026 22:40:59 +0000 Subject: [PATCH 1/2] leak-check: location-only output for fork pull requests A pull request from a fork now gets one line per place, without the rule number. Branches of the repository and pushes keep rule numbers. The success line no longer prints the number of patterns. Co-Authored-By: Claude Fable 5.2 --- README.md | 4 +++- scripts/leak_check.py | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9f89743..90784d0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ Shared GitHub configuration for Matron repositories. request or push when it adds private context: names, internal hosts, references to private systems. The patterns live in the organisation secret `LEAK_PATTERNS` (one Python regex per line), so the list is never published, -and a hit is reported by rule number and location only. +and a hit is reported by rule number and location only. A pull request from +a fork gets the location alone, without the rule number, and the number of +patterns is never printed. Use it from a repository with `caller-example.yml` (copy it to `.github/workflows/leak-check.yml`). Paths that may legitimately match can be diff --git a/scripts/leak_check.py b/scripts/leak_check.py index 6264730..b0e6703 100644 --- a/scripts/leak_check.py +++ b/scripts/leak_check.py @@ -4,7 +4,9 @@ The patterns come from the LEAK_PATTERNS secret (one Python regex per line, `#` comments allowed), so the list itself is never published. A hit is reported by pattern number and location only: printing the matched text -would put it in a public Actions log. +would put it in a public Actions log. On a pull request from a fork the +pattern number is left out too, so the report is the location alone, and the +number of patterns is never printed. Scanned: lines added by the change, file names, commit messages, and for a pull request its title and description. Co-authored-by and Signed-off-by @@ -217,6 +219,15 @@ def main(): # own work, so its identities must be noreply ones; a fork's contributor # chooses their own address. own_branch = bool(pr) and (pr["head"].get("repo") or {}).get("full_name") == (pr["base"].get("repo") or {}).get("full_name") + # A fork's author can probe the list by trying strings and reading which + # rule answers, so a fork gets the location only. A head repository that + # is missing from the event counts as a fork. + from_fork = bool(pr) and not own_branch + + def pattern_hit(n, prep, place): + if from_fork: + return f"{place} matches a private pattern" + return f"rule on line {n} of LEAK_PATTERNS, {prep} {place}" for sha in git("rev-list", f"{base}..{head}").split(): ident = "\n".join([git("log", "-1", "--format=%an <%ae> / %cn <%ce>", sha)] + credits.get(sha, [])) if any(rx.search(ident) for _, rx in patterns): @@ -238,14 +249,14 @@ def main(): for where, text in texts: for n, rx in patterns: if rx.search(text): - hits.append(f"rule on line {n} of LEAK_PATTERNS, in {where}") + hits.append(pattern_hit(n, "in", where)) images = [] for path, line_no, text in added_lines(base, head): if any(fnmatch.fnmatch(path, g) for g in allow): continue for n, rx in patterns: if rx.search(text): - hits.append(f"rule on line {n} of LEAK_PATTERNS, at {path}:{line_no}") + hits.append(pattern_hit(n, "at", f"{path}:{line_no}")) for path in git("diff", "--name-only", "--diff-filter=A", f"{base}...{head}").splitlines(): if re.search(r"\.(png|jpe?g|gif|webp|heic|mp4|mov)$", path, re.I): images.append(path) @@ -253,12 +264,15 @@ def main(): for p in images: print(f"::warning file={p}::new image or video: check by eye that it shows only made-up data") if hits: + # One line per place for a fork: without rule numbers, several rules + # matching the same place would otherwise repeat it. + hits = list(dict.fromkeys(hits)) for h in hits: print(f"::error::{h}") print(f"{len(hits)} private-context hit(s). The matched text is not printed (this log is public);" " look at the named places, take the private detail out (see the repo's CLAUDE.md), and push again.") return 1 - print(f"No private context found ({len(patterns)} patterns, {len(texts)} texts checked).") + print(f"No private context found ({len(texts)} texts checked).") return 0 From bcbe8a3687021c3b45969ec8931eca6c16cab53e Mon Sep 17 00:00:00 2001 From: Dan Barker <1181564+danbarker@users.noreply.github.com> Date: Sat, 14 Nov 2026 23:23:52 +0000 Subject: [PATCH 2/2] leak-check: number changed file names so each is its own place Co-Authored-By: Claude Fable 5.2 --- scripts/leak_check.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/leak_check.py b/scripts/leak_check.py index b0e6703..12e0d74 100644 --- a/scripts/leak_check.py +++ b/scripts/leak_check.py @@ -208,8 +208,9 @@ def main(): else: body.append(line) texts.append((f"commit {sha[:10]} message", "\n".join(body))) - for path in git("diff", "--name-only", f"{base}...{head}").splitlines(): - texts.append(("file name", path)) + # Numbered, so two matching names stay two places without printing either. + for i, path in enumerate(git("diff", "--name-only", f"{base}...{head}").splitlines(), 1): + texts.append((f"file name {i}", path)) allow = load_allow(base) hits = []