227 lines
6.6 KiB
Go
227 lines
6.6 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type secretDependency struct {
|
|
target Path
|
|
dependency Path
|
|
}
|
|
|
|
func classifySecretPaths(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, projections ...*document.Node) map[string]bool {
|
|
return classifySecretPathsWithEvidence(schema, predicate, metadata, final, applications, nil, projections...)
|
|
}
|
|
|
|
func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Path) bool, metadata Metadata, final *document.Node, applications []historyApplication, evidence []*sourceVersionEvidence, projections ...*document.Node) 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)
|
|
}
|
|
for _, application := range applications {
|
|
walkHistoryApplication(application, func(_ Path, path Path, _ *document.Node) {
|
|
if len(application.sequences) != 0 {
|
|
addCandidate(path)
|
|
} else if parsed, err := ParsePath(path.String()); err == nil {
|
|
addCandidate(parsed)
|
|
}
|
|
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 {
|
|
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
|
|
}
|
|
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})
|
|
}
|
|
}
|
|
|
|
secret := make(map[string]bool)
|
|
classifyProjection := func(tree *document.Node) {
|
|
walkDocument(tree, Path{}, func(path Path, node *document.Node) {
|
|
pointer := path.String()
|
|
addSecretPath(paths, pointer)
|
|
if parsed, err := ParsePath(pointer); err == nil {
|
|
addCandidate(parsed)
|
|
}
|
|
if node.Sensitive {
|
|
secret[pointer] = true
|
|
}
|
|
})
|
|
}
|
|
for _, tree := range projections {
|
|
classifyProjection(tree)
|
|
}
|
|
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 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 addSecretEvidencePath(paths map[string]Path, destination, local Path) Path {
|
|
path := appendDestination(destination, local)
|
|
addSecretPath(paths, path.String())
|
|
return path
|
|
}
|
|
|
|
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)
|
|
for current.kind == descriptorPointer {
|
|
current = current.element
|
|
}
|
|
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
|
|
}
|