Skip to content
Open
205 changes: 205 additions & 0 deletions src/CycloneDX.Core/BomRefWalker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
// This file is part of CycloneDX Library for .NET
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

#if NET8_0_OR_GREATER
using System;
using System.Collections.Generic;
using CycloneDX.Models;
using CycloneDX.Models.Vulnerabilities;

namespace CycloneDX
{
/// <summary>
/// Rewrites every "bom-ref"-shaped value in a <see cref="Bom"/> document
/// -- both identifiers (Component/Service/Vulnerability/Annotation
/// BomRef) and back-references to them (Dependency.Ref, Composition's
/// Assemblies/Dependencies string lists, Vulnerability.Affects[].Ref,
/// Annotation subjects) -- through a caller-supplied function.
/// </summary>
/// <remarks>
/// This generalizes traversal code CycloneDX.Utils.CycloneDXUtils.
/// HierarchicalMerge already hand-rolls for bom-ref namespacing (see
/// its private NamespaceComponentBomRefs/NamespaceDependencyBomRefs/
/// NamespaceCompositions/NamespaceVulnerabilitiesRefs/
/// NamespaceAnnotationsBomRefs methods) into one reusable entry point,
/// parameterized on an arbitrary rewrite function instead of always
/// prefixing a namespace. Namespacing becomes
/// <c>RewriteRefs(bom, r => $"{ns}:{r}")</c>; a manual single-ref
/// rename (as the CLI's <c>rename-entity</c> command needs) becomes
/// <c>RewriteRefs(bom, r => r == oldRef ? newRef : r)</c>.
///
/// Scope note: covers Metadata.Component, Components, Services,
/// Dependencies, Compositions, Vulnerabilities, and Annotations -- it
/// does not yet walk the newer (CycloneDX 1.6) Declarations/Definitions
/// sections. Extending it there is a mechanical follow-up, not an
/// architectural one: add another block below following the same
/// pattern.
/// </remarks>
public static class BomRefWalker
{
public static void RewriteRefs(Bom bom, Func<string, string> rewrite)
{
if (bom is null || rewrite is null)
{
return;
}

if (bom.Metadata?.Component != null)
{
RewriteComponentTree(bom.Metadata.Component, rewrite);
}
if (bom.Metadata?.Tools?.Components != null)
{
foreach (var component in bom.Metadata.Tools.Components)
{
RewriteComponentTree(component, rewrite);
}
}
if (bom.Metadata?.Tools?.Services != null)
{
foreach (var service in bom.Metadata.Tools.Services)
{
service.BomRef = rewrite(service.BomRef);
}
}

if (bom.Components != null)
{
foreach (var component in bom.Components)
{
RewriteComponentTree(component, rewrite);
}
}

if (bom.Services != null)
{
foreach (var service in bom.Services)
{
service.BomRef = rewrite(service.BomRef);
}
}

if (bom.Dependencies != null)
{
RewriteDependencyTree(bom.Dependencies, rewrite);
}

if (bom.Compositions != null)
{
foreach (var composition in bom.Compositions)
{
RewriteStringListInPlace(composition.Assemblies, rewrite);
RewriteStringListInPlace(composition.Dependencies, rewrite);
}
}

if (bom.Vulnerabilities != null)
{
foreach (var vulnerability in bom.Vulnerabilities)
{
vulnerability.BomRef = rewrite(vulnerability.BomRef);
if (vulnerability.Affects != null)
{
foreach (var affect in vulnerability.Affects)
{
affect.Ref = rewrite(affect.Ref);
}
}
}
}

if (bom.Annotations != null)
{
foreach (var annotation in bom.Annotations)
{
annotation.BomRef = rewrite(annotation.BomRef);
if (annotation.XmlSubjects != null)
{
for (var i = 0; i < annotation.XmlSubjects.Count; i++)
{
annotation.XmlSubjects[i].Ref = rewrite(annotation.XmlSubjects[i].Ref);
}
}
if (annotation.Annotator?.Component != null)
{
RewriteComponentTree(annotation.Annotator.Component, rewrite);
}
if (annotation.Annotator?.Individual != null)
{
annotation.Annotator.Individual.BomRef = rewrite(annotation.Annotator.Individual.BomRef);
}
if (annotation.Annotator?.Organization != null)
{
annotation.Annotator.Organization.BomRef = rewrite(annotation.Annotator.Organization.BomRef);
}
if (annotation.Annotator?.Service != null)
{
annotation.Annotator.Service.BomRef = rewrite(annotation.Annotator.Service.BomRef);
}
}
}
}

private static void RewriteComponentTree(Component topComponent, Func<string, string> rewrite)
{
var pending = new Stack<Component>();
pending.Push(topComponent);
while (pending.Count > 0)
{
var component = pending.Pop();
if (component.Components != null)
{
foreach (var sub in component.Components)
{
pending.Push(sub);
}
}
component.BomRef = rewrite(component.BomRef);
}
}

private static void RewriteDependencyTree(List<Dependency> dependencies, Func<string, string> rewrite)
{
var pending = new Stack<Dependency>(dependencies);
while (pending.Count > 0)
{
var dependency = pending.Pop();
if (dependency.Dependencies != null)
{
foreach (var sub in dependency.Dependencies)
{
pending.Push(sub);
}
}
dependency.Ref = rewrite(dependency.Ref);
}
}

private static void RewriteStringListInPlace(List<string> refs, Func<string, string> rewrite)
{
if (refs is null)
{
return;
}
for (var i = 0; i < refs.Count; i++)
{
refs[i] = rewrite(refs[i]);
}
}
}
}
#endif
127 changes: 127 additions & 0 deletions src/CycloneDX.Core/MergeableListHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// This file is part of CycloneDX Library for .NET
//
// Licensed under the Apache License, Version 2.0 (the “License”);
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an “AS IS” BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) OWASP Foundation. All Rights Reserved.

#if NET8_0_OR_GREATER
using System;
using System.Collections.Generic;
using CycloneDX.Models;

namespace CycloneDX
{
/// <summary>
/// Strategy-aware list merging for any element type implementing
/// IEquatable/IEquivalent/IMergeable. One generic method merges
/// List&lt;Hash&gt;, List&lt;Component&gt;, List&lt;OrganizationalContact&gt;,
/// etc., dispatching through real interface calls rather than a
/// separate reflection-driven helper per element type. Lives in
/// CycloneDX.Core (rather than alongside CycloneDX.Utils/Merge.cs)
/// specifically so model classes like Component can call it directly
/// from their own MergeWith implementations.
/// </summary>
public static class MergeableListHelper
{
public static List<T> Merge<T>(List<T> list1, List<T> list2, MergeStrategy strategy)
where T : IEquatable<T>, IEquivalent<T>, IMergeable<T>
{
if (list1 is null) return list2;
if (list2 is null) return list1;
if (strategy is null || !strategy.UseEntityMerge)
{
return ExactMatchMerge(list1, list2);
}

var result = new List<T>(list1);
foreach (var incoming in list2)
{
bool merged = false;
for (int i = 0; i < result.Count; i++)
{
var existing = result[i];
if (existing.Equals(incoming) || existing.Equivalent(incoming, strategy))
{
if (existing.MergeWith(incoming, strategy))
{
result[i] = existing;
merged = true;
break;
}
}
}
if (!merged)
{
result.Add(incoming);
}
}
return result;
}

/// <summary>
/// Cheap fallback: dedupe by exact equality only, no MergeWith
/// attempts. Mirrors CycloneDX.Utils.ListMergeHelper&lt;T&gt;'s
/// behavior (kept independently here to avoid a Core-&gt;Utils
/// dependency, which would invert this project's reference graph).
/// </summary>
private static List<T> ExactMatchMerge<T>(List<T> list1, List<T> list2) where T : IEquatable<T>
{
var result = new List<T>(list1);
foreach (var item in list2)
{
bool found = false;
foreach (var existing in result)
{
if (existing.Equals(item))
{
found = true;
break;
}
}
if (!found)
{
result.Add(item);
}
}
return result;
}

/// <summary>Take whichever of two nullable reference values is non-null, preferring <paramref name="a"/>.</summary>
public static T MergeSingle<T>(T a, T b) where T : class => a ?? b;

/// <summary>Union two string lists, preserving order, dropping duplicates. Null if both are null.</summary>
public static List<string> MergeStringList(List<string> a, List<string> b)
{
if (a is null) return b;
if (b is null) return a;
var result = new List<string>(a);
foreach (var s in b)
{
if (!result.Contains(s))
{
result.Add(s);
}
}
return result;
}

/// <summary>Nullable-bool "either says true" merge: null if both unset, else true if either is true.</summary>
public static bool? MergeNullableBoolOr(bool? a, bool? b)
{
if (!a.HasValue && !b.HasValue) return null;
return (a ?? false) || (b ?? false);
}
}
}
#endif
3 changes: 3 additions & 0 deletions src/CycloneDX.Core/Models/Annotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ namespace CycloneDX.Models
{
[ProtoContract]
public class Annotation : IEquatable<Annotation>
#if NET8_0_OR_GREATER
, IMergeable<Annotation>, IEquivalent<Annotation>
#endif
{
[XmlType("subject")]
public class XmlAnnotationSubject
Expand Down
Loading