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

Feature: Add conan detector that parses conan.lock files of conan package manager version 1.x #692

Merged
merged 14 commits into from
Aug 9, 2023
Merged
Show file tree
Hide file tree
Changes from 13 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
11 changes: 11 additions & 0 deletions docs/detectors/conan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Conan Detection
## Requirements
Conan detection relies on a conan.lock file being present.

## Detection strategy
Conan detection is performed by parsing every **conan.lock** found under the scan directory.

## Known limitations
Conan detection will not work if lock files are not being used or not yet generated. So ensure to run the conan build to generate the lock file(s) before running the scan.

Full dependency graph generation is not supported. However, dependency relationships identified/present in the **conan.lock** file is captured.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public class TypedComponentConverter : JsonConverter
{ ComponentType.Git, typeof(GitComponent) },
{ ComponentType.RubyGems, typeof(RubyGemsComponent) },
{ ComponentType.Cargo, typeof(CargoComponent) },
{ ComponentType.Conan, typeof(ConanComponent) },
{ ComponentType.Pip, typeof(PipComponent) },
{ ComponentType.Go, typeof(GoComponent) },
{ ComponentType.DockerImage, typeof(DockerImageComponent) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,7 @@ public enum ComponentType : byte

[EnumMember]
DockerReference = 16,

[EnumMember]
Conan = 17,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;

using PackageUrl;

public class ConanComponent : TypedComponent
{
private ConanComponent()
{
// reserved for deserialization
}

public ConanComponent(string name, string version)
{
this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Conan));
this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Conan));
}

public string Name { get; set; }

public string Version { get; set; }

public override ComponentType Type => ComponentType.Conan;

public override string Id => $"{this.Name} {this.Version} - {this.Type}";

public override PackageURL PackageUrl => new PackageURL("conan", string.Empty, this.Name, this.Version, null, string.Empty);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
namespace Microsoft.ComponentDetection.Detectors.Conan;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Conan.Contracts;
using Microsoft.Extensions.Logging;

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

public override string Id => "ConanLock";

public override IList<string> SearchPatterns => new List<string> { "conan.lock" };

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

public override int Version { get; } = 1;

public override IEnumerable<string> Categories => new List<string> { "Conan" };

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var conanLockFile = processRequest.ComponentStream;

try
{
var conanLock = await JsonSerializer.DeserializeAsync<ConanLock>(conanLockFile.Stream);
this.RecordLockfileVersion(conanLock.Version);

if (!conanLock.HasNodes())
{
return;
}

var packagesDictionary = conanLock.GraphLock.Nodes;
var explicitReferencedDependencies = new HashSet<string>();
var developmentDependencies = new HashSet<string>();
if (packagesDictionary.ContainsKey("0"))
{
packagesDictionary.Remove("0", out var rootNode);
if (rootNode?.Requires != null)
{
explicitReferencedDependencies = new HashSet<string>(rootNode.Requires);
}

if (rootNode?.BuildRequires != null)
{
developmentDependencies = new HashSet<string>(rootNode.BuildRequires);
}
}

foreach (var (packageIndex, package) in packagesDictionary)
{
singleFileComponentRecorder.RegisterUsage(
new DetectedComponent(package.ToComponent()),
isExplicitReferencedDependency: explicitReferencedDependencies.Contains(packageIndex),
isDevelopmentDependency: developmentDependencies.Contains(packageIndex));
}

foreach (var (conanPackageIndex, package) in packagesDictionary)
{
var parentPackages = packagesDictionary.Values.Where(package => package.Requires?.Contains(conanPackageIndex) == true);
foreach (var parentPackage in parentPackages)
{
singleFileComponentRecorder.RegisterUsage(new DetectedComponent(package.ToComponent()), false, parentPackage.ToComponent().Id, isDevelopmentDependency: false);
}
melotic marked this conversation as resolved.
Show resolved Hide resolved
}
}
catch (Exception e)
{
// If something went wrong, just ignore the file
this.Logger.LogError(e, "Failed to process conan.lock file '{ConanLockLocation}'", conanLockFile.Location);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;

using System.Text.Json.Serialization;

public class ConanLock
{
[JsonPropertyName("version")]
public string Version { get; set; }

[JsonPropertyName("profile_host")]
public string ProfileHost { get; set; }

[JsonPropertyName("profile_build")]
public string ProfileBuild { get; set; }

[JsonPropertyName("graph_lock")]
public ConanLockGraph GraphLock { get; set; }

internal bool HasNodes() => this.GraphLock?.Nodes?.Count > 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;

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

public class ConanLockGraph
{
[JsonPropertyName("revisions_enabled")]
public bool RevisionsEnabled { get; set; }

[JsonPropertyName("nodes")]
public Dictionary<string, ConanLockNode> Nodes { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
namespace Microsoft.ComponentDetection.Detectors.Conan.Contracts;

using System;
using System.Linq;
using System.Text.Json.Serialization;
using Microsoft.ComponentDetection.Contracts.TypedComponent;

public class ConanLockNode
{
[JsonPropertyName("context")]
public string Context { get; set; }

[JsonPropertyName("modified")]
public bool? Modified { get; set; }

[JsonPropertyName("options")]
public string Options { get; set; }

[JsonPropertyName("package_id")]
public string PackageId { get; set; }

[JsonPropertyName("path")]
public string Path { get; set; }

[JsonPropertyName("prev")]
public string Previous { get; set; }

[JsonPropertyName("ref")]
public string Reference { get; set; }

[JsonPropertyName("requires")]
public string[] Requires { get; set; }

[JsonPropertyName("build_requires")]
public string[] BuildRequires { get; set; }

internal string Name() => this.Reference == null ? string.Empty : this.Reference.Split('/', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault("Unknown");

internal TypedComponent ToComponent() => new ConanComponent(this.Name(), this.Version());

internal string Version() => this.Reference == null ? string.Empty : this.Reference.Split('/', 2, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Skip(1).FirstOrDefault("None");
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
using Microsoft.ComponentDetection.Common.Telemetry;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.CocoaPods;
using Microsoft.ComponentDetection.Detectors.Conan;
using Microsoft.ComponentDetection.Detectors.Dockerfile;
using Microsoft.ComponentDetection.Detectors.Go;
using Microsoft.ComponentDetection.Detectors.Gradle;
Expand Down Expand Up @@ -77,6 +78,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
// CocoaPods
services.AddSingleton<IComponentDetector, PodComponentDetector>();

// Conan
services.AddSingleton<IComponentDetector, ConanLockComponentDetector>();

// Conda
services.AddSingleton<IComponentDetector, CondaLockComponentDetector>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,18 @@ public void TypedComponent_Serialization_Cargo()
cargoComponent.Version.Should().Be("1.2.3");
}

[TestMethod]
public void TypedComponent_Serialization_Conan()
{
TypedComponent tc = new ConanComponent("SomeConanPackage", "1.2.3");
var result = JsonConvert.SerializeObject(tc);
var deserializedTC = JsonConvert.DeserializeObject<TypedComponent>(result);
deserializedTC.Should().BeOfType(typeof(ConanComponent));
var conanComponent = (ConanComponent)deserializedTC;
conanComponent.Name.Should().Be("SomeConanPackage");
conanComponent.Version.Should().Be("1.2.3");
}

[TestMethod]
public void TypedComponent_Serialization_Pip()
{
Expand Down
Loading
Loading