mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-27 15:11:47 +02:00
v1.88.1.0 fix: harden credential boundaries and owned state (#2942)
* fix(settings): preserve symlinked settings targets
Resolve the selected target for locking, mutation, backup, and rollback; refuse target changes and preserve private modes. Addresses #2830.
* fix(redact): bind masking to original detected spans
Inspired by #2929's anchored-span diagnosis; independently implemented using normalization offsets. Addresses #2930 and the relocation portion of #2912 without changing detection sensitivity.
* fix(evals): exclude operator credentials from prefix admission
Adapts the credential-suffix screen proposed in #2636, with real launched-child regression coverage and deliberate provider-auth exceptions.
* fix(artifacts): retain custom allowlist rules on reinitialization
Preserve the exact user-owned suffix and publish only a successfully assembled replacement. Independently implements the repair reported in #2907.
* test(cso): verify exact masked reads and unmaskable payload refusal
* fix(cso): preserve exact filesystem identities through lease recovery
Preserve 64-bit device/inode identity and nanosecond race checks. Add native NTFS lifecycle coverage for #2927; retain ambiguous legacy-state refusal without claiming Windows PID-reuse recovery is resolved.
* fix(redact): bind pre-push scans to destination and preserve seam context
Uses #2935 (bd07318) as source evidence for push-target range and slice-overlap defects. Independently implemented; no cherry-pick or release metadata adoption.
* test(ci): gate native agent ownership and settings links on macOS
* fix(browse): bind agent lifetimes and cleanup to owned generations
Uses #2931 by Chris Hutton / Claude Fable 5.1 as attributed design input; independently implemented without broad sweeps or copied code. Keep uncertain children and locks rather than deleting foreign state.
* test(ci): include concurrent shutdown controls in the native macOS gate
* v1.88.1.0 fix: harden credential boundaries and owned state
* fix(redact): preserve target provenance and scan boundary semantics
* test(artifacts): read managed rules from atomic allowlist assembly
* fix: preserve native exit observations and fixture prerequisites
* fix: preserve UTF-16 offsets through redaction normalization
This commit is contained in:
@@ -0,0 +1,393 @@
|
||||
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";
|
||||
|
||||
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 });
|
||||
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 });
|
||||
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("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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 }).status).not.toBe(0);
|
||||
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
|
||||
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 }).status).not.toBe(0);
|
||||
const push = spawnSync("git", ["push", "--force", "publish", "main"], { cwd: repo, encoding: "utf8", timeout: 30_000 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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,
|
||||
});
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
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 });
|
||||
expect(push.status).toBe(0);
|
||||
expect(push.stderr).not.toContain("engine.input_too_large");
|
||||
expect(git(origin, "rev-parse", "refs/heads/main")).toBe(tip);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user