Propagate matched sensitivity with one work queue
This commit is contained in:
@@ -675,3 +675,37 @@ func TestMovedDynamicTaggedValueStaysHidden(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMovedDependencyProtectsEarlierMatchedValue(t *testing.T) {
|
||||
type item struct {
|
||||
ID string `yaml:"id"`
|
||||
Secret string `yaml:"secret"`
|
||||
Value string `yaml:"value"`
|
||||
}
|
||||
type config struct {
|
||||
Items []item `yaml:"items"`
|
||||
}
|
||||
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
||||
Secret: func(path Path) bool { return path.String() == "/items/0/secret" },
|
||||
Resolve: func(value *config, r *Resolution) error {
|
||||
value.Items[0], value.Items[1] = value.Items[1], value.Items[0]
|
||||
value.Items[0].Value = value.Items[1].Secret
|
||||
r.RecordNormalization(Keys("items"), "reorder")
|
||||
r.RecordNormalization(Keys("items").Index(0).Key("value"), "derive", Keys("items").Index(1).Key("secret"))
|
||||
return nil
|
||||
},
|
||||
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||
result, err := testLoad(Bytes("records", []byte("items: [{id: a, secret: HIDDEN_A, value: public}, {id: b, value: EARLIER_PRIVATE}]\n")), Options[config]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
explanation, ok := result.Inspection.Explain(Keys("items").Index(0).Key("value"))
|
||||
if !ok {
|
||||
t.Fatal("missing explanation")
|
||||
}
|
||||
for _, change := range explanation.History {
|
||||
if strings.Contains(change.Before.YAML, "EARLIER_PRIVATE") || strings.Contains(change.After.YAML, "EARLIER_PRIVATE") || strings.Contains(change.After.YAML, "HIDDEN_A") {
|
||||
t.Fatalf("sensitive lineage leaked: %#v", change)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package engine
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMovedRemovedFieldHasConsistentHistory(t *testing.T) {
|
||||
type config struct {
|
||||
@@ -94,3 +97,49 @@ func TestMovedInheritedHistoryUsesFinalRecordPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCyclicMatchingProtectsRemovedAndDerivedValues(t *testing.T) {
|
||||
type config struct {
|
||||
Items []map[string]any `yaml:"items"`
|
||||
Derived string `yaml:"derived"`
|
||||
}
|
||||
removed := Keys("items").Index(1).Key("extra")
|
||||
definition := DefinitionWithSequenceIdentityRegistrations(Definition[config]{
|
||||
Secret: func(path Path) bool { return path.String() == "/items/0/extra" },
|
||||
Resolve: func(value *config, r *Resolution) error {
|
||||
value.Derived = value.Items[0]["extra"].(string)
|
||||
delete(value.Items[0], "extra")
|
||||
last := value.Items[len(value.Items)-1]
|
||||
copy(value.Items[1:], value.Items[:len(value.Items)-1])
|
||||
value.Items[0] = last
|
||||
r.RecordNormalization(Keys("items"), "remove field and rotate")
|
||||
r.RecordDerivedDefault(Keys("derived"), "derive from removed value", removed)
|
||||
return nil
|
||||
},
|
||||
}, []SequenceIdentityRegistrationInput{{ExactPath: pathPointer(Keys("items")), IdentityFields: []string{"id"}}})
|
||||
result, err := testLoad(Bytes("records", []byte("items: [{id: a, extra: CYCLIC_PRIVATE_CANARY}, {id: b}, {id: c}, {id: d}]\n")), Options[config]{Definition: definition})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, path := range []Path{Keys("items"), removed, Keys("derived")} {
|
||||
explanation, found := result.Inspection.Explain(path)
|
||||
if !found {
|
||||
t.Fatalf("missing explanation for %s", path.String())
|
||||
}
|
||||
if strings.Contains(explanation.Effective.YAML, "CYCLIC_PRIVATE_CANARY") {
|
||||
t.Fatalf("effective value leaked at %s", path.String())
|
||||
}
|
||||
for _, change := range explanation.History {
|
||||
if strings.Contains(change.Before.YAML, "CYCLIC_PRIVATE_CANARY") || strings.Contains(change.After.YAML, "CYCLIC_PRIVATE_CANARY") {
|
||||
t.Fatalf("history leaked at %s", path.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
var output strings.Builder
|
||||
if err := result.Inspection.WriteEffective(&output); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(output.String(), "CYCLIC_PRIVATE_CANARY") {
|
||||
t.Fatal("effective rendering leaked")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,34 +80,11 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
||||
classifyPath(resolved)
|
||||
}
|
||||
}
|
||||
classified := closeSecretDependencies(paths, secret, dependencies, inherited)
|
||||
var correspondence resolutionCorrespondence
|
||||
if model := metadata.historyEvidence(); model != nil && model.resolutionPayload != nil {
|
||||
// Project from the completed entry classification into a separate map so
|
||||
// one side of a swap cannot influence the other while it is being read.
|
||||
projected := make(map[string]Path)
|
||||
for _, path := range paths {
|
||||
if !pathIsSecret(classified, path) {
|
||||
continue
|
||||
}
|
||||
if target, matched := model.resolutionPayload.correspondence.finalPath(path); matched {
|
||||
projected[target.String()] = target
|
||||
}
|
||||
if prior, matched := model.resolutionPayload.correspondence.entryPath(path); matched {
|
||||
projected[prior.String()] = prior
|
||||
}
|
||||
}
|
||||
for pointer, path := range projected {
|
||||
paths[pointer] = path
|
||||
secret[pointer] = true
|
||||
}
|
||||
for pointer, sensitive := range classified {
|
||||
if sensitive {
|
||||
secret[pointer] = true
|
||||
}
|
||||
}
|
||||
return closeSecretDependencies(paths, secret, dependencies, inherited)
|
||||
correspondence = model.resolutionPayload.correspondence
|
||||
}
|
||||
return classified
|
||||
return closeSecretDependenciesWithCorrespondence(paths, secret, dependencies, inherited, correspondence)
|
||||
}
|
||||
|
||||
func addSecretPath(paths map[string]Path, pointer string) {
|
||||
|
||||
@@ -6,6 +6,13 @@ type secretTrigger struct {
|
||||
}
|
||||
|
||||
func closeSecretDependencies(paths map[string]Path, sensitive map[string]bool, dependencies, inherited []secretDependency) map[string]bool {
|
||||
return closeSecretDependenciesWithCorrespondence(paths, sensitive, dependencies, inherited, resolutionCorrespondence{})
|
||||
}
|
||||
|
||||
// Matching adds endpoint edges to the same work queue as recorded dependencies.
|
||||
// Newly reached coordinates are indexed once, including historical fields that
|
||||
// have no physical descendant at the corresponding final position.
|
||||
func closeSecretDependenciesWithCorrespondence(paths map[string]Path, sensitive map[string]bool, dependencies, inherited []secretDependency, correspondence resolutionCorrespondence) map[string]bool {
|
||||
result := make(map[string]bool, len(sensitive))
|
||||
queue := make([]string, 0, len(sensitive))
|
||||
for pointer, classified := range sensitive {
|
||||
@@ -17,12 +24,14 @@ func closeSecretDependencies(paths map[string]Path, sensitive map[string]bool, d
|
||||
|
||||
var triggers pathPrefixIndex
|
||||
triggerValues := make([]secretTrigger, 0, len(dependencies)+2*len(inherited))
|
||||
fired := make([]bool, 0, cap(triggerValues))
|
||||
insert := func(dependency Path, target string, ordinary bool) {
|
||||
if ordinary && !dependency.valid() {
|
||||
return
|
||||
}
|
||||
id := len(triggerValues)
|
||||
triggerValues = append(triggerValues, secretTrigger{target: target, ordinary: ordinary})
|
||||
fired = append(fired, false)
|
||||
triggers.insert(dependency.String(), id)
|
||||
}
|
||||
for _, dependency := range dependencies {
|
||||
@@ -33,7 +42,46 @@ func closeSecretDependencies(paths map[string]Path, sensitive map[string]bool, d
|
||||
insert(link.target, link.dependency.String(), false)
|
||||
}
|
||||
|
||||
fired := make([]bool, len(triggerValues))
|
||||
var projected map[string]bool
|
||||
if len(correspondence.collections) != 0 {
|
||||
projected = make(map[string]bool, len(paths))
|
||||
}
|
||||
project := func(path Path) {
|
||||
if projected == nil || !path.valid() {
|
||||
return
|
||||
}
|
||||
pointer := path.String()
|
||||
if projected[pointer] {
|
||||
return
|
||||
}
|
||||
projected[pointer] = true
|
||||
add := func(target Path, matched bool) {
|
||||
if !matched {
|
||||
return
|
||||
}
|
||||
targetPointer := target.String()
|
||||
if targetPointer == pointer {
|
||||
return
|
||||
}
|
||||
if _, exists := paths[targetPointer]; !exists {
|
||||
paths[targetPointer] = target
|
||||
}
|
||||
insert(path, targetPointer, false)
|
||||
}
|
||||
add(correspondence.finalPath(path))
|
||||
add(correspondence.entryPath(path))
|
||||
}
|
||||
if projected != nil {
|
||||
// Projection may add missing target coordinates to paths. Snapshot the
|
||||
// original candidates so map insertion cannot affect this traversal.
|
||||
candidates := make([]Path, 0, len(paths))
|
||||
for _, path := range paths {
|
||||
candidates = append(candidates, path)
|
||||
}
|
||||
for _, path := range candidates {
|
||||
project(path)
|
||||
}
|
||||
}
|
||||
activate := func(id int) {
|
||||
if fired[id] {
|
||||
return
|
||||
@@ -48,6 +96,9 @@ func closeSecretDependencies(paths map[string]Path, sensitive map[string]bool, d
|
||||
for head := 0; head < len(queue); head++ {
|
||||
pointer := queue[head]
|
||||
if classified, ok := paths[pointer]; ok && classified.valid() {
|
||||
// Register a synthesized coordinate before visiting triggers, never
|
||||
// while a trigger-index traversal is in progress.
|
||||
project(classified)
|
||||
triggers.visitAncestors(classified.String(), func(id int) {
|
||||
if triggerValues[id].ordinary {
|
||||
activate(id)
|
||||
|
||||
Reference in New Issue
Block a user