@neat.is/core 0.4.20-dev.20260630 → 0.4.23
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-62ELQVIP.js → chunk-IABNGQT2.js} +57 -9
- package/dist/chunk-IABNGQT2.js.map +1 -0
- package/dist/{chunk-GNAX2CBF.js → chunk-O25KZNZK.js} +878 -160
- package/dist/chunk-O25KZNZK.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 +1251 -355
- 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 +903 -151
- 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 +947 -151
- 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 +835 -122
- 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/server.cjs
CHANGED
|
@@ -40,13 +40,13 @@ var init_cjs_shims = __esm({
|
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
// src/auth.ts
|
|
43
|
-
function
|
|
43
|
+
function isLoopbackHost2(host) {
|
|
44
44
|
if (!host) return false;
|
|
45
45
|
return LOOPBACK_HOSTS.has(host);
|
|
46
46
|
}
|
|
47
47
|
function assertBindAuthority(host, token) {
|
|
48
48
|
if (token && token.length > 0) return;
|
|
49
|
-
if (
|
|
49
|
+
if (isLoopbackHost2(host)) return;
|
|
50
50
|
throw new BindAuthorityError(host);
|
|
51
51
|
}
|
|
52
52
|
function mountBearerAuth(app, opts) {
|
|
@@ -617,7 +617,7 @@ function getGraph(project = DEFAULT_PROJECT) {
|
|
|
617
617
|
init_cjs_shims();
|
|
618
618
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
619
619
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
620
|
-
var
|
|
620
|
+
var import_types26 = require("@neat.is/types");
|
|
621
621
|
|
|
622
622
|
// src/extend/index.ts
|
|
623
623
|
init_cjs_shims();
|
|
@@ -1458,22 +1458,164 @@ var rootCauseShapes = {
|
|
|
1458
1458
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1459
1459
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1460
1460
|
};
|
|
1461
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1461
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1462
1462
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1463
1463
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1464
1464
|
const shape = rootCauseShapes[origin.type];
|
|
1465
|
-
if (
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1465
|
+
if (shape) {
|
|
1466
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1467
|
+
const match = shape(graph, origin, walk);
|
|
1468
|
+
if (match) {
|
|
1469
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1470
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1471
|
+
rootCauseNode: match.rootCauseNode,
|
|
1472
|
+
rootCauseReason: reason,
|
|
1473
|
+
traversalPath: walk.path,
|
|
1474
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1475
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1476
|
+
fixRecommendation: match.fixRecommendation
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1481
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1482
|
+
if (crossService) return crossService;
|
|
1483
|
+
}
|
|
1484
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1485
|
+
}
|
|
1486
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1487
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1488
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1489
|
+
}
|
|
1490
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1491
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1492
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1493
|
+
if (relevant.length === 0) return null;
|
|
1494
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1495
|
+
const attrs = latest.attributes ?? {};
|
|
1496
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1497
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1498
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1499
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1500
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1501
|
+
const count = sameMode.length;
|
|
1502
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1503
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1504
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1505
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1506
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1507
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1508
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1509
|
+
return {
|
|
1510
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1511
|
+
rootCauseReason,
|
|
1512
|
+
...fileNode ? { fileNode } : {},
|
|
1513
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1517
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1518
|
+
if (!loc) return null;
|
|
1519
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1520
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1521
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1522
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1523
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1524
|
+
traversalPath,
|
|
1525
|
+
edgeProvenances,
|
|
1526
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1527
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
function isFailingCallEdge(e) {
|
|
1531
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1532
|
+
}
|
|
1533
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1534
|
+
const ids = [serviceId3];
|
|
1535
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1536
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1537
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1538
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1539
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1540
|
+
}
|
|
1541
|
+
return ids;
|
|
1542
|
+
}
|
|
1543
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1544
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1545
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1546
|
+
if (ec !== cc) return ec > cc;
|
|
1547
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1548
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1549
|
+
}
|
|
1550
|
+
return id < curId;
|
|
1551
|
+
}
|
|
1552
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1553
|
+
let best = null;
|
|
1554
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1555
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1556
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1557
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1558
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1559
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1560
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1561
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1562
|
+
best = { nextService: owner.id, edge: e };
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return best;
|
|
1567
|
+
}
|
|
1568
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1569
|
+
const path43 = [originServiceId];
|
|
1570
|
+
const edges = [];
|
|
1571
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1572
|
+
let current = originServiceId;
|
|
1573
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1574
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1575
|
+
if (!hop) break;
|
|
1576
|
+
path43.push(hop.nextService);
|
|
1577
|
+
edges.push(hop.edge);
|
|
1578
|
+
visited.add(hop.nextService);
|
|
1579
|
+
current = hop.nextService;
|
|
1580
|
+
}
|
|
1581
|
+
if (edges.length === 0) return null;
|
|
1582
|
+
return { path: path43, edges, culprit: current };
|
|
1583
|
+
}
|
|
1584
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1585
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1586
|
+
if (!chain) return null;
|
|
1587
|
+
const culprit = chain.culprit;
|
|
1588
|
+
const path43 = [...chain.path];
|
|
1589
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1590
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1591
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1592
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1593
|
+
if (loc) {
|
|
1594
|
+
let rootCauseNode = culprit;
|
|
1595
|
+
if (loc.fileNode) {
|
|
1596
|
+
path43.push(loc.fileNode);
|
|
1597
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1598
|
+
rootCauseNode = loc.fileNode;
|
|
1599
|
+
}
|
|
1600
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1601
|
+
rootCauseNode,
|
|
1602
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1603
|
+
traversalPath: path43,
|
|
1604
|
+
edgeProvenances,
|
|
1605
|
+
confidence,
|
|
1606
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1610
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1611
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1470
1612
|
return import_types.RootCauseResultSchema.parse({
|
|
1471
|
-
rootCauseNode:
|
|
1472
|
-
rootCauseReason:
|
|
1473
|
-
traversalPath:
|
|
1474
|
-
edgeProvenances
|
|
1475
|
-
confidence
|
|
1476
|
-
fixRecommendation:
|
|
1613
|
+
rootCauseNode: culprit,
|
|
1614
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1615
|
+
traversalPath: path43,
|
|
1616
|
+
edgeProvenances,
|
|
1617
|
+
confidence,
|
|
1618
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1477
1619
|
});
|
|
1478
1620
|
}
|
|
1479
1621
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1496,14 +1638,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1496
1638
|
});
|
|
1497
1639
|
}
|
|
1498
1640
|
if (frame.distance >= maxDepth) continue;
|
|
1499
|
-
const
|
|
1500
|
-
for (const [
|
|
1501
|
-
if (enqueued.has(
|
|
1502
|
-
enqueued.add(
|
|
1641
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1642
|
+
for (const [srcId, edge] of incoming) {
|
|
1643
|
+
if (enqueued.has(srcId)) continue;
|
|
1644
|
+
enqueued.add(srcId);
|
|
1503
1645
|
queue.push({
|
|
1504
|
-
nodeId:
|
|
1646
|
+
nodeId: srcId,
|
|
1505
1647
|
distance: frame.distance + 1,
|
|
1506
|
-
path: [...frame.path,
|
|
1648
|
+
path: [...frame.path, srcId],
|
|
1507
1649
|
pathEdges: [...frame.pathEdges, edge]
|
|
1508
1650
|
});
|
|
1509
1651
|
}
|
|
@@ -1559,6 +1701,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1559
1701
|
total: dependencies.length
|
|
1560
1702
|
});
|
|
1561
1703
|
}
|
|
1704
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1705
|
+
if (!graph.hasNode(nodeId)) {
|
|
1706
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1707
|
+
origin: nodeId,
|
|
1708
|
+
dependencies: [],
|
|
1709
|
+
observed: false,
|
|
1710
|
+
inboundObservedCount: 0,
|
|
1711
|
+
hasExtractedOutbound: false
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1715
|
+
const scope = [nodeId];
|
|
1716
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1717
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1718
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1719
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1720
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1721
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
const dependencies = [];
|
|
1725
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1726
|
+
let hasExtractedOutbound = false;
|
|
1727
|
+
for (const src of scope) {
|
|
1728
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1729
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1730
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1731
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1732
|
+
if (!seenEdge.has(e.id)) {
|
|
1733
|
+
seenEdge.add(e.id);
|
|
1734
|
+
dependencies.push(e);
|
|
1735
|
+
}
|
|
1736
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1737
|
+
hasExtractedOutbound = true;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
let inboundObservedCount = 0;
|
|
1742
|
+
for (const tgt of scope) {
|
|
1743
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1744
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1745
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1746
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
dependencies.sort(
|
|
1750
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1751
|
+
);
|
|
1752
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1753
|
+
origin: nodeId,
|
|
1754
|
+
dependencies,
|
|
1755
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1756
|
+
inboundObservedCount,
|
|
1757
|
+
hasExtractedOutbound
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1562
1760
|
|
|
1563
1761
|
// src/divergences.ts
|
|
1564
1762
|
function bucketKey(source, target, type) {
|
|
@@ -1611,10 +1809,16 @@ function gradedConfidence(edge) {
|
|
|
1611
1809
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
1612
1810
|
return clampConfidence(confidenceForEdge(edge));
|
|
1613
1811
|
}
|
|
1812
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1813
|
+
import_types2.EdgeType.CALLS,
|
|
1814
|
+
import_types2.EdgeType.CONNECTS_TO,
|
|
1815
|
+
import_types2.EdgeType.PUBLISHES_TO,
|
|
1816
|
+
import_types2.EdgeType.CONSUMES_FROM
|
|
1817
|
+
]);
|
|
1614
1818
|
function detectMissingDivergences(graph, bucket) {
|
|
1615
1819
|
const out = [];
|
|
1616
1820
|
if (bucket.type === import_types2.EdgeType.CONTAINS) return out;
|
|
1617
|
-
if (bucket.extracted && !bucket.observed) {
|
|
1821
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
1618
1822
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
1619
1823
|
out.push({
|
|
1620
1824
|
type: "missing-observed",
|
|
@@ -1748,6 +1952,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
1748
1952
|
function involvesNode(d, nodeId) {
|
|
1749
1953
|
return d.source === nodeId || d.target === nodeId;
|
|
1750
1954
|
}
|
|
1955
|
+
function suppressHostMismatchHalves(all) {
|
|
1956
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
1957
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
1958
|
+
for (const d of all) {
|
|
1959
|
+
if (d.type !== "host-mismatch") continue;
|
|
1960
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
1961
|
+
declaredHalf.add((0, import_types2.databaseId)(d.extractedHost));
|
|
1962
|
+
}
|
|
1963
|
+
if (observedHalf.size === 0) return all;
|
|
1964
|
+
return all.filter((d) => {
|
|
1965
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
1969
|
+
return true;
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1751
1972
|
function computeDivergences(graph, opts = {}) {
|
|
1752
1973
|
const all = [];
|
|
1753
1974
|
const buckets = bucketEdges(graph);
|
|
@@ -1761,7 +1982,8 @@ function computeDivergences(graph, opts = {}) {
|
|
|
1761
1982
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
1762
1983
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
1763
1984
|
});
|
|
1764
|
-
|
|
1985
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
1986
|
+
let filtered = reconciled;
|
|
1765
1987
|
if (opts.type) {
|
|
1766
1988
|
const allowed = opts.type;
|
|
1767
1989
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -2082,6 +2304,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
2082
2304
|
}
|
|
2083
2305
|
return out;
|
|
2084
2306
|
}
|
|
2307
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
2308
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
2309
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
2310
|
+
const out = [];
|
|
2311
|
+
for (const policy of policies) {
|
|
2312
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
2313
|
+
if (!m) continue;
|
|
2314
|
+
out.push({
|
|
2315
|
+
policyId: policy.id,
|
|
2316
|
+
policyName: policy.name,
|
|
2317
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
2318
|
+
severity: policy.severity,
|
|
2319
|
+
onViolation: resolveOnViolation(policy),
|
|
2320
|
+
ruleType: policy.rule.type,
|
|
2321
|
+
match: m.match,
|
|
2322
|
+
reason: m.reason
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
return out;
|
|
2326
|
+
}
|
|
2327
|
+
function requiredProvenanceList(required) {
|
|
2328
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
2329
|
+
return String(required);
|
|
2330
|
+
}
|
|
2331
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
2332
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
2333
|
+
for (const edgeId of incident) {
|
|
2334
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2335
|
+
if (e.type !== edgeType) continue;
|
|
2336
|
+
if (requiredOtherEnd === void 0) return true;
|
|
2337
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
2338
|
+
}
|
|
2339
|
+
return false;
|
|
2340
|
+
}
|
|
2341
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
2342
|
+
let found = null;
|
|
2343
|
+
graph.forEachNode((subjId, attrs) => {
|
|
2344
|
+
if (found !== null) return;
|
|
2345
|
+
if (subjId === nodeId) return;
|
|
2346
|
+
if (attrs.type !== rule.nodeType) return;
|
|
2347
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
2348
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
2349
|
+
});
|
|
2350
|
+
return found;
|
|
2351
|
+
}
|
|
2352
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
2353
|
+
const rule = policy.rule;
|
|
2354
|
+
switch (rule.type) {
|
|
2355
|
+
case "structural": {
|
|
2356
|
+
if (node.type === rule.fromNodeType) {
|
|
2357
|
+
return {
|
|
2358
|
+
match: "subject",
|
|
2359
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
if (node.type === rule.toNodeType) {
|
|
2363
|
+
return {
|
|
2364
|
+
match: "region",
|
|
2365
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
case "ownership": {
|
|
2371
|
+
if (node.type === rule.nodeType) {
|
|
2372
|
+
return {
|
|
2373
|
+
match: "subject",
|
|
2374
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
return null;
|
|
2378
|
+
}
|
|
2379
|
+
case "blast-radius": {
|
|
2380
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
2381
|
+
if (node.type === rule.nodeType) {
|
|
2382
|
+
return {
|
|
2383
|
+
match: "subject",
|
|
2384
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
2385
|
+
};
|
|
2386
|
+
}
|
|
2387
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
2388
|
+
if (subject) {
|
|
2389
|
+
return {
|
|
2390
|
+
match: "region",
|
|
2391
|
+
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`
|
|
2392
|
+
};
|
|
2393
|
+
}
|
|
2394
|
+
return null;
|
|
2395
|
+
}
|
|
2396
|
+
case "compatibility": {
|
|
2397
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
2398
|
+
if (node.type === import_types3.NodeType.ServiceNode) {
|
|
2399
|
+
return {
|
|
2400
|
+
match: "subject",
|
|
2401
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
2402
|
+
};
|
|
2403
|
+
}
|
|
2404
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
2405
|
+
if (reachesDriverEngine && node.type === import_types3.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types3.EdgeType.CONNECTS_TO)) {
|
|
2406
|
+
return {
|
|
2407
|
+
match: "region",
|
|
2408
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
return null;
|
|
2412
|
+
}
|
|
2413
|
+
case "provenance": {
|
|
2414
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
2415
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
2416
|
+
return {
|
|
2417
|
+
match: "subject",
|
|
2418
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
2419
|
+
};
|
|
2420
|
+
}
|
|
2421
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
2422
|
+
return {
|
|
2423
|
+
match: "region",
|
|
2424
|
+
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`
|
|
2425
|
+
};
|
|
2426
|
+
}
|
|
2427
|
+
return null;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2085
2431
|
async function loadPolicyFile(policyPath) {
|
|
2086
2432
|
let raw;
|
|
2087
2433
|
try {
|
|
@@ -2200,9 +2546,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
2200
2546
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
2201
2547
|
}
|
|
2202
2548
|
}
|
|
2203
|
-
function
|
|
2549
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
2204
2550
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
2205
|
-
const v =
|
|
2551
|
+
const v = attrs[key];
|
|
2206
2552
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2207
2553
|
if (typeof v === "string") {
|
|
2208
2554
|
const n = Number(v);
|
|
@@ -2211,6 +2557,63 @@ function httpResponseStatus(span) {
|
|
|
2211
2557
|
}
|
|
2212
2558
|
return void 0;
|
|
2213
2559
|
}
|
|
2560
|
+
function httpResponseStatus(span) {
|
|
2561
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2562
|
+
}
|
|
2563
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2564
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2565
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2566
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2567
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2568
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2569
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2570
|
+
if (where) return `error on ${where}`;
|
|
2571
|
+
return void 0;
|
|
2572
|
+
}
|
|
2573
|
+
var GRPC_STATUS_NAMES = {
|
|
2574
|
+
1: "CANCELLED",
|
|
2575
|
+
2: "UNKNOWN",
|
|
2576
|
+
3: "INVALID_ARGUMENT",
|
|
2577
|
+
4: "DEADLINE_EXCEEDED",
|
|
2578
|
+
5: "NOT_FOUND",
|
|
2579
|
+
6: "ALREADY_EXISTS",
|
|
2580
|
+
7: "PERMISSION_DENIED",
|
|
2581
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2582
|
+
9: "FAILED_PRECONDITION",
|
|
2583
|
+
10: "ABORTED",
|
|
2584
|
+
11: "OUT_OF_RANGE",
|
|
2585
|
+
12: "UNIMPLEMENTED",
|
|
2586
|
+
13: "INTERNAL",
|
|
2587
|
+
14: "UNAVAILABLE",
|
|
2588
|
+
15: "DATA_LOSS",
|
|
2589
|
+
16: "UNAUTHENTICATED"
|
|
2590
|
+
};
|
|
2591
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2592
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2593
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2594
|
+
if (typeof v === "string") {
|
|
2595
|
+
const n = Number(v);
|
|
2596
|
+
if (Number.isFinite(n)) return n;
|
|
2597
|
+
}
|
|
2598
|
+
return void 0;
|
|
2599
|
+
}
|
|
2600
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2601
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2602
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2603
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2604
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2605
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2606
|
+
}
|
|
2607
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2608
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2609
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2610
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2611
|
+
}
|
|
2612
|
+
return void 0;
|
|
2613
|
+
}
|
|
2614
|
+
function incidentMessage(span) {
|
|
2615
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2616
|
+
}
|
|
2214
2617
|
function nowIso(ctx) {
|
|
2215
2618
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
2216
2619
|
}
|
|
@@ -2230,13 +2633,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
2230
2633
|
`[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.`
|
|
2231
2634
|
);
|
|
2232
2635
|
}
|
|
2233
|
-
function
|
|
2636
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
2234
2637
|
for (const k of keys) {
|
|
2235
|
-
const v =
|
|
2638
|
+
const v = attrs[k];
|
|
2236
2639
|
if (typeof v === "string" && v.length > 0) return v;
|
|
2237
2640
|
}
|
|
2238
2641
|
return void 0;
|
|
2239
2642
|
}
|
|
2643
|
+
function pickAttr(span, ...keys) {
|
|
2644
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2645
|
+
}
|
|
2240
2646
|
function hostFromUrl(u) {
|
|
2241
2647
|
if (!u) return void 0;
|
|
2242
2648
|
try {
|
|
@@ -2248,6 +2654,10 @@ function hostFromUrl(u) {
|
|
|
2248
2654
|
function pickAddress(span) {
|
|
2249
2655
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
2250
2656
|
}
|
|
2657
|
+
function isLoopbackHost(host) {
|
|
2658
|
+
const h = host.toLowerCase();
|
|
2659
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2660
|
+
}
|
|
2251
2661
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
2252
2662
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
2253
2663
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -2355,15 +2765,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
2355
2765
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
2356
2766
|
};
|
|
2357
2767
|
}
|
|
2768
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2769
|
+
if (graph.hasNode((0, import_types4.fileId)(serviceName, relPath))) return relPath;
|
|
2770
|
+
let best = null;
|
|
2771
|
+
graph.forEachNode((_id, attrs) => {
|
|
2772
|
+
const a = attrs;
|
|
2773
|
+
if (a.type !== import_types4.NodeType.FileNode || a.service !== serviceName) return;
|
|
2774
|
+
if (a.discoveredVia === "otel") return;
|
|
2775
|
+
const p = a.path;
|
|
2776
|
+
if (!p) return;
|
|
2777
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2778
|
+
best = p;
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
return best ?? relPath;
|
|
2782
|
+
}
|
|
2358
2783
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
2359
|
-
const
|
|
2784
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2785
|
+
const fileNodeId = (0, import_types4.fileId)(serviceName, relPath);
|
|
2360
2786
|
if (!graph.hasNode(fileNodeId)) {
|
|
2361
|
-
const language = languageForExt(
|
|
2787
|
+
const language = languageForExt(relPath);
|
|
2362
2788
|
const node = {
|
|
2363
2789
|
id: fileNodeId,
|
|
2364
2790
|
type: import_types4.NodeType.FileNode,
|
|
2365
2791
|
service: serviceName,
|
|
2366
|
-
path:
|
|
2792
|
+
path: relPath,
|
|
2367
2793
|
...language ? { language } : {},
|
|
2368
2794
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
2369
2795
|
discoveredVia: "otel"
|
|
@@ -2391,6 +2817,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
2391
2817
|
}
|
|
2392
2818
|
var INFERRED_CONFIDENCE = 0.6;
|
|
2393
2819
|
var STITCH_MAX_DEPTH = 2;
|
|
2820
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2821
|
+
import_types4.EdgeType.CALLS,
|
|
2822
|
+
import_types4.EdgeType.CONNECTS_TO,
|
|
2823
|
+
import_types4.EdgeType.DEPENDS_ON
|
|
2824
|
+
]);
|
|
2394
2825
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
2395
2826
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
2396
2827
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2564,6 +2995,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2564
2995
|
for (const edgeId of outbound) {
|
|
2565
2996
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2566
2997
|
if (edge.provenance !== import_types4.Provenance.EXTRACTED) continue;
|
|
2998
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2567
2999
|
if (graph.hasEdge((0, import_types4.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2568
3000
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2569
3001
|
if (!visited.has(edge.target)) {
|
|
@@ -2596,6 +3028,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2596
3028
|
await import_node_fs5.promises.mkdir(import_node_path5.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2597
3029
|
await import_node_fs5.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2598
3030
|
}
|
|
3031
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
3032
|
+
const sid = (0, import_types4.serviceId)(span.service, span.env);
|
|
3033
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
3034
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
3035
|
+
if (callSite) {
|
|
3036
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
3037
|
+
return (0, import_types4.fileId)(span.service, relPath);
|
|
3038
|
+
}
|
|
3039
|
+
return sid;
|
|
3040
|
+
}
|
|
2599
3041
|
function sanitizeAttributes(attrs) {
|
|
2600
3042
|
const out = {};
|
|
2601
3043
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2626,6 +3068,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
2626
3068
|
};
|
|
2627
3069
|
await appendErrorEvent(ctx, ev);
|
|
2628
3070
|
}
|
|
3071
|
+
async function recordExceptionIncident(ctx, span, ts) {
|
|
3072
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
3073
|
+
const ev = {
|
|
3074
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
3075
|
+
timestamp: ts,
|
|
3076
|
+
service: span.service,
|
|
3077
|
+
traceId: span.traceId,
|
|
3078
|
+
spanId: span.spanId,
|
|
3079
|
+
errorMessage: incidentMessage(span),
|
|
3080
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
3081
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
3082
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
3083
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
3084
|
+
};
|
|
3085
|
+
await appendErrorEvent(ctx, ev);
|
|
3086
|
+
}
|
|
2629
3087
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
|
|
2630
3088
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
2631
3089
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -2682,7 +3140,10 @@ async function handleSpan(ctx, span) {
|
|
|
2682
3140
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2683
3141
|
cacheSpanService(span, nowMs, callSite);
|
|
2684
3142
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2685
|
-
const callSiteEvidence = callSite ? {
|
|
3143
|
+
const callSiteEvidence = callSite ? {
|
|
3144
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
3145
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
3146
|
+
} : void 0;
|
|
2686
3147
|
let affectedNode = sourceId;
|
|
2687
3148
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2688
3149
|
if (span.dbSystem) {
|
|
@@ -2704,7 +3165,7 @@ async function handleSpan(ctx, span) {
|
|
|
2704
3165
|
} else {
|
|
2705
3166
|
const host = pickAddress(span);
|
|
2706
3167
|
let resolvedViaAddress = false;
|
|
2707
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
3168
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2708
3169
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2709
3170
|
if (targetId && targetId !== sourceId) {
|
|
2710
3171
|
upsertObservedEdge(
|
|
@@ -2739,7 +3200,11 @@ async function handleSpan(ctx, span) {
|
|
|
2739
3200
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2740
3201
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2741
3202
|
const fallbackEvidence = parent.callSite ? {
|
|
2742
|
-
file:
|
|
3203
|
+
file: reconcileObservedRelPath(
|
|
3204
|
+
ctx.graph,
|
|
3205
|
+
parent.service,
|
|
3206
|
+
parent.callSite.relPath
|
|
3207
|
+
),
|
|
2743
3208
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2744
3209
|
} : void 0;
|
|
2745
3210
|
upsertObservedEdge(
|
|
@@ -2764,7 +3229,7 @@ async function handleSpan(ctx, span) {
|
|
|
2764
3229
|
service: span.service,
|
|
2765
3230
|
traceId: span.traceId,
|
|
2766
3231
|
spanId: span.spanId,
|
|
2767
|
-
errorMessage: span
|
|
3232
|
+
errorMessage: incidentMessage(span),
|
|
2768
3233
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2769
3234
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2770
3235
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2775,7 +3240,9 @@ async function handleSpan(ctx, span) {
|
|
|
2775
3240
|
}
|
|
2776
3241
|
if (span.statusCode !== 2) {
|
|
2777
3242
|
const status2 = httpResponseStatus(span);
|
|
2778
|
-
if (
|
|
3243
|
+
if (span.exception) {
|
|
3244
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
3245
|
+
} else if (status2 !== void 0 && status2 >= 500) {
|
|
2779
3246
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
|
|
2780
3247
|
} else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
2781
3248
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
|
|
@@ -2940,12 +3407,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2940
3407
|
async function readErrorEvents(errorsPath) {
|
|
2941
3408
|
try {
|
|
2942
3409
|
const raw = await import_node_fs5.promises.readFile(errorsPath, "utf8");
|
|
2943
|
-
|
|
3410
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
3411
|
+
return dedupeIncidents(events);
|
|
2944
3412
|
} catch (err) {
|
|
2945
3413
|
if (err.code === "ENOENT") return [];
|
|
2946
3414
|
throw err;
|
|
2947
3415
|
}
|
|
2948
3416
|
}
|
|
3417
|
+
function isSynthesizedHttpIncident(ev) {
|
|
3418
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
3419
|
+
if (ev.errorType) return false;
|
|
3420
|
+
if (!ev.attributes) return false;
|
|
3421
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
3422
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
3423
|
+
}
|
|
3424
|
+
function dedupeIncidents(events) {
|
|
3425
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3426
|
+
const once = [];
|
|
3427
|
+
for (const ev of events) {
|
|
3428
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
3429
|
+
if (key === void 0) {
|
|
3430
|
+
once.push(ev);
|
|
3431
|
+
continue;
|
|
3432
|
+
}
|
|
3433
|
+
if (seen.has(key)) continue;
|
|
3434
|
+
seen.add(key);
|
|
3435
|
+
once.push(ev);
|
|
3436
|
+
}
|
|
3437
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
3438
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
3439
|
+
for (const ev of once) {
|
|
3440
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
3441
|
+
}
|
|
3442
|
+
return once.filter((ev) => {
|
|
3443
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
3444
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
3445
|
+
});
|
|
3446
|
+
}
|
|
2949
3447
|
function mergeSnapshot(graph, snapshot) {
|
|
2950
3448
|
const exported = snapshot.graph;
|
|
2951
3449
|
let nodesAdded = 0;
|
|
@@ -3026,10 +3524,17 @@ function isConfigFile(name) {
|
|
|
3026
3524
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
3027
3525
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
3028
3526
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3527
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
3029
3528
|
return { match: true, fileType: "env" };
|
|
3030
3529
|
}
|
|
3031
3530
|
return { match: false, fileType: "" };
|
|
3032
3531
|
}
|
|
3532
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3533
|
+
return name === ".env.neat";
|
|
3534
|
+
}
|
|
3535
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3536
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3537
|
+
}
|
|
3033
3538
|
function isTestPath(filePath) {
|
|
3034
3539
|
const normalised = filePath.replace(/\\/g, "/");
|
|
3035
3540
|
const segments = normalised.split("/");
|
|
@@ -3723,7 +4228,9 @@ async function walkSourceFiles(dir) {
|
|
|
3723
4228
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3724
4229
|
if (await isPythonVenvDir(full)) continue;
|
|
3725
4230
|
await walk(full);
|
|
3726
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))
|
|
4231
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
4232
|
+
// would attribute our instrumentation imports to the user's service.
|
|
4233
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
3727
4234
|
out.push(full);
|
|
3728
4235
|
}
|
|
3729
4236
|
}
|
|
@@ -3893,11 +4400,28 @@ function collectJsImports(node, out) {
|
|
|
3893
4400
|
if (child) collectJsImports(child, out);
|
|
3894
4401
|
}
|
|
3895
4402
|
}
|
|
4403
|
+
function collectImportedNames(node, out) {
|
|
4404
|
+
if (node.type === "aliased_import") {
|
|
4405
|
+
const nameNode = node.childForFieldName("name");
|
|
4406
|
+
if (nameNode) out.push(nameNode.text);
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
if (node.type === "dotted_name") {
|
|
4410
|
+
out.push(node.text);
|
|
4411
|
+
return;
|
|
4412
|
+
}
|
|
4413
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4414
|
+
const child = node.namedChild(i);
|
|
4415
|
+
if (child) collectImportedNames(child, out);
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
3896
4418
|
function collectPyImports(node, out) {
|
|
3897
4419
|
if (node.type === "import_from_statement") {
|
|
3898
4420
|
let level = 0;
|
|
3899
4421
|
let modulePath = "";
|
|
4422
|
+
const names = [];
|
|
3900
4423
|
let pastFrom = false;
|
|
4424
|
+
let pastImport = false;
|
|
3901
4425
|
for (let i = 0; i < node.childCount; i++) {
|
|
3902
4426
|
const child = node.child(i);
|
|
3903
4427
|
if (!child) continue;
|
|
@@ -3905,26 +4429,30 @@ function collectPyImports(node, out) {
|
|
|
3905
4429
|
if (child.type === "from") pastFrom = true;
|
|
3906
4430
|
continue;
|
|
3907
4431
|
}
|
|
3908
|
-
if (
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
if (!rc) continue;
|
|
3913
|
-
if (rc.type === "import_prefix") {
|
|
3914
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3915
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3916
|
-
}
|
|
3917
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4432
|
+
if (!pastImport) {
|
|
4433
|
+
if (child.type === "import") {
|
|
4434
|
+
pastImport = true;
|
|
4435
|
+
continue;
|
|
3918
4436
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4437
|
+
if (child.type === "relative_import") {
|
|
4438
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4439
|
+
const rc = child.child(j);
|
|
4440
|
+
if (!rc) continue;
|
|
4441
|
+
if (rc.type === "import_prefix") {
|
|
4442
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
4443
|
+
if (rc.child(k)?.type === ".") level++;
|
|
4444
|
+
}
|
|
4445
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4446
|
+
}
|
|
4447
|
+
} else if (child.type === "dotted_name") {
|
|
4448
|
+
modulePath = child.text;
|
|
4449
|
+
}
|
|
4450
|
+
continue;
|
|
3924
4451
|
}
|
|
4452
|
+
collectImportedNames(child, names);
|
|
3925
4453
|
}
|
|
3926
4454
|
if (level > 0 || modulePath) {
|
|
3927
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4455
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3928
4456
|
}
|
|
3929
4457
|
}
|
|
3930
4458
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -4026,8 +4554,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
4026
4554
|
return null;
|
|
4027
4555
|
}
|
|
4028
4556
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
4029
|
-
if (!imp.modulePath) return null;
|
|
4030
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
4031
4557
|
let baseDir;
|
|
4032
4558
|
if (imp.level > 0) {
|
|
4033
4559
|
baseDir = import_node_path14.default.dirname(importerPath);
|
|
@@ -4035,13 +4561,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
4035
4561
|
} else {
|
|
4036
4562
|
baseDir = serviceDir;
|
|
4037
4563
|
}
|
|
4038
|
-
const
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4564
|
+
const moduleBase = imp.modulePath ? import_node_path14.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4565
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4566
|
+
let needModuleFile = imp.names.length === 0;
|
|
4567
|
+
for (const name of imp.names) {
|
|
4568
|
+
const submoduleFile = import_node_path14.default.join(moduleBase, `${name}.py`);
|
|
4569
|
+
const subpackageInit = import_node_path14.default.join(moduleBase, name, "__init__.py");
|
|
4570
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists2(submoduleFile)) {
|
|
4571
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, submoduleFile)));
|
|
4572
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists2(subpackageInit)) {
|
|
4573
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, subpackageInit)));
|
|
4574
|
+
} else {
|
|
4575
|
+
needModuleFile = true;
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
if (needModuleFile) {
|
|
4579
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path14.default.join(moduleBase, "__init__.py")] : [import_node_path14.default.join(moduleBase, "__init__.py")];
|
|
4580
|
+
for (const candidate of moduleFileCandidates) {
|
|
4581
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
|
|
4582
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, candidate)));
|
|
4583
|
+
break;
|
|
4584
|
+
}
|
|
4042
4585
|
}
|
|
4043
4586
|
}
|
|
4044
|
-
return
|
|
4587
|
+
return [...resolved];
|
|
4045
4588
|
}
|
|
4046
4589
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
4047
4590
|
const importeeFileId = (0, import_types9.fileId)(serviceName, importeeRelPath);
|
|
@@ -4082,17 +4625,18 @@ async function addImports(graph, services) {
|
|
|
4082
4625
|
continue;
|
|
4083
4626
|
}
|
|
4084
4627
|
for (const imp of pyImports) {
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4628
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4629
|
+
for (const resolved of resolvedPaths) {
|
|
4630
|
+
edgesAdded += emitImportEdge(
|
|
4631
|
+
graph,
|
|
4632
|
+
service.pkg.name,
|
|
4633
|
+
importerFileId,
|
|
4634
|
+
relFile,
|
|
4635
|
+
resolved,
|
|
4636
|
+
imp.line,
|
|
4637
|
+
imp.snippet
|
|
4638
|
+
);
|
|
4639
|
+
}
|
|
4096
4640
|
}
|
|
4097
4641
|
continue;
|
|
4098
4642
|
}
|
|
@@ -4728,6 +5272,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4728
5272
|
edgesAdded++;
|
|
4729
5273
|
}
|
|
4730
5274
|
}
|
|
5275
|
+
if (allConfigs.length === 1) {
|
|
5276
|
+
const primary = allConfigs[0];
|
|
5277
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
5278
|
+
const relPath = import_node_path22.default.relative(scanPath, primary.sourceFile);
|
|
5279
|
+
const cfgId = (0, import_types10.configId)(relPath);
|
|
5280
|
+
if (!graph.hasNode(cfgId)) {
|
|
5281
|
+
const cfgNode = {
|
|
5282
|
+
id: cfgId,
|
|
5283
|
+
type: import_types10.NodeType.ConfigNode,
|
|
5284
|
+
name: import_node_path22.default.basename(primary.sourceFile),
|
|
5285
|
+
path: relPath,
|
|
5286
|
+
fileType: isConfigFile(import_node_path22.default.basename(primary.sourceFile)).fileType || "config"
|
|
5287
|
+
};
|
|
5288
|
+
graph.addNode(cfgId, cfgNode);
|
|
5289
|
+
nodesAdded++;
|
|
5290
|
+
}
|
|
5291
|
+
const cfgEdge = {
|
|
5292
|
+
id: (0, import_types5.extractedEdgeId)(service.node.id, cfgId, import_types10.EdgeType.CONFIGURED_BY),
|
|
5293
|
+
source: service.node.id,
|
|
5294
|
+
target: cfgId,
|
|
5295
|
+
type: import_types10.EdgeType.CONFIGURED_BY,
|
|
5296
|
+
provenance: import_types10.Provenance.EXTRACTED,
|
|
5297
|
+
confidence: (0, import_types10.confidenceForExtracted)("structural"),
|
|
5298
|
+
evidence: { file: toPosix2(relPath) }
|
|
5299
|
+
};
|
|
5300
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
5301
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
5302
|
+
edgesAdded++;
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
4731
5305
|
attachIncompatibilities(service, allConfigs);
|
|
4732
5306
|
if (graph.hasNode(service.node.id)) {
|
|
4733
5307
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4739,6 +5313,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4739
5313
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4740
5314
|
delete updated.incompatibilities;
|
|
4741
5315
|
}
|
|
5316
|
+
if (!service.node.dbConnectionTarget) {
|
|
5317
|
+
delete updated.dbConnectionTarget;
|
|
5318
|
+
}
|
|
4742
5319
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4743
5320
|
}
|
|
4744
5321
|
}
|
|
@@ -4916,7 +5493,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4916
5493
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4917
5494
|
if (seen.has(dedupKey)) continue;
|
|
4918
5495
|
seen.add(dedupKey);
|
|
4919
|
-
const confidence = (0, import_types12.confidenceForExtracted)("
|
|
5496
|
+
const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
|
|
4920
5497
|
const ev = {
|
|
4921
5498
|
file: relFile,
|
|
4922
5499
|
line: site.line,
|
|
@@ -4936,7 +5513,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4936
5513
|
target: targetId,
|
|
4937
5514
|
type: import_types12.EdgeType.CALLS,
|
|
4938
5515
|
confidence,
|
|
4939
|
-
confidenceKind: "
|
|
5516
|
+
confidenceKind: "url-literal-service-target",
|
|
4940
5517
|
evidence: ev
|
|
4941
5518
|
});
|
|
4942
5519
|
continue;
|
|
@@ -5442,19 +6019,29 @@ init_cjs_shims();
|
|
|
5442
6019
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
5443
6020
|
var import_node_fs17 = require("fs");
|
|
5444
6021
|
var import_types21 = require("@neat.is/types");
|
|
5445
|
-
function
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
6022
|
+
function readDockerfile(content) {
|
|
6023
|
+
let image = null;
|
|
6024
|
+
const ports = [];
|
|
6025
|
+
let cmd = null;
|
|
6026
|
+
let entrypoint = null;
|
|
6027
|
+
for (const raw of content.split("\n")) {
|
|
5449
6028
|
const line = raw.trim();
|
|
5450
6029
|
if (!line || line.startsWith("#")) continue;
|
|
5451
|
-
if (
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
if (
|
|
5455
|
-
|
|
6030
|
+
if (/^from\s+/i.test(line)) {
|
|
6031
|
+
const candidate = line.split(/\s+/)[1];
|
|
6032
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
6033
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
6034
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
6035
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
6036
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
6037
|
+
}
|
|
6038
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
6039
|
+
entrypoint = line;
|
|
6040
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
6041
|
+
cmd = line;
|
|
6042
|
+
}
|
|
5456
6043
|
}
|
|
5457
|
-
return
|
|
6044
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5458
6045
|
}
|
|
5459
6046
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5460
6047
|
let nodesAdded = 0;
|
|
@@ -5473,14 +6060,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5473
6060
|
);
|
|
5474
6061
|
continue;
|
|
5475
6062
|
}
|
|
5476
|
-
const
|
|
5477
|
-
if (!image) continue;
|
|
5478
|
-
const node = makeInfraNode("container-image", image);
|
|
6063
|
+
const facts = readDockerfile(content);
|
|
6064
|
+
if (!facts.image) continue;
|
|
6065
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5479
6066
|
if (!graph.hasNode(node.id)) {
|
|
5480
6067
|
graph.addNode(node.id, node);
|
|
5481
6068
|
nodesAdded++;
|
|
5482
6069
|
}
|
|
5483
6070
|
const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
|
|
6071
|
+
const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
|
|
5484
6072
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5485
6073
|
graph,
|
|
5486
6074
|
service.pkg.name,
|
|
@@ -5499,12 +6087,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5499
6087
|
provenance: import_types21.Provenance.EXTRACTED,
|
|
5500
6088
|
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5501
6089
|
evidence: {
|
|
5502
|
-
file:
|
|
6090
|
+
file: evidenceFile,
|
|
6091
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5503
6092
|
}
|
|
5504
6093
|
};
|
|
5505
6094
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5506
6095
|
edgesAdded++;
|
|
5507
6096
|
}
|
|
6097
|
+
for (const port of facts.ports) {
|
|
6098
|
+
const portNode = makeInfraNode("port", String(port));
|
|
6099
|
+
if (!graph.hasNode(portNode.id)) {
|
|
6100
|
+
graph.addNode(portNode.id, portNode);
|
|
6101
|
+
nodesAdded++;
|
|
6102
|
+
}
|
|
6103
|
+
const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types21.EdgeType.CONNECTS_TO);
|
|
6104
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
6105
|
+
const portEdge = {
|
|
6106
|
+
id: portEdgeId,
|
|
6107
|
+
source: fileNodeId,
|
|
6108
|
+
target: portNode.id,
|
|
6109
|
+
type: import_types21.EdgeType.CONNECTS_TO,
|
|
6110
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
6111
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
6112
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
6113
|
+
};
|
|
6114
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
6115
|
+
edgesAdded++;
|
|
6116
|
+
}
|
|
5508
6117
|
}
|
|
5509
6118
|
return { nodesAdded, edgesAdded };
|
|
5510
6119
|
}
|
|
@@ -5513,7 +6122,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5513
6122
|
init_cjs_shims();
|
|
5514
6123
|
var import_node_fs18 = require("fs");
|
|
5515
6124
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
6125
|
+
var import_types22 = require("@neat.is/types");
|
|
5516
6126
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
6127
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5517
6128
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5518
6129
|
if (depth > max) return [];
|
|
5519
6130
|
const out = [];
|
|
@@ -5530,24 +6141,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5530
6141
|
}
|
|
5531
6142
|
return out;
|
|
5532
6143
|
}
|
|
6144
|
+
function blockBody(content, from) {
|
|
6145
|
+
const open = content.indexOf("{", from);
|
|
6146
|
+
if (open === -1) return null;
|
|
6147
|
+
let depth = 0;
|
|
6148
|
+
for (let i = open; i < content.length; i++) {
|
|
6149
|
+
const ch = content[i];
|
|
6150
|
+
if (ch === "{") depth++;
|
|
6151
|
+
else if (ch === "}") {
|
|
6152
|
+
depth--;
|
|
6153
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
return null;
|
|
6157
|
+
}
|
|
6158
|
+
function lineAt(content, index) {
|
|
6159
|
+
let line = 1;
|
|
6160
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
6161
|
+
if (content[i] === "\n") line++;
|
|
6162
|
+
}
|
|
6163
|
+
return line;
|
|
6164
|
+
}
|
|
5533
6165
|
async function addTerraformResources(graph, scanPath) {
|
|
5534
6166
|
let nodesAdded = 0;
|
|
6167
|
+
let edgesAdded = 0;
|
|
5535
6168
|
const files = await walkTfFiles(scanPath);
|
|
5536
6169
|
for (const file of files) {
|
|
5537
6170
|
const content = await import_node_fs18.promises.readFile(file, "utf8");
|
|
6171
|
+
const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
|
|
6172
|
+
const resources = [];
|
|
6173
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5538
6174
|
RESOURCE_RE.lastIndex = 0;
|
|
5539
6175
|
let m;
|
|
5540
6176
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5541
|
-
const
|
|
6177
|
+
const type = m[1];
|
|
5542
6178
|
const name = m[2];
|
|
5543
|
-
const node = makeInfraNode(
|
|
6179
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5544
6180
|
if (!graph.hasNode(node.id)) {
|
|
5545
6181
|
graph.addNode(node.id, node);
|
|
5546
6182
|
nodesAdded++;
|
|
5547
6183
|
}
|
|
6184
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
6185
|
+
const resource = {
|
|
6186
|
+
type,
|
|
6187
|
+
name,
|
|
6188
|
+
nodeId: node.id,
|
|
6189
|
+
body: span?.body ?? "",
|
|
6190
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
6191
|
+
};
|
|
6192
|
+
resources.push(resource);
|
|
6193
|
+
byKey.set(`${type}.${name}`, resource);
|
|
6194
|
+
}
|
|
6195
|
+
for (const resource of resources) {
|
|
6196
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6197
|
+
REFERENCE_RE.lastIndex = 0;
|
|
6198
|
+
let ref;
|
|
6199
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
6200
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
6201
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
6202
|
+
const target = byKey.get(key);
|
|
6203
|
+
if (!target) continue;
|
|
6204
|
+
if (seen.has(target.nodeId)) continue;
|
|
6205
|
+
seen.add(target.nodeId);
|
|
6206
|
+
const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types22.EdgeType.DEPENDS_ON);
|
|
6207
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
6208
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
6209
|
+
const edge = {
|
|
6210
|
+
id: edgeId,
|
|
6211
|
+
source: resource.nodeId,
|
|
6212
|
+
target: target.nodeId,
|
|
6213
|
+
type: import_types22.EdgeType.DEPENDS_ON,
|
|
6214
|
+
provenance: import_types22.Provenance.EXTRACTED,
|
|
6215
|
+
confidence: (0, import_types22.confidenceForExtracted)("structural"),
|
|
6216
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
6217
|
+
};
|
|
6218
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
6219
|
+
edgesAdded++;
|
|
6220
|
+
}
|
|
5548
6221
|
}
|
|
5549
6222
|
}
|
|
5550
|
-
return { nodesAdded, edgesAdded
|
|
6223
|
+
return { nodesAdded, edgesAdded };
|
|
5551
6224
|
}
|
|
5552
6225
|
|
|
5553
6226
|
// src/extract/infra/k8s.ts
|
|
@@ -5625,11 +6298,11 @@ var import_node_path35 = __toESM(require("path"), 1);
|
|
|
5625
6298
|
init_cjs_shims();
|
|
5626
6299
|
var import_node_fs20 = require("fs");
|
|
5627
6300
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5628
|
-
var
|
|
6301
|
+
var import_types23 = require("@neat.is/types");
|
|
5629
6302
|
function dropOrphanedFileNodes(graph) {
|
|
5630
6303
|
const orphans = [];
|
|
5631
6304
|
graph.forEachNode((id, attrs) => {
|
|
5632
|
-
if (attrs.type !==
|
|
6305
|
+
if (attrs.type !== import_types23.NodeType.FileNode) return;
|
|
5633
6306
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5634
6307
|
orphans.push(id);
|
|
5635
6308
|
}
|
|
@@ -5642,7 +6315,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5642
6315
|
const bases = [scanPath, ...serviceDirs];
|
|
5643
6316
|
graph.forEachEdge((id, attrs) => {
|
|
5644
6317
|
const edge = attrs;
|
|
5645
|
-
if (edge.provenance !==
|
|
6318
|
+
if (edge.provenance !== import_types23.Provenance.EXTRACTED) return;
|
|
5646
6319
|
const evidenceFile = edge.evidence?.file;
|
|
5647
6320
|
if (!evidenceFile) return;
|
|
5648
6321
|
if (import_node_path34.default.isAbsolute(evidenceFile)) {
|
|
@@ -5802,7 +6475,7 @@ function canonicalJson(value) {
|
|
|
5802
6475
|
init_cjs_shims();
|
|
5803
6476
|
var import_node_fs22 = require("fs");
|
|
5804
6477
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
5805
|
-
var
|
|
6478
|
+
var import_types24 = require("@neat.is/types");
|
|
5806
6479
|
var SCHEMA_VERSION = 4;
|
|
5807
6480
|
function migrateV1ToV2(payload) {
|
|
5808
6481
|
const nodes = payload.graph.nodes;
|
|
@@ -5824,12 +6497,12 @@ function migrateV2ToV3(payload) {
|
|
|
5824
6497
|
for (const edge of edges) {
|
|
5825
6498
|
const attrs = edge.attributes;
|
|
5826
6499
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5827
|
-
attrs.provenance =
|
|
6500
|
+
attrs.provenance = import_types24.Provenance.OBSERVED;
|
|
5828
6501
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5829
6502
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5830
6503
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5831
6504
|
if (type && source && target) {
|
|
5832
|
-
const newId = (0,
|
|
6505
|
+
const newId = (0, import_types24.observedEdgeId)(source, target, type);
|
|
5833
6506
|
attrs.id = newId;
|
|
5834
6507
|
if (edge.key) edge.key = newId;
|
|
5835
6508
|
}
|
|
@@ -5978,7 +6651,7 @@ init_cjs_shims();
|
|
|
5978
6651
|
var import_node_fs23 = require("fs");
|
|
5979
6652
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5980
6653
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
5981
|
-
var
|
|
6654
|
+
var import_types25 = require("@neat.is/types");
|
|
5982
6655
|
function neatHome() {
|
|
5983
6656
|
const override = process.env.NEAT_HOME;
|
|
5984
6657
|
if (override && override.length > 0) return import_node_path38.default.resolve(override);
|
|
@@ -5999,7 +6672,7 @@ async function readRegistry() {
|
|
|
5999
6672
|
throw err;
|
|
6000
6673
|
}
|
|
6001
6674
|
const parsed = JSON.parse(raw);
|
|
6002
|
-
return
|
|
6675
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6003
6676
|
}
|
|
6004
6677
|
async function getProject(name) {
|
|
6005
6678
|
const reg = await readRegistry();
|
|
@@ -6200,6 +6873,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6200
6873
|
}
|
|
6201
6874
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6202
6875
|
});
|
|
6876
|
+
scope.get(
|
|
6877
|
+
"/graph/observed-dependencies/:nodeId",
|
|
6878
|
+
async (req, reply) => {
|
|
6879
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6880
|
+
if (!proj) return;
|
|
6881
|
+
const { nodeId } = req.params;
|
|
6882
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6883
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6884
|
+
}
|
|
6885
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
6886
|
+
}
|
|
6887
|
+
);
|
|
6203
6888
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6204
6889
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6205
6890
|
if (!proj) return;
|
|
@@ -6208,11 +6893,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6208
6893
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6209
6894
|
const parsed = [];
|
|
6210
6895
|
for (const c of candidates) {
|
|
6211
|
-
const r =
|
|
6896
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6212
6897
|
if (!r.success) {
|
|
6213
6898
|
return reply.code(400).send({
|
|
6214
6899
|
error: `unknown divergence type "${c}"`,
|
|
6215
|
-
allowed:
|
|
6900
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6216
6901
|
});
|
|
6217
6902
|
}
|
|
6218
6903
|
parsed.push(r.data);
|
|
@@ -6260,23 +6945,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6260
6945
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6261
6946
|
return { count: sliced.length, total, events: sliced };
|
|
6262
6947
|
});
|
|
6948
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
6949
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6950
|
+
if (!proj) return;
|
|
6951
|
+
const { nodeId } = req.params;
|
|
6952
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6953
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6954
|
+
return;
|
|
6955
|
+
}
|
|
6956
|
+
const epath = errorsPathFor(proj);
|
|
6957
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6958
|
+
const events = await readErrorEvents(epath);
|
|
6959
|
+
const filtered = events.filter(
|
|
6960
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6961
|
+
);
|
|
6962
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6963
|
+
};
|
|
6263
6964
|
scope.get(
|
|
6264
6965
|
"/incidents/:nodeId",
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6271
|
-
}
|
|
6272
|
-
const epath = errorsPathFor(proj);
|
|
6273
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6274
|
-
const events = await readErrorEvents(epath);
|
|
6275
|
-
const filtered = events.filter(
|
|
6276
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6277
|
-
);
|
|
6278
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6279
|
-
}
|
|
6966
|
+
incidentHistoryHandler
|
|
6967
|
+
);
|
|
6968
|
+
scope.get(
|
|
6969
|
+
"/graph/incident-history/:nodeId",
|
|
6970
|
+
incidentHistoryHandler
|
|
6280
6971
|
);
|
|
6281
6972
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6282
6973
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6285,16 +6976,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6285
6976
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6286
6977
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6287
6978
|
}
|
|
6288
|
-
let errorEvent;
|
|
6289
6979
|
const epath = errorsPathFor(proj);
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6980
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
6981
|
+
let errorEvent;
|
|
6982
|
+
if (req.query.errorId) {
|
|
6983
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6293
6984
|
if (!errorEvent) {
|
|
6294
6985
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6295
6986
|
}
|
|
6296
6987
|
}
|
|
6297
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
6988
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6298
6989
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6299
6990
|
return result;
|
|
6300
6991
|
});
|
|
@@ -6425,7 +7116,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6425
7116
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6426
7117
|
let violations = await log.readAll();
|
|
6427
7118
|
if (req.query.severity) {
|
|
6428
|
-
const sev =
|
|
7119
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
|
|
6429
7120
|
if (!sev.success) {
|
|
6430
7121
|
return reply.code(400).send({
|
|
6431
7122
|
error: "invalid severity",
|
|
@@ -6439,10 +7130,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6439
7130
|
}
|
|
6440
7131
|
return { violations };
|
|
6441
7132
|
});
|
|
7133
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7134
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7135
|
+
if (!proj) return;
|
|
7136
|
+
const nodeId = req.query.node;
|
|
7137
|
+
if (!nodeId) {
|
|
7138
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7139
|
+
}
|
|
7140
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7141
|
+
let policies = [];
|
|
7142
|
+
if (policyPath) {
|
|
7143
|
+
try {
|
|
7144
|
+
policies = await loadPolicyFile(policyPath);
|
|
7145
|
+
} catch (err) {
|
|
7146
|
+
return reply.code(400).send({
|
|
7147
|
+
error: "policy.json failed to parse",
|
|
7148
|
+
details: err.message
|
|
7149
|
+
});
|
|
7150
|
+
}
|
|
7151
|
+
}
|
|
7152
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7153
|
+
return { node: nodeId, applicable };
|
|
7154
|
+
});
|
|
6442
7155
|
scope.post("/policies/check", async (req, reply) => {
|
|
6443
7156
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6444
7157
|
if (!proj) return;
|
|
6445
|
-
const parsed =
|
|
7158
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6446
7159
|
if (!parsed.success) {
|
|
6447
7160
|
return reply.code(400).send({
|
|
6448
7161
|
error: "invalid /policies/check body",
|