Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DotNetComponent #1363

Merged
merged 8 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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 @@ -60,7 +60,7 @@ public async Task<CommandLineExecutionResult> ExecuteCommandAsync(
CancellationToken cancellationToken = default,
params string[] parameters)
{
var isCommandLocatable = await this.CanCommandBeLocatedAsync(command, additionalCandidateCommands);
var isCommandLocatable = await this.CanCommandBeLocatedAsync(command, additionalCandidateCommands, workingDirectory, parameters);
if (!isCommandLocatable)
{
throw new InvalidOperationException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,7 @@ public enum ComponentType : byte

[EnumMember]
Swift = 18,

[EnumMember]
DotNet = 19,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;

using System.Text;

#nullable enable

using PackageUrl;

public class DotNetComponent : TypedComponent
{
private DotNetComponent()
{

Check warning on line 12 in src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs#L11-L12

Added lines #L11 - L12 were not covered by tests
/* Reserved for deserialization */
}

Check warning on line 14 in src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs#L14

Added line #L14 was not covered by tests

public DotNetComponent(string? sdkVersion, string? targetFramework = null, string? projectType = null)
{
this.SdkVersion = sdkVersion;
this.TargetFramework = targetFramework;
this.ProjectType = projectType; // application, library, or null
}

/// <summary>
/// SDK Version detected, could be null if no global.json exists and no dotnet is on the path.
/// </summary>
public string? SdkVersion { get; set; }

/// <summary>
/// Target framework for this instance. Null in the case of global.json.
/// </summary>
public string? TargetFramework { get; set; }

/// <summary>
/// Project type: application, library. Null in the case of global.json or if no project output could be discovered.
/// </summary>
public string? ProjectType { get; set; }

public override ComponentType Type => ComponentType.DotNet;

Check warning on line 38 in src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Contracts/TypedComponent/DotNetComponent.cs#L38

Added line #L38 was not covered by tests

/// <summary>
/// Provides an id like `dotnet {SdkVersion} - {TargetFramework} - {ProjectType}` where targetFramework and projectType are only present if not null.
/// </summary>
public override string Id
{
get
{
var builder = new StringBuilder($"dotnet {this.SdkVersion ?? "unknown"}");
if (this.TargetFramework is not null)
{
builder.Append($" - {this.TargetFramework}");

if (this.ProjectType is not null)
{
builder.Append($" - {this.ProjectType}");
}
}

return builder.ToString();
}
}

// TODO - do we need to add a type to prul https://github.com/package-url/purl-spec/blob/main/PURL-TYPES.rst
public override PackageURL PackageUrl => new PackageURL("generic", null, "dotnet-sdk", this.SdkVersion ?? "unknown", null, null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
namespace Microsoft.ComponentDetection.Detectors.DotNet;

#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using global::NuGet.ProjectModel;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;

public class DotNetComponentDetector : FileComponentDetector, IExperimentalDetector
{
private const string GlobalJsonFileName = "global.json";
private readonly ICommandLineInvocationService commandLineInvocationService;
private readonly IDirectoryUtilityService directoryUtilityService;
private readonly IFileUtilityService fileUtilityService;
private readonly IPathUtilityService pathUtilityService;
private readonly LockFileFormat lockFileFormat = new();
private readonly Dictionary<string, string?> sdkVersionCache = [];
private string? sourceDirectory;
private string? sourceFileRootDirectory;

public DotNetComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
ICommandLineInvocationService commandLineInvocationService,
IDirectoryUtilityService directoryUtilityService,
IFileUtilityService fileUtilityService,
IPathUtilityService pathUtilityService,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<DotNetComponentDetector> logger)
{
this.commandLineInvocationService = commandLineInvocationService;
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.directoryUtilityService = directoryUtilityService;
this.fileUtilityService = fileUtilityService;
this.pathUtilityService = pathUtilityService;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id => "DotNet";

public override IList<string> SearchPatterns { get; } = [LockFileFormat.AssetsFileName];

public override IEnumerable<ComponentType> SupportedComponentTypes => [ComponentType.DotNet];

public override int Version { get; } = 1;

public override IEnumerable<string> Categories => ["DotNet"];

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);
return process.ExitCode == 0 ? process.StdOut.Trim() : null;
}

public override Task<IndividualDetectorScanResult> ExecuteDetectorAsync(ScanRequest request, CancellationToken cancellationToken = default)
{
this.sourceDirectory = this.pathUtilityService.NormalizePath(request.SourceDirectory.FullName);
this.sourceFileRootDirectory = this.pathUtilityService.NormalizePath(request.SourceFileRoot?.FullName);

return base.ExecuteDetectorAsync(request, cancellationToken);
}

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
var lockFile = this.lockFileFormat.Read(processRequest.ComponentStream.Stream, processRequest.ComponentStream.Location);

var projectPath = lockFile.PackageSpec.RestoreMetadata.ProjectPath;
var projectDirectory = this.pathUtilityService.GetParentDirectory(projectPath);
var sdkVersion = await this.GetSdkVersionAsync(projectDirectory, cancellationToken);

var projectName = lockFile.PackageSpec.RestoreMetadata.ProjectName;
var projectOutputPath = lockFile.PackageSpec.RestoreMetadata.OutputPath;
var targetType = this.GetProjectType(projectOutputPath, projectName, cancellationToken);

var componentReporter = this.ComponentRecorder.CreateSingleFileComponentRecorder(projectPath);
foreach (var target in lockFile.Targets)
{
var targetFramework = target.TargetFramework?.GetShortFolderName();

componentReporter.RegisterUsage(new DetectedComponent(new DotNetComponent(sdkVersion, targetFramework, targetType)));
}
}

private string? GetProjectType(string projectOutputPath, string projectName, CancellationToken cancellationToken)
{
if (this.directoryUtilityService.Exists(projectOutputPath))
{
var namePattern = (projectName ?? "*") + ".dll";

// 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));
foreach (var candidate in candidates)
{
if (this.IsApplication(candidate))
{
return "application";
}
else
{
return "library";
}
}
}

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

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs#L115

Added line #L115 was not covered by tests

return null;
}

private bool IsApplication(string assemblyPath)
{
try
{
using var peReader = new PEReader(this.fileUtilityService.MakeFileStream(assemblyPath));

// despite the name `IsExe` this is actually based of the CoffHeader Characteristics
return peReader.PEHeaders.IsExe;
}
catch (Exception)
{
return false;

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

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs#L129-L131

Added lines #L129 - L131 were not covered by tests
}
}

/// <summary>
/// Recursively get the sdk version from the project directory or parent directories.
/// </summary>
/// <param name="projectDirectory">Directory to start the search.</param>
/// <param name="cancellationToken">Cancellation token to halt the search.</param>
/// <returns>Sdk version found, or null if no version can be detected.</returns>
private async Task<string?> GetSdkVersionAsync(string projectDirectory, CancellationToken cancellationToken)
{
// normalize since we need to use as a key
projectDirectory = this.pathUtilityService.NormalizePath(projectDirectory);
if (this.sdkVersionCache.TryGetValue(projectDirectory, out var sdkVersion))
{
return sdkVersion;

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

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/dotnet/DotNetComponentDetector.cs#L146-L147

Added lines #L146 - L147 were not covered by tests
}

var parentDirectory = this.pathUtilityService.GetParentDirectory(projectDirectory);
var globalJsonPath = Path.Combine(projectDirectory, GlobalJsonFileName);

if (this.fileUtilityService.Exists(globalJsonPath))
{
var globalJson = await JsonDocument.ParseAsync(this.fileUtilityService.MakeFileStream(globalJsonPath), cancellationToken: cancellationToken);
if (globalJson.RootElement.TryGetProperty("sdk", out var sdk))
{
if (sdk.TryGetProperty("version", out var version))
{
sdkVersion = version.GetString();
var globalJsonComponent = new DetectedComponent(new DotNetComponent(sdkVersion));
var recorder = this.ComponentRecorder.CreateSingleFileComponentRecorder(globalJsonPath);
recorder.RegisterUsage(globalJsonComponent, isExplicitReferencedDependency: true);
}
}
}
else if (projectDirectory.Equals(this.sourceDirectory, StringComparison.OrdinalIgnoreCase) ||
projectDirectory.Equals(this.sourceFileRootDirectory, StringComparison.OrdinalIgnoreCase) ||
string.IsNullOrEmpty(parentDirectory) ||
projectDirectory.Equals(parentDirectory, StringComparison.OrdinalIgnoreCase))
{
// if we are at the source directory, source file root, or have reached a root directory, run `dotnet --version`
// this could fail if dotnet is not on the path, or if the global.json is malformed
sdkVersion = await this.RunDotNetVersionAsync(projectDirectory, cancellationToken);
}
else
{
// recurse up the directory tree
sdkVersion = await this.GetSdkVersionAsync(parentDirectory, cancellationToken);
}

this.sdkVersionCache[projectDirectory] = sdkVersion;

return sdkVersion;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Microsoft.ComponentDetection.Detectors.Yarn;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using DotNet.Globbing;
using global::DotNet.Globbing;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;

using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.DotNet;

/// <summary>
/// Validating the <see cref="DotNetDetectorExperiment"/>.
/// </summary>
public class DotNetDetectorExperiment : IExperimentConfiguration
{
/// <inheritdoc />
public string Name => "DotNetDetector";

/// <inheritdoc />
public bool IsInControlGroup(IComponentDetector componentDetector) => false;

/// <inheritdoc />
public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is DotNetComponentDetector;

/// <inheritdoc />
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
using Microsoft.ComponentDetection.Detectors.CocoaPods;
using Microsoft.ComponentDetection.Detectors.Conan;
using Microsoft.ComponentDetection.Detectors.Dockerfile;
using Microsoft.ComponentDetection.Detectors.DotNet;
using Microsoft.ComponentDetection.Detectors.Go;
using Microsoft.ComponentDetection.Detectors.Gradle;
using Microsoft.ComponentDetection.Detectors.Ivy;
Expand Down Expand Up @@ -65,6 +66,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IExperimentConfiguration, SimplePipExperiment>();
services.AddSingleton<IExperimentConfiguration, RustCliDetectorExperiment>();
services.AddSingleton<IExperimentConfiguration, Go117DetectorExperiment>();
services.AddSingleton<IExperimentConfiguration, DotNetDetectorExperiment>();

// Detectors
// CocoaPods
Expand All @@ -79,6 +81,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
// Dockerfile
services.AddSingleton<IComponentDetector, DockerfileComponentDetector>();

// DotNet
services.AddSingleton<IComponentDetector, DotNetComponentDetector>();

// Go
services.AddSingleton<IComponentDetector, GoComponentDetector>();
services.AddSingleton<IComponentDetector, Go117ComponentDetector>();
Expand Down
Loading
Loading