Skip to content

Commit 7210055

Browse files
committed
Initial implementation of cargo-sbom detector
1 parent f25b012 commit 7210055

File tree

4 files changed

+620
-0
lines changed

4 files changed

+620
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Schema for Cargo SBOM pre-cursor files (*.cargo-sbom.json)
2+
3+
namespace Microsoft.ComponentDetection.Detectors.Rust.Sbom.Contracts;
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
#pragma warning disable SA1402
11+
#pragma warning disable SA1204
12+
13+
/// <summary>
14+
/// Type of dependency.
15+
/// </summary>
16+
public enum SbomKind
17+
{
18+
/// <summary>
19+
/// A dependency linked to the artifact produced by this crate.
20+
/// </summary>
21+
Normal,
22+
23+
/// <summary>
24+
/// A compile-time dependency used to build this crate.
25+
/// </summary>
26+
Build,
27+
28+
/// <summary>
29+
/// An unexpected dependency kind.
30+
/// </summary>
31+
Unknown,
32+
}
33+
34+
/// <summary>
35+
/// Represents the Cargo Software Bill of Materials (SBOM).
36+
/// </summary>
37+
public class CargoSbom
38+
{
39+
/// <summary>
40+
/// Gets or sets the version of the SBOM.
41+
/// </summary>
42+
public int Version { get; set; }
43+
44+
/// <summary>
45+
/// Gets or sets the index of the root crate.
46+
/// </summary>
47+
public int Root { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the list of crates.
51+
/// </summary>
52+
public List<SbomCrate> Crates { get; set; }
53+
54+
/// <summary>
55+
/// Gets or sets the information about rustc used to perform the compilation.
56+
/// </summary>
57+
public Rustc Rustc { get; set; }
58+
59+
/// <summary>
60+
/// Deserialize from JSON.
61+
/// </summary>
62+
/// <returns>Cargo SBOM.</returns>
63+
public static CargoSbom FromJson(string json) => JsonSerializer.Deserialize<CargoSbom>(json, Converter.Settings);
64+
}
65+
66+
/// <summary>
67+
/// Represents a crate in the SBOM.
68+
/// </summary>
69+
public class SbomCrate
70+
{
71+
/// <summary>
72+
/// Gets or sets the Cargo Package ID specification.
73+
/// </summary>
74+
public string Id { get; set; }
75+
76+
/// <summary>
77+
/// Gets or sets the enabled feature flags.
78+
/// </summary>
79+
public List<string> Features { get; set; }
80+
81+
/// <summary>
82+
/// Gets or sets the enabled cfg attributes set by build scripts.
83+
/// </summary>
84+
public List<string> Cfgs { get; set; }
85+
86+
/// <summary>
87+
/// Gets or sets the dependencies for this crate.
88+
/// </summary>
89+
public List<SbomDependency> Dependencies { get; set; }
90+
}
91+
92+
/// <summary>
93+
/// Represents a dependency of a crate.
94+
/// </summary>
95+
public class SbomDependency
96+
{
97+
/// <summary>
98+
/// Gets or sets the index into the crates array.
99+
/// </summary>
100+
public int Index { get; set; }
101+
102+
/// <summary>
103+
/// Gets or sets the kind of dependency.
104+
/// </summary>
105+
public SbomKind Kind { get; set; }
106+
}
107+
108+
/// <summary>
109+
/// Represents information about rustc used to perform the compilation.
110+
/// </summary>
111+
public class Rustc
112+
{
113+
/// <summary>
114+
/// Gets or sets the compiler version.
115+
/// </summary>
116+
public string Version { get; set; }
117+
118+
/// <summary>
119+
/// Gets or sets the compiler wrapper.
120+
/// </summary>
121+
public string Wrapper { get; set; }
122+
123+
/// <summary>
124+
/// Gets or sets the compiler workspace wrapper.
125+
/// </summary>
126+
public string WorkspaceWrapper { get; set; }
127+
128+
/// <summary>
129+
/// Gets or sets the commit hash for rustc.
130+
/// </summary>
131+
public string CommitHash { get; set; }
132+
133+
/// <summary>
134+
/// Gets or sets the host target triple.
135+
/// </summary>
136+
[JsonPropertyName("host")]
137+
public string Host { get; set; }
138+
139+
/// <summary>
140+
/// Gets or sets the verbose version string.
141+
/// </summary>
142+
public string VerboseVersion { get; set; }
143+
}
144+
145+
/// <summary>
146+
/// Deserializes SbomKind.
147+
/// </summary>
148+
internal class SbomKindConverter : JsonConverter<SbomKind>
149+
{
150+
public static readonly SbomKindConverter Singleton = new SbomKindConverter();
151+
152+
public override bool CanConvert(Type t) => t == typeof(SbomKind);
153+
154+
public override SbomKind Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
155+
{
156+
var value = reader.GetString();
157+
return value switch
158+
{
159+
"build" => SbomKind.Build,
160+
"normal" => SbomKind.Normal,
161+
_ => SbomKind.Unknown,
162+
};
163+
}
164+
165+
public override void Write(Utf8JsonWriter writer, SbomKind value, JsonSerializerOptions options) => throw new NotImplementedException();
166+
}
167+
168+
/// <summary>
169+
/// Json converter settings.
170+
/// </summary>
171+
internal static class Converter
172+
{
173+
public static readonly JsonSerializerOptions Settings = new(JsonSerializerDefaults.General)
174+
{
175+
Converters = { SbomKindConverter.Singleton },
176+
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
177+
};
178+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
namespace Microsoft.ComponentDetection.Detectors.Rust;
2+
3+
using System;
4+
using System.Collections.Generic;
5+
using System.IO;
6+
using System.Text.RegularExpressions;
7+
using System.Threading;
8+
using System.Threading.Tasks;
9+
using Microsoft.ComponentDetection.Contracts;
10+
using Microsoft.ComponentDetection.Contracts.Internal;
11+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
12+
using Microsoft.ComponentDetection.Detectors.Rust.Sbom.Contracts;
13+
using Microsoft.Extensions.Logging;
14+
15+
public class RustSbomDetector : FileComponentDetector, IDefaultOffComponentDetector
16+
{
17+
private const string CargoSbomSearchPattern = "*.cargo-sbom.json";
18+
private const string CratesIoSource = "registry+https://github.com/rust-lang/crates.io-index";
19+
20+
/// <summary>
21+
/// Cargo Package ID: source#name@version
22+
/// https://rustwiki.org/en/cargo/reference/pkgid-spec.html.
23+
/// </summary>
24+
private static readonly Regex CargoPackageIdRegex = new Regex(
25+
@"^(?<source>[^#]*)#?(?<name>[\w\-]*)[@#]?(?<version>\d[\S]*)?$",
26+
RegexOptions.Compiled);
27+
28+
public RustSbomDetector(
29+
IComponentStreamEnumerableFactory componentStreamEnumerableFactory,
30+
IObservableDirectoryWalkerFactory walkerFactory,
31+
ILogger<RustSbomDetector> logger)
32+
{
33+
this.ComponentStreamEnumerableFactory = componentStreamEnumerableFactory;
34+
this.Scanner = walkerFactory;
35+
this.Logger = logger;
36+
}
37+
38+
public override string Id => "RustSbom";
39+
40+
public override IList<string> SearchPatterns => [CargoSbomSearchPattern];
41+
42+
public override IEnumerable<ComponentType> SupportedComponentTypes => [ComponentType.Cargo];
43+
44+
public override int Version { get; } = 1;
45+
46+
public override IEnumerable<string> Categories => ["Rust"];
47+
48+
private static bool ParsePackageIdSpec(string dependency, out CargoComponent component)
49+
{
50+
var match = CargoPackageIdRegex.Match(dependency);
51+
var name = match.Groups["name"].Value;
52+
var version = match.Groups["version"].Value;
53+
var source = match.Groups["source"].Value;
54+
55+
if (!match.Success)
56+
{
57+
component = null;
58+
return false;
59+
}
60+
61+
if (string.IsNullOrWhiteSpace(name))
62+
{
63+
name = source[(source.LastIndexOf('/') + 1)..];
64+
}
65+
66+
if (string.IsNullOrWhiteSpace(source))
67+
{
68+
source = null;
69+
}
70+
71+
component = new CargoComponent(name, version, source: source);
72+
return true;
73+
}
74+
75+
protected override async Task OnFileFoundAsync(ProcessRequest processRequest, IDictionary<string, string> detectorArgs, CancellationToken cancellationToken = default)
76+
{
77+
var singleFileComponentRecorder = processRequest.SingleFileComponentRecorder;
78+
var components = processRequest.ComponentStream;
79+
var reader = new StreamReader(components.Stream);
80+
var cargoSbom = CargoSbom.FromJson(await reader.ReadToEndAsync(cancellationToken));
81+
this.RecordLockfileVersion(cargoSbom.Version);
82+
this.ProcessCargoSbom(cargoSbom, singleFileComponentRecorder, components);
83+
}
84+
85+
private void ProcessDependency(CargoSbom sbom, SbomCrate package, ISingleFileComponentRecorder recorder, IComponentStream components, HashSet<int> visitedNodes, CargoComponent parent = null, int depth = 0)
86+
{
87+
foreach (var dependency in package.Dependencies)
88+
{
89+
var dep = sbom.Crates[dependency.Index];
90+
var parentComponent = parent;
91+
if (ParsePackageIdSpec(dep.Id, out var component))
92+
{
93+
if (component.Source == CratesIoSource)
94+
{
95+
parentComponent = component;
96+
recorder.RegisterUsage(new DetectedComponent(component), isExplicitReferencedDependency: depth == 0, parent?.Id, isDevelopmentDependency: false);
97+
}
98+
}
99+
else
100+
{
101+
this.Logger.LogError(null, "Failed to parse Cargo PackageIdSpec '{Id}' in '{Location}'", dep.Id, components.Location);
102+
recorder.RegisterPackageParseFailure(dep.Id);
103+
}
104+
105+
if (visitedNodes.Add(dependency.Index))
106+
{
107+
// Skip processing already processed nodes
108+
this.ProcessDependency(sbom, dep, recorder, components, visitedNodes, parentComponent, depth + 1);
109+
}
110+
}
111+
}
112+
113+
private void ProcessCargoSbom(CargoSbom sbom, ISingleFileComponentRecorder recorder, IComponentStream components)
114+
{
115+
try
116+
{
117+
var visitedNodes = new HashSet<int>();
118+
this.ProcessDependency(sbom, sbom.Crates[sbom.Root], recorder, components, visitedNodes);
119+
}
120+
catch (Exception e)
121+
{
122+
// If something went wrong, just ignore the file
123+
this.Logger.LogError(e, "Failed to process Cargo SBOM file '{FileLocation}'", components.Location);
124+
}
125+
}
126+
}

src/Microsoft.ComponentDetection.Orchestrator/Extensions/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ public static IServiceCollection AddComponentDetection(this IServiceCollection s
137137
// Rust
138138
services.AddSingleton<IComponentDetector, RustCrateDetector>();
139139
services.AddSingleton<IComponentDetector, RustCliDetector>();
140+
services.AddSingleton<IComponentDetector, RustSbomDetector>();
140141

141142
// SPDX
142143
services.AddSingleton<IComponentDetector, Spdx22ComponentDetector>();

0 commit comments

Comments
 (0)