278 lines
10 KiB
Go
278 lines
10 KiB
Go
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"],
|
|
InputExecutionClass: contracts.ExecutionClassDeterministic,
|
|
Chunk: bindings["chunk"],
|
|
ChunkExecutionClass: contracts.ExecutionClassLLMBacked,
|
|
ChunkReferences: redactionTestReferenceTarget(pipeline.StageChunk, "", "chunk-reference-content"),
|
|
Steps: []pipeline.ResolvedPipelineStep{{
|
|
ID: "default",
|
|
ArtifactLanes: []pipeline.ResolvedArtifactLane{{
|
|
ID: "safe-lane",
|
|
ArtifactKind: "safe/artifact",
|
|
Extract: bindings["extract"],
|
|
ExtractExecutionClass: contracts.ExecutionClassLLMBacked,
|
|
Merge: bindings["merge"],
|
|
MergeExecutionClass: contracts.ExecutionClassDeterministic,
|
|
Normalize: bindings["normalize"],
|
|
NormalizeExecutionClass: contracts.ExecutionClassLLMBacked,
|
|
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"],
|
|
OutputExecutionClass: contracts.ExecutionClassDeterministic,
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
for _, executionClass := range []string{"input_execution_class\":\"deterministic", "chunk_execution_class\":\"llm_backed", "extract_execution_class\":\"llm_backed", "merge_execution_class\":\"deterministic", "normalize_execution_class\":\"llm_backed", "output_execution_class\":\"deterministic"} {
|
|
if !strings.Contains(text, executionClass) {
|
|
t.Fatalf("resolved pipeline summary does not retain %q: %s", executionClass, 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"],
|
|
Steps: []pipeline.ResolvedPipelineStep{{
|
|
ID: "default",
|
|
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")
|
|
}
|
|
}
|