Naar inhoud
Pivotick
Main Navigation Startpagina
Aan de slag
Configuratie
Gegenereerde API-documentatie
GalerijProjectenProbeer het uit!

Nederlands

English
简体中文
Français
Deutsch
Português
日本語
العربية
Español

Nederlands

English
简体中文
Français
Deutsch
Português
日本語
العربية
Español

Weergave

Sidebar Navigation

Aan de slag

Projecten met Pivotick

Configuratie

Callbacks

Verbindingslagen

Vereenvoudigen

Indeling

Weergave

Simulatie

Interface aanpassen

Zijbalk

Tooltip

Contextmenu

Filters

Legenda

Gegevenstabel

Interface vormgeven

Interface vertalen

Plug-ins

Pivotick-API

Pivots en verrijking

Pivotresultaten opslaan

Ongedaan maken en geschiedenis

Beveiliging

API-documentatie

Galerij

Basis

Knopen vormgeven

Verbindingen vormgeven

Indelingen

Gebeurtenissen en callbacks

Interface aanpassen

Bewerken en maken

Notities en annotaties

Filteren, zoeken en hiërarchie

Programmatische besturing

Thema's en prestaties

Pivots en verrijking

Uitgelichte voorbeelden

Draai en verrijk ​

paste 9f21 heeft 2.143 correlaties erachter. Als je ze allemaal zou ophalen, zou de grafiek begraven worden, dus een pivot onderhandelt in plaats daarvan: hij maakt reclame voor de telling, weigert voorbij een limiet te komen, en Hiermee kunt u zich beperken tot het getal er één is waar u daadwerkelijk naar kunt kijken.

Druk op P, klik op paste 9f21 en lees het paneel.

  • Er wordt niets gevraagd totdat u de draaimodus opent. Het selecteren van een knooppunt kost geen backend-oproepen.
  • ~2,143 draagt een tilde omdat dit de claim van de aanbieder is. 210 fetched is de eigen telling van de bibliotheek, dus dat is niet het geval.
  • De poort weigert terwijl de telling voorbij is maxCandidates: 2000, en zegt hoe hij moet worden opgetild.
  • Vink URLs aan en 210 komt terug onder de pet, dus Fetch wordt vanzelf ingeschakeld.
  • Wat er binnenkomt is niet de grafiek. De 210 kandidaten openen het tabblad Review van het dok. De canvas beweegt niet totdat je het inslikt.
  • Ingest is niet aan het opslaan. Zodra ze landen, staat op de voet van het paneel 420 unsaved — de 210 knooppunten en de 210 randen die daarbij horen — en biedt Opslaan. Schrijf ze terug uit is een tweede beslissing. Deze provider weigert elke vijfde node, zo luidt de toast Saved 378 of 420, het aantal daalt naar 42, en Opnieuw proberen verzendt precies die.

Of sla het paneel over: klik met de rechtermuisknop op paste 9f21 en op Pivot ▸ worden dezelfde providers weergegeven met de hetzelfde geldt, waarbij het kiezen van een rij ** de run is. Resultaten komen direct binnen, terwijl er maar weinig genoeg zijn nieuw zijn op het canvas en open het tabblad Controleren als er meer zijn.

Selecteer Event 5f2a voor de andere vorm: Objects & attributes declareert autoIngest en autoSave, dus de twaalf objecten landen en worden teruggeschreven zonder venster en zonder gebaar. Passive DNS is de derde toestand — helemaal geen save, dus niets wat het met zich meebrengt, wordt ooit geteld niet opgeslagen.

js
// A **pivot** is a runnable enrichment: two functions and some metadata. `summarize`
// says cheaply what is out there, and its facets become the narrowing controls; `fetch`
// goes and gets it. What comes back are *candidates*, staged in the dock for triage and
// never on the canvas, until someone ingests them.
//
// The whole point is the node with 2,143 correlations. Fetching all of them would ruin
// the graph, so the pivot advertises the count, declares a cap it refuses to fetch past,
// and lets the analyst narrow until the number is one they can actually look at.

/** What this fake source holds, by type. They sum to 2,143; URLs alone is 210. */
const BY_TYPE = [
    { value: 'domain', label: 'Domains', count: 1800 },
    { value: 'url', label: 'URLs', count: 210 },
    { value: 'paste', label: 'Pastes', count: 95 },
    { value: 'ip', label: 'IPs', count: 38 },
]

/** The types a narrowing chose, or all of them when it chose none. */
const chosen = (narrowing) => {
    const picked = Array.isArray(narrowing.type) ? narrowing.type.map(String) : []
    return picked.length ? BY_TYPE.filter((t) => picked.includes(t.value)) : BY_TYPE
}

const correlations = {
    id: 'correlations',
    label: 'Correlations',
    // Nothing correlates with an event, so the entry is absent for one.
    appliesTo: (nodes) => nodes.every((node) => node.getData()?.type !== 'event'),

    // Runs when Pivot mode is entered, and again whenever the origin or the narrowing
    // moves. A real one would POST the ids and the narrowing to a count endpoint.
    summarize: (nodes, narrowing) => ({
        total: chosen(narrowing).reduce((sum, t) => sum + t.count, 0),
        facets: [{
            key: 'type',
            label: 'Type',
            type: 'multiselect',
            options: BY_TYPE.map((t) => ({ label: t.label, value: t.value, count: t.count })),
        }],
    }),

    // The real call, reached only once the advertised count is under the cap.
    fetch: (nodes, narrowing) => {
        const origin = nodes[0]?.id ?? 'paste-9f21'
        const out = { nodes: [], edges: [] }
        for (const type of chosen(narrowing)) {
            // Capped at 210 for the demo's sake; a real source would return `count`.
            for (let i = 0; i < Math.min(type.count, 210); i++) {
                const id = `${type.value}-${i}`
                out.nodes.push({ id, data: { label: `${type.value} ${i}`, type: type.value } })
                out.edges.push({ from: origin, to: id })
            }
        }
        return out
    },

    // Refuse to fetch while the source claims more than this. Narrowing lifts it.
    maxCandidates: 2000,

    // The write half. Ingest puts candidates on the canvas; this is what puts them in
    // the source system, and it is the only thing that clears the panel's unsaved
    // count. A real one would POST them and report what the server accepted.
    //
    // This one deliberately refuses every fifth node, because a partial write is the
    // normal case at any scale and the retry is the interesting part: anything not
    // named in `savedNodeIds` stays unsaved, and **Retry** sends exactly that.
    save: ({ nodes, edges }) => {
        const written = nodes.filter((_, i) => i % 5 !== 0)
        const refused = nodes.length - written.length
        return {
            savedNodeIds: written.map((node) => node.id),
            savedEdgeIds: edges.map((edge) => edge.id),
            message: refused ? `${refused} refused by the server` : undefined,
        }
    },
}

// The other shape a pivot takes: a small, trusted result that needs no triage. It comes
// back as one container carrying its own children and lands straight on the canvas.
//
// These objects are the source's own already, so both halves are automatic: `autoIngest`
// lands them with no triage, `autoSave` writes them back with no gesture.
const expandEvent = {
    id: 'event-objects',
    label: 'Objects & attributes',
    appliesTo: (nodes) => nodes.length === 1 && nodes[0].getData()?.type === 'event',
    autoIngest: true,
    autoSave: true,
    save: () => true,
    fetch: ([node]) => ({
        nodes: [{
            id: `${node.id}-objects`,
            data: { label: 'Objects', type: 'container' },
            children: Array.from({ length: 12 }, (_, i) => ({
                id: `${node.id}-object-${i}`,
                data: { label: `attribute ${i}`, type: 'attribute' },
            })),
        }],
        edges: [{ from: node.id, to: `${node.id}-objects` }],
    }),
}

// A pivot with no `save` at all. Passive DNS is somebody else's observation, not ours to
// write anywhere — so its results are *not savable*: never counted unsaved, and no Save
// offered for them. That third state is what keeps the count honest.
const resolves = {
    id: 'passive-dns',
    label: 'Passive DNS',
    appliesTo: (nodes) => nodes.every((node) => node.getData()?.type === 'domain'),
    fetch: ([node]) => ({
        nodes: Array.from({ length: 6 }, (_, i) => ({
            id: `198.51.100.${20 + i}`,
            data: { label: `198.51.100.${20 + i}`, type: 'ip' },
        })),
        edges: Array.from({ length: 6 }, (_, i) => ({ from: node.id, to: `198.51.100.${20 + i}` })),
    }),
}

const options = {
    // Registering here rather than later is what makes the rail button correct on the
    // very first paint: `UI.pivotMode` defaults to `'auto'`, and the constructor's
    // pivots land before the UI is built.
    pivots: [correlations, expandEvent, resolves],
    UI: {
        mode: 'full',
        sidebar: { collapsed: true },
        // Triage happens in the dock, which starts folded: a staged set unfolds it
        // by itself when it arrives. The height is worth setting explicitly — the
        // default share leaves the candidate table too short to read.
        minimap: false,
        table: { open: false },
        dock: { height: 380 },
    },
    render: {
        defaultNodeStyle: {
            size: 8,
            color: (node) => ({
                paste: '#b48ead', domain: '#ebcb8b', ip: '#88c0d0',
                url: '#a3be8c', event: '#bf616a', container: '#5e81ac',
            }[node.getData()?.type] ?? '#8f9aa8'),
        },
    },
}
js
// A small investigation graph: what the analyst already has. Everything else in this
// card arrives through a pivot.
const data = {
    nodes: [
        { id: 'paste-9f21', data: { label: 'paste 9f21', type: 'paste' } },
        { id: 'evil.example', data: { label: 'evil.example', type: 'domain' } },
        { id: '198.51.100.7', data: { label: '198.51.100.7', type: 'ip' } },
        { id: 'event-5f2a', data: { label: 'Event 5f2a', type: 'event' } },
    ],
    edges: [
        { from: 'paste-9f21', to: 'evil.example' },
        { from: 'evil.example', to: '198.51.100.7' },
        { from: 'evil.example', to: 'event-5f2a' },
    ],
}

Volledige referentie: Pivots & verrijking, en Pivotresultaten opslaan voor de schrijfhelft eindigt deze kaart.

Pager
Volgende paginaAan de slag