Skip to content

Commit 31460f1

Browse files
Semgrep installation and running (#73) [Pluto-1390]
1 parent 79d7df5 commit 31460f1

File tree

9 files changed

+238
-12
lines changed

9 files changed

+238
-12
lines changed

.codacy/codacy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ tools:
88
99
1010
11+

cmd/analyze.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,18 @@ func runDartAnalyzer(workDirectory string, pathsToCheck []string, outputFile str
222222
return tools.RunDartAnalyzer(workDirectory, dartanalyzer.InstallDir, dartanalyzer.Binaries["dart"], pathsToCheck, outputFile, outputFormat)
223223
}
224224

225+
func runSemgrepAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) {
226+
semgrep := config.Config.Tools()["semgrep"]
227+
if semgrep == nil {
228+
log.Fatal("Semgrep tool configuration not found")
229+
}
230+
231+
err := tools.RunSemgrep(workDirectory, semgrep, pathsToCheck, outputFile, outputFormat)
232+
if err != nil {
233+
log.Fatalf("Failed to run Semgrep analysis: %v", err)
234+
}
235+
}
236+
225237
var analyzeCmd = &cobra.Command{
226238
Use: "analyze",
227239
Short: "Runs all configured linters.",
@@ -311,7 +323,9 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st
311323
case "pmd":
312324
return runPmdAnalysis(workDirectory, args, outputFile, outputFormat)
313325
case "pylint":
314-
return runPylintAnalysis(workDirectory, args, outputFile, outputFormat)
326+
return runPylintAnalysis(workDirectory, args, outputFile, outputFormat)
327+
case "semgrep":
328+
return runSemgrepAnalysis(workDirectory, args, outputFile, outputFormat)
315329
case "dartanalyzer":
316330
return runDartAnalyzer(workDirectory, args, outputFile, outputFormat)
317331
default:

plugins/tool-utils_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ func TestGetSupportedTools(t *testing.T) {
167167
"pylint",
168168
"trivy",
169169
"dartanalyzer",
170+
"semgrep",
170171
},
171172
expectedError: false,
172173
},

plugins/tools/semgrep/plugin.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: semgrep
2+
description: Static Analysis Security Testing (SAST) tool
3+
runtime: python
4+
runtime_binaries:
5+
package_manager: python3
6+
execution: python3
7+
binaries:
8+
- name: python
9+
path: "venv/bin/python3"
10+
formatters:
11+
- name: json
12+
flag: "--json"
13+
output_options:
14+
file_flag: "--output"
15+
analysis_options:
16+
default_path: "."

tools/runnerUtils.go

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,26 @@ import (
66
"path/filepath"
77
)
88

9-
// ConfigFileExists checks if a specific configuration file exists in the .codacy/tools-configs/
9+
// ConfigFileExists checks if any of the specified configuration files exist in the .codacy/tools-configs/
1010
// or on the root of the repository directory.
1111
//
1212
// Parameters:
1313
// - conf: The configuration object containing the tools config directory
14-
// - fileName: The configuration file name to check for
14+
// - fileNames: A list of configuration file names to check for
1515
//
1616
// Returns:
17-
// - string: The relative path to the configuration file (for cmd args)
18-
// - bool: True if the file exists, false otherwise
19-
func ConfigFileExists(conf config.ConfigType, fileName string) (string, bool) {
20-
generatedConfigFile := filepath.Join(conf.ToolsConfigDirectory(), fileName)
21-
existingConfigFile := filepath.Join(conf.RepositoryDirectory(), fileName)
17+
// - string: The relative path to the first configuration file found (for cmd args)
18+
// - bool: True if any file exists, false otherwise
19+
func ConfigFileExists(conf config.ConfigType, fileNames ...string) (string, bool) {
20+
for _, fileName := range fileNames {
21+
generatedConfigFile := filepath.Join(conf.ToolsConfigDirectory(), fileName)
22+
existingConfigFile := filepath.Join(conf.RepositoryDirectory(), fileName)
2223

23-
if _, err := os.Stat(generatedConfigFile); err == nil {
24-
return generatedConfigFile, true
25-
} else if _, err := os.Stat(existingConfigFile); err == nil {
26-
return existingConfigFile, true
24+
if _, err := os.Stat(generatedConfigFile); err == nil {
25+
return generatedConfigFile, true
26+
} else if _, err := os.Stat(existingConfigFile); err == nil {
27+
return existingConfigFile, true
28+
}
2729
}
2830

2931
return "", false

tools/semgrepRunner.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package tools
2+
3+
import (
4+
"codacy/cli-v2/config"
5+
"codacy/cli-v2/plugins"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
)
11+
12+
// RunSemgrep executes Semgrep analysis on the specified directory
13+
func RunSemgrep(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
14+
// Construct base command with -m semgrep to run semgrep module
15+
cmdArgs := []string{"scan"}
16+
17+
// Add output format if specified
18+
if outputFormat == "sarif" {
19+
cmdArgs = append(cmdArgs, "--sarif")
20+
}
21+
22+
// Define possible Semgrep config file names
23+
semgrepConfigFiles := []string{".semgrep.yml", ".semgrep.yaml", ".semgrep/semgrep.yml"}
24+
25+
// Check if a config file exists in the expected location and use it if present
26+
if configFile, exists := ConfigFileExists(config.Config, semgrepConfigFiles...); exists {
27+
cmdArgs = append(cmdArgs, "--config", configFile)
28+
} else {
29+
// add --config auto only if no config file exists
30+
cmdArgs = append(cmdArgs, "--config", "auto")
31+
}
32+
33+
// Add files to analyze - if no files specified, analyze current directory
34+
if len(files) > 0 {
35+
cmdArgs = append(cmdArgs, files...)
36+
} else {
37+
cmdArgs = append(cmdArgs, ".")
38+
}
39+
40+
cmdArgs = append(cmdArgs, "--disable-version-check")
41+
42+
// Get Semgrep binary from the specified installation path
43+
semgrepPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "semgrep")
44+
45+
// Create Semgrep command
46+
cmd := exec.Command(semgrepPath, cmdArgs...)
47+
cmd.Dir = workDirectory
48+
49+
if outputFile != "" {
50+
// If output file is specified, create it and redirect output
51+
var outputWriter *os.File
52+
var err error
53+
outputWriter, err = os.Create(filepath.Clean(outputFile))
54+
if err != nil {
55+
return fmt.Errorf("failed to create output file: %w", err)
56+
}
57+
defer outputWriter.Close()
58+
cmd.Stdout = outputWriter
59+
} else {
60+
cmd.Stdout = os.Stdout
61+
}
62+
cmd.Stderr = os.Stderr
63+
64+
// Run Semgrep
65+
if err := cmd.Run(); err != nil {
66+
// Semgrep returns non-zero exit code when it finds issues, which is expected
67+
if _, ok := err.(*exec.ExitError); !ok {
68+
return fmt.Errorf("failed to run semgrep: %w", err)
69+
}
70+
}
71+
72+
return nil
73+
}

tools/semgrepRunner_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package tools
2+
3+
import (
4+
"codacy/cli-v2/plugins"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestRunSemgrepWithSpecificFiles(t *testing.T) {
13+
homeDirectory, err := os.UserHomeDir()
14+
if err != nil {
15+
t.Fatalf("Failed to get home directory: %v", err)
16+
}
17+
currentDirectory, err := os.Getwd()
18+
if err != nil {
19+
t.Fatalf("Failed to get current directory: %v", err)
20+
}
21+
22+
// Set up test directories and files
23+
testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "semgrep")
24+
tempResultFile := filepath.Join(os.TempDir(), "semgrep-specific.sarif")
25+
defer os.Remove(tempResultFile)
26+
27+
// Create tool info for semgrep
28+
toolInfo := &plugins.ToolInfo{
29+
InstallDir: filepath.Join(homeDirectory, ".cache/codacy/tools/[email protected]"),
30+
}
31+
32+
// Specify files to analyze
33+
filesToAnalyze := []string{"sample.js"}
34+
35+
// Run Semgrep analysis on specific files
36+
err = RunSemgrep(testDirectory, toolInfo, filesToAnalyze, tempResultFile, "sarif")
37+
if err != nil {
38+
t.Fatalf("Failed to run semgrep on specific files: %v", err)
39+
}
40+
41+
// Verify file exists and has content
42+
fileInfo, err := os.Stat(tempResultFile)
43+
assert.NoError(t, err, "Failed to stat output file")
44+
assert.Greater(t, fileInfo.Size(), int64(0), "Output file should not be empty")
45+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{
2+
"version": "2.1.0",
3+
"$schema": "http://json.schemastore.org/sarif-2.1.0-rtm.5",
4+
"runs": [
5+
{
6+
"tool": {
7+
"driver": {
8+
"name": "Semgrep",
9+
"version": "1.41.0",
10+
"informationUri": "https://semgrep.dev",
11+
"rules": [
12+
{
13+
"id": "hardcoded-credentials",
14+
"name": "Hardcoded Credentials",
15+
"shortDescription": {
16+
"text": "Hardcoded API key detected"
17+
},
18+
"fullDescription": {
19+
"text": "Found hardcoded API key. This is a security risk."
20+
},
21+
"defaultConfiguration": {
22+
"level": "warning"
23+
},
24+
"help": {
25+
"text": "API keys and other credentials should not be hardcoded in source files. Use environment variables or secure credential storage instead."
26+
}
27+
}
28+
]
29+
}
30+
},
31+
"artifacts": [
32+
{
33+
"location": {
34+
"uri": "testdata/repositories/semgrep/sample.js"
35+
}
36+
}
37+
],
38+
"results": [
39+
{
40+
"ruleId": "hardcoded-credentials",
41+
"level": "warning",
42+
"message": {
43+
"text": "Hardcoded API key detected"
44+
},
45+
"locations": [
46+
{
47+
"physicalLocation": {
48+
"artifactLocation": {
49+
"uri": "testdata/repositories/semgrep/sample.js"
50+
},
51+
"region": {
52+
"startLine": 3,
53+
"startColumn": 16,
54+
"endLine": 3,
55+
"endColumn": 32
56+
}
57+
}
58+
}
59+
]
60+
}
61+
]
62+
}
63+
]
64+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Sample JavaScript file for Semgrep testing
2+
3+
const API_KEY = "1234567890abcdef"; // Hardcoded credential for testing
4+
5+
function helloWorld() {
6+
console.log("Hello, world!");
7+
console.log("Using API Key:", API_KEY);
8+
}
9+
10+
helloWorld();

0 commit comments

Comments
 (0)