99 "os"
1010 "path/filepath"
1111 "strings"
12+ "time"
1213
1314 "codacy/cli-v2/utils/logger"
1415
2526 sbomTag string
2627 sbomRepoName string
2728 sbomEnv string
28- sbomFormat string
29+ sbomFormat string
30+ sbomBaseURL string
31+
32+ sbomHTTPClient httpDoer = & http.Client {Timeout : 5 * time .Minute }
2933)
3034
35+ // httpDoer abstracts the Do method of http.Client for testing.
36+ type httpDoer interface {
37+ Do (req * http.Request ) (* http.Response , error )
38+ }
39+
3140func init () {
3241 uploadSBOMCmd .Flags ().StringVarP (& sbomAPIToken , "api-token" , "a" , "" , "API token for Codacy API (required)" )
3342 uploadSBOMCmd .Flags ().StringVarP (& sbomProvider , "provider" , "p" , "" , "Git provider (gh, gl, bb) (required)" )
@@ -80,33 +89,71 @@ func executeUploadSBOM(imageRef string) int {
8089 }
8190
8291 imageName , tag := parseImageRef (imageRef )
92+ isDigest := strings .Contains (imageRef , "@" )
93+
8394 if sbomTag != "" {
95+ if isDigest {
96+ color .Red ("Error: --tag cannot be used with digest references (image@sha256:...)" )
97+ return 2
98+ }
8499 tag = sbomTag
85100 }
86101 sbomImageName = imageName
87102
103+ var effectiveImageRef string
104+ if isDigest {
105+ effectiveImageRef = fmt .Sprintf ("%s@%s" , imageName , tag )
106+ } else {
107+ effectiveImageRef = fmt .Sprintf ("%s:%s" , imageName , tag )
108+ }
109+
88110 logger .Info ("Starting SBOM upload" , logrus.Fields {
89- "image" : imageRef ,
111+ "image" : effectiveImageRef ,
90112 "provider" : sbomProvider ,
91113 "org" : sbomOrg ,
92114 })
93115
94- // Generate SBOM with Trivy
116+ sbomPath , err := generateSBOM (effectiveImageRef )
117+ if err != nil {
118+ return 2
119+ }
120+ defer os .Remove (sbomPath )
121+
122+ fmt .Printf ("Uploading SBOM to Codacy (org: %s/%s)...\n " , sbomProvider , sbomOrg )
123+ params := sbomUploadParams {
124+ provider : sbomProvider ,
125+ org : sbomOrg ,
126+ apiToken : sbomAPIToken ,
127+ repoName : sbomRepoName ,
128+ env : sbomEnv ,
129+ baseURL : sbomBaseURL ,
130+ }
131+ if err := uploadSBOMToCodacy (sbomPath , sbomImageName , tag , params ); err != nil {
132+ logger .Error ("Failed to upload SBOM" , logrus.Fields {"error" : err .Error ()})
133+ color .Red ("Error: Failed to upload SBOM: %v" , err )
134+ return 1
135+ }
136+
137+ color .Green ("Successfully uploaded SBOM for %s" , effectiveImageRef )
138+ return 0
139+ }
140+
141+ // generateSBOM runs Trivy to generate an SBOM file and returns the path to it.
142+ func generateSBOM (imageRef string ) (string , error ) {
95143 trivyPath , err := getTrivyPath ()
96144 if err != nil {
97145 handleTrivyNotFound (err )
98- return 2
146+ return "" , err
99147 }
100148
101149 tmpFile , err := os .CreateTemp ("" , "codacy-sbom-*" )
102150 if err != nil {
103151 logger .Error ("Failed to create temp file" , logrus.Fields {"error" : err .Error ()})
104152 color .Red ("Error: Failed to create temporary file: %v" , err )
105- return 2
153+ return "" , err
106154 }
107155 tmpFile .Close ()
108156 sbomPath := tmpFile .Name ()
109- defer os .Remove (sbomPath )
110157
111158 fmt .Printf ("Generating SBOM for image: %s\n " , imageRef )
112159 args := []string {"image" , "--format" , sbomFormat , "-o" , sbomPath , imageRef }
@@ -120,20 +167,11 @@ func executeUploadSBOM(imageRef string) int {
120167 color .Red ("Error: Failed to generate SBOM: %v" , err )
121168 }
122169 logger .Error ("Trivy SBOM generation failed" , logrus.Fields {"error" : err .Error ()})
123- return 2
170+ os .Remove (sbomPath )
171+ return "" , err
124172 }
125173 fmt .Println ("SBOM generated successfully" )
126-
127- // Upload SBOM to Codacy
128- fmt .Printf ("Uploading SBOM to Codacy (org: %s/%s)...\n " , sbomProvider , sbomOrg )
129- if err := uploadSBOMToCodacy (sbomPath , sbomImageName , tag ); err != nil {
130- logger .Error ("Failed to upload SBOM" , logrus.Fields {"error" : err .Error ()})
131- color .Red ("Error: Failed to upload SBOM: %v" , err )
132- return 1
133- }
134-
135- color .Green ("Successfully uploaded SBOM for %s:%s" , sbomImageName , tag )
136- return 0
174+ return sbomPath , nil
137175}
138176
139177// parseImageRef splits an image reference into name and tag.
@@ -162,38 +200,31 @@ func parseImageRef(imageRef string) (string, string) {
162200 return imageRef , "latest"
163201}
164202
165- func uploadSBOMToCodacy (sbomPath , imageName , tag string ) error {
166- url := fmt .Sprintf ("https://app.codacy.com/api/v3/organizations/%s/%s/image-sboms" ,
167- sbomProvider , sbomOrg )
168-
169- body := & bytes.Buffer {}
170- writer := multipart .NewWriter (body )
203+ type sbomUploadParams struct {
204+ provider string
205+ org string
206+ apiToken string
207+ repoName string
208+ env string
209+ baseURL string
210+ }
171211
172- // Add the SBOM file
173- sbomFile , err := os . Open ( sbomPath )
174- if err != nil {
175- return fmt . Errorf ( "failed to open SBOM file: %w" , err )
212+ func ( p sbomUploadParams ) uploadURL () string {
213+ base := p . baseURL
214+ if base == "" {
215+ base = "https://app.codacy.com"
176216 }
177- defer sbomFile .Close ()
217+ return fmt .Sprintf ("%s/api/v3/organizations/%s/%s/image-sboms" , base , p .provider , p .org )
218+ }
178219
179- part , err := writer .CreateFormFile ("sbom" , filepath .Base (sbomPath ))
180- if err != nil {
181- return fmt .Errorf ("failed to create form file: %w" , err )
182- }
183- if _ , err := io .Copy (part , sbomFile ); err != nil {
184- return fmt .Errorf ("failed to write SBOM to form: %w" , err )
185- }
220+ func uploadSBOMToCodacy (sbomPath , imageName , tag string , params sbomUploadParams ) error {
221+ url := params .uploadURL ()
186222
187- // Add required fields
188- writer .WriteField ("imageName" , imageName )
189- writer .WriteField ("tag" , tag )
223+ body := & bytes.Buffer {}
224+ writer := multipart .NewWriter (body )
190225
191- // Add optional fields
192- if sbomRepoName != "" {
193- writer .WriteField ("repositoryName" , sbomRepoName )
194- }
195- if sbomEnv != "" {
196- writer .WriteField ("environment" , sbomEnv )
226+ if err := buildSBOMMultipartForm (writer , sbomPath , imageName , tag , params ); err != nil {
227+ return err
197228 }
198229
199230 if err := writer .Close (); err != nil {
@@ -206,9 +237,9 @@ func uploadSBOMToCodacy(sbomPath, imageName, tag string) error {
206237 }
207238 req .Header .Set ("Content-Type" , writer .FormDataContentType ())
208239 req .Header .Set ("Accept" , "application/json" )
209- req .Header .Set ("api-token" , sbomAPIToken )
240+ req .Header .Set ("api-token" , params . apiToken )
210241
211- resp , err := http . DefaultClient .Do (req )
242+ resp , err := sbomHTTPClient .Do (req )
212243 if err != nil {
213244 return fmt .Errorf ("request failed: %w" , err )
214245 }
@@ -221,3 +252,48 @@ func uploadSBOMToCodacy(sbomPath, imageName, tag string) error {
221252
222253 return nil
223254}
255+
256+ // buildSBOMMultipartForm populates the multipart form with the SBOM file and metadata fields.
257+ func buildSBOMMultipartForm (writer * multipart.Writer , sbomPath , imageName , tag string , params sbomUploadParams ) error {
258+ if err := addSBOMFile (writer , sbomPath ); err != nil {
259+ return err
260+ }
261+
262+ fields := map [string ]string {
263+ "imageName" : imageName ,
264+ "tag" : tag ,
265+ }
266+ if params .repoName != "" {
267+ fields ["repositoryName" ] = params .repoName
268+ }
269+ if params .env != "" {
270+ fields ["environment" ] = params .env
271+ }
272+
273+ for name , value := range fields {
274+ if err := writer .WriteField (name , value ); err != nil {
275+ return fmt .Errorf ("failed to write %s field: %w" , name , err )
276+ }
277+ }
278+
279+ return nil
280+ }
281+
282+ // addSBOMFile adds the SBOM file to the multipart form.
283+ func addSBOMFile (writer * multipart.Writer , sbomPath string ) error {
284+ sbomFile , err := os .Open (sbomPath )
285+ if err != nil {
286+ return fmt .Errorf ("failed to open SBOM file: %w" , err )
287+ }
288+ defer sbomFile .Close ()
289+
290+ part , err := writer .CreateFormFile ("sbom" , filepath .Base (sbomPath ))
291+ if err != nil {
292+ return fmt .Errorf ("failed to create form file: %w" , err )
293+ }
294+ if _ , err := io .Copy (part , sbomFile ); err != nil {
295+ return fmt .Errorf ("failed to write SBOM to form: %w" , err )
296+ }
297+
298+ return nil
299+ }
0 commit comments