Skip to content

Commit 3a3d3c3

Browse files
authored
Merge pull request #412 from andreas-hilti/fix/merge
Fix merge of definitions and declarations
2 parents 3b4e1fa + 702a11f commit 3a3d3c3

File tree

4 files changed

+267
-11
lines changed

4 files changed

+267
-11
lines changed

src/CycloneDX.Utils/Merge.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,30 +150,31 @@ public static Bom FlatMerge(Bom bom1, Bom bom2)
150150
var annotationsMerger = new ListMergeHelper<Annotation>();
151151
result.Annotations = annotationsMerger.Merge(bom1.Annotations, bom2.Annotations);
152152

153-
if (bom1.Definitions != null && bom2.Definitions != null)
153+
if (bom1.Definitions != null || bom2.Definitions != null)
154154
{
155155
//this will not take a signature, but it probably makes sense to empty those after a merge anyways.
156156
result.Definitions = new Definitions();
157157
var standardMerger = new ListMergeHelper<Standard>();
158-
result.Definitions.Standards = standardMerger.Merge(bom1.Definitions.Standards, bom2.Definitions.Standards);
158+
result.Definitions.Standards = standardMerger.Merge(bom1.Definitions?.Standards, bom2.Definitions?.Standards);
159159
}
160160

161-
if (bom1.Declarations != null && bom2.Declarations != null)
161+
if (bom1.Declarations != null || bom2.Declarations != null)
162162
{
163163
//dont merge higher level signatures or the affirmation. The previously signed/affirmed data likely is changed.
164164
result.Declarations = new Declarations();
165165
var AssesorMerger = new ListMergeHelper<Assessor>();
166-
result.Declarations.Assessors = AssesorMerger.Merge(bom1.Declarations.Assessors, bom2.Declarations.Assessors);
166+
result.Declarations.Assessors = AssesorMerger.Merge(bom1.Declarations?.Assessors, bom2.Declarations?.Assessors);
167167
var attestationMerger = new ListMergeHelper<Attestation>();
168-
result.Declarations.Attestations = attestationMerger.Merge(bom1.Declarations.Attestations, bom2.Declarations.Attestations);
169-
var claimmerger = new ListMergeHelper<Claim>();
170-
result.Declarations.Claims = claimmerger.Merge(bom1.Declarations.Claims, bom2.Declarations.Claims);
168+
result.Declarations.Attestations = attestationMerger.Merge(bom1.Declarations?.Attestations, bom2.Declarations?.Attestations);
169+
var claimMerger = new ListMergeHelper<Claim>();
170+
result.Declarations.Claims = claimMerger.Merge(bom1.Declarations?.Claims, bom2.Declarations?.Claims);
171171

172-
if (bom1.Declarations?.Targets != null && bom2.Declarations?.Targets != null)
172+
if (bom1.Declarations?.Targets != null || bom2.Declarations?.Targets != null)
173173
{
174-
result.Declarations.Targets.Organizations = new ListMergeHelper<OrganizationalEntity>().Merge(bom1.Declarations.Targets.Organizations, bom2.Declarations.Targets.Organizations);
175-
result.Declarations.Targets.Components = new ListMergeHelper<Component>().Merge(bom1.Declarations.Targets.Components, bom2.Declarations.Targets.Components);
176-
result.Declarations.Targets.Services = new ListMergeHelper<Service>().Merge(bom1.Declarations.Targets.Services, bom2.Declarations.Targets.Services);
174+
result.Declarations.Targets = new Targets();
175+
result.Declarations.Targets.Organizations = new ListMergeHelper<OrganizationalEntity>().Merge(bom1.Declarations?.Targets?.Organizations, bom2.Declarations?.Targets?.Organizations);
176+
result.Declarations.Targets.Components = new ListMergeHelper<Component>().Merge(bom1.Declarations?.Targets?.Components, bom2.Declarations?.Targets?.Components);
177+
result.Declarations.Targets.Services = new ListMergeHelper<Service>().Merge(bom1.Declarations?.Targets?.Services, bom2.Declarations?.Targets?.Services);
177178
}
178179
}
179180

@@ -223,6 +224,10 @@ public static Bom FlatMerge(IEnumerable<Bom> boms, Component bomSubject)
223224

224225
if (bomSubject != null)
225226
{
227+
if (result.Metadata == null)
228+
{
229+
result.Metadata = new Metadata();
230+
}
226231
// use the params provided if possible
227232
result.Metadata.Component = bomSubject;
228233
result.Metadata.Component.BomRef = ComponentBomRefNamespace(result.Metadata.Component);
@@ -242,6 +247,11 @@ public static Bom FlatMerge(IEnumerable<Bom> boms, Component bomSubject)
242247
}
243248
}
244249

250+
if (result.Dependencies == null)
251+
{
252+
result.Dependencies = new List<Dependency>();
253+
}
254+
245255
result.Dependencies.Add(mainDependency);
246256

247257

tests/CycloneDX.Utils.Tests/MergeTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -688,6 +688,46 @@ public void HierarchicalMergeTest1_6(string filename)
688688
Snapshot.Match(jsonString, SnapshotNameExtension.Create(filename));
689689
}
690690

691+
[Theory]
692+
[InlineData("valid-attestation-1.6.json")]
693+
[InlineData("valid-standard-1.6.json")]
694+
public void FlatMergeTest1_6(string filename)
695+
{
696+
var subject = new Component
697+
{
698+
Type = Component.Classification.Application,
699+
Name = "Thing",
700+
Version = "1",
701+
};
702+
var resourceFilename = Path.Join("MergeTests_Resources", filename);
703+
var jsonString = File.ReadAllText(resourceFilename);
704+
705+
var bom1 = Serializer.Deserialize(jsonString);
706+
var bom2 = new Bom
707+
{
708+
Components = new List<Component>
709+
{
710+
new Component
711+
{
712+
Type = Component.Classification.Application,
713+
BomRef = "bom2",
714+
Name = "bom2name"
715+
}
716+
}
717+
};
718+
719+
720+
var result = CycloneDXUtils.FlatMerge(new[] { bom1, bom2 }, subject);
721+
result.SpecVersion = SpecificationVersion.v1_6;
722+
723+
jsonString = Serializer.Serialize(result);
724+
725+
var validationResult = Validator.Validate(jsonString, SpecificationVersion.v1_6);
726+
Assert.True(validationResult.Valid, string.Join(Environment.NewLine, validationResult.Messages));
727+
728+
Snapshot.Match(jsonString, SnapshotNameExtension.Create(filename));
729+
}
730+
691731
[Fact]
692732
public void HierarchicalMergeAnnotationsTest()
693733
{
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"metadata": {
5+
"component": {
6+
"type": "application",
7+
"bom-ref": "Thing@1",
8+
"name": "Thing",
9+
"version": "1"
10+
}
11+
},
12+
"components": [
13+
{
14+
"type": "application",
15+
"bom-ref": "bom2",
16+
"name": "bom2name"
17+
}
18+
],
19+
"dependencies": [
20+
{
21+
"ref": "Thing@1",
22+
"dependsOn": []
23+
}
24+
],
25+
"declarations": {
26+
"assessors": [
27+
{
28+
"bom-ref": "assessor-1",
29+
"thirdParty": true,
30+
"organization": {
31+
"name": "Assessors Inc"
32+
}
33+
}
34+
],
35+
"attestations": [
36+
{
37+
"summary": "Attestation summary here",
38+
"assessor": "assessor-1",
39+
"map": [
40+
{
41+
"requirement": "requirement-1",
42+
"claims": [
43+
"claim-1"
44+
],
45+
"counterClaims": [
46+
"counterClaim-1"
47+
],
48+
"conformance": {
49+
"score": 0.8,
50+
"rationale": "Conformance rationale here",
51+
"mitigationStrategies": [
52+
"mitigationStrategy-1"
53+
]
54+
},
55+
"confidence": {
56+
"score": 1,
57+
"rationale": "Confidence rationale here"
58+
}
59+
}
60+
],
61+
"signature": {
62+
"algorithm": "ES256",
63+
"certificatePath": [
64+
"MIIB...",
65+
"MIID..."
66+
],
67+
"value": "tqIT..."
68+
}
69+
}
70+
],
71+
"claims": [
72+
{
73+
"bom-ref": "claim-1",
74+
"target": "acme-inc",
75+
"predicate": "Predicate here",
76+
"mitigationStrategies": [
77+
"mitigationStrategy-1"
78+
],
79+
"reasoning": "Reasoning here",
80+
"evidence": [
81+
"evidence-1"
82+
],
83+
"counterEvidence": [
84+
"counterEvidence-1"
85+
],
86+
"externalReferences": [
87+
{
88+
"url": "https://alm.example.com",
89+
"type": "issue-tracker"
90+
}
91+
],
92+
"signature": {
93+
"algorithm": "ES256",
94+
"certificatePath": [
95+
"MIIB...",
96+
"MIID..."
97+
],
98+
"value": "tqIT..."
99+
}
100+
}
101+
],
102+
"targets": {
103+
"organizations": [
104+
{
105+
"name": "Acme Inc",
106+
"bom-ref": "acme-inc"
107+
}
108+
]
109+
}
110+
}
111+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.6",
4+
"metadata": {
5+
"component": {
6+
"type": "application",
7+
"bom-ref": "Thing@1",
8+
"name": "Thing",
9+
"version": "1"
10+
}
11+
},
12+
"components": [
13+
{
14+
"type": "application",
15+
"bom-ref": "bom2",
16+
"name": "bom2name"
17+
}
18+
],
19+
"dependencies": [
20+
{
21+
"ref": "Thing@1",
22+
"dependsOn": []
23+
}
24+
],
25+
"definitions": {
26+
"standards": [
27+
{
28+
"bom-ref": "standard-1",
29+
"name": "Sample Standard",
30+
"version": "1.0.0",
31+
"description": "Description here",
32+
"owner": "Acme Inc",
33+
"requirements": [
34+
{
35+
"bom-ref": "requirement-1",
36+
"identifier": "v1",
37+
"title": "Title here"
38+
},
39+
{
40+
"bom-ref": "requirement-1.1",
41+
"identifier": "v1.1",
42+
"title": "Title here",
43+
"parent": "requirement-1"
44+
},
45+
{
46+
"bom-ref": "requirement-1.1.1",
47+
"identifier": "v1.1.1",
48+
"text": "Text of the requirement here",
49+
"descriptions": [
50+
"Supplemental text here"
51+
],
52+
"openCre": [
53+
"CRE:616-305"
54+
],
55+
"parent": "requirement-1.1"
56+
}
57+
],
58+
"levels": [
59+
{
60+
"bom-ref": "level-1",
61+
"identifier": "Level 1",
62+
"description": "Description here",
63+
"requirements": [
64+
"requirement-1.1.1"
65+
]
66+
},
67+
{
68+
"bom-ref": "level-2",
69+
"identifier": "Level 2",
70+
"description": "Description here",
71+
"requirements": [
72+
"requirement-1.1.1"
73+
]
74+
},
75+
{
76+
"bom-ref": "level-3",
77+
"identifier": "Level 3",
78+
"description": "Description here",
79+
"requirements": [
80+
"requirement-1.1.1"
81+
]
82+
}
83+
],
84+
"signature": {
85+
"algorithm": "ES256",
86+
"certificatePath": [
87+
"MIIB...",
88+
"MIID..."
89+
],
90+
"value": "tqIT..."
91+
}
92+
}
93+
]
94+
}
95+
}

0 commit comments

Comments
 (0)