diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs new file mode 100644 index 000000000000..895bd313ac30 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security.Cryptography.X509Certificates; +using Semmle.Util; +using Semmle.Util.Logging; + +namespace Semmle.Extraction.CSharp.DependencyFetching +{ + public class DependabotProxy : IDisposable + { + private readonly string host; + private readonly string port; + + /// + /// The full address of the Dependabot proxy, if available. + /// + internal string Address { get; } + /// + /// The path to the temporary file where the certificate is stored. + /// + internal string? CertificatePath { get; private set; } + /// + /// The certificate used for the Dependabot proxy. + /// + internal X509Certificate2? Certificate { get; private set; } + + internal static DependabotProxy? GetDependabotProxy(ILogger logger, TemporaryDirectory tempWorkingDirectory) + { + // Setting HTTP(S)_PROXY and SSL_CERT_FILE have no effect on Windows or macOS, + // but we would still end up using the Dependabot proxy to check for feed reachability. + // This would result in us discovering that the feeds are reachable, but `dotnet` would + // fail to connect to them. To prevent this from happening, we do not initialise an + // instance of `DependabotProxy` on those platforms. + if (SystemBuildActions.Instance.IsWindows() || SystemBuildActions.Instance.IsMacOs()) return null; + + // Obtain and store the address of the Dependabot proxy, if available. + var host = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyHost); + var port = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyPort); + + if (string.IsNullOrWhiteSpace(host) || string.IsNullOrWhiteSpace(port)) + { + logger.LogInfo("No Dependabot proxy credentials are configured."); + return null; + } + + var result = new DependabotProxy(host, port); + logger.LogInfo($"Dependabot proxy configured at {result.Address}"); + + // Obtain and store the proxy's certificate, if available. + var cert = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyCertificate); + + if (!string.IsNullOrWhiteSpace(cert)) + { + logger.LogInfo("No certificate configured for Dependabot proxy."); + + var certDirPath = new DirectoryInfo(Path.Join(tempWorkingDirectory.DirInfo.FullName, ".dependabot-proxy")); + Directory.CreateDirectory(certDirPath.FullName); + + result.CertificatePath = Path.Join(certDirPath.FullName, "proxy.crt"); + var certFile = new FileInfo(result.CertificatePath); + + using var writer = certFile.CreateText(); + writer.Write(cert); + writer.Close(); + + logger.LogInfo($"Stored Dependabot proxy certificate at {result.CertificatePath}"); + + result.Certificate = X509Certificate2.CreateFromPem(cert); + } + + return result; + } + + private DependabotProxy(string host, string port) + { + this.host = host; + this.port = port; + this.Address = $"http://{this.host}:{this.port}"; + } + + public void Dispose() + { + this.Certificate?.Dispose(); + } + } +} diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs index 4866df1260e2..b8773f0ae4a6 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs @@ -27,6 +27,7 @@ public sealed partial class DependencyManager : IDisposable, ICompilationInfoCon private readonly ILogger logger; private readonly IDiagnosticsWriter diagnosticsWriter; private readonly NugetPackageRestorer nugetPackageRestorer; + private readonly DependabotProxy? dependabotProxy; private readonly IDotNet dotnet; private readonly FileContent fileContent; private readonly FileProvider fileProvider; @@ -106,9 +107,11 @@ void exitCallback(int ret, string msg, bool silent) return BuildScript.Success; }).Run(SystemBuildActions.Instance, startCallback, exitCallback); + dependabotProxy = DependabotProxy.GetDependabotProxy(logger, tempWorkingDirectory); + try { - this.dotnet = DotNet.Make(logger, dotnetPath, tempWorkingDirectory); + this.dotnet = DotNet.Make(logger, dotnetPath, tempWorkingDirectory, dependabotProxy); runtimeLazy = new Lazy(() => new Runtime(dotnet)); } catch @@ -117,7 +120,7 @@ void exitCallback(int ret, string msg, bool silent) throw; } - nugetPackageRestorer = new NugetPackageRestorer(fileProvider, fileContent, dotnet, diagnosticsWriter, logger, this); + nugetPackageRestorer = new NugetPackageRestorer(fileProvider, fileContent, dotnet, dependabotProxy, diagnosticsWriter, logger, this); var dllLocations = fileProvider.Dlls.Select(x => new AssemblyLookupLocation(x)).ToHashSet(); dllLocations.UnionWith(nugetPackageRestorer.Restore()); @@ -542,6 +545,7 @@ private void AnalyseProject(FileInfo project) public void Dispose() { nugetPackageRestorer?.Dispose(); + dependabotProxy?.Dispose(); if (cleanupTempWorkingDirectory) { tempWorkingDirectory?.Dispose(); diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs index edfea049a81b..c1fdcc06e91b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNet.cs @@ -27,11 +27,11 @@ private DotNet(IDotNetCliInvoker dotnetCliInvoker, ILogger logger, TemporaryDire Info(); } - private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory) : this(new DotNetCliInvoker(logger, Path.Combine(dotNetPath ?? string.Empty, "dotnet")), logger, tempWorkingDirectory) { } + private DotNet(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) : this(new DotNetCliInvoker(logger, Path.Combine(dotNetPath ?? string.Empty, "dotnet"), dependabotProxy), logger, tempWorkingDirectory) { } internal static IDotNet Make(IDotNetCliInvoker dotnetCliInvoker, ILogger logger) => new DotNet(dotnetCliInvoker, logger); - public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory) => new DotNet(logger, dotNetPath, tempWorkingDirectory); + public static IDotNet Make(ILogger logger, string? dotNetPath, TemporaryDirectory tempWorkingDirectory, DependabotProxy? dependabotProxy) => new DotNet(logger, dotNetPath, tempWorkingDirectory, dependabotProxy); private void Info() { diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs index 4295cce67167..19f0f3dbe0d9 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DotNetCliInvoker.cs @@ -12,12 +12,14 @@ namespace Semmle.Extraction.CSharp.DependencyFetching internal sealed class DotNetCliInvoker : IDotNetCliInvoker { private readonly ILogger logger; + private readonly DependabotProxy? proxy; public string Exec { get; } - public DotNetCliInvoker(ILogger logger, string exec) + public DotNetCliInvoker(ILogger logger, string exec, DependabotProxy? dependabotProxy) { this.logger = logger; + this.proxy = dependabotProxy; this.Exec = exec; logger.LogInfo($"Using .NET CLI executable: '{Exec}'"); } @@ -38,6 +40,17 @@ private ProcessStartInfo MakeDotnetStartInfo(string args, string? workingDirecto startInfo.EnvironmentVariables["DOTNET_CLI_UI_LANGUAGE"] = "en"; startInfo.EnvironmentVariables["MSBUILDDISABLENODEREUSE"] = "1"; startInfo.EnvironmentVariables["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "true"; + + // Configure the proxy settings, if applicable. + if (this.proxy != null) + { + logger.LogInfo($"Setting up Dependabot proxy at {this.proxy.Address}"); + + startInfo.EnvironmentVariables.Add("HTTP_PROXY", this.proxy.Address); + startInfo.EnvironmentVariables.Add("HTTPS_PROXY", this.proxy.Address); + startInfo.EnvironmentVariables.Add("SSL_CERT_FILE", this.proxy.CertificatePath); + } + return startInfo; } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs index 345cb43453fc..d825e5daeb03 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/EnvironmentVariableNames.cs @@ -74,5 +74,20 @@ internal static class EnvironmentVariableNames /// Specifies the location of the diagnostic directory. /// public const string DiagnosticDir = "CODEQL_EXTRACTOR_CSHARP_DIAGNOSTIC_DIR"; + + /// + /// Specifies the hostname of the Dependabot proxy. + /// + public const string ProxyHost = "CODEQL_PROXY_HOST"; + + /// + /// Specifies the hostname of the Dependabot proxy. + /// + public const string ProxyPort = "CODEQL_PROXY_PORT"; + + /// + /// Contains the certificate used by the Dependabot proxy. + /// + public const string ProxyCertificate = "CODEQL_PROXY_CA_CERTIFICATE"; } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs index f30760981f3a..d0c0af6b768b 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/NugetPackageRestorer.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Net.Http; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; using System.Threading; @@ -20,6 +22,7 @@ internal sealed partial class NugetPackageRestorer : IDisposable private readonly FileProvider fileProvider; private readonly FileContent fileContent; private readonly IDotNet dotnet; + private readonly DependabotProxy? dependabotProxy; private readonly IDiagnosticsWriter diagnosticsWriter; private readonly TemporaryDirectory legacyPackageDirectory; private readonly TemporaryDirectory missingPackageDirectory; @@ -32,6 +35,7 @@ public NugetPackageRestorer( FileProvider fileProvider, FileContent fileContent, IDotNet dotnet, + DependabotProxy? dependabotProxy, IDiagnosticsWriter diagnosticsWriter, ILogger logger, ICompilationInfoContainer compilationInfoContainer) @@ -39,6 +43,7 @@ public NugetPackageRestorer( this.fileProvider = fileProvider; this.fileContent = fileContent; this.dotnet = dotnet; + this.dependabotProxy = dependabotProxy; this.diagnosticsWriter = diagnosticsWriter; this.logger = logger; this.compilationInfoContainer = compilationInfoContainer; @@ -588,7 +593,25 @@ private static async Task ExecuteGetRequest(string address, HttpClient httpClien private bool IsFeedReachable(string feed, int timeoutMilliSeconds, int tryCount, bool allowExceptions = true) { logger.LogInfo($"Checking if Nuget feed '{feed}' is reachable..."); - using HttpClient client = new(); + + // Configure the HttpClient to be aware of the Dependabot Proxy, if used. + HttpClientHandler httpClientHandler = new(); + if (this.dependabotProxy != null) + { + httpClientHandler.Proxy = new WebProxy(this.dependabotProxy.Address); + + if (this.dependabotProxy.Certificate != null) + { + httpClientHandler.ServerCertificateCustomValidationCallback = (message, cert, chain, _) => + { + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + chain.ChainPolicy.CustomTrustStore.Add(this.dependabotProxy.Certificate); + return chain.Build(cert); + }; + } + } + + using HttpClient client = new(httpClientHandler); for (var i = 0; i < tryCount; i++) {