@@ -42,6 +42,12 @@ type AttestationAddOpts struct {
4242 LocalStatePath string
4343 // NoStrictValidation skips strict schema validation
4444 NoStrictValidation bool
45+ // MaxExtractEntries limits the number of entries extracted from an archive.
46+ // Zero defaults to materials.DefaultArchiveLimits().MaxEntries.
47+ MaxExtractEntries int
48+ // MaxExtractSize limits the total uncompressed bytes extracted from an archive.
49+ // Zero defaults to materials.DefaultArchiveLimits().MaxTotalSize.
50+ MaxExtractSize int64
4551}
4652
4753type newCrafterOpts struct {
@@ -56,6 +62,8 @@ type AttestationAdd struct {
5662 casCAPath string
5763 connectionInsecure bool
5864 localStatePath string
65+ maxExtractEntries int
66+ maxExtractSize int64
5967 * newCrafterOpts
6068}
6169
@@ -69,19 +77,31 @@ func NewAttestationAdd(cfg *AttestationAddOpts) (*AttestationAdd, error) {
6977 opts = append (opts , crafter .WithNoStrictValidation (cfg .NoStrictValidation ))
7078 }
7179
80+ defaults := materials .DefaultArchiveLimits ()
81+ maxEntries := cfg .MaxExtractEntries
82+ if maxEntries <= 0 {
83+ maxEntries = defaults .MaxEntries
84+ }
85+ maxSize := cfg .MaxExtractSize
86+ if maxSize <= 0 {
87+ maxSize = defaults .MaxTotalSize
88+ }
89+
7290 return & AttestationAdd {
7391 ActionsOpts : cfg .ActionsOpts ,
7492 newCrafterOpts : & newCrafterOpts {cpConnection : cfg .CPConnection , opts : opts },
7593 casURI : cfg .CASURI ,
7694 casCAPath : cfg .CASCAPath ,
7795 connectionInsecure : cfg .ConnectionInsecure ,
7896 localStatePath : cfg .LocalStatePath ,
97+ maxExtractEntries : maxEntries ,
98+ maxExtractSize : maxSize ,
7999 }, nil
80100}
81101
82102var ErrAttestationNotInitialized = errors .New ("attestation not yet initialized" )
83103
84- func (action * AttestationAdd ) Run (ctx context.Context , attestationID , materialName , materialValue , materialType string , annotations map [string ]string , policyInputFiles []* PolicyInputFromFile ) (* AttestationStatusMaterial , error ) {
104+ func (action * AttestationAdd ) Run (ctx context.Context , attestationID , materialName , materialValue , materialType string , annotations map [string ]string , policyInputFiles []* PolicyInputFromFile ) ([] * AttestationStatusMaterial , error ) {
85105 // initialize the crafter. If attestation-id is provided we assume the attestation is performed using remote state
86106 crafter , err := newCrafter (& newCrafterStateOpts {enableRemoteState : (attestationID != "" ), localStatePath : action .localStatePath }, action .CPConnection , action .opts ... )
87107 if err != nil {
@@ -133,6 +153,31 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
133153 // 3. If materialType is not empty, add material contract free with materialType and materialName
134154 addOpts := runtimeInputAddOpts (runtimeInputs )
135155
156+ // Explode path: --kind set, value is a (non-archive-native) archive.
157+ format , err := shouldExplode (materialType , materialValue )
158+ if err != nil {
159+ return nil , fmt .Errorf ("detecting archive: %w" , err )
160+ }
161+ if format != materials .ArchiveNone {
162+ if len (policyInputFiles ) > 0 {
163+ action .Logger .Warn ().Msg ("--policy-input-from-file is ignored when expanding an archive; evidence cross-links are not recorded for exploded materials" )
164+ }
165+ limits := materials.ArchiveLimits {MaxEntries : action .maxExtractEntries , MaxTotalSize : action .maxExtractSize }
166+ mts , err := crafter .AddMaterialsFromArchive (ctx , attestationID , materialType , materialName , materialValue , format , casBackend , annotations , limits , addOpts ... )
167+ if err != nil {
168+ return nil , fmt .Errorf ("adding materials from archive: %w" , err )
169+ }
170+ results := make ([]* AttestationStatusMaterial , 0 , len (mts ))
171+ for _ , mt := range mts {
172+ r , err := attMaterialToAction (mt )
173+ if err != nil {
174+ return nil , fmt .Errorf ("converting material to action: %w" , err )
175+ }
176+ results = append (results , r )
177+ }
178+ return results , nil
179+ }
180+
136181 var mt * api.Attestation_Material
137182 switch {
138183 case materialName == "" && materialType == "" :
@@ -176,7 +221,21 @@ func (action *AttestationAdd) Run(ctx context.Context, attestationID, materialNa
176221 return nil , fmt .Errorf ("converting material to action: %w" , err )
177222 }
178223
179- return materialResult , nil
224+ return []* AttestationStatusMaterial {materialResult }, nil
225+ }
226+
227+ // shouldExplode decides whether an att-add should explode the value into many
228+ // materials: only when the kind is explodable (SBOM/SARIF) and the value is a
229+ // supported archive. It returns ArchiveNone for every other kind so a regular
230+ // zip provided as e.g. ARTIFACT or EVIDENCE is recorded whole.
231+ func shouldExplode (materialType , value string ) (materials.ArchiveFormat , error ) {
232+ // Only explode kinds that have a meaningful "bundle of the same kind"
233+ // archive form (SBOM, SARIF). Any other kind — including ARTIFACT and
234+ // EVIDENCE — records the archive whole even when the value is a zip/tar.
235+ if ! materials .IsExplodableKind (materialType ) {
236+ return materials .ArchiveNone , nil
237+ }
238+ return materials .DetectArchive (value )
180239}
181240
182241// runtimeInputAddOpts wraps the runtime inputs as crafter add options, or
@@ -315,29 +374,14 @@ func policyInputEvidenceNames(materialName string, policyInputFiles []*PolicyInp
315374 return names
316375}
317376
318- // sanitizeMaterialNamePart lower-cases s and collapses every run of characters
319- // outside [a-z0-9] into a single "-", trimming leading/trailing "-", so the
320- // result is a valid material-name component. Falls back to "input" if nothing
321- // usable remains.
377+ // sanitizeMaterialNamePart sanitizes s into a valid material-name component via
378+ // materials.SanitizeMaterialName, falling back to "input" if nothing usable
379+ // remains.
322380func sanitizeMaterialNamePart (s string ) string {
323- var b strings.Builder
324- pendingHyphen := false
325- for _ , r := range strings .ToLower (s ) {
326- if (r >= 'a' && r <= 'z' ) || (r >= '0' && r <= '9' ) {
327- if pendingHyphen && b .Len () > 0 {
328- b .WriteByte ('-' )
329- }
330- b .WriteRune (r )
331- pendingHyphen = false
332- } else {
333- pendingHyphen = true
334- }
335- }
336-
337- if b .Len () == 0 {
338- return "input"
381+ if name := materials .SanitizeMaterialName (s ); name != "" {
382+ return name
339383 }
340- return b . String ()
384+ return "input"
341385}
342386
343387// GetPolicyEvaluations is a Wrapper around the getPolicyEvaluations
0 commit comments