Skip to content
Merged
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
159 changes: 111 additions & 48 deletions internal/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +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"
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"
Expand Down Expand Up @@ -1262,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))
}
}
Expand Down Expand Up @@ -2322,20 +2324,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)
Expand All @@ -2344,31 +2337,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) < minWindowsPathLength {
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 @@ -3586,7 +3583,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 {
Expand Down Expand Up @@ -3626,6 +3623,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, ":")

Expand Down Expand Up @@ -3661,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:)
Expand All @@ -3694,7 +3695,69 @@ 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.
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 isCompressedTarFile(input) {
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 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("%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("%s: %v", containerImagesFlagError, err)
}
if !exists {
return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, filePath)
}

return nil
}

// getPrefixFromInput extracts the prefix from a container image reference.
Expand Down Expand Up @@ -3744,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
}
Expand All @@ -3775,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
}
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 @@ -2425,6 +2425,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 @@ -2511,6 +2596,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