@@ -19,10 +19,13 @@ import (
1919 "context"
2020 "errors"
2121 "fmt"
22+ "strings"
2223
2324 pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
25+ schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
2426 "github.com/chainloop-dev/chainloop/pkg/attestation/crafter"
2527 api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
28+ "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/materials"
2629 "github.com/chainloop-dev/chainloop/pkg/casclient"
2730 "google.golang.org/grpc"
2831)
@@ -77,13 +80,20 @@ func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
7780
7881var ErrAttestationNotInitialized = errors .New ("attestation not yet initialized" )
7982
80- func (action * AttestationAdd ) Run (ctx context.Context , attestationID , materialName , materialValue , materialType string , annotations map [string ]string ) (* AttestationStatusMaterial , error ) {
83+ func (action * AttestationAdd ) Run (ctx context.Context , attestationID , materialName , materialValue , materialType string , annotations map [string ]string , policyInputFiles [] * PolicyInputFromFile ) (* AttestationStatusMaterial , error ) {
8184 // initialize the crafter. If attestation-id is provided we assume the attestation is performed using remote state
8285 crafter , err := newCrafter (& newCrafterStateOpts {enableRemoteState : (attestationID != "" ), localStatePath : action .localStatePath }, action .CPConnection , action .opts ... )
8386 if err != nil {
8487 return nil , fmt .Errorf ("failed to load crafter: %w" , err )
8588 }
8689
90+ // Resolve runtime policy inputs from the provided files before adding the
91+ // material, so a malformed file aborts the add early.
92+ runtimeInputs , err := buildRuntimeInputs (policyInputFiles )
93+ if err != nil {
94+ return nil , err
95+ }
96+
8797 if initialized , err := crafter .AlreadyInitialized (ctx , attestationID ); err != nil {
8898 return nil , fmt .Errorf ("checking if attestation is already initialized: %w" , err )
8999 } else if ! initialized {
@@ -120,10 +130,12 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
120130 // 2. If materialName is not empty, check if the material is in the contract. If it is, add material from contract
121131 // 2.1. If materialType is empty, try to guess the material kind with auto-detected kind and materialName
122132 // 3. If materialType is not empty, add material contract free with materialType and materialName
133+ addOpts := runtimeInputAddOpts (runtimeInputs )
134+
123135 var mt * api.Attestation_Material
124136 switch {
125137 case materialName == "" && materialType == "" :
126- mt , err = crafter .AddMaterialContactFreeWithAutoDetectedKind (ctx , attestationID , "" , materialValue , casBackend , annotations )
138+ mt , err = crafter .AddMaterialContactFreeWithAutoDetectedKind (ctx , attestationID , "" , materialValue , casBackend , annotations , addOpts ... )
127139 if err != nil {
128140 return nil , fmt .Errorf ("adding material: %w" , err )
129141 }
@@ -132,26 +144,32 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
132144 switch {
133145 // If the material is in the contract, add it from the contract
134146 case crafter .IsMaterialInContract (materialName ):
135- mt , err = crafter .AddMaterialFromContract (ctx , attestationID , materialName , materialValue , casBackend , annotations )
147+ mt , err = crafter .AddMaterialFromContract (ctx , attestationID , materialName , materialValue , casBackend , annotations , addOpts ... )
136148 // If the material is not in the contract and the materialType is not provided, add material contract free with auto-detected kind, guessing the kind
137149 case materialType == "" :
138- mt , err = crafter .AddMaterialContactFreeWithAutoDetectedKind (ctx , attestationID , materialName , materialValue , casBackend , annotations )
150+ mt , err = crafter .AddMaterialContactFreeWithAutoDetectedKind (ctx , attestationID , materialName , materialValue , casBackend , annotations , addOpts ... )
139151 if err != nil {
140152 return nil , fmt .Errorf ("adding material: %w" , err )
141153 }
142154 action .Logger .Info ().Str ("kind" , mt .MaterialType .String ()).Msg ("material kind detected" )
143155 // If the material is not in the contract and has a materialType, add material contract free with the provided materialType
144156 default :
145- mt , err = crafter .AddMaterialContractFree (ctx , attestationID , materialType , materialName , materialValue , casBackend , annotations )
157+ mt , err = crafter .AddMaterialContractFree (ctx , attestationID , materialType , materialName , materialValue , casBackend , annotations , addOpts ... )
146158 }
147159 default :
148- mt , err = crafter .AddMaterialContractFree (ctx , attestationID , materialType , materialName , materialValue , casBackend , annotations )
160+ mt , err = crafter .AddMaterialContractFree (ctx , attestationID , materialType , materialName , materialValue , casBackend , annotations , addOpts ... )
149161 }
150162
151163 if err != nil {
152164 return nil , fmt .Errorf ("adding material: %w" , err )
153165 }
154166
167+ // Record each source file as an EVIDENCE material, cross-linked to the
168+ // evaluated material so the exemption set itself is attested.
169+ if err := action .addPolicyInputEvidence (ctx , crafter , attestationID , mt .GetId (), policyInputFiles , casBackend ); err != nil {
170+ return nil , fmt .Errorf ("recording policy input evidence: %w" , err )
171+ }
172+
155173 materialResult , err := attMaterialToAction (mt )
156174 if err != nil {
157175 return nil , fmt .Errorf ("converting material to action: %w" , err )
@@ -160,6 +178,88 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
160178 return materialResult , nil
161179}
162180
181+ // runtimeInputAddOpts wraps the runtime inputs as crafter add options, or
182+ // returns nil when there are none. Defined at package scope so it can name the
183+ // crafter package type (the Run method shadows it with a local variable).
184+ func runtimeInputAddOpts (runtimeInputs map [string ]string ) []crafter.AddOpt {
185+ if len (runtimeInputs ) == 0 {
186+ return nil
187+ }
188+ return []crafter.AddOpt {crafter .WithRuntimeInputs (runtimeInputs )}
189+ }
190+
191+ // buildRuntimeInputs reads each policy input file and returns a map of policy
192+ // input name to its extracted values, ready to be merged onto contract
193+ // arguments. Values are newline-joined, matching the engine's existing
194+ // multi-value encoding (it splits inputs back on newlines and commas). As with
195+ // contract-declared arguments, individual values must not embed those
196+ // delimiters; path globs, the intended use, never do.
197+ func buildRuntimeInputs (policyInputFiles []* PolicyInputFromFile ) (map [string ]string , error ) {
198+ if len (policyInputFiles ) == 0 {
199+ return nil , nil
200+ }
201+
202+ runtimeInputs := make (map [string ]string , len (policyInputFiles ))
203+ for _ , pif := range policyInputFiles {
204+ values , err := ExtractColumnValues (pif .File , pif .Column )
205+ if err != nil {
206+ return nil , fmt .Errorf ("extracting %q from %q: %w" , pif .Column , pif .File , err )
207+ }
208+ joined := strings .Join (values , "\n " )
209+ if existing := runtimeInputs [pif .Input ]; existing != "" {
210+ runtimeInputs [pif .Input ] = existing + "\n " + joined
211+ } else {
212+ runtimeInputs [pif .Input ] = joined
213+ }
214+ }
215+
216+ return runtimeInputs , nil
217+ }
218+
219+ // addPolicyInputEvidence adds each policy input file as an EVIDENCE material,
220+ // linked back to the evaluated material via the chainloop.material.references
221+ // annotation. The evidence material name is derived as "<material>-<input>";
222+ // when the same input is fed by more than one file, a "-<n>" suffix keeps the
223+ // names unique so no evidence record is silently overwritten.
224+ func (action * AttestationAdd ) addPolicyInputEvidence (ctx context.Context , c * crafter.Crafter , attestationID , materialName string , policyInputFiles []* PolicyInputFromFile , casBackend * casclient.CASBackend ) error {
225+ names := policyInputEvidenceNames (materialName , policyInputFiles )
226+ for i , pif := range policyInputFiles {
227+ annotations := map [string ]string {
228+ materials .AnnotationMaterialReferences : materialName ,
229+ }
230+
231+ if _ , err := c .AddMaterialContractFree (ctx , attestationID , schemaapi .CraftingSchema_Material_EVIDENCE .String (), names [i ], pif .File , casBackend , annotations ); err != nil {
232+ return fmt .Errorf ("adding evidence material %q: %w" , names [i ], err )
233+ }
234+ }
235+
236+ return nil
237+ }
238+
239+ // policyInputEvidenceNames returns the evidence material name for each policy
240+ // input file, in order. Names are "<material>-<input>"; when the same input is
241+ // fed by more than one file, a "-<n>" suffix keeps them unique so no evidence
242+ // record is silently overwritten in the attestation.
243+ func policyInputEvidenceNames (materialName string , policyInputFiles []* PolicyInputFromFile ) []string {
244+ inputCount := make (map [string ]int , len (policyInputFiles ))
245+ for _ , pif := range policyInputFiles {
246+ inputCount [pif .Input ]++
247+ }
248+
249+ names := make ([]string , len (policyInputFiles ))
250+ seen := make (map [string ]int , len (policyInputFiles ))
251+ for i , pif := range policyInputFiles {
252+ name := fmt .Sprintf ("%s-%s" , materialName , pif .Input )
253+ if inputCount [pif .Input ] > 1 {
254+ seen [pif .Input ]++
255+ name = fmt .Sprintf ("%s-%d" , name , seen [pif .Input ])
256+ }
257+ names [i ] = name
258+ }
259+
260+ return names
261+ }
262+
163263// GetPolicyEvaluations is a Wrapper around the getPolicyEvaluations
164264func (action * AttestationAdd ) GetPolicyEvaluations (ctx context.Context , attestationID string ) (map [string ][]* PolicyEvaluation , error ) {
165265 crafter , err := newCrafter (& newCrafterStateOpts {enableRemoteState : (attestationID != "" ), localStatePath : action .localStatePath }, action .CPConnection , action .opts ... )
0 commit comments