og (short for OpenGraph) is a command-line tool that builds BloodHound-compatible OpenGraph JSON from structured graph inputs:
- CSV files containing nodes and edges
- graphify knowledge graphs (
graph.json), converted directly with no intermediate CSV
Both input types can be mixed in a single run and share one ID space, so a graphify code graph can be enriched with hand-written CSV nodes and edges in one pass.
Built using the gopengraph library for BloodHound OpenGraph compatibility.
- Multiple input methods: Read from stdin (pipeline) or specify files via command-line arguments
- Automatic CSV detection: Automatically recognizes node CSVs (with
idandkindscolumns) and edge CSVs (withstart,end, andkindcolumns) - graphify import: Convert a graphify knowledge-graph
graph.jsonstraight to OpenGraph (-g/--graphify) - Multiple file support: Process multiple CSV and/or graphify files in a single run
- Optional source_kind: Add a
source_kindto the metadata when needed - Built with gopengraph: Leverages the official BloodHound gopengraph library
go build -o ogProcess CSV files via pipeline:
cat nodes.csv edges.csv | og > output.jsonPiped stdin is read as CSV automatically only when no input flags are given.
Once any -c/--csv or -g/--graphify flag is present, stdin is consumed
only where an explicit - appears — so scripted runs never block on an idle
inherited pipe.
Process CSV files via command-line arguments:
og -c nodes.csv -c edges.csv > output.json
og --csv nodes.csv --csv edges.csv > output.jsonAdd a source_kind to the metadata:
cat nodes.csv edges.csv | og -s MyDataSource > output.json
cat nodes.csv edges.csv | og --source_kind MyDataSource > output.jsoncat nodes1.csv nodes2.csv edges1.csv edges2.csv | og > opengraph.jsonNode CSVs must have at minimum:
id: Unique identifier for the nodekinds: Node types (comma-separated for multiple kinds)
Additional columns become node properties.
Example:
id,kinds,displayname,email
user-001,User,Alice Smith,alice@example.com
dept-001,"Department,OrgUnit",Engineering,ITEdge CSVs must have:
start: ID of the start nodeend: ID of the end nodekind: Type of the relationship
Additional columns become edge properties.
Note: All edges use match_by: "id" in the OpenGraph output, meaning they reference nodes by their ID field.
Example:
start,end,kind,since
user-001,dept-001,MemberOf,2020-01-15This produces edges that reference nodes by ID:
{
"start": {
"match_by": "id",
"value": "user-001"
},
"end": {
"match_by": "id",
"value": "dept-001"
},
"kind": "MemberOf",
"properties": {
"since": "2020-01-15"
}
}graphify builds a code/knowledge
graph and writes a graph.json. og can convert that file directly to
OpenGraph — no intermediate CSV needed:
og -g graphify-out/graph.json -s Graphify > opengraph.json
og --graphify graph.json --source_kind Graphify > opengraph.json
cat graph.json | og -g - -s Graphify > opengraph.json # '-' reads JSON from stdinYou can mix graphify and CSV inputs in one run (IDs are shared across both).
When any -c/-g flag is present, stdin is only read on an explicit -:
cat extra_nodes.csv | og -g graph.json -c - -c more_edges.csv -s Combined > opengraph.jsonGenerate a graph.json with graphify first (code-only extraction needs no API key):
graphify extract ./your-repo --code-only # local AST, deterministic, no LLMThe converter is schema-tolerant: it accepts both graphify writers (edges under
links or edges), the {"graph": {...}} wrapper, and the common field-name
variants (src/dst, name-as-id, cluster, weight, object-valued
endpoints, …).
Nodes → OpenGraph nodes:
| OpenGraph | Source |
|---|---|
id |
graphify node id (id/node_id/name/…); colons are replaced with _ — BloodHound does not support colons in object ids |
kinds |
a single classified kind, inferred from node_type and label/file heuristics (e.g. Class, Endpoint, Entrypoint, Concept, Function) |
properties |
name, displayname, source_file, community, node_type, file_type, kind_heuristic, plus any extra scalar fields (numbers/bools preserved). BloodHound-reserved property names (objectid, ref) are never emitted |
Emitting a single classified kind is deliberate: gopengraph appends the
import-wide source_kind (-s) to every node on export, so each node ends up
with at most two kinds — within the cap that
cogs enforces. That means graphify output
merges cleanly:
og -g graph.json -s Graphify | cogs -j other-source.json -s Combined > merged.jsonEdges → OpenGraph edges:
| OpenGraph | Source |
|---|---|
kind |
PascalCased relation (calls → Calls, imports_from → ImportsFrom) |
start / end |
node ids, match_by: "id" |
properties |
relation, confidence, confidence_score, include (graphify's confidence filter: EXTRACTED, or INFERRED ≥ 0.85), plus extra scalar fields |
No edges are dropped — low-confidence ones are kept with include: false. A
source_kind (-s) is folded into every node's kinds so the whole import can
be filtered or deleted as a unit in BloodHound.
The tool generates BloodHound-compatible OpenGraph JSON with the following structure:
{
"graph": {
"nodes": [...],
"edges": [...]
},
"metadata": {
"source_kind": "..." // Only included if --source_kind/-s is provided
}
}Run the test suite:
go test -vTest with sample data:
cat testdata/nodes1.csv testdata/nodes2.csv testdata/edges1.csv testdata/edges2.csv | ./og -s ExampleSource-c, --csv: CSV file to process (can be specified multiple times;-reads CSV from stdin)-g, --graphify: graphifygraph.jsonto convert (can be specified multiple times;-reads JSON from stdin)-s, --source_kind: Source kind for the OpenGraph metadata (optional)
See the testdata/ directory for example CSV files:
nodes1.csv- User nodesnodes2.csv- Department nodesedges1.csv- MemberOf relationshipsedges2.csv- Permission relationships