Skip to content

Commit ba5938b

Browse files
authored
Merge pull request #411 from andreas-hilti/feat/merge_annotations
Merge annotations
2 parents fb63b36 + 6f3d554 commit ba5938b

File tree

5 files changed

+510
-1
lines changed

5 files changed

+510
-1
lines changed

src/CycloneDX.Core/Models/Annotation.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@
2020
using System.Xml.Serialization;
2121
using System.Text.Json.Serialization;
2222
using ProtoBuf;
23+
using System.Text.Json;
2324

2425
namespace CycloneDX.Models
2526
{
2627
[ProtoContract]
27-
public class Annotation
28+
public class Annotation : IEquatable<Annotation>
2829
{
2930
[XmlType("subject")]
3031
public class XmlAnnotationSubject
@@ -91,5 +92,27 @@ public DateTime? Timestamp
9192
[XmlElement("text")]
9293
[ProtoMember(5)]
9394
public string Text { get; set; }
95+
96+
97+
public override bool Equals(object obj)
98+
{
99+
var other = obj as Annotation;
100+
if (other == null)
101+
{
102+
return false;
103+
}
104+
105+
return JsonSerializer.Serialize(this, Json.Serializer.SerializerOptionsForHash) == JsonSerializer.Serialize(other, Json.Serializer.SerializerOptionsForHash);
106+
}
107+
108+
public bool Equals(Annotation obj)
109+
{
110+
return JsonSerializer.Serialize(this, Json.Serializer.SerializerOptionsForHash) == JsonSerializer.Serialize(obj, Json.Serializer.SerializerOptionsForHash);
111+
}
112+
113+
public override int GetHashCode()
114+
{
115+
return JsonSerializer.Serialize(this, Json.Serializer.SerializerOptionsForHash).GetHashCode();
116+
}
94117
}
95118
}

src/CycloneDX.Utils/Merge.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2)
147147
var vulnerabilitiesMerger = new ListMergeHelper<Vulnerability>();
148148
result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities);
149149

150+
var annotationsMerger = new ListMergeHelper<Annotation>();
151+
result.Annotations = annotationsMerger.Merge(bom1.Annotations, bom2.Annotations);
152+
150153
if (bom1.Definitions != null && bom2.Definitions != null)
151154
{
152155
//this will not take a signature, but it probably makes sense to empty those after a merge anyways.
@@ -285,6 +288,7 @@ public static Bom HierarchicalMerge(IEnumerable<Bom> boms, Component bomSubject)
285288
result.Dependencies = new List<Dependency>();
286289
result.Compositions = new List<Composition>();
287290
result.Vulnerabilities = new List<Vulnerability>();
291+
result.Annotations = new List<Annotation>();
288292

289293
result.Declarations = new Declarations
290294
{
@@ -401,6 +405,13 @@ bom.SerialNumber is null
401405
result.Vulnerabilities.AddRange(bom.Vulnerabilities);
402406
}
403407

408+
// annotations
409+
if (bom.Annotations != null)
410+
{
411+
NamespaceAnnotationsBomRefs(ComponentBomRefNamespace(bom.Metadata.Component), bom.Annotations);
412+
result.Annotations.AddRange(bom.Annotations);
413+
}
414+
404415
void NamespaceBomRefs(IEnumerable<IHasBomRef> refs) => CycloneDXUtils.NamespaceBomRefs(thisComponent, refs);
405416
void NamespaceReference(IEnumerable<object> refs, string name) => CycloneDXUtils.NamespaceProperty(thisComponent, refs, name);
406417

@@ -472,6 +483,7 @@ bom.SerialNumber is null
472483
if (result.Dependencies.Count == 0) { result.Dependencies = null; }
473484
if (result.Compositions.Count == 0) { result.Compositions = null; }
474485
if (result.Vulnerabilities.Count == 0) { result.Vulnerabilities = null; }
486+
if (result.Annotations.Count == 0) { result.Annotations = null; }
475487

476488
return result;
477489
}
@@ -631,6 +643,44 @@ private static void NamespaceVulnerabilitiesRefs(string bomRefNamespace, List<Vu
631643
}
632644
}
633645

646+
private static void NamespaceAnnotationsBomRefs(string bomRefNamespace, List<Annotation> annotations)
647+
{
648+
var pendingAnnotations = new Stack<Annotation>(annotations);
649+
650+
while (pendingAnnotations.Count > 0)
651+
{
652+
var annotation = pendingAnnotations.Pop();
653+
654+
annotation.BomRef = NamespacedBomRef(bomRefNamespace, annotation.BomRef);
655+
656+
if (annotation.Subjects != null)
657+
{
658+
for (var i = 0; i < annotation.XmlSubjects.Count; i++)
659+
{
660+
annotation.XmlSubjects[i].Ref = NamespacedBomRef(bomRefNamespace, annotation.XmlSubjects[i].Ref);
661+
}
662+
}
663+
664+
if (annotation.Annotator?.Component != null)
665+
{
666+
NamespaceComponentBomRefs(bomRefNamespace, annotation.Annotator?.Component);
667+
}
668+
if (annotation.Annotator?.Individual != null)
669+
{
670+
annotation.Annotator.Individual.BomRef = NamespacedBomRef(bomRefNamespace, annotation.Annotator.Individual.BomRef);
671+
}
672+
if (annotation.Annotator?.Organization != null)
673+
{
674+
annotation.Annotator.Organization.BomRef = NamespacedBomRef(bomRefNamespace, annotation.Annotator.Organization.BomRef);
675+
}
676+
if (annotation.Annotator?.Service != null)
677+
{
678+
annotation.Annotator.Service.BomRef = NamespacedBomRef(bomRefNamespace, annotation.Annotator.Service.BomRef);
679+
}
680+
681+
}
682+
}
683+
634684
private static void NamespaceDependencyBomRefs(string bomRefNamespace, List<Dependency> dependencies)
635685
{
636686
var pendingDependencies = new Stack<Dependency>(dependencies);

tests/CycloneDX.Utils.Tests/MergeTests.cs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,72 @@ public void FlatMergeVulnerabilitiesTest()
175175
Snapshot.Match(result);
176176
}
177177

178+
[Fact]
179+
public void FlatMergeAnnotationsTest()
180+
{
181+
var sbom1 = new Bom
182+
{
183+
Components = new List<Component>
184+
{
185+
new Component
186+
{
187+
Name = "Component1",
188+
Version = "1"
189+
}
190+
},
191+
Annotations = new List<Annotation>
192+
{
193+
new Annotation
194+
{
195+
BomRef = "annotation1",
196+
Subjects = new List<string> { "Component1" },
197+
Text = "Annotation Text",
198+
Annotator = new AnnotatorChoice
199+
{
200+
Individual = new OrganizationalContact
201+
{
202+
BomRef = "individual1",
203+
Name = "individual1",
204+
}
205+
}
206+
}
207+
}
208+
};
209+
var sbom2 = new Bom
210+
{
211+
Components = new List<Component>
212+
{
213+
new Component
214+
{
215+
Name = "Component2",
216+
Version = "1"
217+
}
218+
},
219+
Annotations = new List<Annotation>
220+
{
221+
new Annotation
222+
{
223+
BomRef = "annotation2",
224+
Subjects = new List<string> { "Component2" },
225+
Text = "Annotation Text 2",
226+
Annotator = new AnnotatorChoice
227+
{
228+
Organization = new OrganizationalEntity
229+
{
230+
BomRef = "organization1",
231+
Name = "organization1",
232+
}
233+
}
234+
}
235+
}
236+
};
237+
238+
var result = CycloneDXUtils.FlatMerge(sbom1, sbom2);
239+
240+
Snapshot.Match(result);
241+
}
242+
243+
178244
[Fact]
179245
public void HierarchicalMergeComponentsTest()
180246
{
@@ -621,5 +687,79 @@ public void HierarchicalMergeTest1_6(string filename)
621687

622688
Snapshot.Match(jsonString, SnapshotNameExtension.Create(filename));
623689
}
690+
691+
[Fact]
692+
public void HierarchicalMergeAnnotationsTest()
693+
{
694+
var subject = new Component
695+
{
696+
Name = "Thing",
697+
Version = "1",
698+
};
699+
700+
var sbom1 = new Bom
701+
{
702+
Metadata = new Metadata
703+
{
704+
Component = new Component
705+
{
706+
Name = "System1",
707+
Version = "1",
708+
BomRef = "System1@1"
709+
}
710+
},
711+
Annotations = new List<Annotation>
712+
{
713+
new Annotation
714+
{
715+
BomRef = "annotation1",
716+
Subjects = new List<string> { "System1@1" },
717+
Text = "Annotation Text",
718+
Annotator = new AnnotatorChoice
719+
{
720+
Individual = new OrganizationalContact
721+
{
722+
BomRef = "individual1",
723+
Name = "individual1",
724+
}
725+
}
726+
}
727+
}
728+
};
729+
var sbom2 = new Bom
730+
{
731+
Metadata = new Metadata
732+
{
733+
Component = new Component
734+
{
735+
Name = "System2",
736+
Version = "1",
737+
BomRef = "System2@1"
738+
}
739+
},
740+
Annotations = new List<Annotation>
741+
{
742+
new Annotation
743+
{
744+
BomRef = "annotation2",
745+
Subjects = new List<string> { "System2@1" },
746+
Text = "Annotation Text 2",
747+
Annotator = new AnnotatorChoice
748+
{
749+
Organization = new OrganizationalEntity
750+
{
751+
BomRef = "organization1",
752+
Name = "organization1",
753+
}
754+
}
755+
}
756+
}
757+
};
758+
759+
var result = CycloneDXUtils.HierarchicalMerge(new[] { sbom1, sbom2 }, subject);
760+
761+
Snapshot.Match(result);
762+
}
763+
624764
}
625765
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{
2+
"BomFormat": "CycloneDX",
3+
"SpecVersion": "v1_6",
4+
"SpecVersionString": "1.6",
5+
"SerialNumber": null,
6+
"Version": null,
7+
"Metadata": null,
8+
"Components": [
9+
{
10+
"Type": "Null",
11+
"MimeType": null,
12+
"BomRef": null,
13+
"Supplier": null,
14+
"Author": null,
15+
"Publisher": null,
16+
"Group": null,
17+
"Name": "Component1",
18+
"Version": "1",
19+
"Description": null,
20+
"Scope": null,
21+
"Licenses": null,
22+
"Copyright": null,
23+
"Cpe": null,
24+
"Purl": null,
25+
"Swid": null,
26+
"Modified": null,
27+
"Pedigree": null,
28+
"Evidence": null,
29+
"ModelCard": null,
30+
"CryptoProperties": null,
31+
"XmlSignature": null,
32+
"Signature": null
33+
},
34+
{
35+
"Type": "Null",
36+
"MimeType": null,
37+
"BomRef": null,
38+
"Supplier": null,
39+
"Author": null,
40+
"Publisher": null,
41+
"Group": null,
42+
"Name": "Component2",
43+
"Version": "1",
44+
"Description": null,
45+
"Scope": null,
46+
"Licenses": null,
47+
"Copyright": null,
48+
"Cpe": null,
49+
"Purl": null,
50+
"Swid": null,
51+
"Modified": null,
52+
"Pedigree": null,
53+
"Evidence": null,
54+
"ModelCard": null,
55+
"CryptoProperties": null,
56+
"XmlSignature": null,
57+
"Signature": null
58+
}
59+
],
60+
"Compositions": null,
61+
"Annotations": [
62+
{
63+
"BomRef": "annotation1",
64+
"Subjects": [
65+
"Component1"
66+
],
67+
"XmlSubjects": [
68+
{
69+
"Ref": "Component1"
70+
}
71+
],
72+
"Annotator": {
73+
"Organization": null,
74+
"Individual": {
75+
"Name": "individual1",
76+
"Email": null,
77+
"Phone": null,
78+
"BomRef": "individual1"
79+
},
80+
"Component": null,
81+
"Service": null
82+
},
83+
"Text": "Annotation Text"
84+
},
85+
{
86+
"BomRef": "annotation2",
87+
"Subjects": [
88+
"Component2"
89+
],
90+
"XmlSubjects": [
91+
{
92+
"Ref": "Component2"
93+
}
94+
],
95+
"Annotator": {
96+
"Organization": {
97+
"Name": "organization1",
98+
"Url": null,
99+
"Contact": null,
100+
"BomRef": "organization1",
101+
"Address": null
102+
},
103+
"Individual": null,
104+
"Component": null,
105+
"Service": null
106+
},
107+
"Text": "Annotation Text 2"
108+
}
109+
],
110+
"Definitions": null,
111+
"XmlSignature": null,
112+
"Signature": null
113+
}

0 commit comments

Comments
 (0)