@@ -27,7 +27,6 @@ import (
2727 api "github.com/chainloop-dev/chainloop/pkg/attestation/crafter/api/attestation/v1"
2828 "github.com/chainloop-dev/chainloop/pkg/casclient"
2929 remotename "github.com/google/go-containerregistry/pkg/name"
30-
3130 "github.com/rs/zerolog"
3231)
3332
@@ -43,19 +42,38 @@ type CyclonedxJSONCrafter struct {
4342 * crafterCommon
4443}
4544
46- // mainComponentStruct internal struct to unmarshall the incoming CycloneDX JSON
47- type mainComponentStruct struct {
48- Metadata struct {
49- Component struct {
50- Name string `json:"name"`
51- Type string `json:"type"`
52- Version string `json:"version"`
53- Properties []struct {
54- Name string `json:"name"`
55- Value string `json:"value"`
56- } `json:"properties"`
57- } `json:"component"`
58- } `json:"metadata"`
45+ // cyclonedxDoc internal struct to unmarshall the incoming CycloneDX JSON
46+ type cyclonedxDoc struct {
47+ SpecVersion string `json:"specVersion"`
48+ Metadata json.RawMessage `json:"metadata"`
49+ }
50+
51+ type cyclonedxMetadataV14 struct {
52+ Tools []struct {
53+ Name string `json:"name"`
54+ Version string `json:"version"`
55+ } `json:"tools"`
56+ Component cyclonedxComponent `json:"component"`
57+ }
58+
59+ type cyclonedxComponent struct {
60+ Name string `json:"name"`
61+ Type string `json:"type"`
62+ Version string `json:"version"`
63+ Properties []struct {
64+ Name string `json:"name"`
65+ Value string `json:"value"`
66+ } `json:"properties"`
67+ }
68+
69+ type cyclonedxMetadataV15 struct {
70+ Tools struct {
71+ Components []struct { // available from 1.5 onwards
72+ Name string `json:"name"`
73+ Version string `json:"version"`
74+ } `json:"components"`
75+ } `json:"tools"`
76+ Component cyclonedxComponent `json:"component"`
5977}
6078
6179func NewCyclonedxJSONCrafter (materialSchema * schemaapi.CraftingSchema_Material , backend * casclient.CASBackend , l * zerolog.Logger ) (* CyclonedxJSONCrafter , error ) {
@@ -100,33 +118,62 @@ func (i *CyclonedxJSONCrafter) Craft(ctx context.Context, filePath string) (*api
100118 },
101119 }
102120
103- // Include the main component information if available
104- mainComponent , err := i . extractMainComponent ( f )
105- if err != nil {
106- i .logger .Debug ().Err (err ).Msg ("error extracting main component from sbom , skipping..." )
121+ // parse the file to extract the main information
122+ var doc cyclonedxDoc
123+ if err = json . Unmarshal ( f , & doc ); err != nil {
124+ i .logger .Debug ().Err (err ).Msg ("error decoding file to extract main information , skipping ..." )
107125 }
108126
109- // If the main component is available, include it in the material
110- if mainComponent != nil {
111- res .M .(* api.Attestation_Material_SbomArtifact ).SbomArtifact .MainComponent = & api.Attestation_Material_SBOMArtifact_MainComponent {
112- Name : mainComponent .name ,
113- Kind : mainComponent .kind ,
114- Version : mainComponent .version ,
127+ switch doc .SpecVersion {
128+ case "1.4" :
129+ var metaV14 cyclonedxMetadataV14
130+ if err = json .Unmarshal (doc .Metadata , & metaV14 ); err != nil {
131+ i .logger .Debug ().Err (err ).Msg ("error decoding file to extract main information, skipping ..." )
132+ } else {
133+ i .extractMetadata (m , & metaV14 )
134+ }
135+ default : // 1.5 onwards
136+ var metaV15 cyclonedxMetadataV15
137+ if err = json .Unmarshal (doc .Metadata , & metaV15 ); err != nil {
138+ i .logger .Debug ().Err (err ).Msg ("error decoding file to extract main information, skipping ..." )
139+ } else {
140+ i .extractMetadata (m , & metaV15 )
115141 }
116142 }
117143
118144 return res , nil
119145}
120146
121- // extractMainComponent inspects the SBOM and extracts the main component if any and available
122- func (i * CyclonedxJSONCrafter ) extractMainComponent (rawFile []byte ) (* SBOMMainComponentInfo , error ) {
123- var mainComponent mainComponentStruct
124- err := json .Unmarshal (rawFile , & mainComponent )
125- if err != nil {
126- return nil , fmt .Errorf ("error extracting main component: %w" , err )
147+ func (i * CyclonedxJSONCrafter ) extractMetadata (m * api.Attestation_Material , metadata any ) {
148+ m .Annotations = make (map [string ]string )
149+
150+ switch meta := metadata .(type ) {
151+ case * cyclonedxMetadataV14 :
152+ if err := i .extractMainComponent (m , & meta .Component ); err != nil {
153+ i .logger .Debug ().Err (err ).Msg ("error extracting main component from sbom, skipping..." )
154+ }
155+
156+ if len (meta .Tools ) > 0 {
157+ m .Annotations [AnnotationToolNameKey ] = meta .Tools [0 ].Name
158+ m .Annotations [AnnotationToolVersionKey ] = meta .Tools [0 ].Version
159+ }
160+ case * cyclonedxMetadataV15 :
161+ if err := i .extractMainComponent (m , & meta .Component ); err != nil {
162+ i .logger .Debug ().Err (err ).Msg ("error extracting main component from sbom, skipping..." )
163+ }
164+
165+ if len (meta .Tools .Components ) > 0 {
166+ m .Annotations [AnnotationToolNameKey ] = meta .Tools .Components [0 ].Name
167+ m .Annotations [AnnotationToolVersionKey ] = meta .Tools .Components [0 ].Version
168+ }
169+ default :
170+ i .logger .Debug ().Msg ("unknown metadata version" )
127171 }
172+ }
128173
129- component := mainComponent .Metadata .Component
174+ // extractMainComponent inspects the SBOM and extracts the main component if any and available
175+ func (i * CyclonedxJSONCrafter ) extractMainComponent (m * api.Attestation_Material , component * cyclonedxComponent ) error {
176+ var mainComponent * SBOMMainComponentInfo
130177
131178 // If the version is empty, try to extract it from the properties
132179 if component .Version == "" {
@@ -141,23 +188,32 @@ func (i *CyclonedxJSONCrafter) extractMainComponent(rawFile []byte) (*SBOMMainCo
141188 }
142189
143190 if component .Type != containerComponentKind {
144- return & SBOMMainComponentInfo {
191+ mainComponent = & SBOMMainComponentInfo {
145192 name : component .Name ,
146193 kind : component .Type ,
147194 version : component .Version ,
148- }, nil
195+ }
196+ } else {
197+ // Standardize the name to have the full repository name including the registry and
198+ // sanitize the name to remove the possible tag from the repository name
199+ ref , err := remotename .ParseReference (component .Name )
200+ if err != nil {
201+ return fmt .Errorf ("couldn't parse OCI image repository name: %w" , err )
202+ }
203+
204+ mainComponent = & SBOMMainComponentInfo {
205+ name : ref .Context ().String (),
206+ kind : component .Type ,
207+ version : component .Version ,
208+ }
149209 }
150210
151- // Standardize the name to have the full repository name including the registry and
152- // sanitize the name to remove the possible tag from the repository name
153- ref , err := remotename . ParseReference ( component . Name )
154- if err != nil {
155- return nil , fmt . Errorf ( "couldn't parse OCI image repository name: %w" , err )
211+ // If the main component is available, include it in the material
212+ m . M .( * api. Attestation_Material_SbomArtifact ). SbomArtifact . MainComponent = & api. Attestation_Material_SBOMArtifact_MainComponent {
213+ Name : mainComponent . name ,
214+ Kind : mainComponent . kind ,
215+ Version : mainComponent . version ,
156216 }
157217
158- return & SBOMMainComponentInfo {
159- name : ref .Context ().String (),
160- kind : component .Type ,
161- version : component .Version ,
162- }, nil
218+ return nil
163219}
0 commit comments