From e516bdb428f7164fb3b1e5a413b4761177f5021c Mon Sep 17 00:00:00 2001 From: andreas-hilti <69210561+andreas-hilti@users.noreply.github.com> Date: Sun, 4 Aug 2024 21:30:52 +0200 Subject: [PATCH] Fix metadata tools components (#304) * Fix metadata tools components Signed-off-by: andreas hilti * namespace tool components and services Signed-off-by: andreas hilti * avoid writing null value in tools Signed-off-by: andreas hilti * namespace also nested tools components Signed-off-by: andreas hilti --------- Signed-off-by: andreas hilti --- .../Json/Converters/ToolChoicesConverter.cs | 6 +- src/CycloneDX.Utils/Merge.cs | 51 +++- tests/CycloneDX.Utils.Tests/MergeTests.cs | 216 +++++++++++++++++ ...calMergeDuplicatedToolsComponentsTest.snap | 196 ++++++++++++++++ ....HierarchicalMergeToolsComponentsTest.snap | 219 ++++++++++++++++++ 5 files changed, 679 insertions(+), 9 deletions(-) create mode 100644 tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeDuplicatedToolsComponentsTest.snap create mode 100644 tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeToolsComponentsTest.snap diff --git a/src/CycloneDX.Core/Json/Converters/ToolChoicesConverter.cs b/src/CycloneDX.Core/Json/Converters/ToolChoicesConverter.cs index 16a3328c..d08b29f3 100644 --- a/src/CycloneDX.Core/Json/Converters/ToolChoicesConverter.cs +++ b/src/CycloneDX.Core/Json/Converters/ToolChoicesConverter.cs @@ -85,7 +85,7 @@ public override void Write( } writer.WriteEndArray(); } - else if (value.Components != null || value.Services != null) + else { writer.WriteStartObject(); if (value.Components != null) @@ -100,10 +100,6 @@ public override void Write( } writer.WriteEndObject(); } - else - { - writer.WriteNullValue(); - } } } } diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 36c19596..3c8c40da 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -91,13 +91,19 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var toolsMerger = new ListMergeHelper(); #pragma warning restore 618 var tools = toolsMerger.Merge(bom1.Metadata?.Tools?.Tools, bom2.Metadata?.Tools?.Tools); - if (tools != null) + var toolsComponentsMerger = new ListMergeHelper(); + var toolsComponents = toolsComponentsMerger.Merge(bom1.Metadata?.Tools?.Components, bom2.Metadata?.Tools?.Components); + var toolsServicesMerger = new ListMergeHelper(); + var toolsServices = toolsServicesMerger.Merge(bom1.Metadata?.Tools?.Services, bom2.Metadata?.Tools?.Services); + if (tools != null || toolsComponents != null || toolsServices != null) { result.Metadata = new Metadata { Tools = new ToolChoices { Tools = tools, + Components = toolsComponents, + Services = toolsServices, } }; } @@ -254,6 +260,36 @@ bom.SerialNumber is null { result.Metadata.Tools.Tools.AddRange(bom.Metadata.Tools.Tools); } + if (bom.Metadata?.Tools?.Components?.Count > 0) + { + if (result.Metadata.Tools.Components == null) + { + result.Metadata.Tools.Components = new List(); + } + foreach (var component in bom.Metadata.Tools.Components) + { + NamespaceComponentBomRefs(ComponentBomRefNamespace(bom.Metadata.Component), component); + if (!result.Metadata.Tools.Components.Contains(component)) + { + result.Metadata.Tools.Components.Add(component); + } + } + } + if (bom.Metadata?.Tools?.Services?.Count > 0) + { + if (result.Metadata.Tools.Services == null) + { + result.Metadata.Tools.Services = new List(); + } + foreach (var service in bom.Metadata.Tools.Services) + { + service.BomRef = NamespacedBomRef(bom.Metadata.Component, service.BomRef); + if (!result.Metadata.Tools.Services.Contains(service)) + { + result.Metadata.Tools.Services.Add(service); + } + } + } var thisComponent = bom.Metadata.Component; if (thisComponent.Components is null) bom.Metadata.Component.Components = new List(); @@ -344,6 +380,11 @@ private static string ComponentBomRefNamespace(Component component) } private static void NamespaceComponentBomRefs(Component topComponent) + { + NamespaceComponentBomRefs(ComponentBomRefNamespace(topComponent), topComponent); + } + + private static void NamespaceComponentBomRefs(string bomRefNamespace, Component topComponent) { var components = new Stack(); components.Push(topComponent); @@ -353,12 +394,14 @@ private static void NamespaceComponentBomRefs(Component topComponent) var currentComponent = components.Pop(); if (currentComponent.Components != null) - foreach (var subComponent in currentComponent.Components) { - components.Push(subComponent); + foreach (var subComponent in currentComponent.Components) + { + components.Push(subComponent); + } } - currentComponent.BomRef = NamespacedBomRef(topComponent, currentComponent.BomRef); + currentComponent.BomRef = NamespacedBomRef(bomRefNamespace, currentComponent.BomRef); } } diff --git a/tests/CycloneDX.Utils.Tests/MergeTests.cs b/tests/CycloneDX.Utils.Tests/MergeTests.cs index 186e1cf7..c559e18f 100644 --- a/tests/CycloneDX.Utils.Tests/MergeTests.cs +++ b/tests/CycloneDX.Utils.Tests/MergeTests.cs @@ -288,6 +288,222 @@ public void HierarchicalMergeComponentsTest() Snapshot.Match(result); } + [Fact] + public void HierarchicalMergeToolsComponentsTest() + { + 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" + }, + Tools = new ToolChoices + { + Components = new List + { + new Component + { + Name = "ToolComponent1", + Version = "1", + BomRef = "ToolComponent1@1", + } + } + } + }, + Components = new List + { + new Component + { + Name = "Component1", + Version = "1", + BomRef = "Component1@1" + } + }, + Dependencies = new List + { + new Dependency + { + Ref = "System1@1", + Dependencies = new List + { + new Dependency + { + Ref = "Component1@1" + } + } + } + }, + }; + var sbom2 = new Bom + { + Metadata = new Metadata + { + Component = new Component + { + Name = "System2", + Version = "1", + BomRef = "System2@1" + }, + Tools = new ToolChoices + { + Components = new List + { + new Component + { + Name = "ToolComponent2", + Version = "1", + BomRef = "ToolComponent2@1", + } + } + } + }, + Components = new List + { + new Component + { + Name = "Component2", + Version = "1", + BomRef = "Component2@1" + } + }, + Dependencies = new List + { + new Dependency + { + Ref = "System2@1", + Dependencies = new List + { + new Dependency + { + Ref = "Component2@1" + } + } + } + }, + }; + + var result = CycloneDXUtils.HierarchicalMerge(new[] { sbom1, sbom2 }, subject); + + Snapshot.Match(result); + } + + [Fact] + public void HierarchicalMergeDuplicatedToolsComponentsTest() + { + 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" + }, + Tools = new ToolChoices + { + Components = new List + { + new Component + { + Name = "ToolComponent1", + Version = "1", + } + } + } + }, + Components = new List + { + new Component + { + Name = "Component1", + Version = "1", + BomRef = "Component1@1" + } + }, + Dependencies = new List + { + new Dependency + { + Ref = "System1@1", + Dependencies = new List + { + new Dependency + { + Ref = "Component1@1" + } + } + } + }, + }; + var sbom2 = new Bom + { + Metadata = new Metadata + { + Component = new Component + { + Name = "System2", + Version = "1", + BomRef = "System2@1" + }, + Tools = new ToolChoices + { + Components = new List + { + new Component + { + Name = "ToolComponent1", + Version = "1", + } + } + } + }, + Components = new List + { + new Component + { + Name = "Component2", + Version = "1", + BomRef = "Component2@1" + } + }, + Dependencies = new List + { + new Dependency + { + Ref = "System2@1", + Dependencies = new List + { + new Dependency + { + Ref = "Component2@1" + } + } + } + }, + }; + + var result = CycloneDXUtils.HierarchicalMerge(new[] { sbom1, sbom2 }, subject); + + Snapshot.Match(result); + } + [Fact] public void HierarchicalMergeVulnerabilitiesTest() { diff --git a/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeDuplicatedToolsComponentsTest.snap b/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeDuplicatedToolsComponentsTest.snap new file mode 100644 index 00000000..e2e5d77d --- /dev/null +++ b/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeDuplicatedToolsComponentsTest.snap @@ -0,0 +1,196 @@ +{ + "BomFormat": "CycloneDX", + "SpecVersion": "v1_5", + "SpecVersionString": "1.5", + "SerialNumber": null, + "Version": null, + "Metadata": { + "Tools": { + "Tools": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": null, + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "ToolComponent1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ] + }, + "ProtobufTools": null, + "Authors": null, + "Component": { + "Type": "Null", + "MimeType": null, + "BomRef": "Thing@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Thing", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + }, + "Manufacture": null, + "Supplier": null + }, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System1@1:System1@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "System1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System1@1:Component1@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Component1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Evidence": null, + "ModelCard": null, + "Data": null + }, + { + "Type": "Null", + "MimeType": null, + "BomRef": "System2@1:System2@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "System2", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System2@1:Component2@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Component2", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Dependencies": [ + { + "Ref": "System1@1:System1@1", + "Dependencies": [ + { + "Ref": "System1@1:Component1@1", + "Dependencies": null + } + ] + }, + { + "Ref": "System2@1:System2@1", + "Dependencies": [ + { + "Ref": "System2@1:Component2@1", + "Dependencies": null + } + ] + }, + { + "Ref": "Thing@1", + "Dependencies": [ + { + "Ref": "System1@1:System1@1", + "Dependencies": null + }, + { + "Ref": "System2@1:System2@1", + "Dependencies": null + } + ] + } + ], + "Compositions": null +} diff --git a/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeToolsComponentsTest.snap b/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeToolsComponentsTest.snap new file mode 100644 index 00000000..7e541a5e --- /dev/null +++ b/tests/CycloneDX.Utils.Tests/__snapshots__/MergeTests.HierarchicalMergeToolsComponentsTest.snap @@ -0,0 +1,219 @@ +{ + "BomFormat": "CycloneDX", + "SpecVersion": "v1_5", + "SpecVersionString": "1.5", + "SerialNumber": null, + "Version": null, + "Metadata": { + "Tools": { + "Tools": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System1@1:ToolComponent1@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "ToolComponent1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + }, + { + "Type": "Null", + "MimeType": null, + "BomRef": "System2@1:ToolComponent2@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "ToolComponent2", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ] + }, + "ProtobufTools": null, + "Authors": null, + "Component": { + "Type": "Null", + "MimeType": null, + "BomRef": "Thing@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Thing", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + }, + "Manufacture": null, + "Supplier": null + }, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System1@1:System1@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "System1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System1@1:Component1@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Component1", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Evidence": null, + "ModelCard": null, + "Data": null + }, + { + "Type": "Null", + "MimeType": null, + "BomRef": "System2@1:System2@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "System2", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Components": [ + { + "Type": "Null", + "MimeType": null, + "BomRef": "System2@1:Component2@1", + "Supplier": null, + "Author": null, + "Publisher": null, + "Group": null, + "Name": "Component2", + "Version": "1", + "Description": null, + "Scope": null, + "Licenses": null, + "Copyright": null, + "Cpe": null, + "Purl": null, + "Swid": null, + "Modified": null, + "Pedigree": null, + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Evidence": null, + "ModelCard": null, + "Data": null + } + ], + "Dependencies": [ + { + "Ref": "System1@1:System1@1", + "Dependencies": [ + { + "Ref": "System1@1:Component1@1", + "Dependencies": null + } + ] + }, + { + "Ref": "System2@1:System2@1", + "Dependencies": [ + { + "Ref": "System2@1:Component2@1", + "Dependencies": null + } + ] + }, + { + "Ref": "Thing@1", + "Dependencies": [ + { + "Ref": "System1@1:System1@1", + "Dependencies": null + }, + { + "Ref": "System2@1:System2@1", + "Dependencies": null + } + ] + } + ], + "Compositions": null +}