Skip to content

Commit

Permalink
Merge pull request #17 from patriksvensson/feature/cyclonedx-analyzer
Browse files Browse the repository at this point in the history
Add CycloneDX analyzer
  • Loading branch information
patriksvensson committed Mar 15, 2024
2 parents afaa6d6 + 3e81ddf commit 573806c
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 23 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Covenant requires all projects to have been built, and all dependencies to have
* .NET 5 to .NET 8
* .NET Core
* NPM
* CycloneDX BOM
* `*.cdx.xml` or `bom.xml`

## Installation

Expand Down
7 changes: 7 additions & 0 deletions src/Covenant.Core/Base64EncodedText.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ private Base64EncodedText(string decoded, string encoded)
Encoded = encoded ?? throw new ArgumentNullException(nameof(encoded));
}

public static Base64EncodedText FromEncoded(string encoding)
{
var bytes = Convert.FromBase64String(encoding);
var decoded = Encoding.UTF8.GetString(bytes);
return new Base64EncodedText(decoded, encoding);
}

public static Base64EncodedText? Encode(string? text)
{
if (text == null)
Expand Down
1 change: 1 addition & 0 deletions src/Covenant.Core/Model/BomComponentKind.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public enum BomComponentKind
{
Root,
Library,
Application,
}
9 changes: 9 additions & 0 deletions src/Covenant.Core/Model/BomHashAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@ public enum BomHashAlgorithm
SHA1,
SHA256,
SHA512,
SHA384,
SHA3_256,
SHA3_384,
SHA3_512,
BLAKE2b_256,
BLAKE2b_384,
BLAKE2b_512,
BLAKE3,
MD5,
}
2 changes: 1 addition & 1 deletion src/Covenant.CycloneDx/Covenant.CycloneDx.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="CycloneDX.Core" Version="5.4.0" />
<PackageReference Include="CycloneDX.Core" Version="6.0.0" />
</ItemGroup>

</Project>
20 changes: 14 additions & 6 deletions src/Covenant.CycloneDx/CycloneDxConverter.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using Bom = Covenant.Core.Model.Bom;

namespace Covenant.CycloneDx;

internal static class CycloneDxConverter
Expand All @@ -22,13 +24,18 @@ public static CycloneBom Convert(Bom bom, BomSerializerSettings settings)
Metadata = new CycloneMetadata
{
Component = ConvertComponent(root),
Tools = new List<CycloneTool>
Tools = new CycloneToolChoices
{
new CycloneTool
#pragma warning disable CS0618 // Type or member is obsolete
Tools = new List<CycloneTool>
{
Name = "Covenant",
Vendor = bom.ToolVendor,
Version = bom.ToolVersion,
new CycloneTool
#pragma warning restore CS0618 // Type or member is obsolete
{
Name = "Covenant",
Vendor = bom.ToolVendor,
Version = bom.ToolVersion,
},
},
},
Properties = new List<CycloneProperty>
Expand Down Expand Up @@ -189,7 +196,8 @@ private static CycloneComponent.Classification ConvertKind(BomComponentKind kind
{
BomComponentKind.Root => CycloneComponent.Classification.Application,
BomComponentKind.Library => CycloneComponent.Classification.Library,
_ => throw new NotSupportedException("Unknown hash algorithm"),
BomComponentKind.Application => CycloneComponent.Classification.Application,
_ => throw new NotSupportedException("Unknown component kind"),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/Covenant.CycloneDx/Properties/Usings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@
global using CycloneMetadata = CycloneDX.Models.Metadata;
global using CycloneProperty = CycloneDX.Models.Property;
global using CycloneTool = CycloneDX.Models.Tool;
global using CycloneToolChoices = CycloneDX.Models.ToolChoices;
global using CycloneXmlSerializer = CycloneDX.Xml.Serializer;
2 changes: 1 addition & 1 deletion src/Covenant.Spdx/Covenant.Spdx.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Spdx" Version="0.8.0" />
<PackageReference Include="Spdx" Version="0.9.0" />
</ItemGroup>

<ItemGroup>
Expand Down
8 changes: 4 additions & 4 deletions src/Covenant.Tests/Covenant.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="Shouldly" Version="4.2.1" />
<PackageReference Include="xunit" Version="2.6.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3">
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
Expand Down
19 changes: 19 additions & 0 deletions src/Covenant/Analysis/AnalysisContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ public sealed class AnalysisContext : DiagnosticContext
{
private readonly Graph<BomComponent> _graph;
private readonly Graph<BomComponent> _localGraph;
private readonly HashSet<BomFile> _files;
private readonly HashSet<BomFile> _localFiles;

public IReadOnlyGraph<BomComponent> Graph => _graph;
public IReadOnlyGraph<BomComponent> Delta => _localGraph;
public IReadOnlySet<BomFile> Files => _files;
public IReadOnlySet<BomFile> DeltaFiles => _localFiles;
public DirectoryPath Root { get; }
public ICommandLineResolver Cli { get; }
public CovenantConfiguration Configuration { get; }
Expand All @@ -24,11 +28,15 @@ public sealed class AnalysisContext : DiagnosticContext
Root = root ?? throw new ArgumentNullException(nameof(root));
Cli = settings.Cli;
Configuration = settings.Configuration;

_files = new HashSet<BomFile>();
_localFiles = new HashSet<BomFile>();
}

internal void Reset()
{
_localGraph.Clear();
_localFiles.Clear();
}

public BomComponent AddComponent(BomComponent component)
Expand All @@ -44,6 +52,17 @@ public BomComponent AddComponent(BomComponent component)
return _graph.Add(component);
}

public BomFile AddFile(BomFile file)
{
if (_files.Contains(file))
{
_localFiles.Add(file);
_files.Add(file);
}

return file;
}

public void Connect(BomComponent start, BomComponent end, string? metadata = null)
{
_localGraph.Connect(start, end, metadata);
Expand Down
3 changes: 3 additions & 0 deletions src/Covenant/Analysis/AnalysisService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public sealed class AnalysisService

var root = GetRoot(settings);
var components = new HashSet<BomComponent>();
var files = new HashSet<BomFile>();
var dependencies = new HashSet<BomDependency>();

foreach (var analyzer in _analyzers)
Expand Down Expand Up @@ -56,6 +57,7 @@ public sealed class AnalysisService

// Add all new components
components.AddRange(context.Delta.Nodes);
files.AddRange(context.DeltaFiles);

// Add all new dependencies
foreach (var node in context.Delta.Nodes)
Expand Down Expand Up @@ -86,6 +88,7 @@ public sealed class AnalysisService
{
Components = new List<BomComponent>(components),
Dependencies = new List<BomDependency>(dependencies),
Files = new List<BomFile>(files),
Metadata = settings.Metadata?.Select(
pair => new BomMetadata(pair.Key, pair.Value))?.ToList() ?? new List<BomMetadata>(),
};
Expand Down
196 changes: 196 additions & 0 deletions src/Covenant/Analysis/CycloneDx/CycloneDxAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
namespace Covenant.Analysis.CycloneDx;

using CycloneBom = CycloneDX.Models.Bom;
using CycloneComponent = CycloneDX.Models.Component;
using CycloneHash = CycloneDX.Models.Hash;
using CycloneXmlSerializer = CycloneDX.Xml.Serializer;

public sealed class CycloneDxAnalyzer : Analyzer
{
private const string DisableCycloneDx = "--disable-cyclonedx";

private readonly IFileSystem _fileSystem;
private bool _enabled;

public override string[] Patterns { get; } = { "**/*.cdx.xml", "**/bom.xml" };
public override bool Enabled => _enabled;

public CycloneDxAnalyzer(IFileSystem fileSystem)
{
_fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem));
_enabled = true;
}

public override void Initialize(ICommandLineAugmentor cli)
{
cli.AddOption<bool>(DisableCycloneDx, "Disables the CycloneDX analyzer", false);
}

public override void BeforeAnalysis(AnalysisSettings settings)
{
if (settings.Cli.GetOption<bool>(DisableCycloneDx))
{
_enabled = false;
}
}

public override bool CanHandle(AnalysisContext context, FilePath path)
{
var filename = path.GetFilename();
return filename.FullPath.EndsWith(".cdx.xml") ||
filename.FullPath.Equals("bom.xml", StringComparison.Ordinal);
}

public override void Analyze(AnalysisContext context, FilePath path)
{
var bom = ReadCycloneDxBom(path);
if (bom == null)
{
return;
}

// Consider the SBOM main component a root
context.AddComponent(
ToBomComponent(
context,
bom.Metadata.Component,
BomComponentKind.Root));

foreach (var component in bom.Components)
{
switch (component.Type)
{
case CycloneComponent.Classification.Application:
context.AddComponent(ToBomComponent(context, component, BomComponentKind.Application));
break;
case CycloneComponent.Classification.Framework:
case CycloneComponent.Classification.Library:
// TODO: Add new component kinds
context.AddComponent(ToBomComponent(context, component, BomComponentKind.Library));
break;
case CycloneComponent.Classification.File:
context.AddFile(ToBomFile(component));
break;
case CycloneComponent.Classification.Null:
case CycloneComponent.Classification.Container:
case CycloneComponent.Classification.Firmware:
case CycloneComponent.Classification.Operating_System:
case CycloneComponent.Classification.Device:
case CycloneComponent.Classification.Device_Driver:
case CycloneComponent.Classification.Platform:
case CycloneComponent.Classification.Machine_Learning_Model:
case CycloneComponent.Classification.Data:
// Ignore
break;
default:
throw new InvalidOperationException("Unknown component type");
}
}

foreach (var dependency in bom.Dependencies)
{
var origin = context.Graph.Nodes.FirstOrDefault(x => x.Purl == dependency.Ref);
if (origin == null)
{
context.AddWarning("Component is missing")
.WithContext("Scope", dependency.Ref);
continue;
}

if (dependency.Dependencies != null)
{
foreach (var dep in dependency.Dependencies)
{
var to = context.Graph.Nodes.FirstOrDefault(x => x.Purl == dep.Ref);
if (to == null)
{
context.AddWarning("Component is missing")
.WithContext("Scope", dep.Ref);
continue;
}

context.Connect(origin, to);
}
}
}
}

private BomComponent ToBomComponent(AnalysisContext context, CycloneComponent component, BomComponentKind kind)
{
var bom = new BomComponent(component.Purl, component.Name, component.Version, kind);

if (component.Licenses?.Count > 1)
{
context.AddWarning("Component contains more than one license. This is currently not supported")
.WithContext("Scope", component.Purl);
}

var license = component.Licenses?.FirstOrDefault();
if (license != null)
{
if (license.Expression != null ||
license.License != null)
{
bom.License = new BomLicense
{
Name = license.License?.Name,
Expression = license.Expression,
Id = license.License?.Id,
Url = license.License?.Url,
};

if (license.License?.Text is { Encoding: "base64" })
{
bom.License.Text = Base64EncodedText.FromEncoded(license.License.Text.Content);
}
}
}

return bom;
}

private BomFile ToBomFile(CycloneComponent component)
{
return new BomFile(component.Name, ToBomHah(component.Hashes.FirstOrDefault()));
}

private BomHash ToBomHah(CycloneHash? hash)
{
var alg = hash?.Alg switch
{
null => BomHashAlgorithm.Unknown,
CycloneHash.HashAlgorithm.Null => BomHashAlgorithm.Unknown,
CycloneHash.HashAlgorithm.MD5 => BomHashAlgorithm.MD5,
CycloneHash.HashAlgorithm.SHA_1 => BomHashAlgorithm.SHA1,
CycloneHash.HashAlgorithm.SHA_256 => BomHashAlgorithm.SHA256,
CycloneHash.HashAlgorithm.SHA_384 => BomHashAlgorithm.SHA384,
CycloneHash.HashAlgorithm.SHA_512 => BomHashAlgorithm.SHA512,
CycloneHash.HashAlgorithm.SHA3_256 => BomHashAlgorithm.SHA3_256,
CycloneHash.HashAlgorithm.SHA3_384 => BomHashAlgorithm.SHA3_384,
CycloneHash.HashAlgorithm.SHA3_512 => BomHashAlgorithm.SHA3_512,
CycloneHash.HashAlgorithm.BLAKE2b_256 => BomHashAlgorithm.BLAKE2b_256,
CycloneHash.HashAlgorithm.BLAKE2b_384 => BomHashAlgorithm.BLAKE2b_384,
CycloneHash.HashAlgorithm.BLAKE2b_512 => BomHashAlgorithm.BLAKE2b_512,
CycloneHash.HashAlgorithm.BLAKE3 => BomHashAlgorithm.BLAKE3,
_ => throw new InvalidOperationException("Unknown hash algorithm"),
};

return new BomHash(alg, hash?.Content ?? string.Empty);
}

private CycloneBom? ReadCycloneDxBom(FilePath path)
{
var file = _fileSystem.GetFile(path);
if (!file.Exists)
{
throw new FileNotFoundException("The CycloneDX BOM file could not be found", path.FullPath);
}

using (var stream = file.OpenRead())
using (var reader = new StreamReader(stream))
{
var json = reader.ReadToEnd();
return CycloneXmlSerializer.Deserialize(json);
}
}
}
Loading

0 comments on commit 573806c

Please sign in to comment.