How to visualize STIX 2 data
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:
- Increase
idealEdgeLengthto ≥ 140 so labels don't overlap. - Disable randomization for re-layouts so the operator's mental model stays stable when they tweak the bundle.
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 →