Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codacy/codacy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ tools:
- [email protected]
- [email protected]
- [email protected]
- [email protected]
14 changes: 14 additions & 0 deletions cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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)
}
Expand Down
1 change: 1 addition & 0 deletions plugins/tool-utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func TestGetSupportedTools(t *testing.T) {
"pmd",
"pylint",
"trivy",
"semgrep",
},
expectedError: false,
},
Expand Down
16 changes: 16 additions & 0 deletions plugins/tools/semgrep/plugin.yaml
Original file line number Diff line number Diff line change
@@ -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: "."
24 changes: 13 additions & 11 deletions tools/runnerUtils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
73 changes: 73 additions & 0 deletions tools/semgrepRunner.go
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 13 in tools/semgrepRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/semgrepRunner.go#L13

Method RunSemgrep has a cyclomatic complexity of 8 (limit is 7)
// 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...)

Check failure on line 46 in tools/semgrepRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/semgrepRunner.go#L46

Detected non-static command inside Command.

Check failure on line 46 in tools/semgrepRunner.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/semgrepRunner.go#L46

OS command injection is a critical vulnerability that can lead to a full system compromise as it may allow an adversary to pass in arbitrary commands or arguments to be executed.
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
}
45 changes: 45 additions & 0 deletions tools/semgrepRunner_test.go
Original file line number Diff line number Diff line change
@@ -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/[email protected]"),
}

// 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")
}
64 changes: 64 additions & 0 deletions tools/testdata/repositories/semgrep/expected.sarif
Original file line number Diff line number Diff line change
@@ -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
}
}
}
]
}
]
}
]
}
10 changes: 10 additions & 0 deletions tools/testdata/repositories/semgrep/sample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Sample JavaScript file for Semgrep testing

const API_KEY = "1234567890abcdef"; // Hardcoded credential for testing

Check notice on line 3 in tools/testdata/repositories/semgrep/sample.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/testdata/repositories/semgrep/sample.js#L3

You must place screaming snake case at module scope. If this is not meant to be a module-scoped variable, use camelcase instead.

function helloWorld() {

Check warning on line 5 in tools/testdata/repositories/semgrep/sample.js

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

tools/testdata/repositories/semgrep/sample.js#L5

Missing JSDoc comment.
console.log("Hello, world!");
console.log("Using API Key:", API_KEY);
}

helloWorld();