diff --git a/pkg/cli/compile_config.go b/pkg/cli/compile_config.go index bcd55fc783..922a0daee9 100644 --- a/pkg/cli/compile_config.go +++ b/pkg/cli/compile_config.go @@ -1,5 +1,9 @@ package cli +import ( + "regexp" +) + // CompileConfig holds configuration options for compiling workflows type CompileConfig struct { MarkdownFiles []string // Files to compile (empty for all files) @@ -55,3 +59,104 @@ type ValidationResult struct { Warnings []ValidationError `json:"warnings"` CompiledFile string `json:"compiled_file,omitempty"` } + +// Regex patterns for detecting potential secret key names +var ( + // Match uppercase snake_case identifiers that look like secret names (e.g., MY_SECRET_KEY, GITHUB_TOKEN, API_KEY) + // Excludes common workflow-related keywords + secretNamePattern = regexp.MustCompile(`\b([A-Z][A-Z0-9]*_[A-Z0-9_]+)\b`) + + // Match PascalCase identifiers ending with security-related suffixes (e.g., GitHubToken, ApiKey, DeploySecret) + pascalCaseSecretPattern = regexp.MustCompile(`\b([A-Z][a-z0-9]*(?:[A-Z][a-z0-9]*)*(?:Token|Key|Secret|Password|Credential|Auth))\b`) + + // Common non-sensitive workflow keywords to exclude from redaction + commonWorkflowKeywords = map[string]bool{ + "GITHUB": true, + "ACTIONS": true, + "WORKFLOW": true, + "RUNNER": true, + "JOB": true, + "STEP": true, + "MATRIX": true, + "ENV": true, + "PATH": true, + "HOME": true, + "SHELL": true, + "INPUTS": true, + "OUTPUTS": true, + "NEEDS": true, + "STRATEGY": true, + "CONCURRENCY": true, + "IF": true, + "WITH": true, + "USES": true, + "RUN": true, + "WORKING_DIRECTORY": true, + "CONTINUE_ON_ERROR": true, + "TIMEOUT_MINUTES": true, + } +) + +// sanitizeErrorMessage removes potential secret key names from error messages to prevent +// information disclosure via logs. This prevents exposing details about an organization's +// security infrastructure by redacting secret key names that might appear in error messages. +func sanitizeErrorMessage(message string) string { + if message == "" { + return message + } + + // Redact uppercase snake_case patterns (e.g., MY_SECRET_KEY, API_TOKEN) + sanitized := secretNamePattern.ReplaceAllStringFunc(message, func(match string) string { + // Don't redact common workflow keywords + if commonWorkflowKeywords[match] { + return match + } + return "[REDACTED]" + }) + + // Redact PascalCase patterns ending with security suffixes (e.g., GitHubToken, ApiKey) + sanitized = pascalCaseSecretPattern.ReplaceAllString(sanitized, "[REDACTED]") + + return sanitized +} + +// sanitizeValidationResults creates a sanitized copy of validation results with all +// error and warning messages sanitized to remove potential secret key names. +// This is applied at the JSON output boundary to ensure no sensitive information +// is leaked regardless of where error messages originated. +func sanitizeValidationResults(results []ValidationResult) []ValidationResult { + if results == nil { + return nil + } + + sanitized := make([]ValidationResult, len(results)) + for i, result := range results { + sanitized[i] = ValidationResult{ + Workflow: result.Workflow, + Valid: result.Valid, + CompiledFile: result.CompiledFile, + Errors: make([]ValidationError, len(result.Errors)), + Warnings: make([]ValidationError, len(result.Warnings)), + } + + // Sanitize all error messages + for j, err := range result.Errors { + sanitized[i].Errors[j] = ValidationError{ + Type: err.Type, + Message: sanitizeErrorMessage(err.Message), + Line: err.Line, + } + } + + // Sanitize all warning messages + for j, warn := range result.Warnings { + sanitized[i].Warnings[j] = ValidationError{ + Type: warn.Type, + Message: sanitizeErrorMessage(warn.Message), + Line: warn.Line, + } + } + } + + return sanitized +} diff --git a/pkg/cli/compile_orchestrator.go b/pkg/cli/compile_orchestrator.go index 5e0a7fa443..3f21fd4a2f 100644 --- a/pkg/cli/compile_orchestrator.go +++ b/pkg/cli/compile_orchestrator.go @@ -585,7 +585,10 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) { // Output JSON if requested if jsonOutput { - jsonBytes, err := json.MarshalIndent(validationResults, "", " ") + // Sanitize validation results before JSON marshaling to prevent logging of sensitive information + // This removes potential secret key names from error messages at the output boundary + sanitizedResults := sanitizeValidationResults(validationResults) + jsonBytes, err := json.MarshalIndent(sanitizedResults, "", " ") if err != nil { return workflowDataList, fmt.Errorf("failed to marshal JSON: %w", err) } @@ -980,7 +983,10 @@ func CompileWorkflows(config CompileConfig) ([]*workflow.WorkflowData, error) { // Output JSON if requested if jsonOutput { - jsonBytes, err := json.MarshalIndent(validationResults, "", " ") + // Sanitize validation results before JSON marshaling to prevent logging of sensitive information + // This removes potential secret key names from error messages at the output boundary + sanitizedResults := sanitizeValidationResults(validationResults) + jsonBytes, err := json.MarshalIndent(sanitizedResults, "", " ") if err != nil { return workflowDataList, fmt.Errorf("failed to marshal JSON: %w", err) }