mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-11 07:29:00 +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
@@ -201,6 +201,41 @@ describe('gstack-telemetry-log', () => {
|
||||
expect(event.error_message).toContain('not found');
|
||||
});
|
||||
|
||||
test('redacts credential spans in error_message before they touch disk (#1947)', () => {
|
||||
setConfig('telemetry', 'anonymous');
|
||||
const token = 'ghp_' + 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8';
|
||||
run(
|
||||
`${BIN}/gstack-telemetry-log --skill qa --duration 10 --outcome error --error-message 'push failed: auth ${token} rejected by remote' --session-id red-1`,
|
||||
);
|
||||
|
||||
const lines = readJsonl();
|
||||
expect(lines).toHaveLength(1);
|
||||
const event = JSON.parse(lines[0]);
|
||||
// The span is masked, the surrounding triage context survives.
|
||||
expect(event.error_message).toContain('<REDACTED-github.pat>');
|
||||
expect(event.error_message).toContain('push failed');
|
||||
expect(event.error_message).not.toContain(token);
|
||||
// Raw bytes on disk never contain the token either.
|
||||
expect(lines[0]).not.toContain(token);
|
||||
});
|
||||
|
||||
test('fails closed: error_message becomes null when the redactor is unavailable (#1947)', () => {
|
||||
setConfig('telemetry', 'anonymous');
|
||||
const token = 'ghp_' + 'A1b2C3d4E5f6G7h8I9j0K1l2M3n4O5p6Q7r8';
|
||||
// PATH without bun: the redaction snippet cannot run, so the whole
|
||||
// message must drop — never raw passthrough.
|
||||
run(
|
||||
`${BIN}/gstack-telemetry-log --skill qa --duration 10 --outcome error --error-message 'auth ${token} rejected' --session-id red-2`,
|
||||
{ PATH: '/usr/bin:/bin' },
|
||||
);
|
||||
|
||||
const lines = readJsonl();
|
||||
expect(lines).toHaveLength(1);
|
||||
const event = JSON.parse(lines[0]);
|
||||
expect(event.error_message).toBeNull();
|
||||
expect(lines[0]).not.toContain(token);
|
||||
});
|
||||
|
||||
test('creates analytics directory if missing', () => {
|
||||
// Remove analytics dir
|
||||
const analyticsDir = path.join(tmpDir, 'analytics');
|
||||
|
||||
Reference in New Issue
Block a user