80 lines
3.3 KiB
JavaScript
80 lines
3.3 KiB
JavaScript
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");
|
||
// 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 });
|
||
}
|
||
});
|