@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountBearerAuth,
|
|
3
3
|
readAuthEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-ZX7PCMGZ.js";
|
|
5
5
|
|
|
6
6
|
// src/graph.ts
|
|
7
7
|
import GraphDefault from "graphology";
|
|
@@ -344,7 +344,9 @@ import {
|
|
|
344
344
|
BlastRadiusResultSchema,
|
|
345
345
|
EdgeType,
|
|
346
346
|
NodeType,
|
|
347
|
+
ObservedDependenciesResultSchema,
|
|
347
348
|
PROV_RANK,
|
|
349
|
+
Provenance,
|
|
348
350
|
RootCauseResultSchema,
|
|
349
351
|
TransitiveDependenciesResultSchema
|
|
350
352
|
} from "@neat.is/types";
|
|
@@ -552,22 +554,164 @@ var rootCauseShapes = {
|
|
|
552
554
|
[NodeType.ServiceNode]: serviceRootCauseShape,
|
|
553
555
|
[NodeType.FileNode]: fileRootCauseShape
|
|
554
556
|
};
|
|
555
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
557
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
556
558
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
557
559
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
558
560
|
const shape = rootCauseShapes[origin.type];
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
561
|
+
if (shape) {
|
|
562
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
563
|
+
const match = shape(graph, origin, walk);
|
|
564
|
+
if (match) {
|
|
565
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
566
|
+
return RootCauseResultSchema.parse({
|
|
567
|
+
rootCauseNode: match.rootCauseNode,
|
|
568
|
+
rootCauseReason: reason,
|
|
569
|
+
traversalPath: walk.path,
|
|
570
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
571
|
+
confidence: confidenceFromMix(walk.edges),
|
|
572
|
+
fixRecommendation: match.fixRecommendation
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (origin.type === NodeType.ServiceNode) {
|
|
577
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
578
|
+
if (crossService) return crossService;
|
|
579
|
+
}
|
|
580
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
581
|
+
}
|
|
582
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
583
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
584
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
585
|
+
}
|
|
586
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
587
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
588
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
589
|
+
if (relevant.length === 0) return null;
|
|
590
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
591
|
+
const attrs = latest.attributes ?? {};
|
|
592
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
593
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
594
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
595
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
596
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
597
|
+
const count = sameMode.length;
|
|
598
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
599
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
600
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
601
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
602
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
603
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
604
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
605
|
+
return {
|
|
606
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
607
|
+
rootCauseReason,
|
|
608
|
+
...fileNode ? { fileNode } : {},
|
|
609
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
613
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
614
|
+
if (!loc) return null;
|
|
615
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
616
|
+
const edgeProvenances = loc.fileNode ? [Provenance.OBSERVED] : [];
|
|
564
617
|
return RootCauseResultSchema.parse({
|
|
565
|
-
rootCauseNode:
|
|
566
|
-
rootCauseReason:
|
|
567
|
-
traversalPath
|
|
568
|
-
edgeProvenances
|
|
569
|
-
confidence:
|
|
570
|
-
fixRecommendation:
|
|
618
|
+
rootCauseNode: loc.rootCauseNode,
|
|
619
|
+
rootCauseReason: loc.rootCauseReason,
|
|
620
|
+
traversalPath,
|
|
621
|
+
edgeProvenances,
|
|
622
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
623
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function isFailingCallEdge(e) {
|
|
627
|
+
return e.type === EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
628
|
+
}
|
|
629
|
+
function callSourcesForService(graph, serviceId3) {
|
|
630
|
+
const ids = [serviceId3];
|
|
631
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
632
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
633
|
+
if (e.type !== EdgeType.CONTAINS) continue;
|
|
634
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
635
|
+
if (tgt.type === NodeType.FileNode) ids.push(e.target);
|
|
636
|
+
}
|
|
637
|
+
return ids;
|
|
638
|
+
}
|
|
639
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
640
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
641
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
642
|
+
if (ec !== cc) return ec > cc;
|
|
643
|
+
if (PROV_RANK[e.provenance] !== PROV_RANK[curEdge.provenance]) {
|
|
644
|
+
return PROV_RANK[e.provenance] > PROV_RANK[curEdge.provenance];
|
|
645
|
+
}
|
|
646
|
+
return id < curId;
|
|
647
|
+
}
|
|
648
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
649
|
+
let best = null;
|
|
650
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
651
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
652
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
653
|
+
if (!isFailingCallEdge(e)) continue;
|
|
654
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
655
|
+
const owner = resolveOwningService(graph, e.target);
|
|
656
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
657
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
658
|
+
best = { nextService: owner.id, edge: e };
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return best;
|
|
663
|
+
}
|
|
664
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
665
|
+
const path39 = [originServiceId];
|
|
666
|
+
const edges = [];
|
|
667
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
668
|
+
let current = originServiceId;
|
|
669
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
670
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
671
|
+
if (!hop) break;
|
|
672
|
+
path39.push(hop.nextService);
|
|
673
|
+
edges.push(hop.edge);
|
|
674
|
+
visited.add(hop.nextService);
|
|
675
|
+
current = hop.nextService;
|
|
676
|
+
}
|
|
677
|
+
if (edges.length === 0) return null;
|
|
678
|
+
return { path: path39, edges, culprit: current };
|
|
679
|
+
}
|
|
680
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
681
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
682
|
+
if (!chain) return null;
|
|
683
|
+
const culprit = chain.culprit;
|
|
684
|
+
const path39 = [...chain.path];
|
|
685
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
686
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
687
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
688
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
689
|
+
if (loc) {
|
|
690
|
+
let rootCauseNode = culprit;
|
|
691
|
+
if (loc.fileNode) {
|
|
692
|
+
path39.push(loc.fileNode);
|
|
693
|
+
edgeProvenances.push(Provenance.OBSERVED);
|
|
694
|
+
rootCauseNode = loc.fileNode;
|
|
695
|
+
}
|
|
696
|
+
return RootCauseResultSchema.parse({
|
|
697
|
+
rootCauseNode,
|
|
698
|
+
rootCauseReason: loc.rootCauseReason,
|
|
699
|
+
traversalPath: path39,
|
|
700
|
+
edgeProvenances,
|
|
701
|
+
confidence,
|
|
702
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
706
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
707
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
708
|
+
return RootCauseResultSchema.parse({
|
|
709
|
+
rootCauseNode: culprit,
|
|
710
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
711
|
+
traversalPath: path39,
|
|
712
|
+
edgeProvenances,
|
|
713
|
+
confidence,
|
|
714
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
571
715
|
});
|
|
572
716
|
}
|
|
573
717
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -590,14 +734,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
590
734
|
});
|
|
591
735
|
}
|
|
592
736
|
if (frame.distance >= maxDepth) continue;
|
|
593
|
-
const
|
|
594
|
-
for (const [
|
|
595
|
-
if (enqueued.has(
|
|
596
|
-
enqueued.add(
|
|
737
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
738
|
+
for (const [srcId, edge] of incoming) {
|
|
739
|
+
if (enqueued.has(srcId)) continue;
|
|
740
|
+
enqueued.add(srcId);
|
|
597
741
|
queue.push({
|
|
598
|
-
nodeId:
|
|
742
|
+
nodeId: srcId,
|
|
599
743
|
distance: frame.distance + 1,
|
|
600
|
-
path: [...frame.path,
|
|
744
|
+
path: [...frame.path, srcId],
|
|
601
745
|
pathEdges: [...frame.pathEdges, edge]
|
|
602
746
|
});
|
|
603
747
|
}
|
|
@@ -653,6 +797,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
653
797
|
total: dependencies.length
|
|
654
798
|
});
|
|
655
799
|
}
|
|
800
|
+
function getObservedDependencies(graph, nodeId) {
|
|
801
|
+
if (!graph.hasNode(nodeId)) {
|
|
802
|
+
return ObservedDependenciesResultSchema.parse({
|
|
803
|
+
origin: nodeId,
|
|
804
|
+
dependencies: [],
|
|
805
|
+
observed: false,
|
|
806
|
+
inboundObservedCount: 0,
|
|
807
|
+
hasExtractedOutbound: false
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
811
|
+
const scope = [nodeId];
|
|
812
|
+
if (attrs.type === NodeType.ServiceNode) {
|
|
813
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
814
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
815
|
+
if (e.type !== EdgeType.CONTAINS) continue;
|
|
816
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
817
|
+
if (owned.type === NodeType.FileNode) scope.push(e.target);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const dependencies = [];
|
|
821
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
822
|
+
let hasExtractedOutbound = false;
|
|
823
|
+
for (const src of scope) {
|
|
824
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
825
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
826
|
+
if (e.type === EdgeType.CONTAINS) continue;
|
|
827
|
+
if (e.provenance === Provenance.OBSERVED) {
|
|
828
|
+
if (!seenEdge.has(e.id)) {
|
|
829
|
+
seenEdge.add(e.id);
|
|
830
|
+
dependencies.push(e);
|
|
831
|
+
}
|
|
832
|
+
} else if (e.provenance === Provenance.EXTRACTED) {
|
|
833
|
+
hasExtractedOutbound = true;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
let inboundObservedCount = 0;
|
|
838
|
+
for (const tgt of scope) {
|
|
839
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
840
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
841
|
+
if (e.type === EdgeType.CONTAINS) continue;
|
|
842
|
+
if (e.provenance === Provenance.OBSERVED) inboundObservedCount += 1;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
dependencies.sort(
|
|
846
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
847
|
+
);
|
|
848
|
+
return ObservedDependenciesResultSchema.parse({
|
|
849
|
+
origin: nodeId,
|
|
850
|
+
dependencies,
|
|
851
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
852
|
+
inboundObservedCount,
|
|
853
|
+
hasExtractedOutbound
|
|
854
|
+
});
|
|
855
|
+
}
|
|
656
856
|
|
|
657
857
|
// src/ingest.ts
|
|
658
858
|
import { promises as fs3, existsSync, readFileSync } from "fs";
|
|
@@ -997,6 +1197,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
997
1197
|
}
|
|
998
1198
|
return out;
|
|
999
1199
|
}
|
|
1200
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1201
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1202
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1203
|
+
const out = [];
|
|
1204
|
+
for (const policy of policies) {
|
|
1205
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1206
|
+
if (!m) continue;
|
|
1207
|
+
out.push({
|
|
1208
|
+
policyId: policy.id,
|
|
1209
|
+
policyName: policy.name,
|
|
1210
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1211
|
+
severity: policy.severity,
|
|
1212
|
+
onViolation: resolveOnViolation(policy),
|
|
1213
|
+
ruleType: policy.rule.type,
|
|
1214
|
+
match: m.match,
|
|
1215
|
+
reason: m.reason
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return out;
|
|
1219
|
+
}
|
|
1220
|
+
function requiredProvenanceList(required) {
|
|
1221
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1222
|
+
return String(required);
|
|
1223
|
+
}
|
|
1224
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1225
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1226
|
+
for (const edgeId of incident) {
|
|
1227
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1228
|
+
if (e.type !== edgeType) continue;
|
|
1229
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1230
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1231
|
+
}
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1235
|
+
let found = null;
|
|
1236
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1237
|
+
if (found !== null) return;
|
|
1238
|
+
if (subjId === nodeId) return;
|
|
1239
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1240
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1241
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1242
|
+
});
|
|
1243
|
+
return found;
|
|
1244
|
+
}
|
|
1245
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1246
|
+
const rule = policy.rule;
|
|
1247
|
+
switch (rule.type) {
|
|
1248
|
+
case "structural": {
|
|
1249
|
+
if (node.type === rule.fromNodeType) {
|
|
1250
|
+
return {
|
|
1251
|
+
match: "subject",
|
|
1252
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
if (node.type === rule.toNodeType) {
|
|
1256
|
+
return {
|
|
1257
|
+
match: "region",
|
|
1258
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
case "ownership": {
|
|
1264
|
+
if (node.type === rule.nodeType) {
|
|
1265
|
+
return {
|
|
1266
|
+
match: "subject",
|
|
1267
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
return null;
|
|
1271
|
+
}
|
|
1272
|
+
case "blast-radius": {
|
|
1273
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1274
|
+
if (node.type === rule.nodeType) {
|
|
1275
|
+
return {
|
|
1276
|
+
match: "subject",
|
|
1277
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1281
|
+
if (subject) {
|
|
1282
|
+
return {
|
|
1283
|
+
match: "region",
|
|
1284
|
+
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`
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
case "compatibility": {
|
|
1290
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1291
|
+
if (node.type === NodeType2.ServiceNode) {
|
|
1292
|
+
return {
|
|
1293
|
+
match: "subject",
|
|
1294
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1298
|
+
if (reachesDriverEngine && node.type === NodeType2.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType2.CONNECTS_TO)) {
|
|
1299
|
+
return {
|
|
1300
|
+
match: "region",
|
|
1301
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
case "provenance": {
|
|
1307
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1308
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1309
|
+
return {
|
|
1310
|
+
match: "subject",
|
|
1311
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1315
|
+
return {
|
|
1316
|
+
match: "region",
|
|
1317
|
+
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`
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
return null;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1000
1324
|
async function loadPolicyFile(policyPath) {
|
|
1001
1325
|
let raw;
|
|
1002
1326
|
try {
|
|
@@ -1050,7 +1374,7 @@ var PolicyViolationsLog = class {
|
|
|
1050
1374
|
import {
|
|
1051
1375
|
EdgeType as EdgeType3,
|
|
1052
1376
|
NodeType as NodeType3,
|
|
1053
|
-
Provenance,
|
|
1377
|
+
Provenance as Provenance2,
|
|
1054
1378
|
confidenceForObservedSignal,
|
|
1055
1379
|
databaseId,
|
|
1056
1380
|
extractedEdgeId,
|
|
@@ -1117,9 +1441,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1117
1441
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1118
1442
|
}
|
|
1119
1443
|
}
|
|
1120
|
-
function
|
|
1444
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1121
1445
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1122
|
-
const v =
|
|
1446
|
+
const v = attrs[key];
|
|
1123
1447
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1124
1448
|
if (typeof v === "string") {
|
|
1125
1449
|
const n = Number(v);
|
|
@@ -1128,6 +1452,63 @@ function httpResponseStatus(span) {
|
|
|
1128
1452
|
}
|
|
1129
1453
|
return void 0;
|
|
1130
1454
|
}
|
|
1455
|
+
function httpResponseStatus(span) {
|
|
1456
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
1457
|
+
}
|
|
1458
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
1459
|
+
const status = httpResponseStatusFromAttrs(attrs);
|
|
1460
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
1461
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
1462
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
1463
|
+
if (status !== void 0 && where) return `${status} on ${where}`;
|
|
1464
|
+
if (status !== void 0) return `HTTP ${status}`;
|
|
1465
|
+
if (where) return `error on ${where}`;
|
|
1466
|
+
return void 0;
|
|
1467
|
+
}
|
|
1468
|
+
var GRPC_STATUS_NAMES = {
|
|
1469
|
+
1: "CANCELLED",
|
|
1470
|
+
2: "UNKNOWN",
|
|
1471
|
+
3: "INVALID_ARGUMENT",
|
|
1472
|
+
4: "DEADLINE_EXCEEDED",
|
|
1473
|
+
5: "NOT_FOUND",
|
|
1474
|
+
6: "ALREADY_EXISTS",
|
|
1475
|
+
7: "PERMISSION_DENIED",
|
|
1476
|
+
8: "RESOURCE_EXHAUSTED",
|
|
1477
|
+
9: "FAILED_PRECONDITION",
|
|
1478
|
+
10: "ABORTED",
|
|
1479
|
+
11: "OUT_OF_RANGE",
|
|
1480
|
+
12: "UNIMPLEMENTED",
|
|
1481
|
+
13: "INTERNAL",
|
|
1482
|
+
14: "UNAVAILABLE",
|
|
1483
|
+
15: "DATA_LOSS",
|
|
1484
|
+
16: "UNAUTHENTICATED"
|
|
1485
|
+
};
|
|
1486
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
1487
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
1488
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1489
|
+
if (typeof v === "string") {
|
|
1490
|
+
const n = Number(v);
|
|
1491
|
+
if (Number.isFinite(n)) return n;
|
|
1492
|
+
}
|
|
1493
|
+
return void 0;
|
|
1494
|
+
}
|
|
1495
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
1496
|
+
const grpc = grpcStatusCodeFromAttrs(attrs);
|
|
1497
|
+
if (grpc !== void 0 && grpc !== 0) {
|
|
1498
|
+
const name = GRPC_STATUS_NAMES[grpc] ?? `status ${grpc}`;
|
|
1499
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
1500
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
1501
|
+
}
|
|
1502
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
1503
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
1504
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
1505
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
1506
|
+
}
|
|
1507
|
+
return void 0;
|
|
1508
|
+
}
|
|
1509
|
+
function incidentMessage(span) {
|
|
1510
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
1511
|
+
}
|
|
1131
1512
|
function nowIso(ctx) {
|
|
1132
1513
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1133
1514
|
}
|
|
@@ -1147,13 +1528,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1147
1528
|
`[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.`
|
|
1148
1529
|
);
|
|
1149
1530
|
}
|
|
1150
|
-
function
|
|
1531
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1151
1532
|
for (const k of keys) {
|
|
1152
|
-
const v =
|
|
1533
|
+
const v = attrs[k];
|
|
1153
1534
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1154
1535
|
}
|
|
1155
1536
|
return void 0;
|
|
1156
1537
|
}
|
|
1538
|
+
function pickAttr(span, ...keys) {
|
|
1539
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
1540
|
+
}
|
|
1157
1541
|
function hostFromUrl(u) {
|
|
1158
1542
|
if (!u) return void 0;
|
|
1159
1543
|
try {
|
|
@@ -1165,6 +1549,10 @@ function hostFromUrl(u) {
|
|
|
1165
1549
|
function pickAddress(span) {
|
|
1166
1550
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1167
1551
|
}
|
|
1552
|
+
function isLoopbackHost(host) {
|
|
1553
|
+
const h = host.toLowerCase();
|
|
1554
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
1555
|
+
}
|
|
1168
1556
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1169
1557
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1170
1558
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1272,15 +1660,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1272
1660
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1273
1661
|
};
|
|
1274
1662
|
}
|
|
1663
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
1664
|
+
if (graph.hasNode(fileId(serviceName, relPath))) return relPath;
|
|
1665
|
+
let best = null;
|
|
1666
|
+
graph.forEachNode((_id, attrs) => {
|
|
1667
|
+
const a = attrs;
|
|
1668
|
+
if (a.type !== NodeType3.FileNode || a.service !== serviceName) return;
|
|
1669
|
+
if (a.discoveredVia === "otel") return;
|
|
1670
|
+
const p = a.path;
|
|
1671
|
+
if (!p) return;
|
|
1672
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
1673
|
+
best = p;
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
return best ?? relPath;
|
|
1677
|
+
}
|
|
1275
1678
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1276
|
-
const
|
|
1679
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
1680
|
+
const fileNodeId = fileId(serviceName, relPath);
|
|
1277
1681
|
if (!graph.hasNode(fileNodeId)) {
|
|
1278
|
-
const language = languageForExt(
|
|
1682
|
+
const language = languageForExt(relPath);
|
|
1279
1683
|
const node = {
|
|
1280
1684
|
id: fileNodeId,
|
|
1281
1685
|
type: NodeType3.FileNode,
|
|
1282
1686
|
service: serviceName,
|
|
1283
|
-
path:
|
|
1687
|
+
path: relPath,
|
|
1284
1688
|
...language ? { language } : {},
|
|
1285
1689
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1286
1690
|
discoveredVia: "otel"
|
|
@@ -1294,7 +1698,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1294
1698
|
source: serviceNodeId,
|
|
1295
1699
|
target: fileNodeId,
|
|
1296
1700
|
type: EdgeType3.CONTAINS,
|
|
1297
|
-
provenance:
|
|
1701
|
+
provenance: Provenance2.OBSERVED
|
|
1298
1702
|
};
|
|
1299
1703
|
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
1300
1704
|
}
|
|
@@ -1308,6 +1712,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1308
1712
|
}
|
|
1309
1713
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1310
1714
|
var STITCH_MAX_DEPTH = 2;
|
|
1715
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1716
|
+
EdgeType3.CALLS,
|
|
1717
|
+
EdgeType3.CONNECTS_TO,
|
|
1718
|
+
EdgeType3.DEPENDS_ON
|
|
1719
|
+
]);
|
|
1311
1720
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1312
1721
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1313
1722
|
function spanMintsObservedEdge(kind) {
|
|
@@ -1439,7 +1848,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
1439
1848
|
};
|
|
1440
1849
|
const updated = {
|
|
1441
1850
|
...existing,
|
|
1442
|
-
provenance:
|
|
1851
|
+
provenance: Provenance2.OBSERVED,
|
|
1443
1852
|
lastObserved: ts,
|
|
1444
1853
|
callCount: newSpanCount,
|
|
1445
1854
|
signal: newSignal,
|
|
@@ -1458,7 +1867,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
1458
1867
|
source,
|
|
1459
1868
|
target,
|
|
1460
1869
|
type,
|
|
1461
|
-
provenance:
|
|
1870
|
+
provenance: Provenance2.OBSERVED,
|
|
1462
1871
|
confidence: confidenceForObservedSignal(signal),
|
|
1463
1872
|
lastObserved: ts,
|
|
1464
1873
|
callCount: 1,
|
|
@@ -1480,7 +1889,8 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
1480
1889
|
const outbound = graph.outboundEdges(nodeId);
|
|
1481
1890
|
for (const edgeId of outbound) {
|
|
1482
1891
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
1483
|
-
if (edge.provenance !==
|
|
1892
|
+
if (edge.provenance !== Provenance2.EXTRACTED) continue;
|
|
1893
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
1484
1894
|
if (graph.hasEdge(observedEdgeId(edge.source, edge.target, edge.type))) continue;
|
|
1485
1895
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
1486
1896
|
if (!visited.has(edge.target)) {
|
|
@@ -1503,7 +1913,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
|
|
|
1503
1913
|
source,
|
|
1504
1914
|
target,
|
|
1505
1915
|
type,
|
|
1506
|
-
provenance:
|
|
1916
|
+
provenance: Provenance2.INFERRED,
|
|
1507
1917
|
confidence: INFERRED_CONFIDENCE,
|
|
1508
1918
|
lastObserved: ts
|
|
1509
1919
|
};
|
|
@@ -1513,6 +1923,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
1513
1923
|
await fs3.mkdir(path3.dirname(ctx.errorsPath), { recursive: true });
|
|
1514
1924
|
await fs3.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1515
1925
|
}
|
|
1926
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
1927
|
+
const sid = serviceId(span.service, span.env);
|
|
1928
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
1929
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
1930
|
+
if (callSite) {
|
|
1931
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
1932
|
+
return fileId(span.service, relPath);
|
|
1933
|
+
}
|
|
1934
|
+
return sid;
|
|
1935
|
+
}
|
|
1516
1936
|
function sanitizeAttributes(attrs) {
|
|
1517
1937
|
const out = {};
|
|
1518
1938
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -1521,7 +1941,7 @@ function sanitizeAttributes(attrs) {
|
|
|
1521
1941
|
}
|
|
1522
1942
|
return out;
|
|
1523
1943
|
}
|
|
1524
|
-
function buildErrorEventForReceiver(span) {
|
|
1944
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
1525
1945
|
if (span.statusCode !== 2) return null;
|
|
1526
1946
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1527
1947
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -1531,16 +1951,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
1531
1951
|
service: span.service,
|
|
1532
1952
|
traceId: span.traceId,
|
|
1533
1953
|
spanId: span.spanId,
|
|
1534
|
-
errorMessage: span
|
|
1954
|
+
errorMessage: incidentMessage(span),
|
|
1535
1955
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1536
1956
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1537
1957
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1538
|
-
affectedNode:
|
|
1958
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
1539
1959
|
};
|
|
1540
1960
|
}
|
|
1541
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
1961
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
1542
1962
|
return async (span) => {
|
|
1543
|
-
const ev = buildErrorEventForReceiver(span);
|
|
1963
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
1544
1964
|
if (!ev) return;
|
|
1545
1965
|
await fs3.mkdir(path3.dirname(errorsPath), { recursive: true });
|
|
1546
1966
|
await fs3.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -1568,6 +1988,22 @@ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp,
|
|
|
1568
1988
|
};
|
|
1569
1989
|
await appendErrorEvent(ctx, ev);
|
|
1570
1990
|
}
|
|
1991
|
+
async function recordExceptionIncident(ctx, span, ts) {
|
|
1992
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1993
|
+
const ev = {
|
|
1994
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
1995
|
+
timestamp: ts,
|
|
1996
|
+
service: span.service,
|
|
1997
|
+
traceId: span.traceId,
|
|
1998
|
+
spanId: span.spanId,
|
|
1999
|
+
errorMessage: incidentMessage(span),
|
|
2000
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2001
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2002
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2003
|
+
affectedNode: incidentAffectedNode(span, ctx.graph, ctx.scanPath)
|
|
2004
|
+
};
|
|
2005
|
+
await appendErrorEvent(ctx, ev);
|
|
2006
|
+
}
|
|
1571
2007
|
async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status) {
|
|
1572
2008
|
const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
|
|
1573
2009
|
if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
|
|
@@ -1624,7 +2060,10 @@ async function handleSpan(ctx, span) {
|
|
|
1624
2060
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1625
2061
|
cacheSpanService(span, nowMs, callSite);
|
|
1626
2062
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1627
|
-
const callSiteEvidence = callSite ? {
|
|
2063
|
+
const callSiteEvidence = callSite ? {
|
|
2064
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2065
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2066
|
+
} : void 0;
|
|
1628
2067
|
let affectedNode = sourceId;
|
|
1629
2068
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1630
2069
|
if (span.dbSystem) {
|
|
@@ -1646,7 +2085,7 @@ async function handleSpan(ctx, span) {
|
|
|
1646
2085
|
} else {
|
|
1647
2086
|
const host = pickAddress(span);
|
|
1648
2087
|
let resolvedViaAddress = false;
|
|
1649
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2088
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
1650
2089
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1651
2090
|
if (targetId && targetId !== sourceId) {
|
|
1652
2091
|
upsertObservedEdge(
|
|
@@ -1681,7 +2120,11 @@ async function handleSpan(ctx, span) {
|
|
|
1681
2120
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1682
2121
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
1683
2122
|
const fallbackEvidence = parent.callSite ? {
|
|
1684
|
-
file:
|
|
2123
|
+
file: reconcileObservedRelPath(
|
|
2124
|
+
ctx.graph,
|
|
2125
|
+
parent.service,
|
|
2126
|
+
parent.callSite.relPath
|
|
2127
|
+
),
|
|
1685
2128
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
1686
2129
|
} : void 0;
|
|
1687
2130
|
upsertObservedEdge(
|
|
@@ -1706,7 +2149,7 @@ async function handleSpan(ctx, span) {
|
|
|
1706
2149
|
service: span.service,
|
|
1707
2150
|
traceId: span.traceId,
|
|
1708
2151
|
spanId: span.spanId,
|
|
1709
|
-
errorMessage: span
|
|
2152
|
+
errorMessage: incidentMessage(span),
|
|
1710
2153
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1711
2154
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1712
2155
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -1717,7 +2160,9 @@ async function handleSpan(ctx, span) {
|
|
|
1717
2160
|
}
|
|
1718
2161
|
if (span.statusCode !== 2) {
|
|
1719
2162
|
const status = httpResponseStatus(span);
|
|
1720
|
-
if (
|
|
2163
|
+
if (span.exception) {
|
|
2164
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
2165
|
+
} else if (status !== void 0 && status >= 500) {
|
|
1721
2166
|
await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
|
|
1722
2167
|
} else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
1723
2168
|
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
|
|
@@ -1773,7 +2218,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
|
1773
2218
|
}
|
|
1774
2219
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1775
2220
|
graph.dropEdge(oldEdgeId);
|
|
1776
|
-
const newId = edge.provenance ===
|
|
2221
|
+
const newId = edge.provenance === Provenance2.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance2.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
|
|
1777
2222
|
if (graph.hasEdge(newId)) {
|
|
1778
2223
|
const existing = graph.getEdgeAttributes(newId);
|
|
1779
2224
|
const merged = {
|
|
@@ -1807,12 +2252,12 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1807
2252
|
const project = options.project ?? DEFAULT_PROJECT;
|
|
1808
2253
|
graph.forEachEdge((id, attrs) => {
|
|
1809
2254
|
const e = attrs;
|
|
1810
|
-
if (e.provenance !==
|
|
2255
|
+
if (e.provenance !== Provenance2.OBSERVED) return;
|
|
1811
2256
|
if (!e.lastObserved) return;
|
|
1812
2257
|
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
1813
2258
|
const age = now - new Date(e.lastObserved).getTime();
|
|
1814
2259
|
if (age > threshold) {
|
|
1815
|
-
const updated = { ...e, provenance:
|
|
2260
|
+
const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
|
|
1816
2261
|
graph.replaceEdgeAttributes(id, updated);
|
|
1817
2262
|
events.push({
|
|
1818
2263
|
edgeId: id,
|
|
@@ -1829,8 +2274,8 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1829
2274
|
project,
|
|
1830
2275
|
payload: {
|
|
1831
2276
|
edgeId: id,
|
|
1832
|
-
from:
|
|
1833
|
-
to:
|
|
2277
|
+
from: Provenance2.OBSERVED,
|
|
2278
|
+
to: Provenance2.STALE
|
|
1834
2279
|
}
|
|
1835
2280
|
});
|
|
1836
2281
|
}
|
|
@@ -1882,12 +2327,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1882
2327
|
async function readErrorEvents(errorsPath) {
|
|
1883
2328
|
try {
|
|
1884
2329
|
const raw = await fs3.readFile(errorsPath, "utf8");
|
|
1885
|
-
|
|
2330
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2331
|
+
return dedupeIncidents(events);
|
|
1886
2332
|
} catch (err) {
|
|
1887
2333
|
if (err.code === "ENOENT") return [];
|
|
1888
2334
|
throw err;
|
|
1889
2335
|
}
|
|
1890
2336
|
}
|
|
2337
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2338
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2339
|
+
if (ev.errorType) return false;
|
|
2340
|
+
if (!ev.attributes) return false;
|
|
2341
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2342
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2343
|
+
}
|
|
2344
|
+
function dedupeIncidents(events) {
|
|
2345
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2346
|
+
const once = [];
|
|
2347
|
+
for (const ev of events) {
|
|
2348
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2349
|
+
if (key === void 0) {
|
|
2350
|
+
once.push(ev);
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
if (seen.has(key)) continue;
|
|
2354
|
+
seen.add(key);
|
|
2355
|
+
once.push(ev);
|
|
2356
|
+
}
|
|
2357
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2358
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2359
|
+
for (const ev of once) {
|
|
2360
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2361
|
+
}
|
|
2362
|
+
return once.filter((ev) => {
|
|
2363
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2364
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2365
|
+
});
|
|
2366
|
+
}
|
|
1891
2367
|
function mergeSnapshot(graph, snapshot) {
|
|
1892
2368
|
const exported = snapshot.graph;
|
|
1893
2369
|
let nodesAdded = 0;
|
|
@@ -2021,10 +2497,17 @@ function isConfigFile(name) {
|
|
|
2021
2497
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2022
2498
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2023
2499
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2500
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2024
2501
|
return { match: true, fileType: "env" };
|
|
2025
2502
|
}
|
|
2026
2503
|
return { match: false, fileType: "" };
|
|
2027
2504
|
}
|
|
2505
|
+
function isNeatAuthoredEnvFile(name) {
|
|
2506
|
+
return name === ".env.neat";
|
|
2507
|
+
}
|
|
2508
|
+
function isNeatAuthoredSourceFile(name) {
|
|
2509
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
2510
|
+
}
|
|
2028
2511
|
function isTestPath(filePath) {
|
|
2029
2512
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2030
2513
|
const segments = normalised.split("/");
|
|
@@ -2659,7 +3142,7 @@ import path10 from "path";
|
|
|
2659
3142
|
import {
|
|
2660
3143
|
EdgeType as EdgeType4,
|
|
2661
3144
|
NodeType as NodeType6,
|
|
2662
|
-
Provenance as
|
|
3145
|
+
Provenance as Provenance3,
|
|
2663
3146
|
confidenceForExtracted,
|
|
2664
3147
|
extractedEdgeId as extractedEdgeId3,
|
|
2665
3148
|
fileId as fileId2
|
|
@@ -2674,7 +3157,9 @@ async function walkSourceFiles(dir) {
|
|
|
2674
3157
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2675
3158
|
if (await isPythonVenvDir(full)) continue;
|
|
2676
3159
|
await walk(full);
|
|
2677
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))
|
|
3160
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3161
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3162
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
2678
3163
|
out.push(full);
|
|
2679
3164
|
}
|
|
2680
3165
|
}
|
|
@@ -2746,7 +3231,7 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
|
2746
3231
|
source: serviceNodeId,
|
|
2747
3232
|
target: fileNodeId,
|
|
2748
3233
|
type: EdgeType4.CONTAINS,
|
|
2749
|
-
provenance:
|
|
3234
|
+
provenance: Provenance3.EXTRACTED,
|
|
2750
3235
|
confidence: confidenceForExtracted("structural"),
|
|
2751
3236
|
evidence: { file: relPath }
|
|
2752
3237
|
};
|
|
@@ -2786,7 +3271,7 @@ import JavaScript from "tree-sitter-javascript";
|
|
|
2786
3271
|
import Python from "tree-sitter-python";
|
|
2787
3272
|
import {
|
|
2788
3273
|
EdgeType as EdgeType5,
|
|
2789
|
-
Provenance as
|
|
3274
|
+
Provenance as Provenance4,
|
|
2790
3275
|
confidenceForExtracted as confidenceForExtracted2,
|
|
2791
3276
|
extractedEdgeId as extractedEdgeId4,
|
|
2792
3277
|
fileId as fileId3
|
|
@@ -2849,11 +3334,28 @@ function collectJsImports(node, out) {
|
|
|
2849
3334
|
if (child) collectJsImports(child, out);
|
|
2850
3335
|
}
|
|
2851
3336
|
}
|
|
3337
|
+
function collectImportedNames(node, out) {
|
|
3338
|
+
if (node.type === "aliased_import") {
|
|
3339
|
+
const nameNode = node.childForFieldName("name");
|
|
3340
|
+
if (nameNode) out.push(nameNode.text);
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (node.type === "dotted_name") {
|
|
3344
|
+
out.push(node.text);
|
|
3345
|
+
return;
|
|
3346
|
+
}
|
|
3347
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3348
|
+
const child = node.namedChild(i);
|
|
3349
|
+
if (child) collectImportedNames(child, out);
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
2852
3352
|
function collectPyImports(node, out) {
|
|
2853
3353
|
if (node.type === "import_from_statement") {
|
|
2854
3354
|
let level = 0;
|
|
2855
3355
|
let modulePath = "";
|
|
3356
|
+
const names = [];
|
|
2856
3357
|
let pastFrom = false;
|
|
3358
|
+
let pastImport = false;
|
|
2857
3359
|
for (let i = 0; i < node.childCount; i++) {
|
|
2858
3360
|
const child = node.child(i);
|
|
2859
3361
|
if (!child) continue;
|
|
@@ -2861,26 +3363,30 @@ function collectPyImports(node, out) {
|
|
|
2861
3363
|
if (child.type === "from") pastFrom = true;
|
|
2862
3364
|
continue;
|
|
2863
3365
|
}
|
|
2864
|
-
if (
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
if (!rc) continue;
|
|
2869
|
-
if (rc.type === "import_prefix") {
|
|
2870
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
2871
|
-
if (rc.child(k)?.type === ".") level++;
|
|
2872
|
-
}
|
|
2873
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3366
|
+
if (!pastImport) {
|
|
3367
|
+
if (child.type === "import") {
|
|
3368
|
+
pastImport = true;
|
|
3369
|
+
continue;
|
|
2874
3370
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3371
|
+
if (child.type === "relative_import") {
|
|
3372
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3373
|
+
const rc = child.child(j);
|
|
3374
|
+
if (!rc) continue;
|
|
3375
|
+
if (rc.type === "import_prefix") {
|
|
3376
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
3377
|
+
if (rc.child(k)?.type === ".") level++;
|
|
3378
|
+
}
|
|
3379
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3380
|
+
}
|
|
3381
|
+
} else if (child.type === "dotted_name") {
|
|
3382
|
+
modulePath = child.text;
|
|
3383
|
+
}
|
|
3384
|
+
continue;
|
|
2880
3385
|
}
|
|
3386
|
+
collectImportedNames(child, names);
|
|
2881
3387
|
}
|
|
2882
3388
|
if (level > 0 || modulePath) {
|
|
2883
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3389
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2884
3390
|
}
|
|
2885
3391
|
}
|
|
2886
3392
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -2982,8 +3488,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
2982
3488
|
return null;
|
|
2983
3489
|
}
|
|
2984
3490
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
2985
|
-
if (!imp.modulePath) return null;
|
|
2986
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
2987
3491
|
let baseDir;
|
|
2988
3492
|
if (imp.level > 0) {
|
|
2989
3493
|
baseDir = path12.dirname(importerPath);
|
|
@@ -2991,13 +3495,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
2991
3495
|
} else {
|
|
2992
3496
|
baseDir = serviceDir;
|
|
2993
3497
|
}
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3498
|
+
const moduleBase = imp.modulePath ? path12.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
3499
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
3500
|
+
let needModuleFile = imp.names.length === 0;
|
|
3501
|
+
for (const name of imp.names) {
|
|
3502
|
+
const submoduleFile = path12.join(moduleBase, `${name}.py`);
|
|
3503
|
+
const subpackageInit = path12.join(moduleBase, name, "__init__.py");
|
|
3504
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
3505
|
+
resolved.add(toPosix2(path12.relative(serviceDir, submoduleFile)));
|
|
3506
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
3507
|
+
resolved.add(toPosix2(path12.relative(serviceDir, subpackageInit)));
|
|
3508
|
+
} else {
|
|
3509
|
+
needModuleFile = true;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
if (needModuleFile) {
|
|
3513
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, path12.join(moduleBase, "__init__.py")] : [path12.join(moduleBase, "__init__.py")];
|
|
3514
|
+
for (const candidate of moduleFileCandidates) {
|
|
3515
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
3516
|
+
resolved.add(toPosix2(path12.relative(serviceDir, candidate)));
|
|
3517
|
+
break;
|
|
3518
|
+
}
|
|
2998
3519
|
}
|
|
2999
3520
|
}
|
|
3000
|
-
return
|
|
3521
|
+
return [...resolved];
|
|
3001
3522
|
}
|
|
3002
3523
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3003
3524
|
const importeeFileId = fileId3(serviceName, importeeRelPath);
|
|
@@ -3009,7 +3530,7 @@ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, imp
|
|
|
3009
3530
|
source: importerFileId,
|
|
3010
3531
|
target: importeeFileId,
|
|
3011
3532
|
type: EdgeType5.IMPORTS,
|
|
3012
|
-
provenance:
|
|
3533
|
+
provenance: Provenance4.EXTRACTED,
|
|
3013
3534
|
confidence: confidenceForExtracted2("structural"),
|
|
3014
3535
|
evidence: { file: importerRelPath, line, snippet: snippet2 }
|
|
3015
3536
|
};
|
|
@@ -3038,17 +3559,18 @@ async function addImports(graph, services) {
|
|
|
3038
3559
|
continue;
|
|
3039
3560
|
}
|
|
3040
3561
|
for (const imp of pyImports) {
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3562
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
3563
|
+
for (const resolved of resolvedPaths) {
|
|
3564
|
+
edgesAdded += emitImportEdge(
|
|
3565
|
+
graph,
|
|
3566
|
+
service.pkg.name,
|
|
3567
|
+
importerFileId,
|
|
3568
|
+
relFile,
|
|
3569
|
+
resolved,
|
|
3570
|
+
imp.line,
|
|
3571
|
+
imp.snippet
|
|
3572
|
+
);
|
|
3573
|
+
}
|
|
3052
3574
|
}
|
|
3053
3575
|
continue;
|
|
3054
3576
|
}
|
|
@@ -3083,7 +3605,8 @@ import path20 from "path";
|
|
|
3083
3605
|
import {
|
|
3084
3606
|
EdgeType as EdgeType6,
|
|
3085
3607
|
NodeType as NodeType7,
|
|
3086
|
-
Provenance as
|
|
3608
|
+
Provenance as Provenance5,
|
|
3609
|
+
configId,
|
|
3087
3610
|
databaseId as databaseId2,
|
|
3088
3611
|
confidenceForExtracted as confidenceForExtracted3
|
|
3089
3612
|
} from "@neat.is/types";
|
|
@@ -3670,7 +4193,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3670
4193
|
source: fileNodeId,
|
|
3671
4194
|
target: dbNode.id,
|
|
3672
4195
|
type: EdgeType6.CONNECTS_TO,
|
|
3673
|
-
provenance:
|
|
4196
|
+
provenance: Provenance5.EXTRACTED,
|
|
3674
4197
|
confidence: confidenceForExtracted3("structural"),
|
|
3675
4198
|
evidence: { file: evidenceFile }
|
|
3676
4199
|
};
|
|
@@ -3679,6 +4202,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3679
4202
|
edgesAdded++;
|
|
3680
4203
|
}
|
|
3681
4204
|
}
|
|
4205
|
+
if (allConfigs.length === 1) {
|
|
4206
|
+
const primary = allConfigs[0];
|
|
4207
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4208
|
+
const relPath = path20.relative(scanPath, primary.sourceFile);
|
|
4209
|
+
const cfgId = configId(relPath);
|
|
4210
|
+
if (!graph.hasNode(cfgId)) {
|
|
4211
|
+
const cfgNode = {
|
|
4212
|
+
id: cfgId,
|
|
4213
|
+
type: NodeType7.ConfigNode,
|
|
4214
|
+
name: path20.basename(primary.sourceFile),
|
|
4215
|
+
path: relPath,
|
|
4216
|
+
fileType: isConfigFile(path20.basename(primary.sourceFile)).fileType || "config"
|
|
4217
|
+
};
|
|
4218
|
+
graph.addNode(cfgId, cfgNode);
|
|
4219
|
+
nodesAdded++;
|
|
4220
|
+
}
|
|
4221
|
+
const cfgEdge = {
|
|
4222
|
+
id: extractedEdgeId2(service.node.id, cfgId, EdgeType6.CONFIGURED_BY),
|
|
4223
|
+
source: service.node.id,
|
|
4224
|
+
target: cfgId,
|
|
4225
|
+
type: EdgeType6.CONFIGURED_BY,
|
|
4226
|
+
provenance: Provenance5.EXTRACTED,
|
|
4227
|
+
confidence: confidenceForExtracted3("structural"),
|
|
4228
|
+
evidence: { file: toPosix2(relPath) }
|
|
4229
|
+
};
|
|
4230
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4231
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4232
|
+
edgesAdded++;
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
3682
4235
|
attachIncompatibilities(service, allConfigs);
|
|
3683
4236
|
if (graph.hasNode(service.node.id)) {
|
|
3684
4237
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -3690,6 +4243,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3690
4243
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
3691
4244
|
delete updated.incompatibilities;
|
|
3692
4245
|
}
|
|
4246
|
+
if (!service.node.dbConnectionTarget) {
|
|
4247
|
+
delete updated.dbConnectionTarget;
|
|
4248
|
+
}
|
|
3693
4249
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
3694
4250
|
}
|
|
3695
4251
|
}
|
|
@@ -3702,8 +4258,8 @@ import path21 from "path";
|
|
|
3702
4258
|
import {
|
|
3703
4259
|
EdgeType as EdgeType7,
|
|
3704
4260
|
NodeType as NodeType8,
|
|
3705
|
-
Provenance as
|
|
3706
|
-
configId,
|
|
4261
|
+
Provenance as Provenance6,
|
|
4262
|
+
configId as configId2,
|
|
3707
4263
|
confidenceForExtracted as confidenceForExtracted4
|
|
3708
4264
|
} from "@neat.is/types";
|
|
3709
4265
|
async function walkConfigFiles(dir) {
|
|
@@ -3732,7 +4288,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3732
4288
|
for (const file of configFiles) {
|
|
3733
4289
|
const relPath = path21.relative(scanPath, file);
|
|
3734
4290
|
const node = {
|
|
3735
|
-
id:
|
|
4291
|
+
id: configId2(relPath),
|
|
3736
4292
|
type: NodeType8.ConfigNode,
|
|
3737
4293
|
name: path21.basename(file),
|
|
3738
4294
|
path: relPath,
|
|
@@ -3756,7 +4312,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3756
4312
|
source: fileNodeId,
|
|
3757
4313
|
target: node.id,
|
|
3758
4314
|
type: EdgeType7.CONFIGURED_BY,
|
|
3759
|
-
provenance:
|
|
4315
|
+
provenance: Provenance6.EXTRACTED,
|
|
3760
4316
|
confidence: confidenceForExtracted4("structural"),
|
|
3761
4317
|
evidence: { file: relPath.split(path21.sep).join("/") }
|
|
3762
4318
|
};
|
|
@@ -3773,7 +4329,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3773
4329
|
import {
|
|
3774
4330
|
EdgeType as EdgeType9,
|
|
3775
4331
|
NodeType as NodeType9,
|
|
3776
|
-
Provenance as
|
|
4332
|
+
Provenance as Provenance8,
|
|
3777
4333
|
confidenceForExtracted as confidenceForExtracted6,
|
|
3778
4334
|
passesExtractedFloor as passesExtractedFloor2
|
|
3779
4335
|
} from "@neat.is/types";
|
|
@@ -3785,7 +4341,7 @@ import JavaScript2 from "tree-sitter-javascript";
|
|
|
3785
4341
|
import Python2 from "tree-sitter-python";
|
|
3786
4342
|
import {
|
|
3787
4343
|
EdgeType as EdgeType8,
|
|
3788
|
-
Provenance as
|
|
4344
|
+
Provenance as Provenance7,
|
|
3789
4345
|
confidenceForExtracted as confidenceForExtracted5,
|
|
3790
4346
|
passesExtractedFloor
|
|
3791
4347
|
} from "@neat.is/types";
|
|
@@ -3881,7 +4437,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3881
4437
|
const dedupKey = `${relFile}|${targetId}`;
|
|
3882
4438
|
if (seen.has(dedupKey)) continue;
|
|
3883
4439
|
seen.add(dedupKey);
|
|
3884
|
-
const confidence = confidenceForExtracted5("
|
|
4440
|
+
const confidence = confidenceForExtracted5("url-literal-service-target");
|
|
3885
4441
|
const ev = {
|
|
3886
4442
|
file: relFile,
|
|
3887
4443
|
line: site.line,
|
|
@@ -3901,7 +4457,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3901
4457
|
target: targetId,
|
|
3902
4458
|
type: EdgeType8.CALLS,
|
|
3903
4459
|
confidence,
|
|
3904
|
-
confidenceKind: "
|
|
4460
|
+
confidenceKind: "url-literal-service-target",
|
|
3905
4461
|
evidence: ev
|
|
3906
4462
|
});
|
|
3907
4463
|
continue;
|
|
@@ -3913,7 +4469,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3913
4469
|
source: fileNodeId,
|
|
3914
4470
|
target: targetId,
|
|
3915
4471
|
type: EdgeType8.CALLS,
|
|
3916
|
-
provenance:
|
|
4472
|
+
provenance: Provenance7.EXTRACTED,
|
|
3917
4473
|
confidence,
|
|
3918
4474
|
evidence: ev
|
|
3919
4475
|
};
|
|
@@ -4266,7 +4822,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4266
4822
|
source: fileNodeId,
|
|
4267
4823
|
target: ep.infraId,
|
|
4268
4824
|
type: edgeType,
|
|
4269
|
-
provenance:
|
|
4825
|
+
provenance: Provenance8.EXTRACTED,
|
|
4270
4826
|
confidence,
|
|
4271
4827
|
evidence: ep.evidence
|
|
4272
4828
|
};
|
|
@@ -4288,7 +4844,7 @@ async function addCallEdges(graph, services) {
|
|
|
4288
4844
|
|
|
4289
4845
|
// src/extract/infra/docker-compose.ts
|
|
4290
4846
|
import path28 from "path";
|
|
4291
|
-
import { EdgeType as EdgeType10, Provenance as
|
|
4847
|
+
import { EdgeType as EdgeType10, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
|
|
4292
4848
|
|
|
4293
4849
|
// src/extract/infra/shared.ts
|
|
4294
4850
|
import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
|
|
@@ -4381,7 +4937,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4381
4937
|
source: sourceId,
|
|
4382
4938
|
target: targetId,
|
|
4383
4939
|
type: EdgeType10.DEPENDS_ON,
|
|
4384
|
-
provenance:
|
|
4940
|
+
provenance: Provenance9.EXTRACTED,
|
|
4385
4941
|
confidence: confidenceForExtracted7("structural"),
|
|
4386
4942
|
evidence: { file: evidenceFile }
|
|
4387
4943
|
};
|
|
@@ -4395,20 +4951,30 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4395
4951
|
// src/extract/infra/dockerfile.ts
|
|
4396
4952
|
import path29 from "path";
|
|
4397
4953
|
import { promises as fs15 } from "fs";
|
|
4398
|
-
import { EdgeType as EdgeType11, Provenance as
|
|
4399
|
-
function
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4954
|
+
import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
|
|
4955
|
+
function readDockerfile(content) {
|
|
4956
|
+
let image = null;
|
|
4957
|
+
const ports = [];
|
|
4958
|
+
let cmd = null;
|
|
4959
|
+
let entrypoint = null;
|
|
4960
|
+
for (const raw of content.split("\n")) {
|
|
4403
4961
|
const line = raw.trim();
|
|
4404
4962
|
if (!line || line.startsWith("#")) continue;
|
|
4405
|
-
if (
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
if (
|
|
4409
|
-
|
|
4963
|
+
if (/^from\s+/i.test(line)) {
|
|
4964
|
+
const candidate = line.split(/\s+/)[1];
|
|
4965
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
4966
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
4967
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
4968
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
4969
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
4970
|
+
}
|
|
4971
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
4972
|
+
entrypoint = line;
|
|
4973
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
4974
|
+
cmd = line;
|
|
4975
|
+
}
|
|
4410
4976
|
}
|
|
4411
|
-
return
|
|
4977
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
4412
4978
|
}
|
|
4413
4979
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
4414
4980
|
let nodesAdded = 0;
|
|
@@ -4427,14 +4993,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4427
4993
|
);
|
|
4428
4994
|
continue;
|
|
4429
4995
|
}
|
|
4430
|
-
const
|
|
4431
|
-
if (!image) continue;
|
|
4432
|
-
const node = makeInfraNode("container-image", image);
|
|
4996
|
+
const facts = readDockerfile(content);
|
|
4997
|
+
if (!facts.image) continue;
|
|
4998
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
4433
4999
|
if (!graph.hasNode(node.id)) {
|
|
4434
5000
|
graph.addNode(node.id, node);
|
|
4435
5001
|
nodesAdded++;
|
|
4436
5002
|
}
|
|
4437
5003
|
const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
|
|
5004
|
+
const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
|
|
4438
5005
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4439
5006
|
graph,
|
|
4440
5007
|
service.pkg.name,
|
|
@@ -4450,15 +5017,36 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4450
5017
|
source: fileNodeId,
|
|
4451
5018
|
target: node.id,
|
|
4452
5019
|
type: EdgeType11.RUNS_ON,
|
|
4453
|
-
provenance:
|
|
5020
|
+
provenance: Provenance10.EXTRACTED,
|
|
4454
5021
|
confidence: confidenceForExtracted8("structural"),
|
|
4455
5022
|
evidence: {
|
|
4456
|
-
file:
|
|
5023
|
+
file: evidenceFile,
|
|
5024
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
4457
5025
|
}
|
|
4458
5026
|
};
|
|
4459
5027
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
4460
5028
|
edgesAdded++;
|
|
4461
5029
|
}
|
|
5030
|
+
for (const port of facts.ports) {
|
|
5031
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5032
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5033
|
+
graph.addNode(portNode.id, portNode);
|
|
5034
|
+
nodesAdded++;
|
|
5035
|
+
}
|
|
5036
|
+
const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
|
|
5037
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5038
|
+
const portEdge = {
|
|
5039
|
+
id: portEdgeId,
|
|
5040
|
+
source: fileNodeId,
|
|
5041
|
+
target: portNode.id,
|
|
5042
|
+
type: EdgeType11.CONNECTS_TO,
|
|
5043
|
+
provenance: Provenance10.EXTRACTED,
|
|
5044
|
+
confidence: confidenceForExtracted8("structural"),
|
|
5045
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5046
|
+
};
|
|
5047
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5048
|
+
edgesAdded++;
|
|
5049
|
+
}
|
|
4462
5050
|
}
|
|
4463
5051
|
return { nodesAdded, edgesAdded };
|
|
4464
5052
|
}
|
|
@@ -4466,7 +5054,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4466
5054
|
// src/extract/infra/terraform.ts
|
|
4467
5055
|
import { promises as fs16 } from "fs";
|
|
4468
5056
|
import path30 from "path";
|
|
5057
|
+
import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
|
|
4469
5058
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5059
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
4470
5060
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4471
5061
|
if (depth > max) return [];
|
|
4472
5062
|
const out = [];
|
|
@@ -4483,24 +5073,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4483
5073
|
}
|
|
4484
5074
|
return out;
|
|
4485
5075
|
}
|
|
5076
|
+
function blockBody(content, from) {
|
|
5077
|
+
const open = content.indexOf("{", from);
|
|
5078
|
+
if (open === -1) return null;
|
|
5079
|
+
let depth = 0;
|
|
5080
|
+
for (let i = open; i < content.length; i++) {
|
|
5081
|
+
const ch = content[i];
|
|
5082
|
+
if (ch === "{") depth++;
|
|
5083
|
+
else if (ch === "}") {
|
|
5084
|
+
depth--;
|
|
5085
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
return null;
|
|
5089
|
+
}
|
|
5090
|
+
function lineAt(content, index) {
|
|
5091
|
+
let line = 1;
|
|
5092
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5093
|
+
if (content[i] === "\n") line++;
|
|
5094
|
+
}
|
|
5095
|
+
return line;
|
|
5096
|
+
}
|
|
4486
5097
|
async function addTerraformResources(graph, scanPath) {
|
|
4487
5098
|
let nodesAdded = 0;
|
|
5099
|
+
let edgesAdded = 0;
|
|
4488
5100
|
const files = await walkTfFiles(scanPath);
|
|
4489
5101
|
for (const file of files) {
|
|
4490
5102
|
const content = await fs16.readFile(file, "utf8");
|
|
5103
|
+
const evidenceFile = toPosix2(path30.relative(scanPath, file));
|
|
5104
|
+
const resources = [];
|
|
5105
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4491
5106
|
RESOURCE_RE.lastIndex = 0;
|
|
4492
5107
|
let m;
|
|
4493
5108
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
4494
|
-
const
|
|
5109
|
+
const type = m[1];
|
|
4495
5110
|
const name = m[2];
|
|
4496
|
-
const node = makeInfraNode(
|
|
5111
|
+
const node = makeInfraNode(type, name, "aws");
|
|
4497
5112
|
if (!graph.hasNode(node.id)) {
|
|
4498
5113
|
graph.addNode(node.id, node);
|
|
4499
5114
|
nodesAdded++;
|
|
4500
5115
|
}
|
|
5116
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5117
|
+
const resource = {
|
|
5118
|
+
type,
|
|
5119
|
+
name,
|
|
5120
|
+
nodeId: node.id,
|
|
5121
|
+
body: span?.body ?? "",
|
|
5122
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5123
|
+
};
|
|
5124
|
+
resources.push(resource);
|
|
5125
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5126
|
+
}
|
|
5127
|
+
for (const resource of resources) {
|
|
5128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5129
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5130
|
+
let ref;
|
|
5131
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5132
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5133
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5134
|
+
const target = byKey.get(key);
|
|
5135
|
+
if (!target) continue;
|
|
5136
|
+
if (seen.has(target.nodeId)) continue;
|
|
5137
|
+
seen.add(target.nodeId);
|
|
5138
|
+
const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
|
|
5139
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5140
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5141
|
+
const edge = {
|
|
5142
|
+
id: edgeId,
|
|
5143
|
+
source: resource.nodeId,
|
|
5144
|
+
target: target.nodeId,
|
|
5145
|
+
type: EdgeType12.DEPENDS_ON,
|
|
5146
|
+
provenance: Provenance11.EXTRACTED,
|
|
5147
|
+
confidence: confidenceForExtracted9("structural"),
|
|
5148
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5149
|
+
};
|
|
5150
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5151
|
+
edgesAdded++;
|
|
5152
|
+
}
|
|
4501
5153
|
}
|
|
4502
5154
|
}
|
|
4503
|
-
return { nodesAdded, edgesAdded
|
|
5155
|
+
return { nodesAdded, edgesAdded };
|
|
4504
5156
|
}
|
|
4505
5157
|
|
|
4506
5158
|
// src/extract/infra/k8s.ts
|
|
@@ -4576,7 +5228,7 @@ import path33 from "path";
|
|
|
4576
5228
|
// src/extract/retire.ts
|
|
4577
5229
|
import { existsSync as existsSync2 } from "fs";
|
|
4578
5230
|
import path32 from "path";
|
|
4579
|
-
import { NodeType as NodeType11, Provenance as
|
|
5231
|
+
import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
|
|
4580
5232
|
function dropOrphanedFileNodes(graph) {
|
|
4581
5233
|
const orphans = [];
|
|
4582
5234
|
graph.forEachNode((id, attrs) => {
|
|
@@ -4593,7 +5245,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
4593
5245
|
const toDrop = [];
|
|
4594
5246
|
graph.forEachEdge((id, attrs) => {
|
|
4595
5247
|
const edge = attrs;
|
|
4596
|
-
if (edge.provenance !==
|
|
5248
|
+
if (edge.provenance !== Provenance12.EXTRACTED) return;
|
|
4597
5249
|
if (!edge.evidence?.file) return;
|
|
4598
5250
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4599
5251
|
});
|
|
@@ -4606,7 +5258,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4606
5258
|
const bases = [scanPath, ...serviceDirs];
|
|
4607
5259
|
graph.forEachEdge((id, attrs) => {
|
|
4608
5260
|
const edge = attrs;
|
|
4609
|
-
if (edge.provenance !==
|
|
5261
|
+
if (edge.provenance !== Provenance12.EXTRACTED) return;
|
|
4610
5262
|
const evidenceFile = edge.evidence?.file;
|
|
4611
5263
|
if (!evidenceFile) return;
|
|
4612
5264
|
if (path32.isAbsolute(evidenceFile)) {
|
|
@@ -4687,11 +5339,12 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4687
5339
|
|
|
4688
5340
|
// src/divergences.ts
|
|
4689
5341
|
import {
|
|
5342
|
+
databaseId as databaseId3,
|
|
4690
5343
|
DivergenceResultSchema,
|
|
4691
|
-
EdgeType as
|
|
5344
|
+
EdgeType as EdgeType13,
|
|
4692
5345
|
NodeType as NodeType12,
|
|
4693
5346
|
parseEdgeId,
|
|
4694
|
-
Provenance as
|
|
5347
|
+
Provenance as Provenance13
|
|
4695
5348
|
} from "@neat.is/types";
|
|
4696
5349
|
function bucketKey(source, target, type) {
|
|
4697
5350
|
return `${type}|${source}|${target}`;
|
|
@@ -4705,17 +5358,17 @@ function bucketEdges(graph) {
|
|
|
4705
5358
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4706
5359
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4707
5360
|
switch (provenance) {
|
|
4708
|
-
case
|
|
5361
|
+
case Provenance13.EXTRACTED:
|
|
4709
5362
|
cur.extracted = e;
|
|
4710
5363
|
break;
|
|
4711
|
-
case
|
|
5364
|
+
case Provenance13.OBSERVED:
|
|
4712
5365
|
cur.observed = e;
|
|
4713
5366
|
break;
|
|
4714
|
-
case
|
|
5367
|
+
case Provenance13.INFERRED:
|
|
4715
5368
|
cur.inferred = e;
|
|
4716
5369
|
break;
|
|
4717
5370
|
default:
|
|
4718
|
-
if (e.provenance ===
|
|
5371
|
+
if (e.provenance === Provenance13.STALE) cur.stale = e;
|
|
4719
5372
|
}
|
|
4720
5373
|
buckets.set(key, cur);
|
|
4721
5374
|
});
|
|
@@ -4743,10 +5396,16 @@ function gradedConfidence(edge) {
|
|
|
4743
5396
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4744
5397
|
return clampConfidence(confidenceForEdge(edge));
|
|
4745
5398
|
}
|
|
5399
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
5400
|
+
EdgeType13.CALLS,
|
|
5401
|
+
EdgeType13.CONNECTS_TO,
|
|
5402
|
+
EdgeType13.PUBLISHES_TO,
|
|
5403
|
+
EdgeType13.CONSUMES_FROM
|
|
5404
|
+
]);
|
|
4746
5405
|
function detectMissingDivergences(graph, bucket) {
|
|
4747
5406
|
const out = [];
|
|
4748
|
-
if (bucket.type ===
|
|
4749
|
-
if (bucket.extracted && !bucket.observed) {
|
|
5407
|
+
if (bucket.type === EdgeType13.CONTAINS) return out;
|
|
5408
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
4750
5409
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4751
5410
|
out.push({
|
|
4752
5411
|
type: "missing-observed",
|
|
@@ -4786,7 +5445,7 @@ function declaredHostFor(svc) {
|
|
|
4786
5445
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4787
5446
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4788
5447
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4789
|
-
if (e.type ===
|
|
5448
|
+
if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
|
|
4790
5449
|
return true;
|
|
4791
5450
|
}
|
|
4792
5451
|
}
|
|
@@ -4799,8 +5458,8 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4799
5458
|
const out = [];
|
|
4800
5459
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4801
5460
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4802
|
-
if (edge.type !==
|
|
4803
|
-
if (edge.provenance !==
|
|
5461
|
+
if (edge.type !== EdgeType13.CONNECTS_TO) continue;
|
|
5462
|
+
if (edge.provenance !== Provenance13.OBSERVED) continue;
|
|
4804
5463
|
const target = graph.getNodeAttributes(edge.target);
|
|
4805
5464
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4806
5465
|
const observedHost = target.host?.trim();
|
|
@@ -4824,8 +5483,8 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4824
5483
|
const deps = svc.dependencies ?? {};
|
|
4825
5484
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4826
5485
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4827
|
-
if (edge.type !==
|
|
4828
|
-
if (edge.provenance !==
|
|
5486
|
+
if (edge.type !== EdgeType13.CONNECTS_TO) continue;
|
|
5487
|
+
if (edge.provenance !== Provenance13.OBSERVED) continue;
|
|
4829
5488
|
const target = graph.getNodeAttributes(edge.target);
|
|
4830
5489
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4831
5490
|
for (const pair of compatPairs()) {
|
|
@@ -4880,6 +5539,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4880
5539
|
function involvesNode(d, nodeId) {
|
|
4881
5540
|
return d.source === nodeId || d.target === nodeId;
|
|
4882
5541
|
}
|
|
5542
|
+
function suppressHostMismatchHalves(all) {
|
|
5543
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
5544
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
5545
|
+
for (const d of all) {
|
|
5546
|
+
if (d.type !== "host-mismatch") continue;
|
|
5547
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
5548
|
+
declaredHalf.add(databaseId3(d.extractedHost));
|
|
5549
|
+
}
|
|
5550
|
+
if (observedHalf.size === 0) return all;
|
|
5551
|
+
return all.filter((d) => {
|
|
5552
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
5553
|
+
return false;
|
|
5554
|
+
}
|
|
5555
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
5556
|
+
return true;
|
|
5557
|
+
});
|
|
5558
|
+
}
|
|
4883
5559
|
function computeDivergences(graph, opts = {}) {
|
|
4884
5560
|
const all = [];
|
|
4885
5561
|
const buckets = bucketEdges(graph);
|
|
@@ -4893,7 +5569,8 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4893
5569
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4894
5570
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4895
5571
|
});
|
|
4896
|
-
|
|
5572
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
5573
|
+
let filtered = reconciled;
|
|
4897
5574
|
if (opts.type) {
|
|
4898
5575
|
const allowed = opts.type;
|
|
4899
5576
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -4931,7 +5608,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4931
5608
|
// src/persist.ts
|
|
4932
5609
|
import { promises as fs18 } from "fs";
|
|
4933
5610
|
import path34 from "path";
|
|
4934
|
-
import { Provenance as
|
|
5611
|
+
import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
4935
5612
|
var SCHEMA_VERSION = 4;
|
|
4936
5613
|
function migrateV1ToV2(payload) {
|
|
4937
5614
|
const nodes = payload.graph.nodes;
|
|
@@ -4953,7 +5630,7 @@ function migrateV2ToV3(payload) {
|
|
|
4953
5630
|
for (const edge of edges) {
|
|
4954
5631
|
const attrs = edge.attributes;
|
|
4955
5632
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4956
|
-
attrs.provenance =
|
|
5633
|
+
attrs.provenance = Provenance14.OBSERVED;
|
|
4957
5634
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4958
5635
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4959
5636
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -6077,6 +6754,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6077
6754
|
}
|
|
6078
6755
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6079
6756
|
});
|
|
6757
|
+
scope.get(
|
|
6758
|
+
"/graph/observed-dependencies/:nodeId",
|
|
6759
|
+
async (req, reply) => {
|
|
6760
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6761
|
+
if (!proj) return;
|
|
6762
|
+
const { nodeId } = req.params;
|
|
6763
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6764
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6765
|
+
}
|
|
6766
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
6767
|
+
}
|
|
6768
|
+
);
|
|
6080
6769
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6081
6770
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6082
6771
|
if (!proj) return;
|
|
@@ -6137,23 +6826,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6137
6826
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6138
6827
|
return { count: sliced.length, total, events: sliced };
|
|
6139
6828
|
});
|
|
6829
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
6830
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6831
|
+
if (!proj) return;
|
|
6832
|
+
const { nodeId } = req.params;
|
|
6833
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6834
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6835
|
+
return;
|
|
6836
|
+
}
|
|
6837
|
+
const epath = errorsPathFor(proj);
|
|
6838
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6839
|
+
const events = await readErrorEvents(epath);
|
|
6840
|
+
const filtered = events.filter(
|
|
6841
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6842
|
+
);
|
|
6843
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6844
|
+
};
|
|
6140
6845
|
scope.get(
|
|
6141
6846
|
"/incidents/:nodeId",
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6148
|
-
}
|
|
6149
|
-
const epath = errorsPathFor(proj);
|
|
6150
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6151
|
-
const events = await readErrorEvents(epath);
|
|
6152
|
-
const filtered = events.filter(
|
|
6153
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6154
|
-
);
|
|
6155
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6156
|
-
}
|
|
6847
|
+
incidentHistoryHandler
|
|
6848
|
+
);
|
|
6849
|
+
scope.get(
|
|
6850
|
+
"/graph/incident-history/:nodeId",
|
|
6851
|
+
incidentHistoryHandler
|
|
6157
6852
|
);
|
|
6158
6853
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6159
6854
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6162,16 +6857,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6162
6857
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6163
6858
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6164
6859
|
}
|
|
6165
|
-
let errorEvent;
|
|
6166
6860
|
const epath = errorsPathFor(proj);
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6861
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
6862
|
+
let errorEvent;
|
|
6863
|
+
if (req.query.errorId) {
|
|
6864
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6170
6865
|
if (!errorEvent) {
|
|
6171
6866
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6172
6867
|
}
|
|
6173
6868
|
}
|
|
6174
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
6869
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6175
6870
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6176
6871
|
return result;
|
|
6177
6872
|
});
|
|
@@ -6316,6 +7011,28 @@ function registerRoutes(scope, ctx) {
|
|
|
6316
7011
|
}
|
|
6317
7012
|
return { violations };
|
|
6318
7013
|
});
|
|
7014
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7015
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7016
|
+
if (!proj) return;
|
|
7017
|
+
const nodeId = req.query.node;
|
|
7018
|
+
if (!nodeId) {
|
|
7019
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7020
|
+
}
|
|
7021
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7022
|
+
let policies = [];
|
|
7023
|
+
if (policyPath) {
|
|
7024
|
+
try {
|
|
7025
|
+
policies = await loadPolicyFile(policyPath);
|
|
7026
|
+
} catch (err) {
|
|
7027
|
+
return reply.code(400).send({
|
|
7028
|
+
error: "policy.json failed to parse",
|
|
7029
|
+
details: err.message
|
|
7030
|
+
});
|
|
7031
|
+
}
|
|
7032
|
+
}
|
|
7033
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7034
|
+
return { node: nodeId, applicable };
|
|
7035
|
+
});
|
|
6319
7036
|
scope.post("/policies/check", async (req, reply) => {
|
|
6320
7037
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6321
7038
|
if (!proj) return;
|
|
@@ -6606,6 +7323,7 @@ export {
|
|
|
6606
7323
|
discoverServices,
|
|
6607
7324
|
addServiceNodes,
|
|
6608
7325
|
addServiceAliases,
|
|
7326
|
+
addFiles,
|
|
6609
7327
|
addImports,
|
|
6610
7328
|
addDatabasesAndCompat,
|
|
6611
7329
|
addConfigNodes,
|
|
@@ -6644,4 +7362,4 @@ export {
|
|
|
6644
7362
|
pruneRegistry,
|
|
6645
7363
|
buildApi
|
|
6646
7364
|
};
|
|
6647
|
-
//# sourceMappingURL=chunk-
|
|
7365
|
+
//# sourceMappingURL=chunk-O25KZNZK.js.map
|