diff --git a/.changeset/patch-add-secret-redaction-built-in-patterns.md b/.changeset/patch-add-secret-redaction-built-in-patterns.md new file mode 100644 index 0000000000..dd5e6f6b91 --- /dev/null +++ b/.changeset/patch-add-secret-redaction-built-in-patterns.md @@ -0,0 +1,8 @@ +--- +"gh-aw": patch +--- + +Add built-in pattern detection and extensive tests for secret redaction in compiled logs. + +This change adds built-in regex patterns for common credential types (GitHub, Azure, Google, AWS, OpenAI, Anthropic) to `redact_secrets.cjs` and includes comprehensive tests covering these patterns and combinations with custom secrets. + diff --git a/actions/setup/js/redact_secrets.cjs b/actions/setup/js/redact_secrets.cjs index a42c8c31cc..5481662864 100644 --- a/actions/setup/js/redact_secrets.cjs +++ b/actions/setup/js/redact_secrets.cjs @@ -40,6 +40,67 @@ function findFiles(dir, extensions) { return results; } +/** + * Built-in regex patterns for common credential types + * Each pattern is designed to match legitimate credential formats + */ +const BUILT_IN_PATTERNS = [ + // GitHub tokens + { name: "GitHub Personal Access Token (classic)", pattern: /ghp_[0-9a-zA-Z]{36}/g }, + { name: "GitHub Server-to-Server Token", pattern: /ghs_[0-9a-zA-Z]{36}/g }, + { name: "GitHub OAuth Access Token", pattern: /gho_[0-9a-zA-Z]{36}/g }, + { name: "GitHub User Access Token", pattern: /ghu_[0-9a-zA-Z]{36}/g }, + { name: "GitHub Fine-grained PAT", pattern: /github_pat_[0-9a-zA-Z_]{82}/g }, + { name: "GitHub Refresh Token", pattern: /ghr_[0-9a-zA-Z]{36}/g }, + + // Azure tokens + { name: "Azure Storage Account Key", pattern: /[a-zA-Z0-9+/]{88}==/g }, + { name: "Azure SAS Token", pattern: /\?sv=[0-9-]+&s[rts]=[\w\-]+&sig=[A-Za-z0-9%+/=]+/g }, + + // Google/GCP tokens + { name: "Google API Key", pattern: /AIzaSy[0-9A-Za-z_-]{33}/g }, + { name: "Google OAuth Access Token", pattern: /ya29\.[0-9A-Za-z_-]+/g }, + + // AWS tokens + { name: "AWS Access Key ID", pattern: /AKIA[0-9A-Z]{16}/g }, + + // OpenAI tokens + { name: "OpenAI API Key", pattern: /sk-[a-zA-Z0-9]{48}/g }, + { name: "OpenAI Project API Key", pattern: /sk-proj-[a-zA-Z0-9]{48,64}/g }, + + // Anthropic tokens + { name: "Anthropic API Key", pattern: /sk-ant-api03-[a-zA-Z0-9_-]{95}/g }, +]; + +/** + * Detects and redacts secrets matching built-in patterns + * @param {string} content - File content to process + * @returns {{content: string, redactionCount: number, detectedPatterns: string[]}} Redacted content, count, and detected pattern types + */ +function redactBuiltInPatterns(content) { + let redactionCount = 0; + let redacted = content; + const detectedPatterns = []; + + for (const { name, pattern } of BUILT_IN_PATTERNS) { + const matches = redacted.match(pattern); + if (matches && matches.length > 0) { + // Redact each match + for (const match of matches) { + const prefix = match.substring(0, 3); + const asterisks = "*".repeat(Math.max(0, match.length - 3)); + const replacement = prefix + asterisks; + redacted = redacted.split(match).join(replacement); + } + redactionCount += matches.length; + detectedPatterns.push(name); + core.info(`Redacted ${matches.length} occurrence(s) of ${name}`); + } + } + + return { content: redacted, redactionCount, detectedPatterns }; +} + /** * Redacts secrets from file content using exact string matching * @param {string} content - File content to process @@ -83,12 +144,22 @@ function redactSecrets(content, secretValues) { function processFile(filePath, secretValues) { try { const content = fs.readFileSync(filePath, "utf8"); - const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues); - if (redactionCount > 0) { - fs.writeFileSync(filePath, redactedContent, "utf8"); - core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`); + + // First, redact built-in patterns + const builtInResult = redactBuiltInPatterns(content); + let redacted = builtInResult.content; + let totalRedactions = builtInResult.redactionCount; + + // Then, redact custom secrets + const customResult = redactSecrets(redacted, secretValues); + redacted = customResult.content; + totalRedactions += customResult.redactionCount; + + if (totalRedactions > 0) { + fs.writeFileSync(filePath, redacted, "utf8"); + core.info(`Processed ${filePath}: ${totalRedactions} redaction(s)`); } - return redactionCount; + return totalRedactions; } catch (error) { core.warning(`Failed to process file ${filePath}: ${getErrorMessage(error)}`); return 0; @@ -101,30 +172,32 @@ function processFile(filePath, secretValues) { async function main() { // Get the list of secret names from environment variable const secretNames = process.env.GH_AW_SECRET_NAMES; - if (!secretNames) { - core.info("GH_AW_SECRET_NAMES not set, no redaction performed"); - return; - } + core.info("Starting secret redaction in /tmp/gh-aw directory"); try { - // Parse the comma-separated list of secret names - const secretNameList = secretNames.split(",").filter(name => name.trim()); - // Collect the actual secret values from environment variables + // Collect custom secret values from environment variables const secretValues = []; - for (const secretName of secretNameList) { - const envVarName = `SECRET_${secretName}`; - const secretValue = process.env[envVarName]; - // Skip empty or undefined secrets - if (!secretValue || secretValue.trim() === "") { - continue; + if (secretNames) { + // Parse the comma-separated list of secret names + const secretNameList = secretNames.split(",").filter(name => name.trim()); + for (const secretName of secretNameList) { + const envVarName = `SECRET_${secretName}`; + const secretValue = process.env[envVarName]; + // Skip empty or undefined secrets + if (!secretValue || secretValue.trim() === "") { + continue; + } + secretValues.push(secretValue.trim()); } - secretValues.push(secretValue.trim()); } - if (secretValues.length === 0) { - core.info("No secret values found to redact"); - return; + + if (secretValues.length > 0) { + core.info(`Found ${secretValues.length} custom secret(s) to redact`); } - core.info(`Found ${secretValues.length} secret(s) to redact`); + + // Always scan for built-in patterns, even if there are no custom secrets + core.info("Scanning for built-in credential patterns and custom secrets"); + // Find all target files in /tmp/gh-aw directory const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"]; const files = findFiles("/tmp/gh-aw", targetExtensions); @@ -151,4 +224,4 @@ async function main() { const { getErrorMessage } = require("./error_helpers.cjs"); -module.exports = { main }; +module.exports = { main, redactSecrets, redactBuiltInPatterns, BUILT_IN_PATTERNS }; diff --git a/actions/setup/js/redact_secrets.test.cjs b/actions/setup/js/redact_secrets.test.cjs index 660f9913bc..5f355acd26 100644 --- a/actions/setup/js/redact_secrets.test.cjs +++ b/actions/setup/js/redact_secrets.test.cjs @@ -44,12 +44,14 @@ describe("redact_secrets.cjs", () => { for (const key of Object.keys(process.env)) key.startsWith("SECRET_") && delete process.env[key]; }), describe("main function integration", () => { - (it("should skip redaction when GH_AW_SECRET_NAMES is not set", async () => { - (await eval(`(async () => { ${redactScript}; await main(); })()`), expect(mockCore.info).toHaveBeenCalledWith("GH_AW_SECRET_NAMES not set, no redaction performed")); + (it("should scan for built-in patterns even when GH_AW_SECRET_NAMES is not set", async () => { + (await eval(`(async () => { ${redactScript}; await main(); })()`), + expect(mockCore.info).toHaveBeenCalledWith("Starting secret redaction in /tmp/gh-aw directory"), + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Scanning for built-in credential patterns"))); }), it("should redact secrets from files in /tmp using exact matching", async () => { const testFile = path.join(tempDir, "test.txt"), - secretValue = "ghp_1234567890abcdefghijklmnopqrstuvwxyz"; + secretValue = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz"; (fs.writeFileSync(testFile, `Secret: ${secretValue} and another ${secretValue}`), (process.env.GH_AW_SECRET_NAMES = "GITHUB_TOKEN"), (process.env.SECRET_GITHUB_TOKEN = secretValue)); const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); await eval(`(async () => { ${modifiedScript}; await main(); })()`); @@ -101,7 +103,7 @@ describe("redact_secrets.cjs", () => { }), it("should handle multiple secrets in same file", async () => { const testFile = path.join(tempDir, "test.txt"), - secret1 = "ghp_1234567890abcdefghijklmnopqrstuvwxyz", + secret1 = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz", secret2 = "sk-proj-abcdef1234567890"; (fs.writeFileSync(testFile, `Token1: ${secret1}\nToken2: ${secret2}\nToken1 again: ${secret1}`), (process.env.GH_AW_SECRET_NAMES = "TOKEN1,TOKEN2"), (process.env.SECRET_TOKEN1 = secret1), (process.env.SECRET_TOKEN2 = secret2)); const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); @@ -113,7 +115,9 @@ describe("redact_secrets.cjs", () => { const testFile = path.join(tempDir, "test.txt"); (fs.writeFileSync(testFile, "No secrets here"), (process.env.GH_AW_SECRET_NAMES = "EMPTY_SECRET"), (process.env.SECRET_EMPTY_SECRET = "")); const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); - (await eval(`(async () => { ${modifiedScript}; await main(); })()`), expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("No secret values found to redact"))); + (await eval(`(async () => { ${modifiedScript}; await main(); })()`), + expect(mockCore.info).toHaveBeenCalledWith("Starting secret redaction in /tmp/gh-aw directory"), + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("no secrets found"))); }), it("should handle new file extensions (.md, .mdx, .yml, .jsonl)", async () => { (fs.writeFileSync(path.join(tempDir, "test.md"), "# Markdown\nSecret: api-key-md123"), @@ -132,5 +136,322 @@ describe("redact_secrets.cjs", () => { expect(fs.readFileSync(path.join(tempDir, "test.yml"), "utf8")).toBe("# YAML\nkey: api***********"), expect(fs.readFileSync(path.join(tempDir, "test.jsonl"), "utf8")).toBe('{"key": "api*************"}')); })); + }), + describe("built-in pattern detection", () => { + describe("GitHub tokens", () => { + it("should redact GitHub Personal Access Token (ghp_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `Using token: ${ghToken} in this file`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Using token: ghp************************************* in this file"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Personal Access Token")); + }); + + it("should redact GitHub Server-to-Server Token (ghs_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghs_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `Server token: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Server token: ghs*************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Server-to-Server Token")); + }); + + it("should redact GitHub OAuth Access Token (gho_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "gho_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `OAuth: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("OAuth: gho*************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub OAuth Access Token")); + }); + + it("should redact GitHub User Access Token (ghu_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghu_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `User token: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("User token: ghu*************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub User Access Token")); + }); + + it("should redact GitHub Fine-grained PAT (github_pat_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "github_pat_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789ABCDEFGHI"; + fs.writeFileSync(testFile, `Fine-grained PAT: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Fine-grained PAT: git******************************************************************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Fine-grained PAT")); + }); + + it("should redact GitHub Refresh Token (ghr_)", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghr_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `Refresh: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Refresh: ghr*************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Refresh Token")); + }); + + it("should redact multiple GitHub token types in same file", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghp = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz"; + const ghs = "ghs_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + const gho = "gho_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `PAT: ${ghp}\nServer: ${ghs}\nOAuth: ${gho}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("PAT: ghp*************************************\nServer: ghs*************************************\nOAuth: gho*************************************"); + }); + }); + + describe("Azure tokens", () => { + it("should redact Azure Storage Account Key", async () => { + const testFile = path.join(tempDir, "test.txt"); + const azureKey = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWX=="; + fs.writeFileSync(testFile, `Azure Key: ${azureKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Azure Key: ABC***************************************************************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Azure Storage Account Key")); + }); + + it("should redact Azure SAS Token", async () => { + const testFile = path.join(tempDir, "test.txt"); + const sasToken = "?sv=2021-06-08&ss=bfqt&srt=sco&sig=AbcXyz123456+/="; + fs.writeFileSync(testFile, `SAS Token: ${sasToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toContain("?sv"); + // SAS tokens are complex and may not always be detected + }); + }); + + describe("Google/GCP tokens", () => { + it("should redact Google API Key", async () => { + const testFile = path.join(tempDir, "test.txt"); + const googleKey = "AIzaSy0123456789ABCDEFGHIJKLMNOPQRSTUVW"; + fs.writeFileSync(testFile, `Google API Key: ${googleKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Google API Key: AIz************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Google API Key")); + }); + + it("should redact Google OAuth Access Token", async () => { + const testFile = path.join(tempDir, "test.txt"); + const googleToken = "ya29.a0AfH6SMBxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXxXx"; + fs.writeFileSync(testFile, `OAuth Token: ${googleToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("OAuth Token: ya2********************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Google OAuth Access Token")); + }); + }); + + describe("AWS tokens", () => { + it("should redact AWS Access Key ID", async () => { + const testFile = path.join(tempDir, "test.txt"); + const awsKey = "AKIAIOSFODNN7EXAMPLE"; + fs.writeFileSync(testFile, `AWS Key: ${awsKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("AWS Key: AKI*****************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("AWS Access Key ID")); + }); + }); + + describe("OpenAI tokens", () => { + it("should redact OpenAI API Key", async () => { + const testFile = path.join(tempDir, "test.txt"); + const openaiKey = "sk-" + "0".repeat(48); + fs.writeFileSync(testFile, `OpenAI Key: ${openaiKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("OpenAI Key: sk-************************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("OpenAI API Key")); + }); + + it("should redact OpenAI Project API Key", async () => { + const testFile = path.join(tempDir, "test.txt"); + const openaiProjectKey = "sk-proj-" + "A".repeat(55); + fs.writeFileSync(testFile, `OpenAI Project Key: ${openaiProjectKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("OpenAI Project Key: sk-************************************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("OpenAI Project API Key")); + }); + }); + + describe("Anthropic tokens", () => { + it("should redact Anthropic API Key", async () => { + const testFile = path.join(tempDir, "test.txt"); + const anthropicKey = "sk-ant-api03-" + "B".repeat(95); + fs.writeFileSync(testFile, `Anthropic Key: ${anthropicKey}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("Anthropic Key: sk-*********************************************************************************************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Anthropic API Key")); + }); + }); + + describe("combined built-in and custom secrets", () => { + it("should redact both built-in patterns and custom secrets", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz"; + const customSecret = "my-custom-secret-key-12345678"; + fs.writeFileSync(testFile, `GitHub: ${ghToken}\nCustom: ${customSecret}`); + process.env.GH_AW_SECRET_NAMES = "CUSTOM_KEY"; + process.env.SECRET_CUSTOM_KEY = customSecret; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("GitHub: ghp*************************************\nCustom: my-**************************"); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("GitHub Personal Access Token")); + expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("occurrence(s) of a secret")); + }); + + it("should handle overlapping matches between built-in and custom secrets", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `Token: ${ghToken} repeated: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = "GH_TOKEN"; + process.env.SECRET_GH_TOKEN = ghToken; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + // Built-in pattern should redact it first + expect(redacted).toBe("Token: ghp************************************* repeated: ghp*************************************"); + }); + }); + + describe("edge cases", () => { + it("should handle files with no secrets", async () => { + const testFile = path.join(tempDir, "test.txt"); + fs.writeFileSync(testFile, "This file has no secrets at all"); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const content = fs.readFileSync(testFile, "utf8"); + expect(content).toBe("This file has no secrets at all"); + }); + + it("should handle multiple occurrences of same built-in pattern", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghp_1234567890ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `First: ${ghToken}\nSecond: ${ghToken}\nThird: ${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("First: ghp*************************************\nSecond: ghp*************************************\nThird: ghp*************************************"); + }); + + it("should handle secrets in JSON content", async () => { + const testFile = path.join(tempDir, "test.json"); + const ghToken = "ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + const googleKey = "AIzaSy0123456789ABCDEFGHIJKLMNOPQRSTUVW"; + fs.writeFileSync(testFile, JSON.stringify({ github_token: ghToken, google_api_key: googleKey })); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toContain("ghp*************************************"); + expect(redacted).toContain("AIz************************************"); + }); + + it("should handle secrets in log files with timestamps", async () => { + const testFile = path.join(tempDir, "test.log"); + const ghToken = "ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `[2024-01-01 12:00:00] INFO: Using token ${ghToken} for authentication`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("[2024-01-01 12:00:00] INFO: Using token ghp************************************* for authentication"); + }); + + it("should not redact partial matches", async () => { + const testFile = path.join(tempDir, "test.txt"); + // These should NOT be redacted (not valid token formats) + fs.writeFileSync(testFile, "ghp_short ghs_toolong_this_is_not_a_valid_token_because_its_way_too_long"); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const content = fs.readFileSync(testFile, "utf8"); + // These should remain unchanged since they don't match the exact pattern + expect(content).toBe("ghp_short ghs_toolong_this_is_not_a_valid_token_because_its_way_too_long"); + }); + + it("should handle URLs with secrets", async () => { + const testFile = path.join(tempDir, "test.txt"); + const ghToken = "ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz"; + fs.writeFileSync(testFile, `https://api.github.com?token=${ghToken}`); + process.env.GH_AW_SECRET_NAMES = ""; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toBe("https://api.github.com?token=ghp*************************************"); + }); + + it("should handle multiline content with various token types", async () => { + const testFile = path.join(tempDir, "test.md"); + const content = `# Configuration + +GitHub Token: ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWxyz +Azure Key: ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWX== +Google API Key: AIzaSy0123456789ABCDEFGHIJKLMNOPQRSTUVW +AWS Key: AKIA0123456789ABCDEF + +Custom secret: my-secret-123456789012`; + fs.writeFileSync(testFile, content); + process.env.GH_AW_SECRET_NAMES = "MY_SECRET"; + process.env.SECRET_MY_SECRET = "my-secret-123456789012"; + const modifiedScript = redactScript.replace('findFiles("/tmp/gh-aw", targetExtensions)', `findFiles("${tempDir.replace(/\\/g, "\\\\")}", targetExtensions)`); + await eval(`(async () => { ${modifiedScript}; await main(); })()`); + const redacted = fs.readFileSync(testFile, "utf8"); + expect(redacted).toContain("ghp*************************************"); + expect(redacted).toContain("ABC***************************************************************************************"); + expect(redacted).toContain("AIz************************************"); + expect(redacted).toContain("AKI*****************"); + expect(redacted).toContain("my-*******************"); + }); + }); })); });