251 lines
9.1 KiB
Go
251 lines
9.1 KiB
Go
package engine
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
const (
|
|
taggedCanary = "TAGGED_CANARY_7c65"
|
|
dynamicCanary = "DYNAMIC_CANARY_6a12"
|
|
historicalCanary = "HISTORICAL_CANARY_91ef"
|
|
derivedACanary = "DERIVED_A_CANARY_4c20"
|
|
derivedBCanary = "DERIVED_B_CANARY_831d"
|
|
)
|
|
|
|
var renderTextMarshalCalls atomic.Int64
|
|
var renderYAMLMarshalCalls atomic.Int64
|
|
|
|
type renderText string
|
|
|
|
func (value renderText) MarshalText() ([]byte, error) {
|
|
renderTextMarshalCalls.Add(1)
|
|
return []byte(value), nil
|
|
}
|
|
|
|
func (value *renderText) UnmarshalText(text []byte) error {
|
|
*value = renderText(text)
|
|
return nil
|
|
}
|
|
|
|
type renderCredentials struct {
|
|
Token string `yaml:"token"`
|
|
Nested []string `yaml:"nested"`
|
|
}
|
|
|
|
type renderConfig struct {
|
|
Credentials renderCredentials `yaml:"credentials" configkit:"secret"`
|
|
Custom renderText `yaml:"custom"`
|
|
DerivedA string `yaml:"derived_a"`
|
|
DerivedB string `yaml:"derived_b"`
|
|
Dynamic any `yaml:"dynamic"`
|
|
EmptyMap map[string]string `yaml:"empty_map"`
|
|
EmptySequence []string `yaml:"empty_sequence"`
|
|
Labels map[string]string `yaml:"labels"`
|
|
NilMap map[string]string `yaml:"nil_map"`
|
|
NilSequence []string `yaml:"nil_sequence"`
|
|
Sequence []string `yaml:"sequence"`
|
|
}
|
|
|
|
func (renderConfig) MarshalYAML() (any, error) {
|
|
renderYAMLMarshalCalls.Add(1)
|
|
return map[string]string{"forbidden": "application YAML hook ran"}, nil
|
|
}
|
|
|
|
func TestWriteEffectiveRedactsTagsDynamicHistoryAndDependencyCycles(t *testing.T) {
|
|
renderTextMarshalCalls.Store(0)
|
|
renderYAMLMarshalCalls.Store(0)
|
|
directory := t.TempDir()
|
|
mainPath := writeImportFile(t, directory, "main.yml", `credentials:
|
|
token: TAGGED_CANARY_7c65
|
|
nested: [nested-secret]
|
|
custom: custom value
|
|
dynamic: {retired: HISTORICAL_CANARY_91ef}
|
|
empty_map: {}
|
|
empty_sequence: []
|
|
labels: {hidden: DYNAMIC_CANARY_6a12, public: visible}
|
|
sequence: [second, first]
|
|
`)
|
|
writeImportFile(t, directory, "profiles/testing.yml", "dynamic: selected\n")
|
|
definition := Definition[renderConfig]{
|
|
Secret: func(path Path) bool {
|
|
return path.String() == "/labels/hidden" || path.String() == "/dynamic/retired"
|
|
},
|
|
Resolve: func(configuration *renderConfig, resolution *Resolution) error {
|
|
Normalize(resolution, Keys("derived_a"), &configuration.DerivedA, derivedACanary,
|
|
"derived through cycle", Keys("derived_b"))
|
|
Normalize(resolution, Keys("derived_b"), &configuration.DerivedB, derivedBCanary,
|
|
"derived from retired secret", Keys("derived_a"), Keys("dynamic"))
|
|
resolution.Warning(Keys("derived_a"), "derived output is configured")
|
|
return nil
|
|
},
|
|
}
|
|
result, err := testLoad[renderConfig](File(mainPath), Options[renderConfig]{Definition: definition, Profile: "testing"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Config.Credentials.Token != taggedCanary || result.Config.Labels["hidden"] != dynamicCanary ||
|
|
result.Config.DerivedA != derivedACanary || result.Config.DerivedB != derivedBCanary {
|
|
t.Fatal("typed configuration did not retain trusted values")
|
|
}
|
|
if !result.secrets["/dynamic/retired"] ||
|
|
!result.secrets["/derived_a"] || !result.secrets["/derived_b"] {
|
|
t.Fatalf("secret closure = %#v", result.secrets)
|
|
}
|
|
if renderYAMLMarshalCalls.Load() != 0 {
|
|
t.Fatal("loading called an application YAML marshal hook")
|
|
}
|
|
textCalls := renderTextMarshalCalls.Load()
|
|
|
|
var output bytes.Buffer
|
|
if err := result.WriteEffective(&output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "credentials: '[REDACTED]'\ncustom: custom value\nderived_a: '[REDACTED]'\nderived_b: '[REDACTED]'\ndynamic: selected\nempty_map: {}\nempty_sequence: []\nlabels:\n hidden: '[REDACTED]'\n public: visible\nnil_map: null\nnil_sequence: null\nsequence:\n - second\n - first\n"
|
|
if output.String() != want {
|
|
t.Fatalf("effective output:\n%s\nwant:\n%s", output.String(), want)
|
|
}
|
|
if renderTextMarshalCalls.Load() != textCalls || renderYAMLMarshalCalls.Load() != 0 {
|
|
t.Fatal("rendering called an application marshal hook")
|
|
}
|
|
for _, canary := range []string{taggedCanary, dynamicCanary, historicalCanary, derivedACanary, derivedBCanary, "nested-secret"} {
|
|
if strings.Contains(output.String(), canary) {
|
|
t.Fatalf("effective output contains %q", canary)
|
|
}
|
|
formatted := fmt.Sprintf("%v %#v %v %#v", result.Metadata(), result.Metadata(), result.Diagnostics(), result.Diagnostics())
|
|
if strings.Contains(formatted, canary) {
|
|
t.Fatalf("metadata formatting contains %q", canary)
|
|
}
|
|
}
|
|
|
|
beforeMutation := output.String()
|
|
result.Config.Credentials.Token = "MUTATED_SECRET"
|
|
result.Config.Labels["hidden"] = "MUTATED_DYNAMIC_SECRET"
|
|
result.Config.Sequence[0] = "mutated"
|
|
output.Reset()
|
|
if err := result.WriteEffective(&output); err != nil || output.String() != beforeMutation {
|
|
t.Fatalf("frozen output after caller mutation = %q, %v", output.String(), err)
|
|
}
|
|
|
|
wantWriteError := errors.New("write failed")
|
|
if err := result.WriteEffective(errorWriter{err: wantWriteError}); !errors.Is(err, wantWriteError) {
|
|
t.Fatalf("writer error = %v", err)
|
|
}
|
|
if err := result.WriteEffective(shortWriter{}); !errors.Is(err, io.ErrShortWrite) {
|
|
t.Fatalf("short writer error = %v", err)
|
|
}
|
|
var nilResult *Result[renderConfig]
|
|
if err := nilResult.WriteEffective(io.Discard); !errors.Is(err, errEffectiveUnavailable) {
|
|
t.Fatalf("nil result error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSecretValuesStayOutOfLoadFailuresAndWarnings(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
definition Definition[renderConfig]
|
|
}{
|
|
{name: "parse", input: "credentials: [" + taggedCanary + "\n"},
|
|
{name: "decode", input: "credentials: " + taggedCanary + "\n"},
|
|
{name: "returned hook error", input: "credentials: {token: " + taggedCanary + "}\n", definition: Definition[renderConfig]{
|
|
Resolve: func(*renderConfig, *Resolution) error { return errors.New(taggedCanary) },
|
|
}},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
test.definition.Secret = func(path Path) bool { return path.String() == "/credentials" }
|
|
result, err := testLoad[renderConfig](Bytes("secret-failure.yml", []byte(test.input)), Options[renderConfig]{Definition: test.definition})
|
|
if result != nil || err == nil {
|
|
t.Fatalf("result, error = %#v, %v", result, err)
|
|
}
|
|
var loadError *LoadError
|
|
if !errors.As(err, &loadError) {
|
|
t.Fatalf("error type = %T", err)
|
|
}
|
|
formatted := fmt.Sprintf("%v %#v %v %#v %v %#v", err, err, loadError.Diagnostics(), loadError.Diagnostics(), loadError.Sources(), loadError.Sources())
|
|
if strings.Contains(formatted, taggedCanary) {
|
|
t.Fatalf("failure formatting disclosed a secret: %s", formatted)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDynamicConcreteTagsRemainSecretThroughHistory(t *testing.T) {
|
|
type sensitive struct {
|
|
Token string `yaml:"token" configkit:"secret"`
|
|
}
|
|
type cfg struct {
|
|
Payload any `yaml:"payload"`
|
|
Derived string `yaml:"derived"`
|
|
}
|
|
for _, change := range []string{"retain", "replace", "delete", "introduce"} {
|
|
t.Run(change, func(t *testing.T) {
|
|
const canary = "DYNAMIC_TAG_CANARY"
|
|
definition := Definition[cfg]{Defaults: func() cfg { return cfg{Payload: map[string]any{"nested": []any{sensitive{Token: canary}}}} }}
|
|
path := Keys("payload", "nested").Index(0).Key("token")
|
|
if change == "introduce" {
|
|
definition.Defaults = nil
|
|
}
|
|
definition.Resolve = func(c *cfg, r *Resolution) error {
|
|
if change == "introduce" {
|
|
Normalize(r, Keys("payload"), &c.Payload, any(map[string]any{"nested": []any{sensitive{Token: canary}}}), "create")
|
|
}
|
|
if change == "replace" {
|
|
Normalize(r, Keys("payload"), &c.Payload, any("public"), "replace")
|
|
}
|
|
if change == "delete" {
|
|
Normalize(r, Keys("payload"), &c.Payload, any(nil), "remove")
|
|
}
|
|
Normalize(r, Keys("derived"), &c.Derived, canary, "derive", path)
|
|
return nil
|
|
}
|
|
result, err := testLoad(NoSource(), Options[cfg]{Definition: definition})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var output bytes.Buffer
|
|
if err := result.WriteEffective(&output); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, query := range []Path{Keys(), path, Keys("derived")} {
|
|
explanation, _ := result.Explain(query)
|
|
output.WriteString(fmt.Sprintf("%#v", explanation))
|
|
}
|
|
if strings.Contains(output.String(), canary) {
|
|
t.Fatal("dynamic secret escaped inspection")
|
|
}
|
|
if !result.secrets[path.String()] || !result.secrets["/derived"] {
|
|
t.Fatal("dynamic tag or dependency classification missing")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidatorCannotChangeDynamicSensitivityWithoutChangingText(t *testing.T) {
|
|
type sensitive struct {
|
|
Token string `yaml:"token" configkit:"secret"`
|
|
}
|
|
type cfg struct {
|
|
Payload any `yaml:"payload"`
|
|
}
|
|
_, err := testLoad(Bytes("base", []byte("payload: {token: disposable}\n")), Options[cfg]{Definition: Definition[cfg]{Validate: func(c *cfg, v *Validation) {
|
|
c.Payload = sensitive{Token: c.Payload.(map[string]any)["token"].(string)}
|
|
}}})
|
|
var failure *LoadError
|
|
if !errors.As(err, &failure) {
|
|
t.Fatal("validator changed inspection sensitivity without a mutation error")
|
|
}
|
|
for _, d := range failure.Diagnostics() {
|
|
if d.Code == CodeValidateMutation {
|
|
return
|
|
}
|
|
}
|
|
t.Fatal("missing validation mutation diagnostic")
|
|
}
|