From 2bdeba0b13d1ac1868a3ab2c641674831cf0d52e Mon Sep 17 00:00:00 2001 From: Phavya Jayakumar Date: Fri, 28 Aug 2026 16:45:42 +0530 Subject: [PATCH] XRAY-156685 - Nuget native support --- commands/curation/curationaudit.go | 68 +++- commands/curation/curationaudit_test.go | 263 +++++++++++--- sca/bom/buildinfo/technologies/nuget/nuget.go | 188 +++++++++- .../technologies/nuget/nuget_test.go | 336 ++++++++++++++++++ .../buildinfo/technologies/pnpm/pnpm_test.go | 4 +- .../.jfrog/projects/nuget.yaml | 5 - 6 files changed, 786 insertions(+), 78 deletions(-) delete mode 100644 tests/testdata/projects/package-managers/dotnet/dotnet-curation/.jfrog/projects/nuget.yaml diff --git a/commands/curation/curationaudit.go b/commands/curation/curationaudit.go index 0053baa59..7dee9b614 100644 --- a/commands/curation/curationaudit.go +++ b/commands/curation/curationaudit.go @@ -47,6 +47,7 @@ import ( "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/huggingface" hfdiscovery "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/huggingface/discovery" npmtech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/npm" + nugettech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/nuget" pnpmtech "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/pnpm" "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies/python" "github.com/jfrog/jfrog-cli-security/utils" @@ -1053,7 +1054,7 @@ func (ca *CurationAuditCommand) GetAuth(tech techutils.Technology) (serverDetail func (ca *CurationAuditCommand) getBuildInfoParamsByTech(tech techutils.Technology) (technologies.BuildInfoBomGeneratorParams, error) { var serverDetails *config.ServerDetails var err error - if (tech == techutils.Pipenv || tech == techutils.Pip || tech == techutils.Poetry) && ca.PackageManagerConfig != nil { + if (tech == techutils.Pipenv || tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Nuget) && ca.PackageManagerConfig != nil { serverDetails, err = ca.PackageManagerConfig.ServerDetails() } else { serverDetails, err = ca.ServerDetails() @@ -1134,10 +1135,11 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map return err } } - // Resolve Pipenv/Pip/Poetry's native repo/server before getBuildInfoParamsByTech so install and the - // later probes share an endpoint. Other techs resolve later via SetResolutionRepoInParamsIfExists - // and must not be forced through SetRepo this early (they tolerate having no config file yet). - if (tech == techutils.Pipenv || tech == techutils.Pip || tech == techutils.Poetry) && ca.PackageManagerConfig == nil { + // Resolve Pipenv/Pip/Poetry/NuGet's native repo/server before getBuildInfoParamsByTech so + // install and the later probes share an endpoint. Other techs resolve later via + // SetResolutionRepoInParamsIfExists and must not be forced through SetRepo this early + // (they tolerate having no config file yet). + if (tech == techutils.Pipenv || tech == techutils.Pip || tech == techutils.Poetry || tech == techutils.Nuget) && ca.PackageManagerConfig == nil { if err := ca.SetRepo(tech); err != nil { return err } @@ -1152,9 +1154,9 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map if err != nil { return errorutils.CheckErrorf("failed to get build info params for %s: %v", tech.String(), err) } - // When --run-native is set for npm, or for pnpm (always .npmrc-based), the Artifactory - // details are already populated from .npmrc. Skip the yaml config file lookup. - if (ca.RunNative() && tech == techutils.Npm) || tech == techutils.Pnpm { + // Artifactory details are already populated for --run-native npm, and always for + // pnpm/NuGet (native-only, no yaml config). Skip the yaml config file lookup for these. + if (ca.RunNative() && tech == techutils.Npm) || tech == techutils.Pnpm || tech == techutils.Nuget { params.IgnoreConfigFile = true } // uv has no jf uv-config yaml; skip config file lookup and use server details @@ -1212,6 +1214,11 @@ func (ca *CurationAuditCommand) auditTree(tech techutils.Technology, results map if ca.RunNative() && tech == techutils.Pipenv { ca.pendingWarnings = append(ca.pendingWarnings, "--run-native has no effect for pipenv; the repository is resolved automatically from ~/.pip/pip.conf, or the Artifactory [[source]] entry in your Pipfile") } + // NuGet always resolves natively from the NuGet/.NET CLI's configured sources, + // so --run-native is a no-op here too. + if ca.RunNative() && tech == techutils.Nuget { + ca.pendingWarnings = append(ca.pendingWarnings, "--run-native has no effect for NuGet; the repository is resolved automatically by matching the configured Artifactory server against the sources listed by 'dotnet nuget list source' or 'nuget sources List'") + } // For yarn with no yarn.yaml, fall back to npm.yaml — npm and yarn share the same Artifactory npm API. resolverTech := resolveResolverTechForCuration(tech) serverDetails, err := buildinfo.SetResolutionRepoInParamsIfExists(¶ms, resolverTech) @@ -1634,6 +1641,12 @@ func (ca *CurationAuditCommand) SetRepo(tech techutils.Technology) error { return ca.setRepoFromNpmrc() } + // NuGet always resolves natively from the NuGet/.NET CLI's configured sources, + // regardless of --run-native (no yaml config equivalent exists). + if tech == techutils.Nuget { + return ca.setRepoFromNuGetSource() + } + // Pnpm always reads from .npmrc — there is no 'jf pnpm-config' command. // pnpm shares the npm registry protocol, so the same .npmrc key/URL format applies. if tech == techutils.Pnpm { @@ -1838,10 +1851,9 @@ func (ca *CurationAuditCommand) setRepoFromPyproject() error { // validateRunNativeForTech rejects --run-native for techs that don't implement // native-config semantics. npm uses it to read Artifactory details from .npmrc; -// pnpm/yarn/uv/pip/pipenv/poetry accept it as a no-op (a warning is emitted in auditTree) -// since their resolution is already automatic and has nothing for the flag to -// switch between. Extend the allow-list below when a new tech adds the -// matching native-config flow. +// pnpm/yarn/uv/pip/pipenv/poetry/NuGet accept it as a no-op (auditTree emits a warning) +// since their resolution is already automatic. Extend the allow-list below when a new +// tech adds a matching native-config flow. func validateRunNativeForTech(tech techutils.Technology, runNative bool) error { if !runNative { return nil @@ -1850,6 +1862,9 @@ func validateRunNativeForTech(tech techutils.Technology, runNative bool) error { // both 'jf ' and 'jf ca'. supported := map[techutils.Technology]struct{}{ techutils.Npm: {}, + // NuGet always resolves natively from the NuGet/.NET CLI's configured + // sources, so --run-native is a redundant no-op. + techutils.Nuget: {}, // pnpm always resolves from .npmrc, so --run-native is a redundant no-op // rather than an error (a warning is emitted in auditTree). techutils.Pnpm: {}, @@ -1913,6 +1928,35 @@ func (ca *CurationAuditCommand) setRepoFromNpmrc() error { return nil } +// setRepoFromNuGetSource finds the native NuGet/.NET CLI source whose host matches the +// 'jf c' server, and builds PackageManagerConfig from its Artifactory URL and repo name. +// Credentials always come from the 'jf c' server, never from the native config. +func (ca *CurationAuditCommand) setRepoFromNuGetSource() error { + serverDetails, err := ca.ServerDetails() + if err != nil { + return err + } + if serverDetails == nil || serverDetails.GetArtifactoryUrl() == "" { + return errorutils.CheckErrorf("curation-audit for NuGet requires a configured Artifactory server. Run 'jf c add' to configure a server") + } + + registryConfig, err := nugettech.GetNativeNuGetRegistryConfig(serverDetails) + if err != nil { + return fmt.Errorf("NuGet: %w", err) + } + + resolvedServerDetails := *serverDetails + resolvedServerDetails.ArtifactoryUrl = registryConfig.ArtifactoryUrl + + repoConfig := (&project.RepositoryConfig{}). + SetTargetRepo(registryConfig.RepoName). + SetServerDetails(&resolvedServerDetails) + ca.setPackageManagerConfig(repoConfig) + ca.SetDepsRepo(registryConfig.RepoName) + log.Info(fmt.Sprintf("NuGet: using native source %q (Artifactory URL %q, repository %q)", registryConfig.SourceName, registryConfig.ArtifactoryUrl, registryConfig.RepoName)) + return nil +} + // setRepoFromNpmrcForPnpm reads Artifactory connection details from the project's .npmrc // via the pnpm CLI. pnpm uses the same .npmrc format and registry protocol as npm, so the // URL parsing logic is identical. This is always called for pnpm — there is no 'jf pnpm-config'. diff --git a/commands/curation/curationaudit_test.go b/commands/curation/curationaudit_test.go index 776ed9b3a..97b712d69 100644 --- a/commands/curation/curationaudit_test.go +++ b/commands/curation/curationaudit_test.go @@ -11,6 +11,7 @@ import ( "os/exec" "path/filepath" "regexp" + "runtime" "sort" "strconv" "strings" @@ -668,14 +669,14 @@ func getTestCasesForDoCurationAudit() []testCase { pathToProject: filepath.Join("projects", "package-managers", "go", "curation-project"), createServerWithoutCreds: true, serveResources: map[string]string{ - "v1.5.2.mod": filepath.Join("resources", "quote-v1.5.2.mod"), - "v1.5.2.zip": filepath.Join("resources", "quote-v1.5.2.zip"), - "v1.5.2.info": filepath.Join("resources", "quote-v1.5.2.info"), - "v1.3.0.mod": filepath.Join("resources", "sampler-v1.3.0.mod"), - "v1.3.0.zip": filepath.Join("resources", "sampler-v1.3.0.zip"), - "v1.3.0.info": filepath.Join("resources", "sampler-v1.3.0.info"), - "v0.0.0-20170915032832-14c0d48ead0c.mod": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.mod"), - "v0.0.0-20170915032832-14c0d48ead0c.zip": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.zip"), + "v1.5.2.mod": filepath.Join("resources", "quote-v1.5.2.mod"), + "v1.5.2.zip": filepath.Join("resources", "quote-v1.5.2.zip"), + "v1.5.2.info": filepath.Join("resources", "quote-v1.5.2.info"), + "v1.3.0.mod": filepath.Join("resources", "sampler-v1.3.0.mod"), + "v1.3.0.zip": filepath.Join("resources", "sampler-v1.3.0.zip"), + "v1.3.0.info": filepath.Join("resources", "sampler-v1.3.0.info"), + "v0.0.0-20170915032832-14c0d48ead0c.mod": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.mod"), + "v0.0.0-20170915032832-14c0d48ead0c.zip": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.zip"), "v0.0.0-20170915032832-14c0d48ead0c.info": filepath.Join("resources", "text-v0.0.0-20170915032832-14c0d48ead0c.info"), }, requestToFail: map[string]bool{ @@ -1097,43 +1098,6 @@ func getTestCasesForDoCurationAudit() []testCase { "Cause: executor timeout after 2 attempts with 0 milliseconds wait intervals", "/api/npm/npms/lightweight/-/lightweight-0.1.0.tgz", "lightweight:0.1.0", http.StatusInternalServerError), }, - { - name: "dotnet tree", - tech: techutils.Dotnet, - pathToProject: filepath.Join("projects", "package-managers", "dotnet", "dotnet-curation"), - serveResources: map[string]string{ - "curated-nuget/index.json": filepath.Join("resources", "feed.json"), - "index.json": filepath.Join("resources", "index.json"), - "13.0.3": filepath.Join("resources", "newtonsoft.json.13.0.3.nupkg"), - }, - requestToFail: map[string]bool{ - "/api/nuget/v3/curated-nuget/registration-semver2/Download/newtonsoft.json/13.0.3": false, - }, - expectedResp: map[string]*CurationReport{ - "dotnet-curation": {packagesStatus: []*PackageStatus{ - { - Action: "blocked", - ParentName: "Newtonsoft.Json", - ParentVersion: "13.0.3", - BlockedPackageUrl: "/api/nuget/v3/curated-nuget/registration-semver2/Download/newtonsoft.json/13.0.3", - PackageName: "Newtonsoft.Json", - PackageVersion: "13.0.3", - BlockingReason: "Policy violations", - DepRelation: "direct", - PkgType: "nuget", - Policy: []Policy{ - { - Policy: "pol1", - Condition: "cond1", - }, - }, - }, - }, - totalNumberOfPackages: 1, - }, - }, - allowInsecureTls: true, - }, } return tests } @@ -4632,7 +4596,7 @@ func TestFetchNodeStatusRoutesPipAndPoetryThroughBoundedRedirects(t *testing.T) } // TestValidateRunNativeForTech checks that --run-native is accepted for the -// allow-listed native-config techs (npm, pnpm, yarn, uv) and rejected for all other +// allow-listed native-config techs (npm, pnpm, yarn, uv, NuGet) and rejected for all other // techs with an error that names the offending tech. func TestValidateRunNativeForTech(t *testing.T) { // Sanity: npm and pnpm are allow-listed techs. Both flag states pass. @@ -4668,6 +4632,10 @@ func TestValidateRunNativeForTech(t *testing.T) { assert.NoError(t, validateRunNativeForTech(techutils.Poetry, true)) assert.NoError(t, validateRunNativeForTech(techutils.Poetry, false)) }) + t.Run("NuGet accepts --run-native as a redundant no-op", func(t *testing.T) { + assert.NoError(t, validateRunNativeForTech(techutils.Nuget, true)) + assert.NoError(t, validateRunNativeForTech(techutils.Nuget, false)) + }) // Every other supported tech follows the same contract. Catch silent // acceptance for any tech that's in the doc-table-of-supported but @@ -4677,7 +4645,6 @@ func TestValidateRunNativeForTech(t *testing.T) { techutils.Maven, techutils.Gem, techutils.Go, - techutils.Nuget, techutils.Dotnet, techutils.Conan, techutils.Cocoapods, @@ -4697,6 +4664,134 @@ func TestValidateRunNativeForTech(t *testing.T) { } +// writeFakeDotnetExecutableForTest writes a "dotnet" executable in dir that, when invoked as +// 'dotnet nuget list source', prints sourcesOutput to stdout. +func writeFakeDotnetExecutableForTest(t *testing.T, dir, sourcesOutput string) string { + t.Helper() + if runtime.GOOS == "windows" { + t.Skip("fake dotnet executable is a POSIX shell script") + } + path := filepath.Join(dir, "dotnet") + script := "#!/bin/sh\ncat <<'EOF'\n" + sourcesOutput + "\nEOF\n" + require.NoError(t, os.WriteFile(path, []byte(script), 0o755)) + return path +} + +// TestSetRepoFromNuGetSourceAcceptsMatchingHost verifies the happy path: a configured NuGet +// source whose host matches the 'jf c' server is selected, and the resulting +// PackageManagerConfig carries the 'jf c' credentials plus the repo/URL parsed from the +// native source list. +func TestSetRepoFromNuGetSourceAcceptsMatchingHost(t *testing.T) { + toolDir := t.TempDir() + writeFakeDotnetExecutableForTest(t, toolDir, "Registered Sources:\n"+ + " 1. nuget.org [Enabled]\n"+ + " https://api.nuget.org/v3/index.json\n"+ + " 2. MyArtifactory [Enabled]\n"+ + " https://configured-server.example.com/artifactory/api/nuget/v3/nuget-test-repo/index.json\n") + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + projectDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(projectDir, "app.csproj"), + []byte(``), 0o644)) + restoreCwd := changeDirForTest(t, projectDir) + defer restoreCwd() + + ca := NewCurationAuditCommand() + ca.SetServerDetails(&config.ServerDetails{ + Url: "https://configured-server.example.com/", + ArtifactoryUrl: "https://configured-server.example.com/artifactory/", + AccessToken: "super-secret-token", + }) + + require.NoError(t, ca.setRepoFromNuGetSource()) + require.NotNil(t, ca.PackageManagerConfig) + assert.Equal(t, "nuget-test-repo", ca.PackageManagerConfig.TargetRepo()) + resolvedServer, err := ca.PackageManagerConfig.ServerDetails() + require.NoError(t, err) + assert.Equal(t, "super-secret-token", resolvedServer.AccessToken, + "must reuse the 'jf c' server credentials, not require jf nuget-config") + assert.Equal(t, "https://configured-server.example.com/artifactory/", resolvedServer.ArtifactoryUrl) +} + +// TestSetRepoFromNuGetSourceNoMatchingHost verifies that when none of the configured NuGet +// sources match the 'jf c' server's host, setRepoFromNuGetSource returns a clear, actionable +// error and never attaches credentials. +func TestSetRepoFromNuGetSourceNoMatchingHost(t *testing.T) { + toolDir := t.TempDir() + writeFakeDotnetExecutableForTest(t, toolDir, "Registered Sources:\n"+ + " 1. nuget.org [Enabled]\n"+ + " https://api.nuget.org/v3/index.json\n") + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + projectDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(projectDir, "app.csproj"), + []byte(``), 0o644)) + restoreCwd := changeDirForTest(t, projectDir) + defer restoreCwd() + + ca := NewCurationAuditCommand() + ca.SetServerDetails(&config.ServerDetails{ + Url: "https://configured-server.example.com/", + ArtifactoryUrl: "https://configured-server.example.com/artifactory/", + AccessToken: "super-secret-token", + }) + + setErr := ca.setRepoFromNuGetSource() + require.Error(t, setErr) + assert.Contains(t, setErr.Error(), "could not find a NuGet source") + assert.Nil(t, ca.PackageManagerConfig, "credentials must not be attached when no source matches") +} + +// TestSetRepoFromNuGetSourceNoServerConfigured verifies the clear error surfaced when +// auditing NuGet but no 'jf c' server is configured at all. +func TestSetRepoFromNuGetSourceNoServerConfigured(t *testing.T) { + projectDir := t.TempDir() + restoreCwd := changeDirForTest(t, projectDir) + defer restoreCwd() + + ca := NewCurationAuditCommand() + + setErr := ca.setRepoFromNuGetSource() + require.Error(t, setErr) + assert.Contains(t, setErr.Error(), "requires a configured Artifactory server") + assert.Nil(t, ca.PackageManagerConfig) +} + +// TestSetRepoRoutesNuGetToNativeSourceRegardlessOfRunNative is an integration-style check +// that SetRepo(Nuget) always dispatches to setRepoFromNuGetSource — NuGet has no +// 'jf nuget-config'/nuget.yaml equivalent, so unlike npm this must not depend on --run-native. +func TestSetRepoRoutesNuGetToNativeSourceRegardlessOfRunNative(t *testing.T) { + for _, runNative := range []bool{true, false} { + t.Run(fmt.Sprintf("run-native=%v", runNative), func(t *testing.T) { + toolDir := t.TempDir() + writeFakeDotnetExecutableForTest(t, toolDir, "Registered Sources:\n"+ + " 1. MyArtifactory [Enabled]\n"+ + " https://configured-server.example.com/artifactory/api/nuget/v3/nuget-test-repo/index.json\n") + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + projectDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(projectDir, "app.csproj"), + []byte(``), 0o644)) + restoreCwd := changeDirForTest(t, projectDir) + defer restoreCwd() + restoreHome := clienttestutils.SetEnvWithCallbackAndAssert(t, coreutils.HomeDir, t.TempDir()) + defer restoreHome() + + ca := NewCurationAuditCommand() + ca.SetServerDetails(&config.ServerDetails{ + Url: "https://configured-server.example.com/", + ArtifactoryUrl: "https://configured-server.example.com/artifactory/", + AccessToken: "super-secret-token", + }) + ca.SetRunNative(runNative) + + require.NoError(t, ca.SetRepo(techutils.Nuget)) + require.NotNil(t, ca.PackageManagerConfig) + assert.Equal(t, "nuget-test-repo", ca.PackageManagerConfig.TargetRepo()) + }) + } +} + // TestResolveResolverTechForCuration locks in the npm.yaml ↔ yarn.yaml // fallback for the resolver-config lookup in auditTree. The exact // reason this fallback has to live here, separate from the existing @@ -5626,6 +5721,80 @@ func TestHasCargoProject(t *testing.T) { }) } +// TestDoCurationAudit_Nuget is a full integration test: it runs a real 'dotnet restore' against +// the mock Artifactory server. NuGet has no 'jf nuget-config'; the registry is discovered by +// matching the mock server's dynamic URL against a real, generated NuGet.Config (own harness, +// like TestDoCurationAudit_Cargo, since the URL/port isn't known until the mock server starts). +func TestDoCurationAudit_Nuget(t *testing.T) { + if err := exec.Command("dotnet", "--version").Run(); err != nil { + t.Skip("dotnet SDK not available") + } + cleanUpFlags := setCurationFlagsForTest(t) + defer cleanUpFlags() + + basePathToTests, err := filepath.Abs(TestDataDir) + require.NoError(t, err) + pathToProject := filepath.Join("projects", "package-managers", "dotnet", "dotnet-curation") + + requestToFail := map[string]bool{ + "/api/nuget/v3/curated-nuget/registration-semver2/Download/newtonsoft.json/13.0.3": true, + } + mockServer, serverConfig := curationServer(t, nil, nil, requestToFail, nil, map[string]string{ + "curated-nuget/index.json": filepath.Join(basePathToTests, pathToProject, "resources", "feed.json"), + "index.json": filepath.Join(basePathToTests, pathToProject, "resources", "index.json"), + "13.0.3": filepath.Join(basePathToTests, pathToProject, "resources", "newtonsoft.json.13.0.3.nupkg"), + }) + defer mockServer.Close() + + tt := testCase{pathToProject: pathToProject, allowInsecureTls: true} + cleanUpHome := createTempHomeDirWithConfig(t, basePathToTests, tt, serverConfig) + defer cleanUpHome() + + testDirPath, cleanUpTestPathDir := testUtils.CreateTestProjectEnvAndChdir(t, filepath.Join(basePathToTests, pathToProject)) + defer cleanUpTestPathDir() + + // NuGet has no 'jf nuget-config'; wire the registry via a real NuGet.Config pointing at the + // mock server's dynamic URL, the same way a user's machine/project would be configured. + nugetConfig := fmt.Sprintf(` + + + + + + +`, serverConfig.ArtifactoryUrl) + require.NoError(t, os.WriteFile(filepath.Join(testDirPath, "NuGet.Config"), []byte(nugetConfig), 0600)) + + results, err := createCurationCmdAndRun(tt) + require.NoError(t, err) + + expected := map[string]*CurationReport{ + "dotnet-curation": { + packagesStatus: []*PackageStatus{ + { + Action: "blocked", + ParentName: "Newtonsoft.Json", + ParentVersion: "13.0.3", + BlockedPackageUrl: strings.TrimSuffix(serverConfig.ArtifactoryUrl, "/") + "/api/nuget/v3/curated-nuget/registration-semver2/Download/newtonsoft.json/13.0.3", + PackageName: "Newtonsoft.Json", + PackageVersion: "13.0.3", + BlockingReason: "Policy violations", + DepRelation: "direct", + PkgType: "nuget", + Policy: []Policy{ + { + Policy: "pol1", + Condition: "cond1", + }, + }, + }, + }, + totalNumberOfPackages: 1, + }, + } + assert.Equal(t, expected, results) +} + // skipIfCargoUnavailable skips t if cargo can't actually run -- exec.LookPath alone isn't enough, // since a rustup shim can exist on PATH with no default toolchain configured (e.g. some CI images). func skipIfCargoUnavailable(t *testing.T) { diff --git a/sca/bom/buildinfo/technologies/nuget/nuget.go b/sca/bom/buildinfo/technologies/nuget/nuget.go index 749f8d375..1490aba2d 100644 --- a/sca/bom/buildinfo/technologies/nuget/nuget.go +++ b/sca/bom/buildinfo/technologies/nuget/nuget.go @@ -4,12 +4,15 @@ import ( "errors" "fmt" "io/fs" + "net/url" "os" "os/exec" "path/filepath" + "regexp" "strings" "github.com/jfrog/gofrog/datastructures" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-client-go/utils/errorutils" "github.com/jfrog/jfrog-client-go/utils/io/fileutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -37,6 +40,9 @@ const ( dotnetToolType = "dotnet" nugetToolType = "nuget" globalPackagesNotFoundErrorMessage = "could not find global packages path at:" + artifactoryApiNugetPath = "/api/nuget/" + nugetV3PathSegment = "v3/" + nugetV3IndexJsonSuffix = "/index.json" ) // Generates a temporary duplicate of the project to execute the 'install' command without impacting the original directory and establishing the JFrog configuration file for Artifactory resolution @@ -135,25 +141,19 @@ func runDotnetRestoreAndLoadSolution(params technologies.BuildInfoBomGeneratorPa toolType := bidotnet.ConvertNameToToolType(toolName) var installCommandArgs []string - // Set up an Artifactory server as a resolution server if needed depsRepo := params.DependenciesRepository if depsRepo != "" { - // var serverDetails *config.ServerDetails - // serverDetails, err = params.ServerDetails() - - // Use the pass-through URL if the project is being restored as part of Curation Audit + serverDetails := params.ServerDetails if params.IsCurationCmd { - params.ServerDetails.ArtifactoryUrl += "api/curation/audit" - } - if err != nil { - err = fmt.Errorf("failed to get server details: %s", err.Error()) - return + copied := *serverDetails + copied.ArtifactoryUrl += "api/curation/audit" + serverDetails = &copied } - log.Info(fmt.Sprintf("Resolving dependencies from '%s' from repo '%s'", params.ServerDetails.Url, depsRepo)) + log.Info(fmt.Sprintf("Resolving dependencies from '%s' from repo '%s'", serverDetails.Url, depsRepo)) var configFile *os.File - configFile, err = dotnet.InitNewConfig(tmpWd, depsRepo, params.ServerDetails, false, allowInsecureConnections) + configFile, err = dotnet.InitNewConfig(tmpWd, depsRepo, serverDetails, false, allowInsecureConnections) if err != nil { err = fmt.Errorf("failed while attempting to generate a configuration file for setting up Artifactory as a resolution server") return @@ -327,6 +327,170 @@ func validateInputCommand(completeCommandArgs []string) (string, []string, error return executable, completeCommandArgs[1:], nil } +// NuGetRegistrySourceConfig holds Artifactory connection details parsed from a native +// NuGet/.NET CLI configured package source. +type NuGetRegistrySourceConfig struct { + SourceName string + ArtifactoryUrl string + RepoName string +} + +type nugetSource struct { + name string + url string +} + +// sourceHeaderRegex matches a single "Registered Sources" entry header line, shared by both +// 'dotnet nuget list source' and 'nuget sources List' (detailed format), e.g.: +// +// 1. MyArtifactory [Enabled] +var sourceHeaderRegex = regexp.MustCompile(`^\d+\.\s*(.+?)\s*\[(Enabled|Disabled)\]$`) + +// GetNativeNuGetRegistryConfig resolves NuGet's Artifactory source natively: +// 1. Detect the project's tool (.NET CLI or legacy NuGet CLI). +// 2. List that tool's configured sources ('dotnet nuget list source' / 'nuget sources List'). +// 3. Pick the source whose host matches the 'jf c' configured Artifactory server. +// +// Returns a clear error when no configured source matches. +func GetNativeNuGetRegistryConfig(serverDetails *config.ServerDetails) (*NuGetRegistrySourceConfig, error) { + wd, err := os.Getwd() + if err != nil { + return nil, err + } + toolName, err := getProjectToolName(wd) + if err != nil { + return nil, fmt.Errorf("failed while checking for the project's tool type: %w", err) + } + + sources, err := listNativeNuGetSources(toolName) + if err != nil { + return nil, err + } + + return selectMatchingNuGetSource(sources, serverDetails.GetArtifactoryUrl(), toolName) +} + +// listNativeNuGetSources runs the tool-appropriate list-sources command and parses its output. +func listNativeNuGetSources(toolName string) ([]nugetSource, error) { + var cmd *exec.Cmd + switch toolName { + case dotnetToolType: + cmd = exec.Command(dotnetToolType, "nuget", "list", "source") + case nugetToolType: + cmd = exec.Command(nugetToolType, "sources", "List") + default: + return nil, errorutils.CheckErrorf("unsupported tool type %q for native NuGet source resolution", toolName) + } + // #nosec G204 -- executable and args are hardcoded above, restricted to dotnet/nuget. + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("failed running '%s' to list the configured NuGet sources: %s", cmd.String(), strings.TrimSpace(string(output))) + } + return parseNuGetSourcesOutput(string(output)), nil +} + +// parseNuGetSourcesOutput parses the "Registered Sources" detailed-format output shared by +// 'dotnet nuget list source' and 'nuget sources List', returning only the enabled sources. +func parseNuGetSourcesOutput(output string) []nugetSource { + var lines []string + for _, line := range strings.Split(output, "\n") { + if trimmed := strings.TrimSpace(line); trimmed != "" { + lines = append(lines, trimmed) + } + } + + var sources []nugetSource + for i := 0; i < len(lines); i++ { + match := sourceHeaderRegex.FindStringSubmatch(lines[i]) + if match == nil || match[2] != "Enabled" { + continue + } + if i+1 >= len(lines) || sourceHeaderRegex.MatchString(lines[i+1]) { + // No URL line follows this source header - skip rather than misreading the next header as a URL. + continue + } + sources = append(sources, nugetSource{name: match[1], url: lines[i+1]}) + } + return sources +} + +// selectMatchingNuGetSource returns the configured source whose host matches the configured +// Artifactory server's host, parsed into its Artifactory base URL and repository name. +// Returns a clear error when no configured source matches. +func selectMatchingNuGetSource(sources []nugetSource, artifactoryUrl, toolName string) (*NuGetRegistrySourceConfig, error) { + artifactoryHost, err := hostOf(artifactoryUrl) + if err != nil { + return nil, fmt.Errorf("failed to parse the configured Artifactory URL %q: %w", artifactoryUrl, err) + } + + for _, source := range sources { + sourceHost, hostErr := hostOf(source.url) + if hostErr != nil || !strings.EqualFold(sourceHost, artifactoryHost) { + continue + } + rtBaseUrl, repoName, parseErr := parseArtifactoryNugetSourceUrl(source.url) + if parseErr != nil { + return nil, fmt.Errorf("NuGet source %q (%s) matches the configured Artifactory host %q but is not a recognizable Artifactory NuGet repository URL: %w", source.name, source.url, artifactoryHost, parseErr) + } + return &NuGetRegistrySourceConfig{SourceName: source.name, ArtifactoryUrl: rtBaseUrl, RepoName: repoName}, nil + } + + return nil, errorutils.CheckErrorf( + "could not find a NuGet source configured. Add one via Artifactory's %s 'Set me up' instructions "+ + "(run '%s' to view your currently configured sources)", + setMeUpClientName(toolName), listSourcesCommandString(toolName)) +} + +// setMeUpClientName returns the name of the 'Set me up' client tab matching toolName, so the +// error points the user at the same tab they'd need to follow ('.NET' vs 'NuGet'). +func setMeUpClientName(toolName string) string { + if toolName == nugetToolType { + return "NuGet" + } + return ".NET" +} + +func listSourcesCommandString(toolName string) string { + if toolName == nugetToolType { + return "nuget sources List" + } + return "dotnet nuget list source" +} + +// parseArtifactoryNugetSourceUrl extracts the Artifactory base URL and repository name from a +// NuGet source URL containing "/api/nuget/" (V2) or "/api/nuget/v3//index.json" (V3). +// Supports both standard URLs (https:///artifactory/api/nuget/...) and reverse-proxy URLs +// where the "/artifactory" context root is stripped (e.g. https://nuget.company.com/api/nuget/...). +func parseArtifactoryNugetSourceUrl(sourceUrl string) (rtBaseUrl, repoName string, err error) { + apiIdx := strings.Index(sourceUrl, artifactoryApiNugetPath) + if apiIdx == -1 { + return "", "", fmt.Errorf("NuGet source %q does not appear to be an Artifactory NuGet registry (expected %q in URL)", sourceUrl, artifactoryApiNugetPath) + } + rtBaseUrl = sourceUrl[:apiIdx] + "/" + afterApiNuget := sourceUrl[apiIdx+len(artifactoryApiNugetPath):] + afterApiNuget = strings.TrimPrefix(afterApiNuget, nugetV3PathSegment) + afterApiNuget = strings.TrimSuffix(afterApiNuget, "/") + // The repository name is always the first path segment, whether followed by nothing (V2), + // or by "/index.json" (V3) or extra path segments. + repoName, _, _ = strings.Cut(afterApiNuget, "/") + if repoName == "" || repoName == strings.TrimPrefix(nugetV3IndexJsonSuffix, "/") { + return "", "", fmt.Errorf("could not extract repository name from NuGet source URL %q", sourceUrl) + } + return rtBaseUrl, repoName, nil +} + +// hostOf returns the hostname (without port) of the given URL. +func hostOf(rawUrl string) (string, error) { + parsed, err := url.Parse(rawUrl) + if err != nil { + return "", err + } + if parsed.Hostname() == "" { + return "", fmt.Errorf("no host found in URL %q", rawUrl) + } + return parsed.Hostname(), nil +} + func parseNugetDependencyTree(buildInfo *entities.BuildInfo) (nodes []*xrayUtils.GraphNode, allUniqueDeps []string) { uniqueDepsSet := datastructures.MakeSet[string]() for _, module := range buildInfo.Modules { diff --git a/sca/bom/buildinfo/technologies/nuget/nuget_test.go b/sca/bom/buildinfo/technologies/nuget/nuget_test.go index 46fb12d07..3f7f76bfb 100644 --- a/sca/bom/buildinfo/technologies/nuget/nuget_test.go +++ b/sca/bom/buildinfo/technologies/nuget/nuget_test.go @@ -4,12 +4,14 @@ import ( "encoding/json" "os" "path/filepath" + "runtime" "strings" "testing" bidotnet "github.com/jfrog/build-info-go/build/utils/dotnet" "github.com/jfrog/build-info-go/build/utils/dotnet/solution" "github.com/jfrog/build-info-go/utils" + "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-security/sca/bom/buildinfo/technologies" "github.com/jfrog/jfrog-client-go/utils/log" xrayUtils "github.com/jfrog/jfrog-client-go/xray/services/utils" @@ -374,3 +376,337 @@ func TestSolutionFilePathValidation(t *testing.T) { }) } } + +func TestParseNuGetSourcesOutput(t *testing.T) { + testCases := []struct { + name string + output string + expected []nugetSource + }{ + { + name: "dotnet-style detailed output, single enabled source", + output: "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " https://api.nuget.org/v3/index.json\n", + expected: []nugetSource{ + {name: "nuget.org", url: "https://api.nuget.org/v3/index.json"}, + }, + }, + { + name: "multiple sources, disabled source is skipped", + output: "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " https://api.nuget.org/v3/index.json\n" + + " 2. MyArtifactory [Enabled]\n" + + " https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json\n" + + " 3. OldFeed [Disabled]\n" + + " https://old.example.com/index.json\n", + expected: []nugetSource{ + {name: "nuget.org", url: "https://api.nuget.org/v3/index.json"}, + {name: "MyArtifactory", url: "https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json"}, + }, + }, + { + name: "legacy nuget.exe 'sources List' output — same detailed format", + output: "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " https://api.nuget.org/v3/index.json\n", + expected: []nugetSource{ + {name: "nuget.org", url: "https://api.nuget.org/v3/index.json"}, + }, + }, + { + name: "no sources configured", + output: "Registered Sources:\n There are no sources.\n", + expected: nil, + }, + { + name: "empty output", + output: "", + expected: nil, + }, + { + name: "header with no URL line following (malformed/truncated) is skipped", + output: "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " 2. MyArtifactory [Enabled]\n" + + " https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json\n", + expected: []nugetSource{ + {name: "MyArtifactory", url: "https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json"}, + }, + }, + { + name: "source name containing brackets/spaces is captured correctly", + output: "Registered Sources:\n" + + " 1. My [Company] Feed [Enabled]\n" + + " https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json\n", + expected: []nugetSource{ + {name: "My [Company] Feed", url: "https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got := parseNuGetSourcesOutput(tc.output) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestParseArtifactoryNugetSourceUrl(t *testing.T) { + testCases := []struct { + name string + sourceUrl string + expectedBase string + expectedRepo string + expectErr bool + errMsgContains string + }{ + { + name: "V3 standard URL with /artifactory context root", + sourceUrl: "https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json", + expectedBase: "https://artifactory.example.com/artifactory/", + expectedRepo: "nuget-remote", + }, + { + name: "V2 standard URL with /artifactory context root", + sourceUrl: "https://artifactory.example.com/artifactory/api/nuget/nuget-remote", + expectedBase: "https://artifactory.example.com/artifactory/", + expectedRepo: "nuget-remote", + }, + { + name: "V2 URL with trailing slash", + sourceUrl: "https://artifactory.example.com/artifactory/api/nuget/nuget-remote/", + expectedBase: "https://artifactory.example.com/artifactory/", + expectedRepo: "nuget-remote", + }, + { + name: "V3 reverse-proxy URL without /artifactory context root", + sourceUrl: "https://nuget.company.com/api/nuget/v3/nuget-remote/index.json", + expectedBase: "https://nuget.company.com/", + expectedRepo: "nuget-remote", + }, + { + name: "V2 reverse-proxy URL without /artifactory context root", + sourceUrl: "https://nuget.company.com/api/nuget/nuget-remote", + expectedBase: "https://nuget.company.com/", + expectedRepo: "nuget-remote", + }, + { + name: "non-Artifactory URL", + sourceUrl: "https://api.nuget.org/v3/index.json", + expectErr: true, + errMsgContains: "does not appear to be an Artifactory NuGet registry", + }, + { + name: "Artifactory NuGet API path with no repo name", + sourceUrl: "https://artifactory.example.com/artifactory/api/nuget/v3/index.json", + expectErr: true, + errMsgContains: "could not extract repository name", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + base, repo, err := parseArtifactoryNugetSourceUrl(tc.sourceUrl) + if tc.expectErr { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errMsgContains) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedBase, base) + assert.Equal(t, tc.expectedRepo, repo) + }) + } +} + +func TestHostOf(t *testing.T) { + testCases := []struct { + name string + rawUrl string + expectedHost string + expectErr bool + }{ + {name: "standard https URL", rawUrl: "https://artifactory.example.com/artifactory/api/nuget/v3/repo/index.json", expectedHost: "artifactory.example.com"}, + {name: "URL with port", rawUrl: "https://artifactory.example.com:8081/artifactory/api/nuget/repo", expectedHost: "artifactory.example.com"}, + {name: "no host in URL", rawUrl: "/just/a/path", expectErr: true}, + {name: "empty URL", rawUrl: "", expectErr: true}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + host, err := hostOf(tc.rawUrl) + if tc.expectErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tc.expectedHost, host) + }) + } +} + +func TestSelectMatchingNuGetSource(t *testing.T) { + sources := []nugetSource{ + {name: "nuget.org", url: "https://api.nuget.org/v3/index.json"}, + {name: "MyArtifactory", url: "https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json"}, + } + + t.Run("matches the configured Artifactory host, case-insensitively", func(t *testing.T) { + result, err := selectMatchingNuGetSource(sources, "https://Artifactory.Example.com/artifactory/", dotnetToolType) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "MyArtifactory", result.SourceName) + assert.Equal(t, "https://artifactory.example.com/artifactory/", result.ArtifactoryUrl) + assert.Equal(t, "nuget-remote", result.RepoName) + }) + + t.Run("no configured source matches the host", func(t *testing.T) { + result, err := selectMatchingNuGetSource(sources, "https://other-artifactory.example.com/artifactory/", dotnetToolType) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "could not find a NuGet source configured") + assert.Contains(t, err.Error(), "dotnet nuget list source") + }) + + t.Run("error message references legacy nuget CLI command for nugetToolType", func(t *testing.T) { + _, err := selectMatchingNuGetSource(sources, "https://other-artifactory.example.com/artifactory/", nugetToolType) + require.Error(t, err) + assert.Contains(t, err.Error(), "nuget sources List") + }) + + t.Run("source matches host but is not a recognizable Artifactory NuGet URL", func(t *testing.T) { + malformed := []nugetSource{ + {name: "BadArtifactory", url: "https://artifactory.example.com/some/other/path"}, + } + _, err := selectMatchingNuGetSource(malformed, "https://artifactory.example.com/artifactory/", dotnetToolType) + require.Error(t, err) + assert.Contains(t, err.Error(), "not a recognizable Artifactory NuGet repository URL") + }) + + t.Run("invalid configured Artifactory URL", func(t *testing.T) { + _, err := selectMatchingNuGetSource(sources, "not-a-valid-url-%", dotnetToolType) + require.Error(t, err) + }) +} + +// writeFakeToolExecutable writes an executable in dir named toolName that echoes the given +// stdout content regardless of arguments, for exercising listNativeNuGetSources / +// GetNativeNuGetRegistryConfig without depending on a real dotnet/nuget install. +func writeFakeToolExecutable(t *testing.T, dir, toolName, stdout string) string { + if runtime.GOOS == "windows" { + path := filepath.Join(dir, toolName+".cmd") + script := "@echo off\r\n" + "echo " + stdout + "\r\n" + require.NoError(t, os.WriteFile(path, []byte(script), 0o755)) + return path + } + path := filepath.Join(dir, toolName) + script := "#!/bin/sh\ncat <<'EOF'\n" + stdout + "\nEOF\n" + require.NoError(t, os.WriteFile(path, []byte(script), 0o755)) + return path +} + +func TestListNativeNuGetSources(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake tool executable is a POSIX shell script") + } + fakeOutput := "Registered Sources:\n" + + " 1. MyArtifactory [Enabled]\n" + + " https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json\n" + + toolDir := t.TempDir() + writeFakeToolExecutable(t, toolDir, "dotnet", fakeOutput) + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + sources, err := listNativeNuGetSources(dotnetToolType) + require.NoError(t, err) + require.Len(t, sources, 1) + assert.Equal(t, "MyArtifactory", sources[0].name) +} + +func TestListNativeNuGetSourcesUnsupportedTool(t *testing.T) { + _, err := listNativeNuGetSources("some-other-tool") + require.Error(t, err) + assert.Contains(t, err.Error(), "unsupported tool type") +} + +func TestListNativeNuGetSourcesCommandFailure(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake tool executable is a POSIX shell script") + } + toolDir := t.TempDir() + failingScriptPath := filepath.Join(toolDir, "dotnet") + require.NoError(t, os.WriteFile(failingScriptPath, []byte("#!/bin/sh\necho 'boom' >&2\nexit 1\n"), 0o755)) + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + _, err := listNativeNuGetSources(dotnetToolType) + require.Error(t, err) + assert.Contains(t, err.Error(), "boom") +} + +// TestGetNativeNuGetRegistryConfig_DotnetProject runs GetNativeNuGetRegistryConfig end-to-end +// against a fake 'dotnet' executable and a project directory containing a PackageReference-style +// .csproj (so getProjectToolName resolves to the dotnet CLI), verifying the full resolution +// priority: detect tool -> list sources -> match by host -> parse Artifactory URL/repo. +func TestGetNativeNuGetRegistryConfig_DotnetProject(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake tool executable is a POSIX shell script") + } + fakeOutput := "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " https://api.nuget.org/v3/index.json\n" + + " 2. MyArtifactory [Enabled]\n" + + " https://artifactory.example.com/artifactory/api/nuget/v3/nuget-remote/index.json\n" + + toolDir := t.TempDir() + writeFakeToolExecutable(t, toolDir, "dotnet", fakeOutput) + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + projectDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(projectDir, "app.csproj"), + []byte(""), 0o644)) + + origWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(projectDir)) + defer func() { require.NoError(t, os.Chdir(origWd)) }() + + serverDetails := &config.ServerDetails{ArtifactoryUrl: "https://artifactory.example.com/artifactory/"} + result, err := GetNativeNuGetRegistryConfig(serverDetails) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, "MyArtifactory", result.SourceName) + assert.Equal(t, "https://artifactory.example.com/artifactory/", result.ArtifactoryUrl) + assert.Equal(t, "nuget-remote", result.RepoName) +} + +// TestGetNativeNuGetRegistryConfig_NoMatchingSource verifies the clear, actionable error when +// none of the configured NuGet sources match the configured Artifactory server's host. +func TestGetNativeNuGetRegistryConfig_NoMatchingSource(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("fake tool executable is a POSIX shell script") + } + fakeOutput := "Registered Sources:\n" + + " 1. nuget.org [Enabled]\n" + + " https://api.nuget.org/v3/index.json\n" + + toolDir := t.TempDir() + writeFakeToolExecutable(t, toolDir, "dotnet", fakeOutput) + t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH")) + + projectDir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(projectDir, "app.csproj"), + []byte(""), 0o644)) + + origWd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(projectDir)) + defer func() { require.NoError(t, os.Chdir(origWd)) }() + + serverDetails := &config.ServerDetails{ArtifactoryUrl: "https://artifactory.example.com/artifactory/"} + result, err := GetNativeNuGetRegistryConfig(serverDetails) + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "could not find a NuGet source configured") +} diff --git a/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go b/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go index 6a9eec42b..8feab5115 100644 --- a/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go +++ b/sca/bom/buildinfo/technologies/pnpm/pnpm_test.go @@ -46,7 +46,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) { name: "With transitive dependencies", treeDepth: "1", expectedUniqueDeps: []string{ - "npm://axios:1.19.0", + "npm://axios:1.20.0", "npm://balaganjs:1.0.0", "npm://yargs:13.3.0", "npm://zen-website:1.0.0", @@ -56,7 +56,7 @@ func TestBuildDependencyTreeLimitedDepth(t *testing.T) { Nodes: []*xrayUtils.GraphNode{ { Id: "npm://balaganjs:1.0.0", - Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.19.0"}, {Id: "npm://yargs:13.3.0"}}, + Nodes: []*xrayUtils.GraphNode{{Id: "npm://axios:1.20.0"}, {Id: "npm://yargs:13.3.0"}}, }, }, }, diff --git a/tests/testdata/projects/package-managers/dotnet/dotnet-curation/.jfrog/projects/nuget.yaml b/tests/testdata/projects/package-managers/dotnet/dotnet-curation/.jfrog/projects/nuget.yaml deleted file mode 100644 index db28da599..000000000 --- a/tests/testdata/projects/package-managers/dotnet/dotnet-curation/.jfrog/projects/nuget.yaml +++ /dev/null @@ -1,5 +0,0 @@ -version: 1 -type: nuget -resolver: - repo: curated-nuget - serverId: test \ No newline at end of file