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"));
+ }
+}