Skip to content

Commit afc3de7

Browse files
main fetched
2 parents c3faf54 + 4c1e704 commit afc3de7

File tree

2 files changed

+226
-48
lines changed

2 files changed

+226
-48
lines changed

internal/commands/scan.go

Lines changed: 111 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,19 @@ import (
4343
)
4444

4545
const (
46-
failedCreating = "Failed creating a scan"
47-
failedGetting = "Failed showing a scan"
48-
failedGettingTags = "Failed getting tags"
49-
failedDeleting = "Failed deleting a scan"
50-
failedCanceling = "Failed canceling a scan"
51-
failedGettingAll = "Failed listing"
52-
thresholdLog = "%s: Limit = %d, Current = %v"
53-
thresholdMsgLog = "Threshold check finished with status %s : %s"
54-
mbBytes = 1024.0 * 1024.0
55-
notExploitable = "NOT_EXPLOITABLE"
56-
ignored = "IGNORED"
46+
failedCreating = "Failed creating a scan"
47+
failedGetting = "Failed showing a scan"
48+
failedGettingTags = "Failed getting tags"
49+
failedDeleting = "Failed deleting a scan"
50+
failedCanceling = "Failed canceling a scan"
51+
failedGettingAll = "Failed listing"
52+
thresholdLog = "%s: Limit = %d, Current = %v"
53+
thresholdMsgLog = "Threshold check finished with status %s : %s"
54+
mbBytes = 1024.0 * 1024.0
55+
notExploitable = "NOT_EXPLOITABLE"
56+
ignored = "IGNORED"
57+
minWindowsPathLength = 3
58+
containerImagesFlagError = "--container-images flag error"
5759

5860
git = "git"
5961
invalidSSHSource = "provided source does not need a key. Make sure you are defining the right source or remove the flag --ssh-key"
@@ -1262,7 +1264,7 @@ func addContainersScan(cmd *cobra.Command, resubmitConfig []wrappers.Config) (ma
12621264
continue
12631265
}
12641266
if containerImagesErr := validateContainerImageFormat(containerImageName); containerImagesErr != nil {
1265-
errorMsg := strings.TrimPrefix(containerImagesErr.Error(), "--container-images flag error: ")
1267+
errorMsg := strings.TrimPrefix(containerImagesErr.Error(), containerImagesFlagError+": ")
12661268
validationErrors = append(validationErrors, fmt.Sprintf("User input: '%s' error: %s", containerImageName, errorMsg))
12671269
}
12681270
}
@@ -2322,20 +2324,11 @@ func enforceLocalResolutionForTarFiles(cmd *cobra.Command) error {
23222324
}
23232325

23242326
// isTarFileReference checks if a container image reference points to a tar file.
2325-
// Container-security scan-type related function.
2327+
// Handles both Unix and Windows paths (e.g., C:\path\file.tar).
23262328
func isTarFileReference(imageRef string) bool {
2327-
// Known prefixes that might precede the actual file path
2328-
knownPrefixes := []string{
2329-
dockerArchivePrefix,
2330-
ociArchivePrefix,
2331-
filePrefix,
2332-
ociDirPrefix,
2333-
}
2329+
knownPrefixes := []string{dockerArchivePrefix, ociArchivePrefix, filePrefix, ociDirPrefix}
23342330

2335-
// First, trim quotes from the entire input
23362331
actualRef := strings.Trim(imageRef, "'\"")
2337-
2338-
// Strip known prefixes to get the actual reference
23392332
for _, prefix := range knownPrefixes {
23402333
if strings.HasPrefix(actualRef, prefix) {
23412334
actualRef = strings.TrimPrefix(actualRef, prefix)
@@ -2344,31 +2337,35 @@ func isTarFileReference(imageRef string) bool {
23442337
}
23452338
}
23462339

2347-
// Check if the reference ends with .tar (case-insensitive)
23482340
lowerRef := strings.ToLower(actualRef)
2349-
2350-
// If it ends with .tar, it's a tar file (no tag suffix allowed)
23512341
if strings.HasSuffix(lowerRef, ".tar") {
23522342
return true
23532343
}
23542344

2355-
// If it contains a colon but doesn't end with .tar, check if it's a file.tar:tag format (invalid)
2356-
// A tar file cannot have a tag suffix like file.tar:tag
2345+
if isWindowsAbsolutePath(actualRef) {
2346+
return strings.Contains(lowerRef, ".tar")
2347+
}
2348+
23572349
if strings.Contains(actualRef, ":") {
23582350
parts := strings.Split(actualRef, ":")
2359-
const minPartsForTaggedImage = 2
2360-
if len(parts) >= minPartsForTaggedImage {
2361-
firstPart := strings.ToLower(parts[0])
2362-
// If the part before the colon is a tar file, this is invalid (tar files don't have tags)
2363-
if strings.HasSuffix(firstPart, ".tar") {
2364-
return false
2365-
}
2351+
if len(parts) >= 2 && strings.HasSuffix(strings.ToLower(parts[0]), ".tar") {
2352+
return false
23662353
}
23672354
}
23682355

23692356
return false
23702357
}
23712358

2359+
// isWindowsAbsolutePath checks for Windows drive letter paths (e.g., C:\, D:/).
2360+
func isWindowsAbsolutePath(path string) bool {
2361+
if len(path) < minWindowsPathLength {
2362+
return false
2363+
}
2364+
firstChar := path[0]
2365+
isLetter := (firstChar >= 'A' && firstChar <= 'Z') || (firstChar >= 'a' && firstChar <= 'z')
2366+
return isLetter && path[1] == ':' && (path[2] == '\\' || path[2] == '/')
2367+
}
2368+
23722369
func runCreateScanCommand(
23732370
scansWrapper wrappers.ScansWrapper,
23742371
exportWrapper wrappers.ExportWrapper,
@@ -3586,7 +3583,7 @@ const (
35863583
// Container-security scan-type related function.
35873584
// This function implements comprehensive validation logic for all supported container image formats:
35883585
// - Standard image:tag format
3589-
// - Tar files (.tar)
3586+
// - Tar files (.tar) - including full file paths on Windows (C:\path\file.tar) and Unix (/path/file.tar)
35903587
// - Prefixed formats (docker:, podman:, containerd:, registry:, docker-archive:, oci-archive:, oci-dir:, file:)
35913588
// It provides helpful error messages and hints for common user mistakes.
35923589
func validateContainerImageFormat(containerImage string) error {
@@ -3626,6 +3623,11 @@ func validateContainerImageFormat(containerImage string) error {
36263623
sanitizedInput = containerImage
36273624
}
36283625

3626+
// Check if this looks like a file path before parsing colons
3627+
if looksLikeFilePath(sanitizedInput) {
3628+
return validateFilePath(sanitizedInput)
3629+
}
3630+
36293631
// Step 2: Look for the last colon (:) in the sanitized input
36303632
lastColonIndex := strings.LastIndex(sanitizedInput, ":")
36313633

@@ -3661,23 +3663,22 @@ func validateContainerImageFormat(containerImage string) error {
36613663
// It's a tar file - check if it exists locally
36623664
exists, err := osinstaller.FileExists(sanitizedInput)
36633665
if err != nil {
3664-
return errors.Errorf("--container-images flag error: %v", err)
3666+
return errors.Errorf("%s: %v", containerImagesFlagError, err)
36653667
}
36663668
if !exists {
3667-
return errors.Errorf("--container-images flag error: file '%s' does not exist", sanitizedInput)
3669+
return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, sanitizedInput)
36683670
}
36693671
return nil // Valid tar file
36703672
}
36713673

36723674
// Check for compressed tar files
3673-
if strings.HasSuffix(lowerInput, ".tar.gz") || strings.HasSuffix(lowerInput, ".tar.bz2") ||
3674-
strings.HasSuffix(lowerInput, ".tar.xz") || strings.HasSuffix(lowerInput, ".tgz") {
3675-
return errors.Errorf("--container-images flag error: file '%s' is compressed, use non-compressed format (tar)", sanitizedInput)
3675+
if isCompressedTarFile(sanitizedInput) {
3676+
return errors.Errorf("%s: file '%s' is compressed, use non-compressed format (tar)", containerImagesFlagError, sanitizedInput)
36763677
}
36773678

36783679
// Check if it looks like a tar file extension (contains ".tar." but not a valid extension)
36793680
if strings.Contains(lowerInput, ".tar.") {
3680-
return errors.Errorf("--container-images flag error: image does not have a tag. Did you try to scan a tar file?")
3681+
return errors.Errorf("%s: image does not have a tag. Did you try to scan a tar file?", containerImagesFlagError)
36813682
}
36823683

36833684
// Step 4: Special handling for prefixes that don't require tags (e.g., oci-dir:)
@@ -3694,7 +3695,69 @@ func validateContainerImageFormat(containerImage string) error {
36943695
}
36953696

36963697
// Step 5: Not a tar file, no special prefix, and no colon - assume user forgot to add tag (error)
3697-
return errors.Errorf("--container-images flag error: image does not have a tag")
3698+
return errors.Errorf("%s: image does not have a tag", containerImagesFlagError)
3699+
}
3700+
3701+
// isCompressedTarFile checks if the given path has a compressed tar file extension.
3702+
func isCompressedTarFile(path string) bool {
3703+
lowerPath := strings.ToLower(path)
3704+
return strings.HasSuffix(lowerPath, ".tar.gz") || strings.HasSuffix(lowerPath, ".tar.bz2") ||
3705+
strings.HasSuffix(lowerPath, ".tar.xz") || strings.HasSuffix(lowerPath, ".tgz")
3706+
}
3707+
3708+
// looksLikeFilePath checks if input looks like a file path rather than image:tag.
3709+
func looksLikeFilePath(input string) bool {
3710+
lowerInput := strings.ToLower(input)
3711+
3712+
if isWindowsAbsolutePath(input) {
3713+
return true
3714+
}
3715+
3716+
// If colon exists and part before it looks like a prefix (no separators/dots), it's not a file path
3717+
if colonIndex := strings.Index(input, ":"); colonIndex > 0 {
3718+
beforeColon := input[:colonIndex]
3719+
if !strings.Contains(beforeColon, "/") && !strings.Contains(beforeColon, "\\") && !strings.Contains(beforeColon, ".") {
3720+
return false
3721+
}
3722+
}
3723+
3724+
if strings.HasSuffix(lowerInput, ".tar") {
3725+
return true
3726+
}
3727+
3728+
if isCompressedTarFile(input) {
3729+
return true
3730+
}
3731+
3732+
hasPathSeparators := strings.Contains(input, "/") || strings.Contains(input, "\\")
3733+
if hasPathSeparators && strings.Contains(lowerInput, ".tar") {
3734+
return true
3735+
}
3736+
3737+
return false
3738+
}
3739+
3740+
// validateFilePath validates file path input for tar files.
3741+
func validateFilePath(filePath string) error {
3742+
lowerPath := strings.ToLower(filePath)
3743+
3744+
if isCompressedTarFile(filePath) {
3745+
return errors.Errorf("%s: file '%s' is compressed, use non-compressed format (tar)", containerImagesFlagError, filePath)
3746+
}
3747+
3748+
if !strings.HasSuffix(lowerPath, ".tar") {
3749+
return errors.Errorf("%s: file '%s' is not a valid tar file. Expected .tar extension", containerImagesFlagError, filePath)
3750+
}
3751+
3752+
exists, err := osinstaller.FileExists(filePath)
3753+
if err != nil {
3754+
return errors.Errorf("%s: %v", containerImagesFlagError, err)
3755+
}
3756+
if !exists {
3757+
return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, filePath)
3758+
}
3759+
3760+
return nil
36983761
}
36993762

37003763
// getPrefixFromInput extracts the prefix from a container image reference.
@@ -3744,14 +3807,14 @@ func validatePrefixedContainerImage(containerImage, prefix string) error {
37443807
func validateArchivePrefix(imageRef string) error {
37453808
exists, err := osinstaller.FileExists(imageRef)
37463809
if err != nil {
3747-
return errors.Errorf("--container-images flag error: %v", err)
3810+
return errors.Errorf("%s: %v", containerImagesFlagError, err)
37483811
}
37493812
if !exists {
37503813
// Check if user mistakenly used archive prefix with an image name:tag format
37513814
if strings.Contains(imageRef, ":") && !strings.HasSuffix(strings.ToLower(imageRef), ".tar") {
3752-
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)
3815+
return errors.Errorf("%s: file '%s' does not exist. Did you try to scan an image using image name and tag?", containerImagesFlagError, imageRef)
37533816
}
3754-
return errors.Errorf("--container-images flag error: file '%s' does not exist", imageRef)
3817+
return errors.Errorf("%s: file '%s' does not exist", containerImagesFlagError, imageRef)
37553818
}
37563819
return nil
37573820
}
@@ -3775,10 +3838,10 @@ func validateOCIDirPrefix(imageRef string) error {
37753838

37763839
exists, err := osinstaller.FileExists(pathToCheck)
37773840
if err != nil {
3778-
return errors.Errorf("--container-images flag error: path %s does not exist: %v", pathToCheck, err)
3841+
return errors.Errorf("%s: path %s does not exist: %v", containerImagesFlagError, pathToCheck, err)
37793842
}
37803843
if !exists {
3781-
return errors.Errorf("--container-images flag error: path %s does not exist", pathToCheck)
3844+
return errors.Errorf("%s: path %s does not exist", containerImagesFlagError, pathToCheck)
37823845
}
37833846
return nil
37843847
}

internal/commands/scan_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2425,6 +2425,91 @@ func Test_validateThresholds(t *testing.T) {
24252425

24262426
// TestValidateContainerImageFormat_Comprehensive tests the complete validation logic
24272427
// including input normalization, helpful hints, and all error cases.
2428+
// TestIsWindowsAbsolutePath tests the Windows absolute path detection.
2429+
// Container-security scan-type related test function.
2430+
func TestIsWindowsAbsolutePath(t *testing.T) {
2431+
testCases := []struct {
2432+
name string
2433+
input string
2434+
expected bool
2435+
}{
2436+
// Valid Windows absolute paths
2437+
{name: "C drive with backslash", input: "C:\\Users\\file.tar", expected: true},
2438+
{name: "D drive with backslash", input: "D:\\data\\image.tar", expected: true},
2439+
{name: "C drive with forward slash", input: "C:/Users/file.tar", expected: true},
2440+
{name: "Lowercase drive letter", input: "c:\\path\\file.tar", expected: true},
2441+
2442+
// Not Windows absolute paths
2443+
{name: "Unix absolute path", input: "/path/to/file.tar", expected: false},
2444+
{name: "Relative path", input: "Downloads/file.tar", expected: false},
2445+
{name: "Simple filename", input: "file.tar", expected: false},
2446+
{name: "Image with tag", input: "nginx:latest", expected: false},
2447+
{name: "Too short", input: "C:", expected: false},
2448+
{name: "No path separator after colon", input: "C:file.tar", expected: false},
2449+
}
2450+
2451+
for _, tc := range testCases {
2452+
tc := tc
2453+
t.Run(tc.name, func(t *testing.T) {
2454+
result := isWindowsAbsolutePath(tc.input)
2455+
if result != tc.expected {
2456+
t.Errorf("isWindowsAbsolutePath(%q) = %v, expected %v", tc.input, result, tc.expected)
2457+
}
2458+
})
2459+
}
2460+
}
2461+
2462+
// TestLooksLikeFilePath tests the file path detection logic for cross-platform support.
2463+
// Container-security scan-type related test function.
2464+
// This test validates the looksLikeFilePath function for various Windows and Unix path formats.
2465+
func TestLooksLikeFilePath(t *testing.T) {
2466+
testCases := []struct {
2467+
name string
2468+
input string
2469+
expected bool
2470+
}{
2471+
// Tar file extensions
2472+
{name: "Simple tar file", input: "image.tar", expected: true},
2473+
{name: "Tar.gz file", input: "image.tar.gz", expected: true},
2474+
{name: "Tar.bz2 file", input: "image.tar.bz2", expected: true},
2475+
{name: "Tar.xz file", input: "image.tar.xz", expected: true},
2476+
{name: "Tgz file", input: "image.tgz", expected: true},
2477+
2478+
// Unix-style paths
2479+
{name: "Unix relative path with tar", input: "subdir/image.tar", expected: true},
2480+
{name: "Unix absolute path with tar", input: "/path/to/image.tar", expected: true},
2481+
{name: "Unix path with version in filename", input: "Downloads/alpine_3.21.0_podman.tar", expected: true},
2482+
{name: "Unix nested path", input: "path/to/nested/dir/file.tar", expected: true},
2483+
2484+
// Windows-style paths
2485+
{name: "Windows absolute path with drive letter", input: "C:\\Users\\Downloads\\image.tar", expected: true},
2486+
{name: "Windows path with forward slash after drive", input: "C:/Users/Downloads/image.tar", expected: true},
2487+
{name: "Windows relative path with backslash", input: "Downloads\\alpine_3.21.0_podman.tar", expected: true},
2488+
{name: "Windows D drive path", input: "D:\\data\\images\\test.tar", expected: true},
2489+
2490+
// Not file paths (image:tag format)
2491+
{name: "Simple image:tag", input: "nginx:latest", expected: false},
2492+
{name: "Image with registry", input: "registry.io/namespace/image:tag", expected: false},
2493+
{name: "Image with port", input: "registry.io:5000/image:tag", expected: false},
2494+
{name: "Image without tag", input: "nginx", expected: false},
2495+
2496+
// Edge cases
2497+
{name: "Tar file with dots in name", input: "alpine.3.18.0.tar", expected: true},
2498+
{name: "Tar file with version like name", input: "app_v1.2.3.tar", expected: true},
2499+
{name: "Path with tar in middle", input: "tarball/other.tar", expected: true},
2500+
}
2501+
2502+
for _, tc := range testCases {
2503+
tc := tc
2504+
t.Run(tc.name, func(t *testing.T) {
2505+
result := looksLikeFilePath(tc.input)
2506+
if result != tc.expected {
2507+
t.Errorf("looksLikeFilePath(%q) = %v, expected %v", tc.input, result, tc.expected)
2508+
}
2509+
})
2510+
}
2511+
}
2512+
24282513
// Container-security scan-type related test function.
24292514
// This test validates all supported container image formats, prefixes, tar files,
24302515
// error messages, and helpful hints for the --container-images flag.
@@ -2511,6 +2596,36 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) {
25112596
expectedError: "--container-images flag error: file 'image.tgz' is compressed, use non-compressed format (tar)",
25122597
},
25132598

2599+
// ==================== File Path Tests (Windows and Unix) ====================
2600+
// Note: These tests validate that path-like inputs are correctly recognized as file paths
2601+
{
2602+
name: "Valid tar file with filename containing version number",
2603+
containerImage: "alpine_3.21.0_podman.tar",
2604+
expectedError: "",
2605+
setupFiles: []string{"alpine_3.21.0_podman.tar"},
2606+
},
2607+
{
2608+
name: "Valid tar file with filename containing underscore and version",
2609+
containerImage: "mysql_5.7_backup.tar",
2610+
expectedError: "",
2611+
setupFiles: []string{"mysql_5.7_backup.tar"},
2612+
},
2613+
{
2614+
name: "Invalid - Unix relative path does not exist",
2615+
containerImage: "subdir/image.tar",
2616+
expectedError: "--container-images flag error: file 'subdir/image.tar' does not exist",
2617+
},
2618+
{
2619+
name: "Invalid - Unix nested path does not exist",
2620+
containerImage: "path/to/archive/my-image.tar",
2621+
expectedError: "--container-images flag error: file 'path/to/archive/my-image.tar' does not exist",
2622+
},
2623+
{
2624+
name: "Invalid - file path with version-like name does not exist",
2625+
containerImage: "Downloads/alpine_3.21.0_podman.tar",
2626+
expectedError: "--container-images flag error: file 'Downloads/alpine_3.21.0_podman.tar' does not exist",
2627+
},
2628+
25142629
// ==================== Helpful Hints Tests ====================
25152630
{
25162631
name: "Hint - looks like tar file (wrong extension)",

0 commit comments

Comments
 (0)