import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; import { randomBytes } from "node:crypto"; import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import nodePath from "node:path"; import { test } from "node:test"; const GITHUB_TOKEN_RANDOM_BYTES = 18; test("pre-commit scans the index, redacts findings and fails closed", () => { const hook = nodePath.resolve(".husky/pre-commit"); const cwd = mkdtempSync(nodePath.join(tmpdir(), "frontend-hook-")); const bin = nodePath.join(cwd, "bin"); const marker = nodePath.join(cwd, "downstream"); const token = `ghp_${randomBytes(GITHUB_TOKEN_RANDOM_BYTES).toString("hex")}`; const git = (...arguments_) => execFileSync("git", arguments_, { cwd, stdio: "pipe" }); // eslint-disable-line sonarjs/no-os-command-from-path -- Repository checks use the developer/CI Git installation; arguments never invoke a shell. const run = (path = `${bin}${nodePath.delimiter}${process.env.PATH}`) => spawnSync("/bin/sh", ["-e", hook], { cwd, encoding: "utf8", env: { ...process.env, PATH: path }, }); try { // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. mkdirSync(bin); for (const command of ["npx", "npm"]) { // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync( nodePath.join(bin, command), "#!/bin/sh\necho ran >> downstream\n", { mode: 0o755, }, ); } git("init", "--quiet"); // The hook must run the installed lint-staged binary, never download one. const localBin = nodePath.join(cwd, "node_modules", ".bin"); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. mkdirSync(localBin, { recursive: true }); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync(nodePath.join(localBin, "lint-staged"), "#!/bin/sh\necho ran >> downstream\n", { mode: 0o755 }); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync(nodePath.join(cwd, "sample.txt"), "safe staged content\n"); git("add", "sample.txt"); // An unstaged secret must not be mistaken for the index being committed. // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync(nodePath.join(cwd, "sample.txt"), `token=${token}\n`); let result = run(); assert.equal( result.status, 0, "clean index must pass; install Gitleaks first", ); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. assert.ok(existsSync(marker)); rmSync(marker); git("add", "sample.txt"); result = run(); assert.equal(result.status, 1, "staged secret must block commit"); assert.ok(!`${result.stdout}${result.stderr}`.includes(token)); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. assert.ok(!existsSync(marker), "formatting/dependency checks must not run"); result = run(bin); assert.notEqual(result.status, 0, "missing Gitleaks must block commit"); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. assert.ok(!existsSync(marker)); } finally { rmSync(cwd, { force: true, recursive: true }); } }); test("pre-commit never downloads tools and blocks when lint-staged is missing", () => { const hook = nodePath.resolve(".husky/pre-commit"); const cwd = mkdtempSync(nodePath.join(tmpdir(), "frontend-hook-")); const bin = nodePath.join(cwd, "bin"); const marker = nodePath.join(cwd, "downstream"); try { // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. mkdirSync(bin); // Gitleaks passes; npx/npm must never be reached, even if present on PATH. // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync(nodePath.join(bin, "gitleaks"), "#!/bin/sh\nexit 0\n", { mode: 0o755 }); for (const command of ["npx", "npm"]) { // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. writeFileSync(nodePath.join(bin, command), "#!/bin/sh\necho ran >> downstream\n", { mode: 0o755 }); } const result = spawnSync("/bin/sh", ["-e", hook], { cwd, encoding: "utf8", env: { ...process.env, PATH: `${bin}${nodePath.delimiter}${process.env.PATH}` }, }); assert.notEqual(result.status, 0, "missing lint-staged must block commit"); assert.match(result.stderr, /lint-staged is not installed/); // eslint-disable-next-line security/detect-non-literal-fs-filename -- Path is built from this check’s private mkdtemp directory and fixed filenames. assert.ok(!existsSync(marker), "hook must not invoke npx or npm"); } finally { rmSync(cwd, { force: true, recursive: true }); } });