From e7fc29022dafcab11f5a1889f482b0796b73a6d4 Mon Sep 17 00:00:00 2001 From: attiasas Date: Wed, 28 Oct 2026 12:51:26 +0200 Subject: [PATCH 1/7] XRAY-159762 - Excludes-xspecter Co-authored-by: Cursor --- commands/audit/audit.go | 3 ++ commands/audit/audit_test.go | 51 +++++++++++++++++++++ sca/bom/xrayplugin/xraylibbom.go | 25 +++++++++- sca/bom/xrayplugin/xraylibbom_test.go | 66 +++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 sca/bom/xrayplugin/xraylibbom_test.go diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 5048b6aba..67357cb3f 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -637,6 +637,9 @@ func createScanTargetsFromConfigs(cmdResults *results.SecurityCommandResults, pa return } log.Debug("Creating scan targets from config profile:", configProfile.ProfileName) + if cliExclusions := params.Exclusions(); len(cliExclusions) > 0 && !utils.ElementsEqual(cliExclusions, utils.DefaultScaExcludePatterns) { + log.Debug(fmt.Sprintf("Config profile exclusions replace --exclusions; discarding CLI patterns: %s", strings.Join(cliExclusions, ";"))) + } for _, module := range configProfile.Modules { moduleRoot := rootDir if module.PathFromRoot != "" && module.PathFromRoot != "." { diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 90d9e8228..4557acb74 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -1,6 +1,7 @@ package audit import ( + "bytes" "errors" "fmt" "net/http" @@ -38,6 +39,7 @@ import ( coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/jfrog/jfrog-client-go/utils/log" "github.com/jfrog/jfrog-client-go/xray" xrayServices "github.com/jfrog/jfrog-client-go/xray/services" xrayApi "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -582,6 +584,55 @@ func TestDetectScanTargetsNewFlowKeepsProjectUnderMatchingAncestor(t *testing.T) assert.Equal(t, resolvedProjectDir, cmdRes.Targets[0].Target) } +func TestNewFlowProfileDiscardsCliExclusions(t *testing.T) { + baseDir, cleanUp := createTestDir(t) + defer cleanUp() + projectDir := filepath.Join(baseDir, "project") + assert.NoError(t, os.MkdirAll(projectDir, 0o755)) + defer securityTestUtils.ChangeWDWithCallback(t, projectDir)() + + profile := &services.ConfigProfile{ + ProfileName: "profile", + Modules: []services.Module{{ + PathFromRoot: ".", + ExcludePatterns: []string{"vendor"}, + }}, + } + tests := []struct { + name string + exclusions []string + wantLog bool + }{ + {name: "custom flag is logged as discarded", exclusions: []string{"tools/teleport/examples/*"}, wantLog: true}, + {name: "default flag is not logged", exclusions: utils.DefaultScaExcludePatterns, wantLog: false}, + {name: "empty flag is not logged", exclusions: nil, wantLog: false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var buf bytes.Buffer + origLogger := log.Logger + log.SetLogger(log.NewLogger(log.DEBUG, &buf)) + defer log.SetLogger(origLogger) + + cmdRes := results.NewCommandResults(utils.SourceCode).SetEntitledForJas(true) + params := NewAuditParams() + params.SetBomGenerator(xrayplugin.NewXrayLibBomGenerator()) + params.SetConfigProfile(profile) + params.SetExclusions(tt.exclusions) + detectScanTargets(cmdRes, params) + + require.Len(t, cmdRes.Targets, 1) + assert.Equal(t, []string{"vendor"}, cmdRes.Targets[0].Exclude) + if tt.wantLog { + assert.Contains(t, buf.String(), "tools/teleport/examples/*") + assert.Contains(t, buf.String(), "discard") + } else { + assert.NotContains(t, buf.String(), "discard") + } + }) + } +} + func TestDetectScanTargetsSkipsCliExcludedCwdSingleTarget(t *testing.T) { baseDir, cleanUp := createTestDir(t) defer cleanUp() diff --git a/sca/bom/xrayplugin/xraylibbom.go b/sca/bom/xrayplugin/xraylibbom.go index 908ef10af..5cec469ca 100644 --- a/sca/bom/xrayplugin/xraylibbom.go +++ b/sca/bom/xrayplugin/xraylibbom.go @@ -3,6 +3,7 @@ package xrayplugin import ( "fmt" "os/exec" + "strings" "github.com/CycloneDX/cyclonedx-go" "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -133,7 +134,7 @@ func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target re BomRef: cdxutils.GetFileRef(target.Target), Type: string(cyclonedx.ComponentTypeFile), Name: target.Target, - IgnorePatterns: target.GetCentralConfigExclusions(utils.ScaScan), + IgnorePatterns: ignorePatternsForTarget(target), IncludeDirs: target.Include, Ecosystems: sbg.specificTechs, } @@ -143,6 +144,28 @@ func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target re return scanner.Scan(target.Target, scanConfig) } +// ignorePatternsForTarget is the ignore list sent to the Xray-Lib plugin. +// A config profile replaces the CLI flag. With no profile, the flag is used, and an empty flag falls back to the default list. +func ignorePatternsForTarget(target results.ScanTarget) []string { + var patterns []string + if len(target.CentralConfigModules) > 0 { + patterns = target.GetCentralConfigExclusions(utils.ScaScan) + } else if len(target.Exclude) > 0 { + patterns = target.Exclude + } else { + patterns = utils.DefaultScaExcludePatterns + } + normalized := make([]string, len(patterns)) + for i, pattern := range patterns { + pattern = strings.ReplaceAll(pattern, `\`, "/") + for strings.HasPrefix(pattern, "./") { + pattern = strings.TrimPrefix(pattern, "./") + } + normalized[i] = pattern + } + return normalized +} + func (sbg *XrayLibBomGenerator) getPluginEnvVars() utils.EnvironmentVariables { envVars := utils.EnvironmentVariables{} if sbg.snippetDetection { diff --git a/sca/bom/xrayplugin/xraylibbom_test.go b/sca/bom/xrayplugin/xraylibbom_test.go new file mode 100644 index 000000000..dde6b6816 --- /dev/null +++ b/sca/bom/xrayplugin/xraylibbom_test.go @@ -0,0 +1,66 @@ +package xrayplugin + +import ( + "testing" + + "github.com/jfrog/jfrog-cli-security/utils" + "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-client-go/xsc/services" + "github.com/stretchr/testify/assert" +) + +func TestIgnorePatternsForTarget(t *testing.T) { + profileModule := services.Module{ + ExcludePatterns: []string{`.\vendor`, "./generated"}, + ScanConfig: services.ScanConfig{ + ScaScannerConfig: services.ScaScannerConfig{ExcludePatterns: []string{"*dist*"}}, + }, + } + tests := []struct { + name string + target results.ScanTarget + expected []string + }{ + { + name: "no profile uses the flag patterns", + target: results.ScanTarget{Exclude: []string{"tools/teleport/examples/*"}}, + expected: []string{"tools/teleport/examples/*"}, + }, + { + name: "no profile and no flag uses the defaults", + target: results.ScanTarget{}, + expected: utils.DefaultScaExcludePatterns, + }, + { + name: "leading dot slash and backslashes are normalized", + target: results.ScanTarget{Exclude: []string{`.\tools\teleport`, "./examples/*"}}, + expected: []string{"tools/teleport", "examples/*"}, + }, + { + name: "flag style is not rewritten to doublestar", + target: results.ScanTarget{Exclude: []string{"*node_modules*"}}, + expected: []string{"*node_modules*"}, + }, + { + name: "profile patterns replace the flag", + target: results.ScanTarget{ + Exclude: []string{"tools/teleport/examples/*"}, + CentralConfigModules: []services.Module{profileModule}, + }, + expected: []string{"vendor", "generated", "*dist*"}, + }, + { + name: "empty profile exclusions are not replaced by the flag or the defaults", + target: results.ScanTarget{ + Exclude: utils.DefaultScaExcludePatterns, + CentralConfigModules: []services.Module{{}}, + }, + expected: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.ElementsMatch(t, tt.expected, ignorePatternsForTarget(tt.target)) + }) + } +} From 94758951334049c25c3e395cb6fc8839eb3393db Mon Sep 17 00:00:00 2001 From: attiasas Date: Wed, 28 Oct 2026 14:04:44 +0200 Subject: [PATCH 2/7] XRAY-159762 - Excludes-xspecter Co-authored-by: Cursor --- sca/bom/xrayplugin/xraylibbom.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sca/bom/xrayplugin/xraylibbom.go b/sca/bom/xrayplugin/xraylibbom.go index 5cec469ca..5687e5465 100644 --- a/sca/bom/xrayplugin/xraylibbom.go +++ b/sca/bom/xrayplugin/xraylibbom.go @@ -148,11 +148,12 @@ func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target re // A config profile replaces the CLI flag. With no profile, the flag is used, and an empty flag falls back to the default list. func ignorePatternsForTarget(target results.ScanTarget) []string { var patterns []string - if len(target.CentralConfigModules) > 0 { + switch { + case len(target.CentralConfigModules) > 0: patterns = target.GetCentralConfigExclusions(utils.ScaScan) - } else if len(target.Exclude) > 0 { + case len(target.Exclude) > 0: patterns = target.Exclude - } else { + default: patterns = utils.DefaultScaExcludePatterns } normalized := make([]string, len(patterns)) From e1357d91e4bea9df734823ad5a763756b11db3fc Mon Sep 17 00:00:00 2001 From: attiasas Date: Sun, 1 Nov 2026 12:09:30 +0200 Subject: [PATCH 3/7] CR changes --- sca/bom/xrayplugin/xraylibbom.go | 36 +++++++++++++++++++--- sca/bom/xrayplugin/xraylibbom_test.go | 44 +++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/sca/bom/xrayplugin/xraylibbom.go b/sca/bom/xrayplugin/xraylibbom.go index 5687e5465..e90714db9 100644 --- a/sca/bom/xrayplugin/xraylibbom.go +++ b/sca/bom/xrayplugin/xraylibbom.go @@ -145,7 +145,8 @@ func (sbg *XrayLibBomGenerator) executeScanner(scanner plugin.Scanner, target re } // ignorePatternsForTarget is the ignore list sent to the Xray-Lib plugin. -// A config profile replaces the CLI flag. With no profile, the flag is used, and an empty flag falls back to the default list. +// A config profile replaces the CLI flag. With no profile, the flag is used. +// An omitted flag falls back to the default list. An explicit empty flag stays empty, so a scan can run without exclusions. func ignorePatternsForTarget(target results.ScanTarget) []string { var patterns []string switch { @@ -156,17 +157,42 @@ func ignorePatternsForTarget(target results.ScanTarget) []string { default: patterns = utils.DefaultScaExcludePatterns } - normalized := make([]string, len(patterns)) - for i, pattern := range patterns { - pattern = strings.ReplaceAll(pattern, `\`, "/") + return normalizeIgnorePatterns(patterns) +} + +func normalizeIgnorePatterns(patterns []string) []string { + normalized := make([]string, 0, len(patterns)) + for _, pattern := range patterns { + pattern = normalizeIgnorePatternSeparators(pattern) for strings.HasPrefix(pattern, "./") { pattern = strings.TrimPrefix(pattern, "./") } - normalized[i] = pattern + if pattern != "" { + normalized = append(normalized, pattern) + } } return normalized } +func normalizeIgnorePatternSeparators(pattern string) string { + var normalized strings.Builder + normalized.Grow(len(pattern)) + for i := 0; i < len(pattern); i++ { + if pattern[i] == '\\' && i+1 < len(pattern) && (pattern[i+1] == '*' || pattern[i+1] == '?') { + normalized.WriteByte('\\') + i++ + normalized.WriteByte(pattern[i]) + continue + } + if pattern[i] == '\\' { + normalized.WriteByte('/') + continue + } + normalized.WriteByte(pattern[i]) + } + return normalized.String() +} + func (sbg *XrayLibBomGenerator) getPluginEnvVars() utils.EnvironmentVariables { envVars := utils.EnvironmentVariables{} if sbg.snippetDetection { diff --git a/sca/bom/xrayplugin/xraylibbom_test.go b/sca/bom/xrayplugin/xraylibbom_test.go index dde6b6816..0add9d905 100644 --- a/sca/bom/xrayplugin/xraylibbom_test.go +++ b/sca/bom/xrayplugin/xraylibbom_test.go @@ -3,12 +3,23 @@ package xrayplugin import ( "testing" + "github.com/CycloneDX/cyclonedx-go" + "github.com/jfrog/jfrog-cli-security/sca/bom/xrayplugin/plugin" "github.com/jfrog/jfrog-cli-security/utils" "github.com/jfrog/jfrog-cli-security/utils/results" "github.com/jfrog/jfrog-client-go/xsc/services" "github.com/stretchr/testify/assert" ) +type scannerStub struct { + config plugin.Config +} + +func (s *scannerStub) Scan(_ string, config plugin.Config) (*cyclonedx.BOM, error) { + s.config = config + return nil, nil +} + func TestIgnorePatternsForTarget(t *testing.T) { profileModule := services.Module{ ExcludePatterns: []string{`.\vendor`, "./generated"}, @@ -36,6 +47,26 @@ func TestIgnorePatternsForTarget(t *testing.T) { target: results.ScanTarget{Exclude: []string{`.\tools\teleport`, "./examples/*"}}, expected: []string{"tools/teleport", "examples/*"}, }, + { + name: "explicit empty flag stays empty", + target: results.ScanTarget{Exclude: []string{""}}, + expected: []string{}, + }, + { + name: "dot slash alone stays empty", + target: results.ScanTarget{Exclude: []string{"./"}}, + expected: []string{}, + }, + { + name: "escaped glob characters are preserved", + target: results.ScanTarget{Exclude: []string{`directory\*`, `directory\?`}}, + expected: []string{`directory\*`, `directory\?`}, + }, + { + name: "globstar and trailing slash patterns are preserved", + target: results.ScanTarget{Exclude: []string{"**/directory", "directory/"}}, + expected: []string{"**/directory", "directory/"}, + }, { name: "flag style is not rewritten to doublestar", target: results.ScanTarget{Exclude: []string{"*node_modules*"}}, @@ -64,3 +95,16 @@ func TestIgnorePatternsForTarget(t *testing.T) { }) } } + +func TestExecuteScannerPassesNormalizedIgnorePatterns(t *testing.T) { + scanner := &scannerStub{} + target := results.ScanTarget{ + Target: "test-project", + Exclude: []string{`.\vendor`, "./generated"}, + } + + _, err := NewXrayLibBomGenerator().executeScanner(scanner, target) + + assert.NoError(t, err) + assert.Equal(t, []string{"vendor", "generated"}, scanner.config.IgnorePatterns) +} From 2d30a8606625bfb91a535ec2eebfda8f2104b447 Mon Sep 17 00:00:00 2001 From: attiasas Date: Sun, 1 Nov 2026 15:28:35 +0200 Subject: [PATCH 4/7] XRAY-163145 - Dont-exclude-root-name Co-authored-by: Cursor --- commands/audit/audit_test.go | 50 +++++++++++++++++++++++++++--------- jas/common_test.go | 2 +- utils/paths.go | 12 ++++++--- utils/paths_test.go | 11 ++++++-- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 4557acb74..39b58da0f 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,45 @@ 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) - assert.Empty(t, cmdRes.Targets) + require.NotEmpty(t, cmdRes.Targets) + assert.Equal(t, projectDir, cmdRes.Targets[0].Target) } func TestDetectScanTargetsNewFlowCliExcludedCwdWithNonExcludedInclude(t *testing.T) { 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, From a62c4bbeb275d4073a7693ab092d859db21ffbfa Mon Sep 17 00:00:00 2001 From: attiasas Date: Mon, 2 Nov 2026 15:22:47 +0200 Subject: [PATCH 5/7] XRAY-163145 - Dont-exclude-root-name Co-authored-by: Cursor --- commands/audit/audit.go | 34 ++++----- commands/audit/audit_test.go | 143 +++++++++++++++++++++++++++++++++++ jas/common.go | 5 -- 3 files changed, 159 insertions(+), 23 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 67357cb3f..337c0a8a5 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -630,7 +630,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 +645,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,7 +660,8 @@ 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 { @@ -669,13 +674,17 @@ func createScanTarget(root string, exclude []string, includes ...string) *result if !filepath.IsAbs(includePattern) { includePath = filepath.Join(root, includePattern) } - if utils.IsPathExcludedFromRoot(root, includePath, exclude) { + 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(includePattern, false) { + log.Warn(fmt.Sprintf("The working directory '%s' doesn't exist. Skipping...", includePattern)) + 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) @@ -686,13 +695,6 @@ func createScanTarget(root string, exclude []string, includes ...string) *result 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))) - } return &results.ScanTarget{Target: root, Include: include, Exclude: exclude} } @@ -811,10 +813,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 +833,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 +842,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 +857,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 39b58da0f..3382c1430 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -695,6 +695,149 @@ func TestDetectScanTargetsKeepsExplicitWorkingDirNamedLikeExclusion(t *testing.T 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 TestDetectScanTargetsNewFlowCliExcludedCwdWithNonExcludedInclude(t *testing.T) { baseDir, cleanUp := createTestDir(t) defer cleanUp() 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 } From bf3a2c3f75494d38ce4436fb59ea56aa94bb0cad Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 3 Nov 2026 09:31:48 +0200 Subject: [PATCH 6/7] CR changes --- commands/audit/audit.go | 22 ++++++++++++-- commands/audit/audit_test.go | 57 ++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index 337c0a8a5..fe6267cad 100644 --- a/commands/audit/audit.go +++ b/commands/audit/audit.go @@ -681,23 +681,41 @@ func createScanTarget(root string, exclude []string, keepNamedDirectories bool, dirs.Add(includePath) continue } - if keepNamedDirectories && !fileutils.IsPathExists(includePattern, false) { + if keepNamedDirectories && !isWildcardInclude(includePattern) && !fileutils.IsPathExists(includePattern, false) { log.Warn(fmt.Sprintf("The working directory '%s' doesn't exist. Skipping...", includePattern)) 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...) } + // 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) { diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 3382c1430..517c9c59a 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -838,6 +838,63 @@ func TestDetectScanTargetsNewFlowWarnsWhenWorkingDirIsMissing(t *testing.T) { 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 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) { baseDir, cleanUp := createTestDir(t) defer cleanUp() From 88d1fa7887a872fbe77b3ad33a25ada86dc4f287 Mon Sep 17 00:00:00 2001 From: attiasas Date: Tue, 3 Nov 2026 14:58:11 +0200 Subject: [PATCH 7/7] XRAY-163145 - Dont-exclude-root-name Co-authored-by: Cursor --- commands/audit/audit.go | 69 +++++++++++++++++++++------ commands/audit/audit_test.go | 90 ++++++++++++++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 14 deletions(-) diff --git a/commands/audit/audit.go b/commands/audit/audit.go index fe6267cad..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 @@ -665,25 +702,29 @@ func createScanTarget(root string, exclude []string, keepNamedDirectories bool, 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 !keepNamedDirectories && 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 - } - if keepNamedDirectories && !isWildcardInclude(includePattern) && !fileutils.IsPathExists(includePattern, false) { - log.Warn(fmt.Sprintf("The working directory '%s' doesn't exist. Skipping...", includePattern)) - 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)) diff --git a/commands/audit/audit_test.go b/commands/audit/audit_test.go index 517c9c59a..1794cccce 100644 --- a/commands/audit/audit_test.go +++ b/commands/audit/audit_test.go @@ -868,6 +868,96 @@ func TestDetectScanTargetsNewFlowExpandsWildcardWorkingDir(t *testing.T) { 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()