diff --git a/src/CycloneDX.Core/Json/Serializer.Serialization.cs b/src/CycloneDX.Core/Json/Serializer.Serialization.cs index d934af43..4ef69a39 100644 --- a/src/CycloneDX.Core/Json/Serializer.Serialization.cs +++ b/src/CycloneDX.Core/Json/Serializer.Serialization.cs @@ -108,5 +108,32 @@ internal static string Serialize(Models.ExternalReference externalReference) return JsonSerializer.Serialize(externalReference, _options); } + internal static string Serialize(Models.Standard standard) + { + Contract.Requires(standard != null); + return JsonSerializer.Serialize(standard, _options); + } + + internal static string Serialize(Models.OrganizationalEntity organization) + { + Contract.Requires(organization != null); + return JsonSerializer.Serialize(organization, _options); + } + + internal static string Serialize(Models.Claim obj) + { + Contract.Requires(obj != null); + return JsonSerializer.Serialize(obj, _options); + } + internal static string Serialize(Models.Assessor obj) + { + Contract.Requires(obj != null); + return JsonSerializer.Serialize(obj, _options); + } + internal static string Serialize(Models.Attestation obj) + { + Contract.Requires(obj != null); + return JsonSerializer.Serialize(obj, _options); + } } } diff --git a/src/CycloneDX.Core/Models/Component.cs b/src/CycloneDX.Core/Models/Component.cs index 0ea071f1..f2da3573 100644 --- a/src/CycloneDX.Core/Models/Component.cs +++ b/src/CycloneDX.Core/Models/Component.cs @@ -31,7 +31,7 @@ namespace CycloneDX.Models [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")] [XmlType("component")] [ProtoContract] - public class Component: IEquatable + public class Component: IEquatable, IHasBomRef { [ProtoContract] public enum Classification diff --git a/src/CycloneDX.Core/Models/Declarations/Assessor.cs b/src/CycloneDX.Core/Models/Declarations/Assessor.cs index b8b25d90..b04ec6be 100644 --- a/src/CycloneDX.Core/Models/Declarations/Assessor.cs +++ b/src/CycloneDX.Core/Models/Declarations/Assessor.cs @@ -15,7 +15,9 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using CycloneDX.Core.Models; using ProtoBuf; +using System; using System.Text.Json.Serialization; using System.Xml; using System.Xml.Serialization; @@ -23,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Assessor + public class Assessor : IEquatable, IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -36,5 +38,26 @@ public class Assessor [XmlElement("organization")] [ProtoMember(3)] public OrganizationalEntity Organization { get; set; } + + public override bool Equals(object obj) + { + var other = obj as Assessor; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + + public bool Equals(Assessor obj) + { + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return Json.Serializer.Serialize(this).GetHashCode(); + } } } diff --git a/src/CycloneDX.Core/Models/Declarations/Attestation.cs b/src/CycloneDX.Core/Models/Declarations/Attestation.cs index 037086b6..d1e4546b 100644 --- a/src/CycloneDX.Core/Models/Declarations/Attestation.cs +++ b/src/CycloneDX.Core/Models/Declarations/Attestation.cs @@ -16,6 +16,7 @@ // Copyright (c) OWASP Foundation. All Rights Reserved. using ProtoBuf; +using System; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Xml; @@ -24,7 +25,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Attestation + public class Attestation : IEquatable { [XmlElement("summary")] [ProtoMember(1)] @@ -45,5 +46,26 @@ public class Attestation [XmlIgnore] public Signature Signature { get; set; } + public override bool Equals(object obj) + { + var other = obj as Attestation; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + + public bool Equals(Attestation obj) + { + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return Json.Serializer.Serialize(this).GetHashCode(); + } + } } diff --git a/src/CycloneDX.Core/Models/Declarations/Claim.cs b/src/CycloneDX.Core/Models/Declarations/Claim.cs index 65115f3c..03e88b16 100644 --- a/src/CycloneDX.Core/Models/Declarations/Claim.cs +++ b/src/CycloneDX.Core/Models/Declarations/Claim.cs @@ -15,7 +15,10 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using CycloneDX.Core.Models; +using CycloneDX.Models.Vulnerabilities; using ProtoBuf; +using System; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Xml; @@ -24,7 +27,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Claim + public class Claim : IEquatable, IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -66,5 +69,26 @@ public class Claim public XmlElement XmlSignature { get; set; } [XmlIgnore] public Signature Signature { get; set; } + + public override bool Equals(object obj) + { + var other = obj as Claim; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + + public bool Equals(Claim obj) + { + return CycloneDX.Json.Serializer.Serialize(this) == CycloneDX.Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return CycloneDX.Json.Serializer.Serialize(this).GetHashCode(); + } } } diff --git a/src/CycloneDX.Core/Models/Declarations/DeclarationsEvidence.cs b/src/CycloneDX.Core/Models/Declarations/DeclarationsEvidence.cs index 52e4bb30..bbc56db5 100644 --- a/src/CycloneDX.Core/Models/Declarations/DeclarationsEvidence.cs +++ b/src/CycloneDX.Core/Models/Declarations/DeclarationsEvidence.cs @@ -15,6 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using CycloneDX.Core.Models; using ProtoBuf; using System; using System.Collections.Generic; @@ -25,7 +26,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class DeclarationsEvidence + public class DeclarationsEvidence : IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Definitions/Level.cs b/src/CycloneDX.Core/Models/Definitions/Level.cs index e5173ac0..23db6a52 100644 --- a/src/CycloneDX.Core/Models/Definitions/Level.cs +++ b/src/CycloneDX.Core/Models/Definitions/Level.cs @@ -23,7 +23,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Level + public class Level : IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Definitions/Requirement.cs b/src/CycloneDX.Core/Models/Definitions/Requirement.cs index 2582534f..c6f6e85a 100644 --- a/src/CycloneDX.Core/Models/Definitions/Requirement.cs +++ b/src/CycloneDX.Core/Models/Definitions/Requirement.cs @@ -15,6 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. + using ProtoBuf; using System.Collections.Generic; using System.Text.Json.Serialization; @@ -23,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Requirement + public class Requirement : IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] diff --git a/src/CycloneDX.Core/Models/Definitions/Standard.cs b/src/CycloneDX.Core/Models/Definitions/Standard.cs index 657d5af8..0205dc44 100644 --- a/src/CycloneDX.Core/Models/Definitions/Standard.cs +++ b/src/CycloneDX.Core/Models/Definitions/Standard.cs @@ -16,6 +16,7 @@ // Copyright (c) OWASP Foundation. All Rights Reserved. using ProtoBuf; +using System; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Xml.Serialization; @@ -23,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Standard + public class Standard : IEquatable, IHasBomRef { [XmlAttribute("bom-ref")] [JsonPropertyName("bom-ref")] @@ -62,6 +63,8 @@ public class Standard public List ExternalReferences { get; set; } public bool ShouldSerializeExternalReferences() => ExternalReferences?.Count > 0; + + [XmlAnyElement] public List Any { get; set; } @@ -70,5 +73,29 @@ public class Standard [XmlIgnore] public Signature signature { get; set; } + + + + + public override bool Equals(object obj) + { + var other = obj as Standard; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + + public bool Equals(Standard obj) + { + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return Json.Serializer.Serialize(this).GetHashCode(); + } } } diff --git a/src/CycloneDX.Core/Models/OrganizationalEntity.cs b/src/CycloneDX.Core/Models/OrganizationalEntity.cs index 7bd3e62f..1b7b1a6b 100644 --- a/src/CycloneDX.Core/Models/OrganizationalEntity.cs +++ b/src/CycloneDX.Core/Models/OrganizationalEntity.cs @@ -15,6 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright (c) OWASP Foundation. All Rights Reserved. +using System; using System.Collections.Generic; using System.Text.Json.Serialization; using System.Xml.Serialization; @@ -23,7 +24,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class OrganizationalEntity + public class OrganizationalEntity : IEquatable, IHasBomRef { [XmlElement("name")] [ProtoMember(1)] @@ -45,5 +46,26 @@ public class OrganizationalEntity [XmlElement("address")] [ProtoMember(5)] public PostalAddress Address { get; set; } + + public override bool Equals(object obj) + { + var other = obj as OrganizationalEntity; + if (other == null) + { + return false; + } + + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(other); + } + + public bool Equals(OrganizationalEntity obj) + { + return Json.Serializer.Serialize(this) == Json.Serializer.Serialize(obj); + } + + public override int GetHashCode() + { + return Json.Serializer.Serialize(this).GetHashCode(); + } } } diff --git a/src/CycloneDX.Core/Models/Service.cs b/src/CycloneDX.Core/Models/Service.cs index 20c07f0b..0bb3a261 100644 --- a/src/CycloneDX.Core/Models/Service.cs +++ b/src/CycloneDX.Core/Models/Service.cs @@ -26,7 +26,7 @@ namespace CycloneDX.Models { [ProtoContract] - public class Service: IEquatable + public class Service: IEquatable, IHasBomRef { public Service() { diff --git a/src/CycloneDX.Utils/Merge.cs b/src/CycloneDX.Utils/Merge.cs index 3c8c40da..a2a1f209 100644 --- a/src/CycloneDX.Utils/Merge.cs +++ b/src/CycloneDX.Utils/Merge.cs @@ -17,9 +17,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Reflection; using CycloneDX.Models; using CycloneDX.Models.Vulnerabilities; using CycloneDX.Utils.Exceptions; +using Json.Schema; namespace CycloneDX.Utils { @@ -57,7 +60,7 @@ public List Merge(List list1, List list2) break; } } - if (!found) + if (!found) { result.Add(item); resultHashes.Add(hash); @@ -68,6 +71,18 @@ public List Merge(List list1, List list2) } } + public static class ListExtensions + { + public static void AddRangeIfNotNull(this List list, IEnumerable items) + { + if (items != null) + { + list.AddRange(items); + } + } + } + + public static partial class CycloneDXUtils { /// @@ -87,9 +102,9 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) { var result = new Bom(); - #pragma warning disable 618 +#pragma warning disable 618 var toolsMerger = new ListMergeHelper(); - #pragma warning restore 618 +#pragma warning restore 618 var tools = toolsMerger.Merge(bom1.Metadata?.Tools?.Tools, bom2.Metadata?.Tools?.Tools); var toolsComponentsMerger = new ListMergeHelper(); var toolsComponents = toolsComponentsMerger.Merge(bom1.Metadata?.Tools?.Components, bom2.Metadata?.Tools?.Components); @@ -112,7 +127,7 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) result.Components = componentsMerger.Merge(bom1.Components, bom2.Components); //Add main component if missing - if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) + if (result.Components != null && !(bom2.Metadata?.Component is null) && !result.Components.Contains(bom2.Metadata.Component)) { result.Components.Add(bom2.Metadata.Component); } @@ -132,6 +147,30 @@ public static Bom FlatMerge(Bom bom1, Bom bom2) var vulnerabilitiesMerger = new ListMergeHelper(); result.Vulnerabilities = vulnerabilitiesMerger.Merge(bom1.Vulnerabilities, bom2.Vulnerabilities); + if (bom1.Definitions != null && bom2.Definitions != null) + { + //this will not take a signature, but it probably makes sense to empty those after a merge anyways. + result.Definitions = new Definitions(); + var standardMerger = new ListMergeHelper(); + result.Definitions.Standards = standardMerger.Merge(bom1.Definitions.Standards, bom2.Definitions.Standards); + } + + if (bom1.Declarations != null && bom2.Declarations != null) + { + //dont merge higher level signatures or the affirmation. The previously signed/affirmed data likely is changed. + result.Declarations = new Declarations(); + var standardMerger = new ListMergeHelper(); + var standardMerger2 = new ListMergeHelper(); + var Claim = new ListMergeHelper(); + + if (bom1.Declarations?.Targets != null && bom2.Declarations?.Targets != null) + { + result.Declarations.Targets.Organizations = new ListMergeHelper().Merge(bom1.Declarations.Targets.Organizations, bom2.Declarations.Targets.Organizations); + result.Declarations.Targets.Components = new ListMergeHelper().Merge(bom1.Declarations.Targets.Components, bom2.Declarations.Targets.Components); + result.Declarations.Targets.Services = new ListMergeHelper().Merge(bom1.Declarations.Targets.Services, bom2.Declarations.Targets.Services); + } + } + return result; } @@ -170,7 +209,7 @@ public static Bom FlatMerge(IEnumerable boms) public static Bom FlatMerge(IEnumerable boms, Component bomSubject) { var result = new Bom(); - + foreach (var bom in boms) { result = FlatMerge(result, bom); @@ -185,12 +224,12 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) var mainDependency = new Dependency(); mainDependency.Ref = result.Metadata.Component.BomRef; mainDependency.Dependencies = new List(); - + foreach (var bom in boms) { - if (!(bom.Metadata?.Component is null)) + if (!(bom.Metadata?.Component is null)) { - var dep = new Dependency(); + var dep = new Dependency(); dep.Ref = bom.Metadata.Component.BomRef; mainDependency.Dependencies.Add(dep); @@ -199,7 +238,7 @@ public static Bom FlatMerge(IEnumerable boms, Component bomSubject) result.Dependencies.Add(mainDependency); - + } return result; @@ -228,12 +267,12 @@ public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) result.Metadata = new Metadata { Component = bomSubject, - #pragma warning disable 618 +#pragma warning disable 618 Tools = new ToolChoices { Tools = new List(), } - #pragma warning restore 618 +#pragma warning restore 618 }; } @@ -244,6 +283,25 @@ public static Bom HierarchicalMerge(IEnumerable boms, Component bomSubject) result.Compositions = new List(); result.Vulnerabilities = new List(); + result.Declarations = new Declarations() + { + Assessors = new List(), + Attestations = new List(), + Claims = new List(), + Evidence = new List(), + Targets = new Targets() + { + Components = new List(), + Organizations = new List(), + Services = new List() + } + }; + + result.Definitions = new Definitions() + { + Standards = new List() + }; + var bomSubjectDependencies = new List(); foreach (var bom in boms) @@ -310,11 +368,11 @@ bom.SerialNumber is null // services if (bom.Services != null) - foreach (var service in bom.Services) - { - service.BomRef = NamespacedBomRef(bom.Metadata.Component, service.BomRef); - result.Services.Add(service); - } + foreach (var service in bom.Services) + { + service.BomRef = NamespacedBomRef(bom.Metadata.Component, service.BomRef); + result.Services.Add(service); + } // external references if (!(bom.ExternalReferences is null)) result.ExternalReferences.AddRange(bom.ExternalReferences); @@ -339,11 +397,64 @@ bom.SerialNumber is null NamespaceVulnerabilitiesRefs(ComponentBomRefNamespace(result.Metadata.Component), bom.Vulnerabilities); result.Vulnerabilities.AddRange(bom.Vulnerabilities); } + + void NamespaceBomRefs(IEnumerable refs) => CycloneDXUtils.NamespaceBomRefs(thisComponent, refs); + void NamespaceReference(IEnumerable refs, string name) => CycloneDXUtils.NamespaceProperty(thisComponent, refs, name); + + //Definitions + if (bom.Definitions?.Standards != null) + { + //Namespace all references + NamespaceBomRefs(bom.Definitions.Standards); + foreach (var standard in bom.Definitions.Standards) + { + + NamespaceBomRefs(standard.Requirements); + NamespaceBomRefs(standard.Levels); + NamespaceReference(standard.Levels, nameof(Level.Requirements)); + } + result.Definitions.Standards.AddRange(bom.Definitions.Standards); + } + + //Assesors + NamespaceBomRefs(bom.Declarations?.Assessors); + result.Declarations.Assessors.AddRangeIfNotNull(bom.Declarations?.Assessors); + + //Attestation + NamespaceReference(bom.Declarations?.Attestations, nameof(Attestation.Assessor)); + bom.Declarations?.Attestations?.ForEach(attestation => + { + NamespaceReference(attestation.Map, nameof(Map.Claims)); + NamespaceReference(attestation.Map, nameof(Map.CounterClaims)); + NamespaceReference(attestation.Map, nameof(Map.Requirement)); + result.Declarations.Attestations.AddRangeIfNotNull(bom.Declarations?.Attestations); + NamespaceReference(attestation.Map?.Select(map => map.Conformance), nameof(Conformance.MitigationStrategies)); + }); + + //Claims + NamespaceBomRefs(bom.Declarations?.Claims); + NamespaceReference(bom.Declarations?.Claims, nameof(Claim.Evidence)); + NamespaceReference(bom.Declarations?.Claims, nameof(Claim.CounterEvidence)); + NamespaceReference(bom.Declarations?.Claims, nameof(Claim.Target)); + result.Declarations.Claims.AddRangeIfNotNull(bom.Declarations?.Claims); + + //Evidence + NamespaceBomRefs(bom.Declarations?.Evidence); + result.Declarations.Evidence.AddRangeIfNotNull(bom.Declarations?.Evidence); + + //Targets + NamespaceBomRefs(result.Declarations?.Targets?.Organizations); + NamespaceBomRefs(result.Declarations?.Targets?.Components); + NamespaceBomRefs(result.Declarations?.Targets?.Services); + result.Declarations.Targets.Organizations.AddRangeIfNotNull(bom.Declarations?.Targets?.Organizations); + result.Declarations.Targets.Components.AddRangeIfNotNull(bom.Declarations?.Targets?.Components); + result.Declarations.Targets.Services.AddRangeIfNotNull(bom.Declarations?.Targets?.Services); + } if (bomSubject != null) { - result.Dependencies.Add( new Dependency + result.Dependencies.Add(new Dependency { Ref = result.Metadata.Component.BomRef, Dependencies = bomSubjectDependencies @@ -362,6 +473,92 @@ bom.SerialNumber is null return result; } + private static void NamespaceBomRefs(Component bomSubject, IEnumerable references) + { + if (references == null) return; + foreach (IHasBomRef item in references) + { + item.BomRef = NamespacedBomRef(bomSubject, item.BomRef); + } + } + + /// + /// Applies a namespace transformation to a specified property on a collection of objects. + /// This method can handle properties of type or where T is . + /// + /// The component used in the namespace transformation. + /// The collection of objects whose property values will be transformed. + /// + /// The name of the property to be transformed. + /// The property can be of type or where T is . + /// + /// Thrown when the is null or empty. + /// + /// Thrown when the specified is not found on the objects in , + /// or when the property's type is neither nor where T is . + /// + /// + /// The method iterates over each object in the collection. If the specified property is of type + /// , the method applies the function to the property value and updates it. + /// If the property is of type where T is , the method applies the + /// function to each item in the list, replaces the list with a new one containing the transformed values, and updates the property. + /// + private static void NamespaceProperty(Component bomSubject, IEnumerable references, string property) + { + if (references == null) return; + if (string.IsNullOrEmpty(property)) + { + throw new ArgumentNullException(nameof(property), "Property name cannot be null or empty."); + } + + PropertyInfo propertyInfo = null; + + foreach (var item in references) + { + if (propertyInfo == null) + { + var type = item.GetType(); + propertyInfo = type.GetProperty(property); + + if (propertyInfo == null) + { + throw new ArgumentException($"Property '{property}' not found on type '{type.FullName}'"); + } + } + + // Check if the property is a string + if (propertyInfo.PropertyType == typeof(string)) + { + var currentValue = (string)propertyInfo.GetValue(item); + var newValue = NamespacedBomRef(bomSubject, currentValue); + propertyInfo.SetValue(item, newValue); + } + // Check if the property is a List + else if (propertyInfo.PropertyType == typeof(List)) + { + var currentList = (List)propertyInfo.GetValue(item); + + if (currentList == null) + { + currentList = new List(); + } + + var updatedList = new List(); + foreach (var value in currentList) + { + updatedList.Add(NamespacedBomRef(bomSubject, value)); + } + + propertyInfo.SetValue(item, updatedList); + } + else + { + throw new ArgumentException($"Property '{property}' on type '{propertyInfo.DeclaringType.FullName}' is neither of type string nor List."); + } + } + } + + private static string NamespacedBomRef(Component bomSubject, string bomRef) { return string.IsNullOrEmpty(bomRef) ? null : NamespacedBomRef(ComponentBomRefNamespace(bomSubject), bomRef); @@ -434,10 +631,10 @@ private static void NamespaceDependencyBomRefs(string bomRefNamespace, List