From bbf751582385414cf5fa0b42d70f6a8a045a3500 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Fri, 22 Nov 2024 17:48:37 +0100 Subject: [PATCH 1/9] Create Detector and Component types --- src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs | 3 +++ .../TypedComponent/ComponentType.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs index cde85fd50..cfd1ee53e 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 SwiftPM packages. + SwiftPM, } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs index 3d3593c77..2b6c144db 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] + SwiftPM = 18, } From 505075f46977a3009f7338813e8f393ea93c7f71 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Thu, 28 Nov 2024 13:46:48 +0100 Subject: [PATCH 2/9] Implementation + tests --- .../TypedComponent/SwiftPMComponent.cs | 50 ++ .../swiftpm/Contracts/SwiftPMResolvedFile.cs | 51 ++ .../SwiftPMResolvedComponentDetector.cs | 105 ++++ .../Extensions/ServiceCollectionExtensions.cs | 4 + .../SwiftPMComponentTests.cs | 80 +++ .../SwiftPMResolvedDetectorTests.cs | 471 ++++++++++++++++++ 6 files changed, 761 insertions(+) create mode 100644 src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs create mode 100644 src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs create mode 100644 test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs new file mode 100644 index 000000000..907d4559c --- /dev/null +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs @@ -0,0 +1,50 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent; + +using System; +using System.Collections.Generic; +using PackageUrl; + +/// +/// Represents a SwiftPM component. +/// +public class SwiftPMComponent : 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 SwiftPMComponent(string name, string version, string packageUrl, string hash) + { + this.Name = this.ValidateRequiredInput(name, nameof(name), nameof(ComponentType.SwiftPM)); + this.Version = this.ValidateRequiredInput(version, nameof(version), nameof(ComponentType.SwiftPM)); + this.packageUrl = this.ValidateRequiredInput(packageUrl, nameof(packageUrl), nameof(ComponentType.SwiftPM)); + this.hash = this.ValidateRequiredInput(hash, nameof(hash), nameof(ComponentType.SwiftPM)); + } + + public string Name { get; } + + public string Version { get; } + + public override ComponentType Type => ComponentType.SwiftPM; + + 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/SwiftPMResolvedFile.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs new file mode 100644 index 000000000..4de8142f9 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs @@ -0,0 +1,51 @@ +namespace Microsoft.ComponentDetection.Contracts.TypedComponent; + +using System.Collections.Generic; +using Newtonsoft.Json; + +/// +/// Represents a SwiftPM component. +/// +public class SwiftPMResolvedFile +{ + [JsonProperty("pins")] + public IList Pins { get; set; } + + [JsonProperty("version")] + public int Version { get; set; } + + public class SwiftPMDependency + { + // 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 SwiftPM 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 SwiftPMState State { get; set; } + + public class SwiftPMState + { + // 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/SwiftPMResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs new file mode 100644 index 000000000..50e873a22 --- /dev/null +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs @@ -0,0 +1,105 @@ +namespace Microsoft.ComponentDetection.Detectors.SwiftPM; + +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 SwiftPM components. +/// +public class SwiftPMResolvedComponentDetector : FileComponentDetector, IDefaultOffComponentDetector +{ + public SwiftPMResolvedComponentDetector( + IComponentStreamEnumerableFactory componentStreamEnumerableFactory, + IObservableDirectoryWalkerFactory walkerFactory, + ILogger logger) + { + this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; + this.Scanner = walkerFactory; + this.Logger = logger; + } + + public override string Id { get; } = "SwiftPM"; + + public override IEnumerable Categories => [Enum.GetName(DetectorClass.SwiftPM)]; + + public override IList SearchPatterns { get; } = ["Package.resolved"]; + + public override IEnumerable SupportedComponentTypes => [ComponentType.SwiftPM]; + + public override int Version => 2; + + 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, "SwiftPMComponentDetector: 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) + { + // We are only interested in packages coming from remote sources such as git + // The Package Kind is not an enum because the SwiftPM contract does not specify the possible values. + var targetSwiftPackageKind = "remoteSourceControl"; + 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 detectedSwiftPMComponent = new SwiftPMComponent( + name: package.Identity, + version: version, + packageUrl: package.Location, + hash: package.State.Revision); + var newDetectedSwiftComponent = new DetectedComponent(component: detectedSwiftPMComponent, detector: this); + singleFileComponentRecorder.RegisterUsage(newDetectedSwiftComponent); + + // We also register a Git component for the same package so that the git URL is registered. + // SwiftPM 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); + } + } + } + + /// + /// Reads the stream of the package resolved file and parses it. + /// + /// The stream of the file to parse. + /// The parsed object. + private SwiftPMResolvedFile 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 23f585a91..018735346 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.SwiftPM; using Microsoft.ComponentDetection.Detectors.Vcpkg; using Microsoft.ComponentDetection.Detectors.Yarn; using Microsoft.ComponentDetection.Detectors.Yarn.Parsers; @@ -142,6 +143,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); + // SwiftPM + services.AddSingleton(); + return services; } } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs new file mode 100644 index 000000000..b69ffbec1 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs @@ -0,0 +1,80 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; + +using System; +using System.Collections.Generic; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using PackageUrl; + +[TestClass] +public class SwiftPMComponentTests +{ + [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 SwiftPMComponent(name, version, packageUrl, hash); + + component.Name.Should().Be(name); + component.Version.Should().Be(version); + component.Type.Should().Be(ComponentType.SwiftPM); + component.Id.Should().Be($"{name} {version} - {component.Type}"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenNameIsNull() + { + Action action = () => new SwiftPMComponent(null, "5.9.1", "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*name*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenVersionIsNull() + { + Action action = () => new SwiftPMComponent("alamofire", null, "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*version*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenPackageUrlIsNull() + { + Action action = () => new SwiftPMComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a"); + action.Should().Throw().WithMessage("*packageUrl*"); + } + + [TestMethod] + public void Constructor_ShouldThrowException_WhenHashIsNull() + { + Action action = () => new SwiftPMComponent("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 SwiftPMComponent(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/SwiftPMResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs new file mode 100644 index 000000000..5916e21d6 --- /dev/null +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs @@ -0,0 +1,471 @@ +namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; + +using System; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.ComponentDetection.Contracts; +using Microsoft.ComponentDetection.Contracts.TypedComponent; +using Microsoft.ComponentDetection.Detectors.SwiftPM; +using Microsoft.ComponentDetection.TestsUtilities; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[TestClass] +public class SwiftPMResolvedDetectorTests : 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 SwiftPMComponent( + 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 SwiftPMComponent( + 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 SwiftPMComponent( + 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 SwiftPMComponent( + 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 +} +""" +; +} From 29cb7947c908eed83f6130d2171544f3d22451dc Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Thu, 28 Nov 2024 13:46:55 +0100 Subject: [PATCH 3/9] Verification file --- .../resources/swiftpm/Package.resolved | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/Package.resolved diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/Package.resolved b/test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/Package.resolved new file mode 100644 index 000000000..c8df53a34 --- /dev/null +++ b/test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/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 +} From 7c6631a466b08fa0c743a44fd8bf38f3188958ef Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Thu, 28 Nov 2024 13:47:00 +0100 Subject: [PATCH 4/9] Add docs --- docs/detectors/swiftpm.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 docs/detectors/swiftpm.md diff --git a/docs/detectors/swiftpm.md b/docs/detectors/swiftpm.md new file mode 100644 index 000000000..d8f70d6ec --- /dev/null +++ b/docs/detectors/swiftpm.md @@ -0,0 +1,24 @@ +# Go Detection + +## Requirements + +Swift Package Manager detection requires the following file to be present in the scan directory: + +- `Package.resolved` file + +## Detection strategy + +The detector `SwiftPMResolvedComponentDetector` 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 From 0cd36ad9aa475f377ade3f4ee39a2f1fcc06342f Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Thu, 28 Nov 2024 13:47:09 +0100 Subject: [PATCH 5/9] Fix pipeline --- .github/workflows/snapshot-publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/snapshot-publish.yml b/.github/workflows/snapshot-publish.yml index d6c521200..76124fbe3 100644 --- a/.github/workflows/snapshot-publish.yml +++ b/.github/workflows/snapshot-publish.yml @@ -2,6 +2,8 @@ name: Publish snapshot of test scan env: CD_DETECTOR_EXPERIMENTS: 1 + PipReportSkipFallbackOnFailure: "true" + PIP_INDEX_URL: "https://pypi.python.org/simple" on: push: @@ -54,7 +56,7 @@ jobs: run: dotnet run scan --Verbosity Verbose --SourceDirectory ${{ github.workspace }}/test/Microsoft.ComponentDetection.VerificationTests/resources --Output ${{ github.workspace }}/output --DockerImagesToScan "docker.io/library/debian@sha256:9b0e3056b8cd8630271825665a0613cc27829d6a24906dc0122b3b4834312f7d,mcr.microsoft.com/cbl-mariner/base/core@sha256:c1bc83a3d385eccbb2f7f7da43a726c697e22a996f693a407c35ac7b4387cd59,docker.io/library/alpine@sha256:1304f174557314a7ed9eddb4eab12fed12cb0cd9809e4c28f29af86979a3c870" - --DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff,SimplePip=EnableIfDefaultOff + --DetectorArgs DockerReference=EnableIfDefaultOff,SPDX22SBOM=EnableIfDefaultOff - name: Upload output folder uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 From a16b7971bc67dd90c888b4db02534a12bfbb7496 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Mon, 20 Jan 2025 11:17:32 +0100 Subject: [PATCH 6/9] Rename SwiftPM to Swift --- docs/detectors/{swiftpm.md => swift.md} | 13 ++++---- .../DetectorClass.cs | 4 +-- .../TypedComponent/ComponentType.cs | 2 +- ...{SwiftPMComponent.cs => SwiftComponent.cs} | 18 +++++------ ...PMResolvedFile.cs => SwiftResolvedFile.cs} | 14 ++++----- ...r.cs => SwiftResolvedComponentDetector.cs} | 30 +++++++++---------- .../Extensions/ServiceCollectionExtensions.cs | 6 ++-- ...mponentTests.cs => SwiftComponentTests.cs} | 18 +++++------ ...Tests.cs => SwiftResolvedDetectorTests.cs} | 14 ++++----- .../{swiftpm => swift}/Package.resolved | 0 10 files changed, 59 insertions(+), 60 deletions(-) rename docs/detectors/{swiftpm.md => swift.md} (73%) rename src/Microsoft.ComponentDetection.Contracts/TypedComponent/{SwiftPMComponent.cs => SwiftComponent.cs} (74%) rename src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/{SwiftPMResolvedFile.cs => SwiftResolvedFile.cs} (78%) rename src/Microsoft.ComponentDetection.Detectors/swiftpm/{SwiftPMResolvedComponentDetector.cs => SwiftResolvedComponentDetector.cs} (77%) rename test/Microsoft.ComponentDetection.Detectors.Tests/{SwiftPMComponentTests.cs => SwiftComponentTests.cs} (70%) rename test/Microsoft.ComponentDetection.Detectors.Tests/{SwiftPMResolvedDetectorTests.cs => SwiftResolvedDetectorTests.cs} (97%) rename test/Microsoft.ComponentDetection.VerificationTests/resources/{swiftpm => swift}/Package.resolved (100%) diff --git a/docs/detectors/swiftpm.md b/docs/detectors/swift.md similarity index 73% rename from docs/detectors/swiftpm.md rename to docs/detectors/swift.md index d8f70d6ec..3b44ef630 100644 --- a/docs/detectors/swiftpm.md +++ b/docs/detectors/swift.md @@ -1,24 +1,23 @@ -# Go Detection +# Swift Package Manager Detection ## Requirements Swift Package Manager detection requires the following file to be present in the scan directory: -- `Package.resolved` file +- `Package.resolved` file ## Detection strategy -The detector `SwiftPMResolvedComponentDetector` only parses the `Package.resolved` file to get the dependencies. +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. +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. +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 cfd1ee53e..becfa4f9f 100644 --- a/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs +++ b/src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs @@ -45,6 +45,6 @@ public enum DetectorClass /// Indicates a detector applies to Docker references. DockerReference, - /// Indicates a detector applies to SwiftPM packages. - SwiftPM, + /// 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 2b6c144db..6e2becf9d 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/ComponentType.cs @@ -58,5 +58,5 @@ public enum ComponentType : byte Conan = 17, [EnumMember] - SwiftPM = 18, + Swift = 18, } diff --git a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs similarity index 74% rename from src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs rename to src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs index 907d4559c..b00a9c939 100644 --- a/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftPMComponent.cs +++ b/src/Microsoft.ComponentDetection.Contracts/TypedComponent/SwiftComponent.cs @@ -5,34 +5,34 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using PackageUrl; /// -/// Represents a SwiftPM component. +/// Represents a Swift package manager component. /// -public class SwiftPMComponent : TypedComponent +public class SwiftComponent : TypedComponent { private readonly string packageUrl; private readonly string hash; /// - /// Initializes a new instance of the class. + /// 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 SwiftPMComponent(string name, string version, string packageUrl, string hash) + public SwiftComponent(string name, string version, string packageUrl, string hash) { - this.Name = this.ValidateRequiredInput(name, nameof(name), nameof(ComponentType.SwiftPM)); - this.Version = this.ValidateRequiredInput(version, nameof(version), nameof(ComponentType.SwiftPM)); - this.packageUrl = this.ValidateRequiredInput(packageUrl, nameof(packageUrl), nameof(ComponentType.SwiftPM)); - this.hash = this.ValidateRequiredInput(hash, nameof(hash), nameof(ComponentType.SwiftPM)); + 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.SwiftPM; + public override ComponentType Type => ComponentType.Swift; public override string Id => $"{this.Name} {this.Version} - {this.Type}"; diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs similarity index 78% rename from src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs rename to src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs index 4de8142f9..5d7bf529c 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftPMResolvedFile.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/Contracts/SwiftResolvedFile.cs @@ -4,24 +4,24 @@ namespace Microsoft.ComponentDetection.Contracts.TypedComponent; using Newtonsoft.Json; /// -/// Represents a SwiftPM component. +/// Represents a Swift Package Manager component. /// -public class SwiftPMResolvedFile +public class SwiftResolvedFile { [JsonProperty("pins")] - public IList Pins { get; set; } + public IList Pins { get; set; } [JsonProperty("version")] public int Version { get; set; } - public class SwiftPMDependency + 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 SwiftPM contract does not specify the possible values. + // This is not an enum because the Swift contract does not specify the possible values. [JsonProperty("kind")] public string Kind { get; set; } @@ -31,9 +31,9 @@ public class SwiftPMDependency // Data about the package version and commit hash. [JsonProperty("state")] - public SwiftPMState State { get; set; } + public SwiftState State { get; set; } - public class SwiftPMState + public class SwiftState { // The commit hash of the package. [JsonProperty("revision")] diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs similarity index 77% rename from src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs rename to src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index 50e873a22..e07f6140d 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftPMResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -1,4 +1,4 @@ -namespace Microsoft.ComponentDetection.Detectors.SwiftPM; +namespace Microsoft.ComponentDetection.Detectors.Swift; using System; using System.Collections.Generic; @@ -12,27 +12,27 @@ namespace Microsoft.ComponentDetection.Detectors.SwiftPM; using Newtonsoft.Json; /// -/// Detects SwiftPM components. +/// Detects Swift Package Manager components. /// -public class SwiftPMResolvedComponentDetector : FileComponentDetector, IDefaultOffComponentDetector +public class SwiftResolvedComponentDetector : FileComponentDetector, IDefaultOffComponentDetector { - public SwiftPMResolvedComponentDetector( + public SwiftResolvedComponentDetector( IComponentStreamEnumerableFactory componentStreamEnumerableFactory, IObservableDirectoryWalkerFactory walkerFactory, - ILogger logger) + ILogger logger) { this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory; this.Scanner = walkerFactory; this.Logger = logger; } - public override string Id { get; } = "SwiftPM"; + public override string Id { get; } = "Swift"; - public override IEnumerable Categories => [Enum.GetName(DetectorClass.SwiftPM)]; + public override IEnumerable Categories => [Enum.GetName(DetectorClass.Swift)]; public override IList SearchPatterns { get; } = ["Package.resolved"]; - public override IEnumerable SupportedComponentTypes => [ComponentType.SwiftPM]; + public override IEnumerable SupportedComponentTypes => [ComponentType.Swift]; public override int Version => 2; @@ -47,7 +47,7 @@ protected override Task OnFileFoundAsync( } catch (Exception exception) { - this.Logger.LogError(exception, "SwiftPMComponentDetector: Error processing Package.resolved file: {Location}", processRequest.ComponentStream.Location); + this.Logger.LogError(exception, "SwiftComponentDetector: Error processing Package.resolved file: {Location}", processRequest.ComponentStream.Location); } return Task.CompletedTask; @@ -60,23 +60,23 @@ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileC foreach (var package in parsedResolvedFile.Pins) { // We are only interested in packages coming from remote sources such as git - // The Package Kind is not an enum because the SwiftPM contract does not specify the possible values. + // The Package Kind is not an enum because the Swift Package Manager contract does not specify the possible values. var targetSwiftPackageKind = "remoteSourceControl"; 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 detectedSwiftPMComponent = new SwiftPMComponent( + var detectedSwiftComponent = new SwiftComponent( name: package.Identity, version: version, packageUrl: package.Location, hash: package.State.Revision); - var newDetectedSwiftComponent = new DetectedComponent(component: detectedSwiftPMComponent, detector: this); + 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. - // SwiftPM directly downloads the package from the git URL. + // Swift Package Manager directly downloads the package from the git URL. var detectedGitComponent = new GitComponent( repositoryUrl: new Uri(package.Location), commitHash: package.State.Revision, @@ -92,7 +92,7 @@ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileC /// /// The stream of the file to parse. /// The parsed object. - private SwiftPMResolvedFile ReadAndParseResolvedFile(Stream stream) + private SwiftResolvedFile ReadAndParseResolvedFile(Stream stream) { string resolvedFile; using (var reader = new StreamReader(stream)) @@ -100,6 +100,6 @@ private SwiftPMResolvedFile ReadAndParseResolvedFile(Stream stream) resolvedFile = reader.ReadToEnd(); } - return JsonConvert.DeserializeObject(resolvedFile); + return JsonConvert.DeserializeObject(resolvedFile); } } diff --git a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs index c5150126c..e0c2b2cc8 100644 --- a/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs +++ b/src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs @@ -19,7 +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.SwiftPM; +using Microsoft.ComponentDetection.Detectors.Swift; using Microsoft.ComponentDetection.Detectors.Vcpkg; using Microsoft.ComponentDetection.Detectors.Yarn; using Microsoft.ComponentDetection.Detectors.Yarn.Parsers; @@ -141,8 +141,8 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s services.AddSingleton(); services.AddSingleton(); - // SwiftPM - services.AddSingleton(); + // Swift Package Manager + services.AddSingleton(); return services; } diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs similarity index 70% rename from test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs rename to test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs index b69ffbec1..59ca3a683 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMComponentTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftComponentTests.cs @@ -1,4 +1,4 @@ -namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; +namespace Microsoft.ComponentDetection.Detectors.Tests.Swift; using System; using System.Collections.Generic; @@ -8,7 +8,7 @@ namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; using PackageUrl; [TestClass] -public class SwiftPMComponentTests +public class SwiftComponentTests { [TestMethod] public void Constructor_ShouldInitializeProperties() @@ -18,39 +18,39 @@ public void Constructor_ShouldInitializeProperties() var packageUrl = "https://github.com/Alamofire/Alamofire"; var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftPMComponent(name, version, packageUrl, hash); + var component = new SwiftComponent(name, version, packageUrl, hash); component.Name.Should().Be(name); component.Version.Should().Be(version); - component.Type.Should().Be(ComponentType.SwiftPM); + component.Type.Should().Be(ComponentType.Swift); component.Id.Should().Be($"{name} {version} - {component.Type}"); } [TestMethod] public void Constructor_ShouldThrowException_WhenNameIsNull() { - Action action = () => new SwiftPMComponent(null, "5.9.1", "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + 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 SwiftPMComponent("alamofire", null, "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a"); + 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 SwiftPMComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a"); + Action action = () => new SwiftComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a"); action.Should().Throw().WithMessage("*packageUrl*"); } [TestMethod] public void Constructor_ShouldThrowException_WhenHashIsNull() { - Action action = () => new SwiftPMComponent("alamofire", "5.9.1", "https://github.com/Alamofire/Alamofire", null); + Action action = () => new SwiftComponent("alamofire", "5.9.1", "https://github.com/Alamofire/Alamofire", null); action.Should().Throw().WithMessage("*hash*"); } @@ -62,7 +62,7 @@ public void PackageURL_ShouldReturnCorrectPackageURL() var packageUrl = "https://github.com/Alamofire/Alamofire"; var hash = "f455c2975872ccd2d9c81594c658af65716e9b9a"; - var component = new SwiftPMComponent(name, version, packageUrl, hash); + var component = new SwiftComponent(name, version, packageUrl, hash); var expectedPackageURL = new PackageURL( type: "swift", diff --git a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs similarity index 97% rename from test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs rename to test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs index 5916e21d6..6cfb54f78 100644 --- a/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftPMResolvedDetectorTests.cs +++ b/test/Microsoft.ComponentDetection.Detectors.Tests/SwiftResolvedDetectorTests.cs @@ -1,4 +1,4 @@ -namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; +namespace Microsoft.ComponentDetection.Detectors.Tests.Swift; using System; using System.Linq; @@ -6,12 +6,12 @@ namespace Microsoft.ComponentDetection.Detectors.Tests.SwiftPM; using FluentAssertions; using Microsoft.ComponentDetection.Contracts; using Microsoft.ComponentDetection.Contracts.TypedComponent; -using Microsoft.ComponentDetection.Detectors.SwiftPM; +using Microsoft.ComponentDetection.Detectors.Swift; using Microsoft.ComponentDetection.TestsUtilities; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] -public class SwiftPMResolvedDetectorTests : BaseDetectorTest +public class SwiftResolvedDetectorTests : BaseDetectorTest { [TestMethod] public async Task Test_GivenDetectorWithValidFile_WhenScan_ThenScanIsSuccessfulAndComponentsAreRegistered() @@ -47,7 +47,7 @@ public async Task Test_GivenDetectorWithValidFile_WhenScan_ThenScanIsSuccessfulA var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftPMComponent( + new SwiftComponent( name: "alamofire", version: "5.9.1", packageUrl: "https://github.com/Alamofire/Alamofire", @@ -81,7 +81,7 @@ public async Task Test_GivenDetectorWithValidFileWithMultiplePackages_WhenScan_T var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftPMComponent( + new SwiftComponent( name: "alamofire", version: "5.6.0", packageUrl: "https://github.com/Alamofire/Alamofire", @@ -146,7 +146,7 @@ public async Task Test_GivenDetectorWithValidFileWithDuplicatePackages_WhenScan_ var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftPMComponent( + new SwiftComponent( name: "alamofire", version: "5.9.1", packageUrl: "https://github.com/Alamofire/Alamofire", @@ -412,7 +412,7 @@ public async Task Test_GivenResolvedPackageWithoutVersion_WhenScan_ThenScanIsSuc var typedComponents = detectedComponents.Select(c => c.Component).ToList(); typedComponents.Should().ContainEquivalentOf( - new SwiftPMComponent( + new SwiftComponent( name: "alamofire", version: "f455c2975872ccd2d9c81594c658af65716e9b9a", packageUrl: "https://github.com/Alamofire/Alamofire", diff --git a/test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/Package.resolved b/test/Microsoft.ComponentDetection.VerificationTests/resources/swift/Package.resolved similarity index 100% rename from test/Microsoft.ComponentDetection.VerificationTests/resources/swiftpm/Package.resolved rename to test/Microsoft.ComponentDetection.VerificationTests/resources/swift/Package.resolved From b637bbb370522d4a84ef906874f43effeeeaa669 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Mon, 27 Jan 2025 17:45:49 +0100 Subject: [PATCH 7/9] Change detector version to 1 --- .../swiftpm/SwiftResolvedComponentDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index e07f6140d..91cccecbd 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -34,7 +34,7 @@ public SwiftResolvedComponentDetector( public override IEnumerable SupportedComponentTypes => [ComponentType.Swift]; - public override int Version => 2; + public override int Version => 1; protected override Task OnFileFoundAsync( ProcessRequest processRequest, From 7a9e269f4b86cd0a4f1f739f2398d4188b465fc5 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Mon, 27 Jan 2025 17:46:04 +0100 Subject: [PATCH 8/9] Make TargetSwiftPackageKind a private field --- .../swiftpm/SwiftResolvedComponentDetector.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index 91cccecbd..a9f782edb 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -16,6 +16,10 @@ namespace Microsoft.ComponentDetection.Detectors.Swift; /// 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, @@ -59,10 +63,7 @@ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileC foreach (var package in parsedResolvedFile.Pins) { - // 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. - var targetSwiftPackageKind = "remoteSourceControl"; - if (package.Kind == targetSwiftPackageKind) + if (package.Kind == TargetSwiftPackageKind) { // The version of the package is not always available. var version = package.State.Version ?? package.State.Branch ?? package.State.Revision; From b16f488dfbf67257a95e688fdd57345cb642f953 Mon Sep 17 00:00:00 2001 From: Raul Portugues del Peno Date: Wed, 29 Jan 2025 09:56:17 +0100 Subject: [PATCH 9/9] Add error logging --- .../swiftpm/SwiftResolvedComponentDetector.cs | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs index a9f782edb..6d6edc0c4 100644 --- a/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs +++ b/src/Microsoft.ComponentDetection.Detectors/swiftpm/SwiftResolvedComponentDetector.cs @@ -63,27 +63,34 @@ private void ProcessPackageResolvedFile(ISingleFileComponentRecorder singleFileC foreach (var package in parsedResolvedFile.Pins) { - if (package.Kind == TargetSwiftPackageKind) + try { - // 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); + 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); } } }