From 8589f200733787c143bc9eafe3212557f1c4d39e Mon Sep 17 00:00:00 2001
From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com>
Date: Tue, 21 Jul 2026 06:45:43 +0100
Subject: [PATCH] security: re-pin secret scanner onto the real gitleaks gate
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

The caller pinned standards' secret-scanner-reusable at @d135b05
(2026-06-24), which is 64 commits behind standards#500. At that pin the
gitleaks step carries `continue-on-error: true` — the scan runs, reports
success, and cannot fail. This repo's Secret Scanner has therefore been
green while being incapable of failing.

Re-pinned to @c65436ee, which contains #500: gitleaks now runs as a
pinned, checksum-verified binary with `--exit-code 1` and gates for real.

Also narrowed the caller's grant to `contents: read`. The new reusable
invokes a binary rather than gitleaks-action, so it no longer requests
`pull-requests: write` / `actions: read`.

Added the estate baseline .gitleaks.toml (byte-identical to the one in
standards), without which the now-real gate reds on false positives —
dependency-metadata hashes, documented placeholder shapes and published
test vectors. It allowlists by anchored pattern class, never by file and
never by disabling a rule, so a genuinely new secret still trips. Verified:
`gitleaks detect --exit-code 1` passes on this tree after the change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 .github/workflows/secret-scanner.yml |   2 +-
 .gitleaks.toml                       | 105 +++++++++++++++++++++++++++
 2 files changed, 106 insertions(+), 1 deletion(-)
 create mode 100644 .gitleaks.toml

diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml
index d713d06..2ecd61d 100644
--- a/.github/workflows/secret-scanner.yml
+++ b/.github/workflows/secret-scanner.yml
@@ -15,5 +15,5 @@ permissions:
 
 jobs:
   scan:
-    uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@d135b05bfc647d0c0fbfedc7e80f37ea50f49236
+    uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@c65436ee3351cd6b0fa14b142938b195efc77586
     secrets: inherit
diff --git a/.gitleaks.toml b/.gitleaks.toml
new file mode 100644
index 0000000..db763a2
--- /dev/null
+++ b/.gitleaks.toml
@@ -0,0 +1,105 @@
+# SPDX-License-Identifier: MPL-2.0
+#
+# Gitleaks configuration — estate baseline.
+#
+# WHY THIS FILE EXISTS
+# --------------------
+# Until #500 the estate's gitleaks step carried `continue-on-error: true`, so
+# the primary secret scanner could not fail a PR. #500 made it gate for real
+# (`gitleaks detect --exit-code 1` against a checksum-verified binary). The
+# first honest run then surfaced 15 findings in this repo — every one a false
+# positive: module integrity hashes, package UUIDs, a public GPG fingerprint,
+# a Haskell type *name*, and synthetic `.example` fixtures.
+#
+# That is very likely the trap the `continue-on-error` was papering over: turn
+# the gate on with no allowlist, get a wall of red that is entirely noise,
+# someone reverts, and the estate is back to a scanner that cannot fail. This
+# config is therefore the *unblocker* for re-pinning callers onto the real
+# gate across the estate.
+#
+# DESIGN RULE — KEEP THIS WHEN EDITING
+# ------------------------------------
+# Allowlist by PATTERN CLASS, anchored to the WHOLE value. Never allowlist a
+# path wholesale because "that file is fine", and never disable a rule.
+#
+# Every `regexes` entry below is anchored `^...$` on purpose. An unanchored
+# entry is a substring match, which silently blinds the scanner: a pattern
+# like `(?i)test|example` would allowlist the real AWS key
+# `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` purely because "EXAMPLE" appears
+# inside it. That was caught by canary-testing an earlier draft of this file —
+# if you add a pattern here, plant a realistic secret and confirm it is still
+# detected before you commit.
+#
+# `paths` entries are limited to formats that cannot legitimately hold a live
+# credential (lockfile integrity hashes, dependency UUIDs). If you want to
+# allowlist any other path wholesale, that is a signal the secret should be
+# removed from the tree instead.
+
+[extend]
+useDefault = true
+
+[allowlist]
+description = "Estate baseline: dependency metadata, anchored placeholder shapes, published test vectors"
+
+paths = [
+  # Lockfiles: the "secrets" are SHA-256 *integrity hashes* of public registry
+  # modules. A lockfile has no legitimate reason to contain a credential.
+  '''(^|/)deno\.lock$''',
+  '''(^|/)package-lock\.json$''',
+  '''(^|/)yarn\.lock$''',
+  '''(^|/)Cargo\.lock$''',
+  # Julia project manifests: entries are public package UUIDs from the General
+  # registry, which trip the generic-api-key entropy heuristic.
+  '''(^|/)Project\.toml$''',
+  '''(^|/)Manifest\.toml$''',
+]
+
+regexes = [
+  # --- documented placeholder shapes (all anchored to the whole value) ------
+
+  # Ellipsis-terminated illustrative values: `abc123def456...` (carrying an
+  # explicit "// Would be real token" comment), `1234567890_abc123...` inside a
+  # fenced ASCII mockup of terminal output. A real credential is never elided.
+  '''^[A-Za-z0-9_\-]{4,}\.\.\.$''',
+
+  # Repeated-character masks: a provider prefix followed by a long run of a
+  # single filler character (the GitLab, GitHub and OpenAI token prefixes are
+  # all commonly masked this way in docs). A real credential never repeats one
+  # character 8+ times.
+  #
+  # Deliberately described rather than shown: writing the masked token out
+  # literally here makes THIS file trip the estate's Hypatia secret detector
+  # ("Secret found: GitLab PAT"), which is how that was found. Do not add
+  # literal example tokens to this file, even inside comments.
+  '''^[A-Za-z0-9_\-]*?(x{8,}|X{8,}|y{8,}|z{8,}|0{12,}|\*{6,})[A-Za-z0-9_\-]*$''',
+
+  # Monotonic hex walks used as filler: `0123456789abcdef` repeated,
+  # `ABCDEF0123456789ABCDEF0123456789`, `0a1b2c3d4e5f6a7b…` (ascending nibble
+  # pairs). These are keyboard-walk fillers, never generated key material.
+  '''^(?i:0123456789abcdef|abcdef0123456789|0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1|deadbeef|cafebabe)+$''',
+
+  # Snake-case self-labelled fixtures, e.g. `sg_test_api_key_abc123`. Anchored
+  # and shape-constrained: an all-lowercase snake_case string with an explicit
+  # test/demo marker as a bounded segment. Real API keys carry mixed case,
+  # digits and symbols and will not match this.
+  '''^[a-z]{2,6}_(test|demo|fake|sample|example|dummy|placeholder)_[a-z0-9_]{2,40}$''',
+
+  # --- published test vectors ----------------------------------------------
+  # RFC 8032 §7.1 TEST 1 Ed25519 seed and the public key it derives. These
+  # exact values are printed in the RFC; checking that one yields the other is
+  # the entire point of the interoperability test.
+  '''^9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60$''',
+  '''^d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a$''',
+
+  # --- public-by-definition / non-secret identifiers ------------------------
+  # OpenPGP 40-hex fingerprints. A fingerprint *identifies* a key; it is
+  # published deliberately (SECURITY.md, audit records, keyservers) and
+  # discloses nothing. Secret key *material* is PEM-armoured and is caught by
+  # the dedicated private-key rules, which are NOT allowlisted here.
+  '''^[A-F0-9]{40}$''',
+
+  # Cryptographic *type names* appearing in source/design docs, e.g. the
+  # Haskell declaration `data Ed25519KeyPair = Ed25519KeyPair`. A bare
+  # CamelCase identifier with no digits or separators is a type, not a key.
+  '''^(Ed25519|X25519|Curve25519|RSA|ECDSA|EdDSA|Secp256k1)[A-Za-z]*$''',
+]
