2525 sbomTag string
2626 sbomRepoName string
2727 sbomEnv string
28- sbomFormat string
28+ sbomFormat string
29+ sbomBaseURL string
30+
31+ sbomHTTPClient httpDoer = http .DefaultClient
2932)
3033
34+ // httpDoer abstracts the Do method of http.Client for testing.
35+ type httpDoer interface {
36+ Do (req * http.Request ) (* http.Response , error )
37+ }
38+
3139func init () {
3240 uploadSBOMCmd .Flags ().StringVarP (& sbomAPIToken , "api-token" , "a" , "" , "API token for Codacy API (required)" )
3341 uploadSBOMCmd .Flags ().StringVarP (& sbomProvider , "provider" , "p" , "" , "Git provider (gh, gl, bb) (required)" )
@@ -85,28 +93,55 @@ func executeUploadSBOM(imageRef string) int {
8593 }
8694 sbomImageName = imageName
8795
96+ effectiveImageRef := fmt .Sprintf ("%s:%s" , imageName , tag )
97+
8898 logger .Info ("Starting SBOM upload" , logrus.Fields {
89- "image" : imageRef ,
99+ "image" : effectiveImageRef ,
90100 "provider" : sbomProvider ,
91101 "org" : sbomOrg ,
92102 })
93103
94- // Generate SBOM with Trivy
104+ sbomPath , err := generateSBOM (effectiveImageRef )
105+ if err != nil {
106+ return 2
107+ }
108+ defer os .Remove (sbomPath )
109+
110+ fmt .Printf ("Uploading SBOM to Codacy (org: %s/%s)...\n " , sbomProvider , sbomOrg )
111+ params := sbomUploadParams {
112+ provider : sbomProvider ,
113+ org : sbomOrg ,
114+ apiToken : sbomAPIToken ,
115+ repoName : sbomRepoName ,
116+ env : sbomEnv ,
117+ baseURL : sbomBaseURL ,
118+ }
119+ if err := uploadSBOMToCodacy (sbomPath , sbomImageName , tag , params ); err != nil {
120+ logger .Error ("Failed to upload SBOM" , logrus.Fields {"error" : err .Error ()})
121+ color .Red ("Error: Failed to upload SBOM: %v" , err )
122+ return 1
123+ }
124+
125+ color .Green ("Successfully uploaded SBOM for %s:%s" , sbomImageName , tag )
126+ return 0
127+ }
128+
129+ // generateSBOM runs Trivy to generate an SBOM file and returns the path to it.
130+ func generateSBOM (imageRef string ) (string , error ) {
95131 trivyPath , err := getTrivyPath ()
96132 if err != nil {
97133 handleTrivyNotFound (err )
98- return 2
134+ return "" , err
99135 }
100136
101137 tmpFile , err := os .CreateTemp ("" , "codacy-sbom-*" )
102138 if err != nil {
103139 logger .Error ("Failed to create temp file" , logrus.Fields {"error" : err .Error ()})
104140 color .Red ("Error: Failed to create temporary file: %v" , err )
105- return 2
141+ return "" , err
106142 }
107143 tmpFile .Close ()
108144 sbomPath := tmpFile .Name ()
109- defer os .Remove (sbomPath )
110145
111146 fmt .Printf ("Generating SBOM for image: %s\n " , imageRef )
112147 args := []string {"image" , "--format" , sbomFormat , "-o" , sbomPath , imageRef }
@@ -120,20 +155,11 @@ func executeUploadSBOM(imageRef string) int {
120155 color .Red ("Error: Failed to generate SBOM: %v" , err )
121156 }
122157 logger .Error ("Trivy SBOM generation failed" , logrus.Fields {"error" : err .Error ()})
123- return 2
158+ os .Remove (sbomPath )
159+ return "" , err
124160 }
125161 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
162+ return sbomPath , nil
137163}
138164
139165// parseImageRef splits an image reference into name and tag.
@@ -162,38 +188,31 @@ func parseImageRef(imageRef string) (string, string) {
162188 return imageRef , "latest"
163189}
164190
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 )
191+ type sbomUploadParams struct {
192+ provider string
193+ org string
194+ apiToken string
195+ repoName string
196+ env string
197+ baseURL string
198+ }
171199
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 )
200+ func ( p sbomUploadParams ) uploadURL () string {
201+ base := p . baseURL
202+ if base == "" {
203+ base = "https://app.codacy.com"
176204 }
177- defer sbomFile .Close ()
205+ return fmt .Sprintf ("%s/api/v3/organizations/%s/%s/image-sboms" , base , p .provider , p .org )
206+ }
178207
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- }
208+ func uploadSBOMToCodacy (sbomPath , imageName , tag string , params sbomUploadParams ) error {
209+ url := params .uploadURL ()
186210
187- // Add required fields
188- writer .WriteField ("imageName" , imageName )
189- writer .WriteField ("tag" , tag )
211+ body := & bytes.Buffer {}
212+ writer := multipart .NewWriter (body )
190213
191- // Add optional fields
192- if sbomRepoName != "" {
193- writer .WriteField ("repositoryName" , sbomRepoName )
194- }
195- if sbomEnv != "" {
196- writer .WriteField ("environment" , sbomEnv )
214+ if err := buildSBOMMultipartForm (writer , sbomPath , imageName , tag , params ); err != nil {
215+ return err
197216 }
198217
199218 if err := writer .Close (); err != nil {
@@ -206,9 +225,9 @@ func uploadSBOMToCodacy(sbomPath, imageName, tag string) error {
206225 }
207226 req .Header .Set ("Content-Type" , writer .FormDataContentType ())
208227 req .Header .Set ("Accept" , "application/json" )
209- req .Header .Set ("api-token" , sbomAPIToken )
228+ req .Header .Set ("api-token" , params . apiToken )
210229
211- resp , err := http . DefaultClient .Do (req )
230+ resp , err := sbomHTTPClient .Do (req )
212231 if err != nil {
213232 return fmt .Errorf ("request failed: %w" , err )
214233 }
@@ -221,3 +240,40 @@ func uploadSBOMToCodacy(sbomPath, imageName, tag string) error {
221240
222241 return nil
223242}
243+
244+ // buildSBOMMultipartForm populates the multipart form with the SBOM file and metadata fields.
245+ func buildSBOMMultipartForm (writer * multipart.Writer , sbomPath , imageName , tag string , params sbomUploadParams ) error {
246+ sbomFile , err := os .Open (sbomPath )
247+ if err != nil {
248+ return fmt .Errorf ("failed to open SBOM file: %w" , err )
249+ }
250+ defer sbomFile .Close ()
251+
252+ part , err := writer .CreateFormFile ("sbom" , filepath .Base (sbomPath ))
253+ if err != nil {
254+ return fmt .Errorf ("failed to create form file: %w" , err )
255+ }
256+ if _ , err := io .Copy (part , sbomFile ); err != nil {
257+ return fmt .Errorf ("failed to write SBOM to form: %w" , err )
258+ }
259+
260+ if err := writer .WriteField ("imageName" , imageName ); err != nil {
261+ return fmt .Errorf ("failed to write imageName field: %w" , err )
262+ }
263+ if err := writer .WriteField ("tag" , tag ); err != nil {
264+ return fmt .Errorf ("failed to write tag field: %w" , err )
265+ }
266+
267+ if params .repoName != "" {
268+ if err := writer .WriteField ("repositoryName" , params .repoName ); err != nil {
269+ return fmt .Errorf ("failed to write repositoryName field: %w" , err )
270+ }
271+ }
272+ if params .env != "" {
273+ if err := writer .WriteField ("environment" , params .env ); err != nil {
274+ return fmt .Errorf ("failed to write environment field: %w" , err )
275+ }
276+ }
277+
278+ return nil
279+ }
0 commit comments