Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
/// </summary>
public bool CheckNugetFeedResponsiveness { get; } = EnvironmentVariables.GetBooleanOptOut(EnvironmentVariableNames.CheckNugetFeedResponsiveness);

private readonly Lazy<ImmutableHashSet<Uri>> lazyPrivateRegistryUris;
private ImmutableHashSet<Uri> PrivateRegistryUris => lazyPrivateRegistryUris.Value;

private readonly Lazy<ImmutableHashSet<string>> lazyExplicitFeeds;

/// <summary>
Expand Down Expand Up @@ -96,6 +99,7 @@
this.fileProvider = fileProvider;
this.feedManagerIo = feedManagerIo;
privateRegistryFeeds = registryProxy?.RegistryURLs ?? [];
lazyPrivateRegistryUris = MakePrivateRegistryUris();
HasPrivateRegistryFeeds = privateRegistryFeeds.Count > 0;
privateRegistryBaseFeeds = registryProxy?.RegistryBaseURLs ?? [];
hasPrivateRegistryBaseFeeds = privateRegistryBaseFeeds.Count > 0;
Expand Down Expand Up @@ -123,6 +127,20 @@
{
}

private Lazy<ImmutableHashSet<Uri>> MakePrivateRegistryUris() =>
new Lazy<ImmutableHashSet<Uri>>(() =>
{
var uris = ImmutableHashSet.CreateBuilder<Uri>();
foreach (var registry in privateRegistryFeeds)
{
if (Uri.TryCreate(registry, UriKind.Absolute, out var uri))
{
uris.Add(uri);
}
}
Comment thread
michaelnebel marked this conversation as resolved.
Dismissed
return uris.ToImmutable();
});

private bool IsNugetOrgFeed(string url)
{
try
Expand Down Expand Up @@ -246,6 +264,34 @@
return RestoreFeeds(feedsToUse);
}

private bool IsPrivateRegistryFeed(string feed) =>
privateRegistryFeeds.Contains(feed) ||
(Uri.TryCreate(feed, UriKind.Absolute, out var uri) && PrivateRegistryUris.Contains(uri));

/// <summary>
/// Constructs the NuGet CLI restore sources, excluding private registries that require the registry proxy.
/// An empty list leaves source selection to NuGet only when no filtering or default feeds are needed.
/// </summary>
public List<string> MakeNugetExeRestoreFeeds(string path)
{
var feedsToUse = FeedsToUse(path).Where(feed => !IsPrivateRegistryFeed(feed)).ToList();
var defaultFeeds = (CheckNugetFeedResponsiveness ? ReachableDefaultFeeds : DefaultFeeds)
.Where(feed => !IsPrivateRegistryFeed(feed)).ToList();
var useDefaultFeeds = feedsToUse.Count == 0 && defaultFeeds.Count > 0;

if (!CheckNugetFeedResponsiveness && !HasPrivateRegistryFeeds && !useDefaultFeeds)
{
return [];
}

if (useDefaultFeeds)
{
feedsToUse.AddRange(defaultFeeds);
}

return RestoreFeeds(feedsToUse);
}

private (int initialTimeout, int tryCount) GetFeedRequestSettings(bool isFallback)
{
int timeoutMilliSeconds = isFallback && int.TryParse(Environment.GetEnvironmentVariable(EnvironmentVariableNames.NugetFeedResponsivenessInitialTimeoutForFallback), out timeoutMilliSeconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,24 +163,8 @@ private bool TryRestoreNugetPackage(string packagesConfig)
{
logger.LogInfo($"Restoring file \"{packagesConfig}\"...");

List<string> sourcesArgument = [];
var feedsToUse = feedManager.FeedsToUse(packagesConfig).ToList();
var defaultFeeds = feedManager.CheckNugetFeedResponsiveness
? feedManager.ReachableDefaultFeeds
: feedManager.DefaultFeeds;
var useDefaultFeeds = feedsToUse.Count == 0 && defaultFeeds.Count > 0;

// Explicitly construct the sources to be used for the restore command when checking feed
// responsiveness, using private registries, or falling back to default feeds.
if (feedManager.CheckNugetFeedResponsiveness || feedManager.HasPrivateRegistryFeeds || useDefaultFeeds)
{
if (useDefaultFeeds)
{
feedsToUse.AddRange(defaultFeeds);
}
var restoreFeeds = feedManager.RestoreFeeds(feedsToUse);
sourcesArgument = restoreFeeds.SelectMany<string, string>(feed => ["-Source", feed]).ToList();
}
var sourcesArgument = feedManager.MakeNugetExeRestoreFeeds(packagesConfig)
.SelectMany<string, string>(feed => ["-Source", feed]).ToList();

/* Use nuget.exe to install a package.
* Note that there is a clutch of NuGet assemblies which could be used to
Expand Down
153 changes: 153 additions & 0 deletions csharp/extractor/Semmle.Extraction.Tests/FeedManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,159 @@ public void TestFeedsToUse()
], feedsToUse);
}

/// <summary>
/// Verify that NuGet CLI restore excludes private registries and unreachable feeds,
/// while leaving private registries available to dotnet restore.
/// </summary>
[Fact]
public void TestNugetExeRestoreFeedsExcludePrivateRegistries()
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [],
["E https://example.com/registry2", "E https://feed.from/folder1", "E https://feed.from/folder2"]);
using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(),
new FileProviderStub(), new FeedManagerIOStub(["https://feed.from/folder2"]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");
var dotnetFeeds = feedManager.MakeRestoreFeeds("/path/to/project.csproj");

// Verify
Assert.Equal(["https://feed.from/folder1"], feeds);
Assert.Contains("https://example.com/registry2", dotnetFeeds);
}

/// <summary>
/// Verify that NuGet CLI restore excludes private registries that replace NuGet.org,
/// retaining other configured feeds.
/// </summary>
[Fact]
public void TestNugetExeRestoreFeedsExcludePrivateBaseRegistries()
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [],
["E https://api.nuget.org/v3/index.json", "E https://feed.from/folder1"]);
using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStubWithBaseUrls(),
new FileProviderStub(), new FeedManagerIOStub([]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
Assert.Equal(["https://feed.from/folder1"], feeds);
}

/// <summary>
/// Verify that NuGet CLI restore uses an empty local source when private registries replace the default feeds,
/// regardless of their reachability.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void TestNugetExeRestoreFeedsWithOnlyPrivateRegistries(bool unreachable)
{
// Setup
var logger = new LoggerStub();
var registryProxy = new RegistryProxyStubWithBaseUrls();
var dotnet = new DotNetStub([], [], [], ["E https://api.nuget.org/v3/index.json"]);
using var feedManager = new FeedManager(logger, dotnet, registryProxy,
new FileProviderStub(), new FeedManagerIOStub(unreachable ? registryProxy.RegistryURLs.ToList() : []));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
var source = Assert.Single(feeds);
Assert.True(Path.IsPathFullyQualified(source));
Assert.True(Directory.Exists(source));
}

/// <summary>
/// Verify that NuGet CLI restore explicitly selects reachable feeds when no private registries are configured.
/// </summary>
[Fact]
public void TestNugetExeRestoreFeedsWithoutPrivateRegistries()
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [], ["E https://feed.from/folder1"]);
using var feedManager = new FeedManager(logger, dotnet, null,
new FileProviderStub(), new FeedManagerIOStub([]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
Assert.Equal(["https://feed.from/folder1"], feeds);
}

/// <summary>
/// Verify that NuGet CLI restore falls back to the default NuGet.org feed when no feeds are configured.
/// </summary>
[Fact]
public void TestNugetExeRestoreFeedsUseDefaults()
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [], []);
using var feedManager = new FeedManager(logger, dotnet, null,
new FileProviderStub(), new FeedManagerIOStub([]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds);
}

/// <summary>
/// Verify that non-base private registries do not prevent NuGet CLI restore from falling back to NuGet.org
/// when no usable folder feeds remain.
/// </summary>
[Theory]
[InlineData(false)]
[InlineData(true)]
public void TestNugetExeRestoreFeedsUseDefaultsWithPrivateRegistries(bool unreachableFolderFeed)
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [],
unreachableFolderFeed ? ["E https://feed.from/folder1"] : []);
using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(),
new FileProviderStub(), new FeedManagerIOStub(["https://feed.from/folder1"]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds);
}

/// <summary>
/// Verify that NuGet CLI restore identifies equivalent private registry URLs regardless of host case
/// or explicit default ports, while preserving case-sensitive registry paths.
/// </summary>
[Theory]
[InlineData("https://EXAMPLE.COM/registry2")]
[InlineData("https://example.com:443/registry2")]
[InlineData("https://EXAMPLE.COM:443/registry2")]
public void TestNugetExeRestoreFeedsCompareRegistryUris(string folderFeed)
{
// Setup
var logger = new LoggerStub();
var dotnet = new DotNetStub([], [], [], [$"E {folderFeed}"]);
using var feedManager = new FeedManager(logger, dotnet, new RegistryProxyStub(),
new FileProviderStub(), new FeedManagerIOStub([]));

// Execute
var feeds = feedManager.MakeNugetExeRestoreFeeds("/path/to/packages.config");

// Verify
Assert.Equal(["https://api.nuget.org/v3/index.json"], feeds);
}

/// <summary>
/// Verify that `FeedManager` correctly computes the default feeds and reachable default feeds
/// when no private registries are configured.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
category: minorAnalysis
---

- The C# extractor continues to use the NuGet CLI to restore dependencies from `packages.config` files when private NuGet registries are configured, but excludes those private registries from the CLI's sources. Other applicable feeds remain available, while private registries continue to be used by `dotnet restore`.
Loading