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 event glyph; 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 html render: 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 name against misp-iconify's ~213 known Object glyphs; falls back to a generic object icon 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 type against misp-iconify's ~51 known Attribute glyphs; generic attribute icon otherwise.
  • Title vs. badge — the card's title is the attribute's value (what an analyst actually reads — an IP, a hash, ...); its type shows 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 colour field 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 generic galaxy icon; 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 type slug, generic galaxy icon 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 generic sighting icon; 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 generic sighting icon 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' })