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
8 changes: 7 additions & 1 deletion docs/detectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,20 @@
| NpmLockFileDetector | Stable |
| NpmLockFile3Detector | Experimental |

- NuGet
- [NuGet](nuget.md)

| Detector | Status |
| ------------------------------------------------ | ------ |
| NugetComponentDetector | Stable |
| NugetPackagesConfigDetector | Stable |
| NuGetProjectModelProjectCentricComponentDetector | Stable |

- [Paket](paket.md)

| Detector | Status |
| --------------------- | ------ |
| PaketComponentDetector | Stable |

- [Pip](pip.md)

| Detector | Status |
Expand Down
2 changes: 1 addition & 1 deletion docs/detectors/nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ NuGet Detection depends on the following to successfully run:

- One or more `*.nuspec`, `*.nupkg`, `*.packages.config`, or `.*csproj` files.
- The files each NuGet detector searches for:
- [The `NuGet` detector looks for `*.nupkg`, `*.nuspec`, `nuget.config`, `paket.lock`][1]
- [The `NuGet` detector looks for `*.nupkg`, `*.nuspec`, `nuget.config`][1]
- [The `NuGetPackagesConfig` detector looks for `packages.config`][2]
- [The `NuGetProjectCentric` detector looks for `project.assets.json`][3]

Expand Down
60 changes: 60 additions & 0 deletions docs/detectors/paket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Paket Detection

## Requirements

Paket Detection depends on the following to successfully run:

- One or more `paket.lock` files.
- The Paket detector looks for [`paket.lock`][1] files.

[1]: https://github.com/microsoft/component-detection/blob/main/src/Microsoft.ComponentDetection.Detectors/paket/PaketComponentDetector.cs

## Detection Strategy

Paket Detection is performed by parsing any `paket.lock` files found under the scan directory.

The `paket.lock` file is a lock file that records the concrete dependency resolution of all direct and transitive dependencies of your project. It is generated by [Paket][2], an alternative dependency manager for .NET that is popular in both large-scale C# projects and small-scale F# projects.

[2]: https://fsprojects.github.io/Paket/

## What is Paket?

Paket is a dependency manager for .NET and Mono projects that provides:
- Precise control over package dependencies
- Reproducible builds through lock files
- Support for multiple package sources (NuGet, GitHub, HTTP, Git)
- Better resolution algorithm compared to legacy NuGet

The `paket.lock` file structure is straightforward and human-readable:
```
NUGET
remote: https://api.nuget.org/v3/index.json
PackageName (1.0.0)
DependencyName (>= 2.0.0)
```

## Paket Detector

The Paket detector parses `paket.lock` files to extract:
- Package names and versions
- Direct dependencies (packages explicitly listed)
- Transitive dependencies (dependencies of dependencies)
- Dependency relationships between packages

Currently, the detector focuses on the `NUGET` section of the lock file, which contains NuGet package dependencies. Other dependency types (GITHUB, HTTP, GIT) are not currently supported.

## How It Works

The detector:
1. Locates `paket.lock` files in the scan directory
2. Parses the file line by line
3. Identifies packages (4-space indentation) and their versions
4. Identifies dependencies (6-space indentation) and their version constraints
5. Records all packages as NuGet components
6. Establishes parent-child relationships between packages and their dependencies

## Known Limitations

- Only NuGet dependencies from the `NUGET` section are detected
- GitHub, HTTP, and Git dependencies are not currently supported
- The detector assumes the lock file format follows the standard Paket conventions
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public NuGetComponentDetector(

public override IEnumerable<string> Categories => [Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet)];

public override IList<string> SearchPatterns { get; } = ["*.nupkg", "*.nuspec", NugetConfigFileName, "paket.lock"];
public override IList<string> SearchPatterns { get; } = ["*.nupkg", "*.nuspec", NugetConfigFileName];

public override IEnumerable<ComponentType> SupportedComponentTypes { get; } = [ComponentType.NuGet];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
namespace Microsoft.ComponentDetection.Detectors.Paket;

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.Extensions.Logging;

/// <summary>
/// Detects NuGet packages in paket.lock files.
/// Paket is a dependency manager for .NET that provides better control over package dependencies.
/// </summary>
public sealed class PaketComponentDetector : FileComponentDetector
{
private static readonly Regex PackageLineRegex = new(@"^\s{4}(\S+)\s+\(([^\)]+)\)", RegexOptions.Compiled);
private static readonly Regex DependencyLineRegex = new(@"^\s{6}(\S+)\s+\((.+)\)", RegexOptions.Compiled);

/// <summary>
/// Initializes a new instance of the <see cref="PaketComponentDetector"/> class.
/// </summary>
/// <param name="componentStreamEnumerableFactory">The factory for handing back component streams to File detectors.</param>
/// <param name="walkerFactory">The factory for creating directory walkers.</param>
/// <param name="logger">The logger to use.</param>
public PaketComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<PaketComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

/// <inheritdoc />
public override IList<string> SearchPatterns => ["paket.lock"];

/// <inheritdoc />
public override string Id => "Paket";

/// <inheritdoc />
public override IEnumerable<string> Categories =>
[Enum.GetName(typeof(DetectorClass), DetectorClass.NuGet)];

/// <inheritdoc />
public override IEnumerable<ComponentType> SupportedComponentTypes => [ComponentType.NuGet];

/// <inheritdoc />
public override int Version => 1;

/// <inheritdoc />
protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
try
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
using var reader = new StreamReader(processRequest.ComponentStream.Stream);

var currentSection = string.Empty;
string currentPackageName = null;
string currentPackageVersion = null;
DetectedComponent currentComponent = null;

while (await reader.ReadLineAsync(cancellationToken) is { } line)
{
if (string.IsNullOrWhiteSpace(line))
{
continue;
}

// Check if this is a section header (e.g., NUGET, GITHUB, HTTP)
if (!line.StartsWith(' ') && line.Trim().Length > 0)
{
currentSection = line.Trim();
currentPackageName = null;
currentPackageVersion = null;
currentComponent = null;
continue;
}

// Only process NUGET section for now
if (!currentSection.Equals("NUGET", StringComparison.OrdinalIgnoreCase))
{
continue;
}

// Check if this is a remote line (source URL)
if (line.TrimStart().StartsWith("remote:", StringComparison.OrdinalIgnoreCase))
{
continue;
}

// Check if this is a package line (4 spaces indentation)
var packageMatch = PackageLineRegex.Match(line);
if (packageMatch.Success)
{
currentPackageName = packageMatch.Groups[1].Value;
currentPackageVersion = packageMatch.Groups[2].Value;

currentComponent = new DetectedComponent(
new NuGetComponent(currentPackageName, currentPackageVersion));

singleFileComponentRecorder.RegisterUsage(
currentComponent,
isExplicitReferencedDependency: true);

continue;
}

// Check if this is a dependency line (6 spaces indentation)
var dependencyMatch = DependencyLineRegex.Match(line);
if (dependencyMatch.Success && currentComponent != null)
{
var dependencyName = dependencyMatch.Groups[1].Value;
var dependencyVersionSpec = dependencyMatch.Groups[2].Value;

// Extract the actual version from the version specification
// Version specs can be like ">= 3.3.0" or "1.2.10"
var versionMatch = Regex.Match(dependencyVersionSpec, @"[\d\.]+");
if (versionMatch.Success)
{
var dependencyComponent = new DetectedComponent(
new NuGetComponent(dependencyName, versionMatch.Value));

singleFileComponentRecorder.RegisterUsage(
dependencyComponent,
isExplicitReferencedDependency: false,
parentComponentId: currentComponent.Component.Id);
}
}
}
}
catch (Exception e) when (e is IOException or InvalidOperationException)
{
this.Logger.LogWarning(e, "Failed to read paket.lock file {File}", processRequest.ComponentStream.Location);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
using Microsoft.ComponentDetection.Detectors.Maven;
using Microsoft.ComponentDetection.Detectors.Npm;
using Microsoft.ComponentDetection.Detectors.NuGet;
using Microsoft.ComponentDetection.Detectors.Paket;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.ComponentDetection.Detectors.Pnpm;
using Microsoft.ComponentDetection.Detectors.Poetry;
Expand Down Expand Up @@ -116,6 +117,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IComponentDetector, NuGetPackagesConfigDetector>();
services.AddSingleton<IComponentDetector, NuGetProjectModelProjectCentricComponentDetector>();

// Paket
services.AddSingleton<IComponentDetector, PaketComponentDetector>();

// PIP
services.AddSingleton<IPyPiClient, PyPiClient>();
services.AddSingleton<ISimplePyPiClient, SimplePyPiClient>();
Expand Down
Loading