diff --git a/go.mod b/go.mod index 0cfc449e5..d3b9993ea 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/jfrog/gofrog v1.7.6 github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93 github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 - github.com/jfrog/jfrog-cli-security v1.24.2 + github.com/jfrog/jfrog-cli-security v1.25.0 github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465 github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible github.com/owenrumney/go-sarif/v3 v3.2.3 diff --git a/go.sum b/go.sum index f88140faf..86f495328 100644 --- a/go.sum +++ b/go.sum @@ -148,8 +148,8 @@ github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93 h1:r github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251211075913-35ebcd308e93/go.mod h1:7cCaRhXorlbyXZgiW5bplCExFxlnROaG21K12d8inpQ= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 h1:GYE67ubwl+ZRw3CcXFUi49EwwQp6k+qS8sX0QuHDHO8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5/go.mod h1:BMoGi2rG0udCCeaghqlNgiW3fTmT+TNnfTnBoWFYgcg= -github.com/jfrog/jfrog-cli-security v1.24.2 h1:nyI0lNYR8i6yZYeBDsBJnURYsMnFKEmt7QH4vaNxtGM= -github.com/jfrog/jfrog-cli-security v1.24.2/go.mod h1:3FXD5IkKtdQOm9CZk6cR7q0iC6PaGMnjqzZqRcQp2r0= +github.com/jfrog/jfrog-cli-security v1.25.0 h1:DM29QsMkFLRD6adKCWISe3uHFVaOodIN56NH7ThpKKU= +github.com/jfrog/jfrog-cli-security v1.25.0/go.mod h1:IV/+JhaLmyeMb8IAoJZYUq4gONW5BdrxgdT4w7SXgz0= github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465 h1:Ff3BlNPndrAfa1xFI/ORFzfWTxQxF0buWG61PEJwd3U= github.com/jfrog/jfrog-client-go v1.55.1-0.20251217080430-c92b763b7465/go.mod h1:WQ5Y+oKYyHFAlCbHN925bWhnShTd2ruxZ6YTpb76fpU= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= diff --git a/packagehandlers/commonpackagehandler.go b/packagehandlers/commonpackagehandler.go index a5643af33..9a874d521 100644 --- a/packagehandlers/commonpackagehandler.go +++ b/packagehandlers/commonpackagehandler.go @@ -93,9 +93,10 @@ func getFixedPackage(impactedPackage string, versionOperator string, suggestedFi return } -// Recursively scans the current directory for descriptor files based on the provided list of suffixes, while excluding paths that match the specified exclusion patterns. +// Scans the current directory for descriptor files based on the provided list of suffixes, while excluding paths that match the specified exclusion patterns. +// Only scans the current directory level (non-recursive) to avoid conflicts with auto-detected subdirectory targets that will be processed separately. // The patternsToExclude must be provided as regexp patterns. For instance, if the pattern ".*node_modules.*" is provided, any paths containing "node_modules" will be excluded from the result. -// Returns a slice of all discovered descriptor files, represented as absolute paths. +// Returns a slice of all discovered descriptor files in the current directory, represented as absolute paths. func (cph *CommonPackageHandler) GetAllDescriptorFilesFullPaths(descriptorFilesSuffixes []string, patternsToExclude ...string) (descriptorFilesFullPaths []string, err error) { if len(descriptorFilesSuffixes) == 0 { return @@ -111,6 +112,10 @@ func (cph *CommonPackageHandler) GetAllDescriptorFilesFullPaths(descriptorFilesS return fmt.Errorf("an error has occurred when attempting to access or traverse the file system: %w", innerErr) } + if d.IsDir() && path != "." { + return filepath.SkipDir + } + for _, regexpCompiler := range regexpPatternsCompilers { if match := regexpCompiler.FindString(path); match != "" { return filepath.SkipDir diff --git a/packagehandlers/packagehandlers_test.go b/packagehandlers/packagehandlers_test.go index 2c94a6f97..1513de5db 100644 --- a/packagehandlers/packagehandlers_test.go +++ b/packagehandlers/packagehandlers_test.go @@ -316,7 +316,7 @@ func TestUpdateDependency(t *testing.T) { }, scanDetails: scanDetails, fixSupported: true, - descriptorsToCheck: []string{"build.gradle", filepath.Join("innerProjectForTest", "build.gradle.kts")}, + descriptorsToCheck: []string{"build.gradle"}, }, }, @@ -966,7 +966,7 @@ func TestGetAllDescriptorFilesFullPaths(t *testing.T) { { testProjectRepo: "gradle", suffixesToSearch: []string{groovyDescriptorFileSuffix, kotlinDescriptorFileSuffix}, - expectedResultSuffixes: []string{filepath.Join("innerProjectForTest", "build.gradle.kts"), "build.gradle"}, + expectedResultSuffixes: []string{"build.gradle"}, }, // This test case verifies that paths containing excluded patterns are omitted from the output { diff --git a/scanrepository/scanrepository.go b/scanrepository/scanrepository.go index e9e21e581..beb666bea 100644 --- a/scanrepository/scanrepository.go +++ b/scanrepository/scanrepository.go @@ -162,7 +162,7 @@ func (cfp *ScanRepositoryCmd) setCommandPrerequisites(repository *utils.Reposito } func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (int, error) { - var fixNeeded bool + var isFixNeeded bool totalFindings := 0 // A map that contains the full project paths as a keys // The value is a map of vulnerable package names -> the scanDetails of the vulnerable packages. @@ -203,22 +203,39 @@ func (cfp *ScanRepositoryCmd) scanAndFixProject(repository *utils.Repository) (i if repository.DetectionOnly { continue } - // Prepare the vulnerabilities map for each working dir path - currPathVulnerabilities, err := cfp.getVulnerabilitiesMap(scanResults) - if err != nil { - if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", fullPathWd), err); err != nil { - return totalFindings, err + + for _, target := range scanResults.Targets { + targetPath := target.Target + convertor := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IncludeVulnerabilities: scanResults.IncludesVulnerabilities(), + HasViolationContext: scanResults.HasViolationContext(), + IncludeTargets: []string{targetPath}, + }) + simpleJsonResult, err := convertor.ConvertToSimpleJson(scanResults) + if err != nil { + if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", targetPath), err); err != nil { + return totalFindings, err + } + continue } - continue - } - if len(currPathVulnerabilities) > 0 { - fixNeeded = true + + currPathVulnerabilities, err := cfp.createVulnerabilitiesMapFromSimpleJson(simpleJsonResult) + if err != nil { + if err = utils.CreateErrorIfPartialResultsDisabled(cfp.scanDetails.AllowPartialResults(), fmt.Sprintf("An error occurred while preparing the vulnerabilities map for '%s' working directory. Fixes will be skipped for this working directory", targetPath), err); err != nil { + return totalFindings, err + } + continue + } + if len(currPathVulnerabilities) > 0 { + isFixNeeded = true + log.Debug(fmt.Sprintf("Found %d fixable vulnerabilities in '%s': %s", len(currPathVulnerabilities), targetPath, strings.Join(maps.Keys(currPathVulnerabilities), ", "))) + } + vulnerabilitiesByPathMap[targetPath] = currPathVulnerabilities } - vulnerabilitiesByPathMap[fullPathWd] = currPathVulnerabilities } if repository.DetectionOnly { log.Info(fmt.Sprintf("This command is running in detection mode only. To enable automatic fixing of issues, set the '%s' environment variable to 'false'.", utils.DetectionOnlyEnv)) - } else if fixNeeded { + } else if isFixNeeded { return totalFindings, cfp.fixVulnerablePackages(repository, vulnerabilitiesByPathMap) } return totalFindings, nil @@ -237,19 +254,6 @@ func (cfp *ScanRepositoryCmd) scan(currentWorkingDir string) (*results.SecurityC return auditResults, nil } -func (cfp *ScanRepositoryCmd) getVulnerabilitiesMap(scanResults *results.SecurityCommandResults) (map[string]*utils.VulnerabilityDetails, error) { - vulnerabilitiesMap, err := cfp.createVulnerabilitiesMap(scanResults) - if err != nil { - return nil, err - } - - // Nothing to fix, return - if len(vulnerabilitiesMap) == 0 { - log.Info("Didn't find vulnerable dependencies with existing fix versions for", cfp.scanDetails.RepoName) - } - return vulnerabilitiesMap, nil -} - func (cfp *ScanRepositoryCmd) fixVulnerablePackages(repository *utils.Repository, vulnerabilitiesByWdMap map[string]map[string]*utils.VulnerabilityDetails) (err error) { if cfp.aggregateFixes { err = cfp.fixIssuesSinglePR(repository, vulnerabilitiesByWdMap) @@ -598,6 +602,26 @@ func (cfp *ScanRepositoryCmd) createVulnerabilitiesMap(scanResults *results.Secu return vulnerabilitiesMap, nil } +func (cfp *ScanRepositoryCmd) createVulnerabilitiesMapFromSimpleJson(simpleJsonResult formats.SimpleJsonResults) (map[string]*utils.VulnerabilityDetails, error) { + vulnerabilitiesMap := map[string]*utils.VulnerabilityDetails{} + var err error + + if len(simpleJsonResult.Vulnerabilities) > 0 { + for i := range simpleJsonResult.Vulnerabilities { + if err = cfp.addVulnerabilityToFixVersionsMap(&simpleJsonResult.Vulnerabilities[i], vulnerabilitiesMap); err != nil { + return nil, err + } + } + } else if len(simpleJsonResult.SecurityViolations) > 0 { + for i := range simpleJsonResult.SecurityViolations { + if err = cfp.addVulnerabilityToFixVersionsMap(&simpleJsonResult.SecurityViolations[i], vulnerabilitiesMap); err != nil { + return nil, err + } + } + } + return vulnerabilitiesMap, nil +} + func (cfp *ScanRepositoryCmd) addVulnerabilityToFixVersionsMap(vulnerability *formats.VulnerabilityOrViolationRow, vulnerabilitiesMap map[string]*utils.VulnerabilityDetails) error { if len(vulnerability.FixedVersions) == 0 { return nil diff --git a/scanrepository/scanrepository_test.go b/scanrepository/scanrepository_test.go index 98c548120..45244f7fb 100644 --- a/scanrepository/scanrepository_test.go +++ b/scanrepository/scanrepository_test.go @@ -23,6 +23,7 @@ import ( "github.com/jfrog/jfrog-cli-security/utils/formats" "github.com/jfrog/jfrog-cli-security/utils/formats/violationutils" "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/results/conversion" "github.com/jfrog/jfrog-cli-security/utils/severityutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" @@ -696,6 +697,74 @@ func TestCreateVulnerabilitiesMap(t *testing.T) { } } +func TestMultiTargetVulnerabilitiesMap(t *testing.T) { + cfp := &ScanRepositoryCmd{} + + scanResults := &results.SecurityCommandResults{ + ResultsMetaData: results.ResultsMetaData{ResultContext: results.ResultContext{IncludeVulnerabilities: true}}, + Targets: []*results.TargetResults{ + { + ScanTarget: results.ScanTarget{Target: "project1"}, + ScaResults: &results.ScaScanResults{ + DeprecatedXrayResults: []services.ScanResponse{{ + Vulnerabilities: []services.Vulnerability{{ + Cves: []services.Cve{{Id: "CVE-1"}}, + Severity: "High", + Components: map[string]services.Component{ + "pkg1": { + FixedVersions: []string{"1.0.0"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "pkg1"}}}, + }, + }, + }}, + }}, + }, + }, + { + ScanTarget: results.ScanTarget{Target: "project2"}, + ScaResults: &results.ScaScanResults{ + DeprecatedXrayResults: []services.ScanResponse{{ + Vulnerabilities: []services.Vulnerability{{ + Cves: []services.Cve{{Id: "CVE-2"}}, + Severity: "Critical", + Components: map[string]services.Component{ + "pkg2": { + FixedVersions: []string{"2.0.0"}, + ImpactPaths: [][]services.ImpactPathNode{{{ComponentId: "root"}, {ComponentId: "pkg2"}}}, + }, + }, + }}, + }}, + }, + }, + }, + } + + convertor1 := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IncludeVulnerabilities: true, + IncludeTargets: []string{"project1"}, + }) + simpleJson1, err := convertor1.ConvertToSimpleJson(scanResults) + assert.NoError(t, err) + vulnsMap1, err := cfp.createVulnerabilitiesMapFromSimpleJson(simpleJson1) + assert.NoError(t, err) + assert.Len(t, vulnsMap1, 1) + assert.Contains(t, vulnsMap1, "pkg1") + assert.NotContains(t, vulnsMap1, "pkg2") + + convertor2 := conversion.NewCommandResultsConvertor(conversion.ResultConvertParams{ + IncludeVulnerabilities: true, + IncludeTargets: []string{"project2"}, + }) + simpleJson2, err := convertor2.ConvertToSimpleJson(scanResults) + assert.NoError(t, err) + vulnsMap2, err := cfp.createVulnerabilitiesMapFromSimpleJson(simpleJson2) + assert.NoError(t, err) + assert.Len(t, vulnsMap2, 1) + assert.Contains(t, vulnsMap2, "pkg2") + assert.NotContains(t, vulnsMap2, "pkg1") +} + // Verifies unsupported packages return specific error // Other logic is implemented inside each package-handler. func TestUpdatePackageToFixedVersion(t *testing.T) {