From 8a2a19ccd349609ab40619aab16bbaf966d38c6c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 10 Nov 2026 20:29:59 +0000 Subject: [PATCH] Mask padded credentials as sent to providers --- cmd/audita/main_integration_test.go | 40 +++++++++++++++++++ docs/roadmap/remediation.md | 2 + internal/framework/llm/native_adapter_test.go | 2 +- internal/framework/llm/secrets.go | 5 +++ 4 files changed, 48 insertions(+), 1 deletion(-) diff --git a/cmd/audita/main_integration_test.go b/cmd/audita/main_integration_test.go index 1003d78..931fa91 100644 --- a/cmd/audita/main_integration_test.go +++ b/cmd/audita/main_integration_test.go @@ -122,6 +122,11 @@ func (p *fixtureProvider) serve(w http.ResponseWriter, r *http.Request) { return } content := `{"corrections":[{"id":1,"original_text":"hello world","corrected_text":"Hello world","confidence":0.99}]}` + if p.mode == "invalid-credential-echo" { + sent := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + raw, _ := json.Marshal(map[string]string{"unexpected": sent + "|" + p.secret}) + content = string(raw) + } if p.mode == "repair-filter-later-failure" && !validation { if proposalCall == 1 { content = `{"wrong":true}` @@ -231,6 +236,9 @@ func newProcessFixture(t *testing.T, mode, modules string) processFixture { t.Helper() dir := t.TempDir() p := &fixtureProvider{mode: mode, secret: `selected-secret-"\-123`, entered: make(chan struct{}, 1)} + if mode == "invalid-credential-echo" { + p.secret = " synthetic-echo-credential\t " + } server := httptest.NewServer(http.HandlerFunc(p.serve)) t.Cleanup(server.Close) resources := fmt.Sprintf("backends:\n - id: local\n endpoint: %s/v1\n api_key_env: AUDITA_TEST_PROVIDER_KEY\nprofiles:\n - id: proposal\n backend: local\n model: local\n - id: validation\n backend: local\n model: local\n", server.URL) @@ -944,6 +952,38 @@ func TestSubprocessAllModelFailureStillCompletes(t *testing.T) { } } +func TestSubprocessMasksPaddedCredentialInProviderResponse(t *testing.T) { + f := newProcessFixture(t, "invalid-credential-echo", "[grammar]") + effective := strings.TrimSpace(f.provider.secret) + code, stdout, stderr := f.run(t, true, "--debug") + if code != 0 || f.provider.callCount() != 1 || !strings.Contains(stderr, "1 work units with errors") { + t.Fatalf("code=%d calls=%d stdout=%q stderr=%q", code, f.provider.callCount(), stdout, stderr) + } + if strings.Contains(stderr, effective) || strings.Contains(stdout, effective) { + t.Fatal("credential escaped redaction in command output") + } + responseCaptured := false + err := filepath.WalkDir(f.dir, func(path string, entry os.DirEntry, err error) error { + if err != nil || entry.IsDir() { + return err + } + data, err := os.ReadFile(path) + if err != nil { + return err + } + if bytes.Contains(data, []byte(effective)) { + t.Errorf("credential escaped redaction in %s", entry.Name()) + } + if strings.HasSuffix(entry.Name(), "-response-payload.json") && bytes.Contains(data, []byte("[REDACTED]|[REDACTED]")) { + responseCaptured = true + } + return nil + }) + if err != nil || !responseCaptured { + t.Fatalf("missing redacted provider echo: %v", err) + } +} + func TestSubprocessAllValidationFailureStillCompletes(t *testing.T) { f := newProcessFixture(t, "all-validation-failure", "[grammar, grammar]") code, stdout, stderr := f.run(t, true, "--verbose") diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index eaca97a..d730639 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -352,6 +352,8 @@ still produces normalized output without provider calls. ## R10 — P2: Mask the Credential Value PromptKit Actually Sends +**Status:** Completed. Redaction now includes the raw and trimmed forms of referenced credential values. A local provider regression echoes both forms and checks retained diagnostics, reports, and command output. + **Category** Credential normalization and diagnostic redaction disagree in Stage 4. diff --git a/internal/framework/llm/native_adapter_test.go b/internal/framework/llm/native_adapter_test.go index 02d8fd9..cf09afc 100644 --- a/internal/framework/llm/native_adapter_test.go +++ b/internal/framework/llm/native_adapter_test.go @@ -27,7 +27,7 @@ type transportFunc func(*http.Request) (*http.Response, error) func (f transportFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } func TestRoleClientUsesNativeTargetAndRedactsCredential(t *testing.T) { - t.Setenv("AUDITA_TEST_KEY", "synthetic-secret-value") + t.Setenv("AUDITA_TEST_KEY", " synthetic-secret-value\t ") dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "config.yml"), []byte("version: 3\n"), 0644); err != nil { t.Fatal(err) diff --git a/internal/framework/llm/secrets.go b/internal/framework/llm/secrets.go index 285204e..84f9a39 100644 --- a/internal/framework/llm/secrets.go +++ b/internal/framework/llm/secrets.go @@ -3,6 +3,7 @@ package llm import ( "context" "os" + "strings" "gitea.maximumdirect.net/eric/audita/v3/internal/core/redaction" ) @@ -41,6 +42,10 @@ func (r *Runtime) ConfiguredSecrets(ctx context.Context, additionalEnvNames ...s for name := range names { if value := os.Getenv(name); value != "" { secrets = append(secrets, value) + // PromptKit trims the value before sending it to the provider. + if trimmed := strings.TrimSpace(value); trimmed != "" && trimmed != value { + secrets = append(secrets, trimmed) + } } } return secrets