package config import ( "encoding/json" "strings" "testing" "gitea.maximumdirect.net/eric/notarius/internal/framework/contracts" "gitea.maximumdirect.net/eric/notarius/internal/framework/pipeline" ) func TestRedactedResolvedPipelinePayloadRedactsEveryBinding(t *testing.T) { bindings := map[string]pipeline.ModuleBinding{} for _, name := range []string{ "input", "chunk", "output", "extract", "merge", "normalize", "resolved-validator", "lane-validator", } { bindings[name] = redactionTestBinding(name) } resolved := pipeline.ResolvedPipeline{ ID: "redaction-test", Digest: "sha256:safe-digest", Input: bindings["input"], Chunk: bindings["chunk"], ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"), ArtifactLanes: []pipeline.ResolvedArtifactLane{{ ID: "safe-lane", ArtifactKind: "safe/artifact", Extract: bindings["extract"], Merge: bindings["merge"], Normalize: bindings["normalize"], Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, ExtractReferences: redactionTestReferenceTarget(pipeline.StageExtract, "safe-lane", "extract-reference-content"), MergeReferences: redactionTestReferenceTarget(pipeline.StageMerge, "safe-lane", "merge-reference-content"), NormalizeReferences: redactionTestReferenceTarget(pipeline.StageNormalize, "safe-lane", "normalize-reference-content"), }}, ValidatorChains: []pipeline.ResolvedValidatorChain{{ Stage: pipeline.StageExtract, LaneID: "safe-lane", ModuleKey: "safe-extract-owner", Validators: []pipeline.ResolvedValidator{{ Binding: bindings["resolved-validator"], ExecutionClass: contracts.ExecutionClassDeterministic, Target: pipeline.ValidatorTargetTyped, ArtifactKind: "safe/artifact", }}, }}, Output: bindings["output"], } effective := EffectiveConfig{ Config: Config{Pipelines: map[string]pipeline.PipelineProfile{ "redaction-test": {Input: bindings["input"]}, }}, PipelineID: "redaction-test", ResolvedPipeline: resolved, } payload := effective.RedactedResolvedPipelinePayload() encoded, err := json.Marshal(payload) if err != nil { t.Fatal(err) } text := string(encoded) for name := range bindings { for _, forbidden := range []string{name + "-secret", name + "-nested-secret"} { if strings.Contains(text, forbidden) { t.Fatalf("resolved pipeline summary contains %q: %s", forbidden, text) } } if !strings.Contains(text, name+"-safe") { t.Fatalf("resolved pipeline summary does not retain safe option for %q: %s", name, text) } } for _, content := range []string{ "chunk-reference-content", "extract-reference-content", "merge-reference-content", "normalize-reference-content", } { if strings.Contains(text, content) { t.Fatalf("resolved pipeline summary contains materialized reference content %q", content) } } for _, safe := range []string{"[REDACTED]", "safe-reference-path", "safe-binding-source"} { if !strings.Contains(text, safe) { t.Fatalf("resolved pipeline summary does not retain %q: %s", safe, text) } } payload.Input.Options["safe"] = "mutated" nested := payload.Input.Options["nested"].([]any)[0].([]any)[0].(map[string]any) nested["neighbor"] = "mutated" payload.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content[0] = 'X' payload.ValidatorChains[0].Validators[0].Binding.Options["safe"] = "mutated" assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "input") assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "input") assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.ValidatorChains[0].Validators[0].Binding, "resolved-validator") if got := string(effective.ResolvedPipeline.ChunkReferences.ReferenceSet.Slots["safe-slot"].Items[0].Content); got != "chunk-reference-content" { t.Fatalf("source reference content mutated through redacted payload: %q", got) } } func TestRedactedEffectiveConfigPayloadDoesNotAliasSource(t *testing.T) { binding := redactionTestBinding("effective") effective := EffectiveConfig{ Config: Config{Pipelines: map[string]pipeline.PipelineProfile{ "redaction-test": {Input: binding}, }}, ResolvedPipeline: pipeline.ResolvedPipeline{Input: binding}, } payload := effective.RedactedSummaryPayload().(EffectiveConfig) payload.Config.Pipelines["redaction-test"].Input.Options["safe"] = "mutated" payload.ResolvedPipeline.Input.Options["safe"] = "mutated" assertRedactionTestBindingUnchanged(t, effective.Config.Pipelines["redaction-test"].Input, "effective") assertRedactionTestBindingUnchanged(t, effective.ResolvedPipeline.Input, "effective") } func TestRedactedSummaryPayloadsCoverEveryEffectiveConfigBinding(t *testing.T) { bindings := map[string]pipeline.ModuleBinding{} for _, name := range []string{"input", "chunk", "output", "extract", "merge", "normalize", "lane-validator"} { bindings[name] = redactionTestBinding("summary-" + name) } effective := EffectiveConfig{ Config: Config{Pipelines: map[string]pipeline.PipelineProfile{ "redaction-test": { Input: bindings["input"], Chunk: bindings["chunk"], Output: bindings["output"], Artifacts: map[string]pipeline.ArtifactLaneProfile{ "safe-lane": { Extract: bindings["extract"], Merge: bindings["merge"], Normalize: bindings["normalize"], Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, }, }, }, }}, ResolvedPipeline: pipeline.ResolvedPipeline{ Input: bindings["input"], Chunk: bindings["chunk"], Output: bindings["output"], ArtifactLanes: []pipeline.ResolvedArtifactLane{{ ID: "safe-lane", Extract: bindings["extract"], Merge: bindings["merge"], Normalize: bindings["normalize"], Validators: []pipeline.ModuleBinding{bindings["lane-validator"]}, }}, }, } payload, ok := effective.RedactedSummaryPayload().(EffectiveConfig) if !ok { t.Fatal("RedactedSummaryPayload() returned an unexpected type") } encoded, err := json.Marshal(payload) if err != nil { t.Fatal(err) } text := string(encoded) for name := range bindings { if strings.Contains(text, "summary-"+name+"-secret") || strings.Contains(text, "summary-"+name+"-nested-secret") { t.Fatalf("summary payload contains sensitive option for %q: %s", name, text) } if !strings.Contains(text, "summary-"+name+"-safe") { t.Fatalf("summary payload omitted safe option for %q: %s", name, text) } } if !strings.Contains(text, "[REDACTED]") { t.Fatalf("summary payload contains no redaction marker: %s", text) } } func TestRedactedResolvedPipelinePayloadHandlesTypedOptionContainers(t *testing.T) { type optionMap map[string]string type optionList []optionMap typed := optionList{{ "api_key": "typed-container-secret", "safe": "typed-container-safe", }} effective := EffectiveConfig{ResolvedPipeline: pipeline.ResolvedPipeline{ Input: pipeline.ModuleBinding{Options: map[string]any{"nested": typed}}, }} payload := effective.RedactedResolvedPipelinePayload() nested, ok := payload.Input.Options["nested"].([]any) if !ok || len(nested) != 1 { t.Fatalf("redacted typed list = %#v", payload.Input.Options["nested"]) } item, ok := nested[0].(map[string]any) if !ok { t.Fatalf("redacted typed map = %#v", nested[0]) } if got := item["api_key"]; got != "[REDACTED]" { t.Fatalf("redacted api_key = %v", got) } if got := item["safe"]; got != "typed-container-safe" { t.Fatalf("safe option = %v", got) } item["safe"] = "mutated" if got := typed[0]["safe"]; got != "typed-container-safe" { t.Fatalf("source typed map mutated through redacted payload: %q", got) } } func redactionTestBinding(name string) pipeline.ModuleBinding { return pipeline.ModuleBinding{ Module: "safe-" + name, Options: map[string]any{ "api_key": name + "-secret", "safe": name + "-safe", "nested": []any{[]any{map[string]any{ "password": name + "-nested-secret", "neighbor": name + "-nested-safe", }}}, }, } } func redactionTestReferenceTarget(stage pipeline.ModuleStage, laneID, content string) pipeline.ResolvedReferenceTarget { return pipeline.ResolvedReferenceTarget{ Stage: stage, LaneID: laneID, Module: "safe-reference-module", Bindings: []pipeline.ReferenceBinding{{ Stage: stage, LaneID: laneID, SlotName: "safe-slot", Source: "safe-reference-path", BindingSource: "safe-binding-source", }}, ReferenceSet: contracts.ReferenceSet{Slots: map[string]contracts.ResolvedReferenceSlot{ "safe-slot": { Slot: contracts.ReferenceSlot{Name: "safe-slot"}, Items: []contracts.ReferenceItem{{ SlotName: "safe-slot", Content: []byte(content), Digest: "sha256:safe-reference-digest", BindingSource: "safe-binding-source", }}, }, }}, } } func assertRedactionTestBindingUnchanged(t *testing.T, binding pipeline.ModuleBinding, name string) { t.Helper() if got := binding.Options["safe"]; got != name+"-safe" { t.Fatalf("source safe option = %v, want %q", got, name+"-safe") } nested := binding.Options["nested"].([]any)[0].([]any)[0].(map[string]any) if got := nested["neighbor"]; got != name+"-nested-safe" { t.Fatalf("source nested safe option = %v, want %q", got, name+"-nested-safe") } }