Preserve header sensitivity across colliding path shapes

This commit is contained in:
2026-10-15 22:17:49 +00:00
parent 25cb3c47e7
commit e5a6df7af7
5 changed files with 134 additions and 13 deletions

View File

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

View File

@@ -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`.

View File

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

View File

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

View File

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