How to visualize STIX 2 data

Updated 2026-05-04 · 6 min read

STIX 2.1 bundles look intimidating in raw JSON: dozens of object types, opaque --UUID identifiers, and a flat objects array that hides every relationship. The remedy is a graph view — but a good one, not a hairball. This guide walks through the choices that turn a STIX bundle into a readable STIX 2 relationship graph.

1. Pick the right node set

Not every STIX object belongs in the graph. Render SDOs (indicator, malware, threat-actor, intrusion-set, campaign, infrastructure, vulnerability, attack-pattern, tool, identity, location) as nodes. Render SROs (relationship, sighting) as edges. Skip SCOs by default — observable objects (file, domain-name, ipv4-addr) are usually better expressed as the pattern on an indicator than as their own nodes. Re-introduce them only when you need to show shared infrastructure.

2. Map types to icons consistently

Eyes parse shape and color faster than text. Use a fixed icon-to-type map and don't change it across reports. The OASIS-style icon conventions (skull for malware, mask for threat-actor, target for indicator, gear for tool) are battle-tested. ThreatGraph ships an opinionated set of STIX 2 object icons in ThreatGraph and binds them to types via the cytoscape stylesheet.

3. Use a force-directed layout — but tame it

The default cose-bilkent layout produces good results for 50–300 nodes. Two tweaks that pay off:

4. Edges should explain themselves

STIX relationship_type values are short — indicates, uses, attributed-to — so always render them as edge labels. Use text-rotation: autorotate so labels follow the line angle. Add a triangle arrowhead pointed from source_ref to target_ref — the direction is non-obvious and skipping it is a frequent mistake.

5. Optimize for the report, not just the screen

Always export at 4× scale, on white. ThreatGraph's "report mode" toggle increases node sizes by ~20%, adds a soft drop shadow, and renders labels with a 2 px white outline so they survive whatever Word does to your PNG.

cy.png({
  full: true,
  scale: 4,
  bg: "#FFFFFF"
})

That single function call is the difference between a 1024-px hairball and a crisp 4096-px image you'd staple to a memo.

6. Keep the JSON editable

The graph is the lens, not the source of truth. Keep the bundle JSON in an editor pane next to the graph; round-trip your edits through the graph for sanity, but treat the JSON as the artifact you ship.

Try it on your bundle

The fastest way to see all of this in practice is to open the workspace and paste a bundle. Everything described above is wired in by default.

Open the workspace →