Skip to content
Open
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
10 changes: 5 additions & 5 deletions compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
return nil, err
}
for k, v := range specialVars {
result.Set(k, ast.Var{Value: v})
result.Set(k, ast.Var{Value: v, Secret: false})
}

getRangeFunc := func(dir string) func(k string, v ast.Var) error {
Expand All @@ -63,12 +63,12 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
// This stops empty interface errors when using the templater to replace values later
// Preserve the Sh field so it can be displayed in summary
if !evaluateShVars && newVar.Value == nil {
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh})
result.Set(k, ast.Var{Value: "", Sh: newVar.Sh, Secret: v.Secret})
return nil
}
// If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars {
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh})
result.Set(k, ast.Var{Value: newVar.Value, Sh: newVar.Sh, Secret: v.Secret})
return nil
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
Expand All @@ -77,15 +77,15 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (*
}
// If the variable is already set, we can set it and return
if newVar.Value != nil || newVar.Sh == nil {
result.Set(k, ast.Var{Value: newVar.Value})
result.Set(k, ast.Var{Value: newVar.Value, Secret: v.Secret})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir, env.GetFromVars(result))
if err != nil {
return err
}
result.Set(k, ast.Var{Value: static})
result.Set(k, ast.Var{Value: static, Secret: v.Secret})
return nil
}
}
Expand Down
39 changes: 39 additions & 0 deletions executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,45 @@ func TestVars(t *testing.T) {
)
}

func TestSecretVars(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
WithName("secret vars are masked in logs"),
WithExecutorOptions(
task.WithDir("testdata/secrets"),
),
WithTask("test-secret-masking"),
)
NewExecutorTest(t,
WithName("multiple secrets masked"),
WithExecutorOptions(
task.WithDir("testdata/secrets"),
),
WithTask("test-multiple-secrets"),
)
NewExecutorTest(t,
WithName("mixed secret and public vars"),
WithExecutorOptions(
task.WithDir("testdata/secrets"),
),
WithTask("test-mixed"),
)
NewExecutorTest(t,
WithName("deferred command with secrets"),
WithExecutorOptions(
task.WithDir("testdata/secrets"),
),
WithTask("test-deferred-secret"),
)
NewExecutorTest(t,
WithName("env secret limitation"),
WithExecutorOptions(
task.WithDir("testdata/secrets"),
),
WithTask("test-env-secret-limitation"),
)
}

func TestRequires(t *testing.T) {
t.Parallel()
NewExecutorTest(t,
Expand Down
37 changes: 37 additions & 0 deletions internal/templater/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package templater

import (
"github.com/go-task/task/v3/taskfile/ast"
)

// MaskSecrets replaces template placeholders with their values, masking secrets.
// This function uses the Go templater to resolve all variables ({{.VAR}}) while
// masking secret ones as "*****".
func MaskSecrets(cmdTemplate string, vars *ast.Vars) string {
if vars == nil || vars.Len() == 0 {
return cmdTemplate
}

// Create a cache map with secrets masked
maskedVars := vars.DeepCopy()
for name, v := range maskedVars.All() {
if v.Secret {
// Replace secret value with mask
maskedVars.Set(name, ast.Var{
Value: "*****",
Secret: true,
})
}
}

// Use the templater to resolve the template with masked secrets
cache := &Cache{Vars: maskedVars}
result := Replace(cmdTemplate, cache)

// If there was an error, return the original template
if cache.Err() != nil {
return cmdTemplate
}

return result
}
13 changes: 7 additions & 6 deletions internal/templater/templater.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,15 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var {

func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var {
if v.Ref != "" {
return ast.Var{Value: ResolveRef(v.Ref, cache)}
return ast.Var{Value: ResolveRef(v.Ref, cache), Secret: v.Secret}
}
return ast.Var{
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Value: ReplaceWithExtra(v.Value, cache, extra),
Sh: ReplaceWithExtra(v.Sh, cache, extra),
Live: v.Live,
Ref: v.Ref,
Dir: v.Dir,
Secret: v.Secret,
}
}

Expand Down
12 changes: 11 additions & 1 deletion task.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ func (e *Executor) runDeferred(t *ast.Task, call *Call, i int, vars *ast.Vars, d
extra["EXIT_CODE"] = fmt.Sprintf("%d", *deferredExitCode)
}

// Save template before resolving for secret masking in logs
cmd.CmdTemplate = cmd.Cmd
cmd.Cmd = templater.ReplaceWithExtra(cmd.Cmd, cache, extra)
cmd.Task = templater.ReplaceWithExtra(cmd.Task, cache, extra)
cmd.Vars = templater.ReplaceVarsWithExtra(cmd.Vars, cache, extra)
Expand Down Expand Up @@ -316,7 +318,15 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in
}

if e.Verbose || (!call.Silent && !cmd.Silent && !t.Silent && !e.Taskfile.Silent && !e.Silent) {
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmd.Cmd)
// Get runtime vars for masking
varsForMasking, err := e.Compiler.FastGetVariables(t, call)
if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err)
}

// Mask secret variables in the command template before logging
cmdToLog := templater.MaskSecrets(cmd.CmdTemplate, varsForMasking)
e.Logger.Errf(logger.Green, "task: [%s] %s\n", t.Name(), cmdToLog)
}

if e.Dry {
Expand Down
4 changes: 3 additions & 1 deletion taskfile/ast/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import (

// Cmd is a task command
type Cmd struct {
Cmd string
Cmd string // Resolved command (used for execution and fingerprinting)
CmdTemplate string // Original template before variable resolution (used for secret masking)
Task string
For *For
Silent bool
Expand All @@ -27,6 +28,7 @@ func (c *Cmd) DeepCopy() *Cmd {
}
return &Cmd{
Cmd: c.Cmd,
CmdTemplate: c.CmdTemplate,
Task: c.Task,
For: c.For.DeepCopy(),
Silent: c.Silent,
Expand Down
31 changes: 20 additions & 11 deletions taskfile/ast/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ import (

// Var represents either a static or dynamic variable.
type Var struct {
Value any
Live any
Sh *string
Ref string
Dir string
Value any
Live any
Sh *string
Ref string
Dir string
Secret bool
}

func (v *Var) UnmarshalYAML(node *yaml.Node) error {
Expand All @@ -23,21 +24,29 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error {
key = node.Content[0].Value
}
switch key {
case "sh", "ref", "map":
case "sh", "ref", "map", "value":
var m struct {
Sh *string
Ref string
Map any
Sh *string
Ref string
Map any
Value any
Secret bool
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
v.Value = m.Map
v.Secret = m.Secret
// Handle both "map" and "value" keys
if m.Map != nil {
v.Value = m.Map
} else if m.Value != nil {
v.Value = m.Value
}
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "value" or using a scalar value`, key)
}
default:
var value any
Expand Down
65 changes: 65 additions & 0 deletions testdata/secrets/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
version: '3'

vars:
# Public variable
APP_NAME: myapp

# Secret variable with value
API_KEY:
value: "secret-api-key-123"
secret: true

# Secret variable from shell command
PASSWORD:
sh: "echo 'my-super-secret-password'"
secret: true

# Non-secret variable
PUBLIC_URL: https://example.com

tasks:
test-secret-masking:
desc: Test that secret variables are masked in logs
cmds:
- echo "Deploying {{.APP_NAME}} to {{.PUBLIC_URL}}"
- echo "Using API key {{.API_KEY}}"
- echo "Password is {{.PASSWORD}}"
- echo "Public app name is {{.APP_NAME}}"

test-multiple-secrets:
desc: Test multiple secrets in one command
cmds:
- echo "API={{.API_KEY}} PWD={{.PASSWORD}}"

test-mixed:
desc: Test mix of secret and public vars
vars:
LOCAL_SECRET:
value: "task-level-secret"
secret: true
cmds:
- echo "App={{.APP_NAME}} Secret={{.LOCAL_SECRET}} URL={{.PUBLIC_URL}}"

test-deferred-secret:
desc: Test that deferred commands mask secrets
vars:
DEFERRED_SECRET:
value: "deferred-secret-value"
secret: true
cmds:
- echo "Starting task"
- defer: echo "Cleanup with secret={{.DEFERRED_SECRET}} and app={{.APP_NAME}}"
- echo "Main command executed"

test-env-secret-limitation:
desc: Test showing that env vars with secret flag are NOT masked (limitation)
env:
SECRET_TOKEN:
value: "env-secret-token-123"
PUBLIC_ENV: "public-value"
cmds:
# Templates {{.VAR}} don't work with env - they're empty
- echo "Token via template is {{.SECRET_TOKEN}}"
# Shell $VAR works but is NOT masked (env vars not in template system)
- echo "Token via shell is $SECRET_TOKEN"
- echo "Public env is {{.PUBLIC_ENV}}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
task: [test-deferred-secret] echo "Starting task"
Starting task
task: [test-deferred-secret] echo "Main command executed"
Main command executed
task: [test-deferred-secret] echo "Cleanup with secret=***** and app=myapp"
Cleanup with secret=deferred-secret-value and app=myapp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
task: [test-env-secret-limitation] echo "Token via template is "
Token via template is
task: [test-env-secret-limitation] echo "Token via shell is $SECRET_TOKEN"
Token via shell is env-secret-token-123
task: [test-env-secret-limitation] echo "Public env is "
Public env is
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
task: [test-mixed] echo "App=myapp Secret=***** URL=https://example.com"
App=myapp Secret=task-level-secret URL=https://example.com
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
task: [test-multiple-secrets] echo "API=***** PWD=*****"
API=secret-api-key-123 PWD=my-super-secret-password
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
task: [test-secret-masking] echo "Deploying myapp to https://example.com"
Deploying myapp to https://example.com
task: [test-secret-masking] echo "Using API key *****"
Using API key secret-api-key-123
task: [test-secret-masking] echo "Password is *****"
Password is my-super-secret-password
task: [test-secret-masking] echo "Public app name is myapp"
Public app name is myapp
1 change: 1 addition & 0 deletions variables.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
continue
}
newCmd := cmd.DeepCopy()
newCmd.CmdTemplate = cmd.Cmd
newCmd.Cmd = templater.Replace(cmd.Cmd, cache)
newCmd.Task = templater.Replace(cmd.Task, cache)
newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache)
Expand Down
Loading
Loading