Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
5d7ccf0
add component type
cataggar Jun 5, 2025
5be9389
metadata
cataggar Jun 5, 2025
413d3e3
style
cataggar Jun 6, 2025
298b1d2
start md
cataggar Jun 6, 2025
b7f550a
Merge remote-tracking branch 'origin/main' into uv
cataggar Jun 6, 2025
392e970
fix style
cataggar Jun 6, 2025
8cc618d
fix import style
cataggar Jun 6, 2025
772baf1
add uv test project
cataggar Jun 6, 2025
316324d
add UvLockComponentDetector deps
cataggar Jun 6, 2025
9cf0e81
just use PipComponent
cataggar Jun 6, 2025
b2e686a
add unit tests
cataggar Jun 6, 2025
cd3b461
add explicit component references
cataggar Jun 6, 2025
d9984eb
simplify name warning
cataggar Jun 6, 2025
fff4dbd
one test works
cataggar Jun 6, 2025
5fdb6ba
add UvLock
cataggar Jun 6, 2025
0b840e0
update detector to use UvLock parsing
cataggar Jun 6, 2025
329d936
add metadata
cataggar Jun 6, 2025
50d397f
metadata is for each package
cataggar Jun 6, 2025
80e8c80
punt on explicit
cataggar Jun 6, 2025
b5a9cb0
not yet
cataggar Jun 6, 2025
707cd11
more detector coverage
cataggar Jun 7, 2025
e2398ff
add UvLock coverage
cataggar Jun 7, 2025
ed06cff
undo change
cataggar Jun 9, 2025
60717dc
ComponentType Uv not needed
cataggar Jun 9, 2025
3b4db0a
improve test coverage of UvLock
cataggar Jun 9, 2025
74b9ecd
enable nullable
cataggar Jun 9, 2025
e1a3248
100% line coverage
cataggar Jun 9, 2025
46af1a0
100% branch coverage
cataggar Jun 9, 2025
611cea6
100%
cataggar Jun 9, 2025
98d6def
add IDefaultOffComponentDetector
cataggar Jun 9, 2025
3c63df9
Merge remote-tracking branch 'origin/main' into uv
cataggar Jun 12, 2025
835f3b4
add links
cataggar Jun 12, 2025
424e4df
add UvLockDetectorExperiment
cataggar Jun 12, 2025
6a17664
fix DetectorRestrictionService
cataggar Jun 12, 2025
dc6aa11
add experiment tests
cataggar Jun 12, 2025
2d18463
not the behavior we want
cataggar Jun 12, 2025
5e7fb35
parse package source
cataggar Jun 12, 2025
c7f1dc7
add explicitreference
cataggar Jun 12, 2025
7215227
add test for explicit dep
cataggar Jun 12, 2025
288c567
fix dependency and devDependency
cataggar Jun 12, 2025
e540df8
fix dev and non dev lists
cataggar Jun 12, 2025
d03ee6c
null checks
cataggar Jun 17, 2025
f5097b7
add #nullable enable
cataggar Jun 17, 2025
066e1d8
fix tests
cataggar Jun 17, 2025
58e453f
undo DetectorRestrictionService
cataggar Jun 17, 2025
dc218a5
undo DetectorRestritionServiceTests changes
cataggar Jun 17, 2025
26a21c9
PipReportComponentDetector
cataggar Jun 19, 2025
c2d9c07
update UvLockDetectorExperimentTests
cataggar Jun 20, 2025
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
6 changes: 6 additions & 0 deletions docs/detectors/uv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# uv Detection
## Requirements
[uv](https://docs.astral.sh/uv/) detection relies on a [uv.lock](https://docs.astral.sh/uv/concepts/projects/layout/#the-lockfile) file being present.

## Detection strategy
Comment thread
cataggar marked this conversation as resolved.
uv detection is performed by parsing a <em>uv.lock</em> found under the scan directory.
10 changes: 10 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/uv/UvDependency.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#nullable enable
namespace Microsoft.ComponentDetection.Detectors.Uv
{
public class UvDependency
{
public required string Name { get; init; }

public string? Specifier { get; set; }
}
}
144 changes: 144 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/uv/UvLock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#nullable enable
namespace Microsoft.ComponentDetection.Detectors.Uv
{
using System;
using System.Collections.Generic;
using System.IO;
using Tomlyn;
using Tomlyn.Model;

public class UvLock
{
// a list of packages with their dependencies
public List<UvPackage> Packages { get; set; } = [];

// static method to parse the TOML stream into a UvLock model
public static UvLock Parse(Stream tomlStream)
{
using var reader = new StreamReader(tomlStream);
var tomlContent = reader.ReadToEnd();
var model = Toml.ToModel(tomlContent);
return new UvLock
{
Packages = ParsePackagesFromModel(model),
};
}

internal static List<UvPackage> ParsePackagesFromModel(object? model)
{
if (model is not TomlTable table)
{
throw new InvalidOperationException("TOML root is not a table");
}

if (!table.TryGetValue("package", out var packagesObj) || packagesObj is not TomlTableArray packages)
{
return [];
}

var result = new List<UvPackage>();
foreach (var pkg in packages)
{
var parsed = ParsePackage(pkg);
if (parsed is not null)
{
result.Add(parsed);
}
}

return result;
}

internal static UvPackage? ParsePackage(object? pkg)
{
if (pkg is not TomlTable pkgTable)
{
return null;
}

if (pkgTable.TryGetValue("name", out var nameObj) && nameObj is string name &&
pkgTable.TryGetValue("version", out var versionObj) && versionObj is string version)
{
var uvPackage = new UvPackage
{
Name = name,
Version = version,
Dependencies = [],
MetadataRequiresDist = [],
MetadataRequiresDev = [],
};

if (pkgTable.TryGetValue("dependencies", out var depsObj) && depsObj is TomlArray depsArray)
{
uvPackage.Dependencies = ParseDependenciesArray(depsArray);
}

if (pkgTable.TryGetValue("metadata", out var metadataObj) && metadataObj is TomlTable metadataTable)
{
ParseMetadata(metadataTable, uvPackage);
}

// Parse source
if (pkgTable.TryGetValue("source", out var sourceObj) && sourceObj is TomlTable sourceTable)
{
var source = new UvSource
{
Registry = sourceTable.TryGetValue("registry", out var regObj) && regObj is string reg ? reg : null,
Virtual = sourceTable.TryGetValue("virtual", out var virtObj) && virtObj is string virt ? virt : null,
};
uvPackage.Source = source;
}

return uvPackage;
}

return null;
}

internal static List<UvDependency> ParseDependenciesArray(TomlArray? depsArray)
{
var deps = new List<UvDependency>();
if (depsArray is null)
{
return deps;
}

foreach (var dep in depsArray)
{
if (dep is TomlTable depTable &&
depTable.TryGetValue("name", out var depNameObj) && depNameObj is string depName)
{
var depSpec = depTable.TryGetValue("specifier", out var specObj) && specObj is string s ? s : null;
deps.Add(new UvDependency
{
Name = depName,
Specifier = depSpec,
});
}
}

return deps;
}

internal static void ParseMetadata(TomlTable? metadataTable, UvPackage uvPackage)
{
if (metadataTable is null)
{
return;
}

if (metadataTable.TryGetValue("requires-dist", out var requiresDistObj) && requiresDistObj is TomlArray requiresDistArr)
{
uvPackage.MetadataRequiresDist = ParseDependenciesArray(requiresDistArr);
}

if (metadataTable.TryGetValue("requires-dev", out var requiresDevObj) && requiresDevObj is TomlTable requiresDevTable)
{
if (requiresDevTable.TryGetValue("dev", out var devObj) && devObj is TomlArray devArr)
{
uvPackage.MetadataRequiresDev = ParseDependenciesArray(devArr);
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#nullable enable

namespace Microsoft.ComponentDetection.Detectors.Uv
{
using System;
using System.Collections.Generic;
using System.Linq;
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;

public class UvLockComponentDetector : FileComponentDetector, IDefaultOffComponentDetector
{
public UvLockComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<UvLockComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id => "UvLock";

public override IList<string> SearchPatterns { get; } = ["uv.lock"];

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

public override int Version => 1;

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

internal static bool IsRootPackage(UvPackage pck)
{
return pck.Source?.Virtual != null;
}

protected override Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var file = processRequest.ComponentStream;

try
{
// Parse the file stream into a UvLock model
file.Stream.Position = 0; // Ensure stream is at the beginning
var uvLock = UvLock.Parse(file.Stream);

var rootPackage = uvLock.Packages.FirstOrDefault(IsRootPackage);
var explicitPackages = new HashSet<string>();
var devPackages = new HashSet<string>();

if (rootPackage != null)
{
foreach (var dep in rootPackage.MetadataRequiresDist)
{
explicitPackages.Add(dep.Name);
}

foreach (var devDep in rootPackage.MetadataRequiresDev)
{
devPackages.Add(devDep.Name);
}
}

foreach (var pkg in uvLock.Packages)
{
if (IsRootPackage(pkg))
{
continue;
}

var pipComponent = new PipComponent(pkg.Name, pkg.Version);
var isExplicit = explicitPackages.Contains(pkg.Name);
var isDev = devPackages.Contains(pkg.Name);
var detectedComponent = new DetectedComponent(pipComponent);
singleFileComponentRecorder.RegisterUsage(detectedComponent, isDevelopmentDependency: isDev, isExplicitReferencedDependency: isExplicit);

foreach (var dep in pkg.Dependencies)
{
var depPkg = uvLock.Packages.FirstOrDefault(p => p.Name.Equals(dep.Name, StringComparison.OrdinalIgnoreCase));
if (depPkg != null)
{
var depComponentWithVersion = new PipComponent(depPkg.Name, depPkg.Version);
singleFileComponentRecorder.RegisterUsage(new DetectedComponent(depComponentWithVersion), parentComponentId: pipComponent.Id);
}
else
{
this.Logger.LogWarning("Dependency {DependencyName} not found in uv.lock packages", dep.Name);
}
}
}
}
catch (Exception ex)
{
this.Logger.LogError(ex, "Failed to parse uv.lock file {File}", file.Location);
}

return Task.CompletedTask;
}
}
}
23 changes: 23 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/uv/UvPackage.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#nullable enable
namespace Microsoft.ComponentDetection.Detectors.Uv
{
using System.Collections.Generic;

public class UvPackage
{
public required string Name { get; init; }

public required string Version { get; init; }

public List<UvDependency> Dependencies { get; set; } = [];

// Metadata dependencies (requires-dist)
public List<UvDependency> MetadataRequiresDist { get; set; } = [];

// Metadata dev dependencies (requires-dev)
public List<UvDependency> MetadataRequiresDev { get; set; } = [];

// Source property for uv.lock
public UvSource? Source { get; set; }
}
}
11 changes: 11 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/uv/UvSource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#nullable enable

namespace Microsoft.ComponentDetection.Detectors.Uv
{
public class UvSource
{
public string? Registry { get; set; }

public string? Virtual { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;

using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.Pip;
using Microsoft.ComponentDetection.Detectors.Uv;

/// <summary>
/// Experiment to validate UvLockComponentDetector against PipComponentDetector.
/// </summary>
public class UvLockDetectorExperiment : IExperimentConfiguration

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this should have been DefaultOffComponentDetector to begin with https://github.com/microsoft/component-detection/blob/main/docs/creating-a-new-detector.md#detector-lifecycle.

{
/// <inheritdoc />
public string Name => "UvLockDetectorExperiment";

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

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

/// <inheritdoc />
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
using Microsoft.ComponentDetection.Detectors.Rust;
using Microsoft.ComponentDetection.Detectors.Spdx;
using Microsoft.ComponentDetection.Detectors.Swift;
using Microsoft.ComponentDetection.Detectors.Uv;
using Microsoft.ComponentDetection.Detectors.Vcpkg;
using Microsoft.ComponentDetection.Detectors.Yarn;
using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
Expand Down Expand Up @@ -67,6 +68,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IExperimentConfiguration, RustCliDetectorExperiment>();
services.AddSingleton<IExperimentConfiguration, RustSbomVsCliExperiment>();
services.AddSingleton<IExperimentConfiguration, RustSbomVsCrateExperiment>();
services.AddSingleton<IExperimentConfiguration, UvLockDetectorExperiment>();

// Detectors
// CocoaPods
Expand Down Expand Up @@ -152,6 +154,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
// Swift Package Manager
services.AddSingleton<IComponentDetector, SwiftResolvedComponentDetector>();

// uv
services.AddSingleton<IComponentDetector, UvLockComponentDetector>();

return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
<PackageReference Include="packageurl-dotnet" />
<PackageReference Include="Tomlyn.Signed" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="MSTest.TestAdapter" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
Expand Down
Loading
Loading