diff --git a/docs/detectors/swift.md b/docs/detectors/swift.md
new file mode 100644
index 000000000..3b44ef630
--- /dev/null
+++ b/docs/detectors/swift.md
@@ -0,0 +1,23 @@
+# Swift Package Manager Detection
+
+## Requirements
+
+Swift Package Manager detection requires the following file to be present in the scan directory:
+
+- `Package.resolved` file
+
+## Detection strategy
+
+The detector `SwiftResolvedComponentDetector` only parses the `Package.resolved` file to get the dependencies.
+This file contains a json representation of the resolved dependencies of the project with the transitive dependencies.
+The version, the url and commit hash of the dependencies are stored in this file.
+
+[This is the only reference in the Apple documentation to the `Package.resolved` file.][1]
+
+## Known limitations
+
+Right now the detector does not support parsing `Package.swift` file to get the dependencies.
+It only supports parsing `Package.resolved` file.
+Some projects only commit the `Package.swift`, which is why it is planned to support parsing `Package.swift` in the future.
+
+[1]: https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html#package-dependency
diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs
index cde85fd50..becfa4f9f 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 Swift packages.
+ Swift,
}
diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs
index 3d3593c77..6e2becf9d 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]
+ Swift = 18,
}
diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs
new file mode 100644
index 000000000..b00a9c939
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs
@@ -0,0 +1,50 @@
+namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+using System;
+using System.Collections.Generic;
+using PackageUrl;
+
+///
+/// Represents a Swift package manager component.
+///
+public class SwiftComponent : TypedComponent
+{
+ private readonly string packageUrl;
+
+ private readonly string hash;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the component.
+ /// The version of the component.
+ /// The package URL of the component.
+ /// The hash of the component.
+ public SwiftComponent(string name, string version, string packageUrl, string hash)
+ {
+ this.Name = this.ValidateRequiredInput(name, nameof(name), nameof(ComponentType.Swift));
+ this.Version = this.ValidateRequiredInput(version, nameof(version), nameof(ComponentType.Swift));
+ this.packageUrl = this.ValidateRequiredInput(packageUrl, nameof(packageUrl), nameof(ComponentType.Swift));
+ this.hash = this.ValidateRequiredInput(hash, nameof(hash), nameof(ComponentType.Swift));
+ }
+
+ public string Name { get; }
+
+ public string Version { get; }
+
+ public override ComponentType Type => ComponentType.Swift;
+
+ public override string Id => $"{this.Name} {this.Version} - {this.Type}";
+
+ // The type is swiftpm
+ public PackageURL PackageURL => new PackageURL(
+ type: "swift",
+ @namespace: new Uri(this.packageUrl).Host,
+ name: this.Name,
+ version: this.hash, // Hash has priority over version when creating a PackageURL
+ qualifiers: new SortedDictionary
+ {
+ { "repository_url", this.packageUrl },
+ },
+ subpath: null);
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs
new file mode 100644
index 000000000..5d7bf529c
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs
@@ -0,0 +1,51 @@
+namespace Microsoft.ComponentDetection.Contracts.TypedComponent;
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+///
+/// Represents a Swift Package Manager component.
+///
+public class SwiftResolvedFile
+{
+ [JsonProperty("pins")]
+ public IList Pins { get; set; }
+
+ [JsonProperty("version")]
+ public int Version { get; set; }
+
+ public class SwiftDependency
+ {
+ // The name of the package
+ [JsonProperty("identity")]
+ public string Identity { get; set; }
+
+ // How the package is imported. Example: "remoteSourceControl"
+ // This is not an enum because the Swift contract does not specify the possible values.
+ [JsonProperty("kind")]
+ public string Kind { get; set; }
+
+ // The unique path to the repository where the package is located. Example: Git repo URL.
+ [JsonProperty("location")]
+ public string Location { get; set; }
+
+ // Data about the package version and commit hash.
+ [JsonProperty("state")]
+ public SwiftState State { get; set; }
+
+ public class SwiftState
+ {
+ // The commit hash of the package.
+ [JsonProperty("revision")]
+ public string Revision { get; set; }
+
+ // The version of the package. Might be missing.
+ [JsonProperty("version")]
+ public string Version { get; set; }
+
+ // The branch of the package. Might be missing.
+ [JsonProperty("branch")]
+ public string Branch { get; set; }
+ }
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs
new file mode 100644
index 000000000..6d6edc0c4
--- /dev/null
+++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs
@@ -0,0 +1,113 @@
+namespace Microsoft.ComponentDetection.Detectors.Swift;
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+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;
+using Newtonsoft.Json;
+
+///
+/// Detects Swift Package Manager components.
+///
+public class SwiftResolvedComponentDetector : FileComponentDetector, IDefaultOffComponentDetector
+{
+ // We are only interested in packages coming from remote sources such as git
+ // The Package Kind is not an enum because the Swift Package Manager contract does not specify the possible values.
+ private const string TargetSwiftPackageKind = "remoteSourceControl";
+
+ public SwiftResolvedComponentDetector(
+ IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
+ IObservableDirectoryWalkerFactory walkerFactory,
+ ILogger logger)
+ {
+ this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
+ this.Scanner = walkerFactory;
+ this.Logger = logger;
+ }
+
+ public override string Id { get; } = "Swift";
+
+ public override IEnumerable Categories => [Enum.GetName(DetectorClass.Swift)];
+
+ public override IList SearchPatterns { get; } = ["Package.resolved"];
+
+ public override IEnumerable SupportedComponentTypes => [ComponentType.Swift];
+
+ public override int Version => 1;
+
+ protected override Task OnFileFoundAsync(
+ ProcessRequest processRequest,
+ IDictionary detectorArgs,
+ CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ this.ProcessPackageResolvedFile(processRequest.SingleFileComponentRecorder, processRequest.ComponentStream);
+ }
+ catch (Exception exception)
+ {
+ this.Logger.LogError(exception, "SwiftComponentDetector: Error processing Package.resolved file: {Location}", processRequest.ComponentStream.Location);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileComponentRecorder, IComponentStream componentStream)
+ {
+ var parsedResolvedFile = this.ReadAndParseResolvedFile(componentStream.Stream);
+
+ foreach (var package in parsedResolvedFile.Pins)
+ {
+ try
+ {
+ if (package.Kind == TargetSwiftPackageKind)
+ {
+ // The version of the package is not always available.
+ var version = package.State.Version ?? package.State.Branch ?? package.State.Revision;
+
+ var detectedSwiftComponent = new SwiftComponent(
+ name: package.Identity,
+ version: version,
+ packageUrl: package.Location,
+ hash: package.State.Revision);
+ var newDetectedSwiftComponent = new DetectedComponent(component: detectedSwiftComponent, detector: this);
+ singleFileComponentRecorder.RegisterUsage(newDetectedSwiftComponent);
+
+ // We also register a Git component for the same package so that the git URL is registered.
+ // Swift Package Manager directly downloads the package from the git URL.
+ var detectedGitComponent = new GitComponent(
+ repositoryUrl: new Uri(package.Location),
+ commitHash: package.State.Revision,
+ tag: version);
+ var newDetectedGitComponent = new DetectedComponent(component: detectedGitComponent, detector: this);
+ singleFileComponentRecorder.RegisterUsage(newDetectedGitComponent);
+ }
+ }
+ catch (Exception exception)
+ {
+ this.Logger.LogError(exception, "SwiftComponentDetector: Error processing package: {Package}", package.Identity);
+ }
+ }
+ }
+
+ ///
+ /// Reads the stream of the package resolved file and parses it.
+ ///
+ /// The stream of the file to parse.
+ /// The parsed object.
+ private SwiftResolvedFile ReadAndParseResolvedFile(Stream stream)
+ {
+ string resolvedFile;
+ using (var reader = new StreamReader(stream))
+ {
+ resolvedFile = reader.ReadToEnd();
+ }
+
+ return JsonConvert.DeserializeObject(resolvedFile);
+ }
+}
diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
index 029ce096d..e0c2b2cc8 100644
--- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs
@@ -19,6 +19,7 @@ namespace Microsoft.ComponentDetection.Orchestrator.Extensions;
using Microsoft.ComponentDetection.Detectors.Ruby;
using Microsoft.ComponentDetection.Detectors.Rust;
using Microsoft.ComponentDetection.Detectors.Spdx;
+using Microsoft.ComponentDetection.Detectors.Swift;
using Microsoft.ComponentDetection.Detectors.Vcpkg;
using Microsoft.ComponentDetection.Detectors.Yarn;
using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
@@ -140,6 +141,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton();
services.AddSingleton();
+ // Swift Package Manager
+ services.AddSingleton();
+
return services;
}
}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs
new file mode 100644
index 000000000..59ca3a683
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs
@@ -0,0 +1,80 @@
+namespace Microsoft.ComponentDetection.Detectors.Tests.Swift;
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using PackageUrl;
+
+[TestClass]
+public class SwiftComponentTests
+{
+ [TestMethod]
+ public void Constructor_ShouldInitializeProperties()
+ {
+ var name = "alamofire";
+ var version = "5.9.1";
+ var packageUrl = "https://github.com/Alamofire/Alamofire";
+ var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a";
+
+ var component = new SwiftComponent(name, version, packageUrl, hash);
+
+ component.Name.Should().Be(name);
+ component.Version.Should().Be(version);
+ component.Type.Should().Be(ComponentType.Swift);
+ component.Id.Should().Be($"{name} {version} - {component.Type}");
+ }
+
+ [TestMethod]
+ public void Constructor_ShouldThrowException_WhenNameIsNull()
+ {
+ Action action = () => new SwiftComponent(null, "5.9.1", "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a");
+ action.Should().Throw().WithMessage("*name*");
+ }
+
+ [TestMethod]
+ public void Constructor_ShouldThrowException_WhenVersionIsNull()
+ {
+ Action action = () => new SwiftComponent("alamofire", null, "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a");
+ action.Should().Throw().WithMessage("*version*");
+ }
+
+ [TestMethod]
+ public void Constructor_ShouldThrowException_WhenPackageUrlIsNull()
+ {
+ Action action = () => new SwiftComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a");
+ action.Should().Throw().WithMessage("*packageUrl*");
+ }
+
+ [TestMethod]
+ public void Constructor_ShouldThrowException_WhenHashIsNull()
+ {
+ Action action = () => new SwiftComponent("alamofire", "5.9.1", "https://github.com/Alamofire/Alamofire", null);
+ action.Should().Throw().WithMessage("*hash*");
+ }
+
+ [TestMethod]
+ public void PackageURL_ShouldReturnCorrectPackageURL()
+ {
+ var name = "alamofire";
+ var version = "5.9.1";
+ var packageUrl = "https://github.com/Alamofire/Alamofire";
+ var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a";
+
+ var component = new SwiftComponent(name, version, packageUrl, hash);
+
+ var expectedPackageURL = new PackageURL(
+ type: "swift",
+ @namespace: "github.com",
+ name: name,
+ version: hash,
+ qualifiers: new SortedDictionary
+ {
+ { "repository_url", packageUrl },
+ },
+ subpath: null);
+
+ component.PackageURL.Should().BeEquivalentTo(expectedPackageURL);
+ }
+}
diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs
new file mode 100644
index 000000000..6cfb54f78
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs
@@ -0,0 +1,471 @@
+namespace Microsoft.ComponentDetection.Detectors.Tests.Swift;
+
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Microsoft.ComponentDetection.Contracts;
+using Microsoft.ComponentDetection.Contracts.TypedComponent;
+using Microsoft.ComponentDetection.Detectors.Swift;
+using Microsoft.ComponentDetection.TestsUtilities;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[TestClass]
+public class SwiftResolvedDetectorTests : BaseDetectorTest
+{
+ [TestMethod]
+ public async Task Test_GivenDetectorWithValidFile_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegistered()
+ {
+ var validResolvedPackageFile = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ validResolvedPackageFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+
+ // Two components are detected because this detector registers a SwiftPM and Git component.
+ detectedComponents.Should().HaveCount(2);
+
+ var typedComponents = detectedComponents.Select(c => c.Component).ToList();
+
+ typedComponents.Should().ContainEquivalentOf(
+ new SwiftComponent(
+ name: "alamofire",
+ version: "5.9.1",
+ packageUrl: "https://github.com/Alamofire/Alamofire",
+ hash: "f455c2975872ccd2d9c81594c658af65716e9b9a"));
+
+ typedComponents.Should().ContainEquivalentOf(
+ new GitComponent(
+ repositoryUrl: new Uri("https://github.com/Alamofire/Alamofire"),
+ commitHash: "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ tag: "5.9.1"));
+ }
+
+ // Test for several packages
+ [TestMethod]
+ public async Task Test_GivenDetectorWithValidFileWithMultiplePackages_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegistered()
+ {
+ var validLongResolvedPackageFile = this.validLongResolvedPackageFile;
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ validLongResolvedPackageFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+
+ // Two components are detected because this detector registers a SwiftPM and Git component.
+ detectedComponents.Should().HaveCount(6);
+
+ var typedComponents = detectedComponents.Select(c => c.Component).ToList();
+
+ typedComponents.Should().ContainEquivalentOf(
+ new SwiftComponent(
+ name: "alamofire",
+ version: "5.6.0",
+ packageUrl: "https://github.com/Alamofire/Alamofire",
+ hash: "63dfa86548c4e5d5c6fd6ed42f638e388cbce529"));
+
+ typedComponents.Should().ContainEquivalentOf(
+ new GitComponent(
+ repositoryUrl: new Uri("https://github.com/sideeffect-io/AsyncExtensions"),
+ commitHash: "3442d3d046800f1974bda096faaf0ac510b21154",
+ tag: "0.5.3"));
+
+ typedComponents.Should().ContainEquivalentOf(
+ new GitComponent(
+ repositoryUrl: new Uri("https://github.com/devicekit/DeviceKit.git"),
+ commitHash: "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6",
+ tag: "4.9.0"));
+ }
+
+ // Duplicate packages
+ [TestMethod]
+ public async Task Test_GivenDetectorWithValidFileWithDuplicatePackages_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegisteredAndComponentsAreNotDuplicate()
+ {
+ var duplicatePackages = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ },
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ duplicatePackages)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+
+ // Two components are detected because this detector registers a SwiftPM and Git component.
+ // The duplicate package is not registered.
+ detectedComponents.Should().HaveCount(2);
+
+ var typedComponents = detectedComponents.Select(c => c.Component).ToList();
+
+ typedComponents.Should().ContainEquivalentOf(
+ new SwiftComponent(
+ name: "alamofire",
+ version: "5.9.1",
+ packageUrl: "https://github.com/Alamofire/Alamofire",
+ hash: "f455c2975872ccd2d9c81594c658af65716e9b9a"));
+
+ typedComponents.Should().ContainEquivalentOf(
+ new GitComponent(
+ repositoryUrl: new Uri("https://github.com/Alamofire/Alamofire"),
+ commitHash: "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ tag: "5.9.1"));
+ }
+
+ [TestMethod]
+ public async Task Test_GivenInvalidJSONFile_WhenScan_ThenNoComponentRegisteredAndScanIsSuccessful()
+ {
+ var invalidJSONResolvedPackageFile = """
+{
+ INVALID JSON
+}
+""";
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ invalidJSONResolvedPackageFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenEmptyFile_WhenScan_ThenNoComponentRegisteredAndScanIsSuccessful()
+ {
+ var emptyResolvedPackageFile = string.Empty;
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ emptyResolvedPackageFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResvoledPackageWithoutPins_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithoutPins = """
+{
+ "pins" : [
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutPins)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutIdentity_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var validResolvedPackageFile = """
+{
+ "pins" : [
+ {
+ "identity" : "",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ },
+ {
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SimplyDanny/SwiftLintPlugins",
+ "state" : {
+ "revision" : "6c3d6c32a37224179dc290f21e03d1238f3d963b",
+ "version" : "0.56.2"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ validResolvedPackageFile)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutKind_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithoutKind = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutKind)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutLocation_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithoutLocation = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ "version" : "5.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutLocation)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutState_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithoutState = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire"
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutState)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithEmptyState_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithEmptyState = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {}
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithEmptyState)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutRevision_WhenScan_ThenScanIsSuccessfulAndNoComponentsRegistered()
+ {
+ var resolvedPackageWithoutRevision = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "version" : "5.9.1"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutRevision)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().BeEmpty();
+ }
+
+ [TestMethod]
+ public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuccessfulAndComponentRegisteredWithRevisionHashAsVersion()
+ {
+ var resolvedPackageWithoutVersion = """
+{
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "f455c2975872ccd2d9c81594c658af65716e9b9a"
+ }
+ }
+ ],
+ "version" : 2
+}
+""";
+
+ var (scanResult, componentRecorder) = await this.DetectorTestUtility.WithFile(
+ "Package.resolved",
+ resolvedPackageWithoutVersion)
+ .ExecuteDetectorAsync();
+
+ scanResult.ResultCode.Should().Be(ProcessingResultCode.Success);
+ componentRecorder.GetDetectedComponents().Should().HaveCount(2);
+
+ var detectedComponents = componentRecorder.GetDetectedComponents();
+ var typedComponents = detectedComponents.Select(c => c.Component).ToList();
+
+ typedComponents.Should().ContainEquivalentOf(
+ new SwiftComponent(
+ name: "alamofire",
+ version: "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ packageUrl: "https://github.com/Alamofire/Alamofire",
+ hash: "f455c2975872ccd2d9c81594c658af65716e9b9a"));
+
+ typedComponents.Should().ContainEquivalentOf(
+ new GitComponent(
+ repositoryUrl: new Uri("https://github.com/Alamofire/Alamofire"),
+ commitHash: "f455c2975872ccd2d9c81594c658af65716e9b9a",
+ tag: "f455c2975872ccd2d9c81594c658af65716e9b9a"));
+ }
+
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1201:Elements should appear in the correct order", Justification = "Test data that is better placed at the end of the file.")]
+ private readonly string validLongResolvedPackageFile = """
+{
+ "originHash" : "6ad1e0d3ae43bde33043d3286afc3d98e5be09945ac257218cb6a9dba14466c3",
+ "pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire",
+ "state" : {
+ "revision" : "63dfa86548c4e5d5c6fd6ed42f638e388cbce529",
+ "version" : "5.6.0"
+ }
+ },
+ {
+ "identity" : "asyncextensions",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/sideeffect-io/AsyncExtensions",
+ "state" : {
+ "branch": null,
+ "revision" : "3442d3d046800f1974bda096faaf0ac510b21154",
+ "version" : "0.5.3"
+ }
+ },
+ {
+ "identity" : "devicekit",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/devicekit/DeviceKit.git",
+ "state" : {
+ "revision" : "d37e70cb2646666dcf276d7d3d4a9760a41ff8a6",
+ "version" : "4.9.0"
+ }
+ },
+ {
+ "identity" : "localdependency",
+ "kind" : "localSource",
+ "location" : "../LocalDependency"
+ }
+ ],
+ "version" : 2
+}
+"""
+;
+}
diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/swift/Package.resolved b/test/Microsoft.ComponentDetection.VerificationTests/resources/swift/Package.resolved
new file mode 100644
index 000000000..c8df53a34
--- /dev/null
+++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/swift/Package.resolved
@@ -0,0 +1,60 @@
+{
+ "originHash" : "9e7d32d14f81c4312e8b239503282bdc958b2b52492ede2c123001eeac298b0e",
+ "pins" : [
+ {
+ "identity" : "appauth-ios",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/openid/AppAuth-iOS.git",
+ "state" : {
+ "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2",
+ "version" : "1.7.5"
+ }
+ },
+ {
+ "identity" : "google-api-objectivec-client-for-rest",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/google-api-objectivec-client-for-rest",
+ "state" : {
+ "branch" : "main",
+ "revision" : "a8c1e0b1173659d0be452680582c28556372ef74"
+ }
+ },
+ {
+ "identity" : "googlesignin-ios",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GoogleSignIn-iOS",
+ "state" : {
+ "revision" : "a7965d134c5d3567026c523e0a8a583f73b62b0d",
+ "version" : "7.1.0"
+ }
+ },
+ {
+ "identity" : "gtm-session-fetcher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/gtm-session-fetcher.git",
+ "state" : {
+ "revision" : "a2ab612cb980066ee56d90d60d8462992c07f24b",
+ "version" : "3.5.0"
+ }
+ },
+ {
+ "identity" : "gtmappauth",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/google/GTMAppAuth.git",
+ "state" : {
+ "revision" : "5d7d66f647400952b1758b230e019b07c0b4b22a",
+ "version" : "4.1.1"
+ }
+ },
+ {
+ "identity" : "microsoft-authentication-library-for-objc",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/AzureAD/microsoft-authentication-library-for-objc",
+ "state" : {
+ "revision" : "9ae8b61c868962153d5fa6a2492deddf804b1acd",
+ "version" : "1.4.0"
+ }
+ }
+ ],
+ "version" : 3
+}