Skip to content

Commit

Permalink
feat: SBOM output format
Browse files Browse the repository at this point in the history
  • Loading branch information
JamieMagee committed Jan 4, 2023
1 parent fb6b4a3 commit 96a8a85
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 8 deletions.
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
<ItemGroup>
<PackageVersion Include="CommandLineParser" Version="2.9.1"/>
<PackageVersion Include="coverlet.msbuild" Version="3.2.0"/>
<PackageVersion Include="CycloneDX.Core" Version="5.3.1"/>
<PackageVersion Include="CycloneDX.Spdx.Interop" Version="5.3.1"/>
<PackageVersion Include="CycloneDX.Utils" Version="5.3.1"/>
<PackageVersion Include="Docker.DotNet" Version="3.125.12"/>
<PackageVersion Include="FluentAssertions" Version="6.8.0"/>
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="5.2.9"/>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Microsoft.ComponentDetection.Contracts.ArgumentSets
{
public enum ManifestFileFormat
{
ComponentDetection,
CycloneDx,
Spdx,
}
}
215 changes: 215 additions & 0 deletions src/Microsoft.ComponentDetection.Contracts/Mappers/CycloneDx.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using CycloneDX.Json;
using CycloneDX.Models;
using CycloneDX.Utils;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.TypedComponent;

namespace Microsoft.ComponentDetection.Contracts.Mappers
{
public static class CycloneDx
{
public static string ToCycloneDxString(this ScanResult scanResult) => Serializer.Serialize(scanResult.ToCycloneDx());

public static Bom ToCycloneDx(this ScanResult scanResult) => new Bom
{
SerialNumber = CycloneDXUtils.GenerateSerialNumber(),
Metadata = new Metadata
{
Timestamp = DateTime.UtcNow,
Tools = new List<Tool>
{
new Tool
{
Vendor = "Microsoft",
Name = "Component Detection",
Version = Assembly.GetExecutingAssembly().GetName().Version.ToString(),
ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Vcs,
Url = "https://github.com/microsoft/component-detection",
},
},
},
},
},
Components = scanResult.ComponentsFound.ToComponents(),
};

private static List<Component> ToComponents(this IEnumerable<ScannedComponent> scannedComponents) =>
scannedComponents.Select(sc => sc.ToComponent()).ToList();

private static Component ToComponent(this ScannedComponent scannedComponent)
{
var component = new Component
{
Type = Component.Classification.Library,
Name = scannedComponent.Component.PackageUrl.Name,
Version = scannedComponent.Component.PackageUrl.Version,
Purl = scannedComponent.Component.PackageUrl.ToString(),
Properties = scannedComponent.GenerateProperties(),
};

switch (scannedComponent.Component.Type)
{
case ComponentType.Cargo:
var cargoComponent = (CargoComponent)scannedComponent.Component;
break;
case ComponentType.Conda:
var condaComponent = (CondaComponent)scannedComponent.Component;
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Other,
Url = condaComponent.Url,
},
};
break;
case ComponentType.DockerImage:
var dockerImageComponent = (DockerImageComponent)scannedComponent.Component;
break;
case ComponentType.Git:
var gitComponent = (GitComponent)scannedComponent.Component;
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Vcs,
Url = gitComponent.RepositoryUrl.ToString(),
},
};
break;
case ComponentType.Go:
var goComponent = (GoComponent)scannedComponent.Component;
component.Hashes = new List<Hash>
{
new Hash
{
Alg = Hash.HashAlgorithm.SHA_256,
Content = goComponent.Hash,
},
};
break;
case ComponentType.Linux:
var linuxComponent = (LinuxComponent)scannedComponent.Component;
component.Properties.AddRange(
new List<Property>
{
new Property
{
Name = "distribution", Value = linuxComponent.Distribution,
},
new Property
{
Name = "release", Value = linuxComponent.Release,
},
});
break;
case ComponentType.Maven:
var mavenComponent = (MavenComponent)scannedComponent.Component;
break;
case ComponentType.Npm:
var npmComponent = (NpmComponent)scannedComponent.Component;
if (npmComponent.Author?.Name != null || npmComponent.Author?.Email != null)
{
component.Author = $"{npmComponent.Author?.Name} <{npmComponent.Author?.Email}>";
}

if (npmComponent.Hash != null)
{
component.Hashes = new List<Hash>
{
new Hash
{
Alg = Hash.HashAlgorithm.Null, // algorithm is included in hash
Content = npmComponent.Hash,
},
};
}

break;
case ComponentType.NuGet:
component.Author = string.Join(",", ((NuGetComponent)scannedComponent.Component).Authors);
break;
case ComponentType.Other:
var otherComponent = (OtherComponent)scannedComponent.Component;
component.Hashes = new List<Hash>
{
new Hash
{
Alg = Hash.HashAlgorithm.Null,
Content = otherComponent.Hash,
},
};
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Distribution,
Url = otherComponent.DownloadUrl.ToString(),
},
};
break;
case ComponentType.Pip:
var pipComponent = (PipComponent)scannedComponent.Component;
break;
case ComponentType.Pod:
var podComponent = (PodComponent)scannedComponent.Component;
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Vcs,
Url = podComponent.SpecRepo,
},
};
break;
case ComponentType.RubyGems:
var rubyGemsComponent = (RubyGemsComponent)scannedComponent.Component;
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Vcs,
Url = rubyGemsComponent.Source,
},
};
break;
case ComponentType.Spdx:
var spdxComponent = (SpdxComponent)scannedComponent.Component;
break;
case ComponentType.Vcpkg:
var vcpkgComponent = (VcpkgComponent)scannedComponent.Component;
component.Description = vcpkgComponent.Description;
component.ExternalReferences = new List<ExternalReference>
{
new ExternalReference
{
Type = ExternalReference.ExternalReferenceType.Distribution,
Url = vcpkgComponent.DownloadLocation,
},
};
break;
}

return component;
}

private static List<Property> GenerateProperties(this ScannedComponent scannedComponent)
{
var properties = new List<Property>();
properties.AddRange(scannedComponent.LocationsFoundAt.Select((locationFoundAt, i) => new Property
{
Name = $"component-detection:location:{i}",
Value = locationFoundAt,
}));
return properties;
}
}
}
15 changes: 15 additions & 0 deletions src/Microsoft.ComponentDetection.Contracts/Mappers/Spdx22.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Microsoft.ComponentDetection.Contracts.BcdeModels;

namespace Microsoft.ComponentDetection.Contracts.Mappers
{
using CycloneDX.Spdx.Interop;
using CycloneDX.Spdx.Models.v2_2;
using CycloneDX.Spdx.Serialization;

public static class Spdx22
{
public static string ToSpdxString(this ScanResult scanResult) => JsonSerializer.Serialize(scanResult.ToSpdx());

public static SpdxDocument ToSpdx(this ScanResult scanResult) => scanResult.ToCycloneDx().ToSpdx();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Newtonsoft.Json"/>
<PackageReference Include="packageurl-dotnet"/>
<PackageReference Include="System.Composition.AttributedModel"/>
<PackageReference Include="System.Memory"/>
<PackageReference Include="System.Reactive"/>
<PackageReference Include="System.Threading.Tasks.Dataflow"/>
<PackageReference Include="CycloneDX.Core" />
<PackageReference Include="CycloneDX.Spdx.Interop" />
<PackageReference Include="CycloneDX.Utils" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="packageurl-dotnet" />
<PackageReference Include="System.Composition.AttributedModel" />
<PackageReference Include="System.Memory" />
<PackageReference Include="System.Reactive" />
<PackageReference Include="System.Threading.Tasks.Dataflow" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Composition;
using System.IO;
using CommandLine;
using Microsoft.ComponentDetection.Contracts.ArgumentSets;
using Newtonsoft.Json;

namespace Microsoft.ComponentDetection.Orchestrator.ArgumentSets
Expand Down Expand Up @@ -37,6 +38,9 @@ public class BcdeArguments : BaseArguments, IDetectionArguments
[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.")]
public IEnumerable<string> DetectorsFilter { get; set; }

[Option("ManifestFileFormat", Required = false)]
public ManifestFileFormat ManifestFileFormat { get; set; }

[JsonIgnore]
[Option("ManifestFile", Required = false, HelpText = "The file to write scan results to.")]
public FileInfo ManifestFile { get; set; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.ComponentDetection.Contracts.ArgumentSets;

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

IEnumerable<string> DetectorsFilter { get; set; }

ManifestFileFormat ManifestFileFormat { get; set; }

FileInfo ManifestFile { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.IO;
using System.Threading.Tasks;
using Microsoft.ComponentDetection.Common;
using Microsoft.ComponentDetection.Contracts.ArgumentSets;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.Mappers;
using Microsoft.ComponentDetection.Orchestrator.ArgumentSets;
using Newtonsoft.Json;

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

var outputText = detectionArguments.ManifestFileFormat switch
{
ManifestFileFormat.ComponentDetection => JsonConvert.SerializeObject(scanResult, Formatting.Indented),
ManifestFileFormat.CycloneDx => scanResult.ToCycloneDxString(),
ManifestFileFormat.Spdx => scanResult.ToSpdxString(),
_ => null
};

if (userRequestedManifestPath == null)
{
this.FileWritingService.AppendToFile(ManifestRelativePath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
this.FileWritingService.WriteFile(ManifestRelativePath, outputText);
}
else
{
this.FileWritingService.WriteFile(userRequestedManifestPath, JsonConvert.SerializeObject(scanResult, Formatting.Indented));
this.FileWritingService.WriteFile(userRequestedManifestPath, outputText);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System.Collections.Generic;
using FluentAssertions;
using Microsoft.ComponentDetection.Contracts.BcdeModels;
using Microsoft.ComponentDetection.Contracts.Mappers;
using Microsoft.ComponentDetection.Contracts.TypedComponent;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Microsoft.ComponentDetection.Contracts.Tests.Mappers
{
[TestClass]
public class CycloneDx
{
[TestMethod]
public void ToCycloneDx_HappyPath()
{
var scanResult = new ScanResult
{
ComponentsFound = new List<ScannedComponent>
{
new ScannedComponent
{
Component = new NpmComponent("lodash", "1.2.3"),
LocationsFoundAt = new[]
{
"/src/lodash.js",
},
},
},
};

var result = scanResult.ToCycloneDxString();

result.Should().NotBeEmpty();
}
}
}
Loading

0 comments on commit 96a8a85

Please sign in to comment.