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.