diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs index 3bf843d3fa2c..da8104458653 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,51 @@ 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; + /// + /// 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(); public string? CertificatePath { get; private set; } @@ -56,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}'"); - RegistryURLs.Add(registry.URL); + logger.LogInfo($"Found private registry at '{registry.Url}'"); + registryMapping.AddOrUpdateToLatest(registry.Url, registry.ReplacesBase); } } } 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 6c4593f3400c..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; /// @@ -72,14 +76,33 @@ internal sealed partial class FeedManager : IDisposable /// public ImmutableHashSet ReachableFallbackFeeds => lazyReachableFallbackFeeds.Value; + 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. + /// + public ImmutableHashSet ReachableDefaultFeeds => lazyReachableDefaultFeeds.Value; + public FeedManager(ILogger logger, IDotNet dotnet, IDependabotProxy? dependabotProxy, IFileProvider fileProvider, IFeedManagerIO feedManagerIo) { this.logger = logger; this.dotnet = dotnet; this.fileProvider = fileProvider; this.feedManagerIo = feedManagerIo; - privateRegistryFeeds = dependabotProxy?.RegistryURLs.ToImmutableHashSet() ?? []; + privateRegistryFeeds = dependabotProxy?.RegistryURLs ?? []; HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0; + privateRegistryBaseFeeds = dependabotProxy?.RegistryBaseURLs ?? []; + hasPrivateRegistryBaseFeeds = privateRegistryBaseFeeds.Count > 0; + + DefaultFeeds = hasPrivateRegistryBaseFeeds + ? privateRegistryBaseFeeds + : [PublicApiNugetOrgFeed]; emptyPackageDirectory = new DependencyDirectory("empty", "empty package", logger); lazyExplicitFeeds = new Lazy>(GetExplicitFeeds); @@ -96,6 +119,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) @@ -103,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(); @@ -124,10 +162,18 @@ private IEnumerable GetFeeds(Func> getNugetFeeds) continue; } - if (!string.IsNullOrWhiteSpace(url)) + if (hasPrivateRegistryBaseFeeds && IsNugetOrgFeed(url)) { - yield return 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; } } @@ -266,22 +312,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 +345,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/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.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. diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/PackagesConfigRestorer.cs index d4403bb955ef..861622ca4c02 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,18 @@ private bool TryRestoreNugetPackage(string packagesConfig) List sourcesArgument = []; var feedsToUse = feedManager.FeedsToUse(packagesConfig).ToList(); - var useDefaultFeed = feedsToUse.Count == 0 && IsDefaultFeedReachable; + 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 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(defaultFeeds); } var restoreFeeds = feedManager.RestoreFeeds(feedsToUse); sourcesArgument = restoreFeeds.SelectMany(feed => ["-Source", feed]).ToList(); diff --git a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs index 9c8c762f5989..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 @@ -135,11 +152,17 @@ public void TestDependabotRegistryUrls1() // Verify Assert.NotNull(proxy); - Assert.Equal([], proxy.RegistryURLs); + Assert.Empty(proxy.RegistryURLs); + 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 @@ -158,8 +181,16 @@ public void TestDependabotRegistryUrls2() Assert.Equal([ "https://nuget.pkg.github.com/org/index.json" ], proxy.RegistryURLs); + 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() { @@ -180,6 +211,40 @@ public void TestDependabotRegistryUrls3() Assert.Equal([ "https://example.com/org/index.json" ], proxy.RegistryURLs); + 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 TestDependabotRegistryUrlsReplacesBase() + { + // 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..425024425332 100644 --- a/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs +++ b/csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs @@ -1,8 +1,10 @@ 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; namespace Semmle.Extraction.Tests @@ -10,9 +12,21 @@ 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; + 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() { } } @@ -54,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() @@ -66,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() { @@ -83,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() { @@ -99,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() { @@ -118,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() { @@ -135,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() { @@ -151,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() { @@ -168,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() { @@ -183,5 +230,131 @@ public void TestFeedsToUse() "https://feed.from/folder1" ], 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 TestDefaultFeedsNugetOrg() + { + // Setup + 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); + } + + /// + /// 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 TestDefaultFeedsPrivateRegistries() + { + // 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 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); + Assert.Equal([ + "https://example.com/registry1", + "https://example.com/base2" + ], 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 TestNugetOrgNotReplaced() + { + // 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); + + } + + /// + /// 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() + { + // Setup + var logger = new LoggerStub(); + 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"]); + 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); + } } } 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..a2cf9b41be26 --- /dev/null +++ b/csharp/ql/lib/change-notes/2026-09-03-replaces-base.md @@ -0,0 +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.