diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml index 820f105f..1ee78e08 100644 --- a/.codacy/codacy.yaml +++ b/.codacy/codacy.yaml @@ -6,3 +6,4 @@ tools: - trivy@0.59.1 - pylint@3.3.6 - pmd@6.55.0 + - semgrep@1.78.0 \ No newline at end of file diff --git a/cmd/analyze.go b/cmd/analyze.go index 56dbe9f2..1297e53e 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -226,6 +226,18 @@ func runPylintAnalysis(workDirectory string, pathsToCheck []string, outputFile s } } +func runSemgrepAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) { + semgrep := config.Config.Tools()["semgrep"] + if semgrep == nil { + log.Fatal("Semgrep tool configuration not found") + } + + err := tools.RunSemgrep(workDirectory, semgrep, pathsToCheck, outputFile, outputFormat) + if err != nil { + log.Fatalf("Failed to run Semgrep analysis: %v", err) + } +} + var analyzeCmd = &cobra.Command{ Use: "analyze", Short: "Runs all configured linters.", @@ -312,6 +324,8 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st runPmdAnalysis(workDirectory, args, outputFile, outputFormat) case "pylint": runPylintAnalysis(workDirectory, args, outputFile, outputFormat) + case "semgrep": + runSemgrepAnalysis(workDirectory, args, outputFile, outputFormat) default: log.Printf("Warning: Unsupported tool: %s\n", toolName) } diff --git a/plugins/tool-utils_test.go b/plugins/tool-utils_test.go index 1b931b22..6423ed0d 100644 --- a/plugins/tool-utils_test.go +++ b/plugins/tool-utils_test.go @@ -166,6 +166,7 @@ func TestGetSupportedTools(t *testing.T) { "pmd", "pylint", "trivy", + "semgrep", }, expectedError: false, }, diff --git a/plugins/tools/semgrep/plugin.yaml b/plugins/tools/semgrep/plugin.yaml new file mode 100644 index 00000000..f6ed99ef --- /dev/null +++ b/plugins/tools/semgrep/plugin.yaml @@ -0,0 +1,16 @@ +name: semgrep +description: Static Analysis Security Testing (SAST) tool +runtime: python +runtime_binaries: + package_manager: python3 + execution: python3 +binaries: + - name: python + path: "venv/bin/python3" +formatters: + - name: json + flag: "--json" +output_options: + file_flag: "--output" +analysis_options: + default_path: "." diff --git a/tools/runnerUtils.go b/tools/runnerUtils.go index 46e37986..6ba9201b 100644 --- a/tools/runnerUtils.go +++ b/tools/runnerUtils.go @@ -6,24 +6,26 @@ import ( "path/filepath" ) -// ConfigFileExists checks if a specific configuration file exists in the .codacy/tools-configs/ +// ConfigFileExists checks if any of the specified configuration files exist in the .codacy/tools-configs/ // or on the root of the repository directory. // // Parameters: // - conf: The configuration object containing the tools config directory -// - fileName: The configuration file name to check for +// - fileNames: A list of configuration file names to check for // // Returns: -// - string: The relative path to the configuration file (for cmd args) -// - bool: True if the file exists, false otherwise -func ConfigFileExists(conf config.ConfigType, fileName string) (string, bool) { - generatedConfigFile := filepath.Join(conf.ToolsConfigDirectory(), fileName) - existingConfigFile := filepath.Join(conf.RepositoryDirectory(), fileName) +// - string: The relative path to the first configuration file found (for cmd args) +// - bool: True if any file exists, false otherwise +func ConfigFileExists(conf config.ConfigType, fileNames ...string) (string, bool) { + for _, fileName := range fileNames { + generatedConfigFile := filepath.Join(conf.ToolsConfigDirectory(), fileName) + existingConfigFile := filepath.Join(conf.RepositoryDirectory(), fileName) - if _, err := os.Stat(generatedConfigFile); err == nil { - return generatedConfigFile, true - } else if _, err := os.Stat(existingConfigFile); err == nil { - return existingConfigFile, true + if _, err := os.Stat(generatedConfigFile); err == nil { + return generatedConfigFile, true + } else if _, err := os.Stat(existingConfigFile); err == nil { + return existingConfigFile, true + } } return "", false diff --git a/tools/semgrepRunner.go b/tools/semgrepRunner.go new file mode 100644 index 00000000..64675eb2 --- /dev/null +++ b/tools/semgrepRunner.go @@ -0,0 +1,73 @@ +package tools + +import ( + "codacy/cli-v2/config" + "codacy/cli-v2/plugins" + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// RunSemgrep executes Semgrep analysis on the specified directory +func RunSemgrep(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error { + // Construct base command with -m semgrep to run semgrep module + cmdArgs := []string{"scan"} + + // Add output format if specified + if outputFormat == "sarif" { + cmdArgs = append(cmdArgs, "--sarif") + } + + // Define possible Semgrep config file names + semgrepConfigFiles := []string{".semgrep.yml", ".semgrep.yaml", ".semgrep/semgrep.yml"} + + // Check if a config file exists in the expected location and use it if present + if configFile, exists := ConfigFileExists(config.Config, semgrepConfigFiles...); exists { + cmdArgs = append(cmdArgs, "--config", configFile) + } else { + // add --config auto only if no config file exists + cmdArgs = append(cmdArgs, "--config", "auto") + } + + // Add files to analyze - if no files specified, analyze current directory + if len(files) > 0 { + cmdArgs = append(cmdArgs, files...) + } else { + cmdArgs = append(cmdArgs, ".") + } + + cmdArgs = append(cmdArgs, "--disable-version-check") + + // Get Semgrep binary from the specified installation path + semgrepPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "semgrep") + + // Create Semgrep command + cmd := exec.Command(semgrepPath, cmdArgs...) + cmd.Dir = workDirectory + + if outputFile != "" { + // If output file is specified, create it and redirect output + var outputWriter *os.File + var err error + outputWriter, err = os.Create(filepath.Clean(outputFile)) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outputWriter.Close() + cmd.Stdout = outputWriter + } else { + cmd.Stdout = os.Stdout + } + cmd.Stderr = os.Stderr + + // Run Semgrep + if err := cmd.Run(); err != nil { + // Semgrep returns non-zero exit code when it finds issues, which is expected + if _, ok := err.(*exec.ExitError); !ok { + return fmt.Errorf("failed to run semgrep: %w", err) + } + } + + return nil +} diff --git a/tools/semgrepRunner_test.go b/tools/semgrepRunner_test.go new file mode 100644 index 00000000..235bf0d2 --- /dev/null +++ b/tools/semgrepRunner_test.go @@ -0,0 +1,45 @@ +package tools + +import ( + "codacy/cli-v2/plugins" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRunSemgrepWithSpecificFiles(t *testing.T) { + homeDirectory, err := os.UserHomeDir() + if err != nil { + t.Fatalf("Failed to get home directory: %v", err) + } + currentDirectory, err := os.Getwd() + if err != nil { + t.Fatalf("Failed to get current directory: %v", err) + } + + // Set up test directories and files + testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "semgrep") + tempResultFile := filepath.Join(os.TempDir(), "semgrep-specific.sarif") + defer os.Remove(tempResultFile) + + // Create tool info for semgrep + toolInfo := &plugins.ToolInfo{ + InstallDir: filepath.Join(homeDirectory, ".cache/codacy/tools/semgrep@1.78.0"), + } + + // Specify files to analyze + filesToAnalyze := []string{"sample.js"} + + // Run Semgrep analysis on specific files + err = RunSemgrep(testDirectory, toolInfo, filesToAnalyze, tempResultFile, "sarif") + if err != nil { + t.Fatalf("Failed to run semgrep on specific files: %v", err) + } + + // Verify file exists and has content + fileInfo, err := os.Stat(tempResultFile) + assert.NoError(t, err, "Failed to stat output file") + assert.Greater(t, fileInfo.Size(), int64(0), "Output file should not be empty") +} diff --git a/tools/testdata/repositories/semgrep/expected.sarif b/tools/testdata/repositories/semgrep/expected.sarif new file mode 100644 index 00000000..221a5d5a --- /dev/null +++ b/tools/testdata/repositories/semgrep/expected.sarif @@ -0,0 +1,64 @@ +{ + "version": "2.1.0", + "$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5", + "runs": [ + { + "tool": { + "driver": { + "name": "Semgrep", + "version": "1.41.0", + "informationUri": "https://semgrep.dev", + "rules": [ + { + "id": "hardcoded-credentials", + "name": "Hardcoded Credentials", + "shortDescription": { + "text": "Hardcoded API key detected" + }, + "fullDescription": { + "text": "Found hardcoded API key. This is a security risk." + }, + "defaultConfiguration": { + "level": "warning" + }, + "help": { + "text": "API keys and other credentials should not be hardcoded in source files. Use environment variables or secure credential storage instead." + } + } + ] + } + }, + "artifacts": [ + { + "location": { + "uri": "testdata/repositories/semgrep/sample.js" + } + } + ], + "results": [ + { + "ruleId": "hardcoded-credentials", + "level": "warning", + "message": { + "text": "Hardcoded API key detected" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "testdata/repositories/semgrep/sample.js" + }, + "region": { + "startLine": 3, + "startColumn": 16, + "endLine": 3, + "endColumn": 32 + } + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tools/testdata/repositories/semgrep/sample.js b/tools/testdata/repositories/semgrep/sample.js new file mode 100644 index 00000000..8f430840 --- /dev/null +++ b/tools/testdata/repositories/semgrep/sample.js @@ -0,0 +1,10 @@ +// Sample JavaScript file for Semgrep testing + +const API_KEY = "1234567890abcdef"; // Hardcoded credential for testing + +function helloWorld() { + console.log("Hello, world!"); + console.log("Using API Key:", API_KEY); +} + +helloWorld(); \ No newline at end of file