From 87a0690108207072a62fa7cdfdef2c4351ef0e42 Mon Sep 17 00:00:00 2001 From: Jordan Hury Date: Thu, 13 Aug 2026 18:30:04 +0300 Subject: [PATCH 1/2] XRAY-157127 - Isolate-polyglot-resolvers Co-authored-by: Cursor --- sca/bom/buildinfo/buildinfobom.go | 10 ++- sca/bom/buildinfo/buildinfobom_test.go | 100 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/sca/bom/buildinfo/buildinfobom.go b/sca/bom/buildinfo/buildinfobom.go index bc0c7be4f..2cff5f32c 100644 --- a/sca/bom/buildinfo/buildinfobom.go +++ b/sca/bom/buildinfo/buildinfobom.go @@ -157,12 +157,12 @@ func (b *BuildInfoBomGenerator) buildDependencyTree(scan results.ScanTarget) (*D hasAnyTree := false for _, tech := range techs { log.Debug(fmt.Sprintf("Generating '%s' dependency tree for '%s'...", tech.ToFormal(), scan.Target)) - serverDetails, err := SetResolutionRepoInParamsIfExists(&b.params, tech) + techParams, serverDetails, err := b.resolveTechParams(tech) if err != nil { buildErr = errors.Join(buildErr, fmt.Errorf("failed to set resolution repo in params: %w", err)) continue } - treeResult, err := GetTechDependencyTree(b.params, serverDetails, tech) + treeResult, err := GetTechDependencyTree(techParams, serverDetails, tech) if err != nil { buildErr = errors.Join(buildErr, fmt.Errorf("failed while building '%s' dependency tree: %w", tech, err)) continue @@ -183,6 +183,12 @@ func (b *BuildInfoBomGenerator) buildDependencyTree(scan results.ScanTarget) (*D return merged, buildErr } +func (b *BuildInfoBomGenerator) resolveTechParams(tech techutils.Technology) (techParams technologies.BuildInfoBomGeneratorParams, serverDetails *config.ServerDetails, err error) { + techParams = b.params + serverDetails, err = SetResolutionRepoInParamsIfExists(&techParams, tech) + return +} + func mergeResults(existing, additional *DependencyTreeResult) *DependencyTreeResult { if additional == nil { return existing diff --git a/sca/bom/buildinfo/buildinfobom_test.go b/sca/bom/buildinfo/buildinfobom_test.go index 5c64592af..08b74fb1e 100644 --- a/sca/bom/buildinfo/buildinfobom_test.go +++ b/sca/bom/buildinfo/buildinfobom_test.go @@ -2,16 +2,116 @@ package buildinfo import ( "fmt" + "os" + "path/filepath" "testing" "github.com/CycloneDX/cyclonedx-go" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + coreutils "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies" "github.com/jfrog/jfrog-cli-security/utils/results" + "github.com/jfrog/jfrog-cli-security/utils/techutils" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func isolateResolverConfig(t *testing.T) string { + t.Helper() + t.Setenv(coreutils.HomeDir, t.TempDir()) + dummyHome := t.TempDir() + t.Setenv("HOME", dummyHome) + t.Setenv("USERPROFILE", dummyHome) + require.NoError(t, config.SaveServersConf([]*config.ServerDetails{{ + ServerId: "test", + Url: "http://localhost/", + ArtifactoryUrl: "http://localhost/artifactory/", + }})) + projectRoot := t.TempDir() + originalCwd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(projectRoot)) + t.Cleanup(func() { require.NoError(t, os.Chdir(originalCwd)) }) + return projectRoot +} + +func TestResolveTechParamsIsolation(t *testing.T) { + projectRoot := isolateResolverConfig(t) + projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") + require.NoError(t, os.MkdirAll(projectsDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "go.yaml"), + []byte("version: 1\ntype: go\nresolver:\n serverId: test\n repo: go-vir\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "npm.yaml"), + []byte("version: 1\ntype: npm\nresolver:\n serverId: test\n repo: npm-remote\n"), 0o644)) + + generator := NewBuildInfoBomGenerator() + + goParams, _, err := generator.resolveTechParams(techutils.Go) + require.NoError(t, err) + assert.Equal(t, "go-vir", goParams.DependenciesRepository, "Go must resolve against its own go.yaml") + + npmParams, _, err := generator.resolveTechParams(techutils.Npm) + require.NoError(t, err) + assert.Equal(t, "npm-remote", npmParams.DependenciesRepository, + "npm must resolve against npm.yaml even after Go resolved first (no shared-state leak)") + + assert.Empty(t, generator.params.DependenciesRepository, + "resolving a technology must not mutate the generator's shared params") +} + +func TestResolveTechParamsSkipsConfigLookup(t *testing.T) { + projectRoot := isolateResolverConfig(t) + projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") + require.NoError(t, os.MkdirAll(projectsDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "npm.yaml"), + []byte("version: 1\ntype: npm\nresolver:\n serverId: test\n repo: npm-remote\n"), 0o644)) + + testCases := []struct { + name string + params technologies.BuildInfoBomGeneratorParams + expected string + }{ + { + name: "explicit deps repo", + params: technologies.BuildInfoBomGeneratorParams{DependenciesRepository: "cli-deps-repo"}, + expected: "cli-deps-repo", + }, + { + name: "ignore config file", + params: technologies.BuildInfoBomGeneratorParams{IgnoreConfigFile: true}, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + generator := NewBuildInfoBomGenerator() + generator.params = testCase.params + + npmParams, _, err := generator.resolveTechParams(techutils.Npm) + require.NoError(t, err) + assert.Equal(t, testCase.expected, npmParams.DependenciesRepository) + }) + } +} + +func TestBuildDependencyTreeDoesNotMutateGeneratorParams(t *testing.T) { + projectRoot := isolateResolverConfig(t) + projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") + require.NoError(t, os.MkdirAll(projectsDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "go.yaml"), + []byte("version: 1\ntype: go\nresolver:\n serverId: test\n repo: go-vir\n"), 0o644)) + + generator := NewBuildInfoBomGenerator() + _, _ = generator.buildDependencyTree(results.ScanTarget{ + Target: projectRoot, + Technologies: []techutils.Technology{techutils.Go}, + }) + + assert.Empty(t, generator.params.DependenciesRepository) +} + func TestMergeResults(t *testing.T) { nodeA := &xrayUtils.GraphNode{Id: "npm://a:1"} nodeB := &xrayUtils.GraphNode{Id: "gav://b:2"} From 5ac26e2cefcd12a68b37d5f3be449918a02fc97a Mon Sep 17 00:00:00 2001 From: Jordan Hury Date: Sun, 16 Aug 2026 15:56:31 +0300 Subject: [PATCH 2/2] XRAY-157127 - Deep-copy-server-details-and-trim-tests Co-authored-by: Cursor --- sca/bom/buildinfo/buildinfobom.go | 4 + sca/bom/buildinfo/buildinfobom_test.go | 102 +++++++++---------------- 2 files changed, 40 insertions(+), 66 deletions(-) diff --git a/sca/bom/buildinfo/buildinfobom.go b/sca/bom/buildinfo/buildinfobom.go index 2cff5f32c..3c5865b09 100644 --- a/sca/bom/buildinfo/buildinfobom.go +++ b/sca/bom/buildinfo/buildinfobom.go @@ -185,6 +185,10 @@ func (b *BuildInfoBomGenerator) buildDependencyTree(scan results.ScanTarget) (*D func (b *BuildInfoBomGenerator) resolveTechParams(tech techutils.Technology) (techParams technologies.BuildInfoBomGeneratorParams, serverDetails *config.ServerDetails, err error) { techParams = b.params + if b.params.ServerDetails != nil { + copied := *b.params.ServerDetails + techParams.ServerDetails = &copied + } serverDetails, err = SetResolutionRepoInParamsIfExists(&techParams, tech) return } diff --git a/sca/bom/buildinfo/buildinfobom_test.go b/sca/bom/buildinfo/buildinfobom_test.go index 08b74fb1e..75b45fa16 100644 --- a/sca/bom/buildinfo/buildinfobom_test.go +++ b/sca/bom/buildinfo/buildinfobom_test.go @@ -37,79 +37,49 @@ func isolateResolverConfig(t *testing.T) string { return projectRoot } -func TestResolveTechParamsIsolation(t *testing.T) { - projectRoot := isolateResolverConfig(t) - projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") - require.NoError(t, os.MkdirAll(projectsDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "go.yaml"), - []byte("version: 1\ntype: go\nresolver:\n serverId: test\n repo: go-vir\n"), 0o644)) - require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "npm.yaml"), - []byte("version: 1\ntype: npm\nresolver:\n serverId: test\n repo: npm-remote\n"), 0o644)) +func TestResolveTechParams(t *testing.T) { + t.Run("isolates DependenciesRepository per technology", func(t *testing.T) { + projectRoot := isolateResolverConfig(t) + projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") + require.NoError(t, os.MkdirAll(projectsDir, 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "go.yaml"), + []byte("version: 1\ntype: go\nresolver:\n serverId: test\n repo: go-vir\n"), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "npm.yaml"), + []byte("version: 1\ntype: npm\nresolver:\n serverId: test\n repo: npm-remote\n"), 0o644)) - generator := NewBuildInfoBomGenerator() + generator := NewBuildInfoBomGenerator() - goParams, _, err := generator.resolveTechParams(techutils.Go) - require.NoError(t, err) - assert.Equal(t, "go-vir", goParams.DependenciesRepository, "Go must resolve against its own go.yaml") - - npmParams, _, err := generator.resolveTechParams(techutils.Npm) - require.NoError(t, err) - assert.Equal(t, "npm-remote", npmParams.DependenciesRepository, - "npm must resolve against npm.yaml even after Go resolved first (no shared-state leak)") - - assert.Empty(t, generator.params.DependenciesRepository, - "resolving a technology must not mutate the generator's shared params") -} - -func TestResolveTechParamsSkipsConfigLookup(t *testing.T) { - projectRoot := isolateResolverConfig(t) - projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") - require.NoError(t, os.MkdirAll(projectsDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "npm.yaml"), - []byte("version: 1\ntype: npm\nresolver:\n serverId: test\n repo: npm-remote\n"), 0o644)) - - testCases := []struct { - name string - params technologies.BuildInfoBomGeneratorParams - expected string - }{ - { - name: "explicit deps repo", - params: technologies.BuildInfoBomGeneratorParams{DependenciesRepository: "cli-deps-repo"}, - expected: "cli-deps-repo", - }, - { - name: "ignore config file", - params: technologies.BuildInfoBomGeneratorParams{IgnoreConfigFile: true}, - }, - } + goParams, _, err := generator.resolveTechParams(techutils.Go) + require.NoError(t, err) + assert.Equal(t, "go-vir", goParams.DependenciesRepository) - for _, testCase := range testCases { - t.Run(testCase.name, func(t *testing.T) { - generator := NewBuildInfoBomGenerator() - generator.params = testCase.params + npmParams, _, err := generator.resolveTechParams(techutils.Npm) + require.NoError(t, err) + assert.Equal(t, "npm-remote", npmParams.DependenciesRepository) + assert.Empty(t, generator.params.DependenciesRepository) + }) - npmParams, _, err := generator.resolveTechParams(techutils.Npm) - require.NoError(t, err) - assert.Equal(t, testCase.expected, npmParams.DependenciesRepository) - }) - } -} + t.Run("isolates ServerDetails from in-place mutation", func(t *testing.T) { + shared := &config.ServerDetails{ + ServerId: "test", + Url: "http://localhost/", + ArtifactoryUrl: "http://localhost/artifactory/", + } + generator := NewBuildInfoBomGenerator() + generator.params = technologies.BuildInfoBomGeneratorParams{ + ServerDetails: shared, + DependenciesRepository: "cli-deps-repo", + } -func TestBuildDependencyTreeDoesNotMutateGeneratorParams(t *testing.T) { - projectRoot := isolateResolverConfig(t) - projectsDir := filepath.Join(projectRoot, ".jfrog", "projects") - require.NoError(t, os.MkdirAll(projectsDir, 0o755)) - require.NoError(t, os.WriteFile(filepath.Join(projectsDir, "go.yaml"), - []byte("version: 1\ntype: go\nresolver:\n serverId: test\n repo: go-vir\n"), 0o644)) + techParams, _, err := generator.resolveTechParams(techutils.Nuget) + require.NoError(t, err) + require.NotNil(t, techParams.ServerDetails) + assert.NotSame(t, shared, techParams.ServerDetails) - generator := NewBuildInfoBomGenerator() - _, _ = generator.buildDependencyTree(results.ScanTarget{ - Target: projectRoot, - Technologies: []techutils.Technology{techutils.Go}, + techParams.ServerDetails.ArtifactoryUrl += "api/curation/audit" + assert.Equal(t, "http://localhost/artifactory/", shared.ArtifactoryUrl) + assert.Equal(t, "http://localhost/artifactory/", generator.params.ServerDetails.ArtifactoryUrl) }) - - assert.Empty(t, generator.params.DependenciesRepository) } func TestMergeResults(t *testing.T) {