Skip to content

Commit 96a8a85

Browse files
committed
feat: SBOM output format
1 parent fb6b4a3 commit 96a8a85

File tree

10 files changed

+342
-8
lines changed

10 files changed

+342
-8
lines changed

Directory.Packages.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
<ItemGroup>
1111
<PackageVersion Include="CommandLineParser" Version="2.9.1"/>
1212
<PackageVersion Include="coverlet.msbuild" Version="3.2.0"/>
13+
<PackageVersion Include="CycloneDX.Core" Version="5.3.1"/>
14+
<PackageVersion Include="CycloneDX.Spdx.Interop" Version="5.3.1"/>
15+
<PackageVersion Include="CycloneDX.Utils" Version="5.3.1"/>
1316
<PackageVersion Include="Docker.DotNet" Version="3.125.12"/>
1417
<PackageVersion Include="FluentAssertions" Version="6.8.0"/>
1518
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9"/>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace Microsoft.ComponentDetection.Contracts.ArgumentSets
2+
{
3+
public enum ManifestFileFormat
4+
{
5+
ComponentDetection,
6+
CycloneDx,
7+
Spdx,
8+
}
9+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using CycloneDX.Json;
6+
using CycloneDX.Models;
7+
using CycloneDX.Utils;
8+
using Microsoft.ComponentDetection.Contracts.BcdeModels;
9+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
10+
11+
namespace Microsoft.ComponentDetection.Contracts.Mappers
12+
{
13+
public static class CycloneDx
14+
{
15+
public static string ToCycloneDxString(this ScanResult scanResult) => Serializer.Serialize(scanResult.ToCycloneDx());
16+
17+
public static Bom ToCycloneDx(this ScanResult scanResult) => new Bom
18+
{
19+
SerialNumber = CycloneDXUtils.GenerateSerialNumber(),
20+
Metadata = new Metadata
21+
{
22+
Timestamp = DateTime.UtcNow,
23+
Tools = new List<Tool>
24+
{
25+
new Tool
26+
{
27+
Vendor = "Microsoft",
28+
Name = "Component Detection",
29+
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
30+
ExternalReferences = new List<ExternalReference>
31+
{
32+
new ExternalReference
33+
{
34+
Type = ExternalReference.ExternalReferenceType.Vcs,
35+
Url = "https://github.com/microsoft/component-detection",
36+
},
37+
},
38+
},
39+
},
40+
},
41+
Components = scanResult.ComponentsFound.ToComponents(),
42+
};
43+
44+
private static List<Component> ToComponents(this IEnumerable<ScannedComponent> scannedComponents) =>
45+
scannedComponents.Select(sc => sc.ToComponent()).ToList();
46+
47+
private static Component ToComponent(this ScannedComponent scannedComponent)
48+
{
49+
var component = new Component
50+
{
51+
Type = Component.Classification.Library,
52+
Name = scannedComponent.Component.PackageUrl.Name,
53+
Version = scannedComponent.Component.PackageUrl.Version,
54+
Purl = scannedComponent.Component.PackageUrl.ToString(),
55+
Properties = scannedComponent.GenerateProperties(),
56+
};
57+
58+
switch (scannedComponent.Component.Type)
59+
{
60+
case ComponentType.Cargo:
61+
var cargoComponent = (CargoComponent)scannedComponent.Component;
62+
break;
63+
case ComponentType.Conda:
64+
var condaComponent = (CondaComponent)scannedComponent.Component;
65+
component.ExternalReferences = new List<ExternalReference>
66+
{
67+
new ExternalReference
68+
{
69+
Type = ExternalReference.ExternalReferenceType.Other,
70+
Url = condaComponent.Url,
71+
},
72+
};
73+
break;
74+
case ComponentType.DockerImage:
75+
var dockerImageComponent = (DockerImageComponent)scannedComponent.Component;
76+
break;
77+
case ComponentType.Git:
78+
var gitComponent = (GitComponent)scannedComponent.Component;
79+
component.ExternalReferences = new List<ExternalReference>
80+
{
81+
new ExternalReference
82+
{
83+
Type = ExternalReference.ExternalReferenceType.Vcs,
84+
Url = gitComponent.RepositoryUrl.ToString(),
85+
},
86+
};
87+
break;
88+
case ComponentType.Go:
89+
var goComponent = (GoComponent)scannedComponent.Component;
90+
component.Hashes = new List<Hash>
91+
{
92+
new Hash
93+
{
94+
Alg = Hash.HashAlgorithm.SHA_256,
95+
Content = goComponent.Hash,
96+
},
97+
};
98+
break;
99+
case ComponentType.Linux:
100+
var linuxComponent = (LinuxComponent)scannedComponent.Component;
101+
component.Properties.AddRange(
102+
new List<Property>
103+
{
104+
new Property
105+
{
106+
Name = "distribution", Value = linuxComponent.Distribution,
107+
},
108+
new Property
109+
{
110+
Name = "release", Value = linuxComponent.Release,
111+
},
112+
});
113+
break;
114+
case ComponentType.Maven:
115+
var mavenComponent = (MavenComponent)scannedComponent.Component;
116+
break;
117+
case ComponentType.Npm:
118+
var npmComponent = (NpmComponent)scannedComponent.Component;
119+
if (npmComponent.Author?.Name != null || npmComponent.Author?.Email != null)
120+
{
121+
component.Author = $"{npmComponent.Author?.Name} <{npmComponent.Author?.Email}>";
122+
}
123+
124+
if (npmComponent.Hash != null)
125+
{
126+
component.Hashes = new List<Hash>
127+
{
128+
new Hash
129+
{
130+
Alg = Hash.HashAlgorithm.Null, // algorithm is included in hash
131+
Content = npmComponent.Hash,
132+
},
133+
};
134+
}
135+
136+
break;
137+
case ComponentType.NuGet:
138+
component.Author = string.Join(",", ((NuGetComponent)scannedComponent.Component).Authors);
139+
break;
140+
case ComponentType.Other:
141+
var otherComponent = (OtherComponent)scannedComponent.Component;
142+
component.Hashes = new List<Hash>
143+
{
144+
new Hash
145+
{
146+
Alg = Hash.HashAlgorithm.Null,
147+
Content = otherComponent.Hash,
148+
},
149+
};
150+
component.ExternalReferences = new List<ExternalReference>
151+
{
152+
new ExternalReference
153+
{
154+
Type = ExternalReference.ExternalReferenceType.Distribution,
155+
Url = otherComponent.DownloadUrl.ToString(),
156+
},
157+
};
158+
break;
159+
case ComponentType.Pip:
160+
var pipComponent = (PipComponent)scannedComponent.Component;
161+
break;
162+
case ComponentType.Pod:
163+
var podComponent = (PodComponent)scannedComponent.Component;
164+
component.ExternalReferences = new List<ExternalReference>
165+
{
166+
new ExternalReference
167+
{
168+
Type = ExternalReference.ExternalReferenceType.Vcs,
169+
Url = podComponent.SpecRepo,
170+
},
171+
};
172+
break;
173+
case ComponentType.RubyGems:
174+
var rubyGemsComponent = (RubyGemsComponent)scannedComponent.Component;
175+
component.ExternalReferences = new List<ExternalReference>
176+
{
177+
new ExternalReference
178+
{
179+
Type = ExternalReference.ExternalReferenceType.Vcs,
180+
Url = rubyGemsComponent.Source,
181+
},
182+
};
183+
break;
184+
case ComponentType.Spdx:
185+
var spdxComponent = (SpdxComponent)scannedComponent.Component;
186+
break;
187+
case ComponentType.Vcpkg:
188+
var vcpkgComponent = (VcpkgComponent)scannedComponent.Component;
189+
component.Description = vcpkgComponent.Description;
190+
component.ExternalReferences = new List<ExternalReference>
191+
{
192+
new ExternalReference
193+
{
194+
Type = ExternalReference.ExternalReferenceType.Distribution,
195+
Url = vcpkgComponent.DownloadLocation,
196+
},
197+
};
198+
break;
199+
}
200+
201+
return component;
202+
}
203+
204+
private static List<Property> GenerateProperties(this ScannedComponent scannedComponent)
205+
{
206+
var properties = new List<Property>();
207+
properties.AddRange(scannedComponent.LocationsFoundAt.Select((locationFoundAt, i) => new Property
208+
{
209+
Name = $"component-detection:location:{i}",
210+
Value = locationFoundAt,
211+
}));
212+
return properties;
213+
}
214+
}
215+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using Microsoft.ComponentDetection.Contracts.BcdeModels;
2+
3+
namespace Microsoft.ComponentDetection.Contracts.Mappers
4+
{
5+
using CycloneDX.Spdx.Interop;
6+
using CycloneDX.Spdx.Models.v2_2;
7+
using CycloneDX.Spdx.Serialization;
8+
9+
public static class Spdx22
10+
{
11+
public static string ToSpdxString(this ScanResult scanResult) => JsonSerializer.Serialize(scanResult.ToSpdx());
12+
13+
public static SpdxDocument ToSpdx(this ScanResult scanResult) => scanResult.ToCycloneDx().ToSpdx();
14+
}
15+
}

src/Microsoft.ComponentDetection.Contracts/Microsoft.ComponentDetection.Contracts.csproj

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,15 @@
55
</PropertyGroup>
66

77
<ItemGroup>
8-
<PackageReference Include="Newtonsoft.Json"/>
9-
<PackageReference Include="packageurl-dotnet"/>
10-
<PackageReference Include="System.Composition.AttributedModel"/>
11-
<PackageReference Include="System.Memory"/>
12-
<PackageReference Include="System.Reactive"/>
13-
<PackageReference Include="System.Threading.Tasks.Dataflow"/>
8+
<PackageReference Include="CycloneDX.Core" />
9+
<PackageReference Include="CycloneDX.Spdx.Interop" />
10+
<PackageReference Include="CycloneDX.Utils" />
11+
<PackageReference Include="Newtonsoft.Json" />
12+
<PackageReference Include="packageurl-dotnet" />
13+
<PackageReference Include="System.Composition.AttributedModel" />
14+
<PackageReference Include="System.Memory" />
15+
<PackageReference Include="System.Reactive" />
16+
<PackageReference Include="System.Threading.Tasks.Dataflow" />
1417
</ItemGroup>
1518

1619
</Project>

src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/BcdeArguments.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Composition;
33
using System.IO;
44
using CommandLine;
5+
using Microsoft.ComponentDetection.Contracts.ArgumentSets;
56
using Newtonsoft.Json;
67

78
namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
@@ -37,6 +38,9 @@ public class BcdeArguments : BaseArguments, IDetectionArguments
3738
[Option("DetectorsFilter", Separator = ',', Required = false, HelpText = "A comma separated list with the identifiers of the specific detectors to be used. This is meant to be used for testing purposes only.")]
3839
public IEnumerable<string> DetectorsFilter { get; set; }
3940

41+
[Option("ManifestFileFormat", Required = false)]
42+
public ManifestFileFormat ManifestFileFormat { get; set; }
43+
4044
[JsonIgnore]
4145
[Option("ManifestFile", Required = false, HelpText = "The file to write scan results to.")]
4246
public FileInfo ManifestFile { get; set; }

src/Microsoft.ComponentDetection.Orchestrator/ArgumentSets/IDetectionArguments.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Collections.Generic;
22
using System.IO;
3+
using Microsoft.ComponentDetection.Contracts.ArgumentSets;
34

45
namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
56
{
@@ -18,6 +19,8 @@ public interface IDetectionArguments : IScanArguments
1819
IEnumerable<string> DetectorCategories { get; set; }
1920

2021
IEnumerable<string> DetectorsFilter { get; set; }
22+
23+
ManifestFileFormat ManifestFileFormat { get; set; }
2124

2225
FileInfo ManifestFile { get; set; }
2326

src/Microsoft.ComponentDetection.Orchestrator/Services/BcdeScanCommandService.cs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
using System.IO;
33
using System.Threading.Tasks;
44
using Microsoft.ComponentDetection.Common;
5+
using Microsoft.ComponentDetection.Contracts.ArgumentSets;
56
using Microsoft.ComponentDetection.Contracts.BcdeModels;
7+
using Microsoft.ComponentDetection.Contracts.Mappers;
68
using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
79
using Newtonsoft.Json;
810

@@ -46,13 +48,21 @@ private void WriteComponentManifest(IDetectionArguments detectionArguments, Scan
4648
this.Logger.LogInfo($"Scan Manifest file: {this.FileWritingService.ResolveFilePath(ManifestRelativePath)}");
4749
}
4850

51+
var outputText = detectionArguments.ManifestFileFormat switch
52+
{
53+
ManifestFileFormat.ComponentDetection => JsonConvert.SerializeObject(scanResult, Formatting.Indented),
54+
ManifestFileFormat.CycloneDx => scanResult.ToCycloneDxString(),
55+
ManifestFileFormat.Spdx => scanResult.ToSpdxString(),
56+
_ => null
57+
};
58+
4959
if (userRequestedManifestPath == null)
5060
{
51-
this.FileWritingService.AppendToFile(ManifestRelativePath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
61+
this.FileWritingService.WriteFile(ManifestRelativePath, outputText);
5262
}
5363
else
5464
{
55-
this.FileWritingService.WriteFile(userRequestedManifestPath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
65+
this.FileWritingService.WriteFile(userRequestedManifestPath, outputText);
5666
}
5767
}
5868
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Collections.Generic;
2+
using FluentAssertions;
3+
using Microsoft.ComponentDetection.Contracts.BcdeModels;
4+
using Microsoft.ComponentDetection.Contracts.Mappers;
5+
using Microsoft.ComponentDetection.Contracts.TypedComponent;
6+
using Microsoft.VisualStudio.TestTools.UnitTesting;
7+
8+
namespace Microsoft.ComponentDetection.Contracts.Tests.Mappers
9+
{
10+
[TestClass]
11+
public class CycloneDx
12+
{
13+
[TestMethod]
14+
public void ToCycloneDx_HappyPath()
15+
{
16+
var scanResult = new ScanResult
17+
{
18+
ComponentsFound = new List<ScannedComponent>
19+
{
20+
new ScannedComponent
21+
{
22+
Component = new NpmComponent("lodash", "1.2.3"),
23+
LocationsFoundAt = new[]
24+
{
25+
"/src/lodash.js",
26+
},
27+
},
28+
},
29+
};
30+
31+
var result = scanResult.ToCycloneDxString();
32+
33+
result.Should().NotBeEmpty();
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)