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 }