mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-07 03:38:30 +08:00
Redact common credential patterns from published run output (#1595)
* Redact common credential patterns from published run output * Handle color codes and escape sequences ahead of redacted values Vendor-prefixed formats no longer require a leading word boundary, so a value that follows an ANSI SGR terminator or a serialized JSON escape is still matched. AWS key ids keep a boundary but also accept those cases. sanitizeContent goes back to GitHub-only redaction for inbound content, and the failure annotation is redacted like the tracking comment. * Coerce non-string text content before redacting tool results No-Verification-Needed: one-line coercion in a formatting helper plus regression test
This commit is contained in:
parent
c038e4dcde
commit
0aee57ab82
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { readFileSync, existsSync } from "fs";
|
import { readFileSync, existsSync } from "fs";
|
||||||
import { exit } from "process";
|
import { exit } from "process";
|
||||||
|
import { redactSecrets } from "../github/utils/sanitizer";
|
||||||
|
|
||||||
export type ToolUse = {
|
export type ToolUse = {
|
||||||
type: string;
|
type: string;
|
||||||
@ -163,8 +164,9 @@ export function formatResultContent(content: any): string {
|
|||||||
typeof parsedContent[0] === "object" &&
|
typeof parsedContent[0] === "object" &&
|
||||||
parsedContent[0]?.type === "text"
|
parsedContent[0]?.type === "text"
|
||||||
) {
|
) {
|
||||||
// Extract the text field from the first item
|
// Extract the text field from the first item. Tool output is arbitrary,
|
||||||
contentStr = parsedContent[0]?.text || "";
|
// so `text` is not guaranteed to be a string.
|
||||||
|
contentStr = String(parsedContent[0]?.text || "");
|
||||||
} else {
|
} else {
|
||||||
contentStr = String(content).trim();
|
contentStr = String(content).trim();
|
||||||
}
|
}
|
||||||
@ -172,6 +174,10 @@ export function formatResultContent(content: any): string {
|
|||||||
contentStr = String(content).trim();
|
contentStr = String(content).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Redact before truncating so a credential cannot be split at the cut and
|
||||||
|
// slip past the final redaction pass.
|
||||||
|
contentStr = redactSecrets(contentStr);
|
||||||
|
|
||||||
// Truncate very long results
|
// Truncate very long results
|
||||||
if (contentStr.length > 3000) {
|
if (contentStr.length > 3000) {
|
||||||
contentStr = contentStr.substring(0, 2997) + "...";
|
contentStr = contentStr.substring(0, 2997) + "...";
|
||||||
@ -420,7 +426,9 @@ export function formatTurnsFromData(data: Turn[]): string {
|
|||||||
// Generate markdown
|
// Generate markdown
|
||||||
const markdown = formatGroupedContent(groupedContent);
|
const markdown = formatGroupedContent(groupedContent);
|
||||||
|
|
||||||
return markdown;
|
// Runtime output may contain credentials that are not registered as
|
||||||
|
// workflow secrets, so redact known formats before this gets published.
|
||||||
|
return redactSecrets(markdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
function main(): void {
|
function main(): void {
|
||||||
@ -447,14 +455,8 @@ function main(): void {
|
|||||||
const fileContent = readFileSync(jsonFile, "utf-8");
|
const fileContent = readFileSync(jsonFile, "utf-8");
|
||||||
const data: Turn[] = JSON.parse(fileContent);
|
const data: Turn[] = JSON.parse(fileContent);
|
||||||
|
|
||||||
// Group turns naturally
|
|
||||||
const groupedContent = groupTurnsNaturally(data);
|
|
||||||
|
|
||||||
// Generate markdown
|
|
||||||
const markdown = formatGroupedContent(groupedContent);
|
|
||||||
|
|
||||||
// Print to stdout (so it can be captured by shell)
|
// Print to stdout (so it can be captured by shell)
|
||||||
console.log(markdown);
|
console.log(formatTurnsFromData(data));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error processing file: ${error}`);
|
console.error(`Error processing file: ${error}`);
|
||||||
exit(1);
|
exit(1);
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import { collectActionInputsPresence } from "./collect-inputs";
|
|||||||
import { updateCommentLink } from "./update-comment-link";
|
import { updateCommentLink } from "./update-comment-link";
|
||||||
import { formatTurnsFromData } from "./format-turns";
|
import { formatTurnsFromData } from "./format-turns";
|
||||||
import type { Turn } from "./format-turns";
|
import type { Turn } from "./format-turns";
|
||||||
|
import { redactSecrets } from "../github/utils/sanitizer";
|
||||||
// Base-action imports (used directly instead of subprocess)
|
// Base-action imports (used directly instead of subprocess)
|
||||||
import { setupWorkloadIdentity } from "../../base-action/src/workload-identity";
|
import { setupWorkloadIdentity } from "../../base-action/src/workload-identity";
|
||||||
import type { WorkloadIdentityHandle } from "../../base-action/src/workload-identity";
|
import type { WorkloadIdentityHandle } from "../../base-action/src/workload-identity";
|
||||||
@ -137,7 +138,7 @@ async function writeStepSummary(executionFile: string): Promise<void> {
|
|||||||
fallback +=
|
fallback +=
|
||||||
"Failed to format output (please report). Here's the raw JSON:\n\n";
|
"Failed to format output (please report). Here's the raw JSON:\n\n";
|
||||||
fallback += "```json\n";
|
fallback += "```json\n";
|
||||||
fallback += readFileSync(executionFile, "utf-8");
|
fallback += redactSecrets(readFileSync(executionFile, "utf-8"));
|
||||||
fallback += "\n```\n";
|
fallback += "\n```\n";
|
||||||
await appendFile(summaryFile, fallback);
|
await appendFile(summaryFile, fallback);
|
||||||
} catch {
|
} catch {
|
||||||
@ -317,7 +318,7 @@ async function run() {
|
|||||||
prepareSuccess = false;
|
prepareSuccess = false;
|
||||||
prepareError = errorMessage;
|
prepareError = errorMessage;
|
||||||
}
|
}
|
||||||
core.setFailed(`Action failed with error: ${errorMessage}`);
|
core.setFailed(`Action failed with error: ${redactSecrets(errorMessage)}`);
|
||||||
} finally {
|
} finally {
|
||||||
// Phase 4: Cleanup (always runs)
|
// Phase 4: Cleanup (always runs)
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { GITHUB_SERVER_URL } from "../api/config";
|
import { GITHUB_SERVER_URL } from "../api/config";
|
||||||
|
import { redactSecrets } from "../utils/sanitizer";
|
||||||
|
|
||||||
export type ExecutionDetails = {
|
export type ExecutionDetails = {
|
||||||
total_cost_usd?: number;
|
total_cost_usd?: number;
|
||||||
@ -181,9 +182,11 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
// Build the new body with blank line between header and separator
|
// Build the new body with blank line between header and separator
|
||||||
let newBody = `${header}${links}`;
|
let newBody = `${header}${links}`;
|
||||||
|
|
||||||
// Add error details if available
|
// Add error details if available. The message may embed runtime credentials
|
||||||
|
// (e.g. a token in a git remote URL) that are not registered as workflow
|
||||||
|
// secrets, so redact known formats before posting.
|
||||||
if (actionFailed && errorDetails) {
|
if (actionFailed && errorDetails) {
|
||||||
newBody += `\n\n\`\`\`\n${errorDetails}\n\`\`\``;
|
newBody += `\n\n\`\`\`\n${redactSecrets(errorDetails)}\n\`\`\``;
|
||||||
}
|
}
|
||||||
|
|
||||||
newBody += `\n\n---\n`;
|
newBody += `\n\n---\n`;
|
||||||
|
|||||||
@ -76,40 +76,82 @@ export function sanitizeContent(content: string): string {
|
|||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redact well-known credential formats (GitHub, Anthropic, AWS, Slack, JWTs)
|
||||||
|
* from arbitrary text. Callers don't need to know which vendor a value belongs to.
|
||||||
|
*
|
||||||
|
* Vendor-prefixed formats are matched without a leading word boundary: the
|
||||||
|
* prefix already anchors them, and runtime output frequently puts a word
|
||||||
|
* character directly against the value (e.g. an ANSI color code ending in `m`,
|
||||||
|
* or a serialized JSON escape such as `\n`).
|
||||||
|
*/
|
||||||
|
export function redactSecrets(content: string): string {
|
||||||
|
content = redactGitHubTokens(content);
|
||||||
|
|
||||||
|
// Anthropic API keys: sk-ant-...
|
||||||
|
content = content.replace(
|
||||||
|
/sk-ant-[A-Za-z0-9_-]{20,}/g,
|
||||||
|
"[REDACTED_ANTHROPIC_KEY]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// AWS access key ids: AKIA/ASIA followed by 16 uppercase alphanumerics. All
|
||||||
|
// uppercase alphanumeric, so keep a leading boundary to avoid matching inside
|
||||||
|
// larger blobs; also treat a JSON escape or ANSI color code as a boundary.
|
||||||
|
content = content.replace(
|
||||||
|
/(?:\b|(?<=\\(?:[nrtbf"\\/]|u[0-9a-fA-F]{4}))|(?<=\[[0-9;]*m))(?:AKIA|ASIA)[A-Z0-9]{16}\b/g,
|
||||||
|
"[REDACTED_AWS_KEY_ID]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slack tokens: xoxb-, xoxp-, xoxa-, xoxs-, xoxr-
|
||||||
|
content = content.replace(
|
||||||
|
/xox[abpsr]-[A-Za-z0-9-]{10,}/g,
|
||||||
|
"[REDACTED_SLACK_TOKEN]",
|
||||||
|
);
|
||||||
|
|
||||||
|
// JWT-shaped strings: three base64url segments, the first two starting
|
||||||
|
// with eyJ (base64 of `{"`).
|
||||||
|
content = content.replace(
|
||||||
|
/eyJ[A-Za-z0-9_-]{10,2000}\.eyJ[A-Za-z0-9_-]{10,4000}\.[A-Za-z0-9_-]{10,2000}\b/g,
|
||||||
|
"[REDACTED_JWT]",
|
||||||
|
);
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
export function redactGitHubTokens(content: string): string {
|
export function redactGitHubTokens(content: string): string {
|
||||||
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bghp_[A-Za-z0-9]{36}\b/g,
|
/ghp_[A-Za-z0-9]{36}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bgho_[A-Za-z0-9]{36}\b/g,
|
/gho_[A-Za-z0-9]{36}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// GitHub user-to-server tokens: ghu_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
// GitHub user-to-server tokens: ghu_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bghu_[A-Za-z0-9]{36}\b/g,
|
/ghu_[A-Za-z0-9]{36}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bghs_[A-Za-z0-9]{36}\b/g,
|
/ghs_[A-Za-z0-9]{36}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bghr_[A-Za-z0-9]{36}\b/g,
|
/ghr_[A-Za-z0-9]{36}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
|
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
|
||||||
content = content.replace(
|
content = content.replace(
|
||||||
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
|
/github_pat_[A-Za-z0-9_]{11,221}\b/g,
|
||||||
"[REDACTED_GITHUB_TOKEN]",
|
"[REDACTED_GITHUB_TOKEN]",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -467,6 +467,15 @@ describe("formatResultContent non-string input", () => {
|
|||||||
expect(typeof result).toBe("string");
|
expect(typeof result).toBe("string");
|
||||||
expect(result.length).toBeGreaterThan(0);
|
expect(result.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("handles a text content block whose text field is not a string", () => {
|
||||||
|
expect(() =>
|
||||||
|
formatResultContent('[{"type":"text","text":{"foo":"bar"}}]'),
|
||||||
|
).not.toThrow();
|
||||||
|
expect(formatResultContent('[{"type":"text","text":123}]')).toContain(
|
||||||
|
"123",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("system_other handling", () => {
|
describe("system_other handling", () => {
|
||||||
@ -515,3 +524,106 @@ describe("system_other handling", () => {
|
|||||||
expect(result).toContain("## 🚀 System Initialization");
|
expect(result).toContain("## 🚀 System Initialization");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("credential redaction", () => {
|
||||||
|
test("redacts credentials embedded in tool results", () => {
|
||||||
|
const data: Turn[] = [
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_use",
|
||||||
|
id: "toolu_1",
|
||||||
|
name: "Bash",
|
||||||
|
input: { command: "cat .env" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: "toolu_1",
|
||||||
|
content:
|
||||||
|
"GITHUB_TOKEN=ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW\nAWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatTurnsFromData(data);
|
||||||
|
|
||||||
|
expect(result).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(result).toContain("[REDACTED_AWS_KEY_ID]");
|
||||||
|
expect(result).not.toContain("ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW");
|
||||||
|
expect(result).not.toContain("AKIAIOSFODNN7EXAMPLE");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("redacts credentials embedded in multi-line tool inputs", () => {
|
||||||
|
const data: Turn[] = [
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_use",
|
||||||
|
id: "toolu_2",
|
||||||
|
name: "Write",
|
||||||
|
input: {
|
||||||
|
file_path: ".env",
|
||||||
|
content:
|
||||||
|
"AWS_ACCESS_KEY_ID=x\nGITHUB_TOKEN=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\n",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatTurnsFromData(data);
|
||||||
|
|
||||||
|
expect(result).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(result).not.toContain("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");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("redacts credentials wrapped in ANSI color codes", () => {
|
||||||
|
const key = "sk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-abcdefgh";
|
||||||
|
const data: Turn[] = [
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_use",
|
||||||
|
id: "toolu_3",
|
||||||
|
name: "Bash",
|
||||||
|
input: { command: "node print-config.js" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "user",
|
||||||
|
message: {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: "toolu_3",
|
||||||
|
content: `apiKey: \x1b[32m${key}\x1b[39m\nregion: us-east-1`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatTurnsFromData(data);
|
||||||
|
|
||||||
|
expect(result).toContain("[REDACTED_ANTHROPIC_KEY]");
|
||||||
|
expect(result).not.toContain(key);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
sanitizeContent,
|
sanitizeContent,
|
||||||
stripHtmlComments,
|
stripHtmlComments,
|
||||||
redactGitHubTokens,
|
redactGitHubTokens,
|
||||||
|
redactSecrets,
|
||||||
} from "../src/github/utils/sanitizer";
|
} from "../src/github/utils/sanitizer";
|
||||||
|
|
||||||
describe("stripInvisibleCharacters", () => {
|
describe("stripInvisibleCharacters", () => {
|
||||||
@ -368,7 +369,122 @@ export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("redactSecrets", () => {
|
||||||
|
it("should still redact GitHub tokens", () => {
|
||||||
|
expect(
|
||||||
|
redactSecrets("Token: ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW"),
|
||||||
|
).toBe("Token: [REDACTED_GITHUB_TOKEN]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact Anthropic API keys (sk-ant-)", () => {
|
||||||
|
const key = "sk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-abcdefgh";
|
||||||
|
expect(redactSecrets(`ANTHROPIC_API_KEY=${key}`)).toBe(
|
||||||
|
"ANTHROPIC_API_KEY=[REDACTED_ANTHROPIC_KEY]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact sk- strings that are not sk-ant-", () => {
|
||||||
|
const content =
|
||||||
|
"sk-proj-abcdefghijklmnopqrstuvwxyz0123456789 and sk-ant-short";
|
||||||
|
expect(redactSecrets(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact AWS access key ids", () => {
|
||||||
|
expect(redactSecrets("aws_access_key_id = AKIAIOSFODNN7EXAMPLE")).toBe(
|
||||||
|
"aws_access_key_id = [REDACTED_AWS_KEY_ID]",
|
||||||
|
);
|
||||||
|
expect(redactSecrets("temp creds ASIAIOSFODNN7EXAMPLE end")).toBe(
|
||||||
|
"temp creds [REDACTED_AWS_KEY_ID] end",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact AWS-like strings that do not fit the format", () => {
|
||||||
|
const content =
|
||||||
|
"AKIAtest AKIA123 AKIAIOSFODNN7EXAMPLEXYZ akiaiosfodnn7example";
|
||||||
|
expect(redactSecrets(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact Slack tokens", () => {
|
||||||
|
expect(redactSecrets("token=xoxb-1234567890-abcdefghijkl")).toBe(
|
||||||
|
"token=[REDACTED_SLACK_TOKEN]",
|
||||||
|
);
|
||||||
|
expect(redactSecrets("xoxp-1234567890-1234567890-abc")).toBe(
|
||||||
|
"[REDACTED_SLACK_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact xox strings that do not fit the format", () => {
|
||||||
|
const content = "xoxo-1234567890abc xoxz-1234567890abc xoxb-short";
|
||||||
|
expect(redactSecrets(content)).toBe(content);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact JWT-shaped strings", () => {
|
||||||
|
const jwt =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
|
||||||
|
expect(redactSecrets(`Authorization: Bearer ${jwt}`)).toBe(
|
||||||
|
"Authorization: Bearer [REDACTED_JWT]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact tokens that follow a JSON escape sequence", () => {
|
||||||
|
const serialized = JSON.stringify({
|
||||||
|
content:
|
||||||
|
"line one\nGITHUB_TOKEN=ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW\tsk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWx",
|
||||||
|
});
|
||||||
|
const redacted = redactSecrets(serialized);
|
||||||
|
expect(redacted).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(redacted).toContain("[REDACTED_ANTHROPIC_KEY]");
|
||||||
|
expect(redacted).not.toContain("ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact tokens preceded by ANSI color codes", () => {
|
||||||
|
const ghp = "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";
|
||||||
|
const anthropic =
|
||||||
|
"sk-ant-api03-AbCdEfGhIjKlMnOpQrStUvWxYz0123456789_-abcdefgh";
|
||||||
|
expect(redactSecrets(`\x1b[31m${ghp}\x1b[0m`)).toBe(
|
||||||
|
"\x1b[31m[REDACTED_GITHUB_TOKEN]\x1b[0m",
|
||||||
|
);
|
||||||
|
expect(redactSecrets(`key=\x1b[32m${anthropic}\x1b[39m`)).toBe(
|
||||||
|
"key=\x1b[32m[REDACTED_ANTHROPIC_KEY]\x1b[39m",
|
||||||
|
);
|
||||||
|
expect(redactSecrets(`\x1b[1mAKIAIOSFODNN7EXAMPLE\x1b[0m`)).toBe(
|
||||||
|
"\x1b[1m[REDACTED_AWS_KEY_ID]\x1b[0m",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redact tokens that follow other JSON escapes", () => {
|
||||||
|
const ghp = "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";
|
||||||
|
const serialized = JSON.stringify({
|
||||||
|
colored: `\x1b[31m${ghp}`,
|
||||||
|
formfeed: `\f${ghp}`,
|
||||||
|
quoted: `"AKIAIOSFODNN7EXAMPLE"`,
|
||||||
|
});
|
||||||
|
const redacted = redactSecrets(serialized);
|
||||||
|
expect(redacted).not.toContain(ghp);
|
||||||
|
expect(redacted).not.toContain("AKIAIOSFODNN7EXAMPLE");
|
||||||
|
expect(redacted).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||||
|
expect(redacted).toContain("[REDACTED_AWS_KEY_ID]");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not redact base64 blobs that are not JWTs", () => {
|
||||||
|
// Long base64 without dots, and two-segment strings, are left alone
|
||||||
|
const content =
|
||||||
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJzdWIiOiIxMjM0NTY3ODkw " +
|
||||||
|
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0 " +
|
||||||
|
"aGVsbG8gd29ybGQgdGhpcyBpcyBub3QgYSBqd3Q=";
|
||||||
|
expect(redactSecrets(content)).toBe(content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("sanitizeContent with token redaction", () => {
|
describe("sanitizeContent with token redaction", () => {
|
||||||
|
it("should only redact GitHub tokens from inbound content", () => {
|
||||||
|
const content =
|
||||||
|
"docs example key AKIAIOSFODNN7EXAMPLE and token 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";
|
||||||
|
expect(sanitizeContent(content)).toBe(
|
||||||
|
"docs example key AKIAIOSFODNN7EXAMPLE and token [REDACTED_GITHUB_TOKEN]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("should redact tokens as part of full sanitization", () => {
|
it("should redact tokens as part of full sanitization", () => {
|
||||||
const content = `
|
const content = `
|
||||||
<!-- Hidden comment with token: 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 -->
|
<!-- Hidden comment with token: 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 -->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user