mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-13 16:38:56 +02:00
fix(redact): scan large diffs in line-aligned slices; stop digit-UUIDs matching as cards/phones
The prepush guard blocked any push whose added lines exceeded the engine's 1 MiB cap with engine.input_too_large — a size error naming no credential — which trains people onto GSTACK_REDACT_PREPUSH=skip. Scan in 768 KiB line-aligned slices instead (no pattern is multi-line, so a boundary cannot bisect a secret); a single oversized line still goes to the engine intact and fails closed. Also suppress card/phone matches whose span sits ENTIRELY inside a UUID — digit-only UUID fixtures were 14 of 21 MEDIUM findings on an ordinary branch, the noise level that stops people reading MEDIUM at all. Fixes #2304. Contributed by @luckywenapere (PR #2543). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3fa7739689
commit
d410142c2f
@@ -114,6 +114,64 @@ function addedLinesFor(localSha: string, remoteSha: string): string {
|
||||
return added.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Byte budget per scan() call. Kept comfortably under redact-engine's
|
||||
* DEFAULT_MAX_BYTES (1 MiB) so a slice never trips its oversize guard.
|
||||
*/
|
||||
const SCAN_CHUNK_BYTES = 768 * 1024;
|
||||
|
||||
/**
|
||||
* Scan added lines in line-aligned slices, unioning the findings.
|
||||
*
|
||||
* Why: the engine refuses input over its byte cap and fails closed, which is
|
||||
* right for one scan() call but wrong as a push policy — a feature branch
|
||||
* catching up to a busy main legitimately produces more added lines than the
|
||||
* cap (1,146,782 bytes against the 1 MiB default in the push that prompted
|
||||
* this, and only ~7% of that was the lockfile). The push then blocked on
|
||||
* `engine.input_too_large` — a size error naming no credential — which trains
|
||||
* people to reach for --no-verify, defeating the guardrail far more thoroughly
|
||||
* than a large diff does.
|
||||
*
|
||||
* Slicing loses NO detection coverage, because every pattern is single-line:
|
||||
* none in redact-patterns.ts carries the `m` or `s` flag, the
|
||||
* BEGIN-PRIVATE-KEY patterns capture only the header line rather than the key
|
||||
* body, and the engine itself iterates line by line. A line boundary therefore
|
||||
* cannot bisect a detectable secret, so no inter-slice overlap is needed.
|
||||
*
|
||||
* Fail-closed is preserved: a SINGLE line over the budget is still passed to
|
||||
* the engine intact, so a genuinely unscannable blob (minified bundle,
|
||||
* embedded base64) trips input_too_large and blocks exactly as before.
|
||||
*
|
||||
* Findings' line/col are slice-relative, which is fine here — this hook only
|
||||
* reads severity, id and preview. Do not lift this into the engine, where
|
||||
* callers rely on absolute line numbers.
|
||||
*/
|
||||
function scanAddedLines(added: string, opts: Parameters<typeof scan>[1]): Finding[] {
|
||||
const findings: Finding[] = [];
|
||||
let slice: string[] = [];
|
||||
let sliceBytes = 0;
|
||||
|
||||
const flush = () => {
|
||||
if (slice.length === 0) return;
|
||||
findings.push(...scan(slice.join("\n"), opts).findings);
|
||||
slice = [];
|
||||
sliceBytes = 0;
|
||||
};
|
||||
|
||||
for (const line of added.split("\n")) {
|
||||
// +1 for the newline that rejoins it.
|
||||
const lineBytes = Buffer.byteLength(line, "utf8") + 1;
|
||||
// Close the current slice BEFORE overflowing it. A single oversized line
|
||||
// lands in a slice of its own and is handed to the engine as-is.
|
||||
if (sliceBytes > 0 && sliceBytes + lineBytes > SCAN_CHUNK_BYTES) flush();
|
||||
slice.push(line);
|
||||
sliceBytes += lineBytes;
|
||||
}
|
||||
flush();
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function logSkip(reason: string): void {
|
||||
try {
|
||||
const home = process.env.GSTACK_HOME || path.join(os.homedir(), ".gstack");
|
||||
@@ -165,8 +223,9 @@ function main() {
|
||||
if (!added.trim()) continue;
|
||||
// Visibility doesn't change HIGH behavior; pass private so nothing is treated
|
||||
// as public-strict (HIGH blocks regardless either way).
|
||||
const result = scan(added, { repoVisibility: "private" });
|
||||
for (const f of result.findings) {
|
||||
// Sliced (see scanAddedLines) so a large-but-legitimate diff is actually
|
||||
// scanned rather than blocked unscanned on the engine's size cap.
|
||||
for (const f of scanAddedLines(added, { repoVisibility: "private" })) {
|
||||
if (f.severity === "HIGH") allHigh.push(f);
|
||||
else if (f.severity === "MEDIUM") mediumCount++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user