Skip to content

Add Neo4j output writer for Surfactant SBOM exports#629

Open
willis89pr wants to merge 6 commits into
mainfrom
neo4j
Open

Add Neo4j output writer for Surfactant SBOM exports#629
willis89pr wants to merge 6 commits into
mainfrom
neo4j

Conversation

@willis89pr

@willis89pr willis89pr commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR adds a built-in Neo4j output writer that exports Surfactant/CyTRICS SBOM data into a Neo4j graph database. It also adds a standalone import script for loading an existing SBOM JSON directly into Neo4j.

The writer exports logical SBOM relationships separately from filesystem topology. Logical relationships come from sbom.graph, while filesystem/path topology comes from sbom.fs_tree; mirrored path/symlink edges in sbom.graph are filtered out to avoid duplicate Neo4j relationships.

This PR also improves generate-time logging around input/output plugin selection, filtered logical relationship counts, total graph counts, filesystem tree counts, and SBOM output writing.

What changed

  • Added surfactant/output/neo4j_writer.py

    • Implements a neo4j output plugin.
    • Exports SBOM data into Neo4j using stable node and relationship IDs.
    • Creates/uses a uniqueness constraint on :SBOMEntity(id) to make repeated imports of the same content idempotent.
    • Exports logical relationships from sbom.graph.
    • Filters path nodes and symlink/path edges mirrored into sbom.graph so filesystem topology is not exported twice.
    • Exports filesystem topology from sbom.fs_tree.
    • Writes a JSON import summary to the requested output file.
    • Separates logical_graph_edges, fs_tree_edges, and installed_at_edges in the import summary.
    • Supports batching through NEO4J_BATCH_SIZE.
    • Validates NEO4J_BATCH_SIZE so invalid, zero, or negative values fail clearly.
  • Added scripts/load_sbom_to_neo4j.py

    • Provides a standalone CLI for importing an existing CyTRICS SBOM JSON file into Neo4j.
    • Reads Neo4j connection settings from environment variables.
    • Supports --database and --batch-size CLI options.
    • Validates --batch-size so zero or negative values fail clearly.
    • Handles a missing Neo4j Python driver with a clear install message.
    • Prints an import summary after completion.
  • Registered the Neo4j writer in surfactant/plugin/manager.py

    • Makes the writer available through the normal Surfactant plugin registration path.
  • Improved logging in surfactant/cmd/generate.py

    • Logs selected input/output plugins.
    • Logs whether a new SBOM is created or an existing SBOM is loaded.
    • Logs software counts, filtered logical relationship counts, total graph counts, and filesystem tree counts before writing.
    • Logs when SBOM output writing starts and finishes.

Neo4j graph model

The writer maps SBOM data into these node labels:

  • :SBOM:SBOMEntity
  • :Software:SBOMEntity
  • :Path:SBOMEntity
  • :Hash:SBOMEntity
  • :GraphEntity:SBOMEntity

Logical SBOM relationships from sbom.graph are written using sanitized NetworkX edge keys as Neo4j relationship types.

Filesystem/path edges mirrored into sbom.graph are intentionally skipped during logical relationship export. sbom.fs_tree is treated as the authoritative source for filesystem topology.

Filesystem relationships from sbom.fs_tree are written as:

  • FS_CONTAINS
  • SYMLINK
  • HAS_CONTENT_HASH

Software install-location relationships are written as:

  • INSTALLED_AT

Each node and relationship includes stable IDs derived from the SBOM UUID and source graph data, so imports can be safely re-run without creating duplicate graph entities.

Usage

Using the output plugin through surfactant generate:

export NEO4J_URI=neo4j://localhost:7687
export NEO4J_USER=neo4j
export NEO4J_PASSWORD=your-password

surfactant generate \
  --skip_gather \
  --skip_relationships \
  --output_format neo4j \
  empty-context.json \
  neo4j-import-summary.json \
  existing-sbom.json

Using the standalone loader:

export NEO4J_URI=neo4j://localhost:7687
export NEO4J_USER=neo4j
export NEO4J_PASSWORD=your-password

python scripts/load_sbom_to_neo4j.py existing-sbom.json

Optional environment variables:

export NEO4J_DATABASE=neo4j
export NEO4J_BATCH_SIZE=1000

The standalone loader also supports equivalent CLI options:

python scripts/load_sbom_to_neo4j.py \
  --database neo4j \
  --batch-size 1000 \
  existing-sbom.json

Testing

  • Verified Python syntax with:
python -m py_compile \
  scripts/load_sbom_to_neo4j.py \
  surfactant/cmd/generate.py \
  surfactant/plugin/manager.py \
  surfactant/output/neo4j_writer.py
  • Ran an end-to-end import against a local Neo4j instance through the neo4j output writer:
surfactant --log-level INFO generate \
  --skip_gather \
  --skip_relationships \
  --output_format neo4j \
  . \
  neo4j-import-summary.json \
  <existing-sbom.json>
  • Verified the large-SBOM import completed and Neo4j database counts matched the prepared import rows:
software=58491
paths=80438
hashes=58491
nodes=197421
relationships=293577
logical_graph_edges=7970
fs_tree_edges=152719
installed_at_edges=74397
database_nodes_for_bom=197421
database_relationships_for_bom=293577
  • Verified logical graph edges were exported separately from filesystem topology:
USES=1977
CONTAINS=5993
logical_graph_edges=7970

FS_CONTAINS=76383
HAS_CONTENT_HASH=72041
SYMLINK=4295
fs_tree_edges=152719
  • Not run: full automated test suite.

Notes for reviewers

The Neo4j Python driver is required at runtime for the new writer and standalone loader. Both entry points now handle a missing driver with a clear error message, but this PR does not add dependency metadata for neo4j to the project packaging configuration.

Imports are idempotent for repeated imports of the same SBOM content because nodes and relationships use stable IDs. This PR does not implement replace/sync behavior for removing stale Neo4j entities if a later import uses the same bomUUID but omits entities that were imported previously.

Willis Berrios and others added 2 commits June 9, 2026 11:35
Add a Neo4j output plugin that exports SBOM software, logical graph, filesystem tree, path, and hash data into Neo4j with stable node and relationship IDs.

Register the Neo4j writer with the plugin manager and add a standalone loader script for importing existing CyTRICS SBOM JSON files.

Also add generation-time logging for selected input/output plugins, loaded SBOM graph counts, and SBOM write progress.
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown

✅ No SBOM Changes Detected

For commit 8843037 (Run 27221309320)
Compared against commit 8bd7c79 (Run 27221185739)


def _rel_id(bom_uuid: str, graph_name: str, *parts: Any) -> str:
joined = "|".join(str(part) for part in (bom_uuid, graph_name, *parts))
return f"{bom_uuid}:rel:{hashlib.sha1(joined.encode('utf-8')).hexdigest()}"
@willis89pr willis89pr self-assigned this Jun 9, 2026
@willis89pr
willis89pr requested a review from nightlark June 9, 2026 16:42
Comment thread scripts/load_sbom_to_neo4j.py
Willis Berrios added 4 commits June 9, 2026 13:56
Skip path nodes and symlink/path edges mirrored into sbom.graph when
building Neo4j logical relationship rows. Treat sbom.fs_tree as the
authoritative source for filesystem topology to avoid duplicate Neo4j
relationships.

Update generate and Neo4j writer logging to report filtered logical
relationship counts separately from total graph edge counts. Also split
Neo4j import summary counts into logical graph edges, filesystem tree
edges, and installed-at edges.

Improve Neo4j loader/operator validation by adding clearer missing-driver
handling and rejecting invalid batch sizes.
Declare the Neo4j Python driver dependency in inline script
metadata so compatible runners can resolve the standalone loader's
runtime dependency.
@nightlark nightlark added the enhancement New feature or request label Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants