Skip to content

Commit d4d0d2c

Browse files
add tests and codacy vulns fixed
1 parent 078a26a commit d4d0d2c

File tree

2 files changed

+406
-38
lines changed

2 files changed

+406
-38
lines changed

cmd/container_scan.go

Lines changed: 109 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,47 @@ import (
2020
// Based on Docker image reference specification
2121
var validImageNamePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._\-/:@]*$`)
2222

23+
// lookPath is a variable to allow mocking exec.LookPath in tests
24+
var lookPath = exec.LookPath
25+
26+
// exitFunc is a variable to allow mocking os.Exit in tests
27+
var exitFunc = os.Exit
28+
29+
// CommandRunner interface for running external commands (allows mocking in tests)
30+
type CommandRunner interface {
31+
Run(name string, args []string) error
32+
}
33+
34+
// ExecCommandRunner runs commands using exec.Command
35+
type ExecCommandRunner struct{}
36+
37+
// Run executes a command and returns its exit error
38+
func (r *ExecCommandRunner) Run(name string, args []string) error {
39+
// #nosec G204 -- name comes from exec.LookPath("trivy") with a literal string,
40+
// and args are validated by validateImageName() which checks for shell metacharacters.
41+
// exec.Command passes arguments directly without shell interpretation.
42+
cmd := exec.Command(name, args...)
43+
cmd.Stdout = os.Stdout
44+
cmd.Stderr = os.Stderr
45+
return cmd.Run()
46+
}
47+
48+
// commandRunner is the default command runner, can be replaced in tests
49+
var commandRunner CommandRunner = &ExecCommandRunner{}
50+
51+
// ExitCoder interface for errors that have an exit code
52+
type ExitCoder interface {
53+
ExitCode() int
54+
}
55+
56+
// getExitCode returns the exit code from an error if it implements ExitCoder
57+
func getExitCode(err error) int {
58+
if exitErr, ok := err.(ExitCoder); ok {
59+
return exitErr.ExitCode()
60+
}
61+
return -1
62+
}
63+
2364
// Flag variables for container-scan command
2465
var (
2566
severityFlag string
@@ -90,83 +131,113 @@ func validateImageName(imageName string) error {
90131
return nil
91132
}
92133

93-
// getTrivyPath returns the path to the Trivy binary or exits if not found
94-
func getTrivyPath() string {
95-
trivyPath, err := exec.LookPath("trivy")
134+
// getTrivyPath returns the path to the Trivy binary and an error if not found
135+
func getTrivyPath() (string, error) {
136+
trivyPath, err := lookPath("trivy")
96137
if err != nil {
97-
logger.Error("Trivy not found", logrus.Fields{"error": err.Error()})
98-
color.Red("❌ Error: Trivy is not installed or not found in PATH")
99-
fmt.Println("Please install Trivy to use container scanning.")
100-
fmt.Println("Visit: https://trivy.dev/latest/getting-started/installation/")
101-
fmt.Println("exit-code 2")
102-
os.Exit(2)
138+
return "", err
103139
}
104140
logger.Info("Found Trivy", logrus.Fields{"path": trivyPath})
105-
return trivyPath
141+
return trivyPath, nil
142+
}
143+
144+
// handleTrivyNotFound prints error message and exits with code 2
145+
func handleTrivyNotFound(err error) {
146+
logger.Error("Trivy not found", logrus.Fields{"error": err.Error()})
147+
color.Red("❌ Error: Trivy is not installed or not found in PATH")
148+
fmt.Println("Please install Trivy to use container scanning.")
149+
fmt.Println("Visit: https://trivy.dev/latest/getting-started/installation/")
150+
fmt.Println("exit-code 2")
151+
exitFunc(2)
106152
}
107153

108154
func runContainerScan(_ *cobra.Command, args []string) {
109-
imageNames := args
155+
exitCode := executeContainerScan(args)
156+
exitFunc(exitCode)
157+
}
110158

111-
// Validate all image names first
159+
// executeContainerScan performs the container scan and returns an exit code
160+
// Exit codes: 0 = success, 1 = vulnerabilities found, 2 = error
161+
func executeContainerScan(imageNames []string) int {
162+
if code := validateAllImages(imageNames); code != 0 {
163+
return code
164+
}
165+
logger.Info("Starting container scan", logrus.Fields{"images": imageNames, "count": len(imageNames)})
166+
167+
trivyPath, err := getTrivyPath()
168+
if err != nil {
169+
handleTrivyNotFound(err)
170+
return 2
171+
}
172+
173+
hasVulnerabilities := scanAllImages(imageNames, trivyPath)
174+
if hasVulnerabilities == -1 {
175+
return 2
176+
}
177+
return printScanSummary(hasVulnerabilities == 1, imageNames)
178+
}
179+
180+
func validateAllImages(imageNames []string) int {
112181
for _, imageName := range imageNames {
113182
if err := validateImageName(imageName); err != nil {
114183
logger.Error("Invalid image name", logrus.Fields{"image": imageName, "error": err.Error()})
115184
color.Red("❌ Error: %v", err)
116185
fmt.Println("exit-code 2")
117-
os.Exit(2)
186+
return 2
118187
}
119188
}
189+
return 0
190+
}
120191

121-
logger.Info("Starting container scan", logrus.Fields{"images": imageNames, "count": len(imageNames)})
122-
123-
trivyPath := getTrivyPath()
192+
// scanAllImages scans all images and returns: 0=no vulns, 1=vulns found, -1=error
193+
func scanAllImages(imageNames []string, trivyPath string) int {
124194
hasVulnerabilities := false
125-
126195
for i, imageName := range imageNames {
127-
if len(imageNames) > 1 {
128-
fmt.Printf("\n📦 [%d/%d] Scanning image: %s\n", i+1, len(imageNames), imageName)
129-
fmt.Println(strings.Repeat("-", 50))
130-
} else {
131-
fmt.Printf("🔍 Scanning container image: %s\n\n", imageName)
132-
}
196+
printScanHeader(imageNames, imageName, i)
197+
args := buildTrivyArgs(imageName)
198+
logger.Info("Running Trivy container scan", logrus.Fields{"command": fmt.Sprintf("%s %v", trivyPath, args)})
133199

134-
// #nosec G204 -- imageName is validated by validateImageName() which checks for
135-
// shell metacharacters and enforces a strict character allowlist. Additionally,
136-
// exec.Command passes arguments directly without shell interpretation.
137-
trivyCmd := exec.Command(trivyPath, buildTrivyArgs(imageName)...)
138-
trivyCmd.Stdout = os.Stdout
139-
trivyCmd.Stderr = os.Stderr
140-
141-
logger.Info("Running Trivy container scan", logrus.Fields{"command": trivyCmd.String()})
142-
143-
if err := trivyCmd.Run(); err != nil {
144-
if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 {
200+
if err := commandRunner.Run(trivyPath, args); err != nil {
201+
if getExitCode(err) == 1 {
145202
logger.Warn("Vulnerabilities found in image", logrus.Fields{"image": imageName})
146203
hasVulnerabilities = true
147204
} else {
148205
logger.Error("Failed to run Trivy", logrus.Fields{"error": err.Error(), "image": imageName})
149206
color.Red("❌ Error: Failed to run Trivy for %s: %v", imageName, err)
150207
fmt.Println("exit-code 2")
151-
os.Exit(2)
208+
return -1
152209
}
153210
} else {
154211
logger.Info("No vulnerabilities found in image", logrus.Fields{"image": imageName})
155212
}
156213
}
214+
if hasVulnerabilities {
215+
return 1
216+
}
217+
return 0
218+
}
157219

158-
// Print summary for multiple images
220+
func printScanHeader(imageNames []string, imageName string, index int) {
221+
if len(imageNames) > 1 {
222+
fmt.Printf("\n📦 [%d/%d] Scanning image: %s\n", index+1, len(imageNames), imageName)
223+
fmt.Println(strings.Repeat("-", 50))
224+
} else {
225+
fmt.Printf("🔍 Scanning container image: %s\n\n", imageName)
226+
}
227+
}
228+
229+
func printScanSummary(hasVulnerabilities bool, imageNames []string) int {
159230
fmt.Println()
160231
if hasVulnerabilities {
161232
logger.Warn("Container scan completed with vulnerabilities", logrus.Fields{"images": imageNames})
162233
color.Red("❌ Scanning failed: vulnerabilities found in one or more container images")
163234
fmt.Println("exit-code 1")
164-
os.Exit(1)
235+
return 1
165236
}
166-
167237
logger.Info("Container scan completed successfully", logrus.Fields{"images": imageNames})
168238
color.Green("✅ Success: No vulnerabilities found matching the specified criteria")
169239
fmt.Println("exit-code 0")
240+
return 0
170241
}
171242

172243
// buildTrivyArgs constructs the Trivy command arguments based on flags

0 commit comments

Comments
 (0)