From daacaa6d8fbedb893192767738d56f83173193e3 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 2 Sep 2026 15:04:05 +0200 Subject: [PATCH 01/13] 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 ee9fe7537de7b3201e09e6f0922e92e8ea4ad6dc Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 2 Sep 2026 15:58:39 +0200 Subject: [PATCH 02/13] 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 dcaf038051d5b8ccaa4ffa93fc5989ea1a72872e Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Thu, 3 Sep 2026 15:58:18 +0200 Subject: [PATCH 03/13] 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 1f93077d70cf62d0e308d72f20d50409bb647597 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 11:01:10 +0200 Subject: [PATCH 04/13] 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 02f1792b6d4cff4601661d9313ec7b54bdadf0c5 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 12:28:48 +0200 Subject: [PATCH 05/13] 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 b30141c1bb6da50c221f2db9ddd2008392d1af78 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 13:04:13 +0200 Subject: [PATCH 06/13] 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 28503f683e2ea10bd82fe419580a43f265d3d537 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 14:40:20 +0200 Subject: [PATCH 07/13] 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 4e2e1ae76ddab0748893d399ca7a0fe529edbc05 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Fri, 4 Sep 2026 15:09:38 +0200 Subject: [PATCH 08/13] 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"]); From b2e83bb7807d568c7ca88ba1f94935fab11eeaa3 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Tue, 8 Sep 2026 13:14:54 +0200 Subject: [PATCH 09/13] C#: Address review comments. --- .../DependabotProxy.cs | 30 ++++++++-- .../DependabotProxy.cs | 46 +++++++++++++-- .../Semmle.Extraction.Tests/FeedManager.cs | 59 ++++++++++++++++++- .../change-notes/2026-09-03-replaces-base.md | 2 +- 4 files changed, 122 insertions(+), 15 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs index c4afcc9792bc..da8104458653 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs @@ -20,12 +20,12 @@ public class RegistryConfig /// /// The type of the package registry. /// - public string Type { get; init; } = ""; + public string? Type { get; init; } /// /// The URL of the package registry. /// - public string URL { get; init; } = ""; + public string? Url { get; init; } /// /// A boolean indicating whether this registry replaces the base registry. @@ -42,10 +42,22 @@ public class RegistryConfig private readonly Dictionary registryMapping = []; private ImmutableHashSet? registryURLs; + /// + /// Gets the set of registry URLs that have been configured as part of the organization-level + /// private registry configuration. This includes all registries, regardless of whether they replace + /// the default feeds. + /// public ImmutableHashSet RegistryURLs => registryURLs ??= registryMapping.Keys.ToImmutableHashSet(); private ImmutableHashSet? registryBaseURLs; + /// + /// Gets the set of registry URLs that have been configured as part of the organization-level + /// private registry configuration and that replace the default registry. This is a subset of + /// . + /// If non-empty, the set should be used as a replacement for the default registry during + /// package resolution. + /// public ImmutableHashSet RegistryBaseURLs => registryBaseURLs ??= registryMapping.Where(kvp => kvp.Value).Select(kvp => kvp.Key).ToImmutableHashSet(); @@ -83,16 +95,22 @@ private DependabotProxy(IDependabotProxyConfiguration config, ILogger logger, Te { foreach (RegistryConfig registry in array) { + if (string.IsNullOrWhiteSpace(registry.Url)) + { + logger.LogDebug("Ignoring registry with empty URL."); + continue; + } + // The array contains all configured private registries, not just ones for C#. // We ignore the non-C# ones here. - if (!registry.Type.Equals("nuget_feed")) + if (registry.Type is null || !registry.Type.Equals("nuget_feed")) { - logger.LogDebug($"Ignoring registry at '{registry.URL}' since it is not of type 'nuget_feed'."); + logger.LogDebug($"Ignoring registry at '{registry.Url}' since it is not of type 'nuget_feed'."); continue; } - logger.LogInfo($"Found private registry at '{registry.URL}'"); - registryMapping.AddOrUpdateToLatest(registry.URL, registry.ReplacesBase); + logger.LogInfo($"Found private registry at '{registry.Url}'"); + registryMapping.AddOrUpdateToLatest(registry.Url, registry.ReplacesBase); } } } diff --git a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs index 83d899c50912..71c3943fe8fe 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs @@ -28,8 +28,12 @@ private static TemporaryDirectory MakeTemporaryDirectory() return new TemporaryDirectory(tmp, "testing", new LoggerStub()); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case where the port is not specified. + /// In this case, the registry proxy should not be created. + /// [Fact] - public void TestDependabotProxyCreation1() + public void TestDependabotProxyNoPort() { // Setup var config = new DependabotConfigurationStub @@ -46,8 +50,12 @@ public void TestDependabotProxyCreation1() Assert.Null(proxy); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case where the host is not specified. + /// In this case, the registry proxy should not be created. + /// [Fact] - public void TestDependabotProxyCreation2() + public void TestDependabotProxyNoHost() { // Setup var config = new DependabotConfigurationStub @@ -96,6 +104,10 @@ public void TestDependabotProxyCreation2() -----END CERTIFICATE----- """; + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case + /// where the port, host, and certificate are specified. + /// [Fact] public void TestDependabotProxyCertificate() { @@ -118,8 +130,13 @@ public void TestDependabotProxyCertificate() Assert.NotNull(proxy.CertificatePath); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case + /// where the RegistryURLs environment variable is not a valid JSON list. + /// In this case, the registry proxy should be created, but the list of private registries should be empty. + /// [Fact] - public void TestDependabotRegistryUrls1() + public void TestDependabotRegistryUrlsParseError() { // Setup var config = new DependabotConfigurationStub @@ -139,8 +156,13 @@ public void TestDependabotRegistryUrls1() Assert.Empty(proxy.RegistryBaseURLs); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case + /// where the RegistryURLs environment variable is a valid JSON list with a single entry. + /// In this case, the registry proxy should be created, and the list of private registries should contain the single entry. + /// [Fact] - public void TestDependabotRegistryUrls2() + public void TestDependabotRegistryUrlsSingle() { // Setup var config = new DependabotConfigurationStub @@ -162,6 +184,13 @@ public void TestDependabotRegistryUrls2() Assert.Empty(proxy.RegistryBaseURLs); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case + /// where the RegistryURLs environment variable is a valid JSON list with multiple entries, but only one of them + /// is of type "nuget_feed", which is relevant for C#. + /// In this case, the registry proxy should be created, and the list of private registries should + /// contain only the entry of type "nuget_feed". + /// [Fact] public void TestDependabotRegistryUrls3() { @@ -185,8 +214,15 @@ public void TestDependabotRegistryUrls3() Assert.Empty(proxy.RegistryBaseURLs); } + /// + /// The purpose of this test is to verify that the registry proxy correctly handles the case + /// where the RegistryURLs environment variable is a valid JSON list with multiple entries and one of them + /// is configured to replace the base feeds. + /// In this case, the registry proxy should be created, and the list of private registries should contain all + /// entries, while the list of base registries should contain only the entry that replaces the base feeds. + /// [Fact] - public void TestDependabotReplacesBase1() + public void TestDependabotRegistryUrlsReplacesBase() { // Setup var config = new DependabotConfigurationStub diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index d1b963965abd..425024425332 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -68,6 +68,11 @@ public class FileProviderStub : IFileProvider public ICollection Resources { get; } = new List(); } + /// + /// The purpose of this test class is to verify the behavior of the FeedManager class. + /// The tests use stub implementations of the FeedManager's dependencies to control the behavior of the FeedManager + /// and verify its behavior. + /// public class FeedManagerTests { private static FeedManager MakeFeedManager() @@ -80,6 +85,10 @@ private static FeedManager MakeFeedManager() return new FeedManager(logger, dotnet, dependabotProxy, fileProvider, feedManagerIo); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// explicit feeds. + /// [Fact] public void TestExplicitFeeds() { @@ -97,6 +106,10 @@ public void TestExplicitFeeds() ], actualFeeds); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// inherited feeds. + /// [Fact] public void TestInheritedFeeds() { @@ -113,6 +126,10 @@ public void TestInheritedFeeds() ], inherited); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// all feeds. + /// [Fact] public void TestAllFeeds() { @@ -132,6 +149,10 @@ public void TestAllFeeds() ], all); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// reachable feeds. + /// [Fact] public void TestReachableFeeds() { @@ -149,6 +170,10 @@ public void TestReachableFeeds() ], reachableFeeds); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// reachable explicit feeds. + /// [Fact] public void TestReachableExplicitFeeds() { @@ -165,6 +190,10 @@ public void TestReachableExplicitFeeds() ], reachableFeeds); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// reachable fallback feeds. + /// [Fact] public void TestReachableFallbackFeeds() { @@ -182,6 +211,10 @@ public void TestReachableFallbackFeeds() ], reachableFallback); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// feeds to use for a given file. + /// [Fact] public void TestFeedsToUse() { @@ -198,8 +231,12 @@ public void TestFeedsToUse() ], feedsToUse); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// default feeds and reachable default feeds when no private registries are configured. + /// [Fact] - public void TestDefaultFeeds1() + public void TestDefaultFeedsNugetOrg() { // Setup var feedManager = MakeFeedManager(); @@ -217,8 +254,13 @@ public void TestDefaultFeeds1() ], reachableDefault); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// default feeds, reachable default feeds, and fallback feeds when private registries + /// are configured and some of them replace the default feeds. + /// [Fact] - public void TestDefaultFeeds2() + public void TestDefaultFeedsPrivateRegistries() { // Setup var logger = new LoggerStub(); @@ -247,8 +289,13 @@ public void TestDefaultFeeds2() ], reachableFallback); } + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// all feeds when https://api.nuget.org/v3/index.json is not replaced by any private registries because + /// none of them are configured to replace the base feeds. + /// [Fact] - public void TestNugetOrg() + public void TestNugetOrgNotReplaced() { // Setup var logger = new LoggerStub(); @@ -274,6 +321,12 @@ public void TestNugetOrg() ], allFeeds); } + + /// + /// The purpose of this test is to verify that the FeedManager correctly computes the set of + /// all feeds when https://api.nuget.org/v3/index.json and related NuGet.org URLs are replaced by private + /// registries configured to replace the base feeds. + /// [Fact] public void TestNugetOrgReplacement() { 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 index bd16b0e21711..a2cf9b41be26 100644 --- a/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md +++ b/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md @@ -1,4 +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. +* Private NuGet registries for which the "Replaces base" option is enabled in the organization-level private registry configuration now replace `nuget.org` sources whenever dependencies are downloaded, including sources discovered from NuGet configuration. From dadf67da607ed3b33d0e067346431e02f9e07819 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 9 Sep 2026 14:31:19 +0200 Subject: [PATCH 10/13] C#: Address review comments related to logging. --- .../DependabotProxy.cs | 10 ++++++++-- .../FeedManager.cs | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs index da8104458653..8229a3da1375 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs @@ -97,13 +97,19 @@ private DependabotProxy(IDependabotProxyConfiguration config, ILogger logger, Te { if (string.IsNullOrWhiteSpace(registry.Url)) { - logger.LogDebug("Ignoring registry with empty URL."); + logger.LogError("Ignoring registry with empty URL."); + continue; + } + + if (string.IsNullOrWhiteSpace(registry.Type)) + { + logger.LogError($"Ignoring registry at '{registry.Url}' since it has no type."); continue; } // The array contains all configured private registries, not just ones for C#. // We ignore the non-C# ones here. - if (registry.Type is null || !registry.Type.Equals("nuget_feed")) + if (!registry.Type.Equals("nuget_feed")) { logger.LogDebug($"Ignoring registry at '{registry.Url}' since it is not of type 'nuget_feed'."); continue; diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 3d323d3976e9..6567174ddc0f 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -359,6 +359,10 @@ private List GetReachableFallbackNugetFeeds() logger.LogInfo($"Using NuGet feeds from nuget.config files as fallback feeds: {string.Join(", ", ExplicitFeeds.OrderBy(f => f))}"); } } + else + { + logger.LogInfo($"Using fallback NuGet feeds from environment variable '{EnvironmentVariableNames.FallbackNugetFeeds}'."); + } return GetReachableNuGetFeeds(fallbackFeeds, isFallback: true); } From f8c2820398831edc4fdab85f1ff1d4a37ccecae3 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 9 Sep 2026 14:35:15 +0200 Subject: [PATCH 11/13] C#: Rename GetNugetFeeds to GetNugetFeedsFromConfig. --- .../Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs | 2 +- .../Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs | 2 +- .../Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs | 2 +- csharp/extractor/Semmle.Extraction.Tests/DotNet.cs | 2 +- csharp/extractor/Semmle.Extraction.Tests/DotNetStub.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs index 9958fbce4e71..340d86eb3914 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs @@ -137,7 +137,7 @@ public bool Exec(List execArgs) private static readonly IReadOnlyList nugetListSourceCommandArgs = ["nuget", "list", "source", "--format", "Short"]; - public IList GetNugetFeeds(string nugetConfig) + public IList GetNugetFeedsFromConfig(string nugetConfig) { logger.LogInfo($"Getting NuGet feeds from '{nugetConfig}'..."); return GetResultList([.. nugetListSourceCommandArgs, "--configfile", nugetConfig]); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs index 6567174ddc0f..aeca9c6b2a13 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/FeedManager.cs @@ -181,7 +181,7 @@ private IEnumerable GetFeedsFromFolder(string folderPath) => GetFeeds(() => dotnet.GetNugetFeedsFromFolder(folderPath)); private IEnumerable GetFeedsFromNugetConfig(string nugetConfigPath) => - GetFeeds(() => dotnet.GetNugetFeeds(nugetConfigPath)); + GetFeeds(() => dotnet.GetNugetFeedsFromConfig(nugetConfigPath)); /// /// Constructs the NuGet sources argument for the restore command based on the given feeds. diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs index 0e93fa92813a..06186f1a28d3 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs @@ -13,7 +13,7 @@ public interface IDotNet IList GetListedRuntimes(); IList GetListedSdks(); bool Exec(List execArgs); - IList GetNugetFeeds(string nugetConfig); + IList GetNugetFeedsFromConfig(string nugetConfig); IList GetNugetFeedsFromFolder(string folderPath); } diff --git a/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs b/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs index 77e88a58443a..1d393a418ca9 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/DotNet.cs @@ -285,7 +285,7 @@ public void TestNugetFeeds() var dotnet = MakeDotnet(dotnetCliInvoker); // Execute - dotnet.GetNugetFeeds("abc"); + dotnet.GetNugetFeedsFromConfig("abc"); // Verify var lastArgs = dotnetCliInvoker.GetLastArgs(); diff --git a/csharp/extractor/Semmle.Extraction.Tests/DotNetStub.cs b/csharp/extractor/Semmle.Extraction.Tests/DotNetStub.cs index 119e39fd0974..12a131380e41 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/DotNetStub.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/DotNetStub.cs @@ -30,7 +30,7 @@ public DotNetStub(IList runtimes, IList sdks, IList nuge public bool Exec(List execArgs) => true; - public IList GetNugetFeeds(string nugetConfig) => nugetFeedsFromConfig; + public IList GetNugetFeedsFromConfig(string nugetConfig) => nugetFeedsFromConfig; public IList GetNugetFeedsFromFolder(string folderPath) => nugetFeedsFromFolder; } From 7eaf822b01a9a81c8683cafbd51884d77bab01ba Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 9 Sep 2026 16:30:52 +0200 Subject: [PATCH 12/13] C#: Improve FeedManager unit test explanations. --- .../Semmle.Extraction.Tests/FeedManager.cs | 62 +++++++++++-------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs index 425024425332..2fdecd570954 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -86,8 +86,10 @@ private static FeedManager MakeFeedManager() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// explicit feeds. + /// Verify that `FeedManager` correctly computes the explicit feeds using feeds discovered in nuget.config files and + /// private registries. + /// See the initialization of `DotNetStub` and `DependabotProxyStub` in `MakeFeedManager` for the feeds configured + /// to be returned and classified as explicit feeds. /// [Fact] public void TestExplicitFeeds() @@ -107,8 +109,9 @@ public void TestExplicitFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// inherited feeds. + /// Verify that `FeedManager` correctly computes the inherited feeds using feeds discovered from the environment. + /// See the initialization of `DotNetStub` in `MakeFeedManager` for the feeds configured + /// to be returned and classified as inherited feeds. /// [Fact] public void TestInheritedFeeds() @@ -127,8 +130,10 @@ public void TestInheritedFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// all feeds. + /// Verify that `FeedManager` correctly computes all feeds using feeds discovered in nuget.config files, private registries, + /// and the environment. + /// See the initialization of `DotNetStub` and `DependabotProxyStub` in `MakeFeedManager` for the feeds configured + /// to be returned and included in all feeds. /// [Fact] public void TestAllFeeds() @@ -150,8 +155,10 @@ public void TestAllFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// reachable feeds. + /// Verify that `FeedManager` correctly computes the reachable feeds using feeds discovered in + /// nuget.config files, private registries, and the environment. + /// See the initialization of `FeedManagerIOStub` in `MakeFeedManager` for the feeds configured as unreachable + /// and therefore filtered out of the reachable feeds. /// [Fact] public void TestReachableFeeds() @@ -171,8 +178,10 @@ public void TestReachableFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// reachable explicit feeds. + /// Verify that `FeedManager` correctly computes the reachable explicit feeds using feeds discovered in + /// nuget.config files and private registries. + /// See the initialization of `FeedManagerIOStub` in `MakeFeedManager` for the feeds configured as unreachable + /// and therefore filtered out of the reachable explicit feeds. /// [Fact] public void TestReachableExplicitFeeds() @@ -191,8 +200,10 @@ public void TestReachableExplicitFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// reachable fallback feeds. + /// Verify that `FeedManager` correctly computes the reachable fallback feeds using feeds discovered in + /// nuget.config files and the default NuGet.org feed. + /// See the initialization of `FeedManagerIOStub` in `MakeFeedManager` for the feeds configured as unreachable + /// and therefore filtered out of the reachable fallback feeds. /// [Fact] public void TestReachableFallbackFeeds() @@ -212,8 +223,10 @@ public void TestReachableFallbackFeeds() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// feeds to use for a given file. + /// Verify that `FeedManager` correctly computes the feeds to use for a given packages.config file from feeds discovered + /// in private registries and the environment. + /// See the initialization of `DotNetStub` in `MakeFeedManager` for the feeds configured + /// to be returned and selected for use. /// [Fact] public void TestFeedsToUse() @@ -232,8 +245,8 @@ public void TestFeedsToUse() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// default feeds and reachable default feeds when no private registries are configured. + /// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds + /// when no private registries are configured. /// [Fact] public void TestDefaultFeedsNugetOrg() @@ -255,9 +268,9 @@ public void TestDefaultFeedsNugetOrg() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// default feeds, reachable default feeds, and fallback feeds when private registries - /// are configured and some of them replace the default feeds. + /// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds + /// when private registries are configured to replace the default feeds. + /// See the initialization of `DependabotProxyStubWithBaseUrls` for the feeds configured to replace the default feeds. /// [Fact] public void TestDefaultFeedsPrivateRegistries() @@ -290,9 +303,8 @@ public void TestDefaultFeedsPrivateRegistries() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// all feeds when https://api.nuget.org/v3/index.json is not replaced by any private registries because - /// none of them are configured to replace the base feeds. + /// Verify that `FeedManager` correctly computes all feeds when https://api.nuget.org/v3/index.json is not replaced + /// by a private registry because no private registry is configured to replace the base feed. /// [Fact] public void TestNugetOrgNotReplaced() @@ -323,9 +335,9 @@ public void TestNugetOrgNotReplaced() } /// - /// The purpose of this test is to verify that the FeedManager correctly computes the set of - /// all feeds when https://api.nuget.org/v3/index.json and related NuGet.org URLs are replaced by private - /// registries configured to replace the base feeds. + /// Verify that `FeedManager` correctly computes the explicit and all feeds when https://api.nuget.org/v3/index.json and + /// related NuGet.org URLs are replaced by private registries configured to replace the base feeds. + /// See the initialization of `DependabotProxyStubWithBaseUrls` for the feeds configured as default replacements. /// [Fact] public void TestNugetOrgReplacement() From 93aa3a305f5cc9af6b9675611993346bfe74ece8 Mon Sep 17 00:00:00 2001 From: Michael Nebel Date: Wed, 9 Sep 2026 16:39:46 +0200 Subject: [PATCH 13/13] C#: Update change note. --- csharp/ql/lib/change-notes/2026-09-03-replaces-base.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a2cf9b41be26..2a80f4a89eab 100644 --- a/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md +++ b/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* Private NuGet registries for which the "Replaces base" option is enabled in the organization-level private registry configuration now replace `nuget.org` sources whenever dependencies are downloaded, including sources discovered from NuGet configuration. +* Private NuGet registries for which the "Replaces base" option is enabled in the organization-level private registry configuration now replace default NuGet feeds whenever dependencies are downloaded, including when default NuGet feeds are configured explicitly for a project.