feat(go): better `go.mod` scanning for go >= 1.17 (#751)

This commit is contained in:
Justin Perez 2023-08-29 13:52:08 -07:00 коммит произвёл GitHub
Родитель e2c789e6bd
Коммит b1287e2999
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
2 изменённых файлов: 201 добавлений и 27 удалений

Просмотреть файл

@ -5,8 +5,10 @@ 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.Tasks;
using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Common.Telemetry.Records;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
@ -16,11 +18,11 @@ using Newtonsoft.Json;
public class GoComponentDetector : FileComponentDetector
{
private static readonly Regex GoSumRegex = new Regex(
private static readonly Regex GoSumRegex = new(
@"(?<name>.*)\s+(?<version>.*?)(/go\.mod)?\s+(?<hash>.*)",
RegexOptions.Compiled | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase);
private readonly HashSet<string> projectRoots = new HashSet<string>();
private readonly HashSet<string> projectRoots = new();
private readonly ICommandLineInvocationService commandLineInvocationService;
private readonly IEnvironmentVariableService envVarService;
@ -39,7 +41,7 @@ public class GoComponentDetector : FileComponentDetector
this.Logger = logger;
}
public override string Id { get; } = "Go";
public override string Id => "Go";
public override IEnumerable<string> Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.GoMod) };
@ -47,7 +49,101 @@ public class GoComponentDetector : FileComponentDetector
public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = new[] { ComponentType.Go };
public override int Version => 6;
public override int Version => 7;
protected override Task<IObservable<ProcessRequest>> OnPrepareDetectionAsync(
IObservable<ProcessRequest> processRequests, IDictionary<string, string> 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<ComponentStream> 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(?<version>\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<string, string> detectorArgs)
{
@ -75,7 +171,7 @@ public class GoComponentDetector : FileComponentDetector
{
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");
" More info: https://github.com/microsoft/component-detection/blob/main/docs/detectors/go.md#fallback-detection-strategy");
}
}
catch (Exception ex)
@ -97,7 +193,7 @@ public class GoComponentDetector : FileComponentDetector
case ".MOD":
{
this.Logger.LogDebug("Found Go.mod: {Location}", file.Location);
this.ParseGoModFile(singleFileComponentRecorder, file, record);
await this.ParseGoModFileAsync(singleFileComponentRecorder, file, record);
break;
}
@ -135,8 +231,8 @@ public class GoComponentDetector : FileComponentDetector
}
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.");
"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)
{
@ -159,36 +255,53 @@ public class GoComponentDetector : FileComponentDetector
return true;
}
private void ParseGoModFile(
private void TryRegisterDependencyFromModLine(string line, ISingleFileComponentRecorder singleFileComponentRecorder)
{
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 line = reader.ReadLine();
while (line != null && !line.StartsWith("require ("))
// There can be multiple require( ) sections in go 1.17+. loop over all of them.
while (!reader.EndOfStream)
{
if (line.StartsWith("go "))
var line = await reader.ReadLineAsync();
while (line != null && !line.StartsWith("require ("))
{
goGraphTelemetryRecord.GoModVersion = line[3..].Trim();
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("require "))
{
this.TryRegisterDependencyFromModLine(line[8..], singleFileComponentRecorder);
}
line = await reader.ReadLineAsync();
}
line = reader.ReadLine();
}
// Stopping at the first ) restrict the detection to only the require section.
while ((line = reader.ReadLine()) != null && !line.EndsWith(")"))
{
if (this.TryToCreateGoComponentFromModLine(line, out var goComponent))
// Stopping at the first ) restrict the detection to only the require section.
while ((line = await reader.ReadLineAsync()) != null && !line.EndsWith(")"))
{
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);
this.TryRegisterDependencyFromModLine(line, singleFileComponentRecorder);
}
}
}

Просмотреть файл

@ -183,6 +183,67 @@ $#26^#25%4";
Assert.AreEqual(0, componentRecorder.GetDetectedComponents().Count());
}
[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()
{