Skip to content
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
6 changes: 6 additions & 0 deletions src/CycloneDX.Core/Json/Serializer.Serialization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ internal static string Serialize(Dependency dependency)
return JsonSerializer.Serialize(dependency, getOption());
}

internal static string Serialize(Lifecycles lifecycles)
{
Contract.Requires(lifecycles != null);
return JsonSerializer.Serialize(lifecycles, getOption());
}

internal static string Serialize(Service service)
{
Contract.Requires(service != null);
Expand Down
23 changes: 22 additions & 1 deletion src/CycloneDX.Core/Models/Lifecycles.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
namespace CycloneDX.Models
{
[ProtoContract]
public class Lifecycles
public class Lifecycles: IEquatable<Lifecycles>
{
[ProtoContract]
public enum LifecyclePhase
Expand Down Expand Up @@ -59,5 +59,26 @@ public enum LifecyclePhase
[XmlElement("description")]
[ProtoMember(3)]
public string Description { get; set; }

public override bool Equals(object obj)
{
var other = obj as Lifecycles;
if (other == null)
{
return false;
}

return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other);
}

public bool Equals(Lifecycles obj)
{
return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj);
}

public override int GetHashCode()
{
return Json.Serializer.Serialize(this).GetHashCode();
}
}
}
20 changes: 20 additions & 0 deletions src/CycloneDX.Utils/Merge.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,20 @@ public static Bom FlatMerge(Bom bom1, Bom bom2)
};
}

var lifecyclesMerger = new ListMergeHelper<Lifecycles>();
var lifecycles = lifecyclesMerger.Merge(bom1.Metadata?.Lifecycles, bom2.Metadata?.Lifecycles);
if (lifecycles != null && result.Metadata == null)
{
result.Metadata = new Metadata
{
Lifecycles = lifecycles,
};
}
else if (lifecycles != null)
{
result.Metadata.Lifecycles = lifecycles;
}

var componentsMerger = new ListMergeHelper<Component>();
result.Components = componentsMerger.Merge(bom1.Components, bom2.Components);

Expand Down Expand Up @@ -305,6 +319,7 @@ public static Bom HierarchicalMerge(IEnumerable<Bom> boms, Component bomSubject)
Standards = new List<Standard>()
};

var lifecyclesMerger = new ListMergeHelper<Lifecycles>();
var bomSubjectDependencies = new List<Dependency>();

foreach (var bom in boms)
Expand Down Expand Up @@ -351,6 +366,11 @@ bom.SerialNumber is null
}
}
}
if (bom.Metadata?.Lifecycles?.Count > 0)
{
var lifecycles = lifecyclesMerger.Merge(result.Metadata.Lifecycles, bom.Metadata.Lifecycles);
result.Metadata.Lifecycles = lifecycles;
}

var thisComponent = bom.Metadata.Component;
if (thisComponent.Components is null) bom.Metadata.Component.Components = new List<Component>();
Expand Down
108 changes: 108 additions & 0 deletions tests/CycloneDX.Utils.Tests/MergeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,51 @@ public void FlatMergeVulnerabilitiesTest()
Snapshot.Match(result);
}

[Fact]
public void FlatMergeLifecyclesTest()
{
var sbom1 = new Bom
{
Metadata = new Metadata
{
Lifecycles = new List<Lifecycles>
{
new Lifecycles
{
Phase = Lifecycles.LifecyclePhase.Design
},
new Lifecycles
{
Name = "custom-phase",
Description = "This is a custom phase."
}
}
}
};
var sbom2 = new Bom
{
Metadata = new Metadata
{
Lifecycles = new List<Lifecycles>
{
new Lifecycles
{
Phase = Lifecycles.LifecyclePhase.Build
},
new Lifecycles
{
Name = "custom-phase",
Description = "This is a custom phase."
}
}
}
};

var result = CycloneDXUtils.FlatMerge(sbom1, sbom2);

Snapshot.Match(result);
}

[Fact]
public void HierarchicalMergeComponentsTest()
{
Expand Down Expand Up @@ -573,6 +618,69 @@ public void HierarchicalMergeVulnerabilitiesTest()
Snapshot.Match(result);
}

[Fact]
public void HierarchicalMergeLifecyclesTest()
{
var subject = new Component
{
Name = "Thing",
Version = "1",
};

var sbom1 = new Bom
{
Metadata = new Metadata
{
Component = new Component
{
Name = "System1",
Version = "1",
BomRef = "System1@1"
},
Lifecycles = new List<Lifecycles>
{
new Lifecycles
{
Phase = Lifecycles.LifecyclePhase.Design
},
new Lifecycles
{
Name = "custom-phase",
Description = "This is a custom phase."
}
}
}
};
var sbom2 = new Bom
{
Metadata = new Metadata
{
Component = new Component
{
Name = "System2",
Version = "1",
BomRef = "System2@1"
},
Lifecycles = new List<Lifecycles>
{
new Lifecycles
{
Phase = Lifecycles.LifecyclePhase.Build
},
new Lifecycles
{
Name = "custom-phase",
Description = "This is a custom phase."
}
}
}
};

var result = CycloneDXUtils.HierarchicalMerge(new[] { sbom1, sbom2 }, subject);

Snapshot.Match(result);
}

[Theory]
[InlineData("valid-attestation-1.6.json")]
[InlineData("valid-standard-1.6.json")]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"BomFormat": "CycloneDX",
"SpecVersion": "v1_6",
"SpecVersionString": "1.6",
"SerialNumber": null,
"Version": null,
"Metadata": {
"Tools": null,
"ProtobufTools": null,
"Authors": null,
"Component": null,
"Manufacture": null,
"Supplier": null,
"Lifecycles": [
{
"Phase": "Design",
"Name": null,
"Description": null
},
{
"Name": "custom-phase",
"Description": "This is a custom phase."
},
{
"Phase": "Build",
"Name": null,
"Description": null
}
]
},
"Components": null,
"Compositions": null,
"Definitions": null,
"XmlSignature": null,
"Signature": null
}
Loading