ピボット&エンリッチ
paste 9f21 の背後には 2,143 の相関関係があります。それらをすべて取得するとグラフが埋もれてしまいます。 そのため、代わりに pivot がネゴシエートします。カウントをアドバタイズし、上限を超えたフェッチを拒否し、 実際に確認できる数値になるまで絞り込むことができます。
P を押し、paste 9f21 をクリックしてパネルを読みます。
- ピボット モードに入るまでは何も尋ねられません。 ノードの選択にはバックエンド呼び出しのコストはかかりません。
~2,143にはチルダ が付いています。これはプロバイダーの主張だからです。210 fetchedは ライブラリ自体がカウントするため、カウントされません。- カウントが
maxCandidates: 2000を超えている間、ゲートはを拒否し、ゲートを持ち上げる方法を指示します。 - URLs にチェックを入れると、210 がキャップの下に戻ってくるので、Fetch が自動的にオンになります。
- 到着するのはグラフではありません。 210 人の候補者がドックの [レビュー] タブを開きます。の キャンバスは取り込むまで動きません。
- 取り込みは保存されていません。 着陸すると、パネルの下部に
420 unsavedと表示されます。 210 個のノードとそれに付属する 210 個のエッジ — そして、節約 を提供します。書き戻す アウトは2番目の決定です。このプロバイダーは 5 ノードごとに拒否するため、トーストは次のようになります。Saved 378 of 420、カウントは42に減り、再試行 はそれらを正確に送信します。
または、パネルをスキップします。paste 9f21 を右クリックし、Pivot ▸ と同じプロバイダーがリストされます。 同じカウントで、行 * を選択すると* が実行となります。結果は十分に少ないにもかかわらず、直接表示されます キャンバスに初めて参加する人がいる場合は、「レビュー」タブを開きます。
他の形状として Event 5f2a を選択します。 Objects & attributes は autoIngest を宣言し、 autoSave なので、その 12 個のオブジェクトが着地し、ペインやジェスチャなしで書き戻されます。 Passive DNS は 3 番目の状態です。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' },
],
}完全なリファレンス: ピボットとエンリッチメント、および ピボット結果の保存 このカードの書き込みは半分で終わります。