Skip to content

Commit 7758ad3

Browse files
authored
Add built-in pattern detection for secret redaction in compiled logs (#11175)
1 parent 508ae91 commit 7758ad3

3 files changed

Lines changed: 431 additions & 29 deletions

File tree

.changeset/patch-add-secret-redaction-built-in-patterns.md

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

actions/setup/js/redact_secrets.cjs

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,67 @@ function findFiles(dir, extensions) {
4040
return results;
4141
}
4242

43+
/**
44+
* Built-in regex patterns for common credential types
45+
* Each pattern is designed to match legitimate credential formats
46+
*/
47+
const BUILT_IN_PATTERNS = [
48+
// GitHub tokens
49+
{ name: "GitHub Personal Access Token (classic)", pattern: /ghp_[0-9a-zA-Z]{36}/g },
50+
{ name: "GitHub Server-to-Server Token", pattern: /ghs_[0-9a-zA-Z]{36}/g },
51+
{ name: "GitHub OAuth Access Token", pattern: /gho_[0-9a-zA-Z]{36}/g },
52+
{ name: "GitHub User Access Token", pattern: /ghu_[0-9a-zA-Z]{36}/g },
53+
{ name: "GitHub Fine-grained PAT", pattern: /github_pat_[0-9a-zA-Z_]{82}/g },
54+
{ name: "GitHub Refresh Token", pattern: /ghr_[0-9a-zA-Z]{36}/g },
55+
56+
// Azure tokens
57+
{ name: "Azure Storage Account Key", pattern: /[a-zA-Z0-9+/]{88}==/g },
58+
{ name: "Azure SAS Token", pattern: /\?sv=[0-9-]+&s[rts]=[\w\-]+&sig=[A-Za-z0-9%+/=]+/g },
59+
60+
// Google/GCP tokens
61+
{ name: "Google API Key", pattern: /AIzaSy[0-9A-Za-z_-]{33}/g },
62+
{ name: "Google OAuth Access Token", pattern: /ya29\.[0-9A-Za-z_-]+/g },
63+
64+
// AWS tokens
65+
{ name: "AWS Access Key ID", pattern: /AKIA[0-9A-Z]{16}/g },
66+
67+
// OpenAI tokens
68+
{ name: "OpenAI API Key", pattern: /sk-[a-zA-Z0-9]{48}/g },
69+
{ name: "OpenAI Project API Key", pattern: /sk-proj-[a-zA-Z0-9]{48,64}/g },
70+
71+
// Anthropic tokens
72+
{ name: "Anthropic API Key", pattern: /sk-ant-api03-[a-zA-Z0-9_-]{95}/g },
73+
];
74+
75+
/**
76+
* Detects and redacts secrets matching built-in patterns
77+
* @param {string} content - File content to process
78+
* @returns {{content: string, redactionCount: number, detectedPatterns: string[]}} Redacted content, count, and detected pattern types
79+
*/
80+
function redactBuiltInPatterns(content) {
81+
let redactionCount = 0;
82+
let redacted = content;
83+
const detectedPatterns = [];
84+
85+
for (const { name, pattern } of BUILT_IN_PATTERNS) {
86+
const matches = redacted.match(pattern);
87+
if (matches && matches.length > 0) {
88+
// Redact each match
89+
for (const match of matches) {
90+
const prefix = match.substring(0, 3);
91+
const asterisks = "*".repeat(Math.max(0, match.length - 3));
92+
const replacement = prefix + asterisks;
93+
redacted = redacted.split(match).join(replacement);
94+
}
95+
redactionCount += matches.length;
96+
detectedPatterns.push(name);
97+
core.info(`Redacted ${matches.length} occurrence(s) of ${name}`);
98+
}
99+
}
100+
101+
return { content: redacted, redactionCount, detectedPatterns };
102+
}
103+
43104
/**
44105
* Redacts secrets from file content using exact string matching
45106
* @param {string} content - File content to process
@@ -83,12 +144,22 @@ function redactSecrets(content, secretValues) {
83144
function processFile(filePath, secretValues) {
84145
try {
85146
const content = fs.readFileSync(filePath, "utf8");
86-
const { content: redactedContent, redactionCount } = redactSecrets(content, secretValues);
87-
if (redactionCount > 0) {
88-
fs.writeFileSync(filePath, redactedContent, "utf8");
89-
core.info(`Processed ${filePath}: ${redactionCount} redaction(s)`);
147+
148+
// First, redact built-in patterns
149+
const builtInResult = redactBuiltInPatterns(content);
150+
let redacted = builtInResult.content;
151+
let totalRedactions = builtInResult.redactionCount;
152+
153+
// Then, redact custom secrets
154+
const customResult = redactSecrets(redacted, secretValues);
155+
redacted = customResult.content;
156+
totalRedactions += customResult.redactionCount;
157+
158+
if (totalRedactions > 0) {
159+
fs.writeFileSync(filePath, redacted, "utf8");
160+
core.info(`Processed ${filePath}: ${totalRedactions} redaction(s)`);
90161
}
91-
return redactionCount;
162+
return totalRedactions;
92163
} catch (error) {
93164
core.warning(`Failed to process file ${filePath}: ${getErrorMessage(error)}`);
94165
return 0;
@@ -101,30 +172,32 @@ function processFile(filePath, secretValues) {
101172
async function main() {
102173
// Get the list of secret names from environment variable
103174
const secretNames = process.env.GH_AW_SECRET_NAMES;
104-
if (!secretNames) {
105-
core.info("GH_AW_SECRET_NAMES not set, no redaction performed");
106-
return;
107-
}
175+
108176
core.info("Starting secret redaction in /tmp/gh-aw directory");
109177
try {
110-
// Parse the comma-separated list of secret names
111-
const secretNameList = secretNames.split(",").filter(name => name.trim());
112-
// Collect the actual secret values from environment variables
178+
// Collect custom secret values from environment variables
113179
const secretValues = [];
114-
for (const secretName of secretNameList) {
115-
const envVarName = `SECRET_${secretName}`;
116-
const secretValue = process.env[envVarName];
117-
// Skip empty or undefined secrets
118-
if (!secretValue || secretValue.trim() === "") {
119-
continue;
180+
if (secretNames) {
181+
// Parse the comma-separated list of secret names
182+
const secretNameList = secretNames.split(",").filter(name => name.trim());
183+
for (const secretName of secretNameList) {
184+
const envVarName = `SECRET_${secretName}`;
185+
const secretValue = process.env[envVarName];
186+
// Skip empty or undefined secrets
187+
if (!secretValue || secretValue.trim() === "") {
188+
continue;
189+
}
190+
secretValues.push(secretValue.trim());
120191
}
121-
secretValues.push(secretValue.trim());
122192
}
123-
if (secretValues.length === 0) {
124-
core.info("No secret values found to redact");
125-
return;
193+
194+
if (secretValues.length > 0) {
195+
core.info(`Found ${secretValues.length} custom secret(s) to redact`);
126196
}
127-
core.info(`Found ${secretValues.length} secret(s) to redact`);
197+
198+
// Always scan for built-in patterns, even if there are no custom secrets
199+
core.info("Scanning for built-in credential patterns and custom secrets");
200+
128201
// Find all target files in /tmp/gh-aw directory
129202
const targetExtensions = [".txt", ".json", ".log", ".md", ".mdx", ".yml", ".jsonl"];
130203
const files = findFiles("/tmp/gh-aw", targetExtensions);
@@ -151,4 +224,4 @@ async function main() {
151224

152225
const { getErrorMessage } = require("./error_helpers.cjs");
153226

154-
module.exports = { main };
227+
module.exports = { main, redactSecrets, redactBuiltInPatterns, BUILT_IN_PATTERNS };

0 commit comments

Comments
 (0)