/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ export type EnvironmentSanitizationConfig = { allowedEnvironmentVariables: string[]; blockedEnvironmentVariables: string[]; enableEnvironmentVariableRedaction: boolean; }; export function sanitizeEnvironment( processEnv: NodeJS.ProcessEnv, config: EnvironmentSanitizationConfig, ): NodeJS.ProcessEnv { // Enable strict sanitization in GitHub actions. const isStrictSanitization = !!processEnv['GITHUB_SHA'] || processEnv['SURFACE'] === 'Github'; // Always sanitize when in GitHub actions. if (!config.enableEnvironmentVariableRedaction && !isStrictSanitization) { return { ...processEnv }; } const results: NodeJS.ProcessEnv = {}; const allowedSet = new Set( (config.allowedEnvironmentVariables || []).map((k) => k.toUpperCase()), ); const blockedSet = new Set( (config.blockedEnvironmentVariables || []).map((k) => k.toUpperCase()), ); for (const key in processEnv) { const value = processEnv[key]; if ( !shouldRedactEnvironmentVariable( key, value, allowedSet, blockedSet, isStrictSanitization, ) ) { results[key] = value; } } return results; } export const ALWAYS_ALLOWED_ENVIRONMENT_VARIABLES: ReadonlySet = new Set([ // Cross-platform 'PATH', // Windows specific 'SYSTEMROOT', 'COMSPEC', 'PATHEXT', 'WINDIR', 'TEMP', 'TMP', 'USERPROFILE', 'SYSTEMDRIVE', // Unix/Linux/macOS specific 'HOME', 'LANG', 'SHELL', 'TMPDIR', 'USER', 'LOGNAME', // GitHub Action-related variables 'ADDITIONAL_CONTEXT', 'AVAILABLE_LABELS', 'BRANCH_NAME', 'DESCRIPTION', 'EVENT_NAME', 'GITHUB_ENV', 'IS_PULL_REQUEST', 'ISSUES_TO_TRIAGE', 'ISSUE_BODY', 'ISSUE_NUMBER', 'ISSUE_TITLE', 'PULL_REQUEST_NUMBER', 'REPOSITORY', 'TITLE', 'TRIGGERING_ACTOR', ]); export const NEVER_ALLOWED_ENVIRONMENT_VARIABLES: ReadonlySet = new Set( [ 'CLIENT_ID', 'DB_URI', 'CONNECTION_STRING', 'AWS_DEFAULT_REGION', 'AZURE_CLIENT_ID', 'AZURE_TENANT_ID', 'SLACK_WEBHOOK_URL', 'TWILIO_ACCOUNT_SID', 'DATABASE_URL', 'GOOGLE_CLOUD_PROJECT', 'GOOGLE_CLOUD_ACCOUNT', 'FIREBASE_PROJECT_ID', ], ); export const NEVER_ALLOWED_NAME_PATTERNS = [ /TOKEN/i, /SECRET/i, /PASSWORD/i, /PASSWD/i, /KEY/i, /AUTH/i, /CREDENTIAL/i, /CREDS/i, /PRIVATE/i, /CERT/i, ] as const; export const NEVER_ALLOWED_VALUE_PATTERNS = [ /-----BEGIN (RSA|OPENSSH|EC|PGP) PRIVATE KEY-----/i, /-----BEGIN CERTIFICATE-----/i, // Credentials in URL /(https?|ftp|smtp):\/\/[^:]+:[^@]+@/i, // GitHub tokens (classic, fine-grained, OAuth, etc.) /(ghp|gho|ghu|ghs|ghr|github_pat)_[a-zA-Z0-9_]{36,}/i, // Google API keys /AIzaSy[a-zA-Z0-9_\\-]{33}/i, // Amazon AWS Access Key ID /AKIA[A-Z0-9]{16}/i, // Generic OAuth/JWT tokens /eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*/i, // Stripe API keys /(s|r)k_(live|test)_[0-9a-zA-Z]{24}/i, // Slack tokens (bot, user, etc.) /xox[abpr]-[a-zA-Z0-9-]+/i, ] as const; function shouldRedactEnvironmentVariable( key: string, value: string | undefined, allowedSet?: Set, blockedSet?: Set, isStrictSanitization = false, ): boolean { key = key.toUpperCase(); value = value?.toUpperCase(); // User overrides take precedence. if (allowedSet?.has(key)) { return false; } if (blockedSet?.has(key)) { return true; } // These are never redacted. if ( ALWAYS_ALLOWED_ENVIRONMENT_VARIABLES.has(key) || key.startsWith('GEMINI_CLI_') ) { return false; } // These are always redacted. if (NEVER_ALLOWED_ENVIRONMENT_VARIABLES.has(key)) { return true; } // If in strict mode (e.g. GitHub Action), and not explicitly allowed, redact it. if (isStrictSanitization) { return true; } for (const pattern of NEVER_ALLOWED_NAME_PATTERNS) { if (pattern.test(key)) { return true; } } // Redact if the value looks like a key/cert. if (value) { for (const pattern of NEVER_ALLOWED_VALUE_PATTERNS) { if (pattern.test(value)) { return true; } } } return false; }