Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 81 additions & 24 deletions internal/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -2309,20 +2309,11 @@
}

// 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)
Expand All @@ -2331,31 +2322,35 @@
}
}

// 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 {

Check failure on line 2346 in internal/commands/scan.go

View workflow job for this annotation

GitHub Actions / lint

Magic number: 3, in <condition> detected (mnd)
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,
Expand Down Expand Up @@ -3573,7 +3568,7 @@
// 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 {
Expand Down Expand Up @@ -3613,6 +3608,11 @@
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, ":")

Expand Down Expand Up @@ -3684,6 +3684,63 @@
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.
Expand Down
115 changes: 115 additions & 0 deletions internal/commands/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)",
Expand Down
Loading