diff --git a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs index 276344ad..35123b0d 100644 --- a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentDetector.cs @@ -50,7 +50,7 @@ public class GoComponentDetector : FileComponentDetector public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Go }; - public override int Version => 8; + public override int Version => 7; protected override Task> OnPrepareDetectionAsync( IObservable processRequests, IDictionary detectorArgs) @@ -282,7 +282,6 @@ public class GoComponentDetector : FileComponentDetector GoGraphTelemetryRecord goGraphTelemetryRecord) { using var reader = new StreamReader(file.Stream); - var startString = "require "; // There can be multiple require( ) sections in go 1.17+. loop over all of them. while (!reader.EndOfStream) @@ -298,9 +297,9 @@ public class GoComponentDetector : FileComponentDetector // In go >= 1.17, direct dependencies are listed as "require x/y v1.2.3", and transitive dependencies // are listed in the require () section - if (line.StartsWith(startString)) + if (line.StartsWith("require ")) { - this.TryRegisterDependencyFromModLine(line[startString.Length..], singleFileComponentRecorder); + this.TryRegisterDependencyFromModLine(line[8..], singleFileComponentRecorder); } line = await reader.ReadLineAsync(); @@ -444,15 +443,7 @@ public class GoComponentDetector : FileComponentDetector continue; } - GoComponent goComponent; - if (dependency.Replace != null) - { - goComponent = new GoComponent(dependency.Replace.Path, dependency.Replace.Version); - } - else - { - goComponent = new GoComponent(dependency.Path, dependency.Version); - } + var goComponent = new GoComponent(dependency.Path, dependency.Version); if (dependency.Indirect) { @@ -492,7 +483,5 @@ public class GoComponentDetector : FileComponentDetector public string Version { get; set; } public bool Indirect { get; set; } - - public GoBuildModule Replace { get; set; } } } diff --git a/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs new file mode 100644 index 00000000..1964663f --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/go/GoComponentWithReplaceDetector.cs @@ -0,0 +1,498 @@ +namespace Microsoft.ComponentDetection.Detectors.Go; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reactive.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Common; +using Microsoft.ComponentDetection.Common.Telemetry.Records; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; + +public class GoComponentWithReplaceDetector : FileComponentDetector, IExperimentalDetector +{ + private static readonly Regex GoSumRegex = new( + @"(?.*)\s+(?.*?)(/go\.mod)?\s+(?.*)", + RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + + private readonly HashSet projectRoots = new(); + + private readonly ICommandLineInvocationService commandLineInvocationService; + private readonly IEnvironmentVariableService envVarService; + + public GoComponentWithReplaceDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ICommandLineInvocationService commandLineInvocationService, + IEnvironmentVariableService envVarService, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.commandLineInvocationService = commandLineInvocationService; + this.envVarService = envVarService; + this.Logger = logger; + } + + public override string Id => "GoWithReplace"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.GoMod) }; + + public override IList SearchPatterns { get; } = new List { "go.mod", "go.sum" }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Go }; + + public override int Version => 1; + + protected override Task> OnPrepareDetectionAsync( + IObservable processRequests, IDictionary detectorArgs) + { + // Filter out any go.sum process requests if the adjacent go.mod file is present and has a go version >= 1.17 + var goModProcessRequests = processRequests.Where(processRequest => + { + if (Path.GetFileName(processRequest.ComponentStream.Location) != "go.sum") + { + return true; + } + + var goModFile = this.FindAdjacentGoModComponentStreams(processRequest).FirstOrDefault(); + + if (goModFile == null) + { + this.Logger.LogDebug( + "go.sum file found without an adjacent go.mod file. Location: {Location}", + processRequest.ComponentStream.Location); + + return true; + } + + // parse the go.mod file to get the go version + using var reader = new StreamReader(goModFile.Stream); + var goModFileContents = reader.ReadToEnd(); + goModFile.Stream.Dispose(); + + return this.CheckGoModVersion(goModFileContents, processRequest, goModFile); + }); + + return Task.FromResult(goModProcessRequests); + } + + private IEnumerable FindAdjacentGoModComponentStreams(ProcessRequest processRequest) => + this.ComponentStreamEnumerableFactory.GetComponentStreams( + new FileInfo(processRequest.ComponentStream.Location).Directory, + new[] { "go.mod" }, + (_, _) => false, + false) + .Select(x => + { + // The stream will be disposed at the end of this method, so we need to copy it to a new stream. + var memoryStream = new MemoryStream(); + + x.Stream.CopyTo(memoryStream); + memoryStream.Position = 0; + + return new ComponentStream + { + Stream = memoryStream, + Location = x.Location, + Pattern = x.Pattern, + }; + }); + + private bool CheckGoModVersion(string goModFileContents, ProcessRequest processRequest, ComponentStream goModFile) + { + var goVersionMatch = Regex.Match(goModFileContents, @"go\s(?\d+\.\d+)"); + + if (!goVersionMatch.Success) + { + this.Logger.LogDebug( + "go.sum file found with an adjacent go.mod file that does not contain a go version. Location: {Location}", + processRequest.ComponentStream.Location); + return true; + } + + var goVersion = goVersionMatch.Groups["version"].Value; + if (System.Version.TryParse(goVersion, out var version)) + { + if (version < new Version(1, 17)) + { + this.Logger.LogWarning( + "go.mod file at {GoModLocation} does not have a go version >= 1.17. Scanning this go.sum file: {GoSumLocation} which may lead to over reporting components", + goModFile.Location, + processRequest.ComponentStream.Location); + + return true; + } + + this.Logger.LogInformation( + "go.sum file found with an adjacent go.mod file that has a go version >= 1.17. Will not scan this go.sum file. Location: {Location}", + processRequest.ComponentStream.Location); + + return false; + } + + this.Logger.LogWarning( + "go.sum file found with an adjacent go.mod file that has an invalid go version. Scanning both for components. Location: {Location}", + processRequest.ComponentStream.Location); + + return true; + } + + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs, CancellationToken cancellationToken = default) + { + var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder; + var file = processRequest.ComponentStream; + + var projectRootDirectory = Directory.GetParent(file.Location); + if (this.projectRoots.Any(path => projectRootDirectory.FullName.StartsWith(path))) + { + return; + } + + using var record = new GoGraphTelemetryRecord(); + record.WasGoCliDisabled = false; + record.WasGoFallbackStrategyUsed = false; + + var wasGoCliScanSuccessful = false; + try + { + if (!this.IsGoCliManuallyDisabled()) + { + wasGoCliScanSuccessful = await this.UseGoCliToScanAsync(file.Location, singleFileComponentRecorder, record); + } + else + { + record.WasGoCliDisabled = true; + this.Logger.LogInformation("Go cli scan was manually disabled, fallback strategy performed." + + " More info: https://github.com/microsoft/component-detection/blob/main/docs/detectors/go.md#fallback-detection-strategy"); + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Failed to detect components using go cli. Location: {Location}", file.Location); + } + finally + { + if (wasGoCliScanSuccessful) + { + this.projectRoots.Add(projectRootDirectory.FullName); + } + else + { + record.WasGoFallbackStrategyUsed = true; + var fileExtension = Path.GetExtension(file.Location).ToUpperInvariant(); + switch (fileExtension) + { + case ".MOD": + { + this.Logger.LogDebug("Found Go.mod: {Location}", file.Location); + await this.ParseGoModFileAsync(singleFileComponentRecorder, file, record); + break; + } + + case ".SUM": + { + this.Logger.LogDebug("Found Go.sum: {Location}", file.Location); + this.ParseGoSumFile(singleFileComponentRecorder, file); + break; + } + + default: + { + throw new InvalidOperationException("Unexpected file type detected in go detector"); + } + } + } + } + } + + [SuppressMessage("Maintainability", "CA1508:Avoid dead conditional code", Justification = "False positive")] + private async Task UseGoCliToScanAsync(string location, ISingleFileComponentRecorder singleFileComponentRecorder, GoGraphTelemetryRecord record) + { + record.WasGraphSuccessful = false; + record.DidGoCliCommandFail = false; + var projectRootDirectory = Directory.GetParent(location); + record.ProjectRoot = projectRootDirectory.FullName; + + var isGoAvailable = await this.commandLineInvocationService.CanCommandBeLocatedAsync("go", null, workingDirectory: projectRootDirectory, new[] { "version" }); + record.IsGoAvailable = isGoAvailable; + + if (!isGoAvailable) + { + this.Logger.LogInformation("Go CLI was not found in the system"); + return false; + } + + this.Logger.LogInformation("Go CLI was found in system and will be used to generate dependency graph. " + + "Detection time may be improved by activating fallback strategy (https://github.com/microsoft/component-detection/blob/main/docs/detectors/go.md#fallback-detection-strategy). " + + "But, it will introduce noise into the detected components."); + var goDependenciesProcess = await this.commandLineInvocationService.ExecuteCommandAsync("go", null, workingDirectory: projectRootDirectory, new[] { "list", "-mod=readonly", "-m", "-json", "all" }); + if (goDependenciesProcess.ExitCode != 0) + { + this.Logger.LogError("Go CLI command \"go list -m -json all\" failed with error: {GoDependenciesProcessStdErr}", goDependenciesProcess.StdErr); + this.Logger.LogError("Go CLI could not get dependency build list at location: {Location}. Fallback go.sum/go.mod parsing will be used.", location); + record.DidGoCliCommandFail = true; + record.GoCliCommandError = goDependenciesProcess.StdErr; + return false; + } + + this.RecordBuildDependencies(goDependenciesProcess.StdOut, singleFileComponentRecorder); + + var generateGraphProcess = await this.commandLineInvocationService.ExecuteCommandAsync("go", null, workingDirectory: projectRootDirectory, new List { "mod", "graph" }.ToArray()); + if (generateGraphProcess.ExitCode == 0) + { + this.PopulateDependencyGraph(generateGraphProcess.StdOut, singleFileComponentRecorder); + record.WasGraphSuccessful = true; + } + + return true; + } + + private void TryRegisterDependencyFromModLine(string line, ISingleFileComponentRecorder singleFileComponentRecorder) + { + if (line.Trim().StartsWith("//")) + { + // this is a comment line, ignore it + return; + } + + if (this.TryToCreateGoComponentFromModLine(line, out var goComponent)) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent)); + } + else + { + var lineTrim = line.Trim(); + this.Logger.LogWarning("Line could not be parsed for component [{LineTrim}]", lineTrim); + singleFileComponentRecorder.RegisterPackageParseFailure(lineTrim); + } + } + + private async Task ParseGoModFileAsync( + ISingleFileComponentRecorder singleFileComponentRecorder, + IComponentStream file, + GoGraphTelemetryRecord goGraphTelemetryRecord) + { + using var reader = new StreamReader(file.Stream); + var startString = "require "; + + // There can be multiple require( ) sections in go 1.17+. loop over all of them. + while (!reader.EndOfStream) + { + var line = await reader.ReadLineAsync(); + + while (line != null && !line.StartsWith("require (")) + { + if (line.StartsWith("go ")) + { + goGraphTelemetryRecord.GoModVersion = line[3..].Trim(); + } + + // In go >= 1.17, direct dependencies are listed as "require x/y v1.2.3", and transitive dependencies + // are listed in the require () section + if (line.StartsWith(startString)) + { + this.TryRegisterDependencyFromModLine(line[startString.Length..], singleFileComponentRecorder); + } + + line = await reader.ReadLineAsync(); + } + + // Stopping at the first ) restrict the detection to only the require section. + while ((line = await reader.ReadLineAsync()) != null && !line.EndsWith(")")) + { + this.TryRegisterDependencyFromModLine(line, singleFileComponentRecorder); + } + } + } + + private bool TryToCreateGoComponentFromModLine(string line, out GoComponent goComponent) + { + var lineComponents = Regex.Split(line.Trim(), @"\s+"); + + if (lineComponents.Length < 2) + { + goComponent = null; + return false; + } + + var name = lineComponents[0]; + var version = lineComponents[1]; + goComponent = new GoComponent(name, version); + + return true; + } + + // For more information about the format of the go.sum file + // visit https://golang.org/cmd/go/#hdr-Module_authentication_using_go_sum + private void ParseGoSumFile( + ISingleFileComponentRecorder singleFileComponentRecorder, + IComponentStream file) + { + using var reader = new StreamReader(file.Stream); + + string line; + while ((line = reader.ReadLine()) != null) + { + if (this.TryToCreateGoComponentFromSumLine(line, out var goComponent)) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent)); + } + else + { + var lineTrim = line.Trim(); + this.Logger.LogWarning("Line could not be parsed for component [{LineTrim}]", lineTrim); + singleFileComponentRecorder.RegisterPackageParseFailure(lineTrim); + } + } + } + + private bool TryToCreateGoComponentFromSumLine(string line, out GoComponent goComponent) + { + var m = GoSumRegex.Match(line); + if (m.Success) + { + goComponent = new GoComponent(m.Groups["name"].Value, m.Groups["version"].Value, m.Groups["hash"].Value); + return true; + } + + goComponent = null; + return false; + } + + /// + /// This command only adds edges between parent and child components, it does not add nor remove any entries from the existing build list. + /// + private void PopulateDependencyGraph(string goGraphOutput, ISingleFileComponentRecorder componentRecorder) + { + // Yes, go always returns \n even on Windows + var graphRelationships = goGraphOutput.Split('\n'); + + foreach (var relationship in graphRelationships) + { + var components = relationship.Split(' '); + if (components.Length != 2) + { + if (string.IsNullOrWhiteSpace(relationship)) + { + // normally the last line is an empty string + continue; + } + + this.Logger.LogWarning("Unexpected relationship output from go mod graph: {Relationship}", relationship); + continue; + } + + var isParentParsed = this.TryCreateGoComponentFromRelationshipPart(components[0], out var parentComponent); + var isChildParsed = this.TryCreateGoComponentFromRelationshipPart(components[1], out var childComponent); + + if (!isParentParsed) + { + // These are explicit dependencies, we already have those recorded + continue; + } + + if (isChildParsed) + { + if (this.IsModuleInBuildList(componentRecorder, parentComponent) && this.IsModuleInBuildList(componentRecorder, childComponent)) + { + componentRecorder.RegisterUsage(new DetectedComponent(childComponent), parentComponentId: parentComponent.Id); + } + } + else + { + this.Logger.LogWarning("Failed to parse components from relationship string {Relationship}", relationship); + componentRecorder.RegisterPackageParseFailure(relationship); + } + } + } + + private bool IsModuleInBuildList(ISingleFileComponentRecorder singleFileComponentRecorder, GoComponent component) + { + return singleFileComponentRecorder.GetComponent(component.Id) != null; + } + + private void RecordBuildDependencies(string goListOutput, ISingleFileComponentRecorder singleFileComponentRecorder) + { + var goBuildModules = new List(); + var reader = new JsonTextReader(new StringReader(goListOutput)) + { + SupportMultipleContent = true, + }; + + while (reader.Read()) + { + var serializer = new JsonSerializer(); + var buildModule = serializer.Deserialize(reader); + + goBuildModules.Add(buildModule); + } + + foreach (var dependency in goBuildModules) + { + if (dependency.Main) + { + // main is the entry point module (superfluous as we already have the file location) + continue; + } + + GoComponent goComponent; + if (dependency.Replace != null) + { + goComponent = new GoComponent(dependency.Replace.Path, dependency.Replace.Version); + } + else + { + goComponent = new GoComponent(dependency.Path, dependency.Version); + } + + if (dependency.Indirect) + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent)); + } + else + { + singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent), isExplicitReferencedDependency: true); + } + } + } + + private bool TryCreateGoComponentFromRelationshipPart(string relationship, out GoComponent goComponent) + { + var componentParts = relationship.Split('@'); + if (componentParts.Length != 2) + { + goComponent = null; + return false; + } + + goComponent = new GoComponent(componentParts[0], componentParts[1]); + return true; + } + + private bool IsGoCliManuallyDisabled() + { + return this.envVarService.IsEnvironmentVariableValueTrue("DisableGoCliScan"); + } + + private class GoBuildModule + { + public string Path { get; set; } + + public bool Main { get; set; } + + public string Version { get; set; } + + public bool Indirect { get; set; } + + public GoBuildModule Replace { get; set; } + } +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/GoDetectorReplaceExperiment.cs b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/GoDetectorReplaceExperiment.cs new file mode 100644 index 00000000..95edf69a --- /dev/null +++ b/src/Microsoft.ComponentDetection.Orchestrator/Experiments/Configs/GoDetectorReplaceExperiment.cs @@ -0,0 +1,18 @@ +namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs; + +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Detectors.Go; + +/// +/// Validating the . +/// +public class GoDetectorReplaceExperiment : IExperimentConfiguration +{ + public string Name => "GoWithReplace"; + + public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is GoComponentDetector; + + public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is GoComponentWithReplaceDetector; + + public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true; +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index b23533aa..2e6a2f3e 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -63,6 +63,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); // Detectors // CocoaPods @@ -79,6 +80,7 @@ public static class ServiceCollectionExtensions // Go services.AddSingleton(); + services.AddSingleton(); // Gradle services.AddSingleton(); diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs index 3d46713e..91525474 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentDetectorTests.cs @@ -368,82 +368,6 @@ replace ( await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); } - [TestMethod] - public async Task TestGoDetector_GoGraphReplaceAsync() - { - var buildDependencies = @"{ - ""Path"": ""some-package"", - ""Version"": ""v1.2.3"", - ""Time"": ""2021-12-06T23:04:27Z"", - ""Indirect"": true, - ""GoMod"": ""C:\\test\\go.mod"", - ""GoVersion"": ""1.11"", - ""Replace"": { - ""Path"": ""some-package"", - ""Version"": ""v1.2.4"", - ""Time"": ""2021-12-06T23:04:27Z"", - ""Indirect"": true, - ""GoMod"": ""C:\\test\\go.mod"", - ""GoVersion"": ""1.11"", - } -}" + "\n" + @"{ - ""Path"": ""test"", - ""Version"": ""v2.0.0"", - ""Time"": ""2021-12-06T23:04:27Z"", - ""Indirect"": true, - ""GoMod"": ""C:\\test\\go.mod"", - ""GoVersion"": ""1.11"" -}" + "\n" + @"{ - ""Path"": ""other"", - ""Version"": ""v1.2.0"", - ""Time"": ""2021-12-06T23:04:27Z"", - ""Indirect"": true, - ""GoMod"": ""C:\\test\\go.mod"", - ""GoVersion"": ""1.11"" -}" + "\n" + @"{ - ""Path"": ""a"", - ""Version"": ""v1.5.0"", - ""Time"": ""2020-05-19T17:02:07Z"", - ""Indirect"": true, - ""GoMod"": ""C:\\test\\go.mod"", - ""GoVersion"": ""1.11"" -}"; - var goGraph = "example.com/mainModule some-package@v1.2.3\nsome-package@v1.2.3 other@v1.0.0\nsome-package@v1.2.3 other@v1.2.0\ntest@v2.0.0 a@v1.5.0"; - - this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) - .ReturnsAsync(true); - - this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "list", "-mod=readonly", "-m", "-json", "all" })) - .ReturnsAsync(new CommandLineExecutionResult - { - ExitCode = 0, - StdOut = buildDependencies, - }); - - this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "mod", "graph" })) - .ReturnsAsync(new CommandLineExecutionResult - { - ExitCode = 0, - StdOut = goGraph, - }); - - this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); - - var (scanResult, componentRecorder) = await this.DetectorTestUtility - .WithFile("go.mod", string.Empty) - .ExecuteDetectorAsync(); - - scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); - - var detectedComponents = componentRecorder.GetDetectedComponents(); - detectedComponents.Should().HaveCount(4); - detectedComponents.Should().NotContain(component => component.Component.Id == "other v1.0.0 - Go"); - detectedComponents.Should().ContainSingle(component => component.Component.Id == "other v1.2.0 - Go"); - detectedComponents.Should().ContainSingle(component => component.Component.Id == "some-package v1.2.4 - Go"); - detectedComponents.Should().ContainSingle(component => component.Component.Id == "test v2.0.0 - Go"); - detectedComponents.Should().ContainSingle(component => component.Component.Id == "a v1.5.0 - Go"); - } - [TestMethod] public async Task TestGoDetector_GoGraphHappyPathAsync() { diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentWithReplaceDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentWithReplaceDetectorTests.cs new file mode 100644 index 00000000..c8e9c5f5 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/GoComponentWithReplaceDetectorTests.cs @@ -0,0 +1,579 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Go; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class GoComponentWithReplaceDetectorTests : BaseDetectorTest +{ + private readonly Mock commandLineMock; + private readonly Mock envVarService; + + public GoComponentWithReplaceDetectorTests() + { + this.commandLineMock = new Mock(); + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + this.DetectorTestUtility.AddServiceMock(this.commandLineMock); + + this.envVarService = new Mock(); + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(true); + this.DetectorTestUtility.AddServiceMock(this.envVarService); + } + + [TestMethod] + public async Task TestGoModDetectorWithValidFile_ReturnsSuccessfullyAsync() + { + var goMod = + @"module github.com/Azure/azure-storage-blob-go + +require ( + github.com/Azure/azure-pipeline-go v0.2.1 + github.com/kr/pretty v0.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 + github.com/dgrijalva/jwt-go v3.2.0+incompatible +)"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(4); + + var discoveredComponents = detectedComponents.ToArray(); + discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "github.com/dgrijalva/jwt-go v3.2.0+incompatible - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Should().ContainSingle(); + } + + [TestMethod] + public async Task TestGoModDetector_CommentsOnFile_CommentsAreIgnoredAsync() + { + var goMod = + @"module github.com/Azure/azure-storage-blob-go + +require ( + // comment + github.com/kr/pretty v0.1.0 // indirect +)"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().ContainSingle("there is only one component definition on the file"); + + var discoveredComponents = detectedComponents.ToArray(); + discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Should().ContainSingle(); + } + + [TestMethod] + public async Task TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync() + { + var goSum = + @" +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +)"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.sum", goSum) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(6); + var typedComponents = detectedComponents.Select(d => d.Component).ToList(); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/mock", "v1.1.1", "h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=")); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/mock", "v1.2.0", "h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=")); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/protobuf", "v0.0.0-20161109072736-4bd1920723d7", "h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=")); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/protobuf", "v1.2.0", "h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=")); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/protobuf", "v1.3.1", "h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=")); + typedComponents.Should().Contain( + new GoComponent("github.com/golang/protobuf", "v1.3.2", "h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=")); + } + + [TestMethod] + public async Task TestGoModDetector_MultipleSpaces_ReturnsSuccessfullyAsync() + { + var goMod = + @"module github.com/Azure/azure-storage-blob-go + +require ( + github.com/Azure/azure-pipeline-go v0.2.1 + github.com/kr/pretty v0.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 + github.com/dgrijalva/jwt-go v3.2.0+incompatible +)"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(4); + + var discoveredComponents = detectedComponents.ToArray(); + discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "github.com/dgrijalva/jwt-go v3.2.0+incompatible - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Should().ContainSingle(); + } + + [TestMethod] + public async Task TestGoModDetector_ComponentsWithMultipleLocations_ReturnsSuccessfullyAsync() + { + var goMod1 = + @"module github.com/Azure/azure-storage-blob-go + +require ( + github.com/Azure/azure-pipeline-go v0.2.1 + github.com/kr/pretty v0.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 + github.com/Azure/go-autorest v10.15.2+incompatible +)"; + var goMod2 = + @"module github.com/Azure/azure-storage-blob-go + +require ( + github.com/Azure/azure-pipeline-go v0.2.1 + github.com/kr/pretty v0.1.0 // indirect + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 + github.com/Azure/go-autorest v10.15.2+incompatible +)"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod1) + .WithFile("go.mod", goMod2, fileLocation: Path.Join(Path.GetTempPath(), "another-location", "go.mod")) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Count().Should().Be(4); + + var dependencyGraphs = componentRecorder.GetDependencyGraphsByLocation(); + dependencyGraphs.Keys.Should().HaveCount(2); + + var firstGraph = dependencyGraphs.Values.First(); + var secondGraph = dependencyGraphs.Values.Skip(1).First(); + + firstGraph.GetComponents().Should().BeEquivalentTo(secondGraph.GetComponents()); + } + + [TestMethod] + public async Task TestGoModDetectorInvalidFiles_DoesNotFailAsync() + { + var invalidGoMod = + @" #/bin/sh +lorem ipsum +four score and seven bugs ago +$#26^#25%4"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", invalidGoMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Count().Should().Be(0); + } + + [TestMethod] + public async Task TestGoModDetector_SkipsGoSumFilesAsync() + { + var goMod = + @"module contoso.com/greetings +go 1.18 + +require github.com/go-sql-driver/mysql v1.7.1 // indirect"; + + var goSum = + @"github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U="; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .WithFile("go.mod", goMod, new[] { "go.mod" }) + .WithFile("go.sum", goSum) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().ContainSingle(); + + var component = componentRecorder.GetDetectedComponents().First(); + component.Component.Id.Should().Be("github.com/go-sql-driver/mysql v1.7.1 - Go"); + } + + [TestMethod] + public async Task TestGoModDetector_HandlesTwoRequiresSectionsAsync() + { + var goMod = + @"module microsoft/component-detection + +go 1.18 + +require ( + github.com/go-sql-driver/mysql v1.7.1 + rsc.io/quote v1.5.2 +) + +require ( + golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect + rsc.io/sampler v1.3.0 // indirect +)"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Should().HaveCount(4); + + var expectedComponentIds = new[] + { + "github.com/go-sql-driver/mysql v1.7.1 - Go", "rsc.io/quote v1.5.2 - Go", + "golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c - Go", "rsc.io/sampler v1.3.0 - Go", + }; + + componentRecorder.GetDetectedComponents().Select(c => c.Component.Id).Should().BeEquivalentTo(expectedComponentIds); + } + + [TestMethod] + public async Task TestGoSumDetection_TwoEntriesForTheSameComponent_ReturnsSuccessfullyAsync() + { + var goSum = + @" +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= +github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +)"; + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.sum", goSum) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().ContainSingle(); + } + + [TestMethod] + public async Task TestGoModDetector_DetectorOnlyDetectInsideRequireSectionAsync() + { + var goMod = + @"module github.com/Azure/azure-storage-blob-go + +require ( + github.com/Azure/azure-pipeline-go v0.2.1 + github.com/kr/pretty v0.1.0 // indirect +) +replace ( + github.com/Azure/go-autorest => github.com/Azure/go-autorest v13.3.2+incompatible + github.com/docker/distribution => github.com/docker/distribution v0.0.0-20191216044856-a8371794149d +) +"; + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", goMod) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(2); + + var discoveredComponents = detectedComponents.ToArray(); + discoveredComponents.Where(component => component.Component.Id == "github.com/Azure/azure-pipeline-go v0.2.1 - Go").Should().ContainSingle(); + discoveredComponents.Where(component => component.Component.Id == "github.com/kr/pretty v0.1.0 - Go").Should().ContainSingle(); + } + + [TestMethod] + public async Task TestGoDetector_GoCommandNotFoundAsync() + { + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(false); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); + } + + [TestMethod] + public async Task TestGoDetector_GoCommandThrowsAsync() + { + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(() => throw new InvalidOperationException("Some horrible error occured")); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); + } + + [TestMethod] + public async Task TestGoDetector_GoGraphCommandFailsAsync() + { + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go mod graph", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 1, + }); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); + } + + [TestMethod] + public async Task TestGoDetector_GoGraphCommandThrowsAsync() + { + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go mod graph", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(() => throw new InvalidOperationException("Some horrible error occured")); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); + } + + [TestMethod] + public async Task TestGoDetector_GoGraphReplaceAsync() + { + var buildDependencies = @"{ + ""Path"": ""some-package"", + ""Version"": ""v1.2.3"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"", + ""Replace"": { + ""Path"": ""some-package"", + ""Version"": ""v1.2.4"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"", + } +}" + "\n" + @"{ + ""Path"": ""test"", + ""Version"": ""v2.0.0"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""other"", + ""Version"": ""v1.2.0"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""a"", + ""Version"": ""v1.5.0"", + ""Time"": ""2020-05-19T17:02:07Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}"; + var goGraph = "example.com/mainModule some-package@v1.2.3\nsome-package@v1.2.3 other@v1.0.0\nsome-package@v1.2.3 other@v1.2.0\ntest@v2.0.0 a@v1.5.0"; + + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "list", "-mod=readonly", "-m", "-json", "all" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = buildDependencies, + }); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "mod", "graph" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = goGraph, + }); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(4); + detectedComponents.Should().NotContain(component => component.Component.Id == "other v1.0.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "other v1.2.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "some-package v1.2.4 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "test v2.0.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "a v1.5.0 - Go"); + } + + [TestMethod] + public async Task TestGoDetector_GoGraphHappyPathAsync() + { + var buildDependencies = @"{ + ""Path"": ""some-package"", + ""Version"": ""v1.2.3"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""test"", + ""Version"": ""v2.0.0"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""other"", + ""Version"": ""v1.2.0"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""a"", + ""Version"": ""v1.5.0"", + ""Time"": ""2020-05-19T17:02:07Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}"; + var goGraph = "example.com/mainModule some-package@v1.2.3\nsome-package@v1.2.3 other@v1.0.0\nsome-package@v1.2.3 other@v1.2.0\ntest@v2.0.0 a@v1.5.0"; + + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "list", "-mod=readonly", "-m", "-json", "all" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = buildDependencies, + }); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "mod", "graph" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = goGraph, + }); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(4); + detectedComponents.Should().NotContain(component => component.Component.Id == "other v1.0.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "other v1.2.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "some-package v1.2.3 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "test v2.0.0 - Go"); + detectedComponents.Should().ContainSingle(component => component.Component.Id == "a v1.5.0 - Go"); + } + + [TestMethod] + public async Task TestGoDetector_GoGraphCyclicDependenciesAsync() + { + var buildDependencies = @"{ + ""Path"": ""github.com/prometheus/common"", + ""Version"": ""v0.32.1"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""github.com/prometheus/client_golang"", + ""Version"": ""v1.11.0"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}" + "\n" + @"{ + ""Path"": ""github.com/prometheus/client_golang"", + ""Version"": ""v1.12.1"", + ""Time"": ""2021-12-06T23:04:27Z"", + ""Indirect"": true, + ""GoMod"": ""C:\\test\\go.mod"", + ""GoVersion"": ""1.11"" +}"; + var goGraph = @" +github.com/prometheus/common@v0.32.1 github.com/prometheus/client_golang@v1.11.0 +github.com/prometheus/client_golang@v1.12.1 github.com/prometheus/common@v0.32.1"; + this.commandLineMock.Setup(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "list", "-mod=readonly", "-m", "-json", "all" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = buildDependencies, + }); + + this.commandLineMock.Setup(x => x.ExecuteCommandAsync("go", null, It.IsAny(), new[] { "mod", "graph" })) + .ReturnsAsync(new CommandLineExecutionResult + { + ExitCode = 0, + StdOut = goGraph, + }); + + this.envVarService.Setup(x => x.IsEnvironmentVariableValueTrue("DisableGoCliScan")).Returns(false); + + var (scanResult, componentRecorder) = await this.DetectorTestUtility + .WithFile("go.mod", string.Empty) + .ExecuteDetectorAsync(); + + scanResult.ResultCode.Should().Be(ProcessingResultCode.Success); + + var detectedComponents = componentRecorder.GetDetectedComponents(); + detectedComponents.Should().HaveCount(3); + } + + [TestMethod] + public async Task TestGoDetector_GoCliRequiresEnvVarToRunAsync() + { + await this.TestGoSumDetectorWithValidFile_ReturnsSuccessfullyAsync(); + + this.commandLineMock.Verify(x => x.CanCommandBeLocatedAsync("go", null, It.IsAny(), It.IsAny()), Times.Never); + } +}