From d6a6f5147f124a3eab53c7b0da3dc75c35d16b17 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 30 Oct 2026 09:08:32 +0100 Subject: [PATCH 1/4] C#: Exclude private registries from NuGet CLI restore feeds Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../FeedManager.cs | 24 +++++++++++++++++++ .../PackagesConfigRestorer.cs | 20 ++-------------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index a0b8977ed87a..3505a244917f 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -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 From 082bcae5129d167e5e7d4628e12ac773ec3e7c99 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 30 Oct 2026 09:08:34 +0100 Subject: [PATCH 2/4] C#: Test NuGet CLI restore feed selection Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Semmle.Extraction.Tests/FeedManager.cs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index ae97d2e7e346..95534f5b6baa 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -248,6 +248,113 @@ public void TestFeedsToUse() ], feedsToUse); } + /// + /// Verify that NuGet CLI restore excludes private registries and unreachable feeds, + /// while leaving private registries available to dotnet restore. + /// + [Fact] + public void TestNugetExeRestoreFeedsExcludePrivateRegistries() + { + // Setup + 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"])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + var dotnetFeeds = feedManager.MakeRestoreFeeds("/path/to/project.csproj"); + + // Verify + Assert.Equal(["https://feed.from/folder1"], feeds); + Assert.Contains("https://example.com/registry2", dotnetFeeds); + } + + /// + /// Verify that NuGet CLI restore excludes private registries that replace NuGet.org, + /// retaining other configured feeds. + /// + [Fact] + public void TestNugetExeRestoreFeedsExcludePrivateBaseRegistries() + { + // Setup + 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([])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + Assert.Equal(["https://feed.from/folder1"], feeds); + } + + /// + /// Verify that NuGet CLI restore uses an empty local source when only private registries are configured, + /// regardless of their reachability. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TestNugetExeRestoreFeedsWithOnlyPrivateRegistries(bool unreachable) + { + // Setup + 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() : [])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + var source = Assert.Single(feeds); + Assert.True(Path.IsPathFullyQualified(source)); + Assert.True(Directory.Exists(source)); + } + + /// + /// Verify that NuGet CLI restore explicitly selects reachable feeds when no private registries are configured. + /// + [Fact] + public void TestNugetExeRestoreFeedsWithoutPrivateRegistries() + { + // Setup + 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([])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + Assert.Equal(["https://feed.from/folder1"], feeds); + } + + /// + /// Verify that NuGet CLI restore falls back to the default NuGet.org feed when no feeds are configured. + /// + [Fact] + public void TestNugetExeRestoreFeedsUseDefaults() + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], []); + using var feedManager = new FeedManager(logger, dotnet, null, + new FileProviderStub(), new FeedManagerIOStub([])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds); + } + /// /// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds /// when no private registries are configured. From 3badc71a3aee7346c5b8e98a6f0c2f55f8a57bb8 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 30 Oct 2026 09:08:35 +0100 Subject: [PATCH 3/4] C#: Add change note for NuGet CLI private registry filtering Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../change-notes/2026-10-30-nuget-cli-private-registries.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 csharp/ql/lib/change-notes/2026-10-30-nuget-cli-private-registries.md 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`. From 5f2750664d3cc1b4a015c44f69c093245d9d13f4 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 30 Oct 2026 10:42:57 +0100 Subject: [PATCH 4/4] C#: Fix NuGet CLI fallback and registry URI matching Filter private candidates and defaults before choosing the fallback, and compare registry URI identities while preserving path case. Add regressions for missing or unreachable folder feeds and equivalent registry URLs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../FeedManager.cs | 30 ++++++++++-- .../Semmle.Extraction.Tests/FeedManager.cs | 48 ++++++++++++++++++- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 3505a244917f..3a217c246d9b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -33,6 +33,9 @@ internal sealed partial class FeedManager : IDisposable /// public bool CheckNugetFeedResponsiveness { get; } = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.CheckNugetFeedResponsiveness); + private readonly Lazy> lazyPrivateRegistryUris; + private ImmutableHashSet PrivateRegistryUris => lazyPrivateRegistryUris.Value; + private readonly Lazy> lazyExplicitFeeds; /// @@ -96,6 +99,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, IRegistryProxy? registryProxy this.fileProvider = fileProvider; this.feedManagerIo = feedManagerIo; privateRegistryFeeds = registryProxy?.RegistryURLs ?? []; + lazyPrivateRegistryUris = MakePrivateRegistryUris(); HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; privateRegistryBaseFeeds = registryProxy?.RegistryBaseURLs ?? []; hasPrivateRegistryBaseFeeds = privateRegistryBaseFeeds.Count > 0; @@ -123,6 +127,20 @@ public FeedManager(ILogger logger, IDotNet dotnet, IRegistryProxy? registryProxy { } + private Lazy> MakePrivateRegistryUris() => + new Lazy>(() => + { + var uris = ImmutableHashSet.CreateBuilder(); + foreach (var registry in privateRegistryFeeds) + { + if (Uri.TryCreate(registry, UriKind.Absolute, out var uri)) + { + uris.Add(uri); + } + } + return uris.ToImmutable(); + }); + private bool IsNugetOrgFeed(string url) { try @@ -246,14 +264,19 @@ public List MakeRestoreFeeds(string path) return RestoreFeeds(feedsToUse); } + private bool IsPrivateRegistryFeed(string feed) => + privateRegistryFeeds.Contains(feed) || + (Uri.TryCreate(feed, UriKind.Absolute, out var uri) && PrivateRegistryUris.Contains(uri)); + /// /// 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 feedsToUse = FeedsToUse(path).Where(feed => !IsPrivateRegistryFeed(feed)).ToList(); + var defaultFeeds = (CheckNugetFeedResponsiveness ? ReachableDefaultFeeds : DefaultFeeds) + .Where(feed => !IsPrivateRegistryFeed(feed)).ToList(); var useDefaultFeeds = feedsToUse.Count == 0 && defaultFeeds.Count > 0; if (!CheckNugetFeedResponsiveness && !HasPrivateRegistryFeeds && !useDefaultFeeds) @@ -266,8 +289,7 @@ public List MakeNugetExeRestoreFeeds(string path) feedsToUse.AddRange(defaultFeeds); } - // Filter after selecting defaults so private base feeds cannot be reintroduced. - return RestoreFeeds(feedsToUse.Where(feed => !privateRegistryFeeds.Contains(feed))); + return RestoreFeeds(feedsToUse); } private (int initialTimeout, int tryCount) GetFeedRequestSettings(bool isFallback) diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index 95534f5b6baa..0d7e4fccd147 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -293,7 +293,7 @@ public void TestNugetExeRestoreFeedsExcludePrivateBaseRegistries() } /// - /// Verify that NuGet CLI restore uses an empty local source when only private registries are configured, + /// Verify that NuGet CLI restore uses an empty local source when private registries replace the default feeds, /// regardless of their reachability. /// [Theory] @@ -355,6 +355,52 @@ public void TestNugetExeRestoreFeedsUseDefaults() Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds); } + /// + /// Verify that non-base private registries do not prevent NuGet CLI restore from falling back to NuGet.org + /// when no usable folder feeds remain. + /// + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TestNugetExeRestoreFeedsUseDefaultsWithPrivateRegistries(bool unreachableFolderFeed) + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], + unreachableFolderFeed ? ["E https://feed.from/folder1"] : []); + using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(), + new FileProviderStub(), new FeedManagerIOStub(["https://feed.from/folder1"])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds); + } + + /// + /// Verify that NuGet CLI restore identifies equivalent private registry URLs regardless of host case + /// or explicit default ports, while preserving case-sensitive registry paths. + /// + [Theory] + [InlineData("https://EXAMPLE.COM/registry2")] + [InlineData("https://example.com:443/registry2")] + [InlineData("https://EXAMPLE.COM:443/registry2")] + public void TestNugetExeRestoreFeedsCompareRegistryUris(string folderFeed) + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], [$"E {folderFeed}"]); + using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(), + new FileProviderStub(), new FeedManagerIOStub([])); + + // Execute + var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config"); + + // Verify + Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds); + } + /// /// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds /// when no private registries are configured.