Skip to content

Commit 7fe9d0f

Browse files
Enhance container scan functionality by adding RunWithStderr method to CommandRunner interface and implementing isScanFailure check. Introduced tests for scan failure scenarios and updated MockCommandRunner to support stderr handling.
1 parent 3635fa0 commit 7fe9d0f

File tree

2 files changed

+82
-9
lines changed

2 files changed

+82
-9
lines changed

cmd/container_scan.go

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
package cmd
33

44
import (
5+
"bytes"
56
"fmt"
7+
"io"
68
"os"
79
"os/exec"
810
"regexp"
@@ -28,19 +30,30 @@ var exitFunc = os.Exit
2830
// CommandRunner interface for running external commands (allows mocking in tests)
2931
type CommandRunner interface {
3032
Run(name string, args []string) error
33+
// RunWithStderr runs the command; if stderr is not nil, Trivy stderr is written to both os.Stderr and stderr.
34+
RunWithStderr(name string, args []string, stderr io.Writer) error
3135
}
3236

3337
// ExecCommandRunner runs commands using exec.Command
3438
type ExecCommandRunner struct{}
3539

3640
// Run executes a command and returns its exit error
3741
func (r *ExecCommandRunner) Run(name string, args []string) error {
42+
return r.RunWithStderr(name, args, nil)
43+
}
44+
45+
// RunWithStderr runs the command; if stderr is not nil, command stderr is written to both os.Stderr and stderr.
46+
func (r *ExecCommandRunner) RunWithStderr(name string, args []string, stderr io.Writer) error {
3847
// #nosec G204 -- name comes from config (codacy-installed Trivy path),
3948
// and args are validated by validateImageName() which checks for shell metacharacters.
4049
// exec.Command passes arguments directly without shell interpretation.
4150
cmd := exec.Command(name, args...)
4251
cmd.Stdout = os.Stdout
43-
cmd.Stderr = os.Stderr
52+
if stderr != nil {
53+
cmd.Stderr = io.MultiWriter(os.Stderr, stderr)
54+
} else {
55+
cmd.Stderr = os.Stderr
56+
}
4457
return cmd.Run()
4558
}
4659

@@ -162,7 +175,6 @@ func handleTrivyNotFound(err error) {
162175
logger.Error("Trivy not found", logrus.Fields{"error": err.Error()})
163176
color.Red("❌ Error: Trivy could not be installed or found")
164177
fmt.Println("Run 'codacy-cli init' if you have no project yet, then try container-scan again so Trivy can be installed automatically.")
165-
fmt.Println("exit-code 2")
166178
exitFunc(2)
167179
}
168180

@@ -177,7 +189,6 @@ func executeContainerScan(imageName string) int {
177189
if err := validateImageName(imageName); err != nil {
178190
logger.Error("Invalid image name", logrus.Fields{"image": imageName, "error": err.Error()})
179191
color.Red("❌ Error: %v", err)
180-
fmt.Println("exit-code 2")
181192
return 2
182193
}
183194
logger.Info("Starting container scan", logrus.Fields{"image": imageName})
@@ -195,20 +206,36 @@ func executeContainerScan(imageName string) int {
195206
return printScanSummary(hasVulnerabilities == 1)
196207
}
197208

209+
// isScanFailure returns true if Trivy stderr indicates the scan failed (e.g. image not found, no runtime)
210+
// rather than a successful scan that found vulnerabilities. Trivy uses exit code 1 for both cases.
211+
func isScanFailure(stderr []byte) bool {
212+
s := string(stderr)
213+
return strings.Contains(s, "FATAL") ||
214+
strings.Contains(s, "run error") ||
215+
strings.Contains(s, "image scan error") ||
216+
strings.Contains(s, "unable to find the specified image")
217+
}
218+
198219
// scanImage scans the image and returns: 0=no vulns, 1=vulns found, -1=error
199220
func scanImage(imageName, trivyPath string) int {
200221
fmt.Printf("🔍 Scanning container image: %s\n\n", imageName)
201222
args := buildTrivyArgs(imageName)
202223
logger.Info("Running Trivy container scan", logrus.Fields{"command": fmt.Sprintf("%s %v", trivyPath, args)})
203224

204-
if err := commandRunner.Run(trivyPath, args); err != nil {
205-
if getExitCode(err) == 1 {
225+
var stderrBuf bytes.Buffer
226+
if err := commandRunner.RunWithStderr(trivyPath, args, &stderrBuf); err != nil {
227+
code := getExitCode(err)
228+
if code == 1 && isScanFailure(stderrBuf.Bytes()) {
229+
logger.Error("Scan failed (e.g. image not found or no container runtime)", logrus.Fields{"image": imageName, "error": err.Error()})
230+
color.Red("❌ Scanning failed: unable to scan the container image (e.g. image not found or no container runtime)")
231+
return -1
232+
}
233+
if code == 1 {
206234
logger.Warn("Vulnerabilities found in image", logrus.Fields{"image": imageName})
207235
return 1
208236
}
209237
logger.Error("Failed to run Trivy", logrus.Fields{"error": err.Error(), "image": imageName})
210238
color.Red("❌ Error: Failed to run Trivy for %s: %v", imageName, err)
211-
fmt.Println("exit-code 2")
212239
return -1
213240
}
214241
logger.Info("No vulnerabilities found in image", logrus.Fields{"image": imageName})
@@ -220,7 +247,6 @@ func printScanSummary(hasVulnerabilities bool) int {
220247
if hasVulnerabilities {
221248
logger.Warn("Container scan completed with vulnerabilities", logrus.Fields{})
222249
color.Red("❌ Scanning failed: vulnerabilities found in the container image")
223-
fmt.Println("exit-code 1")
224250
return 1
225251
}
226252
logger.Info("Container scan completed successfully", logrus.Fields{})

cmd/container_scan_test.go

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,34 @@ package cmd
22

33
import (
44
"errors"
5+
"io"
56
"testing"
67

78
"github.com/stretchr/testify/assert"
89
)
910

1011
// MockCommandRunner is a mock implementation of CommandRunner for testing
1112
type MockCommandRunner struct {
12-
RunFunc func(name string, args []string) error
13-
Calls []struct {
13+
RunFunc func(name string, args []string) error
14+
RunWithStderrFunc func(name string, args []string, stderr io.Writer) error
15+
Calls []struct {
1416
Name string
1517
Args []string
1618
}
1719
}
1820

1921
func (m *MockCommandRunner) Run(name string, args []string) error {
22+
return m.RunWithStderr(name, args, nil)
23+
}
24+
25+
func (m *MockCommandRunner) RunWithStderr(name string, args []string, stderr io.Writer) error {
2026
m.Calls = append(m.Calls, struct {
2127
Name string
2228
Args []string
2329
}{Name: name, Args: args})
30+
if m.RunWithStderrFunc != nil {
31+
return m.RunWithStderrFunc(name, args, stderr)
32+
}
2433
if m.RunFunc != nil {
2534
return m.RunFunc(name, args)
2635
}
@@ -206,6 +215,44 @@ func TestExecuteContainerScan_TrivyExecutionError(t *testing.T) {
206215
assert.Equal(t, 2, exitCode)
207216
}
208217

218+
func TestExecuteContainerScan_ScanFailureExit1(t *testing.T) {
219+
state := saveState()
220+
defer state.restore()
221+
222+
getTrivyPathResolver = func() (string, error) {
223+
return "/usr/local/bin/trivy", nil
224+
}
225+
226+
// Trivy exits 1 with FATAL/run error in stderr (e.g. image not found) -> we treat as scan error, not vulnerabilities
227+
scanFailureStderr := "FATAL Fatal error run error: image scan error: unable to find the specified image"
228+
mockRunner := &MockCommandRunner{
229+
RunWithStderrFunc: func(_ string, _ []string, stderr io.Writer) error {
230+
if stderr != nil {
231+
_, _ = stderr.Write([]byte(scanFailureStderr))
232+
}
233+
return &mockExitError{code: 1}
234+
},
235+
}
236+
commandRunner = mockRunner
237+
238+
severityFlag = ""
239+
pkgTypesFlag = ""
240+
ignoreUnfixedFlag = true
241+
242+
exitCode := executeContainerScan("random-string")
243+
assert.Equal(t, 2, exitCode, "Should return exit code 2 when scan failed (not vulnerabilities found)")
244+
assert.Len(t, mockRunner.Calls, 1)
245+
}
246+
247+
func TestIsScanFailure(t *testing.T) {
248+
assert.False(t, isScanFailure(nil), "nil stderr is not a scan failure")
249+
assert.False(t, isScanFailure([]byte("")), "empty stderr is not a scan failure")
250+
assert.False(t, isScanFailure([]byte("Total: 5 (HIGH: 2, CRITICAL: 3)")), "vulnerability table is not a scan failure")
251+
assert.True(t, isScanFailure([]byte("FATAL Fatal error")), "FATAL indicates scan failure")
252+
assert.True(t, isScanFailure([]byte("run error: image scan error")), "run error indicates scan failure")
253+
assert.True(t, isScanFailure([]byte("unable to find the specified image")), "unable to find image indicates scan failure")
254+
}
255+
209256
// Tests for handleTrivyNotFound
210257

211258
func TestHandleTrivyNotFound(t *testing.T) {

0 commit comments

Comments
 (0)