Ir al contenido
Pivotick
Main Navigation Inicio
Primeros pasos
Configuración
Documentación generada de la API
GaleríaProyectos¡Pruébalo!

Español

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

Español

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

Apariencia

Sidebar Navigation

Primeros pasos

Proyectos que usan Pivotick

Configuración

Funciones de retorno

Capas de aristas

Simplificar

Distribución

Renderizado

Simulación

Personalizar la interfaz

Barra lateral

Información emergente

Menú contextual

Filtros

Leyenda

Tabla de datos

Estilos de la interfaz

Traducir la interfaz

Complementos

API de Pivotick

Pivotes y enriquecimiento

Guardar resultados de pivotes

Deshacer e historial

Seguridad

Documentación de la API

Galería

Fundamentos

Estilos de nodos

Estilos de aristas

Distribuciones

Eventos y funciones de retorno

Personalización de la interfaz

Edición y creación

Notas y anotaciones

Filtros, búsqueda y jerarquía

Control programático

Temas y rendimiento

Pivotes y enriquecimiento

Ejemplos destacados

Pivotar y enriquecer ​

paste 9f21 tiene 2,143 correlaciones detrás. Recuperarlos a todos enterraría el gráfico, entonces, en su lugar, un pivote negocia: anuncia el conteo, se niega a superar un límite y le permite limitar hasta que el número sea uno que realmente pueda mirar.

Presione P, haga clic en paste 9f21 y lea el panel.

  • No se pregunta nada hasta que ingresa al modo Pivote. Seleccionar un nodo no cuesta llamadas de backend.
  • ~2,143 lleva tilde porque es el reclamo del proveedor. 210 fetched es el cuenta la propia biblioteca, por lo que no es así.
  • La puerta se niega mientras termina la cuenta maxCandidates: 2000 y dice cómo levantarla.
  • Marque URLs y 210 vuelve debajo del límite, por lo que Fetch se activa solo.
  • Lo que llega no es el gráfico. Los 210 candidatos abren la pestaña Revisar del muelle. el El lienzo no se mueve hasta que lo ingiere.
  • La ingesta no se guarda. Una vez que aterrizan, al pie del panel se lee 420 unsaved — el 210 nodos y los 210 bordes que vienen con ellos, y ofrece Guardar. Escribiéndolos de vuelta salir es una segunda decisión. Este proveedor rechaza uno de cada cinco nodos, por lo que el brindis dice Saved 378 of 420, el recuento cae a 42 y Reintentar envía exactamente esos.

O omita el panel: haga clic con el botón derecho en paste 9f21 y Pivote ▸ enumera los mismos proveedores con el Lo mismo cuenta, donde elegir una fila * es* la carrera. Los resultados llegan directamente, aunque pocos son suficientes. son nuevos en el lienzo y abre la pestaña Revisar cuando haya más.

Seleccione Event 5f2a para la otra forma: Objects & attributes declara autoIngest y autoSave, por lo que sus doce objetos aterrizan y se escriben sin panel ni gesto. Passive DNS es el tercer estado; no hay save en absoluto, por lo que nunca se cuenta nada de lo que aporta no 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' },
    ],
}

Referencia completa: Pivotes y enriquecimiento y Guardar resultados de pivote para la mitad de escritura en la que termina esta tarjeta.

Pager
Página siguientePrimeros pasos