Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Nuget lockfiles #497

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Microsoft.ComponentDetection.Detectors.NuGet;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
Expand All @@ -20,6 +21,7 @@ public class NuGetComponentDetector : FileComponentDetector
private static readonly IEnumerable<string> LowConfidencePackages = new[] { "Newtonsoft.Json" };

public const string NugetConfigFileName = "nuget.config";
public const string NugetLockfileName = "packages.lock.json";

private readonly IList<string> repositoryPathKeyNames = new List<string> { "repositorypath", "globalpackagesfolder" };

Expand All @@ -37,7 +39,15 @@ public NuGetComponentDetector(

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

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

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

Expand Down Expand Up @@ -105,6 +115,12 @@ private async Task ProcessFileAsync(ProcessRequest processRequest)
else if ("paket.lock".Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase))
{
this.ParsePaketLock(processRequest);
return;
Copy link
Member Author

@Porges Porges Apr 10, 2023

Choose a reason for hiding this comment

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

Fixed what seems like a small bug here; it would always continue to the "nuget bytes" parsing and throw a null reference exception which is then caught.

}
else if (NugetLockfileName.Equals(stream.Pattern, StringComparison.OrdinalIgnoreCase))
{
await this.ParseNugetLockfileAsync(processRequest);
return;
}
else
{
Expand Down Expand Up @@ -174,6 +190,41 @@ private void ParsePaketLock(ProcessRequest processRequest)
}
}

private async Task ParseNugetLockfileAsync(ProcessRequest processRequest)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var stream = processRequest.ComponentStream;

NuGetLockfileShape lockfile;
try
{
lockfile = await JsonSerializer.DeserializeAsync<NuGetLockfileShape>(stream.Stream).ConfigureAwait(false);
}
catch (Exception e)
{
this.Logger.LogError(e, "Error loading NuGet lockfile from {Location}", stream.Location);
singleFileComponentRecorder.RegisterPackageParseFailure(stream.Location);
return;
}

if (lockfile.Version != 1)
{
// only version 1 is supported
this.Logger.LogError("Unsupported NuGet lockfile version {Version}", lockfile.Version);
singleFileComponentRecorder.RegisterPackageParseFailure(stream.Location);
return;
}

foreach (var framework in lockfile.Dependencies.Values)
{
foreach (var (name, value) in framework)
{
var component = new NuGetComponent(name, value.Resolved);
singleFileComponentRecorder.RegisterUsage(new DetectedComponent(component));
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if the team would want it included in this PR or not, but packages.lock.json supports reconstructing the whole component graph (see parentComponentId docs link) by using the dependencies property and also isExplicitlyReferencedDependency (same doc link) through the type property (is it Direct or Transitive).

}
}
}

private IList<DirectoryInfo> GetRepositoryPathsFromNugetConfig(IComponentStream componentStream)
{
var potentialPaths = new List<string>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Microsoft.ComponentDetection.Detectors.NuGet;

using System.Collections.Generic;
using System.Text.Json.Serialization;

internal record NuGetLockfileShape
{
[JsonPropertyName("version")]
public int Version { get; set; }

[JsonPropertyName("dependencies")]
public Dictionary<string, Dictionary<string, PackageShape>> Dependencies { get; set; } = new();

public record PackageShape
{
[JsonPropertyName("type")]
public string Type { get; set; }

[JsonPropertyName("resolved")]
public string Resolved { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.NuGet;
using Microsoft.ComponentDetection.Detectors.Tests.Utilities;
using Microsoft.ComponentDetection.TestsUtilities;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand All @@ -23,14 +24,17 @@
public class NuGetComponentDetectorTests : BaseDetectorTest<NuGetComponentDetector>
{
private static readonly IEnumerable<string> DetectorSearchPattern =
new List<string> { "*.nupkg", "*.nuspec", "nuget.config", "paket.lock" };
new List<string> { "*.nupkg", "*.nuspec", "nuget.config", "packages.lock.json", "paket.lock" };

private readonly Mock<ILogger<NuGetComponentDetector>> mockLogger;
private ILogger<NuGetComponentDetector> logger;

public NuGetComponentDetectorTests()
public TestContext TestContext { get; set; }

[TestInitialize]
public void Setup()
{
this.mockLogger = new Mock<ILogger<NuGetComponentDetector>>();
this.DetectorTestUtility.AddServiceMock(this.mockLogger);
this.logger = new TestLogger<NuGetComponentDetector>(this.TestContext);
this.DetectorTestUtility.AddService(this.logger);
}

[TestMethod]
Expand Down Expand Up @@ -114,6 +118,57 @@ public async Task TestNugetDetector_ReturnsValidMixedComponentAsync()
Assert.AreEqual(2, componentRecorder.GetDetectedComponents().Count());
}

[TestMethod]
public async Task TestNugetDetector_ReturnsPackagesLockfileAsync()
{
var lockfile = @"{
""version"": 1,
""dependencies"": {
""net7.0"": {
""Azure.Core"": {
""type"": ""Direct"",
""requested"": ""[1.25.0, )"",
""resolved"": ""1.25.0"",
""contentHash"": ""X8Dd4sAggS84KScWIjEbFAdt2U1KDolQopTPoHVubG2y3CM54f9l6asVrP5Uy384NWXjsspPYaJgz5xHc+KvTA=="",
""dependencies"": {
""Microsoft.Bcl.AsyncInterfaces"": ""1.1.1"",
""System.Diagnostics.DiagnosticSource"": ""4.6.0"",
""System.Memory.Data"": ""1.0.2"",
""System.Numerics.Vectors"": ""4.5.0"",
""System.Text.Encodings.Web"": ""4.7.2"",
""System.Text.Json"": ""4.7.2"",
""System.Threading.Tasks.Extensions"": ""4.5.4""
}
}
},
""net6.0"": {
""Azure.Data.Tables"": {
""type"": ""Direct"",
""requested"": ""[12.5.0, )"",
""resolved"": ""12.5.0"",
""contentHash"": ""XeIxPf+rF1NXkX3NJSB0ZTNgU233vyPXGmaFsR0lUVibtWP/lj+Qu1FcPxoslURcX0KC+UgTb226nqVdHjoweQ=="",
""dependencies"": {
""Azure.Core"": ""1.22.0"",
""System.Text.Json"": ""4.7.2""
}
}
}
}
}";

var (scanResult, componentRecorder) = await this.DetectorTestUtility
.WithFile("packages.lock.json", lockfile)
.ExecuteDetectorAsync();

Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);

// should be 2 components found; one per framework
var components = new HashSet<string>(componentRecorder.GetDetectedComponents().Select(x => x.Component.Id));
Assert.AreEqual(2, components.Count);
Assert.IsTrue(components.Contains("Azure.Core 1.25.0 - NuGet"));
Assert.IsTrue(components.Contains("Azure.Data.Tables 12.5.0 - NuGet"));
}

[TestMethod]
public async Task TestNugetDetector_ReturnsValidPaketComponentAsync()
{
Expand Down Expand Up @@ -170,16 +225,9 @@ public async Task TestNugetDetector_HandlesMalformedComponentsInComponentListAsy
.WithFile("test.nuspec", nuspec)
.WithFile("test.nupkg", validNupkg)
.WithFile("malformed.nupkg", malformedNupkg)
.AddServiceMock(this.mockLogger)
.AddService(this.logger)
.ExecuteDetectorAsync();

this.mockLogger.Verify(x => x.Log(
It.IsAny<LogLevel>(),
It.IsAny<EventId>(),
It.IsAny<It.IsAnyType>(),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()));

Assert.AreEqual(ProcessingResultCode.Success, scanResult.ResultCode);
Assert.AreEqual(2, componentRecorder.GetDetectedComponents().Count());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
namespace Microsoft.ComponentDetection.Detectors.Tests.Utilities;

using System;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;

internal class TestLogger<T> : ILogger<T>, IDisposable
melotic marked this conversation as resolved.
Show resolved Hide resolved
{
private readonly TestContext context;

public TestLogger(TestContext context)
=> this.context = context;

public IDisposable BeginScope<TState>(TState state)
where TState : notnull
=> this;

public void Dispose()
{
}

public bool IsEnabled(LogLevel logLevel) => true;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
=> this.context.WriteLine($"{logLevel} ({eventId}): {formatter(state, exception)}");
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ public DetectorTestUtilityBuilder()
public DetectorTestUtilityBuilder<T> WithFile(string fileName, string fileContents, IEnumerable<string> searchPatterns = null, string fileLocation = null) =>
this.WithFile(fileName, fileContents.ToStream(), searchPatterns, fileLocation);

public DetectorTestUtilityBuilder<T> AddService<TService>(TService it)
where TService : class
{
this.serviceCollection.AddSingleton(it);
return this;
}

public DetectorTestUtilityBuilder<T> AddServiceMock<TMock>(Mock<TMock> mock)
where TMock : class
{
Expand Down