Skip to content

Commit 0f7fc90

Browse files
committed
C#: Check fallback nuget feeds before trying to use them in the fallback restore process
1 parent 161f586 commit 0f7fc90

File tree

10 files changed

+180
-31
lines changed

10 files changed

+180
-31
lines changed

csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.Nuget.cs

Lines changed: 129 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.IO;
44
using System.Linq;
55
using System.Net.Http;
6+
using System.Text;
67
using System.Text.RegularExpressions;
78
using System.Threading;
89
using System.Threading.Tasks;
@@ -14,12 +15,13 @@ public sealed partial class DependencyManager
1415
{
1516
private void RestoreNugetPackages(List<FileInfo> allNonBinaryFiles, IEnumerable<string> allProjects, IEnumerable<string> allSolutions, HashSet<AssemblyLookupLocation> dllLocations)
1617
{
18+
var checkNugetFeedResponsiveness = EnvironmentVariables.GetBoolean(EnvironmentVariableNames.CheckNugetFeedResponsiveness);
1719
try
1820
{
19-
var checkNugetFeedResponsiveness = EnvironmentVariables.GetBoolean(EnvironmentVariableNames.CheckNugetFeedResponsiveness);
2021
if (checkNugetFeedResponsiveness && !CheckFeeds(allNonBinaryFiles))
2122
{
22-
DownloadMissingPackages(allNonBinaryFiles, dllLocations, withNugetConfig: false);
23+
// 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.
24+
DownloadMissingPackagesFromSpecificFeeds(allNonBinaryFiles, dllLocations);
2325
return;
2426
}
2527

@@ -75,7 +77,35 @@ private void RestoreNugetPackages(List<FileInfo> allNonBinaryFiles, IEnumerable<
7577
dllLocations.UnionWith(paths.Select(p => new AssemblyLookupLocation(p)));
7678

7779
LogAllUnusedPackages(dependencies);
78-
DownloadMissingPackages(allNonBinaryFiles, dllLocations);
80+
81+
if (checkNugetFeedResponsiveness)
82+
{
83+
DownloadMissingPackagesFromSpecificFeeds(allNonBinaryFiles, dllLocations);
84+
}
85+
else
86+
{
87+
DownloadMissingPackages(allNonBinaryFiles, dllLocations);
88+
}
89+
}
90+
91+
internal const string PublicNugetFeed = "https://api.nuget.org/v3/index.json";
92+
93+
private List<string> GetReachableFallbackNugetFeeds()
94+
{
95+
var fallbackFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.FallbackNugetFeeds).ToHashSet();
96+
if (fallbackFeeds.Count == 0)
97+
{
98+
fallbackFeeds.Add(PublicNugetFeed);
99+
}
100+
101+
logger.LogInfo("Checking fallback Nuget feed reachability");
102+
var reachableFallbackFeeds = fallbackFeeds.Where(feed => IsFeedReachable(feed)).ToList();
103+
if (reachableFallbackFeeds.Count == 0)
104+
{
105+
logger.LogWarning("No fallback Nuget feeds are reachable. Skipping fallback Nuget package restoration.");
106+
}
107+
108+
return reachableFallbackFeeds;
79109
}
80110

81111
/// <summary>
@@ -148,7 +178,16 @@ private void RestoreProjects(IEnumerable<string> projects, out IEnumerable<strin
148178
CompilationInfos.Add(("Failed project restore with package source error", nugetSourceFailures.ToString()));
149179
}
150180

151-
private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<AssemblyLookupLocation> dllLocations, bool withNugetConfig = true)
181+
private void DownloadMissingPackagesFromSpecificFeeds(List<FileInfo> allNonBinaryFiles, HashSet<AssemblyLookupLocation> dllLocations)
182+
{
183+
var reachableFallbackFeeds = GetReachableFallbackNugetFeeds();
184+
if (reachableFallbackFeeds.Count > 0)
185+
{
186+
DownloadMissingPackages(allNonBinaryFiles, dllLocations, withNugetConfig: false, fallbackNugetFeeds: reachableFallbackFeeds);
187+
}
188+
}
189+
190+
private void DownloadMissingPackages(List<FileInfo> allFiles, HashSet<AssemblyLookupLocation> dllLocations, bool withNugetConfig = true, IEnumerable<string>? fallbackNugetFeeds = null)
152191
{
153192
var alreadyDownloadedPackages = GetRestoredPackageDirectoryNames(packageDirectory.DirInfo);
154193
var alreadyDownloadedLegacyPackages = GetRestoredLegacyPackageNames();
@@ -181,9 +220,10 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<AssemblyLooku
181220
}
182221

183222
logger.LogInfo($"Found {notYetDownloadedPackages.Count} packages that are not yet restored");
223+
using var tempDir = new TemporaryDirectory(ComputeTempDirectory(sourceDir.FullName, "nugetconfig"));
184224
var nugetConfig = withNugetConfig
185225
? GetNugetConfig(allFiles)
186-
: null;
226+
: CreateFallbackNugetConfig(fallbackNugetFeeds, tempDir.DirInfo.FullName);
187227

188228
CompilationInfos.Add(("Fallback nuget restore", notYetDownloadedPackages.Count.ToString()));
189229

@@ -209,6 +249,33 @@ private void DownloadMissingPackages(List<FileInfo> allFiles, ISet<AssemblyLooku
209249
dllLocations.Add(missingPackageDirectory.DirInfo.FullName);
210250
}
211251

252+
private string? CreateFallbackNugetConfig(IEnumerable<string>? fallbackNugetFeeds, string folderPath)
253+
{
254+
if (fallbackNugetFeeds is null)
255+
{
256+
// We're not overriding the inherited Nuget feeds
257+
return null;
258+
}
259+
260+
var sb = new StringBuilder();
261+
fallbackNugetFeeds.ForEach((feed, index) => sb.AppendLine($"<add key=\"feed{index}\" value=\"{feed}\" />"));
262+
263+
var nugetConfigPath = Path.Combine(folderPath, "nuget.config");
264+
logger.LogInfo($"Creating fallback nuget.config file {nugetConfigPath}.");
265+
File.WriteAllText(nugetConfigPath,
266+
$"""
267+
<?xml version="1.0" encoding="utf-8"?>
268+
<configuration>
269+
<packageSources>
270+
<clear />
271+
{sb}
272+
</packageSources>
273+
</configuration>
274+
""");
275+
276+
return nugetConfigPath;
277+
}
278+
212279
private string[] GetAllNugetConfigs(List<FileInfo> allFiles) => allFiles.SelectFileNamesByName("nuget.config").ToArray();
213280

214281
private string? GetNugetConfig(List<FileInfo> allFiles)
@@ -429,18 +496,18 @@ private bool IsFeedReachable(string feed)
429496
private bool CheckFeeds(List<FileInfo> allFiles)
430497
{
431498
logger.LogInfo("Checking Nuget feeds...");
432-
var feeds = GetAllFeeds(allFiles);
499+
var (explicitFeeds, allFeeds) = GetAllFeeds(allFiles);
500+
var inheritedFeeds = allFeeds.Except(explicitFeeds).ToHashSet();
433501

434-
var excludedFeeds = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ExcludedNugetFeedsFromResponsivenessCheck)
435-
?.Split(" ", StringSplitOptions.RemoveEmptyEntries)
502+
var excludedFeeds = EnvironmentVariables.GetURLs(EnvironmentVariableNames.ExcludedNugetFeedsFromResponsivenessCheck)
436503
.ToHashSet() ?? [];
437504

438505
if (excludedFeeds.Count > 0)
439506
{
440507
logger.LogInfo($"Excluded Nuget feeds from responsiveness check: {string.Join(", ", excludedFeeds.OrderBy(f => f))}");
441508
}
442509

443-
var allFeedsReachable = feeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed));
510+
var allFeedsReachable = explicitFeeds.All(feed => excludedFeeds.Contains(feed) || IsFeedReachable(feed));
444511
if (!allFeedsReachable)
445512
{
446513
logger.LogWarning("Found unreachable Nuget feed in C# analysis with build-mode 'none'. This may cause missing dependencies in the analysis.");
@@ -454,13 +521,19 @@ private bool CheckFeeds(List<FileInfo> allFiles)
454521
));
455522
}
456523
CompilationInfos.Add(("All Nuget feeds reachable", allFeedsReachable ? "1" : "0"));
524+
525+
if (inheritedFeeds.Count > 0)
526+
{
527+
logger.LogInfo($"Inherited Nuget feeds: {string.Join(", ", inheritedFeeds.OrderBy(f => f))}");
528+
CompilationInfos.Add(("Inherited Nuget feed count", inheritedFeeds.Count.ToString()));
529+
}
530+
457531
return allFeedsReachable;
458532
}
459533

460-
private IEnumerable<string> GetFeeds(string nugetConfig)
534+
private IEnumerable<string> GetFeeds(Func<IList<string>> getNugetFeeds)
461535
{
462-
logger.LogInfo($"Getting Nuget feeds from '{nugetConfig}'...");
463-
var results = dotnet.GetNugetFeeds(nugetConfig);
536+
var results = getNugetFeeds();
464537
var regex = EnabledNugetFeed();
465538
foreach (var result in results)
466539
{
@@ -479,27 +552,63 @@ private IEnumerable<string> GetFeeds(string nugetConfig)
479552
continue;
480553
}
481554

482-
yield return url;
555+
if (!string.IsNullOrWhiteSpace(url))
556+
{
557+
yield return url;
558+
}
483559
}
484560
}
485561

486-
private HashSet<string> GetAllFeeds(List<FileInfo> allFiles)
562+
private (HashSet<string>, HashSet<string>) GetAllFeeds(List<FileInfo> allFiles)
487563
{
564+
IList<string> GetNugetFeeds(string nugetConfig)
565+
{
566+
logger.LogInfo($"Getting Nuget feeds from '{nugetConfig}'...");
567+
return dotnet.GetNugetFeeds(nugetConfig);
568+
}
569+
570+
IList<string> GetNugetFeedsFromFolder(string folderPath)
571+
{
572+
logger.LogInfo($"Getting Nuget feeds in folder '{folderPath}'...");
573+
return dotnet.GetNugetFeedsFromFolder(folderPath);
574+
}
575+
488576
var nugetConfigs = GetAllNugetConfigs(allFiles);
489-
var feeds = nugetConfigs
490-
.SelectMany(GetFeeds)
491-
.Where(str => !string.IsNullOrWhiteSpace(str))
577+
var explicitFeeds = nugetConfigs
578+
.SelectMany(config => GetFeeds(() => GetNugetFeeds(config)))
492579
.ToHashSet();
493580

494-
if (feeds.Count > 0)
581+
if (explicitFeeds.Count > 0)
495582
{
496-
logger.LogInfo($"Found {feeds.Count} Nuget feeds in nuget.config files: {string.Join(", ", feeds.OrderBy(f => f))}");
583+
logger.LogInfo($"Found {explicitFeeds.Count} Nuget feeds in nuget.config files: {string.Join(", ", explicitFeeds.OrderBy(f => f))}");
497584
}
498585
else
499586
{
500587
logger.LogDebug("No Nuget feeds found in nuget.config files.");
501588
}
502-
return feeds;
589+
590+
// todo: this could be improved.
591+
// 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.
592+
var allFeeds = nugetConfigs
593+
.Select(config =>
594+
{
595+
try
596+
{
597+
return new FileInfo(config).Directory?.FullName;
598+
}
599+
catch (Exception exc)
600+
{
601+
logger.LogWarning($"Failed to get directory of '{config}': {exc}");
602+
}
603+
return null;
604+
})
605+
.Where(folder => folder != null)
606+
.SelectMany(folder => GetFeeds(() => GetNugetFeedsFromFolder(folder!)))
607+
.ToHashSet();
608+
609+
logger.LogInfo($"Found {allFeeds.Count} Nuget feeds (with inherited ones) in nuget.config files: {string.Join(", ", allFeeds.OrderBy(f => f))}");
610+
611+
return (explicitFeeds, allFeeds);
503612
}
504613

505614
[GeneratedRegex(@"<TargetFramework>.*</TargetFramework>", RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.Singleline)]

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,9 @@ public bool AddPackage(string folder, string package)
9595

9696
public IList<string> GetListedSdks() => GetResultList("--list-sdks");
9797

98-
private IList<string> GetResultList(string args)
98+
private IList<string> GetResultList(string args, string? workingDirectory = null)
9999
{
100-
if (dotnetCliInvoker.RunCommand(args, out var results))
100+
if (dotnetCliInvoker.RunCommand(args, workingDirectory, out var results))
101101
{
102102
return results;
103103
}
@@ -111,7 +111,11 @@ public bool Exec(string execArgs)
111111
return dotnetCliInvoker.RunCommand(args);
112112
}
113113

114-
public IList<string> GetNugetFeeds(string nugetConfig) => GetResultList($"nuget list source --format Short --configfile \"{nugetConfig}\"");
114+
private const string nugetListSourceCommand = "nuget list source --format Short";
115+
116+
public IList<string> GetNugetFeeds(string nugetConfig) => GetResultList($"{nugetListSourceCommand} --configfile \"{nugetConfig}\"");
117+
118+
public IList<string> GetNugetFeedsFromFolder(string folderPath) => GetResultList(nugetListSourceCommand, folderPath);
115119

116120
// The version number should be kept in sync with the version .NET version used for building the application.
117121
public const string LatestDotNetSdkVersion = "8.0.101";

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,41 +21,49 @@ public DotNetCliInvoker(ILogger logger, string exec)
2121
this.Exec = exec;
2222
}
2323

24-
private ProcessStartInfo MakeDotnetStartInfo(string args)
24+
private ProcessStartInfo MakeDotnetStartInfo(string args, string? workingDirectory)
2525
{
2626
var startInfo = new ProcessStartInfo(Exec, args)
2727
{
2828
UseShellExecute = false,
2929
RedirectStandardOutput = true,
3030
RedirectStandardError = true
3131
};
32+
if (!string.IsNullOrWhiteSpace(workingDirectory))
33+
{
34+
startInfo.WorkingDirectory = workingDirectory;
35+
}
3236
// Set the .NET CLI language to English to avoid localized output.
3337
startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en";
3438
startInfo.EnvironmentVariables["MSBUILDDISABLENODEREUSE"] = "1";
3539
startInfo.EnvironmentVariables["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "true";
3640
return startInfo;
3741
}
3842

39-
private bool RunCommandAux(string args, out IList<string> output)
43+
private bool RunCommandAux(string args, string? workingDirectory, out IList<string> output)
4044
{
41-
logger.LogInfo($"Running {Exec} {args}");
42-
var pi = MakeDotnetStartInfo(args);
45+
var dirLog = string.IsNullOrWhiteSpace(workingDirectory) ? "" : $" in {workingDirectory}";
46+
logger.LogInfo($"Running {Exec} {args}{dirLog}");
47+
var pi = MakeDotnetStartInfo(args, workingDirectory);
4348
var threadId = Environment.CurrentManagedThreadId;
4449
void onOut(string s) => logger.LogInfo(s, threadId);
4550
void onError(string s) => logger.LogError(s, threadId);
4651
var exitCode = pi.ReadOutput(out output, onOut, onError);
4752
if (exitCode != 0)
4853
{
49-
logger.LogError($"Command {Exec} {args} failed with exit code {exitCode}");
54+
logger.LogError($"Command {Exec} {args}{dirLog} failed with exit code {exitCode}");
5055
return false;
5156
}
5257
return true;
5358
}
5459

5560
public bool RunCommand(string args) =>
56-
RunCommandAux(args, out _);
61+
RunCommandAux(args, null, out _);
5762

5863
public bool RunCommand(string args, out IList<string> output) =>
59-
RunCommandAux(args, out output);
64+
RunCommandAux(args, null, out output);
65+
66+
public bool RunCommand(string args, string? workingDirectory, out IList<string> output) =>
67+
RunCommandAux(args, workingDirectory, out output);
6068
}
6169
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ internal class EnvironmentVariableNames
3737
/// </summary>
3838
public const string NugetFeedResponsivenessRequestCount = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_NUGET_FEEDS_CHECK_LIMIT";
3939

40+
/// <summary>
41+
/// Specifies the NuGet feeds to use for fallback Nuget dependency fetching. The value is a space-separated list of feed URLs.
42+
/// The default value is `https://api.nuget.org/v3/index.json`.
43+
/// </summary>
44+
public const string FallbackNugetFeeds = "CODEQL_EXTRACTOR_CSHARP_BUILDLESS_NUGET_FEEDS_FALLBACK";
45+
4046
/// <summary>
4147
/// Specifies the location of the diagnostic directory.
4248
/// </summary>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public interface IDotNet
1414
IList<string> GetListedSdks();
1515
bool Exec(string execArgs);
1616
IList<string> GetNugetFeeds(string nugetConfig);
17+
IList<string> GetNugetFeedsFromFolder(string folderPath);
1718
}
1819

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

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,11 @@ internal interface IDotNetCliInvoker
1919
/// The output of the command is returned in `output`.
2020
/// </summary>
2121
bool RunCommand(string args, out IList<string> output);
22+
23+
/// <summary>
24+
/// Execute `dotnet <args>` in `<workingDirectory>` and return true if the command succeeded, otherwise false.
25+
/// The output of the command is returned in `output`.
26+
/// </summary>
27+
bool RunCommand(string args, string? workingDirectory, out IList<string> output);
2228
}
2329
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ private void RunMonoNugetCommand(string command, out IList<string> stdout)
243243
private void AddDefaultPackageSource(string nugetConfig)
244244
{
245245
logger.LogInfo("Adding default package source...");
246-
RunMonoNugetCommand($"sources add -Name DefaultNugetOrg -Source https://api.nuget.org/v3/index.json -ConfigFile \"{nugetConfig}\"", out var _);
246+
RunMonoNugetCommand($"sources add -Name DefaultNugetOrg -Source {DependencyManager.PublicNugetFeed} -ConfigFile \"{nugetConfig}\"", out var _);
247247
}
248248

249249
public void Dispose()

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ internal class DotNetCliInvokerStub : IDotNetCliInvoker
1010
{
1111
private readonly IList<string> output;
1212
private string lastArgs = "";
13+
public string WorkingDirectory { get; private set; } = "";
1314
public bool Success { get; set; } = true;
1415

1516
public DotNetCliInvokerStub(IList<string> output)
@@ -32,6 +33,12 @@ public bool RunCommand(string args, out IList<string> output)
3233
return Success;
3334
}
3435

36+
public bool RunCommand(string args, string? workingDirectory, out IList<string> output)
37+
{
38+
WorkingDirectory = workingDirectory ?? "";
39+
return RunCommand(args, out output);
40+
}
41+
3542
public string GetLastArgs() => lastArgs;
3643
}
3744

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public DotNetStub(IList<string> runtimes, IList<string> sdks)
2828
public bool Exec(string execArgs) => true;
2929

3030
public IList<string> GetNugetFeeds(string nugetConfig) => [];
31+
32+
public IList<string> GetNugetFeedsFromFolder(string folderPath) => [];
3133
}
3234

3335
public class RuntimeTests

0 commit comments

Comments
 (0)