Skip to content

Commit 2aee47b

Browse files
authored
Merge pull request #18850 from github/mbg/csharp/inject-proxy-urls
C#: Automatically use configured private registry feeds
2 parents 9dd7b20 + fe1c098 commit 2aee47b

File tree

8 files changed

+154
-37
lines changed

8 files changed

+154
-37
lines changed

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
using System;
2-
using System.Diagnostics;
2+
using System.Collections.Generic;
33
using System.IO;
44
using System.Security.Cryptography.X509Certificates;
55
using Semmle.Util;
66
using Semmle.Util.Logging;
7+
using Newtonsoft.Json;
78

89
namespace Semmle.Extraction.CSharp.DependencyFetching
910
{
1011
public class DependabotProxy : IDisposable
1112
{
13+
/// <summary>
14+
/// Represents configurations for package registries.
15+
/// </summary>
16+
/// <param name="Type">The type of package registry.</param>
17+
/// <param name="URL">The URL of the package registry.</param>
18+
public record class RegistryConfig(string Type, string URL);
19+
1220
private readonly string host;
1321
private readonly string port;
1422

@@ -17,6 +25,10 @@ public class DependabotProxy : IDisposable
1725
/// </summary>
1826
internal string Address { get; }
1927
/// <summary>
28+
/// The URLs of package registries that are configured for the proxy.
29+
/// </summary>
30+
internal HashSet<string> RegistryURLs { get; }
31+
/// <summary>
2032
/// The path to the temporary file where the certificate is stored.
2133
/// </summary>
2234
internal string? CertificatePath { get; private set; }
@@ -67,6 +79,39 @@ public class DependabotProxy : IDisposable
6779
result.Certificate = X509Certificate2.CreateFromPem(cert);
6880
}
6981

82+
// Try to obtain the list of private registry URLs.
83+
var registryURLs = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs);
84+
85+
if (!string.IsNullOrWhiteSpace(registryURLs))
86+
{
87+
try
88+
{
89+
// The value of the environment variable should be a JSON array of objects, such as:
90+
// [ { "type": "nuget_feed", "url": "https://nuget.pkg.github.com/org/index.json" } ]
91+
var array = JsonConvert.DeserializeObject<List<RegistryConfig>>(registryURLs);
92+
if (array is not null)
93+
{
94+
foreach (RegistryConfig config in array)
95+
{
96+
// The array contains all configured private registries, not just ones for C#.
97+
// We ignore the non-C# ones here.
98+
if (!config.Type.Equals("nuget_feed"))
99+
{
100+
logger.LogDebug($"Ignoring registry at '{config.URL}' since it is not of type 'nuget_feed'.");
101+
continue;
102+
}
103+
104+
logger.LogInfo($"Found private registry at '{config.URL}'");
105+
result.RegistryURLs.Add(config.URL);
106+
}
107+
}
108+
}
109+
catch (JsonException ex)
110+
{
111+
logger.LogError($"Unable to parse '{EnvironmentVariableNames.ProxyURLs}': {ex.Message}");
112+
}
113+
}
114+
70115
return result;
71116
}
72117

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

80126
public void Dispose()

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs

+5-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
65
using Newtonsoft.Json.Linq;
76

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

79+
if (restoreSettings.ExtraArgs is not null)
80+
{
81+
args += $" {restoreSettings.ExtraArgs}";
82+
}
83+
8084
return args;
8185
}
8286

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs

+5
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,10 @@ internal static class EnvironmentVariableNames
8989
/// Contains the certificate used by the Dependabot proxy.
9090
/// </summary>
9191
public const string ProxyCertificate = "CODEQL_PROXY_CA_CERTIFICATE";
92+
93+
/// <summary>
94+
/// Contains the URLs of private nuget registries as a JSON array.
95+
/// </summary>
96+
public const string ProxyURLs = "CODEQL_PROXY_URLS";
9297
}
9398
}

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDotNet.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public interface IDotNet
1717
IList<string> GetNugetFeedsFromFolder(string folderPath);
1818
}
1919

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

2222
public partial record class RestoreResult(bool Success, IList<string> Output)
2323
{

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs

+92-32
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,11 @@ public HashSet<AssemblyLookupLocation> Restore()
103103
compilationInfoContainer.CompilationInfos.Add(("NuGet feed responsiveness checked", checkNugetFeedResponsiveness ? "1" : "0"));
104104

105105
HashSet<string>? explicitFeeds = null;
106+
HashSet<string>? allFeeds = null;
106107

107108
try
108109
{
109-
if (checkNugetFeedResponsiveness && !CheckFeeds(out explicitFeeds))
110+
if (checkNugetFeedResponsiveness && !CheckFeeds(out explicitFeeds, out allFeeds))
110111
{
111112
// 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.
112113
var unresponsiveMissingPackageLocation = DownloadMissingPackagesFromSpecificFeeds([], explicitFeeds);
@@ -156,7 +157,7 @@ public HashSet<AssemblyLookupLocation> Restore()
156157

157158
var restoredProjects = RestoreSolutions(out var container);
158159
var projects = fileProvider.Projects.Except(restoredProjects);
159-
RestoreProjects(projects, out var containers);
160+
RestoreProjects(projects, allFeeds, out var containers);
160161

161162
var dependencies = containers.Flatten(container);
162163

@@ -260,8 +261,33 @@ private IEnumerable<string> RestoreSolutions(out DependencyContainer dependencie
260261
/// Populates dependencies with the relative paths to the assets files generated by the restore.
261262
/// </summary>
262263
/// <param name="projects">A list of paths to project files.</param>
263-
private void RestoreProjects(IEnumerable<string> projects, out ConcurrentBag<DependencyContainer> dependencies)
264+
private void RestoreProjects(IEnumerable<string> projects, HashSet<string>? configuredSources, out ConcurrentBag<DependencyContainer> dependencies)
264265
{
266+
// Conservatively, we only set this to a non-null value if a Dependabot proxy is enabled.
267+
// This ensures that we continue to get the old behaviour where feeds are taken from
268+
// `nuget.config` files instead of the command-line arguments.
269+
string? extraArgs = null;
270+
271+
if (this.dependabotProxy is not null)
272+
{
273+
// If the Dependabot proxy is configured, then our main goal is to make `dotnet` aware
274+
// of the private registry feeds. However, since providing them as command-line arguments
275+
// to `dotnet` ignores other feeds that may be configured, we also need to add the feeds
276+
// we have discovered from analysing `nuget.config` files.
277+
var sources = configuredSources ?? new();
278+
this.dependabotProxy.RegistryURLs.ForEach(url => sources.Add(url));
279+
280+
// Add package sources. If any are present, they override all sources specified in
281+
// the configuration file(s).
282+
var feedArgs = new StringBuilder();
283+
foreach (string source in sources)
284+
{
285+
feedArgs.Append($" -s {source}");
286+
}
287+
288+
extraArgs = feedArgs.ToString();
289+
}
290+
265291
var successCount = 0;
266292
var nugetSourceFailures = 0;
267293
ConcurrentBag<DependencyContainer> collectedDependencies = [];
@@ -276,7 +302,7 @@ private void RestoreProjects(IEnumerable<string> projects, out ConcurrentBag<Dep
276302
foreach (var project in projectGroup)
277303
{
278304
logger.LogInfo($"Restoring project {project}...");
279-
var res = dotnet.Restore(new(project, PackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true, TargetWindows: isWindows));
305+
var res = dotnet.Restore(new(project, PackageDirectory.DirInfo.FullName, ForceDotnetRefAssemblyFetching: true, extraArgs, TargetWindows: isWindows));
280306
assets.AddDependenciesRange(res.AssetsFilePaths);
281307
lock (sync)
282308
{
@@ -680,10 +706,42 @@ private bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount,
680706
return (timeoutMilliSeconds, tryCount);
681707
}
682708

683-
private bool CheckFeeds(out HashSet<string> explicitFeeds)
709+
/// <summary>
710+
/// Checks that we can connect to all Nuget feeds that are explicitly configured in configuration files
711+
/// as well as any private package registry feeds that are configured.
712+
/// </summary>
713+
/// <param name="explicitFeeds">Outputs the set of explicit feeds.</param>
714+
/// <param name="allFeeds">Outputs the set of all feeds (explicit and inherited).</param>
715+
/// <returns>True if all feeds are reachable or false otherwise.</returns>
716+
private bool CheckFeeds(out HashSet<string> explicitFeeds, out HashSet<string> allFeeds)
717+
{
718+
(explicitFeeds, allFeeds) = GetAllFeeds();
719+
HashSet<string> feedsToCheck = explicitFeeds;
720+
721+
// If private package registries are configured for C#, then check those
722+
// in addition to the ones that are configured in `nuget.config` files.
723+
this.dependabotProxy?.RegistryURLs.ForEach(url => feedsToCheck.Add(url));
724+
725+
var allFeedsReachable = this.CheckSpecifiedFeeds(feedsToCheck);
726+
727+
var inheritedFeeds = allFeeds.Except(explicitFeeds).ToHashSet();
728+
if (inheritedFeeds.Count > 0)
729+
{
730+
logger.LogInfo($"Inherited Nuget feeds (not checked for reachability): {string.Join(", ", inheritedFeeds.OrderBy(f => f))}");
731+
compilationInfoContainer.CompilationInfos.Add(("Inherited Nuget feed count", inheritedFeeds.Count.ToString()));
732+
}
733+
734+
return allFeedsReachable;
735+
}
736+
737+
/// <summary>
738+
/// Checks that we can connect to the specified Nuget feeds.
739+
/// </summary>
740+
/// <param name="feeds">The set of package feeds to check.</param>
741+
/// <returns>True if all feeds are reachable or false otherwise.</returns>
742+
private bool CheckSpecifiedFeeds(HashSet<string> feeds)
684743
{
685-
logger.LogInfo("Checking Nuget feeds...");
686-
(explicitFeeds, var allFeeds) = GetAllFeeds();
744+
logger.LogInfo("Checking that Nuget feeds are reachable...");
687745

688746
var excludedFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.ExcludedNugetFeedsFromResponsivenessCheck)
689747
.ToHashSet();
@@ -695,7 +753,7 @@ private bool CheckFeeds(out HashSet<string> explicitFeeds)
695753

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

698-
var allFeedsReachable = explicitFeeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed, initialTimeout, tryCount));
756+
var allFeedsReachable = feeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed, initialTimeout, tryCount));
699757
if (!allFeedsReachable)
700758
{
701759
logger.LogWarning("Found unreachable Nuget feed in C# analysis with build-mode 'none'. This may cause missing dependencies in the analysis.");
@@ -710,14 +768,6 @@ private bool CheckFeeds(out HashSet<string> explicitFeeds)
710768
}
711769
compilationInfoContainer.CompilationInfos.Add(("All Nuget feeds reachable", allFeedsReachable ? "1" : "0"));
712770

713-
714-
var inheritedFeeds = allFeeds.Except(explicitFeeds).ToHashSet();
715-
if (inheritedFeeds.Count > 0)
716-
{
717-
logger.LogInfo($"Inherited Nuget feeds (not checked for reachability): {string.Join(", ", inheritedFeeds.OrderBy(f => f))}");
718-
compilationInfoContainer.CompilationInfos.Add(("Inherited Nuget feed count", inheritedFeeds.Count.ToString()));
719-
}
720-
721771
return allFeedsReachable;
722772
}
723773

@@ -766,23 +816,33 @@ private IEnumerable<string> GetFeeds(Func<IList<string>> getNugetFeeds)
766816
}
767817

768818
// todo: this could be improved.
769-
// 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.
770-
var allFeeds = nugetConfigs
771-
.Select(config =>
772-
{
773-
try
774-
{
775-
return new FileInfo(config).Directory?.FullName;
776-
}
777-
catch (Exception exc)
819+
HashSet<string>? allFeeds = null;
820+
821+
if (nugetConfigs.Count > 0)
822+
{
823+
// 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.
824+
allFeeds = nugetConfigs
825+
.Select(config =>
778826
{
779-
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
780-
}
781-
return null;
782-
})
783-
.Where(folder => folder != null)
784-
.SelectMany(folder => GetFeeds(() => dotnet.GetNugetFeedsFromFolder(folder!)))
785-
.ToHashSet();
827+
try
828+
{
829+
return new FileInfo(config).Directory?.FullName;
830+
}
831+
catch (Exception exc)
832+
{
833+
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
834+
}
835+
return null;
836+
})
837+
.Where(folder => folder != null)
838+
.SelectMany(folder => GetFeeds(() => dotnet.GetNugetFeedsFromFolder(folder!)))
839+
.ToHashSet();
840+
}
841+
else
842+
{
843+
// If we haven't found any `nuget.config` files, then obtain a list of feeds from the root source directory.
844+
allFeeds = GetFeeds(() => dotnet.GetNugetFeedsFromFolder(this.fileProvider.SourceDir.FullName)).ToHashSet();
845+
}
786846

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

csharp/extractor/Semmle.Extraction.Tests/DotNet.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public void TestDotnetRestoreProjectToDirectory2()
123123
var dotnet = MakeDotnet(dotnetCliInvoker);
124124

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

128128
// Verify
129129
var lastArgs = dotnetCliInvoker.GetLastArgs();
@@ -141,7 +141,7 @@ public void TestDotnetRestoreProjectToDirectory3()
141141
var dotnet = MakeDotnet(dotnetCliInvoker);
142142

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

146146
// Verify
147147
var lastArgs = dotnetCliInvoker.GetLastArgs();

csharp/ql/integration-tests/all-platforms/standalone_resx/CompilationInfo.expected

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
| All Nuget feeds reachable | 1.0 |
22
| Failed project restore with package source error | 0.0 |
33
| Failed solution restore with package source error | 0.0 |
4+
| Inherited Nuget feed count | 1.0 |
45
| NuGet feed responsiveness checked | 1.0 |
56
| Project files on filesystem | 1.0 |
67
| Reachable fallback Nuget feed count | 1.0 |

csharp/ql/integration-tests/all-platforms/standalone_winforms/CompilationInfo.expected

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
| All Nuget feeds reachable | 1.0 |
22
| Failed project restore with package source error | 0.0 |
33
| Failed solution restore with package source error | 0.0 |
4+
| Inherited Nuget feed count | 1.0 |
45
| NuGet feed responsiveness checked | 1.0 |
56
| Project files on filesystem | 1.0 |
67
| Reachable fallback Nuget feed count | 1.0 |

0 commit comments

Comments
 (0)