1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
import { describe, expect, it } from "bun:test";
import { writeFileSync } from "fs";
import { join } from "path";
import {
hookSecretPatternTypes,
installPackInspectionHook,
} from "../lib/push-policy";
import { SECRET_PATTERNS } from "../lib/security-scan";
describe("hook secret patterns stay in sync with security-scan.ts", () => {
it("covers every critical-severity pattern in SECRET_PATTERNS, and nothing else", () => {
const criticalTypes = SECRET_PATTERNS.filter((p) => p.severity === "critical").map((p) => p.type).sort();
const hookTypes = hookSecretPatternTypes().sort();
expect(hookTypes).toEqual(criticalTypes);
});
});
describe("pre-receive secret scan (real eval.js subprocess)", () => {
async function runEval(contentLines: string[]): Promise<{ active: string[]; stderr: string }> {
const hook = await installPackInspectionHook([], { secretScan: true });
expect(hook).not.toBeNull();
if (!hook) throw new Error("unreachable");
const dir = hook.env.GIT_CONFIG_VALUE_0;
try {
const commitsPath = join(dir, "commits.txt");
const sizesPath = join(dir, "sizes.txt");
const contentsPath = join(dir, "contents.txt");
writeFileSync(commitsPath, "");
writeFileSync(sizesPath, "");
writeFileSync(
contentsPath,
contentLines
.map((l) => l)
.join("\n") + (contentLines.length ? "\n" : "")
);
const evalScriptPath = join(dir, "eval.js");
const rulesJsonPath = join(dir, "rules.json");
const proc = Bun.spawnSync([
"bun",
"run",
evalScriptPath,
"--",
rulesJsonPath,
commitsPath,
sizesPath,
contentsPath,
]);
const stdout = new TextDecoder().decode(proc.stdout).trim();
const stderr = new TextDecoder().decode(proc.stderr);
const active = stdout
.split("\n")
.filter(Boolean)
.filter((line) => line.startsWith("active\t"))
.map((line) => line.slice("active\t".length));
return { active, stderr };
} finally {
await hook.cleanup();
}
}
function fileLine(path: string, content: string): string {
return `${path}\t${Buffer.from(content, "utf8").toString("base64")}`;
}
it("blocks a critical secret (AWS access key) in a normal source file", async () => {
const { active, stderr } = await runEval([
fileLine("src/config.ts", "const AWS_KEY = 'AKIAABCDEFGH1234IJKL';\n"),
]);
expect(stderr).toBe("");
expect(active.length).toBe(1);
expect(active[0]).toContain("AWS Access Key");
expect(active[0]).toContain("src/config.ts:1");
});
it("does not leak the secret value itself into the rejection message", async () => {
const { active } = await runEval([
fileLine("src/config.ts", "const AWS_KEY = 'AKIAABCDEFGH1234IJKL';\n"),
]);
expect(active[0]).not.toContain("AKIAABCDEFGH1234IJKL");
});
it("does not block an obvious placeholder value", async () => {
const { active } = await runEval([
fileLine("src/config.ts", "const AWS_KEY = 'AKIAEXAMPLE1234EXAMP'; // example only\n"),
]);
expect(active.length).toBe(0);
});
it("does not scan skip-listed paths (build output, lockfiles)", async () => {
const { active } = await runEval([
fileLine("dist/bundle.js", "const AWS_KEY = 'AKIAABCDEFGH1234IJKL';\n"),
]);
expect(active.length).toBe(0);
});
it("does not block a clean file with no secret patterns", async () => {
const { active } = await runEval([
fileLine("README.md", "# Hello\n\nJust a normal readme with no secrets.\n"),
]);
expect(active.length).toBe(0);
});
it("scans multiple files and reports one violation per match", async () => {
const { active } = await runEval([
fileLine("a.ts", "AKIAABCDEFGH1234IJKL\n"),
fileLine("b.ts", "-----BEGIN RSA PRIVATE KEY-----\n"),
fileLine("c.ts", "nothing interesting here\n"),
]);
expect(active.length).toBe(2);
});
});
|