@neat.is/core 0.4.20-dev.20260629 → 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-GNAX2CBF.js → chunk-OQ7CHOY2.js} +859 -159
- package/dist/chunk-OQ7CHOY2.js.map +1 -0
- package/dist/{chunk-62ELQVIP.js → chunk-XRE4OQWJ.js} +57 -9
- package/dist/chunk-XRE4OQWJ.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1232 -354
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +884 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +928 -150
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +806 -121
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
|
@@ -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] : [];
|
|
617
|
+
return RootCauseResultSchema.parse({
|
|
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:/, "");
|
|
564
708
|
return RootCauseResultSchema.parse({
|
|
565
|
-
rootCauseNode:
|
|
566
|
-
rootCauseReason:
|
|
567
|
-
traversalPath:
|
|
568
|
-
edgeProvenances
|
|
569
|
-
confidence
|
|
570
|
-
fixRecommendation:
|
|
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");
|
|
@@ -1624,7 +2044,10 @@ async function handleSpan(ctx, span) {
|
|
|
1624
2044
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1625
2045
|
cacheSpanService(span, nowMs, callSite);
|
|
1626
2046
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1627
|
-
const callSiteEvidence = callSite ? {
|
|
2047
|
+
const callSiteEvidence = callSite ? {
|
|
2048
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2049
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2050
|
+
} : void 0;
|
|
1628
2051
|
let affectedNode = sourceId;
|
|
1629
2052
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1630
2053
|
if (span.dbSystem) {
|
|
@@ -1646,7 +2069,7 @@ async function handleSpan(ctx, span) {
|
|
|
1646
2069
|
} else {
|
|
1647
2070
|
const host = pickAddress(span);
|
|
1648
2071
|
let resolvedViaAddress = false;
|
|
1649
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2072
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
1650
2073
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
1651
2074
|
if (targetId && targetId !== sourceId) {
|
|
1652
2075
|
upsertObservedEdge(
|
|
@@ -1681,7 +2104,11 @@ async function handleSpan(ctx, span) {
|
|
|
1681
2104
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1682
2105
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
1683
2106
|
const fallbackEvidence = parent.callSite ? {
|
|
1684
|
-
file:
|
|
2107
|
+
file: reconcileObservedRelPath(
|
|
2108
|
+
ctx.graph,
|
|
2109
|
+
parent.service,
|
|
2110
|
+
parent.callSite.relPath
|
|
2111
|
+
),
|
|
1685
2112
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
1686
2113
|
} : void 0;
|
|
1687
2114
|
upsertObservedEdge(
|
|
@@ -1706,7 +2133,7 @@ async function handleSpan(ctx, span) {
|
|
|
1706
2133
|
service: span.service,
|
|
1707
2134
|
traceId: span.traceId,
|
|
1708
2135
|
spanId: span.spanId,
|
|
1709
|
-
errorMessage: span
|
|
2136
|
+
errorMessage: incidentMessage(span),
|
|
1710
2137
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1711
2138
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1712
2139
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -1773,7 +2200,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
|
1773
2200
|
}
|
|
1774
2201
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1775
2202
|
graph.dropEdge(oldEdgeId);
|
|
1776
|
-
const newId = edge.provenance ===
|
|
2203
|
+
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
2204
|
if (graph.hasEdge(newId)) {
|
|
1778
2205
|
const existing = graph.getEdgeAttributes(newId);
|
|
1779
2206
|
const merged = {
|
|
@@ -1807,12 +2234,12 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1807
2234
|
const project = options.project ?? DEFAULT_PROJECT;
|
|
1808
2235
|
graph.forEachEdge((id, attrs) => {
|
|
1809
2236
|
const e = attrs;
|
|
1810
|
-
if (e.provenance !==
|
|
2237
|
+
if (e.provenance !== Provenance2.OBSERVED) return;
|
|
1811
2238
|
if (!e.lastObserved) return;
|
|
1812
2239
|
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
1813
2240
|
const age = now - new Date(e.lastObserved).getTime();
|
|
1814
2241
|
if (age > threshold) {
|
|
1815
|
-
const updated = { ...e, provenance:
|
|
2242
|
+
const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
|
|
1816
2243
|
graph.replaceEdgeAttributes(id, updated);
|
|
1817
2244
|
events.push({
|
|
1818
2245
|
edgeId: id,
|
|
@@ -1829,8 +2256,8 @@ async function markStaleEdges(graph, options = {}) {
|
|
|
1829
2256
|
project,
|
|
1830
2257
|
payload: {
|
|
1831
2258
|
edgeId: id,
|
|
1832
|
-
from:
|
|
1833
|
-
to:
|
|
2259
|
+
from: Provenance2.OBSERVED,
|
|
2260
|
+
to: Provenance2.STALE
|
|
1834
2261
|
}
|
|
1835
2262
|
});
|
|
1836
2263
|
}
|
|
@@ -1882,12 +2309,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
1882
2309
|
async function readErrorEvents(errorsPath) {
|
|
1883
2310
|
try {
|
|
1884
2311
|
const raw = await fs3.readFile(errorsPath, "utf8");
|
|
1885
|
-
|
|
2312
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2313
|
+
return dedupeIncidents(events);
|
|
1886
2314
|
} catch (err) {
|
|
1887
2315
|
if (err.code === "ENOENT") return [];
|
|
1888
2316
|
throw err;
|
|
1889
2317
|
}
|
|
1890
2318
|
}
|
|
2319
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2320
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2321
|
+
if (ev.errorType) return false;
|
|
2322
|
+
if (!ev.attributes) return false;
|
|
2323
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2324
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2325
|
+
}
|
|
2326
|
+
function dedupeIncidents(events) {
|
|
2327
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2328
|
+
const once = [];
|
|
2329
|
+
for (const ev of events) {
|
|
2330
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2331
|
+
if (key === void 0) {
|
|
2332
|
+
once.push(ev);
|
|
2333
|
+
continue;
|
|
2334
|
+
}
|
|
2335
|
+
if (seen.has(key)) continue;
|
|
2336
|
+
seen.add(key);
|
|
2337
|
+
once.push(ev);
|
|
2338
|
+
}
|
|
2339
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2340
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2341
|
+
for (const ev of once) {
|
|
2342
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2343
|
+
}
|
|
2344
|
+
return once.filter((ev) => {
|
|
2345
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2346
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
1891
2349
|
function mergeSnapshot(graph, snapshot) {
|
|
1892
2350
|
const exported = snapshot.graph;
|
|
1893
2351
|
let nodesAdded = 0;
|
|
@@ -2021,10 +2479,17 @@ function isConfigFile(name) {
|
|
|
2021
2479
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2022
2480
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2023
2481
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2482
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2024
2483
|
return { match: true, fileType: "env" };
|
|
2025
2484
|
}
|
|
2026
2485
|
return { match: false, fileType: "" };
|
|
2027
2486
|
}
|
|
2487
|
+
function isNeatAuthoredEnvFile(name) {
|
|
2488
|
+
return name === ".env.neat";
|
|
2489
|
+
}
|
|
2490
|
+
function isNeatAuthoredSourceFile(name) {
|
|
2491
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
2492
|
+
}
|
|
2028
2493
|
function isTestPath(filePath) {
|
|
2029
2494
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2030
2495
|
const segments = normalised.split("/");
|
|
@@ -2659,7 +3124,7 @@ import path10 from "path";
|
|
|
2659
3124
|
import {
|
|
2660
3125
|
EdgeType as EdgeType4,
|
|
2661
3126
|
NodeType as NodeType6,
|
|
2662
|
-
Provenance as
|
|
3127
|
+
Provenance as Provenance3,
|
|
2663
3128
|
confidenceForExtracted,
|
|
2664
3129
|
extractedEdgeId as extractedEdgeId3,
|
|
2665
3130
|
fileId as fileId2
|
|
@@ -2674,7 +3139,9 @@ async function walkSourceFiles(dir) {
|
|
|
2674
3139
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2675
3140
|
if (await isPythonVenvDir(full)) continue;
|
|
2676
3141
|
await walk(full);
|
|
2677
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))
|
|
3142
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3143
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3144
|
+
!isNeatAuthoredSourceFile(entry.name)) {
|
|
2678
3145
|
out.push(full);
|
|
2679
3146
|
}
|
|
2680
3147
|
}
|
|
@@ -2746,7 +3213,7 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
|
2746
3213
|
source: serviceNodeId,
|
|
2747
3214
|
target: fileNodeId,
|
|
2748
3215
|
type: EdgeType4.CONTAINS,
|
|
2749
|
-
provenance:
|
|
3216
|
+
provenance: Provenance3.EXTRACTED,
|
|
2750
3217
|
confidence: confidenceForExtracted("structural"),
|
|
2751
3218
|
evidence: { file: relPath }
|
|
2752
3219
|
};
|
|
@@ -2786,7 +3253,7 @@ import JavaScript from "tree-sitter-javascript";
|
|
|
2786
3253
|
import Python from "tree-sitter-python";
|
|
2787
3254
|
import {
|
|
2788
3255
|
EdgeType as EdgeType5,
|
|
2789
|
-
Provenance as
|
|
3256
|
+
Provenance as Provenance4,
|
|
2790
3257
|
confidenceForExtracted as confidenceForExtracted2,
|
|
2791
3258
|
extractedEdgeId as extractedEdgeId4,
|
|
2792
3259
|
fileId as fileId3
|
|
@@ -2849,11 +3316,28 @@ function collectJsImports(node, out) {
|
|
|
2849
3316
|
if (child) collectJsImports(child, out);
|
|
2850
3317
|
}
|
|
2851
3318
|
}
|
|
3319
|
+
function collectImportedNames(node, out) {
|
|
3320
|
+
if (node.type === "aliased_import") {
|
|
3321
|
+
const nameNode = node.childForFieldName("name");
|
|
3322
|
+
if (nameNode) out.push(nameNode.text);
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
if (node.type === "dotted_name") {
|
|
3326
|
+
out.push(node.text);
|
|
3327
|
+
return;
|
|
3328
|
+
}
|
|
3329
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3330
|
+
const child = node.namedChild(i);
|
|
3331
|
+
if (child) collectImportedNames(child, out);
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
2852
3334
|
function collectPyImports(node, out) {
|
|
2853
3335
|
if (node.type === "import_from_statement") {
|
|
2854
3336
|
let level = 0;
|
|
2855
3337
|
let modulePath = "";
|
|
3338
|
+
const names = [];
|
|
2856
3339
|
let pastFrom = false;
|
|
3340
|
+
let pastImport = false;
|
|
2857
3341
|
for (let i = 0; i < node.childCount; i++) {
|
|
2858
3342
|
const child = node.child(i);
|
|
2859
3343
|
if (!child) continue;
|
|
@@ -2861,26 +3345,30 @@ function collectPyImports(node, out) {
|
|
|
2861
3345
|
if (child.type === "from") pastFrom = true;
|
|
2862
3346
|
continue;
|
|
2863
3347
|
}
|
|
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;
|
|
3348
|
+
if (!pastImport) {
|
|
3349
|
+
if (child.type === "import") {
|
|
3350
|
+
pastImport = true;
|
|
3351
|
+
continue;
|
|
2874
3352
|
}
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
3353
|
+
if (child.type === "relative_import") {
|
|
3354
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
3355
|
+
const rc = child.child(j);
|
|
3356
|
+
if (!rc) continue;
|
|
3357
|
+
if (rc.type === "import_prefix") {
|
|
3358
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
3359
|
+
if (rc.child(k)?.type === ".") level++;
|
|
3360
|
+
}
|
|
3361
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
3362
|
+
}
|
|
3363
|
+
} else if (child.type === "dotted_name") {
|
|
3364
|
+
modulePath = child.text;
|
|
3365
|
+
}
|
|
3366
|
+
continue;
|
|
2880
3367
|
}
|
|
3368
|
+
collectImportedNames(child, names);
|
|
2881
3369
|
}
|
|
2882
3370
|
if (level > 0 || modulePath) {
|
|
2883
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3371
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2884
3372
|
}
|
|
2885
3373
|
}
|
|
2886
3374
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -2982,8 +3470,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
2982
3470
|
return null;
|
|
2983
3471
|
}
|
|
2984
3472
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
2985
|
-
if (!imp.modulePath) return null;
|
|
2986
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
2987
3473
|
let baseDir;
|
|
2988
3474
|
if (imp.level > 0) {
|
|
2989
3475
|
baseDir = path12.dirname(importerPath);
|
|
@@ -2991,13 +3477,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
2991
3477
|
} else {
|
|
2992
3478
|
baseDir = serviceDir;
|
|
2993
3479
|
}
|
|
2994
|
-
const
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3480
|
+
const moduleBase = imp.modulePath ? path12.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
3481
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
3482
|
+
let needModuleFile = imp.names.length === 0;
|
|
3483
|
+
for (const name of imp.names) {
|
|
3484
|
+
const submoduleFile = path12.join(moduleBase, `${name}.py`);
|
|
3485
|
+
const subpackageInit = path12.join(moduleBase, name, "__init__.py");
|
|
3486
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
3487
|
+
resolved.add(toPosix2(path12.relative(serviceDir, submoduleFile)));
|
|
3488
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
3489
|
+
resolved.add(toPosix2(path12.relative(serviceDir, subpackageInit)));
|
|
3490
|
+
} else {
|
|
3491
|
+
needModuleFile = true;
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
if (needModuleFile) {
|
|
3495
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, path12.join(moduleBase, "__init__.py")] : [path12.join(moduleBase, "__init__.py")];
|
|
3496
|
+
for (const candidate of moduleFileCandidates) {
|
|
3497
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
3498
|
+
resolved.add(toPosix2(path12.relative(serviceDir, candidate)));
|
|
3499
|
+
break;
|
|
3500
|
+
}
|
|
2998
3501
|
}
|
|
2999
3502
|
}
|
|
3000
|
-
return
|
|
3503
|
+
return [...resolved];
|
|
3001
3504
|
}
|
|
3002
3505
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3003
3506
|
const importeeFileId = fileId3(serviceName, importeeRelPath);
|
|
@@ -3009,7 +3512,7 @@ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, imp
|
|
|
3009
3512
|
source: importerFileId,
|
|
3010
3513
|
target: importeeFileId,
|
|
3011
3514
|
type: EdgeType5.IMPORTS,
|
|
3012
|
-
provenance:
|
|
3515
|
+
provenance: Provenance4.EXTRACTED,
|
|
3013
3516
|
confidence: confidenceForExtracted2("structural"),
|
|
3014
3517
|
evidence: { file: importerRelPath, line, snippet: snippet2 }
|
|
3015
3518
|
};
|
|
@@ -3038,17 +3541,18 @@ async function addImports(graph, services) {
|
|
|
3038
3541
|
continue;
|
|
3039
3542
|
}
|
|
3040
3543
|
for (const imp of pyImports) {
|
|
3041
|
-
const
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3544
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
3545
|
+
for (const resolved of resolvedPaths) {
|
|
3546
|
+
edgesAdded += emitImportEdge(
|
|
3547
|
+
graph,
|
|
3548
|
+
service.pkg.name,
|
|
3549
|
+
importerFileId,
|
|
3550
|
+
relFile,
|
|
3551
|
+
resolved,
|
|
3552
|
+
imp.line,
|
|
3553
|
+
imp.snippet
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
3052
3556
|
}
|
|
3053
3557
|
continue;
|
|
3054
3558
|
}
|
|
@@ -3083,7 +3587,8 @@ import path20 from "path";
|
|
|
3083
3587
|
import {
|
|
3084
3588
|
EdgeType as EdgeType6,
|
|
3085
3589
|
NodeType as NodeType7,
|
|
3086
|
-
Provenance as
|
|
3590
|
+
Provenance as Provenance5,
|
|
3591
|
+
configId,
|
|
3087
3592
|
databaseId as databaseId2,
|
|
3088
3593
|
confidenceForExtracted as confidenceForExtracted3
|
|
3089
3594
|
} from "@neat.is/types";
|
|
@@ -3670,7 +4175,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3670
4175
|
source: fileNodeId,
|
|
3671
4176
|
target: dbNode.id,
|
|
3672
4177
|
type: EdgeType6.CONNECTS_TO,
|
|
3673
|
-
provenance:
|
|
4178
|
+
provenance: Provenance5.EXTRACTED,
|
|
3674
4179
|
confidence: confidenceForExtracted3("structural"),
|
|
3675
4180
|
evidence: { file: evidenceFile }
|
|
3676
4181
|
};
|
|
@@ -3679,6 +4184,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3679
4184
|
edgesAdded++;
|
|
3680
4185
|
}
|
|
3681
4186
|
}
|
|
4187
|
+
if (allConfigs.length === 1) {
|
|
4188
|
+
const primary = allConfigs[0];
|
|
4189
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4190
|
+
const relPath = path20.relative(scanPath, primary.sourceFile);
|
|
4191
|
+
const cfgId = configId(relPath);
|
|
4192
|
+
if (!graph.hasNode(cfgId)) {
|
|
4193
|
+
const cfgNode = {
|
|
4194
|
+
id: cfgId,
|
|
4195
|
+
type: NodeType7.ConfigNode,
|
|
4196
|
+
name: path20.basename(primary.sourceFile),
|
|
4197
|
+
path: relPath,
|
|
4198
|
+
fileType: isConfigFile(path20.basename(primary.sourceFile)).fileType || "config"
|
|
4199
|
+
};
|
|
4200
|
+
graph.addNode(cfgId, cfgNode);
|
|
4201
|
+
nodesAdded++;
|
|
4202
|
+
}
|
|
4203
|
+
const cfgEdge = {
|
|
4204
|
+
id: extractedEdgeId2(service.node.id, cfgId, EdgeType6.CONFIGURED_BY),
|
|
4205
|
+
source: service.node.id,
|
|
4206
|
+
target: cfgId,
|
|
4207
|
+
type: EdgeType6.CONFIGURED_BY,
|
|
4208
|
+
provenance: Provenance5.EXTRACTED,
|
|
4209
|
+
confidence: confidenceForExtracted3("structural"),
|
|
4210
|
+
evidence: { file: toPosix2(relPath) }
|
|
4211
|
+
};
|
|
4212
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4213
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4214
|
+
edgesAdded++;
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
3682
4217
|
attachIncompatibilities(service, allConfigs);
|
|
3683
4218
|
if (graph.hasNode(service.node.id)) {
|
|
3684
4219
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -3690,6 +4225,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3690
4225
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
3691
4226
|
delete updated.incompatibilities;
|
|
3692
4227
|
}
|
|
4228
|
+
if (!service.node.dbConnectionTarget) {
|
|
4229
|
+
delete updated.dbConnectionTarget;
|
|
4230
|
+
}
|
|
3693
4231
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
3694
4232
|
}
|
|
3695
4233
|
}
|
|
@@ -3702,8 +4240,8 @@ import path21 from "path";
|
|
|
3702
4240
|
import {
|
|
3703
4241
|
EdgeType as EdgeType7,
|
|
3704
4242
|
NodeType as NodeType8,
|
|
3705
|
-
Provenance as
|
|
3706
|
-
configId,
|
|
4243
|
+
Provenance as Provenance6,
|
|
4244
|
+
configId as configId2,
|
|
3707
4245
|
confidenceForExtracted as confidenceForExtracted4
|
|
3708
4246
|
} from "@neat.is/types";
|
|
3709
4247
|
async function walkConfigFiles(dir) {
|
|
@@ -3732,7 +4270,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3732
4270
|
for (const file of configFiles) {
|
|
3733
4271
|
const relPath = path21.relative(scanPath, file);
|
|
3734
4272
|
const node = {
|
|
3735
|
-
id:
|
|
4273
|
+
id: configId2(relPath),
|
|
3736
4274
|
type: NodeType8.ConfigNode,
|
|
3737
4275
|
name: path21.basename(file),
|
|
3738
4276
|
path: relPath,
|
|
@@ -3756,7 +4294,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3756
4294
|
source: fileNodeId,
|
|
3757
4295
|
target: node.id,
|
|
3758
4296
|
type: EdgeType7.CONFIGURED_BY,
|
|
3759
|
-
provenance:
|
|
4297
|
+
provenance: Provenance6.EXTRACTED,
|
|
3760
4298
|
confidence: confidenceForExtracted4("structural"),
|
|
3761
4299
|
evidence: { file: relPath.split(path21.sep).join("/") }
|
|
3762
4300
|
};
|
|
@@ -3773,7 +4311,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3773
4311
|
import {
|
|
3774
4312
|
EdgeType as EdgeType9,
|
|
3775
4313
|
NodeType as NodeType9,
|
|
3776
|
-
Provenance as
|
|
4314
|
+
Provenance as Provenance8,
|
|
3777
4315
|
confidenceForExtracted as confidenceForExtracted6,
|
|
3778
4316
|
passesExtractedFloor as passesExtractedFloor2
|
|
3779
4317
|
} from "@neat.is/types";
|
|
@@ -3785,7 +4323,7 @@ import JavaScript2 from "tree-sitter-javascript";
|
|
|
3785
4323
|
import Python2 from "tree-sitter-python";
|
|
3786
4324
|
import {
|
|
3787
4325
|
EdgeType as EdgeType8,
|
|
3788
|
-
Provenance as
|
|
4326
|
+
Provenance as Provenance7,
|
|
3789
4327
|
confidenceForExtracted as confidenceForExtracted5,
|
|
3790
4328
|
passesExtractedFloor
|
|
3791
4329
|
} from "@neat.is/types";
|
|
@@ -3881,7 +4419,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3881
4419
|
const dedupKey = `${relFile}|${targetId}`;
|
|
3882
4420
|
if (seen.has(dedupKey)) continue;
|
|
3883
4421
|
seen.add(dedupKey);
|
|
3884
|
-
const confidence = confidenceForExtracted5("
|
|
4422
|
+
const confidence = confidenceForExtracted5("url-literal-service-target");
|
|
3885
4423
|
const ev = {
|
|
3886
4424
|
file: relFile,
|
|
3887
4425
|
line: site.line,
|
|
@@ -3901,7 +4439,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3901
4439
|
target: targetId,
|
|
3902
4440
|
type: EdgeType8.CALLS,
|
|
3903
4441
|
confidence,
|
|
3904
|
-
confidenceKind: "
|
|
4442
|
+
confidenceKind: "url-literal-service-target",
|
|
3905
4443
|
evidence: ev
|
|
3906
4444
|
});
|
|
3907
4445
|
continue;
|
|
@@ -3913,7 +4451,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3913
4451
|
source: fileNodeId,
|
|
3914
4452
|
target: targetId,
|
|
3915
4453
|
type: EdgeType8.CALLS,
|
|
3916
|
-
provenance:
|
|
4454
|
+
provenance: Provenance7.EXTRACTED,
|
|
3917
4455
|
confidence,
|
|
3918
4456
|
evidence: ev
|
|
3919
4457
|
};
|
|
@@ -4266,7 +4804,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4266
4804
|
source: fileNodeId,
|
|
4267
4805
|
target: ep.infraId,
|
|
4268
4806
|
type: edgeType,
|
|
4269
|
-
provenance:
|
|
4807
|
+
provenance: Provenance8.EXTRACTED,
|
|
4270
4808
|
confidence,
|
|
4271
4809
|
evidence: ep.evidence
|
|
4272
4810
|
};
|
|
@@ -4288,7 +4826,7 @@ async function addCallEdges(graph, services) {
|
|
|
4288
4826
|
|
|
4289
4827
|
// src/extract/infra/docker-compose.ts
|
|
4290
4828
|
import path28 from "path";
|
|
4291
|
-
import { EdgeType as EdgeType10, Provenance as
|
|
4829
|
+
import { EdgeType as EdgeType10, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
|
|
4292
4830
|
|
|
4293
4831
|
// src/extract/infra/shared.ts
|
|
4294
4832
|
import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
|
|
@@ -4381,7 +4919,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4381
4919
|
source: sourceId,
|
|
4382
4920
|
target: targetId,
|
|
4383
4921
|
type: EdgeType10.DEPENDS_ON,
|
|
4384
|
-
provenance:
|
|
4922
|
+
provenance: Provenance9.EXTRACTED,
|
|
4385
4923
|
confidence: confidenceForExtracted7("structural"),
|
|
4386
4924
|
evidence: { file: evidenceFile }
|
|
4387
4925
|
};
|
|
@@ -4395,20 +4933,30 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4395
4933
|
// src/extract/infra/dockerfile.ts
|
|
4396
4934
|
import path29 from "path";
|
|
4397
4935
|
import { promises as fs15 } from "fs";
|
|
4398
|
-
import { EdgeType as EdgeType11, Provenance as
|
|
4399
|
-
function
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4936
|
+
import { EdgeType as EdgeType11, Provenance as Provenance10, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
|
|
4937
|
+
function readDockerfile(content) {
|
|
4938
|
+
let image = null;
|
|
4939
|
+
const ports = [];
|
|
4940
|
+
let cmd = null;
|
|
4941
|
+
let entrypoint = null;
|
|
4942
|
+
for (const raw of content.split("\n")) {
|
|
4403
4943
|
const line = raw.trim();
|
|
4404
4944
|
if (!line || line.startsWith("#")) continue;
|
|
4405
|
-
if (
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
if (
|
|
4409
|
-
|
|
4945
|
+
if (/^from\s+/i.test(line)) {
|
|
4946
|
+
const candidate = line.split(/\s+/)[1];
|
|
4947
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
4948
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
4949
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
4950
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
4951
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
4952
|
+
}
|
|
4953
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
4954
|
+
entrypoint = line;
|
|
4955
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
4956
|
+
cmd = line;
|
|
4957
|
+
}
|
|
4410
4958
|
}
|
|
4411
|
-
return
|
|
4959
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
4412
4960
|
}
|
|
4413
4961
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
4414
4962
|
let nodesAdded = 0;
|
|
@@ -4427,14 +4975,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4427
4975
|
);
|
|
4428
4976
|
continue;
|
|
4429
4977
|
}
|
|
4430
|
-
const
|
|
4431
|
-
if (!image) continue;
|
|
4432
|
-
const node = makeInfraNode("container-image", image);
|
|
4978
|
+
const facts = readDockerfile(content);
|
|
4979
|
+
if (!facts.image) continue;
|
|
4980
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
4433
4981
|
if (!graph.hasNode(node.id)) {
|
|
4434
4982
|
graph.addNode(node.id, node);
|
|
4435
4983
|
nodesAdded++;
|
|
4436
4984
|
}
|
|
4437
4985
|
const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
|
|
4986
|
+
const evidenceFile = toPosix2(path29.relative(scanPath, dockerfilePath));
|
|
4438
4987
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4439
4988
|
graph,
|
|
4440
4989
|
service.pkg.name,
|
|
@@ -4450,15 +4999,36 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4450
4999
|
source: fileNodeId,
|
|
4451
5000
|
target: node.id,
|
|
4452
5001
|
type: EdgeType11.RUNS_ON,
|
|
4453
|
-
provenance:
|
|
5002
|
+
provenance: Provenance10.EXTRACTED,
|
|
4454
5003
|
confidence: confidenceForExtracted8("structural"),
|
|
4455
5004
|
evidence: {
|
|
4456
|
-
file:
|
|
5005
|
+
file: evidenceFile,
|
|
5006
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
4457
5007
|
}
|
|
4458
5008
|
};
|
|
4459
5009
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
4460
5010
|
edgesAdded++;
|
|
4461
5011
|
}
|
|
5012
|
+
for (const port of facts.ports) {
|
|
5013
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5014
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5015
|
+
graph.addNode(portNode.id, portNode);
|
|
5016
|
+
nodesAdded++;
|
|
5017
|
+
}
|
|
5018
|
+
const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType11.CONNECTS_TO);
|
|
5019
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5020
|
+
const portEdge = {
|
|
5021
|
+
id: portEdgeId,
|
|
5022
|
+
source: fileNodeId,
|
|
5023
|
+
target: portNode.id,
|
|
5024
|
+
type: EdgeType11.CONNECTS_TO,
|
|
5025
|
+
provenance: Provenance10.EXTRACTED,
|
|
5026
|
+
confidence: confidenceForExtracted8("structural"),
|
|
5027
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5028
|
+
};
|
|
5029
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5030
|
+
edgesAdded++;
|
|
5031
|
+
}
|
|
4462
5032
|
}
|
|
4463
5033
|
return { nodesAdded, edgesAdded };
|
|
4464
5034
|
}
|
|
@@ -4466,7 +5036,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4466
5036
|
// src/extract/infra/terraform.ts
|
|
4467
5037
|
import { promises as fs16 } from "fs";
|
|
4468
5038
|
import path30 from "path";
|
|
5039
|
+
import { EdgeType as EdgeType12, Provenance as Provenance11, confidenceForExtracted as confidenceForExtracted9 } from "@neat.is/types";
|
|
4469
5040
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5041
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
4470
5042
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
4471
5043
|
if (depth > max) return [];
|
|
4472
5044
|
const out = [];
|
|
@@ -4483,24 +5055,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
4483
5055
|
}
|
|
4484
5056
|
return out;
|
|
4485
5057
|
}
|
|
5058
|
+
function blockBody(content, from) {
|
|
5059
|
+
const open = content.indexOf("{", from);
|
|
5060
|
+
if (open === -1) return null;
|
|
5061
|
+
let depth = 0;
|
|
5062
|
+
for (let i = open; i < content.length; i++) {
|
|
5063
|
+
const ch = content[i];
|
|
5064
|
+
if (ch === "{") depth++;
|
|
5065
|
+
else if (ch === "}") {
|
|
5066
|
+
depth--;
|
|
5067
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5068
|
+
}
|
|
5069
|
+
}
|
|
5070
|
+
return null;
|
|
5071
|
+
}
|
|
5072
|
+
function lineAt(content, index) {
|
|
5073
|
+
let line = 1;
|
|
5074
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5075
|
+
if (content[i] === "\n") line++;
|
|
5076
|
+
}
|
|
5077
|
+
return line;
|
|
5078
|
+
}
|
|
4486
5079
|
async function addTerraformResources(graph, scanPath) {
|
|
4487
5080
|
let nodesAdded = 0;
|
|
5081
|
+
let edgesAdded = 0;
|
|
4488
5082
|
const files = await walkTfFiles(scanPath);
|
|
4489
5083
|
for (const file of files) {
|
|
4490
5084
|
const content = await fs16.readFile(file, "utf8");
|
|
5085
|
+
const evidenceFile = toPosix2(path30.relative(scanPath, file));
|
|
5086
|
+
const resources = [];
|
|
5087
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
4491
5088
|
RESOURCE_RE.lastIndex = 0;
|
|
4492
5089
|
let m;
|
|
4493
5090
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
4494
|
-
const
|
|
5091
|
+
const type = m[1];
|
|
4495
5092
|
const name = m[2];
|
|
4496
|
-
const node = makeInfraNode(
|
|
5093
|
+
const node = makeInfraNode(type, name, "aws");
|
|
4497
5094
|
if (!graph.hasNode(node.id)) {
|
|
4498
5095
|
graph.addNode(node.id, node);
|
|
4499
5096
|
nodesAdded++;
|
|
4500
5097
|
}
|
|
5098
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5099
|
+
const resource = {
|
|
5100
|
+
type,
|
|
5101
|
+
name,
|
|
5102
|
+
nodeId: node.id,
|
|
5103
|
+
body: span?.body ?? "",
|
|
5104
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5105
|
+
};
|
|
5106
|
+
resources.push(resource);
|
|
5107
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5108
|
+
}
|
|
5109
|
+
for (const resource of resources) {
|
|
5110
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5111
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5112
|
+
let ref;
|
|
5113
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5114
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5115
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5116
|
+
const target = byKey.get(key);
|
|
5117
|
+
if (!target) continue;
|
|
5118
|
+
if (seen.has(target.nodeId)) continue;
|
|
5119
|
+
seen.add(target.nodeId);
|
|
5120
|
+
const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType12.DEPENDS_ON);
|
|
5121
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5122
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5123
|
+
const edge = {
|
|
5124
|
+
id: edgeId,
|
|
5125
|
+
source: resource.nodeId,
|
|
5126
|
+
target: target.nodeId,
|
|
5127
|
+
type: EdgeType12.DEPENDS_ON,
|
|
5128
|
+
provenance: Provenance11.EXTRACTED,
|
|
5129
|
+
confidence: confidenceForExtracted9("structural"),
|
|
5130
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5131
|
+
};
|
|
5132
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5133
|
+
edgesAdded++;
|
|
5134
|
+
}
|
|
4501
5135
|
}
|
|
4502
5136
|
}
|
|
4503
|
-
return { nodesAdded, edgesAdded
|
|
5137
|
+
return { nodesAdded, edgesAdded };
|
|
4504
5138
|
}
|
|
4505
5139
|
|
|
4506
5140
|
// src/extract/infra/k8s.ts
|
|
@@ -4576,7 +5210,7 @@ import path33 from "path";
|
|
|
4576
5210
|
// src/extract/retire.ts
|
|
4577
5211
|
import { existsSync as existsSync2 } from "fs";
|
|
4578
5212
|
import path32 from "path";
|
|
4579
|
-
import { NodeType as NodeType11, Provenance as
|
|
5213
|
+
import { NodeType as NodeType11, Provenance as Provenance12 } from "@neat.is/types";
|
|
4580
5214
|
function dropOrphanedFileNodes(graph) {
|
|
4581
5215
|
const orphans = [];
|
|
4582
5216
|
graph.forEachNode((id, attrs) => {
|
|
@@ -4593,7 +5227,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
4593
5227
|
const toDrop = [];
|
|
4594
5228
|
graph.forEachEdge((id, attrs) => {
|
|
4595
5229
|
const edge = attrs;
|
|
4596
|
-
if (edge.provenance !==
|
|
5230
|
+
if (edge.provenance !== Provenance12.EXTRACTED) return;
|
|
4597
5231
|
if (!edge.evidence?.file) return;
|
|
4598
5232
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4599
5233
|
});
|
|
@@ -4606,7 +5240,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4606
5240
|
const bases = [scanPath, ...serviceDirs];
|
|
4607
5241
|
graph.forEachEdge((id, attrs) => {
|
|
4608
5242
|
const edge = attrs;
|
|
4609
|
-
if (edge.provenance !==
|
|
5243
|
+
if (edge.provenance !== Provenance12.EXTRACTED) return;
|
|
4610
5244
|
const evidenceFile = edge.evidence?.file;
|
|
4611
5245
|
if (!evidenceFile) return;
|
|
4612
5246
|
if (path32.isAbsolute(evidenceFile)) {
|
|
@@ -4687,11 +5321,12 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4687
5321
|
|
|
4688
5322
|
// src/divergences.ts
|
|
4689
5323
|
import {
|
|
5324
|
+
databaseId as databaseId3,
|
|
4690
5325
|
DivergenceResultSchema,
|
|
4691
|
-
EdgeType as
|
|
5326
|
+
EdgeType as EdgeType13,
|
|
4692
5327
|
NodeType as NodeType12,
|
|
4693
5328
|
parseEdgeId,
|
|
4694
|
-
Provenance as
|
|
5329
|
+
Provenance as Provenance13
|
|
4695
5330
|
} from "@neat.is/types";
|
|
4696
5331
|
function bucketKey(source, target, type) {
|
|
4697
5332
|
return `${type}|${source}|${target}`;
|
|
@@ -4705,17 +5340,17 @@ function bucketEdges(graph) {
|
|
|
4705
5340
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4706
5341
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4707
5342
|
switch (provenance) {
|
|
4708
|
-
case
|
|
5343
|
+
case Provenance13.EXTRACTED:
|
|
4709
5344
|
cur.extracted = e;
|
|
4710
5345
|
break;
|
|
4711
|
-
case
|
|
5346
|
+
case Provenance13.OBSERVED:
|
|
4712
5347
|
cur.observed = e;
|
|
4713
5348
|
break;
|
|
4714
|
-
case
|
|
5349
|
+
case Provenance13.INFERRED:
|
|
4715
5350
|
cur.inferred = e;
|
|
4716
5351
|
break;
|
|
4717
5352
|
default:
|
|
4718
|
-
if (e.provenance ===
|
|
5353
|
+
if (e.provenance === Provenance13.STALE) cur.stale = e;
|
|
4719
5354
|
}
|
|
4720
5355
|
buckets.set(key, cur);
|
|
4721
5356
|
});
|
|
@@ -4743,10 +5378,16 @@ function gradedConfidence(edge) {
|
|
|
4743
5378
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4744
5379
|
return clampConfidence(confidenceForEdge(edge));
|
|
4745
5380
|
}
|
|
5381
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
5382
|
+
EdgeType13.CALLS,
|
|
5383
|
+
EdgeType13.CONNECTS_TO,
|
|
5384
|
+
EdgeType13.PUBLISHES_TO,
|
|
5385
|
+
EdgeType13.CONSUMES_FROM
|
|
5386
|
+
]);
|
|
4746
5387
|
function detectMissingDivergences(graph, bucket) {
|
|
4747
5388
|
const out = [];
|
|
4748
|
-
if (bucket.type ===
|
|
4749
|
-
if (bucket.extracted && !bucket.observed) {
|
|
5389
|
+
if (bucket.type === EdgeType13.CONTAINS) return out;
|
|
5390
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
4750
5391
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4751
5392
|
out.push({
|
|
4752
5393
|
type: "missing-observed",
|
|
@@ -4786,7 +5427,7 @@ function declaredHostFor(svc) {
|
|
|
4786
5427
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4787
5428
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4788
5429
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4789
|
-
if (e.type ===
|
|
5430
|
+
if (e.type === EdgeType13.CONFIGURED_BY && e.provenance === Provenance13.EXTRACTED) {
|
|
4790
5431
|
return true;
|
|
4791
5432
|
}
|
|
4792
5433
|
}
|
|
@@ -4799,8 +5440,8 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4799
5440
|
const out = [];
|
|
4800
5441
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4801
5442
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4802
|
-
if (edge.type !==
|
|
4803
|
-
if (edge.provenance !==
|
|
5443
|
+
if (edge.type !== EdgeType13.CONNECTS_TO) continue;
|
|
5444
|
+
if (edge.provenance !== Provenance13.OBSERVED) continue;
|
|
4804
5445
|
const target = graph.getNodeAttributes(edge.target);
|
|
4805
5446
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4806
5447
|
const observedHost = target.host?.trim();
|
|
@@ -4824,8 +5465,8 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4824
5465
|
const deps = svc.dependencies ?? {};
|
|
4825
5466
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4826
5467
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4827
|
-
if (edge.type !==
|
|
4828
|
-
if (edge.provenance !==
|
|
5468
|
+
if (edge.type !== EdgeType13.CONNECTS_TO) continue;
|
|
5469
|
+
if (edge.provenance !== Provenance13.OBSERVED) continue;
|
|
4829
5470
|
const target = graph.getNodeAttributes(edge.target);
|
|
4830
5471
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4831
5472
|
for (const pair of compatPairs()) {
|
|
@@ -4880,6 +5521,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4880
5521
|
function involvesNode(d, nodeId) {
|
|
4881
5522
|
return d.source === nodeId || d.target === nodeId;
|
|
4882
5523
|
}
|
|
5524
|
+
function suppressHostMismatchHalves(all) {
|
|
5525
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
5526
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
5527
|
+
for (const d of all) {
|
|
5528
|
+
if (d.type !== "host-mismatch") continue;
|
|
5529
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
5530
|
+
declaredHalf.add(databaseId3(d.extractedHost));
|
|
5531
|
+
}
|
|
5532
|
+
if (observedHalf.size === 0) return all;
|
|
5533
|
+
return all.filter((d) => {
|
|
5534
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
5535
|
+
return false;
|
|
5536
|
+
}
|
|
5537
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
5538
|
+
return true;
|
|
5539
|
+
});
|
|
5540
|
+
}
|
|
4883
5541
|
function computeDivergences(graph, opts = {}) {
|
|
4884
5542
|
const all = [];
|
|
4885
5543
|
const buckets = bucketEdges(graph);
|
|
@@ -4893,7 +5551,8 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4893
5551
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4894
5552
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4895
5553
|
});
|
|
4896
|
-
|
|
5554
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
5555
|
+
let filtered = reconciled;
|
|
4897
5556
|
if (opts.type) {
|
|
4898
5557
|
const allowed = opts.type;
|
|
4899
5558
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -4931,7 +5590,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4931
5590
|
// src/persist.ts
|
|
4932
5591
|
import { promises as fs18 } from "fs";
|
|
4933
5592
|
import path34 from "path";
|
|
4934
|
-
import { Provenance as
|
|
5593
|
+
import { Provenance as Provenance14, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
4935
5594
|
var SCHEMA_VERSION = 4;
|
|
4936
5595
|
function migrateV1ToV2(payload) {
|
|
4937
5596
|
const nodes = payload.graph.nodes;
|
|
@@ -4953,7 +5612,7 @@ function migrateV2ToV3(payload) {
|
|
|
4953
5612
|
for (const edge of edges) {
|
|
4954
5613
|
const attrs = edge.attributes;
|
|
4955
5614
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4956
|
-
attrs.provenance =
|
|
5615
|
+
attrs.provenance = Provenance14.OBSERVED;
|
|
4957
5616
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4958
5617
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4959
5618
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -6077,6 +6736,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6077
6736
|
}
|
|
6078
6737
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6079
6738
|
});
|
|
6739
|
+
scope.get(
|
|
6740
|
+
"/graph/observed-dependencies/:nodeId",
|
|
6741
|
+
async (req, reply) => {
|
|
6742
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6743
|
+
if (!proj) return;
|
|
6744
|
+
const { nodeId } = req.params;
|
|
6745
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6746
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6747
|
+
}
|
|
6748
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
6749
|
+
}
|
|
6750
|
+
);
|
|
6080
6751
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6081
6752
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6082
6753
|
if (!proj) return;
|
|
@@ -6137,23 +6808,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6137
6808
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6138
6809
|
return { count: sliced.length, total, events: sliced };
|
|
6139
6810
|
});
|
|
6811
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
6812
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6813
|
+
if (!proj) return;
|
|
6814
|
+
const { nodeId } = req.params;
|
|
6815
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
6816
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6817
|
+
return;
|
|
6818
|
+
}
|
|
6819
|
+
const epath = errorsPathFor(proj);
|
|
6820
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6821
|
+
const events = await readErrorEvents(epath);
|
|
6822
|
+
const filtered = events.filter(
|
|
6823
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6824
|
+
);
|
|
6825
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6826
|
+
};
|
|
6140
6827
|
scope.get(
|
|
6141
6828
|
"/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
|
-
}
|
|
6829
|
+
incidentHistoryHandler
|
|
6830
|
+
);
|
|
6831
|
+
scope.get(
|
|
6832
|
+
"/graph/incident-history/:nodeId",
|
|
6833
|
+
incidentHistoryHandler
|
|
6157
6834
|
);
|
|
6158
6835
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6159
6836
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6162,16 +6839,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6162
6839
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6163
6840
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6164
6841
|
}
|
|
6165
|
-
let errorEvent;
|
|
6166
6842
|
const epath = errorsPathFor(proj);
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6843
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
6844
|
+
let errorEvent;
|
|
6845
|
+
if (req.query.errorId) {
|
|
6846
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6170
6847
|
if (!errorEvent) {
|
|
6171
6848
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6172
6849
|
}
|
|
6173
6850
|
}
|
|
6174
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
6851
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6175
6852
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6176
6853
|
return result;
|
|
6177
6854
|
});
|
|
@@ -6316,6 +6993,28 @@ function registerRoutes(scope, ctx) {
|
|
|
6316
6993
|
}
|
|
6317
6994
|
return { violations };
|
|
6318
6995
|
});
|
|
6996
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
6997
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6998
|
+
if (!proj) return;
|
|
6999
|
+
const nodeId = req.query.node;
|
|
7000
|
+
if (!nodeId) {
|
|
7001
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7002
|
+
}
|
|
7003
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7004
|
+
let policies = [];
|
|
7005
|
+
if (policyPath) {
|
|
7006
|
+
try {
|
|
7007
|
+
policies = await loadPolicyFile(policyPath);
|
|
7008
|
+
} catch (err) {
|
|
7009
|
+
return reply.code(400).send({
|
|
7010
|
+
error: "policy.json failed to parse",
|
|
7011
|
+
details: err.message
|
|
7012
|
+
});
|
|
7013
|
+
}
|
|
7014
|
+
}
|
|
7015
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7016
|
+
return { node: nodeId, applicable };
|
|
7017
|
+
});
|
|
6319
7018
|
scope.post("/policies/check", async (req, reply) => {
|
|
6320
7019
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6321
7020
|
if (!proj) return;
|
|
@@ -6606,6 +7305,7 @@ export {
|
|
|
6606
7305
|
discoverServices,
|
|
6607
7306
|
addServiceNodes,
|
|
6608
7307
|
addServiceAliases,
|
|
7308
|
+
addFiles,
|
|
6609
7309
|
addImports,
|
|
6610
7310
|
addDatabasesAndCompat,
|
|
6611
7311
|
addConfigNodes,
|
|
@@ -6644,4 +7344,4 @@ export {
|
|
|
6644
7344
|
pruneRegistry,
|
|
6645
7345
|
buildApi
|
|
6646
7346
|
};
|
|
6647
|
-
//# sourceMappingURL=chunk-
|
|
7347
|
+
//# sourceMappingURL=chunk-OQ7CHOY2.js.map
|