@@ -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,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
108154func 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