Skip to content

Commit 3635fa0

Browse files
Refactor container scan command to accept a single image argument and update related tests. Removed support for multiple images in command usage and adjusted the execution logic accordingly.
1 parent 027955b commit 3635fa0

File tree

2 files changed

+47
-215
lines changed

2 files changed

+47
-215
lines changed

cmd/container_scan.go

Lines changed: 36 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -75,30 +75,27 @@ func init() {
7575
}
7676

7777
var containerScanCmd = &cobra.Command{
78-
Use: "container-scan <IMAGE_NAME> [IMAGE_NAME...]",
79-
Short: "Scan container images for vulnerabilities using Trivy",
80-
Long: `Scan one or more container images for vulnerabilities using Trivy.
78+
Use: "container-scan <IMAGE_NAME>",
79+
Short: "Scan a container image for vulnerabilities using Trivy",
80+
Long: `Scan a container image for vulnerabilities using Trivy.
8181
8282
By default, scans for HIGH and CRITICAL vulnerabilities in OS packages,
8383
ignoring unfixed issues. Use flags to override these defaults.
8484
8585
The --exit-code 1 flag is always applied (not user-configurable) to ensure
86-
the command fails when vulnerabilities are found in any image.`,
87-
Example: ` # Scan a single image
86+
the command fails when vulnerabilities are found.`,
87+
Example: ` # Scan an image
8888
codacy-cli container-scan myapp:latest
8989
90-
# Scan multiple images
91-
codacy-cli container-scan myapp:latest nginx:alpine redis:7
92-
93-
# Scan only for CRITICAL vulnerabilities across multiple images
94-
codacy-cli container-scan --severity CRITICAL myapp:latest nginx:alpine
90+
# Scan only for CRITICAL vulnerabilities
91+
codacy-cli container-scan --severity CRITICAL myapp:latest
9592
9693
# Scan all severities and package types
9794
codacy-cli container-scan --severity LOW,MEDIUM,HIGH,CRITICAL --pkg-types os,library myapp:latest
9895
9996
# Include unfixed vulnerabilities
10097
codacy-cli container-scan --ignore-unfixed=false myapp:latest`,
101-
Args: cobra.MinimumNArgs(1),
98+
Args: cobra.ExactArgs(1),
10299
Run: runContainerScan,
103100
}
104101

@@ -170,89 +167,63 @@ func handleTrivyNotFound(err error) {
170167
}
171168

172169
func runContainerScan(_ *cobra.Command, args []string) {
173-
exitCode := executeContainerScan(args)
170+
exitCode := executeContainerScan(args[0])
174171
exitFunc(exitCode)
175172
}
176173

177174
// executeContainerScan performs the container scan and returns an exit code
178175
// Exit codes: 0 = success, 1 = vulnerabilities found, 2 = error
179-
func executeContainerScan(imageNames []string) int {
180-
if code := validateAllImages(imageNames); code != 0 {
181-
return code
176+
func executeContainerScan(imageName string) int {
177+
if err := validateImageName(imageName); err != nil {
178+
logger.Error("Invalid image name", logrus.Fields{"image": imageName, "error": err.Error()})
179+
color.Red("❌ Error: %v", err)
180+
fmt.Println("exit-code 2")
181+
return 2
182182
}
183-
logger.Info("Starting container scan", logrus.Fields{"images": imageNames, "count": len(imageNames)})
183+
logger.Info("Starting container scan", logrus.Fields{"image": imageName})
184184

185185
trivyPath, err := getTrivyPath()
186186
if err != nil {
187187
handleTrivyNotFound(err)
188188
return 2
189189
}
190190

191-
hasVulnerabilities := scanAllImages(imageNames, trivyPath)
191+
hasVulnerabilities := scanImage(imageName, trivyPath)
192192
if hasVulnerabilities == -1 {
193193
return 2
194194
}
195-
return printScanSummary(hasVulnerabilities == 1, imageNames)
195+
return printScanSummary(hasVulnerabilities == 1)
196196
}
197197

198-
func validateAllImages(imageNames []string) int {
199-
for _, imageName := range imageNames {
200-
if err := validateImageName(imageName); err != nil {
201-
logger.Error("Invalid image name", logrus.Fields{"image": imageName, "error": err.Error()})
202-
color.Red("❌ Error: %v", err)
203-
fmt.Println("exit-code 2")
204-
return 2
205-
}
206-
}
207-
return 0
208-
}
198+
// scanImage scans the image and returns: 0=no vulns, 1=vulns found, -1=error
199+
func scanImage(imageName, trivyPath string) int {
200+
fmt.Printf("🔍 Scanning container image: %s\n\n", imageName)
201+
args := buildTrivyArgs(imageName)
202+
logger.Info("Running Trivy container scan", logrus.Fields{"command": fmt.Sprintf("%s %v", trivyPath, args)})
209203

210-
// scanAllImages scans all images and returns: 0=no vulns, 1=vulns found, -1=error
211-
func scanAllImages(imageNames []string, trivyPath string) int {
212-
hasVulnerabilities := false
213-
for i, imageName := range imageNames {
214-
printScanHeader(imageNames, imageName, i)
215-
args := buildTrivyArgs(imageName)
216-
logger.Info("Running Trivy container scan", logrus.Fields{"command": fmt.Sprintf("%s %v", trivyPath, args)})
217-
218-
if err := commandRunner.Run(trivyPath, args); err != nil {
219-
if getExitCode(err) == 1 {
220-
logger.Warn("Vulnerabilities found in image", logrus.Fields{"image": imageName})
221-
hasVulnerabilities = true
222-
} else {
223-
logger.Error("Failed to run Trivy", logrus.Fields{"error": err.Error(), "image": imageName})
224-
color.Red("❌ Error: Failed to run Trivy for %s: %v", imageName, err)
225-
fmt.Println("exit-code 2")
226-
return -1
227-
}
228-
} else {
229-
logger.Info("No vulnerabilities found in image", logrus.Fields{"image": imageName})
204+
if err := commandRunner.Run(trivyPath, args); err != nil {
205+
if getExitCode(err) == 1 {
206+
logger.Warn("Vulnerabilities found in image", logrus.Fields{"image": imageName})
207+
return 1
230208
}
209+
logger.Error("Failed to run Trivy", logrus.Fields{"error": err.Error(), "image": imageName})
210+
color.Red("❌ Error: Failed to run Trivy for %s: %v", imageName, err)
211+
fmt.Println("exit-code 2")
212+
return -1
231213
}
232-
if hasVulnerabilities {
233-
return 1
234-
}
214+
logger.Info("No vulnerabilities found in image", logrus.Fields{"image": imageName})
235215
return 0
236216
}
237217

238-
func printScanHeader(imageNames []string, imageName string, index int) {
239-
if len(imageNames) > 1 {
240-
fmt.Printf("\n📦 [%d/%d] Scanning image: %s\n", index+1, len(imageNames), imageName)
241-
fmt.Println(strings.Repeat("-", 50))
242-
} else {
243-
fmt.Printf("🔍 Scanning container image: %s\n\n", imageName)
244-
}
245-
}
246-
247-
func printScanSummary(hasVulnerabilities bool, imageNames []string) int {
218+
func printScanSummary(hasVulnerabilities bool) int {
248219
fmt.Println()
249220
if hasVulnerabilities {
250-
logger.Warn("Container scan completed with vulnerabilities", logrus.Fields{"images": imageNames})
251-
color.Red("❌ Scanning failed: vulnerabilities found in one or more container images")
221+
logger.Warn("Container scan completed with vulnerabilities", logrus.Fields{})
222+
color.Red("❌ Scanning failed: vulnerabilities found in the container image")
252223
fmt.Println("exit-code 1")
253224
return 1
254225
}
255-
logger.Info("Container scan completed successfully", logrus.Fields{"images": imageNames})
226+
logger.Info("Container scan completed successfully", logrus.Fields{})
256227
color.Green("✅ Success: No vulnerabilities found matching the specified criteria")
257228
return 0
258229
}

cmd/container_scan_test.go

Lines changed: 11 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func TestExecuteContainerScan_Success(t *testing.T) {
109109
pkgTypesFlag = ""
110110
ignoreUnfixedFlag = true
111111

112-
exitCode := executeContainerScan([]string{"alpine:latest"})
112+
exitCode := executeContainerScan("alpine:latest")
113113
assert.Equal(t, 0, exitCode)
114114
assert.Len(t, mockRunner.Calls, 1)
115115
assert.Equal(t, "/usr/local/bin/trivy", mockRunner.Calls[0].Name)
@@ -149,46 +149,16 @@ func TestExecuteContainerScan_VulnerabilitiesFound(t *testing.T) {
149149
pkgTypesFlag = ""
150150
ignoreUnfixedFlag = true
151151

152-
exitCode := executeContainerScan([]string{"alpine:latest"})
152+
exitCode := executeContainerScan("alpine:latest")
153153
assert.Equal(t, 1, exitCode, "Should return exit code 1 when vulnerabilities are found")
154154
assert.Len(t, mockRunner.Calls, 1)
155155
}
156156

157-
func TestExecuteContainerScan_MultipleImages_SomeWithVulnerabilities(t *testing.T) {
158-
state := saveState()
159-
defer state.restore()
160-
161-
getTrivyPathResolver = func() (string, error) {
162-
return "/usr/local/bin/trivy", nil
163-
}
164-
165-
callCount := 0
166-
mockRunner := &MockCommandRunner{
167-
RunFunc: func(_ string, _ []string) error {
168-
callCount++
169-
// Second image has vulnerabilities
170-
if callCount == 2 {
171-
return &mockExitError{code: 1}
172-
}
173-
return nil
174-
},
175-
}
176-
commandRunner = mockRunner
177-
178-
severityFlag = ""
179-
pkgTypesFlag = ""
180-
ignoreUnfixedFlag = true
181-
182-
exitCode := executeContainerScan([]string{"alpine:latest", "nginx:latest", "redis:7"})
183-
assert.Equal(t, 1, exitCode, "Should return exit code 1 when any image has vulnerabilities")
184-
assert.Len(t, mockRunner.Calls, 3, "Should scan all images even if one has vulnerabilities")
185-
}
186-
187157
func TestExecuteContainerScan_InvalidImageName(t *testing.T) {
188158
state := saveState()
189159
defer state.restore()
190160

191-
exitCode := executeContainerScan([]string{"nginx;rm -rf /"})
161+
exitCode := executeContainerScan("nginx;rm -rf /")
192162
assert.Equal(t, 2, exitCode)
193163
}
194164

@@ -206,45 +176,12 @@ func TestExecuteContainerScan_TrivyNotFound(t *testing.T) {
206176
capturedExitCode = code
207177
}
208178

209-
exitCode := executeContainerScan([]string{"alpine:latest"})
179+
exitCode := executeContainerScan("alpine:latest")
210180
// handleTrivyNotFound calls exitFunc(2), then returns 2
211181
assert.Equal(t, 2, capturedExitCode)
212182
assert.Equal(t, 2, exitCode)
213183
}
214184

215-
func TestExecuteContainerScan_MultipleImages_AllPass(t *testing.T) {
216-
state := saveState()
217-
defer state.restore()
218-
219-
getTrivyPathResolver = func() (string, error) {
220-
return "/usr/local/bin/trivy", nil
221-
}
222-
223-
mockRunner := &MockCommandRunner{
224-
RunFunc: func(_ string, _ []string) error {
225-
return nil
226-
},
227-
}
228-
commandRunner = mockRunner
229-
230-
severityFlag = ""
231-
pkgTypesFlag = ""
232-
ignoreUnfixedFlag = true
233-
234-
exitCode := executeContainerScan([]string{"alpine:latest", "nginx:latest", "redis:7"})
235-
assert.Equal(t, 0, exitCode)
236-
assert.Len(t, mockRunner.Calls, 3)
237-
}
238-
239-
func TestExecuteContainerScan_MultipleImages_OneInvalid(t *testing.T) {
240-
state := saveState()
241-
defer state.restore()
242-
243-
// Should fail validation before running any scans
244-
exitCode := executeContainerScan([]string{"alpine:latest", "nginx;bad", "redis:7"})
245-
assert.Equal(t, 2, exitCode)
246-
}
247-
248185
func TestExecuteContainerScan_TrivyExecutionError(t *testing.T) {
249186
state := saveState()
250187
defer state.restore()
@@ -265,27 +202,10 @@ func TestExecuteContainerScan_TrivyExecutionError(t *testing.T) {
265202
pkgTypesFlag = ""
266203
ignoreUnfixedFlag = true
267204

268-
exitCode := executeContainerScan([]string{"alpine:latest"})
205+
exitCode := executeContainerScan("alpine:latest")
269206
assert.Equal(t, 2, exitCode)
270207
}
271208

272-
func TestExecuteContainerScan_EmptyImageList(t *testing.T) {
273-
state := saveState()
274-
defer state.restore()
275-
276-
getTrivyPathResolver = func() (string, error) {
277-
return "/usr/local/bin/trivy", nil
278-
}
279-
280-
mockRunner := &MockCommandRunner{}
281-
commandRunner = mockRunner
282-
283-
// Empty list should succeed with no scans performed
284-
exitCode := executeContainerScan([]string{})
285-
assert.Equal(t, 0, exitCode)
286-
assert.Len(t, mockRunner.Calls, 0)
287-
}
288-
289209
// Tests for handleTrivyNotFound
290210

291211
func TestHandleTrivyNotFound(t *testing.T) {
@@ -448,7 +368,7 @@ func TestContainerScanCommandSkipsValidation(t *testing.T) {
448368
}
449369

450370
func TestContainerScanCommandRequiresArg(t *testing.T) {
451-
assert.Equal(t, "container-scan <IMAGE_NAME> [IMAGE_NAME...]", containerScanCmd.Use, "Command use should match expected format")
371+
assert.Equal(t, "container-scan <IMAGE_NAME>", containerScanCmd.Use, "Command use should match expected format")
452372

453373
err := containerScanCmd.Args(containerScanCmd, []string{})
454374
assert.Error(t, err, "Should error when no args provided")
@@ -457,10 +377,7 @@ func TestContainerScanCommandRequiresArg(t *testing.T) {
457377
assert.NoError(t, err, "Should not error when one arg provided")
458378

459379
err = containerScanCmd.Args(containerScanCmd, []string{"image1", "image2"})
460-
assert.NoError(t, err, "Should not error when multiple args provided")
461-
462-
err = containerScanCmd.Args(containerScanCmd, []string{"image1", "image2", "image3"})
463-
assert.NoError(t, err, "Should not error when many args provided")
380+
assert.Error(t, err, "Should error when multiple args provided")
464381
}
465382

466383
func TestContainerScanFlagDefaults(t *testing.T) {
@@ -551,80 +468,24 @@ func TestBuildTrivyArgsDefaultsApplied(t *testing.T) {
551468
assert.Contains(t, args, "--ignore-unfixed", "--ignore-unfixed should be present when enabled")
552469
}
553470

554-
// Tests for multiple image support
555-
556-
func TestValidateMultipleImages(t *testing.T) {
557-
// All valid images should pass
558-
validImages := []string{"alpine:latest", "nginx:1.21", "redis:7"}
559-
for _, img := range validImages {
560-
err := validateImageName(img)
561-
assert.NoError(t, err, "Valid image %s should not error", img)
562-
}
563-
}
564-
565-
func TestValidateMultipleImagesFailsOnInvalid(t *testing.T) {
566-
// Test that validation catches invalid images in a list
567-
images := []string{"alpine:latest", "nginx;malicious", "redis:7"}
568-
569-
var firstError error
570-
for _, img := range images {
571-
if err := validateImageName(img); err != nil {
572-
firstError = err
573-
break
574-
}
575-
}
576-
577-
assert.Error(t, firstError, "Should catch invalid image in list")
578-
assert.Contains(t, firstError.Error(), "disallowed character", "Should report specific error")
579-
}
580-
581-
func TestBuildTrivyArgsForMultipleImages(t *testing.T) {
471+
func TestBuildTrivyArgsWithDifferentImages(t *testing.T) {
582472
severityFlag = "CRITICAL"
583473
pkgTypesFlag = ""
584474
ignoreUnfixedFlag = true
585475

586476
images := []string{"alpine:latest", "nginx:1.21", "redis:7"}
587477

588-
// Verify each image gets correct args with same flags
589478
for _, img := range images {
590479
args := buildTrivyArgs(img)
591-
592480
assert.Equal(t, img, args[len(args)-1], "Image name should be last argument")
593481
assert.Contains(t, args, "--severity", "Should contain severity flag")
594482
assert.Contains(t, args, "CRITICAL", "Should use configured severity")
595483
}
596484
}
597485

598-
func TestContainerScanCommandAcceptsMultipleImages(t *testing.T) {
599-
tests := []struct {
600-
name string
601-
args []string
602-
errMsg string
603-
}{
604-
{
605-
name: "single image",
606-
args: []string{"alpine:latest"},
607-
},
608-
{
609-
name: "two images",
610-
args: []string{"alpine:latest", "nginx:1.21"},
611-
},
612-
{
613-
name: "three images",
614-
args: []string{"alpine:latest", "nginx:1.21", "redis:7"},
615-
},
616-
{
617-
name: "many images",
618-
args: []string{"img1:v1", "img2:v2", "img3:v3", "img4:v4", "img5:v5"},
619-
},
620-
}
621-
622-
for _, tt := range tests {
623-
t.Run(tt.name, func(t *testing.T) {
624-
err := containerScanCmd.Args(containerScanCmd, tt.args)
625-
assert.NoError(t, err, "Command should accept %d image(s)", len(tt.args))
626-
})
627-
}
486+
func TestContainerScanCommandAcceptsExactlyOneImage(t *testing.T) {
487+
err := containerScanCmd.Args(containerScanCmd, []string{"alpine:latest"})
488+
assert.NoError(t, err, "Command should accept single image")
628489
}
629490

630491
func TestContainerScanCommandRejectsNoImages(t *testing.T) {

0 commit comments

Comments
 (0)