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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
53 changes: 26 additions & 27 deletions internal/commands/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand Down
39 changes: 30 additions & 9 deletions internal/commands/scan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <image-name>:<image-tag>",
},
{
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 ====================
Expand All @@ -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 <image-name>:<image-tag>",
},

// ==================== Containerd Daemon Tests ====================
Expand All @@ -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 <image-name>:<image-tag>",
},

// ==================== Registry Tests ====================
Expand All @@ -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 ====================
Expand Down Expand Up @@ -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) ====================
{
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
Loading