From 68cb75577a65174d3e0842d9540d9a066c3ade10 Mon Sep 17 00:00:00 2001 From: Dima R <90623914+cx-dmitri-rivin@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:09:12 +0200 Subject: [PATCH 1/5] path fix --- internal/commands/scan.go | 105 +++++++++++++++++++++++------- internal/commands/scan_test.go | 115 +++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 24 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 2dd7c3ca4..c9fc875ae 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -2309,20 +2309,11 @@ func enforceLocalResolutionForTarFiles(cmd *cobra.Command) error { } // isTarFileReference checks if a container image reference points to a tar file. -// Container-security scan-type related function. +// Handles both Unix and Windows paths (e.g., C:\path\file.tar). func isTarFileReference(imageRef string) bool { - // Known prefixes that might precede the actual file path - knownPrefixes := []string{ - dockerArchivePrefix, - ociArchivePrefix, - filePrefix, - ociDirPrefix, - } + knownPrefixes := []string{dockerArchivePrefix, ociArchivePrefix, filePrefix, ociDirPrefix} - // First, trim quotes from the entire input actualRef := strings.Trim(imageRef, "'\"") - - // Strip known prefixes to get the actual reference for _, prefix := range knownPrefixes { if strings.HasPrefix(actualRef, prefix) { actualRef = strings.TrimPrefix(actualRef, prefix) @@ -2331,31 +2322,35 @@ func isTarFileReference(imageRef string) bool { } } - // Check if the reference ends with .tar (case-insensitive) lowerRef := strings.ToLower(actualRef) - - // If it ends with .tar, it's a tar file (no tag suffix allowed) if strings.HasSuffix(lowerRef, ".tar") { return true } - // If it contains a colon but doesn't end with .tar, check if it's a file.tar:tag format (invalid) - // A tar file cannot have a tag suffix like file.tar:tag + if isWindowsAbsolutePath(actualRef) { + return strings.Contains(lowerRef, ".tar") + } + if strings.Contains(actualRef, ":") { parts := strings.Split(actualRef, ":") - const minPartsForTaggedImage = 2 - if len(parts) >= minPartsForTaggedImage { - firstPart := strings.ToLower(parts[0]) - // If the part before the colon is a tar file, this is invalid (tar files don't have tags) - if strings.HasSuffix(firstPart, ".tar") { - return false - } + if len(parts) >= 2 && strings.HasSuffix(strings.ToLower(parts[0]), ".tar") { + return false } } return false } +// isWindowsAbsolutePath checks for Windows drive letter paths (e.g., C:\, D:/). +func isWindowsAbsolutePath(path string) bool { + if len(path) < 3 { + return false + } + firstChar := path[0] + isLetter := (firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z') + return isLetter && path[1] == ':' && (path[2] == '\\' || path[2] == '/') +} + func runCreateScanCommand( scansWrapper wrappers.ScansWrapper, exportWrapper wrappers.ExportWrapper, @@ -3573,7 +3568,7 @@ const ( // Container-security scan-type related function. // This function implements comprehensive validation logic for all supported container image formats: // - Standard image:tag format -// - Tar files (.tar) +// - Tar files (.tar) - including full file paths on Windows (C:\path\file.tar) and Unix (/path/file.tar) // - Prefixed formats (docker:, podman:, containerd:, registry:, docker-archive:, oci-archive:, oci-dir:, file:) // It provides helpful error messages and hints for common user mistakes. func validateContainerImageFormat(containerImage string) error { @@ -3613,6 +3608,11 @@ func validateContainerImageFormat(containerImage string) error { sanitizedInput = containerImage } + // Check if this looks like a file path before parsing colons + if looksLikeFilePath(sanitizedInput) { + return validateFilePath(sanitizedInput) + } + // Step 2: Look for the last colon (:) in the sanitized input lastColonIndex := strings.LastIndex(sanitizedInput, ":") @@ -3684,6 +3684,63 @@ func validateContainerImageFormat(containerImage string) error { return errors.Errorf("--container-images flag error: image does not have a tag") } +// looksLikeFilePath checks if input looks like a file path rather than image:tag. +func looksLikeFilePath(input string) bool { + lowerInput := strings.ToLower(input) + + if isWindowsAbsolutePath(input) { + return true + } + + // If colon exists and part before it looks like a prefix (no separators/dots), it's not a file path + if colonIndex := strings.Index(input, ":"); colonIndex > 0 { + beforeColon := input[:colonIndex] + if !strings.Contains(beforeColon, "/") && !strings.Contains(beforeColon, "\\") && !strings.Contains(beforeColon, ".") { + return false + } + } + + if strings.HasSuffix(lowerInput, ".tar") { + return true + } + + if strings.HasSuffix(lowerInput, ".tar.gz") || strings.HasSuffix(lowerInput, ".tar.bz2") || + strings.HasSuffix(lowerInput, ".tar.xz") || strings.HasSuffix(lowerInput, ".tgz") { + return true + } + + hasPathSeparators := strings.Contains(input, "/") || strings.Contains(input, "\\") + if hasPathSeparators && strings.Contains(lowerInput, ".tar") { + return true + } + + return false +} + +// validateFilePath validates file path input for tar files. +func validateFilePath(filePath string) error { + lowerPath := strings.ToLower(filePath) + + if strings.HasSuffix(lowerPath, ".tar.gz") || strings.HasSuffix(lowerPath, ".tar.bz2") || + strings.HasSuffix(lowerPath, ".tar.xz") || strings.HasSuffix(lowerPath, ".tgz") { + return errors.Errorf("--container-images flag error: file '%s' is compressed, use non-compressed format (tar)", filePath) + } + + if !strings.HasSuffix(lowerPath, ".tar") { + return errors.Errorf("--container-images flag error: file '%s' is not a valid tar file. Expected .tar extension", filePath) + } + + exists, err := osinstaller.FileExists(filePath) + if err != nil { + return errors.Errorf("--container-images flag error: %v", err) + } + if !exists { + return errors.Errorf("--container-images flag error: file '%s' does not exist", filePath) + } + + return nil +} + // getPrefixFromInput extracts the prefix from a container image reference. // Container-security scan-type related function. // Helper function to identify which known prefix is used in the input. diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index a88a2c78a..0a7dc0199 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -2401,6 +2401,91 @@ func Test_validateThresholds(t *testing.T) { // TestValidateContainerImageFormat_Comprehensive tests the complete validation logic // including input normalization, helpful hints, and all error cases. +// TestIsWindowsAbsolutePath tests the Windows absolute path detection. +// Container-security scan-type related test function. +func TestIsWindowsAbsolutePath(t *testing.T) { + testCases := []struct { + name string + input string + expected bool + }{ + // Valid Windows absolute paths + {name: "C drive with backslash", input: "C:\\Users\\file.tar", expected: true}, + {name: "D drive with backslash", input: "D:\\data\\image.tar", expected: true}, + {name: "C drive with forward slash", input: "C:/Users/file.tar", expected: true}, + {name: "Lowercase drive letter", input: "c:\\path\\file.tar", expected: true}, + + // Not Windows absolute paths + {name: "Unix absolute path", input: "/path/to/file.tar", expected: false}, + {name: "Relative path", input: "Downloads/file.tar", expected: false}, + {name: "Simple filename", input: "file.tar", expected: false}, + {name: "Image with tag", input: "nginx:latest", expected: false}, + {name: "Too short", input: "C:", expected: false}, + {name: "No path separator after colon", input: "C:file.tar", expected: false}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + result := isWindowsAbsolutePath(tc.input) + if result != tc.expected { + t.Errorf("isWindowsAbsolutePath(%q) = %v, expected %v", tc.input, result, tc.expected) + } + }) + } +} + +// TestLooksLikeFilePath tests the file path detection logic for cross-platform support. +// Container-security scan-type related test function. +// This test validates the looksLikeFilePath function for various Windows and Unix path formats. +func TestLooksLikeFilePath(t *testing.T) { + testCases := []struct { + name string + input string + expected bool + }{ + // Tar file extensions + {name: "Simple tar file", input: "image.tar", expected: true}, + {name: "Tar.gz file", input: "image.tar.gz", expected: true}, + {name: "Tar.bz2 file", input: "image.tar.bz2", expected: true}, + {name: "Tar.xz file", input: "image.tar.xz", expected: true}, + {name: "Tgz file", input: "image.tgz", expected: true}, + + // Unix-style paths + {name: "Unix relative path with tar", input: "subdir/image.tar", expected: true}, + {name: "Unix absolute path with tar", input: "/path/to/image.tar", expected: true}, + {name: "Unix path with version in filename", input: "Downloads/alpine_3.21.0_podman.tar", expected: true}, + {name: "Unix nested path", input: "path/to/nested/dir/file.tar", expected: true}, + + // Windows-style paths + {name: "Windows absolute path with drive letter", input: "C:\\Users\\Downloads\\image.tar", expected: true}, + {name: "Windows path with forward slash after drive", input: "C:/Users/Downloads/image.tar", expected: true}, + {name: "Windows relative path with backslash", input: "Downloads\\alpine_3.21.0_podman.tar", expected: true}, + {name: "Windows D drive path", input: "D:\\data\\images\\test.tar", expected: true}, + + // Not file paths (image:tag format) + {name: "Simple image:tag", input: "nginx:latest", expected: false}, + {name: "Image with registry", input: "registry.io/namespace/image:tag", expected: false}, + {name: "Image with port", input: "registry.io:5000/image:tag", expected: false}, + {name: "Image without tag", input: "nginx", expected: false}, + + // Edge cases + {name: "Tar file with dots in name", input: "alpine.3.18.0.tar", expected: true}, + {name: "Tar file with version like name", input: "app_v1.2.3.tar", expected: true}, + {name: "Path with tar in middle", input: "tarball/other.tar", expected: true}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + result := looksLikeFilePath(tc.input) + if result != tc.expected { + t.Errorf("looksLikeFilePath(%q) = %v, expected %v", tc.input, result, tc.expected) + } + }) + } +} + // Container-security scan-type related test function. // This test validates all supported container image formats, prefixes, tar files, // error messages, and helpful hints for the --container-images flag. @@ -2487,6 +2572,36 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { expectedError: "--container-images flag error: file 'image.tgz' is compressed, use non-compressed format (tar)", }, + // ==================== File Path Tests (Windows and Unix) ==================== + // Note: These tests validate that path-like inputs are correctly recognized as file paths + { + name: "Valid tar file with filename containing version number", + containerImage: "alpine_3.21.0_podman.tar", + expectedError: "", + setupFiles: []string{"alpine_3.21.0_podman.tar"}, + }, + { + name: "Valid tar file with filename containing underscore and version", + containerImage: "mysql_5.7_backup.tar", + expectedError: "", + setupFiles: []string{"mysql_5.7_backup.tar"}, + }, + { + name: "Invalid - Unix relative path does not exist", + containerImage: "subdir/image.tar", + expectedError: "--container-images flag error: file 'subdir/image.tar' does not exist", + }, + { + name: "Invalid - Unix nested path does not exist", + containerImage: "path/to/archive/my-image.tar", + expectedError: "--container-images flag error: file 'path/to/archive/my-image.tar' does not exist", + }, + { + name: "Invalid - file path with version-like name does not exist", + containerImage: "Downloads/alpine_3.21.0_podman.tar", + expectedError: "--container-images flag error: file 'Downloads/alpine_3.21.0_podman.tar' does not exist", + }, + // ==================== Helpful Hints Tests ==================== { name: "Hint - looks like tar file (wrong extension)", From c3477e23c7ed3761df2e1b29e5d82e9fb59c23b4 Mon Sep 17 00:00:00 2001 From: Dima R <90623914+cx-dmitri-rivin@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:43:53 +0200 Subject: [PATCH 2/5] Fix golangci-lint magic number error in isWindowsAbsolutePath - Add minWindowsPathLength constant to replace magic number 3 - Update isWindowsAbsolutePath to use the constant - Resolves mnd linter error: Magic number: 3, in detected --- internal/commands/scan.go | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 9d1fd354e..d86bb0468 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -43,17 +43,18 @@ import ( ) const ( - failedCreating = "Failed creating a scan" - failedGetting = "Failed showing a scan" - failedGettingTags = "Failed getting tags" - failedDeleting = "Failed deleting a scan" - failedCanceling = "Failed canceling a scan" - failedGettingAll = "Failed listing" - thresholdLog = "%s: Limit = %d, Current = %v" - thresholdMsgLog = "Threshold check finished with status %s : %s" - mbBytes = 1024.0 * 1024.0 - notExploitable = "NOT_EXPLOITABLE" - ignored = "IGNORED" + failedCreating = "Failed creating a scan" + failedGetting = "Failed showing a scan" + failedGettingTags = "Failed getting tags" + failedDeleting = "Failed deleting a scan" + failedCanceling = "Failed canceling a scan" + failedGettingAll = "Failed listing" + thresholdLog = "%s: Limit = %d, Current = %v" + thresholdMsgLog = "Threshold check finished with status %s : %s" + mbBytes = 1024.0 * 1024.0 + notExploitable = "NOT_EXPLOITABLE" + ignored = "IGNORED" + minWindowsPathLength = 3 git = "git" invalidSSHSource = "provided source does not need a key. Make sure you are defining the right source or remove the flag --ssh-key" @@ -2356,7 +2357,7 @@ func isTarFileReference(imageRef string) bool { // isWindowsAbsolutePath checks for Windows drive letter paths (e.g., C:\, D:/). func isWindowsAbsolutePath(path string) bool { - if len(path) < 3 { + if len(path) < minWindowsPathLength { return false } firstChar := path[0] From 35f747efbad7a1c8e0eafe824423aa9a39bb96e7 Mon Sep 17 00:00:00 2001 From: Dima R <90623914+cx-dmitri-rivin@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:08:49 +0200 Subject: [PATCH 3/5] Remove file existence checks from validation - let container resolver handle it - File existence checks were too strict and caused integration test failures - Container resolver will handle non-existent files with proper error messages - This allows for more flexible testing scenarios and better separation of concerns - Updated unit tests to reflect the new behavior --- internal/commands/scan.go | 57 ++++++++++------------------------ internal/commands/scan_test.go | 48 ++++++++++++++++------------ 2 files changed, 44 insertions(+), 61 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index d86bb0468..590a7a928 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -30,7 +30,6 @@ import ( exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" "github.com/checkmarx/ast-cli/internal/logger" "github.com/checkmarx/ast-cli/internal/services" - "github.com/checkmarx/ast-cli/internal/services/osinstaller" "github.com/google/uuid" "github.com/pkg/errors" @@ -3659,14 +3658,8 @@ func validateContainerImageFormat(containerImage string) error { // Step 3: No colon found - check if it's a tar file or special prefix that doesn't require tags lowerInput := strings.ToLower(sanitizedInput) if strings.HasSuffix(lowerInput, ".tar") { - // It's a tar file - check if it exists locally - exists, err := osinstaller.FileExists(sanitizedInput) - if err != nil { - return errors.Errorf("--container-images flag error: %v", err) - } - if !exists { - return errors.Errorf("--container-images flag error: file '%s' does not exist", sanitizedInput) - } + // It's a tar file - validation passed + // Note: We don't check file existence here for the same reasons as in validateFilePath return nil // Valid tar file } @@ -3744,13 +3737,10 @@ func validateFilePath(filePath string) error { return errors.Errorf("--container-images flag error: file '%s' is not a valid tar file. Expected .tar extension", filePath) } - exists, err := osinstaller.FileExists(filePath) - if err != nil { - return errors.Errorf("--container-images flag error: %v", err) - } - if !exists { - return errors.Errorf("--container-images flag error: file '%s' does not exist", filePath) - } + // Note: We don't check file existence here because: + // 1. The file might be created later in the workflow + // 2. The container resolver will handle non-existent files with proper error messages + // 3. This allows for more flexible testing scenarios return nil } @@ -3800,16 +3790,14 @@ func validatePrefixedContainerImage(containerImage, prefix string) error { // validateArchivePrefix validates archive-based prefixes (file:, docker-archive:, oci-archive:). // Container-security scan-type related function. func validateArchivePrefix(imageRef string) error { - exists, err := osinstaller.FileExists(imageRef) - if err != nil { - return errors.Errorf("--container-images flag error: %v", err) - } - if !exists { - // Check if user mistakenly used archive prefix with an image name:tag format - if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") { - return errors.Errorf("--container-images flag error: file '%s' does not exist. Did you try to scan an image using image name and tag?", imageRef) - } - return errors.Errorf("--container-images flag error: file '%s' does not exist", imageRef) + // Note: We don't check file existence here for the same reasons as in validateFilePath + // The container resolver will handle non-existent files with proper error messages + + // Check if user mistakenly used archive prefix with an image name:tag format + if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") { + // This looks like they tried to use image:tag format with an archive prefix + // Provide a helpful hint + return errors.Errorf("--container-images flag error: archive prefix expects a file path, not image:tag format. Found: '%s'", imageRef) } return nil } @@ -3822,22 +3810,9 @@ func validateOCIDirPrefix(imageRef string) error { // 2. Files (like .tar files) // 3. Can have optional :tag suffix - pathToCheck := imageRef - if strings.Contains(imageRef, ":") { - // Handle case like "oci-dir:/path/to/dir:tag" or "oci-dir:name.tar:tag" - pathParts := strings.Split(imageRef, ":") - if len(pathParts) > 0 && pathParts[0] != "" { - pathToCheck = pathParts[0] - } - } + // Note: We don't check path existence here for the same reasons as in validateFilePath + // The container resolver will handle non-existent paths with proper error messages - exists, err := osinstaller.FileExists(pathToCheck) - if err != nil { - return errors.Errorf("--container-images flag error: path %s does not exist: %v", pathToCheck, err) - } - if !exists { - return errors.Errorf("--container-images flag error: path %s does not exist", pathToCheck) - } return nil } diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index e51614dc4..16ed36a08 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -2569,9 +2569,10 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image-with-path.tar"}, }, { - name: "Invalid - tar file does not exist", + name: "Valid - tar file (existence checked by container resolver)", containerImage: "nonexistent.tar", - expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", + expectedError: "", + setupFiles: []string{"nonexistent.tar"}, }, // ==================== Compressed Tar Tests ==================== @@ -2611,19 +2612,22 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"mysql_5.7_backup.tar"}, }, { - name: "Invalid - Unix relative path does not exist", + name: "Valid - Unix relative path (existence checked by container resolver)", containerImage: "subdir/image.tar", - expectedError: "--container-images flag error: file 'subdir/image.tar' does not exist", + expectedError: "", + setupFiles: []string{"subdir/image.tar"}, }, { - name: "Invalid - Unix nested path does not exist", + name: "Valid - Unix nested path (existence checked by container resolver)", containerImage: "path/to/archive/my-image.tar", - expectedError: "--container-images flag error: file 'path/to/archive/my-image.tar' does not exist", + expectedError: "", + setupFiles: []string{"path/to/archive/my-image.tar"}, }, { - name: "Invalid - file path with version-like name does not exist", + name: "Valid - file path with version-like name (existence checked by container resolver)", containerImage: "Downloads/alpine_3.21.0_podman.tar", - expectedError: "--container-images flag error: file 'Downloads/alpine_3.21.0_podman.tar' does not exist", + expectedError: "", + setupFiles: []string{"Downloads/alpine_3.21.0_podman.tar"}, }, // ==================== Helpful Hints Tests ==================== @@ -2652,19 +2656,20 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"prefixed-image.tar"}, }, { - name: "Invalid file prefix - missing file", + name: "Valid file prefix (existence checked by container resolver)", containerImage: "file:nonexistent.tar", - expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", + expectedError: "", + setupFiles: []string{"nonexistent.tar"}, }, { name: "Hint - file prefix with image name", containerImage: "file:nginx:latest", - expectedError: "--container-images flag error: file 'nginx:latest' does not exist. Did you try to scan an image using image name and tag?", + expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'nginx:latest'", }, { name: "Hint - file prefix with image (no tag)", containerImage: "file:alpine:3.18", - expectedError: "--container-images flag error: file 'alpine:3.18' does not exist. Did you try to scan an image using image name and tag?", + expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'alpine:3.18'", }, // ==================== Docker Archive Tests ==================== @@ -2675,14 +2680,15 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image.tar"}, }, { - name: "Invalid docker-archive - missing file", + name: "Valid docker-archive (existence checked by container resolver)", containerImage: "docker-archive:nonexistent.tar", - expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", + expectedError: "", + setupFiles: []string{"nonexistent.tar"}, }, { name: "Hint - docker-archive with image name", containerImage: "docker-archive:nginx:latest", - expectedError: "--container-images flag error: file 'nginx:latest' does not exist. Did you try to scan an image using image name and tag?", + expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'nginx:latest'", }, // ==================== OCI Archive Tests ==================== @@ -2693,14 +2699,15 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image.tar"}, }, { - name: "Invalid oci-archive - missing file", + name: "Valid oci-archive (existence checked by container resolver)", containerImage: "oci-archive:nonexistent.tar", - expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", + expectedError: "", + setupFiles: []string{"nonexistent.tar"}, }, { name: "Hint - oci-archive with image name", containerImage: "oci-archive:ubuntu:22.04", - expectedError: "--container-images flag error: file 'ubuntu:22.04' does not exist. Did you try to scan an image using image name and tag?", + expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'ubuntu:22.04'", }, // ==================== Docker Daemon Tests ==================== @@ -2786,9 +2793,10 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupDirs: []string{"oci-image-dir"}, }, { - name: "Invalid oci-dir - directory does not exist", + name: "Valid oci-dir (existence checked by container resolver)", containerImage: "oci-dir:nonexistent-dir", - expectedError: "--container-images flag error: path nonexistent-dir does not exist", + expectedError: "", + setupDirs: []string{"nonexistent-dir"}, }, { name: "Valid oci-dir with tar file", From ed55460c004bf7d76ab07ae679fd651c1a2f9ca3 Mon Sep 17 00:00:00 2001 From: Dima R <90623914+cx-dmitri-rivin@users.noreply.github.com> Date: Wed, 28 Jan 2026 12:15:07 +0200 Subject: [PATCH 4/5] Revert "Remove file existence checks from validation - let container resolver handle it" This reverts commit 35f747efbad7a1c8e0eafe824423aa9a39bb96e7. --- internal/commands/scan.go | 57 ++++++++++++++++++++++++---------- internal/commands/scan_test.go | 48 ++++++++++++---------------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 590a7a928..d86bb0468 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -30,6 +30,7 @@ import ( exitCodes "github.com/checkmarx/ast-cli/internal/constants/exit-codes" "github.com/checkmarx/ast-cli/internal/logger" "github.com/checkmarx/ast-cli/internal/services" + "github.com/checkmarx/ast-cli/internal/services/osinstaller" "github.com/google/uuid" "github.com/pkg/errors" @@ -3658,8 +3659,14 @@ func validateContainerImageFormat(containerImage string) error { // Step 3: No colon found - check if it's a tar file or special prefix that doesn't require tags lowerInput := strings.ToLower(sanitizedInput) if strings.HasSuffix(lowerInput, ".tar") { - // It's a tar file - validation passed - // Note: We don't check file existence here for the same reasons as in validateFilePath + // It's a tar file - check if it exists locally + exists, err := osinstaller.FileExists(sanitizedInput) + if err != nil { + return errors.Errorf("--container-images flag error: %v", err) + } + if !exists { + return errors.Errorf("--container-images flag error: file '%s' does not exist", sanitizedInput) + } return nil // Valid tar file } @@ -3737,10 +3744,13 @@ func validateFilePath(filePath string) error { return errors.Errorf("--container-images flag error: file '%s' is not a valid tar file. Expected .tar extension", filePath) } - // Note: We don't check file existence here because: - // 1. The file might be created later in the workflow - // 2. The container resolver will handle non-existent files with proper error messages - // 3. This allows for more flexible testing scenarios + exists, err := osinstaller.FileExists(filePath) + if err != nil { + return errors.Errorf("--container-images flag error: %v", err) + } + if !exists { + return errors.Errorf("--container-images flag error: file '%s' does not exist", filePath) + } return nil } @@ -3790,14 +3800,16 @@ func validatePrefixedContainerImage(containerImage, prefix string) error { // validateArchivePrefix validates archive-based prefixes (file:, docker-archive:, oci-archive:). // Container-security scan-type related function. func validateArchivePrefix(imageRef string) error { - // Note: We don't check file existence here for the same reasons as in validateFilePath - // The container resolver will handle non-existent files with proper error messages - - // Check if user mistakenly used archive prefix with an image name:tag format - if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") { - // This looks like they tried to use image:tag format with an archive prefix - // Provide a helpful hint - return errors.Errorf("--container-images flag error: archive prefix expects a file path, not image:tag format. Found: '%s'", imageRef) + exists, err := osinstaller.FileExists(imageRef) + if err != nil { + return errors.Errorf("--container-images flag error: %v", err) + } + if !exists { + // Check if user mistakenly used archive prefix with an image name:tag format + if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") { + return errors.Errorf("--container-images flag error: file '%s' does not exist. Did you try to scan an image using image name and tag?", imageRef) + } + return errors.Errorf("--container-images flag error: file '%s' does not exist", imageRef) } return nil } @@ -3810,9 +3822,22 @@ func validateOCIDirPrefix(imageRef string) error { // 2. Files (like .tar files) // 3. Can have optional :tag suffix - // Note: We don't check path existence here for the same reasons as in validateFilePath - // The container resolver will handle non-existent paths with proper error messages + pathToCheck := imageRef + if strings.Contains(imageRef, ":") { + // Handle case like "oci-dir:/path/to/dir:tag" or "oci-dir:name.tar:tag" + pathParts := strings.Split(imageRef, ":") + if len(pathParts) > 0 && pathParts[0] != "" { + pathToCheck = pathParts[0] + } + } + exists, err := osinstaller.FileExists(pathToCheck) + if err != nil { + return errors.Errorf("--container-images flag error: path %s does not exist: %v", pathToCheck, err) + } + if !exists { + return errors.Errorf("--container-images flag error: path %s does not exist", pathToCheck) + } return nil } diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 16ed36a08..e51614dc4 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -2569,10 +2569,9 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image-with-path.tar"}, }, { - name: "Valid - tar file (existence checked by container resolver)", + name: "Invalid - tar file does not exist", containerImage: "nonexistent.tar", - expectedError: "", - setupFiles: []string{"nonexistent.tar"}, + expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", }, // ==================== Compressed Tar Tests ==================== @@ -2612,22 +2611,19 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"mysql_5.7_backup.tar"}, }, { - name: "Valid - Unix relative path (existence checked by container resolver)", + name: "Invalid - Unix relative path does not exist", containerImage: "subdir/image.tar", - expectedError: "", - setupFiles: []string{"subdir/image.tar"}, + expectedError: "--container-images flag error: file 'subdir/image.tar' does not exist", }, { - name: "Valid - Unix nested path (existence checked by container resolver)", + name: "Invalid - Unix nested path does not exist", containerImage: "path/to/archive/my-image.tar", - expectedError: "", - setupFiles: []string{"path/to/archive/my-image.tar"}, + expectedError: "--container-images flag error: file 'path/to/archive/my-image.tar' does not exist", }, { - name: "Valid - file path with version-like name (existence checked by container resolver)", + name: "Invalid - file path with version-like name does not exist", containerImage: "Downloads/alpine_3.21.0_podman.tar", - expectedError: "", - setupFiles: []string{"Downloads/alpine_3.21.0_podman.tar"}, + expectedError: "--container-images flag error: file 'Downloads/alpine_3.21.0_podman.tar' does not exist", }, // ==================== Helpful Hints Tests ==================== @@ -2656,20 +2652,19 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"prefixed-image.tar"}, }, { - name: "Valid file prefix (existence checked by container resolver)", + name: "Invalid file prefix - missing file", containerImage: "file:nonexistent.tar", - expectedError: "", - setupFiles: []string{"nonexistent.tar"}, + expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", }, { name: "Hint - file prefix with image name", containerImage: "file:nginx:latest", - expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'nginx:latest'", + expectedError: "--container-images flag error: file 'nginx:latest' does not exist. Did you try to scan an image using image name and tag?", }, { name: "Hint - file prefix with image (no tag)", containerImage: "file:alpine:3.18", - expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'alpine:3.18'", + expectedError: "--container-images flag error: file 'alpine:3.18' does not exist. Did you try to scan an image using image name and tag?", }, // ==================== Docker Archive Tests ==================== @@ -2680,15 +2675,14 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image.tar"}, }, { - name: "Valid docker-archive (existence checked by container resolver)", + name: "Invalid docker-archive - missing file", containerImage: "docker-archive:nonexistent.tar", - expectedError: "", - setupFiles: []string{"nonexistent.tar"}, + expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", }, { name: "Hint - docker-archive with image name", containerImage: "docker-archive:nginx:latest", - expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'nginx:latest'", + expectedError: "--container-images flag error: file 'nginx:latest' does not exist. Did you try to scan an image using image name and tag?", }, // ==================== OCI Archive Tests ==================== @@ -2699,15 +2693,14 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupFiles: []string{"image.tar"}, }, { - name: "Valid oci-archive (existence checked by container resolver)", + name: "Invalid oci-archive - missing file", containerImage: "oci-archive:nonexistent.tar", - expectedError: "", - setupFiles: []string{"nonexistent.tar"}, + expectedError: "--container-images flag error: file 'nonexistent.tar' does not exist", }, { name: "Hint - oci-archive with image name", containerImage: "oci-archive:ubuntu:22.04", - expectedError: "--container-images flag error: archive prefix expects a file path, not image:tag format. Found: 'ubuntu:22.04'", + expectedError: "--container-images flag error: file 'ubuntu:22.04' does not exist. Did you try to scan an image using image name and tag?", }, // ==================== Docker Daemon Tests ==================== @@ -2793,10 +2786,9 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { setupDirs: []string{"oci-image-dir"}, }, { - name: "Valid oci-dir (existence checked by container resolver)", + name: "Invalid oci-dir - directory does not exist", containerImage: "oci-dir:nonexistent-dir", - expectedError: "", - setupDirs: []string{"nonexistent-dir"}, + expectedError: "--container-images flag error: path nonexistent-dir does not exist", }, { name: "Valid oci-dir with tar file", From fc62f2503d05aee71ce030d1315e6e7f285023e1 Mon Sep 17 00:00:00 2001 From: Dima R <90623914+cx-dmitri-rivin@users.noreply.github.com> Date: Thu, 29 Jan 2026 13:27:51 +0200 Subject: [PATCH 5/5] cr fixes --- internal/commands/scan.go | 71 +++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 33 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index d86bb0468..85944edcf 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -43,18 +43,19 @@ import ( ) const ( - failedCreating = "Failed creating a scan" - failedGetting = "Failed showing a scan" - failedGettingTags = "Failed getting tags" - failedDeleting = "Failed deleting a scan" - failedCanceling = "Failed canceling a scan" - failedGettingAll = "Failed listing" - thresholdLog = "%s: Limit = %d, Current = %v" - thresholdMsgLog = "Threshold check finished with status %s : %s" - mbBytes = 1024.0 * 1024.0 - notExploitable = "NOT_EXPLOITABLE" - ignored = "IGNORED" - minWindowsPathLength = 3 + failedCreating = "Failed creating a scan" + failedGetting = "Failed showing a scan" + failedGettingTags = "Failed getting tags" + failedDeleting = "Failed deleting a scan" + failedCanceling = "Failed canceling a scan" + failedGettingAll = "Failed listing" + thresholdLog = "%s: Limit = %d, Current = %v" + thresholdMsgLog = "Threshold check finished with status %s : %s" + mbBytes = 1024.0 * 1024.0 + notExploitable = "NOT_EXPLOITABLE" + ignored = "IGNORED" + minWindowsPathLength = 3 + containerImagesFlagError = "--container-images flag error" git = "git" invalidSSHSource = "provided source does not need a key. Make sure you are defining the right source or remove the flag --ssh-key" @@ -1263,7 +1264,7 @@ func addContainersScan(cmd *cobra.Command, resubmitConfig []wrappers.Config) (ma continue } if containerImagesErr := validateContainerImageFormat(containerImageName); containerImagesErr != nil { - errorMsg := strings.TrimPrefix(containerImagesErr.Error(), "--container-images flag error: ") + errorMsg := strings.TrimPrefix(containerImagesErr.Error(), containerImagesFlagError+": ") validationErrors = append(validationErrors, fmt.Sprintf("User input: '%s' error: %s", containerImageName, errorMsg)) } } @@ -3662,23 +3663,22 @@ func validateContainerImageFormat(containerImage string) error { // It's a tar file - check if it exists locally exists, err := osinstaller.FileExists(sanitizedInput) if err != nil { - return errors.Errorf("--container-images flag error: %v", err) + return errors.Errorf("%s: %v", containerImagesFlagError, err) } if !exists { - return errors.Errorf("--container-images flag error: file '%s' does not exist", sanitizedInput) + return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, sanitizedInput) } return nil // Valid tar file } // Check for compressed tar files - if strings.HasSuffix(lowerInput, ".tar.gz") || strings.HasSuffix(lowerInput, ".tar.bz2") || - strings.HasSuffix(lowerInput, ".tar.xz") || strings.HasSuffix(lowerInput, ".tgz") { - return errors.Errorf("--container-images flag error: file '%s' is compressed, use non-compressed format (tar)", sanitizedInput) + if isCompressedTarFile(sanitizedInput) { + return errors.Errorf("%s: file '%s' is compressed, use non-compressed format (tar)", containerImagesFlagError, sanitizedInput) } // Check if it looks like a tar file extension (contains ".tar." but not a valid extension) if strings.Contains(lowerInput, ".tar.") { - return errors.Errorf("--container-images flag error: image does not have a tag. Did you try to scan a tar file?") + return errors.Errorf("%s: image does not have a tag. Did you try to scan a tar file?", containerImagesFlagError) } // Step 4: Special handling for prefixes that don't require tags (e.g., oci-dir:) @@ -3695,7 +3695,14 @@ func validateContainerImageFormat(containerImage string) error { } // Step 5: Not a tar file, no special prefix, and no colon - assume user forgot to add tag (error) - return errors.Errorf("--container-images flag error: image does not have a tag") + return errors.Errorf("%s: image does not have a tag", containerImagesFlagError) +} + +// isCompressedTarFile checks if the given path has a compressed tar file extension. +func isCompressedTarFile(path string) bool { + lowerPath := strings.ToLower(path) + return strings.HasSuffix(lowerPath, ".tar.gz") || strings.HasSuffix(lowerPath, ".tar.bz2") || + strings.HasSuffix(lowerPath, ".tar.xz") || strings.HasSuffix(lowerPath, ".tgz") } // looksLikeFilePath checks if input looks like a file path rather than image:tag. @@ -3718,8 +3725,7 @@ func looksLikeFilePath(input string) bool { return true } - if strings.HasSuffix(lowerInput, ".tar.gz") || strings.HasSuffix(lowerInput, ".tar.bz2") || - strings.HasSuffix(lowerInput, ".tar.xz") || strings.HasSuffix(lowerInput, ".tgz") { + if isCompressedTarFile(input) { return true } @@ -3735,21 +3741,20 @@ func looksLikeFilePath(input string) bool { func validateFilePath(filePath string) error { lowerPath := strings.ToLower(filePath) - if strings.HasSuffix(lowerPath, ".tar.gz") || strings.HasSuffix(lowerPath, ".tar.bz2") || - strings.HasSuffix(lowerPath, ".tar.xz") || strings.HasSuffix(lowerPath, ".tgz") { - return errors.Errorf("--container-images flag error: file '%s' is compressed, use non-compressed format (tar)", filePath) + if isCompressedTarFile(filePath) { + return errors.Errorf("%s: file '%s' is compressed, use non-compressed format (tar)", containerImagesFlagError, filePath) } if !strings.HasSuffix(lowerPath, ".tar") { - return errors.Errorf("--container-images flag error: file '%s' is not a valid tar file. Expected .tar extension", filePath) + return errors.Errorf("%s: file '%s' is not a valid tar file. Expected .tar extension", containerImagesFlagError, filePath) } exists, err := osinstaller.FileExists(filePath) if err != nil { - return errors.Errorf("--container-images flag error: %v", err) + return errors.Errorf("%s: %v", containerImagesFlagError, err) } if !exists { - return errors.Errorf("--container-images flag error: file '%s' does not exist", filePath) + return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, filePath) } return nil @@ -3802,14 +3807,14 @@ func validatePrefixedContainerImage(containerImage, prefix string) error { func validateArchivePrefix(imageRef string) error { exists, err := osinstaller.FileExists(imageRef) if err != nil { - return errors.Errorf("--container-images flag error: %v", err) + return errors.Errorf("%s: %v", containerImagesFlagError, err) } if !exists { // Check if user mistakenly used archive prefix with an image name:tag format if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") { - return errors.Errorf("--container-images flag error: file '%s' does not exist. Did you try to scan an image using image name and tag?", imageRef) + return errors.Errorf("%s: file '%s' does not exist. Did you try to scan an image using image name and tag?", containerImagesFlagError, imageRef) } - return errors.Errorf("--container-images flag error: file '%s' does not exist", imageRef) + return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, imageRef) } return nil } @@ -3833,10 +3838,10 @@ func validateOCIDirPrefix(imageRef string) error { exists, err := osinstaller.FileExists(pathToCheck) if err != nil { - return errors.Errorf("--container-images flag error: path %s does not exist: %v", pathToCheck, err) + return errors.Errorf("%s: path %s does not exist: %v", containerImagesFlagError, pathToCheck, err) } if !exists { - return errors.Errorf("--container-images flag error: path %s does not exist", pathToCheck) + return errors.Errorf("%s: path %s does not exist", containerImagesFlagError, pathToCheck) } return nil }