Skip to content

Add Swift Package Manager component detection support #1316

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/detectors/swift.md
Original file line number Diff line number Diff line change
@@ -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
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 Swift packages.</summary>
Swift,
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,7 @@ public enum ComponentType : byte

[EnumMember]
Conan = 17,

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

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

/// <summary>
/// Represents a Swift package manager component.
/// </summary>
public class SwiftComponent : TypedComponent
{
private readonly Uri packageUrl;

private readonly string hash;

/// <summary>
/// Initializes a new instance of the <see cref="SwiftComponent"/> 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 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.ValidateRequiredInput(packageUrl, nameof(packageUrl), nameof(ComponentType.Swift));
this.packageUrl = new Uri(packageUrl);
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}";

// Example PackageURL -> pkg:swift/github.com/apple/swift-asn1
// type: swift
// namespace: github.com/apple
// name: swift-asn1
public PackageURL PackageURL => new PackageURL(
type: "swift",
@namespace: this.GetNamespaceFromPackageUrl(),
name: this.Name,
version: this.Version,
qualifiers: new SortedDictionary<string, string>
{
{ "repository_url", this.packageUrl.AbsoluteUri },
},
subpath: null);

private string GetNamespaceFromPackageUrl()
{
// In the case of github.com, the namespace should contain the user/organization
// See https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#swift
var uppercaseHost = this.packageUrl.Host.ToUpperInvariant();
if (uppercaseHost.Contains("GITHUB.COM"))
{
// The first segment of the URL will contain the user or organization for GitHub
var firstSegment = this.packageUrl.Segments[1].Trim('/');
return $"{this.packageUrl.Host}/{firstSegment}";
}

// In the default case of a generic host, the namespace should be the just the host
return this.packageUrl.Host;
}
}
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 Swift Package Manager component.
/// </summary>
public class SwiftResolvedFile
{
[JsonProperty("pins")]
public IList<SwiftDependency> 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; }
}
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Detects Swift Package Manager components.
/// </summary>
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<SwiftResolvedComponentDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id { get; } = "Swift";

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

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

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

public override int Version => 1;

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, "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);
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);
singleFileComponentRecorder.RegisterUsage(newDetectedGitComponent);
}
}
catch (Exception exception)
{
this.Logger.LogError(exception, "SwiftComponentDetector: Error processing package: {Package}", package.Identity);
}
}
}

/// <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 SwiftResolvedFile ReadAndParseResolvedFile(Stream stream)
{
string resolvedFile;
using (var reader = new StreamReader(stream))
{
resolvedFile = reader.ReadToEnd();
}

return JsonConvert.DeserializeObject<SwiftResolvedFile>(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.Swift;
using Microsoft.ComponentDetection.Detectors.Vcpkg;
using Microsoft.ComponentDetection.Detectors.Yarn;
using Microsoft.ComponentDetection.Detectors.Yarn.Parsers;
Expand Down Expand Up @@ -143,6 +144,9 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IYarnLockFileFactory, YarnLockFileFactory>();
services.AddSingleton<IComponentDetector, YarnLockComponentDetector>();

// Swift Package Manager
services.AddSingleton<IComponentDetector, SwiftResolvedComponentDetector>();

return services;
}
}
Loading
Loading