diff --git a/go.mod b/go.mod index cc5e89579..4b477d2d7 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/checkmarx/ast-cli go 1.24.13 require ( - github.com/Checkmarx/containers-resolver v1.0.31 + github.com/Checkmarx/containers-resolver v1.0.32 github.com/Checkmarx/containers-types v1.0.9 github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63 github.com/Checkmarx/gen-ai-wrapper v1.0.3 @@ -54,7 +54,7 @@ require ( github.com/BobuSumisu/aho-corasick v1.0.3 // indirect github.com/BurntSushi/toml v1.5.0 // indirect github.com/Checkmarx/containers-images-extractor v1.0.22 - github.com/Checkmarx/containers-syft-packages-extractor v1.0.23 // indirect + github.com/Checkmarx/containers-syft-packages-extractor v1.0.24 // indirect github.com/CycloneDX/cyclonedx-go v0.9.2 // indirect github.com/DataDog/zstd v1.5.6 // indirect github.com/Masterminds/goutils v1.1.1 // indirect diff --git a/go.sum b/go.sum index 5558ed23c..9659c579f 100644 --- a/go.sum +++ b/go.sum @@ -67,10 +67,10 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Checkmarx/containers-images-extractor v1.0.22 h1:kJZgwk28LwJZ7Xky+kzwL+JSZOlpwrGsZQhhz4L2t6s= github.com/Checkmarx/containers-images-extractor v1.0.22/go.mod h1:HyzVb8TtTDf56hGlSakalPXtzjJ6VhTYe9fmAcOS+V8= -github.com/Checkmarx/containers-resolver v1.0.31 h1:Xd4D9rvGxNXc0STHZdIWtQC4SkrD65MLpU+S6H9tb/0= -github.com/Checkmarx/containers-resolver v1.0.31/go.mod h1:hQ5lw0dCc+va4jm47TpeVqRzU71/SpoE+T3e69vCZwI= -github.com/Checkmarx/containers-syft-packages-extractor v1.0.23 h1:qP4OBlCVF6BbOO0gzcoOzAtfdx7+M1kU3OsY2xBvy8E= -github.com/Checkmarx/containers-syft-packages-extractor v1.0.23/go.mod h1:OPGYISPnKtVFl2mZrClErv83ZLjUPKjdQQsXLmx++oY= +github.com/Checkmarx/containers-resolver v1.0.32 h1:clCWHZ2hCgBvEudLkmelxuTsS30XS0U+Wbhr7dKVJbs= +github.com/Checkmarx/containers-resolver v1.0.32/go.mod h1:qW1Na7dekGfJNf3fm6tnVTzHbw/FJj7nSBnPYc4YTvI= +github.com/Checkmarx/containers-syft-packages-extractor v1.0.24 h1:+BxJgYGD6olWFaQ6B+a1JHMJHYjbRokhT1/j1N/CnuE= +github.com/Checkmarx/containers-syft-packages-extractor v1.0.24/go.mod h1:OPGYISPnKtVFl2mZrClErv83ZLjUPKjdQQsXLmx++oY= github.com/Checkmarx/containers-types v1.0.9 h1:LbHDj9LZ0x3f28wDx398WC19sw0U0EfEewHMLStBwvs= github.com/Checkmarx/containers-types v1.0.9/go.mod h1:KR0w8XCosq3+6jRCfQrH7i//Nj2u11qaUJM62CREFZA= github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63 h1:SCuTcE+CFvgjbIxUNL8rsdB2sAhfuNx85HvxImKta3g= diff --git a/internal/commands/scan.go b/internal/commands/scan.go index fb3a7583b..9f3ecba7e 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -2271,7 +2271,7 @@ func definePathForZipFileOrDirectory(cmd *cobra.Command) (zipFile, sourceDir str return zipFile, sourceDir, err } -// enforceLocalResolutionForTarFiles checks if any container image is a tar file +// enforceLocalResolutionForTarFiles checks if any container image is a tar file or oci-dir // and enforces local resolution by setting the --containers-local-resolution flag. // Container-security scan-type related function. func enforceLocalResolutionForTarFiles(cmd *cobra.Command) error { @@ -2292,7 +2292,7 @@ func enforceLocalResolutionForTarFiles(cmd *cobra.Command) error { // Parse container images list containerImagesList := strings.Split(strings.TrimSpace(containerImagesFlag), ",") - hasTarFile := false + needsLocalResolution := false for _, containerImageName := range containerImagesList { // Normalize input: trim spaces and quotes @@ -2306,15 +2306,21 @@ func enforceLocalResolutionForTarFiles(cmd *cobra.Command) error { // Check if this is a tar file by checking if it contains a tar file reference if isTarFileReference(containerImageName) { - hasTarFile = true + needsLocalResolution = true + break + } + + // Check if this is an oci-dir reference - these also require local resolution + if strings.HasPrefix(containerImageName, ociDirPrefix) { + needsLocalResolution = true break } } - // If at least one tar file is found, enforce local resolution - if hasTarFile { - logger.PrintIfVerbose("Detected tar file(s) in --container-images flag") - fmt.Println("Warning: Tar file(s) detected in --container-images. Automatically enabling --containers-local-resolution flag.") + // If at least one tar file or oci-dir is found, enforce local resolution + if needsLocalResolution { + logger.PrintIfVerbose("Detected tar file(s) or oci-dir in --container-images flag") + fmt.Println("Warning: Tar file(s) or oci-dir detected in --container-images. Automatically enabling --containers-local-resolution flag.") // Set the flag to true err := cmd.Flags().Set(commonParams.ContainerResolveLocallyFlag, "true") @@ -3626,6 +3632,11 @@ func validateContainerImageFormat(containerImage string) error { sanitizedInput = containerImage } + // Route prefixed inputs (oci-dir:, docker:, etc.) directly to their specific validators + if hasKnownSource { + return validatePrefixedContainerImage(containerImage, getPrefixFromInput(containerImage, knownSources)) + } + // Check if this looks like a file path before parsing colons if looksLikeFilePath(sanitizedInput) { return validateFilePath(sanitizedInput) @@ -3644,11 +3655,6 @@ func validateContainerImageFormat(containerImage string) error { return errors.Errorf("Invalid value for --container-images flag. Image name and tag cannot be empty. Found: image='%s', tag='%s'", imageName, imageTag) } - // For prefixed inputs, also validate the prefix-specific requirements - if hasKnownSource { - return validatePrefixedContainerImage(containerImage, getPrefixFromInput(containerImage, knownSources)) - } - // Check if this looks like an invalid prefix attempt (e.g., "invalid-prefix:file.tar") // If the "tag" ends with .tar and the "image name" looks like a simple prefix (no / or .) // then the user likely intended to use a prefix format but used an unknown prefix @@ -3684,20 +3690,7 @@ func validateContainerImageFormat(containerImage string) error { 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:) - if hasKnownSource { - prefix := getPrefixFromInput(containerImage, knownSources) - // oci-dir can reference directories without tags, validate it - if prefix == ociDirPrefix { - return validatePrefixedContainerImage(containerImage, prefix) - } - // Archive prefixes (file:, docker-archive:, oci-archive:) can reference files without tags - if prefix == filePrefix || prefix == dockerArchivePrefix || prefix == ociArchivePrefix { - return validatePrefixedContainerImage(containerImage, prefix) - } - } - - // Step 5: Not a tar file, no special prefix, and no colon - assume user forgot to add tag (error) + // Step 4: Not a tar file, no special prefix, and no colon - assume user forgot to add tag (error) return errors.Errorf("%s: image does not have a tag", containerImagesFlagError) } @@ -3831,13 +3824,19 @@ func validateOCIDirPrefix(imageRef string) error { // 3. Can have optional :tag suffix pathToCheck := imageRef - if strings.Contains(imageRef, ":") { + + // Handle Windows absolute paths (e.g., C:\path\to\dir) before splitting on colons + // Windows paths have a drive letter followed by colon and path separator + if !isWindowsAbsolutePath(imageRef) && strings.Contains(imageRef, ":") { // Handle case like "oci-dir:/path/to/dir:tag" or "oci-dir:name.tar:tag" + // For Unix paths, we can safely split on colon to extract the tag pathParts := strings.Split(imageRef, ":") if len(pathParts) > 0 && pathParts[0] != "" { pathToCheck = pathParts[0] } } + // For Windows absolute paths, use the entire imageRef as pathToCheck + // since the colon is part of the drive letter (e.g., C:\path\to\dir) exists, err := osinstaller.FileExists(pathToCheck) if err != nil { diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index e51614dc4..1c20053f5 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -2717,12 +2717,12 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { { name: "Invalid docker prefix - missing tag", containerImage: "docker:nginx", - expectedError: "image does not have a tag", + expectedError: "Prefix 'docker:' expects format :", }, { name: "Invalid docker prefix - empty", containerImage: "docker:", - expectedError: "image does not have a tag", + expectedError: "After prefix 'docker:', the image reference cannot be empty", }, // ==================== Podman Daemon Tests ==================== @@ -2734,7 +2734,7 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { { name: "Invalid podman prefix - missing tag", containerImage: "podman:alpine", - expectedError: "image does not have a tag", + expectedError: "Prefix 'podman:' expects format :", }, // ==================== Containerd Daemon Tests ==================== @@ -2746,7 +2746,7 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { { name: "Invalid containerd prefix - missing tag", containerImage: "containerd:nginx", - expectedError: "image does not have a tag", + expectedError: "Prefix 'containerd:' expects format :", }, // ==================== Registry Tests ==================== @@ -2763,7 +2763,7 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { { name: "Invalid registry - just URL without image", containerImage: "registry:myregistry.com", - expectedError: "image does not have a tag", + expectedError: "Registry format must specify a single image, not just a registry URL", }, // ==================== OCI-Dir Tests ==================== @@ -2796,6 +2796,22 @@ func TestValidateContainerImageFormat_Comprehensive(t *testing.T) { expectedError: "", setupFiles: []string{"image.tar"}, }, + // Windows full path tests + { + name: "oci-dir with Windows full path - C drive backslash", + containerImage: "oci-dir:C:\\Users\\test\\docker.io\\library\\alpine", + expectedError: "--container-images flag error: path C:\\Users\\test\\docker.io\\library\\alpine does not exist", + }, + { + name: "oci-dir with Windows full path - C drive forward slash", + containerImage: "oci-dir:C:/Users/test/docker.io/library/alpine", + expectedError: "--container-images flag error: path C:/Users/test/docker.io/library/alpine does not exist", + }, + { + name: "oci-dir with Windows full path - D drive", + containerImage: "oci-dir:D:\\data\\images\\my-image", + expectedError: "--container-images flag error: path D:\\data\\images\\my-image does not exist", + }, // ==================== Dir Prefix (Forbidden) ==================== { @@ -4515,7 +4531,7 @@ func TestIsTarFileReference(t *testing.T) { } } -// TestEnforceLocalResolutionForTarFiles tests the automatic enforcement of local resolution when tar files are detected. +// TestEnforceLocalResolutionForTarFiles tests the automatic enforcement of local resolution when tar files or oci-dir are detected. // Container-security scan-type related test function. func TestEnforceLocalResolutionForTarFiles(t *testing.T) { testCases := []struct { @@ -4530,15 +4546,20 @@ func TestEnforceLocalResolutionForTarFiles(t *testing.T) { {"Already enabled", "alpine.tar", true, true, false}, {"Only image:tag", "nginx:latest,alpine:3.18", false, false, false}, {"Non-tar prefixes", "docker:nginx:latest,registry:ubuntu:22.04", false, false, false}, - {"Invalid tar:tag format", "oci-dir:file.tar:latest", false, false, false}, - // Should enable local resolution + // Should enable local resolution - tar files {"Single tar", "alpine.tar", false, true, true}, {"Mixed tar+image", "nginx:latest,alpine.tar", false, true, true}, {"Tar with spaces/quotes", " 'alpine.tar' ,nginx:latest", false, true, true}, {"Prefixed tar", "docker-archive:alpine.tar", false, true, true}, {"oci-dir tar", "oci-dir:image.tar", false, true, true}, {"Tar at end", "nginx:latest,ubuntu.tar", false, true, true}, + + // Should enable local resolution - oci-dir directories + {"oci-dir directory", "oci-dir:my-alpine-image", false, true, true}, + {"oci-dir with path", "oci-dir:/path/to/oci-layout", false, true, true}, + {"oci-dir with tag suffix", "oci-dir:file.tar:latest", false, true, true}, + {"Mixed oci-dir+image", "nginx:latest,oci-dir:my-image", false, true, true}, } for _, tc := range testCases { @@ -4578,7 +4599,7 @@ func TestEnforceLocalResolutionForTarFiles(t *testing.T) { t.Errorf("Expected local resolution=%v, got=%v", tc.expectedLocalResolution, actualLocalResolution) } - hasWarning := strings.Contains(output, "Warning:") && strings.Contains(output, "Tar file") + hasWarning := strings.Contains(output, "Warning:") && (strings.Contains(output, "Tar file") || strings.Contains(output, "oci-dir")) if tc.expectWarning && !hasWarning { t.Errorf("Expected warning but got: %s", output) } else if !tc.expectWarning && hasWarning {