المحورية والإثراء
paste 9f21 لديه 2,143 ارتباطات خلفه. جلبهم جميعا سوف يدفن الرسم البياني، لذلك يتفاوض محور بدلاً من ذلك: فهو يعلن عن العدد، ويرفض تجاوز الحد الأقصى، و يتيح لك التضييق حتى يصبح الرقم هو الرقم الذي يمكنك النظر إليه بالفعل.
اضغط على P، ثم انقر على paste 9f21، واقرأ اللوحة.
- لا يُطلب منك أي شيء حتى تدخل الوضع المحوري. لا يكلف تحديد العقدة أي مكالمات خلفية.
~2,143يرتدي التلدة لأنها مطالبة المزود.210 fetchedهو عدد المكتبة الخاصة، لذلك لا.- البوابة ترفض بينما انتهى العد
maxCandidates: 2000، وتقول كيفية رفعها. - ضع علامة على URLs وسيعود 210 أسفل الحد الأقصى، لذلك يتم تشغيل Fetch من تلقاء نفسه.
- ما يصل ليس الرسم البياني. يفتح المرشحون البالغ عددهم 210 علامة تبويب المراجعة في قفص الاتهام. ال القماش لا يتحرك حتى تبتلعه.
- التناول لا يعني الحفظ. بمجرد هبوطهم، تقرأ أسفل اللوحة
420 unsaved— ال 210 عقدة و210 حواف تأتي معها - وتقدم حفظ. كتابتهم مرة أخرى الخروج هو القرار الثاني. يرفض هذا المزود كل عقدة خامسة، لذلك يقرأ النخبSaved 378 of 420، ينخفض العدد إلى42، وإعادة المحاولة يرسل تلك بالضبط.
أو تخطي اللوحة: انقر بزر الماوس الأيمن على paste 9f21 وPivot ▸ يسرد نفس المزودين الذين لديهم نفس التهم، حيث اختيار صف هو التشغيل. النتائج تهبط مباشرة في حين أن القليل منها يكفي جديدة على اللوحة القماشية، وافتح علامة التبويب مراجعة عندما يكون هناك المزيد.
حدد Event 5f2a للشكل الآخر: Objects & attributes يعلن autoIngest و autoSave، لذا فإن كائناتها الاثني عشر تهبط وتتم كتابتها مرة أخرى بدون جزء أو إيماءة. Passive DNS هي الحالة الثالثة — لا save على الإطلاق، لذلك لا يتم احتساب أي شيء يجلبه على الإطلاق غير محفوظ.
// 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'),
},
},
}// 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' },
],
}المرجع الكامل: المحاور والإثراء، و حفظ النتائج المحورية لكتابة نصف هذه البطاقة تنتهي.