1616package action
1717
1818import (
19+ "bytes"
1920 "context"
21+ "crypto/sha256"
2022 "encoding/json"
2123 "fmt"
2224 "os"
@@ -25,9 +27,13 @@ import (
2527 pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2628 "github.com/chainloop-dev/chainloop/pkg/attestation"
2729 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
30+ v1 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2831 "github.com/chainloop-dev/chainloop/pkg/attestation/renderer"
32+ crChainloop "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
2933 "github.com/chainloop-dev/chainloop/pkg/attestation/signer"
34+ "github.com/chainloop-dev/chainloop/pkg/casclient"
3035 "github.com/chainloop-dev/chainloop/pkg/policies"
36+ intoto "github.com/in-toto/attestation/go/v1"
3137 "github.com/secure-systems-lab/go-securesystemslib/dsse"
3238 protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
3339 "google.golang.org/grpc"
@@ -38,9 +44,11 @@ import (
3844type AttestationPushOpts struct {
3945 * ActionsOpts
4046 KeyPath , CLIVersion , CLIDigest , BundlePath string
41-
42- LocalStatePath string
43- SignServerOpts * SignServerOpts
47+ CASURI string
48+ CASCAPath string
49+ ConnectionInsecure bool
50+ LocalStatePath string
51+ SignServerOpts * SignServerOpts
4452}
4553
4654// SignServerOpts holds SignServer integration options
@@ -60,6 +68,9 @@ type AttestationResult struct {
6068type AttestationPush struct {
6169 * ActionsOpts
6270 keyPath , cliVersion , cliDigest , bundlePath string
71+ casURI string
72+ casCAPath string
73+ connectionInsecure bool
6374 localStatePath string
6475 signServerOpts * SignServerOpts
6576 * newCrafterOpts
@@ -68,14 +79,17 @@ type AttestationPush struct {
6879func NewAttestationPush (cfg * AttestationPushOpts ) (* AttestationPush , error ) {
6980 opts := []crafter.NewOpt {crafter .WithLogger (& cfg .Logger ), crafter .WithAuthRawToken (cfg .AuthTokenRaw )}
7081 return & AttestationPush {
71- ActionsOpts : cfg .ActionsOpts ,
72- keyPath : cfg .KeyPath ,
73- cliVersion : cfg .CLIVersion ,
74- cliDigest : cfg .CLIDigest ,
75- bundlePath : cfg .BundlePath ,
76- signServerOpts : cfg .SignServerOpts ,
77- localStatePath : cfg .LocalStatePath ,
78- newCrafterOpts : & newCrafterOpts {cpConnection : cfg .CPConnection , opts : opts },
82+ ActionsOpts : cfg .ActionsOpts ,
83+ keyPath : cfg .KeyPath ,
84+ cliVersion : cfg .CLIVersion ,
85+ cliDigest : cfg .CLIDigest ,
86+ bundlePath : cfg .BundlePath ,
87+ casURI : cfg .CASURI ,
88+ casCAPath : cfg .CASCAPath ,
89+ connectionInsecure : cfg .ConnectionInsecure ,
90+ signServerOpts : cfg .SignServerOpts ,
91+ localStatePath : cfg .LocalStatePath ,
92+ newCrafterOpts : & newCrafterOpts {cpConnection : cfg .CPConnection , opts : opts },
7993 }, nil
8094}
8195
@@ -205,6 +219,29 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
205219 // Update the status result with the definitive push-phase evaluation against the final statement
206220 attestationStatus .PolicyEvaluations , attestationStatus .HasPolicyViolations = getPolicyEvaluations (crafter )
207221
222+ // Upload policy evaluations bundle to CAS when an external backend is available
223+ if evaluations := crafter .CraftingState .GetAttestation ().GetPolicyEvaluations (); ! crafter .CraftingState .DryRun && len (evaluations ) > 0 {
224+ casBackend := & casclient.CASBackend {Name : "not-set" }
225+ workflowRunID := crafter .CraftingState .GetAttestation ().GetWorkflow ().GetWorkflowRunId ()
226+ _ , connectionCloserFn , getCASErr := getCASBackend (ctx , attClient , workflowRunID , action .casCAPath , action .casURI , action .connectionInsecure , action .Logger , casBackend )
227+ if connectionCloserFn != nil {
228+ // nolint: errcheck
229+ defer connectionCloserFn ()
230+ }
231+
232+ if getCASErr != nil || casBackend .Uploader == nil {
233+ action .Logger .Debug ().Msg ("CAS backend is inline, skipping policy evaluations bundle upload" )
234+ } else {
235+ ref , uploadErr := uploadPolicyEvaluationsBundle (ctx , evaluations , casBackend .Uploader )
236+ if uploadErr != nil {
237+ return nil , fmt .Errorf ("uploading policy evaluations bundle to CAS: %w" , uploadErr )
238+ }
239+ if ref != nil {
240+ renderer .SetPolicyEvaluationsRef (ref )
241+ }
242+ }
243+ }
244+
208245 // render final attestation with all the evaluated policies inside
209246 envelope , bundle , err := renderer .Render (ctx )
210247 if err != nil {
@@ -318,3 +355,31 @@ func decodeEnvelope(rawEnvelope []byte) (*dsse.Envelope, error) {
318355
319356 return envelope , nil
320357}
358+
359+ // uploadPolicyEvaluationsBundle serializes policy evaluations as a protobuf bundle,
360+ // uploads to CAS, and returns a ResourceDescriptor referencing the uploaded object.
361+ // Returns (nil, nil) when there are no evaluations or no uploader.
362+ func uploadPolicyEvaluationsBundle (ctx context.Context , evaluations []* v1.PolicyEvaluation , uploader casclient.Uploader ) (* intoto.ResourceDescriptor , error ) {
363+ if len (evaluations ) == 0 || uploader == nil {
364+ return nil , nil
365+ }
366+
367+ bundle := & v1.PolicyEvaluationBundle {Evaluations : evaluations }
368+ data , err := protojson .Marshal (bundle )
369+ if err != nil {
370+ return nil , fmt .Errorf ("marshaling policy evaluation bundle: %w" , err )
371+ }
372+
373+ hexDigest := fmt .Sprintf ("%x" , sha256 .Sum256 (data ))
374+ digest := fmt .Sprintf ("sha256:%s" , hexDigest )
375+
376+ if _ , err := uploader .Upload (ctx , bytes .NewReader (data ), "policy-evaluations.json" , digest ); err != nil {
377+ return nil , fmt .Errorf ("uploading policy evaluation bundle: %w" , err )
378+ }
379+
380+ return & intoto.ResourceDescriptor {
381+ Name : "policy-evaluations" ,
382+ Digest : map [string ]string {"sha256" : hexDigest },
383+ MediaType : crChainloop .PolicyEvaluationsBundleMediaType ,
384+ }, nil
385+ }
0 commit comments