@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.
- package/catalox-seeds/inputs/entity-descriptors/assets.json +166 -0
- package/catalox-seeds/inputs/entity-descriptors/subnets.json +115 -1
- package/catalox-seeds/inputs/entity-descriptors/topology-zones.json +52 -1
- package/catalox-seeds/inputs/entity-descriptors/vulnerabilities-groups.json +10 -2
- package/catalox-seeds/inputs/entity-descriptors/vulnerabilities.json +79 -7
- package/catalox-seeds/inputs/graph-descriptors/asset-subnet-paths-graph.json +41 -0
- package/catalox-seeds/inputs/graph-descriptors/asset-topology-vulnerabilities-graph.json +81 -0
- package/catalox-seeds/inputs/graph-descriptors/asset-vulnerability-group-graph.json +38 -0
- package/catalox-seeds/inputs/graph-descriptors/vulnerability-asset-subnet-graph.json +40 -0
- package/catalox-seeds/inputs/list-descriptors/assets-exploitability-decisions-list.json +4 -0
- package/catalox-seeds/inputs/list-descriptors/assets-potential-impact-decisions-list.json +16 -8
- package/catalox-seeds/inputs/manifest.json +4 -0
- package/catalox-seeds/inputs/memorix-catalog-ids.json +2 -1
- package/catalox-seeds/memorix-retrieval-descriptors.manifest.json +1186 -34
- package/dist/client/catalox-adapter.d.ts.map +1 -1
- package/dist/client/catalox-adapter.js +10 -1
- package/dist/client/catalox-adapter.js.map +1 -1
- package/dist/client/catalox-like.d.ts +6 -0
- package/dist/client/catalox-like.d.ts.map +1 -1
- package/dist/data/identity.d.ts +19 -1
- package/dist/data/identity.d.ts.map +1 -1
- package/dist/data/identity.js +41 -25
- package/dist/data/identity.js.map +1 -1
- package/dist/data/record-info.d.ts +1 -1
- package/dist/data/record-info.d.ts.map +1 -1
- package/dist/descriptors/catalog-ids.d.ts +1 -0
- package/dist/descriptors/catalog-ids.d.ts.map +1 -1
- package/dist/descriptors/catalog-ids.js +1 -0
- package/dist/descriptors/catalog-ids.js.map +1 -1
- package/dist/descriptors/descriptor-types.d.ts +3 -1
- package/dist/descriptors/descriptor-types.d.ts.map +1 -1
- package/dist/errors/issues.d.ts +2 -0
- package/dist/errors/issues.d.ts.map +1 -1
- package/dist/errors/issues.js.map +1 -1
- package/dist/graphs/build-graph-directory.d.ts +28 -0
- package/dist/graphs/build-graph-directory.d.ts.map +1 -0
- package/dist/graphs/build-graph-directory.js +253 -0
- package/dist/graphs/build-graph-directory.js.map +1 -0
- package/dist/graphs/build-graph-document.d.ts +13 -0
- package/dist/graphs/build-graph-document.d.ts.map +1 -0
- package/dist/graphs/build-graph-document.js +888 -0
- package/dist/graphs/build-graph-document.js.map +1 -0
- package/dist/graphs/fallback-graph-capabilities.d.ts +16 -0
- package/dist/graphs/fallback-graph-capabilities.d.ts.map +1 -0
- package/dist/graphs/fallback-graph-capabilities.js +376 -0
- package/dist/graphs/fallback-graph-capabilities.js.map +1 -0
- package/dist/graphs/index.d.ts +9 -0
- package/dist/graphs/index.d.ts.map +1 -0
- package/dist/graphs/index.js +8 -0
- package/dist/graphs/index.js.map +1 -0
- package/dist/graphs/infer-graph-descriptor.d.ts +17 -0
- package/dist/graphs/infer-graph-descriptor.d.ts.map +1 -0
- package/dist/graphs/infer-graph-descriptor.js +234 -0
- package/dist/graphs/infer-graph-descriptor.js.map +1 -0
- package/dist/graphs/load-graph-descriptor.d.ts +31 -0
- package/dist/graphs/load-graph-descriptor.d.ts.map +1 -0
- package/dist/graphs/load-graph-descriptor.js +141 -0
- package/dist/graphs/load-graph-descriptor.js.map +1 -0
- package/dist/graphs/record-access.d.ts +36 -0
- package/dist/graphs/record-access.d.ts.map +1 -0
- package/dist/graphs/record-access.js +178 -0
- package/dist/graphs/record-access.js.map +1 -0
- package/dist/graphs/types.d.ts +70 -0
- package/dist/graphs/types.d.ts.map +1 -0
- package/dist/graphs/types.js +17 -0
- package/dist/graphs/types.js.map +1 -0
- package/dist/graphs/validate-graph-metadata.d.ts +36 -0
- package/dist/graphs/validate-graph-metadata.d.ts.map +1 -0
- package/dist/graphs/validate-graph-metadata.js +71 -0
- package/dist/graphs/validate-graph-metadata.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/retrieval/fetch-list.d.ts.map +1 -1
- package/dist/retrieval/fetch-list.js +3 -1
- package/dist/retrieval/fetch-list.js.map +1 -1
- package/dist/retrieval/resolve-fields.d.ts.map +1 -1
- package/dist/retrieval/resolve-fields.js +7 -1
- package/dist/retrieval/resolve-fields.js.map +1 -1
- package/dist/retrieval/resolve-list-analytics.d.ts +2 -2
- package/dist/retrieval/resolve-list-analytics.d.ts.map +1 -1
- package/dist/retrieval/resolve-list-analytics.js.map +1 -1
- package/dist/retrieval/resolve-property-source.d.ts.map +1 -1
- package/dist/retrieval/resolve-property-source.js +10 -3
- package/dist/retrieval/resolve-property-source.js.map +1 -1
- package/dist/seeds/default-seed-spec.d.ts.map +1 -1
- package/dist/seeds/default-seed-spec.js +25 -1
- package/dist/seeds/default-seed-spec.js.map +1 -1
- package/dist/tests/catalox-adapter-scope.test.d.ts +2 -0
- package/dist/tests/catalox-adapter-scope.test.d.ts.map +1 -0
- package/dist/tests/catalox-adapter-scope.test.js +26 -0
- package/dist/tests/catalox-adapter-scope.test.js.map +1 -0
- package/dist/tests/fetch-list.test.js +28 -0
- package/dist/tests/fetch-list.test.js.map +1 -1
- package/dist/tests/graphs.test.d.ts +2 -0
- package/dist/tests/graphs.test.d.ts.map +1 -0
- package/dist/tests/graphs.test.js +423 -0
- package/dist/tests/graphs.test.js.map +1 -0
- package/dist/tests/identity.test.js +13 -1
- package/dist/tests/identity.test.js.map +1 -1
- package/dist/tests/infer-graph.test.d.ts +2 -0
- package/dist/tests/infer-graph.test.d.ts.map +1 -0
- package/dist/tests/infer-graph.test.js +19 -0
- package/dist/tests/infer-graph.test.js.map +1 -0
- package/dist/tests/resolve-filters.test.js +8 -0
- package/dist/tests/resolve-filters.test.js.map +1 -1
- package/dist/tests/seed-manifest.test.js +3 -0
- package/dist/tests/seed-manifest.test.js.map +1 -1
- package/dist/tests/validate-graph-metadata.test.d.ts +2 -0
- package/dist/tests/validate-graph-metadata.test.d.ts.map +1 -0
- package/dist/tests/validate-graph-metadata.test.js +81 -0
- package/dist/tests/validate-graph-metadata.test.js.map +1 -0
- 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
|