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 +}