Simplifier le graphique
La majeure partie de ce graphique est une répétition : huit adresses IP sur les deux événements, six domaines sur un, neuf fichiers sur un hôte, dix observations. UI.simplify.rules plie les nœuds qui jouent le même rôle en un groupe dessiné à leur place. Les données sont intactes : graph.getNodes() renvoie toujours chaque nœud.
Deux règles s'appliquent ici, dans l'ordre. Par capteur est propre à l'application : elle donne à chaque observation son capteur comme clé, et les nœuds partageant une clé deviennent un groupe. La règle de voisin intégrée plie ensuite les nœuds d'un type liés exactement aux mêmes nœuds.
- Ouvrez le mode Simplify sur le rail pour désactiver une règle ou modifier son plus petit groupe.
- Double-cliquez sur un groupe pour remettre ses membres sur le canevas. Faites glisser la puce au-dessus d'eux pour les rapprocher ; son × les replie.
- Sélectionnez un groupe : la barre latérale répertorie ses membres, chacun avec Retirez.
La page Simplify couvre les règles, les règles personnalisées et l'apparence du groupe.
js
const plural = { ip: 'IPs', domain: 'domains', file: 'files', sighting: 'sightings' }
const simplify = {
rules: [
// An app rule: a key per node, and nodes sharing a key become one group.
// It runs first, so the sightings split by sensor before the neighbour
// rule would fold all ten of them together.
{
kind: 'custom',
id: 'bySensor',
label: 'By sensor',
description: 'Sightings reported by the same sensor.',
partition: (view) => new Map(view.nodes
.filter(node => view.typeOf(node) === 'sighting')
.map(node => [node.id, node.getData().sensor])),
},
// The built-in rule: one type, exactly the same neighbours, five or more.
{ kind: 'neighbours', minSize: 5 },
],
typeLabel: (type, count) => `${count} ${plural[type] ?? type}`,
}js
const options = {
UI: {
mode: 'full',
// Off: this card is about neither the minimap nor the data dock.
minimap: false,
table: false,
simplify,
},
// Longer links, so the labels under the groups have room.
simulation: { physics: 'manual', d3LinkDistance: 70 },
render: {
nodeTypeAccessor: (node) => node.getData().type,
nodeStyleMap: {
event: { color: '#7c3aed', size: 16 },
host: { color: '#475569', size: 14 },
ip: { color: '#0ea5e9' },
domain: { color: '#10b981' },
file: { color: '#e11d48' },
sighting: { color: '#f59e0b' },
},
// A group made by the app's rule names its sensor.
groupStyle: (group) => group.rule === 'bySensor'
? { text: () => `${group.members.length} from ${group.members[0].getData().sensor}` }
: undefined,
},
}js
// Two events share eight IPs, the first alone has six domains, a host has nine
// files, and the second event has ten sightings from two sensors.
const ips = Array.from({ length: 8 }, (_, i) => ({ id: `ip-${i}`, data: { type: 'ip', label: `198.51.100.${10 + i}` } }))
const domains = Array.from({ length: 6 }, (_, i) => ({ id: `dom-${i}`, data: { type: 'domain', label: `site-${i}.example` } }))
const files = Array.from({ length: 9 }, (_, i) => ({ id: `file-${i}`, data: { type: 'file', label: `sample-${i}.bin` } }))
const sightings = Array.from({ length: 10 }, (_, i) => ({
id: `seen-${i}`,
data: { type: 'sighting', label: `Sighting ${i + 1}`, sensor: i < 6 ? 'north' : 'south' },
}))
const data = {
nodes: [
{ id: 'ev-1', data: { type: 'event', label: 'Event 1' } },
{ id: 'ev-2', data: { type: 'event', label: 'Event 2' } },
{ id: 'host', data: { type: 'host', label: 'Build host' } },
...ips, ...domains, ...files, ...sightings,
],
edges: [
...ips.flatMap(ip => [{ from: 'ev-1', to: ip.id }, { from: 'ev-2', to: ip.id }]),
...domains.map(domain => ({ from: 'ev-1', to: domain.id })),
...files.map(file => ({ from: 'host', to: file.id })),
...sightings.map(sighting => ({ from: 'ev-2', to: sighting.id })),
{ from: 'ev-2', to: 'host' },
],
}