diff --git a/engine_test.go b/engine_test.go index 0bc2a8be..c44befa7 100644 --- a/engine_test.go +++ b/engine_test.go @@ -268,6 +268,49 @@ func TestEngineTriggerWorkflow_RedactsSensitiveResultsInDebugLogs(t *testing.T) } } +func TestEngineTriggerWorkflow_RedactsSensitiveResultHeadersInDebugLogs(t *testing.T) { + app := newMockApplication() + engine := NewStdEngine(app, app.Logger()) + loadAllPlugins(t, engine) + + handler := &mockWorkflowHandler{ + name: "mock.handler", + handlesFor: []string{"header-result-workflow"}, + results: map[string]any{ + "headers": map[string]any{ + "Authorization": "Bearer jwt.secret.value", + "Cookie": "sid=session-secret", + "Set-Cookie": "sid=session-secret; HttpOnly", + "X-API-Key": "api-secret", + "Content-Type": "application/json", + }, + }, + } + engine.RegisterWorkflowHandler(handler) + + holder := &module.PipelineResultHolder{} + ctx := context.WithValue(context.Background(), module.PipelineResultContextKey, holder) + if err := engine.TriggerWorkflow(ctx, "header-result-workflow", "run", map[string]any{}); err != nil { + t.Fatalf("TriggerWorkflow failed: %v", err) + } + + logText := strings.Join(app.logger.logs, "\n") + for _, leaked := range []string{"jwt.secret.value", "session-secret", "api-secret"} { + if strings.Contains(logText, leaked) { + t.Fatalf("debug logs leaked sensitive header value %q:\n%s", leaked, logText) + } + } + if !strings.Contains(logText, module.RedactionPlaceholder) { + t.Fatalf("debug logs should include redaction placeholder, got:\n%s", logText) + } + + raw := holder.Get() + headers := raw["headers"].(map[string]any) + if headers["Authorization"] != "Bearer jwt.secret.value" { + t.Fatalf("pipeline result holder must preserve raw Authorization header, got %#v", headers["Authorization"]) + } +} + func TestEngineTriggerWorkflow_RedactsSensitiveInputInDebugLogs(t *testing.T) { app := newMockApplication() engine := NewStdEngine(app, app.Logger()) diff --git a/module/step_output_redactor.go b/module/step_output_redactor.go index 18645349..2cb27973 100644 --- a/module/step_output_redactor.go +++ b/module/step_output_redactor.go @@ -9,7 +9,10 @@ var SensitiveFieldPatterns = []string{ "password", "token", "credential", + "authorization", + "cookie", "api_key", + "api-key", "apikey", "private_key", "access_key", diff --git a/module/step_output_redactor_test.go b/module/step_output_redactor_test.go index 77ad3dc7..5db874db 100644 --- a/module/step_output_redactor_test.go +++ b/module/step_output_redactor_test.go @@ -97,6 +97,33 @@ func TestRedactStepOutput_NestedMaps(t *testing.T) { } } +func TestRedactStepOutput_NestedSensitiveHeaders(t *testing.T) { + output := map[string]any{ + "headers": map[string]any{ + "Authorization": "Bearer jwt.secret.value", + "Cookie": "sid=session-secret", + "Set-Cookie": "sid=session-secret; HttpOnly", + "X-API-Key": "api-secret", + "X-Scenario90-Seed-Token": "seed-secret", + "Content-Type": "application/json", + }, + } + + got := RedactStepOutput(output) + headers, ok := got["headers"].(map[string]any) + if !ok { + t.Fatalf("headers should remain a map, got %T", got["headers"]) + } + for _, key := range []string{"Authorization", "Cookie", "Set-Cookie", "X-API-Key", "X-Scenario90-Seed-Token"} { + if headers[key] != RedactionPlaceholder { + t.Fatalf("%s should be redacted, got %#v", key, headers[key]) + } + } + if headers["Content-Type"] != "application/json" { + t.Fatalf("Content-Type should be preserved, got %#v", headers["Content-Type"]) + } +} + func TestRedactStepOutput_NestedSlices(t *testing.T) { output := map[string]any{ "rows": []any{