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`.