Skip to content
Open
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
2 changes: 2 additions & 0 deletions .github/workflows/detector-version-bump-reminder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ permissions:

jobs:
comment:
# Fork pushes cannot find PRs opened against the upstream repository.
if: github.event.repository.fork == false
Comment thread
ryanbrandenburg marked this conversation as resolved.
runs-on: ubuntu-latest
steps:
- name: Harden Runner
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Component Detection supports detecting libraries from the following ecosystems:
| [Conda (Python)](docs/detectors/conda.md) | ✔ | ✔ |
| [Docker Compose](docs/detectors/dockercompose.md) | ✔ | ❌ |
| [Dockerfile](docs/detectors/dockerfile.md) | ✔ | ❌ |
| [DotNet SDK](docs/detectors/dotnet.md) | ✔ | ❌ |
| [DotNet SDK](docs/detectors/nuget.md#msbuildbinarylog) | ✔ | ❌ |
| [Go](docs/detectors/go.md) | ✔ | ✔ (with Go 1.11+) |
| [Gradle (lockfiles only)](docs/detectors/gradle.md) | ✔ | ❌ |
| [Helm](docs/detectors/helm.md) | ✔ | ❌ |
Expand Down
15 changes: 4 additions & 11 deletions docs/detectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,6 @@
| --------------------------- | ---------- |
| DockerfileComponentDetector | Experimental |

- [DotNet](dotnet.md)

| Detector | Status |
| ----------------------- | ------ |
| DotNetComponentDetector | Stable |

- [Go](go.md)

| Detector | Status |
Expand Down Expand Up @@ -86,8 +80,7 @@
| ------------------------------------------------ | ---------- |
| NuGetComponentDetector | Stable |
| NuGetPackagesConfigDetector | Stable |
| NuGetProjectModelProjectCentricComponentDetector | Stable |
| MSBuildBinaryLogComponentDetector | Experimental |
| MSBuildBinaryLogComponentDetector | Stable |

- [Pip](pip.md)

Expand Down Expand Up @@ -129,9 +122,9 @@

- [Swift](swift.md)

| Detector | Status |
| ------------------------------- | ---------- |
| SwiftResolvedComponentDetector | DefaultOff |
| Detector | Status |
| ------------------------------ | ------------ |
| SwiftResolvedComponentDetector | Experimental |

- [Uv](uv.md)

Expand Down
48 changes: 0 additions & 48 deletions docs/detectors/dotnet.md

This file was deleted.

71 changes: 45 additions & 26 deletions docs/detectors/nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ NuGet Detection depends on the following to successfully run:
- The files each NuGet detector searches for:
- [The `NuGet` detector looks for `*.nupkg`, `*.nuspec`, `nuget.config`, `paket.lock`][1]
- [The `NuGetPackagesConfig` detector looks for `packages.config`][2]
- [The `NuGetProjectCentric` detector looks for `project.assets.json`][3]
- [The `MSBuildBinaryLog` detector looks for `project.assets.json` and optionally uses `*.binlog` files][3]

[1]: https://github.com/microsoft/component-detection/blob/13f3e9f32c94bf6189fbd0bfbdf2e68cc60fccd9/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetComponentDetector.cs#L40
[2]: https://github.com/microsoft/component-detection/blob/13f3e9f32c94bf6189fbd0bfbdf2e68cc60fccd9/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetPackagesConfigDetector.cs#L25
[3]: https://github.com/microsoft/component-detection/blob/13f3e9f32c94bf6189fbd0bfbdf2e68cc60fccd9/src/Microsoft.ComponentDetection.Detectors/nuget/NuGetProjectModelProjectCentricComponentDetector.cs#L205
[3]: https://github.com/microsoft/component-detection/blob/main/src/Microsoft.ComponentDetection.Detectors/nuget/MSBuildBinaryLogComponentDetector.cs

## Detection Strategy

NuGet Detection is performed by parsing any `*.nuspec`, `*.nupkg`, `*.packages.config`, or `*.project.assets` files found under the scan directory. By searching for all `*.nuspec,` `*.nupkg` files on disk the global NuGet cache gets searched which can include packages that are not included in the final build.

## NuGetProjectCentric
## MSBuildBinaryLog

The default-on `MSBuildBinaryLog` detector replaces the former `NuGetProjectCentric` and `DotNet`
detectors. It raises NuGet components referenced by projects that use the latest NuGet (v3 or later)
and build-integrated `PackageReference` [items][4], and it records the corresponding DotNet SDK
components. NuGet components represent both direct dependencies and transitive dependencies brought
in by references from direct package and project references. Packages that contribute no assets to
the project or exclusively contribute [Compile assets][5] are treated as development dependencies.

The `NuGetProjectCentric` detector raises NuGet components referenced by projects that use the latest NuGet (v3 or later) and build-integrated `PackageReference` [items][4]. These components represent both direct dependencies and transitive dependencies brought in by references from direct package and project references. Packages that contribute no assets to the project or exclusively contribute [Compile assets][5] are treated as development dependencies.
It looks for `project.assets.json` files and separately discovers `*.binlog` files. The binlog provides
build-time context that isn't available from `project.assets.json` alone.

The .NET SDK will perform conflict resolution for all packages during the build. This process will remove assets from packages that overlap with assets of the same name that come from the .NET framework that's used by the project. For example if a project references `System.Text.Json` version `6.0.0` and targets `net8.0` which includes a newer `System.Text.Json` the .NET SDK will ignore all the assets provided by the `System.Text.Json` package and only use those provided by the framework. Unfortunately the result of this process is not persisted in any build artifact. To approximate this we capture a list of packages per framework version that would lose to the framework assets. When examining packages referenced by a project for a given framework, if we find that its included in the list we'll mark it as a development dependency.

Expand All @@ -30,28 +38,14 @@ Future versions of the .NET SDK have moved this framework conflict resolution in
[5]: https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#controlling-dependency-assets
[6]: https://github.com/NuGet/Home/blob/451c27180d14214bca60483caee57f0dc737b8cf/accepted/2024/prune-package-reference.md

## NuGetPackagesConfig

The `NuGetPackagesConfig` detector raises NuGet components referenced by projects or solutions that use the older NuGet (v2) `packages.config` [file][7].

[7]: https://learn.microsoft.com/en-us/nuget/reference/packages-config

## MSBuildBinaryLog

The `MSBuildBinaryLog` detector is an **Experimental** detector intended to eventually replace both the `NuGetProjectCentric` and `DotNet` detectors. It combines MSBuild binary log (binlog) information with `project.assets.json` to provide enhanced component detection with project-level classifications.

As an experimental detector, it runs automatically whenever a scan is performed, but its results are not reported as part of the normal scan output. Instead, the results are compared against the existing `NuGetProjectCentric` and `DotNet` detectors and recorded as telemetry so maintainers can evaluate parity before promoting the detector to default.

It looks for `project.assets.json` files and separately discovers `*.binlog` files. The binlog provides build-time context that isn't available from `project.assets.json` alone.

### MSBuild Properties

The detector extracts the following MSBuild properties from binlog data:

| Property | Usage |
| --- | --- |
| `NETCoreSdkVersion` | Registered as the SDK version for the DotNet component. More accurate than `dotnet --version`, which can differ due to `global.json` rollforward. |
| `OutputType` | Classifies projects as "application" (`Exe`, `WinExe`, `AppContainerExe`) or "library" (`Library`, `Module`). The `DotNet` detector uses PE header inspection, which requires compiled output. |
| `OutputType` | Classifies projects as "application" (`Exe`, `WinExe`, `AppContainerExe`) or "library" (`Library`, `Module`). |
| `ProjectAssetsFile` | Maps binlog project info to the corresponding `project.assets.json` on disk. |
| `TargetFramework` / `TargetFrameworks` | Identifies inner builds for multi-targeted projects and determines per-TFM properties. |
| `IsTestProject` | When `true`, all dependencies of the project are marked as development dependencies. |
Expand All @@ -75,16 +69,41 @@ For multi-targeted projects, the detector understands the MSBuild outer/inner bu

### Fallback Mode

When no binlog is available for a project, the detector falls back to standard NuGet detection behavior (equivalent to the `NuGetProjectCentric` detector).
When no binlog is available for a project, or a binlog cannot be parsed, the detector falls back to
the existing `project.assets.json`-based NuGet and DotNet detection behavior. A parse failure logs a
warning and emits `FailedParsingFile` telemetry containing the detector ID, binlog path, and exception
message. To enable the richer build context, place a `*.binlog` in the scan directory (for example,
build with `dotnet build -bl`).

For DotNet SDK detection, fallback processing starts at the project location from
`project.assets.json` and searches parent directories for a [`global.json`][7]. It runs
`dotnet --version` from the selected directory to resolve the SDK version, stopping the upward search
at `SourceDirectory`, `SourceFileRoot`, or the filesystem root. If `dotnet` is unavailable, it uses
the SDK version declared in `global.json` when possible.

The detector registers the target frameworks from `project.assets.json` and classifies each project
as an `application`, `library`, or `unknown`. Binlog processing uses `OutputType`; fallback processing
uses the output assembly's [PE COFF characteristics][8] when the assembly is available. Project type
is qualified with `-selfcontained` when either the binlog sets `SelfContained` or `PublishAot`, or the
lock file indicates a corresponding runtime package download or a `Microsoft.DotNet.ILCompiler`
reference. This distinction matters because self-contained applications bundle the .NET runtime and
must service it independently.

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

### Enabling the Detector
## NuGetPackagesConfig

Pass `--DetectorArgs MSBuildBinaryLog=EnableIfDefaultOff` and ensure a `*.binlog` file is present in the scan directory (e.g., by building with `dotnet build -bl`).
The `NuGetPackagesConfig` detector raises NuGet components referenced by projects or solutions that
use the older NuGet (v2) [`packages.config` file][9].

## Known Limitations
[9]: https://learn.microsoft.com/en-us/nuget/reference/packages-config

- Any components that are only found in `*.nuspec` or `*.nupkg` files will not be detected with the latest NuGet Detector approach, because the NuGet detector that scans `*.nuspec` or `*.nupkg` files overreports. This is due to of NuGet's [restore behaviour][8] which downloads all possible dependencies before [resolving the final dependency graph][9].
## Known Limitations

[8]: https://learn.microsoft.com/en-us/nuget/consume-packages/package-restore#package-restore-behavior
[9]: https://learn.microsoft.com/en-us/nuget/concepts/dependency-resolution
- Any components that are only found in `*.nuspec` or `*.nupkg` files will not be detected with the latest NuGet Detector approach, because the NuGet detector that scans `*.nuspec` or `*.nupkg` files overreports. This is due to NuGet's [restore behavior][10], which downloads all possible dependencies before [resolving the final dependency graph][11].
- Without matching binlog data, project type can be `unknown` when the output assembly is unavailable or is stored outside the output path recorded in `project.assets.json`.
- Without matching binlog data, SDK version detection depends on `dotnet --version` or a usable `global.json`.

[10]: https://learn.microsoft.com/en-us/nuget/consume-packages/package-restore#package-restore-behavior
[11]: https://learn.microsoft.com/en-us/nuget/concepts/dependency-resolution
6 changes: 6 additions & 0 deletions docs/detectors/pnpm.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ Pnpm detection relies on the presence of lockfiles generated by the pnpm package

The detector supports lockfile versions 5, 6, and 9, with version 9 being the maximum supported version.

Lockfiles can contain one or two YAML documents. When an environment document is
present, it comes before the project document. Both documents are scanned so that
config and package-manager dependencies are included alongside project dependencies.
Empty documents, more than two documents, and inconsistent declared lockfile versions
are rejected as invalid lockfiles.

## Detection strategy

Pnpm detection is performed by parsing lockfiles found in the scan directory. The `PnpmComponentDetectorFactory` acts as a version-aware factory that:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,20 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
/// </summary>
public class SwiftComponent : TypedComponent
{
private readonly Uri packageUrl;
[JsonPropertyName("name")]
public string Name { get; set; }

private readonly string hash;
[JsonPropertyName("version")]
public string Version { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="SwiftComponent"/> class.
/// </summary>
/// <param name="name">The name of the component.</param>
/// <param name="version">The version of the component.</param>
/// <param name="packageUrl">The package URL of the component.</param>
/// <param name="hash">The hash of the component.</param>
public SwiftComponent(string name, string version, string packageUrl, string hash)
{
this.Name = this.ValidateRequiredInput(name, nameof(name), nameof(ComponentType.Swift));
this.Version = this.ValidateRequiredInput(version, nameof(version), nameof(ComponentType.Swift));
this.ValidateRequiredInput(packageUrl, nameof(packageUrl), nameof(ComponentType.Swift));
this.packageUrl = new Uri(packageUrl);
this.hash = this.ValidateRequiredInput(hash, nameof(hash), nameof(ComponentType.Swift));
}
[JsonPropertyName("kind")]
public string Kind { get; set; }

[JsonPropertyName("name")]
public string Name { get; }
[JsonPropertyName("commitHash")]
public string CommitHash { get; set; }

[JsonPropertyName("version")]
public string Version { get; }
[JsonPropertyName("repositoryUrl")]
public Uri RepositoryUrl { get; set; }

[JsonIgnore]
public override ComponentType Type => ComponentType.Swift;
Expand All @@ -52,25 +41,25 @@ public SwiftComponent(string name, string version, string packageUrl, string has
version: this.Version,
qualifiers: new SortedDictionary<string, string>
{
{ "repository_url", this.packageUrl.AbsoluteUri },
{ "repository_url", this.RepositoryUrl.AbsoluteUri },
},
subpath: null);

protected override string ComputeBaseId() => $"{this.Name} {this.Version} - {this.Type}";
protected override string ComputeBaseId() => $"{this.RepositoryUrl.AbsoluteUri} {this.Version} - {this.Type}";

private string GetNamespaceFromPackageUrl()
{
// In the case of github.com, the namespace should contain the user/organization
// See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#swift
var uppercaseHost = this.packageUrl.Host.ToUpperInvariant();
var uppercaseHost = this.RepositoryUrl.Host.ToUpperInvariant();
if (uppercaseHost.Contains("GITHUB.COM"))
{
// The first segment of the URL will contain the user or organization for GitHub
var firstSegment = this.packageUrl.Segments[1].Trim('/');
return $"{this.packageUrl.Host}/{firstSegment}";
var firstSegment = this.RepositoryUrl.Segments[1].Trim('/');
return $"{this.RepositoryUrl.Host}/{firstSegment}";
}

// In the default case of a generic host, the namespace should be the just the host
return this.packageUrl.Host;
return this.RepositoryUrl.Host;
}
}
Loading