Pular para o conteúdo
Pivotick
Main Navigation Início
Primeiros passos
Configuração
Referência da API
GaleriaProjetosExperimente!

Português

English
简体中文
Français
Deutsch

Português

English
简体中文
Français
Deutsch

Aparência

Sidebar Navigation

Primeiros passos

Projetos que usam o Pivotick

Configuração

Funções de retorno

Camadas de arestas

Simplificar

Layout

Renderização

Simulação

Personalizar a interface

Barra lateral

Dica

Menu de contexto

Filtros

Legenda

Tabela de dados

Estilo da interface

Traduzir a interface

Plugins

API Pivotick

Pivôs e enriquecimento

Salvar resultados dos pivôs

Desfazer e histórico

Segurança

Documentação da API

Galeria

Fundamentos

Estilo dos nós

Estilo das arestas

Layouts

Eventos e funções de retorno

Personalização da interface

Edição e criação

Notas e anotações

Filtros, pesquisa e hierarquia

Controle por programação

Temas e desempenho

Pivôs e enriquecimento

Demonstrações completas

Dinamizar e enriquecer ​

paste 9f21 tem 2.143 correlações por trás dele. Buscar todos eles enterraria o gráfico, então, em vez disso, um pivot negocia: ele anuncia a contagem, se recusa a ultrapassar um limite e permite restringir até que o número seja um que você possa realmente ver.

Pressione P, clique em paste 9f21 e leia o painel.

  • Nada é solicitado até que você entre no modo Pivot. Selecionar um nó não custa nenhuma chamada de back-end.
  • ~2,143 usa um til porque é a reivindicação do provedor. 210 fetched é a contagem da própria biblioteca, então isso não acontece.
  • O portão recusa enquanto a contagem termina maxCandidates: 2000 e diz como levantá-lo.
  • Tick URLs e 210 volta para baixo do limite, então Fetch é ativado sozinho.
  • O que chega não é o gráfico. Os 210 candidatos abrem a aba Revisão do dock. A tela não se move até você ingerir.
  • Ingerir não é salvar. Assim que pousam, a parte inferior do painel indica 420 unsaved — os 210 nós e as 210 arestas que vieram com eles — e oferece Salvar. Escrevê-los de volta é uma segunda decisão. Este provedor recusa cada quinto nó, então o brinde lê Saved 378 of 420, a contagem cai para 42 e Retry envia exatamente esses.

Ou pule o painel: clique com o botão direito em paste 9f21 e Pivot ▸ lista os mesmos provedores com as mesmas contagens, onde escolher uma linha é a execução. Os resultados chegam diretamente, embora poucos deles sejam novos na tela, e abra a guia Revisão quando houver mais.

Selecione Event 5f2a para a outra forma: Objects & attributes declara autoIngest e autoSave, de modo que seus doze objetos pousem e sejam escritos de volta sem painel e sem gesto. Passive DNS é o terceiro estado - não há save, então nada que ele traz é contado como não salvo.

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' },
    ],
}

Referência completa: Pivots e enriquecimento e Salvando os resultados do pivô para a metade de gravação em que este cartão termina.

Pager
Próxima páginaPrimeiros passos