11package cmd
22
33import (
4+ "bufio"
45 "fmt"
56 "os"
67 "os/exec"
8+ "path/filepath"
79 "regexp"
810 "strings"
911
@@ -12,49 +14,69 @@ import (
1214 "github.com/fatih/color"
1315 "github.com/sirupsen/logrus"
1416 "github.com/spf13/cobra"
17+ "gopkg.in/yaml.v3"
1518)
1619
1720// validImageNamePattern validates Docker image references
1821// Allows: registry/namespace/image:tag or image@sha256:digest
1922// Based on Docker image reference specification
2023var validImageNamePattern = regexp .MustCompile (`^[a-zA-Z0-9][a-zA-Z0-9._\-/:@]*$` )
2124
25+ // dockerfileFromPattern matches FROM instructions in Dockerfiles
26+ var dockerfileFromPattern = regexp .MustCompile (`(?i)^\s*FROM\s+([^\s]+)` )
27+
2228// Flag variables for container-scan command
2329var (
2430 severityFlag string
2531 pkgTypesFlag string
2632 ignoreUnfixedFlag bool
33+ dockerfileFlag string
34+ composeFileFlag string
2735)
2836
2937func init () {
3038 containerScanCmd .Flags ().StringVar (& severityFlag , "severity" , "" , "Comma-separated list of severities to scan for (default: HIGH,CRITICAL)" )
3139 containerScanCmd .Flags ().StringVar (& pkgTypesFlag , "pkg-types" , "" , "Comma-separated list of package types to scan (default: os)" )
3240 containerScanCmd .Flags ().BoolVar (& ignoreUnfixedFlag , "ignore-unfixed" , true , "Ignore unfixed vulnerabilities" )
41+ containerScanCmd .Flags ().StringVar (& dockerfileFlag , "dockerfile" , "" , "Path to Dockerfile for image auto-detection (useful in CI)" )
42+ containerScanCmd .Flags ().StringVar (& composeFileFlag , "compose-file" , "" , "Path to docker-compose.yml for image auto-detection (useful in CI)" )
3343 rootCmd .AddCommand (containerScanCmd )
3444}
3545
3646var containerScanCmd = & cobra.Command {
37- Use : "container-scan [FLAGS] < IMAGE_NAME> " ,
47+ Use : "container-scan [FLAGS] [ IMAGE_NAME] " ,
3848 Short : "Scan container images for vulnerabilities using Trivy" ,
3949 Long : `Scan container images for vulnerabilities using Trivy.
4050
4151By default, scans for HIGH and CRITICAL vulnerabilities in OS packages,
4252ignoring unfixed issues. Use flags to override these defaults.
4353
54+ If no image is specified, the command will auto-detect images from:
55+ 1. Dockerfile (FROM instruction) - scans the base image
56+ 2. docker-compose.yml (image fields) - scans all referenced images
57+
58+ Use --dockerfile or --compose-file flags to specify explicit paths (useful in CI/CD).
59+
4460The --exit-code 1 flag is always applied (not user-configurable) to ensure
4561the command fails when vulnerabilities are found.` ,
46- Example : ` # Default behavior (HIGH,CRITICAL severity, os packages only)
62+ Example : ` # Auto-detect from Dockerfile or docker-compose.yml in current directory
63+ codacy-cli container-scan
64+
65+ # Specify Dockerfile path (useful in CI/CD)
66+ codacy-cli container-scan --dockerfile ./docker/Dockerfile.prod
67+
68+ # Specify docker-compose file path
69+ codacy-cli container-scan --compose-file ./deploy/docker-compose.yml
70+
71+ # Scan a specific image
4772 codacy-cli container-scan myapp:latest
4873
4974 # Scan only for CRITICAL vulnerabilities
5075 codacy-cli container-scan --severity CRITICAL myapp:latest
5176
52- # Scan all severities and package types
53- codacy-cli container-scan --severity LOW,MEDIUM,HIGH,CRITICAL --pkg-types os,library myapp:latest
54-
55- # Include unfixed vulnerabilities
56- codacy-cli container-scan --ignore-unfixed=false myapp:latest` ,
57- Args : cobra .ExactArgs (1 ),
77+ # CI/CD example: scan all images before deploy
78+ codacy-cli container-scan --dockerfile ./Dockerfile --severity HIGH,CRITICAL` ,
79+ Args : cobra .MaximumNArgs (1 ),
5880 Run : runContainerScan ,
5981}
6082
@@ -124,25 +146,223 @@ func handleTrivyResult(err error, imageName string) {
124146}
125147
126148func runContainerScan (cmd * cobra.Command , args []string ) {
127- imageName := args [0 ]
149+ var images []string
150+
151+ if len (args ) > 0 {
152+ images = []string {args [0 ]}
153+ } else {
154+ images = detectImages ()
155+ if len (images ) == 0 {
156+ color .Red ("❌ Error: No image specified and none found in Dockerfile or docker-compose.yml" )
157+ fmt .Println ("Usage: codacy-cli container-scan <IMAGE_NAME>" )
158+ os .Exit (1 )
159+ }
160+ }
161+
162+ scanImages (images )
163+ }
164+
165+ // scanImages validates and scans multiple images
166+ func scanImages (images []string ) {
167+ trivyPath := getTrivyPath ()
168+ hasFailures := false
169+
170+ for _ , imageName := range images {
171+ if err := validateImageName (imageName ); err != nil {
172+ logger .Error ("Invalid image name" , logrus.Fields {"image" : imageName , "error" : err .Error ()})
173+ color .Red ("❌ Error: %v" , err )
174+ hasFailures = true
175+ continue
176+ }
177+
178+ logger .Info ("Starting container scan" , logrus.Fields {"image" : imageName })
179+ fmt .Printf ("🔍 Scanning container image: %s\n \n " , imageName )
180+
181+ trivyCmd := exec .Command (trivyPath , buildTrivyArgs (imageName )... )
182+ trivyCmd .Stdout = os .Stdout
183+ trivyCmd .Stderr = os .Stderr
184+
185+ logger .Info ("Running Trivy container scan" , logrus.Fields {"command" : trivyCmd .String ()})
128186
129- if err := validateImageName (imageName ); err != nil {
130- logger .Error ("Invalid image name" , logrus.Fields {"image" : imageName , "error" : err .Error ()})
131- color .Red ("❌ Error: %v" , err )
187+ if err := trivyCmd .Run (); err != nil {
188+ hasFailures = true
189+ handleScanError (err , imageName )
190+ } else {
191+ logger .Info ("Container scan completed successfully" , logrus.Fields {"image" : imageName })
192+ fmt .Println ()
193+ color .Green ("✅ Success: No vulnerabilities found in %s" , imageName )
194+ }
195+
196+ if len (images ) > 1 {
197+ fmt .Println ("\n " + strings .Repeat ("-" , 60 ) + "\n " )
198+ }
199+ }
200+
201+ if hasFailures {
132202 os .Exit (1 )
133203 }
204+ }
134205
135- logger .Info ("Starting container scan" , logrus.Fields {"image" : imageName })
206+ // handleScanError processes scan errors without exiting (for multi-image scans)
207+ func handleScanError (err error , imageName string ) {
208+ if exitError , ok := err .(* exec.ExitError ); ok && exitError .ExitCode () == 1 {
209+ logger .Warn ("Container scan completed with vulnerabilities" , logrus.Fields {
210+ "image" : imageName , "exit_code" : 1 ,
211+ })
212+ fmt .Println ()
213+ color .Red ("❌ Vulnerabilities found in %s" , imageName )
214+ return
215+ }
216+ logger .Error ("Failed to run Trivy" , logrus.Fields {"error" : err .Error ()})
217+ color .Red ("❌ Error scanning %s: %v" , imageName , err )
218+ }
136219
137- trivyPath := getTrivyPath ()
138- trivyCmd := exec .Command (trivyPath , buildTrivyArgs (imageName )... )
139- trivyCmd .Stdout = os .Stdout
140- trivyCmd .Stderr = os .Stderr
220+ // detectImages auto-detects images from Dockerfile or docker-compose.yml
221+ func detectImages () []string {
222+ // Priority 0: Check explicit --dockerfile flag
223+ if dockerfileFlag != "" {
224+ if images := parseDockerfile (dockerfileFlag ); len (images ) > 0 {
225+ color .Cyan ("📄 Found images in %s:" , dockerfileFlag )
226+ for _ , img := range images {
227+ fmt .Printf (" • %s\n " , img )
228+ }
229+ fmt .Println ()
230+ return images
231+ }
232+ color .Yellow ("⚠️ No FROM instructions found in %s" , dockerfileFlag )
233+ return nil
234+ }
141235
142- logger .Info ("Running Trivy container scan" , logrus.Fields {"command" : trivyCmd .String ()})
143- fmt .Printf ("🔍 Scanning container image: %s\n \n " , imageName )
236+ // Priority 0: Check explicit --compose-file flag
237+ if composeFileFlag != "" {
238+ if images := parseDockerCompose (composeFileFlag ); len (images ) > 0 {
239+ color .Cyan ("📄 Found images in %s:" , composeFileFlag )
240+ for _ , img := range images {
241+ fmt .Printf (" • %s\n " , img )
242+ }
243+ fmt .Println ()
244+ return images
245+ }
246+ color .Yellow ("⚠️ No images found in %s" , composeFileFlag )
247+ return nil
248+ }
249+
250+ // Priority 1: Auto-detect Dockerfile in current directory
251+ if images := parseDockerfile ("Dockerfile" ); len (images ) > 0 {
252+ color .Cyan ("📄 Found images in Dockerfile:" )
253+ for _ , img := range images {
254+ fmt .Printf (" • %s\n " , img )
255+ }
256+ fmt .Println ()
257+ return images
258+ }
259+
260+ // Priority 2: Auto-detect docker-compose files
261+ composeFiles := []string {"docker-compose.yml" , "docker-compose.yaml" , "compose.yml" , "compose.yaml" }
262+ for _ , composeFile := range composeFiles {
263+ if images := parseDockerCompose (composeFile ); len (images ) > 0 {
264+ color .Cyan ("📄 Found images in %s:" , composeFile )
265+ for _ , img := range images {
266+ fmt .Printf (" • %s\n " , img )
267+ }
268+ fmt .Println ()
269+ return images
270+ }
271+ }
272+
273+ return nil
274+ }
275+
276+ // parseDockerfile extracts FROM images from a Dockerfile
277+ func parseDockerfile (path string ) []string {
278+ file , err := os .Open (path )
279+ if err != nil {
280+ return nil
281+ }
282+ defer file .Close ()
283+
284+ var images []string
285+ seen := make (map [string ]bool )
286+ scanner := bufio .NewScanner (file )
287+
288+ for scanner .Scan () {
289+ line := scanner .Text ()
290+ matches := dockerfileFromPattern .FindStringSubmatch (line )
291+ if len (matches ) > 1 {
292+ image := matches [1 ]
293+ // Skip build stage aliases (e.g., FROM golang:1.21 AS builder)
294+ // and scratch images
295+ if image != "scratch" && ! seen [image ] {
296+ seen [image ] = true
297+ images = append (images , image )
298+ }
299+ }
300+ }
301+
302+ return images
303+ }
304+
305+ // dockerComposeConfig represents the structure of docker-compose.yml
306+ type dockerComposeConfig struct {
307+ Services map [string ]struct {
308+ Image string `yaml:"image"`
309+ Build * struct {
310+ Context string `yaml:"context"`
311+ Dockerfile string `yaml:"dockerfile"`
312+ } `yaml:"build"`
313+ } `yaml:"services"`
314+ }
315+
316+ // parseDockerCompose extracts images from docker-compose.yml
317+ func parseDockerCompose (path string ) []string {
318+ data , err := os .ReadFile (path )
319+ if err != nil {
320+ return nil
321+ }
322+
323+ var config dockerComposeConfig
324+ if err := yaml .Unmarshal (data , & config ); err != nil {
325+ logger .Warn ("Failed to parse docker-compose file" , logrus.Fields {"path" : path , "error" : err .Error ()})
326+ return nil
327+ }
328+
329+ var images []string
330+ seen := make (map [string ]bool )
331+
332+ for serviceName , service := range config .Services {
333+ // If service has an image defined, use it
334+ if service .Image != "" && ! seen [service .Image ] {
335+ seen [service .Image ] = true
336+ images = append (images , service .Image )
337+ }
338+
339+ // If service has a build context with Dockerfile, parse it
340+ if service .Build != nil {
341+ dockerfilePath := "Dockerfile"
342+ if service .Build .Dockerfile != "" {
343+ dockerfilePath = service .Build .Dockerfile
344+ }
345+ if service .Build .Context != "" {
346+ dockerfilePath = filepath .Join (service .Build .Context , dockerfilePath )
347+ }
348+
349+ if dockerfileImages := parseDockerfile (dockerfilePath ); len (dockerfileImages ) > 0 {
350+ for _ , img := range dockerfileImages {
351+ if ! seen [img ] {
352+ seen [img ] = true
353+ images = append (images , img )
354+ logger .Info ("Found base image from Dockerfile" , logrus.Fields {
355+ "service" : serviceName ,
356+ "dockerfile" : dockerfilePath ,
357+ "image" : img ,
358+ })
359+ }
360+ }
361+ }
362+ }
363+ }
144364
145- handleTrivyResult ( trivyCmd . Run (), imageName )
365+ return images
146366}
147367
148368// buildTrivyArgs constructs the Trivy command arguments based on flags
0 commit comments