@takodotid/circuit 0.43.1 → 1.0.0-rc.2

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.
Files changed (256) hide show
  1. package/README.md +115 -37
  2. package/package.json +21 -58
  3. package/src/checks/across.ts +402 -0
  4. package/src/checks/built-in.ts +256 -0
  5. package/src/checks/device.ts +436 -0
  6. package/src/checks/index.ts +53 -0
  7. package/src/cli/advisories.ts +108 -0
  8. package/src/cli/app.ts +71 -0
  9. package/src/cli/editor-types.generated.ts +30 -0
  10. package/src/cli/editor-types.ts +69 -0
  11. package/src/cli/format.ts +124 -0
  12. package/src/cli/git-hooks.ts +79 -0
  13. package/src/cli/index.ts +350 -1170
  14. package/src/cli/migrate.ts +554 -0
  15. package/src/cli/project-commands.ts +656 -0
  16. package/src/cli/releases.ts +374 -0
  17. package/src/cli/remember.ts +339 -0
  18. package/src/cli/rename.ts +27 -33
  19. package/src/cli/secrets-password.ts +60 -0
  20. package/src/cli/secrets.ts +258 -0
  21. package/src/cli/setup.ts +300 -0
  22. package/src/cli/signing.ts +23 -0
  23. package/src/cli/text-editor.ts +268 -0
  24. package/src/cli/ui.ts +298 -0
  25. package/src/cli/version.ts +2 -0
  26. package/src/cli/versions.ts +158 -0
  27. package/src/config/address.ts +102 -0
  28. package/src/config/circuit.d.ts +30 -0
  29. package/src/config/common.ts +65 -0
  30. package/src/config/communities.ts +479 -0
  31. package/src/config/define.ts +684 -0
  32. package/src/config/device.ts +164 -0
  33. package/src/config/firewall.ts +150 -0
  34. package/src/config/index.ts +22 -0
  35. package/src/{schema → config}/interfaces.ts +121 -85
  36. package/src/config/keys.ts +434 -0
  37. package/src/config/management.ts +61 -0
  38. package/src/config/merge.ts +335 -0
  39. package/src/config/models.ts +70 -0
  40. package/src/config/monitoring.ts +64 -0
  41. package/src/config/network.ts +180 -0
  42. package/src/config/presets.ts +149 -0
  43. package/src/config/routing.ts +401 -0
  44. package/src/config/services.ts +51 -0
  45. package/src/config/switching.ts +84 -0
  46. package/src/config/system.ts +103 -0
  47. package/src/{schema → config}/time-zones.ts +1 -1
  48. package/src/core/address.ts +1 -1
  49. package/src/core/cabling.ts +8 -43
  50. package/src/core/graph.ts +40 -0
  51. package/src/{integrations → core}/registries.ts +5 -3
  52. package/src/core/secrets-file.ts +539 -0
  53. package/src/core/secrets.ts +83 -64
  54. package/src/core/topology.ts +103 -146
  55. package/src/core/utils.ts +12 -1
  56. package/src/model.ts +83 -0
  57. package/src/platforms/derive.ts +64 -37
  58. package/src/platforms/finding.ts +9 -0
  59. package/src/platforms/fortios/firewall.ts +76 -64
  60. package/src/platforms/fortios/index.ts +189 -125
  61. package/src/platforms/fortios/names.ts +51 -18
  62. package/src/platforms/fortios/render.ts +69 -62
  63. package/src/platforms/fortios/routing.ts +89 -58
  64. package/src/platforms/fortios/vdoms.ts +47 -16
  65. package/src/platforms/junos/TRAPS.md +2 -2
  66. package/src/platforms/junos/bgp.ts +63 -42
  67. package/src/platforms/junos/index.ts +122 -89
  68. package/src/platforms/junos/render.ts +139 -104
  69. package/src/platforms/platform.ts +10 -8
  70. package/src/platforms/raisecom-ros/TRAPS.md +1 -1
  71. package/src/platforms/raisecom-ros/index.ts +89 -70
  72. package/src/platforms/raisecom-ros/render.ts +49 -45
  73. package/src/platforms/routeros/TRAPS.md +3 -3
  74. package/src/platforms/routeros/index.ts +121 -60
  75. package/src/platforms/routeros/render.ts +214 -195
  76. package/src/platforms/vrp/TRAPS.md +1 -1
  77. package/src/platforms/vrp/index.ts +74 -50
  78. package/src/platforms/vrp/render.ts +67 -56
  79. package/src/project/project.ts +725 -0
  80. package/src/studio/app/env.d.ts +9 -0
  81. package/src/studio/app/index.html +18 -0
  82. package/src/studio/app/src/App.vue +95 -0
  83. package/src/studio/app/src/components/AddressesView.vue +38 -0
  84. package/src/studio/app/src/components/CablesView.vue +29 -0
  85. package/src/studio/app/src/components/CompareView.vue +160 -0
  86. package/src/studio/app/src/components/DataTable.vue +49 -0
  87. package/src/studio/app/src/components/DeviceView.vue +237 -0
  88. package/src/studio/app/src/components/DevicesView.vue +32 -0
  89. package/src/studio/app/src/components/Faceplate.vue +75 -0
  90. package/src/studio/app/src/components/GraphView.vue +282 -0
  91. package/src/studio/app/src/components/LineDiff.vue +24 -0
  92. package/src/studio/app/src/components/ProblemsView.vue +42 -0
  93. package/src/studio/app/src/components/SearchView.vue +72 -0
  94. package/src/studio/app/src/components/SecretsView.vue +112 -0
  95. package/src/studio/app/src/components/SessionsView.vue +35 -0
  96. package/src/studio/app/src/components/Settings.vue +95 -0
  97. package/src/studio/app/src/components/ToolsView.vue +118 -0
  98. package/src/studio/app/src/components/VlansView.vue +50 -0
  99. package/src/studio/app/src/lib/csv.ts +19 -0
  100. package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
  101. package/src/studio/app/src/lib/format.ts +38 -0
  102. package/src/studio/app/src/lib/net.ts +68 -0
  103. package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
  104. package/src/studio/app/src/lib/store.ts +71 -0
  105. package/src/studio/app/src/lib/tools.ts +217 -0
  106. package/src/studio/app/src/main.ts +9 -0
  107. package/src/studio/{public → app/src}/style.css +14 -4
  108. package/src/studio/app/tsconfig.json +17 -0
  109. package/src/studio/app/vite.config.ts +16 -0
  110. package/src/studio/assets.generated.ts +3 -0
  111. package/src/studio/data.ts +96 -188
  112. package/src/studio/git.ts +27 -43
  113. package/src/studio/server.ts +196 -107
  114. package/src/studio/types.ts +141 -0
  115. package/src/transport/console.ts +3 -3
  116. package/src/transport/ssh.ts +1 -1
  117. package/dist/chunks/index-3b343xva.js +0 -177
  118. package/dist/chunks/index-6wb41q67.js +0 -62
  119. package/dist/chunks/index-n7w3tg5y.js +0 -149
  120. package/dist/chunks/index-yzw86rc8.js +0 -921
  121. package/dist/chunks/new-w3zys5y4.js +0 -172
  122. package/dist/chunks/server-cxr9ynxc.js +0 -259
  123. package/dist/cli/index.js +0 -12922
  124. package/dist/helpers/index.js +0 -881
  125. package/dist/index.js +0 -28
  126. package/dist/types/core/address-space.d.ts +0 -62
  127. package/dist/types/core/address.d.ts +0 -44
  128. package/dist/types/core/cabling.d.ts +0 -41
  129. package/dist/types/core/cross-checks.d.ts +0 -28
  130. package/dist/types/core/define.d.ts +0 -79
  131. package/dist/types/core/project-files.d.ts +0 -12
  132. package/dist/types/core/schema-docs.d.ts +0 -25
  133. package/dist/types/core/secrets.d.ts +0 -62
  134. package/dist/types/core/topology.d.ts +0 -96
  135. package/dist/types/core/utils.d.ts +0 -10
  136. package/dist/types/core/validator.d.ts +0 -23
  137. package/dist/types/helpers/bgp.d.ts +0 -195
  138. package/dist/types/helpers/cables.d.ts +0 -69
  139. package/dist/types/helpers/checks.d.ts +0 -36
  140. package/dist/types/helpers/exchange.d.ts +0 -55
  141. package/dist/types/helpers/filters.d.ts +0 -51
  142. package/dist/types/helpers/index.d.ts +0 -14
  143. package/dist/types/helpers/route-server.d.ts +0 -52
  144. package/dist/types/helpers/upstreams.d.ts +0 -192
  145. package/dist/types/index.d.ts +0 -10
  146. package/dist/types/integrations/librenms.d.ts +0 -60
  147. package/dist/types/integrations/netbox.d.ts +0 -89
  148. package/dist/types/integrations/peeringdb.d.ts +0 -24
  149. package/dist/types/integrations/prometheus.d.ts +0 -24
  150. package/dist/types/integrations/registries.d.ts +0 -8
  151. package/dist/types/platforms/bgp-wait.d.ts +0 -24
  152. package/dist/types/platforms/catalog.d.ts +0 -109
  153. package/dist/types/platforms/derive.d.ts +0 -17
  154. package/dist/types/platforms/fortios/firewall.d.ts +0 -47
  155. package/dist/types/platforms/fortios/index.d.ts +0 -140
  156. package/dist/types/platforms/fortios/models.d.ts +0 -14
  157. package/dist/types/platforms/fortios/names.d.ts +0 -52
  158. package/dist/types/platforms/fortios/plan.d.ts +0 -114
  159. package/dist/types/platforms/fortios/render.d.ts +0 -77
  160. package/dist/types/platforms/fortios/routing.d.ts +0 -56
  161. package/dist/types/platforms/fortios/tree.d.ts +0 -37
  162. package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
  163. package/dist/types/platforms/index.d.ts +0 -13
  164. package/dist/types/platforms/junos/bgp.d.ts +0 -44
  165. package/dist/types/platforms/junos/index.d.ts +0 -114
  166. package/dist/types/platforms/junos/models.d.ts +0 -14
  167. package/dist/types/platforms/junos/render.d.ts +0 -154
  168. package/dist/types/platforms/line-diff.d.ts +0 -67
  169. package/dist/types/platforms/platform.d.ts +0 -135
  170. package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
  171. package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
  172. package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
  173. package/dist/types/platforms/routeros/export.d.ts +0 -59
  174. package/dist/types/platforms/routeros/index.d.ts +0 -116
  175. package/dist/types/platforms/routeros/models.d.ts +0 -18
  176. package/dist/types/platforms/routeros/plan.d.ts +0 -58
  177. package/dist/types/platforms/routeros/render.d.ts +0 -250
  178. package/dist/types/platforms/vrp/index.d.ts +0 -83
  179. package/dist/types/platforms/vrp/models.d.ts +0 -12
  180. package/dist/types/platforms/vrp/render.d.ts +0 -96
  181. package/dist/types/schema/common.d.ts +0 -30
  182. package/dist/types/schema/device.d.ts +0 -126
  183. package/dist/types/schema/firewall.d.ts +0 -131
  184. package/dist/types/schema/index.d.ts +0 -8
  185. package/dist/types/schema/interfaces.d.ts +0 -205
  186. package/dist/types/schema/policy.d.ts +0 -107
  187. package/dist/types/schema/routing.d.ts +0 -161
  188. package/dist/types/schema/system.d.ts +0 -198
  189. package/dist/types/schema/time-zones.d.ts +0 -2
  190. package/dist/types/transport/console.d.ts +0 -115
  191. package/dist/types/transport/path-check.d.ts +0 -32
  192. package/dist/types/transport/ssh.d.ts +0 -27
  193. package/src/cli/graph-format.ts +0 -91
  194. package/src/cli/new.ts +0 -234
  195. package/src/core/address-space.ts +0 -320
  196. package/src/core/cross-checks.ts +0 -369
  197. package/src/core/define.ts +0 -124
  198. package/src/core/project-files.ts +0 -35
  199. package/src/core/schema-docs.ts +0 -174
  200. package/src/core/validator.ts +0 -508
  201. package/src/helpers/bgp.ts +0 -473
  202. package/src/helpers/cables.ts +0 -74
  203. package/src/helpers/checks.ts +0 -201
  204. package/src/helpers/exchange.ts +0 -123
  205. package/src/helpers/filters.ts +0 -107
  206. package/src/helpers/index.ts +0 -25
  207. package/src/helpers/route-server.ts +0 -212
  208. package/src/helpers/upstreams.ts +0 -369
  209. package/src/index.ts +0 -13
  210. package/src/integrations/librenms.ts +0 -291
  211. package/src/integrations/netbox.ts +0 -620
  212. package/src/integrations/peeringdb.ts +0 -173
  213. package/src/integrations/prometheus.ts +0 -90
  214. package/src/schema/common.ts +0 -41
  215. package/src/schema/device.ts +0 -145
  216. package/src/schema/firewall.ts +0 -140
  217. package/src/schema/index.ts +0 -8
  218. package/src/schema/policy.ts +0 -115
  219. package/src/schema/routing.ts +0 -173
  220. package/src/schema/system.ts +0 -203
  221. package/src/studio/public/app.js +0 -186
  222. package/src/studio/public/compare.js +0 -250
  223. package/src/studio/public/dom.js +0 -138
  224. package/src/studio/public/evaluate.js +0 -687
  225. package/src/studio/public/export.js +0 -130
  226. package/src/studio/public/graph.js +0 -1541
  227. package/src/studio/public/index.html +0 -83
  228. package/src/studio/public/net.js +0 -134
  229. package/src/studio/public/tables.js +0 -1289
  230. package/src/studio/public/tools.js +0 -610
  231. package/templates/colocation/checks.ts +0 -53
  232. package/templates/colocation/circuit.config.ts +0 -9
  233. package/templates/colocation/edge-01.ts +0 -133
  234. package/templates/colocation/routing.ts +0 -13
  235. package/templates/colocation/tenancy.ts +0 -76
  236. package/templates/colocation/tenants/acme.ts +0 -19
  237. package/templates/colocation/tenants/index.ts +0 -6
  238. package/templates/colocation/tor-01.ts +0 -33
  239. package/templates/edge-router/checks.ts +0 -20
  240. package/templates/edge-router/circuit.config.ts +0 -8
  241. package/templates/edge-router/edge-01.ts +0 -110
  242. package/templates/edge-router/routing.ts +0 -13
  243. package/templates/project/AGENTS.md +0 -51
  244. package/templates/project/CLAUDE.md +0 -1
  245. package/templates/project/README.md +0 -16
  246. package/templates/project/gitignore +0 -5
  247. package/templates/project/tsconfig.json +0 -13
  248. package/templates/single-site/checks.ts +0 -26
  249. package/templates/single-site/circuit.config.ts +0 -8
  250. package/templates/single-site/router.ts +0 -86
  251. package/templates/single-site/site.ts +0 -18
  252. package/templates/single-site/switch.ts +0 -34
  253. /package/src/studio/{public → app/public}/favicon.svg +0 -0
  254. /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
  255. /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
  256. /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
@@ -1,174 +0,0 @@
1
- // The schema's own comments, read as data: each type, what it is, and each field with its type and description. The schema reference page and Studio both show them.
2
-
3
- export type Field = { path: string; type: string; description: string };
4
- export type TypeDoc = { name: string; description: string; definition?: string; includes?: string[]; fields: Field[] };
5
-
6
- export class SchemaDocs {
7
- /** The schema files in reading order, with the title each gets. */
8
- static readonly FILES = [
9
- ["device.ts", "Device"],
10
- ["interfaces.ts", "Ports and interfaces"],
11
- ["routing.ts", "Routing"],
12
- ["policy.ts", "Route policies"],
13
- ["firewall.ts", "Firewall and ACLs"],
14
- ["system.ts", "System and management"],
15
- ["common.ts", "Common values"],
16
- ] as const;
17
-
18
- /** The text of a JSDoc comment, without its markers, as one paragraph. */
19
- private static commentText(lines: string[]): string {
20
- return lines
21
- .map((line) => line.replace(/^\s*\/\*\*\s?|\s*\*\/\s*$|^\s*\*\s?/g, "").trim())
22
- .filter(Boolean)
23
- .join(" ");
24
- }
25
-
26
- /** What a type parameter stands for, where it is not one of the device's names. */
27
- private static readonly PARAMETER_MEANING: Record<string, string> = { Pl: "platform", M: "model" };
28
-
29
- /** A type as a reader needs it: "name" for each of the device's names, and no parameter list on the schema's own types. */
30
- private static withoutGenerics(type: string, generics: Set<string>): string {
31
- let result = type;
32
- while (/NoInfer<([^<>]*)>/.test(result)) {
33
- result = result.replace(/NoInfer<([^<>]*)>/g, (_, inner: string) => (inner.includes("|") ? `(${inner})` : inner));
34
- }
35
-
36
- for (const name of generics) {
37
- result = result.replace(new RegExp(`\\b${name}<[^<>]*(?:<[^<>]*>[^<>]*)*>`, "g"), name);
38
- }
39
-
40
- return (
41
- result
42
- // A quoted value, such as the "M" of a rate, is left as it is.
43
- .replace(/"[^"]*"|\b(Pl|M|N|V|I|P|S|G|A|AS|U|F|C|L|K)\b/g, (found, parameter?: string) =>
44
- parameter ? (SchemaDocs.PARAMETER_MEANING[parameter] ?? "name") : found
45
- )
46
- .replace(/\bname(?: \| name)+\b/g, "name")
47
- .replace(/\(name\)/g, "name")
48
- );
49
- }
50
-
51
- private static parse(source: string, generics: Set<string>): TypeDoc[] {
52
- const lines = source.split("\n");
53
- const types: TypeDoc[] = [];
54
-
55
- let comment: string[] = [];
56
- let inComment = false;
57
- let current: TypeDoc | undefined;
58
- let depth = 0;
59
- const parents: string[] = [];
60
- let header: string | undefined;
61
-
62
- /** Reads a whole declaration: a type it aliases, the types whose fields it also has, and whether a body opens. */
63
- function finishHeader(): void {
64
- const text = header!;
65
- header = undefined;
66
-
67
- // The part after `=`, with the generic parameter list before it left out.
68
- const body = text.slice(text.indexOf("> =") >= 0 ? text.indexOf("> =") + 3 : text.indexOf("=") + 1).trim();
69
-
70
- // A template literal type, such as `${number}:${number}`, holds braces but opens no body.
71
- if (!body.includes("{") || body.startsWith("`")) {
72
- current!.definition = body.replace(/;$/, "").replace(/^\|\s*/, "");
73
- } else {
74
- const before = body.slice(0, body.indexOf("{"));
75
- const included = before
76
- .split("&")
77
- .map((part) => part.trim())
78
- .filter((part) => /^\w/.test(part));
79
- if (included.length) current!.includes = included;
80
- }
81
-
82
- depth = (text.match(/{/g) ?? []).length - (text.match(/}/g) ?? []).length;
83
- }
84
-
85
- for (const line of lines) {
86
- const trimmed = line.trim();
87
-
88
- if (trimmed.startsWith("/**")) {
89
- comment = [trimmed];
90
- inComment = !trimmed.endsWith("*/");
91
- continue;
92
- }
93
-
94
- if (inComment) {
95
- comment.push(trimmed);
96
- if (trimmed.endsWith("*/")) inComment = false;
97
- continue;
98
- }
99
-
100
- // A declaration can span lines before its body opens or it ends.
101
- if (header !== undefined) {
102
- header += ` ${trimmed}`;
103
- if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
104
- continue;
105
- }
106
-
107
- // A type the schema does not export is documented too, since exported types take its fields.
108
- const declaration = /^(?:export )?type (\w+)(<)?/.exec(trimmed);
109
- if (declaration && depth === 0) {
110
- if (declaration[2]) generics.add(declaration[1]!);
111
-
112
- current = { name: declaration[1]!, description: SchemaDocs.commentText(comment), fields: [] };
113
- types.push(current);
114
- comment = [];
115
-
116
- header = trimmed;
117
- if (trimmed.endsWith("{") || trimmed.endsWith(";")) finishHeader();
118
- continue;
119
- }
120
-
121
- if (!current) continue;
122
-
123
- const field = /^(?:readonly\s+)?("?[\w-]+"?)(\?)?:\s*(.*)$/.exec(trimmed);
124
- if (field && depth >= 1) {
125
- const [, name, optional, rest] = field;
126
- const path = [...parents, name!.replace(/"/g, "")].join(".");
127
- const opensObject = rest!.trim().endsWith("{");
128
- const type = opensObject ? "object" : rest!.replace(/;$/, "").trim();
129
-
130
- current.fields.push({ path: `${path}${optional ? "?" : ""}`, type, description: SchemaDocs.commentText(comment) });
131
- comment = [];
132
-
133
- if (opensObject) {
134
- parents.push(name!.replace(/"/g, ""));
135
- depth++;
136
- }
137
- continue;
138
- }
139
-
140
- const opens = (trimmed.match(/{/g) ?? []).length;
141
- const closes = (trimmed.match(/}/g) ?? []).length;
142
-
143
- for (let index = 0; index < closes - opens; index++) {
144
- if (parents.length >= depth - 1 && parents.length > 0) parents.pop();
145
- }
146
-
147
- depth += opens - closes;
148
- if (depth <= 0) {
149
- depth = 0;
150
- parents.length = 0;
151
- }
152
-
153
- comment = [];
154
- }
155
-
156
- return types;
157
- }
158
-
159
- /** Every type in each schema file, in the order given. Every file is read first, so a type is known to take parameters wherever it is used. */
160
- static read(sources: readonly string[]): TypeDoc[][] {
161
- const generics = new Set<string>();
162
- const parsed = sources.map((source) => SchemaDocs.parse(source, generics));
163
-
164
- for (const types of parsed) {
165
- for (const type of types) {
166
- if (type.definition) type.definition = SchemaDocs.withoutGenerics(type.definition, generics);
167
- if (type.includes) type.includes = type.includes.map((name) => SchemaDocs.withoutGenerics(name, generics));
168
- for (const field of type.fields) field.type = SchemaDocs.withoutGenerics(field.type, generics);
169
- }
170
- }
171
-
172
- return parsed;
173
- }
174
- }
@@ -1,508 +0,0 @@
1
- // Rules every network has to satisfy, whatever its design. Types catch most of this at compile time; these run again at load, because config is data and may not come from TypeScript.
2
-
3
- import { X509Certificate } from "node:crypto";
4
- import { Utils } from "./utils";
5
- import { Platforms } from "../platforms";
6
- import { Catalog } from "../platforms/catalog";
7
- import type { Device, Switched } from "../schema";
8
- import { Address } from "./address";
9
- import { CrossChecks } from "./cross-checks";
10
- import type { Finding, Network } from "./define";
11
-
12
- type Report = (message: string) => void;
13
-
14
- /** Names a device declares, and helpers to report a reference to one it does not. */
15
- class Names {
16
- readonly ports: string[];
17
-
18
- constructor(
19
- readonly device: Device,
20
- private readonly report: Report
21
- ) {
22
- let ports: string[] = [];
23
-
24
- try {
25
- ports = Catalog.ports(device.platform, device.model);
26
- } catch (error) {
27
- report((error as Error).message);
28
- }
29
-
30
- this.ports = ports;
31
- }
32
-
33
- isInterface(name: string): boolean {
34
- return name in (this.device.interfaces ?? {}) || this.ports.includes(name);
35
- }
36
-
37
- /** Report `name` when it is set and not declared. */
38
- expect(what: string, name: string | undefined, declared: boolean): void {
39
- if (name !== undefined && !declared) this.report(`${what} '${name}' is not declared`);
40
- }
41
-
42
- expectInterface(what: string, name: string | undefined): void {
43
- this.expect(what, name, name !== undefined && this.isInterface(name));
44
- }
45
- }
46
-
47
- type Checker = (where: string, value: string) => void;
48
-
49
- export class Validator {
50
- static run(network: Network): Finding[] {
51
- const findings: Finding[] = [];
52
-
53
- const seen = new Set<string>();
54
-
55
- for (const device of network.devices) {
56
- if (seen.has(device.name)) findings.push({ level: "error", device: device.name, message: "declared twice" });
57
- seen.add(device.name);
58
- }
59
-
60
- for (const device of network.devices) {
61
- const report: Report = (message) => findings.push({ level: "error", device: device.name, message });
62
- Validator.checkDevice(device, report);
63
- findings.push(...Validator.checkLimits(device));
64
-
65
- const adapter = Platforms.find(device.platform);
66
- if (!adapter) report(`no adapter for platform ${device.platform}`);
67
- else {
68
- for (const field of adapter.unsupported(device)) report(`${device.platform} cannot express ${field}`);
69
- findings.push(...(adapter.checks?.(device) ?? []));
70
- }
71
- }
72
-
73
- findings.push(...CrossChecks.run(network.devices));
74
- for (const check of network.checks ?? []) findings.push(...check(network.devices));
75
- return findings;
76
- }
77
-
78
- private static checkDevice(device: Device, report: Report): void {
79
- const names = new Names(device, report);
80
- const cidr: Checker = (where, value) => {
81
- if (!Address.isCidr(value)) report(`${where}: '${value}' is not an address with a length`);
82
- };
83
- const ip: Checker = (where, value) => {
84
- if (!Address.isValid(value)) report(`${where}: '${value}' is not an address`);
85
- };
86
-
87
- if (!device.users?.[device.connection.user]) report(`connection user '${device.connection.user}' is not one of its users`);
88
- if (!device.connection.host && !device.connection.console)
89
- report("connection has neither a host nor a console to reach the device through");
90
-
91
- Validator.checkVlans(device, report);
92
- Validator.checkAddresses(device, report, cidr, ip);
93
- Validator.checkPorts(device, names, report);
94
- Validator.checkInterfaces(device, names, report, ip);
95
- Validator.checkRouting(device, names, report, cidr, ip);
96
- Validator.checkPolicies(device, names, report, cidr);
97
- Validator.checkFirewall(device, names, cidr);
98
- Validator.checkServices(device, names, report, cidr);
99
- Validator.checkCertificates(device, names, report);
100
- Validator.checkVdoms(device, names, report, cidr, ip);
101
- }
102
-
103
- /** A management VDOM: FortiOS only, not `root`, with declared interfaces, and routes that leave through its own. */
104
- private static checkVdoms(device: Device, names: Names, report: Report, cidr: Checker, ip: Checker): void {
105
- const management = device.vdoms?.management;
106
- if (!management) return;
107
-
108
- if (device.platform !== "fortios") report("vdoms are a FortiOS setting");
109
- if (management.name === "root") report("vdoms.management.name is root, the VDOM everything else is in; give it a name of its own");
110
-
111
- for (const name of management.interfaces) {
112
- names.expectInterface("vdoms.management interface", name);
113
- const port = device.ports?.[name as keyof typeof device.ports];
114
- if (port?.lag) report(`vdoms.management interface ${name} is in LAG ${port.lag}; list the LAG`);
115
- }
116
-
117
- for (const route of management.static ?? []) {
118
- const where = `vdoms.management static route ${route.prefix}`;
119
- cidr(where, route.prefix);
120
- if (route.via) ip(where, route.via);
121
- if (!route.via && !route.interface && !route.blackhole) report(`${where} has no next hop`);
122
- if (route.interface && !management.interfaces.includes(route.interface))
123
- report(`${where} leaves through ${route.interface}, which is not in the VDOM`);
124
- }
125
- }
126
-
127
- /** What the model's hardware holds, from the catalog. A LAG with too many members is an error; too many switch rules is a warning, since the limit is only roughly known. */
128
- private static checkLimits(device: Device): Finding[] {
129
- const limits = Catalog.limits(device.platform, device.model);
130
- const findings: Finding[] = [];
131
- const report = (level: Finding["level"], message: string) => findings.push({ level, device: device.name, message });
132
-
133
- if (limits.lag_members !== undefined) {
134
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
135
- if (iface.type !== "lag") continue;
136
-
137
- const members = Object.values(device.ports ?? {}).filter((port) => port?.lag === name).length;
138
- if (members > limits.lag_members) {
139
- report("error", `LAG ${name} has ${members} members; ${device.model} holds at most ${limits.lag_members}`);
140
- }
141
- }
142
- }
143
-
144
- if (limits.switch_rules === undefined && limits.switch_port_ranges === undefined) return findings;
145
-
146
- const rules = Platforms.find(device.platform)?.switchRules?.(device);
147
- if (!rules) return findings;
148
-
149
- const after = "A rule it cannot hold is invalid, and so is every rule after it";
150
-
151
- if (limits.switch_rules !== undefined && rules.count > limits.switch_rules) {
152
- report("warning", `ACLs make ${rules.count} switch rules; a ${device.model} held about ${limits.switch_rules}. ${after}`);
153
- }
154
-
155
- if (limits.switch_port_ranges !== undefined && rules.ranges > limits.switch_port_ranges) {
156
- report(
157
- "warning",
158
- `ACLs use ${rules.ranges} distinct port ranges; a ${device.model} held ${limits.switch_port_ranges}. ${after}`
159
- );
160
- }
161
-
162
- return findings;
163
- }
164
-
165
- private static checkVlans(device: Device, report: Report): void {
166
- const ids = new Map<number, string>();
167
-
168
- for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
169
- if (!Number.isInteger(vlan.id) || vlan.id < 1 || vlan.id > 4094) report(`VLAN ${name} id ${vlan.id} is outside 1-4094`);
170
- if (ids.has(vlan.id)) report(`VLANs ${ids.get(vlan.id)} and ${name} share id ${vlan.id}`);
171
- ids.set(vlan.id, name);
172
- }
173
- }
174
-
175
- /** Every address is valid, and no address sits on two interfaces of one routing table. Two VRFs may hold the same address. */
176
- private static checkAddresses(device: Device, report: Report, cidr: Checker, ip: Checker): void {
177
- const holderOf = new Map<string, string>();
178
- const holders = [
179
- ...Object.entries(device.ports ?? {}).map(([name, settings]) => ({ where: `port ${name}`, settings })),
180
- ...Object.entries(device.interfaces ?? {}).map(([name, settings]) => ({ where: `interface ${name}`, settings })),
181
- ];
182
-
183
- for (const { where, settings } of holders) {
184
- for (const entry of settings?.addresses ?? []) {
185
- const address = typeof entry === "string" ? entry : entry.address;
186
- cidr(where, address);
187
- if (typeof entry !== "string") ip(where, entry.peer);
188
-
189
- const bare = address.split("/")[0]!;
190
- const key = `${settings?.vrf ?? ""} ${bare}`;
191
- if (holderOf.has(key)) report(`${bare} is on both ${holderOf.get(key)} and ${where}`);
192
- holderOf.set(key, where);
193
- }
194
-
195
- for (const group of settings?.vrrp ?? []) {
196
- ip(`${where} VRRP ${group.id}`, group.address);
197
- if (group.id < 1 || group.id > 255) report(`${where} VRRP id ${group.id} is outside 1-255`);
198
- }
199
- }
200
- }
201
-
202
- private static checkPorts(device: Device, names: Names, report: Report): void {
203
- const vlans = device.vlans ?? {};
204
-
205
- for (const [name, port] of Object.entries(device.ports ?? {})) {
206
- if (!port) continue;
207
-
208
- const switched = Boolean(port.access_vlan || port.trunk_vlans);
209
-
210
- if (!names.ports.includes(name)) report(`port ${name} does not exist on ${device.model}`);
211
- if (port.access_vlan && port.trunk_vlans) report(`port ${name} is both access and trunk`);
212
- if (port.native_vlan && !port.trunk_vlans) report(`port ${name} has a native VLAN but is not a trunk`);
213
- if (switched && port.addresses?.length) report(`port ${name} is switched and routed at once`);
214
-
215
- if (port.lag && (switched || port.addresses)) {
216
- report(`port ${name} is in a LAG and states its own switching or addresses; the LAG holds them`);
217
- }
218
-
219
- for (const vlan of [port.access_vlan, port.native_vlan, ...(port.trunk_vlans ?? [])]) {
220
- names.expect(`port ${name} VLAN`, vlan, vlan === undefined || vlan in vlans);
221
- }
222
-
223
- names.expect(`port ${name} ACL`, port.acl, port.acl !== undefined && port.acl in (device.acls ?? {}));
224
- names.expect(`port ${name} LAG`, port.lag, port.lag !== undefined && device.interfaces?.[port.lag]?.type === "lag");
225
- names.expect(`port ${name} VRF`, port.vrf, port.vrf !== undefined && port.vrf in (device.vrfs ?? {}));
226
- Validator.checkFrames(`port ${name}`, port, report);
227
- }
228
- }
229
-
230
- /** A MAC limit is a whole number above 0, and an ethertype filter lets at least one kind in, each once. */
231
- private static checkFrames(where: string, settings: Switched<string>, report: Report): void {
232
- const limit = settings.mac_limit;
233
- if (limit !== undefined && !(Number.isInteger(limit) && limit >= 1))
234
- report(`${where} mac_limit ${limit} is not a whole number above 0`);
235
-
236
- const kinds = settings.ethertypes;
237
- if (kinds && !kinds.length) report(`${where} ethertypes is empty, so it would drop every frame`);
238
- if (kinds && new Set(kinds).size !== kinds.length) report(`${where} ethertypes names a kind twice`);
239
- }
240
-
241
- private static checkInterfaces(device: Device, names: Names, report: Report, ip: Checker): void {
242
- const vlans = device.vlans ?? {};
243
-
244
- for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
245
- if (names.ports.includes(name)) report(`interface ${name} has the name of a port`);
246
- names.expect(`interface ${name} VRF`, iface.vrf, iface.vrf !== undefined && iface.vrf in (device.vrfs ?? {}));
247
-
248
- if (iface.type === "vlan") names.expect(`interface ${name} VLAN`, iface.vlan, iface.vlan in vlans);
249
- if (iface.type === "gre" || iface.type === "vxlan") {
250
- ip(`interface ${name}`, iface.local);
251
- ip(`interface ${name}`, iface.remote);
252
- }
253
-
254
- if (iface.type === "lag") {
255
- Validator.checkFrames(`LAG ${name}`, iface, report);
256
-
257
- const members = Object.values(device.ports ?? {}).filter((port) => port?.lag === name);
258
- if (!members.length) report(`LAG ${name} has no member ports`);
259
- }
260
-
261
- if (iface.type === "bridge") {
262
- if (iface.members.length < 2) report(`bridge ${name} joins fewer than two interfaces`);
263
-
264
- for (const member of iface.members) {
265
- const joined = device.interfaces?.[member];
266
- if (joined?.type !== "vlan") report(`bridge ${name}: member ${member} is not a VLAN interface`);
267
- else if (joined.addresses?.length) report(`bridge ${name}: member ${member} has an address of its own`);
268
-
269
- const others = Object.entries(device.interfaces ?? {}).filter(
270
- ([other, settings]) => other !== name && settings.type === "bridge" && settings.members.includes(member)
271
- );
272
- if (others.length) report(`bridge ${name}: member ${member} is also in bridge ${others[0]![0]}`);
273
- }
274
- }
275
-
276
- if (iface.type === "wireguard") {
277
- for (const peer of iface.peers ?? []) {
278
- for (const address of peer.allowed_addresses) {
279
- if (!Address.isCidr(address))
280
- report(`interface ${name} peer ${peer.name}: '${address}' is not an address with a length`);
281
- }
282
- }
283
- }
284
- }
285
- }
286
-
287
- private static checkRouting(device: Device, names: Names, report: Report, cidr: Checker, ip: Checker): void {
288
- const routing = device.routing;
289
- const policies = device.policies ?? {};
290
-
291
- for (const route of routing?.static ?? []) {
292
- cidr("static route", route.prefix);
293
- if (!route.via && !route.interface && !route.blackhole) report(`static route ${route.prefix} has no next hop`);
294
- if (route.via) ip(`static route ${route.prefix}`, route.via);
295
- if (route.bfd && !route.via) report(`static route ${route.prefix} has bfd without via, the address BFD watches`);
296
- names.expectInterface(`static route ${route.prefix} interface`, route.interface);
297
- names.expect(`static route ${route.prefix} VRF`, route.vrf, route.vrf !== undefined && route.vrf in (device.vrfs ?? {}));
298
- names.expect(
299
- `static route ${route.prefix} table`,
300
- route.table,
301
- route.table !== undefined && (route.table === "main" || route.table in (routing?.tables ?? {}))
302
- );
303
- if (route.vrf && Utils.otherTable(route.table))
304
- report(`static route ${route.prefix} has both vrf and table; a route is in one of them`);
305
- }
306
-
307
- Validator.checkTables(device, names, report, cidr);
308
-
309
- const bgp = routing?.bgp;
310
- const isPolicy = (policy: string | undefined) => policy !== undefined && policy in policies;
311
-
312
- for (const prefix of bgp?.networks ?? []) cidr("BGP network", prefix);
313
-
314
- const isTable = (table: string | undefined) => table !== undefined && (table === "main" || table in (routing?.tables ?? {}));
315
-
316
- for (const [name, group] of Object.entries(bgp?.groups ?? {})) {
317
- for (const policy of [group.import, group.export]) {
318
- names.expect(`BGP group ${name} policy`, policy, isPolicy(policy));
319
- }
320
-
321
- names.expect(`BGP group ${name} table`, group.table, isTable(group.table));
322
- }
323
-
324
- for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
325
- ip(`neighbor ${name}`, neighbor.address);
326
-
327
- const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
328
- names.expect(`neighbor ${name} group`, neighbor.group, group !== undefined);
329
-
330
- if (!(neighbor.remote_as ?? group?.remote_as)) report(`neighbor ${name} has no remote_as`);
331
-
332
- for (const policy of [neighbor.import, neighbor.export]) {
333
- names.expect(`neighbor ${name} policy`, policy, isPolicy(policy));
334
- }
335
-
336
- names.expect(`neighbor ${name} table`, neighbor.table, isTable(neighbor.table));
337
-
338
- const local = neighbor.local_address ?? group?.local_address;
339
- if (local && Address.family(local) !== Address.family(neighbor.address))
340
- report(`neighbor ${name} local_address and address differ in family`);
341
- }
342
-
343
- for (const [areaId, area] of Object.entries(routing?.ospf?.areas ?? {})) {
344
- if (!/^\d+\.\d+\.\d+\.\d+$/.test(areaId)) report(`OSPF area '${areaId}' is not written as a dotted area ID`);
345
- for (const iface of Object.keys(area.interfaces)) names.expectInterface(`OSPF area ${areaId} interface`, iface);
346
- }
347
- }
348
-
349
- /** Routing tables have names of their own and something installing routes in them, and rules name a table and can each be reached. */
350
- private static checkTables(device: Device, names: Names, report: Report, cidr: Checker): void {
351
- const routing = device.routing;
352
- const tables = routing?.tables ?? {};
353
- const bgp = routing?.bgp;
354
-
355
- const installed = new Set<string>();
356
- for (const route of routing?.static ?? []) {
357
- const table = Utils.otherTable(route.table);
358
- if (table) installed.add(table);
359
- }
360
- for (const neighbor of Object.values(bgp?.neighbors ?? {})) {
361
- const table = Utils.otherTable(neighbor.table ?? (neighbor.group ? bgp?.groups?.[neighbor.group]?.table : undefined));
362
- if (table) installed.add(table);
363
- }
364
-
365
- for (const name of Object.keys(tables)) {
366
- if (name === "main") report("routing.tables has main, the main table every device has; give the table another name");
367
- if (name in (device.vrfs ?? {})) report(`routing.tables ${name} has the name of a VRF; give the table another name`);
368
- if (name !== "main" && !installed.has(name))
369
- report(
370
- `routing.tables ${name}: nothing installs routes in it; give a static route or a BGP neighbor table: "${name}", or remove the table`
371
- );
372
- }
373
-
374
- const rules = routing?.rules ?? [];
375
- rules.forEach((rule, index) => {
376
- const where = `routing.rules ${index + 1}${rule.description ? ` (${rule.description})` : ""}`;
377
- const match = rule.match ?? {};
378
-
379
- names.expect(`${where} table`, rule.table, rule.table === "main" || rule.table in tables);
380
- names.expectInterface(`${where} in_interface`, match.in_interface);
381
- if (match.src) cidr(where, match.src);
382
- if (match.dst) cidr(where, match.dst);
383
-
384
- if (match.src && match.dst && Address.isCidr(match.src) && Address.isCidr(match.dst)) {
385
- if (Address.family(Address.host(match.src)) !== Address.family(Address.host(match.dst)))
386
- report(`${where}: src and dst differ in family, so no packet matches both`);
387
- }
388
-
389
- if (rule.table === "main" && rule.fallback)
390
- report(`${where} looks up main and has fallback; fallback is for another table, so remove it`);
391
-
392
- const matchesAll = !match.src && !match.dst && !match.in_interface;
393
- if (matchesAll && index < rules.length - 1)
394
- report(`${where} matches every packet, so the rules after it never run; give it a match, or move it last`);
395
- });
396
- }
397
-
398
- private static checkPolicies(device: Device, names: Names, report: Report, cidr: Checker): void {
399
- const policies = device.policies ?? {};
400
- const sets = device.prefix_sets ?? {};
401
-
402
- for (const [name, rules] of Object.entries(policies)) {
403
- rules.forEach((rule, index) => {
404
- // A device runs a call for every route, so a match beside it would read as a condition it isn't.
405
- const beside = (["match", "set", "action"] as const).filter((key) => rule.call !== undefined && rule[key] !== undefined);
406
- if (beside.length) {
407
- report(
408
- `policy ${name} rule ${index + 1} has call and ${beside.join(", ")}: a call is a rule of its own, run for every route. Put ${beside.join(", ")} in the next rule, or the match inside ${rule.call}`
409
- );
410
- }
411
-
412
- names.expect(`policy ${name} call`, rule.call, rule.call !== undefined && rule.call in policies);
413
- const set = rule.match?.prefix_set;
414
- names.expect(`policy ${name} prefix set`, set, set !== undefined && set in sets);
415
- if (rule.match?.prefix) cidr(`policy ${name}`, rule.match.prefix);
416
- });
417
- }
418
-
419
- // A policy that calls itself, directly or not, never finishes.
420
- const visit = (policy: string, path: string[]): void => {
421
- if (path.includes(policy)) return report(`policy ${[...path, policy].join(" -> ")} calls itself`);
422
-
423
- for (const rule of policies[policy] ?? []) if (rule.call) visit(rule.call, [...path, policy]);
424
- };
425
- for (const policy of Object.keys(policies)) visit(policy, []);
426
- }
427
-
428
- private static checkFirewall(device: Device, names: Names, cidr: Checker): void {
429
- const firewall = device.firewall;
430
- const sets = firewall?.address_sets ?? {};
431
-
432
- for (const [name, members] of Object.entries(sets)) {
433
- for (const member of members) {
434
- if (!Address.isValid(member)) cidr(`address set ${name}`, member);
435
- }
436
- }
437
-
438
- const filterChains = [firewall?.filter?.input, firewall?.filter?.forward, firewall?.filter?.output];
439
- const filterRules = filterChains.flatMap((chain) => chain?.rules ?? []);
440
- const natRules = [...(firewall?.nat?.source ?? []), ...(firewall?.nat?.destination ?? [])];
441
-
442
- for (const rule of [...filterRules, ...natRules]) {
443
- const match = rule.match;
444
- if (!match) continue;
445
-
446
- for (const set of [match.src_set, match.dst_set]) {
447
- names.expect("address set", set, set !== undefined && set in sets);
448
- }
449
-
450
- for (const iface of [match.in_interface, match.out_interface]) names.expectInterface("interface", iface);
451
-
452
- for (const address of [match.src, match.dst]) {
453
- if (address) cidr("firewall rule", address);
454
- }
455
- }
456
- }
457
-
458
- private static checkServices(device: Device, names: Names, report: Report, cidr: Checker): void {
459
- for (const [name, server] of Object.entries(device.dhcp ?? {})) {
460
- names.expectInterface(`DHCP ${name} interface`, server.interface);
461
- cidr(`DHCP ${name}`, server.network);
462
- if (!Address.isCidr(server.network)) continue;
463
-
464
- const reserved = (server.reservations ?? []).map((reservation) => reservation.address);
465
- const addresses = [server.gateway, ...server.pool, ...reserved];
466
-
467
- for (const address of addresses) {
468
- if (!Address.contains(server.network, address)) report(`DHCP ${name}: ${address} is outside ${server.network}`);
469
- }
470
- }
471
-
472
- for (const [name, relay] of Object.entries(device.dhcp_relay ?? {})) {
473
- names.expectInterface(`DHCP relay ${name} interface`, relay.interface);
474
- }
475
-
476
- for (const iface of device.flow_export?.interfaces ?? []) names.expectInterface("flow export interface", iface);
477
-
478
- const lldp = device.lldp;
479
- if (lldp && lldp !== true) for (const iface of lldp.interfaces) names.expectInterface("LLDP interface", iface);
480
-
481
- const management = device.management;
482
- for (const iface of management?.native?.interfaces ?? []) names.expectInterface("native management interface", iface);
483
- for (const prefix of management?.allow ?? []) cidr("management allow", prefix);
484
- if (management?.snmp && !management.snmp.community && !management.snmp.users)
485
- report("SNMP is on with neither a community nor a user");
486
- }
487
-
488
- private static checkCertificates(device: Device, names: Names, report: Report): void {
489
- const certificates = device.certificates ?? {};
490
-
491
- for (const [name, certificate] of Object.entries(certificates)) {
492
- try {
493
- new X509Certificate(certificate.certificate);
494
- } catch {
495
- report(`certificate ${name} is not a PEM certificate`);
496
- }
497
- }
498
-
499
- for (const service of ["https", "api_tls"] as const) {
500
- const name = device.management?.[service]?.certificate;
501
- names.expect(`${service} certificate`, name, name !== undefined && name in certificates);
502
-
503
- if (name && certificates[name] && !certificates[name].private_key) {
504
- report(`${service} certificate ${name} has no private key to serve with`);
505
- }
506
- }
507
- }
508
- }