diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs index 08b142219a97..3bf843d3fa2c 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxy.cs @@ -18,84 +18,54 @@ public class DependabotProxy : IDependabotProxy /// The URL of the package registry. public record class RegistryConfig(string Type, string URL); - private readonly string host; - private readonly string port; - public string Address { get; } - public HashSet RegistryURLs { get; } + public HashSet RegistryURLs { get; } = []; public string? CertificatePath { get; private set; } public X509Certificate2? Certificate { get; private set; } - internal static IDependabotProxy? GetDependabotProxy( - ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory) + private DependabotProxy(IDependabotProxyConfiguration config, 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}"); + Address = $"http://{config.Host}:{config.Port}"; - // Obtain and store the proxy's certificate, if available. - var cert = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyCertificate); - - if (!string.IsNullOrWhiteSpace(cert)) + if (!string.IsNullOrWhiteSpace(config.Certificate)) { 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); + CertificatePath = Path.Join(certDirPath.FullName, "proxy.crt"); + var certFile = new FileInfo(CertificatePath); using var writer = certFile.CreateText(); - writer.Write(cert); + writer.Write(config.Certificate); writer.Close(); - logger.LogInfo($"Stored Dependabot proxy certificate at {result.CertificatePath}"); + logger.LogInfo($"Stored Dependabot proxy certificate at {CertificatePath}"); - result.Certificate = X509Certificate2.CreateFromPem(cert); + Certificate = X509Certificate2.CreateFromPem(config.Certificate); } - // Try to obtain the list of private registry URLs. - var registryURLs = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs); - - if (!string.IsNullOrWhiteSpace(registryURLs)) + if (!string.IsNullOrWhiteSpace(config.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>(registryURLs); + var array = JsonConvert.DeserializeObject>(config.RegistryURLs); if (array is not null) { - foreach (RegistryConfig config in array) + foreach (RegistryConfig registry 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")) + if (!registry.Type.Equals("nuget_feed")) { - logger.LogDebug($"Ignoring registry at '{config.URL}' since it is not of type 'nuget_feed'."); + logger.LogDebug($"Ignoring registry at '{registry.URL}' since it is not of type 'nuget_feed'."); continue; } - logger.LogInfo($"Found private registry at '{config.URL}'"); - result.RegistryURLs.Add(config.URL); + logger.LogInfo($"Found private registry at '{registry.URL}'"); + RegistryURLs.Add(registry.URL); } } } @@ -104,6 +74,39 @@ public record class RegistryConfig(string Type, string URL); logger.LogError($"Unable to parse '{EnvironmentVariableNames.ProxyURLs}': {ex.Message}"); } } + } + + internal static IDependabotProxy? Make(ILogger logger, IDiagnosticsWriter diagnosticsWriter, 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; + } + + return Make(new DependabotProxyConfiguration(), logger, diagnosticsWriter, tempWorkingDirectory); + } + + /// + /// Creates an instance of the Dependabot proxy using the specified configuration. + /// Returns null if the proxy cannot be created. + /// This overload is exposed primarily to enable platform-independent unit testing. + /// + internal static IDependabotProxy? Make( + IDependabotProxyConfiguration proxyConfig, ILogger logger, IDiagnosticsWriter diagnosticsWriter, TemporaryDirectory tempWorkingDirectory) + { + if (string.IsNullOrWhiteSpace(proxyConfig.Host) || string.IsNullOrWhiteSpace(proxyConfig.Port)) + { + logger.LogDebug("No Dependabot proxy credentials are configured."); + return null; + } + + var result = new DependabotProxy(proxyConfig, logger, tempWorkingDirectory); + logger.LogInfo($"Dependabot proxy configured at {result.Address}"); // Emit a diagnostic for the discovered private registries, so that it is easy // for users to see that they were picked up. @@ -125,17 +128,9 @@ public record class RegistryConfig(string Type, string URL); return result; } - private DependabotProxy(string host, string port) - { - this.host = host; - this.port = port; - this.Address = $"http://{this.host}:{this.port}"; - this.RegistryURLs = new HashSet(); - } - public void Dispose() { - this.Certificate?.Dispose(); + Certificate?.Dispose(); } } } diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxyConfiguration.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxyConfiguration.cs new file mode 100644 index 000000000000..2d81f94aea2c --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependabotProxyConfiguration.cs @@ -0,0 +1,15 @@ +using System; + +namespace Semmle.Extraction.CSharp.DependencyFetching +{ + public class DependabotProxyConfiguration : IDependabotProxyConfiguration + { + public string? Host { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyHost); + + public string? Port { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyPort); + + public string? Certificate { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyCertificate); + + public string? RegistryURLs { get; } = Environment.GetEnvironmentVariable(EnvironmentVariableNames.ProxyURLs); + } +} diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs index 707fabbf83fb..a985947c0c12 100644 --- a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/DependencyManager.cs @@ -106,7 +106,7 @@ void exitCallback(int ret, string msg, bool silent) return BuildScript.Success; }).Run(SystemBuildActions.Instance, startCallback, exitCallback); - dependabotProxy = DependabotProxy.GetDependabotProxy(logger, diagnosticsWriter, tempWorkingDirectory); + dependabotProxy = DependabotProxy.Make(logger, diagnosticsWriter, tempWorkingDirectory); try { diff --git a/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxyConfiguration.cs b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxyConfiguration.cs new file mode 100644 index 000000000000..c67ee4fc39df --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.CSharp.DependencyFetching/IDependabotProxyConfiguration.cs @@ -0,0 +1,21 @@ +using System; + +namespace Semmle.Extraction.CSharp.DependencyFetching +{ + public interface IDependabotProxyConfiguration + { + // The host of the Dependabot proxy, if available. + string? Host { get; } + + // The port of the Dependabot proxy, if available. + string? Port { get; } + + // The certificate of the Dependabot proxy, if available. + string? Certificate { get; } + + // The list of package registries that are configured for the proxy, if any. + // 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" } ] + string? RegistryURLs { get; } + } +} diff --git a/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs new file mode 100644 index 000000000000..9c8c762f5989 --- /dev/null +++ b/csharp/extractor/Semmle.Extraction.Tests/DependabotProxy.cs @@ -0,0 +1,185 @@ +using Xunit; +using System; +using System.IO; +using Semmle.Extraction.CSharp.DependencyFetching; +using Semmle.Util; + +namespace Semmle.Extraction.Tests +{ + public class DependabotConfigurationStub : IDependabotProxyConfiguration + { + public string? Host { get; set; } + public string? Port { get; set; } + public string? Certificate { get; set; } + public string? RegistryURLs { get; set; } + } + + public class DiagnosticsWriterStub : IDiagnosticsWriter + { + public void AddEntry(Semmle.Util.DiagnosticMessage entry) { } + public void Dispose() { } + } + + public class DependabotProxyTests + { + private static TemporaryDirectory MakeTemporaryDirectory() + { + var tmp = Path.Join(Path.GetTempPath(), "DependabotProxyTests", Guid.NewGuid().ToString()); + return new TemporaryDirectory(tmp, "testing", new LoggerStub()); + } + + [Fact] + public void TestDependabotProxyCreation1() + { + // Setup + var config = new DependabotConfigurationStub + { + Host = "localhost", + Port = "", + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.Null(proxy); + } + + [Fact] + public void TestDependabotProxyCreation2() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.Null(proxy); + } + + private const string ExampleCertificate = """ + -----BEGIN CERTIFICATE----- + MIIFJTCCAw2gAwIBAgIUDImU6YnuAqJ1QuRp+OpJQPnPu6wwDQYJKoZIhvcNAQEL + BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDkwMTEyMjUzMVoXDTI3MDkw + MTEyMjUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEF + AAOCAg8AMIICCgKCAgEAnlp7yQ1VuocMwIZWlCle3bEM86+1ED6BFfPFpIrRhfUT + c+5IvPng8TIZPO4mROp5G9YDZfOtXW2bwktyZNUhsBcxqUT1lmXit21vc5W9Gxx5 + 4G8nyF4/FcjFkmxkZifxiUCdBceDcE7+kx2itq/a7gLPlyTzvz5etu1nHEC3Jg/y + TVhAwdwysgAo9WymFCczDa2ga6nOPBOaxwLnoPl9041KSu5oIo9QC0Im+US1R18Q + /mXa+wkmjf+bYAkE/pZie8z8Q7h9yppTngGzkoDEebFYyaMr8MXlFdWS8f/eMwSp + iMFSsmlCqgUbA672APxzOcuSMMYrblzGkvZp23qbNjwQuQKlgAYBTSGltLv4U8JF + ePNcgDCY6RG55rNvF1gk1L2h25jcw1LX6fSvQGCOkzNmP03AhqZBUigO1Zt0zLwi + K4m0bH7nPLJFEN6tI3tybyZeC2RVyiSHvOkgx35Qj8RQ3XMVkImJNBYOMc2MkmMZ + ux6XMiHqXCON4zaWuWSovciZeMAQAspCrzVDLH6p2DWEfw/zDfQNU3iLk21sZGei + 0GKzs8zrxUcqOU9V4Cnm+7JJ6eqS72f1+wX0ROb3djC6KgCE/NaHqo4apiI3K+CH + T0rVRsJIHyT39YO1c1I1vhAKRSH5kQVe3qRfIT/AuaDLQY6WqGPzrOkem78sjtsC + AwEAAaNvMG0wHQYDVR0OBBYEFK0DP5MD6mhEcdcm346uwoPL2NFGMB8GA1UdIwQY + MBaAFK0DP5MD6mhEcdcm346uwoPL2NFGMA8GA1UdEwEB/wQFMAMBAf8wGgYDVR0R + BBMwEYIJbG9jYWxob3N0hwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQCc5u8qNHHG + kONjfvq7Denq6QaEt4dZZDDODAvgUzZnnBjEhgrp7zfxtbyU/I0+DWnKQMKA9wPM + ktiFGd0lldEqoT+E7b0kN124lBGqZ/uYkhsWZ0Nc5dD+UB9oJszwOc5KNuquOnr6 + SbsfXVm4yLvVLXl67c0jvqvRgGg9/6Q6eMzohW6abMdbYhS28/DsJhCea/dV3+L1 + oVJ3O/A8e86m174ZCGE8s9UtnVYylBkAryDqaaQLdOBQ2C7uxdRAUNHSIa2JlqUc + 5+cod8lFojKb74hbgj6wkXyajsFttqYMh7CeASsnjZXDQ4MC3DqqDVCZuNvJ85Rt + ya3Tljp4Ln2AAAoKC3REUeU8PQqpk1vVIj0FSr3RvBTvwzyNfWFVqyBiXTATuV9n + 6AemqqXo5MZrHHeRaSTF8A70Jxbt9yx75xQxp3O3tdEL1Mxbl9X7c/hizOfLbeHH + IkAgzALQgi87Zbf2tOhRwH5NrB4ijyUUfovRHUwzsZOoTNqlVeNzbDRVbegx9V99 + /3vwNZgpStGl/JYhN9qY5hJKnC64ltMvuNGpLeJCGyFkrtFS8gKkgR7VKrGo7h3+ + Zo8rz8TFjP7RmSgQbrmFuPqNOGXzPidu2sMMFacKV7Rn4bEtHzW3MDhqVD4w/pGD + L0xpnWjzLYltVjz8mo07yh+zQ10G71Cl1w== + -----END CERTIFICATE----- + """; + + [Fact] + public void TestDependabotProxyCertificate() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + Host = "localhost", + Certificate = ExampleCertificate + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.NotNull(proxy); + Assert.Equal("http://localhost:8080", proxy.Address); + Assert.NotNull(proxy.Certificate); + Assert.NotNull(proxy.CertificatePath); + } + + [Fact] + public void TestDependabotRegistryUrls1() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + Host = "localhost", + RegistryURLs = "Doesn't parse as a JSON list" + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.NotNull(proxy); + Assert.Equal([], proxy.RegistryURLs); + } + + [Fact] + public void TestDependabotRegistryUrls2() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + Host = "localhost", + RegistryURLs = "[ { \"type\": \"nuget_feed\", \"url\": \"https://nuget.pkg.github.com/org/index.json\" } ]" + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.NotNull(proxy); + Assert.Equal([ + "https://nuget.pkg.github.com/org/index.json" + ], proxy.RegistryURLs); + } + + [Fact] + public void TestDependabotRegistryUrls3() + { + // Setup + var config = new DependabotConfigurationStub + { + Port = "8080", + Host = "localhost", + RegistryURLs = "[ { \"type\": \"nuget_feed\", \"url\": \"https://example.com/org/index.json\" }, { \"type\": \"wrong_type\", \"url\": \"https://nuget.pkg.github.com/org/index.json\" } ]" + }; + + // Execute + using var tempWorkingDirectory = MakeTemporaryDirectory(); + using var proxy = DependabotProxy.Make(config, new LoggerStub(), new DiagnosticsWriterStub(), tempWorkingDirectory); + + // Verify + Assert.NotNull(proxy); + Assert.Equal([ + "https://example.com/org/index.json" + ], proxy.RegistryURLs); + } + } +}