Move sensitivity classification to shared history

This commit is contained in:
2026-10-26 17:25:11 +00:00
parent 408dd13c24
commit 8155465322
4 changed files with 150 additions and 54 deletions

View File

@@ -610,7 +610,11 @@ Reuse metadata-location, secret-location, CLI, and provenance tests. Focused che
## Stage 13 — Migrate Historical Sensitivity Readers
Owners: `secret.go`, `secret_propagation.go`, model candidates/dependencies.
Status: pending.
Status: complete. Validation:
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Secret|Sensitive|Redact|Diff' -count=1`,
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test . -run 'Secret|Sensitive|Redact|Diff' -count=1`,
`GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./...`, and `git diff --check`.
1. Replace reads of metadata history and private inheritance copies with the
complete model candidate and dependency services. Preserve original resolution

View File

@@ -3,6 +3,8 @@ package engine
import (
"slices"
"sort"
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
)
type historySelection struct {
@@ -21,6 +23,7 @@ type historySelection struct {
type historySensitivityCandidate struct {
path Path
sensitive bool
classify bool
dependencies []Path
}
@@ -200,16 +203,123 @@ func (model *historyModel) sensitivityCandidates() []historySensitivityCandidate
return nil
}
candidates := make([]historySensitivityCandidate, 0, len(model.observations))
for _, observation := range model.observations {
candidate := historySensitivityCandidate{path: observation.path.clone(), sensitive: observation.sensitive}
if attribution, found := model.resolveAttribution(observation.attributionRef); found {
candidate.dependencies = attribution.dependencies
indices := make(map[string][]int)
add := func(path Path, sensitive, classify bool) int {
if !path.valid() {
return -1
}
candidates = append(candidates, candidate)
pointer := path.String()
for _, index := range indices[pointer] {
if sameConcreteHistoryPath(candidates[index].path, path) {
candidates[index].sensitive = candidates[index].sensitive || sensitive
candidates[index].classify = candidates[index].classify || classify
return index
}
}
index := len(candidates)
candidates = append(candidates, historySensitivityCandidate{path: path.clone(), sensitive: sensitive, classify: classify})
indices[pointer] = append(indices[pointer], index)
return index
}
for _, observation := range model.observations {
concreteCandidate := observation.role == historyRoleControl
if observation.role == historyRoleContribution {
if application, found := model.applicationPayload(observation.payload); found && len(application.sequences) != 0 {
concreteCandidate = true
}
}
index := add(observation.path, observation.sensitive, concreteCandidate)
if parsed, err := ParsePath(observation.path.String()); err == nil {
add(parsed, observation.sensitive, true)
}
if index < 0 {
continue
}
if attribution, found := model.resolveAttribution(observation.attributionRef); found {
for _, dependency := range attribution.dependencies {
if dependency.valid() {
appendDistinctHistoryPath(&candidates[index].dependencies, dependency)
add(dependency, false, true)
}
}
}
}
_, inherited := model.sensitivityDependencies()
for _, link := range inherited {
add(link.target, false, true)
add(link.dependency, false, true)
}
return candidates
}
func appendDistinctHistoryPath(paths *[]Path, path Path) {
for _, existing := range *paths {
if sameConcreteHistoryPath(existing, path) {
return
}
}
*paths = append(*paths, path.clone())
}
// sensitivityDependencies returns the original attribution edges and the
// bidirectional links represented by inheritance payloads. It intentionally
// does not derive edges from the public metadata projection, which may omit
// evidence needed to keep historical values redacted.
func (model *historyModel) sensitivityDependencies() (dependencies, inherited []secretDependency) {
if model == nil {
return nil, nil
}
for _, observation := range model.observations {
if observation.phase == historyPhaseInheritance {
continue
}
attribution, found := model.resolveAttribution(observation.attributionRef)
if !found {
continue
}
for _, dependency := range attribution.dependencies {
if observation.reportedPath.valid() && dependency.valid() {
dependencies = append(dependencies, secretDependency{
target: observation.reportedPath.clone(), dependency: dependency.clone(),
})
}
}
}
for _, payload := range model.inheritancePayloads {
for _, operation := range payload.operations {
if operation.control != nil {
control := operation.control.path
for _, contributor := range operation.controlOrigin.contributors {
if control.valid() && contributor.source.valid() {
inherited = append(inherited, secretDependency{
target: control.clone(), dependency: contributor.source.clone(),
})
}
}
continue
}
if operation.transfer == nil || operation.parent == nil {
continue
}
transfer := operation.transfer
walkDocument(operation.parent, transfer.to, func(destination Path, _ *document.Node) {
relative, ok := relativeHistoryPath(transfer.to, destination)
if !ok {
return
}
source := appendInheritancePath(transfer.from, relative)
if _, found := operation.parentOrigins[source.String()]; !found {
return
}
inherited = append(inherited, secretDependency{
target: destination.clone(), dependency: source,
})
})
}
}
return dependencies, inherited
}
func (model *historyModel) resolveAttribution(ref historyAttributionRef) (historyAttribution, bool) {
if model == nil || ref < 0 || int(ref) >= len(model.attributions) {
return historyAttribution{}, false

View File

@@ -40,58 +40,37 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
addSecretPath(paths, path.String())
})
}
var dependencies []secretDependency
for pointer, history := range metadata.history {
addSecretPath(paths, pointer)
if path, err := ParsePath(pointer); err == nil {
addCandidate(path)
}
for _, change := range history {
paths[change.Path.String()] = change.Path.clone()
for _, dependency := range change.Dependencies {
secret := make(map[string]bool)
var dependencies, inherited []secretDependency
if model := metadata.currentHistoryModel(); model != nil {
dependencies, inherited = model.sensitivityDependencies()
for _, candidate := range model.sensitivityCandidates() {
pointer := candidate.path.String()
paths[pointer] = candidate.path.clone()
if candidate.classify {
addCandidate(candidate.path)
}
if candidate.sensitive {
secret[pointer] = true
}
for _, dependency := range candidate.dependencies {
paths[dependency.String()] = dependency.clone()
addCandidate(dependency)
dependencies = append(dependencies, secretDependency{
target: change.Path.clone(), dependency: dependency.clone(),
})
}
}
}
var inherited []secretDependency
for _, operation := range metadata.inheritance {
if operation.control != nil {
control := operation.control.path
paths[control.String()] = control.clone()
addCandidate(control)
for _, contributor := range operation.controlOrigin.contributors {
paths[contributor.source.String()] = contributor.source.clone()
addCandidate(contributor.source)
inherited = append(inherited, secretDependency{target: control.clone(), dependency: contributor.source.clone()})
}
continue
for _, edge := range dependencies {
paths[edge.target.String()] = edge.target.clone()
paths[edge.dependency.String()] = edge.dependency.clone()
addCandidate(edge.dependency)
}
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()
addCandidate(source)
addCandidate(destination)
inherited = append(inherited, secretDependency{target: destination, dependency: source})
for _, edge := range inherited {
paths[edge.target.String()] = edge.target.clone()
paths[edge.dependency.String()] = edge.dependency.clone()
addCandidate(edge.target)
addCandidate(edge.dependency)
}
}
secret := make(map[string]bool)
classifyProjection := func(tree *document.Node) {
walkDocument(tree, Path{}, func(path Path, node *document.Node) {
pointer := path.String()

View File

@@ -40,7 +40,9 @@ func TestSecretClassificationParsesTypedHistoryAfterNumericShapeChanges(t *testi
earlier := parseCompositionDocument(t, "earlier", test.earlier)
final := parseCompositionDocument(t, "final", test.final)
pointer := test.historical.String()
metadata := Metadata{history: map[string][]Change{pointer: {{Path: test.historical}}}}
builder := newHistoryBuilder()
builder.observeDocument(earlier, historyPhaseSources)
metadata := Metadata{historyModel: builder.freeze()}
seen := false
classified := classifySecretPaths(nil, func(path Path) bool {
if path.String() != pointer {
@@ -63,9 +65,10 @@ func TestSecretClassificationParsesTypedHistoryAfterNumericShapeChanges(t *testi
func TestSecretPredicateReceivesDetachedHistoricalPath(t *testing.T) {
path := Keys("records").Key("1")
metadata := Metadata{history: map[string][]Change{
path.String(): {{Path: path}},
}}
root := parseCompositionDocument(t, "history", "records: {'1': value}\n")
builder := newHistoryBuilder()
builder.observeDocument(root, historyPhaseSources)
metadata := Metadata{historyModel: builder.freeze()}
seen := false
classified := classifySecretPaths(nil, func(candidate Path) bool {
if candidate.String() != path.String() {