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] 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)",