Skip to content

Attach any “floating” SBOM cycles to the top‐level system object #448

@willis89pr

Description

@willis89pr

Motivation
When two or more SBOMs are merged, we currently:

  1. Identify “root” nodes (zero incoming edges) and attach each to the system object.
  2. Log any directed cycles, but leave them unconnected if they don’t include a root.

Unattached (“floating”) cycles can lead to incomplete graphs and missing relationships in downstream consumers.


Expected Behavior

  • Detect each cycle in the merged graph.
  • For any cycle that contains no root node, pick one representative node (e.g. at random) and create a Contains (or configured) relationship from the system object to that node.

After this change, every component in the merged SBOM—whether part of an acyclic tree or a standalone cycle—will be reachable from the top‐level system.


Actual Behavior

# …inside merge():
roots = [n for n, deg in merged_sbom.graph.in_degree() if deg == 0]
logger.info(f"ROOT NODES: {roots}")

cycles = list(nx.simple_cycles(merged_sbom.graph))
if cycles:
    logger.warning(f"SBOM CYCLE(S) DETECTED: {cycles}")
else:
    logger.info("No cycles detected in SBOM graph")
# …no further handling of floating cycles

Floating cycles are merely logged; no relationships are added. This logic was implemented in PR #433


Steps to Reproduce

  1. Create two minimal SBOM inputs that, when merged, form a directed cycle (A → B → C → A) and do not connect to any other component.

  2. Run:

    surfactant merge --add_system sbom1.json sbom2.json
  3. Observe in the logs:

    SBOM CYCLE(S) DETECTED: [['A','B','C']]  
    
  4. Inspect the output SBOM: neither A, B, nor C is attached to the system object.


Proposed Fix

  1. After detecting cycles, filter out any cycles that intersect the roots list.

  2. For each remaining (“floating”) cycle:

    import random
    
    floating_cycles = [c for c in cycles if not any(node in roots for node in c)]
    for cycle in floating_cycles:
        # choose a random cycle member
        anchor = random.choice(cycle)
        merged_sbom.create_relationship(
            system_obj.UUID,
            anchor,
            system_relationship
        )
        logger.info(f"Attached floating cycle {cycle} via node {anchor}")
  3. Add unit tests in tests/test_merge.py to verify that both tree‐structured components and standalone cycles end up reachable from the system.


Additional Notes

  • Allow deterministic seeding of random in tests for reproducibility.
  • Consider making the “anchor” selection configurable (e.g. always pick the first node) if determinism is preferred.
  • Update documentation to reflect that all components—including cycles—will be connected to the system object.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions