172 lines
4.7 KiB
Go
172 lines
4.7 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"go.maximumdirect.net/configkit/internal/document"
|
|
)
|
|
|
|
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)
|
|
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
|
|
}
|
|
}
|
|
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())
|
|
addCandidate(resolved)
|
|
classifyPath(resolved)
|
|
}
|
|
}
|
|
return closeSecretDependencies(paths, secret, dependencies, inherited)
|
|
}
|
|
|
|
func addSecretPath(paths map[string]Path, pointer string) {
|
|
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 = ¤t.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
|
|
}
|