Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Swift Package Manager component detection support #1316

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion .github/workflows/snapshot-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
raporpe marked this conversation as resolved.
Show resolved Hide resolved

on:
push:
Expand Down Expand Up @@ -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
raporpe marked this conversation as resolved.
Show resolved Hide resolved

- name: Upload output folder
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
Expand Down
24 changes: 24 additions & 0 deletions docs/detectors/swiftpm.md
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions src/Microsoft.ComponentDetection.Contracts/DetectorClass.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,7 @@ public enum DetectorClass

/// <summary>Indicates a detector applies to Docker references.</summary>
DockerReference,

/// <summary> Indicates a detector applies to SwiftPM packages.</summary>
SwiftPM,
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ public enum ComponentType : byte

[EnumMember]
Conan = 17,

[EnumMember]
SwiftPM = 18,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;

using System;
using System.Collections.Generic;
using PackageUrl;

/// <summary>
/// Represents a SwiftPM component.
/// </summary>
public class SwiftPMComponent : TypedComponent
{
private readonly string packageUrl;

private readonly string hash;

/// <summary>
/// Initializes a new instance of the <see cref="SwiftPMComponent"/> class.
/// </summary>
/// <param name="name">The name of the component.</param>
/// <param name="version">The version of the component.</param>
/// <param name="packageUrl">The package URL of the component.</param>
/// <param name="hash">The hash of the component.</param>
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<string, string>
{
{ "repository_url", this.packageUrl },
},
subpath: null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
namespace Microsoft.ComponentDetection.Contracts.TypedComponent;

using System.Collections.Generic;
using Newtonsoft.Json;

/// <summary>
/// Represents a SwiftPM component.
/// </summary>
public class SwiftPMResolvedFile
{
[JsonProperty("pins")]
public IList<SwiftPMDependency> 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; }
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Detects SwiftPM components.
/// </summary>
public class SwiftPMResolvedComponentDetector : FileComponentDetector, IDefaultOffComponentDetector
{
public SwiftPMResolvedComponentDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<SwiftPMResolvedComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id { get; } = "SwiftPM";
raporpe marked this conversation as resolved.
Show resolved Hide resolved

public override IEnumerable<string> Categories => [Enum.GetName(DetectorClass.SwiftPM)];

public override IList<string> SearchPatterns { get; } = ["Package.resolved"];

public override IEnumerable<ComponentType> SupportedComponentTypes => [ComponentType.SwiftPM];

public override int Version => 2;

protected override Task OnFileFoundAsync(
ProcessRequest processRequest,
IDictionary<string, string> 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);
}
}
}

/// <summary>
/// Reads the stream of the package resolved file and parses it.
/// </summary>
/// <param name="stream">The stream of the file to parse.</param>
/// <returns>The parsed object.</returns>
private SwiftPMResolvedFile ReadAndParseResolvedFile(Stream stream)
{
string resolvedFile;
using (var reader = new StreamReader(stream))
{
resolvedFile = reader.ReadToEnd();
}

return JsonConvert.DeserializeObject<SwiftPMResolvedFile>(resolvedFile);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -142,6 +143,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IYarnLockFileFactory, YarnLockFileFactory>();
services.AddSingleton<IComponentDetector, YarnLockComponentDetector>();

// SwiftPM
services.AddSingleton<IComponentDetector, SwiftPMResolvedComponentDetector>();

return services;
}
}
Original file line number Diff line number Diff line change
@@ -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<ArgumentException>().WithMessage("*name*");
}

[TestMethod]
public void Constructor_ShouldThrowException_WhenVersionIsNull()
{
Action action = () => new SwiftPMComponent("alamofire", null, "https://github.com/Alamofire/Alamofire", "f455c2975872ccd2d9c81594c658af65716e9b9a");
action.Should().Throw<ArgumentException>().WithMessage("*version*");
}

[TestMethod]
public void Constructor_ShouldThrowException_WhenPackageUrlIsNull()
{
Action action = () => new SwiftPMComponent("alamofire", "5.9.1", null, "f455c2975872ccd2d9c81594c658af65716e9b9a");
action.Should().Throw<ArgumentException>().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<ArgumentException>().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<string, string>
{
{ "repository_url", packageUrl },
},
subpath: null);

component.PackageURL.Should().BeEquivalentTo(expectedPackageURL);
}
}
Loading
Loading