@neat.is/core 0.4.20-dev.20260629 → 0.4.22
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/dist/{chunk-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-GNAX2CBF.js → chunk-OQ7CHOY2.js} +859 -159
- package/dist/chunk-OQ7CHOY2.js.map +1 -0
- package/dist/{chunk-62ELQVIP.js → chunk-XRE4OQWJ.js} +57 -9
- package/dist/chunk-XRE4OQWJ.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1232 -354
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +884 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +928 -150
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +806 -121
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -41,13 +41,13 @@ var init_cjs_shims = __esm({
|
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
// src/auth.ts
|
|
44
|
-
function
|
|
44
|
+
function isLoopbackHost2(host) {
|
|
45
45
|
if (!host) return false;
|
|
46
46
|
return LOOPBACK_HOSTS.has(host);
|
|
47
47
|
}
|
|
48
48
|
function assertBindAuthority(host, token) {
|
|
49
49
|
if (token && token.length > 0) return;
|
|
50
|
-
if (
|
|
50
|
+
if (isLoopbackHost2(host)) return;
|
|
51
51
|
throw new BindAuthorityError(host);
|
|
52
52
|
}
|
|
53
53
|
function mountBearerAuth(app, opts) {
|
|
@@ -573,6 +573,22 @@ async function buildOtelReceiver(opts) {
|
|
|
573
573
|
};
|
|
574
574
|
return decorated;
|
|
575
575
|
}
|
|
576
|
+
async function listenSteppingOtlp(app, requestedPort, host) {
|
|
577
|
+
let port = requestedPort;
|
|
578
|
+
for (let attempt = 0; ; attempt++) {
|
|
579
|
+
try {
|
|
580
|
+
return await app.listen({ port, host });
|
|
581
|
+
} catch (err) {
|
|
582
|
+
const code = err.code;
|
|
583
|
+
const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
|
|
584
|
+
if (!canStep) throw err;
|
|
585
|
+
console.warn(
|
|
586
|
+
`otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
|
|
587
|
+
);
|
|
588
|
+
port += OTLP_STEP_STRIDE;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
576
592
|
function logSpanHandler(span) {
|
|
577
593
|
const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : "<root>";
|
|
578
594
|
const status2 = span.statusCode === 2 ? "ERROR" : "OK";
|
|
@@ -581,7 +597,7 @@ function logSpanHandler(span) {
|
|
|
581
597
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
582
598
|
);
|
|
583
599
|
}
|
|
584
|
-
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
|
|
600
|
+
var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
|
|
585
601
|
var init_otel = __esm({
|
|
586
602
|
"src/otel.ts"() {
|
|
587
603
|
"use strict";
|
|
@@ -597,6 +613,8 @@ var init_otel = __esm({
|
|
|
597
613
|
exportTraceServiceRequestType = null;
|
|
598
614
|
exportTraceServiceResponseType = null;
|
|
599
615
|
cachedProtobufResponseBody = null;
|
|
616
|
+
OTLP_STEP_ATTEMPTS = 8;
|
|
617
|
+
OTLP_STEP_STRIDE = 1;
|
|
600
618
|
}
|
|
601
619
|
});
|
|
602
620
|
|
|
@@ -1272,22 +1290,164 @@ var rootCauseShapes = {
|
|
|
1272
1290
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1273
1291
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1274
1292
|
};
|
|
1275
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1293
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1276
1294
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1277
1295
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1278
1296
|
const shape = rootCauseShapes[origin.type];
|
|
1279
|
-
if (
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1297
|
+
if (shape) {
|
|
1298
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1299
|
+
const match = shape(graph, origin, walk);
|
|
1300
|
+
if (match) {
|
|
1301
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1302
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1303
|
+
rootCauseNode: match.rootCauseNode,
|
|
1304
|
+
rootCauseReason: reason,
|
|
1305
|
+
traversalPath: walk.path,
|
|
1306
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1307
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1308
|
+
fixRecommendation: match.fixRecommendation
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1313
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1314
|
+
if (crossService) return crossService;
|
|
1315
|
+
}
|
|
1316
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1317
|
+
}
|
|
1318
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1319
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1320
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1321
|
+
}
|
|
1322
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1323
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1324
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1325
|
+
if (relevant.length === 0) return null;
|
|
1326
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1327
|
+
const attrs = latest.attributes ?? {};
|
|
1328
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1329
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1330
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1331
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1332
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1333
|
+
const count = sameMode.length;
|
|
1334
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1335
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1336
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1337
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1338
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1339
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1340
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1341
|
+
return {
|
|
1342
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1343
|
+
rootCauseReason,
|
|
1344
|
+
...fileNode ? { fileNode } : {},
|
|
1345
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1346
|
+
};
|
|
1347
|
+
}
|
|
1348
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1349
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1350
|
+
if (!loc) return null;
|
|
1351
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1352
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1284
1353
|
return import_types.RootCauseResultSchema.parse({
|
|
1285
|
-
rootCauseNode:
|
|
1286
|
-
rootCauseReason:
|
|
1287
|
-
traversalPath
|
|
1288
|
-
edgeProvenances
|
|
1289
|
-
confidence:
|
|
1290
|
-
fixRecommendation:
|
|
1354
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1355
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1356
|
+
traversalPath,
|
|
1357
|
+
edgeProvenances,
|
|
1358
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1359
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
function isFailingCallEdge(e) {
|
|
1363
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1364
|
+
}
|
|
1365
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1366
|
+
const ids = [serviceId3];
|
|
1367
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1368
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1369
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1370
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1371
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1372
|
+
}
|
|
1373
|
+
return ids;
|
|
1374
|
+
}
|
|
1375
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1376
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1377
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1378
|
+
if (ec !== cc) return ec > cc;
|
|
1379
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1380
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1381
|
+
}
|
|
1382
|
+
return id < curId;
|
|
1383
|
+
}
|
|
1384
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1385
|
+
let best = null;
|
|
1386
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1387
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1388
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1389
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1390
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1391
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1392
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1393
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1394
|
+
best = { nextService: owner.id, edge: e };
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
return best;
|
|
1399
|
+
}
|
|
1400
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1401
|
+
const path43 = [originServiceId];
|
|
1402
|
+
const edges = [];
|
|
1403
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1404
|
+
let current = originServiceId;
|
|
1405
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1406
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1407
|
+
if (!hop) break;
|
|
1408
|
+
path43.push(hop.nextService);
|
|
1409
|
+
edges.push(hop.edge);
|
|
1410
|
+
visited.add(hop.nextService);
|
|
1411
|
+
current = hop.nextService;
|
|
1412
|
+
}
|
|
1413
|
+
if (edges.length === 0) return null;
|
|
1414
|
+
return { path: path43, edges, culprit: current };
|
|
1415
|
+
}
|
|
1416
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1417
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1418
|
+
if (!chain) return null;
|
|
1419
|
+
const culprit = chain.culprit;
|
|
1420
|
+
const path43 = [...chain.path];
|
|
1421
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1422
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1423
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1424
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1425
|
+
if (loc) {
|
|
1426
|
+
let rootCauseNode = culprit;
|
|
1427
|
+
if (loc.fileNode) {
|
|
1428
|
+
path43.push(loc.fileNode);
|
|
1429
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1430
|
+
rootCauseNode = loc.fileNode;
|
|
1431
|
+
}
|
|
1432
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1433
|
+
rootCauseNode,
|
|
1434
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1435
|
+
traversalPath: path43,
|
|
1436
|
+
edgeProvenances,
|
|
1437
|
+
confidence,
|
|
1438
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1442
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1443
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1444
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1445
|
+
rootCauseNode: culprit,
|
|
1446
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1447
|
+
traversalPath: path43,
|
|
1448
|
+
edgeProvenances,
|
|
1449
|
+
confidence,
|
|
1450
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1291
1451
|
});
|
|
1292
1452
|
}
|
|
1293
1453
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1310,14 +1470,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1310
1470
|
});
|
|
1311
1471
|
}
|
|
1312
1472
|
if (frame.distance >= maxDepth) continue;
|
|
1313
|
-
const
|
|
1314
|
-
for (const [
|
|
1315
|
-
if (enqueued.has(
|
|
1316
|
-
enqueued.add(
|
|
1473
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1474
|
+
for (const [srcId, edge] of incoming) {
|
|
1475
|
+
if (enqueued.has(srcId)) continue;
|
|
1476
|
+
enqueued.add(srcId);
|
|
1317
1477
|
queue.push({
|
|
1318
|
-
nodeId:
|
|
1478
|
+
nodeId: srcId,
|
|
1319
1479
|
distance: frame.distance + 1,
|
|
1320
|
-
path: [...frame.path,
|
|
1480
|
+
path: [...frame.path, srcId],
|
|
1321
1481
|
pathEdges: [...frame.pathEdges, edge]
|
|
1322
1482
|
});
|
|
1323
1483
|
}
|
|
@@ -1373,6 +1533,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1373
1533
|
total: dependencies.length
|
|
1374
1534
|
});
|
|
1375
1535
|
}
|
|
1536
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1537
|
+
if (!graph.hasNode(nodeId)) {
|
|
1538
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1539
|
+
origin: nodeId,
|
|
1540
|
+
dependencies: [],
|
|
1541
|
+
observed: false,
|
|
1542
|
+
inboundObservedCount: 0,
|
|
1543
|
+
hasExtractedOutbound: false
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1547
|
+
const scope = [nodeId];
|
|
1548
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1549
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1550
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1551
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1552
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1553
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const dependencies = [];
|
|
1557
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1558
|
+
let hasExtractedOutbound = false;
|
|
1559
|
+
for (const src of scope) {
|
|
1560
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1561
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1562
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1563
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1564
|
+
if (!seenEdge.has(e.id)) {
|
|
1565
|
+
seenEdge.add(e.id);
|
|
1566
|
+
dependencies.push(e);
|
|
1567
|
+
}
|
|
1568
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1569
|
+
hasExtractedOutbound = true;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
let inboundObservedCount = 0;
|
|
1574
|
+
for (const tgt of scope) {
|
|
1575
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1576
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1577
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1578
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
dependencies.sort(
|
|
1582
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1583
|
+
);
|
|
1584
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1585
|
+
origin: nodeId,
|
|
1586
|
+
dependencies,
|
|
1587
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1588
|
+
inboundObservedCount,
|
|
1589
|
+
hasExtractedOutbound
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1376
1592
|
|
|
1377
1593
|
// src/policy.ts
|
|
1378
1594
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -1642,6 +1858,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
1642
1858
|
}
|
|
1643
1859
|
return out;
|
|
1644
1860
|
}
|
|
1861
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1862
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1863
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1864
|
+
const out = [];
|
|
1865
|
+
for (const policy of policies) {
|
|
1866
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1867
|
+
if (!m) continue;
|
|
1868
|
+
out.push({
|
|
1869
|
+
policyId: policy.id,
|
|
1870
|
+
policyName: policy.name,
|
|
1871
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1872
|
+
severity: policy.severity,
|
|
1873
|
+
onViolation: resolveOnViolation(policy),
|
|
1874
|
+
ruleType: policy.rule.type,
|
|
1875
|
+
match: m.match,
|
|
1876
|
+
reason: m.reason
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
return out;
|
|
1880
|
+
}
|
|
1881
|
+
function requiredProvenanceList(required) {
|
|
1882
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1883
|
+
return String(required);
|
|
1884
|
+
}
|
|
1885
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1886
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1887
|
+
for (const edgeId of incident) {
|
|
1888
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1889
|
+
if (e.type !== edgeType) continue;
|
|
1890
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1891
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1892
|
+
}
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1896
|
+
let found = null;
|
|
1897
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1898
|
+
if (found !== null) return;
|
|
1899
|
+
if (subjId === nodeId) return;
|
|
1900
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1901
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1902
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1903
|
+
});
|
|
1904
|
+
return found;
|
|
1905
|
+
}
|
|
1906
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1907
|
+
const rule = policy.rule;
|
|
1908
|
+
switch (rule.type) {
|
|
1909
|
+
case "structural": {
|
|
1910
|
+
if (node.type === rule.fromNodeType) {
|
|
1911
|
+
return {
|
|
1912
|
+
match: "subject",
|
|
1913
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
if (node.type === rule.toNodeType) {
|
|
1917
|
+
return {
|
|
1918
|
+
match: "region",
|
|
1919
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
return null;
|
|
1923
|
+
}
|
|
1924
|
+
case "ownership": {
|
|
1925
|
+
if (node.type === rule.nodeType) {
|
|
1926
|
+
return {
|
|
1927
|
+
match: "subject",
|
|
1928
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
return null;
|
|
1932
|
+
}
|
|
1933
|
+
case "blast-radius": {
|
|
1934
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1935
|
+
if (node.type === rule.nodeType) {
|
|
1936
|
+
return {
|
|
1937
|
+
match: "subject",
|
|
1938
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1942
|
+
if (subject) {
|
|
1943
|
+
return {
|
|
1944
|
+
match: "region",
|
|
1945
|
+
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
case "compatibility": {
|
|
1951
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1952
|
+
if (node.type === import_types2.NodeType.ServiceNode) {
|
|
1953
|
+
return {
|
|
1954
|
+
match: "subject",
|
|
1955
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1959
|
+
if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
|
|
1960
|
+
return {
|
|
1961
|
+
match: "region",
|
|
1962
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
case "provenance": {
|
|
1968
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1969
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1970
|
+
return {
|
|
1971
|
+
match: "subject",
|
|
1972
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1976
|
+
return {
|
|
1977
|
+
match: "region",
|
|
1978
|
+
reason: rule.targetNodeId !== void 0 ? `this node sits on a ${rule.edgeType} edge to ${rule.targetNodeId}, which must carry ${requiredList} provenance` : `this node sits on a ${rule.edgeType} edge, which must carry ${requiredList} provenance`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
return null;
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1645
1985
|
async function loadPolicyFile(policyPath) {
|
|
1646
1986
|
let raw;
|
|
1647
1987
|
try {
|
|
@@ -1750,9 +2090,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1750
2090
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1751
2091
|
}
|
|
1752
2092
|
}
|
|
1753
|
-
function
|
|
2093
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1754
2094
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1755
|
-
const v =
|
|
2095
|
+
const v = attrs[key];
|
|
1756
2096
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1757
2097
|
if (typeof v === "string") {
|
|
1758
2098
|
const n = Number(v);
|
|
@@ -1761,6 +2101,63 @@ function httpResponseStatus(span) {
|
|
|
1761
2101
|
}
|
|
1762
2102
|
return void 0;
|
|
1763
2103
|
}
|
|
2104
|
+
function httpResponseStatus(span) {
|
|
2105
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2106
|
+
}
|
|
2107
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2108
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2109
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2110
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2111
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2112
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2113
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2114
|
+
if (where) return `error on ${where}`;
|
|
2115
|
+
return void 0;
|
|
2116
|
+
}
|
|
2117
|
+
var GRPC_STATUS_NAMES = {
|
|
2118
|
+
1: "CANCELLED",
|
|
2119
|
+
2: "UNKNOWN",
|
|
2120
|
+
3: "INVALID_ARGUMENT",
|
|
2121
|
+
4: "DEADLINE_EXCEEDED",
|
|
2122
|
+
5: "NOT_FOUND",
|
|
2123
|
+
6: "ALREADY_EXISTS",
|
|
2124
|
+
7: "PERMISSION_DENIED",
|
|
2125
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2126
|
+
9: "FAILED_PRECONDITION",
|
|
2127
|
+
10: "ABORTED",
|
|
2128
|
+
11: "OUT_OF_RANGE",
|
|
2129
|
+
12: "UNIMPLEMENTED",
|
|
2130
|
+
13: "INTERNAL",
|
|
2131
|
+
14: "UNAVAILABLE",
|
|
2132
|
+
15: "DATA_LOSS",
|
|
2133
|
+
16: "UNAUTHENTICATED"
|
|
2134
|
+
};
|
|
2135
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2136
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2137
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2138
|
+
if (typeof v === "string") {
|
|
2139
|
+
const n = Number(v);
|
|
2140
|
+
if (Number.isFinite(n)) return n;
|
|
2141
|
+
}
|
|
2142
|
+
return void 0;
|
|
2143
|
+
}
|
|
2144
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2145
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2146
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2147
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2148
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2149
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2150
|
+
}
|
|
2151
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2152
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2153
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2154
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2155
|
+
}
|
|
2156
|
+
return void 0;
|
|
2157
|
+
}
|
|
2158
|
+
function incidentMessage(span) {
|
|
2159
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2160
|
+
}
|
|
1764
2161
|
function nowIso(ctx) {
|
|
1765
2162
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1766
2163
|
}
|
|
@@ -1780,13 +2177,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1780
2177
|
`[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
|
|
1781
2178
|
);
|
|
1782
2179
|
}
|
|
1783
|
-
function
|
|
2180
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1784
2181
|
for (const k of keys) {
|
|
1785
|
-
const v =
|
|
2182
|
+
const v = attrs[k];
|
|
1786
2183
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1787
2184
|
}
|
|
1788
2185
|
return void 0;
|
|
1789
2186
|
}
|
|
2187
|
+
function pickAttr(span, ...keys) {
|
|
2188
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2189
|
+
}
|
|
1790
2190
|
function hostFromUrl(u) {
|
|
1791
2191
|
if (!u) return void 0;
|
|
1792
2192
|
try {
|
|
@@ -1798,6 +2198,10 @@ function hostFromUrl(u) {
|
|
|
1798
2198
|
function pickAddress(span) {
|
|
1799
2199
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1800
2200
|
}
|
|
2201
|
+
function isLoopbackHost(host) {
|
|
2202
|
+
const h = host.toLowerCase();
|
|
2203
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2204
|
+
}
|
|
1801
2205
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1802
2206
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1803
2207
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1905,15 +2309,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1905
2309
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1906
2310
|
};
|
|
1907
2311
|
}
|
|
2312
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2313
|
+
if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
|
|
2314
|
+
let best = null;
|
|
2315
|
+
graph.forEachNode((_id, attrs) => {
|
|
2316
|
+
const a = attrs;
|
|
2317
|
+
if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
|
|
2318
|
+
if (a.discoveredVia === "otel") return;
|
|
2319
|
+
const p = a.path;
|
|
2320
|
+
if (!p) return;
|
|
2321
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2322
|
+
best = p;
|
|
2323
|
+
}
|
|
2324
|
+
});
|
|
2325
|
+
return best ?? relPath;
|
|
2326
|
+
}
|
|
1908
2327
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1909
|
-
const
|
|
2328
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2329
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
|
|
1910
2330
|
if (!graph.hasNode(fileNodeId)) {
|
|
1911
|
-
const language = languageForExt(
|
|
2331
|
+
const language = languageForExt(relPath);
|
|
1912
2332
|
const node = {
|
|
1913
2333
|
id: fileNodeId,
|
|
1914
2334
|
type: import_types3.NodeType.FileNode,
|
|
1915
2335
|
service: serviceName,
|
|
1916
|
-
path:
|
|
2336
|
+
path: relPath,
|
|
1917
2337
|
...language ? { language } : {},
|
|
1918
2338
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1919
2339
|
discoveredVia: "otel"
|
|
@@ -1941,6 +2361,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1941
2361
|
}
|
|
1942
2362
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1943
2363
|
var STITCH_MAX_DEPTH = 2;
|
|
2364
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2365
|
+
import_types3.EdgeType.CALLS,
|
|
2366
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2367
|
+
import_types3.EdgeType.DEPENDS_ON
|
|
2368
|
+
]);
|
|
1944
2369
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1945
2370
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1946
2371
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2114,6 +2539,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2114
2539
|
for (const edgeId of outbound) {
|
|
2115
2540
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2116
2541
|
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
2542
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2117
2543
|
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2118
2544
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2119
2545
|
if (!visited.has(edge.target)) {
|
|
@@ -2146,6 +2572,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2146
2572
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2147
2573
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2148
2574
|
}
|
|
2575
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
2576
|
+
const sid = (0, import_types3.serviceId)(span.service, span.env);
|
|
2577
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
2578
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
2579
|
+
if (callSite) {
|
|
2580
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
2581
|
+
return (0, import_types3.fileId)(span.service, relPath);
|
|
2582
|
+
}
|
|
2583
|
+
return sid;
|
|
2584
|
+
}
|
|
2149
2585
|
function sanitizeAttributes(attrs) {
|
|
2150
2586
|
const out = {};
|
|
2151
2587
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2154,7 +2590,7 @@ function sanitizeAttributes(attrs) {
|
|
|
2154
2590
|
}
|
|
2155
2591
|
return out;
|
|
2156
2592
|
}
|
|
2157
|
-
function buildErrorEventForReceiver(span) {
|
|
2593
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
2158
2594
|
if (span.statusCode !== 2) return null;
|
|
2159
2595
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2160
2596
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -2164,16 +2600,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
2164
2600
|
service: span.service,
|
|
2165
2601
|
traceId: span.traceId,
|
|
2166
2602
|
spanId: span.spanId,
|
|
2167
|
-
errorMessage: span
|
|
2603
|
+
errorMessage: incidentMessage(span),
|
|
2168
2604
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2169
2605
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2170
2606
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2171
|
-
affectedNode: (
|
|
2607
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
2172
2608
|
};
|
|
2173
2609
|
}
|
|
2174
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
2610
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
2175
2611
|
return async (span) => {
|
|
2176
|
-
const ev = buildErrorEventForReceiver(span);
|
|
2612
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
2177
2613
|
if (!ev) return;
|
|
2178
2614
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
|
|
2179
2615
|
await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -2257,7 +2693,10 @@ async function handleSpan(ctx, span) {
|
|
|
2257
2693
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2258
2694
|
cacheSpanService(span, nowMs, callSite);
|
|
2259
2695
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2260
|
-
const callSiteEvidence = callSite ? {
|
|
2696
|
+
const callSiteEvidence = callSite ? {
|
|
2697
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2698
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2699
|
+
} : void 0;
|
|
2261
2700
|
let affectedNode = sourceId;
|
|
2262
2701
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2263
2702
|
if (span.dbSystem) {
|
|
@@ -2279,7 +2718,7 @@ async function handleSpan(ctx, span) {
|
|
|
2279
2718
|
} else {
|
|
2280
2719
|
const host = pickAddress(span);
|
|
2281
2720
|
let resolvedViaAddress = false;
|
|
2282
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2721
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2283
2722
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2284
2723
|
if (targetId && targetId !== sourceId) {
|
|
2285
2724
|
upsertObservedEdge(
|
|
@@ -2314,7 +2753,11 @@ async function handleSpan(ctx, span) {
|
|
|
2314
2753
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2315
2754
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2316
2755
|
const fallbackEvidence = parent.callSite ? {
|
|
2317
|
-
file:
|
|
2756
|
+
file: reconcileObservedRelPath(
|
|
2757
|
+
ctx.graph,
|
|
2758
|
+
parent.service,
|
|
2759
|
+
parent.callSite.relPath
|
|
2760
|
+
),
|
|
2318
2761
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2319
2762
|
} : void 0;
|
|
2320
2763
|
upsertObservedEdge(
|
|
@@ -2339,7 +2782,7 @@ async function handleSpan(ctx, span) {
|
|
|
2339
2782
|
service: span.service,
|
|
2340
2783
|
traceId: span.traceId,
|
|
2341
2784
|
spanId: span.spanId,
|
|
2342
|
-
errorMessage: span
|
|
2785
|
+
errorMessage: incidentMessage(span),
|
|
2343
2786
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2344
2787
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2345
2788
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2515,12 +2958,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2515
2958
|
async function readErrorEvents(errorsPath) {
|
|
2516
2959
|
try {
|
|
2517
2960
|
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
2518
|
-
|
|
2961
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2962
|
+
return dedupeIncidents(events);
|
|
2519
2963
|
} catch (err) {
|
|
2520
2964
|
if (err.code === "ENOENT") return [];
|
|
2521
2965
|
throw err;
|
|
2522
2966
|
}
|
|
2523
2967
|
}
|
|
2968
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2969
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2970
|
+
if (ev.errorType) return false;
|
|
2971
|
+
if (!ev.attributes) return false;
|
|
2972
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2973
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2974
|
+
}
|
|
2975
|
+
function dedupeIncidents(events) {
|
|
2976
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2977
|
+
const once = [];
|
|
2978
|
+
for (const ev of events) {
|
|
2979
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2980
|
+
if (key === void 0) {
|
|
2981
|
+
once.push(ev);
|
|
2982
|
+
continue;
|
|
2983
|
+
}
|
|
2984
|
+
if (seen.has(key)) continue;
|
|
2985
|
+
seen.add(key);
|
|
2986
|
+
once.push(ev);
|
|
2987
|
+
}
|
|
2988
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2989
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2990
|
+
for (const ev of once) {
|
|
2991
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2992
|
+
}
|
|
2993
|
+
return once.filter((ev) => {
|
|
2994
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2995
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2996
|
+
});
|
|
2997
|
+
}
|
|
2524
2998
|
function mergeSnapshot(graph, snapshot) {
|
|
2525
2999
|
const exported = snapshot.graph;
|
|
2526
3000
|
let nodesAdded = 0;
|
|
@@ -2601,10 +3075,17 @@ function isConfigFile(name) {
|
|
|
2601
3075
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2602
3076
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2603
3077
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3078
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2604
3079
|
return { match: true, fileType: "env" };
|
|
2605
3080
|
}
|
|
2606
3081
|
return { match: false, fileType: "" };
|
|
2607
3082
|
}
|
|
3083
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3084
|
+
return name === ".env.neat";
|
|
3085
|
+
}
|
|
3086
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3087
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3088
|
+
}
|
|
2608
3089
|
function isTestPath(filePath) {
|
|
2609
3090
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2610
3091
|
const segments = normalised.split("/");
|
|
@@ -3298,7 +3779,9 @@ async function walkSourceFiles(dir) {
|
|
|
3298
3779
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3299
3780
|
if (await isPythonVenvDir(full)) continue;
|
|
3300
3781
|
await walk(full);
|
|
3301
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name))
|
|
3782
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3783
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3784
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
3302
3785
|
out.push(full);
|
|
3303
3786
|
}
|
|
3304
3787
|
}
|
|
@@ -3468,11 +3951,28 @@ function collectJsImports(node, out) {
|
|
|
3468
3951
|
if (child) collectJsImports(child, out);
|
|
3469
3952
|
}
|
|
3470
3953
|
}
|
|
3954
|
+
function collectImportedNames(node, out) {
|
|
3955
|
+
if (node.type === "aliased_import") {
|
|
3956
|
+
const nameNode = node.childForFieldName("name");
|
|
3957
|
+
if (nameNode) out.push(nameNode.text);
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
if (node.type === "dotted_name") {
|
|
3961
|
+
out.push(node.text);
|
|
3962
|
+
return;
|
|
3963
|
+
}
|
|
3964
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3965
|
+
const child = node.namedChild(i);
|
|
3966
|
+
if (child) collectImportedNames(child, out);
|
|
3967
|
+
}
|
|
3968
|
+
}
|
|
3471
3969
|
function collectPyImports(node, out) {
|
|
3472
3970
|
if (node.type === "import_from_statement") {
|
|
3473
3971
|
let level = 0;
|
|
3474
3972
|
let modulePath = "";
|
|
3973
|
+
const names = [];
|
|
3475
3974
|
let pastFrom = false;
|
|
3975
|
+
let pastImport = false;
|
|
3476
3976
|
for (let i = 0; i < node.childCount; i++) {
|
|
3477
3977
|
const child = node.child(i);
|
|
3478
3978
|
if (!child) continue;
|
|
@@ -3480,26 +3980,30 @@ function collectPyImports(node, out) {
|
|
|
3480
3980
|
if (child.type === "from") pastFrom = true;
|
|
3481
3981
|
continue;
|
|
3482
3982
|
}
|
|
3483
|
-
if (
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
if (!rc) continue;
|
|
3488
|
-
if (rc.type === "import_prefix") {
|
|
3489
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3490
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3491
|
-
}
|
|
3492
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3983
|
+
if (!pastImport) {
|
|
3984
|
+
if (child.type === "import") {
|
|
3985
|
+
pastImport = true;
|
|
3986
|
+
continue;
|
|
3493
3987
|
}
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3988
|
+
if (child.type === "relative_import") {
|
|
3989
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3990
|
+
const rc = child.child(j);
|
|
3991
|
+
if (!rc) continue;
|
|
3992
|
+
if (rc.type === "import_prefix") {
|
|
3993
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
3994
|
+
if (rc.child(k)?.type === ".") level++;
|
|
3995
|
+
}
|
|
3996
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3997
|
+
}
|
|
3998
|
+
} else if (child.type === "dotted_name") {
|
|
3999
|
+
modulePath = child.text;
|
|
4000
|
+
}
|
|
4001
|
+
continue;
|
|
3499
4002
|
}
|
|
4003
|
+
collectImportedNames(child, names);
|
|
3500
4004
|
}
|
|
3501
4005
|
if (level > 0 || modulePath) {
|
|
3502
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4006
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3503
4007
|
}
|
|
3504
4008
|
}
|
|
3505
4009
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3601,8 +4105,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3601
4105
|
return null;
|
|
3602
4106
|
}
|
|
3603
4107
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3604
|
-
if (!imp.modulePath) return null;
|
|
3605
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3606
4108
|
let baseDir;
|
|
3607
4109
|
if (imp.level > 0) {
|
|
3608
4110
|
baseDir = import_node_path12.default.dirname(importerPath);
|
|
@@ -3610,13 +4112,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3610
4112
|
} else {
|
|
3611
4113
|
baseDir = serviceDir;
|
|
3612
4114
|
}
|
|
3613
|
-
const
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
4115
|
+
const moduleBase = imp.modulePath ? import_node_path12.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4116
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4117
|
+
let needModuleFile = imp.names.length === 0;
|
|
4118
|
+
for (const name of imp.names) {
|
|
4119
|
+
const submoduleFile = import_node_path12.default.join(moduleBase, `${name}.py`);
|
|
4120
|
+
const subpackageInit = import_node_path12.default.join(moduleBase, name, "__init__.py");
|
|
4121
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
4122
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, submoduleFile)));
|
|
4123
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
4124
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, subpackageInit)));
|
|
4125
|
+
} else {
|
|
4126
|
+
needModuleFile = true;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
if (needModuleFile) {
|
|
4130
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path12.default.join(moduleBase, "__init__.py")] : [import_node_path12.default.join(moduleBase, "__init__.py")];
|
|
4131
|
+
for (const candidate of moduleFileCandidates) {
|
|
4132
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4133
|
+
resolved.add(toPosix2(import_node_path12.default.relative(serviceDir, candidate)));
|
|
4134
|
+
break;
|
|
4135
|
+
}
|
|
3617
4136
|
}
|
|
3618
4137
|
}
|
|
3619
|
-
return
|
|
4138
|
+
return [...resolved];
|
|
3620
4139
|
}
|
|
3621
4140
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3622
4141
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3657,17 +4176,18 @@ async function addImports(graph, services) {
|
|
|
3657
4176
|
continue;
|
|
3658
4177
|
}
|
|
3659
4178
|
for (const imp of pyImports) {
|
|
3660
|
-
const
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
4179
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4180
|
+
for (const resolved of resolvedPaths) {
|
|
4181
|
+
edgesAdded += emitImportEdge(
|
|
4182
|
+
graph,
|
|
4183
|
+
service.pkg.name,
|
|
4184
|
+
importerFileId,
|
|
4185
|
+
relFile,
|
|
4186
|
+
resolved,
|
|
4187
|
+
imp.line,
|
|
4188
|
+
imp.snippet
|
|
4189
|
+
);
|
|
4190
|
+
}
|
|
3671
4191
|
}
|
|
3672
4192
|
continue;
|
|
3673
4193
|
}
|
|
@@ -4303,6 +4823,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4303
4823
|
edgesAdded++;
|
|
4304
4824
|
}
|
|
4305
4825
|
}
|
|
4826
|
+
if (allConfigs.length === 1) {
|
|
4827
|
+
const primary = allConfigs[0];
|
|
4828
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4829
|
+
const relPath = import_node_path20.default.relative(scanPath, primary.sourceFile);
|
|
4830
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4831
|
+
if (!graph.hasNode(cfgId)) {
|
|
4832
|
+
const cfgNode = {
|
|
4833
|
+
id: cfgId,
|
|
4834
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4835
|
+
name: import_node_path20.default.basename(primary.sourceFile),
|
|
4836
|
+
path: relPath,
|
|
4837
|
+
fileType: isConfigFile(import_node_path20.default.basename(primary.sourceFile)).fileType || "config"
|
|
4838
|
+
};
|
|
4839
|
+
graph.addNode(cfgId, cfgNode);
|
|
4840
|
+
nodesAdded++;
|
|
4841
|
+
}
|
|
4842
|
+
const cfgEdge = {
|
|
4843
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4844
|
+
source: service.node.id,
|
|
4845
|
+
target: cfgId,
|
|
4846
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4847
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4848
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4849
|
+
evidence: { file: toPosix2(relPath) }
|
|
4850
|
+
};
|
|
4851
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4852
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4853
|
+
edgesAdded++;
|
|
4854
|
+
}
|
|
4855
|
+
}
|
|
4306
4856
|
attachIncompatibilities(service, allConfigs);
|
|
4307
4857
|
if (graph.hasNode(service.node.id)) {
|
|
4308
4858
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4314,6 +4864,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4314
4864
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4315
4865
|
delete updated.incompatibilities;
|
|
4316
4866
|
}
|
|
4867
|
+
if (!service.node.dbConnectionTarget) {
|
|
4868
|
+
delete updated.dbConnectionTarget;
|
|
4869
|
+
}
|
|
4317
4870
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4318
4871
|
}
|
|
4319
4872
|
}
|
|
@@ -4491,7 +5044,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4491
5044
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4492
5045
|
if (seen.has(dedupKey)) continue;
|
|
4493
5046
|
seen.add(dedupKey);
|
|
4494
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
5047
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4495
5048
|
const ev = {
|
|
4496
5049
|
file: relFile,
|
|
4497
5050
|
line: site.line,
|
|
@@ -4511,7 +5064,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4511
5064
|
target: targetId,
|
|
4512
5065
|
type: import_types11.EdgeType.CALLS,
|
|
4513
5066
|
confidence,
|
|
4514
|
-
confidenceKind: "
|
|
5067
|
+
confidenceKind: "url-literal-service-target",
|
|
4515
5068
|
evidence: ev
|
|
4516
5069
|
});
|
|
4517
5070
|
continue;
|
|
@@ -5017,19 +5570,29 @@ init_cjs_shims();
|
|
|
5017
5570
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
5018
5571
|
var import_node_fs15 = require("fs");
|
|
5019
5572
|
var import_types20 = require("@neat.is/types");
|
|
5020
|
-
function
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5573
|
+
function readDockerfile(content) {
|
|
5574
|
+
let image = null;
|
|
5575
|
+
const ports = [];
|
|
5576
|
+
let cmd = null;
|
|
5577
|
+
let entrypoint = null;
|
|
5578
|
+
for (const raw of content.split("\n")) {
|
|
5024
5579
|
const line = raw.trim();
|
|
5025
5580
|
if (!line || line.startsWith("#")) continue;
|
|
5026
|
-
if (
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
if (
|
|
5030
|
-
|
|
5581
|
+
if (/^from\s+/i.test(line)) {
|
|
5582
|
+
const candidate = line.split(/\s+/)[1];
|
|
5583
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5584
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5585
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5586
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5587
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5588
|
+
}
|
|
5589
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5590
|
+
entrypoint = line;
|
|
5591
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5592
|
+
cmd = line;
|
|
5593
|
+
}
|
|
5031
5594
|
}
|
|
5032
|
-
return
|
|
5595
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5033
5596
|
}
|
|
5034
5597
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5035
5598
|
let nodesAdded = 0;
|
|
@@ -5048,14 +5611,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5048
5611
|
);
|
|
5049
5612
|
continue;
|
|
5050
5613
|
}
|
|
5051
|
-
const
|
|
5052
|
-
if (!image) continue;
|
|
5053
|
-
const node = makeInfraNode("container-image", image);
|
|
5614
|
+
const facts = readDockerfile(content);
|
|
5615
|
+
if (!facts.image) continue;
|
|
5616
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5054
5617
|
if (!graph.hasNode(node.id)) {
|
|
5055
5618
|
graph.addNode(node.id, node);
|
|
5056
5619
|
nodesAdded++;
|
|
5057
5620
|
}
|
|
5058
5621
|
const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
|
|
5622
|
+
const evidenceFile = toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath));
|
|
5059
5623
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5060
5624
|
graph,
|
|
5061
5625
|
service.pkg.name,
|
|
@@ -5074,12 +5638,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5074
5638
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
5075
5639
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5076
5640
|
evidence: {
|
|
5077
|
-
file:
|
|
5641
|
+
file: evidenceFile,
|
|
5642
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5078
5643
|
}
|
|
5079
5644
|
};
|
|
5080
5645
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5081
5646
|
edgesAdded++;
|
|
5082
5647
|
}
|
|
5648
|
+
for (const port of facts.ports) {
|
|
5649
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5650
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5651
|
+
graph.addNode(portNode.id, portNode);
|
|
5652
|
+
nodesAdded++;
|
|
5653
|
+
}
|
|
5654
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5655
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5656
|
+
const portEdge = {
|
|
5657
|
+
id: portEdgeId,
|
|
5658
|
+
source: fileNodeId,
|
|
5659
|
+
target: portNode.id,
|
|
5660
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5661
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5662
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5663
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5664
|
+
};
|
|
5665
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5666
|
+
edgesAdded++;
|
|
5667
|
+
}
|
|
5083
5668
|
}
|
|
5084
5669
|
return { nodesAdded, edgesAdded };
|
|
5085
5670
|
}
|
|
@@ -5088,7 +5673,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5088
5673
|
init_cjs_shims();
|
|
5089
5674
|
var import_node_fs16 = require("fs");
|
|
5090
5675
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5676
|
+
var import_types21 = require("@neat.is/types");
|
|
5091
5677
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5678
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5092
5679
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5093
5680
|
if (depth > max) return [];
|
|
5094
5681
|
const out = [];
|
|
@@ -5105,24 +5692,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5105
5692
|
}
|
|
5106
5693
|
return out;
|
|
5107
5694
|
}
|
|
5695
|
+
function blockBody(content, from) {
|
|
5696
|
+
const open = content.indexOf("{", from);
|
|
5697
|
+
if (open === -1) return null;
|
|
5698
|
+
let depth = 0;
|
|
5699
|
+
for (let i = open; i < content.length; i++) {
|
|
5700
|
+
const ch = content[i];
|
|
5701
|
+
if (ch === "{") depth++;
|
|
5702
|
+
else if (ch === "}") {
|
|
5703
|
+
depth--;
|
|
5704
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5705
|
+
}
|
|
5706
|
+
}
|
|
5707
|
+
return null;
|
|
5708
|
+
}
|
|
5709
|
+
function lineAt(content, index) {
|
|
5710
|
+
let line = 1;
|
|
5711
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5712
|
+
if (content[i] === "\n") line++;
|
|
5713
|
+
}
|
|
5714
|
+
return line;
|
|
5715
|
+
}
|
|
5108
5716
|
async function addTerraformResources(graph, scanPath) {
|
|
5109
5717
|
let nodesAdded = 0;
|
|
5718
|
+
let edgesAdded = 0;
|
|
5110
5719
|
const files = await walkTfFiles(scanPath);
|
|
5111
5720
|
for (const file of files) {
|
|
5112
5721
|
const content = await import_node_fs16.promises.readFile(file, "utf8");
|
|
5722
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, file));
|
|
5723
|
+
const resources = [];
|
|
5724
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5113
5725
|
RESOURCE_RE.lastIndex = 0;
|
|
5114
5726
|
let m;
|
|
5115
5727
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5116
|
-
const
|
|
5728
|
+
const type = m[1];
|
|
5117
5729
|
const name = m[2];
|
|
5118
|
-
const node = makeInfraNode(
|
|
5730
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5119
5731
|
if (!graph.hasNode(node.id)) {
|
|
5120
5732
|
graph.addNode(node.id, node);
|
|
5121
5733
|
nodesAdded++;
|
|
5122
5734
|
}
|
|
5735
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5736
|
+
const resource = {
|
|
5737
|
+
type,
|
|
5738
|
+
name,
|
|
5739
|
+
nodeId: node.id,
|
|
5740
|
+
body: span?.body ?? "",
|
|
5741
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5742
|
+
};
|
|
5743
|
+
resources.push(resource);
|
|
5744
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5745
|
+
}
|
|
5746
|
+
for (const resource of resources) {
|
|
5747
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5748
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5749
|
+
let ref;
|
|
5750
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5751
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5752
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5753
|
+
const target = byKey.get(key);
|
|
5754
|
+
if (!target) continue;
|
|
5755
|
+
if (seen.has(target.nodeId)) continue;
|
|
5756
|
+
seen.add(target.nodeId);
|
|
5757
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5758
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5759
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5760
|
+
const edge = {
|
|
5761
|
+
id: edgeId,
|
|
5762
|
+
source: resource.nodeId,
|
|
5763
|
+
target: target.nodeId,
|
|
5764
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5765
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5766
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5767
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5768
|
+
};
|
|
5769
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5770
|
+
edgesAdded++;
|
|
5771
|
+
}
|
|
5123
5772
|
}
|
|
5124
5773
|
}
|
|
5125
|
-
return { nodesAdded, edgesAdded
|
|
5774
|
+
return { nodesAdded, edgesAdded };
|
|
5126
5775
|
}
|
|
5127
5776
|
|
|
5128
5777
|
// src/extract/infra/k8s.ts
|
|
@@ -5200,11 +5849,11 @@ var import_node_path33 = __toESM(require("path"), 1);
|
|
|
5200
5849
|
init_cjs_shims();
|
|
5201
5850
|
var import_node_fs18 = require("fs");
|
|
5202
5851
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
5203
|
-
var
|
|
5852
|
+
var import_types22 = require("@neat.is/types");
|
|
5204
5853
|
function dropOrphanedFileNodes(graph) {
|
|
5205
5854
|
const orphans = [];
|
|
5206
5855
|
graph.forEachNode((id, attrs) => {
|
|
5207
|
-
if (attrs.type !==
|
|
5856
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5208
5857
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5209
5858
|
orphans.push(id);
|
|
5210
5859
|
}
|
|
@@ -5217,7 +5866,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5217
5866
|
const bases = [scanPath, ...serviceDirs];
|
|
5218
5867
|
graph.forEachEdge((id, attrs) => {
|
|
5219
5868
|
const edge = attrs;
|
|
5220
|
-
if (edge.provenance !==
|
|
5869
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5221
5870
|
const evidenceFile = edge.evidence?.file;
|
|
5222
5871
|
if (!evidenceFile) return;
|
|
5223
5872
|
if (import_node_path32.default.isAbsolute(evidenceFile)) {
|
|
@@ -5300,7 +5949,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5300
5949
|
init_cjs_shims();
|
|
5301
5950
|
var import_node_fs19 = require("fs");
|
|
5302
5951
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5303
|
-
var
|
|
5952
|
+
var import_types23 = require("@neat.is/types");
|
|
5304
5953
|
var SCHEMA_VERSION = 4;
|
|
5305
5954
|
function migrateV1ToV2(payload) {
|
|
5306
5955
|
const nodes = payload.graph.nodes;
|
|
@@ -5322,12 +5971,12 @@ function migrateV2ToV3(payload) {
|
|
|
5322
5971
|
for (const edge of edges) {
|
|
5323
5972
|
const attrs = edge.attributes;
|
|
5324
5973
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5325
|
-
attrs.provenance =
|
|
5974
|
+
attrs.provenance = import_types23.Provenance.OBSERVED;
|
|
5326
5975
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5327
5976
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5328
5977
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5329
5978
|
if (type && source && target) {
|
|
5330
|
-
const newId = (0,
|
|
5979
|
+
const newId = (0, import_types23.observedEdgeId)(source, target, type);
|
|
5331
5980
|
attrs.id = newId;
|
|
5332
5981
|
if (edge.key) edge.key = newId;
|
|
5333
5982
|
}
|
|
@@ -5419,7 +6068,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
5419
6068
|
init_cjs_shims();
|
|
5420
6069
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5421
6070
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5422
|
-
var
|
|
6071
|
+
var import_types26 = require("@neat.is/types");
|
|
5423
6072
|
|
|
5424
6073
|
// src/extend/index.ts
|
|
5425
6074
|
init_cjs_shims();
|
|
@@ -5738,7 +6387,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
5738
6387
|
|
|
5739
6388
|
// src/divergences.ts
|
|
5740
6389
|
init_cjs_shims();
|
|
5741
|
-
var
|
|
6390
|
+
var import_types24 = require("@neat.is/types");
|
|
5742
6391
|
function bucketKey(source, target, type) {
|
|
5743
6392
|
return `${type}|${source}|${target}`;
|
|
5744
6393
|
}
|
|
@@ -5746,22 +6395,22 @@ function bucketEdges(graph) {
|
|
|
5746
6395
|
const buckets = /* @__PURE__ */ new Map();
|
|
5747
6396
|
graph.forEachEdge((id, attrs) => {
|
|
5748
6397
|
const e = attrs;
|
|
5749
|
-
const parsed = (0,
|
|
6398
|
+
const parsed = (0, import_types24.parseEdgeId)(id);
|
|
5750
6399
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5751
6400
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5752
6401
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5753
6402
|
switch (provenance) {
|
|
5754
|
-
case
|
|
6403
|
+
case import_types24.Provenance.EXTRACTED:
|
|
5755
6404
|
cur.extracted = e;
|
|
5756
6405
|
break;
|
|
5757
|
-
case
|
|
6406
|
+
case import_types24.Provenance.OBSERVED:
|
|
5758
6407
|
cur.observed = e;
|
|
5759
6408
|
break;
|
|
5760
|
-
case
|
|
6409
|
+
case import_types24.Provenance.INFERRED:
|
|
5761
6410
|
cur.inferred = e;
|
|
5762
6411
|
break;
|
|
5763
6412
|
default:
|
|
5764
|
-
if (e.provenance ===
|
|
6413
|
+
if (e.provenance === import_types24.Provenance.STALE) cur.stale = e;
|
|
5765
6414
|
}
|
|
5766
6415
|
buckets.set(key, cur);
|
|
5767
6416
|
});
|
|
@@ -5770,7 +6419,7 @@ function bucketEdges(graph) {
|
|
|
5770
6419
|
function nodeIsFrontier(graph, nodeId) {
|
|
5771
6420
|
if (!graph.hasNode(nodeId)) return false;
|
|
5772
6421
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5773
|
-
return attrs.type ===
|
|
6422
|
+
return attrs.type === import_types24.NodeType.FrontierNode;
|
|
5774
6423
|
}
|
|
5775
6424
|
function clampConfidence(n) {
|
|
5776
6425
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5789,10 +6438,16 @@ function gradedConfidence(edge) {
|
|
|
5789
6438
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5790
6439
|
return clampConfidence(confidenceForEdge(edge));
|
|
5791
6440
|
}
|
|
6441
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6442
|
+
import_types24.EdgeType.CALLS,
|
|
6443
|
+
import_types24.EdgeType.CONNECTS_TO,
|
|
6444
|
+
import_types24.EdgeType.PUBLISHES_TO,
|
|
6445
|
+
import_types24.EdgeType.CONSUMES_FROM
|
|
6446
|
+
]);
|
|
5792
6447
|
function detectMissingDivergences(graph, bucket) {
|
|
5793
6448
|
const out = [];
|
|
5794
|
-
if (bucket.type ===
|
|
5795
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6449
|
+
if (bucket.type === import_types24.EdgeType.CONTAINS) return out;
|
|
6450
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5796
6451
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5797
6452
|
out.push({
|
|
5798
6453
|
type: "missing-observed",
|
|
@@ -5832,7 +6487,7 @@ function declaredHostFor(svc) {
|
|
|
5832
6487
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5833
6488
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5834
6489
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5835
|
-
if (e.type ===
|
|
6490
|
+
if (e.type === import_types24.EdgeType.CONFIGURED_BY && e.provenance === import_types24.Provenance.EXTRACTED) {
|
|
5836
6491
|
return true;
|
|
5837
6492
|
}
|
|
5838
6493
|
}
|
|
@@ -5845,10 +6500,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5845
6500
|
const out = [];
|
|
5846
6501
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5847
6502
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5848
|
-
if (edge.type !==
|
|
5849
|
-
if (edge.provenance !==
|
|
6503
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6504
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5850
6505
|
const target = graph.getNodeAttributes(edge.target);
|
|
5851
|
-
if (target.type !==
|
|
6506
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5852
6507
|
const observedHost = target.host?.trim();
|
|
5853
6508
|
if (!observedHost) continue;
|
|
5854
6509
|
if (observedHost === declaredHost) continue;
|
|
@@ -5870,10 +6525,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5870
6525
|
const deps = svc.dependencies ?? {};
|
|
5871
6526
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5872
6527
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5873
|
-
if (edge.type !==
|
|
5874
|
-
if (edge.provenance !==
|
|
6528
|
+
if (edge.type !== import_types24.EdgeType.CONNECTS_TO) continue;
|
|
6529
|
+
if (edge.provenance !== import_types24.Provenance.OBSERVED) continue;
|
|
5875
6530
|
const target = graph.getNodeAttributes(edge.target);
|
|
5876
|
-
if (target.type !==
|
|
6531
|
+
if (target.type !== import_types24.NodeType.DatabaseNode) continue;
|
|
5877
6532
|
for (const pair of compatPairs()) {
|
|
5878
6533
|
if (pair.engine !== target.engine) continue;
|
|
5879
6534
|
const declared = deps[pair.driver];
|
|
@@ -5926,6 +6581,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5926
6581
|
function involvesNode(d, nodeId) {
|
|
5927
6582
|
return d.source === nodeId || d.target === nodeId;
|
|
5928
6583
|
}
|
|
6584
|
+
function suppressHostMismatchHalves(all) {
|
|
6585
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6586
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6587
|
+
for (const d of all) {
|
|
6588
|
+
if (d.type !== "host-mismatch") continue;
|
|
6589
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6590
|
+
declaredHalf.add((0, import_types24.databaseId)(d.extractedHost));
|
|
6591
|
+
}
|
|
6592
|
+
if (observedHalf.size === 0) return all;
|
|
6593
|
+
return all.filter((d) => {
|
|
6594
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6595
|
+
return false;
|
|
6596
|
+
}
|
|
6597
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6598
|
+
return true;
|
|
6599
|
+
});
|
|
6600
|
+
}
|
|
5929
6601
|
function computeDivergences(graph, opts = {}) {
|
|
5930
6602
|
const all = [];
|
|
5931
6603
|
const buckets = bucketEdges(graph);
|
|
@@ -5934,12 +6606,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5934
6606
|
}
|
|
5935
6607
|
graph.forEachNode((nodeId, attrs) => {
|
|
5936
6608
|
const n = attrs;
|
|
5937
|
-
if (n.type !==
|
|
6609
|
+
if (n.type !== import_types24.NodeType.ServiceNode) return;
|
|
5938
6610
|
const svc = n;
|
|
5939
6611
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5940
6612
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5941
6613
|
});
|
|
5942
|
-
|
|
6614
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6615
|
+
let filtered = reconciled;
|
|
5943
6616
|
if (opts.type) {
|
|
5944
6617
|
const allowed = opts.type;
|
|
5945
6618
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5967,7 +6640,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5967
6640
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5968
6641
|
return a.target.localeCompare(b.target);
|
|
5969
6642
|
});
|
|
5970
|
-
return
|
|
6643
|
+
return import_types24.DivergenceResultSchema.parse({
|
|
5971
6644
|
divergences: filtered,
|
|
5972
6645
|
totalAffected: filtered.length,
|
|
5973
6646
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -6108,7 +6781,7 @@ init_cjs_shims();
|
|
|
6108
6781
|
var import_node_fs23 = require("fs");
|
|
6109
6782
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
6110
6783
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
6111
|
-
var
|
|
6784
|
+
var import_types25 = require("@neat.is/types");
|
|
6112
6785
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6113
6786
|
var LOCK_RETRY_MS = 50;
|
|
6114
6787
|
function neatHome() {
|
|
@@ -6257,10 +6930,10 @@ async function readRegistry() {
|
|
|
6257
6930
|
throw err;
|
|
6258
6931
|
}
|
|
6259
6932
|
const parsed = JSON.parse(raw);
|
|
6260
|
-
return
|
|
6933
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6261
6934
|
}
|
|
6262
6935
|
async function writeRegistry(reg) {
|
|
6263
|
-
const validated =
|
|
6936
|
+
const validated = import_types25.RegistryFileSchema.parse(reg);
|
|
6264
6937
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6265
6938
|
}
|
|
6266
6939
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -6581,6 +7254,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6581
7254
|
}
|
|
6582
7255
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6583
7256
|
});
|
|
7257
|
+
scope.get(
|
|
7258
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7259
|
+
async (req, reply) => {
|
|
7260
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7261
|
+
if (!proj) return;
|
|
7262
|
+
const { nodeId } = req.params;
|
|
7263
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7264
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7265
|
+
}
|
|
7266
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7267
|
+
}
|
|
7268
|
+
);
|
|
6584
7269
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6585
7270
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6586
7271
|
if (!proj) return;
|
|
@@ -6589,11 +7274,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6589
7274
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6590
7275
|
const parsed = [];
|
|
6591
7276
|
for (const c of candidates) {
|
|
6592
|
-
const r =
|
|
7277
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6593
7278
|
if (!r.success) {
|
|
6594
7279
|
return reply.code(400).send({
|
|
6595
7280
|
error: `unknown divergence type "${c}"`,
|
|
6596
|
-
allowed:
|
|
7281
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6597
7282
|
});
|
|
6598
7283
|
}
|
|
6599
7284
|
parsed.push(r.data);
|
|
@@ -6641,23 +7326,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6641
7326
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6642
7327
|
return { count: sliced.length, total, events: sliced };
|
|
6643
7328
|
});
|
|
7329
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
7330
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7331
|
+
if (!proj) return;
|
|
7332
|
+
const { nodeId } = req.params;
|
|
7333
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7334
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7335
|
+
return;
|
|
7336
|
+
}
|
|
7337
|
+
const epath = errorsPathFor(proj);
|
|
7338
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7339
|
+
const events = await readErrorEvents(epath);
|
|
7340
|
+
const filtered = events.filter(
|
|
7341
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7342
|
+
);
|
|
7343
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7344
|
+
};
|
|
6644
7345
|
scope.get(
|
|
6645
7346
|
"/incidents/:nodeId",
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6652
|
-
}
|
|
6653
|
-
const epath = errorsPathFor(proj);
|
|
6654
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6655
|
-
const events = await readErrorEvents(epath);
|
|
6656
|
-
const filtered = events.filter(
|
|
6657
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6658
|
-
);
|
|
6659
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6660
|
-
}
|
|
7347
|
+
incidentHistoryHandler
|
|
7348
|
+
);
|
|
7349
|
+
scope.get(
|
|
7350
|
+
"/graph/incident-history/:nodeId",
|
|
7351
|
+
incidentHistoryHandler
|
|
6661
7352
|
);
|
|
6662
7353
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6663
7354
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6666,16 +7357,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6666
7357
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6667
7358
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6668
7359
|
}
|
|
6669
|
-
let errorEvent;
|
|
6670
7360
|
const epath = errorsPathFor(proj);
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
7361
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7362
|
+
let errorEvent;
|
|
7363
|
+
if (req.query.errorId) {
|
|
7364
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6674
7365
|
if (!errorEvent) {
|
|
6675
7366
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6676
7367
|
}
|
|
6677
7368
|
}
|
|
6678
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7369
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6679
7370
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6680
7371
|
return result;
|
|
6681
7372
|
});
|
|
@@ -6806,7 +7497,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6806
7497
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6807
7498
|
let violations = await log.readAll();
|
|
6808
7499
|
if (req.query.severity) {
|
|
6809
|
-
const sev =
|
|
7500
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
|
|
6810
7501
|
if (!sev.success) {
|
|
6811
7502
|
return reply.code(400).send({
|
|
6812
7503
|
error: "invalid severity",
|
|
@@ -6820,10 +7511,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6820
7511
|
}
|
|
6821
7512
|
return { violations };
|
|
6822
7513
|
});
|
|
7514
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7515
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7516
|
+
if (!proj) return;
|
|
7517
|
+
const nodeId = req.query.node;
|
|
7518
|
+
if (!nodeId) {
|
|
7519
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7520
|
+
}
|
|
7521
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7522
|
+
let policies = [];
|
|
7523
|
+
if (policyPath) {
|
|
7524
|
+
try {
|
|
7525
|
+
policies = await loadPolicyFile(policyPath);
|
|
7526
|
+
} catch (err) {
|
|
7527
|
+
return reply.code(400).send({
|
|
7528
|
+
error: "policy.json failed to parse",
|
|
7529
|
+
details: err.message
|
|
7530
|
+
});
|
|
7531
|
+
}
|
|
7532
|
+
}
|
|
7533
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7534
|
+
return { node: nodeId, applicable };
|
|
7535
|
+
});
|
|
6823
7536
|
scope.post("/policies/check", async (req, reply) => {
|
|
6824
7537
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6825
7538
|
if (!proj) return;
|
|
6826
|
-
const parsed =
|
|
7539
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6827
7540
|
if (!parsed.success) {
|
|
6828
7541
|
return reply.code(400).send({
|
|
6829
7542
|
error: "invalid /policies/check body",
|
|
@@ -7113,6 +7826,7 @@ function unroutedErrorsPath(neatHome2) {
|
|
|
7113
7826
|
}
|
|
7114
7827
|
|
|
7115
7828
|
// src/daemon.ts
|
|
7829
|
+
var import_types27 = require("@neat.is/types");
|
|
7116
7830
|
function daemonJsonPath(scanPath) {
|
|
7117
7831
|
return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
|
|
7118
7832
|
}
|
|
@@ -7215,6 +7929,19 @@ function isTokenContained(needle, haystack) {
|
|
|
7215
7929
|
const tokens = haystack.split(/[-_]/);
|
|
7216
7930
|
return tokens.includes(needle);
|
|
7217
7931
|
}
|
|
7932
|
+
function serviceNameMatchesProject(serviceName, project) {
|
|
7933
|
+
if (serviceName === project) return true;
|
|
7934
|
+
if (isTokenPrefix(project, serviceName)) return true;
|
|
7935
|
+
if (isTokenContained(project, serviceName)) return true;
|
|
7936
|
+
return false;
|
|
7937
|
+
}
|
|
7938
|
+
function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
7939
|
+
if (!serviceName) return true;
|
|
7940
|
+
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
7941
|
+
return graph.someNode(
|
|
7942
|
+
(_id, attrs) => attrs.type === import_types27.NodeType.ServiceNode && attrs.name === serviceName
|
|
7943
|
+
);
|
|
7944
|
+
}
|
|
7218
7945
|
async function bootstrapProject(entry) {
|
|
7219
7946
|
const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
|
|
7220
7947
|
try {
|
|
@@ -7512,7 +8239,9 @@ async function startDaemon(opts = {}) {
|
|
|
7512
8239
|
singleProject: singleProject && singleProjectPath ? { name: singleProject, path: singleProjectPath } : void 0
|
|
7513
8240
|
});
|
|
7514
8241
|
restAddress = await restApp.listen({ port: restPort, host });
|
|
7515
|
-
console.log(
|
|
8242
|
+
console.log(
|
|
8243
|
+
`neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`
|
|
8244
|
+
);
|
|
7516
8245
|
} catch (err) {
|
|
7517
8246
|
for (const slot of slots.values()) {
|
|
7518
8247
|
teardownSlot(slot);
|
|
@@ -7543,6 +8272,10 @@ async function startDaemon(opts = {}) {
|
|
|
7543
8272
|
warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
|
|
7544
8273
|
return null;
|
|
7545
8274
|
}
|
|
8275
|
+
if (!spanBelongsToSingleProject(slot2.graph, singleProject, serviceName)) {
|
|
8276
|
+
await recordUnroutedSpan(serviceName, traceId);
|
|
8277
|
+
return null;
|
|
8278
|
+
}
|
|
7546
8279
|
return slot2;
|
|
7547
8280
|
}
|
|
7548
8281
|
const liveEntries = await listProjects().catch(() => []);
|
|
@@ -7605,7 +8338,7 @@ async function startDaemon(opts = {}) {
|
|
|
7605
8338
|
onErrorSpanSync: async (span) => {
|
|
7606
8339
|
const slot = await resolveTargetSlot(span.service, span.traceId);
|
|
7607
8340
|
if (!slot) return;
|
|
7608
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8341
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7609
8342
|
},
|
|
7610
8343
|
// Project-scoped route (issue #367) — the URL already named the
|
|
7611
8344
|
// project. Resolution is a direct slot lookup; service.name resolves
|
|
@@ -7628,10 +8361,10 @@ async function startDaemon(opts = {}) {
|
|
|
7628
8361
|
onProjectErrorSpanSync: async (project, span) => {
|
|
7629
8362
|
const slot = await resolveSlotByName(project, span.service, span.traceId);
|
|
7630
8363
|
if (!slot) return;
|
|
7631
|
-
await makeErrorSpanWriter(slot.paths.errorsPath)(span);
|
|
8364
|
+
await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span);
|
|
7632
8365
|
}
|
|
7633
8366
|
});
|
|
7634
|
-
otlpAddress = await otlpApp
|
|
8367
|
+
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
7635
8368
|
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
7636
8369
|
} catch (err) {
|
|
7637
8370
|
for (const slot of slots.values()) {
|
|
@@ -7786,7 +8519,8 @@ async function startDaemon(opts = {}) {
|
|
|
7786
8519
|
otlpAddress,
|
|
7787
8520
|
bootstrap: tracker,
|
|
7788
8521
|
initialBootstrap,
|
|
7789
|
-
daemonRecord
|
|
8522
|
+
daemonRecord,
|
|
8523
|
+
neatHome: home
|
|
7790
8524
|
};
|
|
7791
8525
|
}
|
|
7792
8526
|
// Annotate the CommonJS export names for ESM import in node:
|