paperclip/packages/adapter-utils/src/command-redaction.test.ts

762 lines
32 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
REDACTED_COMMAND_TEXT_VALUE,
redactCommandText,
redactDiagnosticText,
} from "./command-redaction.js";
describe("redactDiagnosticText", () => {
it("redacts a JSON secret field value", () => {
const input = '{"token":"opaque-value","status":"error"}';
const output = redactDiagnosticText(input);
expect(output).not.toContain("opaque-value");
expect(output).toContain(`"token":"${REDACTED_COMMAND_TEXT_VALUE}"`);
// The non-secret field keeps its value.
expect(output).toContain('"status":"error"');
});
it("redacts an api_key JSON field with whitespace around the colon", () => {
const input = '{ "api_key" : "sk-secret-123" }';
const output = redactDiagnosticText(input);
expect(output).not.toContain("sk-secret-123");
expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE);
});
it("redacts an escaped-JSON secret field value", () => {
// A diagnostic can carry a JSON string, so the double quotes appear as `\"`.
const input = '{\\"token\\":\\"opaque-value\\"}';
const output = redactDiagnosticText(input);
expect(output).not.toContain("opaque-value");
expect(output).toContain(
`\\"token\\":\\"${REDACTED_COMMAND_TEXT_VALUE}\\"`,
);
});
it("still redacts a shell KEY=value secret", () => {
const input = "ANTHROPIC_API_KEY=super-secret-value claude --print";
const output = redactDiagnosticText(input);
expect(output).not.toContain("super-secret-value");
expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE);
});
it("redacts an escaped quoted assignment across a literal newline", () => {
const input = String.raw`authorization=\"Bearer first-line
second-line\" status=401`;
const expected = String.raw`authorization=\"***REDACTED***\" status=401`;
const output = redactDiagnosticText(input);
expect(output).toBe(expected);
expect(redactDiagnosticText(output)).toBe(expected);
});
it("keeps non-secret text and non-secret JSON fields intact", () => {
const input = '{"status":"ok","message":"probe finished"}';
expect(redactDiagnosticText(input)).toBe(input);
});
it("redacts the secret but keeps a non-secret marker in the same string", () => {
const input = 'DIAGMARKER1234 said {"authorization":"Bearer opaque"}';
const output = redactDiagnosticText(input);
expect(output).toContain("DIAGMARKER1234");
expect(output).not.toContain("opaque");
});
it("redacts a JSON secret value that contains an escaped quote", () => {
// The value holds an escaped quote, so a naive matcher stops at the `\"` and
// leaves the rest of the credential. The marker sits after the escaped quote.
const input = '{"token":"pre\\"MARKERQUOTE_A"}';
const output = redactDiagnosticText(input);
expect(output).not.toContain("MARKERQUOTE_A");
expect(output).toContain(`"token":"${REDACTED_COMMAND_TEXT_VALUE}"`);
});
it("redacts a JSON secret value that contains an escaped backslash", () => {
const input = '{"secret":"pre\\\\MARKERBACKSLASH_A"}';
const output = redactDiagnosticText(input);
expect(output).not.toContain("MARKERBACKSLASH_A");
expect(output).toContain(`"secret":"${REDACTED_COMMAND_TEXT_VALUE}"`);
});
it("redacts an escaped-JSON secret value that contains an escaped quote", () => {
// A diagnostic can carry a serialized JSON string, so the whole JSON is
// escaped a second time. The inner value still holds an escaped quote.
const innerJson = '{"token":"pre\\"MARKERQUOTE_B"}';
const input = JSON.stringify(innerJson);
const output = redactDiagnosticText(input);
expect(output).not.toContain("MARKERQUOTE_B");
expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE);
});
it("redacts an escaped-JSON secret value that contains an escaped backslash", () => {
const innerJson = '{"password":"pre\\\\MARKERBACKSLASH_B"}';
const input = JSON.stringify(innerJson);
const output = redactDiagnosticText(input);
expect(output).not.toContain("MARKERBACKSLASH_B");
expect(output).toContain(REDACTED_COMMAND_TEXT_VALUE);
});
});
describe("redactCommandText header secrets", () => {
it("redacts a double-quoted X-API-Key header value", () => {
const input = 'curl -H "X-API-Key: abc" https://example.test/api/agents/me';
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test/api/agents/me`,
);
});
it("redacts a single-quoted lowercase x-api-key header value", () => {
const input = "curl -H 'x-api-key: abc' https://example.test/api/agents/me";
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
`curl -H 'x-api-key: ${REDACTED_COMMAND_TEXT_VALUE}' https://example.test/api/agents/me`,
);
});
it("redacts an unquoted header value and other credential header names", () => {
expect(redactCommandText("curl -H X-API-Key:abc https://example.test")).toBe(
`curl -H X-API-Key:${REDACTED_COMMAND_TEXT_VALUE} https://example.test`,
);
expect(redactCommandText('curl -H "Api-Key: abc"')).toBe(
`curl -H "Api-Key: ${REDACTED_COMMAND_TEXT_VALUE}"`,
);
expect(redactCommandText('curl -H "X-Auth-Token: abc"')).toBe(
`curl -H "X-Auth-Token: ${REDACTED_COMMAND_TEXT_VALUE}"`,
);
expect(redactCommandText('curl -H "X-Paperclip-Api-Key: abc"')).toBe(
`curl -H "X-Paperclip-Api-Key: ${REDACTED_COMMAND_TEXT_VALUE}"`,
);
});
it("keeps a non-secret header untouched", () => {
const input = 'curl -H "Content-Type: application/json" -H "Accept: application/json" https://example.test';
expect(redactCommandText(input)).toBe(input);
});
it("keeps the bearer header output byte for byte identical", () => {
// The bearer rule already redacted this shape. The header rule keeps the
// scheme, so the output must not change.
const input = 'curl -H "Authorization: Bearer abc" https://example.test';
expect(redactCommandText(input)).toBe(
`curl -H "Authorization: Bearer ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("now redacts a basic authorization header value", () => {
const input = 'curl -H "Authorization: Basic dXNlcjpwdw==" https://example.test';
const output = redactCommandText(input);
expect(output).not.toContain("dXNlcjpwdw==");
expect(output).toBe(
`curl -H "Authorization: Basic ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("redacts an escaped-quoted value and keeps its escaped quotes", () => {
// An outer shell writes quote syntax for an inner shell this way, and the
// caller's own authorization rules write the same shape. Keeping the
// escaped quotes makes both agree on the result.
const input = String.raw`prefix Authorization: \"Bearer nested\" suffix`;
const output = redactCommandText(input);
expect(output).not.toContain("nested");
expect(output).toBe(
String.raw`prefix Authorization: \"Bearer ` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" suffix`,
);
// This is exactly what the caller's chain feeds back in, so it must not
// move again.
const settled =
String.raw`prefix Authorization: \"` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" suffix`;
expect(redactCommandText(settled)).toBe(settled);
});
it("redacts an escaped-quoted value passed to a nested shell", () => {
const input = String.raw`sh -c "curl -H X-API-Key:\"abc123\" https://example.test"`;
const output = redactCommandText(input);
expect(output).not.toContain("abc123");
expect(output).toBe(
String.raw`sh -c "curl -H X-API-Key:\"` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" https://example.test"`,
);
});
it("redacts a truncated escaped-quoted value", () => {
const input = String.raw`X-API-Key:\"abc`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
String.raw`X-API-Key:\"` + REDACTED_COMMAND_TEXT_VALUE,
);
});
it("redacts a header secret inside a serialized command string", () => {
const input = String.raw`{"command":"curl -H \"X-API-Key: abc\" https://example.test"}`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
String.raw`{"command":"curl -H \"X-API-Key: ` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" https://example.test"}`,
);
});
it("is idempotent over a header secret", () => {
const input = 'curl -H "X-API-Key: abc" -H "Authorization: Bearer def"';
const once = redactCommandText(input);
expect(redactCommandText(once)).toBe(once);
expect(redactDiagnosticText(once)).toBe(once);
});
it("redacts an entire quoted digest credential, not just its first parameter", () => {
const input =
`curl -H 'Authorization: Digest username="alice", realm="r", nonce="n", uri="/x", response="deadbeef"' https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("alice");
expect(output).not.toContain("deadbeef");
expect(output).not.toContain("nonce");
expect(output).toBe(
`curl -H 'Authorization: Digest ${REDACTED_COMMAND_TEXT_VALUE}' https://example.test`,
);
});
it("redacts an unquoted digest credential and stops at the next field", () => {
// A log line carries the header without shell quoting. The parameter list
// ends at the last comma-joined `key=value`, so the trailing status field
// survives.
const input =
'Authorization: Digest username="alice", nonce="n", response="deadbeef" status=401';
const output = redactCommandText(input);
expect(output).not.toContain("alice");
expect(output).not.toContain("deadbeef");
expect(output).toBe(
`Authorization: Digest ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
});
it("redacts an entire quoted sigv4 credential, not just the scheme name", () => {
const input =
'curl -H "Authorization: AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/20260903/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc123"';
const output = redactCommandText(input);
expect(output).not.toContain("AKIAEXAMPLE");
expect(output).not.toContain("abc123");
expect(output).toBe(
`curl -H "Authorization: AWS4-HMAC-SHA256 ${REDACTED_COMMAND_TEXT_VALUE}"`,
);
});
it("redacts an unquoted sigv4 credential and stops at the next word", () => {
const input =
"Authorization: AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE/20260903/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=abc123 retry";
const output = redactCommandText(input);
expect(output).not.toContain("AKIAEXAMPLE");
expect(output).not.toContain("abc123");
expect(output).toBe(
`Authorization: AWS4-HMAC-SHA256 ${REDACTED_COMMAND_TEXT_VALUE} retry`,
);
});
it("keeps an already redacted unquoted header bounded", () => {
// The server redaction feeds this shape in after its own rules. The trailing
// word must survive.
const input = `prefix Authorization: ${REDACTED_COMMAND_TEXT_VALUE} suffix`;
expect(redactCommandText(input)).toBe(input);
});
it("keeps hint words that are not header names untouched", () => {
expect(redactCommandText("GET /v1/tokens:list")).toBe("GET /v1/tokens:list");
expect(redactCommandText("auth: failed")).toBe("auth: failed");
});
it("keeps a www-authenticate challenge untouched", () => {
// The challenge parameters are diagnostics, not credentials.
const input =
'WWW-Authenticate: Bearer realm="paperclip", error="invalid_token"';
expect(redactCommandText(input)).toBe(input);
});
it("keeps an empty quoted header argument untouched", () => {
// A quoted value must open with a non-blank character, so there is nothing
// to hide here and the argument stays byte for byte.
const input = 'curl -H "X-API-Key: " -H "X-Auth-Token:" https://example.test';
expect(redactCommandText(input)).toBe(input);
});
it("redacts an escaped-quoted value that follows a scheme word", () => {
const input = String.raw`Authorization: Basic \"abc\"defg retry`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
String.raw`Authorization: Basic \"` + REDACTED_COMMAND_TEXT_VALUE + String.raw`\" retry`,
);
expect(redactCommandText(output)).toBe(output);
});
it("reads an even backslash run before a quote as a bare quote", () => {
// `\\"` is an escaped backslash followed by a real quote, not an escaped
// quote, so the escaped branches decline it and the value still redacts.
const input = String.raw`foo\\"X-API-Key: abc" bar`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
String.raw`foo\\"X-API-Key: ` + REDACTED_COMMAND_TEXT_VALUE + '" bar',
);
});
it("consumes a suffix segment adjacent to a serialized quoted header argument", () => {
// The suffix is part of the same shell word as the header, so it is part
// of the credential at every serialization depth.
let text = 'curl -H "X-API-Key: SECRET"TAILMARK;echo safe';
for (let depth = 1; depth <= 3; depth += 1) {
text = JSON.stringify(text);
const output = redactCommandText(text);
expect(output).not.toContain("SECRET");
expect(output).not.toContain("TAILMARK");
expect(output).toContain(";echo safe");
expect(() => JSON.parse(output)).not.toThrow();
expect(redactCommandText(output)).toBe(output);
}
});
it("keeps a serializer's closing delimiter when the argument is truncated", () => {
// A run log can cut a serialized command inside the header argument. The
// truncated value stops before the enclosing string's own quote, even
// when the cut lands after a backslash, so the string stays well formed.
const cuts = [
'curl -H "X-API-Key: SECRET',
'curl -H X-API-Key:"SECRET',
'curl -H "X-API-Key: SECRET\\',
'curl -H "X-API-Key: SECRET\\\\',
'curl -H X-API-Key:"SECRET\\',
];
for (const cut of cuts) {
for (const text of [JSON.stringify(cut), JSON.stringify(JSON.stringify(cut))]) {
const output = redactCommandText(text);
expect(output).not.toContain("SECRET");
expect(() => JSON.parse(output)).not.toThrow();
expect(redactCommandText(output)).toBe(output);
}
}
});
it("redacts a bare apikey header value", () => {
// Supabase sends the key under an unhyphenated `apikey` header.
expect(redactCommandText("apikey: abc")).toBe(
`apikey: ${REDACTED_COMMAND_TEXT_VALUE}`,
);
});
it("redacts a proxy-authorization header value", () => {
const input = 'curl -H "Proxy-Authorization: Basic dXNlcjpwdw=="';
const output = redactCommandText(input);
expect(output).not.toContain("dXNlcjpwdw==");
expect(output).toBe(
`curl -H "Proxy-Authorization: Basic ${REDACTED_COMMAND_TEXT_VALUE}"`,
);
});
it("is idempotent over a multi-part credential", () => {
const input =
`curl -H 'Authorization: Digest username="alice", response="deadbeef"' https://example.test`;
const once = redactCommandText(input);
expect(redactCommandText(once)).toBe(once);
expect(redactDiagnosticText(once)).toBe(once);
});
it("redacts past an escaped quote inside a double-quoted header value", () => {
// The shell escape does not end the argument, so the value runs on past it.
const input = String.raw`curl -H "X-API-Key: abc\"def" https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("def");
expect(output).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("redacts across a backslash-newline continuation inside a double-quoted value", () => {
// A shell line continuation inside double quotes is part of the argument.
const input = 'curl -H "X-API-Key: abc\\\ndef" https://example.test';
const output = redactCommandText(input);
expect(output).not.toContain("def");
expect(output).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
const crlf = 'curl -H "X-API-Key: abc\\\r\ndef" https://example.test';
expect(redactCommandText(crlf)).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("redacts a backslash inside a single-quoted header value", () => {
// A shell single quote has no escapes, so the backslash is part of the value.
const input = String.raw`curl -H 'X-API-Key: abc\def' https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
`curl -H 'X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}' https://example.test`,
);
});
it("redacts a double-quoted value that is itself an escaped quoted string", () => {
const input = String.raw`curl -H "Authorization: \"Bearer nested\"" https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("nested");
expect(output).toBe(
`curl -H "Authorization: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("redacts an entire serialized digest credential", () => {
// The header argument is escaped inside a JSON string, so its quotes read as
// `\"` and its own embedded quotes as `\\\"`. The value must still run to the
// end of the argument.
const input = String.raw`{"command":"curl -H \"Authorization: Digest username=\\\"alice\\\", response=\\\"deadbeef\\\"\" https://x"}`;
const output = redactCommandText(input);
expect(output).not.toContain("alice");
expect(output).not.toContain("deadbeef");
expect(output).toBe(
String.raw`{"command":"curl -H \"Authorization: Digest ` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" https://x"}`,
);
});
it("redacts past an embedded escaped quote in a serialized header value", () => {
const input = String.raw`{"command":"curl -H \"X-API-Key: abc\\\"def\" https://x"}`;
const output = redactCommandText(input);
expect(output).not.toContain("def");
expect(output).toBe(
String.raw`{"command":"curl -H \"X-API-Key: ` +
REDACTED_COMMAND_TEXT_VALUE +
String.raw`\" https://x"}`,
);
});
it("is idempotent over a serialized multi-part credential", () => {
const input = String.raw`{"command":"curl -H \"Authorization: Digest username=\\\"alice\\\", response=\\\"deadbeef\\\"\" https://x"}`;
const once = redactCommandText(input);
expect(redactCommandText(once)).toBe(once);
expect(redactDiagnosticText(once)).toBe(once);
});
it("redacts a value whose quotes cover only the value", () => {
// `X-API-Key:"abc123"` is one shell word, so the quoted part is the value.
// The value keeps its own delimiters, which makes a second pass a no-op.
const R = REDACTED_COMMAND_TEXT_VALUE;
const input = `curl -H X-API-Key:"abc123" https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("abc123");
expect(output).toBe(`curl -H X-API-Key:"${R}" https://example.test`);
expect(redactCommandText(`curl -H X-API-Key:'abc' https://x`)).toBe(
`curl -H X-API-Key:'${R}' https://x`,
);
expect(redactCommandText(`curl -H X-API-Key:$'abc' https://x`)).toBe(
`curl -H X-API-Key:$'${R}' https://x`,
);
});
it("is stable over a value-only quoted header with a following command", () => {
// The preserved delimiters keep the second pass from reading the
// placeholder as a bare token and eating the separator.
const R = REDACTED_COMMAND_TEXT_VALUE;
const once = redactCommandText(`curl -H X-API-Key:"abc"123;echo done`);
expect(once).toBe(`curl -H X-API-Key:"${R}";echo done`);
expect(redactCommandText(once)).toBe(once);
expect(redactDiagnosticText(once)).toBe(once);
});
it("redacts a segment adjacent to a quoted header argument", () => {
// The trailing `123` joins the same shell word, so it is part of the value.
const input = `curl -H "X-API-Key: abc"123 https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("123");
expect(output).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}" https://example.test`,
);
});
it("redacts across an unquoted escape pair", () => {
// `\ ` escapes the space, so the word continues past it.
const input = String.raw`curl -H X-API-Key:abc\ 123 https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("123");
expect(output).toBe(
`curl -H X-API-Key:${REDACTED_COMMAND_TEXT_VALUE} https://example.test`,
);
});
it("redacts an ANSI-C quoted header argument", () => {
const input = String.raw`curl -H $'X-API-Key: abc\'123' https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).not.toContain("123");
expect(output).toBe(
`curl -H $'X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}' https://example.test`,
);
});
it("keeps an unterminated quote out of the value", () => {
// A lone quote does not open a segment, so the word ends before it.
const input = String.raw`X-API-Key: abc"tail`;
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toBe(
`X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}"tail`,
);
});
it("redacts a concealed credential parameter list", () => {
// RFC 9729 writes the proof and key identifier as authentication parameters.
const input =
"Authorization: Concealed k=YmFzZW1lbnQ, a=PUBLICKEY, s=2055, v=VERIFY, p=PROOFSECRET status=401";
const output = redactCommandText(input);
expect(output).not.toContain("PROOFSECRET");
expect(output).not.toContain("YmFzZW1lbnQ");
expect(output).toBe(
`Authorization: Concealed ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
});
it("redacts a quoted concealed credential to the closing quote", () => {
const input = `curl -H "Authorization: Concealed k=YmFzZW1lbnQ, p=PROOFSECRET" https://x`;
const output = redactCommandText(input);
expect(output).not.toContain("PROOFSECRET");
expect(output).toBe(
`curl -H "Authorization: Concealed ${REDACTED_COMMAND_TEXT_VALUE}" https://x`,
);
});
it("redacts a digest credential whose parameter carries a quoted-pair", () => {
// HTTP quoted-string syntax allows an escaped character inside a parameter.
const input = String.raw`Authorization: Digest username="al\"ice", nonce="n", response="abc123" status=401`;
const output = redactCommandText(input);
expect(output).not.toContain("abc123");
expect(output).not.toContain("al");
expect(output).toBe(
`Authorization: Digest ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
expect(redactDiagnosticText(input)).toBe(
`Authorization: Digest ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
});
it("redacts a quoted header argument whose closing quote never arrives", () => {
// A truncated run log ends the line mid-argument. The value runs to the end
// of the line instead of to a closing quote.
const R = REDACTED_COMMAND_TEXT_VALUE;
expect(redactCommandText(`curl -H "X-API-Key: abc`)).toBe(
`curl -H "X-API-Key: ${R}`,
);
expect(redactCommandText(`curl -H 'X-API-Key: abc`)).toBe(
`curl -H 'X-API-Key: ${R}`,
);
expect(redactCommandText(`curl -H $'X-API-Key: abc`)).toBe(
`curl -H $'X-API-Key: ${R}`,
);
// A lone trailing backslash is part of the truncated value.
expect(redactCommandText('curl -H "X-API-Key: abc\\')).toBe(
`curl -H "X-API-Key: ${R}`,
);
// The next line is a separate line, so it stays as it is.
expect(redactCommandText('curl -H "X-API-Key: abc\nsecond line')).toBe(
`curl -H "X-API-Key: ${R}\nsecond line`,
);
});
it("redacts a value that opens with an escape pair", () => {
// `X-API-Key:\ abc123` is one shell word whose first value byte is escaped.
const input = String.raw`curl -H X-API-Key:\ abc123 https://example.test`;
const output = redactCommandText(input);
expect(output).not.toContain("abc123");
expect(output).toBe(
`curl -H X-API-Key:${REDACTED_COMMAND_TEXT_VALUE} https://example.test`,
);
});
it("redacts a raw header value that contains a shell metacharacter", () => {
// A raw HTTP diagnostic carries an opaque credential, so `;` inside the
// value is a credential byte and the whole token goes.
const input = "tool: X-API-Key: abc;def status=401";
const output = redactCommandText(input);
expect(output).not.toContain("def");
expect(output).toBe(
`tool: X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
expect(redactDiagnosticText(input)).toBe(
`tool: X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE} status=401`,
);
});
it("takes the whole raw token when a command shares that shape", () => {
// The same bytes read as a shell command would end the word at `;`. The
// raw-token reading wins, which over-redacts here and never under-redacts.
expect(redactCommandText("X-API-Key:abc;echo done")).toBe(
`X-API-Key:${REDACTED_COMMAND_TEXT_VALUE} done`,
);
});
it("stops a continuation segment at a shell metacharacter", () => {
// After a closing quote the word really does end at `;`, so the next
// command survives.
expect(redactCommandText(`curl -H "X-API-Key: abc"123;echo done`)).toBe(
`curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}";echo done`,
);
});
it("keeps a shell separator after a quoted header argument", () => {
// A metacharacter ends the shell word, so the pipeline and the next command
// survive the redaction.
expect(redactCommandText(`curl -H 'x-api-key: abc'|head`)).toBe(
`curl -H 'x-api-key: ${REDACTED_COMMAND_TEXT_VALUE}'|head`,
);
expect(
redactCommandText(`sh -c 'curl -H "X-API-Key: abc"; echo done'`),
).toBe(
`sh -c 'curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}"; echo done'`,
);
expect(redactCommandText(`(curl -H "X-API-Key: abc")`)).toBe(
`(curl -H "X-API-Key: ${REDACTED_COMMAND_TEXT_VALUE}")`,
);
});
it("is stable and keeps serialized commands parseable", () => {
const shellWordForms = [
`curl -H X-API-Key:"abc123" https://example.test`,
`curl -H "X-API-Key: abc"123 https://example.test`,
String.raw`curl -H X-API-Key:abc\ 123 https://example.test`,
String.raw`curl -H $'X-API-Key: abc\'123' https://example.test`,
];
const pinnedForms = [
`curl -H X-API-Key:"abc"123;echo done`,
`curl -H X-API-Key:'abc' https://x`,
`curl -H X-API-Key:$'abc' https://x`,
String.raw`sh -c "curl -H X-API-Key:\"abc123\" https://example.test"`,
String.raw`X-API-Key:\"abc`,
`curl -H "Authorization: Bearer abc" https://example.test`,
`curl -H "X-API-Key: " -H "X-Auth-Token:" https://example.test`,
`prefix Authorization: ${REDACTED_COMMAND_TEXT_VALUE} suffix`,
String.raw`prefix Authorization: \"Bearer nested\" suffix`,
'Authorization: Digest username="alice", response="deadbeef" status=401',
"Authorization: AWS4-HMAC-SHA256 Credential=AKIAEXAMPLE, Signature=abc123 retry",
String.raw`X-API-Key: abc"tail`,
];
for (const input of [...shellWordForms, ...pinnedForms]) {
const once = redactCommandText(input);
expect(redactCommandText(once)).toBe(once);
expect(redactDiagnosticText(once)).toBe(once);
}
// The escaped-quoted branch keeps a serialized command valid JSON.
const serializedForms = [
String.raw`{"command":"curl -H \"X-API-Key: abc\" https://example.test"}`,
String.raw`{"command":"curl -H \"Authorization: Digest username=\\\"alice\\\", response=\\\"deadbeef\\\"\" https://x"}`,
];
for (const input of serializedForms) {
const once = redactCommandText(input);
expect(() => JSON.parse(once)).not.toThrow();
expect(redactCommandText(once)).toBe(once);
}
});
it("redacts a serializer-nested value-only escaped-quoted credential", () => {
// `JSON.stringify` writes the inner shell's `\"` delimiter as `\\\"`. The
// value is delimited by the whole backslash run, so the extra layer changes
// nothing about which bytes belong to the credential.
const R = REDACTED_COMMAND_TEXT_VALUE;
const input = JSON.stringify({
command: String.raw`sh -c "curl -H Authorization:\"Digest username=alice, response=SECRETTAIL\" https://example.test"`,
status: "safe",
});
const output = redactCommandText(input);
expect(output).not.toContain("alice");
expect(output).not.toContain("SECRETTAIL");
expect(output).toContain(
String.raw`Authorization:\\\"Digest ` + R + String.raw`\\\"`,
);
const parsed = JSON.parse(output) as { command: string; status: string };
expect(parsed.status).toBe("safe");
expect(parsed.command).toBe(
String.raw`sh -c "curl -H Authorization:\"Digest ` +
R +
String.raw`\" https://example.test"`,
);
expect(redactCommandText(output)).toBe(output);
expect(redactDiagnosticText(output)).toBe(output);
});
it("redacts a serialized escaped-quoted argument one layer deeper", () => {
const R = REDACTED_COMMAND_TEXT_VALUE;
const input = JSON.stringify({
command: String.raw`curl -H \"X-API-Key: abc\" https://example.test`,
status: "safe",
});
const output = redactCommandText(input);
expect(output).not.toContain("abc");
expect(output).toContain(String.raw`\\\"X-API-Key: ` + R + String.raw`\\\"`);
const parsed = JSON.parse(output) as { command: string; status: string };
expect(parsed.status).toBe("safe");
expect(parsed.command).toBe(
String.raw`curl -H \"X-API-Key: ` + R + String.raw`\" https://example.test`,
);
expect(redactCommandText(output)).toBe(output);
expect(redactDiagnosticText(output)).toBe(output);
});
it("redacts an escaped-quoted argument three serialization layers deep", () => {
// Nothing in the rule counts layers, so a run of seven backslashes reads
// exactly like a run of one.
const R = REDACTED_COMMAND_TEXT_VALUE;
const input = JSON.stringify(
JSON.stringify({
command: String.raw`curl -H \"X-API-Key: abc\" https://example.test`,
status: "safe",
}),
);
const output = redactCommandText(input);
expect(output).not.toContain("abc");
const parsed = JSON.parse(JSON.parse(output) as string) as {
command: string;
status: string;
};
expect(parsed.status).toBe("safe");
expect(parsed.command).toBe(
String.raw`curl -H \"X-API-Key: ` + R + String.raw`\" https://example.test`,
);
expect(redactCommandText(output)).toBe(output);
expect(redactDiagnosticText(output)).toBe(output);
});
it("keeps a dangling trailing backslash inside an escaped-quoted value", () => {
// A truncated log can end mid-escape. The backslash does not begin the
// closer, so it belongs to the value.
const R = REDACTED_COMMAND_TEXT_VALUE;
const expected = String.raw`X-API-Key:\"` + R;
for (const tail of ["\\", "\\\\"]) {
const input = String.raw`X-API-Key:\"abc123` + tail;
const output = redactCommandText(input);
expect(output).not.toContain("abc123");
expect(output).toBe(expected);
expect(redactCommandText(output)).toBe(output);
expect(redactDiagnosticText(output)).toBe(output);
}
});
it("keeps an empty escaped-quoted header argument untouched", () => {
// The value must open with a non-blank character, so there is nothing to
// hide here and the argument stays byte for byte.
const input = String.raw`\"X-API-Key: \" https://example.test`;
expect(redactCommandText(input)).toBe(input);
expect(redactDiagnosticText(input)).toBe(input);
});
it("redacts a header secret inside a diagnostic and keeps a JSON secret field working", () => {
const input = 'command failed: curl -H "X-API-Key: abc" -> {"token":"opaque-value"}';
const output = redactDiagnosticText(input);
expect(output).not.toContain("abc");
expect(output).not.toContain("opaque-value");
expect(output).toContain("command failed:");
});
});