10000 C#: Automatically use configured private registry feeds by mbg · Pull Request #18850 · github/codeql · GitHub
[go: up one dir, main page]

Skip to content

C#: Automatically use configured private registry feeds #18850

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
6b2f348
C#: Add `CODEQL_PROXY_URLS` environment variable
mbg Jan 6, 2025
63d5517
C#: Add list of registries to `DependabotProxy`
mbg Jan 7, 2025
11efb55
C#: Parse environment variables to obtain list of registry URLs
mbg Jan 7, 2025
726123c
C#: Allow specifying package feeds for `dotnet restore` as command li…
mbg Jan 7, 2025
0db6a26
C#: Propagate explicit feeds to `RestoreProjects`
mbg Feb 24, 2025
6b15f77
C#: Fix test failures
mbg Mar 3, 2025
a8dde15
C#: Only provide feeds on command line if Dependabot proxy is enabled
mbg Mar 14, 2025
9560593
C#: Fix `.ToList()` being called on `null`
mbg Mar 14, 2025
b6c74fe
C#: Narrow `Exception` to `JsonException`
mbg Mar 14, 2025
284f612
C#: Use `StringBuilder` for feed arguments in `GetRestoreArgs`
mbg Mar 14, 2025
51874b8
Apply suggestions from code review
mbg Mar 17, 2025
7a92a72
C#: Change `RegistryConfig` to a record class
mbg Mar 18, 2025
d564529
C#: Change `RestoreSettings` to have general `extraArgs` parameter
mbg Mar 24, 2025
92eab47
C#: Refactor `CheckFeeds` to have an overloaded variant that accepts …
mbg Mar 24, 2025
4448369
C#: Check that private package registry feeds are reachable
mbg Mar 24, 2025
7cea2ad
Apply suggestions from code review
mbg Mar 25, 2025
d2b88ae
C#: Rename overloaded `CheckFeeds` method and fix comment
mbg Mar 25, 2025
4d3b024
C#: Do not manually add public feed when private registries are used
mbg Mar 25, 2025
73ca2eb
C#: Use `allFeeds` rather than `explicitFeeds` for `RestoreProjects`
mbg Mar 25, 2025
be95d33
C#: Obtain all feeds from source directory if there are no `nuget.con…
mbg Mar 25, 2025
fe1c098
C#: Accept changes to `.expected` files
mbg Mar 25, 2025
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
10000
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using Semmle.Util;
using Semmle.Util.Logging;
using Newtonsoft.Json;

namespace Semmle.Extraction.CSharp.DependencyFetching
{
public class DependabotProxy : IDisposable
{
/// <summary>
/// Represents configurations for package registries.
/// </summary>
/// <param name="Type">The type of package registry.</param>
/// <param name="URL">The URL of the package registry.</param>
public record class RegistryConfig(string Type, string URL);

private readonly string host;
private readonly string port;

Expand All @@ -17,6 +25,10 @@ public class DependabotProxy : IDisposable
/// </summary>
internal string Address { get; }
/// <summary>
/// The URLs of package registries that are configured for the proxy.
/// </summary>
internal HashSet<string> RegistryURLs { get; }
/// <summary>
/// The path to the temporary file where the certificate is stored.
/// </summary>
internal string? CertificatePath { get; private set; }
Expand Down Expand Up @@ -67,6 +79,39 @@ public class DependabotProxy : IDisposable
result.Certificate = X509Certificate2.CreateFromPem(cert);
}

// Try to obtain the list of private registry URLs.
var registryURLs = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs);

if (!string.IsNullOrWhiteSpace(registryURLs))
{
try
{
// The value of the environment variable should be a JSON array of objects, such as:
// [ { "type": "nuget_feed", "url": "https://nuget.pkg.github.com/org/index.json" } ]
var array = JsonConvert.DeserializeObject<List<RegistryConfig>>(registryURLs);
if (array is not null)
{
foreach (RegistryConfig config in array)
{
// The array contains all configured private registries, not just ones for C#.
// We ignore the non-C# ones here.
if (!config.Type.Equals("nuget_feed"))
{
logger.LogDebug($"Ignoring registry at '{config.URL}' since it is not of type 'nuget_feed'.");
continue;
}

logger.LogInfo($"Found private registry at '{config.URL}'");
result.RegistryURLs.Add(config.URL);
}
}
}
catch (JsonException ex)
{
logger.LogError($"Unable to parse '{EnvironmentVariableNames.ProxyURLs}': {ex.Message}");
}
}

return result;
}

Expand All @@ -75,6 +120,7 @@ private DependabotProxy(string host, string port)
this.host = host;
this.port = port;
this.Address = $"http://{this.host}:{this.port}";
this.RegistryURLs = new HashSet<string>();
}

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Newtonsoft.Json.Linq;

using Semmle.Util;
Expand Down Expand Up @@ -77,6 +76,11 @@ private string GetRestoreArgs(RestoreSettings restoreSettings)
args += " /p:EnableWindowsTargeting=true";
}

if (restoreSettings.ExtraArgs is not null)
{
args += $" {restoreSettings.ExtraArgs}";
}

return args;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,5 +89,10 @@ internal static class EnvironmentVariableNames
/// Contains the certificate used by the Dependabot proxy.
/// </summary>
public const string ProxyCertificate = "CODEQL_PROXY_CA_CERTIFICATE";

/// <summary>
/// Contains the URLs of private nuget registries as a JSON array.
/// </summary>
public const string ProxyURLs = "CODEQL_PROXY_URLS";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IDotNet
IList<string> GetNugetFeedsFromFolder(string folderPath);
}

public record class RestoreSettings(string File, string PackageDirectory, bool ForceDotnetRefAssemblyFetching, string? PathToNugetConfig = null, bool ForceReevaluation = false, bool TargetWindows = false);
public record class RestoreSettings(string File, string PackageDirectory, bool ForceDotnetRefAssemblyFetching, string? ExtraArgs = null, string? PathToNugetConfig = null, bool ForceReevaluation = false, bool TargetWindows = false);

public partial record class RestoreResult(bool Success, IList<string> Output)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,11 @@
compilationInfoContainer.CompilationInfos.Add(("NuGet feed responsiveness checked", checkNugetFeedResponsiveness ? "1" : "0"));

HashSet<string>? explicitFeeds = null;
HashSet<string>? allFeeds = null;

try
{
if (checkNugetFeedResponsiveness && !CheckFeeds(out explicitFeeds))
if (checkNugetFeedResponsiveness && !CheckFeeds(out explicitFeeds, out allFeeds))
{
// todo: we could also check the reachability of the inherited nuget feeds, but to use those in the fallback we would need to handle authentication too.
var unresponsiveMissingPackageLocation = DownloadMissingPackagesFromSpecificFeeds(explicitFeeds);
Expand Down Expand Up @@ -156,7 +157,7 @@

var restoredProjects = RestoreSolutions(out var container);
var projects = fileProvider.Projects.Except(restoredProjects);
RestoreProjects(projects, out var containers);
RestoreProjects(projects, allFeeds, out var containers);

var dependencies = containers.Flatten(container);

Expand Down Expand Up @@ -260,8 +261,33 @@
/// Populates dependencies with the relative paths to the assets files generated by the restore.
/// </summary>
/// <param name="projects">A list of paths to project files.</param>
private void RestoreProjects(IEnumerable<string> projects, out ConcurrentBag<DependencyContainer> dependencies)
private void RestoreProjects(IEnumerable<string> projects, HashSet<string>? configuredSources, out ConcurrentBag<DependencyContainer> dependencies)
{
// Conservatively, we only set this to a non-null value if a Dependabot proxy is enabled.
// This ensures that we continue to get the old behaviour where feeds are taken from
// `nuget.config` files instead of the command-line arguments.
string? extraArgs = null;

if (this.dependabotProxy is not null)
{
// If the Dependabot proxy is configured, then our main goal is to make `dotnet` aware
// of the private registry feeds. However, since providing them as command-line arguments
// to `dotnet` ignores other feeds that may be configured, we also need to add the feeds
// we have discovered from analysing `nuget.config` files.
var sources = configuredSources ?? new();
this.dependabotProxy.RegistryURLs.ForEach(url => sources.Add(url));

// Add package sources. If any are present, they override all sources specified in
// the configuration file(s).
var feedArgs = new StringBuilder();
foreach (string source in sources)
{
feedArgs.Append($" -s {source}");
}

extraArgs = feedArgs.ToString();
}

var successCount = 0;
var nugetSourceFailures = 0;
ConcurrentBag<DependencyContainer> collectedDependencies = [];
Expand All @@ -276,7 +302,7 @@
foreach (var project in projectGroup)
{
logger.LogInfo($"Restoring project {project}...");
var res = dotnet.Restore(new(project, PackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true, TargetWindows: isWindows));
var res = dotnet.Restore(new(project, PackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true, extraArgs, TargetWindows: isWindows));
assets.AddDependenciesRange(res.AssetsFilePaths);
lock (sync)
{
Expand Down Expand Up @@ -674,10 +700,42 @@
return (timeoutMilliSeconds, tryCount);
}

private bool CheckFeeds(out HashSet<string> explicitFeeds)
/// <summary>
/// Checks that we can connect to all Nuget feeds that are explicitly configured in configuration files
/// as well as any private package registry feeds that are configured.
/// </summary>
/// <param name="explicitFeeds">Outputs the set of explicit feeds.</param>
/// <param name="allFeeds">Outputs the set of all feeds (explicit and inherited).</param>
/// <returns>True if all feeds are reachable or false otherwise.</returns>
private bool CheckFeeds(out HashSet<string> explicitFeeds, out HashSet<string> allFeeds)
{
(explicitFeeds, allFeeds) = GetAllFeeds();
HashSet<string> feedsToCheck = explicitFeeds;

// If private package registries are configured for C#, then check those
// in addition to the ones that are configured in `nuget.config` files.
this.dependabotProxy?.RegistryURLs.ForEach(url => feedsToCheck.Add(url));

var allFeedsReachable = this.CheckSpecifiedFeeds(feedsToCheck);

var inheritedFeeds = allFeeds.Except(explicitFeeds).ToHashSet();
if (inheritedFeeds.Count > 0)
{
logger.LogInfo($"Inherited Nuget feeds (not checked for reachability): {string.Join(", ", inheritedFeeds.OrderBy(f => f))}");
compilationInfoContainer.CompilationInfos.Add(("Inherited Nuget feed count", inheritedFeeds.Count.ToString()));
}

return allFeedsReachable;
}

/// <summary>
/// Checks that we can connect to the specified Nuget feeds.
/// </summary>
/// <param name="feeds">The set of package feeds to check.</param>
/// <returns>True if all feeds are reachable or false otherwise.</returns>
private bool CheckSpecifiedFeeds(HashSet<string> feeds)
{
logger.LogInfo("Checking Nuget feeds...");
(explicitFeeds, var allFeeds) = GetAllFeeds();
logger.LogInfo("Checking that Nuget feeds are reachable...");

var excludedFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.ExcludedNugetFeedsFromResponsivenessCheck)
.ToHashSet();
Expand All @@ -689,7 +747,7 @@

var (initialTimeout, tryCount) = GetFeedRequestSettings(isFallback: false);

var allFeedsReachable = explicitFeeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed, initialTimeout, tryCount));
var allFeedsReachable = feeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed, initialTimeout, tryCount));
if (!allFeedsReachable)
{
logger.LogWarning("Found unreachable Nuget feed in C# analysis with build-mode 'none'. This may cause missing dependencies in the analysis.");
Expand All @@ -704,14 +762,6 @@
}
compilationInfoContainer.CompilationInfos.Add(("All Nuget feeds reachable", allFeedsReachable ? "1" : "0"));


var inheritedFeeds = allFeeds.Except(explicitFeeds).ToHashSet();
if (inheritedFeeds.Count > 0)
{
logger.LogInfo($"Inherited Nuget feeds (not checked for reachability): {string.Join(", ", inheritedFeeds.OrderBy(f => f))}");
compilationInfoContainer.CompilationInfos.Add(("Inherited Nuget feed count", inheritedFeeds.Count.ToString()));
}

return allFeedsReachable;
}

Expand Down Expand Up @@ -760,23 +810,33 @@
}

// todo: this could be improved.
// We don't have to get the feeds from each of the folders from below, it would be enought to check the folders that recursively contain the others.
var allFeeds = nugetConfigs
.Select(config =>
{
try
{
return new FileInfo(config).Directory?.FullName;
}
catch (Exception exc)
HashSet<string>? allFeeds = null;

if (nugetConfigs.Count > 0)
{
// We don't have to get the feeds from each of the folders from below, it would be enought to check the folders that recursively contain the others.
allFeeds = nugetConfigs
.Select(config =>
{
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
}
return null;
})
.Where(folder => folder != null)
.SelectMany(folder => GetFeeds(() => dotnet.GetNugetFeedsFromFolder(folder!)))
.ToHashSet();
try
{
return new FileInfo(config).Directory?.FullName;
}
catch (Exception exc)
{
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
}
return null;
})
.Where(folder => folder != null)
.SelectMany(folder => GetFeeds(() => dotnet.GetNugetFeedsFromFolder(folder!)))
.ToHashSet();
}
Comment on lines +831 to +834

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 3 months ago

To fix the problem, we should catch specific exceptions that are likely to be thrown by the operation being performed. In this case, FileInfo(config).Directory?.FullName can throw exceptions such as ArgumentException, PathTooLongException, NotSupportedException, UnauthorizedAccessException, and DirectoryNotFoundException. We should catch these specific exceptions and log them accordingly.

Suggested changeset 1
csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs
--- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs
+++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs
@@ -830,5 +830,21 @@
                         }
-                        catch (Exception exc)
+                        catch (ArgumentException exc)
                         {
-                            logger.LogWarning($"Failed to get directory of '{config}': {exc}");
+                            logger.LogWarning($"Failed to get directory of '{config}' due to an argument exception: {exc}");
+                        }
+                        catch (PathTooLongException exc)
+                        {
+                            logger.LogWarning($"Failed to get directory of '{config}' due to a path too long exception: {exc}");
+                        }
+                        catch (NotSupportedException exc)
+                        {
+                            logger.LogWarning($"Failed to get directory of '{config}' due to a not supported exception: {exc}");
+                        }
+                        catch (UnauthorizedAccessException exc)
+                        {
+                            logger.LogWarning($"Failed to get directory of '{config}' due to an unauthorized access exception: {exc}");
+                        }
+                        catch (DirectoryNotFoundException exc)
+                        {
+                            logger.LogWarning($"Failed to get directory of '{config}' due to a directory not found exception: {exc}");
                         }
EOF
@@ -830,5 +830,21 @@
}
catch (Exception exc)
catch (ArgumentException exc)
{
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
logger.LogWarning($"Failed to get directory of '{config}' due to an argument exception: {exc}");
}
catch (PathTooLongException exc)
{
logger.LogWarning($"Failed to get directory of '{config}' due to a path too long exception: {exc}");
}
catch (NotSupportedException exc)
{
logger.LogWarning($"Failed to get directory of '{config}' due to a not supported exception: {exc}");
}
catch (UnauthorizedAccessException exc)
{
logger.LogWarning($"Failed to get directory of '{config}' due to an unauthorized access exception: {exc}");
}
catch (DirectoryNotFoundException exc)
{
logger.LogWarning($"Failed to get directory of '{config}' due to a directory not found exception: {exc}");
}
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
else
{
// If we haven't found any `nuget.config` files, then obtain a list of feeds from the root source directory.
allFeeds = GetFeeds(() => dotnet.GetNugetFeedsFromFolder(this.fileProvider.SourceDir.FullName)).ToHashSet();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice improvement!

}

logger.LogInfo($"Found {allFeeds.Count} Nuget feeds (with inherited ones) in nuget.config files: {string.Join(", ", allFeeds.OrderBy(f => f))}");

Expand Down
4 changes: 2 additions & 2 deletions csharp/extractor/Semmle.Extraction.Tests/DotNet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public void TestDotnetRestoreProjectToDirectory2()
var dotnet = MakeDotnet(dotnetCliInvoker);

// Execute
var res = dotnet.Restore(new("myproject.csproj", "mypackages", false, "myconfig.config"));
var res = dotnet.Restore(new("myproject.csproj", "mypackages", false, null, "myconfig.config"));

// Verify
var lastArgs = dotnetCliInvoker.GetLastArgs();
Expand All @@ -141,7 +141,7 @@ public void TestDotnetRestoreProjectToDirectory3()
var dotnet = MakeDotnet(dotnetCliInvoker);

// Execute
var res = dotnet.Restore(new("myproject.csproj", "mypackages", false, "myconfig.config", true));
var res = dotnet.Restore(new("myproject.csproj", "mypackages", false, null, "myconfig.config", true));

// Verify
var lastArgs = dotnetCliInvoker.GetLastArgs();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
| All Nuget feeds reachable | 1.0 |
| Failed project restore with package source error | 0.0 |
| Failed solution restore with package source error | 0.0 |
| Inherited Nuget feed count | 1.0 |
| NuGet feed responsiveness checked | 1.0 |
| Project files on filesystem | 1.0 |
| Reachable fallback Nuget feed count | 1.0 |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
| All Nuget feeds reachable | 1.0 |
| Failed project restore with package source error | 0.0 |
| Failed solution restore with package source error | 0.0 |
| Inherited Nuget feed count | 1.0 |
| NuGet feed responsiveness checked | 1.0 |
| Project files on filesystem | 1.0 |
| Reachable fallback Nuget feed count | 1.0 |
Expand Down
0