Propagate matched sensitivity with one work queue

This commit is contained in:
2026-11-02 17:33:44 +00:00
parent 34c3241dae
commit ebf98ffc62
5 changed files with 149 additions and 29 deletions

View File

@@ -125,7 +125,16 @@ operation path solely to satisfy an ancestor check.
## Finding 3 — Sensitivity propagation rescans the whole collection quadratically
**Priority:** P2. **Status:** Open.
**Priority:** P2. **Status:** Complete.
Completion: matching edges and real dependencies now share one indexed work
queue. Candidate coordinates are registered once, newly reached historical
coordinates are registered on demand, and each trigger fires at most once;
classification no longer rebuilds closure in full-map passes. Conservative
historical masking is retained. The existing earlier-matched-value regression
is retained in this fix, and a cyclic rotation regression covers removed fields
and dependency-derived values. Focused sensitivity tests and
`./scripts/check.sh` passed.
**Code:** [secret classification](../../internal/engine/secret.go),
`classifySecretPathsWithEvidence`, working-tree lines 83–122; associated

View File

@@ -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)
}
}
}

View File

@@ -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")
}
}

View File

@@ -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) {

View File

@@ -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)