mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
433 lines
24 KiB
TypeScript
433 lines
24 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test";
|
||
import { spawnSync } from "node:child_process";
|
||
import * as fs from "node:fs";
|
||
import * as os from "node:os";
|
||
import * as path from "node:path";
|
||
import { scan } from "../lib/redact-engine";
|
||
import { gitEnv } from "./helpers/git-env";
|
||
|
||
const REDACT = path.resolve(import.meta.dir, "../bin/gstack-redact");
|
||
const roots: string[] = [];
|
||
const key = ["AKIA", "1234567890ABCDEF"].join("");
|
||
|
||
function git(cwd: string, ...args: string[]): string {
|
||
const result = spawnSync("git", args, { cwd, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
if (result.status !== 0) throw new Error(`git ${args[0]} failed: ${result.stderr}`);
|
||
return result.stdout.trim();
|
||
}
|
||
|
||
function fixture(initialSecret = true, sha256 = false): { repo: string; origin: string; publish: string; head: string } {
|
||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "prepush-target-"));
|
||
roots.push(root);
|
||
const repo = path.join(root, "repo");
|
||
const origin = path.join(root, "origin.git");
|
||
const publish = path.join(root, "publish.git");
|
||
fs.mkdirSync(repo);
|
||
const format = sha256 ? ["--object-format=sha256"] : [];
|
||
git(root, "init", "--bare", "-q", "-b", "main", ...format, origin);
|
||
git(root, "init", "--bare", "-q", "-b", "main", ...format, publish);
|
||
git(repo, "init", "-q", "-b", "main", ...format);
|
||
git(repo, "config", "user.name", "Fixture");
|
||
git(repo, "config", "user.email", "fixture@example.test");
|
||
fs.writeFileSync(path.join(repo, "config.txt"), initialSecret ? `key ${key}\n` : "clean\n");
|
||
git(repo, "add", "config.txt");
|
||
git(repo, "commit", "-qm", "seed");
|
||
const head = git(repo, "rev-parse", "HEAD");
|
||
git(repo, "remote", "add", "origin", origin);
|
||
git(repo, "push", "-q", "-u", "origin", "main");
|
||
git(repo, "remote", "add", "publish", publish);
|
||
const install = spawnSync("bun", [REDACT, "install-prepush-hook"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(install.status).toBe(0);
|
||
return { repo, origin, publish, head };
|
||
}
|
||
|
||
function seedIndependentRemote(repo: string, publish: string): string {
|
||
const producer = path.join(path.dirname(repo), "producer");
|
||
fs.mkdirSync(producer);
|
||
git(producer, "init", "-q", "-b", "main");
|
||
git(producer, "config", "user.name", "Fixture");
|
||
git(producer, "config", "user.email", "fixture@example.test");
|
||
fs.writeFileSync(path.join(producer, "clean.txt"), "different history\n");
|
||
git(producer, "add", "clean.txt");
|
||
git(producer, "commit", "-qm", "remote tip");
|
||
git(producer, "remote", "add", "publish", publish);
|
||
git(producer, "push", "-q", "publish", "main");
|
||
return git(publish, "rev-parse", "refs/heads/main");
|
||
}
|
||
|
||
afterEach(() => {
|
||
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||
});
|
||
|
||
describe("fixture installs ignore the developer's hook config (#3353)", () => {
|
||
test("a global, system or environment core.hooksPath is neither rewritten nor run", () => {
|
||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "prepush-target-global-"));
|
||
roots.push(root);
|
||
const hooks = path.join(root, "global-hooks");
|
||
fs.mkdirSync(hooks);
|
||
const original = "#!/bin/sh\necho developer-hook >&2\nexit 1\n";
|
||
fs.writeFileSync(path.join(hooks, "pre-push"), original, { mode: 0o755 });
|
||
const config = path.join(root, "gitconfig");
|
||
fs.writeFileSync(config, `[core]\n\thooksPath = ${JSON.stringify(hooks)}\n`);
|
||
const keys = ["GIT_CONFIG_GLOBAL", "GIT_CONFIG_SYSTEM", "GIT_CONFIG_NOSYSTEM", "GIT_CONFIG_COUNT",
|
||
"GIT_CONFIG_KEY_0", "GIT_CONFIG_VALUE_0", "GIT_CONFIG_PARAMETERS"] as const;
|
||
const prev = Object.fromEntries(keys.map(key => [key, process.env[key]]));
|
||
try {
|
||
process.env.GIT_CONFIG_GLOBAL = config;
|
||
process.env.GIT_CONFIG_SYSTEM = config;
|
||
delete process.env.GIT_CONFIG_NOSYSTEM;
|
||
process.env.GIT_CONFIG_COUNT = "1";
|
||
process.env.GIT_CONFIG_KEY_0 = "core.hooksPath";
|
||
process.env.GIT_CONFIG_VALUE_0 = hooks;
|
||
process.env.GIT_CONFIG_PARAMETERS = `'core.hooksPath=${hooks}'`;
|
||
const { repo } = fixture(false);
|
||
expect(fs.readdirSync(hooks)).toEqual(["pre-push"]);
|
||
expect(fs.readFileSync(path.join(hooks, "pre-push"), "utf8")).toBe(original);
|
||
const installed = git(repo, "rev-parse", "--path-format=absolute", "--git-path", "hooks/pre-push");
|
||
expect(fs.readFileSync(installed, "utf8")).toContain("gstack-redact pre-push (managed)");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status, push.stderr).toBe(0);
|
||
expect(push.stderr).not.toContain("developer-hook");
|
||
} finally {
|
||
for (const [key, value] of Object.entries(prev)) {
|
||
if (value === undefined) delete process.env[key];
|
||
else process.env[key] = value;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
describe("installed pre-push guard uses the actual destination", () => {
|
||
test("origin tracking at HEAD cannot excuse the same credential on first push to publish", () => {
|
||
const { repo, publish, head } = fixture();
|
||
expect(git(repo, "rev-parse", "origin/main")).toBe(head);
|
||
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
|
||
});
|
||
|
||
test("a configured remote's different push URL cannot reuse its fetch tracking tip", () => {
|
||
const { repo, publish } = fixture();
|
||
git(repo, "remote", "set-url", "--push", "origin", publish);
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
|
||
});
|
||
|
||
for (const [name, symbolicHead] of [
|
||
["a local branch cannot shadow the target's qualified remote-tracking base", true],
|
||
["the main fallback cannot resolve to a colliding local branch", false],
|
||
] as const) {
|
||
test(name, () => {
|
||
const { repo, publish, head: base } = fixture(false);
|
||
fs.writeFileSync(path.join(repo, "leak.txt"), `key ${key}\n`);
|
||
git(repo, "add", "leak.txt");
|
||
git(repo, "commit", "-qm", "secret after target base");
|
||
const tip = git(repo, "rev-parse", "HEAD");
|
||
git(repo, "update-ref", "refs/remotes/publish/main", base);
|
||
if (symbolicHead) git(repo, "symbolic-ref", "refs/remotes/publish/HEAD", "refs/remotes/publish/main");
|
||
git(repo, "update-ref", "refs/heads/publish/main", tip);
|
||
expect(git(repo, "rev-parse", "publish/main")).toBe(tip);
|
||
expect(git(repo, "rev-parse", "refs/remotes/publish/main")).toBe(base);
|
||
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
|
||
});
|
||
}
|
||
|
||
test("the second fetch URL is not represented by the first URL's tracking ref", () => {
|
||
const { repo, origin, publish } = fixture();
|
||
const second = path.join(path.dirname(repo), "second.git");
|
||
git(path.dirname(repo), "init", "--bare", "-q", "-b", "main", second);
|
||
git(publish, "fetch", "-q", origin, "main:refs/heads/main");
|
||
git(repo, "fetch", "-q", "publish");
|
||
expect(git(repo, "rev-parse", "refs/remotes/publish/main")).toBe(git(repo, "rev-parse", "HEAD"));
|
||
git(repo, "remote", "set-url", "--add", "publish", second);
|
||
expect(git(repo, "remote", "get-url", "publish")).toBe(publish);
|
||
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(second, "for-each-ref", "--format=%(refname)")).toBe("");
|
||
});
|
||
|
||
test("direct URL push cannot borrow origin's tracking history", () => {
|
||
const { repo, publish } = fixture();
|
||
const push = spawnSync("git", ["push", publish, "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(publish, "for-each-ref", "--format=%(refname)")).toBe("");
|
||
});
|
||
|
||
test("an advertised remote tip absent from local objects cannot justify a guessed base", () => {
|
||
const { repo, publish } = fixture();
|
||
const remoteTip = seedIndependentRemote(repo, publish);
|
||
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000, env: gitEnv() }).status).not.toBe(0);
|
||
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(publish, "rev-parse", "refs/heads/main")).toBe(remoteTip);
|
||
});
|
||
|
||
test("an absent advertised tip permits a clean forced update after conservative scanning", () => {
|
||
const { repo, publish, head } = fixture(false);
|
||
const remoteTip = seedIndependentRemote(repo, publish);
|
||
expect(spawnSync("git", ["cat-file", "-e", remoteTip], { cwd: repo, timeout: 30_000, env: gitEnv() }).status).not.toBe(0);
|
||
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(git(publish, "rev-parse", "refs/heads/main")).toBe(head);
|
||
});
|
||
|
||
test("new SHA-256 branch scans the correct empty tree and allows a clean push", () => {
|
||
const { repo, publish } = fixture(false, true);
|
||
const push = spawnSync("git", ["push", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(git(publish, "rev-parse", "refs/heads/main")).toHaveLength(64);
|
||
fs.writeFileSync(path.join(repo, "leak.txt"), `key ${key}\n`);
|
||
git(repo, "add", "leak.txt");
|
||
git(repo, "commit", "-qm", "new secret");
|
||
const blocked = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(blocked.status).toBe(1);
|
||
expect(blocked.stderr).toContain("aws.access_key");
|
||
});
|
||
|
||
test("one leaky ref blocks a multi-ref push without updating its clean neighbor", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
git(repo, "checkout", "-q", "-b", "leak");
|
||
fs.writeFileSync(path.join(repo, "leak.txt"), `key ${key}\n`);
|
||
git(repo, "add", "leak.txt");
|
||
git(repo, "commit", "-qm", "leaky branch");
|
||
git(repo, "checkout", "-q", "main");
|
||
fs.writeFileSync(path.join(repo, "clean.txt"), "safe update\n");
|
||
git(repo, "add", "clean.txt");
|
||
git(repo, "commit", "-qm", "clean branch");
|
||
const push = spawnSync("git", ["push", "origin", "main", "leak"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
expect(git(origin, "for-each-ref", "--format=%(refname)", "refs/heads/leak")).toBe("");
|
||
});
|
||
|
||
test("a deleted ref alongside a clean update is permitted", () => {
|
||
const { repo, origin } = fixture(false);
|
||
git(repo, "checkout", "-q", "-b", "disposable");
|
||
fs.writeFileSync(path.join(repo, "disposable.txt"), "safe\n");
|
||
git(repo, "add", "disposable.txt");
|
||
git(repo, "commit", "-qm", "temporary branch");
|
||
git(repo, "push", "-q", "origin", "disposable");
|
||
git(repo, "checkout", "-q", "main");
|
||
fs.writeFileSync(path.join(repo, "clean.txt"), "safe update\n");
|
||
git(repo, "add", "clean.txt");
|
||
git(repo, "commit", "-qm", "clean update");
|
||
const head = git(repo, "rev-parse", "HEAD");
|
||
const push = spawnSync("git", ["push", "origin", "main", ":disposable"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
expect(git(origin, "for-each-ref", "--format=%(refname)", "refs/heads/disposable")).toBe("");
|
||
});
|
||
|
||
test("installed wrapper forwards refs and rejects malformed input instead of passing it on", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
const hook = git(repo, "rev-parse", "--git-path", "hooks/pre-push");
|
||
const result = spawnSync("bash", [hook, "origin", origin], {
|
||
cwd: repo, input: "refs/heads/main malformed\n", encoding: "utf8", timeout: 30_000, env: gitEnv(),
|
||
});
|
||
expect(result.status, result.stderr || result.error?.message).toBe(1);
|
||
expect(result.stderr).toContain("could not parse a pre-push ref line");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
});
|
||
|
||
test("a HIGH proximity finding across line-aligned scan slices cannot pass", () => {
|
||
const { repo } = fixture(false);
|
||
const value = ["AbCdEfGhIjKlMnOpQrStU", "vWxYz0123456789AbCd"].join("");
|
||
const payload = `${"x".repeat(768 * 1024 - 43)}\naws_secret_access_key=\n${value}\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).toContain("aws.secret_key");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "near seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.secret_key");
|
||
});
|
||
|
||
test("an unlabeled high-entropy value near a seam stays clean", () => {
|
||
const { repo } = fixture(false);
|
||
const value = ["AbCdEfGhIjKlMnOpQrStU", "vWxYz0123456789AbCd"].join("");
|
||
const payload = `${"x".repeat(768 * 1024 - 43)}\nordinary description\n${value}\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).not.toContain("aws.secret_key");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "unlabeled seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(push.stderr).not.toContain("aws.secret_key");
|
||
});
|
||
|
||
test("overlap does not count a single finding twice", () => {
|
||
const { repo } = fixture(false);
|
||
const payload = `${"x".repeat(768 * 1024 - 75)}\nkey ${key}\n${"x".repeat(100)}\n`;
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "one finding at seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr.match(/HIGH aws\.access_key/g)).toHaveLength(1);
|
||
});
|
||
|
||
test("supplementary NFKC expansion cannot move a seam finding out of its owning core", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
const prefix = "\uFA6C".repeat(64);
|
||
const credential = `key ${key}\n`;
|
||
const padding = "x".repeat(768 * 1024 - Buffer.byteLength(prefix) - credential.length - 1);
|
||
const payload = `${prefix}${padding}\n${credential}${"z".repeat(100)}\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).toContain("aws.access_key");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "supplementary normalization seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr.match(/HIGH aws\.access_key/g)).toHaveLength(1);
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
});
|
||
|
||
test("a partial-line overlap cannot manufacture an anchored assignment finding", () => {
|
||
const { repo } = fixture(false);
|
||
const value = ["8Fk2pQ9vXz4wL7mN", "3rT6yB1cD5eG0hJq"].join("");
|
||
const assignment = `API_KEY=${value} `;
|
||
const suffix = "z".repeat(16 * 1024 - assignment.length - 6);
|
||
const first = "!" + "x".repeat(768 * 1024 - 61 - assignment.length - suffix.length) + assignment + suffix;
|
||
const payload = `${first}\nshort\n${"z".repeat(100)}\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).not.toContain("env.kv");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "mid-line control");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(push.stderr).not.toContain("MEDIUM finding");
|
||
});
|
||
|
||
test("mid-line carry cannot create a HIGH word boundary around an embedded key", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
const first = "x".repeat(768 * 1024 - 1 - 16_384) + key + " ".repeat(16_384 - key.length);
|
||
const payload = `${first}\nordinary\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).not.toContain("aws.access_key");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "embedded key control");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(push.stderr).not.toContain("aws.access_key");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).not.toBe(head);
|
||
});
|
||
|
||
test("a long Bearer span retains its later Authorization context across the seam", () => {
|
||
const { repo, origin } = fixture(false);
|
||
const alphabet = "7pFb4ZaCuG8wDsVk2EnHy6Qt9Jr5Lx0M";
|
||
const token = Array.from({ length: 20_480 }, (_, i) => alphabet[(i * 7 + Math.floor(i / 31)) % 32]).join("");
|
||
const bearerLine = `Bearer ${token}`;
|
||
const payload = `${"x".repeat(768 * 1024 - bearerLine.length - 2)}\n${bearerLine}\nAuthorization\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).toContain("auth.bearer");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "long bearer advisory");
|
||
const tip = git(repo, "rev-parse", "HEAD");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(push.stderr).toContain("MEDIUM finding");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(tip);
|
||
});
|
||
|
||
test("zero-width padding cannot move normalized proximity context out of the overlap", () => {
|
||
const { repo } = fixture(false);
|
||
const value = ["AbCdEfGhIjKlMnOpQrStU", "vWxYz0123456789AbCd"].join("");
|
||
const payload = `${"x".repeat(768 * 1024 - 43)}\naws_secret_access_key=\n${"\u200b".repeat(35_000)}\n${value}\n`;
|
||
expect(scan(payload).findings.map((finding) => finding.id)).toContain("aws.secret_key");
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "invisible seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.secret_key");
|
||
});
|
||
|
||
test("context needing more than the engine byte cap blocks instead of scanning a truncated window", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
const value = ["AbCdEfGhIjKlMnOpQrStU", "vWxYz0123456789AbCd"].join("");
|
||
const payload = `${"x".repeat(768 * 1024 - 43)}\naws_secret_access_key=\n${"\u200b".repeat(450_000)}\n${value}\n`;
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "unscannable context");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("engine.input_too_large");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
});
|
||
|
||
for (const [name, label, expected] of [
|
||
["HTML entity", "aws&secret_access_key=", true],
|
||
["fullwidth Unicode", "aws_secret_access_key=", true],
|
||
["broken entity separated by zero-width", "aws&\u200bamp;secret_access_key=", false],
|
||
] as const) {
|
||
test(`${name} near a seam keeps the detector's exact normalization semantics`, () => {
|
||
const { repo } = fixture(false);
|
||
const value = ["AbCdEfGhIjKlMnOpQrStU", "vWxYz0123456789AbCd"].join("");
|
||
const payload = `${"x".repeat(768 * 1024 - 43)}\n${label}\n${value}\n`;
|
||
expect(scan(payload).findings.some((finding) => finding.id === "aws.secret_key")).toBe(expected);
|
||
fs.writeFileSync(path.join(repo, "payload.txt"), payload);
|
||
git(repo, "add", "payload.txt");
|
||
git(repo, "commit", "-qm", "normalization seam");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(expected ? 1 : 0);
|
||
expect(push.stderr.includes("aws.secret_key")).toBe(expected);
|
||
});
|
||
}
|
||
|
||
test("a large clean diff and a sub-cap long line pass, while an over-cap line fails closed", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
fs.writeFileSync(path.join(repo, "large.txt"), "ordinary content\n".repeat(160_000));
|
||
git(repo, "add", "large.txt");
|
||
git(repo, "commit", "-qm", "large clean diff");
|
||
let push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
fs.writeFileSync(path.join(repo, "long.txt"), "x".repeat(900_000) + "\n");
|
||
git(repo, "add", "long.txt");
|
||
git(repo, "commit", "-qm", "long clean line");
|
||
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
const priorTip = git(origin, "rev-parse", "refs/heads/main");
|
||
expect(priorTip).not.toBe(head);
|
||
fs.writeFileSync(path.join(repo, "too-long.txt"), "x".repeat(1_100_000) + "\n");
|
||
git(repo, "add", "too-long.txt");
|
||
git(repo, "commit", "-qm", "over cap line");
|
||
push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 60_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("engine.input_too_large");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(priorTip);
|
||
});
|
||
|
||
test("a detectable credential at the end of a long single line is still blocked", () => {
|
||
const { repo, origin, head } = fixture(false);
|
||
fs.writeFileSync(path.join(repo, "long.txt"), `${"x".repeat(900_000)} key ${key}\n`);
|
||
git(repo, "add", "long.txt");
|
||
git(repo, "commit", "-qm", "long line with key");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(1);
|
||
expect(push.stderr).toContain("aws.access_key");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(head);
|
||
});
|
||
|
||
test("neighboring individually scannable long clean lines do not force an oversize failure", () => {
|
||
const { repo, origin } = fixture(false);
|
||
fs.writeFileSync(path.join(repo, "long.txt"), `${"x".repeat(900_000)}\n${"y".repeat(900_000)}\n`);
|
||
git(repo, "add", "long.txt");
|
||
git(repo, "commit", "-qm", "neighboring long lines");
|
||
const tip = git(repo, "rev-parse", "HEAD");
|
||
const push = spawnSync("git", ["push", "origin", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000, env: gitEnv() });
|
||
expect(push.status).toBe(0);
|
||
expect(push.stderr).not.toContain("engine.input_too_large");
|
||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(tip);
|
||
});
|
||
});
|