mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-12 07:59:02 +02:00
fix(telemetry): redact error_message spans before they leave the machine (#1947)
error_message was uploaded with only quote/newline escaping — stack traces
and failed-API errors can embed credentials, private paths, and hostnames,
and the sync path strips only _repo_slug/_branch.
New lib/redact-engine.ts export redactFindingSpans(): replaces EVERY
finding's span with <REDACTED-{id}> regardless of tier (applyRedactions is
the interactive PII-only path and exits nonzero on credential findings, so
it can't serve machine egress). Returns null when a span can't be located —
callers drop the whole payload rather than risk a leak.
gstack-telemetry-log pipes error_message through it at LOG time, so the
local JSONL at rest is clean too; surrounding text survives for crash
triage. FAIL CLOSED: bun missing, engine error, or non-JSON-string output
all null the field. Tests pin: embedded ghp_ token → <REDACTED-github.pat>
with context intact; redactor unavailable → null; raw bytes on disk never
contain the token.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
b3085f137e
commit
c8f078b482
@@ -427,6 +427,28 @@ export function applyRedactions(
|
||||
return { body, diff: diffLines.reverse().join("\n"), skipped };
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace EVERY finding's span with `<REDACTED-{id}>`, regardless of tier or
|
||||
* autoRedactable. For machine egress surfaces (telemetry error_message,
|
||||
* #1947) where structure preservation doesn't matter and fail-closed beats
|
||||
* fidelity. Returns null when any finding's span cannot be located — the
|
||||
* caller must then drop the whole payload rather than risk leaking an
|
||||
* unlocated secret. (Contrast applyRedactions, which is the interactive,
|
||||
* autoRedactable-only, structure-preserving path.)
|
||||
*/
|
||||
export function redactFindingSpans(input: string, opts: ScanOptions = {}): string | null {
|
||||
const { findings } = scan(input, opts);
|
||||
const targets = findings.map((f) => ({ f, ...locateSpan(input, f) }));
|
||||
if (targets.some((t) => t.start < 0)) return null;
|
||||
// Right-to-left so earlier offsets remain valid after splicing.
|
||||
targets.sort((a, b) => b.start - a.start);
|
||||
let body = input;
|
||||
for (const t of targets) {
|
||||
body = body.slice(0, t.start) + `<REDACTED-${t.f.id}>` + body.slice(t.end);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function locateSpan(input: string, f: Finding): { start: number; end: number } {
|
||||
// Re-derive the offset from line/col on the original text.
|
||||
let offset = 0;
|
||||
|
||||
Reference in New Issue
Block a user