diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index a0b8977ed87a..260ba64ef425 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -31,7 +31,7 @@ internal sealed partial class FeedManager : IDisposable /// /// Gets whether the reachability of the NuGet feeds should be checked before using them for restore. /// - public bool CheckNugetFeedResponsiveness { get; } = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.CheckNugetFeedResponsiveness); + public bool CheckNugetFeedResponsiveness { get; init; } = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.CheckNugetFeedResponsiveness); private readonly Lazy> lazyExplicitFeeds; @@ -246,6 +246,30 @@ public List MakeRestoreFeeds(string path) return RestoreFeeds(feedsToUse); } + /// + /// Constructs the NuGet CLI restore sources, excluding private registries that require the registry proxy. + /// An empty list leaves source selection to NuGet only when no filtering or default feeds are needed. + /// + public List MakeNugetExeRestoreFeeds(string path) + { + var feedsToUse = FeedsToUse(path).ToList(); + var defaultFeeds = CheckNugetFeedResponsiveness ? ReachableDefaultFeeds : DefaultFeeds; + var useDefaultFeeds = feedsToUse.Count == 0 && defaultFeeds.Count > 0; + + if (!CheckNugetFeedResponsiveness && !HasPrivateRegistryFeeds && !useDefaultFeeds) + { + return []; + } + + if (useDefaultFeeds) + { + feedsToUse.AddRange(defaultFeeds); + } + + // Filter after selecting defaults so private base feeds cannot be reintroduced. + return RestoreFeeds(feedsToUse.Where(feed => !privateRegistryFeeds.Contains(feed))); + } + private (int initialTimeout, int tryCount) GetFeedRequestSettings(bool isFallback) { int timeoutMilliSeconds = isFallback && int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessInitialTimeoutForFallback), out timeoutMilliSeconds) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs index 2348eb809f8c..f696f584278f 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs @@ -163,24 +163,8 @@ private bool TryRestoreNugetPackage(string packagesConfig) { logger.LogInfo($"Restoring file \"{packagesConfig}\"..."); - List sourcesArgument = []; - var feedsToUse = feedManager.FeedsToUse(packagesConfig).ToList(); - var defaultFeeds = feedManager.CheckNugetFeedResponsiveness - ? feedManager.ReachableDefaultFeeds - : feedManager.DefaultFeeds; - var useDefaultFeeds = feedsToUse.Count == 0 && defaultFeeds.Count > 0; - - // Explicitly construct the sources to be used for the restore command when checking feed - // responsiveness, using private registries, or falling back to default feeds. - if (feedManager.CheckNugetFeedResponsiveness || feedManager.HasPrivateRegistryFeeds || useDefaultFeeds) - { - if (useDefaultFeeds) - { - feedsToUse.AddRange(defaultFeeds); - } - var restoreFeeds = feedManager.RestoreFeeds(feedsToUse); - sourcesArgument = restoreFeeds.SelectMany(feed => ["-Source", feed]).ToList(); - } + var sourcesArgument = feedManager.MakeNugetExeRestoreFeeds(packagesConfig) + .SelectMany(feed => ["-Source", feed]).ToList(); /* Use nuget.exe to install a package. * Note that there is a clutch of NuGet assemblies which could be used to diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index ae97d2e7e346..cb28da1c6871 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -248,6 +248,99 @@ public void TestFeedsToUse() ], feedsToUse); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestNugetExeRestoreFeedsExcludePrivateRegistries(bool checkResponsiveness) + { + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], + ["E https://example.com/registry2", "E https://feed.from/folder1", "E https://feed.from/folder2"]); + using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(), + new FileProviderStub(), new FeedManagerIOStub(["https://feed.from/folder2"])) + { + CheckNugetFeedResponsiveness = checkResponsiveness + }; + + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config").ToHashSet(); + Assert.True(feeds.SetEquals(checkResponsiveness + ? ["https://feed.from/folder1"] + : ["https://feed.from/folder1", "https://feed.from/folder2"])); + Assert.Contains("https://example.com/registry2", feedManager.MakeRestoreFeeds("/path/to/project.csproj")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestNugetExeRestoreFeedsExcludePrivateBaseRegistries(bool checkResponsiveness) + { + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], + ["E https://api.nuget.org/v3/index.json", "E https://feed.from/folder1"]); + using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStubWithBaseUrls(), + new FileProviderStub(), new FeedManagerIOStub([])) + { + CheckNugetFeedResponsiveness = checkResponsiveness + }; + + Assert.Equal(["https://feed.from/folder1"], feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config")); + } + + [Theory] + [InlineData(true, false)] + [InlineData(false, false)] + [InlineData(true, true)] + [InlineData(false, true)] + public void TestNugetExeRestoreFeedsWithOnlyPrivateRegistries(bool checkResponsiveness, bool unreachable) + { + var logger = new LoggerStub(); + var registryProxy = new RegistryProxyStubWithBaseUrls(); + var dotnet = new DotNetStub([], [], [], ["E https://api.nuget.org/v3/index.json"]); + using var feedManager = new FeedManager(logger, dotnet, registryProxy, + new FileProviderStub(), new FeedManagerIOStub(unreachable ? registryProxy.RegistryURLs.ToList() : [])) + { + CheckNugetFeedResponsiveness = checkResponsiveness + }; + + var source = Assert.Single(feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config")); + Assert.True(Path.IsPathFullyQualified(source)); + Assert.True(Directory.Exists(source)); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestNugetExeRestoreFeedsWithoutPrivateRegistries(bool checkResponsiveness) + { + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], ["E https://feed.from/folder1"]); + using var feedManager = new FeedManager(logger, dotnet, null, + new FileProviderStub(), new FeedManagerIOStub([])) + { + CheckNugetFeedResponsiveness = checkResponsiveness + }; + + Assert.Equal(checkResponsiveness ? ["https://feed.from/folder1"] : [], + feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TestNugetExeRestoreFeedsUseDefaults(bool checkResponsiveness) + { + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], []); + using var feedManager = new FeedManager(logger, dotnet, null, + new FileProviderStub(), new FeedManagerIOStub([])) + { + CheckNugetFeedResponsiveness = checkResponsiveness + }; + + Assert.Equal(["https://api.nuget.org/v3/index.json"], + feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config")); + } + /// /// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds /// when no private registries are configured. diff --git a/csharp/ql/lib/change-notes/2026-10-30-nuget-cli-private-registries.md b/csharp/ql/lib/change-notes/2026-10-30-nuget-cli-private-registries.md new file mode 100644 index 000000000000..4fae6b6ff015 --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-10-30-nuget-cli-private-registries.md @@ -0,0 +1,5 @@ +--- +category: minorAnalysis +--- + +- The C# extractor continues to use the NuGet CLI to restore dependencies from `packages.config` files when private NuGet registries are configured, but excludes those private registries from the CLI's sources. Other applicable feeds remain available, while private registries continue to be used by `dotnet restore`.