diff --git a/cmd/upload_sbom.go b/cmd/upload_sbom.go new file mode 100644 index 00000000..6fd2181a --- /dev/null +++ b/cmd/upload_sbom.go @@ -0,0 +1,299 @@ +package cmd + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "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 + sbomBaseURL string + + sbomHTTPClient httpDoer = &http.Client{Timeout: 5 * time.Minute} +) + +// httpDoer abstracts the Do method of http.Client for testing. +type httpDoer interface { + Do(req *http.Request) (*http.Response, error) +} + +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 ", + 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 -p gh -o my-org -r my-repo myapp:latest + + # Use SPDX format instead + codacy-cli upload-sbom -a -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 { + 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) + isDigest := strings.Contains(imageRef, "@") + + if sbomTag != "" { + if isDigest { + color.Red("Error: --tag cannot be used with digest references (image@sha256:...)") + return 2 + } + tag = sbomTag + } + sbomImageName = imageName + + var effectiveImageRef string + if isDigest { + effectiveImageRef = fmt.Sprintf("%s@%s", imageName, tag) + } else { + effectiveImageRef = fmt.Sprintf("%s:%s", imageName, tag) + } + + logger.Info("Starting SBOM upload", logrus.Fields{ + "image": effectiveImageRef, + "provider": sbomProvider, + "org": sbomOrg, + }) + + sbomPath, err := generateSBOM(effectiveImageRef) + if err != nil { + return 2 + } + defer os.Remove(sbomPath) + + fmt.Printf("Uploading SBOM to Codacy (org: %s/%s)...\n", sbomProvider, sbomOrg) + params := sbomUploadParams{ + provider: sbomProvider, + org: sbomOrg, + apiToken: sbomAPIToken, + repoName: sbomRepoName, + env: sbomEnv, + baseURL: sbomBaseURL, + } + if err := uploadSBOMToCodacy(sbomPath, sbomImageName, tag, params); 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", effectiveImageRef) + return 0 +} + +// generateSBOM runs Trivy to generate an SBOM file and returns the path to it. +func generateSBOM(imageRef string) (string, error) { + trivyPath, err := getTrivyPath() + if err != nil { + handleTrivyNotFound(err) + return "", err + } + + 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 "", err + } + tmpFile.Close() + sbomPath := tmpFile.Name() + + 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()}) + os.Remove(sbomPath) + return "", err + } + fmt.Println("SBOM generated successfully") + return sbomPath, nil +} + +// 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" +} + +type sbomUploadParams struct { + provider string + org string + apiToken string + repoName string + env string + baseURL string +} + +func (p sbomUploadParams) uploadURL() string { + base := p.baseURL + if base == "" { + base = "https://app.codacy.com" + } + return fmt.Sprintf("%s/api/v3/organizations/%s/%s/image-sboms", base, p.provider, p.org) +} + +func uploadSBOMToCodacy(sbomPath, imageName, tag string, params sbomUploadParams) error { + url := params.uploadURL() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + if err := buildSBOMMultipartForm(writer, sbomPath, imageName, tag, params); err != nil { + return err + } + + if err := writer.Close(); err != nil { + return fmt.Errorf("failed to close multipart writer: %w", err) + } + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Accept", "application/json") + req.Header.Set("api-token", params.apiToken) + + resp, err := sbomHTTPClient.Do(req) + if err != nil { + return fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("API returned status %d: %s", resp.StatusCode, string(respBody)) + } + + return nil +} + +// buildSBOMMultipartForm populates the multipart form with the SBOM file and metadata fields. +func buildSBOMMultipartForm(writer *multipart.Writer, sbomPath, imageName, tag string, params sbomUploadParams) error { + if err := addSBOMFile(writer, sbomPath); err != nil { + return err + } + + fields := map[string]string{ + "imageName": imageName, + "tag": tag, + } + if params.repoName != "" { + fields["repositoryName"] = params.repoName + } + if params.env != "" { + fields["environment"] = params.env + } + + for name, value := range fields { + if err := writer.WriteField(name, value); err != nil { + return fmt.Errorf("failed to write %s field: %w", name, err) + } + } + + return nil +} + +// addSBOMFile adds the SBOM file to the multipart form. +func addSBOMFile(writer *multipart.Writer, sbomPath string) error { + 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) + } + + return nil +} diff --git a/cmd/upload_sbom_test.go b/cmd/upload_sbom_test.go new file mode 100644 index 00000000..4a4d98df --- /dev/null +++ b/cmd/upload_sbom_test.go @@ -0,0 +1,278 @@ +package cmd + +import ( + "errors" + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type sbomTestState struct { + apiToken string + provider string + org string + repoName string + env string + tag string + format string + baseURL string + httpClient httpDoer +} + +func saveSBOMState() sbomTestState { + return sbomTestState{ + apiToken: sbomAPIToken, + provider: sbomProvider, + org: sbomOrg, + repoName: sbomRepoName, + env: sbomEnv, + tag: sbomTag, + format: sbomFormat, + baseURL: sbomBaseURL, + httpClient: sbomHTTPClient, + } +} + +func (s sbomTestState) restore() { + sbomAPIToken = s.apiToken + sbomProvider = s.provider + sbomOrg = s.org + sbomRepoName = s.repoName + sbomEnv = s.env + sbomTag = s.tag + sbomFormat = s.format + sbomBaseURL = s.baseURL + sbomHTTPClient = s.httpClient +} + +// setSBOMDefaults sets the minimum required SBOM globals for tests +func setSBOMDefaults() { + sbomProvider = "gh" + sbomOrg = "test-org" + sbomAPIToken = "test-token" + sbomRepoName = "" + sbomEnv = "" + sbomTag = "" + sbomFormat = "cyclonedx" + sbomBaseURL = "" +} + +func TestParseImageRef(t *testing.T) { + tests := []struct { + input string + wantName string + wantTag string + }{ + {"myapp:latest", "myapp", "latest"}, + {"myapp:v1.0.0", "myapp", "v1.0.0"}, + {"myapp", "myapp", "latest"}, + {"ghcr.io/codacy/app:v2", "ghcr.io/codacy/app", "v2"}, + {"registry.example.com:5000/myapp:tag", "registry.example.com:5000/myapp", "tag"}, + {"nginx@sha256:abc123", "nginx", "sha256:abc123"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + name, tag := parseImageRef(tt.input) + assert.Equal(t, tt.wantName, name) + assert.Equal(t, tt.wantTag, tag) + }) + } +} + +func TestExecuteUploadSBOM_InvalidImage(t *testing.T) { + state := saveState() + defer state.restore() + + exitCode := executeUploadSBOM("nginx;rm -rf /") + assert.Equal(t, 2, exitCode) +} + +func TestExecuteUploadSBOM_InvalidFormat(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + setSBOMDefaults() + sbomFormat = "invalid-format" + + exitCode := executeUploadSBOM("alpine:latest") + assert.Equal(t, 2, exitCode) +} + +func TestExecuteUploadSBOM_TrivyNotFound(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + var capturedExitCode int + exitFunc = func(code int) { + capturedExitCode = code + } + + getTrivyPathResolver = func() (string, error) { + return "", errors.New("trivy not found") + } + setSBOMDefaults() + + exitCode := executeUploadSBOM("alpine:latest") + assert.Equal(t, 2, capturedExitCode) + assert.Equal(t, 2, exitCode) +} + +func TestExecuteUploadSBOM_TrivyGenerationFails(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + getTrivyPathResolver = func() (string, error) { + return "/usr/local/bin/trivy", nil + } + + mockRunner := &MockCommandRunner{ + RunWithStderrFunc: func(_ string, _ []string, stderr io.Writer) error { + if stderr != nil { + _, _ = stderr.Write([]byte("FATAL Fatal error")) + } + return &mockExitError{code: 1} + }, + } + commandRunner = mockRunner + setSBOMDefaults() + + exitCode := executeUploadSBOM("alpine:latest") + assert.Equal(t, 2, exitCode) +} + +func TestExecuteUploadSBOM_TrivyCalledWithCorrectFormat(t *testing.T) { + formats := []string{"cyclonedx", "spdx-json"} + + for _, format := range formats { + t.Run(format, func(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + getTrivyPathResolver = func() (string, error) { + return "/usr/local/bin/trivy", nil + } + + mockRunner := &MockCommandRunner{ + RunWithStderrFunc: func(_ string, args []string, _ io.Writer) error { + for i, arg := range args { + if arg == "-o" && i+1 < len(args) { + _ = os.WriteFile(args[i+1], []byte(`{}`), 0644) + break + } + } + return nil + }, + } + commandRunner = mockRunner + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + setSBOMDefaults() + sbomFormat = format + sbomBaseURL = server.URL + sbomHTTPClient = server.Client() + + exitCode := executeUploadSBOM("alpine:latest") + assert.Equal(t, 0, exitCode) + + assert.Len(t, mockRunner.Calls, 1) + assert.Contains(t, mockRunner.Calls[0].Args, "--format") + assert.Contains(t, mockRunner.Calls[0].Args, format) + }) + } +} + +func TestExecuteUploadSBOM_DigestImagePassedCorrectly(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + getTrivyPathResolver = func() (string, error) { + return "/usr/local/bin/trivy", nil + } + + var capturedImageRef string + mockRunner := &MockCommandRunner{ + RunWithStderrFunc: func(_ string, args []string, _ io.Writer) error { + capturedImageRef = args[len(args)-1] + for i, arg := range args { + if arg == "-o" && i+1 < len(args) { + os.WriteFile(args[i+1], []byte(`{}`), 0644) + break + } + } + return nil + }, + } + commandRunner = mockRunner + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + setSBOMDefaults() + sbomBaseURL = server.URL + sbomHTTPClient = server.Client() + + exitCode := executeUploadSBOM("nginx@sha256:abc123def456") + assert.Equal(t, 0, exitCode) + assert.Equal(t, "nginx@sha256:abc123def456", capturedImageRef) +} + +func TestExecuteUploadSBOM_DigestWithTagRejected(t *testing.T) { + state := saveState() + defer state.restore() + ss := saveSBOMState() + defer ss.restore() + + setSBOMDefaults() + sbomTag = "latest" + + exitCode := executeUploadSBOM("nginx@sha256:abc123def456") + assert.Equal(t, 2, exitCode) +} + +func TestUploadSBOMToCodacy_FileNotFound(t *testing.T) { + params := sbomUploadParams{ + provider: "gh", + org: "test-org", + apiToken: "test-token", + } + err := uploadSBOMToCodacy("/nonexistent/file.json", "myapp", "latest", params) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to open SBOM file") +} + +func TestUploadSBOMSkipsValidation(t *testing.T) { + result := shouldSkipValidation("upload-sbom") + assert.True(t, result, "upload-sbom should skip validation") +} + +func TestUploadSBOMCommandRequiresArg(t *testing.T) { + err := uploadSBOMCmd.Args(uploadSBOMCmd, []string{}) + assert.Error(t, err, "Should error when no args provided") + + err = uploadSBOMCmd.Args(uploadSBOMCmd, []string{"myapp:latest"}) + assert.NoError(t, err, "Should accept single image") + + err = uploadSBOMCmd.Args(uploadSBOMCmd, []string{"img1", "img2"}) + assert.Error(t, err, "Should error when multiple args provided") +} diff --git a/cmd/validation.go b/cmd/validation.go index ea3cea74..8936222e 100644 --- a/cmd/validation.go +++ b/cmd/validation.go @@ -84,6 +84,7 @@ func shouldSkipValidation(cmdName string) bool { "codacy-cli", // root command when called without subcommands "update", "container-scan", // container scanning doesn't need codacy.yaml + "upload-sbom", // SBOM upload doesn't need codacy.yaml } for _, skipCmd := range skipCommands { diff --git a/integration-tests/init-with-token/expected/codacy.yaml b/integration-tests/init-with-token/expected/codacy.yaml index c610a1a8..d5754b6a 100644 --- a/integration-tests/init-with-token/expected/codacy.yaml +++ b/integration-tests/init-with-token/expected/codacy.yaml @@ -5,7 +5,7 @@ runtimes: tools: - eslint@8.57.0 - lizard@1.17.31 - - opengrep@1.16.4 + - opengrep@1.17.0 - pmd@6.55.0 - pylint@3.3.9 - trivy@0.69.3 diff --git a/plugins/tools/trivy/test/expected.sarif b/plugins/tools/trivy/test/expected.sarif index 4a542dba..264569c0 100644 --- a/plugins/tools/trivy/test/expected.sarif +++ b/plugins/tools/trivy/test/expected.sarif @@ -34,7 +34,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2021-33203\nSeverity: MEDIUM\nFixed Version: 2.2.24, 3.1.12, 3.2.4\nLink: [CVE-2021-33203](https://avd.aquasec.com/nvd/cve-2021-33203)" }, "ruleId": "CVE-2021-33203", - "ruleIndex": 12 + "ruleIndex": 14 }, { "level": "error", @@ -61,7 +61,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2022-36359\nSeverity: HIGH\nFixed Version: 3.2.15, 4.0.7\nLink: [CVE-2022-36359](https://avd.aquasec.com/nvd/cve-2022-36359)" }, "ruleId": "CVE-2022-36359", - "ruleIndex": 9 + "ruleIndex": 11 }, { "level": "error", @@ -88,7 +88,7 @@ "text": "Package: cross-spawn\nInstalled Version: 7.0.3\nVulnerability CVE-2024-21538\nSeverity: HIGH\nFixed Version: 7.0.5, 6.0.6\nLink: [CVE-2024-21538](https://avd.aquasec.com/nvd/cve-2024-21538)" }, "ruleId": "CVE-2024-21538", - "ruleIndex": 2 + "ruleIndex": 3 }, { "level": "warning", @@ -115,7 +115,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2024-45231\nSeverity: MEDIUM\nFixed Version: 5.1.1, 5.0.9, 4.2.16\nLink: [CVE-2024-45231](https://avd.aquasec.com/nvd/cve-2024-45231)" }, "ruleId": "CVE-2024-45231", - "ruleIndex": 13 + "ruleIndex": 15 }, { "level": "warning", @@ -142,7 +142,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2025-48432\nSeverity: MEDIUM\nFixed Version: 5.2.2, 5.1.10, 4.2.22\nLink: [CVE-2025-48432](https://avd.aquasec.com/nvd/cve-2025-48432)" }, "ruleId": "CVE-2025-48432", - "ruleIndex": 14 + "ruleIndex": 16 }, { "level": "error", @@ -169,7 +169,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2025-57833\nSeverity: HIGH\nFixed Version: 4.2.24, 5.1.12, 5.2.6\nLink: [CVE-2025-57833](https://avd.aquasec.com/nvd/cve-2025-57833)" }, "ruleId": "CVE-2025-57833", - "ruleIndex": 10 + "ruleIndex": 12 }, { "level": "note", @@ -196,7 +196,7 @@ "text": "Package: brace-expansion\nInstalled Version: 1.1.11\nVulnerability CVE-2025-5889\nSeverity: LOW\nFixed Version: 2.0.2, 1.1.12, 3.0.1, 4.0.1\nLink: [CVE-2025-5889](https://avd.aquasec.com/nvd/cve-2025-5889)" }, "ruleId": "CVE-2025-5889", - "ruleIndex": 1 + "ruleIndex": 2 }, { "level": "error", @@ -223,7 +223,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2025-64458\nSeverity: HIGH\nFixed Version: 5.2.8, 5.1.14, 4.2.26\nLink: [CVE-2025-64458](https://avd.aquasec.com/nvd/cve-2025-64458)" }, "ruleId": "CVE-2025-64458", - "ruleIndex": 11 + "ruleIndex": 13 }, { "level": "error", @@ -250,7 +250,7 @@ "text": "Package: django\nInstalled Version: 1.11.29\nVulnerability CVE-2025-64459\nSeverity: CRITICAL\nFixed Version: 5.2.8, 5.1.14, 4.2.26\nLink: [CVE-2025-64459](https://avd.aquasec.com/nvd/cve-2025-64459)" }, "ruleId": "CVE-2025-64459", - "ruleIndex": 8 + "ruleIndex": 10 }, { "level": "warning", @@ -277,7 +277,7 @@ "text": "Package: js-yaml\nInstalled Version: 4.1.0\nVulnerability CVE-2025-64718\nSeverity: MEDIUM\nFixed Version: 4.1.1, 3.14.2\nLink: [CVE-2025-64718](https://avd.aquasec.com/nvd/cve-2025-64718)" }, "ruleId": "CVE-2025-64718", - "ruleIndex": 4 + "ruleIndex": 6 }, { "level": "warning", @@ -331,7 +331,7 @@ "text": "Package: minimatch\nInstalled Version: 3.1.2\nVulnerability CVE-2026-26996\nSeverity: HIGH\nFixed Version: 10.2.1, 9.0.6, 8.0.5, 7.4.7, 6.2.1, 5.1.7, 4.2.4, 3.1.3\nLink: [CVE-2026-26996](https://avd.aquasec.com/nvd/cve-2026-26996)" }, "ruleId": "CVE-2026-26996", - "ruleIndex": 5 + "ruleIndex": 7 }, { "level": "error", @@ -358,7 +358,7 @@ "text": "Package: minimatch\nInstalled Version: 3.1.2\nVulnerability CVE-2026-27903\nSeverity: HIGH\nFixed Version: 10.2.3, 9.0.7, 8.0.6, 7.4.8, 6.2.2, 5.1.8, 4.2.5, 3.1.3\nLink: [CVE-2026-27903](https://avd.aquasec.com/nvd/cve-2026-27903)" }, "ruleId": "CVE-2026-27903", - "ruleIndex": 6 + "ruleIndex": 8 }, { "level": "error", @@ -385,7 +385,7 @@ "text": "Package: minimatch\nInstalled Version: 3.1.2\nVulnerability CVE-2026-27904\nSeverity: HIGH\nFixed Version: 10.2.3, 9.0.7, 8.0.6, 7.4.8, 6.2.2, 5.1.8, 4.2.5, 3.1.4\nLink: [CVE-2026-27904](https://avd.aquasec.com/nvd/cve-2026-27904)" }, "ruleId": "CVE-2026-27904", - "ruleIndex": 7 + "ruleIndex": 9 }, { "level": "error", @@ -412,7 +412,61 @@ "text": "Package: flatted\nInstalled Version: 3.3.1\nVulnerability CVE-2026-32141\nSeverity: HIGH\nFixed Version: 3.4.0\nLink: [CVE-2026-32141](https://avd.aquasec.com/nvd/cve-2026-32141)" }, "ruleId": "CVE-2026-32141", - "ruleIndex": 3 + "ruleIndex": 4 + }, + { + "level": "error", + "locations": [ + { + "message": { + "text": "package-lock.json: flatted@3.3.1" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "package-lock.json", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 823, + "startColumn": 1, + "startLine": 819 + } + } + } + ], + "message": { + "text": "Package: flatted\nInstalled Version: 3.3.1\nVulnerability CVE-2026-33228\nSeverity: HIGH\nFixed Version: 3.4.2\nLink: [CVE-2026-33228](https://avd.aquasec.com/nvd/cve-2026-33228)" + }, + "ruleId": "CVE-2026-33228", + "ruleIndex": 5 + }, + { + "level": "warning", + "locations": [ + { + "message": { + "text": "package-lock.json: brace-expansion@1.1.11" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "package-lock.json", + "uriBaseId": "ROOTPATH" + }, + "region": { + "endColumn": 1, + "endLine": 357, + "startColumn": 1, + "startLine": 349 + } + } + } + ], + "message": { + "text": "Package: brace-expansion\nInstalled Version: 1.1.11\nVulnerability CVE-2026-33750\nSeverity: MEDIUM\nFixed Version: 5.0.5, 3.0.2, 2.0.3, 1.1.13\nLink: [CVE-2026-33750](https://avd.aquasec.com/nvd/cve-2026-33750)" + }, + "ruleId": "CVE-2026-33750", + "ruleIndex": 1 } ], "tool": { diff --git a/tools/lizard/lizardConfigCreator.go b/tools/lizard/lizardConfigCreator.go index a55c5b16..c3998be4 100644 --- a/tools/lizard/lizardConfigCreator.go +++ b/tools/lizard/lizardConfigCreator.go @@ -19,7 +19,7 @@ func CreateLizardConfig(toolsConfigDir string, patterns []domain.PatternConfigur for _, pattern := range patterns { patternDefinition := pattern.PatternDefinition metricType := getMetricTypeFromPatternId(patternDefinition.Id) - + if metricType == "" { fmt.Printf("Warning: Invalid pattern ID format: %s\n", patternDefinition.Id) continue