Project consumed version evidence through sequence appends
This commit is contained in:
@@ -13,7 +13,13 @@ stricter final identity validation, broader version policy, or new release gates
|
|||||||
|
|
||||||
## 1. Project consumed version evidence through authored sequence appends — P1
|
## 1. Project consumed version evidence through authored sequence appends — P1
|
||||||
|
|
||||||
- [ ] Addressed and regression-tested.
|
- [x] Addressed and regression-tested.
|
||||||
|
|
||||||
|
Completion: retained sensitivity paths now project declarations and ancestors
|
||||||
|
through assigned authored-list ranges without changing source-local evidence.
|
||||||
|
Public regressions cover imports and grouped underlays, leaf/container masking,
|
||||||
|
and unclassified declarations. Focused source-version tests, the everyday check,
|
||||||
|
and `go test -race ./internal/engine` passed.
|
||||||
|
|
||||||
**Owners:** `projectAcquiredDestinations` / `projectSourceVersionEvidence` in
|
**Owners:** `projectAcquiredDestinations` / `projectSourceVersionEvidence` in
|
||||||
[imports.go](../../internal/engine/imports.go), and evidence classification in
|
[imports.go](../../internal/engine/imports.go), and evidence classification in
|
||||||
|
|||||||
@@ -3,14 +3,66 @@ package configkit_test
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestFacadeSourceVersionEvidenceFollowsAuthoredSequenceRanges(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
for _, grouped := range []bool{false, true} {
|
||||||
|
for _, sensitivePath := range []string{"", "/records/1/version", "/records/1"} {
|
||||||
|
t.Run(fmt.Sprintf("grouped=%t/secret=%s", grouped, sensitivePath), func(t *testing.T) {
|
||||||
|
definition := configkit.Definition[map[string]any]{
|
||||||
|
SequenceCompositions: []configkit.SequenceComposition{{Path: configkit.Keys("records"), IdentityField: "id"}},
|
||||||
|
Version: &configkit.VersionRule{Path: configkit.Keys("records").Index(0).Key("version"), Check: func(configkit.Version, *configkit.Validation) error { return nil }},
|
||||||
|
Secret: func(path configkit.Path) bool { return sensitivePath != "" && path.String() == sensitivePath },
|
||||||
|
}
|
||||||
|
first := "records: [{id: first, version: first-version}]\n"
|
||||||
|
child := "records: [{id: child, version: PRIVATE-VERSION-CANARY}]\n"
|
||||||
|
var result *configkit.Result[map[string]any]
|
||||||
|
var err error
|
||||||
|
if grouped {
|
||||||
|
result, err = configkit.LoadLayered(configkit.Input{Source: configkit.Bytes("main", []byte(first))}, configkit.LayeredOptions[map[string]any]{
|
||||||
|
Definition: definition, SourceVersionMode: configkit.SourceVersionModeRequired,
|
||||||
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{
|
||||||
|
{Source: configkit.Bytes("first", []byte(first))}, {Source: configkit.Bytes("child", []byte(child))},
|
||||||
|
}}},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
filesystem := configkit.NewFilesystem("versions", fstest.MapFS{
|
||||||
|
"main.yml": {Data: []byte("configkit: {imports: [{path: child.yml}]}\n" + first)},
|
||||||
|
"child.yml": {Data: []byte(child)},
|
||||||
|
})
|
||||||
|
result, err = configkit.Load(filesystem.File("main.yml"), configkit.Options[map[string]any]{
|
||||||
|
Definition: definition, Imports: true, SourceVersionMode: configkit.SourceVersionModeRequired,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sources := result.Sources()
|
||||||
|
check := sources[len(sources)-1].VersionCheck
|
||||||
|
if check == nil || check.Path.String() != "/records/0/version" || check.Location == nil || check.Location.SourceID != sources[len(sources)-1].ID {
|
||||||
|
t.Fatalf("source-local evidence changed: %#v", check)
|
||||||
|
}
|
||||||
|
if sensitivePath != "" {
|
||||||
|
if !check.Redacted || check.Version.Text != "" {
|
||||||
|
t.Fatal("shifted version evidence was disclosed")
|
||||||
|
}
|
||||||
|
} else if check.Redacted || check.Version.Text != "PRIVATE-VERSION-CANARY" {
|
||||||
|
t.Fatal("unclassified version evidence was lost")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type sourceVersionModeReader struct {
|
type sourceVersionModeReader struct {
|
||||||
*strings.Reader
|
*strings.Reader
|
||||||
reads int
|
reads int
|
||||||
|
|||||||
@@ -662,7 +662,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
}
|
}
|
||||||
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
||||||
contributionInsideItem = true
|
contributionInsideItem = true
|
||||||
projectSourceVersionEvidence(contribution.processing, contributionPrefix)
|
projectSourceVersionEvidence(contribution.processing, contributionPrefix, nil, sourceIndex)
|
||||||
} else if contributionInsideItem {
|
} else if contributionInsideItem {
|
||||||
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
||||||
contribution.source.Destination = exactDestination.clone()
|
contribution.source.Destination = exactDestination.clone()
|
||||||
@@ -672,7 +672,9 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
sources[position].Destination = exactDestination
|
sources[position].Destination = exactDestination
|
||||||
sources[position].destinationUnresolved = false
|
sources[position].destinationUnresolved = false
|
||||||
}
|
}
|
||||||
projectSourceVersionEvidence(contribution.processing, exactDestination)
|
projectSourceVersionEvidence(contribution.processing, exactDestination, nil, sourceIndex)
|
||||||
|
} else {
|
||||||
|
projectSourceVersionEvidence(contribution.processing, contribution.destination, currentComposition.sequenceRanges, sourceIndex)
|
||||||
}
|
}
|
||||||
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
||||||
for _, nestedScope := range contribution.nested.acquisition {
|
for _, nestedScope := range contribution.nested.acquisition {
|
||||||
@@ -684,9 +686,28 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
projectScope(composed, scope, Path{}, false, "")
|
projectScope(composed, scope, Path{}, false, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path) {
|
func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path, ranges []sequenceRange, sourceIndex int) {
|
||||||
if processing != nil && processing.evidence != nil {
|
if processing != nil && processing.evidence != nil {
|
||||||
processing.evidence.destination = destination.clone()
|
evidence := processing.evidence
|
||||||
|
evidence.destination = destination.clone()
|
||||||
|
// Rebuild from source-local paths on each handoff: an input can be
|
||||||
|
// composed first on its own and then at a different offset in its tier.
|
||||||
|
evidence.projectedPaths = nil
|
||||||
|
paths := evidence.sensitivityPaths()
|
||||||
|
for index, path := range paths {
|
||||||
|
for _, assigned := range ranges {
|
||||||
|
if assigned.sourceIndex != sourceIndex || !pathContains(assigned.path, path) || len(path.segments) <= len(assigned.path.segments) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
position := len(assigned.path.segments)
|
||||||
|
item, ok := schemaIndex(path.segments[position])
|
||||||
|
if ok && path.segments[position].kind != pathSegmentKey {
|
||||||
|
path.segments[position] = pathSegment{kind: pathSegmentIndex, index: assigned.start + item}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
paths[index] = path
|
||||||
|
}
|
||||||
|
evidence.projectedPaths = paths
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,15 +125,8 @@ func classifySecretPathsWithEvidence(schema *schemaDescriptor, predicate func(Pa
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
candidate := item.resolvedPath
|
for _, resolved := range item.sensitivityPaths() {
|
||||||
if !candidate.valid() {
|
addSecretPath(paths, resolved.String())
|
||||||
candidate = item.path
|
|
||||||
}
|
|
||||||
resolved := addSecretEvidencePath(paths, item.destination, candidate)
|
|
||||||
addCandidate(resolved)
|
|
||||||
classifyPath(resolved)
|
|
||||||
for _, ancestor := range item.ancestors {
|
|
||||||
resolved := addSecretEvidencePath(paths, item.destination, ancestor.path)
|
|
||||||
addCandidate(resolved)
|
addCandidate(resolved)
|
||||||
classifyPath(resolved)
|
classifyPath(resolved)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,16 +30,17 @@ type sourceVersionAncestor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type sourceVersionEvidence struct {
|
type sourceVersionEvidence struct {
|
||||||
sourceID string
|
sourceID string
|
||||||
path Path
|
path Path
|
||||||
resolvedPath Path
|
resolvedPath Path
|
||||||
destination Path
|
destination Path
|
||||||
version Version
|
projectedPaths []Path
|
||||||
location *Location
|
version Version
|
||||||
ancestors []sourceVersionAncestor
|
location *Location
|
||||||
absence sourceVersionPathAbsence
|
ancestors []sourceVersionAncestor
|
||||||
checked bool
|
absence sourceVersionPathAbsence
|
||||||
redacted bool
|
checked bool
|
||||||
|
redacted bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sourceVersionState struct {
|
type sourceVersionState struct {
|
||||||
@@ -80,7 +81,7 @@ func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity
|
|||||||
redacted := evidence.redacted
|
redacted := evidence.redacted
|
||||||
if conservative {
|
if conservative {
|
||||||
redacted = redacted || version.Text != ""
|
redacted = redacted || version.Text != ""
|
||||||
} else if sensitivity.isSensitiveAtOrAbove(appendDestination(evidence.destination, path).String()) {
|
} else if sensitivity.isSensitiveAtOrAbove(evidence.sensitivityPaths()[0].String()) {
|
||||||
redacted = true
|
redacted = true
|
||||||
}
|
}
|
||||||
if redacted {
|
if redacted {
|
||||||
@@ -91,6 +92,24 @@ func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sensitivityPaths preserves source-local evidence while describing its assigned
|
||||||
|
// application coordinates. Before assembly, the original mount is the fallback.
|
||||||
|
func (evidence *sourceVersionEvidence) sensitivityPaths() []Path {
|
||||||
|
if evidence.projectedPaths != nil {
|
||||||
|
return evidence.projectedPaths
|
||||||
|
}
|
||||||
|
local := evidence.resolvedPath
|
||||||
|
if !local.valid() {
|
||||||
|
local = evidence.path
|
||||||
|
}
|
||||||
|
paths := make([]Path, 0, len(evidence.ancestors)+1)
|
||||||
|
paths = append(paths, appendDestination(evidence.destination, local))
|
||||||
|
for _, ancestor := range evidence.ancestors {
|
||||||
|
paths = append(paths, appendDestination(evidence.destination, ancestor.path))
|
||||||
|
}
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) {
|
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) {
|
||||||
if state == nil || state.mode == sourceVersionModeDefault {
|
if state == nil || state.mode == sourceVersionModeDefault {
|
||||||
return sourceVersionProcessingResult{}, nil
|
return sourceVersionProcessingResult{}, nil
|
||||||
|
|||||||
Reference in New Issue
Block a user