fix(redact): env.kv stops flagging cacheKey-style names; prepush exclusion scoped to the push remote

Two calibration/coverage fixes in the redaction guard:

- env.kv's zero-or-more-prefix regex fired on ANY identifier ending in a
  credential suffix, so ordinary code (cacheKey:, sortKey:, partitionKey:,
  hotkey:, even monkey:) with an 8+-char entropic value hit a MEDIUM confirm
  prompt — a gate that cries wolf gets ignored. A name now only counts when
  its shape is credential-semantic: suffix separated by _/-/. (api_key,
  x-access-key, AUTH.TOKEN), a bare suffix (key:, token:), ALL-CAPS env style
  (APIKEY=, MY_APIKEY=), or a camel compound with a credential prefix
  (apiKey, authToken, clientSecret). The value stays capture group 1, so the
  shape check lives in validate (isCredentialShapedEnvName), not the regex.

- gstack-redact-prepush's narrowing excluded commits reachable from ANY
  remote (`--not --remotes`), so a secret that had only ever reached a
  private/local-path remote was never scanned when later pushed to a PUBLIC
  remote. The exclusion is now scoped to the push target
  (`--remotes=<name>/*`) via the remote name git hands pre-push as $1 (the
  installed wrapper already forwards "$@"); stdin/CLI invocations and URL
  pushes without a configured name fall back to the historical all-remotes
  behavior. #2592's catch-up-merge fix is unaffected: upstream commits come
  from the same remote being pushed to.

New coverage: env.kv negative controls (cacheKey/sortKey/partitionKey/
hotkey/monkey/idempotencyKey) + positive controls for all four name shapes;
end-to-end hook tests proving a second-remote secret blocks a push to origin
while origin-published catch-up content still doesn't, plus both fallbacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 14:13:15 -07:00
co-authored by Claude Fable 5
parent 4a95ce61a0
commit 412ad5c1f9
4 changed files with 221 additions and 13 deletions
+23
View File
@@ -183,6 +183,29 @@ describe("MEDIUM demoted credential-shaped patterns (TENSION-1)", () => {
expect(ids("apiKey: YOUR_API_KEY_HERE")).not.toContain("env.kv");
expect(ids("api_key=${MY_VAR}")).not.toContain("env.kv");
});
// T1 calibration: the zero-or-more-prefix net matched ANY identifier ending
// in a suffix, so ordinary code (`cacheKey: <entropic id>`) hit a MEDIUM
// confirm prompt. Name shape must be credential-semantic to count.
test("env.kv ignores non-credential names ending in a suffix (entropic values)", () => {
const v = "8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJ";
expect(ids(`cacheKey: ${v}`)).not.toContain("env.kv");
expect(ids(`sortKey: ${v}`)).not.toContain("env.kv");
expect(ids(`partitionKey: ${v}`)).not.toContain("env.kv");
expect(ids(`hotkey: ${v}`)).not.toContain("env.kv");
expect(ids(`monkey: ${v}`)).not.toContain("env.kv");
expect(ids(`idempotencyKey: ${v}`)).not.toContain("env.kv");
});
test("env.kv still fires on every credential-shaped name form", () => {
const v = "8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJ";
expect(ids(`api_key=${v}`)).toContain("env.kv"); // (i) separator
expect(ids(`API_KEY=${v}`)).toContain("env.kv"); // (i) + ALL-CAPS
expect(ids(`x-access-key: ${v}`)).toContain("env.kv"); // (i) dash separator
expect(ids(`key: ${v}`)).toContain("env.kv"); // (ii) bare suffix
expect(ids(`APIKEY=${v}`)).toContain("env.kv"); // (iii) ALL-CAPS compound
expect(ids(`apiKey: ${v}`)).toContain("env.kv"); // (iv) credential camel
expect(ids(`authToken: ${v}`)).toContain("env.kv"); // (iv) credential camel
expect(ids(`clientSecret: ${v}`)).toContain("env.kv"); // (iv) credential camel
});
test("env.kv stays MEDIUM (calibration: generic net, not a blocker)", () => {
const f = scan("api_key=8Fk2pQ9vXz4wL7mN3rT6yB1cD5eG0hJ", { repoVisibility: "private" })
.findings.find((x) => x.id === "env.kv");