Skip to content

Initial implementation of cargo-sbom detector #1387

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Schema for Cargo SBOM pre-cursor files (*.cargo-sbom.json)

namespace Microsoft.ComponentDetection.Detectors.Rust.Sbom.Contracts;

using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

#pragma warning disable SA1402
#pragma warning disable SA1204

/// <summary>
/// Type of dependency.
/// </summary>
public enum SbomKind
{
/// <summary>
/// A dependency linked to the artifact produced by this crate.
/// </summary>
Normal,

/// <summary>
/// A compile-time dependency used to build this crate.
/// </summary>
Build,

/// <summary>
/// An unexpected dependency kind.
/// </summary>
Unknown,
}

/// <summary>
/// Represents the Cargo Software Bill of Materials (SBOM).
/// </summary>
public class CargoSbom
{
/// <summary>
/// Gets or sets the version of the SBOM.
/// </summary>
public int Version { get; set; }

/// <summary>
/// Gets or sets the index of the root crate.
/// </summary>
public int Root { get; set; }

/// <summary>
/// Gets or sets the list of crates.
/// </summary>
public List<SbomCrate> Crates { get; set; }

/// <summary>
/// Gets or sets the information about rustc used to perform the compilation.
/// </summary>
public Rustc Rustc { get; set; }

/// <summary>
/// Deserialize from JSON.
/// </summary>
/// <returns>Cargo SBOM.</returns>
public static CargoSbom FromJson(string json) => JsonSerializer.Deserialize<CargoSbom>(json, Converter.Settings);
}

/// <summary>
/// Represents a crate in the SBOM.
/// </summary>
public class SbomCrate
{
/// <summary>
/// Gets or sets the Cargo Package ID specification.
/// </summary>
public string Id { get; set; }

/// <summary>
/// Gets or sets the enabled feature flags.
/// </summary>
public List<string> Features { get; set; }

/// <summary>
/// Gets or sets the enabled cfg attributes set by build scripts.
/// </summary>
public List<string> Cfgs { get; set; }

Check warning on line 84 in src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs#L84

Added line #L84 was not covered by tests

/// <summary>
/// Gets or sets the dependencies for this crate.
/// </summary>
public List<SbomDependency> Dependencies { get; set; }
}

/// <summary>
/// Represents a dependency of a crate.
/// </summary>
public class SbomDependency
{
/// <summary>
/// Gets or sets the index into the crates array.
/// </summary>
public int Index { get; set; }

/// <summary>
/// Gets or sets the kind of dependency.
/// </summary>
public SbomKind Kind { get; set; }
}

/// <summary>
/// Represents information about rustc used to perform the compilation.
/// </summary>
public class Rustc
{
/// <summary>
/// Gets or sets the compiler version.
/// </summary>
public string Version { get; set; }

/// <summary>
/// Gets or sets the compiler wrapper.
/// </summary>
public string Wrapper { get; set; }

/// <summary>
/// Gets or sets the compiler workspace wrapper.
/// </summary>
public string WorkspaceWrapper { get; set; }

/// <summary>
/// Gets or sets the commit hash for rustc.
/// </summary>
public string CommitHash { get; set; }

/// <summary>
/// Gets or sets the host target triple.
/// </summary>
[JsonPropertyName("host")]
public string Host { get; set; }

/// <summary>
/// Gets or sets the verbose version string.
/// </summary>
public string VerboseVersion { get; set; }
}

/// <summary>
/// Deserializes SbomKind.
/// </summary>
internal class SbomKindConverter : JsonConverter<SbomKind>
{
public static readonly SbomKindConverter Singleton = new SbomKindConverter();

public override bool CanConvert(Type t) => t == typeof(SbomKind);

public override SbomKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var value = reader.GetString();
return value switch
{
"build" => SbomKind.Build,
"normal" => SbomKind.Normal,
_ => SbomKind.Unknown,

Check warning on line 161 in src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs#L161

Added line #L161 was not covered by tests
};
}

public override void Write(Utf8JsonWriter writer, SbomKind value, JsonSerializerOptions options) => throw new NotImplementedException();

Check warning on line 165 in src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/Contracts/CargoSbom.cs#L165

Added line #L165 was not covered by tests
}

/// <summary>
/// Json converter settings.
/// </summary>
internal static class Converter
{
public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)
{
Converters = { SbomKindConverter.Singleton },
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
}
126 changes: 126 additions & 0 deletions src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
namespace Microsoft.ComponentDetection.Detectors.Rust;

using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Contracts.Internal;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.ComponentDetection.Detectors.Rust.Sbom.Contracts;
using Microsoft.Extensions.Logging;

public class RustSbomDetector : FileComponentDetector, IDefaultOffComponentDetector
{
private const string CargoSbomSearchPattern = "*.cargo-sbom.json";
private const string CratesIoSource = "registry+https://github.com/rust-lang/crates.io-index";

/// <summary>
/// Cargo Package ID: source#name@version
/// https://rustwiki.org/en/cargo/reference/pkgid-spec.html.
/// </summary>
private static readonly Regex CargoPackageIdRegex = new Regex(
@"^(?<source>[^#]*)#?(?<name>[\w\-]*)[@#]?(?<version>\d[\S]*)?$",
RegexOptions.Compiled);

public RustSbomDetector(
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
IObservableDirectoryWalkerFactory walkerFactory,
ILogger<RustSbomDetector> logger)
{
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
this.Scanner = walkerFactory;
this.Logger = logger;
}

public override string Id => "RustSbom";

public override IList<string> SearchPatterns => [CargoSbomSearchPattern];

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

public override int Version { get; } = 1;

public override IEnumerable<string> Categories => ["Rust"];

private static bool ParsePackageIdSpec(string dependency, out CargoComponent component)
{
var match = CargoPackageIdRegex.Match(dependency);
var name = match.Groups["name"].Value;
var version = match.Groups["version"].Value;
var source = match.Groups["source"].Value;

if (!match.Success)
{
component = null;
return false;

Check warning on line 58 in src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs#L56-L58

Added lines #L56 - L58 were not covered by tests
}

if (string.IsNullOrWhiteSpace(name))
{
name = source[(source.LastIndexOf('/') + 1)..];
}

if (string.IsNullOrWhiteSpace(source))
{
source = null;
}

Check warning on line 69 in src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs#L67-L69

Added lines #L67 - L69 were not covered by tests

component = new CargoComponent(name, version, source: source);
return true;
}

protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
{
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
var components = processRequest.ComponentStream;
var reader = new StreamReader(components.Stream);
var cargoSbom = CargoSbom.FromJson(await reader.ReadToEndAsync(cancellationToken));
this.RecordLockfileVersion(cargoSbom.Version);
this.ProcessCargoSbom(cargoSbom, singleFileComponentRecorder, components);
}

private void ProcessDependency(CargoSbom sbom, SbomCrate package, ISingleFileComponentRecorder recorder, IComponentStream components, HashSet<int> visitedNodes, CargoComponent parent = null, int depth = 0)
{
foreach (var dependency in package.Dependencies)
{
var dep = sbom.Crates[dependency.Index];
var parentComponent = parent;
if (ParsePackageIdSpec(dep.Id, out var component))
{
if (component.Source == CratesIoSource)
{
parentComponent = component;
recorder.RegisterUsage(new DetectedComponent(component), isExplicitReferencedDependency: depth == 0, parent?.Id, isDevelopmentDependency: false);
}
}
else
{
this.Logger.LogError(null, "Failed to parse Cargo PackageIdSpec '{Id}' in '{Location}'", dep.Id, components.Location);
recorder.RegisterPackageParseFailure(dep.Id);
}

Check warning on line 103 in src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs#L100-L103

Added lines #L100 - L103 were not covered by tests

if (visitedNodes.Add(dependency.Index))
{
// Skip processing already processed nodes
this.ProcessDependency(sbom, dep, recorder, components, visitedNodes, parentComponent, depth + 1);
}
}
}

private void ProcessCargoSbom(CargoSbom sbom, ISingleFileComponentRecorder recorder, IComponentStream components)
{
try
{
var visitedNodes = new HashSet<int>();
this.ProcessDependency(sbom, sbom.Crates[sbom.Root], recorder, components, visitedNodes);
}
catch (Exception e)
{

Check warning on line 121 in src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs#L120-L121

Added lines #L120 - L121 were not covered by tests
// If something went wrong, just ignore the file
this.Logger.LogError(e, "Failed to process Cargo SBOM file '{FileLocation}'", components.Location);
}

Check warning on line 124 in src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs

View check run for this annotation

Codecov / codecov/patch

src/Microsoft.ComponentDetection.Detectors/rust/RustSbomDetector.cs#L123-L124

Added lines #L123 - L124 were not covered by tests
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;

using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.Rust;

/// <summary>
/// Validating the Rust SBOM detector against the Rust CLI detector.
/// </summary>
public class RustSbomVsCliExperiment : IExperimentConfiguration
{
/// <inheritdoc />
public string Name => "RustSbomVsCliExperiment";

/// <inheritdoc/>
public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is RustCliDetector;

/// <inheritdoc/>
public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is RustSbomDetector;

/// <inheritdoc />
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Microsoft.ComponentDetection.Orchestrator.Experiments.Configs;

using Microsoft.ComponentDetection.Contracts;
using Microsoft.ComponentDetection.Detectors.Rust;

/// <summary>
/// Validating the Rust SBOM detector against the Rust crate detector.
/// </summary>
public class RustSbomVsCrateExperiment : IExperimentConfiguration
{
/// <inheritdoc />
public string Name => "RustSbomVsCrateExperiment";

/// <inheritdoc/>
public bool IsInControlGroup(IComponentDetector componentDetector) => componentDetector is RustCrateDetector;

/// <inheritdoc/>
public bool IsInExperimentGroup(IComponentDetector componentDetector) => componentDetector is RustSbomDetector;

/// <inheritdoc />
public bool ShouldRecord(IComponentDetector componentDetector, int numComponents) => true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
services.AddSingleton<IExperimentProcessor, DefaultExperimentProcessor>();
services.AddSingleton<IExperimentConfiguration, SimplePipExperiment>();
services.AddSingleton<IExperimentConfiguration, RustCliDetectorExperiment>();
services.AddSingleton<IExperimentConfiguration, RustSbomVsCliExperiment>();
services.AddSingleton<IExperimentConfiguration, RustSbomVsCrateExperiment>();
services.AddSingleton<IExperimentConfiguration, Go117DetectorExperiment>();
services.AddSingleton<IExperimentConfiguration, DotNetDetectorExperiment>();

Expand Down Expand Up @@ -137,6 +139,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
// Rust
services.AddSingleton<IComponentDetector, RustCrateDetector>();
services.AddSingleton<IComponentDetector, RustCliDetector>();
services.AddSingleton<IComponentDetector, RustSbomDetector>();

// SPDX
services.AddSingleton<IComponentDetector, Spdx22ComponentDetector>();
Expand Down
Loading
Loading