diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 67357cb3f..f69e8062f 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -198,7 +198,7 @@ func shouldIncludeSnippetDetection(params *AuditParams) bool { } func GetTargetsInfo(workingDirs []string, bomGenerator bom.SbomGenerator, scansToPerform []utils.SubScanType, includeSbom bool, rootDir string) (projectPath string, includeDirs []string, isRecursiveScan bool, err error) { - includeDirs, err = utils.GetFullPathsWorkingDirs(workingDirs) + includeDirs, err = resolveAuditWorkingDirs(rootDir, workingDirs) if err != nil { return } @@ -221,6 +221,43 @@ func GetTargetsInfo(workingDirs []string, bomGenerator bom.SbomGenerator, scansT return } +// resolveAuditWorkingDirs makes working directories absolute against the scan root. +// A relative directory is joined to rootDir when that root is set, and to the process directory otherwise. +// Wildcard patterns stay as given. An absolute pattern such as /repo/src/* does not match the repo-relative path src/api produced while listing the scan root. +func resolveAuditWorkingDirs(rootDir string, workingDirs []string) ([]string, error) { + if len(workingDirs) == 0 { + return nil, nil + } + base := rootDir + if base == "" { + cwd, err := coreutils.GetWorkingDirectory() + if err != nil { + return nil, err + } + base = cwd + } + base, err := filepath.Abs(base) + if err != nil { + return nil, err + } + resolved := make([]string, 0, len(workingDirs)) + for _, wd := range workingDirs { + if isWildcardInclude(wd) { + resolved = append(resolved, wd) + continue + } + if !filepath.IsAbs(wd) { + wd = filepath.Join(base, wd) + } + absWd, err := filepath.Abs(wd) + if err != nil { + return nil, err + } + resolved = append(resolved, absWd) + } + return resolved, nil +} + func isNewFlow(bomGenerator bom.SbomGenerator) bool { if _, ok := bomGenerator.(*xrayplugin.XrayLibBomGenerator); ok { return true @@ -630,7 +667,7 @@ func createScanTargetsFromConfigs(cmdResults *results.SecurityCommandResults, pa msg += fmt.Sprintf(" and working dirs: %s", strings.Join(includeDirs, ", ")) } log.Debug(msg) - if scanTarget := createScanTarget(rootDir, params.Exclusions(), includeDirs...); scanTarget != nil { + if scanTarget := createScanTarget(rootDir, params.Exclusions(), true, includeDirs...); scanTarget != nil { scanTarget.Technologies = detectTechnologiesInTarget(*scanTarget, params) cmdResults.NewScanResults(*scanTarget) } @@ -645,7 +682,11 @@ func createScanTargetsFromConfigs(cmdResults *results.SecurityCommandResults, pa if module.PathFromRoot != "" && module.PathFromRoot != "." { moduleRoot = filepath.Join(rootDir, module.PathFromRoot) } - scanTarget := createScanTarget(moduleRoot, module.ExcludePatterns, module.IncludePatterns...) + if moduleRoot != rootDir && utils.IsPathExcludedFromRoot(rootDir, moduleRoot, module.ExcludePatterns) { + log.Warn(fmt.Sprintf("The module directory '%s' matches exclusion patterns %s. Skipping...", moduleRoot, strings.Join(module.ExcludePatterns, ", "))) + continue + } + scanTarget := createScanTarget(moduleRoot, module.ExcludePatterns, false, module.IncludePatterns...) if scanTarget == nil { continue } @@ -656,46 +697,66 @@ func createScanTargetsFromConfigs(cmdResults *results.SecurityCommandResults, pa } // Create a scan target from the given root directory, exclude patterns and optionally include patterns. -func createScanTarget(root string, exclude []string, includes ...string) *results.ScanTarget { +// keepNamedDirectories keeps a directory the user named, even when that directory's own name matches an exclusion. +func createScanTarget(root string, exclude []string, keepNamedDirectories bool, includes ...string) *results.ScanTarget { dirs := datastructures.MakeSet[string]() // Validate include patterns for _, includePattern := range includes { - // Check if the include pattern is a file or a directory. - if isDir, err := fileutils.IsDirExists(includePattern, false); err != nil { - log.Warn(fmt.Sprintf("Failed to check if '%s' is a directory: %s", includePattern, err.Error())) - continue - } else if isDir { + // A wildcard is not a filesystem path. Stat-ing it on Windows returns an error that is not "not exist", which drops the pattern before listing. + if !isWildcardInclude(includePattern) { includePath := includePattern if !filepath.IsAbs(includePattern) { includePath = filepath.Join(root, includePattern) } - if utils.IsPathExcludedFromRoot(root, includePath, exclude) { - log.Debug(fmt.Sprintf("Skipping include path '%s' as it matches exclusion patterns %s", includePath, strings.Join(exclude, ", "))) + isDir, err := fileutils.IsDirExists(includePath, false) + if err != nil { + log.Warn(fmt.Sprintf("Failed to check if '%s' is a directory: %s", includePath, err.Error())) + continue + } + if isDir { + if !keepNamedDirectories && utils.IsPathExcludedFromRoot(root, includePath, exclude) { + log.Debug(fmt.Sprintf("Skipping include path '%s' as it matches exclusion patterns %s", includePath, strings.Join(exclude, ", "))) + continue + } + dirs.Add(includePath) + continue + } + if keepNamedDirectories && !fileutils.IsPathExists(includePath, false) { + log.Warn(fmt.Sprintf("The working directory '%s' doesn't exist. Skipping...", includePattern)) continue } - dirs.Add(includePath) - continue } // the pattern is not a directory, so we need to list the directories in the pattern. log.Debug(fmt.Sprintf("The pattern '%s' is not a directory, listing directories in the pattern...", includePattern)) - includeDirs, err := utils.ListDirs(root, includePattern == root, true, true, utils.GetExcludePattern(exclude, utils.DefaultScaExcludePatterns, includePattern == root), includePattern) + // A pattern such as src/* has to walk into subdirectories. A literal name is matched only at the root. + listRecursive := includePattern == root || isNestedWildcard(includePattern) + includeDirs, err := utils.ListDirs(root, listRecursive, true, true, utils.GetExcludePattern(exclude, utils.DefaultScaExcludePatterns, listRecursive), includePattern) if err != nil { log.Warn(fmt.Sprintf("Failed to list directories for '%s': %s", includePattern, err.Error())) continue } dirs.AddElements(includeDirs...) } - include := dirs.ToSlice() - if utils.IsPathExcludedFromRoot(root, root, exclude) { - if len(include) == 0 { - log.Warn(fmt.Sprintf("The working directory '%s' matches exclusion patterns %s. Skipping...", root, strings.Join(exclude, ", "))) - return nil - } - log.Debug(fmt.Sprintf("Root directory '%s' is excluded; creating scan target from %d explicit include path(s)", root, len(include))) + // Named includes that resolve to nothing must not fall back to scanning the project root. + // An empty Include is the root scan. A request that names no includes still uses that fallback. + if keepNamedDirectories && len(includes) > 0 && dirs.Size() == 0 { + return nil } + include := dirs.ToSlice() return &results.ScanTarget{Target: root, Include: include, Exclude: exclude} } +// isWildcardInclude reports whether includePattern is expanded by directory listing. +// A literal missing directory is skipped. A pattern such as src/* is not a path on disk. +func isWildcardInclude(includePattern string) bool { + return strings.Contains(includePattern, "*") +} + +// isNestedWildcard reports whether includePattern can match a directory below the project root. +func isNestedWildcard(includePattern string) bool { + return strings.Contains(includePattern, "*") && strings.ContainsAny(includePattern, `/\`) +} + func detectTechnologiesInTarget(target results.ScanTarget, otherParams *AuditParams) (technologies []techutils.Technology) { detectedTechnologies := datastructures.MakeSet[techutils.Technology]() for _, included := range jas.GetRootsFromTarget(target) { @@ -811,10 +872,6 @@ func detectScaTargetsFromTechnologies(cmdResults *results.SecurityCommandResults log.Warn("The working directory", requestedDirectory, "doesn't exist. Skipping SCA scan...") continue } - if isExcluded := utils.IsPathExcludedFromRoot(requestedDirectory, requestedDirectory, exclusions); isExcluded { - log.Warn(fmt.Sprintf("The working directory '%s' matches exclusion patterns %s. Skipping...", requestedDirectory, strings.Join(exclusions, ", "))) - continue - } dirsToDetect = append(dirsToDetect, requestedDirectory) } for _, requestedDirectory := range dirsToDetect { @@ -835,7 +892,7 @@ func detectScaTargetsFromTechnologies(cmdResults *results.SecurityCommandResults // No technology was detected, add scan without descriptors. (so no sca scan will be performed and set at target level) if len(workingDirs) == 0 { // Requested technology (from params) descriptors/indicators were not found or recursive scan with NoTech value, add scan without descriptors. - scanTarget := createScanTarget(requestedDirectory, exclusions) + scanTarget := createScanTarget(requestedDirectory, exclusions, false) if scanTarget == nil { continue } @@ -844,7 +901,7 @@ func detectScaTargetsFromTechnologies(cmdResults *results.SecurityCommandResults } for workingDir, descriptors := range workingDirs { // Add scan for each detected working directory. - scanTarget := createScanTarget(workingDir, exclusions) + scanTarget := createScanTarget(workingDir, exclusions, false) if scanTarget == nil { continue } @@ -859,7 +916,7 @@ func detectScaTargetsFromTechnologies(cmdResults *results.SecurityCommandResults // If this working dir produced no targets (e.g. JAS-only with no package managers), // still create a target so secrets/IaC/SAST can run. if len(cmdResults.Targets) == targetsBefore { - if scanTarget := createScanTarget(requestedDirectory, exclusions); scanTarget != nil { + if scanTarget := createScanTarget(requestedDirectory, exclusions, false); scanTarget != nil { cmdResults.NewScanResults(*scanTarget) } } diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 4557acb74..1794cccce 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -584,6 +584,27 @@ func TestDetectScanTargetsNewFlowKeepsProjectUnderMatchingAncestor(t *testing.T) assert.Equal(t, resolvedProjectDir, cmdRes.Targets[0].Target) } +func TestDetectScanTargetsNewFlowKeepsProjectNamedLikeExclusion(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + + projectDir := filepath.Join(baseDir, "my-test-project") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + createEmptyFile(t, filepath.Join(projectDir, "package.json")) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetExclusions([]string{"*test*"}) + + detectScanTargets(cmdRes, params) + require.NotEmpty(t, cmdRes.Targets) + resolvedProjectDir, err := filepath.EvalSymlinks(projectDir) + require.NoError(t, err) + assert.Equal(t, resolvedProjectDir, cmdRes.Targets[0].Target) +} + func TestNewFlowProfileDiscardsCliExclusions(t *testing.T) { baseDir, cleanUp := createTestDir(t) defer cleanUp() @@ -633,40 +654,335 @@ func TestNewFlowProfileDiscardsCliExclusions(t *testing.T) { } } -func TestDetectScanTargetsSkipsCliExcludedCwdSingleTarget(t *testing.T) { +func TestDetectScanTargetsKeepsProjectNamedLikeExclusion(t *testing.T) { baseDir, cleanUp := createTestDir(t) defer cleanUp() - excludedCwd := filepath.Join(baseDir, "parent_only_excluded_cli_audit") - assert.NoError(t, os.MkdirAll(excludedCwd, 0o755)) - defer securityTestUtils.ChangeWDWithCallback(t, excludedCwd)() + projectDir := filepath.Join(baseDir, "my-test-project") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + createEmptyFile(t, filepath.Join(projectDir, "package.json")) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) params := NewAuditParams() - params.SetExclusions([]string{"*parent_only_excluded_cli_audit*"}) + params.SetExclusions([]string{"*test*"}) detectScanTargets(cmdRes, params) - assert.Empty(t, cmdRes.Targets) + require.NotEmpty(t, cmdRes.Targets) + resolvedProjectDir, err := filepath.EvalSymlinks(projectDir) + require.NoError(t, err) + assert.Equal(t, resolvedProjectDir, cmdRes.Targets[0].Target) } -func TestDetectScanTargetsSkipsCliExcludedExplicitWorkingDir(t *testing.T) { +func TestDetectScanTargetsKeepsExplicitWorkingDirNamedLikeExclusion(t *testing.T) { baseDir, cleanUp := createTestDir(t) defer cleanUp() - excludedOnly := filepath.Join(baseDir, "cli_exclude_workdir_only_audit") - assert.NoError(t, os.MkdirAll(excludedOnly, 0o755)) - createEmptyFile(t, filepath.Join(excludedOnly, "package.json")) + projectDir := filepath.Join(baseDir, "cli_exclude_workdir_only_audit") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + createEmptyFile(t, filepath.Join(projectDir, "package.json")) defer securityTestUtils.ChangeWDWithCallback(t, baseDir)() cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) params := NewAuditParams() - params.SetWorkingDirs([]string{excludedOnly}) + params.SetWorkingDirs([]string{projectDir}) params.SetIsRecursiveScan(false) params.SetExclusions([]string{"*cli_exclude_workdir_only_audit*"}) detectScanTargets(cmdRes, params) + require.NotEmpty(t, cmdRes.Targets) + assert.Equal(t, projectDir, cmdRes.Targets[0].Target) +} + +func TestDetectScanTargetsNewFlowKeepsWorkingDirNamedLikeExclusion(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + projectDir := filepath.Join(resolvedBase, "my-test-proj") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + createEmptyFile(t, filepath.Join(projectDir, "package.json")) + + defer securityTestUtils.ChangeWDWithCallback(t, resolvedBase)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true).SetSecretValidation(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs([]string{projectDir}) + params.SetExclusions([]string{"*test*"}) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Contains(t, cmdRes.Targets[0].Include, projectDir) + assert.Equal(t, []string{"*test*"}, cmdRes.Targets[0].Exclude) +} + +func TestDetectScanTargetsDoesNotWarnWhenRootNameMatchesExclusion(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + projectDir := filepath.Join(resolvedBase, "my-test-proj") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + createEmptyFile(t, filepath.Join(projectDir, "package.json")) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() + + var buf bytes.Buffer + origLogger := log.Logger + log.SetLogger(log.NewLogger(log.WARN, &buf)) + defer log.SetLogger(origLogger) + + for _, newFlow := range []bool{false, true} { + buf.Reset() + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + if newFlow { + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + } + params.SetExclusions([]string{"*test*"}) + detectScanTargets(cmdRes, params) + require.NotEmpty(t, cmdRes.Targets) + assert.NotContains(t, buf.String(), "matches exclusion patterns") + } +} + +func TestDetectScanTargetsNewFlowSkipsProfileIncludeNamedLikeExclusion(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + projectDir := filepath.Join(resolvedBase, "project") + testDir := filepath.Join(projectDir, "test") + assert.NoError(t, os.MkdirAll(testDir, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() + + profile := &services.ConfigProfile{ + ProfileName: "profile", + Modules: []services.Module{{ + PathFromRoot: ".", + ExcludePatterns: []string{"*test*"}, + IncludePatterns: []string{testDir}, + }}, + } + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetConfigProfile(profile) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.NotContains(t, cmdRes.Targets[0].Include, testDir) +} + +func TestDetectScanTargetsNewFlowSkipsProfileModuleRootNamedLikeExclusion(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + projectDir := filepath.Join(resolvedBase, "project") + testDir := filepath.Join(projectDir, "test") + assert.NoError(t, os.MkdirAll(testDir, 0o755)) + createEmptyFile(t, filepath.Join(testDir, "package.json")) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() + + profile := &services.ConfigProfile{ + ProfileName: "profile", + Modules: []services.Module{{ + PathFromRoot: "test", + ExcludePatterns: []string{"*test*"}, + }}, + } + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetConfigProfile(profile) + + detectScanTargets(cmdRes, params) + + assert.Empty(t, cmdRes.Targets) +} + +func TestDetectScanTargetsNewFlowWarnsWhenWorkingDirIsMissing(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + projectDir := filepath.Join(resolvedBase, "proj") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + missingDir := filepath.Join(resolvedBase, "missing-proj") + defer securityTestUtils.ChangeWDWithCallback(t, resolvedBase)() + + var buf bytes.Buffer + origLogger := log.Logger + log.SetLogger(log.NewLogger(log.WARN, &buf)) + defer log.SetLogger(origLogger) + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs([]string{projectDir, missingDir}) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Contains(t, cmdRes.Targets[0].Include, projectDir) + assert.NotContains(t, cmdRes.Targets[0].Include, missingDir) + assert.Contains(t, buf.String(), "missing-proj") +} + +func TestDetectScanTargetsNewFlowExpandsWildcardWorkingDir(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + apiDir := filepath.Join(resolvedBase, "src", "api") + webDir := filepath.Join(resolvedBase, "src", "web") + assert.NoError(t, os.MkdirAll(apiDir, 0o755)) + assert.NoError(t, os.MkdirAll(webDir, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, resolvedBase)() + + var buf bytes.Buffer + origLogger := log.Logger + log.SetLogger(log.NewLogger(log.WARN, &buf)) + defer log.SetLogger(origLogger) + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs([]string{filepath.Join("src", "*")}) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Contains(t, cmdRes.Targets[0].Include, apiDir) + assert.Contains(t, cmdRes.Targets[0].Include, webDir) + assert.NotContains(t, buf.String(), "doesn't exist") +} + +func TestGetTargetsInfoResolvesRelativeWorkingDirAgainstRoot(t *testing.T) { + root, cleanRoot := createTestDir(t) + defer cleanRoot() + appDir := filepath.Join(root, "app") + assert.NoError(t, os.MkdirAll(appDir, 0o755)) + + otherDir, cleanOther := createTestDir(t) + defer cleanOther() + defer securityTestUtils.ChangeWDWithCallback(t, otherDir)() + + _, includeDirs, _, err := GetTargetsInfo([]string{"app"}, xrayplugin.NewXrayLibBomGenerator(), nil, false, root) + require.NoError(t, err) + require.Len(t, includeDirs, 1) + got, err := filepath.EvalSymlinks(includeDirs[0]) + require.NoError(t, err) + want, err := filepath.EvalSymlinks(appDir) + require.NoError(t, err) + assert.Equal(t, want, got) +} + +func TestGetTargetsInfoKeepsAbsoluteWorkingDir(t *testing.T) { + root, cleanRoot := createTestDir(t) + defer cleanRoot() + otherDir, cleanOther := createTestDir(t) + defer cleanOther() + appDir := filepath.Join(otherDir, "app") + assert.NoError(t, os.MkdirAll(appDir, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, root)() + + _, includeDirs, _, err := GetTargetsInfo([]string{appDir}, xrayplugin.NewXrayLibBomGenerator(), nil, false, root) + require.NoError(t, err) + require.Len(t, includeDirs, 1) + got, err := filepath.EvalSymlinks(includeDirs[0]) + require.NoError(t, err) + want, err := filepath.EvalSymlinks(appDir) + require.NoError(t, err) + assert.Equal(t, want, got) +} + +func TestDetectScanTargetsNewFlowResolvesRelativeWorkingDirAgainstRoot(t *testing.T) { + root, cleanRoot := createTestDir(t) + defer cleanRoot() + resolvedRoot, err := filepath.EvalSymlinks(root) + require.NoError(t, err) + appDir := filepath.Join(resolvedRoot, "app") + assert.NoError(t, os.MkdirAll(appDir, 0o755)) + + otherDir, cleanOther := createTestDir(t) + defer cleanOther() + defer securityTestUtils.ChangeWDWithCallback(t, otherDir)() + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetRootDir(resolvedRoot) + params.SetWorkingDirs([]string{"app"}) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Contains(t, cmdRes.Targets[0].Include, appDir) +} + +func TestDetectScanTargetsNewFlowExpandsWildcardAfterWorkingDirNormalization(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + + apiDir := filepath.Join(resolvedBase, "src", "api") + webDir := filepath.Join(resolvedBase, "src", "web") + assert.NoError(t, os.MkdirAll(apiDir, 0o755)) + assert.NoError(t, os.MkdirAll(webDir, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, resolvedBase)() + + _, includeDirs, _, err := GetTargetsInfo([]string{filepath.Join("src", "*")}, xrayplugin.NewXrayLibBomGenerator(), nil, false, "") + require.NoError(t, err) + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs(includeDirs) + + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Contains(t, cmdRes.Targets[0].Include, apiDir) + assert.Contains(t, cmdRes.Targets[0].Include, webDir) +} + +func TestDetectScanTargetsNewFlowOmitsTargetWhenAllWorkingDirsMissing(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + resolvedBase, err := filepath.EvalSymlinks(baseDir) + require.NoError(t, err) + defer securityTestUtils.ChangeWDWithCallback(t, resolvedBase)() + + var buf bytes.Buffer + origLogger := log.Logger + log.SetLogger(log.NewLogger(log.WARN, &buf)) + defer log.SetLogger(origLogger) + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetWorkingDirs([]string{ + filepath.Join(resolvedBase, "missing-a"), + filepath.Join(resolvedBase, "missing-b"), + }) + + detectScanTargets(cmdRes, params) + assert.Empty(t, cmdRes.Targets) + assert.Contains(t, buf.String(), "missing-a") + assert.Contains(t, buf.String(), "missing-b") } func TestDetectScanTargetsNewFlowCliExcludedCwdWithNonExcludedInclude(t *testing.T) { diff --git a/jas/common.go b/jas/common.go index 51eb14522..a4650b842 100644 --- a/jas/common.go +++ b/jas/common.go @@ -407,11 +407,6 @@ func ShouldSkipScannerByModule(target results.ScanTarget, scanType jasutils.JasS log.Debug(fmt.Sprintf("Skipping %s scan as requested by local module config...", scanType)) return true } - exclusions := target.GetDeprecatedAppsConfigModuleExclusions(scanType) - if len(exclusions) > 0 && utils.IsPathExcludedFromRoot(target.Target, target.Target, exclusions) { - log.Debug(fmt.Sprintf("Skipping %s scan as target is excluded by local module config...", scanType)) - return true - } return false } diff --git a/jas/common_test.go b/jas/common_test.go index 3320273d0..a0c1a3eff 100644 --- a/jas/common_test.go +++ b/jas/common_test.go @@ -81,7 +81,7 @@ func TestShouldSkipScannerByModule(t *testing.T) { Target: filepath.Join(string(filepath.Separator), "ci", "attestation", "myproject"), DeprecatedAppsConfigModule: &module, }, jasutils.IaC)) - assert.True(t, ShouldSkipScannerByModule(results.ScanTarget{ + assert.False(t, ShouldSkipScannerByModule(results.ScanTarget{ Target: filepath.Join(string(filepath.Separator), "ci", "my-test-project"), DeprecatedAppsConfigModule: &module, }, jasutils.IaC)) diff --git a/utils/paths.go b/utils/paths.go index 24efd8581..1fffd9aaa 100644 --- a/utils/paths.go +++ b/utils/paths.go @@ -117,27 +117,31 @@ func GetFullPathsWorkingDirs(workingDirs []string) ([]string, error) { } // IsPathExcludedFromRoot reports whether path matches exclusions relative to root. -// The scan root, and any path outside it, is compared by its own name, so a parent directory outside the project cannot exclude it. -// A path inside root is compared by its path from root. A path that cannot be made relative is not excluded. +// Only a path inside root is compared, by its path from root. +// The scan root itself, and any path outside it, is not excluded, so the project's own name and a parent folder cannot exclude it. +// A path that cannot be made relative is not excluded. func IsPathExcludedFromRoot(root, path string, exclusions []string) bool { matchPath, err := exclusionMatchPath(root, path) if err != nil { log.Warn("Failed to compare path for exclusions:", err.Error()) return false } + if matchPath == "" { + return false + } return IsPathExcluded(matchPath, exclusions) } func exclusionMatchPath(root, path string) (string, error) { if root == "" { - return filepath.Base(path), nil + return "", nil } rel, err := filepath.Rel(root, path) if err != nil { return "", err } if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) { - return filepath.Base(path), nil + return "", nil } return rel, nil } diff --git a/utils/paths_test.go b/utils/paths_test.go index 95e568977..4377a63dc 100644 --- a/utils/paths_test.go +++ b/utils/paths_test.go @@ -20,11 +20,11 @@ func TestIsPathExcludedFromRoot(t *testing.T) { expected bool }{ { - name: "Project folder name matches", + name: "Project folder name does not exclude it", root: filepath.Join(string(filepath.Separator), "ci", "my-test-project"), path: filepath.Join(string(filepath.Separator), "ci", "my-test-project"), exclusions: []string{"*test*"}, - expected: true, + expected: false, }, { name: "Subfolder inside the project matches", @@ -47,6 +47,13 @@ func TestIsPathExcludedFromRoot(t *testing.T) { exclusions: []string{"*test*"}, expected: false, }, + { + name: "Path outside the project is kept when its own name matches", + root: root, + path: filepath.Join(string(filepath.Separator), "ci", "my-test-sibling"), + exclusions: []string{"*test*"}, + expected: false, + }, { name: "Ancestor of the project does not exclude it", root: ancestorRoot,