Getting started
Using Pivotick Graph Transformer
This repo has no npm package and no build step for the library itself — it's meant to be added as a git submodule directly inside a Pivotick-based app, which then imports the TypeScript source by relative path.
Installation
Add it as a submodule and pull in its own submodule (the vendored misp-iconify icon set):
git submodule add https://github.com/ecrou-exact/pivotick-graph-transformer.git vendor/pivotick-graph-transformer
git submodule update --init --recursive
Usage in your code
Importing a format's package (e.g. packages/misp/src/index) registers its converter with
the shared GraphRegistry. Convert your source JSON to get the exact { nodes, edges }
shape Pivotick's own constructor expects, and hand that straight to it:
import { Pivotick } from 'pivotick'
import { GraphRegistry } from './vendor/pivotick-graph-transformer/packages/core/src/index'
import './vendor/pivotick-graph-transformer/packages/misp/src/index' // registers the MISP importer
const mispEvent = { Event: { /* ...your MISP Event export... */ } }
// { nodes, edges } — ready to hand straight to Pivotick.
const data = GraphRegistry.getImporter('misp').convert(mispEvent, { theme: 'light' })
new Pivotick(document.getElementById('app'), data, {
isDirected: true,
render: { type: 'svg' },
layout: { type: 'force' },
UI: { theme: 'light' } // Pivotick reads its own theme from here, not a top-level `theme`
})
Every section below is exactly this pipeline against a small example fixture — the same converter the Demo page uses on a full real event.
MISP concept reference
How each MISP concept becomes a graph
Event
The root of every MISP export. It always becomes the graph's root node.
- Color — a fixed teal accent (
#1892B1), Event's own identity color; it never varies. - Icon — misp-iconify's single generic
eventglyph; there's only one kind of Event. - Shape — Pivotick has no native rectangle (only circle/square/triangle/hexagon, always as-wide-as-tall), so the visible card is a custom
htmlrender: an icon in a hexagon-clipped badge, the title beside it, inside a bordered rounded rectangle. - Size — the largest text/icon of any concept, so it reads as the starting point at a glance regardless of how long its title is.
Object
A MISP Object (e.g. file, domain-ip) hangs off its Event as a child node.
- Color — a fixed muted brown-grey (
#524948), deliberately neutral: dozens of different Object types share it, only the icon changes. - Icon — looked up by the Object's own
nameagainst misp-iconify's ~213 known Object glyphs; falls back to a genericobjecticon when the name isn't recognized. - Badge — the Object's
meta-category(e.g. "file", "network") as a small pill under the name. - Size — the middle tier, between Event (biggest) and Attribute (smallest).
Attribute
An Attribute can attach directly to an Event, or nest inside an Object (carrying an
object_relation — its role within that Object's template).
- Color — a fixed yellow-green (
#97CC04), the quietest of the three card colors: a graph can have dozens of Attributes, and they shouldn't compete with Event/Object for attention. - Icon — looked up by the Attribute's
typeagainst misp-iconify's ~51 known Attribute glyphs; genericattributeicon otherwise. - Title vs. badge — the card's title is the attribute's value (what an analyst actually reads — an IP, a hash, ...); its
typeshows as a small badge underneath, never squeezed into one "type: value" string. - Size — the smallest tier: Attributes are the graph's leaves.
Directly on an Event
Nested in an Object
Tag
A freeform or taxonomy label attached to an Event, Object, or Attribute.
- Color — no fixed accent: the tag's own
colourfield is the chip's background, whatever an analyst or taxonomy picked. - Text color — automatically switched to black or white based on that background's perceived luminance, so a tag stays readable no matter how light or dark its own color is (see "tlp:white" below: a near-white background gets black text and a subtle border so it doesn't disappear against the page).
- Shape — a rounded pill, not a card: a tag is a short label, not an entity with its own sub-fields, so it doesn't need an icon badge or a properties panel's worth of room.
- Deduplication — the same tag id shared by several entities collapses into one shared node with an edge from each — see "source:community" below, attached to both the Event and its Attribute.
Galaxy / Galaxy Cluster
Driven by an entity's own structured Galaxy array — independent of any matching
is_galaxy Tag, which still renders separately as a plain Tag chip (Tags and Galaxies are two
parallel MISP representations, not one a variant of the other).
- "Galaxy clusters" (grouping node) — a fixed purple (
#8B5CF6) with the genericgalaxyicon; it signals "this entity's galaxies are grouped here", it isn't a galaxy itself. - Galaxy type (e.g. "Attack Pattern") — color is not fixed: a deterministic hash of the galaxy's display name (ported directly from MISP's own "Overmind" theme) picks a hue, so the same galaxy type renders in the same color family everywhere. Icon comes from misp-iconify's ~131 known galaxy-type glyphs by the galaxy's
typeslug, genericgalaxyicon otherwise. - Cluster detail (e.g. "Valid Accounts - T1078") — a pill in the same hue family as its parent type, with a translucent tinted background (rather than solid) plus a subtle metallic-sheen gradient, matching MISP's own badge look.
Sighting
Sightings on an Attribute summarize into one "Sightings" node rather than one node per raw Sighting — a widely-seen indicator can have dozens.
- "Sightings" (grouping node) — a fixed magenta (
#890096, deliberately a different purple from Galaxy's, so the two grouping concepts stay visually distinct) with the genericsightingicon; its badge is the total count. - Positive — green (
#16A34A), a hand-drawn thumb-up pictogram (misp-iconify has no like/dislike icon, so this one connected shape — a "fist" block with a notch sticking out the top — is drawn here instead). - False positive — red (
#DC2626), the same pictogram mirrored so the notch points down. - Expired — amber (
#F59E0B), falling back to the genericsightingicon since no dedicated pictogram exists. - Each of the three only appears if its count is above zero.
viewMode
Choosing how much of the graph to show
Same MISP data every time — options.viewMode on the convert() call above is
what changes which nodes are nested, filtered, or left out.
Detailed (default)
Every Tag, Attribute, and Object hangs directly off its parent, flat — nothing nested, nothing filtered. This is what every example in the MISP concept reference above already shows.
// viewMode omitted entirely — same result as passing 'detailed'
const data = GraphRegistry.getImporter('misp').convert(mispEvent)
Grouped
Each parent's Tags and Attributes collapse behind one summary node apiece — Pivotick's own
node-expansion "+" (RendererOptions.enableNodeExpansion), so a busy Event reads as a
handful of clickable groups instead of every leaf at once. Objects and the Event root are unaffected; a
Galaxy's own "Galaxy clusters" grouping nests two levels deep the same way. Same Event as Detailed
above — only viewMode changed.
const data = GraphRegistry.getImporter('misp').convert(mispEvent, { viewMode: 'grouped' })
Relations
No Event root, no Tags/Galaxies/Sightings — only what participates in the Object Reference graph. An Object shows only when it references something, is referenced itself, or owns a referenced Attribute; a bare event-level Attribute shows only when some Object references it. A shown Object nests all of its own Attributes behind its own native "+" instead of beside it — the Object is the expandable unit here, unlike Grouped's separate summary node.
Below: the file Object connects to network-connection and to the IP it
drops (an event-level Attribute, pulled in only because it's referenced); the unreferenced
vulnerability Object and the plain comment Attribute are both left out entirely.
const data = GraphRegistry.getImporter('misp').convert(mispEvent, { viewMode: 'relations' })
Correlation
Not a convert() viewMode — its input is every Event at once, not one, so it's a
separate method: correlateEvents(events, options). Two signals, both drawn: MISP's own
precomputed RelatedEvent (read straight from the export — this is what makes it work even
when Attribute is empty, a summary response rather than a full pull), and any Attribute
value shared by two or more different Events (honouring disable_correlation), which
becomes one shared hub node with an edge to every Event that carries it.
Below: two Events sharing the same ip-dst value link through a shared hub; the first
Event's own RelatedEvent also names a third, older Event — shown from just the summary
MISP sent along with it, since that's all a related Event ever carries.
import { MispEventImporter } from './vendor/pivotick-graph-transformer/packages/misp/src/index'
const importer = GraphRegistry.getImporter('misp') as MispEventImporter
const data = importer.correlateEvents([eventA, eventB], { viewMode: 'correlation' })