Protect paired history snapshots from disclosure
This commit is contained in:
@@ -368,6 +368,8 @@ Stage 11 verifies the resulting disclosure through the safe rendering boundary.
|
|||||||
|
|
||||||
## Stage 11 — Mask paired snapshots and preserve safe disclosure
|
## Stage 11 — Mask paired snapshots and preserve safe disclosure
|
||||||
|
|
||||||
|
**Status:** Complete. Focused moved, removed, dynamic-tag, dependency, secret, and diff checks passed.
|
||||||
|
|
||||||
**Scope:** snapshot rendering and inspection integration.
|
**Scope:** snapshot rendering and inspection integration.
|
||||||
|
|
||||||
1. Mask a moved explanation's before and after snapshots using their respective
|
1. Mask a moved explanation's before and after snapshots using their respective
|
||||||
|
|||||||
@@ -588,3 +588,90 @@ func TestRegisteredResolutionKeepsRemovedSensitivity(t *testing.T) {
|
|||||||
t.Fatal("removed historical sensitivity lost")
|
t.Fatal("removed historical sensitivity lost")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMovedAndRemovedSensitiveValuesStayHidden(t *testing.T) {
|
||||||
|
type item struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Items []item `yaml:"items"`
|
||||||
|
Derived string `yaml:"derived"`
|
||||||
|
}
|
||||||
|
for _, remove := range []bool{false, true} {
|
||||||
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
||||||
|
Secret: func(path Path) bool { return path.String() == "/items/1/value" },
|
||||||
|
Resolve: func(value *config, r *Resolution) error {
|
||||||
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||||
|
value.Derived = value.Items[1].Value
|
||||||
|
r.RecordNormalization(Keys("items").Index(1).Key("value"), "retain value", Keys("derived"))
|
||||||
|
if remove {
|
||||||
|
value.Items = value.Items[:1]
|
||||||
|
}
|
||||||
|
r.RecordNormalization(Keys("items"), "update records")
|
||||||
|
r.RecordNormalization(Keys("derived"), "derive", Keys("items").Index(1).Key("value"))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||||
|
result, err := testLoad(Bytes("records", []byte("items: [{id: a, value: PRIVATE_CANARY}, {id: b, value: other}]\n")), Options[config]{Definition: definition})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, path := range []Path{Keys("items"), Keys("items").Index(0), Keys("items").Index(1), Keys("derived")} {
|
||||||
|
explanation, ok := result.Inspection.Explain(path)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.Contains(explanation.Effective.YAML, "PRIVATE_CANARY") {
|
||||||
|
t.Fatalf("effective leaked at %s", path.String())
|
||||||
|
}
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if strings.Contains(change.Before.YAML, "PRIVATE_CANARY") || strings.Contains(change.After.YAML, "PRIVATE_CANARY") {
|
||||||
|
t.Fatalf("history leaked at %s: %#v", path.String(), change)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var output strings.Builder
|
||||||
|
if err := result.Inspection.WriteEffective(&output); err != nil || strings.Contains(output.String(), "PRIVATE_CANARY") {
|
||||||
|
t.Fatalf("effective output leaked: %q, %v", output.String(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMovedDynamicTaggedValueStaysHidden(t *testing.T) {
|
||||||
|
type sensitive struct {
|
||||||
|
ID string `yaml:"id"`
|
||||||
|
Token string `yaml:"token" configkit:"secret"`
|
||||||
|
}
|
||||||
|
type config struct {
|
||||||
|
Items []any `yaml:"items"`
|
||||||
|
}
|
||||||
|
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
||||||
|
Defaults: func() config {
|
||||||
|
return config{Items: []any{sensitive{ID: "a", Token: "DYNAMIC_PRIVATE_CANARY"}, sensitive{ID: "b", Token: "other"}}}
|
||||||
|
},
|
||||||
|
Resolve: func(value *config, r *Resolution) error {
|
||||||
|
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||||
|
r.RecordNormalization(Keys("items"), "reorder")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||||
|
result, err := testLoad(NoSource(), Options[config]{Definition: definition})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, path := range []Path{Keys("items"), Keys("items").Index(1).Key("token")} {
|
||||||
|
explanation, ok := result.Inspection.Explain(path)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing explanation %s", path.String())
|
||||||
|
}
|
||||||
|
if strings.Contains(explanation.Effective.YAML, "DYNAMIC_PRIVATE_CANARY") {
|
||||||
|
t.Fatal("effective value leaked")
|
||||||
|
}
|
||||||
|
for _, change := range explanation.History {
|
||||||
|
if strings.Contains(change.Before.YAML, "DYNAMIC_PRIVATE_CANARY") || strings.Contains(change.After.YAML, "DYNAMIC_PRIVATE_CANARY") {
|
||||||
|
t.Fatal("history leaked")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
|||||||
if target, matched := model.resolutionPayload.correspondence.finalPath(path); matched {
|
if target, matched := model.resolutionPayload.correspondence.finalPath(path); matched {
|
||||||
projected[target.String()] = target
|
projected[target.String()] = target
|
||||||
}
|
}
|
||||||
|
if prior, matched := model.resolutionPayload.correspondence.entryPath(path); matched {
|
||||||
|
projected[prior.String()] = prior
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for pointer, path := range projected {
|
for pointer, path := range projected {
|
||||||
paths[pointer] = path
|
paths[pointer] = path
|
||||||
|
|||||||
Reference in New Issue
Block a user