From 73c4986f3dc00e32903dcaf961e21d25d82588c8 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 25 Sep 2026 19:42:22 +0000 Subject: [PATCH] Preserve Audita module selections in YAML output --- docs/roadmap/remediation.md | 7 +++- internal/config/credential_projection.go | 27 ++++++++------- internal/config/credential_projection_test.go | 33 +++++++++++++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) create mode 100644 internal/config/credential_projection_test.go diff --git a/docs/roadmap/remediation.md b/docs/roadmap/remediation.md index 2de813a..1bf260b 100644 --- a/docs/roadmap/remediation.md +++ b/docs/roadmap/remediation.md @@ -9,7 +9,12 @@ transcription review is retained below as historical context. ## 1. Preserve omitted modules in direct YAML serialization -Priority: P2. Status: Pending. +Priority: P2. Status: Complete. + +The safe YAML projection now omits nil modules while retaining explicit empty +and ordered/repeated selections. A round-trip regression failed before the fix +and passes afterward. Config package tests, including credential-safety coverage, +and its race tests pass. Location: `internal/config/credential_projection.go:25` (`AuditaConfig.MarshalYAML`), especially the plain slice field at line 30 and assignment at line 40. diff --git a/internal/config/credential_projection.go b/internal/config/credential_projection.go index 9169ee8..92ab0be 100644 --- a/internal/config/credential_projection.go +++ b/internal/config/credential_projection.go @@ -24,20 +24,25 @@ func (cfg StorageS3Config) MarshalYAML() (any, error) { // MarshalYAML exposes only public Audita credential reference names. func (cfg AuditaConfig) MarshalYAML() (any, error) { type safeAudita struct { - Binary string `yaml:"binary"` - Timeout string `yaml:"timeout"` - Credentials []string `yaml:"credentials,omitempty"` - Modules []string `yaml:"modules"` - Overlay string `yaml:"overlay"` - ProposalProfile string `yaml:"proposal_profile"` - ValidationProfile string `yaml:"validation_profile"` - TranscriptDescription string `yaml:"transcript_description"` - ConfigPath string `yaml:"config_path"` - OutputSchema string `yaml:"output_schema"` + Binary string `yaml:"binary"` + Timeout string `yaml:"timeout"` + Credentials []string `yaml:"credentials,omitempty"` + Modules *[]string `yaml:"modules,omitempty"` + Overlay string `yaml:"overlay"` + ProposalProfile string `yaml:"proposal_profile"` + ValidationProfile string `yaml:"validation_profile"` + TranscriptDescription string `yaml:"transcript_description"` + ConfigPath string `yaml:"config_path"` + OutputSchema string `yaml:"output_schema"` + } + // A nil pointer omits the override; a pointer to an empty slice disables modules. + var modules *[]string + if cfg.Modules != nil { + modules = &cfg.Modules } return safeAudita{ Binary: cfg.Binary, Timeout: cfg.Timeout, Credentials: secretReferenceNames(cfg.Credentials), - Modules: cfg.Modules, Overlay: cfg.Overlay, ProposalProfile: cfg.ProposalProfile, + Modules: modules, Overlay: cfg.Overlay, ProposalProfile: cfg.ProposalProfile, ValidationProfile: cfg.ValidationProfile, TranscriptDescription: cfg.TranscriptDescription, ConfigPath: cfg.ConfigPath, OutputSchema: cfg.OutputSchema, }, nil diff --git a/internal/config/credential_projection_test.go b/internal/config/credential_projection_test.go new file mode 100644 index 0000000..ee40545 --- /dev/null +++ b/internal/config/credential_projection_test.go @@ -0,0 +1,33 @@ +package config + +import ( + "reflect" + "testing" + + "gopkg.in/yaml.v3" +) + +func TestAuditaYAMLPreservesModuleSelection(t *testing.T) { + for _, tc := range []struct { + name string + modules []string + }{ + {name: "omitted"}, + {name: "explicit empty", modules: []string{}}, + {name: "ordered repetitions", modules: []string{"grammar", "names", "grammar"}}, + } { + t.Run(tc.name, func(t *testing.T) { + data, err := yaml.Marshal(AuditaConfig{Modules: tc.modules}) + if err != nil { + t.Fatal(err) + } + var decoded AuditaConfig + if err := yaml.Unmarshal(data, &decoded); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(decoded.Modules, tc.modules) { + t.Fatalf("modules = %#v, want %#v; YAML:\n%s", decoded.Modules, tc.modules, data) + } + }) + } +}