Preserve secret sensitivity across sibling inheritance

This commit is contained in:
2026-09-29 22:56:10 +00:00
parent f916e7d1f9
commit 501d59f21b
3 changed files with 182 additions and 0 deletions

View File

@@ -244,6 +244,13 @@ history budgets. Run the everyday checker.
## Stage 6 — Preserve sensitivity through inheritance ## Stage 6 — Preserve sensitivity through inheritance
**Completed 2026-09-29.** Sensitivity classification now follows corresponding
source/destination paths through structural transfers and controls to a fixed
point, while leaving neighboring fields unclassified. Historical replacement
evidence remains redacted, and inherited secret references remain unresolved
until the existing typed lookup point. Focused secret/history tests and
`./scripts/check.sh` passed. Public loading remains unchanged.
Connect transfer dependencies to existing secret classification and historical Connect transfer dependencies to existing secret classification and historical
redaction. Classify both source and destination paths under schema and redaction. Classify both source and destination paths under schema and
`Definition.Secret` rules; propagate sensitivity through chains/nested copies. `Definition.Secret` rules; propagate sensitivity through chains/nested copies.

View File

@@ -0,0 +1,136 @@
package engine
import (
"bytes"
"reflect"
"strings"
"testing"
)
func TestInheritanceSensitivityFollowsCopiesWithoutMaskingNeighbors(t *testing.T) {
const canary = "INHERITED-CANARY-CREDENTIAL"
input := parseCompositionDocument(t, "source-1", "parent: {token: "+canary+", plain: visible}\nchild: {extends: parent}\nnext: {extends: child}\nother: {extends: parent}\n")
for _, classified := range []Path{Keys("parent", "token"), Keys("next", "token")} {
t.Run(classified.String(), func(t *testing.T) {
inspection, _ := inheritanceInspectionForTest(t, input)
inspection.secrets = classifySecretPaths(nil, func(path Path) bool {
return path.String() == classified.String()
}, inspection.metadata, inspection.final, inspection.applications, inspection.input)
for _, path := range []Path{Keys("parent", "token"), Keys("child", "token"), Keys("next", "token"), Keys("other", "token")} {
explanation, found := inspection.Explain(path)
if !found || !explanation.Effective.Redacted || strings.Contains(explanation.Effective.YAML, canary) {
t.Fatalf("sensitive copy %s = %#v, %v", path, explanation, found)
}
for _, change := range explanation.History {
if strings.Contains(change.Before.YAML, canary) || strings.Contains(change.After.YAML, canary) {
t.Fatalf("historical canary at %s: %#v", path, change)
}
}
}
plain, found := inspection.Explain(Keys("other", "plain"))
if !found || plain.Effective.Redacted || plain.Effective.YAML != "visible\n" {
t.Fatalf("unrelated field = %#v, %v", plain, found)
}
var output bytes.Buffer
if err := inspection.WriteEffective(&output); err != nil || strings.Contains(output.String(), canary) || !strings.Contains(output.String(), "visible") {
t.Fatalf("effective rendering = %q, %v", output.String(), err)
}
})
}
}
func TestInheritanceSensitivityPreservesDisplacedHistoryAndControl(t *testing.T) {
const canary = "DISPLACED-CANARY-CREDENTIAL"
inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", "parent: {branch: {token: "+canary+"}}\nchild: {extends: parent, branch: public}\n"))
inspection.secrets = classifySecretPaths(nil, func(path Path) bool {
return path.String() == Keys("parent", "branch", "token").String() || path.String() == Keys("child", inheritanceKey).String()
}, inspection.metadata, inspection.final, inspection.applications, inspection.input)
path := Keys("child", "branch", "token")
explanation, found := inspection.Explain(path)
if !found || explanation.Effective.Present {
t.Fatalf("displaced sensitive path = %#v, %v", explanation, found)
}
for _, change := range explanation.History {
if strings.Contains(change.Before.YAML, canary) || strings.Contains(change.After.YAML, canary) ||
(change.After.Present && !change.After.Redacted) {
t.Fatalf("displaced secret leaked: %#v", explanation.History)
}
}
var rendered bytes.Buffer
if err := inspection.WriteExplanation(&rendered, path); err != nil || strings.Contains(rendered.String(), canary) {
t.Fatalf("displaced explanation rendering = %q, %v", rendered.String(), err)
}
control, found := inspection.Explain(Keys("child", inheritanceKey))
if !found || control.Effective.Present || len(control.History) == 0 || !control.History[0].After.Redacted {
t.Fatalf("sensitive control history = %#v, %v", control, found)
}
info, found := inspection.Metadata().Lookup(Keys("child", "branch"))
if !found || len(info.Origin.Inheritance) != 0 {
t.Fatalf("independent replacement provenance = %#v, %v", info, found)
}
}
func TestInheritanceSensitivityIncludesSchemaTaggedSource(t *testing.T) {
type entry struct {
Token string `yaml:"token"`
}
type configuration struct {
Parent struct {
Token string `yaml:"token" configkit:"secret"`
} `yaml:"parent"`
Child entry `yaml:"child"`
}
schema, issue := buildSchema(reflect.TypeFor[configuration](), testSecretBinding())
if issue != nil {
t.Fatal(issue)
}
inspection, _ := inheritanceInspectionForTest(t, parseCompositionDocument(t, "source-1", "parent: {token: TAGGED-CANARY}\nchild: {extends: parent}\n"))
inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, inspection.applications, inspection.input)
child, found := inspection.Explain(Keys("child", "token"))
if !found || !child.Effective.Redacted || strings.Contains(child.Effective.YAML, "TAGGED-CANARY") {
t.Fatalf("tagged source copy = %#v, %v", child, found)
}
}
func TestInheritedSecretReferencesResolveOnlyAfterExpansion(t *testing.T) {
type entry struct {
Credential Secret `yaml:"credential"`
}
type configuration struct {
Entries map[string]entry `yaml:"entries"`
}
schema, issue := buildSchema(reflect.TypeFor[configuration](), testSecretBinding())
if issue != nil {
t.Fatal(issue)
}
composed, diagnostics := composeDocuments(parseCompositionDocument(t, "source-1", "entries:\n parent: {credential: INHERITED_SECRET_NAME}\n child: {extends: parent}\n"))
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
expanded, diagnostics := expandInheritanceWithOrigins(composed.root, composed.origins, defaultProjectionLimits())
if len(diagnostics) != 0 {
t.Fatal(diagnostics)
}
lookupCount := 0
decoded, issues := decodeConfiguration(schema, reflect.Zero(reflect.TypeFor[configuration]()), expanded.root)
if len(issues) != 0 || lookupCount != 0 {
t.Fatalf("decoding and lookup order = %#v, %d", issues, lookupCount)
}
resolved := resolveSecretReferences(decoded.value, schema, expanded.root, func(name string) (string, bool) {
lookupCount++
if name != "INHERITED_SECRET_NAME" {
t.Fatalf("inherited reference name = %q", name)
}
return "RESOLVED-CREDENTIAL-CANARY", true
})
if len(resolved.diagnostics) != 0 || lookupCount != 1 ||
decoded.value.Interface().(configuration).Entries["child"].Credential.Value() != "RESOLVED-CREDENTIAL-CANARY" {
t.Fatalf("inherited reference resolution = %#v, %d", resolved.diagnostics, lookupCount)
}
inspection, _ := inheritanceInspectionFromComposition(t, composed)
inspection.secrets = classifySecretPaths(schema, nil, inspection.metadata, inspection.final, inspection.applications, inspection.input)
var rendered bytes.Buffer
if err := inspection.WriteEffective(&rendered); err != nil || strings.Contains(rendered.String(), "RESOLVED-CREDENTIAL-CANARY") {
t.Fatalf("reference rendering = %q, %v", rendered.String(), err)
}
}

View File

@@ -34,6 +34,35 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me
} }
} }
} }
var inherited []secretDependency
for _, operation := range metadata.inheritance {
if operation.control != nil {
control := operation.control.path
paths[control.String()] = control.clone()
for _, contributor := range operation.controlOrigin.contributors {
paths[contributor.source.String()] = contributor.source.clone()
inherited = append(inherited, secretDependency{target: control.clone(), dependency: contributor.source.clone()})
}
continue
}
if operation.transfer == nil {
continue
}
for pointer := range operation.parentOrigins {
source, err := ParsePath(pointer)
if err != nil {
continue
}
relative, ok := relativeHistoryPath(operation.transfer.from, source)
if !ok {
continue
}
destination := appendInheritancePath(operation.transfer.to, relative)
paths[pointer] = source
paths[destination.String()] = destination.clone()
inherited = append(inherited, secretDependency{target: destination, dependency: source})
}
}
secret := make(map[string]bool) secret := make(map[string]bool)
for _, tree := range append(projections, final) { for _, tree := range append(projections, final) {
@@ -62,6 +91,16 @@ func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, me
changed = true changed = true
} }
} }
for _, link := range inherited {
if pathIsSecret(secret, link.dependency) && !secret[link.target.String()] {
secret[link.target.String()] = true
changed = true
}
if pathIsSecret(secret, link.target) && !secret[link.dependency.String()] {
secret[link.dependency.String()] = true
changed = true
}
}
if !changed { if !changed {
return secret return secret
} }