132 lines
6.0 KiB
Go
132 lines
6.0 KiB
Go
package artifacts
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
|
)
|
|
|
|
type ExtractionEvidenceState string
|
|
|
|
const (
|
|
ExtractionEvidenceValid ExtractionEvidenceState = "valid"
|
|
ExtractionEvidenceAbsent ExtractionEvidenceState = "absent"
|
|
ExtractionEvidenceObsolete ExtractionEvidenceState = "obsolete"
|
|
ExtractionEvidenceUnsafe ExtractionEvidenceState = "unsafe"
|
|
)
|
|
|
|
// ExtractionEvidence is a policy-neutral proof of configured extraction output.
|
|
type ExtractionEvidence struct {
|
|
State ExtractionEvidenceState
|
|
Reason, ProducerRunID string
|
|
Outputs map[string]string
|
|
}
|
|
|
|
// InspectExtractionEvidence verifies structure, confinement, identity, contracts, and payload bytes.
|
|
func InspectExtractionEvidence(paths SessionPaths, m *manifest.Manifest, configured map[string]ExtractionArtifactDefinition) ExtractionEvidence {
|
|
if m == nil || len(configured) == 0 {
|
|
return ExtractionEvidence{State: ExtractionEvidenceAbsent, Reason: "extraction evidence is absent"}
|
|
}
|
|
r := m.Stages[extractStageName]
|
|
if r == nil || r.Name != extractStageName || r.Status != manifest.StatusSucceeded {
|
|
return ExtractionEvidence{State: ExtractionEvidenceAbsent, Reason: "extract stage has no succeeded result"}
|
|
}
|
|
runID := extractionMetadataString(r.Metadata, extractionMetadataRun)
|
|
if !safeExtractionPathSegment(runID) {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result has an invalid producing run ID"}
|
|
}
|
|
root := filepath.Clean(filepath.Join(paths.ArtifactsDir, "notarius", runID))
|
|
if !filepath.IsAbs(root) || extractionMetadataString(r.Metadata, extractionMetadataRoot) != root {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result does not identify its canonical immutable bundle"}
|
|
}
|
|
info, err := os.Lstat(root)
|
|
if os.IsNotExist(err) {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "immutable Notarius bundle is missing"}
|
|
}
|
|
if err != nil || info.Mode()&os.ModeSymlink != 0 {
|
|
return ExtractionEvidence{State: ExtractionEvidenceUnsafe, Reason: "immutable Notarius bundle is unsafe"}
|
|
}
|
|
if !info.IsDir() {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "immutable Notarius bundle is not a directory"}
|
|
}
|
|
if !safeExistingExtractionDirectory(paths.Root, root) {
|
|
return ExtractionEvidence{State: ExtractionEvidenceUnsafe, Reason: "immutable Notarius bundle is unsafe"}
|
|
}
|
|
receiptRunID, receiptPipelineID := extractionReceiptIdentity(r.Metadata)
|
|
if receiptRunID == "" || receiptPipelineID == "" {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result has incompatible Notarius receipt identity"}
|
|
}
|
|
expected := make(map[string]ExtractionArtifactDefinition, len(configured))
|
|
for key, d := range configured {
|
|
expected[ExtractionArtifactSourceID(key)] = d
|
|
}
|
|
seen, outputs := map[string]struct{}{}, map[string]string{}
|
|
indexSeen := false
|
|
for _, out := range r.Outputs {
|
|
if strings.TrimSpace(out.ProducerRunID) != runID {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract output producer identity is inconsistent"}
|
|
}
|
|
if out.SourceID == "" {
|
|
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 {
|
|
return ExtractionEvidence{State: state, Reason: reason}
|
|
}
|
|
indexSeen = true
|
|
continue
|
|
}
|
|
d, ok := expected[out.SourceID]
|
|
if !ok || out.Kind != extractionLaneKind {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result source set differs from current configuration"}
|
|
}
|
|
if _, duplicate := seen[out.SourceID]; duplicate {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result contains a duplicate configured source"}
|
|
}
|
|
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 {
|
|
return ExtractionEvidence{State: state, Reason: reason}
|
|
}
|
|
seen[out.SourceID] = struct{}{}
|
|
outputs[out.SourceID] = out.LocalPath
|
|
}
|
|
if !indexSeen || len(seen) != len(expected) || len(r.Outputs) != len(expected)+1 {
|
|
return ExtractionEvidence{State: ExtractionEvidenceObsolete, Reason: "extract result is incomplete"}
|
|
}
|
|
return ExtractionEvidence{State: ExtractionEvidenceValid, ProducerRunID: runID, Outputs: outputs}
|
|
}
|
|
|
|
func inspectExtractionPayload(root, path, checksum string) (ExtractionEvidenceState, string) {
|
|
if !filepath.IsAbs(path) || !pathWithinExtractionRoot(root, path) || strings.TrimSpace(checksum) == "" {
|
|
return ExtractionEvidenceUnsafe, "extract output path or checksum is unsafe"
|
|
}
|
|
info, err := os.Lstat(path)
|
|
if os.IsNotExist(err) {
|
|
return ExtractionEvidenceObsolete, "extract output is missing"
|
|
}
|
|
if !safeExtractionComponents(root, path) {
|
|
return ExtractionEvidenceUnsafe, "extract output path contains unsafe components"
|
|
}
|
|
if err != nil || !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
|
return ExtractionEvidenceUnsafe, "extract output is not a regular file"
|
|
}
|
|
actual, err := SHA256File(path)
|
|
if err != nil {
|
|
return ExtractionEvidenceUnsafe, "extract output checksum cannot be read"
|
|
}
|
|
if actual != checksum {
|
|
return ExtractionEvidenceObsolete, "extract output checksum does not match durable bytes"
|
|
}
|
|
body, err := fileops.ReadRegularFile(path, MaxExtractionPayloadBytes)
|
|
if err != nil || !json.Valid(body) {
|
|
return ExtractionEvidenceObsolete, "extract output is not valid JSON"
|
|
}
|
|
return ExtractionEvidenceValid, ""
|
|
}
|