Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions scripts/load_sbom_to_neo4j.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Standalone loader for importing an existing CyTRICS SBOM into Neo4j.

Usage:

export NEO4J_URI=neo4j://localhost:7687
export NEO4J_USER=neo4j
export NEO4J_PASSWORD=your-password
python load_sbom_to_neo4j.py existing-sbom.json

This script assumes it is run inside a Surfactant checkout or environment where
`surfactant` is importable. It imports `export_sbom_to_neo4j` from the sibling
neo4j_writer.py file.
"""
Comment thread
willis89pr marked this conversation as resolved.

from __future__ import annotations

import argparse
import json
import os

from neo4j import GraphDatabase

from surfactant.input_readers.cytrics_reader import read_sbom
from surfactant.output.neo4j_writer import export_sbom_to_neo4j


def main() -> None:
parser = argparse.ArgumentParser(
description="Import an existing Surfactant/CyTRICS SBOM into Neo4j"
)
parser.add_argument("sbom_json", help="Path to an existing CyTRICS SBOM JSON file")
parser.add_argument("--database", default=os.environ.get("NEO4J_DATABASE", "neo4j"))
parser.add_argument(
"--batch-size", type=int, default=int(os.environ.get("NEO4J_BATCH_SIZE", "1000"))
)
args = parser.parse_args()

uri = os.environ.get("NEO4J_URI")
user = os.environ.get("NEO4J_USER", "neo4j")
password = os.environ.get("NEO4J_PASSWORD")
if not uri:
raise SystemExit("NEO4J_URI is required, for example neo4j://localhost:7687")
if password is None:
raise SystemExit("NEO4J_PASSWORD is required")

with open(args.sbom_json, encoding="utf-8") as infile:
sbom = read_sbom(infile)

with GraphDatabase.driver(uri, auth=(user, password)) as driver:
summary = export_sbom_to_neo4j(
sbom,
driver=driver,
database=args.database,
batch_size=args.batch_size,
)

print(json.dumps(summary, indent=2, sort_keys=True))


if __name__ == "__main__":
main()
29 changes: 29 additions & 0 deletions surfactant/cmd/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,15 @@ def sbom(
output_writer = find_io_plugin(pm, output_format, "write_sbom")
input_reader = find_io_plugin(pm, input_format, "read_sbom")

logger.info(
f'Using SBOM output writer "{pm.get_canonical_name(output_writer)}" '
f'for output_format="{output_format}"'
)
logger.info(
f'Using SBOM input reader "{pm.get_canonical_name(input_reader)}" '
f'for input_format="{input_format}"'
)

contextQ: queue.Queue[ContextEntry] = queue.Queue()

for cfg_entry in specimen_context:
Expand All @@ -527,10 +536,25 @@ def sbom(
new_sbom: SBOM
# Click has Sentinel.UNSET type that doesn't have READ attribute, which may appear when running regression test script
if not input_sbom or not hasattr(input_sbom, "read"):
logger.info("No input SBOM supplied; creating a new empty SBOM")
new_sbom = SBOM()
else:
logger.info(f"Reading input SBOM from {getattr(input_sbom, 'name', '<stream>')}")
new_sbom = input_reader.read_sbom(input_sbom)

graph = getattr(new_sbom, "graph", None)
fs_tree = getattr(new_sbom, "fs_tree", None)

logger.info(
"Loaded input SBOM: "
f"software={len(getattr(new_sbom, 'software', []) or [])}, "
f"relationships={len(getattr(new_sbom, '_loaded_relationships', []) or [])}, "
f"graph_nodes={graph.number_of_nodes() if graph is not None else 'None'}, "
f"graph_edges={graph.number_of_edges() if graph is not None else 'None'}, "
f"fs_tree_nodes={fs_tree.number_of_nodes() if fs_tree is not None else 'None'}, "
f"fs_tree_edges={fs_tree.number_of_edges() if fs_tree is not None else 'None'}"
)

_set_sbom_author(new_sbom, author_name, author_type)

# gather metadata for files and add/augment software entries in the sbom
Expand Down Expand Up @@ -886,7 +910,12 @@ def sbom(
logger.info("Skipping relationships based on imports metadata")

# TODO should contents from different containers go in different SBOM files, so new portions can be added bit-by-bit with a final merge?
logger.info(
f"Calling output writer {pm.get_canonical_name(output_writer)} "
f"for {getattr(sbom_outfile, 'name', '<stream>')}"
)
output_writer.write_sbom(new_sbom, sbom_outfile)
logger.info(f"Finished writing SBOM output to {getattr(sbom_outfile, 'name', '<stream>')}")


def resolve_link(
Expand Down
Loading
Loading