Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions pkg/workflow/awf_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func ComputeAWFExcludeEnvVarNames(workflowData *WorkflowData, coreSecretVarNames
if workflowData.MCPScripts != nil {
for _, toolConfig := range workflowData.MCPScripts.Tools {
for envName, envValue := range toolConfig.Env {
if strings.Contains(envValue, "${{ secrets.") || ContainsJobOutputExpr(envValue) {
if len(ExtractSecretsFromValue(envValue)) > 0 || ContainsJobOutputExpr(envValue) {
addUnique(envName)
}
}
Expand All @@ -146,7 +146,7 @@ func ComputeAWFExcludeEnvVarNames(workflowData *WorkflowData, coreSecretVarNames
// engine.env vars that contain a secret reference or a job-output expression.
if workflowData.EngineConfig != nil {
for varName, varValue := range workflowData.EngineConfig.Env {
if strings.Contains(varValue, "${{ secrets.") || ContainsJobOutputExpr(varValue) {
if len(ExtractSecretsFromValue(varValue)) > 0 || ContainsJobOutputExpr(varValue) {
addUnique(varName)
}
}
Expand All @@ -156,7 +156,7 @@ func ComputeAWFExcludeEnvVarNames(workflowData *WorkflowData, coreSecretVarNames
agentConfig := getAgentConfig(workflowData)
if agentConfig != nil {
for varName, varValue := range agentConfig.Env {
if strings.Contains(varValue, "${{ secrets.") || ContainsJobOutputExpr(varValue) {
if len(ExtractSecretsFromValue(varValue)) > 0 || ContainsJobOutputExpr(varValue) {
addUnique(varName)
}
}
Expand Down
38 changes: 38 additions & 0 deletions pkg/workflow/awf_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,44 @@ func TestComputeAWFExcludeEnvVarNames(t *testing.T) {
}
}

func TestComputeAWFExcludeEnvVarNamesDetectsSecretExpressionsAcrossSources(t *testing.T) {
expressions := []struct {
name string
value string
}{
{name: "compact expression", value: "${{secrets.TOKEN}}"},
{name: "mixed-case context and name", value: "${{ Secrets.Token }}"},
{name: "variable-first fallback", value: "${{ vars.A || secrets.B }}"},
{name: "github-token-first fallback", value: "${{ github.token || secrets.X }}"},
}
sources := []string{"engine", "agent", "mcp-scripts"}

for _, source := range sources {
for _, expression := range expressions {
t.Run(source+"/"+expression.name, func(t *testing.T) {
const envName = "CONFIGURED_ENV"
workflowData := &WorkflowData{}
switch source {
case "engine":
workflowData.EngineConfig = &EngineConfig{Env: map[string]string{envName: expression.value}}
case "agent":
workflowData.SandboxConfig = &SandboxConfig{
Agent: &AgentSandboxConfig{Env: map[string]string{envName: expression.value}},
}
case "mcp-scripts":
workflowData.MCPScripts = &MCPScriptsConfig{
Tools: map[string]*MCPScriptToolConfig{
"example": {Env: map[string]string{envName: expression.value}},
},
}
}

assert.Contains(t, ComputeAWFExcludeEnvVarNames(workflowData, nil), envName)
})
}
}
}

// TestMainAgentRunUsesStandardCreditsExpressionNotDetectionExpression verifies that
// a standard (non-detection) main-agent run emits the main-agent credits expression
// (vars.GH_AW_DEFAULT_MAX_AI_CREDITS) and not the detection-specific one, so a future
Expand Down
2 changes: 1 addition & 1 deletion pkg/workflow/secret_extraction.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ var secretLog = logger.New("workflow:secret_extraction")
// Pre-compiled regex patterns for ExtractSecretsFromValue (performance optimization)
var (
// secretsNamePattern extracts the secret variable name from an expression
secretsNamePattern = regexp.MustCompile(`secrets\.([A-Z_][A-Z0-9_]*)`)
secretsNamePattern = regexp.MustCompile(`(?i)secrets\.([a-z_][a-z0-9_]*)`)

// jobOutputBodyDotPattern matches needs.JOB.outputs.OUTPUT anywhere within an expression body
// using dot notation. The word boundary ensures we don't match partial identifiers.
Expand Down
35 changes: 35 additions & 0 deletions pkg/workflow/secret_extraction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,41 @@ func TestSharedExtractSecretsFromValue(t *testing.T) {
"DD_API_KEY": "${{ secrets.DD_API_KEY }}",
},
},
{
name: "compact secret expression",
value: "${{secrets.TOKEN}}",
expected: map[string]string{
"TOKEN": "${{secrets.TOKEN}}",
},
},
{
name: "mixed-case context and secret name",
value: "${{ Secrets.Token }}",
expected: map[string]string{
"Token": "${{ Secrets.Token }}",
},
},
{
name: "lowercase secret name",
value: "${{ secrets.lowercase_token }}",
expected: map[string]string{
"lowercase_token": "${{ secrets.lowercase_token }}",
},
},
{
name: "secret after variable fallback",
value: "${{ vars.A || secrets.B }}",
expected: map[string]string{
"B": "${{ vars.A || secrets.B }}",
},
},
{
name: "secret after github token fallback",
value: "${{ github.token || secrets.X }}",
expected: map[string]string{
"X": "${{ github.token || secrets.X }}",
},
},
{
name: "secret with default value",
value: "${{ secrets.DD_SITE || 'datadoghq.com' }}",
Expand Down
Loading