跳转到内容
Pivotick
Main Navigation 首页
快速入门
配置
API 参考
示例库项目在线体验

简体中文

English
Français
Deutsch
Português

简体中文

English
Français
Deutsch
Português

外观

Sidebar Navigation

快速入门

使用 Pivotick 的项目

配置

回调

边图层

简化

布局

渲染

模拟

自定义界面

侧栏

提示框

上下文菜单

筛选

图例

数据表格

界面样式

翻译界面

插件

Pivotick API

拓展与数据增强

保存拓展结果

撤销与历史记录

安全

API 文档

示例库

基础

节点样式

边样式

布局

事件与回调

界面自定义

编辑与创建

备注与注释

筛选、搜索与层级

编程控制

主题与性能

拓展与数据增强

综合示例

拓展和丰富 ​

paste 9f21背后有2,143相关性。全部获取它们会掩盖图表,因此 pivot 会进行协商:它会公布计数,拒绝获取超过上限的数据,并让您缩小范围,直到数字成为您实际上可以查看的数字。

按P,单击paste 9f21,然后读取面板。

  • 在进入 Pivot 模式之前不会询问任何内容。 选择节点的后端调用成本为零。
  • ~2,143 带有波形符,因为它是提供商的声明。 210 fetched是图书馆自己的计数,所以不是。
  • 当计数超过maxCandidates: 2000时,门拒绝,并说明如何抬起它。
  • 勾选URLs,210 又回到上限之下,因此 Fetch 会自行打开。
  • 到达的不是图表。 210 位候选人打开了 Dock 的“审核”选项卡。在您摄取之前,画布不会移动。
  • 摄取不会保存。一旦着陆,面板底部会显示420 unsaved——随之而来的210个节点和210条边——并提供保存。将它们写回是第二个决定。该提供程序每五个节点都会拒绝一次,因此 toast 读取 Saved 378 of 420,计数下降到 42,并且 Retry 恰好发送这些内容。

或者跳过面板:右键单击 paste 9f21 和 透视 ▸ 列出具有相同计数的相同提供商,其中选择一行 * 是* 运行。当画布上新的结果很少时,结果会直接登陆,当有更多结果时,打开“审阅”选项卡。

选择 Event 5f2a 作为其他形状:Objects & attributes 声明 autoIngest 和 autoSave,因此它的 12 个对象落地并在没有窗格和手势的情况下写回。 Passive DNS是第三种状态——根本没有save,所以它带来的任何东西都不算未保存。

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

完整参考:拓展和丰富,以及保存拓展结果,用于此卡结束时的写入一半。

Pager
下一页快速入门