From 89eee7b9647210cfd149a6df8f73a401484dbea1 Mon Sep 17 00:00:00 2001 From: Virgil Date: Wed, 1 Apr 2026 21:22:32 +0000 Subject: [PATCH] Redact no_log task results --- executor.go | 42 +++++++++++++++++++++++++++-- executor_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 2 deletions(-) diff --git a/executor.go b/executor.go index 9ea999f..5708f39 100644 --- a/executor.go +++ b/executor.go @@ -602,13 +602,18 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin e.results[host][task.Register] = result } + displayResult := result + if task.NoLog { + displayResult = redactTaskResult(result) + } + // Handle notify if result.Changed && task.Notify != nil { e.handleNotify(task.Notify) } if e.OnTaskEnd != nil { - e.OnTaskEnd(host, task, result) + e.OnTaskEnd(host, task, displayResult) } if NormalizeModule(task.Module) == "ansible.builtin.meta" { @@ -619,7 +624,7 @@ func (e *Executor) runTaskOnHost(ctx context.Context, host string, hosts []strin if result.Failed && !task.IgnoreErrors { e.markBatchHostFailed(host) - return coreerr.E("Executor.runTaskOnHost", "task failed: "+result.Msg, nil) + return taskFailureError(task, result) } if result.Failed { e.markBatchHostFailed(host) @@ -663,6 +668,39 @@ func (e *Executor) checkMaxFailPercentage(play *Play, hosts []string) error { return nil } +func redactTaskResult(result *TaskResult) *TaskResult { + if result == nil { + return nil + } + + redacted := *result + redacted.Msg = "censored due to no_log" + redacted.Stdout = "" + redacted.Stderr = "" + redacted.Data = nil + if len(result.Results) > 0 { + redacted.Results = make([]TaskResult, len(result.Results)) + for i := range result.Results { + redacted.Results[i] = *redactTaskResult(&result.Results[i]) + } + } + + return &redacted +} + +func taskFailureError(task *Task, result *TaskResult) error { + if task != nil && task.NoLog { + return coreerr.E("Executor.runTaskOnHost", "task failed", nil) + } + + msg := "task failed" + if result != nil && result.Msg != "" { + msg += ": " + result.Msg + } + + return coreerr.E("Executor.runTaskOnHost", msg, nil) +} + // runTaskWithRetries executes a task once or multiple times when retries, // delay, or until are configured. func (e *Executor) runTaskWithRetries(ctx context.Context, host string, task *Task, play *Play, execute func() (*TaskResult, error)) (*TaskResult, error) { diff --git a/executor_test.go b/executor_test.go index 7523464..b305036 100644 --- a/executor_test.go +++ b/executor_test.go @@ -635,6 +635,75 @@ func TestExecutor_RunTaskOnHost_Good_CheckModeSkipsMutatingTask(t *testing.T) { assert.True(t, e.results["host1"]["shell_result"].Skipped) } +// --- no_log --- + +func TestExecutor_RunTaskOnHost_Good_NoLogRedactsCallbackResult(t *testing.T) { + e := NewExecutor("/tmp") + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "host1": {}, + }, + }, + }) + e.clients["host1"] = &SSHClient{} + + var ended *TaskResult + task := &Task{ + Name: "Sensitive debug", + Module: "debug", + Args: map[string]any{"msg": "top secret"}, + Register: "debug_result", + NoLog: true, + } + + e.OnTaskEnd = func(_ string, _ *Task, result *TaskResult) { + ended = result + } + + err := e.runTaskOnHost(context.Background(), "host1", []string{"host1"}, task, &Play{}) + require.NoError(t, err) + + require.NotNil(t, ended) + assert.Equal(t, "censored due to no_log", ended.Msg) + assert.Empty(t, ended.Stdout) + assert.Empty(t, ended.Stderr) + assert.Nil(t, ended.Data) + require.NotNil(t, e.results["host1"]["debug_result"]) + assert.Equal(t, "top secret", e.results["host1"]["debug_result"].Msg) +} + +func TestExecutor_RunTaskOnHost_Bad_NoLogHidesFailureMessage(t *testing.T) { + e := NewExecutor("/tmp") + e.SetInventoryDirect(&Inventory{ + All: &InventoryGroup{ + Hosts: map[string]*Host{ + "host1": {}, + }, + }, + }) + e.clients["host1"] = &SSHClient{} + + var ended *TaskResult + task := &Task{ + Name: "Sensitive failure", + Module: "fail", + Args: map[string]any{"msg": "super secret"}, + NoLog: true, + } + + e.OnTaskEnd = func(_ string, _ *Task, result *TaskResult) { + ended = result + } + + err := e.runTaskOnHost(context.Background(), "host1", []string{"host1"}, task, &Play{}) + require.Error(t, err) + assert.Contains(t, err.Error(), "task failed") + assert.NotContains(t, err.Error(), "super secret") + require.NotNil(t, ended) + assert.Equal(t, "censored due to no_log", ended.Msg) +} + // --- normalizeConditions --- func TestExecutor_NormalizeConditions_Good_String(t *testing.T) {