Preserve header sensitivity across colliding path shapes
This commit is contained in:
@@ -692,3 +692,18 @@ unsafe evidence query, or release gate. Reviewed behavioral coverage in
|
||||
No runtime code changed in this documentation unit, so the race suite was not
|
||||
rerun. No environmental limitation affected validation; checks ran on
|
||||
Linux/amd64 only. Publication and release preparation remain outside this work.
|
||||
|
||||
## Post-implementation remediation evidence
|
||||
|
||||
The subsequent review found coverage gaps beyond the original stage validation;
|
||||
the [remediation record](remediation.md#shared-source-version-review-2026-10-15)
|
||||
owns their status. Stage 9–12 evidence now also includes
|
||||
`TestSourceVersionEvidenceClassificationKeepsCollidingShapes` and
|
||||
`TestFacadeSourceVersionEvidenceRedactsCollidingShapes`. Both failed before the
|
||||
shape-collision fix and pass afterward. Classification preserves original
|
||||
mapping-key and sequence-index candidates independently without erasing payload
|
||||
classification. Focused source-version/sensitivity/history tests,
|
||||
`GOTOOLCHAIN=local ./scripts/check.sh`, and
|
||||
`GOTOOLCHAIN=local GOPROXY=off go test -race ./...` passed on
|
||||
`go1.26.9 linux/amd64`. Existing historical/dependency propagation and neighboring
|
||||
public-value coverage remain green; no new sensitivity policy was introduced.
|
||||
|
||||
@@ -162,7 +162,14 @@ issues remain. Findings 1 and 2 above remain completed historical findings.
|
||||
|
||||
### 3. Preserve distinct semantic shapes during evidence classification (P1)
|
||||
|
||||
Status: open.
|
||||
Status: complete. Original evidence shapes are classified independently before
|
||||
their results enter the existing pointer-based sensitivity/dependency closure.
|
||||
Evidence no longer overwrites payload candidates. New engine and public
|
||||
regressions failed before the fix and pass afterward, covering both acquisition
|
||||
orders, schema tags, predicate observation of both shapes, and unchanged final
|
||||
payload. Focused source-version/sensitivity/history tests, the everyday check,
|
||||
and the full race suite passed on `go1.26.9 linux/amd64`. The implementation
|
||||
plan records the additional Stage 9–12 regression evidence.
|
||||
|
||||
Affected: `classifySecretPathsWithEvidence` and `addSecretEvidencePath` in
|
||||
[secret.go](../../internal/engine/secret.go), lines 78–104 at `020d720`.
|
||||
|
||||
@@ -16,6 +16,68 @@ type sourceVersionModeReader struct {
|
||||
reads int
|
||||
}
|
||||
|
||||
func TestFacadeSourceVersionEvidenceRedactsCollidingShapes(t *testing.T) {
|
||||
t.Parallel()
|
||||
type header struct {
|
||||
Version string `yaml:"version"`
|
||||
}
|
||||
type secretHeader struct {
|
||||
Version string `yaml:"version" configkit:"secret"`
|
||||
}
|
||||
type configuration struct {
|
||||
Meta map[string]header `yaml:"meta"`
|
||||
Data struct {
|
||||
Meta map[string]secretHeader `yaml:"meta"`
|
||||
} `yaml:"data"`
|
||||
}
|
||||
path, err := configkit.ParsePath("/meta/0/version")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, reverse := range []bool{false, true} {
|
||||
groups := []configkit.Underlay{
|
||||
{Inputs: []configkit.Input{{
|
||||
Source: configkit.Bytes("mapping", []byte("meta: {\"0\": {version: PRIVATE-HEADER-CANARY}}\n")),
|
||||
Destination: configkit.Keys("data"),
|
||||
}}},
|
||||
{Inputs: []configkit.Input{{
|
||||
Source: configkit.Bytes("sequence", []byte("meta: [{version: another-header}]\n")),
|
||||
Destination: configkit.Keys("data"),
|
||||
}}},
|
||||
}
|
||||
if reverse {
|
||||
groups[0], groups[1] = groups[1], groups[0]
|
||||
}
|
||||
loaded, err := configkit.LoadLayered(configkit.Input{
|
||||
Source: configkit.Bytes("main", []byte("meta: {\"0\": {version: main}}\ndata: {meta: null}\n")),
|
||||
}, configkit.LayeredOptions[configuration]{
|
||||
Definition: configkit.Definition[configuration]{Version: &configkit.VersionRule{
|
||||
Path: path, Check: func(configkit.Version, *configkit.Validation) error { return nil },
|
||||
}},
|
||||
SourceVersionMode: configkit.SourceVersionModeRequired, Underlays: groups,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if loaded.Config.Meta["0"].Version != "main" || loaded.Config.Data.Meta != nil {
|
||||
t.Fatalf("reverse=%v: final payload changed: %#v", reverse, loaded.Config)
|
||||
}
|
||||
foundMapping := false
|
||||
for _, source := range loaded.Sources() {
|
||||
if source.Label != "mapping" {
|
||||
continue
|
||||
}
|
||||
foundMapping = true
|
||||
if check := source.VersionCheck; check == nil || !check.Version.Present || !check.Redacted || check.Version.Text != "" {
|
||||
t.Errorf("reverse=%v: schema-tagged header was not redacted", reverse)
|
||||
}
|
||||
}
|
||||
if !foundMapping {
|
||||
t.Fatal("missing mapping source record")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (reader *sourceVersionModeReader) Read(data []byte) (int, error) {
|
||||
reader.reads++
|
||||
return reader.Reader.Read(data)
|
||||
|
||||
@@ -75,6 +75,19 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
||||
classifyProjection(tree)
|
||||
}
|
||||
classifyProjection(final)
|
||||
classifyPath := func(path Path) {
|
||||
if root, ok := schemaSecretRoot(schema, path); ok {
|
||||
secret[root.String()] = true
|
||||
}
|
||||
if predicate != nil && predicate(path.clone()) {
|
||||
secret[path.String()] = true
|
||||
}
|
||||
}
|
||||
for _, path := range paths {
|
||||
classifyPath(path)
|
||||
}
|
||||
// Classify each original evidence shape before folding results into pointer
|
||||
// sensitivity. Numeric mapping keys and sequence indices can share a pointer.
|
||||
for _, item := range evidence {
|
||||
if item == nil {
|
||||
continue
|
||||
@@ -83,25 +96,18 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
||||
if !candidate.valid() {
|
||||
candidate = item.path
|
||||
}
|
||||
addSecretEvidencePath(paths, item.destination, candidate)
|
||||
classifyPath(addSecretEvidencePath(paths, item.destination, candidate))
|
||||
for _, ancestor := range item.ancestors {
|
||||
addSecretEvidencePath(paths, item.destination, ancestor.path)
|
||||
}
|
||||
}
|
||||
for pointer, path := range paths {
|
||||
if root, ok := schemaSecretRoot(schema, path); ok {
|
||||
secret[root.String()] = true
|
||||
}
|
||||
if predicate != nil && predicate(path.clone()) {
|
||||
secret[pointer] = true
|
||||
classifyPath(addSecretEvidencePath(paths, item.destination, ancestor.path))
|
||||
}
|
||||
}
|
||||
return closeSecretDependencies(paths, secret, dependencies, inherited)
|
||||
}
|
||||
|
||||
func addSecretEvidencePath(paths map[string]Path, destination, local Path) {
|
||||
func addSecretEvidencePath(paths map[string]Path, destination, local Path) Path {
|
||||
path := appendDestination(destination, local)
|
||||
paths[path.String()] = path.clone()
|
||||
addSecretPath(paths, path.String())
|
||||
return path
|
||||
}
|
||||
|
||||
func addSecretPath(paths map[string]Path, pointer string) {
|
||||
|
||||
@@ -169,3 +169,34 @@ func TestSourceVersionEvidenceSensitivityUsesOriginalMountedPaths(t *testing.T)
|
||||
t.Fatalf("unclassified or semantic evidence candidates = %#v; original path seen %v, ancestor predicate seen %v", classified, sawOriginalSequencePath, sawAncestorPredicate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceVersionEvidenceClassificationKeepsCollidingShapes(t *testing.T) {
|
||||
t.Parallel()
|
||||
type header struct {
|
||||
Version string `yaml:"version" configkit:"secret"`
|
||||
}
|
||||
type configuration struct {
|
||||
Meta map[string]header `yaml:"meta"`
|
||||
}
|
||||
schema, issue := buildSchema(reflect.TypeFor[configuration](), testSecretBinding())
|
||||
if issue != nil {
|
||||
t.Fatal(issue)
|
||||
}
|
||||
keyPath := Keys("meta", "0", "version")
|
||||
indexPath := Keys("meta").Index(0).Key("version")
|
||||
for _, reverse := range []bool{false, true} {
|
||||
evidence := []*sourceVersionEvidence{{resolvedPath: keyPath}, {resolvedPath: indexPath}}
|
||||
if reverse {
|
||||
evidence[0], evidence[1] = evidence[1], evidence[0]
|
||||
}
|
||||
var sawKey, sawIndex bool
|
||||
classified := classifySecretPathsWithEvidence(schema, func(path Path) bool {
|
||||
sawKey = sawKey || reflect.DeepEqual(path, keyPath)
|
||||
sawIndex = sawIndex || reflect.DeepEqual(path, indexPath)
|
||||
return false
|
||||
}, Metadata{}, nil, nil, evidence)
|
||||
if !classified[keyPath.String()] || !sawKey || !sawIndex {
|
||||
t.Errorf("reverse=%v: classified=%v, key seen=%v, index seen=%v", reverse, classified, sawKey, sawIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user