@@ -20,6 +20,47 @@ import (
2020// Based on Docker image reference specification
2121var 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
2465var (
2566 severityFlag string
@@ -90,37 +131,52 @@ 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
108154func runContainerScan (_ * cobra.Command , args []string ) {
109- imageNames := args
155+ exitCode := executeContainerScan (args )
156+ exitFunc (exitCode )
157+ }
110158
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 {
111162 // Validate all image names first
112163 for _ , imageName := range imageNames {
113164 if err := validateImageName (imageName ); err != nil {
114165 logger .Error ("Invalid image name" , logrus.Fields {"image" : imageName , "error" : err .Error ()})
115166 color .Red ("❌ Error: %v" , err )
116167 fmt .Println ("exit-code 2" )
117- os . Exit ( 2 )
168+ return 2
118169 }
119170 }
120171
121172 logger .Info ("Starting container scan" , logrus.Fields {"images" : imageNames , "count" : len (imageNames )})
122173
123- trivyPath := getTrivyPath ()
174+ trivyPath , err := getTrivyPath ()
175+ if err != nil {
176+ handleTrivyNotFound (err )
177+ return 2 // This won't be reached due to exitFunc, but needed for testing
178+ }
179+
124180 hasVulnerabilities := false
125181
126182 for i , imageName := range imageNames {
@@ -131,24 +187,18 @@ func runContainerScan(_ *cobra.Command, args []string) {
131187 fmt .Printf ("🔍 Scanning container image: %s\n \n " , imageName )
132188 }
133189
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 ()})
190+ args := buildTrivyArgs (imageName )
191+ logger .Info ("Running Trivy container scan" , logrus.Fields {"command" : fmt .Sprintf ("%s %v" , trivyPath , args )})
142192
143- if err := trivyCmd .Run (); err != nil {
144- if exitError , ok := err .( * exec. ExitError ); ok && exitError . ExitCode ( ) == 1 {
193+ if err := commandRunner .Run (trivyPath , args ); err != nil {
194+ if getExitCode ( err ) == 1 {
145195 logger .Warn ("Vulnerabilities found in image" , logrus.Fields {"image" : imageName })
146196 hasVulnerabilities = true
147197 } else {
148198 logger .Error ("Failed to run Trivy" , logrus.Fields {"error" : err .Error (), "image" : imageName })
149199 color .Red ("❌ Error: Failed to run Trivy for %s: %v" , imageName , err )
150200 fmt .Println ("exit-code 2" )
151- os . Exit ( 2 )
201+ return 2
152202 }
153203 } else {
154204 logger .Info ("No vulnerabilities found in image" , logrus.Fields {"image" : imageName })
@@ -161,12 +211,13 @@ func runContainerScan(_ *cobra.Command, args []string) {
161211 logger .Warn ("Container scan completed with vulnerabilities" , logrus.Fields {"images" : imageNames })
162212 color .Red ("❌ Scanning failed: vulnerabilities found in one or more container images" )
163213 fmt .Println ("exit-code 1" )
164- os . Exit ( 1 )
214+ return 1
165215 }
166216
167217 logger .Info ("Container scan completed successfully" , logrus.Fields {"images" : imageNames })
168218 color .Green ("✅ Success: No vulnerabilities found matching the specified criteria" )
169219 fmt .Println ("exit-code 0" )
220+ return 0
170221}
171222
172223// buildTrivyArgs constructs the Trivy command arguments based on flags
0 commit comments