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
|
No runtime code changed in this documentation unit, so the race suite was not
|
||||||
rerun. No environmental limitation affected validation; checks ran on
|
rerun. No environmental limitation affected validation; checks ran on
|
||||||
Linux/amd64 only. Publication and release preparation remain outside this work.
|
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)
|
### 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
|
Affected: `classifySecretPathsWithEvidence` and `addSecretEvidencePath` in
|
||||||
[secret.go](../../internal/engine/secret.go), lines 78–104 at `020d720`.
|
[secret.go](../../internal/engine/secret.go), lines 78–104 at `020d720`.
|
||||||
|
|||||||
@@ -16,6 +16,68 @@ type sourceVersionModeReader struct {
|
|||||||
reads int
|
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) {
|
func (reader *sourceVersionModeReader) Read(data []byte) (int, error) {
|
||||||
reader.reads++
|
reader.reads++
|
||||||
return reader.Reader.Read(data)
|
return reader.Reader.Read(data)
|
||||||
|
|||||||
@@ -75,6 +75,19 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
|||||||
classifyProjection(tree)
|
classifyProjection(tree)
|
||||||
}
|
}
|
||||||
classifyProjection(final)
|
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 {
|
for _, item := range evidence {
|
||||||
if item == nil {
|
if item == nil {
|
||||||
continue
|
continue
|
||||||
@@ -83,25 +96,18 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
|||||||
if !candidate.valid() {
|
if !candidate.valid() {
|
||||||
candidate = item.path
|
candidate = item.path
|
||||||
}
|
}
|
||||||
addSecretEvidencePath(paths, item.destination, candidate)
|
classifyPath(addSecretEvidencePath(paths, item.destination, candidate))
|
||||||
for _, ancestor := range item.ancestors {
|
for _, ancestor := range item.ancestors {
|
||||||
addSecretEvidencePath(paths, item.destination, ancestor.path)
|
classifyPath(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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return closeSecretDependencies(paths, secret, dependencies, inherited)
|
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)
|
path := appendDestination(destination, local)
|
||||||
paths[path.String()] = path.clone()
|
addSecretPath(paths, path.String())
|
||||||
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func addSecretPath(paths map[string]Path, pointer string) {
|
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)
|
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