diff --git a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs index 0ed8aa095..de7e7eb4a 100644 --- a/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs +++ b/src/Microsoft.ComponentDetection.Contracts/BcdeModels/TypedComponentConverter.cs @@ -27,6 +27,7 @@ public class TypedComponentConverter : JsonConverter { ComponentType.DockerReference, typeof(DockerReferenceComponent) }, { ComponentType.Vcpkg, typeof(VcpkgComponent) }, { ComponentType.Spdx, typeof(SpdxComponent) }, + { ComponentType.Pub, typeof(PubComponent) }, }; public override bool CanWrite diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs index 3da44b17e..1e4307fa3 100644 --- a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs +++ b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs @@ -44,4 +44,7 @@ public enum DetectorClass /// Indicates a detector applies to Docker references. DockerReference, + + /// Indicates a detector applies to Pub references. + Pub, } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs index 371ad8367..62c3f80d5 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs @@ -56,4 +56,7 @@ public enum ComponentType : byte [EnumMember] Conan = 17, + + [EnumMember] + Pub = 18, } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PubComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PubComponent.cs new file mode 100644 index 000000000..c8ce9ac56 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/PubComponent.cs @@ -0,0 +1,72 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent; + +public class PubComponent : TypedComponent +{ + public PubComponent() + { + /* Reserved for deserialization */ + } + + public PubComponent(string name, string version, string dependency, string hash = null, string url = null) + { + this.Name = this.ValidateRequiredInput(name, nameof(this.Name), nameof(ComponentType.Pub)); + this.Version = this.ValidateRequiredInput(version, nameof(this.Version), nameof(ComponentType.Pub)); + this.Dependency = dependency; + this.Hash = hash; // Not required; + this.Url = url; + } + + public string Name { get; } + + public string Version { get; set; } + + public string Hash { get; set; } + + public string Url { get; set; } + + public string Dependency { get; set; } + + public override ComponentType Type => ComponentType.Pub; + + public override string Id => $"{this.Name} {this.Version} - {this.Type}"; + + public override string ToString() + { + return $"Name={this.Name}\tVersion={this.Version}\tUrl={this.Url}"; + } + + protected bool Equals(PubComponent other) => this.Name == other.Name && this.Version == other.Version && this.Hash == other.Hash && this.Url == other.Url && this.Dependency == other.Dependency; + + public override bool Equals(object obj) + { + if (obj is null) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj.GetType() != this.GetType()) + { + return false; + } + + return this.Equals((PubComponent)obj); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = this.Name != null ? this.Name.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (this.Version != null ? this.Version.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (this.Hash != null ? this.Hash.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (this.Url != null ? this.Url.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (this.Dependency != null ? this.Dependency.GetHashCode() : 0); + return hashCode; + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pub/PubComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/pub/PubComponentDetector.cs new file mode 100644 index 000000000..873582bc1 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pub/PubComponentDetector.cs @@ -0,0 +1,67 @@ +namespace Microsoft.ComponentDetection.Detectors.Pub; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.Internal; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.Extensions.Logging; +using YamlDotNet.Serialization; + +public class PubComponentDetector : FileComponentDetector +{ + public PubComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + + public override string Id => "pub"; + + public override IEnumerable Categories => new[] { Enum.GetName(typeof(DetectorClass), DetectorClass.Pub) }; + + public override IEnumerable SupportedComponentTypes { get; } = new[] { ComponentType.Pub }; + + public override int Version => 1; + + public override IList SearchPatterns => new List { "pubspec.lock" }; + + protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary detectorArgs) + { + using var reader = new StreamReader(processRequest.ComponentStream.Stream); + var text = await reader.ReadToEndAsync(); + + var deserializer = new DeserializerBuilder().IgnoreUnmatchedProperties().Build(); + try + { + var parsedFile = deserializer.Deserialize(text); + this.Logger.LogDebug("SDK {Dart}", parsedFile.Sdks.Dart); + + foreach (var package in parsedFile.Packages) + { + if (package.Value.Source == "hosted") + { + var component = new PubComponent( + package.Value.GetName(), + package.Value.Version, + package.Value.Dependency, + package.Value.GetSha256(), + package.Value.GePackageDownloadedSource()); + this.Logger.LogInformation("Registering component {Package}", component); + + processRequest.SingleFileComponentRecorder.RegisterUsage(new DetectedComponent(component)); + } + } + } + catch (Exception ex) + { + this.Logger.LogError(ex, "Error while parsing lock file"); + } + } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLock.cs b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLock.cs new file mode 100644 index 000000000..75808a4af --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLock.cs @@ -0,0 +1,18 @@ +namespace Microsoft.ComponentDetection.Detectors.Pub; + +using System.Collections.Generic; +using System.Runtime.Serialization; +using YamlDotNet.Serialization; + +/// +/// Model of the pub-spec lock file. +/// +[DataContract] +public class PubSpecLock +{ + [YamlMember(Alias = "packages")] + public Dictionary Packages { get; set; } + + [YamlMember(Alias = "sdks")] + public SDK Sdks { get; set; } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackage.cs b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackage.cs new file mode 100644 index 000000000..e1e345487 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackage.cs @@ -0,0 +1,44 @@ +namespace Microsoft.ComponentDetection.Detectors.Pub; + +using System.Runtime.Serialization; +using YamlDotNet.Serialization; + +/// +/// Model of the pub-spec lock file. +/// +[DataContract] +public class PubSpecLockPackage +{ + [YamlMember(Alias = "source")] + public string Source { get; set; } + + [YamlMember(Alias = "version")] + public string Version { get; set; } + + [YamlMember(Alias = "dependency")] + public string Dependency { get; set; } + + [YamlMember(Alias = "description")] + public dynamic Description { get; set; } + + /// + /// /// Returns the description\sha256 path + /// The value can be null. + /// + /// Returns the package SHA-256 as in the pubspec.lock file. + public string GetSha256() => this.Description["sha256"]; + + /// + /// Returns the description\url path + /// The value can be null. + /// + /// Returns the package url as in the pubspec.lock file. + public string GePackageDownloadedSource() => this.Description["url"]; + + /// + /// Returns the description\name path + /// The value can be null. + /// + /// Returns the package name as in the pubspec.lock file. + public string GetName() => this.Description["name"]; +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackageDescription.cs b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackageDescription.cs new file mode 100644 index 000000000..009443748 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pub/PubSpecLockPackageDescription.cs @@ -0,0 +1,20 @@ +namespace Microsoft.ComponentDetection.Detectors.Pub; + +using System.Runtime.Serialization; +using YamlDotNet.Serialization; + +/// +/// Model of the pub-spec lock file. +/// +[DataContract] +public class PubSpecLockPackageDescription +{ + [YamlMember(Alias = "name")] + public string Name { get; set; } + + [YamlMember(Alias = "sha256")] + public string Sha256 { get; set; } + + [YamlMember(Alias = "url")] + public string Url { get; set; } +} diff --git a/src/Microsoft.ComponentDetection.Detectors/pub/SDK.cs b/src/Microsoft.ComponentDetection.Detectors/pub/SDK.cs new file mode 100644 index 000000000..76c518e89 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/pub/SDK.cs @@ -0,0 +1,14 @@ +namespace Microsoft.ComponentDetection.Detectors.Pub; + +using System.Runtime.Serialization; +using YamlDotNet.Serialization; + +[DataContract] +public class SDK +{ + [YamlMember(Alias = "dart")] + public string Dart { get; set; } + + [YamlMember(Alias = "flutter")] + public string Flutter { get; set; } +} diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index efbdb0a3c..145c4bb0d 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -16,6 +16,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions; using Microsoft.ComponentDetection.Detectors.Pip; using Microsoft.ComponentDetection.Detectors.Pnpm; using Microsoft.ComponentDetection.Detectors.Poetry; +using Microsoft.ComponentDetection.Detectors.Pub; using Microsoft.ComponentDetection.Detectors.Ruby; using Microsoft.ComponentDetection.Detectors.Rust; using Microsoft.ComponentDetection.Detectors.Spdx; @@ -139,6 +140,8 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); + // Pub + services.AddSingleton(); return services; } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/PubComponentDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/PubComponentDetectorTests.cs new file mode 100644 index 000000000..d93f15df7 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/PubComponentDetectorTests.cs @@ -0,0 +1,87 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests; + +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.Pub; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +[TestCategory("Governance/All")] +[TestCategory("Governance/ComponentDetection")] +public class PubComponentDetectorTests : BaseDetectorTest +{ + private readonly string testPubSpecLockFile = @"# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + analyzer: + dependency: ""direct dev"" + description: + name: analyzer + sha256: ""sh1"" + url: ""https://pub.dev"" + source: hosted + version: ""5.13.0"" + archive: + dependency: transitive + description: + name: archive + sha256: ""sh2"" + url: ""https://pub.dev"" + source: hosted + version: ""3.4.4"" + async: + dependency: direct main + description: + name: async + sha256: ""sh3"" + url: ""https://pub.dev"" + source: hosted + version: ""2.11.0"" + flutter: + dependency: ""direct main"" + description: flutter + source: sdk + version: ""0.0.0"" +sdks: + dart: "">=3.0.0 <4.0.0"" + flutter: "">=3.10.0"" +"; + + [TestMethod] + public async Task TestDetectorAsync() + { + var (result, componentRecorder) = await this.DetectorTestUtility + .WithFile("pubspec.lock", this.testPubSpecLockFile) + .ExecuteDetectorAsync(); + + result.ResultCode.Should().Be(ProcessingResultCode.Success); + componentRecorder.GetDetectedComponents().Count().Should().Be(3); + + var components = componentRecorder.GetDetectedComponents().Select(x => x.Component).ToArray(); + + components.Should().Contain(new PubComponent( + "analyzer", + "5.13.0", + "direct dev", + "sh1", + "https://pub.dev")); + + components.Should().Contain(new PubComponent( + "archive", + "3.4.4", + "transitive", + "sh2", + "https://pub.dev")); + + components.Should().Contain(new PubComponent( + "async", + "2.11.0", + "direct main", + "sh3", + "https://pub.dev")); + } +}