369 lines
11 KiB
Go
369 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"slices"
|
|
"sort"
|
|
|
|
"gitea.maximumdirect.net/eric/ConfigKit/internal/document"
|
|
)
|
|
|
|
type historySelection struct {
|
|
path Path
|
|
phase historyPhase
|
|
payload historyPayloadRef
|
|
reportedPath Path
|
|
role historyRole
|
|
before bool
|
|
after bool
|
|
attribution historyAttribution
|
|
hasAttribution bool
|
|
previous []Origin
|
|
}
|
|
|
|
type historySensitivityCandidate struct {
|
|
path Path
|
|
sensitive bool
|
|
classify bool
|
|
dependencies []Path
|
|
}
|
|
|
|
func (model *historyModel) selectHistory(path Path) []historySelection {
|
|
if model == nil || !path.valid() || !model.hasObservedShape(path) {
|
|
return nil
|
|
}
|
|
|
|
candidateSet := make(map[int]struct{})
|
|
for _, observationID := range model.observationsByPointer[path.String()] {
|
|
candidateSet[observationID] = struct{}{}
|
|
}
|
|
model.reportedPathIndex.visitAncestors(path.String(), func(observationID int) {
|
|
candidateSet[observationID] = struct{}{}
|
|
})
|
|
model.reportedPathIndex.visitDescendants(path.String(), func(observationID int) {
|
|
candidateSet[observationID] = struct{}{}
|
|
})
|
|
candidates := make([]int, 0, len(candidateSet))
|
|
for observationID := range candidateSet {
|
|
candidates = append(candidates, observationID)
|
|
}
|
|
slices.Sort(candidates)
|
|
|
|
selections := make([]historySelection, 0, len(candidates))
|
|
selectionByPayload := make(map[historyPayloadRef]int)
|
|
for _, observationID := range candidates {
|
|
observation := model.observations[observationID]
|
|
if observation.role == historyRoleInventory {
|
|
continue
|
|
}
|
|
reportedPath := observation.reportedPath
|
|
attributionRef := observation.attributionRef
|
|
if observation.role == historyRoleContribution {
|
|
if !historyObservationMatchesQuery(path, observation.path) {
|
|
continue
|
|
}
|
|
application, found := model.applicationPayload(observation.payload)
|
|
if !found {
|
|
continue
|
|
}
|
|
if pathContains(observation.path, path) && len(observation.path.segments) < len(path.segments) {
|
|
local, ok := historyApplicationLocalPath(application, path)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if _, present := documentAtPathWithLookup(application.value, local, model.documentLookup); !present {
|
|
continue
|
|
}
|
|
}
|
|
if operationPath, relevant := applicationOperationPath(application, path, model.documentLookup); relevant {
|
|
reportedPath = operationPath
|
|
}
|
|
} else if !historyObservationMatchesQuery(path, observation.path) {
|
|
continue
|
|
}
|
|
selectionIndex, found := selectionByPayload[observation.payload]
|
|
if !found {
|
|
selectionIndex = len(selections)
|
|
selectionByPayload[observation.payload] = selectionIndex
|
|
selection := historySelection{
|
|
path: path.clone(),
|
|
phase: observation.phase,
|
|
payload: observation.payload,
|
|
reportedPath: reportedPath.clone(),
|
|
role: observation.role,
|
|
}
|
|
if attribution, ok := model.resolveAttribution(attributionRef); ok {
|
|
selection.attribution = attribution
|
|
selection.hasAttribution = true
|
|
}
|
|
selections = append(selections, selection)
|
|
}
|
|
selection := &selections[selectionIndex]
|
|
selection.before = selection.before || observation.beforePresent
|
|
selection.after = selection.after || observation.afterPresent
|
|
}
|
|
if len(selections) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var previous *Origin
|
|
for index := range selections {
|
|
selection := &selections[index]
|
|
if selection.hasAttribution && metadataHistoryRole(selection.role) {
|
|
if previous != nil {
|
|
selection.previous = []Origin{cloneOrigin(*previous)}
|
|
}
|
|
origin := cloneOrigin(selection.attribution.origin)
|
|
previous = &origin
|
|
}
|
|
}
|
|
return selections
|
|
}
|
|
|
|
func historyObservationMatchesQuery(query, observed Path) bool {
|
|
if matchesPathShape(query, observed) {
|
|
return true
|
|
}
|
|
return pathContains(query, observed) && pathShapePrefixMatches(query, observed)
|
|
}
|
|
|
|
func (model *historyModel) hasObservedShape(path Path) bool {
|
|
if model == nil || !path.valid() {
|
|
return false
|
|
}
|
|
for _, candidate := range model.shapesByPointer[path.String()] {
|
|
if matchesPathShape(path, candidate) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (model *historyModel) concreteShapes(pointer string) []Path {
|
|
if model == nil {
|
|
return nil
|
|
}
|
|
shapes := model.shapesByPointer[pointer]
|
|
cloned := make([]Path, len(shapes))
|
|
for index, path := range shapes {
|
|
cloned[index] = path.clone()
|
|
}
|
|
return cloned
|
|
}
|
|
|
|
func (model *historyModel) observedPaths() []Path {
|
|
if model == nil {
|
|
return nil
|
|
}
|
|
paths := make([]Path, len(model.shapeOrder))
|
|
for index, path := range model.shapeOrder {
|
|
paths[index] = path.clone()
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (model *historyModel) historicalPaths() []Path {
|
|
if model == nil {
|
|
return []Path{}
|
|
}
|
|
pointers := make([]string, 0, len(model.shapesByPointer))
|
|
for pointer := range model.shapesByPointer {
|
|
pointers = append(pointers, pointer)
|
|
}
|
|
sort.Strings(pointers)
|
|
paths := make([]Path, 0, len(pointers))
|
|
for _, pointer := range pointers {
|
|
path, err := ParsePath(pointer)
|
|
if err == nil {
|
|
paths = append(paths, path)
|
|
}
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func (model *historyModel) latestHistoricalLocation(path Path) (Location, bool) {
|
|
selections := model.selectHistory(path)
|
|
for selectionIndex := len(selections) - 1; selectionIndex >= 0; selectionIndex-- {
|
|
selection := selections[selectionIndex]
|
|
if selection.hasAttribution {
|
|
if location, found := latestOriginDocumentLocation(selection.attribution.origin); found {
|
|
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true
|
|
}
|
|
}
|
|
for previousIndex := len(selection.previous) - 1; previousIndex >= 0; previousIndex-- {
|
|
if location, found := latestOriginDocumentLocation(selection.previous[previousIndex]); found {
|
|
return Location{SourceID: location.SourceID, Line: location.Line, Column: location.Column}, true
|
|
}
|
|
}
|
|
}
|
|
return Location{}, false
|
|
}
|
|
|
|
func (model *historyModel) sensitivityCandidates() []historySensitivityCandidate {
|
|
if model == nil {
|
|
return nil
|
|
}
|
|
candidates := make([]historySensitivityCandidate, 0, len(model.observations))
|
|
indices := make(map[string][]int)
|
|
add := func(path Path, sensitive, classify bool) int {
|
|
if !path.valid() {
|
|
return -1
|
|
}
|
|
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
|
|
}
|
|
attribution := model.attributions[ref]
|
|
return historyAttribution{
|
|
origin: cloneOrigin(attribution.origin),
|
|
reasons: append([]string(nil), attribution.reasons...),
|
|
dependencies: clonePaths(attribution.dependencies),
|
|
}, true
|
|
}
|
|
|
|
func (model *historyModel) applicationPayload(ref historyPayloadRef) (historyApplication, bool) {
|
|
if model == nil || ref.id < 0 {
|
|
return historyApplication{}, false
|
|
}
|
|
var payloads []historySourcePayload
|
|
switch ref.phase {
|
|
case historyPhaseSources:
|
|
payloads = model.sourcePayloads
|
|
case historyPhaseCLI:
|
|
payloads = model.cliPayloads
|
|
default:
|
|
return historyApplication{}, false
|
|
}
|
|
if ref.id >= len(payloads) || payloads[ref.id].kind != historySourceApplicationPayload {
|
|
return historyApplication{}, false
|
|
}
|
|
return payloads[ref.id].application, true
|
|
}
|
|
|
|
func pathShapePrefixMatches(left, right Path) bool {
|
|
length := min(len(left.segments), len(right.segments))
|
|
leftPrefix := Path{segments: clonePathSegments(left.segments[:length], 0)}
|
|
rightPrefix := Path{segments: clonePathSegments(right.segments[:length], 0)}
|
|
return matchesPathShape(leftPrefix, rightPrefix)
|
|
}
|
|
|
|
func metadataHistoryRole(role historyRole) bool {
|
|
switch role {
|
|
case historyRoleDefinition, historyRoleCompositeDefinition, historyRoleOperation, historyRoleControl:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|