Skip to content

feat: store policy evaluations in CAS during attestation push#2949

Merged
migmartri merged 10 commits into
chainloop-dev:mainfrom
migmartri:feat/policy-evaluations-cas-storage
Mar 28, 2026
Merged

feat: store policy evaluations in CAS during attestation push#2949
migmartri merged 10 commits into
chainloop-dev:mainfrom
migmartri:feat/policy-evaluations-cas-storage

Conversation

@migmartri

@migmartri migmartri commented Mar 28, 2026

Copy link
Copy Markdown
Member

Summary

  • Add PolicyEvaluationBundle protobuf message that wraps a flat list of PolicyEvaluation for CAS storage
  • During attestation push, serialize the bundle, upload to CAS, and embed a policyEvaluationsRef ResourceDescriptor in the attestation predicate alongside the existing inline policyEvaluations field
  • CAS upload is best-effort and conditional on having an external CAS backend available; inline mode continues to work unchanged
{
  {
      "policyEvaluationsRef": {
         "digest": {
            "sha256": "ab823985f247b0983545864268dfabe035f4411030d7bb7cc212bf34afe08726"
         },
         "media_type": "application/vnd.chainloop.policy-evaluations.v1+json",
         "name": "policy-evaluations"
      },
      "policyHasViolations": true,
      "runnerEnvironment": "unknown",
      "runnerType": "RUNNER_TYPE_UNSPECIFIED",
      "signingCA": "fileCA"
   }
}
cat policy-evaluations.json | jq
{
  "evaluations": [
    {
      "name": "sbom-freshness",
      "materialName": "material-1774697312409081135",
      "sources": [
        "cGFja2FnZSBtYWluCgppbXBvcnQgcmVnby52MQoKcmVzdWx0IDo9IHsKCSJza2lwcGVkIjogc2tpcHBlZCwKCSJ2aW9sYXRpb25zIjogdmlvbGF0aW9ucywKCSJza2lwX3JlYXNvbiI6IHNraXBfcmVhc29uLAoJImlnbm9yZSI6IGlnbm9yZSwKfQoKZGVmYXVsdCBza2lwX3JlYXNvbiA6PSAiIgoKc2tpcF9yZWFzb24gOj0gbSBpZiB7Cglub3QgdmFsaWRfaW5wdXQKCW0gOj0gImludmFsaWQgaW5wdXQiCn0KCmRlZmF1bHQgc2tpcHBlZCA6PSB0cnVlCgpza2lwcGVkIDo9IGZhbHNlIGlmIHZhbGlkX2lucHV0CgpkZWZhdWx0IGlnbm9yZSA6PSBmYWxzZQoKIyBWYWxpZCBpZiBTQk9NX0NZQ0xPTkVEWF9KU09OIG1hdGVyaWFsIGlzIHByb3ZpZGVkCnZhbGlkX2lucHV0IGlmIHsKCWlucHV0LmNoYWlubG9vcF9tZXRhZGF0YS5hbm5vdGF0aW9uc1siY2hhaW5sb29wLm1hdGVyaWFsLnR5cGUiXSA9PSAiU0JPTV9DWUNMT05FRFhfSlNPTiIKfQoKIyBEZWZhdWx0IGZyZXNobmVzcyBsaW1pdCAoMzAgZGF5cykKZGVmYXVsdCBmcmVzaG5lc3NfZGF5cyA6PSAzMAoKIyBQb2xpY3kgaW5wdXRzIC0gY29udmVydCBzdHJpbmcgdG8gbnVtYmVyCmZyZXNobmVzc19kYXlzIDo9IHRvX251bWJlcihpbnB1dC5hcmdzLmZyZXNobmVzc19kYXlzKSBpZiBpbnB1dC5hcmdzLmZyZXNobmVzc19kYXlzCgojIFRpbWUgY2FsY3VsYXRpb25zCm5hbm9zZWNzX3Blcl9zZWNvbmQgOj0gKDEwMDAgKiAxMDAwKSAqIDEwMDAKbmFub3NlY3NfcGVyX2RheSA6PSAoKDI0ICogNjApICogNjApICogbmFub3NlY3NfcGVyX3NlY29uZAptYXhpbXVtX2FnZSA6PSBmcmVzaG5lc3NfZGF5cyAqIG5hbm9zZWNzX3Blcl9kYXkKCiMgU0JPTSBmcmVzaG5lc3MgdmFsaWRhdGlvbgp2aW9sYXRpb25zIGNvbnRhaW5zIG1zZyBpZiB7CglpbnB1dC5tZXRhZGF0YS50aW1lc3RhbXAKCXNib21fbnMgOj0gdGltZS5wYXJzZV9yZmMzMzM5X25zKGlucHV0Lm1ldGFkYXRhLnRpbWVzdGFtcCkKCWV4Y2VlZGluZyA6PSB0aW1lLm5vd19ucygpIC0gKHNib21fbnMgKyBtYXhpbXVtX2FnZSkKCWV4Y2VlZGluZyA+IDAKCW1zZyA6PSBzcHJpbnRmKCJTQk9NIGNyZWF0ZWQgYXQgJXMgaXMgdG9vIG9sZCAoYWdlIGxpbWl0OiAlZCBkYXlzKSIsIFtpbnB1dC5tZXRhZGF0YS50aW1lc3RhbXAsIGZyZXNobmVzc19kYXlzXSkKfQoKIyBNaXNzaW5nIHRpbWVzdGFtcCB2YWxpZGF0aW9uCnZpb2xhdGlvbnMgY29udGFpbnMgbXNnIGlmIHsKCW5vdCBpbnB1dC5tZXRhZGF0YS50aW1lc3RhbXAKCW1zZyA6PSAiU0JPTSBtZXRhZGF0YS50aW1lc3RhbXAgZmllbGQgaXMgbWlzc2luZyBvciBudWxsIgp9Cg=="
      ],
      "referenceDigest": "sha256:05e4fa0eb477740f836dd0a5afc86f7746b960cf096df1d49a3bc786cf57e831",
      "referenceName": "file:///home/migmartri/work/chainloop/chainloop/docs/examples/policies/sbom-freshness/policy.yaml",
      "description": "Validates that SBOM timestamp is within acceptable age limit",
      "violations": [
        {
          "subject": "sbom-freshness",
          "message": "SBOM created at 2024-04-08T13:37:52Z is too old (age limit: 30 days)"
        }
      ],
      "type": "SBOM_CYCLONEDX_JSON",
      "policyReference": {
        "name": "sbom-freshness",
        "digest": "sha256:05e4fa0eb477740f836dd0a5afc86f7746b960cf096df1d49a3bc786cf57e831",
        "uri": "file:///home/migmartri/work/chainloop/chainloop/docs/examples/policies/sbom-freshness/policy.yaml"
      }
    }
  ]
}

Closes #2947

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Add a new policyEvaluationsRef field to the attestation predicate that
will hold a CAS reference to the externalized policy evaluations bundle.
This includes a setter method on RendererV02 and a forwarding method on
AttestationRenderer to allow the push action to set the reference after
uploading the bundle to CAS.

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Serialize all PolicyEvaluation protos into a PolicyEvaluationBundle,
upload to CAS, and set the resulting ResourceDescriptor on the renderer
so it appears in the attestation predicate. The upload is best-effort:
if CAS is unavailable, a warning is logged and the inline
policyEvaluations field remains for backward compatibility.

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
…rage

Convert separate ref tests to table-driven test. Add debug log when CAS
backend is unavailable during push.

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 9 files

// uploadPolicyEvaluationsBundle serializes policy evaluations as a protobuf bundle,
// uploads to CAS, and returns a ResourceDescriptor referencing the uploaded object.
// Returns (nil, nil) when there are no evaluations or no uploader.
func uploadPolicyEvaluationsBundle(ctx context.Context, evaluations []*v1.PolicyEvaluation, uploader casclient.Uploader) (*intoto.ResourceDescriptor, error) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any code already for uploading to CAS that we can reuse, maybe from the crafter material part? I want to make sure that we don't try reimplement the same logic. Today we upload AI_CONFIG materials, etc

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing uploadAndCraft() in pkg/attestation/crafter/materials/materials.go is material-specific — it takes a file path, produces an Attestation_Material proto, and handles inline fallback, size checks, skip-upload flags, and material annotations. None of that applies here.

The actual shared primitive is Uploader.Upload(ctx, reader, filename, digest) from the casclient package, which is what we use directly. The uploadPolicyEvaluationsBundle function adds only the protobuf serialization + SHA256 + ResourceDescriptor construction, all specific to this use case.

Key difference from materials: when no CAS backend is available, materials fall back to inline storage (InlineCas = true). Here we intentionally skip (the existing inline policyEvaluations predicate field already serves as the fallback, per the spec).

Check evaluations count before establishing CAS connection to avoid
unnecessary network round-trip. Consolidate redundant conditional blocks
into a single if/else.

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>

@jiparis jiparis left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cool!

Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="app/cli/pkg/action/attestation_push.go">

<violation number="1" location="app/cli/pkg/action/attestation_push.go:233">
P2: The debug message is too specific for the condition and can misreport backend errors as "inline" mode, making operational debugging harder.</violation>

<violation number="2" location="app/cli/pkg/action/attestation_push.go:237">
P1: CAS upload failure is now treated as fatal, which breaks the intended best-effort behavior for policy-evaluations CAS storage during attestation push.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

} else {
ref, uploadErr := uploadPolicyEvaluationsBundle(ctx, evaluations, casBackend.Uploader)
if uploadErr != nil {
return nil, fmt.Errorf("uploading policy evaluations bundle to CAS: %w", uploadErr)

@cubic-dev-ai cubic-dev-ai Bot Mar 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: CAS upload failure is now treated as fatal, which breaks the intended best-effort behavior for policy-evaluations CAS storage during attestation push.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/pkg/action/attestation_push.go, line 237:

<comment>CAS upload failure is now treated as fatal, which breaks the intended best-effort behavior for policy-evaluations CAS storage during attestation push.</comment>

<file context>
@@ -230,12 +230,13 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
 			if uploadErr != nil {
-				action.Logger.Warn().Err(uploadErr).Msg("failed to upload policy evaluations bundle to CAS")
-			} else if ref != nil {
+				return nil, fmt.Errorf("uploading policy evaluations bundle to CAS: %w", uploadErr)
+			}
+			if ref != nil {
</file context>
Suggested change
return nil, fmt.Errorf("uploading policy evaluations bundle to CAS: %w", uploadErr)
action.Logger.Warn().Err(uploadErr).Msg("failed to upload policy evaluations bundle to CAS")
Fix with Cubic

}

if getCASErr != nil || casBackend.Uploader == nil {
action.Logger.Debug().Msg("CAS backend is inline, skipping policy evaluations bundle upload")

@cubic-dev-ai cubic-dev-ai Bot Mar 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The debug message is too specific for the condition and can misreport backend errors as "inline" mode, making operational debugging harder.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/cli/pkg/action/attestation_push.go, line 233:

<comment>The debug message is too specific for the condition and can misreport backend errors as "inline" mode, making operational debugging harder.</comment>

<file context>
@@ -230,12 +230,13 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru
 
 		if getCASErr != nil || casBackend.Uploader == nil {
-			action.Logger.Debug().Msg("CAS backend not available, skipping policy evaluations bundle upload")
+			action.Logger.Debug().Msg("CAS backend is inline, skipping policy evaluations bundle upload")
 		} else {
 			ref, uploadErr := uploadPolicyEvaluationsBundle(ctx, evaluations, casBackend.Uploader)
</file context>
Suggested change
action.Logger.Debug().Msg("CAS backend is inline, skipping policy evaluations bundle upload")
action.Logger.Debug().Err(getCASErr).Msg("CAS backend not available, skipping policy evaluations bundle upload")
Fix with Cubic

@migmartri migmartri merged commit 6fb0282 into chainloop-dev:main Mar 28, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Store policy evaluations in CAS when content addressable storage is enabled

2 participants