From d31122daf9254be53ff9c4380cdeaa51be986928 Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 13:00:50 +0200 Subject: [PATCH 1/7] feature: analyze all from config --- cmd/analyze.go | 157 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 136 insertions(+), 21 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 8821d501..68f87898 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -9,6 +9,7 @@ import ( "log" "net/http" "os" + "path/filepath" "github.com/spf13/cobra" ) @@ -92,7 +93,7 @@ type Pattern struct { func init() { analyzeCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file for analysis results") - analyzeCmd.Flags().StringVarP(&toolToAnalyze, "tool", "t", "", "Which tool to run analysis with") + analyzeCmd.Flags().StringVarP(&toolToAnalyze, "tool", "t", "", "Optional: Specific tool to run analysis with. If not specified, all configured tools will be run") analyzeCmd.Flags().StringVar(&outputFormat, "format", "", "Output format (use 'sarif' for SARIF format)") analyzeCmd.Flags().BoolVar(&autoFix, "fix", false, "Apply auto fix to your issues when available") rootCmd.AddCommand(analyzeCmd) @@ -224,35 +225,149 @@ func runPylintAnalysis(workDirectory string, pathsToCheck []string, outputFile s var analyzeCmd = &cobra.Command{ Use: "analyze", - Short: "Runs all linters.", - Long: "Runs all tools for all runtimes.", + Short: "Runs all configured linters.", + Long: "Runs all configured tools for code analysis. Use --tool flag to run a specific tool.", Run: func(cmd *cobra.Command, args []string) { workDirectory, err := os.Getwd() if err != nil { log.Fatal(err) } - log.Printf("Running %s...\n", toolToAnalyze) - if outputFormat == "sarif" { - log.Println("Output will be in SARIF format") + // If a specific tool is specified, only run that tool + if toolToAnalyze != "" { + log.Printf("Running %s...\n", toolToAnalyze) + runTool(workDirectory, toolToAnalyze, args, outputFile) + return } - if outputFile != "" { - log.Println("Output will be available at", outputFile) + + // Run all configured tools + tools := config.Config.Tools() + if len(tools) == 0 { + log.Fatal("No tools configured. Please run 'codacy-cli init' and 'codacy-cli install' first") } - switch toolToAnalyze { - case "eslint": - runEslintAnalysis(workDirectory, args, autoFix, outputFile, outputFormat) - case "trivy": - runTrivyAnalysis(workDirectory, args, outputFile, outputFormat) - case "pmd": - runPmdAnalysis(workDirectory, args, outputFile, outputFormat) - case "pylint": - runPylintAnalysis(workDirectory, args, outputFile, outputFormat) - case "": - log.Fatal("You need to specify a tool to run analysis with, e.g., '--tool eslint'") - default: - log.Fatal("Trying to run unsupported tool: ", toolToAnalyze) + log.Println("Running all configured tools...") + + if outputFormat == "sarif" { + // Create temporary directory for individual tool outputs + tmpDir, err := os.MkdirTemp("", "codacy-analysis-*") + if err != nil { + log.Fatalf("Failed to create temporary directory: %v", err) + } + defer os.RemoveAll(tmpDir) + + var sarifOutputs []string + for toolName := range tools { + log.Printf("Running %s...\n", toolName) + tmpFile := filepath.Join(tmpDir, fmt.Sprintf("%s.sarif", toolName)) + runTool(workDirectory, toolName, args, tmpFile) + sarifOutputs = append(sarifOutputs, tmpFile) + } + + // Merge all SARIF outputs + if err := mergeSarifOutputs(sarifOutputs, outputFile); err != nil { + log.Fatalf("Failed to merge SARIF outputs: %v", err) + } + } else { + // Run tools without merging outputs + for toolName := range tools { + log.Printf("Running %s...\n", toolName) + runTool(workDirectory, toolName, args, outputFile) + } } }, } + +func runTool(workDirectory string, toolName string, args []string, outputFile string) { + switch toolName { + case "eslint": + runEslintAnalysis(workDirectory, args, autoFix, outputFile, outputFormat) + case "trivy": + runTrivyAnalysis(workDirectory, args, outputFile, outputFormat) + case "pmd": + runPmdAnalysis(workDirectory, args, outputFile, outputFormat) + case "pylint": + runPylintAnalysis(workDirectory, args, outputFile, outputFormat) + default: + log.Printf("Warning: Unsupported tool: %s\n", toolName) + } +} + +func mergeSarifOutputs(inputFiles []string, outputFile string) error { + var mergedSarif Sarif + mergedSarif.Runs = make([]struct { + Tool struct { + Driver struct { + Name string `json:"name"` + Version string `json:"version"` + Rules []struct { + ID string `json:"id"` + HelpURI string `json:"helpUri"` + ShortDescription struct { + Text string `json:"text"` + } `json:"shortDescription"` + } `json:"rules"` + } `json:"driver"` + } `json:"tool"` + Artifacts []struct { + Location struct { + URI string `json:"uri"` + } `json:"location"` + } `json:"artifacts"` + Results []struct { + Level string `json:"level"` + Message struct { + Text string `json:"text"` + } `json:"message"` + Locations []struct { + PhysicalLocation struct { + ArtifactLocation struct { + URI string `json:"uri"` + Index int `json:"index"` + } `json:"artifactLocation"` + Region struct { + StartLine int `json:"startLine"` + StartColumn int `json:"startColumn"` + EndLine int `json:"endLine"` + EndColumn int `json:"endColumn"` + } `json:"region"` + } `json:"physicalLocation"` + } `json:"locations"` + RuleID string `json:"ruleId"` + RuleIndex int `json:"ruleIndex"` + } `json:"results"` + }, 0) + + for _, file := range inputFiles { + data, err := os.ReadFile(file) + if err != nil { + if os.IsNotExist(err) { + // Skip if file doesn't exist (tool might have failed) + continue + } + return fmt.Errorf("failed to read SARIF file %s: %w", file, err) + } + + var sarif Sarif + if err := json.Unmarshal(data, &sarif); err != nil { + return fmt.Errorf("failed to parse SARIF file %s: %w", file, err) + } + + mergedSarif.Runs = append(mergedSarif.Runs, sarif.Runs...) + } + + // Create output file + out, err := os.Create(outputFile) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer out.Close() + + encoder := json.NewEncoder(out) + encoder.SetIndent("", " ") + if err := encoder.Encode(mergedSarif); err != nil { + return fmt.Errorf("failed to write merged SARIF: %w", err) + } + + return nil +} From 1700ee64272241c1ce43511a9b6c383a3acb7926 Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 13:50:10 +0200 Subject: [PATCH 2/7] refactor: move SARIF merging logic to utils package - Moved the `mergeSarifOutputs` function from `analyze.go` to `utils/sarif.go` for better code organization. - Updated the `analyze.go` file to use the new utility function for merging SARIF outputs. - Enhanced the `Rule` struct in the utils package to use `map[string]interface{}` for properties. --- cmd/analyze.go | 83 ++------------------------------- results.sarif | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ utils/sarif.go | 49 ++++++++++++++++++-- 3 files changed, 171 insertions(+), 83 deletions(-) create mode 100644 results.sarif diff --git a/cmd/analyze.go b/cmd/analyze.go index 68f87898..8aae2996 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -11,6 +11,8 @@ import ( "os" "path/filepath" + "codacy/cli-v2/utils" + "github.com/spf13/cobra" ) @@ -265,7 +267,7 @@ var analyzeCmd = &cobra.Command{ } // Merge all SARIF outputs - if err := mergeSarifOutputs(sarifOutputs, outputFile); err != nil { + if err := utils.MergeSarifOutputs(sarifOutputs, outputFile); err != nil { log.Fatalf("Failed to merge SARIF outputs: %v", err) } } else { @@ -292,82 +294,3 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st log.Printf("Warning: Unsupported tool: %s\n", toolName) } } - -func mergeSarifOutputs(inputFiles []string, outputFile string) error { - var mergedSarif Sarif - mergedSarif.Runs = make([]struct { - Tool struct { - Driver struct { - Name string `json:"name"` - Version string `json:"version"` - Rules []struct { - ID string `json:"id"` - HelpURI string `json:"helpUri"` - ShortDescription struct { - Text string `json:"text"` - } `json:"shortDescription"` - } `json:"rules"` - } `json:"driver"` - } `json:"tool"` - Artifacts []struct { - Location struct { - URI string `json:"uri"` - } `json:"location"` - } `json:"artifacts"` - Results []struct { - Level string `json:"level"` - Message struct { - Text string `json:"text"` - } `json:"message"` - Locations []struct { - PhysicalLocation struct { - ArtifactLocation struct { - URI string `json:"uri"` - Index int `json:"index"` - } `json:"artifactLocation"` - Region struct { - StartLine int `json:"startLine"` - StartColumn int `json:"startColumn"` - EndLine int `json:"endLine"` - EndColumn int `json:"endColumn"` - } `json:"region"` - } `json:"physicalLocation"` - } `json:"locations"` - RuleID string `json:"ruleId"` - RuleIndex int `json:"ruleIndex"` - } `json:"results"` - }, 0) - - for _, file := range inputFiles { - data, err := os.ReadFile(file) - if err != nil { - if os.IsNotExist(err) { - // Skip if file doesn't exist (tool might have failed) - continue - } - return fmt.Errorf("failed to read SARIF file %s: %w", file, err) - } - - var sarif Sarif - if err := json.Unmarshal(data, &sarif); err != nil { - return fmt.Errorf("failed to parse SARIF file %s: %w", file, err) - } - - mergedSarif.Runs = append(mergedSarif.Runs, sarif.Runs...) - } - - // Create output file - out, err := os.Create(outputFile) - if err != nil { - return fmt.Errorf("failed to create output file: %w", err) - } - defer out.Close() - - encoder := json.NewEncoder(out) - encoder.SetIndent("", " ") - if err := encoder.Encode(mergedSarif); err != nil { - return fmt.Errorf("failed to write merged SARIF: %w", err) - } - - return nil -} diff --git a/results.sarif b/results.sarif new file mode 100644 index 00000000..b68c2b30 --- /dev/null +++ b/results.sarif @@ -0,0 +1,122 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "name": "Trivy", + "version": "0.59.1", + "informationUri": "https://github.com/aquasecurity/trivy", + "rules": [ + { + "id": "CVE-2024-21538", + "shortDescription": { + "text": "cross-spawn: regular expression denial of service" + }, + "properties": { + "precision": "very-high", + "security-severity": "7.5", + "tags": [ + "vulnerability", + "security", + "HIGH" + ] + } + } + ] + } + }, + "results": [ + { + "ruleId": "CVE-2024-21538", + "level": "error", + "message": { + "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)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "tools/testdata/repositories/trivy/src/package-lock.json" + }, + "region": { + "startLine": 515, + "startColumn": 1 + } + } + } + ] + } + ] + }, + { + "tool": { + "driver": { + "name": "Pylint", + "version": "3.3.6", + "informationUri": "https://pylint.org", + "rules": null + } + }, + "results": [] + }, + { + "tool": { + "driver": { + "name": "PMD", + "version": "6.55.0", + "informationUri": "https://pmd.github.io/pmd/", + "rules": [ + { + "id": "UnusedPrivateField", + "shortDescription": { + "text": "Avoid unused private fields such as 'x'." + }, + "properties": { + "priority": 3, + "ruleset": "Best Practices", + "tags": [ + "Best Practices" + ] + } + } + ] + } + }, + "results": [ + { + "ruleId": "UnusedPrivateField", + "level": "", + "message": { + "text": "Avoid unused private fields such as 'x'." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "/Users/czak/GIT/codacy/codacy-cli-v2/tools/testdata/repositories/pmd/RulesBreaker.java" + }, + "region": { + "startLine": 18, + "startColumn": 17 + } + } + } + ] + } + ] + }, + { + "tool": { + "driver": { + "name": "ESLint", + "version": "9.3.0", + "informationUri": "https://eslint.org", + "rules": [] + } + }, + "results": [] + } + ] +} diff --git a/utils/sarif.go b/utils/sarif.go index a2611da2..fb2b012b 100644 --- a/utils/sarif.go +++ b/utils/sarif.go @@ -2,6 +2,8 @@ package utils import ( "encoding/json" + "fmt" + "os" ) // PylintIssue represents a single issue in Pylint's JSON output @@ -41,9 +43,9 @@ type Driver struct { } type Rule struct { - ID string `json:"id"` - ShortDescription MessageText `json:"shortDescription"` - Properties map[string]string `json:"properties"` + ID string `json:"id"` + ShortDescription MessageText `json:"shortDescription"` + Properties map[string]interface{} `json:"properties"` } type Result struct { @@ -169,3 +171,44 @@ func createEmptySarifReport() []byte { sarifData, _ := json.MarshalIndent(emptyReport, "", " ") return sarifData } + +// MergeSarifOutputs combines multiple SARIF files into a single output file +func MergeSarifOutputs(inputFiles []string, outputFile string) error { + var mergedSarif SarifReport + mergedSarif.Version = "2.1.0" + mergedSarif.Schema = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json" + mergedSarif.Runs = make([]Run, 0) + + for _, file := range inputFiles { + data, err := os.ReadFile(file) + if err != nil { + if os.IsNotExist(err) { + // Skip if file doesn't exist (tool might have failed) + continue + } + return fmt.Errorf("failed to read SARIF file %s: %w", file, err) + } + + var sarif SarifReport + if err := json.Unmarshal(data, &sarif); err != nil { + return fmt.Errorf("failed to parse SARIF file %s: %w", file, err) + } + + mergedSarif.Runs = append(mergedSarif.Runs, sarif.Runs...) + } + + // Create output file + out, err := os.Create(outputFile) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer out.Close() + + encoder := json.NewEncoder(out) + encoder.SetIndent("", " ") + if err := encoder.Encode(mergedSarif); err != nil { + return fmt.Errorf("failed to write merged SARIF: %w", err) + } + + return nil +} From f92c728e4f786f7c640d93d7144f694c7f6ec279 Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 13:51:31 +0200 Subject: [PATCH 3/7] try analyze in it test --- .github/workflows/go.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9e9f5372..d7b0583a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -81,6 +81,7 @@ jobs: if: matrix.os != 'windows-latest' run: | ./cli-v2 install + ./cli-v2 analyze --format sarif --output results.sarif # Disable windows it test for now. # - name: Install dependencies from .codacy/codacy.yaml (Windows) # if: matrix.os == 'windows-latest' From ad95c0918dd83e88cf8712882bcb95fd2278cc0c Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 13:56:20 +0200 Subject: [PATCH 4/7] fix add init --- .github/workflows/go.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index d7b0583a..9c192ba1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -80,7 +80,8 @@ jobs: - name: Install dependencies from .codacy/codacy.yaml if: matrix.os != 'windows-latest' run: | - ./cli-v2 install + ./cli-v2 install + ./cli-v2 init ./cli-v2 analyze --format sarif --output results.sarif # Disable windows it test for now. # - name: Install dependencies from .codacy/codacy.yaml (Windows) From 082fa35292a21349556fa02ea88709445302322b Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 15:01:45 +0200 Subject: [PATCH 5/7] revert it test --- .github/workflows/go.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9c192ba1..9e9f5372 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -80,9 +80,7 @@ jobs: - name: Install dependencies from .codacy/codacy.yaml if: matrix.os != 'windows-latest' run: | - ./cli-v2 install - ./cli-v2 init - ./cli-v2 analyze --format sarif --output results.sarif + ./cli-v2 install # Disable windows it test for now. # - name: Install dependencies from .codacy/codacy.yaml (Windows) # if: matrix.os == 'windows-latest' From 14658d743461fbbedeaec55f181f5c6d83f834fc Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 15:04:27 +0200 Subject: [PATCH 6/7] Remove results.sarif --- results.sarif | 122 -------------------------------------------------- 1 file changed, 122 deletions(-) delete mode 100644 results.sarif diff --git a/results.sarif b/results.sarif deleted file mode 100644 index b68c2b30..00000000 --- a/results.sarif +++ /dev/null @@ -1,122 +0,0 @@ -{ - "version": "2.1.0", - "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", - "runs": [ - { - "tool": { - "driver": { - "name": "Trivy", - "version": "0.59.1", - "informationUri": "https://github.com/aquasecurity/trivy", - "rules": [ - { - "id": "CVE-2024-21538", - "shortDescription": { - "text": "cross-spawn: regular expression denial of service" - }, - "properties": { - "precision": "very-high", - "security-severity": "7.5", - "tags": [ - "vulnerability", - "security", - "HIGH" - ] - } - } - ] - } - }, - "results": [ - { - "ruleId": "CVE-2024-21538", - "level": "error", - "message": { - "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)" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "tools/testdata/repositories/trivy/src/package-lock.json" - }, - "region": { - "startLine": 515, - "startColumn": 1 - } - } - } - ] - } - ] - }, - { - "tool": { - "driver": { - "name": "Pylint", - "version": "3.3.6", - "informationUri": "https://pylint.org", - "rules": null - } - }, - "results": [] - }, - { - "tool": { - "driver": { - "name": "PMD", - "version": "6.55.0", - "informationUri": "https://pmd.github.io/pmd/", - "rules": [ - { - "id": "UnusedPrivateField", - "shortDescription": { - "text": "Avoid unused private fields such as 'x'." - }, - "properties": { - "priority": 3, - "ruleset": "Best Practices", - "tags": [ - "Best Practices" - ] - } - } - ] - } - }, - "results": [ - { - "ruleId": "UnusedPrivateField", - "level": "", - "message": { - "text": "Avoid unused private fields such as 'x'." - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "/Users/czak/GIT/codacy/codacy-cli-v2/tools/testdata/repositories/pmd/RulesBreaker.java" - }, - "region": { - "startLine": 18, - "startColumn": 17 - } - } - } - ] - } - ] - }, - { - "tool": { - "driver": { - "name": "ESLint", - "version": "9.3.0", - "informationUri": "https://eslint.org", - "rules": [] - } - }, - "results": [] - } - ] -} From 3cdee94996905932023af0c21dcc069a73fee548 Mon Sep 17 00:00:00 2001 From: "andrzej.janczak" Date: Tue, 8 Apr 2025 16:17:20 +0200 Subject: [PATCH 7/7] improve sarif --- utils/sarif.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/utils/sarif.go b/utils/sarif.go index fb2b012b..af1d2cb2 100644 --- a/utils/sarif.go +++ b/utils/sarif.go @@ -43,9 +43,18 @@ type Driver struct { } type Rule struct { - ID string `json:"id"` - ShortDescription MessageText `json:"shortDescription"` - Properties map[string]interface{} `json:"properties"` + ID string `json:"id"` + ShortDescription MessageText `json:"shortDescription"` + Properties RuleProperties `json:"properties"` +} + +type RuleProperties struct { + Priority int `json:"priority,omitempty"` + Ruleset string `json:"ruleset,omitempty"` + Tags []string `json:"tags,omitempty"` + // Add other common properties that might be present + Precision string `json:"precision,omitempty"` + SecuritySeverity string `json:"security-severity,omitempty"` } type Result struct {