From ce2f76ca74a186548e57bfecccc28da88fbb0c8b Mon Sep 17 00:00:00 2001 From: atishj99 Date: Thu, 2 Jul 2026 16:54:49 +0530 Subject: [PATCH 1/4] combining all changes --- internal/commands/scan.go | 5 ++--- internal/commands/scan_test.go | 10 +++++----- internal/commands/util/remediation.go | 2 +- internal/services/projects.go | 1 - internal/wrappers/feature-flags-http.go | 13 +++++++++++++ internal/wrappers/feature-flags.go | 9 ++++----- 6 files changed, 25 insertions(+), 15 deletions(-) diff --git a/internal/commands/scan.go b/internal/commands/scan.go index 583c596b5..9e8b2e7c2 100644 --- a/internal/commands/scan.go +++ b/internal/commands/scan.go @@ -65,7 +65,7 @@ const ( containerVolumeFlag = "-v" containerNameFlag = "--name" containerRemove = "--rm" - containerImage = "checkmarx/kics:v2.1.20" + containerImage = "checkmarx/kics@sha256:643071cf0c1657eaea695a48b49d2d61b7e625bb87c51505530e624e0c0a1ad1" // v2.1.20 containerScan = "scan" containerScanPathFlag = "-p" containerScanPath = "/path" @@ -1184,9 +1184,8 @@ func overrideSastConfigValue(sastFastScanChanged, sastIncrementalChanged, sastLi func addAiscScan(featureFlagWrapper wrappers.FeatureFlagsWrapper, resubmitConfig []wrappers.Config) map[string]interface{} { // Add the aisc resubmit config, currently no value is passed in config - aiSupplyChainEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagWrapper, wrappers.AISupplyChainEnabled) aiSupplyChainGAEnabled, _ := wrappers.GetSpecificFeatureFlag(featureFlagWrapper, wrappers.AISupplyChainGAEnabled) - if scanTypeEnabled(commonParams.AiscType) && aiSupplyChainEnabled.Status && aiSupplyChainGAEnabled.Status { + if scanTypeEnabled(commonParams.AiscType) && aiSupplyChainGAEnabled.Status { aiscMapConfig := make(map[string]interface{}) aiscConfig := wrappers.AISCConfig{} aiscMapConfig[resultsMapType] = commonParams.AiscType diff --git a/internal/commands/scan_test.go b/internal/commands/scan_test.go index 971e3d02c..e44f607ee 100644 --- a/internal/commands/scan_test.go +++ b/internal/commands/scan_test.go @@ -836,7 +836,7 @@ func TestAddAiscScan_WhenAiscEnabledAndFeatureFlagEnabled_ShouldReturnConfig(t * var resubmitConfig []wrappers.Config mock.Flag = wrappers.FeatureFlagResponseModel{ - Name: wrappers.AISupplyChainEnabled, + Name: wrappers.AISupplyChainGAEnabled, Status: true, } defer clearFlags() @@ -862,7 +862,7 @@ func TestAddAiscScan_WhenAiscDisabled_ShouldReturnNil(t *testing.T) { wrappers.ClearCache() var resubmitConfig []wrappers.Config mock.Flag = wrappers.FeatureFlagResponseModel{ - Name: wrappers.AISupplyChainEnabled, + Name: wrappers.AISupplyChainGAEnabled, Status: true, } defer clearFlags() @@ -878,7 +878,7 @@ func TestAddAiscScan_WhenFeatureFlagDisabled_ShouldReturnNil(t *testing.T) { wrappers.ClearCache() var resubmitConfig []wrappers.Config mock.Flag = wrappers.FeatureFlagResponseModel{ - Name: wrappers.AISupplyChainEnabled, + Name: wrappers.AISupplyChainGAEnabled, Status: false, } defer clearFlags() @@ -900,7 +900,7 @@ func TestAddAiscScan_WithResubmitConfig_ShouldHandleCorrectly(t *testing.T) { }, } mock.Flag = wrappers.FeatureFlagResponseModel{ - Name: wrappers.AISupplyChainEnabled, + Name: wrappers.AISupplyChainGAEnabled, Status: true, } defer clearFlags() @@ -919,7 +919,7 @@ func TestAddAiscScan_ConfigStructure_ShouldHaveCorrectFormat(t *testing.T) { wrappers.ClearCache() var resubmitConfig []wrappers.Config mock.Flag = wrappers.FeatureFlagResponseModel{ - Name: wrappers.AISupplyChainEnabled, + Name: wrappers.AISupplyChainGAEnabled, Status: true, } defer clearFlags() diff --git a/internal/commands/util/remediation.go b/internal/commands/util/remediation.go index 7322a5fa8..a74c180ef 100644 --- a/internal/commands/util/remediation.go +++ b/internal/commands/util/remediation.go @@ -27,7 +27,7 @@ const ( filesContainerVolume = ":/files" resultsContainerLocation = "/kics/" containerRemove = "--rm" - ContainerImage = "checkmarx/kics:v2.1.20" + ContainerImage = "checkmarx/kics@sha256:643071cf0c1657eaea695a48b49d2d61b7e625bb87c51505530e624e0c0a1ad1" // v2.1.20 containerNameFlag = "--name" remediateCommand = "remediate" resultsFlag = "--results" diff --git a/internal/services/projects.go b/internal/services/projects.go index 875568746..090e6e78d 100644 --- a/internal/services/projects.go +++ b/internal/services/projects.go @@ -198,7 +198,6 @@ func updateProject(project *wrappers.ProjectResponseModel, } else { projModel.MainBranch = project.MainBranch } - projModel.RepoURL = project.RepoURL if projectTags == "" && projectPrivatePackage == "" && isBranchPrimary == false { logger.PrintIfVerbose("No tags or branch to update. Skipping project update.") diff --git a/internal/wrappers/feature-flags-http.go b/internal/wrappers/feature-flags-http.go index 3f0c77314..e98b60294 100644 --- a/internal/wrappers/feature-flags-http.go +++ b/internal/wrappers/feature-flags-http.go @@ -4,8 +4,11 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "strings" + "time" + "github.com/checkmarx/ast-cli/internal/logger" "github.com/pkg/errors" "github.com/spf13/viper" @@ -87,6 +90,16 @@ func (f FeatureFlagsHTTPWrapper) GetSpecificFlag(flagName string) (*FeatureFlagR return &model, nil case http.StatusNotFound: return nil, errors.New("feature flags not found") + case http.StatusTooManyRequests: + waitSeconds := defaultRateLimitWaitSeconds + if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" { + if parsed, parseErr := strconv.Atoi(retryAfter); parseErr == nil && parsed > 0 { + waitSeconds = parsed + } + } + logger.PrintIfVerbose(fmt.Sprintf("Feature flags rate limited (429). Waiting %d seconds before retry.", waitSeconds)) + time.Sleep(time.Duration(waitSeconds) * time.Second) + return nil, errors.New("failed to load feature flags for tenant") default: return nil, errors.New("failed to load feature flags for tenant") } diff --git a/internal/wrappers/feature-flags.go b/internal/wrappers/feature-flags.go index a5718fbd4..ec0a31bd4 100644 --- a/internal/wrappers/feature-flags.go +++ b/internal/wrappers/feature-flags.go @@ -22,9 +22,6 @@ const maxRetries = 3 const IncreaseFileUploadLimit = "INCREASE_FILE_UPLOAD_LIMIT" const ScaDeltaScanEnabled = "SCA_DELTASCAN_ENABLED" -// AISupplyChainEnabled is the feature flag for AI Supply Chain Engine. -const AISupplyChainEnabled = "AI_SUPPLY_CHAIN_ENGINE_ENABLED" - // AISupplyChainGAEnabled is the feature flag for AI Supply Chain Engine GA. const AISupplyChainGAEnabled = "AI_SUPPLY_CHAIN_ENGINE_GA_ENABLED" @@ -113,8 +110,10 @@ func GetSpecificFeatureFlag(featureFlagsWrapper FeatureFlagsWrapper, flagName st if len(featureFlags) == 0 || DefaultFFLoad { _ = HandleFeatureFlags(featureFlagsWrapper) } - // Take the value from FeatureFlags - return &FeatureFlagResponseModel{Name: flagName, Status: featureFlags[flagName]}, nil + defaultValue := featureFlags[flagName] + featureFlagsCache[flagName] = defaultValue // prevent re-fetch on next call + return &FeatureFlagResponseModel{Name: flagName, Status: defaultValue}, nil + } UpdateSpecificFeatureFlagMap(flagName, *specificFlag) From 0be8a2f9b6d43d466733d5400b5b03fe508d8b6f Mon Sep 17 00:00:00 2001 From: atishj99 Date: Thu, 2 Jul 2026 18:08:00 +0530 Subject: [PATCH 2/4] fixing lint issue --- internal/commands/util/remediation.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/commands/util/remediation.go b/internal/commands/util/remediation.go index a74c180ef..c30438aff 100644 --- a/internal/commands/util/remediation.go +++ b/internal/commands/util/remediation.go @@ -27,6 +27,7 @@ const ( filesContainerVolume = ":/files" resultsContainerLocation = "/kics/" containerRemove = "--rm" + // ContainerImage is the KICS container image with pinned SHA256 digest. ContainerImage = "checkmarx/kics@sha256:643071cf0c1657eaea695a48b49d2d61b7e625bb87c51505530e624e0c0a1ad1" // v2.1.20 containerNameFlag = "--name" remediateCommand = "remediate" From 69aa46db96436bc52120d51ba15adf10044d2c60 Mon Sep 17 00:00:00 2001 From: atishj99 Date: Fri, 3 Jul 2026 12:50:28 +0530 Subject: [PATCH 3/4] Adding check in astcli to check supported manifest files --- .../ossrealtime/oss-realtime.go | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/internal/services/realtimeengine/ossrealtime/oss-realtime.go b/internal/services/realtimeengine/ossrealtime/oss-realtime.go index 9316b1a7e..bf65b0c39 100644 --- a/internal/services/realtimeengine/ossrealtime/oss-realtime.go +++ b/internal/services/realtimeengine/ossrealtime/oss-realtime.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "path/filepath" "strings" "github.com/Checkmarx/manifest-parser/pkg/parser" @@ -74,6 +75,10 @@ func (o *OssRealtimeService) RunOssRealtimeScan(filePath, ignoredFilePath string return nil, errorconstants.NewRealtimeEngineError("invalid file path").Error() } + if err := validateSupportedManifestFile(filePath); err != nil { + return nil, err + } + pkgs, err := parseManifest(filePath) if err != nil { logger.PrintfIfVerbose("Failed to parse manifest file %s: %v", filePath, err) @@ -174,6 +179,55 @@ func getPackageEntryFromPackageMap( return &entry } +// validateSupportedManifestFile checks if the manifest file format is supported by OSS realtime scanner. +func validateSupportedManifestFile(filePath string) error { + manifestFileName := filepath.Base(filePath) + manifestFileExtension := filepath.Ext(manifestFileName) + + // Check supported extensions + supportedExtensions := map[string]bool{ + ".csproj": true, + ".sbt": true, + } + + // Check supported filenames + supportedFilenames := map[string]bool{ + "pom.xml": true, + "package.json": true, + "Directory.Packages.props": true, + "packages.config": true, + "go.mod": true, + "build.gradle": true, + "build.gradle.kts": true, + "libs.versions.toml": true, + "setup.cfg": true, + "setup.py": true, + "pyproject.toml": true, + } + + // Check by extension + if supportedExtensions[manifestFileExtension] { + return nil + } + + // Check by filename + if supportedFilenames[manifestFileName] { + return nil + } + + // Special handling for .txt files (check prefix) + if manifestFileExtension == ".txt" { + if strings.HasPrefix(manifestFileName, "requirement") || + strings.HasPrefix(manifestFileName, "packages") || + strings.HasPrefix(manifestFileName, "constraint") { + return nil + } + } + + // Manifest format is not supported + return errorconstants.NewRealtimeEngineError(fmt.Sprintf("OSS Realtime scanner doesn't currently support scanning '%s' file.", manifestFileName)).Error() +} + // parseManifest parses the manifest file and returns a list of packages. func parseManifest(filePath string) ([]models.Package, error) { manifestParser := parser.ParsersFactory(filePath) From 5adec42d0181b1ad1d3827f5635b7d80bf947699 Mon Sep 17 00:00:00 2001 From: atishj99 Date: Mon, 6 Jul 2026 17:00:26 +0530 Subject: [PATCH 4/4] assing testcases for verification of change --- .../ossrealtime/oss-realtime_test.go | 142 ++++++++++++++++++ test/integration/oss-realtime_test.go | 14 ++ 2 files changed, 156 insertions(+) diff --git a/internal/services/realtimeengine/ossrealtime/oss-realtime_test.go b/internal/services/realtimeengine/ossrealtime/oss-realtime_test.go index 3fc4e975c..a3868cd55 100644 --- a/internal/services/realtimeengine/ossrealtime/oss-realtime_test.go +++ b/internal/services/realtimeengine/ossrealtime/oss-realtime_test.go @@ -388,3 +388,145 @@ func TestOssRealtimeScan_CsprojFile_ReturnsLocations(t *testing.T) { "Location %d endIndex should be %d", i, expected.endIndex) } } + +// Tests for validateSupportedManifestFile function +func TestValidateSupportedManifestFile_ValidExtensions(t *testing.T) { + tests := []struct { + name string + filePath string + isValid bool + }{ + { + name: "CsprojExtension", + filePath: "project.csproj", + isValid: true, + }, + { + name: "SbtExtension", + filePath: "build.sbt", + isValid: true, + }, + { + name: "CsprojWithPath", + filePath: "/path/to/project.csproj", + isValid: true, + }, + { + name: "SbtWithPath", + filePath: "src/build.sbt", + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSupportedManifestFile(tt.filePath) + if tt.isValid { + assert.Nil(t, err, "Should accept %s file", tt.filePath) + } else { + assert.NotNil(t, err, "Should reject %s file", tt.filePath) + } + }) + } +} + +func TestValidateSupportedManifestFile_ValidFilenames(t *testing.T) { + tests := []struct { + name string + filePath string + }{ + {name: "PomXml", filePath: "pom.xml"}, + {name: "PackageJson", filePath: "package.json"}, + {name: "GoMod", filePath: "go.mod"}, + {name: "BuildGradle", filePath: "build.gradle"}, + {name: "BuildGradleKts", filePath: "build.gradle.kts"}, + {name: "LibsVersionsToml", filePath: "libs.versions.toml"}, + {name: "SetupCfg", filePath: "setup.cfg"}, + {name: "SetupPy", filePath: "setup.py"}, + {name: "PyprojectToml", filePath: "pyproject.toml"}, + {name: "PackagesConfig", filePath: "packages.config"}, + {name: "DirectoryPackagesProps", filePath: "Directory.Packages.props"}, + {name: "PomXmlWithPath", filePath: "/path/to/pom.xml"}, + {name: "PackageJsonWithPath", filePath: "src/package.json"}, + {name: "GoModWithPath", filePath: "project/go.mod"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSupportedManifestFile(tt.filePath) + assert.Nil(t, err, "Should accept %s file", tt.filePath) + }) + } +} + +func TestValidateSupportedManifestFile_ValidTxtFilesWithPrefixes(t *testing.T) { + tests := []struct { + name string + filePath string + }{ + {name: "RequirementsTxt", filePath: "requirements.txt"}, + {name: "RequirementsDevTxt", filePath: "requirements-dev.txt"}, + {name: "PackagesTxt", filePath: "packages.txt"}, + {name: "ConstraintsTxt", filePath: "constraints.txt"}, + {name: "RequirementsWithPath", filePath: "/path/to/requirements.txt"}, + {name: "PackagesWithPath", filePath: "config/packages.txt"}, + {name: "ConstraintsWithPath", filePath: "src/constraints.txt"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSupportedManifestFile(tt.filePath) + assert.Nil(t, err, "Should accept %s file", tt.filePath) + }) + } +} + +func TestValidateSupportedManifestFile_InvalidTxtFilesWithoutPrefixes(t *testing.T) { + tests := []struct { + name string + filePath string + }{ + {name: "UnrelatedTxt", filePath: "readme.txt"}, + {name: "DataTxt", filePath: "data.txt"}, + {name: "ConfigTxt", filePath: "config.txt"}, + {name: "InfoTxt", filePath: "info.txt"}, + {name: "UnrelatedTxtWithPath", filePath: "/path/to/random.txt"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSupportedManifestFile(tt.filePath) + assert.NotNil(t, err, "Should reject %s file (missing prefix)", tt.filePath) + // Verify error message indicates unsupported format + assert.Contains(t, err.Error(), "OSS Realtime scanner doesn't currently support scanning") + }) + } +} + +func TestValidateSupportedManifestFile_UnsupportedFormats(t *testing.T) { + tests := []struct { + name string + filePath string + }{ + {name: "RubyGemfile", filePath: "Gemfile"}, + {name: "PHPComposer", filePath: "composer.json"}, + {name: "RustCargo", filePath: "Cargo.toml"}, + {name: "PythonPipfile", filePath: "Pipfile"}, + {name: "JavaGradleProperties", filePath: "gradle.properties"}, + {name: "NodeNpmLock", filePath: "package-lock.json"}, + {name: "YarnLock", filePath: "yarn.lock"}, + {name: "UnsupportedExtension", filePath: "manifest.xml"}, + {name: "RandomFile", filePath: "random.txt"}, + {name: "NoPyproject", filePath: "pyproject.yaml"}, + {name: "PomWithDifferentName", filePath: "maven.xml"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSupportedManifestFile(tt.filePath) + assert.NotNil(t, err, "Should reject %s file (unsupported format)", tt.filePath) + // Verify error message indicates unsupported format + assert.Contains(t, err.Error(), "OSS Realtime scanner doesn't currently support scanning") + }) + } +} diff --git a/test/integration/oss-realtime_test.go b/test/integration/oss-realtime_test.go index 0ccfcff5d..cdd7971dc 100644 --- a/test/integration/oss-realtime_test.go +++ b/test/integration/oss-realtime_test.go @@ -71,6 +71,20 @@ func TestOssRealtimeScan_PackageJsonFile_Success(t *testing.T) { defer deleteCacheFile() } +func TestOssRealtimeScan_UnsupportedManifestFormat_ReturnsError(t *testing.T) { + // Test with unsupported file format - should return error (using a .py file which is not a supported manifest) + args := []string{ + "scan", "oss-realtime", + flag(commonParams.SourcesFlag), "data/python-vul-file.py", + } + err, _ := executeCommand(t, args...) + // Should fail with error + assert.NotNil(t, err, "Should fail with unsupported manifest format") + // Error message should indicate unsupported format + assert.Contains(t, err.Error(), "OSS Realtime scanner doesn't currently support scanning", + "Error message should indicate that format is not supported") +} + func validateCacheFileExist() bool { cacheFilePath := os.TempDir() + "/oss-realtime-cache.json" if _, err := os.Stat(cacheFilePath); os.IsNotExist(err) {