@takodotid/circuit 0.34.3 → 0.36.0
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/README.md +14 -15
- package/dist/chunks/index-20kcjxjg.js +174 -0
- package/dist/chunks/index-2r4bbhac.js +977 -0
- package/dist/chunks/index-n7w3tg5y.js +149 -0
- package/dist/chunks/new-w3zys5y4.js +172 -0
- package/dist/chunks/server-gzsf5me5.js +259 -0
- package/dist/cli/index.js +11345 -9024
- package/dist/helpers/index.js +594 -0
- package/dist/index.js +9 -28
- package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
- package/dist/types/core/address.d.ts +44 -0
- package/dist/types/core/cabling.d.ts +41 -0
- package/dist/types/core/cross-checks.d.ts +28 -0
- package/dist/types/core/define.d.ts +25 -3
- package/dist/types/core/project-files.d.ts +12 -0
- package/dist/types/core/schema-docs.d.ts +13 -4
- package/dist/types/core/secrets.d.ts +62 -21
- package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
- package/dist/types/core/utils.d.ts +8 -0
- package/dist/types/core/validator.d.ts +19 -0
- package/dist/types/helpers/bgp.d.ts +97 -0
- package/dist/types/helpers/checks.d.ts +32 -0
- package/dist/types/helpers/exchange.d.ts +49 -0
- package/dist/types/helpers/filters.d.ts +51 -0
- package/dist/types/helpers/index.d.ts +10 -0
- package/dist/types/helpers/route-server.d.ts +52 -0
- package/dist/types/index.d.ts +5 -3
- package/dist/types/integrations/librenms.d.ts +60 -0
- package/dist/types/integrations/netbox.d.ts +89 -0
- package/dist/types/integrations/peeringdb.d.ts +24 -0
- package/dist/types/integrations/prometheus.d.ts +24 -0
- package/dist/types/integrations/registries.d.ts +8 -0
- package/dist/types/platforms/bgp-wait.d.ts +18 -0
- package/dist/types/platforms/catalog.d.ts +109 -0
- package/dist/types/platforms/derive.d.ts +17 -0
- package/dist/types/platforms/fortios/firewall.d.ts +45 -0
- package/dist/types/platforms/fortios/index.d.ts +134 -0
- package/dist/types/platforms/fortios/names.d.ts +52 -0
- package/dist/types/platforms/fortios/plan.d.ts +104 -0
- package/dist/types/platforms/fortios/render.d.ts +75 -0
- package/dist/types/platforms/fortios/routing.d.ts +56 -0
- package/dist/types/platforms/fortios/tree.d.ts +37 -0
- package/dist/types/platforms/index.d.ts +13 -0
- package/dist/types/platforms/junos/bgp.d.ts +44 -0
- package/dist/types/platforms/junos/index.d.ts +114 -0
- package/dist/types/platforms/junos/render.d.ts +154 -0
- package/dist/types/platforms/line-diff.d.ts +67 -0
- package/dist/types/platforms/platform.d.ts +120 -0
- package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
- package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
- package/dist/types/platforms/routeros/export.d.ts +59 -0
- package/dist/types/platforms/routeros/index.d.ts +96 -0
- package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
- package/dist/types/platforms/routeros/plan.d.ts +54 -0
- package/dist/types/platforms/routeros/render.d.ts +240 -0
- package/dist/types/platforms/vrp/index.d.ts +83 -0
- package/dist/types/platforms/vrp/render.d.ts +96 -0
- package/dist/types/schema/common.d.ts +12 -6
- package/dist/types/schema/index.d.ts +1 -0
- package/dist/types/schema/interfaces.d.ts +14 -6
- package/dist/types/schema/policy.d.ts +18 -10
- package/dist/types/schema/routing.d.ts +14 -1
- package/dist/types/schema/system.d.ts +16 -6
- package/dist/types/schema/time-zones.d.ts +2 -0
- package/dist/types/transport/console.d.ts +79 -23
- package/dist/types/transport/path-check.d.ts +32 -0
- package/dist/types/transport/ssh.d.ts +17 -15
- package/package.json +18 -12
- package/src/cli/graph-format.ts +91 -0
- package/src/cli/index.ts +1074 -484
- package/src/cli/new.ts +208 -175
- package/src/cli/rename.ts +97 -91
- package/src/core/address-space.ts +320 -0
- package/src/core/address.ts +202 -0
- package/src/core/cabling.ts +154 -0
- package/src/core/cross-checks.ts +364 -0
- package/src/core/define.ts +26 -3
- package/src/core/project-files.ts +35 -0
- package/src/core/schema-docs.ts +138 -130
- package/src/core/secrets.ts +170 -76
- package/src/core/topology.ts +656 -0
- package/src/core/utils.ts +37 -0
- package/src/core/validator.ts +407 -0
- package/src/helpers/bgp.ts +301 -0
- package/src/helpers/checks.ts +178 -0
- package/src/helpers/exchange.ts +117 -0
- package/src/helpers/filters.ts +107 -0
- package/src/helpers/index.ts +12 -0
- package/src/helpers/route-server.ts +212 -0
- package/src/index.ts +6 -3
- package/src/integrations/librenms.ts +291 -0
- package/src/integrations/netbox.ts +620 -0
- package/src/integrations/peeringdb.ts +173 -0
- package/src/integrations/prometheus.ts +90 -0
- package/src/integrations/registries.ts +25 -0
- package/src/platforms/bgp-wait.ts +41 -0
- package/src/platforms/catalog.ts +69 -0
- package/src/platforms/derive.ts +117 -0
- package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
- package/src/platforms/fortios/firewall.ts +377 -0
- package/src/platforms/fortios/index.ts +1060 -0
- package/src/platforms/fortios/names.ts +130 -0
- package/src/platforms/fortios/plan.ts +929 -0
- package/src/platforms/fortios/render.ts +779 -0
- package/src/platforms/fortios/routing.ts +476 -0
- package/src/platforms/fortios/tree.ts +129 -0
- package/src/platforms/index.ts +36 -0
- package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
- package/src/platforms/junos/bgp.ts +293 -0
- package/src/platforms/junos/index.ts +1006 -0
- package/src/platforms/junos/render.ts +967 -0
- package/src/platforms/line-diff.ts +244 -0
- package/src/platforms/platform.ts +122 -0
- package/src/platforms/raisecom-ros/TRAPS.md +40 -0
- package/src/platforms/raisecom-ros/index.ts +731 -0
- package/src/platforms/raisecom-ros/render.ts +328 -0
- package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
- package/src/platforms/routeros/export.ts +342 -0
- package/src/platforms/routeros/index.ts +636 -0
- package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
- package/src/platforms/routeros/plan.ts +444 -0
- package/src/platforms/routeros/render.ts +1483 -0
- package/src/platforms/vrp/TRAPS.md +45 -0
- package/src/platforms/vrp/index.ts +652 -0
- package/src/platforms/vrp/render.ts +615 -0
- package/src/schema/common.ts +15 -6
- package/src/schema/index.ts +1 -0
- package/src/schema/interfaces.ts +15 -5
- package/src/schema/policy.ts +22 -10
- package/src/schema/routing.ts +16 -1
- package/src/schema/system.ts +16 -6
- package/src/schema/time-zones.ts +482 -0
- package/src/studio/data.ts +172 -168
- package/src/studio/git.ts +95 -90
- package/src/studio/public/app.js +161 -150
- package/src/studio/public/compare.js +219 -212
- package/src/studio/public/diff.js +77 -75
- package/src/studio/public/dom.js +118 -108
- package/src/studio/public/evaluate.js +584 -545
- package/src/studio/public/export.js +120 -118
- package/src/studio/public/graph.js +1343 -1266
- package/src/studio/public/net.js +115 -109
- package/src/studio/public/tables.js +1135 -1083
- package/src/studio/public/tools.js +561 -529
- package/src/studio/server.ts +202 -196
- package/src/transport/console.ts +363 -271
- package/src/transport/path-check.ts +91 -0
- package/src/transport/ssh.ts +98 -94
- package/templates/colocation/checks.ts +11 -6
- package/templates/colocation/edge-01.ts +9 -9
- package/templates/colocation/routing.ts +2 -2
- package/templates/colocation/tenancy.ts +3 -3
- package/templates/colocation/tor-01.ts +3 -3
- package/templates/edge-router/checks.ts +4 -4
- package/templates/edge-router/edge-01.ts +5 -5
- package/templates/edge-router/routing.ts +2 -2
- package/templates/project/AGENTS.md +5 -1
- package/templates/single-site/checks.ts +2 -2
- package/dist/chunks/index-47debt16.js +0 -956
- package/dist/chunks/index-v8zgr7wm.js +0 -128
- package/dist/chunks/new-ny89s7y7.js +0 -166
- package/dist/chunks/server-tjbb3rqc.js +0 -251
- package/dist/presets/index.js +0 -394
- package/dist/types/adapters/devices/catalog.d.ts +0 -86
- package/dist/types/adapters/devices/derive.d.ts +0 -7
- package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
- package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
- package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
- package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
- package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
- package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
- package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
- package/dist/types/adapters/devices/index.d.ts +0 -3
- package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
- package/dist/types/adapters/devices/junos/index.d.ts +0 -42
- package/dist/types/adapters/devices/junos/render.d.ts +0 -52
- package/dist/types/adapters/devices/lines.d.ts +0 -56
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
- package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
- package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
- package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
- package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
- package/dist/types/adapters/devices/types.d.ts +0 -40
- package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
- package/dist/types/adapters/registries/index.d.ts +0 -3
- package/dist/types/adapters/registries/librenms.d.ts +0 -15
- package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
- package/dist/types/adapters/registries/prometheus.d.ts +0 -16
- package/dist/types/core/across.d.ts +0 -4
- package/dist/types/core/addr.d.ts +0 -34
- package/dist/types/core/files.d.ts +0 -6
- package/dist/types/core/merge.d.ts +0 -4
- package/dist/types/core/time.d.ts +0 -2
- package/dist/types/core/validate.d.ts +0 -2
- package/dist/types/presets/index.d.ts +0 -183
- package/src/adapters/devices/catalog.ts +0 -41
- package/src/adapters/devices/derive.ts +0 -59
- package/src/adapters/devices/fortios/firewall.ts +0 -362
- package/src/adapters/devices/fortios/index.ts +0 -923
- package/src/adapters/devices/fortios/names.ts +0 -115
- package/src/adapters/devices/fortios/plan.ts +0 -888
- package/src/adapters/devices/fortios/render.ts +0 -768
- package/src/adapters/devices/fortios/routing.ts +0 -444
- package/src/adapters/devices/fortios/tree.ts +0 -121
- package/src/adapters/devices/index.ts +0 -9
- package/src/adapters/devices/junos/bgp.ts +0 -269
- package/src/adapters/devices/junos/index.ts +0 -862
- package/src/adapters/devices/junos/render.ts +0 -872
- package/src/adapters/devices/lines.ts +0 -212
- package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
- package/src/adapters/devices/raisecom-ros/index.ts +0 -700
- package/src/adapters/devices/routeros/export.ts +0 -321
- package/src/adapters/devices/routeros/index.ts +0 -346
- package/src/adapters/devices/routeros/plan.ts +0 -398
- package/src/adapters/devices/routeros/render.ts +0 -1490
- package/src/adapters/devices/types.ts +0 -38
- package/src/adapters/devices/vrp/TRAPS.md +0 -26
- package/src/adapters/devices/vrp/index.ts +0 -865
- package/src/adapters/registries/index.ts +0 -19
- package/src/adapters/registries/librenms.ts +0 -92
- package/src/adapters/registries/peeringdb.ts +0 -169
- package/src/adapters/registries/prometheus.ts +0 -71
- package/src/cli/graph.ts +0 -89
- package/src/core/across.ts +0 -350
- package/src/core/addr.ts +0 -174
- package/src/core/addresses.ts +0 -316
- package/src/core/files.ts +0 -32
- package/src/core/graph.ts +0 -640
- package/src/core/merge.ts +0 -19
- package/src/core/time.ts +0 -15
- package/src/core/validate.ts +0 -348
- package/src/presets/index.ts +0 -579
- /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
- /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
- /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
- /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// What Studio's tools work out from the config: where an address is and how each device would reach it, the VLAN path between two devices, which firewall rule a packet meets, and what a route policy does to a route.
|
|
3
3
|
// They follow the schema's own description of each field, read from the files: a device can still behave otherwise, so they say what the config means, not what the device did.
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Net } from "./net.js";
|
|
6
6
|
|
|
7
7
|
/** @typedef {import("../../schema").Device} Device */
|
|
8
8
|
/** @typedef {import("../../schema").FilterMatch<string, string>} FilterMatch */
|
|
@@ -11,638 +11,677 @@ import { covers, familyOf, networkOf, parseRange, samePrefix } from "./net.js";
|
|
|
11
11
|
/** @typedef {import("../../schema").Match<string>} RouteMatch */
|
|
12
12
|
/** @typedef {import("../../schema").Sets} Sets */
|
|
13
13
|
/** @typedef {import("../../schema").Ports} Ports */
|
|
14
|
-
/** @typedef {import("../../core/
|
|
14
|
+
/** @typedef {import("../../core/topology").GraphEdge} GraphEdge */
|
|
15
15
|
/** @typedef {import("../data").StudioData} StudioData */
|
|
16
16
|
|
|
17
17
|
/** Whether a condition holds: yes, no, or not known from what was given. @typedef {"yes" | "no" | "unknown"} Answer */
|
|
18
18
|
/** @typedef {{ answer: Answer, why: string[] }} Check */
|
|
19
|
+
/** @typedef {{ device: string, table: string, kind: "local" | "connected" | "static" | "none", prefix?: string, via?: string, through?: string, note?: string }} RouteFound */
|
|
20
|
+
/** @typedef {{ device: string, where: string, rule: number, description?: string, action: string, side: string }} RuleFound */
|
|
21
|
+
/** @typedef {{ family?: "ipv4" | "ipv6", protocol?: string, src?: string, dst?: string, srcPort?: number, dstPort?: number, inInterface?: string, outInterface?: string, state?: string, flags?: string[] }} Packet */
|
|
22
|
+
/** @typedef {{ prefix: string, asPathLength?: number, rpki?: string, communities: string[], largeCommunities: string[], inRegistrySets: string[] }} Route */
|
|
23
|
+
/** @typedef {{ localPref?: number, med?: number, prepend: number, communities: string[], largeCommunities: string[], nextHop?: string, preferredSource?: string, blackhole?: boolean }} Attributes */
|
|
24
|
+
/** @typedef {{ policy: string, rule: number, description?: string, check: Check, changed: string[], decision?: string, call?: string }} PolicyStep */
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
export class Evaluate {
|
|
27
|
+
/**
|
|
28
|
+
* Every condition must hold. One that does not decides; otherwise one not known leaves the whole not known.
|
|
29
|
+
*
|
|
30
|
+
* @param {Check[]} checks
|
|
31
|
+
* @returns {Check}
|
|
32
|
+
*/
|
|
33
|
+
static #all(checks) {
|
|
34
|
+
const no = checks.filter((check) => check.answer === "no");
|
|
35
|
+
if (no.length) return { answer: "no", why: no.flatMap((check) => check.why) };
|
|
29
36
|
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
const unknown = checks.filter((check) => check.answer === "unknown");
|
|
38
|
+
if (unknown.length) return { answer: "unknown", why: unknown.flatMap((check) => check.why) };
|
|
32
39
|
|
|
33
|
-
|
|
34
|
-
}
|
|
40
|
+
return { answer: "yes", why: [] };
|
|
41
|
+
}
|
|
35
42
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
/**
|
|
44
|
+
* @param {boolean} holds
|
|
45
|
+
* @param {string} why
|
|
46
|
+
* @returns {Check}
|
|
47
|
+
*/
|
|
48
|
+
static #test(holds, why) {
|
|
49
|
+
return { answer: holds ? "yes" : "no", why: holds ? [] : [why] };
|
|
50
|
+
}
|
|
42
51
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/**
|
|
53
|
+
* @param {string} why
|
|
54
|
+
* @returns {Check}
|
|
55
|
+
*/
|
|
56
|
+
static #unknown(why) {
|
|
57
|
+
return { answer: "unknown", why: [why] };
|
|
58
|
+
}
|
|
48
59
|
|
|
49
|
-
// Where an address is.
|
|
60
|
+
// Where an address is.
|
|
50
61
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The addresses on a device's ports and interfaces, with where each is and the VRF it routes in.
|
|
64
|
+
*
|
|
65
|
+
* @param {Device} device
|
|
66
|
+
*/
|
|
67
|
+
static #addressesOf(device) {
|
|
68
|
+
const routed =
|
|
69
|
+
/** @type {[string, { addresses?: readonly (string | { address: string, peer: string })[], vrf?: string } | undefined][]} */ ([
|
|
70
|
+
...Object.entries(device.ports ?? {}),
|
|
71
|
+
...Object.entries(device.interfaces ?? {}),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
return routed.flatMap(([name, settings]) =>
|
|
75
|
+
(settings?.addresses ?? []).map((entry) => ({
|
|
76
|
+
name,
|
|
77
|
+
address: typeof entry === "string" ? entry : entry.address,
|
|
78
|
+
peer: typeof entry === "string" ? undefined : entry.peer,
|
|
79
|
+
vrf: settings?.vrf,
|
|
80
|
+
}))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
53
83
|
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
peer: typeof entry === "string" ? undefined : entry.peer,
|
|
71
|
-
vrf: settings?.vrf,
|
|
72
|
-
}))
|
|
73
|
-
);
|
|
74
|
-
}
|
|
84
|
+
/**
|
|
85
|
+
* Where an address is in the network, how each device would reach it from what the files say, and every rule that names it.
|
|
86
|
+
*
|
|
87
|
+
* @param {StudioData} data
|
|
88
|
+
* @param {string} text an address, such as `10.0.10.5`
|
|
89
|
+
*/
|
|
90
|
+
static trace(data, text) {
|
|
91
|
+
const target = Net.parseRange(text);
|
|
92
|
+
if (!target || target.first !== target.last) return undefined;
|
|
93
|
+
|
|
94
|
+
// Who holds it, and the segment or block it is in.
|
|
95
|
+
const held = data.devices.flatMap((device) =>
|
|
96
|
+
Evaluate.#addressesOf(device)
|
|
97
|
+
.filter((entry) => Net.samePrefix(entry.address.split("/")[0] ?? "", text))
|
|
98
|
+
.map((entry) => ({ device: device.name, where: entry.name, address: entry.address }))
|
|
99
|
+
);
|
|
75
100
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
const { spaces, elsewhere } = data.addresses;
|
|
102
|
+
const segments = [...spaces.flatMap((space) => space.segments), ...elsewhere.segments];
|
|
103
|
+
const blocks = [...spaces.flatMap((space) => space.blocks), ...elsewhere.blocks];
|
|
104
|
+
const segment = segments.find((candidate) => Net.covers(candidate.prefix, text));
|
|
105
|
+
const block = blocks.find((candidate) => Net.covers(candidate.prefix, text));
|
|
106
|
+
const space = spaces.find((candidate) => Net.covers(candidate.prefix, text));
|
|
107
|
+
const uses = segment?.uses.filter((use) => {
|
|
108
|
+
const [first, last] = use.address.split("-");
|
|
109
|
+
if (!last) return Net.samePrefix(first ?? "", text);
|
|
110
|
+
const from = Net.parseRange(first ?? "");
|
|
111
|
+
const to = Net.parseRange(last);
|
|
112
|
+
return Boolean(from && to && from.first <= target.first && target.first <= to.first);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// How each device would reach it: the longest prefix that covers it, in each routing table, among its own addresses and its static routes.
|
|
116
|
+
/** @type {RouteFound[]} */
|
|
117
|
+
const routes = [];
|
|
118
|
+
|
|
119
|
+
for (const device of data.devices) {
|
|
120
|
+
/** @type {Map<string, RouteFound & { length: number, rank: number }>} */
|
|
121
|
+
const best = new Map();
|
|
122
|
+
|
|
123
|
+
/** @param {RouteFound & { length: number, rank: number }} found */
|
|
124
|
+
const offer = (found) => {
|
|
125
|
+
const current = best.get(found.table);
|
|
126
|
+
const better = !current || found.length > current.length || (found.length === current.length && found.rank < current.rank);
|
|
127
|
+
if (better) best.set(found.table, found);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
for (const entry of Evaluate.#addressesOf(device)) {
|
|
131
|
+
const table = entry.vrf ? `VRF ${entry.vrf}` : "main";
|
|
132
|
+
const range = Net.parseRange(entry.address);
|
|
133
|
+
if (!range) continue;
|
|
134
|
+
|
|
135
|
+
if (Net.samePrefix(entry.address.split("/")[0] ?? "", text)) {
|
|
136
|
+
offer({ device: device.name, table, kind: "local", prefix: entry.address, through: entry.name, length: 129, rank: 0 });
|
|
137
|
+
} else if (entry.peer && Net.samePrefix(entry.peer, text)) {
|
|
138
|
+
offer({
|
|
139
|
+
device: device.name,
|
|
140
|
+
table,
|
|
141
|
+
kind: "connected",
|
|
142
|
+
prefix: `${entry.peer} as the peer of ${entry.address}`,
|
|
143
|
+
through: entry.name,
|
|
144
|
+
length: 128,
|
|
145
|
+
rank: 1,
|
|
146
|
+
});
|
|
147
|
+
} else if (!entry.peer && Net.covers(entry.address, text)) {
|
|
148
|
+
const network = Net.prefixOf(entry.address);
|
|
149
|
+
offer({
|
|
150
|
+
device: device.name,
|
|
151
|
+
table,
|
|
152
|
+
kind: "connected",
|
|
153
|
+
prefix: network,
|
|
154
|
+
through: entry.name,
|
|
155
|
+
length: range.length,
|
|
156
|
+
rank: 1,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
121
160
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!range) continue;
|
|
161
|
+
for (const route of device.routing?.static ?? []) {
|
|
162
|
+
const range = Net.parseRange(route.prefix);
|
|
163
|
+
if (!range || !Net.covers(route.prefix, text)) continue;
|
|
126
164
|
|
|
127
|
-
|
|
128
|
-
offer({ device: device.name, table, kind: "local", prefix: entry.address, through: entry.name, length: 129, rank: 0 });
|
|
129
|
-
} else if (entry.peer && samePrefix(entry.peer, text)) {
|
|
130
|
-
offer({
|
|
131
|
-
device: device.name,
|
|
132
|
-
table,
|
|
133
|
-
kind: "connected",
|
|
134
|
-
prefix: `${entry.peer} as the peer of ${entry.address}`,
|
|
135
|
-
through: entry.name,
|
|
136
|
-
length: 128,
|
|
137
|
-
rank: 1,
|
|
138
|
-
});
|
|
139
|
-
} else if (!entry.peer && covers(entry.address, text)) {
|
|
140
|
-
const network = networkOf(entry.address);
|
|
165
|
+
const table = route.vrf ? `VRF ${route.vrf}` : "main";
|
|
141
166
|
offer({
|
|
142
167
|
device: device.name,
|
|
143
168
|
table,
|
|
144
|
-
kind: "
|
|
145
|
-
prefix:
|
|
146
|
-
|
|
169
|
+
kind: "static",
|
|
170
|
+
prefix: route.prefix,
|
|
171
|
+
via: route.via,
|
|
172
|
+
through: route.interface,
|
|
173
|
+
note: route.blackhole ? "discarded: a blackhole route" : route.description,
|
|
147
174
|
length: range.length,
|
|
148
|
-
rank: 1,
|
|
175
|
+
rank: 2 + (route.distance ?? 1),
|
|
149
176
|
});
|
|
150
177
|
}
|
|
151
|
-
}
|
|
152
178
|
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
offer({
|
|
159
|
-
device: device.name,
|
|
160
|
-
table,
|
|
161
|
-
kind: "static",
|
|
162
|
-
prefix: route.prefix,
|
|
163
|
-
via: route.via,
|
|
164
|
-
through: route.interface,
|
|
165
|
-
note: route.blackhole ? "discarded: a blackhole route" : route.description,
|
|
166
|
-
length: range.length,
|
|
167
|
-
rank: 2 + (route.distance ?? 1),
|
|
168
|
-
});
|
|
179
|
+
if (!best.size) routes.push({ device: device.name, table: "main", kind: "none" });
|
|
180
|
+
for (const found of best.values()) {
|
|
181
|
+
const { length: _length, rank: _rank, ...route } = found;
|
|
182
|
+
routes.push(route);
|
|
183
|
+
}
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
// Every rule that names it, by address, prefix or address set.
|
|
187
|
+
/** @type {RuleFound[]} */
|
|
188
|
+
const rules = [];
|
|
189
|
+
|
|
190
|
+
for (const device of data.devices) {
|
|
191
|
+
const sets = device.firewall?.address_sets ?? {};
|
|
192
|
+
const inSet = (/** @type {string | undefined} */ name) =>
|
|
193
|
+
Boolean(name && (sets[name] ?? []).some((entry) => Net.covers(entry, text)));
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @param {string} where
|
|
197
|
+
* @param {readonly { description?: string, match?: { src?: string, dst?: string, src_set?: string, dst_set?: string }, action: string }[]} list
|
|
198
|
+
*/
|
|
199
|
+
const look = (where, list) => {
|
|
200
|
+
for (const [index, rule] of list.entries()) {
|
|
201
|
+
const match = rule.match ?? {};
|
|
202
|
+
const sides = [
|
|
203
|
+
(match.src && Net.covers(match.src, text)) || inSet(match.src_set) ? "source" : "",
|
|
204
|
+
(match.dst && Net.covers(match.dst, text)) || inSet(match.dst_set) ? "destination" : "",
|
|
205
|
+
].filter(Boolean);
|
|
206
|
+
if (sides.length)
|
|
207
|
+
rules.push({
|
|
208
|
+
device: device.name,
|
|
209
|
+
where,
|
|
210
|
+
rule: index + 1,
|
|
211
|
+
description: rule.description,
|
|
212
|
+
action: rule.action,
|
|
213
|
+
side: sides.join(" and "),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
185
217
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const look = (where, list) => {
|
|
191
|
-
for (const [index, rule] of list.entries()) {
|
|
192
|
-
const match = rule.match ?? {};
|
|
193
|
-
const sides = [
|
|
194
|
-
(match.src && covers(match.src, text)) || inSet(match.src_set) ? "source" : "",
|
|
195
|
-
(match.dst && covers(match.dst, text)) || inSet(match.dst_set) ? "destination" : "",
|
|
196
|
-
].filter(Boolean);
|
|
197
|
-
if (sides.length)
|
|
198
|
-
rules.push({
|
|
199
|
-
device: device.name,
|
|
200
|
-
where,
|
|
201
|
-
rule: index + 1,
|
|
202
|
-
description: rule.description,
|
|
203
|
-
action: rule.action,
|
|
204
|
-
side: sides.join(" and "),
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
};
|
|
218
|
+
for (const [chain, settings] of Object.entries(device.firewall?.filter ?? {})) look(`filter ${chain}`, settings?.rules ?? []);
|
|
219
|
+
for (const [chain, list] of Object.entries(device.firewall?.nat ?? {})) look(`NAT ${chain}`, list ?? []);
|
|
220
|
+
for (const [name, list] of Object.entries(device.acls ?? {})) look(`ACL ${name}`, list);
|
|
221
|
+
}
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
for (const [chain, list] of Object.entries(device.firewall?.nat ?? {})) look(`NAT ${chain}`, list ?? []);
|
|
211
|
-
for (const [name, list] of Object.entries(device.acls ?? {})) look(`ACL ${name}`, list);
|
|
223
|
+
return { address: text, family: target.family, held, segment, uses: uses ?? [], block, space, routes, rules };
|
|
212
224
|
}
|
|
213
225
|
|
|
214
|
-
|
|
215
|
-
}
|
|
226
|
+
// The VLAN path between two devices.
|
|
216
227
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const goal = `device:${to}`;
|
|
231
|
-
const cables = data.graph.edges.filter((edge) => edge.layer === "l2" && edge.vlans?.some((carried) => carried.id === vlan));
|
|
232
|
-
|
|
233
|
-
/** @type {Map<string, GraphEdge | null>} */
|
|
234
|
-
const cameBy = new Map([[start, null]]);
|
|
235
|
-
const queue = [start];
|
|
236
|
-
|
|
237
|
-
for (let at = queue.shift(); at !== undefined; at = queue.shift()) {
|
|
238
|
-
if (at === goal) break;
|
|
239
|
-
|
|
240
|
-
for (const cable of cables) {
|
|
241
|
-
const next = cable.from === at ? cable.to : cable.to === at ? cable.from : undefined;
|
|
242
|
-
if (next === undefined || cameBy.has(next)) continue;
|
|
243
|
-
|
|
244
|
-
cameBy.set(next, cable);
|
|
245
|
-
queue.push(next);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
228
|
+
/**
|
|
229
|
+
* The fewest cables that carry a VLAN from one device to another, from the graph's VLAN layer. Undefined when no such path exists.
|
|
230
|
+
*
|
|
231
|
+
* @param {StudioData} data
|
|
232
|
+
* @param {number} vlan
|
|
233
|
+
* @param {string} from device name
|
|
234
|
+
* @param {string} to device name
|
|
235
|
+
* @returns {GraphEdge[] | undefined}
|
|
236
|
+
*/
|
|
237
|
+
static vlanPath(data, vlan, from, to) {
|
|
238
|
+
const start = `device:${from}`;
|
|
239
|
+
const goal = `device:${to}`;
|
|
240
|
+
const cables = data.graph.edges.filter((edge) => edge.layer === "l2" && edge.vlans?.some((carried) => carried.id === vlan));
|
|
248
241
|
|
|
249
|
-
|
|
242
|
+
/** @type {Map<string, GraphEdge | null>} */
|
|
243
|
+
const cameBy = new Map([[start, null]]);
|
|
244
|
+
const queue = [start];
|
|
250
245
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
for (let at = goal, cable = cameBy.get(goal); cable; cable = cameBy.get(at)) {
|
|
254
|
-
path.unshift(cable);
|
|
255
|
-
at = cable.from === at ? cable.to : cable.from;
|
|
256
|
-
}
|
|
246
|
+
for (let at = queue.shift(); at !== undefined; at = queue.shift()) {
|
|
247
|
+
if (at === goal) break;
|
|
257
248
|
|
|
258
|
-
|
|
259
|
-
|
|
249
|
+
for (const cable of cables) {
|
|
250
|
+
const next = cable.from === at ? cable.to : cable.to === at ? cable.from : undefined;
|
|
251
|
+
if (next === undefined || cameBy.has(next)) continue;
|
|
260
252
|
|
|
261
|
-
|
|
253
|
+
cameBy.set(next, cable);
|
|
254
|
+
queue.push(next);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
262
257
|
|
|
263
|
-
|
|
258
|
+
if (!cameBy.has(goal)) return undefined;
|
|
264
259
|
|
|
265
|
-
/**
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return list.some((entry) => (typeof entry === "number" ? entry === port : entry.min <= port && port <= entry.max));
|
|
272
|
-
}
|
|
260
|
+
/** @type {GraphEdge[]} */
|
|
261
|
+
const path = [];
|
|
262
|
+
for (let at = goal, cable = cameBy.get(goal); cable; cable = cameBy.get(at)) {
|
|
263
|
+
path.unshift(cable);
|
|
264
|
+
at = cable.from === at ? cable.to : cable.from;
|
|
265
|
+
}
|
|
273
266
|
|
|
274
|
-
|
|
275
|
-
* Whether a packet meets a rule's conditions, as the schema describes each one.
|
|
276
|
-
*
|
|
277
|
-
* @param {FilterMatch} match
|
|
278
|
-
* @param {Packet} packet
|
|
279
|
-
* @param {Record<string, readonly string[]>} sets the device's address sets
|
|
280
|
-
* @returns {Check}
|
|
281
|
-
*/
|
|
282
|
-
export function packetMeets(match, packet, sets) {
|
|
283
|
-
/** @type {Check[]} */
|
|
284
|
-
const checks = [];
|
|
285
|
-
const family = packet.family ?? familyOf(packet.src ?? "") ?? familyOf(packet.dst ?? "");
|
|
286
|
-
|
|
287
|
-
if (match.family) checks.push(family ? test(match.family === family, `it is for ${match.family} only`) : unknown("its family"));
|
|
288
|
-
|
|
289
|
-
// A rule with addresses of one family applies to that family only.
|
|
290
|
-
for (const address of [match.src, match.dst]) {
|
|
291
|
-
const own = address ? familyOf(address) : undefined;
|
|
292
|
-
if (own && family && own !== family) checks.push(test(false, `it names ${own} addresses`));
|
|
267
|
+
return path;
|
|
293
268
|
}
|
|
294
269
|
|
|
295
|
-
|
|
296
|
-
checks.push(packet.protocol ? test(match.protocol === packet.protocol, `it is for ${match.protocol}`) : unknown("the protocol"));
|
|
270
|
+
// Which firewall rule a packet meets.
|
|
297
271
|
|
|
298
272
|
/**
|
|
299
|
-
* @param {
|
|
300
|
-
* @param {
|
|
301
|
-
* @param {string} side
|
|
273
|
+
* @param {Ports} ports
|
|
274
|
+
* @param {number} port
|
|
302
275
|
*/
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
address(match.src, packet.src, "source");
|
|
309
|
-
address(match.dst, packet.dst, "destination");
|
|
276
|
+
static #portIn(ports, port) {
|
|
277
|
+
const list = Array.isArray(ports) ? ports : [ports];
|
|
278
|
+
return list.some((entry) => (typeof entry === "number" ? entry === port : entry.min <= port && port <= entry.max));
|
|
279
|
+
}
|
|
310
280
|
|
|
311
281
|
/**
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
* @param {
|
|
282
|
+
* Whether a packet meets a rule's conditions, as the schema describes each one.
|
|
283
|
+
*
|
|
284
|
+
* @param {FilterMatch} match
|
|
285
|
+
* @param {Packet} packet
|
|
286
|
+
* @param {Record<string, readonly string[]>} sets the device's address sets
|
|
287
|
+
* @returns {Check}
|
|
315
288
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
given
|
|
321
|
-
? test(
|
|
322
|
-
members.some((entry) => covers(entry, given)),
|
|
323
|
-
`its ${side} is in the set ${name}`
|
|
324
|
-
)
|
|
325
|
-
: unknown(`the ${side} address`)
|
|
326
|
-
);
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
set(match.src_set, packet.src, "source");
|
|
330
|
-
set(match.dst_set, packet.dst, "destination");
|
|
289
|
+
static #packetMeets(match, packet, sets) {
|
|
290
|
+
/** @type {Check[]} */
|
|
291
|
+
const checks = [];
|
|
292
|
+
const family = packet.family ?? Net.family(packet.src ?? "") ?? Net.family(packet.dst ?? "");
|
|
331
293
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
*/
|
|
337
|
-
const port = (rule, given, side) => {
|
|
338
|
-
if (rule === undefined) return;
|
|
339
|
-
if (packet.protocol && packet.protocol !== "tcp" && packet.protocol !== "udp")
|
|
340
|
-
return checks.push(test(false, "it names ports, which only TCP and UDP have"));
|
|
341
|
-
checks.push(
|
|
342
|
-
given !== undefined ? test(portIn(rule, given), `its ${side} port is ${JSON.stringify(rule)}`) : unknown(`the ${side} port`)
|
|
343
|
-
);
|
|
344
|
-
};
|
|
294
|
+
if (match.family)
|
|
295
|
+
checks.push(
|
|
296
|
+
family ? Evaluate.#test(match.family === family, `it is for ${match.family} only`) : Evaluate.#unknown("its family")
|
|
297
|
+
);
|
|
345
298
|
|
|
346
|
-
|
|
347
|
-
|
|
299
|
+
// A rule with addresses of one family applies to that family only.
|
|
300
|
+
for (const address of [match.src, match.dst]) {
|
|
301
|
+
const own = address ? Net.family(address) : undefined;
|
|
302
|
+
if (own && family && own !== family) checks.push(Evaluate.#test(false, `it names ${own} addresses`));
|
|
303
|
+
}
|
|
348
304
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (match.out_interface)
|
|
356
|
-
checks.push(
|
|
357
|
-
packet.outInterface
|
|
358
|
-
? test(match.out_interface === packet.outInterface, `it is for packets leaving through ${match.out_interface}`)
|
|
359
|
-
: unknown("where the packet leaves")
|
|
360
|
-
);
|
|
361
|
-
if (match.state)
|
|
362
|
-
checks.push(
|
|
363
|
-
packet.state
|
|
364
|
-
? test(match.state.includes(/** @type {never} */ (packet.state)), `it is for ${match.state.join(" or ")} packets`)
|
|
365
|
-
: unknown("the connection state")
|
|
366
|
-
);
|
|
305
|
+
if (match.protocol)
|
|
306
|
+
checks.push(
|
|
307
|
+
packet.protocol
|
|
308
|
+
? Evaluate.#test(match.protocol === packet.protocol, `it is for ${match.protocol}`)
|
|
309
|
+
: Evaluate.#unknown("the protocol")
|
|
310
|
+
);
|
|
367
311
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
312
|
+
/**
|
|
313
|
+
* @param {string | undefined} rule
|
|
314
|
+
* @param {string | undefined} given
|
|
315
|
+
* @param {string} side
|
|
316
|
+
*/
|
|
317
|
+
const address = (rule, given, side) => {
|
|
318
|
+
if (!rule) return;
|
|
375
319
|
checks.push(
|
|
376
|
-
test(
|
|
320
|
+
given ? Evaluate.#test(Net.covers(rule, given), `its ${side} is ${rule}`) : Evaluate.#unknown(`the ${side} address`)
|
|
377
321
|
);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
322
|
+
};
|
|
380
323
|
|
|
381
|
-
|
|
382
|
-
|
|
324
|
+
address(match.src, packet.src, "source");
|
|
325
|
+
address(match.dst, packet.dst, "destination");
|
|
383
326
|
|
|
384
|
-
/**
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
steps.push({ rule: index + 1, description: rule.description, action: rule.action, check });
|
|
402
|
-
|
|
403
|
-
if (check.answer === "yes") return { steps, verdict: rule.action, by: `rule ${index + 1}` };
|
|
404
|
-
if (check.answer === "unknown") return { steps, verdict: undefined, by: `rule ${index + 1}` };
|
|
405
|
-
}
|
|
327
|
+
/**
|
|
328
|
+
* @param {string | undefined} name
|
|
329
|
+
* @param {string | undefined} given
|
|
330
|
+
* @param {string} side
|
|
331
|
+
*/
|
|
332
|
+
const set = (name, given, side) => {
|
|
333
|
+
if (!name) return;
|
|
334
|
+
const members = sets[name] ?? [];
|
|
335
|
+
checks.push(
|
|
336
|
+
given
|
|
337
|
+
? Evaluate.#test(
|
|
338
|
+
members.some((entry) => Net.covers(entry, given)),
|
|
339
|
+
`its ${side} is in the set ${name}`
|
|
340
|
+
)
|
|
341
|
+
: Evaluate.#unknown(`the ${side} address`)
|
|
342
|
+
);
|
|
343
|
+
};
|
|
406
344
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
verdict: settings?.default ?? "accept",
|
|
410
|
-
by: settings?.default ? "the chain's default" : "the chain's default, accept when none is written",
|
|
411
|
-
};
|
|
412
|
-
}
|
|
345
|
+
set(match.src_set, packet.src, "source");
|
|
346
|
+
set(match.dst_set, packet.dst, "destination");
|
|
413
347
|
|
|
414
|
-
|
|
348
|
+
/**
|
|
349
|
+
* @param {Ports | undefined} rule
|
|
350
|
+
* @param {number | undefined} given
|
|
351
|
+
* @param {string} side
|
|
352
|
+
*/
|
|
353
|
+
const port = (rule, given, side) => {
|
|
354
|
+
if (rule === undefined) return;
|
|
355
|
+
if (packet.protocol && packet.protocol !== "tcp" && packet.protocol !== "udp")
|
|
356
|
+
return checks.push(Evaluate.#test(false, "it names ports, which only TCP and UDP have"));
|
|
357
|
+
checks.push(
|
|
358
|
+
given !== undefined
|
|
359
|
+
? Evaluate.#test(Evaluate.#portIn(rule, given), `its ${side} port is ${JSON.stringify(rule)}`)
|
|
360
|
+
: Evaluate.#unknown(`the ${side} port`)
|
|
361
|
+
);
|
|
362
|
+
};
|
|
415
363
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
/** @typedef {{ policy: string, rule: number, description?: string, check: Check, changed: string[], decision?: string, call?: string }} PolicyStep */
|
|
364
|
+
port(match.src_port, packet.srcPort, "source");
|
|
365
|
+
port(match.dst_port, packet.dstPort, "destination");
|
|
419
366
|
|
|
420
|
-
|
|
421
|
-
* Whether a route meets a policy rule's conditions, as the schema describes each one. A prefix set holds a route whose prefix is one of its own or inside one.
|
|
422
|
-
*
|
|
423
|
-
* @param {RouteMatch} match
|
|
424
|
-
* @param {Route} route
|
|
425
|
-
* @param {Device} device
|
|
426
|
-
* @returns {Check}
|
|
427
|
-
*/
|
|
428
|
-
export function routeMeets(match, route, device) {
|
|
429
|
-
/** @type {Check[]} */
|
|
430
|
-
const checks = [];
|
|
431
|
-
const range = parseRange(route.prefix);
|
|
432
|
-
const family = range?.family;
|
|
433
|
-
|
|
434
|
-
if (match.family) checks.push(test(match.family === family, `it is for ${match.family} only`));
|
|
435
|
-
if (match.prefix) checks.push(test(samePrefix(match.prefix, route.prefix), `it is for ${match.prefix} exactly`));
|
|
436
|
-
|
|
437
|
-
if (match.prefix_set) {
|
|
438
|
-
const set = device.prefix_sets?.[match.prefix_set];
|
|
439
|
-
if (set && "source" in set)
|
|
367
|
+
if (match.in_interface)
|
|
440
368
|
checks.push(
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
)
|
|
369
|
+
packet.inInterface
|
|
370
|
+
? Evaluate.#test(match.in_interface === packet.inInterface, `it is for packets arriving on ${match.in_interface}`)
|
|
371
|
+
: Evaluate.#unknown("where the packet arrives")
|
|
445
372
|
);
|
|
446
|
-
|
|
373
|
+
if (match.out_interface)
|
|
447
374
|
checks.push(
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
375
|
+
packet.outInterface
|
|
376
|
+
? Evaluate.#test(
|
|
377
|
+
match.out_interface === packet.outInterface,
|
|
378
|
+
`it is for packets leaving through ${match.out_interface}`
|
|
379
|
+
)
|
|
380
|
+
: Evaluate.#unknown("where the packet leaves")
|
|
381
|
+
);
|
|
382
|
+
if (match.state)
|
|
383
|
+
checks.push(
|
|
384
|
+
packet.state
|
|
385
|
+
? Evaluate.#test(
|
|
386
|
+
match.state.includes(/** @type {never} */ (packet.state)),
|
|
387
|
+
`it is for ${match.state.join(" or ")} packets`
|
|
388
|
+
)
|
|
389
|
+
: Evaluate.#unknown("the connection state")
|
|
452
390
|
);
|
|
453
|
-
}
|
|
454
391
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
392
|
+
if (match.tcp_flags) {
|
|
393
|
+
if (packet.protocol && packet.protocol !== "tcp") checks.push(Evaluate.#test(false, "it names TCP flags"));
|
|
394
|
+
else if (!packet.flags) checks.push(Evaluate.#unknown("the TCP flags"));
|
|
395
|
+
else {
|
|
396
|
+
const flags = packet.flags;
|
|
397
|
+
const set = match.tcp_flags.set ?? [];
|
|
398
|
+
const unset = match.tcp_flags.unset ?? [];
|
|
399
|
+
checks.push(
|
|
400
|
+
Evaluate.#test(
|
|
401
|
+
set.every((flag) => flags.includes(flag)) && unset.every((flag) => !flags.includes(flag)),
|
|
402
|
+
"its TCP flags differ"
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
467
406
|
}
|
|
407
|
+
|
|
408
|
+
return Evaluate.#all(checks);
|
|
468
409
|
}
|
|
469
410
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
411
|
+
/**
|
|
412
|
+
* A packet through one of a device's filter chains: each rule in order until one matches, then the chain's default, accept when it has none.
|
|
413
|
+
*
|
|
414
|
+
* @param {Device} device
|
|
415
|
+
* @param {"input" | "forward" | "output"} chain
|
|
416
|
+
* @param {Packet} packet
|
|
417
|
+
*/
|
|
418
|
+
static filterPacket(device, chain, packet) {
|
|
419
|
+
const settings = device.firewall?.filter?.[chain];
|
|
420
|
+
const rules = settings?.rules ?? [];
|
|
421
|
+
const sets = device.firewall?.address_sets ?? {};
|
|
422
|
+
|
|
423
|
+
/** @type {{ rule: number, description?: string, action: string, check: Check }[]} */
|
|
424
|
+
const steps = [];
|
|
475
425
|
|
|
476
|
-
|
|
477
|
-
}
|
|
426
|
+
for (const [index, rule] of rules.entries()) {
|
|
427
|
+
const check = Evaluate.#packetMeets(rule.match ?? {}, packet, sets);
|
|
428
|
+
steps.push({ rule: index + 1, description: rule.description, action: rule.action, check });
|
|
478
429
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Apply a rule's changes to a route's attributes, and say what changed.
|
|
493
|
-
*
|
|
494
|
-
* @param {Sets} set
|
|
495
|
-
* @param {Attributes} attributes
|
|
496
|
-
*/
|
|
497
|
-
function apply(set, attributes) {
|
|
498
|
-
/** @type {string[]} */
|
|
499
|
-
const changed = [];
|
|
500
|
-
|
|
501
|
-
if (set.local_pref !== undefined) changed.push(`local preference ${(attributes.localPref = set.local_pref)}`);
|
|
502
|
-
if (set.med !== undefined) changed.push(`MED ${(attributes.med = set.med)}`);
|
|
503
|
-
if (set.prepend)
|
|
504
|
-
changed.push(
|
|
505
|
-
`prepend our AS ${set.prepend} ${set.prepend === 1 ? "time" : "times"} (${(attributes.prepend += set.prepend)} in all)`
|
|
506
|
-
);
|
|
430
|
+
if (check.answer === "yes") return { steps, verdict: rule.action, by: `rule ${index + 1}` };
|
|
431
|
+
if (check.answer === "unknown") return { steps, verdict: undefined, by: `rule ${index + 1}` };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
steps,
|
|
436
|
+
verdict: settings?.default ?? "accept",
|
|
437
|
+
by: settings?.default ? "the chain's default" : "the chain's default, accept when none is written",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// What a route policy does to a route.
|
|
507
442
|
|
|
508
443
|
/**
|
|
509
|
-
*
|
|
510
|
-
*
|
|
511
|
-
* @param {
|
|
512
|
-
* @param {
|
|
513
|
-
* @param {
|
|
444
|
+
* Whether a route meets a policy rule's conditions, as the schema describes each one. A prefix set holds a route whose prefix is one of its own or inside one.
|
|
445
|
+
*
|
|
446
|
+
* @param {RouteMatch} match
|
|
447
|
+
* @param {Route} route
|
|
448
|
+
* @param {Device} device
|
|
449
|
+
* @returns {Check}
|
|
514
450
|
*/
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
451
|
+
static #routeMeets(match, route, device) {
|
|
452
|
+
/** @type {Check[]} */
|
|
453
|
+
const checks = [];
|
|
454
|
+
const range = Net.parseRange(route.prefix);
|
|
455
|
+
const family = range?.family;
|
|
456
|
+
|
|
457
|
+
if (match.family) checks.push(Evaluate.#test(match.family === family, `it is for ${match.family} only`));
|
|
458
|
+
if (match.prefix) checks.push(Evaluate.#test(Net.samePrefix(match.prefix, route.prefix), `it is for ${match.prefix} exactly`));
|
|
459
|
+
|
|
460
|
+
if (match.prefix_set) {
|
|
461
|
+
const set = device.prefix_sets?.[match.prefix_set];
|
|
462
|
+
if (set && "source" in set)
|
|
463
|
+
checks.push(
|
|
464
|
+
Evaluate.#test(
|
|
465
|
+
route.inRegistrySets.includes(match.prefix_set),
|
|
466
|
+
`the route is not in ${match.prefix_set}, fetched from ${set.source.registry}`
|
|
467
|
+
)
|
|
468
|
+
);
|
|
469
|
+
else
|
|
470
|
+
checks.push(
|
|
471
|
+
Evaluate.#test(
|
|
472
|
+
Boolean(set?.prefixes.some((prefix) => Net.covers(prefix, route.prefix))),
|
|
473
|
+
`the route is not in the prefix set ${match.prefix_set}`
|
|
474
|
+
)
|
|
475
|
+
);
|
|
521
476
|
}
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
477
|
+
|
|
478
|
+
const length = range?.length ?? 0;
|
|
479
|
+
if (match.prefix_length?.min !== undefined)
|
|
480
|
+
checks.push(Evaluate.#test(length >= match.prefix_length.min, `it is for /${match.prefix_length.min} or longer`));
|
|
481
|
+
if (match.prefix_length?.max !== undefined)
|
|
482
|
+
checks.push(Evaluate.#test(length <= match.prefix_length.max, `it is for /${match.prefix_length.max} or shorter`));
|
|
483
|
+
|
|
484
|
+
if (match.as_path_length) {
|
|
485
|
+
const { min, max } = match.as_path_length;
|
|
486
|
+
if (route.asPathLength === undefined) checks.push(Evaluate.#unknown("the AS path length"));
|
|
487
|
+
else {
|
|
488
|
+
if (min !== undefined) checks.push(Evaluate.#test(route.asPathLength >= min, `it is for AS paths of ${min} or more`));
|
|
489
|
+
if (max !== undefined) checks.push(Evaluate.#test(route.asPathLength <= max, `it is for AS paths of ${max} or fewer`));
|
|
490
|
+
}
|
|
525
491
|
}
|
|
526
|
-
};
|
|
527
492
|
|
|
528
|
-
|
|
529
|
-
|
|
493
|
+
if (match.rpki)
|
|
494
|
+
checks.push(
|
|
495
|
+
route.rpki
|
|
496
|
+
? Evaluate.#test(match.rpki === route.rpki, `it is for routes RPKI finds ${match.rpki}`)
|
|
497
|
+
: Evaluate.#unknown("the RPKI state")
|
|
498
|
+
);
|
|
499
|
+
if (match.community)
|
|
500
|
+
checks.push(Evaluate.#test(route.communities.includes(match.community), `the route does not carry ${match.community}`));
|
|
501
|
+
if (match.large_community)
|
|
502
|
+
checks.push(
|
|
503
|
+
Evaluate.#test(route.largeCommunities.includes(match.large_community), `the route does not carry ${match.large_community}`)
|
|
504
|
+
);
|
|
530
505
|
|
|
531
|
-
|
|
532
|
-
if (set.preferred_source) changed.push(`preferred source ${(attributes.preferredSource = set.preferred_source)}`);
|
|
533
|
-
if (set.blackhole) {
|
|
534
|
-
attributes.blackhole = true;
|
|
535
|
-
changed.push("blackholed: traffic to it is dropped here");
|
|
506
|
+
return Evaluate.#all(checks);
|
|
536
507
|
}
|
|
537
508
|
|
|
538
|
-
|
|
539
|
-
|
|
509
|
+
/**
|
|
510
|
+
* Whether a community matches a pattern in which `*` stands for any value of a field.
|
|
511
|
+
*
|
|
512
|
+
* @param {string} pattern
|
|
513
|
+
* @param {string} community
|
|
514
|
+
*/
|
|
515
|
+
static #fits(pattern, community) {
|
|
516
|
+
const want = pattern.split(":");
|
|
517
|
+
const have = community.split(":");
|
|
518
|
+
return want.length === have.length && want.every((field, index) => field === "*" || field === have[index]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Apply a rule's changes to a route's attributes, and say what changed.
|
|
523
|
+
*
|
|
524
|
+
* @param {Sets} set
|
|
525
|
+
* @param {Attributes} attributes
|
|
526
|
+
*/
|
|
527
|
+
static #apply(set, attributes) {
|
|
528
|
+
/** @type {string[]} */
|
|
529
|
+
const changed = [];
|
|
530
|
+
|
|
531
|
+
if (set.local_pref !== undefined) changed.push(`local preference ${(attributes.localPref = set.local_pref)}`);
|
|
532
|
+
if (set.med !== undefined) changed.push(`MED ${(attributes.med = set.med)}`);
|
|
533
|
+
if (set.prepend)
|
|
534
|
+
changed.push(
|
|
535
|
+
`prepend our AS ${set.prepend} ${set.prepend === 1 ? "time" : "times"} (${(attributes.prepend += set.prepend)} in all)`
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* @param {"communities" | "largeCommunities"} key
|
|
540
|
+
* @param {readonly string[] | undefined} replace
|
|
541
|
+
* @param {readonly string[] | undefined} add
|
|
542
|
+
* @param {readonly string[] | undefined} remove
|
|
543
|
+
* @param {string} name
|
|
544
|
+
*/
|
|
545
|
+
const communities = (key, replace, add, remove, name) => {
|
|
546
|
+
if (replace) changed.push(`${name} set to ${(attributes[key] = [...replace]).join(", ") || "none"}`);
|
|
547
|
+
if (remove?.length) {
|
|
548
|
+
const before = attributes[key].length;
|
|
549
|
+
attributes[key] = attributes[key].filter((community) => !remove.some((pattern) => Evaluate.#fits(pattern, community)));
|
|
550
|
+
changed.push(`${before - attributes[key].length} ${name} removed by ${remove.join(", ")}`);
|
|
551
|
+
}
|
|
552
|
+
if (add?.length) {
|
|
553
|
+
attributes[key] = [...new Set([...attributes[key], ...add])];
|
|
554
|
+
changed.push(`${name} added: ${add.join(", ")}`);
|
|
555
|
+
}
|
|
556
|
+
};
|
|
540
557
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
558
|
+
communities("communities", set.communities, set.add_communities, set.remove_communities, "communities");
|
|
559
|
+
communities(
|
|
560
|
+
"largeCommunities",
|
|
561
|
+
set.large_communities,
|
|
562
|
+
set.add_large_communities,
|
|
563
|
+
set.remove_large_communities,
|
|
564
|
+
"large communities"
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (set.next_hop) changed.push(`next hop ${(attributes.nextHop = set.next_hop)}`);
|
|
568
|
+
if (set.preferred_source) changed.push(`preferred source ${(attributes.preferredSource = set.preferred_source)}`);
|
|
569
|
+
if (set.blackhole) {
|
|
570
|
+
attributes.blackhole = true;
|
|
571
|
+
changed.push("blackholed: traffic to it is dropped here");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return changed;
|
|
575
|
+
}
|
|
553
576
|
|
|
554
577
|
/**
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
* @
|
|
578
|
+
* A route through one of a device's route policies: `call`, then `match`, then `set`, then `action`, rule by rule, until a decision. A route the policy itself never accepts is rejected; one a called policy leaves undecided goes on in the caller.
|
|
579
|
+
*
|
|
580
|
+
* @param {Device} device
|
|
581
|
+
* @param {string} policy
|
|
582
|
+
* @param {Route} route
|
|
558
583
|
*/
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
584
|
+
static routeThrough(device, policy, route) {
|
|
585
|
+
/** @type {PolicyStep[]} */
|
|
586
|
+
const steps = [];
|
|
587
|
+
/** @type {Attributes} */
|
|
588
|
+
const attributes = { prepend: 0, communities: [...route.communities], largeCommunities: [...route.largeCommunities] };
|
|
562
589
|
|
|
563
|
-
|
|
564
|
-
|
|
590
|
+
/**
|
|
591
|
+
* @param {string} name
|
|
592
|
+
* @param {number} depth
|
|
593
|
+
* @returns {"accept" | "reject" | "unknown" | undefined}
|
|
594
|
+
*/
|
|
595
|
+
const run = (name, depth) => {
|
|
596
|
+
const rules = device.policies?.[name] ?? [];
|
|
597
|
+
if (depth > 16) return "unknown";
|
|
598
|
+
|
|
599
|
+
for (const [index, rule] of rules.entries()) {
|
|
600
|
+
const step = { policy: name, rule: index + 1, description: rule.description };
|
|
601
|
+
|
|
602
|
+
if (rule.call) {
|
|
603
|
+
const called = run(rule.call, depth + 1);
|
|
604
|
+
if (called) {
|
|
605
|
+
steps.push({ ...step, call: rule.call, check: { answer: "yes", why: [] }, changed: [], decision: called });
|
|
606
|
+
return called;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const current = { ...route, communities: attributes.communities, largeCommunities: attributes.largeCommunities };
|
|
611
|
+
const check = Evaluate.#routeMeets(rule.match ?? {}, current, device);
|
|
565
612
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
steps.push({ ...step, call: rule.call, check: { answer: "yes", why: [] }, changed: [], decision: called });
|
|
570
|
-
return called;
|
|
613
|
+
if (check.answer === "unknown") {
|
|
614
|
+
steps.push({ ...step, ...(rule.call ? { call: rule.call } : {}), check, changed: [] });
|
|
615
|
+
return "unknown";
|
|
571
616
|
}
|
|
572
|
-
}
|
|
573
617
|
|
|
574
|
-
|
|
575
|
-
|
|
618
|
+
if (check.answer === "no") {
|
|
619
|
+
steps.push({ ...step, ...(rule.call ? { call: rule.call } : {}), check, changed: [] });
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
576
622
|
|
|
577
|
-
|
|
578
|
-
steps.push({
|
|
579
|
-
|
|
623
|
+
const changed = Evaluate.#apply(rule.set ?? {}, attributes);
|
|
624
|
+
steps.push({
|
|
625
|
+
...step,
|
|
626
|
+
...(rule.call ? { call: rule.call } : {}),
|
|
627
|
+
check,
|
|
628
|
+
changed,
|
|
629
|
+
...(rule.action ? { decision: rule.action } : {}),
|
|
630
|
+
});
|
|
631
|
+
if (rule.action) return rule.action;
|
|
580
632
|
}
|
|
581
633
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
634
|
+
return undefined;
|
|
635
|
+
};
|
|
586
636
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
...(rule.call ? { call: rule.call } : {}),
|
|
591
|
-
check,
|
|
592
|
-
changed,
|
|
593
|
-
...(rule.action ? { decision: rule.action } : {}),
|
|
594
|
-
});
|
|
595
|
-
if (rule.action) return rule.action;
|
|
596
|
-
}
|
|
637
|
+
const decided = run(policy, 0);
|
|
638
|
+
return { steps, decision: decided ?? "reject", byDefault: decided === undefined, attributes };
|
|
639
|
+
}
|
|
597
640
|
|
|
598
|
-
|
|
599
|
-
};
|
|
641
|
+
// What changed between two networks.
|
|
600
642
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
643
|
+
/**
|
|
644
|
+
* An edge's place in the graph, the same whichever way round it was written, to match it across two networks.
|
|
645
|
+
*
|
|
646
|
+
* @param {GraphEdge} edge
|
|
647
|
+
*/
|
|
648
|
+
static changeKey(edge) {
|
|
649
|
+
return `${edge.layer} ${[edge.from, edge.to].sort().join(" ")} ${edge.label}`;
|
|
650
|
+
}
|
|
604
651
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
added: after.graph.edges.filter((edge) => !edgesBefore.has(changeKey(edge))),
|
|
641
|
-
removed: before.graph.edges.filter((edge) => !edgesAfter.has(changeKey(edge))),
|
|
642
|
-
},
|
|
643
|
-
findings: {
|
|
644
|
-
added: after.graph.findings.filter((one) => !findingsBefore.has(finding(one))),
|
|
645
|
-
resolved: before.graph.findings.filter((one) => !findingsAfter.has(finding(one))),
|
|
646
|
-
},
|
|
647
|
-
};
|
|
652
|
+
/**
|
|
653
|
+
* What differs between the network before and after: devices added, removed or rendered differently; lines of the graph added or gone; and what validate finds newly or no longer.
|
|
654
|
+
*
|
|
655
|
+
* @param {StudioData} before
|
|
656
|
+
* @param {StudioData} after
|
|
657
|
+
*/
|
|
658
|
+
static compareNetworks(before, after) {
|
|
659
|
+
const names = (/** @type {StudioData} */ network) => new Set(network.devices.map((device) => device.name));
|
|
660
|
+
const was = names(before);
|
|
661
|
+
const is = names(after);
|
|
662
|
+
|
|
663
|
+
const added = [...is].filter((name) => !was.has(name));
|
|
664
|
+
const removed = [...was].filter((name) => !is.has(name));
|
|
665
|
+
const changed = [...is].filter((name) => was.has(name) && before.rendered[name] !== after.rendered[name]);
|
|
666
|
+
|
|
667
|
+
const keys = (/** @type {StudioData} */ network) => new Set(network.graph.edges.map(Evaluate.changeKey));
|
|
668
|
+
const edgesBefore = keys(before);
|
|
669
|
+
const edgesAfter = keys(after);
|
|
670
|
+
|
|
671
|
+
const finding = (/** @type {import("../../core/define").Finding} */ one) => `${one.level} ${one.device ?? ""} ${one.message}`;
|
|
672
|
+
const findingsBefore = new Set(before.graph.findings.map(finding));
|
|
673
|
+
const findingsAfter = new Set(after.graph.findings.map(finding));
|
|
674
|
+
|
|
675
|
+
return {
|
|
676
|
+
devices: { added, removed, changed },
|
|
677
|
+
edges: {
|
|
678
|
+
added: after.graph.edges.filter((edge) => !edgesBefore.has(Evaluate.changeKey(edge))),
|
|
679
|
+
removed: before.graph.edges.filter((edge) => !edgesAfter.has(Evaluate.changeKey(edge))),
|
|
680
|
+
},
|
|
681
|
+
findings: {
|
|
682
|
+
added: after.graph.findings.filter((one) => !findingsBefore.has(finding(one))),
|
|
683
|
+
resolved: before.graph.findings.filter((one) => !findingsAfter.has(finding(one))),
|
|
684
|
+
},
|
|
685
|
+
};
|
|
686
|
+
}
|
|
648
687
|
}
|