Skip to content

Commit aef5d6f

Browse files
ericstjgrvillicCopilot
authored
Detect self-contained projects in DotNetComponentDetector (#1689)
* Detect self-contained projects in DotNetComponentDetector Report whether a .NET project target is self-contained by appending -selfcontained to the projectType (e.g. application-selfcontained). Uses a heuristic based on the assets file: for each target framework, if any PackageDownload name starts with a FrameworkReference name followed by .Runtime, the target is considered self-contained. This covers both SelfContained=true and PublishAot=true scenarios, as both result in runtime package downloads in the assets file. This relationship is not a guarantee. The actual relationship is defined by the SDK's KnownFramework items, but we can't read those from build assets. This convention has been followed for all in-support framework versions and should be acceptable. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Greg Villicana <58237075+grvillic@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f5a6003 commit aef5d6f

File tree

4 files changed

+589
-5
lines changed

4 files changed

+589
-5
lines changed

docs/detectors/dotnet.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ and have unreported vulnerabilities. `TargetFramework` is determined from the `
2424
the type of the project is determined by locating the project's output assembly in a subdirectory of the
2525
output path and reading the PE COFF header's characteristics for `IMAGE_FILE_EXECUTABLE_IMAGE`[2].
2626

27+
The `ProjectType` value is further qualified with a `-selfcontained` suffix (e.g. `application-selfcontained`
28+
or `library-selfcontained`) when the project is detected as self-contained. A project is considered
29+
self-contained when either:
30+
- Its `project.assets.json` indicates that a framework reference (e.g. `Microsoft.NETCore.App`) has a
31+
corresponding runtime package download (e.g. `Microsoft.NETCore.App.Runtime.*`) listed in the target
32+
framework's `downloadDependencies`. This covers `SelfContained=true` scenarios.
33+
- The target references `Microsoft.DotNet.ILCompiler`, which indicates native AOT compilation
34+
(`PublishAot=true`) and therefore an implicitly self-contained deployment.
35+
36+
Self-contained applications bundle the .NET runtime and are responsible for servicing it, so this
37+
distinction is important for vulnerability tracking.
38+
2739
[1]: https://learn.microsoft.com/en-us/dotnet/core/tools/global-json
2840
[2]: https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#characteristics
2941

@@ -33,4 +45,4 @@ If the `dotnet` executable is not on the path the detector may fail to locate th
3345
project. The detector will fallback to parsing the `global.json` in this case if it is present.
3446
Detection of the output type is done by locating the output assembly under the output path specified in
3547
`project.assets.json`. Some build systems may place project intermediates in a different location. In this
36-
case the project type will be reported as `unknown`.
48+
case the project type will be reported as `unknown` and the `-selfcontained` suffix will not be appended.

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ public DotNetComponent(string sdkVersion, string targetFramework = null, string
3838
public string TargetFramework { get; set; }
3939

4040
/// <summary>
41-
/// Project type: application, library. Null in the case of global.json or if no project output could be discovered.
41+
/// Project type: application, library, application-selfcontained, library-selfcontained, or unknown.
42+
/// Set to "unknown" when the project output could not be discovered (e.g. global.json or missing output assembly).
43+
/// The "-selfcontained" suffix is appended when the project bundles the .NET runtime
44+
/// (i.e. the target framework has a runtime package download matching a framework reference,
45+
/// or the target references Microsoft.DotNet.ILCompiler indicating native AOT).
4246
/// </summary>
4347
[JsonPropertyName("projectType")]
4448
public string ProjectType { get; set; }

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ namespace Microsoft.ComponentDetection.Detectors.DotNet;
1010
using System.Text.Json;
1111
using System.Threading;
1212
using System.Threading.Tasks;
13+
using global::NuGet.Frameworks;
1314
using global::NuGet.ProjectModel;
1415
using Microsoft.ComponentDetection.Contracts;
1516
using Microsoft.ComponentDetection.Contracts.Internal;
@@ -201,9 +202,11 @@ protected override async Task OnFileFoundAsync(ProcessRequest processRequest, ID
201202
var componentReporter = this.ComponentRecorder.CreateSingleFileComponentRecorder(projectPath);
202203
foreach (var target in lockFile.Targets ?? [])
203204
{
204-
var targetFramework = target.TargetFramework?.GetShortFolderName();
205+
var targetFramework = target.TargetFramework;
206+
var isSelfContained = this.IsSelfContained(lockFile.PackageSpec, targetFramework, target);
207+
var targetTypeWithSelfContained = this.GetTargetTypeWithSelfContained(targetType, isSelfContained);
205208

206-
componentReporter.RegisterUsage(new DetectedComponent(new DotNetComponent(sdkVersion, targetFramework, targetType)));
209+
componentReporter.RegisterUsage(new DetectedComponent(new DotNetComponent(sdkVersion, targetFramework?.GetShortFolderName(), targetTypeWithSelfContained)));
207210
}
208211
}
209212

@@ -247,6 +250,59 @@ private bool IsApplication(string assemblyPath)
247250
return peReader.PEHeaders.IsExe;
248251
}
249252

253+
private bool IsSelfContained(PackageSpec packageSpec, NuGetFramework? targetFramework, LockFileTarget target)
254+
{
255+
// PublishAot projects reference Microsoft.DotNet.ILCompiler, which implies
256+
// native AOT compilation and therefore a self-contained deployment.
257+
if (target?.Libraries != null &&
258+
target.Libraries.Any(lib => "Microsoft.DotNet.ILCompiler".Equals(lib.Name, StringComparison.OrdinalIgnoreCase)))
259+
{
260+
return true;
261+
}
262+
263+
if (packageSpec?.TargetFrameworks == null || targetFramework == null)
264+
{
265+
return false;
266+
}
267+
268+
var targetFrameworkInfo = packageSpec.TargetFrameworks.FirstOrDefault(tf => tf.FrameworkName == targetFramework);
269+
if (targetFrameworkInfo == null)
270+
{
271+
return false;
272+
}
273+
274+
var frameworkReferences = targetFrameworkInfo.FrameworkReferences;
275+
var packageDownloads = targetFrameworkInfo.DownloadDependencies;
276+
277+
if (frameworkReferences == null || frameworkReferences.Count == 0 || packageDownloads.IsDefaultOrEmpty)
278+
{
279+
return false;
280+
}
281+
282+
foreach (var frameworkRef in frameworkReferences)
283+
{
284+
var frameworkName = frameworkRef.Name;
285+
var hasRuntimeDownload = packageDownloads.Any(pd => pd.Name.StartsWith($"{frameworkName}.Runtime", StringComparison.OrdinalIgnoreCase));
286+
287+
if (hasRuntimeDownload)
288+
{
289+
return true;
290+
}
291+
}
292+
293+
return false;
294+
}
295+
296+
private string? GetTargetTypeWithSelfContained(string? targetType, bool isSelfContained)
297+
{
298+
if (string.IsNullOrWhiteSpace(targetType))
299+
{
300+
return targetType;
301+
}
302+
303+
return isSelfContained ? $"{targetType}-selfcontained" : targetType;
304+
}
305+
250306
/// <summary>
251307
/// Recursively get the sdk version from the project directory or parent directories.
252308
/// </summary>

0 commit comments

Comments
 (0)