feat(controlplane): enforce workflow contracts when attestations are stored - #3465
Conversation
…stored The workflow contract was only enforced by the CLI. crafter.ValidateAttestation had no caller in the control plane, so Store accepted any well-formed, signed attestation regardless of whether it satisfied the contract revision pinned on the run. The bundle is signed by the same client that decides whether to run that check, so a valid signature says nothing about contract compliance. SaveAttestation now loads the contract revision pinned on the workflow run at init time and rejects an attestation whose required materials are missing, before anything is persisted. Choke groups are honoured, and materials the contract does not declare remain allowed. The material presence check is extracted into a validator shared by the CLI and the control plane so both enforce identical semantics. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 513023ae-e4c2-4c52-ba7b-5ef838eda385
AI Session Checks — 🟡 84% · ✅ 0 failing
|
| Status | Attribution | File | Lines |
|---|---|---|---|
| modified | ai | app/controlplane/pkg/biz/workflowrun_contract_integration_test.go |
+275 / -0 |
| created | ai | pkg/attestation/renderer/chainloop/materials_roundtrip_test.go |
+147 / -0 |
| modified | ai | pkg/attestation/crafter/api/attestation/v1/crafting_state_validations_test.go |
+111 / -0 |
| modified | ai | app/controlplane/pkg/biz/workflowrun.go |
+92 / -5 |
| modified | ai | pkg/attestation/crafter/api/attestation/v1/crafting_state_validations.go |
+25 / -3 |
| modified | ai | app/controlplane/internal/service/service_test.go |
+10 / -0 |
| modified | ai | app/controlplane/internal/service/attestation.go |
+8 / -0 |
Policies (4)
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | ai-config-ai-agents-allowed |
ai-coding-session-513023 |
- |
| ✅ Passed | ai-config-no-dangerous-commands |
ai-coding-session-513023 |
- |
| ✅ Passed | ai-config-no-secrets |
ai-coding-session-513023 |
- |
| ✅ Passed | ai-config-mcp-servers-allowed |
ai-coding-session-513023 |
- |
Security Checks — ✅ 5 passing
✅ secret-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | secrets-detection |
- |
✅ sast-scan
| Status | Policy | Messages |
|---|---|---|
| ✅ Passed | owasp-top10-2025 |
- |
| ✅ Passed | sast |
- |
| ✅ Passed | cwe-top25 |
- |
| ✅ Passed | cwe-top26-40-cusp |
- |
⏭️ 3 scans not applied
| Scan | Reason |
|---|---|
vulnerability-scan |
no manifest/lockfile changed |
github-actions-scan |
no workflow files changed |
iac-scan |
no IaC files changed |
PR validation — ⚠️ 1 failing
| Status | Policy | Material | Messages |
|---|---|---|---|
| ✅ Passed | pr-min-approvals |
pr-info |
- |
| ✅ Passed | pr-description-required |
pr-info |
- |
pr-user-story-linked |
pr-info |
PR/MR #3465 does not reference a user story or issue in title, description, or branch 'feat(controlplane): enforce workflow contracts when attestations are stored'. Expected patterns: ["(?i)[A-Z]+-[0-9]+", "#[0-9]+", "(?i)[A-Z]{2", "}-[0-9]+", "(?i)gh-[0-9]+", "(?i)\[[A-Z]+-[0-9]+\]"] |
Powered by Chainloop and Chainloop Trace
There was a problem hiding this comment.
All reported issues were addressed across 8 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
When skip_db_storage is enabled the bundle is uploaded to the CAS backend before SaveAttestation runs, so an attestation rejected for violating its contract would already have left an orphaned blob behind. Add ValidateAttestationContract, a side-effect-free check the synchronous CAS path runs before uploading. SaveAttestation keeps its own check, which remains the authoritative one since it sits on the path every attestation takes. Assisted-by: Claude Code Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Chainloop-Trace-Sessions: 513023ae-e4c2-4c52-ba7b-5ef838eda385
Summary
The workflow contract was enforced only by the CLI.
crafter.ValidateAttestationhad no caller in the control plane, soAttestationService.Storeaccepted any well-formed, signed attestation regardless of whether it satisfied the contract revision pinned on the run. Because the bundle is signed by the same client that decides whether to run that check, a valid signature says nothing about contract compliance — a modified or custom crafter could push an attestation that satisfies no contract.The control plane is now the authority.
WorkflowRunUseCase.SaveAttestationloads the contract revision pinned on the workflow run at init time and rejects an attestation whose required materials are missing, before anything is written to the database or CAS. The rejection surfaces as a client error naming the missing materials and the revision they were measured against.This applies to all new attestations. Already-stored attestations are not revalidated.
Behaviour
The CLI keeps its own check, which still gives fast local feedback before a push.
The material presence check is extracted out of
CraftingState.ValidateCompleteinto a validator shared by the CLI and the control plane, so the two cannot drift apart.Out of scope
Contract-declared annotations, whether contract-declared policies were actually evaluated, and the
BlockOnPolicyViolationstrategy all remain client-enforced.AI disclosure
This contribution was produced with AI assistance (Claude Code). Each commit carries an
Assisted-by: Claude Codetrailer.