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 @@ -58,20 +58,67 @@

private string? NormalizeDirectory(string? path) => string.IsNullOrEmpty(path) ? path : Path.TrimEndingDirectorySeparator(this.pathUtilityService.NormalizePath(path));

/// <summary>
/// Given a path relative to sourceDirectory, and the same path in another filesystem,
/// determine what path could be replaced with root.
/// </summary>
/// <param name="rootBasedPath">Some path under root, including the root path.</param>
/// <param name="rebasePath">Path to the same file as <paramref name="rootBasedPath"/> but in a different root.</param>
/// <returns>Portion of <paramref name="rebasePath"/> that corresponds to root, or null if it can not be rebased.</returns>
private string? GetRootRebasePath(string rootBasedPath, string? rebasePath)
{
if (string.IsNullOrEmpty(rebasePath) || string.IsNullOrEmpty(this.sourceDirectory) || string.IsNullOrEmpty(rootBasedPath))
{
return null;

Check warning on line 72 in src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs#L71-L72

Added lines #L71 - L72 were not covered by tests
}

// sourceDirectory is normalized, normalize others
rootBasedPath = this.pathUtilityService.NormalizePath(rootBasedPath);
rebasePath = this.pathUtilityService.NormalizePath(rebasePath);

// nothing to do if the paths are the same
if (rebasePath.Equals(rootBasedPath, StringComparison.Ordinal))
{
return null;
}

// find the relative path under root.
var rootRelativePath = this.pathUtilityService.NormalizePath(Path.GetRelativePath(this.sourceDirectory!, rootBasedPath));

// if the rebase path has the same relative portion, then we have a replacement.
if (rebasePath.EndsWith(rootRelativePath))
{
return rebasePath[..^rootRelativePath.Length];
}

// The path didn't have a common relative path, it might have been copied from a completely different location since it was built.
// We cannot rebase the paths.
return null;
}

private async Task<string?> RunDotNetVersionAsync(string workingDirectoryPath, CancellationToken cancellationToken)
{
var workingDirectory = new DirectoryInfo(workingDirectoryPath);

var process = await this.commandLineInvocationService.ExecuteCommandAsync("dotnet", ["dotnet.exe"], workingDirectory, cancellationToken, "--version").ConfigureAwait(false);
try
{
var process = await this.commandLineInvocationService.ExecuteCommandAsync("dotnet", ["dotnet.exe"], workingDirectory, cancellationToken, "--version").ConfigureAwait(false);

if (process.ExitCode != 0)
{
// debug only - it could be that dotnet is not actually on the path and specified directly by the build scripts.
this.Logger.LogDebug("Failed to invoke 'dotnet --version'. Return: {Return} StdErr: {StdErr} StdOut: {StdOut}.", process.ExitCode, process.StdErr, process.StdOut);
return null;
}

if (process.ExitCode != 0)
return process.StdOut.Trim();
}
catch (InvalidOperationException ioe)
{
// debug only - it could be that dotnet is not actually on the path and specified directly by the build scripts.
this.Logger.LogDebug("Failed to invoke 'dotnet --version'. Return: {Return} StdErr: {StdErr} StdOut: {StdOut}.", process.ExitCode, process.StdErr, process.StdOut);
this.Logger.LogDebug("Failed to invoke 'dotnet --version'. {Message}", ioe.Message);
return null;
}

return process.StdOut.Trim();
}

public override Task<IndividualDetectorScanResult> ExecuteDetectorAsync(ScanRequest request, CancellationToken cancellationToken = default)
Expand All @@ -86,7 +133,20 @@
{
var lockFile = this.lockFileFormat.Read(processRequest.ComponentStream.Stream, processRequest.ComponentStream.Location);

var projectAssetsDirectory = this.pathUtilityService.GetParentDirectory(processRequest.ComponentStream.Location);
var projectPath = lockFile.PackageSpec.RestoreMetadata.ProjectPath;
var projectOutputPath = lockFile.PackageSpec.RestoreMetadata.OutputPath;

// The output path should match the location that the assets file, if it doesn't we could be analyzing paths
// on a different filesystem root than they were created.
// Attempt to rebase paths based on the difference between this file's location and the output path.
var rebasePath = this.GetRootRebasePath(projectAssetsDirectory, projectOutputPath);

if (rebasePath is not null)
{
projectPath = Path.Combine(this.sourceDirectory!, Path.GetRelativePath(rebasePath, projectPath));
projectOutputPath = Path.Combine(this.sourceDirectory!, Path.GetRelativePath(rebasePath, projectOutputPath));
}

if (!this.fileUtilityService.Exists(projectPath))
{
Expand All @@ -98,11 +158,13 @@
var sdkVersion = await this.GetSdkVersionAsync(projectDirectory, cancellationToken);

var projectName = lockFile.PackageSpec.RestoreMetadata.ProjectName;
var projectOutputPath = lockFile.PackageSpec.RestoreMetadata.OutputPath;

if (!this.directoryUtilityService.Exists(projectOutputPath))
{
this.Logger.LogWarning("Project output path {ProjectOutputPath} specified by {ProjectAssetsPath} does not exist.", projectOutputPath, processRequest.ComponentStream.Location);

// default to use the location of the assets file.
projectOutputPath = projectAssetsDirectory;
}

var targetType = this.GetProjectType(projectOutputPath, projectName, cancellationToken);
Expand All @@ -120,11 +182,11 @@
{
if (this.directoryUtilityService.Exists(projectOutputPath))
{
var namePattern = (projectName ?? "*") + ".dll";
var namePattern = projectName ?? "*";

// look for the compiled output, first as dll then as exe.
var candidates = this.directoryUtilityService.EnumerateFiles(projectOutputPath, namePattern, SearchOption.AllDirectories)
.Concat(this.directoryUtilityService.EnumerateFiles(projectOutputPath, namePattern, SearchOption.AllDirectories));
var candidates = this.directoryUtilityService.EnumerateFiles(projectOutputPath, namePattern + ".dll", SearchOption.AllDirectories)
.Concat(this.directoryUtilityService.EnumerateFiles(projectOutputPath, namePattern + ".exe", SearchOption.AllDirectories));
foreach (var candidate in candidates)
{
try
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
Expand Down Expand Up @@ -62,7 +63,7 @@ public DotNetComponentDetectorTests()
this.mockDirectoryUtilityService.Setup(x => x.Exists(It.IsAny<string>())).Returns((string p) => this.DirectoryExists(p));

// ignore pattern and search option since we don't really need them for tests
this.mockDirectoryUtilityService.Setup(x => x.EnumerateFiles(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<SearchOption>())).Returns((string d, string p, SearchOption s) => this.EnumerateFilesRecursive(d));
this.mockDirectoryUtilityService.Setup(x => x.EnumerateFiles(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<SearchOption>())).Returns((string d, string p, SearchOption s) => this.EnumerateFilesRecursive(d, p));

this.mockPathUtilityService.Setup(x => x.NormalizePath(It.IsAny<string>())).Returns((string p) => p); // don't do normalization
this.mockPathUtilityService.Setup(x => x.GetParentDirectory(It.IsAny<string>())).Returns((string p) => Path.GetDirectoryName(p));
Expand Down Expand Up @@ -91,24 +92,30 @@ private Stream OpenFile(string path)

private bool DirectoryExists(string directory) => this.files.ContainsKey(directory);

private IEnumerable<string> EnumerateFilesRecursive(string directory)
private IEnumerable<string> EnumerateFilesRecursive(string directory, string pattern)
{
if (this.files.TryGetValue(directory, out var fileNames))
{
// a basic approximation of globbing
var patternRegex = new Regex(pattern.Replace(".", "\\.").Replace("*", ".*"));

foreach (var fileName in fileNames.Keys)
{
var filePath = Path.Combine(directory, fileName);

if (fileName.EndsWith(Path.DirectorySeparatorChar))
{
foreach (var subFile in this.EnumerateFilesRecursive(Path.TrimEndingDirectorySeparator(filePath)))
foreach (var subFile in this.EnumerateFilesRecursive(Path.TrimEndingDirectorySeparator(filePath), pattern))
{
yield return subFile;
}
}
else
{
yield return filePath;
if (patternRegex.IsMatch(fileName))
{
yield return filePath;
}
}
}
}
Expand Down Expand Up @@ -267,6 +274,54 @@ public async Task TestDotNetDetectorGlobalJsonRollForward_ReturnsSDKVersion()
discoveredComponents.Where(component => component.Component.Id == "8.0.808 net8.0 unknown - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorGlobalJsonDotNetVersionFails_ReturnsSDKVersion()
{
var projectPath = Path.Combine(RootDir, "path", "to", "project");
var projectAssets = ProjectAssets("projectName", "does-not-exist", projectPath, "net8.0");
var globalJson = GlobalJson("8.0.100");
this.AddFile(projectPath, null);
this.AddFile(Path.Combine(RootDir, "path", "global.json"), globalJson);
this.SetCommandResult(-1);

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile("project.assets.json", projectAssets)
.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 == "8.0.100 unknown unknown - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "8.0.100 net8.0 unknown - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorGlobalJsonDotNetVersionThrows_ReturnsSDKVersion()
{
var projectPath = Path.Combine(RootDir, "path", "to", "project");
var projectAssets = ProjectAssets("projectName", "does-not-exist", projectPath, "net8.0");
var globalJson = GlobalJson("8.0.100");
this.AddFile(projectPath, null);
this.AddFile(Path.Combine(RootDir, "path", "global.json"), globalJson);
this.SetCommandResult((c, d) => throw new InvalidOperationException());

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile("project.assets.json", projectAssets)
.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 == "8.0.100 unknown unknown - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "8.0.100 net8.0 unknown - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorNoGlobalJson_ReturnsDotNetVersion()
{
Expand Down Expand Up @@ -388,6 +443,40 @@ public async Task TestDotNetDetectorMultipleProjectsWithDifferentOutputTypeAndSd
discoveredComponents.Where(component => component.Component.Id == "1.2.3 net48 application - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorExe()
{
// dotnet from global.json will be 4.5.6
var globalJson = GlobalJson("4.5.6");
var globalJsonDir = Path.Combine(RootDir, "path");
this.AddFile(Path.Combine(globalJsonDir, "global.json"), globalJson);

this.SetCommandResult(0, "4.5.6");

// set up an application - not under global.json
var applicationProjectName = "application";
var applicationProjectPath = Path.Combine(RootDir, "path", "to", "project", $"{applicationProjectName}.csproj");
this.AddFile(applicationProjectPath, null);
var applicationOutputPath = Path.Combine(Path.GetDirectoryName(applicationProjectPath), "obj");
var applicationAssetsPath = Path.Combine(applicationOutputPath, "project.assets.json");
var applicationAssets = ProjectAssets("application", applicationOutputPath, applicationProjectPath, "net4.8");
var applicationAssemblyStream = File.OpenRead(Assembly.GetEntryAssembly().Location);
this.AddFile(Path.Combine(applicationOutputPath, "Release", "net4.8", "application.exe"), applicationAssemblyStream);

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile(applicationAssetsPath, applicationAssets)
.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 == "4.5.6 unknown unknown - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "4.5.6 net48 application - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorInvalidOutputAssembly()
{
Expand Down Expand Up @@ -458,4 +547,52 @@ public async Task TestDotNetDetectorNoGlobalJsonSourceRoot()
var discoveredComponents = detectedComponents.ToArray();
discoveredComponents.Where(component => component.Component.Id == "0.0.0 net8.0 unknown - DotNet").Should().ContainSingle();
}

[TestMethod]
public async Task TestDotNetDetectorRebasePaths()
{
// DetectorTestUtility runs under Path.GetTempPath()
var scanRoot = Path.TrimEndingDirectorySeparator(Path.GetTempPath());

// dotnet from global.json will be 4.5.6
var globalJson = GlobalJson("4.5.6");
var globalJsonDir = Path.Combine(scanRoot, "path");
this.AddFile(Path.Combine(globalJsonDir, "global.json"), globalJson);

// make sure we find global.json and read it
this.SetCommandResult(-1);

// set up a library project - under global.json
var libraryProjectName = "library";

var libraryProjectPath = Path.Combine(scanRoot, "path", "to", "project", $"{libraryProjectName}.csproj");
var libraryBuildProjectPath = Path.Combine(RootDir, "path", "to", "project", $"{libraryProjectName}.csproj");
this.AddFile(libraryProjectPath, null);

var libraryOutputPath = Path.Combine(Path.GetDirectoryName(libraryProjectPath), "obj");
var libraryBuildOutputPath = Path.Combine(Path.GetDirectoryName(libraryBuildProjectPath), "obj");
var libraryAssetsPath = Path.Combine(libraryOutputPath, "project.assets.json");

// use "build" paths to simulate an Assets file that has a different root. Here the build assets have RootDir, but the scanned filesystem has scanRoot.
var libraryAssets = ProjectAssets("library", libraryBuildOutputPath, libraryBuildProjectPath, "net8.0", "net6.0", "netstandard2.0");
var libraryAssemblyStream = File.OpenRead(typeof(DotNetComponent).Assembly.Location);
this.AddFile(Path.Combine(libraryOutputPath, "Release", "net8.0", "library.dll"), libraryAssemblyStream);
this.AddFile(Path.Combine(libraryOutputPath, "Release", "net6.0", "library.dll"), libraryAssemblyStream);
this.AddFile(Path.Combine(libraryOutputPath, "Release", "netstandard2.0", "library.dll"), libraryAssemblyStream);

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile(libraryAssetsPath, libraryAssets)
.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 == "4.5.6 unknown unknown - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "4.5.6 net8.0 library - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "4.5.6 net6.0 library - DotNet").Should().ContainSingle();
discoveredComponents.Where(component => component.Component.Id == "4.5.6 netstandard2.0 library - DotNet").Should().ContainSingle();
}
}
Loading