Files

172 lines
4.7 KiB
Go
Raw Permalink Normal View History

package engine
2026-10-16 15:44:44 +00:00
import (
"slices"
"go.maximumdirect.net/configkit/internal/document"
2026-10-16 15:44:44 +00:00
)
type secretDependency struct {
target Path
dependency Path
}
func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata) map[string]bool {
return classifySecretPathsWithEvidence(schema, predicate, metadata, nil)
}
func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, evidence []*sourceVersionEvidence) map[string]bool {
paths := make(map[string]Path)
2026-10-16 15:44:44 +00:00
candidateShapes := make(map[string][]Path)
var candidates []Path
addCandidate := func(path Path) {
pointer := path.String()
for _, prior := range candidateShapes[pointer] {
if slices.Equal(path.segments, prior.segments) {
return
}
}
cloned := path.clone()
candidateShapes[pointer] = append(candidateShapes[pointer], cloned)
candidates = append(candidates, cloned)
}
secret := make(map[string]bool)
var dependencies, inherited []secretDependency
if model := metadata.historyEvidence(); model != nil {
dependencies, inherited = model.sensitivityDependencies()
for _, candidate := range model.sensitivityCandidates(inherited) {
pointer := candidate.path.String()
paths[pointer] = candidate.path.clone()
if candidate.classify {
addCandidate(candidate.path)
}
if candidate.sensitive {
secret[pointer] = true
}
}
for _, edge := range dependencies {
paths[edge.target.String()] = edge.target.clone()
paths[edge.dependency.String()] = edge.dependency.clone()
addCandidate(edge.dependency)
}
for _, edge := range inherited {
paths[edge.target.String()] = edge.target.clone()
paths[edge.dependency.String()] = edge.dependency.clone()
addCandidate(edge.target)
addCandidate(edge.dependency)
}
}
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
}
}
2026-10-16 15:44:44 +00:00
for _, path := range candidates {
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
}
for _, resolved := range item.sensitivityPaths() {
addSecretPath(paths, resolved.String())
2026-10-16 15:44:44 +00:00
addCandidate(resolved)
classifyPath(resolved)
}
}
return closeSecretDependencies(paths, secret, dependencies, inherited)
}
func addSecretPath(paths map[string]Path, pointer string) {
2026-10-09 04:10:41 +00:00
if existing, found := paths[pointer]; found && existing.valid() {
// Accumulator keys already match their paths' canonical spelling. History
// can install typed segments, which still need the parsed replacement.
parsed := true
for _, segment := range existing.segments {
if segment.kind != pathSegmentParsed {
parsed = false
break
}
}
if parsed {
return
}
}
if path, err := ParsePath(pointer); err == nil {
paths[pointer] = path
}
}
func schemaSecretRoot(schema *schemaDescriptor, path Path) (Path, bool) {
if schema == nil || schema.root == nil || !path.valid() {
return Path{}, false
}
current := schema.root
walked := Path{}
for _, segment := range path.segments {
walked.segments = append(walked.segments, segment)
var ok bool
current, ok = terminalDescriptor(current)
if !ok {
return Path{}, false
}
switch current.kind {
case descriptorStruct:
if segment.kind == pathSegmentIndex {
return Path{}, false
}
position, ok := current.fieldByName[segment.key]
var field *fieldDescriptor
if ok {
field = &current.fields[position]
} else {
field = current.inlineMap
}
if field == nil {
return Path{}, false
}
if field.secret {
return walked.clone(), true
}
if field.inlineMap {
current = field.descriptor.element
} else {
current = field.descriptor
}
case descriptorMap:
if segment.kind == pathSegmentIndex {
return Path{}, false
}
current = current.element
case descriptorSlice, descriptorArray:
if _, ok := schemaIndex(segment); !ok {
return Path{}, false
}
current = current.element
case descriptorDynamic:
return Path{}, false
default:
return Path{}, false
}
}
return Path{}, false
}
// Dynamic type replacement can change classification even when projected text
// is equal. Validators must preserve this inspection metadata as well as values.
func projectedSensitivity(root *document.Node) map[string]Path {
result := make(map[string]Path)
walkDocument(root, Path{}, func(path Path, node *document.Node) {
if node.Sensitive {
result[path.String()] = path.clone()
}
})
return result
}