@neat.is/core 0.4.20-dev.20260630 → 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/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)) {
|
|
@@ -2682,7 +3114,10 @@ async function handleSpan(ctx, span) {
|
|
|
2682
3114
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2683
3115
|
cacheSpanService(span, nowMs, callSite);
|
|
2684
3116
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2685
|
-
const callSiteEvidence = callSite ? {
|
|
3117
|
+
const callSiteEvidence = callSite ? {
|
|
3118
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
3119
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
3120
|
+
} : void 0;
|
|
2686
3121
|
let affectedNode = sourceId;
|
|
2687
3122
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2688
3123
|
if (span.dbSystem) {
|
|
@@ -2704,7 +3139,7 @@ async function handleSpan(ctx, span) {
|
|
|
2704
3139
|
} else {
|
|
2705
3140
|
const host = pickAddress(span);
|
|
2706
3141
|
let resolvedViaAddress = false;
|
|
2707
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
3142
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2708
3143
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2709
3144
|
if (targetId && targetId !== sourceId) {
|
|
2710
3145
|
upsertObservedEdge(
|
|
@@ -2739,7 +3174,11 @@ async function handleSpan(ctx, span) {
|
|
|
2739
3174
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2740
3175
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2741
3176
|
const fallbackEvidence = parent.callSite ? {
|
|
2742
|
-
file:
|
|
3177
|
+
file: reconcileObservedRelPath(
|
|
3178
|
+
ctx.graph,
|
|
3179
|
+
parent.service,
|
|
3180
|
+
parent.callSite.relPath
|
|
3181
|
+
),
|
|
2743
3182
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2744
3183
|
} : void 0;
|
|
2745
3184
|
upsertObservedEdge(
|
|
@@ -2764,7 +3203,7 @@ async function handleSpan(ctx, span) {
|
|
|
2764
3203
|
service: span.service,
|
|
2765
3204
|
traceId: span.traceId,
|
|
2766
3205
|
spanId: span.spanId,
|
|
2767
|
-
errorMessage: span
|
|
3206
|
+
errorMessage: incidentMessage(span),
|
|
2768
3207
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2769
3208
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2770
3209
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2940,12 +3379,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2940
3379
|
async function readErrorEvents(errorsPath) {
|
|
2941
3380
|
try {
|
|
2942
3381
|
const raw = await import_node_fs5.promises.readFile(errorsPath, "utf8");
|
|
2943
|
-
|
|
3382
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
3383
|
+
return dedupeIncidents(events);
|
|
2944
3384
|
} catch (err) {
|
|
2945
3385
|
if (err.code === "ENOENT") return [];
|
|
2946
3386
|
throw err;
|
|
2947
3387
|
}
|
|
2948
3388
|
}
|
|
3389
|
+
function isSynthesizedHttpIncident(ev) {
|
|
3390
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
3391
|
+
if (ev.errorType) return false;
|
|
3392
|
+
if (!ev.attributes) return false;
|
|
3393
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
3394
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
3395
|
+
}
|
|
3396
|
+
function dedupeIncidents(events) {
|
|
3397
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3398
|
+
const once = [];
|
|
3399
|
+
for (const ev of events) {
|
|
3400
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
3401
|
+
if (key === void 0) {
|
|
3402
|
+
once.push(ev);
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
if (seen.has(key)) continue;
|
|
3406
|
+
seen.add(key);
|
|
3407
|
+
once.push(ev);
|
|
3408
|
+
}
|
|
3409
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
3410
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
3411
|
+
for (const ev of once) {
|
|
3412
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
3413
|
+
}
|
|
3414
|
+
return once.filter((ev) => {
|
|
3415
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
3416
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
3417
|
+
});
|
|
3418
|
+
}
|
|
2949
3419
|
function mergeSnapshot(graph, snapshot) {
|
|
2950
3420
|
const exported = snapshot.graph;
|
|
2951
3421
|
let nodesAdded = 0;
|
|
@@ -3026,10 +3496,17 @@ function isConfigFile(name) {
|
|
|
3026
3496
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
3027
3497
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
3028
3498
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3499
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
3029
3500
|
return { match: true, fileType: "env" };
|
|
3030
3501
|
}
|
|
3031
3502
|
return { match: false, fileType: "" };
|
|
3032
3503
|
}
|
|
3504
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3505
|
+
return name === ".env.neat";
|
|
3506
|
+
}
|
|
3507
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3508
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3509
|
+
}
|
|
3033
3510
|
function isTestPath(filePath) {
|
|
3034
3511
|
const normalised = filePath.replace(/\\/g, "/");
|
|
3035
3512
|
const segments = normalised.split("/");
|
|
@@ -3723,7 +4200,9 @@ async function walkSourceFiles(dir) {
|
|
|
3723
4200
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3724
4201
|
if (await isPythonVenvDir(full)) continue;
|
|
3725
4202
|
await walk(full);
|
|
3726
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path12.default.extname(entry.name))
|
|
4203
|
+
} 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
|
|
4204
|
+
// would attribute our instrumentation imports to the user's service.
|
|
4205
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
3727
4206
|
out.push(full);
|
|
3728
4207
|
}
|
|
3729
4208
|
}
|
|
@@ -3893,11 +4372,28 @@ function collectJsImports(node, out) {
|
|
|
3893
4372
|
if (child) collectJsImports(child, out);
|
|
3894
4373
|
}
|
|
3895
4374
|
}
|
|
4375
|
+
function collectImportedNames(node, out) {
|
|
4376
|
+
if (node.type === "aliased_import") {
|
|
4377
|
+
const nameNode = node.childForFieldName("name");
|
|
4378
|
+
if (nameNode) out.push(nameNode.text);
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
if (node.type === "dotted_name") {
|
|
4382
|
+
out.push(node.text);
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4386
|
+
const child = node.namedChild(i);
|
|
4387
|
+
if (child) collectImportedNames(child, out);
|
|
4388
|
+
}
|
|
4389
|
+
}
|
|
3896
4390
|
function collectPyImports(node, out) {
|
|
3897
4391
|
if (node.type === "import_from_statement") {
|
|
3898
4392
|
let level = 0;
|
|
3899
4393
|
let modulePath = "";
|
|
4394
|
+
const names = [];
|
|
3900
4395
|
let pastFrom = false;
|
|
4396
|
+
let pastImport = false;
|
|
3901
4397
|
for (let i = 0; i < node.childCount; i++) {
|
|
3902
4398
|
const child = node.child(i);
|
|
3903
4399
|
if (!child) continue;
|
|
@@ -3905,26 +4401,30 @@ function collectPyImports(node, out) {
|
|
|
3905
4401
|
if (child.type === "from") pastFrom = true;
|
|
3906
4402
|
continue;
|
|
3907
4403
|
}
|
|
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;
|
|
4404
|
+
if (!pastImport) {
|
|
4405
|
+
if (child.type === "import") {
|
|
4406
|
+
pastImport = true;
|
|
4407
|
+
continue;
|
|
3918
4408
|
}
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
4409
|
+
if (child.type === "relative_import") {
|
|
4410
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4411
|
+
const rc = child.child(j);
|
|
4412
|
+
if (!rc) continue;
|
|
4413
|
+
if (rc.type === "import_prefix") {
|
|
4414
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
4415
|
+
if (rc.child(k)?.type === ".") level++;
|
|
4416
|
+
}
|
|
4417
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4418
|
+
}
|
|
4419
|
+
} else if (child.type === "dotted_name") {
|
|
4420
|
+
modulePath = child.text;
|
|
4421
|
+
}
|
|
4422
|
+
continue;
|
|
3924
4423
|
}
|
|
4424
|
+
collectImportedNames(child, names);
|
|
3925
4425
|
}
|
|
3926
4426
|
if (level > 0 || modulePath) {
|
|
3927
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4427
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3928
4428
|
}
|
|
3929
4429
|
}
|
|
3930
4430
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -4026,8 +4526,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
4026
4526
|
return null;
|
|
4027
4527
|
}
|
|
4028
4528
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
4029
|
-
if (!imp.modulePath) return null;
|
|
4030
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
4031
4529
|
let baseDir;
|
|
4032
4530
|
if (imp.level > 0) {
|
|
4033
4531
|
baseDir = import_node_path14.default.dirname(importerPath);
|
|
@@ -4035,13 +4533,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
4035
4533
|
} else {
|
|
4036
4534
|
baseDir = serviceDir;
|
|
4037
4535
|
}
|
|
4038
|
-
const
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4536
|
+
const moduleBase = imp.modulePath ? import_node_path14.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4537
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4538
|
+
let needModuleFile = imp.names.length === 0;
|
|
4539
|
+
for (const name of imp.names) {
|
|
4540
|
+
const submoduleFile = import_node_path14.default.join(moduleBase, `${name}.py`);
|
|
4541
|
+
const subpackageInit = import_node_path14.default.join(moduleBase, name, "__init__.py");
|
|
4542
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists2(submoduleFile)) {
|
|
4543
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, submoduleFile)));
|
|
4544
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists2(subpackageInit)) {
|
|
4545
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, subpackageInit)));
|
|
4546
|
+
} else {
|
|
4547
|
+
needModuleFile = true;
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
if (needModuleFile) {
|
|
4551
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path14.default.join(moduleBase, "__init__.py")] : [import_node_path14.default.join(moduleBase, "__init__.py")];
|
|
4552
|
+
for (const candidate of moduleFileCandidates) {
|
|
4553
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists2(candidate)) {
|
|
4554
|
+
resolved.add(toPosix2(import_node_path14.default.relative(serviceDir, candidate)));
|
|
4555
|
+
break;
|
|
4556
|
+
}
|
|
4042
4557
|
}
|
|
4043
4558
|
}
|
|
4044
|
-
return
|
|
4559
|
+
return [...resolved];
|
|
4045
4560
|
}
|
|
4046
4561
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
4047
4562
|
const importeeFileId = (0, import_types9.fileId)(serviceName, importeeRelPath);
|
|
@@ -4082,17 +4597,18 @@ async function addImports(graph, services) {
|
|
|
4082
4597
|
continue;
|
|
4083
4598
|
}
|
|
4084
4599
|
for (const imp of pyImports) {
|
|
4085
|
-
const
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4600
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4601
|
+
for (const resolved of resolvedPaths) {
|
|
4602
|
+
edgesAdded += emitImportEdge(
|
|
4603
|
+
graph,
|
|
4604
|
+
service.pkg.name,
|
|
4605
|
+
importerFileId,
|
|
4606
|
+
relFile,
|
|
4607
|
+
resolved,
|
|
4608
|
+
imp.line,
|
|
4609
|
+
imp.snippet
|
|
4610
|
+
);
|
|
4611
|
+
}
|
|
4096
4612
|
}
|
|
4097
4613
|
continue;
|
|
4098
4614
|
}
|
|
@@ -4728,6 +5244,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4728
5244
|
edgesAdded++;
|
|
4729
5245
|
}
|
|
4730
5246
|
}
|
|
5247
|
+
if (allConfigs.length === 1) {
|
|
5248
|
+
const primary = allConfigs[0];
|
|
5249
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
5250
|
+
const relPath = import_node_path22.default.relative(scanPath, primary.sourceFile);
|
|
5251
|
+
const cfgId = (0, import_types10.configId)(relPath);
|
|
5252
|
+
if (!graph.hasNode(cfgId)) {
|
|
5253
|
+
const cfgNode = {
|
|
5254
|
+
id: cfgId,
|
|
5255
|
+
type: import_types10.NodeType.ConfigNode,
|
|
5256
|
+
name: import_node_path22.default.basename(primary.sourceFile),
|
|
5257
|
+
path: relPath,
|
|
5258
|
+
fileType: isConfigFile(import_node_path22.default.basename(primary.sourceFile)).fileType || "config"
|
|
5259
|
+
};
|
|
5260
|
+
graph.addNode(cfgId, cfgNode);
|
|
5261
|
+
nodesAdded++;
|
|
5262
|
+
}
|
|
5263
|
+
const cfgEdge = {
|
|
5264
|
+
id: (0, import_types5.extractedEdgeId)(service.node.id, cfgId, import_types10.EdgeType.CONFIGURED_BY),
|
|
5265
|
+
source: service.node.id,
|
|
5266
|
+
target: cfgId,
|
|
5267
|
+
type: import_types10.EdgeType.CONFIGURED_BY,
|
|
5268
|
+
provenance: import_types10.Provenance.EXTRACTED,
|
|
5269
|
+
confidence: (0, import_types10.confidenceForExtracted)("structural"),
|
|
5270
|
+
evidence: { file: toPosix2(relPath) }
|
|
5271
|
+
};
|
|
5272
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
5273
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
5274
|
+
edgesAdded++;
|
|
5275
|
+
}
|
|
5276
|
+
}
|
|
4731
5277
|
attachIncompatibilities(service, allConfigs);
|
|
4732
5278
|
if (graph.hasNode(service.node.id)) {
|
|
4733
5279
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4739,6 +5285,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4739
5285
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4740
5286
|
delete updated.incompatibilities;
|
|
4741
5287
|
}
|
|
5288
|
+
if (!service.node.dbConnectionTarget) {
|
|
5289
|
+
delete updated.dbConnectionTarget;
|
|
5290
|
+
}
|
|
4742
5291
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4743
5292
|
}
|
|
4744
5293
|
}
|
|
@@ -4916,7 +5465,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4916
5465
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4917
5466
|
if (seen.has(dedupKey)) continue;
|
|
4918
5467
|
seen.add(dedupKey);
|
|
4919
|
-
const confidence = (0, import_types12.confidenceForExtracted)("
|
|
5468
|
+
const confidence = (0, import_types12.confidenceForExtracted)("url-literal-service-target");
|
|
4920
5469
|
const ev = {
|
|
4921
5470
|
file: relFile,
|
|
4922
5471
|
line: site.line,
|
|
@@ -4936,7 +5485,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4936
5485
|
target: targetId,
|
|
4937
5486
|
type: import_types12.EdgeType.CALLS,
|
|
4938
5487
|
confidence,
|
|
4939
|
-
confidenceKind: "
|
|
5488
|
+
confidenceKind: "url-literal-service-target",
|
|
4940
5489
|
evidence: ev
|
|
4941
5490
|
});
|
|
4942
5491
|
continue;
|
|
@@ -5442,19 +5991,29 @@ init_cjs_shims();
|
|
|
5442
5991
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
5443
5992
|
var import_node_fs17 = require("fs");
|
|
5444
5993
|
var import_types21 = require("@neat.is/types");
|
|
5445
|
-
function
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5994
|
+
function readDockerfile(content) {
|
|
5995
|
+
let image = null;
|
|
5996
|
+
const ports = [];
|
|
5997
|
+
let cmd = null;
|
|
5998
|
+
let entrypoint = null;
|
|
5999
|
+
for (const raw of content.split("\n")) {
|
|
5449
6000
|
const line = raw.trim();
|
|
5450
6001
|
if (!line || line.startsWith("#")) continue;
|
|
5451
|
-
if (
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
if (
|
|
5455
|
-
|
|
6002
|
+
if (/^from\s+/i.test(line)) {
|
|
6003
|
+
const candidate = line.split(/\s+/)[1];
|
|
6004
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
6005
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
6006
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
6007
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
6008
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
6009
|
+
}
|
|
6010
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
6011
|
+
entrypoint = line;
|
|
6012
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
6013
|
+
cmd = line;
|
|
6014
|
+
}
|
|
5456
6015
|
}
|
|
5457
|
-
return
|
|
6016
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5458
6017
|
}
|
|
5459
6018
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5460
6019
|
let nodesAdded = 0;
|
|
@@ -5473,14 +6032,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5473
6032
|
);
|
|
5474
6033
|
continue;
|
|
5475
6034
|
}
|
|
5476
|
-
const
|
|
5477
|
-
if (!image) continue;
|
|
5478
|
-
const node = makeInfraNode("container-image", image);
|
|
6035
|
+
const facts = readDockerfile(content);
|
|
6036
|
+
if (!facts.image) continue;
|
|
6037
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5479
6038
|
if (!graph.hasNode(node.id)) {
|
|
5480
6039
|
graph.addNode(node.id, node);
|
|
5481
6040
|
nodesAdded++;
|
|
5482
6041
|
}
|
|
5483
6042
|
const relDockerfile = toPosix2(import_node_path31.default.relative(service.dir, dockerfilePath));
|
|
6043
|
+
const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, dockerfilePath));
|
|
5484
6044
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5485
6045
|
graph,
|
|
5486
6046
|
service.pkg.name,
|
|
@@ -5499,12 +6059,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5499
6059
|
provenance: import_types21.Provenance.EXTRACTED,
|
|
5500
6060
|
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5501
6061
|
evidence: {
|
|
5502
|
-
file:
|
|
6062
|
+
file: evidenceFile,
|
|
6063
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5503
6064
|
}
|
|
5504
6065
|
};
|
|
5505
6066
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5506
6067
|
edgesAdded++;
|
|
5507
6068
|
}
|
|
6069
|
+
for (const port of facts.ports) {
|
|
6070
|
+
const portNode = makeInfraNode("port", String(port));
|
|
6071
|
+
if (!graph.hasNode(portNode.id)) {
|
|
6072
|
+
graph.addNode(portNode.id, portNode);
|
|
6073
|
+
nodesAdded++;
|
|
6074
|
+
}
|
|
6075
|
+
const portEdgeId = (0, import_types5.extractedEdgeId)(fileNodeId, portNode.id, import_types21.EdgeType.CONNECTS_TO);
|
|
6076
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
6077
|
+
const portEdge = {
|
|
6078
|
+
id: portEdgeId,
|
|
6079
|
+
source: fileNodeId,
|
|
6080
|
+
target: portNode.id,
|
|
6081
|
+
type: import_types21.EdgeType.CONNECTS_TO,
|
|
6082
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
6083
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
6084
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
6085
|
+
};
|
|
6086
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
6087
|
+
edgesAdded++;
|
|
6088
|
+
}
|
|
5508
6089
|
}
|
|
5509
6090
|
return { nodesAdded, edgesAdded };
|
|
5510
6091
|
}
|
|
@@ -5513,7 +6094,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5513
6094
|
init_cjs_shims();
|
|
5514
6095
|
var import_node_fs18 = require("fs");
|
|
5515
6096
|
var import_node_path32 = __toESM(require("path"), 1);
|
|
6097
|
+
var import_types22 = require("@neat.is/types");
|
|
5516
6098
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
6099
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5517
6100
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5518
6101
|
if (depth > max) return [];
|
|
5519
6102
|
const out = [];
|
|
@@ -5530,24 +6113,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5530
6113
|
}
|
|
5531
6114
|
return out;
|
|
5532
6115
|
}
|
|
6116
|
+
function blockBody(content, from) {
|
|
6117
|
+
const open = content.indexOf("{", from);
|
|
6118
|
+
if (open === -1) return null;
|
|
6119
|
+
let depth = 0;
|
|
6120
|
+
for (let i = open; i < content.length; i++) {
|
|
6121
|
+
const ch = content[i];
|
|
6122
|
+
if (ch === "{") depth++;
|
|
6123
|
+
else if (ch === "}") {
|
|
6124
|
+
depth--;
|
|
6125
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
6126
|
+
}
|
|
6127
|
+
}
|
|
6128
|
+
return null;
|
|
6129
|
+
}
|
|
6130
|
+
function lineAt(content, index) {
|
|
6131
|
+
let line = 1;
|
|
6132
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
6133
|
+
if (content[i] === "\n") line++;
|
|
6134
|
+
}
|
|
6135
|
+
return line;
|
|
6136
|
+
}
|
|
5533
6137
|
async function addTerraformResources(graph, scanPath) {
|
|
5534
6138
|
let nodesAdded = 0;
|
|
6139
|
+
let edgesAdded = 0;
|
|
5535
6140
|
const files = await walkTfFiles(scanPath);
|
|
5536
6141
|
for (const file of files) {
|
|
5537
6142
|
const content = await import_node_fs18.promises.readFile(file, "utf8");
|
|
6143
|
+
const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, file));
|
|
6144
|
+
const resources = [];
|
|
6145
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5538
6146
|
RESOURCE_RE.lastIndex = 0;
|
|
5539
6147
|
let m;
|
|
5540
6148
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5541
|
-
const
|
|
6149
|
+
const type = m[1];
|
|
5542
6150
|
const name = m[2];
|
|
5543
|
-
const node = makeInfraNode(
|
|
6151
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5544
6152
|
if (!graph.hasNode(node.id)) {
|
|
5545
6153
|
graph.addNode(node.id, node);
|
|
5546
6154
|
nodesAdded++;
|
|
5547
6155
|
}
|
|
6156
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
6157
|
+
const resource = {
|
|
6158
|
+
type,
|
|
6159
|
+
name,
|
|
6160
|
+
nodeId: node.id,
|
|
6161
|
+
body: span?.body ?? "",
|
|
6162
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
6163
|
+
};
|
|
6164
|
+
resources.push(resource);
|
|
6165
|
+
byKey.set(`${type}.${name}`, resource);
|
|
6166
|
+
}
|
|
6167
|
+
for (const resource of resources) {
|
|
6168
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6169
|
+
REFERENCE_RE.lastIndex = 0;
|
|
6170
|
+
let ref;
|
|
6171
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
6172
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
6173
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
6174
|
+
const target = byKey.get(key);
|
|
6175
|
+
if (!target) continue;
|
|
6176
|
+
if (seen.has(target.nodeId)) continue;
|
|
6177
|
+
seen.add(target.nodeId);
|
|
6178
|
+
const edgeId = (0, import_types5.extractedEdgeId)(resource.nodeId, target.nodeId, import_types22.EdgeType.DEPENDS_ON);
|
|
6179
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
6180
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
6181
|
+
const edge = {
|
|
6182
|
+
id: edgeId,
|
|
6183
|
+
source: resource.nodeId,
|
|
6184
|
+
target: target.nodeId,
|
|
6185
|
+
type: import_types22.EdgeType.DEPENDS_ON,
|
|
6186
|
+
provenance: import_types22.Provenance.EXTRACTED,
|
|
6187
|
+
confidence: (0, import_types22.confidenceForExtracted)("structural"),
|
|
6188
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
6189
|
+
};
|
|
6190
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
6191
|
+
edgesAdded++;
|
|
6192
|
+
}
|
|
5548
6193
|
}
|
|
5549
6194
|
}
|
|
5550
|
-
return { nodesAdded, edgesAdded
|
|
6195
|
+
return { nodesAdded, edgesAdded };
|
|
5551
6196
|
}
|
|
5552
6197
|
|
|
5553
6198
|
// src/extract/infra/k8s.ts
|
|
@@ -5625,11 +6270,11 @@ var import_node_path35 = __toESM(require("path"), 1);
|
|
|
5625
6270
|
init_cjs_shims();
|
|
5626
6271
|
var import_node_fs20 = require("fs");
|
|
5627
6272
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5628
|
-
var
|
|
6273
|
+
var import_types23 = require("@neat.is/types");
|
|
5629
6274
|
function dropOrphanedFileNodes(graph) {
|
|
5630
6275
|
const orphans = [];
|
|
5631
6276
|
graph.forEachNode((id, attrs) => {
|
|
5632
|
-
if (attrs.type !==
|
|
6277
|
+
if (attrs.type !== import_types23.NodeType.FileNode) return;
|
|
5633
6278
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5634
6279
|
orphans.push(id);
|
|
5635
6280
|
}
|
|
@@ -5642,7 +6287,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5642
6287
|
const bases = [scanPath, ...serviceDirs];
|
|
5643
6288
|
graph.forEachEdge((id, attrs) => {
|
|
5644
6289
|
const edge = attrs;
|
|
5645
|
-
if (edge.provenance !==
|
|
6290
|
+
if (edge.provenance !== import_types23.Provenance.EXTRACTED) return;
|
|
5646
6291
|
const evidenceFile = edge.evidence?.file;
|
|
5647
6292
|
if (!evidenceFile) return;
|
|
5648
6293
|
if (import_node_path34.default.isAbsolute(evidenceFile)) {
|
|
@@ -5802,7 +6447,7 @@ function canonicalJson(value) {
|
|
|
5802
6447
|
init_cjs_shims();
|
|
5803
6448
|
var import_node_fs22 = require("fs");
|
|
5804
6449
|
var import_node_path36 = __toESM(require("path"), 1);
|
|
5805
|
-
var
|
|
6450
|
+
var import_types24 = require("@neat.is/types");
|
|
5806
6451
|
var SCHEMA_VERSION = 4;
|
|
5807
6452
|
function migrateV1ToV2(payload) {
|
|
5808
6453
|
const nodes = payload.graph.nodes;
|
|
@@ -5824,12 +6469,12 @@ function migrateV2ToV3(payload) {
|
|
|
5824
6469
|
for (const edge of edges) {
|
|
5825
6470
|
const attrs = edge.attributes;
|
|
5826
6471
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5827
|
-
attrs.provenance =
|
|
6472
|
+
attrs.provenance = import_types24.Provenance.OBSERVED;
|
|
5828
6473
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5829
6474
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5830
6475
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5831
6476
|
if (type && source && target) {
|
|
5832
|
-
const newId = (0,
|
|
6477
|
+
const newId = (0, import_types24.observedEdgeId)(source, target, type);
|
|
5833
6478
|
attrs.id = newId;
|
|
5834
6479
|
if (edge.key) edge.key = newId;
|
|
5835
6480
|
}
|
|
@@ -5978,7 +6623,7 @@ init_cjs_shims();
|
|
|
5978
6623
|
var import_node_fs23 = require("fs");
|
|
5979
6624
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
5980
6625
|
var import_node_path38 = __toESM(require("path"), 1);
|
|
5981
|
-
var
|
|
6626
|
+
var import_types25 = require("@neat.is/types");
|
|
5982
6627
|
function neatHome() {
|
|
5983
6628
|
const override = process.env.NEAT_HOME;
|
|
5984
6629
|
if (override && override.length > 0) return import_node_path38.default.resolve(override);
|
|
@@ -5999,7 +6644,7 @@ async function readRegistry() {
|
|
|
5999
6644
|
throw err;
|
|
6000
6645
|
}
|
|
6001
6646
|
const parsed = JSON.parse(raw);
|
|
6002
|
-
return
|
|
6647
|
+
return import_types25.RegistryFileSchema.parse(parsed);
|
|
6003
6648
|
}
|
|
6004
6649
|
async function getProject(name) {
|
|
6005
6650
|
const reg = await readRegistry();
|
|
@@ -6200,6 +6845,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6200
6845
|
}
|
|
6201
6846
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6202
6847
|
});
|
|
6848
|
+
scope.get(
|
|
6849
|
+
"/graph/observed-dependencies/:nodeId",
|
|
6850
|
+
async (req, reply) => {
|
|
6851
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6852
|
+
if (!proj) return;
|
|
6853
|
+
const { nodeId } = req.params;
|
|
6854
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6855
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6856
|
+
}
|
|
6857
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
6858
|
+
}
|
|
6859
|
+
);
|
|
6203
6860
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6204
6861
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6205
6862
|
if (!proj) return;
|
|
@@ -6208,11 +6865,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6208
6865
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6209
6866
|
const parsed = [];
|
|
6210
6867
|
for (const c of candidates) {
|
|
6211
|
-
const r =
|
|
6868
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(c);
|
|
6212
6869
|
if (!r.success) {
|
|
6213
6870
|
return reply.code(400).send({
|
|
6214
6871
|
error: `unknown divergence type "${c}"`,
|
|
6215
|
-
allowed:
|
|
6872
|
+
allowed: import_types26.DivergenceTypeSchema.options
|
|
6216
6873
|
});
|
|
6217
6874
|
}
|
|
6218
6875
|
parsed.push(r.data);
|
|
@@ -6260,23 +6917,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6260
6917
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6261
6918
|
return { count: sliced.length, total, events: sliced };
|
|
6262
6919
|
});
|
|
6920
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
6921
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6922
|
+
if (!proj) return;
|
|
6923
|
+
const { nodeId } = req.params;
|
|
6924
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6925
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6926
|
+
return;
|
|
6927
|
+
}
|
|
6928
|
+
const epath = errorsPathFor(proj);
|
|
6929
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6930
|
+
const events = await readErrorEvents(epath);
|
|
6931
|
+
const filtered = events.filter(
|
|
6932
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6933
|
+
);
|
|
6934
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6935
|
+
};
|
|
6263
6936
|
scope.get(
|
|
6264
6937
|
"/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
|
-
}
|
|
6938
|
+
incidentHistoryHandler
|
|
6939
|
+
);
|
|
6940
|
+
scope.get(
|
|
6941
|
+
"/graph/incident-history/:nodeId",
|
|
6942
|
+
incidentHistoryHandler
|
|
6280
6943
|
);
|
|
6281
6944
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6282
6945
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6285,16 +6948,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6285
6948
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6286
6949
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6287
6950
|
}
|
|
6288
|
-
let errorEvent;
|
|
6289
6951
|
const epath = errorsPathFor(proj);
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6952
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
6953
|
+
let errorEvent;
|
|
6954
|
+
if (req.query.errorId) {
|
|
6955
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6293
6956
|
if (!errorEvent) {
|
|
6294
6957
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6295
6958
|
}
|
|
6296
6959
|
}
|
|
6297
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
6960
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6298
6961
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6299
6962
|
return result;
|
|
6300
6963
|
});
|
|
@@ -6425,7 +7088,7 @@ function registerRoutes(scope, ctx) {
|
|
|
6425
7088
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
6426
7089
|
let violations = await log.readAll();
|
|
6427
7090
|
if (req.query.severity) {
|
|
6428
|
-
const sev =
|
|
7091
|
+
const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
|
|
6429
7092
|
if (!sev.success) {
|
|
6430
7093
|
return reply.code(400).send({
|
|
6431
7094
|
error: "invalid severity",
|
|
@@ -6439,10 +7102,32 @@ function registerRoutes(scope, ctx) {
|
|
|
6439
7102
|
}
|
|
6440
7103
|
return { violations };
|
|
6441
7104
|
});
|
|
7105
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7106
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7107
|
+
if (!proj) return;
|
|
7108
|
+
const nodeId = req.query.node;
|
|
7109
|
+
if (!nodeId) {
|
|
7110
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7111
|
+
}
|
|
7112
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7113
|
+
let policies = [];
|
|
7114
|
+
if (policyPath) {
|
|
7115
|
+
try {
|
|
7116
|
+
policies = await loadPolicyFile(policyPath);
|
|
7117
|
+
} catch (err) {
|
|
7118
|
+
return reply.code(400).send({
|
|
7119
|
+
error: "policy.json failed to parse",
|
|
7120
|
+
details: err.message
|
|
7121
|
+
});
|
|
7122
|
+
}
|
|
7123
|
+
}
|
|
7124
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7125
|
+
return { node: nodeId, applicable };
|
|
7126
|
+
});
|
|
6442
7127
|
scope.post("/policies/check", async (req, reply) => {
|
|
6443
7128
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6444
7129
|
if (!proj) return;
|
|
6445
|
-
const parsed =
|
|
7130
|
+
const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
6446
7131
|
if (!parsed.success) {
|
|
6447
7132
|
return reply.code(400).send({
|
|
6448
7133
|
error: "invalid /policies/check body",
|