-
Notifications
You must be signed in to change notification settings - Fork 10
feat: generate and upload trivy SBOM into codacy #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,223 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| package cmd | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "bytes" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "io" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "mime/multipart" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "net/http" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "codacy/cli-v2/utils/logger" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/fatih/color" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/sirupsen/logrus" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/spf13/cobra" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomAPIToken string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomProvider string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomOrg string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomImageName string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomTag string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomRepoName string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomEnv string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomFormat string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func init() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomAPIToken, "api-token", "a", "", "API token for Codacy API (required)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomProvider, "provider", "p", "", "Git provider (gh, gl, bb) (required)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomOrg, "organization", "o", "", "Organization name on the Git provider (required)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomTag, "tag", "t", "", "Docker image tag (defaults to image tag or 'latest')") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomRepoName, "repository", "r", "", "Repository name (optional)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVarP(&sbomEnv, "environment", "e", "", "Environment where the image is deployed (optional)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.Flags().StringVar(&sbomFormat, "format", "cyclonedx", "SBOM format: cyclonedx or spdx-json (default cyclonedx, smaller output)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.MarkFlagRequired("api-token") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.MarkFlagRequired("provider") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadSBOMCmd.MarkFlagRequired("organization") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| rootCmd.AddCommand(uploadSBOMCmd) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var uploadSBOMCmd = &cobra.Command{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Use: "upload-sbom <IMAGE_NAME>", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Short: "Generate and upload an SBOM for a Docker image to Codacy", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long: `Generate an SBOM (Software Bill of Materials) for a Docker image using Trivy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| and upload it to Codacy for vulnerability tracking. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| By default, Trivy generates a CycloneDX SBOM (smaller output). Use --format | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| to switch to spdx-json if needed. Both formats are accepted by the Codacy API.`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Example: ` # Generate and upload SBOM | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codacy-cli upload-sbom -a <api-token> -p gh -o my-org -r my-repo myapp:latest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Use SPDX format instead | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| codacy-cli upload-sbom -a <api-token> -p gh -o my-org -r my-repo --format spdx-json myapp:v1.0.0`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Args: cobra.ExactArgs(1), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Run: runUploadSBOM, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func runUploadSBOM(_ *cobra.Command, args []string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exitCode := executeUploadSBOM(args[0]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| exitFunc(exitCode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // executeUploadSBOM generates (or reads) an SBOM and uploads it to Codacy. Returns exit code. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func executeUploadSBOM(imageRef string) int { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check warning on line 70 in cmd/upload_sbom.go
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := validateImageName(imageRef); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Error("Invalid image name", logrus.Fields{"image": imageRef, "error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sbomFormat != "cyclonedx" && sbomFormat != "spdx-json" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: --format must be 'cyclonedx' or 'spdx-json'") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| imageName, tag := parseImageRef(imageRef) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sbomTag != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tag = sbomTag | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomImageName = imageName | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Info("Starting SBOM upload", logrus.Fields{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "image": imageRef, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "provider": sbomProvider, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "org": sbomOrg, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Generate SBOM with Trivy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| trivyPath, err := getTrivyPath() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| handleTrivyNotFound(err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tmpFile, err := os.CreateTemp("", "codacy-sbom-*") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Error("Failed to create temp file", logrus.Fields{"error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: Failed to create temporary file: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tmpFile.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomPath := tmpFile.Name() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer os.Remove(sbomPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Generating SBOM for image: %s\n", imageRef) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args := []string{"image", "--format", sbomFormat, "-o", sbomPath, imageRef} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Info("Running Trivy SBOM generation", logrus.Fields{"command": fmt.Sprintf("%s %v", trivyPath, args)}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var stderrBuf bytes.Buffer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := commandRunner.RunWithStderr(trivyPath, args, &stderrBuf); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if isScanFailure(stderrBuf.Bytes()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: Failed to generate SBOM (image not found or no container runtime)") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: Failed to generate SBOM: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Error("Trivy SBOM generation failed", logrus.Fields{"error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Println("SBOM generated successfully") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Upload SBOM to Codacy | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Printf("Uploading SBOM to Codacy (org: %s/%s)...\n", sbomProvider, sbomOrg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := uploadSBOMToCodacy(sbomPath, sbomImageName, tag); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.Error("Failed to upload SBOM", logrus.Fields{"error": err.Error()}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Red("Error: Failed to upload SBOM: %v", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| color.Green("Successfully uploaded SBOM for %s:%s", sbomImageName, tag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // parseImageRef splits an image reference into name and tag. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // e.g. "myapp:v1.0.0" -> ("myapp", "v1.0.0"), "myapp" -> ("myapp", "latest") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func parseImageRef(imageRef string) (string, string) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Handle digest references (image@sha256:...) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if idx := strings.Index(imageRef, "@"); idx != -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return imageRef[:idx], imageRef[idx+1:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find the last colon that is part of the tag (not the registry port) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastSlash := strings.LastIndex(imageRef, "/") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagPart := imageRef | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if lastSlash != -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tagPart = imageRef[lastSlash:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if idx := strings.LastIndex(tagPart, ":"); idx != -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| absIdx := idx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if lastSlash != -1 { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| absIdx = lastSlash + idx | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return imageRef[:absIdx], imageRef[absIdx+1:] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return imageRef, "latest" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func uploadSBOMToCodacy(sbomPath, imageName, tag string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url := fmt.Sprintf("https://app.codacy.com/api/v3/organizations/%s/%s/image-sboms", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomProvider, sbomOrg) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| body := &bytes.Buffer{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer := multipart.NewWriter(body) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add the SBOM file | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sbomFile, err := os.Open(sbomPath) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to open SBOM file: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defer sbomFile.Close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| part, err := writer.CreateFormFile("sbom", filepath.Base(sbomPath)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to create form file: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if _, err := io.Copy(part, sbomFile); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to write SBOM to form: %w", err) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add required fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer.WriteField("imageName", imageName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer.WriteField("tag", tag) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Add optional fields | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sbomRepoName != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer.WriteField("repositoryName", sbomRepoName) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if sbomEnv != "" { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer.WriteField("environment", sbomEnv) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+188
to
+196
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| writer.WriteField("imageName", imageName) | |
| writer.WriteField("tag", tag) | |
| // Add optional fields | |
| if sbomRepoName != "" { | |
| writer.WriteField("repositoryName", sbomRepoName) | |
| } | |
| if sbomEnv != "" { | |
| writer.WriteField("environment", sbomEnv) | |
| if err := writer.WriteField("imageName", imageName); err != nil { | |
| return fmt.Errorf("failed to write imageName field: %w", err) | |
| } | |
| if err := writer.WriteField("tag", tag); err != nil { | |
| return fmt.Errorf("failed to write tag field: %w", err) | |
| } | |
| // Add optional fields | |
| if sbomRepoName != "" { | |
| if err := writer.WriteField("repositoryName", sbomRepoName); err != nil { | |
| return fmt.Errorf("failed to write repositoryName field: %w", err) | |
| } | |
| } | |
| if sbomEnv != "" { | |
| if err := writer.WriteField("environment", sbomEnv); err != nil { | |
| return fmt.Errorf("failed to write environment field: %w", err) | |
| } |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using http.DefaultClient without a timeout can cause the CLI to hang indefinitely on network issues. Prefer a dedicated http.Client{Timeout: ...} (there’s already a 10s timeout used in codacy-client/client.go) and/or allow injecting the client for tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
uploadSBOMToCodacyrelies on package-level globals (sbomProvider,sbomOrg,sbomAPIToken, etc.) instead of its parameters, which makes the function harder to test, non-reentrant, and more error-prone (state leaks between invocations). Consider passing provider/org/token/repo/env (and optionally an*http.Client/base URL) as explicit parameters or encapsulating them in a struct.