Add deterministic analysis input identities
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactmodel"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||
)
|
||||
@@ -23,7 +24,15 @@ const (
|
||||
type ExtractionEvidence struct {
|
||||
State ExtractionEvidenceState
|
||||
Reason, ProducerRunID string
|
||||
Outputs map[string]string
|
||||
Outputs map[string]ExtractionEvidenceOutput
|
||||
}
|
||||
|
||||
// ExtractionEvidenceOutput is the verified semantic identity of one lane.
|
||||
type ExtractionEvidenceOutput struct {
|
||||
Path string
|
||||
Checksum string
|
||||
Size int64
|
||||
Contract *artifactmodel.ContractMetadata
|
||||
}
|
||||
|
||||
// InspectExtractionEvidence verifies structure, confinement, identities, contracts, and payload bytes.
|
||||
@@ -72,7 +81,7 @@ func InspectExtractionEvidence(
|
||||
for key, d := range configured {
|
||||
expected[ExtractionArtifactSourceID(key)] = d
|
||||
}
|
||||
seen, outputs := map[string]struct{}{}, map[string]string{}
|
||||
seen, outputs := map[string]struct{}{}, map[string]ExtractionEvidenceOutput{}
|
||||
indexSeen := false
|
||||
for _, out := range r.Outputs {
|
||||
if strings.TrimSpace(out.ProducerRunID) != runID {
|
||||
@@ -82,7 +91,7 @@ func InspectExtractionEvidence(
|
||||
if indexSeen || out.Kind != extractionIndexKind || filepath.Clean(out.LocalPath) != filepath.Join(root, "index.json") {
|
||||
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract index path is not canonical"}
|
||||
}
|
||||
if state, reason := inspectExtractionPayload(root, out.LocalPath, out.Checksum); state != ExtractionEvidenceValid {
|
||||
if state, reason, _ := inspectExtractionPayload(root, out.LocalPath, out.Checksum); state != ExtractionEvidenceValid {
|
||||
return ExtractionEvidence{State: state, Reason: reason}
|
||||
}
|
||||
indexSeen = true
|
||||
@@ -98,11 +107,15 @@ func InspectExtractionEvidence(
|
||||
if !compatibleCatalogExtractionContract(out.Contract, d) || !compatibleCatalogExtractionProvenance(out.ExternalProvenance, receiptRunID, receiptPipelineID, d) {
|
||||
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract output contract or provenance is incompatible"}
|
||||
}
|
||||
if state, reason := inspectExtractionPayload(root, out.LocalPath, out.Checksum); state != ExtractionEvidenceValid {
|
||||
state, reason, size := inspectExtractionPayload(root, out.LocalPath, out.Checksum)
|
||||
if state != ExtractionEvidenceValid {
|
||||
return ExtractionEvidence{State: state, Reason: reason}
|
||||
}
|
||||
seen[out.SourceID] = struct{}{}
|
||||
outputs[out.SourceID] = out.LocalPath
|
||||
outputs[out.SourceID] = ExtractionEvidenceOutput{
|
||||
Path: out.LocalPath, Checksum: out.Checksum, Size: size,
|
||||
Contract: cloneArtifactContract(out.Contract),
|
||||
}
|
||||
}
|
||||
if !indexSeen || len(seen) != len(expected) || len(r.Outputs) != len(expected)+1 {
|
||||
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result is incomplete"}
|
||||
@@ -110,30 +123,30 @@ func InspectExtractionEvidence(
|
||||
return ExtractionEvidence{State: ExtractionEvidenceValid, ProducerRunID: runID, Outputs: outputs}
|
||||
}
|
||||
|
||||
func inspectExtractionPayload(root, path, checksum string) (ExtractionEvidenceState, string) {
|
||||
func inspectExtractionPayload(root, path, checksum string) (ExtractionEvidenceState, string, int64) {
|
||||
if !filepath.IsAbs(path) || !pathWithinExtractionRoot(root, path) || strings.TrimSpace(checksum) == "" {
|
||||
return ExtractionEvidenceUnsafe, "extract output path or checksum is unsafe"
|
||||
return ExtractionEvidenceUnsafe, "extract output path or checksum is unsafe", 0
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return ExtractionEvidenceObsolete, "extract output is missing"
|
||||
return ExtractionEvidenceObsolete, "extract output is missing", 0
|
||||
}
|
||||
if !safeExtractionComponents(root, path) {
|
||||
return ExtractionEvidenceUnsafe, "extract output path contains unsafe components"
|
||||
return ExtractionEvidenceUnsafe, "extract output path contains unsafe components", 0
|
||||
}
|
||||
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return ExtractionEvidenceUnsafe, "extract output is not a regular file"
|
||||
return ExtractionEvidenceUnsafe, "extract output is not a regular file", 0
|
||||
}
|
||||
actual, err := SHA256File(path)
|
||||
if err != nil {
|
||||
return ExtractionEvidenceUnsafe, "extract output checksum cannot be read"
|
||||
return ExtractionEvidenceUnsafe, "extract output checksum cannot be read", 0
|
||||
}
|
||||
if actual != checksum {
|
||||
return ExtractionEvidenceObsolete, "extract output checksum does not match durable bytes"
|
||||
return ExtractionEvidenceObsolete, "extract output checksum does not match durable bytes", 0
|
||||
}
|
||||
body, err := fileops.ReadRegularFile(path, MaxExtractionPayloadBytes)
|
||||
if err != nil || !json.Valid(body) {
|
||||
return ExtractionEvidenceObsolete, "extract output is not valid JSON"
|
||||
return ExtractionEvidenceObsolete, "extract output is not valid JSON", 0
|
||||
}
|
||||
return ExtractionEvidenceValid, ""
|
||||
return ExtractionEvidenceValid, "", info.Size()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user