diff --git a/docs/scope-d-delivery-envelopes.md b/docs/scope-d-delivery-envelopes.md new file mode 100644 index 0000000..74be752 --- /dev/null +++ b/docs/scope-d-delivery-envelopes.md @@ -0,0 +1,96 @@ +# SCOPE-D Delivery Envelopes + +CloudShell Fog participates in the SCOPE-D platform as an authorized edge operator bastion and mesh assurance node. + +This document defines the first delivery boundary between SCOPE-D and CloudShell Fog. + +## Role + +CloudShell Fog is not a scanner and not an autonomous execution agent in this flow. It accepts reviewable, policy-gated SCOPE-D delivery envelopes for edge assurance workflows. + +The initial envelope validator accepts only non-executing review packages. + +## Accepted source + +`sourceSystem` must be: + +```text +scope-d +``` + +## Accepted purposes + +- `edge_assurance_review` +- `policy_gated_delivery_review` + +## Required fields + +A delivery envelope must include: + +- schema version; +- envelope id; +- source system; +- purpose; +- artifact references; +- required policy references; +- operator approval requirement; +- non-execution flags; +- receipt hash. + +## Prohibited capabilities in v0.1 + +The validator rejects any envelope that requests: + +- execution; +- prior execution; +- network access; +- mutation; +- credential access; +- payload delivery. + +## Expected SCOPE-D artifact references + +SCOPE-D should reference artifacts such as: + +- intelligence enrichment export; +- detection candidate export; +- cyber graph export; +- operator case bundle; +- client assurance report; +- PolicyFabric approval record. + +## Validation path + +The Go package is: + +```text +internal/delivery +``` + +The validator is: + +```go +Validate(envelope DeliveryEnvelope) error +``` + +The receipt helper is: + +```go +ComputeReceiptHash(envelope DeliveryEnvelope) string +``` + +## Test path + +```bash +go test ./internal/delivery +``` + +## Next slices + +1. Add API endpoint for staging delivery envelopes. +2. Persist staged envelopes in the existing session/audit subsystem. +3. Add PolicyFabric signature verification. +4. Add SCOPE-D receipt verification. +5. Add edge sync status for Noetica. +6. Add read-only operator review endpoint. +7. Add deployment receipt generation after approved future delivery modes. diff --git a/go.mod b/go.mod index 6f2befa..a66d1e9 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -45,6 +46,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect diff --git a/go.sum b/go.sum index 1784dab..010a54b 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -77,6 +79,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= diff --git a/internal/delivery/envelope.go b/internal/delivery/envelope.go new file mode 100644 index 0000000..68f03ca --- /dev/null +++ b/internal/delivery/envelope.go @@ -0,0 +1,97 @@ +// Package delivery validates policy-gated SCOPE-D delivery envelopes before +// they are accepted by CloudShell Fog as edge assurance work. +package delivery + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "strings" +) + +// DeliveryEnvelope describes a non-executing SCOPE-D work package that may be +// staged for operator review at a CloudShell Fog edge node. +type DeliveryEnvelope struct { + SchemaVersion string `json:"schemaVersion"` + EnvelopeID string `json:"envelopeId"` + SourceSystem string `json:"sourceSystem"` + Purpose string `json:"purpose"` + ArtifactRefs []string `json:"artifactRefs"` + RequiredPolicyRefs []string `json:"requiredPolicyRefs"` + OperatorApprovalRequired bool `json:"operatorApprovalRequired"` + ExecutionAllowed bool `json:"executionAllowed"` + ExecutionPerformed bool `json:"executionPerformed"` + NetworkAccessAllowed bool `json:"networkAccessAllowed"` + MutationAllowed bool `json:"mutationAllowed"` + CredentialAccessAllowed bool `json:"credentialAccessAllowed"` + PayloadDeliveryAllowed bool `json:"payloadDeliveryAllowed"` + ReceiptHash string `json:"receiptHash"` +} + +// Sentinel validation errors. +var ( + ErrUnsupportedSchema = errors.New("unsupported delivery envelope schema") + ErrInvalidEnvelopeID = errors.New("invalid delivery envelope id") + ErrInvalidSourceSystem = errors.New("invalid source system") + ErrInvalidPurpose = errors.New("invalid delivery purpose") + ErrMissingArtifacts = errors.New("missing artifact references") + ErrMissingPolicies = errors.New("missing required policy references") + ErrOperatorApprovalMissing = errors.New("operator approval requirement missing") + ErrUnsafeCapability = errors.New("unsafe delivery capability requested") + ErrInvalidReceiptHash = errors.New("invalid receipt hash") +) + +// Validate enforces CloudShell Fog's default posture for SCOPE-D delivery: +// reviewable, receipt-backed, policy-gated, and non-executing. +func Validate(envelope DeliveryEnvelope) error { + if envelope.SchemaVersion != "0.1.0" { + return ErrUnsupportedSchema + } + if !strings.HasPrefix(envelope.EnvelopeID, "cloudshell-fog-delivery:") { + return ErrInvalidEnvelopeID + } + if envelope.SourceSystem != "scope-d" { + return ErrInvalidSourceSystem + } + if envelope.Purpose != "edge_assurance_review" && envelope.Purpose != "policy_gated_delivery_review" { + return ErrInvalidPurpose + } + if len(envelope.ArtifactRefs) == 0 { + return ErrMissingArtifacts + } + if len(envelope.RequiredPolicyRefs) == 0 { + return ErrMissingPolicies + } + if !envelope.OperatorApprovalRequired { + return ErrOperatorApprovalMissing + } + if envelope.ExecutionAllowed || envelope.ExecutionPerformed || envelope.NetworkAccessAllowed || envelope.MutationAllowed || envelope.CredentialAccessAllowed || envelope.PayloadDeliveryAllowed { + return ErrUnsafeCapability + } + if !strings.HasPrefix(envelope.ReceiptHash, "sha256:") || len(strings.TrimPrefix(envelope.ReceiptHash, "sha256:")) != 64 { + return ErrInvalidReceiptHash + } + return nil +} + +// ComputeReceiptHash returns a deterministic hash over the policy-significant +// fields. It intentionally excludes ReceiptHash itself. +func ComputeReceiptHash(envelope DeliveryEnvelope) string { + material := strings.Join([]string{ + envelope.SchemaVersion, + envelope.EnvelopeID, + envelope.SourceSystem, + envelope.Purpose, + strings.Join(envelope.ArtifactRefs, ","), + strings.Join(envelope.RequiredPolicyRefs, ","), + fmt.Sprintf("approval=%t", envelope.OperatorApprovalRequired), + fmt.Sprintf("executionAllowed=%t", envelope.ExecutionAllowed), + fmt.Sprintf("networkAccessAllowed=%t", envelope.NetworkAccessAllowed), + fmt.Sprintf("mutationAllowed=%t", envelope.MutationAllowed), + fmt.Sprintf("credentialAccessAllowed=%t", envelope.CredentialAccessAllowed), + fmt.Sprintf("payloadDeliveryAllowed=%t", envelope.PayloadDeliveryAllowed), + }, "|") + sum := sha256.Sum256([]byte(material)) + return "sha256:" + hex.EncodeToString(sum[:]) +} diff --git a/internal/delivery/envelope_test.go b/internal/delivery/envelope_test.go new file mode 100644 index 0000000..7f0f49c --- /dev/null +++ b/internal/delivery/envelope_test.go @@ -0,0 +1,86 @@ +package delivery + +import ( + "errors" + "testing" +) + +func validEnvelope() DeliveryEnvelope { + envelope := DeliveryEnvelope{ + SchemaVersion: "0.1.0", + EnvelopeID: "cloudshell-fog-delivery:scope-d-demo-001", + SourceSystem: "scope-d", + Purpose: "edge_assurance_review", + ArtifactRefs: []string{"scope-d://cyber-graph-export/demo", "scope-d://detection-candidate-export/demo"}, + RequiredPolicyRefs: []string{"policyfabric://approval/operator-review-required"}, + OperatorApprovalRequired: true, + ExecutionAllowed: false, + ExecutionPerformed: false, + NetworkAccessAllowed: false, + MutationAllowed: false, + CredentialAccessAllowed: false, + PayloadDeliveryAllowed: false, + } + envelope.ReceiptHash = ComputeReceiptHash(envelope) + return envelope +} + +func TestValidateAcceptsSafeScopeDEnvelope(t *testing.T) { + if err := Validate(validEnvelope()); err != nil { + t.Fatalf("expected valid envelope, got %v", err) + } +} + +func TestValidateRejectsExecutionAllowed(t *testing.T) { + envelope := validEnvelope() + envelope.ExecutionAllowed = true + envelope.ReceiptHash = ComputeReceiptHash(envelope) + if err := Validate(envelope); !errors.Is(err, ErrUnsafeCapability) { + t.Fatalf("expected unsafe capability error, got %v", err) + } +} + +func TestValidateRejectsNetworkMutationCredentialAndPayloadCapabilities(t *testing.T) { + cases := map[string]func(*DeliveryEnvelope){ + "network": func(e *DeliveryEnvelope) { e.NetworkAccessAllowed = true }, + "mutation": func(e *DeliveryEnvelope) { e.MutationAllowed = true }, + "credential": func(e *DeliveryEnvelope) { e.CredentialAccessAllowed = true }, + "payload": func(e *DeliveryEnvelope) { e.PayloadDeliveryAllowed = true }, + } + for name, mutate := range cases { + t.Run(name, func(t *testing.T) { + envelope := validEnvelope() + mutate(&envelope) + envelope.ReceiptHash = ComputeReceiptHash(envelope) + if err := Validate(envelope); !errors.Is(err, ErrUnsafeCapability) { + t.Fatalf("expected unsafe capability error, got %v", err) + } + }) + } +} + +func TestValidateRequiresOperatorApproval(t *testing.T) { + envelope := validEnvelope() + envelope.OperatorApprovalRequired = false + envelope.ReceiptHash = ComputeReceiptHash(envelope) + if err := Validate(envelope); !errors.Is(err, ErrOperatorApprovalMissing) { + t.Fatalf("expected operator approval error, got %v", err) + } +} + +func TestValidateRequiresPolicyRefs(t *testing.T) { + envelope := validEnvelope() + envelope.RequiredPolicyRefs = nil + envelope.ReceiptHash = ComputeReceiptHash(envelope) + if err := Validate(envelope); !errors.Is(err, ErrMissingPolicies) { + t.Fatalf("expected missing policies error, got %v", err) + } +} + +func TestValidateRequiresReceiptHashShape(t *testing.T) { + envelope := validEnvelope() + envelope.ReceiptHash = "sha256:not-a-real-hash" + if err := Validate(envelope); !errors.Is(err, ErrInvalidReceiptHash) { + t.Fatalf("expected invalid receipt hash error, got %v", err) + } +}