Blame · Line-by-line history
push-policy-newbranch-scan.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 74202f0 | 1 | /** |
| 2 | * Regression: the pre-receive secret scan must inspect ONLY what a push | |
| 3 | * actually introduces — not the entire repo tree. | |
| 4 | * | |
| 5 | * The bug: for a NEW branch (oldSha = zero-SHA), buildPreReceiveScript() used | |
| 6 | * LOG_RANGE="$NEW" and a `git diff <empty-tree> $NEW`, which reports every | |
| 7 | * file in the whole repo as "changed". So pushing a clean feature branch was | |
| 8 | * rejected because a PRE-EXISTING committed test fixture (crafted to look like | |
| 9 | * a real secret, to test the scanner itself) matched a pattern — even though | |
| 10 | * the branch never touched it. This broke the documented | |
| 11 | * "push a feature branch → open a PR" workflow for every new branch. | |
| 12 | * | |
| 13 | * These tests drive the ACTUAL generated hook via a real `git push` into a | |
| 14 | * bare repo (same as production), rather than re-implementing the range logic. | |
| 15 | */ | |
| 16 | import { describe, expect, it, beforeAll } from "bun:test"; | |
| 17 | import { mkdtempSync, writeFileSync } from "fs"; | |
| 18 | import { tmpdir } from "os"; | |
| 19 | import { join } from "path"; | |
| 20 | import { installPackInspectionHook } from "../lib/push-policy"; | |
| 21 | ||
| 22 | // A fake-but-realistic AWS key that is NOT caught by the placeholder filter — | |
| 23 | // the kind of value a scanner-test fixture legitimately commits. | |
| 24 | const FIXTURE_SECRET = "AKIAABCDEFGH1234IJKL"; | |
| 25 | ||
| 26 | function git(cwd: string, args: string[], allowFail = false) { | |
| 27 | const proc = Bun.spawnSync(["git", ...args], { | |
| 28 | cwd, | |
| 29 | env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }, | |
| 30 | }); | |
| 31 | const exitCode = proc.exitCode; | |
| 32 | const stderr = new TextDecoder().decode(proc.stderr); | |
| 33 | if (!allowFail && exitCode !== 0) { | |
| 34 | throw new Error(`git ${args.join(" ")} failed (${exitCode}):\n${stderr}`); | |
| 35 | } | |
| 36 | return { exitCode, stderr, stdout: new TextDecoder().decode(proc.stdout) }; | |
| 37 | } | |
| 38 | ||
| 39 | const GIT_OK = (() => { | |
| 40 | try { | |
| 41 | return Bun.spawnSync(["git", "--version"]).exitCode === 0; | |
| 42 | } catch { | |
| 43 | return false; | |
| 44 | } | |
| 45 | })(); | |
| 46 | ||
| 47 | describe.skipIf(!GIT_OK)("pre-receive scan only inspects pushed changes", () => { | |
| 48 | let bare: string; | |
| 49 | let work: string; | |
| 50 | ||
| 51 | beforeAll(async () => { | |
| 52 | const root = mkdtempSync(join(tmpdir(), "gc-newbranch-")); | |
| 53 | bare = join(root, "bare.git"); | |
| 54 | work = join(root, "work"); | |
| 55 | ||
| 56 | git(root, ["init", "--bare", "-b", "main", bare]); | |
| 57 | git(root, ["init", "-b", "main", work]); | |
| 58 | git(work, ["config", "user.email", "t@example.com"]); | |
| 59 | git(work, ["config", "user.name", "Test"]); | |
| 60 | git(work, ["config", "commit.gpgsign", "false"]); | |
| 61 | ||
| 62 | // Seed main with a pre-existing fixture that CONTAINS a secret pattern, | |
| 63 | // pushed BEFORE the hook is installed (so it lands, exactly like the real | |
| 64 | // fixtures that predate the scanner). | |
| 65 | writeFileSync(join(work, "fixture.ts"), `const K = '${FIXTURE_SECRET}';\n`); | |
| 66 | git(work, ["add", "."]); | |
| 67 | git(work, ["commit", "-m", "seed fixture (has a secret pattern)"]); | |
| 68 | git(work, ["remote", "add", "origin", bare]); | |
| 69 | git(work, ["push", "-u", "origin", "main"]); | |
| 70 | ||
| 71 | // Now arm the pre-receive hook on the bare repo. | |
| 72 | const hook = await installPackInspectionHook([], { secretScan: true }); | |
| 73 | if (!hook) throw new Error("hook install returned null"); | |
| 74 | git(bare, ["config", "core.hooksPath", hook.env.GIT_CONFIG_VALUE_0]); | |
| 75 | }); | |
| 76 | ||
| 77 | it("accepts a new branch that only adds a clean file (does NOT rescan the pre-existing fixture)", () => { | |
| 78 | git(work, ["checkout", "-b", "clean-feature", "main"]); | |
| 79 | writeFileSync(join(work, "clean.ts"), "export const ok = 1;\n"); | |
| 80 | git(work, ["add", "clean.ts"]); | |
| 81 | git(work, ["commit", "-m", "add a clean file"]); | |
| 82 | ||
| 83 | const res = git(work, ["push", "origin", "clean-feature"], true); | |
| 84 | // Before the fix this failed with "Secret scan: possible AWS Access Key | |
| 85 | // in fixture.ts" — a file the branch never touched. | |
| 86 | expect(res.stderr).not.toContain("Secret scan"); | |
| 87 | expect(res.exitCode).toBe(0); | |
| 88 | }); | |
| 89 | ||
| 90 | it("still blocks a new branch that itself introduces a real secret", () => { | |
| 91 | git(work, ["checkout", "-b", "leaky-feature", "main"]); | |
| 92 | writeFileSync(join(work, "leak.ts"), `const K = '${FIXTURE_SECRET}';\n`); | |
| 93 | git(work, ["add", "leak.ts"]); | |
| 94 | git(work, ["commit", "-m", "oops committed a secret"]); | |
| 95 | ||
| 96 | const res = git(work, ["push", "origin", "leaky-feature"], true); | |
| 97 | expect(res.exitCode).not.toBe(0); | |
| 98 | expect(res.stderr).toContain("Secret scan"); | |
| 99 | expect(res.stderr).toContain("leak.ts"); | |
| 100 | // And it must NOT also (falsely) flag the untouched fixture. | |
| 101 | expect(res.stderr).not.toContain("fixture.ts"); | |
| 102 | }); | |
| 103 | }); |