cogs (short for "Combine OpenGraphS") is a command-line tool that merges multiple BloodHound OpenGraph JSON files into a single unified graph.
Built using the gopengraph library for BloodHound OpenGraph compatibility.
- Multiple input methods: Read from stdin (pipeline) or specify files via command-line arguments
- Smart node merging: Union operation on node properties (last wins on conflicts)
- Kinds merging: Automatically merges node kinds up to 2 types maximum
- Edge deduplication: Merges edges with the same start/end/kind, combining properties
- Metadata handling: Intelligently handles source_kind across multiple graphs
- Validation: Validates all input graphs against BloodHound OpenGraph schema
go build -o cogsMerge graphs via pipeline:
cat graph1.json graph2.json | cogs > merged.jsonMerge graphs via command-line arguments:
cogs -j graph1.json -j graph2.json > merged.json
cogs --json graph1.json --json graph2.json > merged.jsonMix both methods:
cat graph1.json | cogs -j graph2.json -j graph3.json > merged.jsonOverride the source_kind in the merged output:
cat graph1.json graph2.json | cogs -s CombinedSource > merged.json
cogs -j graph1.json -j graph2.json --source_kind CombinedSource > merged.jsonWhen nodes with the same id appear in multiple graphs:
- Properties: Union of all properties, last value wins on conflicts
- Kinds: Union of all kinds, maximum 2 kinds allowed (error if exceeded)
Example:
// Graph 1
{"id": "user-1", "kinds": ["User"], "properties": {"name": "Alice", "age": 30}}
// Graph 2
{"id": "user-1", "kinds": ["Admin"], "properties": {"email": "alice@example.com", "age": 31}}
// Merged Result
{"id": "user-1", "kinds": ["User", "Admin"], "properties": {"name": "Alice", "email": "alice@example.com", "age": 31}}Edges with identical start/end/kind/match_by are considered duplicates:
- Properties: Union of all properties, last value wins on conflicts
- Edges with different start, end, or kind are kept as separate edges
Example:
// Graph 1
{"start": {"match_by": "id", "value": "user-1"}, "end": {"match_by": "id", "value": "user-2"},
"kind": "Knows", "properties": {"since": "2020", "confidence": 0.8}}
// Graph 2
{"start": {"match_by": "id", "value": "user-1"}, "end": {"match_by": "id", "value": "user-2"},
"kind": "Knows", "properties": {"verified": true, "confidence": 0.9}}
// Merged Result
{"start": {"match_by": "id", "value": "user-1"}, "end": {"match_by": "id", "value": "user-2"},
"kind": "Knows", "properties": {"since": "2020", "verified": true, "confidence": 0.9}}The source_kind field in metadata follows these rules:
- Same across all graphs: Preserved in output
- Different across graphs: Dropped from output (empty metadata)
- Override flag (
-s): Always takes precedence - All empty: Output has empty metadata
Examples:
# All graphs have source_kind "TestSource" → Output has "TestSource"
cat graph1.json graph2.json | cogs
# Graphs have different source_kinds → Output has no metadata
cat graph1.json graph2.json | cogs
# Override regardless of input
cat graph1.json graph2.json | cogs -s MySourcecogs will exit with an error if:
- Invalid JSON: Input is not valid JSON
- Invalid OpenGraph: Missing required fields (
graph.nodes,graph.edges, nodeid, etc.) - Too many kinds: Merging would result in more than 2 kinds for a node
- Empty node ID: A node has an empty
idfield - Empty kinds array: A node has no kinds
- Invalid edge: Missing required fields (
start,end,kind,match_by)
All input graphs are validated against the BloodHound OpenGraph schema:
- Nodes must have:
id,kinds(1-2 values),properties - Edges must have:
start.value,start.match_by,end.value,end.match_by,kind - Optional:
propertieson edges,metadata.source_kind
Run the test suite:
go test -vTest with sample data:
cat testdata/graph1.json testdata/graph2.json | ./cogs-j, --json: JSON file to process (can be specified multiple times)-s, --source_kind: Source kind for the merged OpenGraph metadata (optional, overrides input)
cat testdata/graph1.json testdata/graph2.json | ./cogsOutput includes "source_kind": "TestSource" since both inputs have the same value.
cat testdata/graph1.json testdata/graph3-different-source.json | ./cogsOutput has empty metadata since source_kinds differ.
./cogs -j testdata/graph1.json -j testdata/graph2.json -s MergedDataOutput has "source_kind": "MergedData" regardless of inputs.
cat testdata/graph1.json testdata/graph4-kinds-merge.json | ./cogsMerges properties and kinds for user-001.
cogs is designed to work seamlessly with the og tool:
# Generate multiple OpenGraph files from CSVs
cat users1.csv | og -s Source1 > graph1.json
cat users2.csv | og -s Source2 > graph2.json
# Combine them with cogs
cogs -j graph1.json -j graph2.json -s CombinedUsers > merged.json
# Or in a pipeline
cat users1.csv | og | cogs -j additional.json > combined.json