@x12i/memorix-retrieval 1.35.0 → 1.37.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.
Files changed (114) hide show
  1. package/catalox-seeds/inputs/entity-descriptors/assets.json +166 -0
  2. package/catalox-seeds/inputs/entity-descriptors/subnets.json +115 -1
  3. package/catalox-seeds/inputs/entity-descriptors/topology-zones.json +52 -1
  4. package/catalox-seeds/inputs/entity-descriptors/vulnerabilities-groups.json +10 -2
  5. package/catalox-seeds/inputs/entity-descriptors/vulnerabilities.json +79 -7
  6. package/catalox-seeds/inputs/graph-descriptors/asset-subnet-paths-graph.json +41 -0
  7. package/catalox-seeds/inputs/graph-descriptors/asset-topology-vulnerabilities-graph.json +81 -0
  8. package/catalox-seeds/inputs/graph-descriptors/asset-vulnerability-group-graph.json +38 -0
  9. package/catalox-seeds/inputs/graph-descriptors/vulnerability-asset-subnet-graph.json +40 -0
  10. package/catalox-seeds/inputs/list-descriptors/assets-exploitability-decisions-list.json +4 -0
  11. package/catalox-seeds/inputs/list-descriptors/assets-potential-impact-decisions-list.json +16 -8
  12. package/catalox-seeds/inputs/manifest.json +4 -0
  13. package/catalox-seeds/inputs/memorix-catalog-ids.json +2 -1
  14. package/catalox-seeds/memorix-retrieval-descriptors.manifest.json +1186 -34
  15. package/dist/client/catalox-adapter.d.ts.map +1 -1
  16. package/dist/client/catalox-adapter.js +10 -1
  17. package/dist/client/catalox-adapter.js.map +1 -1
  18. package/dist/client/catalox-like.d.ts +6 -0
  19. package/dist/client/catalox-like.d.ts.map +1 -1
  20. package/dist/data/identity.d.ts +19 -1
  21. package/dist/data/identity.d.ts.map +1 -1
  22. package/dist/data/identity.js +41 -25
  23. package/dist/data/identity.js.map +1 -1
  24. package/dist/data/record-info.d.ts +1 -1
  25. package/dist/data/record-info.d.ts.map +1 -1
  26. package/dist/descriptors/catalog-ids.d.ts +1 -0
  27. package/dist/descriptors/catalog-ids.d.ts.map +1 -1
  28. package/dist/descriptors/catalog-ids.js +1 -0
  29. package/dist/descriptors/catalog-ids.js.map +1 -1
  30. package/dist/descriptors/descriptor-types.d.ts +3 -1
  31. package/dist/descriptors/descriptor-types.d.ts.map +1 -1
  32. package/dist/errors/issues.d.ts +2 -0
  33. package/dist/errors/issues.d.ts.map +1 -1
  34. package/dist/errors/issues.js.map +1 -1
  35. package/dist/graphs/build-graph-directory.d.ts +28 -0
  36. package/dist/graphs/build-graph-directory.d.ts.map +1 -0
  37. package/dist/graphs/build-graph-directory.js +253 -0
  38. package/dist/graphs/build-graph-directory.js.map +1 -0
  39. package/dist/graphs/build-graph-document.d.ts +13 -0
  40. package/dist/graphs/build-graph-document.d.ts.map +1 -0
  41. package/dist/graphs/build-graph-document.js +888 -0
  42. package/dist/graphs/build-graph-document.js.map +1 -0
  43. package/dist/graphs/fallback-graph-capabilities.d.ts +16 -0
  44. package/dist/graphs/fallback-graph-capabilities.d.ts.map +1 -0
  45. package/dist/graphs/fallback-graph-capabilities.js +376 -0
  46. package/dist/graphs/fallback-graph-capabilities.js.map +1 -0
  47. package/dist/graphs/index.d.ts +9 -0
  48. package/dist/graphs/index.d.ts.map +1 -0
  49. package/dist/graphs/index.js +8 -0
  50. package/dist/graphs/index.js.map +1 -0
  51. package/dist/graphs/infer-graph-descriptor.d.ts +17 -0
  52. package/dist/graphs/infer-graph-descriptor.d.ts.map +1 -0
  53. package/dist/graphs/infer-graph-descriptor.js +234 -0
  54. package/dist/graphs/infer-graph-descriptor.js.map +1 -0
  55. package/dist/graphs/load-graph-descriptor.d.ts +31 -0
  56. package/dist/graphs/load-graph-descriptor.d.ts.map +1 -0
  57. package/dist/graphs/load-graph-descriptor.js +141 -0
  58. package/dist/graphs/load-graph-descriptor.js.map +1 -0
  59. package/dist/graphs/record-access.d.ts +36 -0
  60. package/dist/graphs/record-access.d.ts.map +1 -0
  61. package/dist/graphs/record-access.js +178 -0
  62. package/dist/graphs/record-access.js.map +1 -0
  63. package/dist/graphs/types.d.ts +70 -0
  64. package/dist/graphs/types.d.ts.map +1 -0
  65. package/dist/graphs/types.js +17 -0
  66. package/dist/graphs/types.js.map +1 -0
  67. package/dist/graphs/validate-graph-metadata.d.ts +36 -0
  68. package/dist/graphs/validate-graph-metadata.d.ts.map +1 -0
  69. package/dist/graphs/validate-graph-metadata.js +71 -0
  70. package/dist/graphs/validate-graph-metadata.js.map +1 -0
  71. package/dist/index.d.ts +1 -0
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/retrieval/fetch-list.d.ts.map +1 -1
  76. package/dist/retrieval/fetch-list.js +3 -1
  77. package/dist/retrieval/fetch-list.js.map +1 -1
  78. package/dist/retrieval/resolve-fields.d.ts.map +1 -1
  79. package/dist/retrieval/resolve-fields.js +7 -1
  80. package/dist/retrieval/resolve-fields.js.map +1 -1
  81. package/dist/retrieval/resolve-list-analytics.d.ts +2 -2
  82. package/dist/retrieval/resolve-list-analytics.d.ts.map +1 -1
  83. package/dist/retrieval/resolve-list-analytics.js.map +1 -1
  84. package/dist/retrieval/resolve-property-source.d.ts.map +1 -1
  85. package/dist/retrieval/resolve-property-source.js +10 -3
  86. package/dist/retrieval/resolve-property-source.js.map +1 -1
  87. package/dist/seeds/default-seed-spec.d.ts.map +1 -1
  88. package/dist/seeds/default-seed-spec.js +25 -1
  89. package/dist/seeds/default-seed-spec.js.map +1 -1
  90. package/dist/tests/catalox-adapter-scope.test.d.ts +2 -0
  91. package/dist/tests/catalox-adapter-scope.test.d.ts.map +1 -0
  92. package/dist/tests/catalox-adapter-scope.test.js +26 -0
  93. package/dist/tests/catalox-adapter-scope.test.js.map +1 -0
  94. package/dist/tests/fetch-list.test.js +28 -0
  95. package/dist/tests/fetch-list.test.js.map +1 -1
  96. package/dist/tests/graphs.test.d.ts +2 -0
  97. package/dist/tests/graphs.test.d.ts.map +1 -0
  98. package/dist/tests/graphs.test.js +423 -0
  99. package/dist/tests/graphs.test.js.map +1 -0
  100. package/dist/tests/identity.test.js +13 -1
  101. package/dist/tests/identity.test.js.map +1 -1
  102. package/dist/tests/infer-graph.test.d.ts +2 -0
  103. package/dist/tests/infer-graph.test.d.ts.map +1 -0
  104. package/dist/tests/infer-graph.test.js +19 -0
  105. package/dist/tests/infer-graph.test.js.map +1 -0
  106. package/dist/tests/resolve-filters.test.js +8 -0
  107. package/dist/tests/resolve-filters.test.js.map +1 -1
  108. package/dist/tests/seed-manifest.test.js +3 -0
  109. package/dist/tests/seed-manifest.test.js.map +1 -1
  110. package/dist/tests/validate-graph-metadata.test.d.ts +2 -0
  111. package/dist/tests/validate-graph-metadata.test.d.ts.map +1 -0
  112. package/dist/tests/validate-graph-metadata.test.js +81 -0
  113. package/dist/tests/validate-graph-metadata.test.js.map +1 -0
  114. package/package.json +13 -2
@@ -0,0 +1,888 @@
1
+ import { getByPath } from "../data/path.js";
2
+ import { loadMemorixEntityDescriptorByName } from "../descriptors/discover-entities.js";
3
+ import { resolveMemorixGraphDescriptor } from "./load-graph-descriptor.js";
4
+ import { resolveEntityGraphCapability, } from "./fallback-graph-capabilities.js";
5
+ import { conceptFromRecord, fetchContentLenses, findSnapshotByIdentifierKey, findSnapshotByRecordId, focusLookupObjectTypes, listSnapshots, recordIdOf, } from "./record-access.js";
6
+ function parseTiers(tiers) {
7
+ if (tiers == null)
8
+ return null;
9
+ const list = Array.isArray(tiers)
10
+ ? tiers
11
+ : tiers.split(",").map((t) => t.trim()).filter(Boolean);
12
+ return list.length ? new Set(list) : null;
13
+ }
14
+ function upsertNode(map, node) {
15
+ const existing = map.get(node.id);
16
+ if (!existing) {
17
+ map.set(node.id, node);
18
+ return;
19
+ }
20
+ if (!existing.recordId && node.recordId)
21
+ existing.recordId = node.recordId;
22
+ if (!existing.objectType && node.objectType)
23
+ existing.objectType = node.objectType;
24
+ if (!existing.content && node.content)
25
+ existing.content = node.content;
26
+ }
27
+ function addEdge(edges, edge) {
28
+ const id = edge.id ?? `${edge.type}:${edge.from}->${edge.to}`;
29
+ if (edges.some((e) => (e.id ?? `${e.type}:${e.from}->${e.to}`) === id))
30
+ return;
31
+ edges.push({ ...edge, id });
32
+ }
33
+ function tierForObjectType(descriptor, objectType) {
34
+ return (descriptor.nodes.find((n) => n.objectType === objectType)?.tier ??
35
+ descriptor.tiers.find((t) => t.objectTypes.includes(objectType))?.id);
36
+ }
37
+ function nodeSpecFor(descriptor, objectType) {
38
+ return descriptor.nodes.find((n) => n.objectType === objectType);
39
+ }
40
+ function buildId(kind, value) {
41
+ const v = value.trim();
42
+ if (!v)
43
+ return "";
44
+ return `${kind}:${v}`;
45
+ }
46
+ function titleForId(id, kind) {
47
+ if (id.startsWith(`${kind}:`))
48
+ return id.slice(kind.length + 1);
49
+ return id;
50
+ }
51
+ function syntheticConcept(kind, value) {
52
+ const id = buildId(kind, value);
53
+ return {
54
+ identifier: { kind, [kind]: value, identifierKey: id },
55
+ title: value,
56
+ };
57
+ }
58
+ function passesWhen(emission, item) {
59
+ const when = emission.when;
60
+ if (!when)
61
+ return true;
62
+ if (!item) {
63
+ return (!when.itemFieldExists &&
64
+ !when.itemArrayIncludes &&
65
+ !when.itemFieldMissing);
66
+ }
67
+ if (when.itemFieldExists) {
68
+ const v = item[when.itemFieldExists];
69
+ if (v == null || v === "")
70
+ return false;
71
+ }
72
+ if (when.itemFieldMissing) {
73
+ const v = item[when.itemFieldMissing];
74
+ if (v != null && v !== "")
75
+ return false;
76
+ }
77
+ if (when.itemArrayIncludes) {
78
+ const arr = item[when.itemArrayIncludes.field];
79
+ if (!Array.isArray(arr) || !arr.map(String).includes(when.itemArrayIncludes.value)) {
80
+ return false;
81
+ }
82
+ }
83
+ return true;
84
+ }
85
+ function readDataObject(doc) {
86
+ return (doc.data && typeof doc.data === "object" ? doc.data : {});
87
+ }
88
+ function asStringList(value) {
89
+ if (value == null || value === "")
90
+ return [];
91
+ if (Array.isArray(value)) {
92
+ return value.map((v) => String(v).trim()).filter(Boolean);
93
+ }
94
+ return [String(value).trim()].filter(Boolean);
95
+ }
96
+ function readVulnerabilityFamily(doc) {
97
+ if (!doc)
98
+ return "";
99
+ const data = readDataObject(doc);
100
+ const raw = data.vulnerabilityFamily ??
101
+ data.pluginFamily ??
102
+ getByPath(doc, "data.vulnerabilityFamily") ??
103
+ getByPath(doc, "data.pluginFamily");
104
+ return raw == null || raw === "" ? "" : String(raw).trim();
105
+ }
106
+ function collectTopologyGap(doc, opts) {
107
+ const data = readDataObject(doc);
108
+ const classifications = Array.isArray(data.assetIpClassifications)
109
+ ? data.assetIpClassifications
110
+ : [];
111
+ const subnetCidrs = asStringList(data.subnetCidrs);
112
+ const zones = asStringList(data.zones);
113
+ const gapReasons = classifications
114
+ .map((c) => String(c.reason ?? "").trim())
115
+ .filter((r) => r.includes("NO_CANONICAL_SUBNET_MATCH") || r === "UNPARSEABLE_IP");
116
+ if (opts.hadRealAssociatedTopology && !gapReasons.length)
117
+ return null;
118
+ if (!opts.hadRealAssociatedTopology ||
119
+ gapReasons.length ||
120
+ (!subnetCidrs.length && classifications.length)) {
121
+ return {
122
+ reasons: [
123
+ ...new Set(gapReasons.length
124
+ ? gapReasons
125
+ : opts.projectedTopology
126
+ ? ["OBSERVED_ZONES_WITHOUT_CANONICAL_SUBNET"]
127
+ : ["MISSING_ASSOCIATED_TOPOLOGY"]),
128
+ ],
129
+ classifications: classifications.map((c) => ({
130
+ ip: c.ip,
131
+ scope: c.scope,
132
+ confidence: c.confidence,
133
+ reason: c.reason,
134
+ reasoning: c.reasoning,
135
+ })),
136
+ hasAssociatedTopology: opts.hadRealAssociatedTopology,
137
+ hasSubnetCidrs: subnetCidrs.length > 0,
138
+ hasObservedZones: zones.length > 0 || opts.projectedTopology,
139
+ };
140
+ }
141
+ return null;
142
+ }
143
+ function normalizePathFacts(raw) {
144
+ if (!raw)
145
+ return [];
146
+ const list = Array.isArray(raw)
147
+ ? raw
148
+ : Array.isArray(raw.items)
149
+ ? raw.items
150
+ : Array.isArray(raw.paths)
151
+ ? raw.paths
152
+ : [];
153
+ const out = [];
154
+ for (const entry of list) {
155
+ if (!entry || typeof entry !== "object")
156
+ continue;
157
+ const e = entry;
158
+ const hops = asStringList(e.hops ?? e.zones ?? e.path);
159
+ const from = e.from != null ? String(e.from) : hops[0];
160
+ const to = e.to != null
161
+ ? String(e.to)
162
+ : hops.length > 1
163
+ ? hops[hops.length - 1]
164
+ : undefined;
165
+ // When from/to are explicit, `hops` are intermediate zones.
166
+ // When only a zone list is provided, intermediates are the middle entries.
167
+ const mid = e.from != null || e.to != null
168
+ ? hops
169
+ : hops.length > 2
170
+ ? hops.slice(1, -1)
171
+ : asStringList(e.hop);
172
+ if (!from || !to)
173
+ continue;
174
+ out.push({
175
+ title: e.title != null
176
+ ? String(e.title)
177
+ : [from, ...mid, to].filter(Boolean).join(" → "),
178
+ from,
179
+ to,
180
+ hops: mid,
181
+ viaAsset: typeof e.viaAsset === "boolean" || typeof e.viaAsset === "string"
182
+ ? e.viaAsset
183
+ : e.via != null
184
+ ? String(e.via)
185
+ : undefined,
186
+ });
187
+ }
188
+ return out;
189
+ }
190
+ function resolveIdRef(ref, ctx) {
191
+ switch (ref.source) {
192
+ case "concept":
193
+ return ctx.concept?.identifier.identifierKey
194
+ ? [ctx.concept.identifier.identifierKey]
195
+ : [];
196
+ case "relatedConcept":
197
+ return ctx.relatedConcept?.identifier.identifierKey
198
+ ? [ctx.relatedConcept.identifier.identifierKey]
199
+ : [];
200
+ case "literal":
201
+ return [buildId(ref.kind, ref.value)].filter(Boolean);
202
+ case "itemField": {
203
+ if (!ctx.item)
204
+ return [];
205
+ const raw = ctx.item[ref.field];
206
+ if (raw == null || raw === "")
207
+ return [];
208
+ return [buildId(ref.kind, String(raw))].filter(Boolean);
209
+ }
210
+ case "itemArrayField": {
211
+ if (!ctx.item)
212
+ return [];
213
+ const arr = ctx.item[ref.field];
214
+ if (!Array.isArray(arr))
215
+ return [];
216
+ return arr
217
+ .map((v) => buildId(ref.kind, String(v)))
218
+ .filter(Boolean);
219
+ }
220
+ default:
221
+ return [];
222
+ }
223
+ }
224
+ function readJoinValue(doc, sourcePath) {
225
+ let value = getByPath(doc, sourcePath);
226
+ if (value != null && value !== "")
227
+ return value;
228
+ // Fallbacks for common identity shapes
229
+ if (sourcePath === "data.assetIp" || sourcePath.endsWith(".assetIp")) {
230
+ value = getByPath(doc, "data.assetIps");
231
+ if (Array.isArray(value) && value.length)
232
+ return value[0];
233
+ const concept = conceptFromRecord(doc);
234
+ if (concept?.identifier.kind === "ipAddress" && concept.identifier.ipAddress) {
235
+ return concept.identifier.ipAddress;
236
+ }
237
+ }
238
+ if (sourcePath === "data.pluginId") {
239
+ const concept = conceptFromRecord(doc);
240
+ if (concept?.identifier.pluginId)
241
+ return concept.identifier.pluginId;
242
+ }
243
+ return value;
244
+ }
245
+ async function loadEntitySafe(client, objectType) {
246
+ try {
247
+ return await loadMemorixEntityDescriptorByName(client, objectType);
248
+ }
249
+ catch {
250
+ return null;
251
+ }
252
+ }
253
+ async function maybeAttachContent(client, node, objectType, recordId, lenses, enabled) {
254
+ if (!enabled || !recordId || !lenses?.length)
255
+ return;
256
+ node.content = await fetchContentLenses(client, objectType, recordId, lenses);
257
+ }
258
+ function ensureSyntheticNode(nodes, id, kind, objectType, tier) {
259
+ if (nodes.has(id))
260
+ return;
261
+ const value = titleForId(id, kind);
262
+ upsertNode(nodes, {
263
+ id,
264
+ tier,
265
+ concept: syntheticConcept(kind, value),
266
+ objectType,
267
+ });
268
+ }
269
+ export async function buildMemorixGraphDocument(client, input) {
270
+ const resolved = input.descriptor != null
271
+ ? {
272
+ descriptor: input.descriptor,
273
+ defined: true,
274
+ source: "catalox",
275
+ }
276
+ : await resolveMemorixGraphDescriptor(client, input.graphId ?? "asset-topology-vulnerabilities-graph");
277
+ const descriptor = resolved.descriptor;
278
+ const tierFilter = parseTiers(input.tiers);
279
+ const loadLenses = Boolean(input.lenses);
280
+ const defaultFocusObjectType = descriptor.focusObjectType ?? "assets";
281
+ const maxDepth = descriptor.maxDepth ?? 2;
282
+ const includeTemplates = descriptor.includeEdgeTemplates
283
+ ? new Set(descriptor.includeEdgeTemplates)
284
+ : null;
285
+ const nodes = new Map();
286
+ const edges = [];
287
+ const notes = {
288
+ graphId: descriptor.id,
289
+ descriptorDefined: resolved.defined,
290
+ descriptorSource: resolved.source,
291
+ };
292
+ if (resolved.inferenceWarnings?.length) {
293
+ notes.inferenceWarnings = resolved.inferenceWarnings;
294
+ }
295
+ const inferredCaps = resolved.inferredCapabilities ?? {};
296
+ const entityCache = new Map();
297
+ const getEntity = async (objectType) => {
298
+ if (!entityCache.has(objectType)) {
299
+ entityCache.set(objectType, await loadEntitySafe(client, objectType));
300
+ }
301
+ return entityCache.get(objectType) ?? null;
302
+ };
303
+ // Directory lists every descriptor node type; Focus must resolve the same set.
304
+ // Prefer kind prefix from identifierKey (cidr: → subnets), then fall back across types.
305
+ // Also: when Directory passes recordId + identifierKey, a miss on the default
306
+ // focusObjectType must not block identifierKey / other collections.
307
+ const kindByObjectType = {};
308
+ for (const n of descriptor.nodes) {
309
+ const cap = resolveEntityGraphCapability(n.objectType, null, inferredCaps[n.objectType]);
310
+ kindByObjectType[n.objectType] = n.kind ?? cap?.node.kind;
311
+ }
312
+ const lookupTypes = focusLookupObjectTypes({
313
+ focusObjectType: defaultFocusObjectType,
314
+ focusIdentifierKey: input.focusIdentifierKey,
315
+ nodes: descriptor.nodes,
316
+ kindByObjectType,
317
+ });
318
+ let focusDoc = null;
319
+ let focusObjectType = defaultFocusObjectType;
320
+ if (input.focusRecordId) {
321
+ for (const ot of lookupTypes) {
322
+ try {
323
+ const doc = await findSnapshotByRecordId(client, ot, input.focusRecordId);
324
+ if (doc) {
325
+ focusDoc = doc;
326
+ focusObjectType = ot;
327
+ break;
328
+ }
329
+ }
330
+ catch {
331
+ /* entity/collection missing for this type — try next */
332
+ }
333
+ }
334
+ }
335
+ if (!focusDoc && input.focusIdentifierKey) {
336
+ for (const ot of lookupTypes) {
337
+ try {
338
+ const doc = await findSnapshotByIdentifierKey(client, ot, input.focusIdentifierKey);
339
+ if (doc) {
340
+ focusDoc = doc;
341
+ focusObjectType = ot;
342
+ break;
343
+ }
344
+ }
345
+ catch {
346
+ /* entity/collection missing for this type — try next */
347
+ }
348
+ }
349
+ }
350
+ if (!focusDoc) {
351
+ return {
352
+ focus: input.focusRecordId
353
+ ? { recordId: input.focusRecordId }
354
+ : input.focusIdentifierKey
355
+ ? {
356
+ concept: {
357
+ identifier: {
358
+ kind: "unknown",
359
+ identifierKey: input.focusIdentifierKey,
360
+ unknown: input.focusIdentifierKey,
361
+ },
362
+ title: input.focusIdentifierKey,
363
+ },
364
+ }
365
+ : undefined,
366
+ nodes: [],
367
+ edges: [],
368
+ notes: { ...notes, warning: "focus_not_found" },
369
+ };
370
+ }
371
+ const focusConcept = conceptFromRecord(focusDoc);
372
+ const focusRecordId = recordIdOf(focusDoc);
373
+ if (!focusConcept) {
374
+ return { nodes: [], edges: [], notes: { ...notes, warning: "focus_missing_concept" } };
375
+ }
376
+ const focusTier = tierForObjectType(descriptor, focusObjectType);
377
+ if (tierFilter && focusTier && !tierFilter.has(focusTier)) {
378
+ return {
379
+ focus: { recordId: focusRecordId || undefined, concept: focusConcept },
380
+ nodes: [],
381
+ edges: [],
382
+ notes: { ...notes, warning: "focus_tier_filtered" },
383
+ };
384
+ }
385
+ const focusEntity = await getEntity(focusObjectType);
386
+ const focusCap = resolveEntityGraphCapability(focusObjectType, focusEntity, inferredCaps[focusObjectType]);
387
+ const focusSpec = nodeSpecFor(descriptor, focusObjectType);
388
+ const focusLenses = focusSpec?.contentLenses ?? focusCap?.node.contentLenses ?? ["snapshot"];
389
+ const focusNode = {
390
+ id: focusConcept.identifier.identifierKey,
391
+ tier: focusTier,
392
+ concept: focusConcept,
393
+ objectType: focusObjectType,
394
+ recordId: focusRecordId || undefined,
395
+ };
396
+ await maybeAttachContent(client, focusNode, focusObjectType, focusRecordId || undefined, focusLenses, loadLenses);
397
+ upsertNode(nodes, focusNode);
398
+ const focusAssetZones = new Set();
399
+ {
400
+ const data = readDataObject(focusDoc);
401
+ for (const z of asStringList(data.zones))
402
+ focusAssetZones.add(z);
403
+ const topo = Array.isArray(focusDoc.associatedTopology)
404
+ ? focusDoc.associatedTopology
405
+ : [];
406
+ for (const item of topo) {
407
+ if (item.zone != null && String(item.zone).trim()) {
408
+ focusAssetZones.add(String(item.zone).trim());
409
+ }
410
+ }
411
+ }
412
+ let hadRealAssociatedTopology = false;
413
+ let projectedTopology = false;
414
+ const emitInFamily = (pluginId, familyValue, tier) => {
415
+ if (!familyValue || !pluginId)
416
+ return;
417
+ const familyId = buildId("vulnerabilityFamily", familyValue);
418
+ if (!familyId)
419
+ return;
420
+ if (tierFilter && tier && !tierFilter.has(tier))
421
+ return;
422
+ ensureSyntheticNode(nodes, familyId, "vulnerabilityFamily", "vulnerabilities-groups", tier);
423
+ const familyNode = nodes.get(familyId);
424
+ if (familyNode?.concept) {
425
+ familyNode.concept = {
426
+ ...familyNode.concept,
427
+ title: familyValue === "pending"
428
+ ? "pending (data.vulnerabilityFamily not in sampled reports for this finding)"
429
+ : familyValue,
430
+ };
431
+ }
432
+ addEdge(edges, {
433
+ type: "inFamily",
434
+ from: pluginId,
435
+ to: familyId,
436
+ tier,
437
+ });
438
+ };
439
+ const attachPathViaAsset = (pathId, targetZoneId, pathTier, explicitVia) => {
440
+ const targetZone = titleForId(targetZoneId, "zoneName");
441
+ const viaKey = typeof explicitVia === "string" && explicitVia.trim()
442
+ ? explicitVia.trim().startsWith("ipAddress:")
443
+ ? explicitVia.trim()
444
+ : buildId("ipAddress", explicitVia.trim())
445
+ : explicitVia === true ||
446
+ (explicitVia == null && focusAssetZones.has(targetZone))
447
+ ? focusConcept.identifier.identifierKey
448
+ : "";
449
+ if (!viaKey)
450
+ return;
451
+ if (!nodes.has(viaKey) && viaKey === focusConcept.identifier.identifierKey) {
452
+ upsertNode(nodes, focusNode);
453
+ }
454
+ if (!nodes.has(viaKey))
455
+ return;
456
+ addEdge(edges, {
457
+ type: "pathViaAsset",
458
+ from: pathId,
459
+ to: viaKey,
460
+ tier: pathTier,
461
+ });
462
+ };
463
+ const queue = [{ objectType: focusObjectType, doc: focusDoc, depth: 0 }];
464
+ const visited = new Set([
465
+ `${focusObjectType}:${focusConcept.identifier.identifierKey}`,
466
+ ]);
467
+ const emitEdgesForItem = async (template, frame, frameConcept, item, relatedDoc, relatedObjectType) => {
468
+ const relatedConcept = relatedDoc ? conceptFromRecord(relatedDoc) : null;
469
+ for (const emission of template.edges) {
470
+ if (!passesWhen(emission, item))
471
+ continue;
472
+ const fromIds = resolveIdRef(emission.from, {
473
+ concept: frameConcept,
474
+ relatedConcept,
475
+ item,
476
+ });
477
+ const toIds = resolveIdRef(emission.to, {
478
+ concept: frameConcept,
479
+ relatedConcept,
480
+ item,
481
+ });
482
+ if (!fromIds.length || !toIds.length)
483
+ continue;
484
+ const neighborSide = emission.neighbor ?? "to";
485
+ const neighborObjectType = neighborSide === "from"
486
+ ? relatedObjectType ?? emission.toObjectType
487
+ : emission.toObjectType;
488
+ const neighborKind = emission.toKind;
489
+ const neighborTier = tierForObjectType(descriptor, neighborObjectType);
490
+ if (tierFilter && neighborTier && !tierFilter.has(neighborTier)) {
491
+ continue;
492
+ }
493
+ for (const fromId of fromIds) {
494
+ for (const toId of toIds) {
495
+ if (!fromId || !toId || fromId === toId)
496
+ continue;
497
+ // Upsert endpoint nodes
498
+ for (const [endpointId, side] of [
499
+ [fromId, "from"],
500
+ [toId, "to"],
501
+ ]) {
502
+ if (nodes.has(endpointId))
503
+ continue;
504
+ const isNeighbor = side === neighborSide;
505
+ if (isNeighbor && relatedDoc && relatedConcept) {
506
+ const relatedRecordId = recordIdOf(relatedDoc);
507
+ upsertNode(nodes, {
508
+ id: endpointId,
509
+ tier: neighborTier,
510
+ concept: relatedConcept,
511
+ objectType: neighborObjectType,
512
+ recordId: relatedRecordId || undefined,
513
+ });
514
+ }
515
+ else if (isNeighbor) {
516
+ ensureSyntheticNode(nodes, endpointId, neighborKind, neighborObjectType, neighborTier);
517
+ }
518
+ else {
519
+ // Other endpoint — synthetic from id kind
520
+ const kindGuess = endpointId.includes(":")
521
+ ? endpointId.slice(0, endpointId.indexOf(":"))
522
+ : "unknown";
523
+ const ot = descriptor.nodes.find((n) => (n.kind ?? "") === kindGuess)?.objectType ??
524
+ frame.objectType;
525
+ ensureSyntheticNode(nodes, endpointId, kindGuess, ot, tierForObjectType(descriptor, ot));
526
+ }
527
+ }
528
+ if (emission.synthesizePath) {
529
+ const fromTitle = titleForId(fromId, emission.from.source === "literal" ? emission.from.kind : neighborKind);
530
+ const toTitle = titleForId(toId, emission.toKind);
531
+ const title = (emission.synthesizePath.titleTemplate ?? "{from} → {to}")
532
+ .replace("{from}", fromTitle)
533
+ .replace("{to}", toTitle);
534
+ const pathId = `pathId:${title}`;
535
+ const pathTier = neighborTier ?? focusTier;
536
+ if (!tierFilter || !pathTier || tierFilter.has(pathTier)) {
537
+ upsertNode(nodes, {
538
+ id: pathId,
539
+ tier: pathTier,
540
+ concept: {
541
+ identifier: {
542
+ kind: "pathId",
543
+ pathId: title,
544
+ identifierKey: pathId,
545
+ },
546
+ title,
547
+ },
548
+ });
549
+ addEdge(edges, {
550
+ type: "pathSource",
551
+ from: pathId,
552
+ to: fromId,
553
+ tier: pathTier,
554
+ });
555
+ addEdge(edges, {
556
+ type: "pathTarget",
557
+ from: pathId,
558
+ to: toId,
559
+ tier: pathTier,
560
+ });
561
+ attachPathViaAsset(pathId, toId, pathTier);
562
+ }
563
+ }
564
+ else {
565
+ addEdge(edges, {
566
+ type: emission.type,
567
+ from: fromId,
568
+ to: toId,
569
+ tier: neighborTier ?? focusTier,
570
+ });
571
+ if (emission.type === "inGroup" &&
572
+ (neighborKind === "pluginId" ||
573
+ relatedConcept?.identifier.kind === "pluginId")) {
574
+ const pluginId = neighborSide === "to" ? toId : fromId;
575
+ const family = readVulnerabilityFamily(relatedDoc) ||
576
+ readVulnerabilityFamily(frame.doc);
577
+ emitInFamily(pluginId, family || "pending", neighborTier ?? focusTier);
578
+ }
579
+ if (emission.type === "inZone" && item?.zone != null) {
580
+ focusAssetZones.add(String(item.zone).trim());
581
+ }
582
+ if (emission.type === "observedInZone" && item?.zone != null) {
583
+ focusAssetZones.add(String(item.zone).trim());
584
+ }
585
+ }
586
+ }
587
+ }
588
+ // Enqueue related record for further template expansion
589
+ if (relatedDoc && relatedConcept && relatedObjectType && frame.depth + 1 < maxDepth) {
590
+ const key = `${relatedObjectType}:${relatedConcept.identifier.identifierKey}`;
591
+ if (!visited.has(key)) {
592
+ visited.add(key);
593
+ queue.push({
594
+ objectType: relatedObjectType,
595
+ doc: relatedDoc,
596
+ depth: frame.depth + 1,
597
+ });
598
+ }
599
+ }
600
+ }
601
+ };
602
+ const runTemplate = async (template, frame, frameConcept) => {
603
+ const src = template.source;
604
+ if (src.kind === "associatedArray") {
605
+ let items = Array.isArray(frame.doc[src.key])
606
+ ? frame.doc[src.key]
607
+ : [];
608
+ const hadRealItems = items.length > 0;
609
+ // Fallback: synthesize topology items from data fields when associated bucket missing
610
+ if (!items.length && src.key === "associatedTopology") {
611
+ const data = readDataObject(frame.doc);
612
+ // Subnet focus: expose zone/reachability in the same item shape assets use.
613
+ if (frame.objectType === "subnets") {
614
+ const cidr = data.cidr != null
615
+ ? String(data.cidr).trim()
616
+ : frameConcept.identifier.kind === "cidr"
617
+ ? String(frameConcept.identifier.cidr ??
618
+ frameConcept.identifier.identifierKey.replace(/^cidr:/, "")).trim()
619
+ : "";
620
+ if (cidr) {
621
+ items = [
622
+ {
623
+ cidr,
624
+ zone: data.zone != null ? String(data.zone).trim() : "",
625
+ reachableFromZones: asStringList(data.reachableFromZones),
626
+ },
627
+ ];
628
+ }
629
+ }
630
+ const subnetCidrs = asStringList(data.subnetCidrs);
631
+ if (!items.length && subnetCidrs.length) {
632
+ const cidr = subnetCidrs[0];
633
+ let zone = asStringList(data.zones)[0] ?? "";
634
+ let reachableFromZones = [];
635
+ try {
636
+ const matched = await listSnapshots(client, "subnets", {
637
+ query: { "data.cidr": cidr },
638
+ limit: 1,
639
+ });
640
+ const subnet = matched[0];
641
+ if (subnet) {
642
+ const subnetData = readDataObject(subnet);
643
+ if (!zone && subnetData.zone != null)
644
+ zone = String(subnetData.zone);
645
+ reachableFromZones = asStringList(subnetData.reachableFromZones);
646
+ }
647
+ }
648
+ catch {
649
+ /* ignore lookup failures — keep empty reachability */
650
+ }
651
+ items = [{ cidr, zone, reachableFromZones }];
652
+ }
653
+ else if (!items.length) {
654
+ const zones = asStringList(data.zones);
655
+ if (zones.length) {
656
+ items = zones.map((zone) => ({ zone }));
657
+ }
658
+ }
659
+ }
660
+ if (src.key === "associatedTopology" && items.length) {
661
+ if (hadRealItems)
662
+ hadRealAssociatedTopology = true;
663
+ else
664
+ projectedTopology = true;
665
+ for (const item of items) {
666
+ if (item.zone != null && String(item.zone).trim()) {
667
+ focusAssetZones.add(String(item.zone).trim());
668
+ }
669
+ }
670
+ }
671
+ for (const item of items) {
672
+ await emitEdgesForItem(template, frame, frameConcept, item, null, null);
673
+ }
674
+ // Enqueue real subnet snapshots so subnet-path-facts / possiblePaths can expand.
675
+ if (src.key === "associatedTopology" && frame.depth + 1 < maxDepth) {
676
+ for (const item of items) {
677
+ const cidr = item.cidr != null ? String(item.cidr).trim() : "";
678
+ if (!cidr)
679
+ continue;
680
+ try {
681
+ const matched = await listSnapshots(client, "subnets", {
682
+ query: { "data.cidr": cidr },
683
+ limit: 1,
684
+ });
685
+ const subnet = matched[0];
686
+ if (!subnet)
687
+ continue;
688
+ const subnetConcept = conceptFromRecord(subnet);
689
+ if (!subnetConcept)
690
+ continue;
691
+ const key = `subnets:${subnetConcept.identifier.identifierKey}`;
692
+ if (visited.has(key))
693
+ continue;
694
+ visited.add(key);
695
+ const subnetRecordId = recordIdOf(subnet);
696
+ upsertNode(nodes, {
697
+ id: subnetConcept.identifier.identifierKey,
698
+ tier: tierForObjectType(descriptor, "subnets"),
699
+ concept: subnetConcept,
700
+ objectType: "subnets",
701
+ recordId: subnetRecordId || undefined,
702
+ });
703
+ queue.push({
704
+ objectType: "subnets",
705
+ doc: subnet,
706
+ depth: frame.depth + 1,
707
+ });
708
+ }
709
+ catch {
710
+ /* ignore */
711
+ }
712
+ }
713
+ }
714
+ return;
715
+ }
716
+ if (src.kind === "queryByField") {
717
+ const joinValue = readJoinValue(frame.doc, src.sourcePath);
718
+ if (joinValue == null || joinValue === "")
719
+ return;
720
+ const query = Array.isArray(joinValue)
721
+ ? { [src.targetPath]: { $in: joinValue } }
722
+ : { [src.targetPath]: joinValue };
723
+ let related = [];
724
+ try {
725
+ related = await listSnapshots(client, src.objectType, {
726
+ query,
727
+ limit: src.limit ?? 50,
728
+ });
729
+ }
730
+ catch {
731
+ related = [];
732
+ }
733
+ if (src.objectType === "vulnerabilities") {
734
+ notes.vulnerabilityCount = related.length;
735
+ }
736
+ for (const rel of related) {
737
+ await emitEdgesForItem(template, frame, frameConcept, null, rel, src.objectType);
738
+ }
739
+ return;
740
+ }
741
+ if (src.kind === "relation") {
742
+ const entity = await getEntity(frame.objectType);
743
+ const relation = entity?.relations?.[src.key];
744
+ if (!relation)
745
+ return;
746
+ const targetEntity = relation.targetEntity ?? relation.targetObjectType;
747
+ if (!targetEntity)
748
+ return;
749
+ const joinValue = readJoinValue(frame.doc, relation.source.path);
750
+ if (joinValue == null || joinValue === "")
751
+ return;
752
+ const query = Array.isArray(joinValue)
753
+ ? { [relation.target.path]: { $in: joinValue } }
754
+ : { [relation.target.path]: joinValue };
755
+ let related = [];
756
+ try {
757
+ related = await listSnapshots(client, targetEntity, {
758
+ query,
759
+ limit: 50,
760
+ });
761
+ }
762
+ catch {
763
+ related = [];
764
+ }
765
+ for (const rel of related) {
766
+ await emitEdgesForItem(template, frame, frameConcept, null, rel, targetEntity);
767
+ }
768
+ return;
769
+ }
770
+ if (src.kind === "pathFacts") {
771
+ const factsRaw = frame.doc[src.key];
772
+ if (!factsRaw)
773
+ return;
774
+ const facts = normalizePathFacts(factsRaw);
775
+ if (!facts.length) {
776
+ notes.pathFactsPresent = true;
777
+ return;
778
+ }
779
+ notes.pathFactsPresent = true;
780
+ const pathTier = tierForObjectType(descriptor, "topology-zones") ?? focusTier;
781
+ if (tierFilter && pathTier && !tierFilter.has(pathTier))
782
+ return;
783
+ const subnetZone = String(readDataObject(frame.doc).zone ?? "").trim() ||
784
+ [...focusAssetZones][0] ||
785
+ "";
786
+ for (const fact of facts) {
787
+ const fromZone = String(fact.from ?? "").trim();
788
+ const toZone = String(fact.to ?? "").trim();
789
+ if (!fromZone || !toZone)
790
+ continue;
791
+ const hops = (fact.hops ?? []).map((h) => String(h).trim()).filter(Boolean);
792
+ const title = fact.title?.trim() ||
793
+ [fromZone, ...hops, toZone].join(" → ");
794
+ const pathId = `pathId:${title}`;
795
+ upsertNode(nodes, {
796
+ id: pathId,
797
+ tier: pathTier,
798
+ concept: {
799
+ identifier: {
800
+ kind: "pathId",
801
+ pathId: title,
802
+ identifierKey: pathId,
803
+ },
804
+ title,
805
+ },
806
+ });
807
+ const fromId = buildId("zoneName", fromZone);
808
+ const toId = buildId("zoneName", toZone);
809
+ ensureSyntheticNode(nodes, fromId, "zoneName", "topology-zones", pathTier);
810
+ ensureSyntheticNode(nodes, toId, "zoneName", "topology-zones", pathTier);
811
+ addEdge(edges, {
812
+ type: "pathSource",
813
+ from: pathId,
814
+ to: fromId,
815
+ tier: pathTier,
816
+ });
817
+ for (const hop of hops) {
818
+ const hopId = buildId("zoneName", hop);
819
+ ensureSyntheticNode(nodes, hopId, "zoneName", "topology-zones", pathTier);
820
+ addEdge(edges, {
821
+ type: "pathHop",
822
+ from: pathId,
823
+ to: hopId,
824
+ tier: pathTier,
825
+ });
826
+ }
827
+ addEdge(edges, {
828
+ type: "pathTarget",
829
+ from: pathId,
830
+ to: toId,
831
+ tier: pathTier,
832
+ });
833
+ const viaExplicit = fact.viaAsset ??
834
+ (subnetZone && (toZone === subnetZone || hops.includes(subnetZone))
835
+ ? true
836
+ : undefined);
837
+ attachPathViaAsset(pathId, toId, pathTier, viaExplicit);
838
+ }
839
+ }
840
+ };
841
+ while (queue.length) {
842
+ const frame = queue.shift();
843
+ const frameConcept = conceptFromRecord(frame.doc);
844
+ if (!frameConcept)
845
+ continue;
846
+ const entity = await getEntity(frame.objectType);
847
+ const cap = resolveEntityGraphCapability(frame.objectType, entity, inferredCaps[frame.objectType]);
848
+ const templates = (cap?.edgeTemplates ?? []).filter((t) => includeTemplates ? includeTemplates.has(t.id) : true);
849
+ for (const template of templates) {
850
+ await runTemplate(template, frame, frameConcept);
851
+ }
852
+ }
853
+ const focusKey = focusConcept.identifier.identifierKey;
854
+ notes.focusAssetId = focusKey;
855
+ notes.focusObjectType = focusObjectType;
856
+ notes.joinPoint = `${focusKey} connects topology tier ↔ vulnerabilities tier`;
857
+ const analytics = focusDoc.analytics;
858
+ if (analytics && typeof analytics === "object") {
859
+ const a = analytics;
860
+ const vulns = a.vulnerabilities;
861
+ if (vulns && typeof vulns === "object") {
862
+ notes.assetAnalytics = vulns;
863
+ }
864
+ else {
865
+ notes.assetAnalytics = analytics;
866
+ }
867
+ }
868
+ const namedInReports = getByPath(focusDoc, "data.namedInReports");
869
+ if (Array.isArray(namedInReports) && namedInReports.length) {
870
+ notes.namedInReports = namedInReports;
871
+ }
872
+ const topologyGap = collectTopologyGap(focusDoc, {
873
+ hadRealAssociatedTopology,
874
+ projectedTopology,
875
+ });
876
+ if (topologyGap)
877
+ notes.topologyGap = topologyGap;
878
+ return {
879
+ focus: {
880
+ recordId: focusRecordId || undefined,
881
+ concept: focusConcept,
882
+ },
883
+ nodes: [...nodes.values()],
884
+ edges,
885
+ notes,
886
+ };
887
+ }
888
+ //# sourceMappingURL=build-graph-document.js.map