From 74d2612541aea145c812785cc18b7743a5b51d60 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 2 Sep 2026 15:04:05 +0200 Subject: [PATCH 1/8] C#: DependabotProxy now parses replaces-base and exposes a list of URLs to replace the default NuGet feed. --- .../DependabotProxy.cs | 37 ++++++++++++++++--- .../FeedManager.cs | 2 +- .../IDependabotProxy.cs | 9 ++++- .../DependabotProxy.cs | 31 +++++++++++++++- .../Semmle.Extraction.Tests/FeedManager.cs | 4 +- 5 files changed, 73 insertions(+), 10 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs index 3bf843d3fa2c..c4afcc9792bc 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Immutable; using System.Collections.Generic; using System.IO; using System.Security.Cryptography.X509Certificates; @@ -14,13 +15,39 @@ public class DependabotProxy : IDependabotProxy /// /// Represents configurations for package registries. /// - /// The type of package registry. - /// The URL of the package registry. - public record class RegistryConfig(string Type, string URL); + public class RegistryConfig + { + /// + /// The type of the package registry. + /// + public string Type { get; init; } = ""; + + /// + /// The URL of the package registry. + /// + public string URL { get; init; } = ""; + + /// + /// A boolean indicating whether this registry replaces the base registry. + /// + [JsonProperty("replaces-base")] + public bool ReplacesBase { get; init; } = false; + }; public string Address { get; } - public HashSet RegistryURLs { get; } = []; + /// + /// A dictionary mapping registry URLs to a boolean indicating whether they replace the base registry. + /// + private readonly Dictionary registryMapping = []; + + private ImmutableHashSet? registryURLs; + public ImmutableHashSet RegistryURLs => + registryURLs ??= registryMapping.Keys.ToImmutableHashSet(); + + private ImmutableHashSet? registryBaseURLs; + public ImmutableHashSet RegistryBaseURLs => + registryBaseURLs ??= registryMapping.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToImmutableHashSet(); public string? CertificatePath { get; private set; } @@ -65,7 +92,7 @@ private DependabotProxy(IDependabotProxyConfiguration config, ILogger logger, Te } logger.LogInfo($"Found private registry at '{registry.URL}'"); - RegistryURLs.Add(registry.URL); + registryMapping.AddOrUpdateToLatest(registry.URL, registry.ReplacesBase); } } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 6c4593f3400c..f286f3a3652f 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -78,7 +78,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP this.dotnet = dotnet; this.fileProvider = fileProvider; this.feedManagerIo = feedManagerIo; - privateRegistryFeeds = dependabotProxy?.RegistryURLs.ToImmutableHashSet() ?? []; + privateRegistryFeeds = dependabotProxy?.RegistryURLs ?? []; HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxy.cs index 37a11900fddf..aafaf851e356 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxy.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Collections.Immutable; using System.Security.Cryptography.X509Certificates; namespace Semmle.Extraction.CSharp.DependencyFetching @@ -14,7 +14,12 @@ public interface IDependabotProxy : IDisposable /// /// The URLs of package registries that are configured for the proxy. /// - HashSet RegistryURLs { get; } + ImmutableHashSet RegistryURLs { get; } + + /// + /// The URLs of package registries that replace the base registry. + /// + ImmutableHashSet RegistryBaseURLs { get; } /// /// The path to the temporary file where the certificate is stored. diff --git a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs index 9c8c762f5989..83d899c50912 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs @@ -135,7 +135,8 @@ public void TestDependabotRegistryUrls1() // Verify Assert.NotNull(proxy); - Assert.Equal([], proxy.RegistryURLs); + Assert.Empty(proxy.RegistryURLs); + Assert.Empty(proxy.RegistryBaseURLs); } [Fact] @@ -158,6 +159,7 @@ public void TestDependabotRegistryUrls2() Assert.Equal([ "https://nuget.pkg.github.com/org/index.json" ], proxy.RegistryURLs); + Assert.Empty(proxy.RegistryBaseURLs); } [Fact] @@ -180,6 +182,33 @@ public void TestDependabotRegistryUrls3() Assert.Equal([ "https://example.com/org/index.json" ], proxy.RegistryURLs); + Assert.Empty(proxy.RegistryBaseURLs); + } + + [Fact] + public void TestDependabotReplacesBase1() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + Host = "localhost", + RegistryURLs = "[ { \"type\": \"nuget_feed\", \"url\": \"https://example.com/org/index.json\", \"replaces-base\": true }, { \"type\": \"nuget_feed\", \"url\": \"https://example2.com/org/index.json\", \"replaces-base\": false } ]" + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.NotNull(proxy); + Assert.Equal([ + "https://example.com/org/index.json", + "https://example2.com/org/index.json" + ], proxy.RegistryURLs); + Assert.Equal([ + "https://example.com/org/index.json", + ], proxy.RegistryBaseURLs); } } } diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index f70efdb4cdcc..c6eb024d16b2 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -4,13 +4,15 @@ using System.IO; using System.Linq; using Semmle.Extraction.CSharp.DependencyFetching; +using System.Collections.Immutable; namespace Semmle.Extraction.Tests { public class DependabotProxyStub : IDependabotProxy { public string Address { get; } = ""; - public HashSet RegistryURLs { get; } = ["https://example.com/registry1", "https://example.com/registry2"]; + public ImmutableHashSet RegistryURLs { get; } = ["https://example.com/registry1", "https://example.com/registry2"]; + public ImmutableHashSet RegistryBaseURLs { get; } = []; public string? CertificatePath { get; } = null; public System.Security.Cryptography.X509Certificates.X509Certificate2? Certificate { get; } = null; From 1cf5d23d1f25c7792fc7f5505a7c0473374c9a85 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 2 Sep 2026 15:58:39 +0200 Subject: [PATCH 2/8] C#: Use the RegistryUrls from the dependabot proxy as default feeds is replaces-base is set, otherwise use nuget.org. --- .../EnvironmentVariableNames.cs | 1 - .../FeedManager.cs | 32 +++++------ .../PackagesConfigRestorer.cs | 14 ++--- .../Semmle.Extraction.Tests/FeedManager.cs | 56 ++++++++++++++++++- 4 files changed, 73 insertions(+), 30 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs index b1134ad21e24..94a87037cdbc 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs @@ -56,7 +56,6 @@ internal static class EnvironmentVariableNames /// /// Specifies the NuGet feeds to use for fallback NuGet dependency fetching. The value is a space-separated list of feed URLs. - /// The default value is `https://api.nuget.org/v3/index.json`. /// public const string FallbackNugetFeeds = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_NUGET_FEEDS_FALLBACK"; diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index f286f3a3652f..217a0ec11718 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -17,6 +17,7 @@ internal sealed partial class FeedManager : IDisposable private readonly IFileProvider fileProvider; private readonly DependencyDirectory emptyPackageDirectory; private readonly ImmutableHashSet privateRegistryFeeds; + private readonly ImmutableHashSet defaultFeeds; private readonly IFeedManagerIO feedManagerIo; /// @@ -72,6 +73,13 @@ internal sealed partial class FeedManager : IDisposable /// public ImmutableHashSet ReachableFallbackFeeds => lazyReachableFallbackFeeds.Value; + private readonly Lazy> lazyReachableDefaultFeeds; + + /// + /// Gets the list of reachable default NuGet feeds. + /// + public ImmutableHashSet ReachableDefaultFeeds => lazyReachableDefaultFeeds.Value; + public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider, IFeedManagerIO feedManagerIo) { this.logger = logger; @@ -80,6 +88,9 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP this.feedManagerIo = feedManagerIo; privateRegistryFeeds = dependabotProxy?.RegistryURLs ?? []; HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; + defaultFeeds = dependabotProxy?.RegistryBaseURLs.Any() == true + ? dependabotProxy.RegistryBaseURLs + : [PublicNugetOrgFeed]; emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger); lazyExplicitFeeds = new Lazy>(GetExplicitFeeds); @@ -96,6 +107,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP var reachableFallbackFeeds = GetReachableFallbackNugetFeeds(); return reachableFallbackFeeds.ToImmutableHashSet(); }); + lazyReachableDefaultFeeds = new Lazy>(() => CheckSpecifiedFeeds(defaultFeeds)); } public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider) @@ -266,22 +278,6 @@ private ImmutableHashSet CheckSpecifiedFeeds(ImmutableHashSet fe return reachable.Union(feeds.Where(feed => excludedFeeds.Contains(feed))).ToImmutableHashSet(); } - /// - /// Return true if the default NuGet feed is reachable, false otherwise. - /// If the reachability check is disabled, this method will always return true. - /// - /// True if the default NuGet feed is reachable, false otherwise. - public bool IsDefaultFeedReachable() - { - if (CheckNugetFeedResponsiveness) - { - var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback: false); - return feedManagerIo.IsFeedReachable(PublicNugetOrgFeed, initialTimeout, tryCount); - } - - return true; - } - /// /// Tests which of the feeds given by are reachable. /// @@ -315,8 +311,8 @@ private List GetReachableFallbackNugetFeeds() var fallbackFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.FallbackNugetFeeds).ToHashSet(); if (fallbackFeeds.Count == 0) { - fallbackFeeds.Add(PublicNugetOrgFeed); - logger.LogInfo($"No fallback NuGet feeds specified. Adding default feed: {PublicNugetOrgFeed}"); + fallbackFeeds.UnionWith(defaultFeeds); + logger.LogInfo($"No fallback NuGet feeds specified. Adding default feeds: {string.Join(", ", defaultFeeds.OrderBy(f => f))}"); var shouldAddNugetConfigFeeds = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.AddNugetConfigFeedsToFallback); logger.LogInfo($"Adding feeds from nuget.config to fallback restore: {shouldAddNugetConfigFeeds}"); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs index d4403bb955ef..964a24441387 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs @@ -67,10 +67,6 @@ private class NugetExeWrapper : IPackagesConfigRestore private bool IsWindows => SystemBuildActions.Instance.IsWindows(); - private bool? isDefaultFeedReachable; - private bool IsDefaultFeedReachable => - isDefaultFeedReachable ??= feedManager.IsDefaultFeedReachable(); - /// /// Create the package manager for a specified source tree. /// @@ -169,15 +165,15 @@ private bool TryRestoreNugetPackage(string packagesConfig) List sourcesArgument = []; var feedsToUse = feedManager.FeedsToUse(packagesConfig).ToList(); - var useDefaultFeed = feedsToUse.Count == 0 && IsDefaultFeedReachable; + var useDefaultFeeds = feedsToUse.Count == 0 && feedManager.ReachableDefaultFeeds.Count > 0; // Explicitly construct the sources to be used for the restore command when checking feed - // responsiveness, using private registries, or falling back to nuget.org. - if (feedManager.CheckNugetFeedResponsiveness || feedManager.HasPrivateRegistryFeeds || useDefaultFeed) + // responsiveness, using private registries, or falling back to default feeds. + if (feedManager.CheckNugetFeedResponsiveness || feedManager.HasPrivateRegistryFeeds || useDefaultFeeds) { - if (useDefaultFeed) + if (useDefaultFeeds) { - feedsToUse.Add(FeedManager.PublicNugetOrgFeed); + feedsToUse.AddRange(feedManager.ReachableDefaultFeeds); } var restoreFeeds = feedManager.RestoreFeeds(feedsToUse); sourcesArgument = restoreFeeds.SelectMany(feed => ["-Source", feed]).ToList(); diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index c6eb024d16b2..0a66b4766c1f 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -1,10 +1,11 @@ using Xunit; using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.IO; using System.Linq; +using System.Security.Cryptography.X509Certificates; using Semmle.Extraction.CSharp.DependencyFetching; -using System.Collections.Immutable; namespace Semmle.Extraction.Tests { @@ -14,7 +15,18 @@ public class DependabotProxyStub : IDependabotProxy public ImmutableHashSet RegistryURLs { get; } = ["https://example.com/registry1", "https://example.com/registry2"]; public ImmutableHashSet RegistryBaseURLs { get; } = []; public string? CertificatePath { get; } = null; - public System.Security.Cryptography.X509Certificates.X509Certificate2? Certificate { get; } = null; + public X509Certificate2? Certificate { get; } = null; + + public void Dispose() { } + } + + public class DependabotProxyStubWithBaseUrls : IDependabotProxy + { + public string Address { get; } = ""; + public ImmutableHashSet RegistryURLs { get; } = ["https://example.com/registry1", "https://example.com/registry2", "https://example.com/base1", "https://example.com/base2"]; + public ImmutableHashSet RegistryBaseURLs { get; } = ["https://example.com/base1", "https://example.com/base2"]; + public string? CertificatePath { get; } = null; + public X509Certificate2? Certificate { get; } = null; public void Dispose() { } } @@ -185,5 +197,45 @@ public void TestFeedsToUse() "https://feed.from/folder1" ], feedsToUse); } + + [Fact] + public void TestDefaultFeeds1() + { + // Setup + var feedManager = MakeFeedManager(); + + // Execute + var reachableDefault = feedManager.ReachableDefaultFeeds; + + // Verify + Assert.Equal([ + "https://api.nuget.org/v3/index.json" + ], reachableDefault); + } + + [Fact] + public void TestDefaultFeeds2() + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], []); + var dependabotProxy = new DependabotProxyStubWithBaseUrls(); + var fileProvider = new FileProviderStub(); + var feedManagerIo = new FeedManagerIOStub(["https://example.com/registry2", "https://example.com/base1"]); + var feedManager = new FeedManager(logger, dotnet, dependabotProxy, fileProvider, feedManagerIo); + + // Execute + var reachableDefault = feedManager.ReachableDefaultFeeds; + var reachableFallback = feedManager.ReachableFallbackFeeds; + + // Verify + Assert.Equal([ + "https://example.com/base2" + ], reachableDefault); + Assert.Equal([ + "https://example.com/registry1", + "https://example.com/base2" + ], reachableFallback); + } } } From f10882c886fc453beab3e14ee7e6e1169e7e059a Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 3 Sep 2026 15:58:18 +0200 Subject: [PATCH 3/8] C#: Add change-note. --- csharp/ql/lib/change-notes/2026-09-03-replaces-base.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 csharp/ql/lib/change-notes/2026-09-03-replaces-base.md diff --git a/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md b/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md new file mode 100644 index 000000000000..bd16b0e21711 --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* In `build-mode: none`, private NuGet registries configured with `replaces-base: true` in the organization-level private registry configuration now replace `nuget.org` sources whenever dependencies are downloaded, including sources discovered from NuGet configuration. From 1b1a22a34ef5e03f9631d4f5de1bdb409d968308 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 11:01:10 +0200 Subject: [PATCH 4/8] C#: Preserve semantics of reachability for default feeds when using [mono] nuget. --- .../FeedManager.cs | 15 ++++++++++----- .../PackagesConfigRestorer.cs | 7 +++++-- .../Semmle.Extraction.Tests/FeedManager.cs | 9 +++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 217a0ec11718..9a184c945578 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -17,7 +17,6 @@ internal sealed partial class FeedManager : IDisposable private readonly IFileProvider fileProvider; private readonly DependencyDirectory emptyPackageDirectory; private readonly ImmutableHashSet privateRegistryFeeds; - private readonly ImmutableHashSet defaultFeeds; private readonly IFeedManagerIO feedManagerIo; /// @@ -75,6 +74,12 @@ internal sealed partial class FeedManager : IDisposable private readonly Lazy> lazyReachableDefaultFeeds; + /// + /// Gets the list of default NuGet feeds that are configured in the environment. + /// This is either the public NuGet feed or a set of feeds specified by the environment. + /// + public ImmutableHashSet DefaultFeeds { get; init; } + /// /// Gets the list of reachable default NuGet feeds. /// @@ -88,7 +93,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP this.feedManagerIo = feedManagerIo; privateRegistryFeeds = dependabotProxy?.RegistryURLs ?? []; HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; - defaultFeeds = dependabotProxy?.RegistryBaseURLs.Any() == true + DefaultFeeds = dependabotProxy?.RegistryBaseURLs.Any() == true ? dependabotProxy.RegistryBaseURLs : [PublicNugetOrgFeed]; emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger); @@ -107,7 +112,7 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP var reachableFallbackFeeds = GetReachableFallbackNugetFeeds(); return reachableFallbackFeeds.ToImmutableHashSet(); }); - lazyReachableDefaultFeeds = new Lazy>(() => CheckSpecifiedFeeds(defaultFeeds)); + lazyReachableDefaultFeeds = new Lazy>(() => CheckSpecifiedFeeds(DefaultFeeds)); } public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider) @@ -311,8 +316,8 @@ private List GetReachableFallbackNugetFeeds() var fallbackFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.FallbackNugetFeeds).ToHashSet(); if (fallbackFeeds.Count == 0) { - fallbackFeeds.UnionWith(defaultFeeds); - logger.LogInfo($"No fallback NuGet feeds specified. Adding default feeds: {string.Join(", ", defaultFeeds.OrderBy(f => f))}"); + fallbackFeeds.UnionWith(DefaultFeeds); + logger.LogInfo($"No fallback NuGet feeds specified. Adding default feeds: {string.Join(", ", DefaultFeeds.OrderBy(f => f))}"); var shouldAddNugetConfigFeeds = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.AddNugetConfigFeedsToFallback); logger.LogInfo($"Adding feeds from nuget.config to fallback restore: {shouldAddNugetConfigFeeds}"); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs index 964a24441387..861622ca4c02 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs @@ -165,7 +165,10 @@ private bool TryRestoreNugetPackage(string packagesConfig) List sourcesArgument = []; var feedsToUse = feedManager.FeedsToUse(packagesConfig).ToList(); - var useDefaultFeeds = feedsToUse.Count == 0 && feedManager.ReachableDefaultFeeds.Count > 0; + 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. @@ -173,7 +176,7 @@ private bool TryRestoreNugetPackage(string packagesConfig) { if (useDefaultFeeds) { - feedsToUse.AddRange(feedManager.ReachableDefaultFeeds); + feedsToUse.AddRange(defaultFeeds); } var restoreFeeds = feedManager.RestoreFeeds(feedsToUse); sourcesArgument = restoreFeeds.SelectMany(feed => ["-Source", feed]).ToList(); diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index 0a66b4766c1f..37aca8003e71 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -205,9 +205,13 @@ public void TestDefaultFeeds1() var feedManager = MakeFeedManager(); // Execute + var defaultFeeds = feedManager.DefaultFeeds; var reachableDefault = feedManager.ReachableDefaultFeeds; // Verify + Assert.Equal([ + "https://api.nuget.org/v3/index.json" + ], defaultFeeds); Assert.Equal([ "https://api.nuget.org/v3/index.json" ], reachableDefault); @@ -225,10 +229,15 @@ public void TestDefaultFeeds2() var feedManager = new FeedManager(logger, dotnet, dependabotProxy, fileProvider, feedManagerIo); // Execute + var defaultFeeds = feedManager.DefaultFeeds; var reachableDefault = feedManager.ReachableDefaultFeeds; var reachableFallback = feedManager.ReachableFallbackFeeds; // Verify + Assert.Equal([ + "https://example.com/base1", + "https://example.com/base2" + ], defaultFeeds); Assert.Equal([ "https://example.com/base2" ], reachableDefault); From d6ae24f538267211ac50a7fc7d53508e3f39a931 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 12:28:48 +0200 Subject: [PATCH 5/8] C#: Remove isNullOrEmpty check as it is implied by the case above. --- .../FeedManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 9a184c945578..c37d9e57a349 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -141,10 +141,7 @@ private IEnumerable GetFeeds(Func> getNugetFeeds) continue; } - if (!string.IsNullOrWhiteSpace(url)) - { - yield return url; - } + yield return url; } } From 6e71be14f884a92ed2e1ef1cb9568fb592372dd9 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 13:04:13 +0200 Subject: [PATCH 6/8] C#: Make a complete replacement of the default nuget.org feed in case private registries with replaces-base: true is set. --- .../FeedManager.cs | 40 +++++++++++-- .../Semmle.Extraction.Tests/FeedManager.cs | 57 +++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index c37d9e57a349..3d323d3976e9 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -10,13 +10,17 @@ namespace Semmle.Extraction.CSharp.DependencyFetching { internal sealed partial class FeedManager : IDisposable { - internal const string PublicNugetOrgFeed = "https://api.nuget.org/v3/index.json"; + private const string PublicNugetOrg = "nuget.org"; + private const string PublicDotNugetOrg = $".{PublicNugetOrg}"; + internal const string PublicApiNugetOrgFeed = $"https://api{PublicDotNugetOrg}/v3/index.json"; private readonly ILogger logger; private readonly IDotNet dotnet; private readonly IFileProvider fileProvider; private readonly DependencyDirectory emptyPackageDirectory; private readonly ImmutableHashSet privateRegistryFeeds; + private readonly bool hasPrivateRegistryBaseFeeds; + private readonly ImmutableHashSet privateRegistryBaseFeeds; private readonly IFeedManagerIO feedManagerIo; /// @@ -93,9 +97,12 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP this.feedManagerIo = feedManagerIo; privateRegistryFeeds = dependabotProxy?.RegistryURLs ?? []; HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; - DefaultFeeds = dependabotProxy?.RegistryBaseURLs.Any() == true - ? dependabotProxy.RegistryBaseURLs - : [PublicNugetOrgFeed]; + privateRegistryBaseFeeds = dependabotProxy?.RegistryBaseURLs ?? []; + hasPrivateRegistryBaseFeeds = privateRegistryBaseFeeds.Count > 0; + + DefaultFeeds = hasPrivateRegistryBaseFeeds + ? privateRegistryBaseFeeds + : [PublicApiNugetOrgFeed]; emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger); lazyExplicitFeeds = new Lazy>(GetExplicitFeeds); @@ -120,6 +127,20 @@ public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotP { } + private bool IsNugetOrgFeed(string url) + { + try + { + var uri = new Uri(url); + return uri.Host.EndsWith(PublicDotNugetOrg, StringComparison.InvariantCultureIgnoreCase) || + string.Equals(uri.Host, PublicNugetOrg, StringComparison.InvariantCultureIgnoreCase); + } + catch (UriFormatException) + { + return false; + } + } + private IEnumerable GetFeeds(Func> getNugetFeeds) { var results = getNugetFeeds(); @@ -141,6 +162,17 @@ private IEnumerable GetFeeds(Func> getNugetFeeds) continue; } + if (hasPrivateRegistryBaseFeeds && IsNugetOrgFeed(url)) + { + // Use private registry base feeds. + foreach (var feed in privateRegistryBaseFeeds) + { + logger.LogDebug($"Using private registry base feed '{feed}'."); + yield return feed; + } + continue; + } + yield return url; } } diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index 37aca8003e71..13d96e77cbfa 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -246,5 +246,62 @@ public void TestDefaultFeeds2() "https://example.com/base2" ], reachableFallback); } + + [Fact] + public void TestNugetOrg() + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], [], ["E https://api.nuget.org/v3/index.json"]); + var dependabotProxy = new DependabotProxyStub(); + var fileProvider = new FileProviderStub(); + var feedManagerIo = new FeedManagerIOStub(["https://example.com/registry2", "https://example.com/base1"]); + var feedManager = new FeedManager(logger, dotnet, dependabotProxy, fileProvider, feedManagerIo); + + // Execute + var explicitFeeds = feedManager.ExplicitFeeds; + var allFeeds = feedManager.AllFeeds; + + // Verify + Assert.Equal([ + "https://example.com/registry1", + "https://example.com/registry2", + ], explicitFeeds); + Assert.Equal([ + "https://example.com/registry1", + "https://example.com/registry2", + "https://api.nuget.org/v3/index.json" + ], allFeeds); + + } + [Fact] + public void TestNugetOrgReplacement() + { + // Setup + var logger = new LoggerStub(); + var dotnet = new DotNetStub([], [], ["E https://api.nuget.org/v3/index.json"], ["E https://api.nuget.org/v3/index.json"]); + var dependabotProxy = new DependabotProxyStubWithBaseUrls(); + var fileProvider = new FileProviderStub(); + var feedManagerIo = new FeedManagerIOStub(["https://example.com/registry2", "https://example.com/base1"]); + var feedManager = new FeedManager(logger, dotnet, dependabotProxy, fileProvider, feedManagerIo); + + // Execute + var explicitFeeds = feedManager.ExplicitFeeds; + var allFeeds = feedManager.AllFeeds; + + // Verify + Assert.Equal([ + "https://example.com/base1", + "https://example.com/base2", + "https://example.com/registry1", + "https://example.com/registry2" + ], explicitFeeds); + Assert.Equal([ + "https://example.com/base1", + "https://example.com/base2", + "https://example.com/registry1", + "https://example.com/registry2", + ], allFeeds); + } } } From ae89e13b17528e720caaac62b4fe3dd5fc7a77c1 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 14:40:20 +0200 Subject: [PATCH 7/8] C#: Exclude TryRestoreManually fallback without nugetSources when private registries are configured (to make implementation consistent). --- .../NugetPackageRestorer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs index 85d6056d7218..f62105f2b482 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs @@ -460,7 +460,7 @@ private bool TryRestorePackageManually(string package, List nugetSources return true; } - if (!feedManager.CheckNugetFeedResponsiveness && res.HasNugetPackageSourceError && nugetSources.Count > 0) + if (!feedManager.CheckNugetFeedResponsiveness && !feedManager.HasPrivateRegistryFeeds && res.HasNugetPackageSourceError && nugetSources.Count > 0) { logger.LogDebug($"Trying to restore '{package}' without explicitly providing NuGet sources."); // Restore could not be completed because the listed source is unavailable. Try without an explicit restore source argument. From 983dbbce272579d8ffc540f518c1c2e699474bf8 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 15:09:38 +0200 Subject: [PATCH 8/8] C#: Minor improvements to unit test. --- csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index 13d96e77cbfa..d1b963965abd 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -279,7 +279,7 @@ public void TestNugetOrgReplacement() { // Setup var logger = new LoggerStub(); - var dotnet = new DotNetStub([], [], ["E https://api.nuget.org/v3/index.json"], ["E https://api.nuget.org/v3/index.json"]); + var dotnet = new DotNetStub([], [], ["E https://www.nuget.org/api/v2/"], ["E https://api.nuget.org/v3/index.json"]); var dependabotProxy = new DependabotProxyStubWithBaseUrls(); var fileProvider = new FileProviderStub(); var feedManagerIo = new FeedManagerIOStub(["https://example.com/registry2", "https://example.com/base1"]);