chainlesschain 0.47.8 → 0.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/chainlesschain.js +0 -0
- package/package.json +10 -8
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/activitypub.js +533 -0
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +441 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/social.js +265 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +114 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/ueba.js +403 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Social Graph — typed directed edges between DIDs, with a real-time
|
|
3
|
+
* event stream for "graph changed" notifications.
|
|
4
|
+
*
|
|
5
|
+
* Scope (CLI-first MVP):
|
|
6
|
+
* - add/remove edges (follow / friend / like / mention / block)
|
|
7
|
+
* - neighbor queries (out/in, optional edge type filter)
|
|
8
|
+
* - graph snapshot (nodes + edges) for one-shot exports
|
|
9
|
+
* - EventEmitter-style subscribe/unsubscribe so any consumer
|
|
10
|
+
* (CLI `watch`, future WebSocket route, Desktop renderer) can
|
|
11
|
+
* render graph changes live instead of polling
|
|
12
|
+
*
|
|
13
|
+
* What this module is NOT:
|
|
14
|
+
* - A centrality / community-detection engine. That lives in the
|
|
15
|
+
* desktop `social-graph.js` and operates on snapshots produced
|
|
16
|
+
* here. Keeping the CLI lib thin makes the event stream easy to
|
|
17
|
+
* reason about and test.
|
|
18
|
+
*
|
|
19
|
+
* Persistence: optional. When a `db` handle is passed into
|
|
20
|
+
* `ensureGraphTables(db)` and mutation helpers, edges survive
|
|
21
|
+
* process restarts; otherwise the graph is in-memory only.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { EventEmitter } from "events";
|
|
25
|
+
|
|
26
|
+
/* ── Edge types ────────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
export const EDGE_TYPES = Object.freeze([
|
|
29
|
+
"follow",
|
|
30
|
+
"friend",
|
|
31
|
+
"like",
|
|
32
|
+
"mention",
|
|
33
|
+
"block",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function _validateEdgeType(edgeType) {
|
|
37
|
+
if (!EDGE_TYPES.includes(edgeType)) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Invalid edge type "${edgeType}"; expected one of ${EDGE_TYPES.join(", ")}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ── In-memory state ──────────────────────────────────────── */
|
|
45
|
+
|
|
46
|
+
// Adjacency: Map<sourceDid, Map<`${targetDid}|${type}`, edge>>
|
|
47
|
+
const _outgoing = new Map();
|
|
48
|
+
// Reverse adjacency: Map<targetDid, Map<`${sourceDid}|${type}`, edge>>
|
|
49
|
+
const _incoming = new Map();
|
|
50
|
+
// Node metadata: Map<did, { did, firstSeen, lastSeen, edgeCount }>
|
|
51
|
+
const _nodes = new Map();
|
|
52
|
+
|
|
53
|
+
// Shared EventEmitter. Bumped to a high max so many CLI `watch`
|
|
54
|
+
// processes or downstream WS clients don't trip the listener warning.
|
|
55
|
+
const _bus = new EventEmitter();
|
|
56
|
+
_bus.setMaxListeners(0);
|
|
57
|
+
|
|
58
|
+
/* ── Schema ───────────────────────────────────────────────── */
|
|
59
|
+
|
|
60
|
+
export function ensureGraphTables(db) {
|
|
61
|
+
if (!db) return;
|
|
62
|
+
db.exec(`
|
|
63
|
+
CREATE TABLE IF NOT EXISTS social_graph_edges (
|
|
64
|
+
source_did TEXT NOT NULL,
|
|
65
|
+
target_did TEXT NOT NULL,
|
|
66
|
+
edge_type TEXT NOT NULL,
|
|
67
|
+
weight REAL DEFAULT 1.0,
|
|
68
|
+
metadata TEXT,
|
|
69
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
70
|
+
updated_at TEXT DEFAULT (datetime('now')),
|
|
71
|
+
PRIMARY KEY (source_did, target_did, edge_type)
|
|
72
|
+
)
|
|
73
|
+
`);
|
|
74
|
+
db.exec(`
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_social_graph_source
|
|
76
|
+
ON social_graph_edges (source_did, edge_type);
|
|
77
|
+
`);
|
|
78
|
+
db.exec(`
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_social_graph_target
|
|
80
|
+
ON social_graph_edges (target_did, edge_type);
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Helpers ──────────────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
function _edgeKey(targetDid, edgeType) {
|
|
87
|
+
return `${targetDid}|${edgeType}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _touchNode(did, now) {
|
|
91
|
+
let node = _nodes.get(did);
|
|
92
|
+
if (!node) {
|
|
93
|
+
node = { did, firstSeen: now, lastSeen: now, edgeCount: 0 };
|
|
94
|
+
_nodes.set(did, node);
|
|
95
|
+
_bus.emit("node:added", { did, at: now });
|
|
96
|
+
} else {
|
|
97
|
+
node.lastSeen = now;
|
|
98
|
+
}
|
|
99
|
+
return node;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function _incEdgeCount(did, delta) {
|
|
103
|
+
const node = _nodes.get(did);
|
|
104
|
+
if (!node) return;
|
|
105
|
+
node.edgeCount = Math.max(0, node.edgeCount + delta);
|
|
106
|
+
if (node.edgeCount === 0 && delta < 0) {
|
|
107
|
+
_nodes.delete(did);
|
|
108
|
+
_bus.emit("node:removed", { did });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Mutations ────────────────────────────────────────────── */
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add or update a directed edge. Idempotent — same (source, target,
|
|
116
|
+
* type) triple gets its weight/metadata updated and emits `edge:updated`
|
|
117
|
+
* instead of `edge:added`.
|
|
118
|
+
*
|
|
119
|
+
* @returns {{edge: object, created: boolean}}
|
|
120
|
+
*/
|
|
121
|
+
export function addEdge(db, sourceDid, targetDid, edgeType, opts = {}) {
|
|
122
|
+
if (!sourceDid || !targetDid) {
|
|
123
|
+
throw new Error("sourceDid and targetDid are required");
|
|
124
|
+
}
|
|
125
|
+
if (sourceDid === targetDid) {
|
|
126
|
+
throw new Error("Self-edges are not allowed");
|
|
127
|
+
}
|
|
128
|
+
_validateEdgeType(edgeType);
|
|
129
|
+
const weight = Number.isFinite(opts.weight) ? opts.weight : 1.0;
|
|
130
|
+
const metadata = opts.metadata ?? null;
|
|
131
|
+
const now = opts.timestamp || new Date().toISOString();
|
|
132
|
+
|
|
133
|
+
_touchNode(sourceDid, now);
|
|
134
|
+
_touchNode(targetDid, now);
|
|
135
|
+
|
|
136
|
+
const outBucket = _outgoing.get(sourceDid) || new Map();
|
|
137
|
+
const inBucket = _incoming.get(targetDid) || new Map();
|
|
138
|
+
const outKey = _edgeKey(targetDid, edgeType);
|
|
139
|
+
const inKey = _edgeKey(sourceDid, edgeType);
|
|
140
|
+
|
|
141
|
+
const existing = outBucket.get(outKey);
|
|
142
|
+
const created = !existing;
|
|
143
|
+
const edge = {
|
|
144
|
+
sourceDid,
|
|
145
|
+
targetDid,
|
|
146
|
+
edgeType,
|
|
147
|
+
weight,
|
|
148
|
+
metadata,
|
|
149
|
+
createdAt: existing ? existing.createdAt : now,
|
|
150
|
+
updatedAt: now,
|
|
151
|
+
};
|
|
152
|
+
outBucket.set(outKey, edge);
|
|
153
|
+
inBucket.set(inKey, edge);
|
|
154
|
+
_outgoing.set(sourceDid, outBucket);
|
|
155
|
+
_incoming.set(targetDid, inBucket);
|
|
156
|
+
|
|
157
|
+
if (created) {
|
|
158
|
+
_incEdgeCount(sourceDid, 1);
|
|
159
|
+
_incEdgeCount(targetDid, 1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (db) {
|
|
163
|
+
const stmt = db.prepare(`
|
|
164
|
+
INSERT INTO social_graph_edges (source_did, target_did, edge_type, weight, metadata, created_at, updated_at)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
166
|
+
ON CONFLICT(source_did, target_did, edge_type) DO UPDATE SET
|
|
167
|
+
weight = excluded.weight,
|
|
168
|
+
metadata = excluded.metadata,
|
|
169
|
+
updated_at = excluded.updated_at
|
|
170
|
+
`);
|
|
171
|
+
stmt.run(
|
|
172
|
+
sourceDid,
|
|
173
|
+
targetDid,
|
|
174
|
+
edgeType,
|
|
175
|
+
weight,
|
|
176
|
+
metadata ? JSON.stringify(metadata) : null,
|
|
177
|
+
edge.createdAt,
|
|
178
|
+
edge.updatedAt,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
_bus.emit(created ? "edge:added" : "edge:updated", { ...edge });
|
|
183
|
+
_bus.emit("change", {
|
|
184
|
+
kind: created ? "edge:added" : "edge:updated",
|
|
185
|
+
edge: { ...edge },
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return { edge: { ...edge }, created };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Remove an edge. Returns { removed: boolean, edge? }.
|
|
193
|
+
*/
|
|
194
|
+
export function removeEdge(db, sourceDid, targetDid, edgeType) {
|
|
195
|
+
_validateEdgeType(edgeType);
|
|
196
|
+
const outBucket = _outgoing.get(sourceDid);
|
|
197
|
+
const inBucket = _incoming.get(targetDid);
|
|
198
|
+
if (!outBucket || !inBucket) return { removed: false };
|
|
199
|
+
|
|
200
|
+
const outKey = _edgeKey(targetDid, edgeType);
|
|
201
|
+
const edge = outBucket.get(outKey);
|
|
202
|
+
if (!edge) return { removed: false };
|
|
203
|
+
|
|
204
|
+
outBucket.delete(outKey);
|
|
205
|
+
inBucket.delete(_edgeKey(sourceDid, edgeType));
|
|
206
|
+
if (outBucket.size === 0) _outgoing.delete(sourceDid);
|
|
207
|
+
if (inBucket.size === 0) _incoming.delete(targetDid);
|
|
208
|
+
|
|
209
|
+
_incEdgeCount(sourceDid, -1);
|
|
210
|
+
_incEdgeCount(targetDid, -1);
|
|
211
|
+
|
|
212
|
+
if (db) {
|
|
213
|
+
db.prepare(
|
|
214
|
+
`DELETE FROM social_graph_edges
|
|
215
|
+
WHERE source_did = ? AND target_did = ? AND edge_type = ?`,
|
|
216
|
+
).run(sourceDid, targetDid, edgeType);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const payload = { ...edge };
|
|
220
|
+
_bus.emit("edge:removed", payload);
|
|
221
|
+
_bus.emit("change", { kind: "edge:removed", edge: payload });
|
|
222
|
+
|
|
223
|
+
return { removed: true, edge: payload };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ── Queries ──────────────────────────────────────────────── */
|
|
227
|
+
|
|
228
|
+
export function getNode(did) {
|
|
229
|
+
const node = _nodes.get(did);
|
|
230
|
+
return node ? { ...node } : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* List outgoing edges from `did`. Optionally filter by `edgeType`.
|
|
235
|
+
*/
|
|
236
|
+
export function getOutgoing(did, edgeType) {
|
|
237
|
+
const bucket = _outgoing.get(did);
|
|
238
|
+
if (!bucket) return [];
|
|
239
|
+
const out = [];
|
|
240
|
+
for (const edge of bucket.values()) {
|
|
241
|
+
if (edgeType && edge.edgeType !== edgeType) continue;
|
|
242
|
+
out.push({ ...edge });
|
|
243
|
+
}
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* List incoming edges to `did`. Optionally filter by `edgeType`.
|
|
249
|
+
*/
|
|
250
|
+
export function getIncoming(did, edgeType) {
|
|
251
|
+
const bucket = _incoming.get(did);
|
|
252
|
+
if (!bucket) return [];
|
|
253
|
+
const out = [];
|
|
254
|
+
for (const edge of bucket.values()) {
|
|
255
|
+
if (edgeType && edge.edgeType !== edgeType) continue;
|
|
256
|
+
out.push({ ...edge });
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Union of outgoing + incoming neighbors (returns unique DIDs).
|
|
263
|
+
* Direction filter: "out" | "in" | "both" (default "both").
|
|
264
|
+
*/
|
|
265
|
+
export function getNeighbors(did, opts = {}) {
|
|
266
|
+
const { direction = "both", edgeType } = opts;
|
|
267
|
+
const result = new Set();
|
|
268
|
+
if (direction === "out" || direction === "both") {
|
|
269
|
+
for (const e of getOutgoing(did, edgeType)) result.add(e.targetDid);
|
|
270
|
+
}
|
|
271
|
+
if (direction === "in" || direction === "both") {
|
|
272
|
+
for (const e of getIncoming(did, edgeType)) result.add(e.sourceDid);
|
|
273
|
+
}
|
|
274
|
+
return [...result];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Snapshot of the entire graph. Callers should treat this as a
|
|
279
|
+
* point-in-time view; subsequent mutations do not propagate into
|
|
280
|
+
* an already-returned snapshot.
|
|
281
|
+
*/
|
|
282
|
+
export function getGraphSnapshot(opts = {}) {
|
|
283
|
+
const { edgeType } = opts;
|
|
284
|
+
const nodes = [..._nodes.values()].map((n) => ({ ...n }));
|
|
285
|
+
const edges = [];
|
|
286
|
+
for (const bucket of _outgoing.values()) {
|
|
287
|
+
for (const edge of bucket.values()) {
|
|
288
|
+
if (edgeType && edge.edgeType !== edgeType) continue;
|
|
289
|
+
edges.push({ ...edge });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
nodes,
|
|
294
|
+
edges,
|
|
295
|
+
stats: {
|
|
296
|
+
nodeCount: nodes.length,
|
|
297
|
+
edgeCount: edges.length,
|
|
298
|
+
types: _countByType(edges),
|
|
299
|
+
generatedAt: new Date().toISOString(),
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function _countByType(edges) {
|
|
305
|
+
const out = {};
|
|
306
|
+
for (const e of edges) out[e.edgeType] = (out[e.edgeType] || 0) + 1;
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* ── Persistence ──────────────────────────────────────────── */
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Load edges from SQLite into memory. Safe to call repeatedly;
|
|
314
|
+
* existing in-memory edges are replaced.
|
|
315
|
+
*/
|
|
316
|
+
export function loadFromDb(db) {
|
|
317
|
+
if (!db) return 0;
|
|
318
|
+
_outgoing.clear();
|
|
319
|
+
_incoming.clear();
|
|
320
|
+
_nodes.clear();
|
|
321
|
+
const rows = db
|
|
322
|
+
.prepare(
|
|
323
|
+
`SELECT source_did, target_did, edge_type, weight, metadata, created_at, updated_at
|
|
324
|
+
FROM social_graph_edges`,
|
|
325
|
+
)
|
|
326
|
+
.all();
|
|
327
|
+
for (const row of rows) {
|
|
328
|
+
const metadata = row.metadata ? JSON.parse(row.metadata) : null;
|
|
329
|
+
// Hydrate without re-emitting events (bulk load).
|
|
330
|
+
_hydrateEdge({
|
|
331
|
+
sourceDid: row.source_did,
|
|
332
|
+
targetDid: row.target_did,
|
|
333
|
+
edgeType: row.edge_type,
|
|
334
|
+
weight: row.weight,
|
|
335
|
+
metadata,
|
|
336
|
+
createdAt: row.created_at,
|
|
337
|
+
updatedAt: row.updated_at,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
return rows.length;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function _hydrateEdge(edge) {
|
|
344
|
+
_touchNode(edge.sourceDid, edge.createdAt);
|
|
345
|
+
_touchNode(edge.targetDid, edge.createdAt);
|
|
346
|
+
const outBucket = _outgoing.get(edge.sourceDid) || new Map();
|
|
347
|
+
const inBucket = _incoming.get(edge.targetDid) || new Map();
|
|
348
|
+
outBucket.set(_edgeKey(edge.targetDid, edge.edgeType), edge);
|
|
349
|
+
inBucket.set(_edgeKey(edge.sourceDid, edge.edgeType), edge);
|
|
350
|
+
_outgoing.set(edge.sourceDid, outBucket);
|
|
351
|
+
_incoming.set(edge.targetDid, inBucket);
|
|
352
|
+
_incEdgeCount(edge.sourceDid, 1);
|
|
353
|
+
_incEdgeCount(edge.targetDid, 1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* ── Subscription ─────────────────────────────────────────── */
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Subscribe to graph events. Returns an unsubscribe function.
|
|
360
|
+
*
|
|
361
|
+
* Events emitted:
|
|
362
|
+
* - "edge:added" → { sourceDid, targetDid, edgeType, weight, ... }
|
|
363
|
+
* - "edge:updated" → (same shape; fired when addEdge upserts)
|
|
364
|
+
* - "edge:removed" → { sourceDid, targetDid, edgeType, ... }
|
|
365
|
+
* - "node:added" → { did, at }
|
|
366
|
+
* - "node:removed" → { did }
|
|
367
|
+
* - "change" → { kind, edge } (union wrapper for NDJSON tails)
|
|
368
|
+
*/
|
|
369
|
+
export function subscribe(listener, opts = {}) {
|
|
370
|
+
const { events } = opts;
|
|
371
|
+
const names =
|
|
372
|
+
Array.isArray(events) && events.length > 0
|
|
373
|
+
? events
|
|
374
|
+
: [
|
|
375
|
+
"edge:added",
|
|
376
|
+
"edge:updated",
|
|
377
|
+
"edge:removed",
|
|
378
|
+
"node:added",
|
|
379
|
+
"node:removed",
|
|
380
|
+
];
|
|
381
|
+
const wrapped = {};
|
|
382
|
+
for (const name of names) {
|
|
383
|
+
wrapped[name] = (payload) => listener({ type: name, payload });
|
|
384
|
+
_bus.on(name, wrapped[name]);
|
|
385
|
+
}
|
|
386
|
+
return function unsubscribe() {
|
|
387
|
+
for (const name of Object.keys(wrapped)) {
|
|
388
|
+
_bus.removeListener(name, wrapped[name]);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Raw EventEmitter access — mainly for tests and the `change` union
|
|
395
|
+
* event that `subscribe` doesn't forward by default.
|
|
396
|
+
*/
|
|
397
|
+
export function getEventBus() {
|
|
398
|
+
return _bus;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ── Reset (for testing) ─────────────────────────────────── */
|
|
402
|
+
|
|
403
|
+
export function _resetState() {
|
|
404
|
+
_outgoing.clear();
|
|
405
|
+
_incoming.clear();
|
|
406
|
+
_nodes.clear();
|
|
407
|
+
_bus.removeAllListeners();
|
|
408
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* STIX 2.1 Indicator Parser — extracts IoC (indicators of compromise)
|
|
3
|
+
* from STIX 2.1 bundles or loose objects.
|
|
4
|
+
*
|
|
5
|
+
* Supports the most common observable types used in threat feeds:
|
|
6
|
+
* file-hash (md5/sha1/sha256/sha512), ipv4-addr, ipv6-addr,
|
|
7
|
+
* domain-name, url, email-addr.
|
|
8
|
+
*
|
|
9
|
+
* Reference: https://docs.oasis-open.org/cti/stix/v2.1/stix-v2.1.html
|
|
10
|
+
*
|
|
11
|
+
* The STIX pattern grammar is too rich for a full parser to live here;
|
|
12
|
+
* this module handles the simple `[<object-path> = '<value>']` shape
|
|
13
|
+
* (optionally joined by OR / AND / FOLLOWEDBY) that covers the vast
|
|
14
|
+
* majority of real-world indicator feeds (AlienVault OTX, MISP export,
|
|
15
|
+
* MITRE ATT&CK, etc.).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export const IOC_TYPES = Object.freeze([
|
|
19
|
+
"file-md5",
|
|
20
|
+
"file-sha1",
|
|
21
|
+
"file-sha256",
|
|
22
|
+
"file-sha512",
|
|
23
|
+
"ipv4",
|
|
24
|
+
"ipv6",
|
|
25
|
+
"domain",
|
|
26
|
+
"url",
|
|
27
|
+
"email",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map a STIX object-path + hash-key to one of our IOC_TYPES.
|
|
32
|
+
* Returns null for unsupported observable types.
|
|
33
|
+
*/
|
|
34
|
+
function _classify(objectPath, hashKey) {
|
|
35
|
+
const p = objectPath.toLowerCase();
|
|
36
|
+
switch (p) {
|
|
37
|
+
case "file:hashes":
|
|
38
|
+
switch ((hashKey || "").toUpperCase()) {
|
|
39
|
+
case "MD5":
|
|
40
|
+
return "file-md5";
|
|
41
|
+
case "SHA-1":
|
|
42
|
+
case "SHA1":
|
|
43
|
+
return "file-sha1";
|
|
44
|
+
case "SHA-256":
|
|
45
|
+
case "SHA256":
|
|
46
|
+
return "file-sha256";
|
|
47
|
+
case "SHA-512":
|
|
48
|
+
case "SHA512":
|
|
49
|
+
return "file-sha512";
|
|
50
|
+
default:
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
case "ipv4-addr:value":
|
|
54
|
+
return "ipv4";
|
|
55
|
+
case "ipv6-addr:value":
|
|
56
|
+
return "ipv6";
|
|
57
|
+
case "domain-name:value":
|
|
58
|
+
return "domain";
|
|
59
|
+
case "url:value":
|
|
60
|
+
return "url";
|
|
61
|
+
case "email-addr:value":
|
|
62
|
+
return "email";
|
|
63
|
+
default:
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const _termRegex =
|
|
69
|
+
/([a-z0-9_-]+(?::[a-z0-9_-]+)?)(?:\.'([^']+)'|\.([a-z0-9_-]+))?\s*=\s*'((?:\\'|[^'])*)'/gi;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse a STIX pattern string and yield raw matches of the simple
|
|
73
|
+
* comparison form: `<object:path>[.<key>] = '<value>'`. Joiners
|
|
74
|
+
* (OR/AND/FOLLOWEDBY) between terms are ignored — each matched term
|
|
75
|
+
* becomes its own IoC.
|
|
76
|
+
*/
|
|
77
|
+
export function parseStixPattern(pattern) {
|
|
78
|
+
if (typeof pattern !== "string") return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
let m;
|
|
81
|
+
_termRegex.lastIndex = 0;
|
|
82
|
+
while ((m = _termRegex.exec(pattern)) !== null) {
|
|
83
|
+
const [, rawObjectPath, quotedKey, bareKey, rawValue] = m;
|
|
84
|
+
const hashKey = quotedKey || bareKey || null;
|
|
85
|
+
const objectPath = hashKey
|
|
86
|
+
? `${rawObjectPath.toLowerCase()}`
|
|
87
|
+
: rawObjectPath.toLowerCase();
|
|
88
|
+
const iocType = _classify(objectPath, hashKey);
|
|
89
|
+
if (!iocType) continue;
|
|
90
|
+
const value = rawValue.replace(/\\'/g, "'");
|
|
91
|
+
out.push({ type: iocType, value });
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Extract indicators from a parsed STIX 2.1 object. Returns an array
|
|
98
|
+
* of `{type, value, source: {indicatorId, name, labels, confidence,
|
|
99
|
+
* valid_from, valid_until}}` entries.
|
|
100
|
+
*/
|
|
101
|
+
export function extractIndicatorsFromObject(obj) {
|
|
102
|
+
if (!obj || typeof obj !== "object") return [];
|
|
103
|
+
if (obj.type !== "indicator") return [];
|
|
104
|
+
const pattern = obj.pattern;
|
|
105
|
+
if (!pattern) return [];
|
|
106
|
+
if (obj.pattern_type && obj.pattern_type !== "stix") {
|
|
107
|
+
// Only STIX-pattern indicators are supported; feeds may ship
|
|
108
|
+
// snort/yara/pcre patterns which we ignore.
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
const iocs = parseStixPattern(pattern);
|
|
112
|
+
const source = {
|
|
113
|
+
indicatorId: obj.id || null,
|
|
114
|
+
name: obj.name || null,
|
|
115
|
+
labels: Array.isArray(obj.indicator_types)
|
|
116
|
+
? obj.indicator_types.slice()
|
|
117
|
+
: Array.isArray(obj.labels)
|
|
118
|
+
? obj.labels.slice()
|
|
119
|
+
: [],
|
|
120
|
+
confidence: typeof obj.confidence === "number" ? obj.confidence : null,
|
|
121
|
+
validFrom: obj.valid_from || null,
|
|
122
|
+
validUntil: obj.valid_until || null,
|
|
123
|
+
};
|
|
124
|
+
return iocs.map((ioc) => ({ ...ioc, source }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Extract indicators from a STIX 2.1 bundle (`{type:"bundle", objects:[…]}`)
|
|
129
|
+
* or a loose array of STIX objects. Unknown inputs return [].
|
|
130
|
+
*/
|
|
131
|
+
export function extractIndicatorsFromBundle(bundle) {
|
|
132
|
+
if (!bundle) return [];
|
|
133
|
+
const objects = Array.isArray(bundle)
|
|
134
|
+
? bundle
|
|
135
|
+
: bundle.type === "bundle" && Array.isArray(bundle.objects)
|
|
136
|
+
? bundle.objects
|
|
137
|
+
: null;
|
|
138
|
+
if (!objects) return [];
|
|
139
|
+
const out = [];
|
|
140
|
+
for (const obj of objects) {
|
|
141
|
+
for (const ioc of extractIndicatorsFromObject(obj)) {
|
|
142
|
+
out.push(ioc);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Best-effort classifier for an arbitrary observable string supplied
|
|
150
|
+
* by the user (via `cc compliance threat-intel match <value>`).
|
|
151
|
+
* Returns one of IOC_TYPES or "unknown".
|
|
152
|
+
*/
|
|
153
|
+
export function classifyObservable(value) {
|
|
154
|
+
if (typeof value !== "string") return "unknown";
|
|
155
|
+
const v = value.trim();
|
|
156
|
+
if (!v) return "unknown";
|
|
157
|
+
if (/^[a-f0-9]{32}$/i.test(v)) return "file-md5";
|
|
158
|
+
if (/^[a-f0-9]{40}$/i.test(v)) return "file-sha1";
|
|
159
|
+
if (/^[a-f0-9]{64}$/i.test(v)) return "file-sha256";
|
|
160
|
+
if (/^[a-f0-9]{128}$/i.test(v)) return "file-sha512";
|
|
161
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(v)) return "ipv4";
|
|
162
|
+
if (/^[0-9a-f:]+$/i.test(v) && v.includes(":")) return "ipv6";
|
|
163
|
+
if (/^https?:\/\//i.test(v)) return "url";
|
|
164
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v)) return "email";
|
|
165
|
+
if (/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(v)) return "domain";
|
|
166
|
+
return "unknown";
|
|
167
|
+
}
|