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
14 changes: 13 additions & 1 deletion docs/detectors/dotnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ and have unreported vulnerabilities. `TargetFramework` is determined from the `
the type of the project is determined by locating the project's output assembly in a subdirectory of the
output path and reading the PE COFF header's characteristics for `IMAGE_FILE_EXECUTABLE_IMAGE`[2].

The `ProjectType` value is further qualified with a `-selfcontained` suffix (e.g. `application-selfcontained`
or `library-selfcontained`) when the project is detected as self-contained. A project is considered
self-contained when either:
- Its `project.assets.json` indicates that a framework reference (e.g. `Microsoft.NETCore.App`) has a
corresponding runtime package download (e.g. `Microsoft.NETCore.App.Runtime.*`) listed in the target
framework's `downloadDependencies`. This covers `SelfContained=true` scenarios.
- The target references `Microsoft.DotNet.ILCompiler`, which indicates native AOT compilation
(`PublishAot=true`) and therefore an implicitly self-contained deployment.

Self-contained applications bundle the .NET runtime and are responsible for servicing it, so this
distinction is important for vulnerability tracking.

[1]: https://learn.microsoft.com/en-us/dotnet/core/tools/global-json
[2]: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics

Expand All @@ -33,4 +45,4 @@ If the `dotnet` executable is not on the path the detector may fail to locate th
project. The detector will fallback to parsing the `global.json` in this case if it is present.
Detection of the output type is done by locating the output assembly under the output path specified in
`project.assets.json`. Some build systems may place project intermediates in a different location. In this
case the project type will be reported as `unknown`.
case the project type will be reported as `unknown` and the `-selfcontained` suffix will not be appended.
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ public DotNetComponent(string sdkVersion, string targetFramework = null, string
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.
/// Project type: application, library, application-selfcontained, library-selfcontained, or unknown.
/// Set to "unknown" when the project output could not be discovered (e.g. global.json or missing output assembly).
/// The "-selfcontained" suffix is appended when the project bundles the .NET runtime
/// (i.e. the target framework has a runtime package download matching a framework reference,
/// or the target references Microsoft.DotNet.ILCompiler indicating native AOT).
/// </summary>
[JsonPropertyName("projectType")]
public string ProjectType { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace Microsoft.ComponentDetection.Detectors.DotNet;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using global::NuGet.Frameworks;
using global::NuGet.ProjectModel;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
Expand Down Expand Up @@ -201,9 +202,11 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
var componentReporter = this.ComponentRecorder.CreateSingleFileComponentRecorder(projectPath);
foreach (var target in lockFile.Targets ?? [])
{
var targetFramework = target.TargetFramework?.GetShortFolderName();
var targetFramework = target.TargetFramework;
var isSelfContained = this.IsSelfContained(lockFile.PackageSpec, targetFramework, target);
var targetTypeWithSelfContained = this.GetTargetTypeWithSelfContained(targetType, isSelfContained);

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

Expand Down Expand Up @@ -247,6 +250,59 @@ private bool IsApplication(string assemblyPath)
return peReader.PEHeaders.IsExe;
}

private bool IsSelfContained(PackageSpec packageSpec, NuGetFramework? targetFramework, LockFileTarget target)
{
// PublishAot projects reference Microsoft.DotNet.ILCompiler, which implies
// native AOT compilation and therefore a self-contained deployment.
if (target?.Libraries != null &&
target.Libraries.Any(lib => "Microsoft.DotNet.ILCompiler".Equals(lib.Name, StringComparison.OrdinalIgnoreCase)))
{
return true;
}

if (packageSpec?.TargetFrameworks == null || targetFramework == null)
{
return false;
}

var targetFrameworkInfo = packageSpec.TargetFrameworks.FirstOrDefault(tf => tf.FrameworkName == targetFramework);
if (targetFrameworkInfo == null)
{
return false;
}

var frameworkReferences = targetFrameworkInfo.FrameworkReferences;
var packageDownloads = targetFrameworkInfo.DownloadDependencies;

if (frameworkReferences == null || frameworkReferences.Count == 0 || packageDownloads.IsDefaultOrEmpty)
Comment thread
grvillic marked this conversation as resolved.
{
return false;
}

foreach (var frameworkRef in frameworkReferences)
{
var frameworkName = frameworkRef.Name;
var hasRuntimeDownload = packageDownloads.Any(pd => pd.Name.StartsWith($"{frameworkName}.Runtime", StringComparison.OrdinalIgnoreCase));

if (hasRuntimeDownload)
{
return true;
}
}

return false;
}

private string? GetTargetTypeWithSelfContained(string? targetType, bool isSelfContained)
{
if (string.IsNullOrWhiteSpace(targetType))
{
return targetType;
}

return isSelfContained ? $"{targetType}-selfcontained" : targetType;
Comment thread
ericstj marked this conversation as resolved.
}
Comment thread
ericstj marked this conversation as resolved.

/// <summary>
/// Recursively get the sdk version from the project directory or parent directories.
/// </summary>
Expand Down
Loading
Loading