diff --git a/xray/commands/audit/audit.go b/xray/commands/audit/audit.go index 41eca4ae7..2303ef0a2 100644 --- a/xray/commands/audit/audit.go +++ b/xray/commands/audit/audit.go @@ -91,7 +91,8 @@ func (auditCmd *AuditCommand) Run() (err error) { SetWorkingDirs(workingDirs). SetMinSeverityFilter(auditCmd.minSeverityFilter). SetFixableOnly(auditCmd.fixableOnly). - SetGraphBasicParams(auditCmd.AuditBasicParams) + SetGraphBasicParams(auditCmd.AuditBasicParams). + SetThirdPartyApplicabilityScan(auditCmd.thirdPartyApplicabilityScan) auditResults, err := RunAudit(auditParams) if err != nil { return @@ -186,7 +187,7 @@ func RunAudit(auditParams *AuditParams) (results *Results, err error) { // Run scanners only if the user is entitled for Advanced Security if results.ExtendedScanResults.EntitledForJas { - results.JasError = runJasScannersAndSetResults(results.ExtendedScanResults, auditParams.DirectDependencies(), serverDetails, auditParams.workingDirs, auditParams.Progress(), auditParams.xrayGraphScanParams.MultiScanId) + results.JasError = runJasScannersAndSetResults(results.ExtendedScanResults, auditParams.DirectDependencies(), serverDetails, auditParams.workingDirs, auditParams.Progress(), auditParams.xrayGraphScanParams.MultiScanId, auditParams.thirdPartyApplicabilityScan) } return } diff --git a/xray/commands/audit/auditparams.go b/xray/commands/audit/auditparams.go index 69c83a55f..9dc42e3c2 100644 --- a/xray/commands/audit/auditparams.go +++ b/xray/commands/audit/auditparams.go @@ -13,6 +13,8 @@ type AuditParams struct { minSeverityFilter string *xrayutils.AuditBasicParams xrayVersion string + // Include third party dependencies source code in the applicability scan. + thirdPartyApplicabilityScan bool } func NewAuditParams() *AuditParams { @@ -75,3 +77,8 @@ func (params *AuditParams) SetMinSeverityFilter(minSeverityFilter string) *Audit params.minSeverityFilter = minSeverityFilter return params } + +func (params *AuditParams) SetThirdPartyApplicabilityScan(includeThirdPartyDeps bool) *AuditParams { + params.thirdPartyApplicabilityScan = includeThirdPartyDeps + return params +} diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager.go b/xray/commands/audit/jas/applicability/applicabilitymanager.go index 8c46f4865..b038e4371 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager.go @@ -25,6 +25,7 @@ type ApplicabilityScanManager struct { directDependenciesCves []string xrayResults []services.ScanResponse scanner *jas.JasScanner + thirdPartyScan bool } // The getApplicabilityScanResults function runs the applicability scan flow, which includes the following steps: @@ -37,8 +38,8 @@ type ApplicabilityScanManager struct { // bool: true if the user is entitled to the applicability scan, false otherwise. // error: An error object (if any). func RunApplicabilityScan(xrayResults []services.ScanResponse, directDependencies []string, - scannedTechnologies []coreutils.Technology, scanner *jas.JasScanner) (results []*sarif.Run, err error) { - applicabilityScanManager := newApplicabilityScanManager(xrayResults, directDependencies, scanner) + scannedTechnologies []coreutils.Technology, scanner *jas.JasScanner, thirdPartyContextualAnalysis bool) (results []*sarif.Run, err error) { + applicabilityScanManager := newApplicabilityScanManager(xrayResults, directDependencies, scanner, thirdPartyContextualAnalysis) if !applicabilityScanManager.shouldRunApplicabilityScan(scannedTechnologies) { log.Debug("The technologies that have been scanned are currently not supported for contextual analysis scanning, or we couldn't find any vulnerable direct dependencies. Skipping....") return @@ -51,13 +52,14 @@ func RunApplicabilityScan(xrayResults []services.ScanResponse, directDependencie return } -func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, directDependencies []string, scanner *jas.JasScanner) (manager *ApplicabilityScanManager) { +func newApplicabilityScanManager(xrayScanResults []services.ScanResponse, directDependencies []string, scanner *jas.JasScanner, thirdPartyScan bool) (manager *ApplicabilityScanManager) { directDependenciesCves := extractDirectDependenciesCvesFromScan(xrayScanResults, directDependencies) return &ApplicabilityScanManager{ applicabilityScanResults: []*sarif.Run{}, directDependenciesCves: directDependenciesCves, xrayResults: xrayScanResults, scanner: scanner, + thirdPartyScan: thirdPartyScan, } } @@ -140,6 +142,11 @@ type scanConfiguration struct { } func (asm *ApplicabilityScanManager) createConfigFile(workingDir string) error { + skipDirs := jas.SkippedDirs + if asm.thirdPartyScan { + log.Info("Including node modules folder in applicability scan") + skipDirs = removeElementFromSlice(skipDirs, jas.NodeModulesPattern) + } configFileContent := applicabilityScanConfig{ Scans: []scanConfiguration{ { @@ -148,7 +155,7 @@ func (asm *ApplicabilityScanManager) createConfigFile(workingDir string) error { Type: applicabilityScanType, GrepDisable: false, CveWhitelist: asm.directDependenciesCves, - SkippedDirs: jas.SkippedDirs, + SkippedDirs: skipDirs, }, }, } @@ -160,3 +167,11 @@ func (asm *ApplicabilityScanManager) createConfigFile(workingDir string) error { func (asm *ApplicabilityScanManager) runAnalyzerManager() error { return asm.scanner.AnalyzerManager.Exec(asm.scanner.ConfigFileName, applicabilityScanCommand, filepath.Dir(asm.scanner.AnalyzerManager.AnalyzerManagerFullPath), asm.scanner.ServerDetails) } + +func removeElementFromSlice(skipDirs []string, element string) []string { + deleteIndex := slices.Index(skipDirs, element) + if deleteIndex == -1 { + return skipDirs + } + return slices.Delete(skipDirs, deleteIndex, deleteIndex+1) +} diff --git a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go index f887763c5..e4a232c44 100644 --- a/xray/commands/audit/jas/applicability/applicabilitymanager_test.go +++ b/xray/commands/audit/jas/applicability/applicabilitymanager_test.go @@ -17,7 +17,7 @@ func TestNewApplicabilityScanManager_InputIsValid(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) // Assert if assert.NotNil(t, applicabilityManager) { @@ -31,7 +31,7 @@ func TestNewApplicabilityScanManager_DependencyTreeDoesntExist(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, nil, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, nil, scanner, false) // Assert if assert.NotNil(t, applicabilityManager) { @@ -68,9 +68,14 @@ func TestNewApplicabilityScanManager_NoDirectDependenciesInScan(t *testing.T) { // Act scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner, false) + assertApplicabilityScanner(t, applicabilityManager) + // ThirdPartyContextual shouldn't change anything here as this is not npm. + applicabilityManager = newApplicabilityScanManager(noDirectDependenciesResults, mockDirectDependencies, scanner, true) + assertApplicabilityScanner(t, applicabilityManager) +} - // Assert +func assertApplicabilityScanner(t *testing.T, applicabilityManager *ApplicabilityScanManager) { if assert.NotNil(t, applicabilityManager) { assert.NotEmpty(t, applicabilityManager.scanner.ConfigFileName) assert.NotEmpty(t, applicabilityManager.scanner.ResultsFileName) @@ -84,7 +89,7 @@ func TestNewApplicabilityScanManager_MultipleDependencyTrees(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockMultiRootDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockMultiRootDirectDependencies, scanner, false) // Assert if assert.NotNil(t, applicabilityManager) { @@ -110,7 +115,7 @@ func TestNewApplicabilityScanManager_ViolationsDontExistInResults(t *testing.T) defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(noViolationScanResponse, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(noViolationScanResponse, mockDirectDependencies, scanner, false) // Assert if assert.NotNil(t, applicabilityManager) { @@ -136,7 +141,7 @@ func TestNewApplicabilityScanManager_VulnerabilitiesDontExist(t *testing.T) { defer cleanUp() // Act - applicabilityManager := newApplicabilityScanManager(noVulnerabilitiesScanResponse, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(noVulnerabilitiesScanResponse, mockDirectDependencies, scanner, false) // Assert if assert.NotNil(t, applicabilityManager) { @@ -150,8 +155,7 @@ func TestApplicabilityScanManager_ShouldRun_TechnologiesNotEligibleForScan(t *te scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - results, err := RunApplicabilityScan(jas.FakeBasicXrayResults, mockDirectDependencies, - []coreutils.Technology{coreutils.Nuget, coreutils.Go}, scanner) + results, err := RunApplicabilityScan(jas.FakeBasicXrayResults, mockDirectDependencies, []coreutils.Technology{coreutils.Nuget, coreutils.Go}, scanner, false) // Assert assert.Nil(t, results) @@ -163,7 +167,7 @@ func TestApplicabilityScanManager_ShouldRun_ScanResultsAreEmpty(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(nil, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(nil, mockDirectDependencies, scanner, false) // Assert eligible := applicabilityManager.shouldRunApplicabilityScan([]coreutils.Technology{coreutils.Npm}) @@ -247,7 +251,7 @@ func TestCreateConfigFile_VerifyFileWasCreated(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, scanner, false) currWd, err := coreutils.GetWorkingDirectory() assert.NoError(t, err) @@ -271,7 +275,7 @@ func TestParseResults_EmptyResults_AllCvesShouldGetUnknown(t *testing.T) { scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "empty-results.sarif") // Act @@ -288,7 +292,7 @@ func TestParseResults_ApplicableCveExist(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "applicable-cve-results.sarif") // Act @@ -305,7 +309,7 @@ func TestParseResults_AllCvesNotApplicable(t *testing.T) { // Arrange scanner, cleanUp := jas.InitJasTest(t) defer cleanUp() - applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner) + applicabilityManager := newApplicabilityScanManager(jas.FakeBasicXrayResults, mockDirectDependencies, scanner, false) applicabilityManager.scanner.ResultsFileName = filepath.Join(jas.GetTestDataPath(), "applicability-scan", "no-applicable-cves-results.sarif") // Act diff --git a/xray/commands/audit/jas/common.go b/xray/commands/audit/jas/common.go index 3814e06d0..1f4082bfe 100644 --- a/xray/commands/audit/jas/common.go +++ b/xray/commands/audit/jas/common.go @@ -19,8 +19,12 @@ import ( "gopkg.in/yaml.v3" ) +const ( + NodeModulesPattern = "**/*node_modules*/**" +) + var ( - SkippedDirs = []string{"**/*test*/**", "**/*venv*/**", "**/*node_modules*/**", "**/*target*/**"} + SkippedDirs = []string{"**/*test*/**", "**/*venv*/**", NodeModulesPattern, "**/*target*/**"} mapSeverityToScore = map[string]string{ "": "0.0", diff --git a/xray/commands/audit/jasrunner.go b/xray/commands/audit/jasrunner.go index 9f917004b..76a59ff8e 100644 --- a/xray/commands/audit/jasrunner.go +++ b/xray/commands/audit/jasrunner.go @@ -14,7 +14,7 @@ import ( ) func runJasScannersAndSetResults(scanResults *utils.ExtendedScanResults, directDependencies []string, - serverDetails *config.ServerDetails, workingDirs []string, progress io.ProgressMgr, multiScanId string) (err error) { + serverDetails *config.ServerDetails, workingDirs []string, progress io.ProgressMgr, multiScanId string, thirdPartyApplicabilityScan bool) (err error) { if serverDetails == nil || len(serverDetails.Url) == 0 { log.Warn("To include 'Advanced Security' scan as part of the audit output, please run the 'jf c add' command before running this command.") return @@ -30,10 +30,14 @@ func runJasScannersAndSetResults(scanResults *utils.ExtendedScanResults, directD if progress != nil { progress.SetHeadlineMsg("Running applicability scanning") } - scanResults.ApplicabilityScanResults, err = applicability.RunApplicabilityScan(scanResults.XrayResults, directDependencies, scanResults.ScannedTechnologies, scanner) + scanResults.ApplicabilityScanResults, err = applicability.RunApplicabilityScan(scanResults.XrayResults, directDependencies, scanResults.ScannedTechnologies, scanner, thirdPartyApplicabilityScan) if err != nil { return } + // Don't execute other scanners when scanning third party dependencies. + if thirdPartyApplicabilityScan { + return + } if progress != nil { progress.SetHeadlineMsg("Running secrets scanning") } diff --git a/xray/commands/audit/jasrunner_test.go b/xray/commands/audit/jasrunner_test.go index b6bd121df..0d92f1b18 100644 --- a/xray/commands/audit/jasrunner_test.go +++ b/xray/commands/audit/jasrunner_test.go @@ -22,14 +22,14 @@ func TestGetExtendedScanResults_AnalyzerManagerDoesntExist(t *testing.T) { assert.NoError(t, os.Unsetenv(coreutils.HomeDir)) }() scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} - err = runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, &jas.FakeServerDetails, nil, nil, "") + err = runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, &jas.FakeServerDetails, nil, nil, "", false) // Expect error: assert.Error(t, err) } func TestGetExtendedScanResults_ServerNotValid(t *testing.T) { scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Pip}} - err := runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, nil, nil, "") + err := runJasScannersAndSetResults(scanResults, []string{"issueId_1_direct_dependency", "issueId_2_direct_dependency"}, nil, nil, nil, "", false) assert.NoError(t, err) } @@ -37,7 +37,7 @@ func TestGetExtendedScanResults_AnalyzerManagerReturnsError(t *testing.T) { mockDirectDependencies := []string{"issueId_2_direct_dependency", "issueId_1_direct_dependency"} assert.NoError(t, rtutils.DownloadAnalyzerManagerIfNeeded()) scanResults := &utils.ExtendedScanResults{XrayResults: jas.FakeBasicXrayResults, ScannedTechnologies: []coreutils.Technology{coreutils.Yarn}} - err := runJasScannersAndSetResults(scanResults, mockDirectDependencies, &jas.FakeServerDetails, nil, nil, "") + err := runJasScannersAndSetResults(scanResults, mockDirectDependencies, &jas.FakeServerDetails, nil, nil, "", false) // Expect error: assert.ErrorContains(t, err, "failed to run Applicability scan") diff --git a/xray/commands/audit/sca/npm/npm.go b/xray/commands/audit/sca/npm/npm.go index 7a646e349..269c6993e 100644 --- a/xray/commands/audit/sca/npm/npm.go +++ b/xray/commands/audit/sca/npm/npm.go @@ -5,14 +5,14 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "golang.org/x/exp/slices" ) const ( - npmPackageTypeIdentifier = "npm://" - ignoreScriptsFlag = "--ignore-scripts" + ignoreScriptsFlag = "--ignore-scripts" ) func BuildDependencyTree(npmArgs []string) (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { @@ -58,9 +58,9 @@ func addIgnoreScriptsFlag(npmArgs []string) []string { func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo *biutils.PackageInfo) (*xrayUtils.GraphNode, []string) { treeMap := make(map[string][]string) for _, dependency := range dependencies { - dependencyId := npmPackageTypeIdentifier + dependency.Id + dependencyId := utils.NpmPackageTypeIdentifier + dependency.Id for _, requestedByNode := range dependency.RequestedBy { - parent := npmPackageTypeIdentifier + requestedByNode[0] + parent := utils.NpmPackageTypeIdentifier + requestedByNode[0] if children, ok := treeMap[parent]; ok { treeMap[parent] = appendUniqueChild(children, dependencyId) } else { @@ -68,7 +68,7 @@ func parseNpmDependenciesList(dependencies []buildinfo.Dependency, packageInfo * } } } - return sca.BuildXrayDependencyTree(treeMap, npmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) + return sca.BuildXrayDependencyTree(treeMap, utils.NpmPackageTypeIdentifier+packageInfo.BuildInfoModuleId()) } func appendUniqueChild(children []string, candidateDependency string) []string { diff --git a/xray/commands/audit/sca/npm/npm_test.go b/xray/commands/audit/sca/npm/npm_test.go index aaa9ea47c..ff4d0aa01 100644 --- a/xray/commands/audit/sca/npm/npm_test.go +++ b/xray/commands/audit/sca/npm/npm_test.go @@ -3,6 +3,7 @@ package npm import ( "encoding/json" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "os" "testing" @@ -102,7 +103,7 @@ func TestParseNpmDependenciesList(t *testing.T) { } expectedUniqueDeps := []string{xrayDependenciesTree.Id} for _, dep := range dependencies { - expectedUniqueDeps = append(expectedUniqueDeps, npmPackageTypeIdentifier+dep.Id) + expectedUniqueDeps = append(expectedUniqueDeps, utils.NpmPackageTypeIdentifier+dep.Id) } assert.ElementsMatch(t, uniqueDeps, expectedUniqueDeps, "First is actual, Second is Expected") diff --git a/xray/commands/audit/sca/yarn/yarn.go b/xray/commands/audit/sca/yarn/yarn.go index 9df1333c9..ed15121dc 100644 --- a/xray/commands/audit/sca/yarn/yarn.go +++ b/xray/commands/audit/sca/yarn/yarn.go @@ -4,15 +4,12 @@ import ( biUtils "github.com/jfrog/build-info-go/build/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" "github.com/jfrog/jfrog-cli-core/v2/xray/commands/audit/sca" + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" ) -const ( - npmPackageTypeIdentifier = "npm://" -) - func BuildDependencyTree() (dependencyTrees []*xrayUtils.GraphNode, uniqueDeps []string, err error) { currentDir, err := coreutils.GetWorkingDirectory() if err != nil { @@ -55,5 +52,5 @@ func parseYarnDependenciesMap(dependencies map[string]*biUtils.YarnDependency, r } func getXrayDependencyId(yarnDependency *biUtils.YarnDependency) string { - return npmPackageTypeIdentifier + yarnDependency.Name() + ":" + yarnDependency.Details.Version + return utils.NpmPackageTypeIdentifier + yarnDependency.Name() + ":" + yarnDependency.Details.Version } diff --git a/xray/commands/audit/sca/yarn/yarn_test.go b/xray/commands/audit/sca/yarn/yarn_test.go index bb24f3c0d..aadcdc814 100644 --- a/xray/commands/audit/sca/yarn/yarn_test.go +++ b/xray/commands/audit/sca/yarn/yarn_test.go @@ -1,6 +1,7 @@ package yarn import ( + "github.com/jfrog/jfrog-cli-core/v2/xray/utils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" "testing" @@ -18,30 +19,30 @@ func TestParseYarnDependenciesList(t *testing.T) { "pack5@npm:5.0.0": {Value: "pack5@npm:5.0.0", Details: biutils.YarnDepDetails{Version: "5.0.0", Dependencies: []biutils.YarnDependencyPointer{{Locator: "pack2@npm:2.0.0"}}}}, } - rootXrayId := npmPackageTypeIdentifier + "@jfrog/pack3:3.0.0" + rootXrayId := utils.NpmPackageTypeIdentifier + "@jfrog/pack3:3.0.0" expectedTree := &xrayUtils.GraphNode{ Id: rootXrayId, Nodes: []*xrayUtils.GraphNode{ - {Id: npmPackageTypeIdentifier + "pack1:1.0.0", + {Id: utils.NpmPackageTypeIdentifier + "pack1:1.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: npmPackageTypeIdentifier + "pack4:4.0.0", + {Id: utils.NpmPackageTypeIdentifier + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, - {Id: npmPackageTypeIdentifier + "pack2:2.0.0", + {Id: utils.NpmPackageTypeIdentifier + "pack2:2.0.0", Nodes: []*xrayUtils.GraphNode{ - {Id: npmPackageTypeIdentifier + "pack4:4.0.0", + {Id: utils.NpmPackageTypeIdentifier + "pack4:4.0.0", Nodes: []*xrayUtils.GraphNode{}}, - {Id: npmPackageTypeIdentifier + "pack5:5.0.0", + {Id: utils.NpmPackageTypeIdentifier + "pack5:5.0.0", Nodes: []*xrayUtils.GraphNode{}}, }}, }, } expectedUniqueDeps := []string{ - npmPackageTypeIdentifier + "pack1:1.0.0", - npmPackageTypeIdentifier + "pack2:2.0.0", - npmPackageTypeIdentifier + "pack4:4.0.0", - npmPackageTypeIdentifier + "pack5:5.0.0", - npmPackageTypeIdentifier + "@jfrog/pack3:3.0.0", + utils.NpmPackageTypeIdentifier + "pack1:1.0.0", + utils.NpmPackageTypeIdentifier + "pack2:2.0.0", + utils.NpmPackageTypeIdentifier + "pack4:4.0.0", + utils.NpmPackageTypeIdentifier + "pack5:5.0.0", + utils.NpmPackageTypeIdentifier + "@jfrog/pack3:3.0.0", } xrayDependenciesTree, uniqueDeps := parseYarnDependenciesMap(yarnDependencies, rootXrayId) diff --git a/xray/commands/audit/scarunner.go b/xray/commands/audit/scarunner.go index 7f209fa29..f78ac8dc2 100644 --- a/xray/commands/audit/scarunner.go +++ b/xray/commands/audit/scarunner.go @@ -96,15 +96,14 @@ func runScaScanOnWorkingDir(params *AuditParams, results *Results, workingDir, r continue } techResults = sca.BuildImpactPathsForScanResponse(techResults, fullDependencyTrees) - var directDependencies []string - if tech == coreutils.Pip { - // When building pip dependency tree using pipdeptree, some of the direct dependencies are recognized as transitive and missed by the CA scanner. - // Our solution for this case is to send all dependencies to the CA scanner. - directDependencies = getDirectDependenciesFromTree([]*xrayCmdUtils.GraphNode{flattenTree}) + + var dependenciesForApplicabilityScan []string + if shouldUseAllDependencies(params.thirdPartyApplicabilityScan, tech) { + dependenciesForApplicabilityScan = getDirectDependenciesFromTree([]*xrayCmdUtils.GraphNode{flattenTree}) } else { - directDependencies = getDirectDependenciesFromTree(fullDependencyTrees) + dependenciesForApplicabilityScan = getDirectDependenciesFromTree(fullDependencyTrees) } - params.AppendDirectDependencies(directDependencies) + params.AppendDependenciesForApplicabilityScan(dependenciesForApplicabilityScan) results.ExtendedScanResults.XrayResults = append(results.ExtendedScanResults.XrayResults, techResults...) if !results.IsMultipleRootProject { @@ -115,6 +114,14 @@ func runScaScanOnWorkingDir(params *AuditParams, results *Results, workingDir, r return } +// When building pip dependency tree using pipdeptree, some of the direct dependencies are recognized as transitive and missed by the CA scanner. +// Our solution for this case is to send all dependencies to the CA scanner. +// When thirdPartyApplicabilityScan is true, use flatten graph to include all the dependencies in applicability scanning. +// Only npm is supported for this flag. +func shouldUseAllDependencies(thirdPartyApplicabilityScan bool, tech coreutils.Technology) bool { + return tech == coreutils.Pip || (thirdPartyApplicabilityScan && tech == coreutils.Npm) +} + // This function retrieves the dependency trees of the scanned project and extracts a set that contains only the direct dependencies. func getDirectDependenciesFromTree(dependencyTrees []*xrayCmdUtils.GraphNode) []string { directDependencies := datastructures.MakeSet[string]() diff --git a/xray/utils/auditbasicparams.go b/xray/utils/auditbasicparams.go index f8e3abba3..ff990e9fc 100644 --- a/xray/utils/auditbasicparams.go +++ b/xray/utils/auditbasicparams.go @@ -6,26 +6,26 @@ import ( ) type AuditBasicParams struct { - serverDetails *config.ServerDetails - outputFormat OutputFormat - progress ioUtils.ProgressMgr - directDependencies []string - excludeTestDependencies bool - useWrapper bool - insecureTls bool - pipRequirementsFile string - technologies []string - args []string - depsRepo string - ignoreConfigFile bool + serverDetails *config.ServerDetails + outputFormat OutputFormat + progress ioUtils.ProgressMgr + dependenciesForApplicabilityScan []string + excludeTestDependencies bool + useWrapper bool + insecureTls bool + pipRequirementsFile string + technologies []string + args []string + depsRepo string + ignoreConfigFile bool } func (abp *AuditBasicParams) DirectDependencies() []string { - return abp.directDependencies + return abp.dependenciesForApplicabilityScan } -func (abp *AuditBasicParams) AppendDirectDependencies(directDependencies []string) *AuditBasicParams { - abp.directDependencies = append(abp.directDependencies, directDependencies...) +func (abp *AuditBasicParams) AppendDependenciesForApplicabilityScan(directDependencies []string) *AuditBasicParams { + abp.dependenciesForApplicabilityScan = append(abp.dependenciesForApplicabilityScan, directDependencies...) return abp } diff --git a/xray/utils/resultstable.go b/xray/utils/resultstable.go index 08c0bd5da..4f098e114 100644 --- a/xray/utils/resultstable.go +++ b/xray/utils/resultstable.go @@ -3,6 +3,7 @@ package utils import ( "fmt" "os" + "path/filepath" "sort" "strconv" "strings" @@ -26,6 +27,8 @@ const ( rootIndex = 0 directDependencyIndex = 1 directDependencyPathLength = 2 + nodeModules = "node_modules" + NpmPackageTypeIdentifier = "npm://" ) // PrintViolationsTable prints the violations in 4 tables: security violations, license compliance violations, operational risk violations and ignore rule URLs. @@ -90,7 +93,7 @@ func prepareViolations(violations []services.Violation, extendedResults *Extende cves := convertCves(violation.Cves) if extendedResults.EntitledForJas { for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], extendedResults.ApplicabilityScanResults) + cves[i].Applicability = getCveApplicabilityField(cves[i], extendedResults.ApplicabilityScanResults, violation.Components) } } applicabilityStatus := getApplicableCveStatus(extendedResults.EntitledForJas, extendedResults.ApplicabilityScanResults, cves) @@ -212,7 +215,7 @@ func prepareVulnerabilities(vulnerabilities []services.Vulnerability, extendedRe cves := convertCves(vulnerability.Cves) if extendedResults.EntitledForJas { for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], extendedResults.ApplicabilityScanResults) + cves[i].Applicability = getCveApplicabilityField(cves[i], extendedResults.ApplicabilityScanResults, vulnerability.Components) } } applicabilityStatus := getApplicableCveStatus(extendedResults.EntitledForJas, extendedResults.ApplicabilityScanResults, cves) @@ -949,42 +952,51 @@ func getApplicableCveStatus(entitledForJas bool, applicabilityScanResults []*sar return NotApplicable } -func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sarif.Run) *formats.Applicability { - applicability := &formats.Applicability{Status: string(ApplicabilityUndetermined)} +func getCveApplicabilityField(cve formats.CveRow, applicabilityScanResults []*sarif.Run, components map[string]services.Component) *formats.Applicability { + if len(applicabilityScanResults) == 0 { + return nil + } + + applicability := formats.Applicability{} + resultFound := false for _, applicabilityRun := range applicabilityScanResults { - foundResult, _ := applicabilityRun.GetResultByRuleId(CveToApplicabilityRuleId(cve.Id)) - if foundResult == nil { + result, _ := applicabilityRun.GetResultByRuleId(CveToApplicabilityRuleId(cve.Id)) + if result == nil { continue } - applicability = &formats.Applicability{} - if IsApplicableResult(foundResult) { - applicability.Status = string(Applicable) - } else { - applicability.Status = string(NotApplicable) + resultFound = true + rule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cve.Id)) + if rule != nil { + applicability.ScannerDescription = GetRuleFullDescription(rule) } - - foundRule, _ := applicabilityRun.GetRuleById(CveToApplicabilityRuleId(cve.Id)) - if foundRule != nil { - applicability.ScannerDescription = GetRuleFullDescription(foundRule) - } - // Add new evidences from locations - for _, location := range foundResult.Locations { + for _, location := range result.Locations { + fileName := GetRelativeLocationFileName(location, applicabilityRun.Invocations) + if shouldDisqualifyEvidence(components, fileName) { + continue + } applicability.Evidence = append(applicability.Evidence, formats.Evidence{ Location: formats.Location{ - File: GetRelativeLocationFileName(location, applicabilityRun.Invocations), + File: fileName, StartLine: GetLocationStartLine(location), StartColumn: GetLocationStartColumn(location), EndLine: GetLocationEndLine(location), EndColumn: GetLocationEndColumn(location), Snippet: GetLocationSnippet(location), }, - Reason: GetResultMsgText(foundResult), + Reason: GetResultMsgText(result), }) } - break } - return applicability + switch { + case !resultFound: + applicability.Status = string(ApplicabilityUndetermined) + case len(applicability.Evidence) == 0: + applicability.Status = string(NotApplicable) + default: + applicability.Status = string(Applicable) + } + return &applicability } func printApplicableCveValue(applicableValue ApplicabilityStatus, isTable bool) string { @@ -997,3 +1009,39 @@ func printApplicableCveValue(applicableValue ApplicabilityStatus, isTable bool) } return string(applicableValue) } + +// Relevant only when "third-party-contextual-analysis" flag is on, +// which mean we scan the environment folders as well (node_modules for example...) +// When a certain package is reported applicable, and the evidence found +// is inside the source code of the same package, we should disqualify it. +// +// For example, +// Cve applicability was found inside the 'mquery' package. +// filePath = myProject/node_modules/mquery/badCode.js , disqualify = True. +// Disqualify the above evidence, as the reported applicability is used inside its own package. +// +// filePath = myProject/node_modules/mpath/badCode.js , disqualify = False. +// Found use of a badCode inside the node_modules from a different package, report applicable. +func shouldDisqualifyEvidence(components map[string]services.Component, evidenceFilePath string) (disqualify bool) { + for key := range components { + if !strings.HasPrefix(key, NpmPackageTypeIdentifier) { + return + } + dependencyName := extractDependencyNameFromComponent(key, NpmPackageTypeIdentifier) + // Check both Unix & Windows paths. + if strings.Contains(evidenceFilePath, nodeModules+"/"+dependencyName) || strings.Contains(evidenceFilePath, filepath.Join(nodeModules, dependencyName)) { + return true + } + } + return +} + +func extractDependencyNameFromComponent(key string, techIdentifier string) (dependencyName string) { + packageAndVersion := strings.TrimPrefix(key, techIdentifier) + split := strings.Split(packageAndVersion, ":") + if len(split) < 2 { + return + } + dependencyName = split[0] + return +} diff --git a/xray/utils/resultstable_test.go b/xray/utils/resultstable_test.go index a3c35f24e..d7b7c5056 100644 --- a/xray/utils/resultstable_test.go +++ b/xray/utils/resultstable_test.go @@ -521,7 +521,7 @@ func TestGetApplicableCveValue(t *testing.T) { for _, testCase := range testCases { cves := convertCves(testCase.cves) for i := range cves { - cves[i].Applicability = getCveApplicabilityField(cves[i], testCase.scanResults.ApplicabilityScanResults) + cves[i].Applicability = getCveApplicabilityField(cves[i], testCase.scanResults.ApplicabilityScanResults, nil) } applicableValue := getApplicableCveStatus(testCase.scanResults.EntitledForJas, testCase.scanResults.ApplicabilityScanResults, cves) assert.Equal(t, testCase.expectedResult, applicableValue) @@ -637,6 +637,43 @@ func TestSortVulnerabilityOrViolationRows(t *testing.T) { } } +func TestShouldDisqualifyEvidence(t *testing.T) { + testCases := []struct { + name string + component map[string]services.Component + filePath string + disqualify bool + }{ + { + name: "package folders", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", + disqualify: true, + }, { + name: "nested folders", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/someDep/node_modules/protobufjs/src/badCode.js", + disqualify: true, + }, { + name: "applicability in node modules", + component: map[string]services.Component{"npm://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/mquery/src/badCode.js", + disqualify: false, + }, { + // Only npm supported + name: "not npm", + component: map[string]services.Component{"yarn://protobufjs:6.11.2": {}}, + filePath: "file:///Users/jfrog/test/node_modules/protobufjs/src/badCode.js", + disqualify: false, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, tc.disqualify, shouldDisqualifyEvidence(tc.component, tc.filePath)) + }) + } +} + func newBoolPtr(v bool) *bool { return &v }