Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public Go117ComponentDetector(

public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = [ComponentType.Go];

public override int Version => 2;
public override int Version => 3;

protected override Task<IObservable<ProcessRequest>> OnPrepareDetectionAsync(
IObservable<ProcessRequest> processRequests,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Microsoft.ComponentDetection.Detectors.Go;

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
Expand All @@ -15,11 +17,50 @@

public GoModParser(ILogger logger) => this.logger = logger;

/// <summary>
/// Checks whether the input path is a potential local file system path
/// 1. '.' checks whether the path is relative to current directory.
/// 2. '..' checks whether the path is relative to some ancestor directory.
/// 3. IsRootedPath checks whether it is an absolute path.
/// </summary>
/// <param name="path">Candidate path.</param>
/// <returns>true if potential local file system path.</returns>
private static bool IsLocalPath(string path)
{
return path.StartsWith('.') || path.StartsWith("..") || Path.IsPathRooted(path);
}

/// <summary>
/// Tries to extract source token from replace directive.
/// </summary>
/// <param name="directiveLine">String containing a directive after replace token.</param>
/// <param name="replaceDirectives">HashSet where the token is placed if replace directive substitutes a local path.</param>
private static void TryExtractReplaceDirective(string directiveLine, HashSet<string> replaceDirectives)
{
var parts = directiveLine.Split("=>", StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
var source = parts[0].Trim().Split(' ')[0];
var target = parts[1].Trim();

if (IsLocalPath(target))
{
replaceDirectives.Add(source);
}
}
}

public async Task<bool> ParseAsync(
ISingleFileComponentRecorder singleFileComponentRecorder,
IComponentStream file,
GoGraphTelemetryRecord record)
{
// Collect replace directives that point to a local path
var replaceDirectives = await this.GetAllReplacePathDirectivesAsync(file);

// Rewind stream after reading replace directives
file.Stream.Seek(0, SeekOrigin.Begin);

using var reader = new StreamReader(file.Stream);

// There can be multiple require( ) sections in go 1.17+. loop over all of them.
Expand All @@ -38,7 +79,7 @@
// are listed in the require () section
if (line.StartsWith(StartString))
{
this.TryRegisterDependencyFromModLine(line[StartString.Length..], singleFileComponentRecorder);
this.TryRegisterDependencyFromModLine(file, line[StartString.Length..], singleFileComponentRecorder, replaceDirectives);
}

line = await reader.ReadLineAsync();
Expand All @@ -47,14 +88,14 @@
// Stopping at the first ) restrict the detection to only the require section.
while ((line = await reader.ReadLineAsync()) != null && !line.EndsWith(')'))
{
this.TryRegisterDependencyFromModLine(line, singleFileComponentRecorder);
this.TryRegisterDependencyFromModLine(file, line, singleFileComponentRecorder, replaceDirectives);
}
}

return true;
}

private void TryRegisterDependencyFromModLine(string line, ISingleFileComponentRecorder singleFileComponentRecorder)
private void TryRegisterDependencyFromModLine(IComponentStream file, string line, ISingleFileComponentRecorder singleFileComponentRecorder, HashSet<string> replaceDirectives)
{
if (line.Trim().StartsWith("//"))
{
Expand All @@ -64,6 +105,15 @@

if (this.TryToCreateGoComponentFromModLine(line, out var goComponent))
{
if (replaceDirectives.Contains(goComponent.Name))
{
// Skip registering this dependency since it's replaced by a local path
// we will be reading this dependency somewhere else
this.logger.LogInformation("Skipping {GoComponentId} from {Location} because it's a local reference.", goComponent.Id, file.Location);
return;
}

this.logger.LogError("Registering {GoComponent} from {Location}", goComponent.Name, file.Location);
singleFileComponentRecorder.RegisterUsage(new DetectedComponent(goComponent));
}
else
Expand All @@ -90,4 +140,47 @@

return true;
}

private async Task<HashSet<string>> GetAllReplacePathDirectivesAsync(IComponentStream file)
{
var replacedDirectives = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
const string singleReplaceDirectiveBegin = "replace ";
const string multiReplaceDirectiveBegin = "replace (";
using (var reader = new StreamReader(file.Stream, leaveOpen: true))
{
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
if (line == null)
{
continue;

Check warning on line 156 in src/Microsoft.ComponentDetection.Detectors/go/Parsers/GoModParser.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/go/Parsers/GoModParser.cs#L155-L156

Added lines #L155 - L156 were not covered by tests
}

line = line.Trim();

// Multiline block: replace (
if (line.StartsWith(multiReplaceDirectiveBegin))
{
while ((line = await reader.ReadLineAsync()) != null)
{
line = line.Trim();
if (line == ")")
{
break;
}

TryExtractReplaceDirective(line, replacedDirectives);
}
}
else if (line.StartsWith(singleReplaceDirectiveBegin))
{
// single line block: replace
var directiveContent = line[singleReplaceDirectiveBegin.Length..].Trim();
TryExtractReplaceDirective(directiveContent, replacedDirectives);
}
}
}

return replacedDirectives;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests;
using FluentAssertions;
using Microsoft.ComponentDetection.Common.Telemetry.Records;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Detectors.Go;
using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -212,4 +213,45 @@ public async Task Go117ModDetector_ExecutingGoVersionFails_DetectorDoesNotFail()

goModParserMock.Verify(parser => parser.ParseAsync(It.IsAny<ISingleFileComponentRecorder>(), It.IsAny<IComponentStream>(), It.IsAny<GoGraphTelemetryRecord>()), Times.Once);
}

[TestMethod]
public async Task Go117ModDetector_VerifyLocalReferencesIgnored()
{
var goModFilePath = "./TestFiles/go_WithLocalReferences.mod"; // Replace with your actual file path
var fileStream = new FileStream(goModFilePath, FileMode.Open, FileAccess.Read);

var goModParser = new GoModParser(this.mockLogger.Object);
var mockSingleFileComponentRecorder = new Mock<ISingleFileComponentRecorder>();

var capturedComponents = new List<DetectedComponent>();
var expectedComponentIds = new List<string>()
{
"github.com/grafana/grafana-app-sdk v0.23.1 - Go",
"k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f - Go",
};

mockSingleFileComponentRecorder
.Setup(m => m.RegisterUsage(
It.IsAny<DetectedComponent>(),
It.IsAny<bool>(),
It.IsAny<string>(),
It.IsAny<bool?>(),
It.IsAny<DependencyScope?>(),
It.IsAny<string>()))
.Callback<DetectedComponent, bool, string, bool?, DependencyScope?, string>((comp, _, _, _, _, _) =>
{
capturedComponents.Add(comp);
});

var mockComponentStream = new Mock<IComponentStream>();
mockComponentStream.Setup(mcs => mcs.Stream).Returns(fileStream);
mockComponentStream.Setup(mcs => mcs.Location).Returns("Location");

var result = await goModParser.ParseAsync(mockSingleFileComponentRecorder.Object, mockComponentStream.Object, new GoGraphTelemetryRecord());
result.Should().BeTrue();
capturedComponents
.Select(c => c.Component.Id)
.Should()
.BeEquivalentTo(expectedComponentIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@
<None Update="Mocks\test.component-detection-pip-report.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="TestFiles\go_WithLocalReferences.mod">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<Folder Include="TestFiles\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module github.com/Go117Tests

go 1.23.5

replace github.com/grafana/grafana => ../../..
replace k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f => k8s.io/kube-openapi v1.1.1


require (
github.com/grafana/grafana v0.0.0-00010101000000-000000000000
github.com/grafana/grafana-app-sdk v0.23.1
k8s.io/apimachinery v0.32.0
k8s.io/apiserver v0.32.0
k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f
)

replace (
k8s.io/apimachinery => ../
k8s.io/apiserver => ./a/b/c
)

replace github.com/grafana/grafana-app-sdk => github.com/grafana/grafana-app-sdk v0.22.1
Loading