@takodotid/circuit 0.25.0 → 0.26.1

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 (43) hide show
  1. package/README.md +7 -7
  2. package/dist/chunks/{index-wm9ypkjq.js → index-22sbskdx.js} +13 -5
  3. package/dist/chunks/{server-n2q4tzy4.js → server-1ftkqkx5.js} +1 -1
  4. package/dist/cli/index.js +2990 -264
  5. package/dist/types/adapters/devices/catalog.d.ts +14 -0
  6. package/dist/types/adapters/devices/fortios/firewall.d.ts +21 -0
  7. package/dist/types/adapters/devices/fortios/index.d.ts +55 -0
  8. package/dist/types/adapters/devices/fortios/models.d.ts +14 -0
  9. package/dist/types/adapters/devices/fortios/names.d.ts +32 -0
  10. package/dist/types/adapters/devices/fortios/plan.d.ts +26 -0
  11. package/dist/types/adapters/devices/fortios/render.d.ts +36 -0
  12. package/dist/types/adapters/devices/fortios/routing.d.ts +32 -0
  13. package/dist/types/adapters/devices/fortios/tree.d.ts +33 -0
  14. package/dist/types/schema/common.d.ts +1 -1
  15. package/dist/types/schema/device.d.ts +1 -1
  16. package/dist/types/schema/firewall.d.ts +3 -3
  17. package/dist/types/schema/interfaces.d.ts +13 -13
  18. package/dist/types/schema/policy.d.ts +3 -3
  19. package/dist/types/schema/routing.d.ts +7 -7
  20. package/dist/types/schema/system.d.ts +7 -7
  21. package/package.json +5 -2
  22. package/src/adapters/devices/catalog.ts +2 -1
  23. package/src/adapters/devices/fortios/TRAPS.md +40 -0
  24. package/src/adapters/devices/fortios/firewall.ts +352 -0
  25. package/src/adapters/devices/fortios/index.ts +800 -0
  26. package/src/adapters/devices/fortios/models.ts +7 -0
  27. package/src/adapters/devices/fortios/names.ts +78 -0
  28. package/src/adapters/devices/fortios/plan.ts +840 -0
  29. package/src/adapters/devices/fortios/render.ts +734 -0
  30. package/src/adapters/devices/fortios/routing.ts +444 -0
  31. package/src/adapters/devices/fortios/tree.ts +121 -0
  32. package/src/adapters/devices/index.ts +2 -1
  33. package/src/adapters/devices/junos/TRAPS.md +2 -2
  34. package/src/adapters/devices/raisecom-ros/TRAPS.md +2 -2
  35. package/src/adapters/devices/routeros/TRAPS.md +5 -5
  36. package/src/adapters/devices/vrp/TRAPS.md +4 -4
  37. package/src/schema/common.ts +1 -1
  38. package/src/schema/device.ts +1 -1
  39. package/src/schema/firewall.ts +3 -3
  40. package/src/schema/interfaces.ts +13 -13
  41. package/src/schema/policy.ts +3 -3
  42. package/src/schema/routing.ts +7 -7
  43. package/src/schema/system.ts +7 -7
package/dist/cli/index.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  addressesOf,
12
12
  freeOfLength,
13
13
  studioData
14
- } from "../chunks/index-wm9ypkjq.js";
14
+ } from "../chunks/index-22sbskdx.js";
15
15
  import {
16
16
  familyOf2,
17
17
  split,
@@ -202,6 +202,2732 @@ function upload(client, path, content) {
202
202
  });
203
203
  }
204
204
 
205
+ // src/adapters/devices/fortios/names.ts
206
+ function nameOf(device, name) {
207
+ if (device.interfaces && name in device.interfaces)
208
+ return name;
209
+ return spell(device.platform, device.model, name);
210
+ }
211
+ function settingsOf(device, name) {
212
+ return device.interfaces?.[name] ?? device.ports?.[name];
213
+ }
214
+ function addressesOf2(settings) {
215
+ return (settings?.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
216
+ }
217
+ var withLength = (value) => value.includes("/") ? value : `${value}/${familyOf2(value) === "ipv4" ? 32 : 128}`;
218
+ var familyOfPrefix = (value) => familyOf2(split(withLength(value)).addr);
219
+ var subnet = (prefix) => `${host2(prefix)} ${netmask(split(prefix).len)}`;
220
+ var network2 = (prefix) => network(withLength(prefix));
221
+ function vrfNumber(device, vrf) {
222
+ return vrf ? Object.keys(device.vrfs ?? {}).indexOf(vrf) + 1 : 0;
223
+ }
224
+ function routedHolders(device) {
225
+ const holders = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
226
+ return holders.filter((holder) => holder[1] !== undefined && !holder[1].lag);
227
+ }
228
+ function interfaceFor(device, address, vrf) {
229
+ let best;
230
+ for (const [name, settings] of routedHolders(device)) {
231
+ if ((settings.vrf ?? undefined) !== vrf)
232
+ continue;
233
+ for (const own of addressesOf2(settings)) {
234
+ const length = split(own).len;
235
+ const holds = familyOf2(host2(own)) === familyOf2(address) && contains2(network2(own), address);
236
+ if (holds && length > (best?.length ?? -1))
237
+ best = { name, length };
238
+ }
239
+ }
240
+ return best?.name;
241
+ }
242
+ function holderOf(device, address) {
243
+ return routedHolders(device).find(([, settings]) => addressesOf2(settings).some((own) => host2(own) === address))?.[0];
244
+ }
245
+ function addressed(device) {
246
+ return routedHolders(device).filter(([, settings]) => addressesOf2(settings).length > 0).map(([name]) => name);
247
+ }
248
+
249
+ // src/adapters/devices/fortios/tree.ts
250
+ var UNSET = "\x00unset";
251
+
252
+ class Block {
253
+ settings = new Map;
254
+ blocks = new Map;
255
+ entries = new Map;
256
+ set(key, value) {
257
+ this.settings.set(key, String(value));
258
+ return this;
259
+ }
260
+ block(header) {
261
+ if (!this.blocks.has(header))
262
+ this.blocks.set(header, new Block);
263
+ return this.blocks.get(header);
264
+ }
265
+ entry(id, plain = false) {
266
+ const written = typeof id === "number" || plain ? String(id) : quote(id);
267
+ const key = bare(written);
268
+ if (!this.entries.has(key))
269
+ this.entries.set(key, { id: written, block: new Block });
270
+ return this.entries.get(key).block;
271
+ }
272
+ isEmpty() {
273
+ return !this.settings.size && !this.blocks.size && !this.entries.size;
274
+ }
275
+ }
276
+ function quote(text) {
277
+ return `"${text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
278
+ }
279
+ var quoteAll = (texts) => texts.map(quote).join(" ");
280
+ function wordsOf(value) {
281
+ return value.match(/"(?:[^"\\]|\\.)*"|'[^']*'|\S+/g) ?? [];
282
+ }
283
+ function bare(word) {
284
+ if (word.startsWith('"'))
285
+ return word.slice(1, -1).replace(/\\(.)/g, "$1");
286
+ if (word.startsWith("'"))
287
+ return word.slice(1, -1);
288
+ return word;
289
+ }
290
+ function linesOf(block, indent = "") {
291
+ const lines = [];
292
+ for (const [key, value] of block.settings)
293
+ lines.push(value === UNSET ? `${indent}unset ${key}` : `${indent}set ${key} ${value}`);
294
+ for (const [header, child] of block.blocks)
295
+ lines.push(`${indent}config ${header}`, ...linesOf(child, indent + " "), `${indent}end`);
296
+ for (const { id, block: entry } of block.entries.values()) {
297
+ lines.push(`${indent}edit ${id}`, ...linesOf(entry, indent + " "), `${indent}next`);
298
+ }
299
+ return lines;
300
+ }
301
+ function print(root) {
302
+ const lines = [];
303
+ for (const [section, block] of root.blocks)
304
+ lines.push(`config ${section}`, ...linesOf(block, " "), "end");
305
+ return lines.join(`
306
+ `) + `
307
+ `;
308
+ }
309
+ var unclosed = (line) => (line.replace(/\\./g, "").match(/"/g)?.length ?? 0) % 2 === 1;
310
+ function parse(text) {
311
+ const root = new Block;
312
+ const stack = [{ block: root, isEntry: false }];
313
+ const lines = text.replace(/\r/g, "").split(`
314
+ `);
315
+ for (let index = 0;index < lines.length; index++) {
316
+ let line = lines[index].trim();
317
+ if (!line || line.startsWith("#"))
318
+ continue;
319
+ while (unclosed(line) && index + 1 < lines.length)
320
+ line += `
321
+ ` + lines[++index];
322
+ const current = stack.at(-1).block;
323
+ const [verb] = line.split(/\s+/, 1);
324
+ const rest = line.slice(verb.length).trim();
325
+ if (verb === "config") {
326
+ const child = current.block(rest);
327
+ stack.push({ block: child, isEntry: false });
328
+ } else if (verb === "edit") {
329
+ const key = bare(rest);
330
+ if (!current.entries.has(key))
331
+ current.entries.set(key, { id: rest, block: new Block });
332
+ stack.push({ block: current.entries.get(key).block, isEntry: true });
333
+ } else if (verb === "next") {
334
+ if (stack.at(-1).isEntry)
335
+ stack.pop();
336
+ } else if (verb === "end") {
337
+ if (stack.at(-1).isEntry)
338
+ stack.pop();
339
+ if (stack.length > 1)
340
+ stack.pop();
341
+ } else if (verb === "set") {
342
+ const [key] = rest.split(/\s+/, 1);
343
+ current.settings.set(key, rest.slice(key.length).trim());
344
+ } else if (verb === "unset") {
345
+ current.settings.set(rest, UNSET);
346
+ }
347
+ }
348
+ return root;
349
+ }
350
+
351
+ // src/adapters/devices/fortios/firewall.ts
352
+ var PROTOCOL_NUMBER = { tcp: 6, udp: 17, icmp: 1, gre: 47, esp: 50, ah: 51, ospf: 89, vrrp: 112 };
353
+ var SERVICES = {
354
+ ssh: { protocol: "tcp", port: 22 },
355
+ https: { protocol: "tcp", port: 443 },
356
+ http: { protocol: "tcp", port: 80 },
357
+ telnet: { protocol: "tcp", port: 23 },
358
+ snmp: { protocol: "udp", port: 161 },
359
+ native: { protocol: "tcp", port: 541 }
360
+ };
361
+ function natRules(device) {
362
+ const nat = device.firewall?.nat;
363
+ return [...nat?.source ?? [], ...nat?.destination ?? []];
364
+ }
365
+ function familiesOf(device, match) {
366
+ if (match?.family)
367
+ return [match.family];
368
+ const prefixes = [match?.src, match?.dst].filter((prefix) => prefix !== undefined);
369
+ if (prefixes.length)
370
+ return [...new Set(prefixes.map(familyOfPrefix))];
371
+ let families = ["ipv4", "ipv6"];
372
+ for (const set of [match?.src_set, match?.dst_set]) {
373
+ if (!set)
374
+ continue;
375
+ const present = new Set((device.firewall?.address_sets?.[set] ?? []).map(familyOfPrefix));
376
+ families = families.filter((family) => present.has(family));
377
+ }
378
+ return families;
379
+ }
380
+ function portList(ports) {
381
+ if (ports === undefined)
382
+ return [];
383
+ const list = Array.isArray(ports) ? ports : [ports];
384
+ return list.map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
385
+ }
386
+ function serviceOf(match, family) {
387
+ const protocol = match?.protocol;
388
+ const destinations = portList(match?.dst_port);
389
+ const sources = portList(match?.src_port);
390
+ if (!protocol && !destinations.length && !sources.length)
391
+ return { name: "ALL" };
392
+ if (!protocol || protocol === "tcp" || protocol === "udp") {
393
+ const ranges = (destinations.length ? destinations : ["1-65535"]).flatMap((destination) => sources.length ? sources.map((source) => `${destination}:${source}`) : [destination]);
394
+ const protocols = protocol ? [protocol] : ["tcp", "udp"];
395
+ const name = `${protocols.join("+")}/${destinations.join(",") || "any"}${sources.length ? ` from ${sources.join(",")}` : ""}`;
396
+ const block = new Block;
397
+ for (const each of protocols)
398
+ block.set(`${each}-portrange`, ranges.join(" "));
399
+ return { name, block };
400
+ }
401
+ if (protocol === "icmp") {
402
+ const kind = family === "ipv4" ? "ICMP" : "ICMP6";
403
+ return { name: kind.toLowerCase(), block: new Block().set("protocol", kind).set("icmptype", UNSET) };
404
+ }
405
+ const number = PROTOCOL_NUMBER[protocol];
406
+ return { name: `protocol/${number}`, block: new Block().set("protocol", "IP").set("protocol-number", number) };
407
+ }
408
+ function isImplied(rule) {
409
+ const state = rule.match?.state;
410
+ return rule.action === "accept" && state !== undefined && state.length > 0 && state.every((one) => one === "established" || one === "related");
411
+ }
412
+
413
+ class Objects {
414
+ root;
415
+ device;
416
+ constructor(root, device) {
417
+ this.root = root;
418
+ this.device = device;
419
+ }
420
+ address(prefix, set, family) {
421
+ if (set)
422
+ return this.group(set, this.device.firewall?.address_sets?.[set] ?? [], family);
423
+ if (!prefix)
424
+ return "all";
425
+ return this.prefix(prefix, family);
426
+ }
427
+ prefix(prefix, family) {
428
+ const name = network2(prefix);
429
+ if (family === "ipv4")
430
+ this.root.block("firewall address").entry(name).set("subnet", subnet(name));
431
+ else
432
+ this.root.block("firewall address6").entry(name).set("ip6", name);
433
+ return name;
434
+ }
435
+ group(name, members, family) {
436
+ const names = members.filter((member) => familyOfPrefix(member) === family).map((member) => this.prefix(member, family));
437
+ const table = family === "ipv4" ? "firewall addrgrp" : "firewall addrgrp6";
438
+ this.root.block(table).entry(name).set("member", quoteAll([...new Set(names)]));
439
+ return name;
440
+ }
441
+ pool(address) {
442
+ this.root.block("firewall ippool").entry(address).set("startip", address).set("endip", address);
443
+ }
444
+ service(match, family) {
445
+ const { name, block } = serviceOf(match, family);
446
+ if (!block)
447
+ return name;
448
+ const entry = this.root.block("firewall service custom").entry(name);
449
+ for (const [key, value] of block.settings)
450
+ entry.set(key, value);
451
+ return name;
452
+ }
453
+ }
454
+ function firewall(root, device) {
455
+ const fw = device.firewall;
456
+ const objects = new Objects(root, device);
457
+ const forward = fw?.filter?.forward;
458
+ const policies = { ipv4: 0, ipv6: 0 };
459
+ for (const rule of forward?.rules ?? []) {
460
+ if (isImplied(rule))
461
+ continue;
462
+ for (const family of familiesOf(device, rule.match)) {
463
+ const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
464
+ policy(table.entry(++policies[family]), objects, device, rule, family);
465
+ }
466
+ }
467
+ if (forward?.default !== "drop") {
468
+ for (const family of ["ipv4", "ipv6"]) {
469
+ const table = root.block(family === "ipv4" ? "firewall policy" : "firewall policy6");
470
+ policy(table.entry(++policies[family]), objects, device, { action: "accept" }, family);
471
+ }
472
+ }
473
+ const localIns = { ipv4: 0, ipv6: 0 };
474
+ const localInEntry = (family) => root.block(family === "ipv4" ? "firewall local-in-policy" : "firewall local-in-policy6").entry(++localIns[family]);
475
+ management(localInEntry, objects, device);
476
+ const input = fw?.filter?.input;
477
+ for (const rule of input?.rules ?? []) {
478
+ if (isImplied(rule))
479
+ continue;
480
+ for (const family of familiesOf(device, rule.match))
481
+ localIn(localInEntry(family), objects, device, rule, family);
482
+ }
483
+ if (input?.default === "drop") {
484
+ for (const family of ["ipv4", "ipv6"])
485
+ localIn(localInEntry(family), objects, device, { action: "drop" }, family);
486
+ }
487
+ (fw?.nat?.source ?? []).forEach((rule, index) => sourceNat(root.block("firewall central-snat-map").entry(index + 1), objects, device, rule));
488
+ (fw?.nat?.destination ?? []).forEach((rule, index) => destinationNat(root.block("firewall vip").entry(`dnat-${index + 1}`), device, rule));
489
+ acls(root, objects, device);
490
+ }
491
+ function management(entry, objects, device) {
492
+ const config = device.management ?? {};
493
+ const guards = new Map;
494
+ for (const [service, listener] of Object.entries(SERVICES)) {
495
+ const settings = config[service];
496
+ const sources = settings?.allow ?? config.allow;
497
+ if (!settings || !sources?.length)
498
+ continue;
499
+ const name = settings.allow ? `circuit-${service}` : "circuit-management";
500
+ const guard = guards.get(name) ?? { sources, services: [] };
501
+ guard.services.push(objects.service({ protocol: listener.protocol, dst_port: settings.port ?? listener.port }, "ipv4"));
502
+ guards.set(name, guard);
503
+ }
504
+ for (const [name, guard] of guards) {
505
+ for (const family of ["ipv4", "ipv6"]) {
506
+ const services = quoteAll(guard.services);
507
+ if (guard.sources.some((source) => familyOfPrefix(source) === family)) {
508
+ const accept = entry(family);
509
+ accept.set("intf", quote("any"));
510
+ accept.set("srcaddr", quote(objects.group(name, guard.sources, family)));
511
+ accept.set("dstaddr", quote("all"));
512
+ accept.set("action", "accept");
513
+ accept.set("service", services);
514
+ accept.set("schedule", quote("always"));
515
+ accept.set("comments", quote("Management from allowed sources"));
516
+ }
517
+ const deny = entry(family);
518
+ deny.set("intf", quote("any"));
519
+ deny.set("srcaddr", quote("all"));
520
+ deny.set("dstaddr", quote("all"));
521
+ deny.set("action", "deny");
522
+ deny.set("service", services);
523
+ deny.set("schedule", quote("always"));
524
+ deny.set("comments", quote("Management from anywhere else"));
525
+ }
526
+ }
527
+ }
528
+ function policy(entry, objects, device, rule, family) {
529
+ const match = rule.match;
530
+ entry.set("srcintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
531
+ entry.set("dstintf", quote(match?.out_interface ? nameOf(device, match.out_interface) : "any"));
532
+ entry.set("srcaddr", quote(objects.address(match?.src, match?.src_set, family)));
533
+ entry.set("dstaddr", quote(objects.address(match?.dst, match?.dst_set, family)));
534
+ entry.set("action", rule.action === "accept" ? "accept" : "deny");
535
+ entry.set("schedule", quote("always"));
536
+ entry.set("service", quote(objects.service(match, family)));
537
+ if (rule.action !== "accept")
538
+ entry.set("logtraffic", "disable");
539
+ if (rule.action === "accept" && !device.hardware_offload && !rule.offload)
540
+ entry.set("auto-asic-offload", "disable");
541
+ if (rule.description)
542
+ entry.set("comments", quote(rule.description));
543
+ if (rule.action === "reject")
544
+ entry.set("send-deny-packet", "enable");
545
+ }
546
+ function localIn(entry, objects, device, rule, family) {
547
+ const match = rule.match;
548
+ entry.set("intf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
549
+ entry.set("srcaddr", quote(objects.address(match?.src, match?.src_set, family)));
550
+ entry.set("dstaddr", quote(objects.address(match?.dst, match?.dst_set, family)));
551
+ entry.set("action", rule.action === "accept" ? "accept" : "deny");
552
+ entry.set("service", quote(objects.service(match, family)));
553
+ entry.set("schedule", quote("always"));
554
+ if (rule.description)
555
+ entry.set("comments", quote(rule.description));
556
+ }
557
+ function sourceNat(entry, objects, device, rule) {
558
+ const match = rule.match;
559
+ entry.set("orig-addr", quote(objects.address(match?.src, match?.src_set, "ipv4")));
560
+ entry.set("srcintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
561
+ entry.set("dst-addr", quote(objects.address(match?.dst, match?.dst_set, "ipv4")));
562
+ entry.set("dstintf", quote(match?.out_interface ? nameOf(device, match.out_interface) : "any"));
563
+ if (match?.protocol)
564
+ entry.set("protocol", PROTOCOL_NUMBER[match.protocol]);
565
+ const ports = portList(match?.src_port);
566
+ if (rule.action === "snat") {
567
+ objects.pool(rule.to);
568
+ entry.set("nat-ippool", quote(rule.to));
569
+ if (ports.length)
570
+ entry.set("orig-port", ports[0]).set("nat-port", rule.to_port ?? ports[0]);
571
+ } else if (ports.length) {
572
+ entry.set("orig-port", ports[0]);
573
+ }
574
+ if (rule.description)
575
+ entry.set("comments", quote(rule.description));
576
+ }
577
+ function destinationNat(entry, device, rule) {
578
+ if (rule.action !== "dnat")
579
+ return;
580
+ const match = rule.match;
581
+ if (rule.description)
582
+ entry.set("comment", quote(rule.description));
583
+ if (match?.dst)
584
+ entry.set("extip", host2(withLength(match.dst)));
585
+ entry.set("extintf", quote(match?.in_interface ? nameOf(device, match.in_interface) : "any"));
586
+ const sources = match?.src ? [match.src] : match?.src_set ? device.firewall?.address_sets?.[match.src_set] ?? [] : [];
587
+ if (sources.length)
588
+ entry.set("src-filter", quoteAll(sources.map(network2)));
589
+ const ports = portList(match?.dst_port);
590
+ if (ports.length)
591
+ entry.set("portforward", "enable");
592
+ entry.set("mappedip", quote(rule.to));
593
+ if (ports.length) {
594
+ if (match?.protocol === "udp")
595
+ entry.set("protocol", "udp");
596
+ entry.set("extport", ports[0]);
597
+ entry.set("mappedport", rule.to_port ?? ports[0]);
598
+ }
599
+ }
600
+ function acls(root, objects, device) {
601
+ const counters = { ipv4: 0, ipv6: 0 };
602
+ for (const [port, settings] of Object.entries(device.ports ?? {})) {
603
+ if (!settings?.acl)
604
+ continue;
605
+ for (const rule of device.acls?.[settings.acl] ?? []) {
606
+ if (rule.action !== "drop")
607
+ continue;
608
+ const match = rule.match;
609
+ for (const family of familiesOf(device, match)) {
610
+ const entry = root.block(family === "ipv4" ? "firewall acl" : "firewall acl6").entry(++counters[family]);
611
+ if (rule.description)
612
+ entry.set("comments", quote(rule.description));
613
+ entry.set("interface", quote(nameOf(device, port)));
614
+ entry.set("srcaddr", quote(objects.address(match?.src, undefined, family)));
615
+ entry.set("dstaddr", quote(objects.address(match?.dst, undefined, family)));
616
+ entry.set("service", quote(objects.service(match, family)));
617
+ }
618
+ }
619
+ }
620
+ }
621
+ function acceptsBeforeDrop(rules) {
622
+ const lastDrop = rules.map((rule) => rule.action).lastIndexOf("drop");
623
+ return rules.slice(0, Math.max(0, lastDrop)).some((rule) => rule.action === "accept");
624
+ }
625
+
626
+ // src/adapters/devices/fortios/routing.ts
627
+ var REJECT_ALL = "circuit-reject-all";
628
+ var FILLED = "Filled by circuit refresh";
629
+ var PREFIX_LIST = { ipv4: "router prefix-list", ipv6: "router prefix-list6" };
630
+ var BITS = { ipv4: 32, ipv6: 128 };
631
+ function neighborSettings(device, neighbor) {
632
+ const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
633
+ return { ...group, ...neighbor };
634
+ }
635
+ function familiesOfNeighbor(device, neighbor) {
636
+ return [...neighborSettings(device, neighbor).families ?? [familyOf2(neighbor.address)]];
637
+ }
638
+ function usesBfd(device) {
639
+ const areas = Object.values(device.routing?.ospf?.areas ?? {});
640
+ const ospf = areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
641
+ const bgp = Object.values(device.routing?.bgp?.neighbors ?? {}).some((neighbor) => neighborSettings(device, neighbor).bfd);
642
+ return ospf || bgp;
643
+ }
644
+ function routing(root, device) {
645
+ prefixLists(root, device);
646
+ ospf(root, device);
647
+ ospf6(root, device);
648
+ bgp(root, device);
649
+ }
650
+ var listPrefix = (prefix, family) => family === "ipv4" ? subnet(network2(prefix)) : network2(prefix);
651
+ function listRule(rule, family, prefix, lengths = {}) {
652
+ rule.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
653
+ rule.set("ge", lengths.ge ?? UNSET);
654
+ rule.set("le", lengths.le ?? UNSET);
655
+ }
656
+ function bounds(prefix, family, min = 0, max = BITS[family]) {
657
+ const own = split(network2(prefix)).len;
658
+ const low = Math.max(min, own);
659
+ if (max < low)
660
+ return;
661
+ if (low === own && max === own)
662
+ return {};
663
+ return { ...low > own ? { ge: low } : {}, le: max };
664
+ }
665
+ function prefixLists(root, device) {
666
+ for (const [name, set] of Object.entries(device.prefix_sets ?? {})) {
667
+ if ("source" in set) {
668
+ const { registry, query, family } = set.source;
669
+ const entry = root.block(PREFIX_LIST[family]).entry(name).set("comments", quote(`${FILLED} from ${registry} ${query}`));
670
+ const rule = entry.block("rule").entry(1);
671
+ listRule(rule, family, family === "ipv4" ? "0.0.0.0/0" : "::/0", { le: BITS[family] });
672
+ rule.set("action", "deny");
673
+ continue;
674
+ }
675
+ const family = set.prefixes[0] ? familyOfPrefix(set.prefixes[0]) : "ipv4";
676
+ const entry = root.block(PREFIX_LIST[family]).entry(name);
677
+ set.prefixes.forEach((prefix, index) => listRule(entry.block("rule").entry(index + 1), family, prefix, bounds(prefix, family)));
678
+ }
679
+ }
680
+ function networksOf(device, name, family) {
681
+ return addressesOf2(settingsOf(device, name)).filter((address) => familyOfPrefix(address) === family).map(network2);
682
+ }
683
+ function ospf(root, device) {
684
+ const ospf = device.routing?.ospf;
685
+ if (!ospf || !(ospf.families ?? ["ipv4"]).includes("ipv4"))
686
+ return;
687
+ const block = root.block("router ospf");
688
+ if (device.routing?.router_id)
689
+ block.set("router-id", device.routing.router_id);
690
+ const passive = [];
691
+ let networks = 0;
692
+ for (const [areaId, area] of Object.entries(ospf.areas)) {
693
+ block.block("area").entry(areaId, true);
694
+ for (const [name, settings] of Object.entries(area.interfaces)) {
695
+ const iface = nameOf(device, name);
696
+ if (settings?.passive)
697
+ passive.push(iface);
698
+ if (settings?.cost !== undefined || settings?.network === "point-to-point" || settings?.bfd) {
699
+ const entry = block.block("ospf-interface").entry(iface);
700
+ entry.set("interface", quote(iface));
701
+ if (settings.cost !== undefined)
702
+ entry.set("cost", settings.cost);
703
+ if (settings.network === "point-to-point")
704
+ entry.set("network-type", "point-to-point");
705
+ if (settings.bfd)
706
+ entry.set("bfd", "enable");
707
+ }
708
+ for (const prefix of networksOf(device, name, "ipv4")) {
709
+ const entry = block.block("network").entry(++networks);
710
+ entry.set("prefix", subnet(prefix));
711
+ if (areaId !== "0.0.0.0")
712
+ entry.set("area", areaId);
713
+ }
714
+ }
715
+ }
716
+ if (passive.length)
717
+ block.set("passive-interface", quoteAll(passive));
718
+ for (const source of ospf.redistribute ?? [])
719
+ block.block(`redistribute "${source}"`).set("status", "enable");
720
+ }
721
+ function ospf6(root, device) {
722
+ const ospf = device.routing?.ospf;
723
+ if (!ospf?.families?.includes("ipv6"))
724
+ return;
725
+ const block = root.block("router ospf6");
726
+ if (device.routing?.router_id)
727
+ block.set("router-id", device.routing.router_id);
728
+ const passive = [];
729
+ for (const [areaId, area] of Object.entries(ospf.areas)) {
730
+ block.block("area").entry(areaId, true);
731
+ for (const [name, settings] of Object.entries(area.interfaces)) {
732
+ const iface = nameOf(device, name);
733
+ if (settings?.passive)
734
+ passive.push(iface);
735
+ const entry = block.block("ospf6-interface").entry(iface);
736
+ entry.set("interface", quote(iface));
737
+ if (areaId !== "0.0.0.0")
738
+ entry.set("area-id", areaId);
739
+ if (settings?.cost !== undefined)
740
+ entry.set("cost", settings.cost);
741
+ if (settings?.network === "point-to-point")
742
+ entry.set("network-type", "point-to-point");
743
+ if (settings?.bfd)
744
+ entry.set("bfd", "enable");
745
+ }
746
+ }
747
+ if (passive.length)
748
+ block.set("passive-interface", quoteAll(passive));
749
+ for (const source of ospf.redistribute ?? [])
750
+ block.block(`redistribute "${source}"`).set("status", "enable");
751
+ }
752
+ function bgp(root, device) {
753
+ const bgp = device.routing?.bgp;
754
+ if (!bgp)
755
+ return;
756
+ const block = root.block("router bgp");
757
+ block.set("as", bgp.asn);
758
+ if (device.routing?.router_id)
759
+ block.set("router-id", device.routing.router_id);
760
+ const maps = new RouteMaps(root, device);
761
+ for (const neighbor of Object.values(bgp.neighbors ?? {})) {
762
+ const settings = neighborSettings(device, neighbor);
763
+ const families = familiesOfNeighbor(device, neighbor);
764
+ const entry = block.block("neighbor").entry(neighbor.address);
765
+ if (!families.includes("ipv4"))
766
+ entry.set("activate", "disable");
767
+ if (!families.includes("ipv6"))
768
+ entry.set("activate6", "disable");
769
+ if (settings.bfd)
770
+ entry.set("bfd", "enable");
771
+ if (settings.multihop)
772
+ entry.set("ebgp-enforce-multihop", "enable");
773
+ if (settings.passive)
774
+ entry.set("passive", "enable");
775
+ if (settings.description)
776
+ entry.set("description", quote(settings.description));
777
+ if (settings.remote_as !== undefined)
778
+ entry.set("remote-as", settings.remote_as);
779
+ for (const family of families) {
780
+ const suffix = family === "ipv4" ? "" : "6";
781
+ if (settings.max_prefixes !== undefined)
782
+ entry.set(`maximum-prefix${suffix}`, settings.max_prefixes);
783
+ entry.set(`route-map-in${suffix}`, quote(maps.of(settings.import, family)));
784
+ entry.set(`route-map-out${suffix}`, quote(maps.of(settings.export, family)));
785
+ }
786
+ if (settings.keepalive !== undefined)
787
+ entry.set("keep-alive-timer", settings.keepalive);
788
+ if (settings.hold_time !== undefined)
789
+ entry.set("holdtime-timer", settings.hold_time);
790
+ const source = settings.local_address ? holderOf(device, settings.local_address) : undefined;
791
+ if (source)
792
+ entry.set("update-source", quote(nameOf(device, source)));
793
+ if (settings.password)
794
+ entry.set("password", quote(marker(settings.password)));
795
+ }
796
+ const networks = { ipv4: 0, ipv6: 0 };
797
+ for (const prefix of bgp.networks ?? []) {
798
+ const family = familyOfPrefix(prefix);
799
+ const entry = block.block(family === "ipv4" ? "network" : "network6").entry(++networks[family]);
800
+ entry.set(family === "ipv4" ? "prefix" : "prefix6", listPrefix(prefix, family));
801
+ }
802
+ }
803
+ function familyOfRule(device, rule) {
804
+ const match = rule.match;
805
+ if (match?.family)
806
+ return match.family;
807
+ if (match?.prefix)
808
+ return familyOfPrefix(match.prefix);
809
+ const set = match?.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
810
+ if (set && "source" in set)
811
+ return set.source.family;
812
+ if (set?.prefixes[0])
813
+ return familyOfPrefix(set.prefixes[0]);
814
+ return;
815
+ }
816
+ function flatten(device, policy, seen = [policy]) {
817
+ const flat = [];
818
+ for (const rule of device.policies?.[policy] ?? []) {
819
+ if (rule.call && !seen.includes(rule.call))
820
+ flat.push(...flatten(device, rule.call, [...seen, rule.call]));
821
+ if (rule.match || rule.set || rule.action)
822
+ flat.push(rule);
823
+ }
824
+ return flat;
825
+ }
826
+ var slug = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
827
+ var patternOf = (pattern) => `^${pattern.replace(/\*/g, "[0-9]+")}$`;
828
+
829
+ class RouteMaps {
830
+ root;
831
+ device;
832
+ built = new Set;
833
+ constructor(root, device) {
834
+ this.root = root;
835
+ this.device = device;
836
+ }
837
+ of(policy, family) {
838
+ if (!policy) {
839
+ if (!this.built.has(REJECT_ALL)) {
840
+ this.built.add(REJECT_ALL);
841
+ this.root.block("router route-map").entry(REJECT_ALL).block("rule").entry(1).set("action", "deny");
842
+ }
843
+ return REJECT_ALL;
844
+ }
845
+ const name = family === "ipv4" ? policy : `${policy}-v6`;
846
+ if (this.built.has(name))
847
+ return name;
848
+ this.built.add(name);
849
+ const map = this.root.block("router route-map").entry(name);
850
+ let number = 0;
851
+ for (const rule of flatten(this.device, policy)) {
852
+ const only = familyOfRule(this.device, rule);
853
+ if (only && only !== family)
854
+ continue;
855
+ this.rule(map.block("rule").entry(++number), rule, family, `${name}-${number}`);
856
+ }
857
+ return name;
858
+ }
859
+ rule(entry, rule, family, label) {
860
+ const match = rule.match ?? {};
861
+ const set = rule.set ?? {};
862
+ const address = family === "ipv4" ? "match-ip-address" : "match-ip6-address";
863
+ if (match.prefix) {
864
+ this.list(label, family, [[match.prefix, {}]]);
865
+ entry.set(address, quote(label));
866
+ } else if (match.prefix_set && match.prefix_length) {
867
+ const name = `${match.prefix_set}-${match.prefix_length.min ?? 0}-${match.prefix_length.max ?? BITS[family]}`;
868
+ const prefixes = this.prefixesOf(match.prefix_set);
869
+ this.list(name, family, prefixes.map((prefix) => [prefix, bounds(prefix, family, match.prefix_length?.min, match.prefix_length?.max)]));
870
+ entry.set(address, quote(name));
871
+ } else if (match.prefix_set) {
872
+ entry.set(address, quote(match.prefix_set));
873
+ } else if (match.prefix_length) {
874
+ const { min = 0, max = BITS[family] } = match.prefix_length;
875
+ const name = `circuit-length-${min}-${max}`;
876
+ const everything = family === "ipv4" ? "0.0.0.0/0" : "::/0";
877
+ this.list(name, family, [[everything, bounds(everything, family, min, max)]]);
878
+ entry.set(address, quote(name));
879
+ }
880
+ if (match.community)
881
+ entry.set("match-community", quote(this.communities(`circuit-match-${slug(match.community)}`, [match.community], false)));
882
+ if (set.local_pref !== undefined)
883
+ entry.set("set-local-preference", set.local_pref);
884
+ if (set.med !== undefined)
885
+ entry.set("set-metric", set.med);
886
+ if (set.prepend !== undefined)
887
+ entry.set("set-aspath", quote(Array(set.prepend).fill(this.device.routing.bgp.asn).join(" ")));
888
+ if (set.communities) {
889
+ entry.set("set-community", quoteAll([...set.communities, ...set.add_communities ?? []]));
890
+ } else {
891
+ if (set.add_communities)
892
+ entry.set("set-community", quoteAll(set.add_communities)).set("set-community-additive", "enable");
893
+ if (set.remove_communities) {
894
+ const name = `circuit-remove-${slug(set.remove_communities.join(" "))}`;
895
+ entry.set("set-community-delete", quote(this.communities(name, set.remove_communities, true)));
896
+ }
897
+ }
898
+ if (set.next_hop && familyOf2(set.next_hop) === family)
899
+ entry.set(family === "ipv4" ? "set-ip-nexthop" : "set-ip6-nexthop", set.next_hop);
900
+ if (rule.action === "reject")
901
+ entry.set("action", "deny");
902
+ }
903
+ prefixesOf(set) {
904
+ const prefixes = this.device.prefix_sets?.[set];
905
+ return prefixes && "prefixes" in prefixes ? [...prefixes.prefixes] : [];
906
+ }
907
+ list(name, family, rules) {
908
+ const rulesBlock = this.root.block(PREFIX_LIST[family]).entry(name).block("rule");
909
+ let number = 0;
910
+ for (const [prefix, lengths] of rules)
911
+ if (lengths)
912
+ listRule(rulesBlock.entry(++number), family, prefix, lengths);
913
+ }
914
+ communities(name, values, patterns) {
915
+ const list = this.root.block("router community-list").entry(name);
916
+ if (patterns)
917
+ list.set("type", "expanded");
918
+ values.forEach((value, index) => {
919
+ const rule = list.block("rule").entry(index + 1).set("action", "permit");
920
+ if (patterns)
921
+ rule.set("regexp", quote(patternOf(value)));
922
+ else
923
+ rule.set("match", quote(value));
924
+ });
925
+ return name;
926
+ }
927
+ }
928
+ function routingGaps(device) {
929
+ const found = [];
930
+ const routing = device.routing;
931
+ if (routing?.rpki)
932
+ found.push("routing.rpki, the platform has no route origin validation");
933
+ if (routing?.router_id && !routing.ospf && !routing.bgp)
934
+ found.push("routing.router_id without OSPF or BGP, which each hold it");
935
+ const ospf = routing?.ospf;
936
+ if (ospf?.families?.includes("ipv6") && !routing?.router_id)
937
+ found.push("routing.ospf for IPv6 without routing.router_id");
938
+ for (const area of Object.values(ospf?.areas ?? {})) {
939
+ for (const name of Object.keys(area.interfaces)) {
940
+ if (settingsOf(device, name)?.vrf)
941
+ found.push(`routing.ospf on ${name}, which routes in a VRF: OSPF runs in the default table`);
942
+ const ipv4 = addressesOf2(settingsOf(device, name)).some((address) => familyOf2(host2(address)) === "ipv4");
943
+ if ((ospf?.families ?? ["ipv4"]).includes("ipv4") && !ipv4)
944
+ found.push(`routing.ospf on ${name}, which has no IPv4 address for OSPF to run on`);
945
+ }
946
+ }
947
+ for (const [name, neighbor] of Object.entries(routing?.bgp?.neighbors ?? {})) {
948
+ const where = `routing.bgp.neighbors.${name}`;
949
+ const settings = neighborSettings(device, neighbor);
950
+ if (settings.role)
951
+ found.push(`${where}.role, the platform has no BGP roles`);
952
+ if (settings.local_address && !holderOf(device, settings.local_address)) {
953
+ found.push(`${where}.local_address, no interface holds ${settings.local_address}`);
954
+ }
955
+ if (familyOf2(neighbor.address) === "ipv6" && familiesOfNeighbor(device, neighbor).includes("ipv4")) {
956
+ found.push(`${where}.families ipv4 over an IPv6 session`);
957
+ }
958
+ }
959
+ for (const [name, rules] of Object.entries(device.policies ?? {})) {
960
+ rules.forEach((rule, index) => found.push(...ruleGaps(device, `policies.${name}[${index}]`, rule)));
961
+ }
962
+ return found;
963
+ }
964
+ function ruleGaps(device, where, rule) {
965
+ const found = [];
966
+ const match = rule.match ?? {};
967
+ const set = rule.set ?? {};
968
+ if ((rule.match || rule.set) && !rule.action)
969
+ found.push(`${where}, a rule without action: the platform stops at the first rule that matches`);
970
+ for (const field of ["as_path_length", "rpki", "large_community"]) {
971
+ if (match[field] !== undefined)
972
+ found.push(`${where}.match.${field}`);
973
+ }
974
+ for (const field of [
975
+ "large_communities",
976
+ "add_large_communities",
977
+ "remove_large_communities",
978
+ "preferred_source",
979
+ "blackhole"
980
+ ]) {
981
+ if (set[field] !== undefined)
982
+ found.push(`${where}.set.${field}`);
983
+ }
984
+ if (match.prefix && match.prefix_length)
985
+ found.push(`${where}.match.prefix with prefix_length`);
986
+ if (match.prefix && match.prefix_set)
987
+ found.push(`${where}.match.prefix with prefix_set`);
988
+ const prefixSet = match.prefix_set ? device.prefix_sets?.[match.prefix_set] : undefined;
989
+ if (prefixSet && "source" in prefixSet && match.prefix_length) {
990
+ found.push(`${where}.match.prefix_length with a prefix set a registry fills`);
991
+ }
992
+ return found;
993
+ }
994
+
995
+ // src/adapters/devices/fortios/plan.ts
996
+ var FACTORY_ADDRESSES = [
997
+ "none",
998
+ "login.microsoftonline.com",
999
+ "login.microsoft.com",
1000
+ "login.windows.net",
1001
+ "gmail.com",
1002
+ "wildcard.google.com",
1003
+ "wildcard.dropbox.com",
1004
+ "all",
1005
+ "FIREWALL_AUTH_PORTAL_ADDRESS",
1006
+ "FABRIC_DEVICE",
1007
+ "SSLVPN_TUNNEL_ADDR1"
1008
+ ];
1009
+ var FACTORY_SERVICES = [
1010
+ "ALL",
1011
+ "ALL_TCP",
1012
+ "ALL_UDP",
1013
+ "ALL_ICMP",
1014
+ "ALL_ICMP6",
1015
+ "GRE",
1016
+ "AH",
1017
+ "ESP",
1018
+ "AOL",
1019
+ "BGP",
1020
+ "DHCP",
1021
+ "DNS",
1022
+ "FINGER",
1023
+ "FTP",
1024
+ "FTP_GET",
1025
+ "FTP_PUT",
1026
+ "GOPHER",
1027
+ "H323",
1028
+ "HTTP",
1029
+ "HTTPS",
1030
+ "IKE",
1031
+ "IMAP",
1032
+ "IMAPS",
1033
+ "Internet-Locator-Service",
1034
+ "IRC",
1035
+ "L2TP",
1036
+ "LDAP",
1037
+ "NetMeeting",
1038
+ "NFS",
1039
+ "NNTP",
1040
+ "NTP",
1041
+ "OSPF",
1042
+ "PC-Anywhere",
1043
+ "PING",
1044
+ "TIMESTAMP",
1045
+ "INFO_REQUEST",
1046
+ "INFO_ADDRESS",
1047
+ "ONC-RPC",
1048
+ "DCE-RPC",
1049
+ "POP3",
1050
+ "POP3S",
1051
+ "PPTP",
1052
+ "QUAKE",
1053
+ "RAUDIO",
1054
+ "REXEC",
1055
+ "RIP",
1056
+ "RLOGIN",
1057
+ "RSH",
1058
+ "SCCP",
1059
+ "SIP",
1060
+ "SIP-MSNmessenger",
1061
+ "SAMBA",
1062
+ "SMTP",
1063
+ "SMTPS",
1064
+ "SNMP",
1065
+ "SSH",
1066
+ "SYSLOG",
1067
+ "TALK",
1068
+ "TELNET",
1069
+ "TFTP",
1070
+ "MGCP",
1071
+ "UUCP",
1072
+ "VDOLIVE",
1073
+ "WAIS",
1074
+ "WINFRAME",
1075
+ "X-WINDOWS",
1076
+ "PING6",
1077
+ "MS-SQL",
1078
+ "MYSQL",
1079
+ "RDP",
1080
+ "VNC",
1081
+ "DHCP6",
1082
+ "SQUID",
1083
+ "SOCKS",
1084
+ "WINS",
1085
+ "RADIUS",
1086
+ "RADIUS-OLD",
1087
+ "CVSPSERVER",
1088
+ "AFS3",
1089
+ "TRACEROUTE",
1090
+ "RTSP",
1091
+ "MMS",
1092
+ "KERBEROS",
1093
+ "LDAP_UDP",
1094
+ "SMB",
1095
+ "NONE",
1096
+ "webproxy"
1097
+ ];
1098
+ var FACTORY_CERTIFICATES = [
1099
+ "Fortinet_CA_SSL",
1100
+ "Fortinet_CA_Untrusted",
1101
+ "Fortinet_SSL",
1102
+ "Fortinet_SSL_RSA1024",
1103
+ "Fortinet_SSL_RSA2048",
1104
+ "Fortinet_SSL_RSA4096",
1105
+ "Fortinet_SSL_DSA1024",
1106
+ "Fortinet_SSL_DSA2048",
1107
+ "Fortinet_SSL_ECDSA256",
1108
+ "Fortinet_SSL_ECDSA384",
1109
+ "Fortinet_SSL_ECDSA521",
1110
+ "Fortinet_SSL_ED25519",
1111
+ "Fortinet_SSL_ED448"
1112
+ ];
1113
+ var ORDERED = { ordered: true };
1114
+ var TABLES = {
1115
+ "system interface": {
1116
+ factory: ["modem", "ssl.root"],
1117
+ created: (entry) => {
1118
+ if (entry.settings.get("type") === "aggregate")
1119
+ return ["device-identification", "lldp-transmission"];
1120
+ if (entry.settings.has("interface") && !entry.settings.has("type"))
1121
+ return ["device-identification", "role"];
1122
+ return [];
1123
+ },
1124
+ fixed: ["type", "interface"]
1125
+ },
1126
+ "system accprofile": { factory: ["prof_admin"] },
1127
+ "vpn certificate local": { factory: FACTORY_CERTIFICATES },
1128
+ "system session-helper": { byContent: true },
1129
+ "firewall address": { factory: FACTORY_ADDRESSES },
1130
+ "firewall address6": { factory: ["SSLVPN_TUNNEL_IPv6_ADDR1", "all", "none"] },
1131
+ "firewall addrgrp": { factory: ["G Suite", "Microsoft Office 365"] },
1132
+ "firewall service custom": { factory: FACTORY_SERVICES },
1133
+ "firewall policy": ORDERED,
1134
+ "firewall policy6": ORDERED,
1135
+ "firewall local-in-policy": ORDERED,
1136
+ "firewall local-in-policy6": ORDERED,
1137
+ "firewall central-snat-map": ORDERED
1138
+ };
1139
+ var IGNORED = new Set(["uuid", "snmp-index"]);
1140
+ var IGNORED_IN = { "system global": ["cfg-save", "cfg-revert-timeout", "alias"] };
1141
+ var KEPT = { "vpn certificate local": ["certificate", "private-key"], "vpn certificate ca": ["ca"] };
1142
+ var DENY = { action: "deny" };
1143
+ var DEFAULTS = {
1144
+ "system global": {
1145
+ "admin-ssh-port": "22",
1146
+ "admin-sport": "443",
1147
+ "admin-port": "80",
1148
+ "admin-telnet-port": "23",
1149
+ "admin-ssh-grace-time": "120"
1150
+ },
1151
+ "system interface": { mtu: "1500" },
1152
+ "system snmp community": { "query-v2c-port": "161" },
1153
+ "system snmp user": { "query-port": "161" },
1154
+ "system dhcp server": { "dns-service": "specify" },
1155
+ "firewall policy": DENY,
1156
+ "firewall policy6": DENY,
1157
+ "firewall local-in-policy": DENY,
1158
+ "firewall local-in-policy6": DENY
1159
+ };
1160
+ var SECRET_KEYS = new Set(["private-key"]);
1161
+ var CERTIFICATES = new Set(["certificate", "ca"]);
1162
+ var FIRST = new Set(["ntpsync"]);
1163
+ var LISTS = new Set([
1164
+ "allowaccess",
1165
+ "ip6-allowaccess",
1166
+ "member",
1167
+ "srcintf",
1168
+ "dstintf",
1169
+ "srcaddr",
1170
+ "dstaddr",
1171
+ "service",
1172
+ "src-filter",
1173
+ "dhcp-relay-ip"
1174
+ ]);
1175
+ var isSecret2 = (value) => /<secret:|<redacted>/.test(value) || value.startsWith("ENC ");
1176
+ function canonical(key, value) {
1177
+ if (value === UNSET)
1178
+ return UNSET;
1179
+ if (isSecret2(value) || SECRET_KEYS.has(key))
1180
+ return "<secret>";
1181
+ if (CERTIFICATES.has(key))
1182
+ return bare(value).replace(/\s+/g, "");
1183
+ const words = wordsOf(value).map(bare);
1184
+ return (LISTS.has(key) ? words.sort() : words).join(" ");
1185
+ }
1186
+ function changesFixed(table, before, after) {
1187
+ const value = (block, key) => canonical(key, block.settings.get(key) ?? "");
1188
+ return (table?.fixed ?? []).some((key) => value(before, key) !== value(after, key));
1189
+ }
1190
+ function isFactory(table, entry) {
1191
+ return table?.factory?.includes(bare(entry.id)) ?? false;
1192
+ }
1193
+ function clean(text) {
1194
+ const root = parse(text);
1195
+ for (const [section, block] of root.blocks) {
1196
+ const table = TABLES[section];
1197
+ for (const [key, entry] of block.entries)
1198
+ if (isFactory(table, entry))
1199
+ block.entries.delete(key);
1200
+ for (const key of IGNORED_IN[section] ?? [])
1201
+ block.settings.delete(key);
1202
+ dropIgnored(block);
1203
+ const kept = KEPT[section];
1204
+ if (!kept)
1205
+ continue;
1206
+ for (const { block: entry } of block.entries.values()) {
1207
+ for (const key of entry.settings.keys())
1208
+ if (!kept.includes(key))
1209
+ entry.settings.delete(key);
1210
+ entry.blocks.clear();
1211
+ }
1212
+ }
1213
+ return print(root);
1214
+ }
1215
+ function dropIgnored(block) {
1216
+ for (const key of block.settings.keys())
1217
+ if (IGNORED.has(key))
1218
+ block.settings.delete(key);
1219
+ for (const child of block.blocks.values())
1220
+ dropIgnored(child);
1221
+ for (const { block: entry } of block.entries.values())
1222
+ dropIgnored(entry);
1223
+ }
1224
+ var labelOf = (path) => path.length ? `${path.join(" ")}: ` : "";
1225
+ function shown(sign, path, block) {
1226
+ const label = labelOf(path);
1227
+ const lines = linesOf(block).map((line) => `${sign} ${label}${line.trim()}`);
1228
+ return lines.length ? lines : [`${sign} ${path.join(" ")}`];
1229
+ }
1230
+ function creation(entry, table, id = entry.id) {
1231
+ const unset = (table?.created?.(entry.block) ?? []).filter((key) => !entry.block.settings.has(key)).map((key) => `unset ${key}`);
1232
+ return [`edit ${id}`, ...linesOf(entry.block).map((line) => line.trim()), ...unset, "next"];
1233
+ }
1234
+ function compare(path, want, have, table, top, context) {
1235
+ const send = [];
1236
+ const show = [];
1237
+ const removals = [];
1238
+ const removed = [];
1239
+ const label = labelOf(path);
1240
+ const nested = (header, wanted, held) => {
1241
+ const diff = compare([...path, header], wanted, held, undefined, false, context);
1242
+ if (!diff.send.length)
1243
+ return;
1244
+ send.push(`config ${header}`, ...diff.send, "end");
1245
+ show.push(...diff.show);
1246
+ };
1247
+ const unset = (key, old) => {
1248
+ send.push(`unset ${key}`);
1249
+ show.push(`- ${label}set ${key} ${old}`);
1250
+ };
1251
+ const gone = [...have.settings].filter(([key]) => !want.settings.has(key) && !IGNORED.has(key));
1252
+ for (const [key, old] of gone)
1253
+ if (FIRST.has(key))
1254
+ unset(key, old);
1255
+ for (const [header, child] of [...have.blocks].reverse())
1256
+ if (!want.blocks.has(header))
1257
+ nested(header, new Block, child);
1258
+ for (const [key, old] of gone.reverse())
1259
+ if (!FIRST.has(key))
1260
+ unset(key, old);
1261
+ for (const [key, value] of want.settings) {
1262
+ const old = have.settings.get(key);
1263
+ const current = old ?? DEFAULTS[context.section]?.[key];
1264
+ const same = current !== undefined && canonical(key, current) === canonical(key, value);
1265
+ if (same && !(context.secrets && value.includes("<secret:")))
1266
+ continue;
1267
+ send.push(value === UNSET ? `unset ${key}` : `set ${key} ${value}`);
1268
+ if (old !== undefined)
1269
+ show.push(`- ${label}set ${key} ${old}`);
1270
+ show.push(`+ ${label}set ${key} ${value}`);
1271
+ }
1272
+ for (const [header, child] of want.blocks)
1273
+ nested(header, child, have.blocks.get(header) ?? new Block);
1274
+ const entries = table?.byContent ? byContent(path, want, have) : byId(path, want, have, table, context);
1275
+ send.push(...entries.send);
1276
+ show.push(...entries.show);
1277
+ if (top) {
1278
+ removals.push(...entries.removals);
1279
+ removed.push(...entries.removed);
1280
+ } else {
1281
+ send.push(...entries.removals);
1282
+ show.push(...entries.removed);
1283
+ }
1284
+ return { send, show, removals, removed };
1285
+ }
1286
+ function byId(path, want, have, table, context) {
1287
+ const send = [];
1288
+ const show = [];
1289
+ const removals = [];
1290
+ const removed = [];
1291
+ for (const [key, entry] of want.entries) {
1292
+ const old = have.entries.get(key);
1293
+ const where = [...path, bare(entry.id)];
1294
+ const replaced = old && changesFixed(table, old.block, entry.block);
1295
+ if (!old || replaced) {
1296
+ if (old) {
1297
+ send.push(`delete ${old.id}`);
1298
+ show.push(...shown("-", where, old.block));
1299
+ }
1300
+ send.push(...creation(entry, table));
1301
+ show.push(...shown("+", where, entry.block));
1302
+ continue;
1303
+ }
1304
+ const diff = compare(where, entry.block, old.block, undefined, false, context);
1305
+ if (!diff.send.length)
1306
+ continue;
1307
+ send.push(`edit ${entry.id}`, ...diff.send, "next");
1308
+ show.push(...diff.show);
1309
+ }
1310
+ const gone = [...have.entries].filter(([key, entry]) => !want.entries.has(key) && !isFactory(table, entry));
1311
+ gone.sort(([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface")));
1312
+ for (const [, entry] of gone) {
1313
+ removals.push(`delete ${entry.id}`);
1314
+ removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
1315
+ }
1316
+ if (table?.ordered) {
1317
+ const moved = moves(want, have);
1318
+ send.push(...moved);
1319
+ show.push(...moved.map((line) => `+ ${labelOf(path)}${line}`));
1320
+ }
1321
+ return { send, show, removals, removed };
1322
+ }
1323
+ function moves(want, have) {
1324
+ const wanted = [...want.entries.keys()];
1325
+ const order = [...have.entries.keys()].filter((key) => want.entries.has(key));
1326
+ order.push(...wanted.filter((key) => !have.entries.has(key)));
1327
+ const send = [];
1328
+ const idOf = (key) => want.entries.get(key).id;
1329
+ for (let index = 0;index < wanted.length; index++) {
1330
+ if (order[index] === wanted[index])
1331
+ continue;
1332
+ const key = wanted[index];
1333
+ send.push(index === 0 ? `move ${idOf(key)} before ${idOf(order[0])}` : `move ${idOf(key)} after ${idOf(wanted[index - 1])}`);
1334
+ order.splice(order.indexOf(key), 1);
1335
+ order.splice(index, 0, key);
1336
+ }
1337
+ return send;
1338
+ }
1339
+ function signature(block) {
1340
+ return [...block.settings].map(([key, value]) => `${key} ${canonical(key, value)}`).sort().join(`
1341
+ `);
1342
+ }
1343
+ function byContent(path, want, have) {
1344
+ const send = [];
1345
+ const show = [];
1346
+ const removals = [];
1347
+ const removed = [];
1348
+ const unmatched = [...have.entries.values()];
1349
+ const added = [];
1350
+ for (const entry of want.entries.values()) {
1351
+ const index = unmatched.findIndex((old) => signature(old.block) === signature(entry.block));
1352
+ if (index >= 0)
1353
+ unmatched.splice(index, 1);
1354
+ else
1355
+ added.push(entry);
1356
+ }
1357
+ let next = Math.max(0, ...[...have.entries.keys()].map(Number).filter(Number.isInteger)) + 1;
1358
+ for (const entry of added) {
1359
+ const id = String(next++);
1360
+ send.push(...creation(entry, undefined, id));
1361
+ show.push(...shown("+", [...path, id], entry.block));
1362
+ }
1363
+ for (const entry of unmatched) {
1364
+ removals.push(`delete ${entry.id}`);
1365
+ removed.push(...shown("-", [...path, bare(entry.id)], entry.block));
1366
+ }
1367
+ return { send, show, removals, removed };
1368
+ }
1369
+ var NAMES_INTERFACE = new Set(["device", "interface", "srcintf", "dstintf", "intf", "extintf", "update-source", "passive-interface"]);
1370
+ function interfacesNamed(block) {
1371
+ return [...block.settings].filter(([key]) => NAMES_INTERFACE.has(key)).flatMap(([, value]) => wordsOf(value).map(bare));
1372
+ }
1373
+ var KEYED_BY_INTERFACE = new Set(["system dns-server"]);
1374
+ var INTERFACE_TABLES = new Set(["system interface", "system gre-tunnel", "system vxlan"]);
1375
+ function released(want, have) {
1376
+ const interfaces = have.blocks.get("system interface") ?? new Block;
1377
+ const wanted = want.blocks.get("system interface") ?? new Block;
1378
+ const members = (block) => wordsOf(block?.settings.get("member") ?? "").map(bare);
1379
+ const freed = new Set;
1380
+ const gone = new Set;
1381
+ const trims = [];
1382
+ const deferred = new Set;
1383
+ const changesSpeed = (port) => {
1384
+ const speed = (block) => canonical("speed", block?.settings.get("speed") ?? "");
1385
+ return speed(interfaces.entries.get(port)?.block) !== speed(wanted.entries.get(port)?.block);
1386
+ };
1387
+ for (const [key, entry] of wanted.entries) {
1388
+ const before = interfaces.entries.get(key)?.block;
1389
+ for (const member of members(entry.block))
1390
+ if (!members(before).includes(member))
1391
+ freed.add(member);
1392
+ if (before && changesFixed(TABLES["system interface"], before, entry.block))
1393
+ freed.add(key);
1394
+ }
1395
+ for (const [key, entry] of interfaces.entries) {
1396
+ if (entry.block.settings.get("type") !== "aggregate")
1397
+ continue;
1398
+ const kept = members(entry.block).filter((member) => members(wanted.entries.get(key)?.block).includes(member) && !changesSpeed(member));
1399
+ if (kept.length === members(entry.block).length)
1400
+ continue;
1401
+ if (kept.length && wanted.entries.has(key)) {
1402
+ trims.push({ id: entry.id, members: kept });
1403
+ continue;
1404
+ }
1405
+ if (!wanted.entries.has(key) && !members(entry.block).some((member) => freed.has(member))) {
1406
+ for (const member of members(entry.block))
1407
+ deferred.add(member);
1408
+ continue;
1409
+ }
1410
+ freed.add(key);
1411
+ gone.add(key);
1412
+ }
1413
+ let grew = true;
1414
+ while (grew) {
1415
+ grew = false;
1416
+ for (const [section, block] of have.blocks) {
1417
+ if (!INTERFACE_TABLES.has(section))
1418
+ continue;
1419
+ for (const [key, entry] of block.entries) {
1420
+ if (freed.has(key) || !interfacesNamed(entry.block).some((name) => freed.has(name)))
1421
+ continue;
1422
+ freed.add(key);
1423
+ grew = true;
1424
+ }
1425
+ }
1426
+ }
1427
+ return { freed, gone, trims, deferred };
1428
+ }
1429
+ var HELD_BY = { interface: "server-mode" };
1430
+ function release(block, path, freed, gone, isEntry) {
1431
+ const send = [];
1432
+ const show = [];
1433
+ const label = labelOf(path);
1434
+ if (!isEntry) {
1435
+ for (const [key, value] of [...block.settings]) {
1436
+ if (!NAMES_INTERFACE.has(key) || !wordsOf(value).some((word) => freed.has(bare(word))))
1437
+ continue;
1438
+ const holder = HELD_BY[key];
1439
+ if (holder && block.settings.has(holder)) {
1440
+ send.push(`unset ${holder}`);
1441
+ show.push(`- ${label}set ${holder} ${block.settings.get(holder)}`, `- ${label}set ${key} ${value}`);
1442
+ block.settings.delete(holder);
1443
+ block.settings.delete(key);
1444
+ continue;
1445
+ }
1446
+ send.push(`unset ${key}`);
1447
+ show.push(`- ${label}set ${key} ${value}`);
1448
+ block.settings.delete(key);
1449
+ }
1450
+ }
1451
+ for (const [header, child] of block.blocks) {
1452
+ const inner = release(child, [...path, header], freed, gone, false);
1453
+ if (inner.send.length)
1454
+ send.push(`config ${header}`, ...inner.send, "end");
1455
+ show.push(...inner.show);
1456
+ }
1457
+ const entries = [...block.entries].sort(([, a], [, b]) => Number(b.block.settings.has("interface")) - Number(a.block.settings.has("interface")));
1458
+ for (const [key, entry] of entries) {
1459
+ const where = [...path, bare(entry.id)];
1460
+ const named = interfacesNamed(entry.block).some((name) => freed.has(name));
1461
+ if (named || gone.has(key)) {
1462
+ send.push(`delete ${entry.id}`);
1463
+ show.push(...shown("-", where, entry.block));
1464
+ block.entries.delete(key);
1465
+ continue;
1466
+ }
1467
+ const inner = release(entry.block, where, freed, gone, true);
1468
+ if (inner.send.length)
1469
+ send.push(`edit ${entry.id}`, ...inner.send, "next");
1470
+ show.push(...inner.show);
1471
+ }
1472
+ return { send, show };
1473
+ }
1474
+ function movedAddresses(want, have) {
1475
+ const wanted = want.blocks.get("system interface");
1476
+ const held = have.blocks.get("system interface");
1477
+ if (!wanted || !held)
1478
+ return;
1479
+ const send = [];
1480
+ const show = [];
1481
+ const same = (key, a, b) => canonical(key, a ?? "") === canonical(key, b ?? "");
1482
+ for (const [key, entry] of held.entries) {
1483
+ const after = wanted.entries.get(key)?.block;
1484
+ const before = entry.block;
1485
+ const lines = [];
1486
+ const label = labelOf([bare(entry.id)]);
1487
+ const primaryMoves = before.settings.has("ip") && !same("ip", before.settings.get("ip"), after?.settings.get("ip"));
1488
+ const secondaries = before.blocks.get("secondaryip");
1489
+ const keptSecondaries = after?.blocks.get("secondaryip");
1490
+ const goneSecondaries = [...secondaries?.entries ?? []].filter(([id, secondary]) => primaryMoves || !same("ip", secondary.block.settings.get("ip"), keptSecondaries?.entries.get(id)?.block.settings.get("ip")));
1491
+ if (goneSecondaries.length) {
1492
+ lines.push("config secondaryip", ...goneSecondaries.map(([, secondary]) => `delete ${secondary.id}`), "end");
1493
+ for (const [id, secondary] of goneSecondaries) {
1494
+ show.push(`- ${label}secondaryip ${bare(secondary.id)}: set ip ${secondary.block.settings.get("ip")}`);
1495
+ secondaries.entries.delete(id);
1496
+ }
1497
+ }
1498
+ if (primaryMoves) {
1499
+ lines.push("unset ip");
1500
+ show.push(`- ${label}set ip ${before.settings.get("ip")}`);
1501
+ before.settings.delete("ip");
1502
+ }
1503
+ const ipv6 = before.blocks.get("ipv6");
1504
+ const keptIpv6 = after?.blocks.get("ipv6");
1505
+ const extra = ipv6?.blocks.get("ip6-extra-addr");
1506
+ const goneExtra = [...extra?.entries ?? []].filter(([id]) => !keptIpv6?.blocks.get("ip6-extra-addr")?.entries.has(id));
1507
+ const v6Lines = [];
1508
+ if (goneExtra.length) {
1509
+ v6Lines.push("config ip6-extra-addr", ...goneExtra.map(([, address]) => `delete ${address.id}`), "end");
1510
+ for (const [id, address] of goneExtra) {
1511
+ show.push(`- ${label}ipv6 ip6-extra-addr ${bare(address.id)}`);
1512
+ extra.entries.delete(id);
1513
+ }
1514
+ }
1515
+ if (ipv6?.settings.has("ip6-address") && !same("ip6-address", ipv6.settings.get("ip6-address"), keptIpv6?.settings.get("ip6-address"))) {
1516
+ v6Lines.push("unset ip6-address");
1517
+ show.push(`- ${label}ipv6: set ip6-address ${ipv6.settings.get("ip6-address")}`);
1518
+ ipv6.settings.delete("ip6-address");
1519
+ }
1520
+ if (v6Lines.length)
1521
+ lines.push("config ipv6", ...v6Lines, "end");
1522
+ if (lines.length)
1523
+ send.push(`edit ${entry.id}`, ...lines, "next");
1524
+ }
1525
+ return send.length ? { title: "system interface, addresses that move", show, send: ["config system interface", ...send, "end"] } : undefined;
1526
+ }
1527
+ function plan(desired, current, options) {
1528
+ const want = separateTunnels(parse(desired));
1529
+ const have = separateTunnels(parse(clean(current)));
1530
+ keepFilled(want, have);
1531
+ const first = [];
1532
+ const steps = [];
1533
+ const removals = [];
1534
+ const { freed, gone, trims, deferred } = released(want, have);
1535
+ const late = deferredPorts(want, have, deferred, options.secrets);
1536
+ if (freed.size) {
1537
+ for (const section of [...have.blocks.keys()].reverse()) {
1538
+ const block = have.blocks.get(section);
1539
+ if (section === TUNNELS) {
1540
+ for (const key of [...block.entries.keys()])
1541
+ if (freed.has(key))
1542
+ block.entries.delete(key);
1543
+ continue;
1544
+ }
1545
+ const { send, show } = release(block, [], freed, KEYED_BY_INTERFACE.has(section) ? freed : section === "system interface" ? gone : new Set, false);
1546
+ if (send.length)
1547
+ first.push({ title: `${section}, released`, show, send: [`config ${section}`, ...send, "end"] });
1548
+ }
1549
+ }
1550
+ if (trims.length) {
1551
+ const interfaces = have.blocks.get("system interface");
1552
+ const send = trims.flatMap(({ id, members }) => [`edit ${id}`, `set member ${quoteAll(members)}`, "next"]);
1553
+ const show = trims.map(({ id, members }) => `+ ${bare(id)}: set member ${quoteAll(members)}`);
1554
+ first.push({ title: "system interface, ports leaving a LAG", show, send: ["config system interface", ...send, "end"] });
1555
+ for (const { id, members } of trims)
1556
+ interfaces.entries.get(bare(id)).block.settings.set("member", quoteAll(members));
1557
+ }
1558
+ const moved = movedAddresses(want, have);
1559
+ if (moved)
1560
+ first.push(moved);
1561
+ for (const [section, block] of want.blocks) {
1562
+ const header = section === TUNNELS ? "system interface" : section;
1563
+ const context = { section: header, secrets: options.secrets };
1564
+ const table = section === TUNNELS ? undefined : TABLES[header];
1565
+ const diff = compare([], block, have.blocks.get(section) ?? new Block, table, true, context);
1566
+ if (diff.send.length)
1567
+ steps.push({ title: section, show: diff.show, send: [`config ${header}`, ...diff.send, "end"] });
1568
+ if (diff.removals.length && section !== TUNNELS) {
1569
+ removals.unshift({ title: `${section}, removed`, show: diff.removed, send: [`config ${header}`, ...diff.removals, "end"] });
1570
+ }
1571
+ }
1572
+ return { steps: [...first, ...steps, ...removals, ...late], problems: [] };
1573
+ }
1574
+ var TUNNELS = "system interface, tunnels";
1575
+ function separateTunnels(root) {
1576
+ const interfaces = root.blocks.get("system interface");
1577
+ if (!interfaces)
1578
+ return root;
1579
+ const tunnels = new Block;
1580
+ for (const [key, entry] of interfaces.entries) {
1581
+ const type = entry.block.settings.get("type");
1582
+ if (type !== "tunnel" && type !== "vxlan")
1583
+ continue;
1584
+ entry.block.settings.delete("interface");
1585
+ tunnels.entries.set(key, entry);
1586
+ interfaces.entries.delete(key);
1587
+ }
1588
+ const sections = [...root.blocks];
1589
+ root.blocks.clear();
1590
+ for (const [section, block] of sections) {
1591
+ root.blocks.set(section, block);
1592
+ if (section === "system vxlan")
1593
+ root.blocks.set(TUNNELS, tunnels);
1594
+ }
1595
+ if (!root.blocks.has(TUNNELS))
1596
+ root.blocks.set(TUNNELS, tunnels);
1597
+ return root;
1598
+ }
1599
+ function keepFilled(want, have) {
1600
+ for (const table of Object.values(PREFIX_LIST)) {
1601
+ for (const [key, entry] of want.blocks.get(table)?.entries ?? []) {
1602
+ if (!bare(entry.block.settings.get("comments") ?? "").startsWith(FILLED))
1603
+ continue;
1604
+ const held = have.blocks.get(table)?.entries.get(key)?.block.blocks.get("rule");
1605
+ if (held)
1606
+ entry.block.blocks.set("rule", held);
1607
+ }
1608
+ }
1609
+ }
1610
+ function deferredPorts(want, have, ports, secrets) {
1611
+ const wanted = want.blocks.get("system interface");
1612
+ const held = have.blocks.get("system interface");
1613
+ if (!ports.size || !wanted || !held)
1614
+ return [];
1615
+ const later = { want: new Block, have: new Block };
1616
+ for (const port of ports) {
1617
+ const after = wanted.entries.get(port);
1618
+ const before = held.entries.get(port);
1619
+ if (!after || !before)
1620
+ continue;
1621
+ later.want.entries.set(port, after);
1622
+ later.have.entries.set(port, before);
1623
+ wanted.entries.delete(port);
1624
+ held.entries.delete(port);
1625
+ }
1626
+ const diff = compare([], later.want, later.have, TABLES["system interface"], true, { section: "system interface", secrets });
1627
+ if (!diff.send.length)
1628
+ return [];
1629
+ return [
1630
+ { title: "system interface, ports of a LAG that went", show: diff.show, send: ["config system interface", ...diff.send, "end"] }
1631
+ ];
1632
+ }
1633
+
1634
+ // src/adapters/devices/fortios/render.ts
1635
+ var BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
1636
+ var SECTIONS = [
1637
+ "vpn certificate local",
1638
+ "vpn certificate ca",
1639
+ "system global",
1640
+ BANNER,
1641
+ "system settings",
1642
+ "system accprofile",
1643
+ "system admin",
1644
+ "system interface",
1645
+ "system gre-tunnel",
1646
+ "system vxlan",
1647
+ "system dns",
1648
+ "system dns-server",
1649
+ "system ntp",
1650
+ "system snmp sysinfo",
1651
+ "system snmp community",
1652
+ "system snmp user",
1653
+ "log memory setting",
1654
+ "log memory filter",
1655
+ "log disk setting",
1656
+ ...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
1657
+ "system sflow",
1658
+ "system netflow",
1659
+ "system dhcp server",
1660
+ "router static",
1661
+ "router static6",
1662
+ "router prefix-list",
1663
+ "router prefix-list6",
1664
+ "router community-list",
1665
+ "router route-map",
1666
+ "router ospf",
1667
+ "router ospf6",
1668
+ "router bgp",
1669
+ "system session-helper",
1670
+ "firewall address",
1671
+ "firewall address6",
1672
+ "firewall addrgrp",
1673
+ "firewall addrgrp6",
1674
+ "firewall service custom",
1675
+ "firewall ippool",
1676
+ "firewall vip",
1677
+ "firewall policy",
1678
+ "firewall policy6",
1679
+ "firewall local-in-policy",
1680
+ "firewall local-in-policy6",
1681
+ "firewall central-snat-map",
1682
+ "firewall acl",
1683
+ "firewall acl6"
1684
+ ];
1685
+ var FULL = ["vpn certificate local", "vpn certificate ca"];
1686
+ var ZONES = {
1687
+ "00": ["Pacific/Kwajalein"],
1688
+ "01": ["Pacific/Midway", "Pacific/Pago_Pago"],
1689
+ "02": ["Pacific/Honolulu"],
1690
+ "03": ["America/Anchorage"],
1691
+ "04": ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
1692
+ "05": ["America/Phoenix"],
1693
+ "06": ["America/Denver", "America/Edmonton"],
1694
+ "07": ["America/Guatemala", "America/Costa_Rica", "America/El_Salvador", "America/Tegucigalpa", "America/Managua"],
1695
+ "08": ["America/Chicago", "America/Winnipeg"],
1696
+ "09": ["America/Mexico_City"],
1697
+ "10": ["America/Regina"],
1698
+ "11": ["America/Bogota", "America/Lima", "America/Guayaquil"],
1699
+ "12": ["America/New_York", "America/Toronto"],
1700
+ "13": ["America/Indiana/Indianapolis"],
1701
+ "14": ["America/Halifax"],
1702
+ "15": ["America/La_Paz"],
1703
+ "16": ["America/Santiago"],
1704
+ "17": ["America/St_Johns"],
1705
+ "18": ["America/Sao_Paulo"],
1706
+ "19": ["America/Argentina/Buenos_Aires"],
1707
+ "20": ["America/Nuuk", "America/Godthab"],
1708
+ "21": ["Atlantic/South_Georgia"],
1709
+ "22": ["Atlantic/Azores"],
1710
+ "23": ["Atlantic/Cape_Verde"],
1711
+ "24": ["Africa/Monrovia"],
1712
+ "25": ["Europe/London", "Europe/Dublin", "Europe/Lisbon", "Atlantic/Canary"],
1713
+ "26": ["Europe/Amsterdam", "Europe/Berlin", "Europe/Zurich", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna"],
1714
+ "27": ["Europe/Belgrade", "Europe/Bratislava", "Europe/Budapest", "Europe/Ljubljana", "Europe/Prague"],
1715
+ "28": ["Europe/Brussels", "Europe/Copenhagen", "Europe/Madrid", "Europe/Paris"],
1716
+ "29": ["Europe/Sarajevo", "Europe/Skopje", "Europe/Warsaw", "Europe/Zagreb"],
1717
+ "30": ["Africa/Lagos"],
1718
+ "31": ["Europe/Athens", "Europe/Sofia", "Europe/Vilnius"],
1719
+ "32": ["Europe/Bucharest"],
1720
+ "33": ["Africa/Cairo"],
1721
+ "34": ["Africa/Harare", "Africa/Johannesburg"],
1722
+ "35": ["Europe/Helsinki", "Europe/Riga", "Europe/Tallinn"],
1723
+ "36": ["Asia/Jerusalem"],
1724
+ "37": ["Asia/Baghdad"],
1725
+ "38": ["Asia/Kuwait", "Asia/Riyadh"],
1726
+ "39": ["Europe/Volgograd"],
1727
+ "40": ["Africa/Nairobi"],
1728
+ "41": ["Asia/Tehran"],
1729
+ "42": ["Asia/Dubai", "Asia/Muscat"],
1730
+ "43": ["Asia/Baku"],
1731
+ "44": ["Asia/Kabul"],
1732
+ "45": ["Asia/Yekaterinburg"],
1733
+ "46": ["Asia/Karachi", "Asia/Tashkent"],
1734
+ "47": ["Asia/Kolkata", "Asia/Calcutta"],
1735
+ "48": ["Asia/Kathmandu"],
1736
+ "49": ["Asia/Almaty", "Asia/Novosibirsk"],
1737
+ "50": ["Asia/Dhaka"],
1738
+ "51": ["Asia/Colombo"],
1739
+ "52": ["Asia/Yangon", "Asia/Rangoon"],
1740
+ "53": ["Asia/Jakarta", "Asia/Bangkok", "Asia/Ho_Chi_Minh"],
1741
+ "54": ["Asia/Krasnoyarsk"],
1742
+ "55": ["Asia/Shanghai", "Asia/Chongqing", "Asia/Hong_Kong", "Asia/Urumqi", "Asia/Irkutsk"],
1743
+ "56": ["Asia/Ulaanbaatar"],
1744
+ "57": ["Asia/Kuala_Lumpur", "Asia/Singapore"],
1745
+ "58": ["Australia/Perth"],
1746
+ "59": ["Asia/Taipei"],
1747
+ "60": ["Asia/Tokyo", "Asia/Seoul"],
1748
+ "61": ["Asia/Yakutsk"],
1749
+ "62": ["Australia/Adelaide"],
1750
+ "63": ["Australia/Darwin"],
1751
+ "64": ["Australia/Brisbane"],
1752
+ "65": ["Australia/Sydney", "Australia/Melbourne", "Australia/Canberra"],
1753
+ "66": ["Pacific/Guam", "Pacific/Port_Moresby"],
1754
+ "67": ["Australia/Hobart"],
1755
+ "68": ["Asia/Vladivostok"],
1756
+ "69": ["Asia/Magadan"],
1757
+ "70": ["Pacific/Guadalcanal", "Pacific/Noumea"],
1758
+ "71": ["Pacific/Auckland"],
1759
+ "72": ["Pacific/Fiji", "Asia/Kamchatka", "Pacific/Majuro"],
1760
+ "73": ["Pacific/Tongatapu"],
1761
+ "74": ["America/Caracas"],
1762
+ "75": ["America/Montevideo"],
1763
+ "76": ["Pacific/Kiritimati"],
1764
+ "77": ["America/Guyana"],
1765
+ "78": ["Africa/Windhoek"],
1766
+ "79": ["Africa/Casablanca"],
1767
+ "80": ["UTC", "Etc/UTC", "Etc/GMT", "GMT"],
1768
+ "81": ["America/Mazatlan", "America/Chihuahua"],
1769
+ "82": ["Pacific/Chatham"],
1770
+ "83": ["Europe/Moscow"],
1771
+ "84": ["Europe/Minsk"],
1772
+ "85": ["Europe/Istanbul"],
1773
+ "86": ["Pacific/Apia"],
1774
+ "87": ["America/Asuncion"]
1775
+ };
1776
+ function zoneOf(timezone = "UTC") {
1777
+ return Object.entries(ZONES).find(([, places]) => places.includes(timezone))?.[0];
1778
+ }
1779
+ var PROFILE = { admin: "super_admin", operator: "circuit-operator", "read-only": "circuit-read-only" };
1780
+ var PERMISSION_GROUPS = [
1781
+ "secfabgrp",
1782
+ "ftviewgrp",
1783
+ "authgrp",
1784
+ "sysgrp",
1785
+ "netgrp",
1786
+ "loggrp",
1787
+ "fwgrp",
1788
+ "vpngrp",
1789
+ "utmgrp",
1790
+ "wanoptgrp",
1791
+ "wifi"
1792
+ ];
1793
+ var SSH_KEYS = 3;
1794
+ var RESOLVERS = { device: 2, dhcp: 4 };
1795
+ var COLLECTORS = 4;
1796
+ var VRFS = 31;
1797
+ var LEVEL = {
1798
+ emergency: "emergency",
1799
+ alert: "alert",
1800
+ critical: "critical",
1801
+ error: "error",
1802
+ warning: "warning",
1803
+ notice: "notification",
1804
+ info: "information",
1805
+ debug: "debug"
1806
+ };
1807
+ var HELPERS = {
1808
+ ftp: [["ftp", 6, 21]],
1809
+ tftp: [["tftp", 17, 69]],
1810
+ sip: [["sip", 17, 5060]],
1811
+ h323: [
1812
+ ["h323", 6, 1720],
1813
+ ["ras", 17, 1719]
1814
+ ],
1815
+ pptp: [["pptp", 6, 1723]],
1816
+ rtsp: [
1817
+ ["rtsp", 6, 554],
1818
+ ["rtsp", 6, 7070],
1819
+ ["rtsp", 6, 8554]
1820
+ ]
1821
+ };
1822
+ var SPEEDS = {
1823
+ "40g": { "40g": "40000full" },
1824
+ "10g": { "10g": "10000full", "1g": "1000full" },
1825
+ "1g": { "1g": "1000full", "100m": "100full" }
1826
+ };
1827
+ function allowAccess(device) {
1828
+ const management = device.management ?? {};
1829
+ const services = ["ping"];
1830
+ if (management.https)
1831
+ services.push("https");
1832
+ if (management.ssh)
1833
+ services.push("ssh");
1834
+ if (management.snmp)
1835
+ services.push("snmp");
1836
+ if (management.http)
1837
+ services.push("http");
1838
+ if (management.telnet)
1839
+ services.push("telnet");
1840
+ if (management.native)
1841
+ services.push("fgfm");
1842
+ return services.join(" ");
1843
+ }
1844
+ function render(device) {
1845
+ const root = new Block;
1846
+ for (const section of SECTIONS)
1847
+ root.block(section);
1848
+ const section = (name) => root.block(name);
1849
+ certificates(section("vpn certificate local"), section("vpn certificate ca"), device);
1850
+ global(section("system global"), device);
1851
+ if (device.system?.banner)
1852
+ section(BANNER).set("buffer", quote(device.system.banner));
1853
+ if (natRules(device).length)
1854
+ section("system settings").set("central-nat", "enable");
1855
+ section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
1856
+ if (usesBfd(device))
1857
+ section("system settings").set("bfd", "enable");
1858
+ users(section("system accprofile"), section("system admin"), device);
1859
+ interfaces(root, device);
1860
+ dns(section("system dns"), section("system dns-server"), device);
1861
+ ntp(section("system ntp"), device);
1862
+ snmp(root, device);
1863
+ logging(root, device);
1864
+ flowExport(root, device);
1865
+ dhcpServers(section("system dhcp server"), device);
1866
+ staticRoutes(section("router static"), section("router static6"), device);
1867
+ routing(root, device);
1868
+ helpers(section("system session-helper"), device);
1869
+ firewall(root, device);
1870
+ return print(root);
1871
+ }
1872
+ function certificates(local, trusted, device) {
1873
+ for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
1874
+ const text = quote(certificate.certificate.trim());
1875
+ if (certificate.private_key)
1876
+ local.entry(name).set("private-key", quote(marker(certificate.private_key))).set("certificate", text);
1877
+ else
1878
+ trusted.entry(name).set("ca", text);
1879
+ }
1880
+ }
1881
+ function global(block, device) {
1882
+ const management = device.management ?? {};
1883
+ block.set("hostname", quote(device.name));
1884
+ const zone = zoneOf(device.system?.timezone);
1885
+ if (zone && zone !== "00")
1886
+ block.set("timezone", zone);
1887
+ if (management.ssh?.port)
1888
+ block.set("admin-ssh-port", management.ssh.port);
1889
+ if (management.ssh?.auth_timeout)
1890
+ block.set("admin-ssh-grace-time", management.ssh.auth_timeout);
1891
+ if (management.ssh?.weak_crypto)
1892
+ block.set("strong-crypto", "disable");
1893
+ if (management.https?.port)
1894
+ block.set("admin-sport", management.https.port);
1895
+ if (management.https?.certificate)
1896
+ block.set("admin-server-cert", quote(management.https.certificate));
1897
+ if (management.http?.port)
1898
+ block.set("admin-port", management.http.port);
1899
+ if (management.telnet?.port)
1900
+ block.set("admin-telnet-port", management.telnet.port);
1901
+ if (management.http)
1902
+ block.set("admin-https-redirect", "disable");
1903
+ if (device.system?.banner)
1904
+ block.set("pre-login-banner", "enable");
1905
+ if (device.lldp === true)
1906
+ block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
1907
+ }
1908
+ function users(profiles, admins, device) {
1909
+ const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
1910
+ for (const role of ["operator", "read-only"]) {
1911
+ if (!roles.has(role))
1912
+ continue;
1913
+ const profile = profiles.entry(PROFILE[role]);
1914
+ for (const group of PERMISSION_GROUPS)
1915
+ profile.set(group, role === "operator" && group !== "sysgrp" ? "read-write" : "read");
1916
+ }
1917
+ for (const [name, user] of Object.entries(device.users ?? {})) {
1918
+ const admin = admins.entry(name);
1919
+ admin.set("accprofile", quote(PROFILE[user.role]));
1920
+ if (user.description)
1921
+ admin.set("comments", quote(user.description));
1922
+ admin.set("vdom", quote("root"));
1923
+ (user.ssh_keys ?? []).forEach((key, index) => admin.set(`ssh-public-key${index + 1}`, quote(key)));
1924
+ admin.set("password", quote(marker(user.password)));
1925
+ }
1926
+ }
1927
+ function interfaces(root, device) {
1928
+ const block = root.block("system interface");
1929
+ const access = allowAccess(device);
1930
+ for (const port of portsOf(device.platform, device.model)) {
1931
+ const settings = device.ports?.[port];
1932
+ const entry = block.entry(spell(device.platform, device.model, port));
1933
+ entry.set("vdom", quote("root"));
1934
+ entry.set("type", "physical");
1935
+ if (!settings) {
1936
+ redirects(entry, device);
1937
+ entry.set("status", "down");
1938
+ continue;
1939
+ }
1940
+ if (settings.description)
1941
+ entry.set("description", quote(settings.description));
1942
+ const speed = settings.speed && SPEEDS[port.split("-")[0]]?.[settings.speed];
1943
+ if (speed)
1944
+ entry.set("speed", speed);
1945
+ if (settings.lag)
1946
+ continue;
1947
+ redirects(entry, device);
1948
+ if (settings.mtu)
1949
+ entry.set("mtu-override", "enable").set("mtu", settings.mtu);
1950
+ routed(entry, device, port, settings, access);
1951
+ }
1952
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
1953
+ if (iface.type !== "lag")
1954
+ continue;
1955
+ const members = Object.entries(device.ports ?? {}).filter(([, port]) => port?.lag === name).map(([port]) => spell(device.platform, device.model, port));
1956
+ const entry = block.entry(name);
1957
+ entry.set("vdom", quote("root"));
1958
+ entry.set("type", "aggregate");
1959
+ if (members.length)
1960
+ entry.set("member", quoteAll(members));
1961
+ if (iface.mode === "static")
1962
+ entry.set("lacp-mode", "static");
1963
+ if (iface.description)
1964
+ entry.set("description", quote(iface.description));
1965
+ if (iface.mtu)
1966
+ entry.set("mtu-override", "enable").set("mtu", iface.mtu);
1967
+ redirects(entry, device);
1968
+ routed(entry, device, name, iface, access);
1969
+ }
1970
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
1971
+ if (iface.type !== "vlan")
1972
+ continue;
1973
+ const parent = vlanParent(device, iface.vlan);
1974
+ const entry = block.entry(name);
1975
+ entry.set("vdom", quote("root"));
1976
+ if (parent)
1977
+ entry.set("interface", quote(nameOf(device, parent)));
1978
+ entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
1979
+ if (iface.description)
1980
+ entry.set("description", quote(iface.description));
1981
+ if (iface.mtu)
1982
+ entry.set("mtu-override", "enable").set("mtu", iface.mtu);
1983
+ redirects(entry, device);
1984
+ routed(entry, device, name, iface, access);
1985
+ }
1986
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
1987
+ if (iface.type !== "loopback")
1988
+ continue;
1989
+ const entry = block.entry(name);
1990
+ entry.set("vdom", quote("root"));
1991
+ entry.set("type", "loopback");
1992
+ if (iface.description)
1993
+ entry.set("description", quote(iface.description));
1994
+ redirects(entry, device);
1995
+ routed(entry, device, name, iface, access);
1996
+ }
1997
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
1998
+ if (iface.type !== "gre" && iface.type !== "vxlan")
1999
+ continue;
2000
+ const outgoing = holderOf(device, iface.local);
2001
+ const via = outgoing ? quote(nameOf(device, outgoing)) : '""';
2002
+ const ipv6 = familyOf2(iface.remote) === "ipv6";
2003
+ if (iface.type === "gre") {
2004
+ const tunnel = root.block("system gre-tunnel").entry(name).set("interface", via);
2005
+ if (ipv6)
2006
+ tunnel.set("ip-version", 6).set("remote-gw6", iface.remote).set("local-gw6", iface.local);
2007
+ else
2008
+ tunnel.set("remote-gw", iface.remote).set("local-gw", iface.local);
2009
+ } else {
2010
+ const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", iface.vni);
2011
+ if (ipv6)
2012
+ tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", quote(iface.remote));
2013
+ else
2014
+ tunnel.set("remote-ip", quote(iface.remote));
2015
+ if (iface.port !== undefined && iface.port !== 4789)
2016
+ tunnel.set("dstport", iface.port);
2017
+ }
2018
+ const entry = block.entry(name);
2019
+ entry.set("vdom", quote("root"));
2020
+ entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
2021
+ entry.set("interface", via);
2022
+ if (iface.type === "vxlan" && iface.mac)
2023
+ entry.set("macaddr", iface.mac.toLowerCase());
2024
+ if (iface.description)
2025
+ entry.set("description", quote(iface.description));
2026
+ if (iface.mtu)
2027
+ entry.set("mtu-override", "enable").set("mtu", iface.mtu);
2028
+ redirects(entry, device);
2029
+ routed(entry, device, name, iface, access);
2030
+ }
2031
+ }
2032
+ function redirects(entry, device) {
2033
+ if (!device.system?.ip?.icmp_redirects)
2034
+ entry.set("icmp-send-redirect", "disable");
2035
+ }
2036
+ function vlanParent(device, vlan) {
2037
+ const carriers = [
2038
+ ...Object.entries(device.ports ?? {}).filter(([, port]) => !port?.lag && port?.trunk_vlans?.includes(vlan)),
2039
+ ...Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "lag" && iface.trunk_vlans?.includes(vlan))
2040
+ ];
2041
+ return carriers.length === 1 ? carriers[0][0] : undefined;
2042
+ }
2043
+ function routed(entry, device, name, settings, access) {
2044
+ const addresses = addressesOf2(settings);
2045
+ const ipv4 = addresses.filter((address) => familyOf2(host2(address)) === "ipv4");
2046
+ const ipv6 = addresses.filter((address) => familyOf2(host2(address)) === "ipv6");
2047
+ const vrf = vrfNumber(device, settings.vrf);
2048
+ if (vrf)
2049
+ entry.set("vrf", vrf);
2050
+ const peer = settings.addresses?.find((item) => typeof item !== "string");
2051
+ if (peer) {
2052
+ entry.set("ip", `${host2(peer.address)} 255.255.255.255`);
2053
+ entry.set("remote-ip", `${peer.peer} ${netmask(split(peer.address).len)}`);
2054
+ entry.set("allowaccess", access);
2055
+ } else if (ipv4.length) {
2056
+ entry.set("ip", subnet(ipv4[0]));
2057
+ entry.set("allowaccess", access);
2058
+ }
2059
+ if (!peer && ipv4.length > 1) {
2060
+ entry.set("secondary-IP", "enable");
2061
+ const secondaries = entry.block("secondaryip");
2062
+ ipv4.slice(1).forEach((address, index) => secondaries.entry(index + 1).set("ip", subnet(address)).set("allowaccess", access));
2063
+ }
2064
+ const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
2065
+ const relayed = relays.flatMap((relay) => relay.servers);
2066
+ const relayed4 = relayed.filter((server) => familyOf2(server) === "ipv4");
2067
+ const relayed6 = relayed.filter((server) => familyOf2(server) === "ipv6");
2068
+ if (ipv6.length || relayed6.length) {
2069
+ const block = entry.block("ipv6");
2070
+ if (ipv6.length) {
2071
+ block.set("ip6-address", ipv6[0]);
2072
+ block.set("ip6-allowaccess", access);
2073
+ }
2074
+ if (settings.ipv6_ra) {
2075
+ block.set("ip6-send-adv", "enable");
2076
+ const prefixes = block.block("ip6-prefix-list");
2077
+ for (const address of ipv6)
2078
+ prefixes.entry(network2(address), true);
2079
+ }
2080
+ if (relayed6.length)
2081
+ block.set("dhcp6-relay-service", "enable").set("dhcp6-relay-ip", relayed6.join(" "));
2082
+ if (ipv6.length > 1) {
2083
+ const extra = block.block("ip6-extra-addr");
2084
+ for (const address of ipv6.slice(1))
2085
+ extra.entry(address, true);
2086
+ }
2087
+ }
2088
+ if (settings.vrrp?.length) {
2089
+ const groups = entry.block("vrrp");
2090
+ for (const group of settings.vrrp) {
2091
+ const vrrp = groups.entry(group.id).set("vrip", group.address);
2092
+ if (group.priority !== undefined && group.priority !== 100)
2093
+ vrrp.set("priority", group.priority);
2094
+ if (group.interval !== undefined && group.interval !== 1)
2095
+ vrrp.set("adv-interval", group.interval);
2096
+ if (!group.preempt)
2097
+ vrrp.set("preempt", "disable");
2098
+ }
2099
+ }
2100
+ if (relayed4.length) {
2101
+ entry.set("dhcp-relay-service", "enable");
2102
+ entry.set("dhcp-relay-ip", quoteAll(relayed4));
2103
+ }
2104
+ const listed = typeof device.lldp === "object" && device.lldp.interfaces.includes(name);
2105
+ if (settings.lldp === false)
2106
+ entry.set("lldp-transmission", "disable").set("lldp-reception", "disable");
2107
+ else if (listed || settings.lldp === true && device.lldp !== true)
2108
+ entry.set("lldp-transmission", "enable").set("lldp-reception", "enable");
2109
+ const flow = device.flow_export;
2110
+ const tunnel = ["gre", "vxlan"].includes(device.interfaces?.[name]?.type ?? "");
2111
+ const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name));
2112
+ if (sampled && flow.protocol === "sflow") {
2113
+ entry.set("sflow-sampler", "enable");
2114
+ if (flow.sampling !== undefined && flow.sampling !== 2000)
2115
+ entry.set("sample-rate", flow.sampling);
2116
+ }
2117
+ if (sampled && flow.protocol === "netflow-v9")
2118
+ entry.set("netflow-sampler", "both");
2119
+ }
2120
+ function dns(block, servers, device) {
2121
+ const resolvers = device.system?.dns?.servers ?? [];
2122
+ const ipv4 = resolvers.filter((server) => familyOf2(server) === "ipv4");
2123
+ const ipv6 = resolvers.filter((server) => familyOf2(server) === "ipv6");
2124
+ if (ipv4[0])
2125
+ block.set("primary", ipv4[0]);
2126
+ if (ipv4[1])
2127
+ block.set("secondary", ipv4[1]);
2128
+ if (ipv6[0])
2129
+ block.set("ip6-primary", ipv6[0]);
2130
+ if (ipv6[1])
2131
+ block.set("ip6-secondary", ipv6[1]);
2132
+ if (device.system?.dns?.serve)
2133
+ for (const name of addressed(device))
2134
+ servers.entry(nameOf(device, name));
2135
+ }
2136
+ function ntp(block, device) {
2137
+ const ntp = device.system?.ntp;
2138
+ const servers = ntp?.servers ?? [];
2139
+ if (servers.length) {
2140
+ block.set("ntpsync", "enable");
2141
+ block.set("type", "custom");
2142
+ const list = block.block("ntpserver");
2143
+ servers.forEach((server, index) => list.entry(index + 1).set("server", quote(server)));
2144
+ }
2145
+ if (ntp?.serve) {
2146
+ block.set("server-mode", "enable");
2147
+ block.set("interface", quoteAll(addressed(device).map((name) => nameOf(device, name))));
2148
+ }
2149
+ }
2150
+ function snmp(root, device) {
2151
+ const snmp = device.management?.snmp;
2152
+ if (!snmp)
2153
+ return;
2154
+ const sysinfo = root.block("system snmp sysinfo");
2155
+ sysinfo.set("status", "enable");
2156
+ if (snmp.contact)
2157
+ sysinfo.set("contact-info", quote(snmp.contact));
2158
+ if (snmp.location)
2159
+ sysinfo.set("location", quote(snmp.location));
2160
+ if (snmp.community) {
2161
+ const community = root.block("system snmp community").entry(1);
2162
+ community.set("name", quote(marker(snmp.community)));
2163
+ const allow = snmp.allow ?? device.management?.allow ?? [];
2164
+ const ipv4 = allow.filter((prefix) => familyOf2(host2(prefix)) === "ipv4");
2165
+ const ipv6 = allow.filter((prefix) => familyOf2(host2(prefix)) === "ipv6");
2166
+ ipv4.forEach((prefix, index) => community.block("hosts").entry(index + 1).set("ip", subnet(network2(prefix))));
2167
+ ipv6.forEach((prefix, index) => community.block("hosts6").entry(index + 1).set("ipv6", network2(prefix)));
2168
+ community.set("query-v1-status", "disable");
2169
+ if (snmp.port)
2170
+ community.set("query-v2c-port", snmp.port);
2171
+ community.set("trap-v1-status", "disable");
2172
+ community.set("trap-v2c-status", "disable");
2173
+ }
2174
+ for (const [name, user] of Object.entries(snmp.users ?? {})) {
2175
+ const entry = root.block("system snmp user").entry(name);
2176
+ entry.set("security-level", "auth-priv");
2177
+ if (snmp.port)
2178
+ entry.set("query-port", snmp.port);
2179
+ entry.set("auth-pwd", quote(marker(user.auth_password)));
2180
+ if (user.privacy === "des")
2181
+ entry.set("priv-proto", "des");
2182
+ entry.set("priv-pwd", quote(marker(user.privacy_password)));
2183
+ }
2184
+ }
2185
+ function logging(root, device) {
2186
+ const logging = device.system?.logging;
2187
+ root.block("log memory setting").set("status", logging?.local ? "enable" : "disable");
2188
+ if (logging?.local && logging.local.level !== "info")
2189
+ root.block("log memory filter").set("severity", LEVEL[logging.local.level]);
2190
+ root.block("log disk setting").set("status", "disable");
2191
+ (logging?.remote ?? []).slice(0, COLLECTORS).forEach((collector, index) => {
2192
+ const number = index === 0 ? "" : String(index + 1);
2193
+ const setting = root.block(`log syslogd${number} setting`);
2194
+ setting.set("status", "enable");
2195
+ setting.set("server", quote(collector.address));
2196
+ if (collector.port && collector.port !== 514)
2197
+ setting.set("port", collector.port);
2198
+ if (collector.level !== "info")
2199
+ root.block(`log syslogd${number} filter`).set("severity", LEVEL[collector.level]);
2200
+ });
2201
+ }
2202
+ function flowExport(root, device) {
2203
+ const flow = device.flow_export;
2204
+ const collector = flow?.collectors[0];
2205
+ if (!flow || !collector)
2206
+ return;
2207
+ if (flow.protocol === "sflow") {
2208
+ const block = root.block("system sflow").set("collector-ip", collector.address);
2209
+ if (collector.port && collector.port !== 6343)
2210
+ block.set("collector-port", collector.port);
2211
+ }
2212
+ if (flow.protocol === "netflow-v9") {
2213
+ const block = root.block("system netflow").set("collector-ip", collector.address);
2214
+ if (collector.port && collector.port !== 2055)
2215
+ block.set("collector-port", collector.port);
2216
+ }
2217
+ }
2218
+ function dhcpServers(block, device) {
2219
+ Object.values(device.dhcp ?? {}).forEach((server, index) => {
2220
+ const entry = block.entry(index + 1);
2221
+ if (server.lease_time !== undefined && server.lease_time !== 604800)
2222
+ entry.set("lease-time", server.lease_time);
2223
+ entry.set("default-gateway", server.gateway);
2224
+ entry.set("netmask", netmask(split(server.network).len));
2225
+ entry.set("interface", quote(nameOf(device, server.interface)));
2226
+ entry.set("dns-service", "specify");
2227
+ entry.block("ip-range").entry(1).set("start-ip", server.pool[0]).set("end-ip", server.pool[1]);
2228
+ (server.reservations ?? []).forEach((reservation, number) => {
2229
+ const reserved = entry.block("reserved-address").entry(number + 1);
2230
+ reserved.set("ip", reservation.address).set("mac", reservation.mac.toLowerCase());
2231
+ if (reservation.description)
2232
+ reserved.set("description", quote(reservation.description));
2233
+ });
2234
+ (server.dns ?? []).forEach((resolver, number) => entry.set(`dns-server${number + 1}`, resolver));
2235
+ });
2236
+ }
2237
+ function staticRoutes(ipv4, ipv6, device) {
2238
+ const counters = { ipv4: 0, ipv6: 0 };
2239
+ for (const route of device.routing?.static ?? []) {
2240
+ const family = familyOf2(split(route.prefix).addr);
2241
+ const entry = (family === "ipv4" ? ipv4 : ipv6).entry(++counters[family]);
2242
+ const prefix = network2(route.prefix);
2243
+ if (family === "ipv6" && prefix !== "::/0")
2244
+ entry.set("dst", prefix);
2245
+ if (family === "ipv4" && prefix !== "0.0.0.0/0")
2246
+ entry.set("dst", subnet(prefix));
2247
+ if (route.via)
2248
+ entry.set("gateway", route.via);
2249
+ const outgoing = route.interface ?? (route.via ? interfaceFor(device, route.via, route.vrf) : undefined);
2250
+ if (outgoing && !route.blackhole)
2251
+ entry.set("device", quote(nameOf(device, outgoing)));
2252
+ if (route.distance !== undefined && route.distance !== 10)
2253
+ entry.set("distance", route.distance);
2254
+ if (route.description)
2255
+ entry.set("comment", quote(route.description));
2256
+ if (route.blackhole)
2257
+ entry.set("blackhole", "enable");
2258
+ }
2259
+ }
2260
+ function helpers(block, device) {
2261
+ let number = 0;
2262
+ for (const helper of device.firewall?.helpers ?? []) {
2263
+ for (const [name, protocol, port] of HELPERS[helper] ?? [])
2264
+ block.entry(++number).set("name", name).set("protocol", protocol).set("port", port);
2265
+ }
2266
+ }
2267
+
2268
+ // src/adapters/devices/fortios/index.ts
2269
+ var SUPPORTED = "6.2";
2270
+ var NAME_LENGTH = { interface: 15, object: 79 };
2271
+ var RESERVED_INTERFACES = new Set(["any", "modem", "ssl.root"]);
2272
+ var PROMPT = /(?:^|\n)[^\n]*[#$] $/;
2273
+ var QUESTION = /\(y\/n\)\s*$/;
2274
+ var BANNER2 = /\(Press 'a' to accept\):\s*$/;
2275
+ var LOGGED_OUT = /(?:^|\n)[^\n]*login: $/;
2276
+ var PAGER = /--More-- \r? *\r/g;
2277
+ var REFUSED = /Command fail\. Return code|Unknown action|parse error|discard the setting|MUST be set|Can not config|isn't supported|(?:^|\n)Invalid /;
2278
+
2279
+ class Cli {
2280
+ channel;
2281
+ newline;
2282
+ prompt = "";
2283
+ constructor(channel, newline = `
2284
+ `) {
2285
+ this.channel = channel;
2286
+ this.newline = newline;
2287
+ }
2288
+ async ready(timeout = 30000) {
2289
+ await this.collect(timeout);
2290
+ }
2291
+ async run(line, timeout = 60000) {
2292
+ this.channel.write(line + this.newline);
2293
+ const output = await this.collect(timeout);
2294
+ const lines = output.split(`
2295
+ `);
2296
+ return lines.slice(1, -1).join(`
2297
+ `);
2298
+ }
2299
+ answer(reply, timeout = 60000) {
2300
+ this.channel.write(reply);
2301
+ return this.collect(timeout);
2302
+ }
2303
+ collect(timeout) {
2304
+ return new Promise((done, fail) => {
2305
+ let raw = "";
2306
+ let settle;
2307
+ const text = () => raw.replace(PAGER, "").replace(/\r/g, "");
2308
+ const stop = () => {
2309
+ clearTimeout(limit);
2310
+ clearTimeout(settle);
2311
+ this.channel.removeListener("data", onData);
2312
+ this.channel.removeListener("close", onClose);
2313
+ };
2314
+ const finish = () => {
2315
+ stop();
2316
+ const output = text();
2317
+ this.prompt = output.split(`
2318
+ `).at(-1) ?? "";
2319
+ done(output);
2320
+ };
2321
+ const onData = (chunk) => {
2322
+ raw += chunk.toString("utf8");
2323
+ if (raw.endsWith("--More-- "))
2324
+ this.channel.write(" ");
2325
+ if (BANNER2.test(raw))
2326
+ this.channel.write("a");
2327
+ clearTimeout(settle);
2328
+ if (LOGGED_OUT.test(text())) {
2329
+ stop();
2330
+ fail(new Error("the device ended the session"));
2331
+ } else if (PROMPT.test(text()) || QUESTION.test(text())) {
2332
+ settle = setTimeout(finish, 300);
2333
+ }
2334
+ };
2335
+ const onClose = () => {
2336
+ stop();
2337
+ fail(new Error("the device ended the session"));
2338
+ };
2339
+ const limit = setTimeout(() => {
2340
+ stop();
2341
+ fail(new Error(`no prompt within ${timeout / 1000} seconds: ${text().slice(-200)}`));
2342
+ }, timeout);
2343
+ this.channel.on("data", onData);
2344
+ this.channel.on("close", onClose);
2345
+ });
2346
+ }
2347
+ }
2348
+ async function login(device) {
2349
+ const user = device.users?.[device.connection.user];
2350
+ if (!user)
2351
+ throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
2352
+ return connect({
2353
+ host: device.connection.host,
2354
+ port: device.connection.port,
2355
+ user: device.connection.user,
2356
+ password: resolve(user.password)
2357
+ });
2358
+ }
2359
+ async function session(device, work) {
2360
+ const client = await login(device);
2361
+ try {
2362
+ const channel = await shell(client, 4000);
2363
+ const cli = new Cli(channel);
2364
+ await cli.ready();
2365
+ const result = await work(cli);
2366
+ channel.end();
2367
+ return result;
2368
+ } finally {
2369
+ client.end();
2370
+ }
2371
+ }
2372
+ async function checkVersion(cli, device) {
2373
+ const status = await cli.run("get system status");
2374
+ const version = /^Version: \S+ v(\d+)\.(\d+)\.(\d+)/m.exec(status);
2375
+ if (!version)
2376
+ throw new Error(`${device.name} did not say which FortiOS it runs: ${status.slice(0, 200)}`);
2377
+ if (`${version[1]}.${version[2]}` !== SUPPORTED) {
2378
+ throw new Error(`${device.name} runs FortiOS ${version[1]}.${version[2]}.${version[3]}; this adapter renders for FortiOS ${SUPPORTED}`);
2379
+ }
2380
+ }
2381
+ function scrub(text) {
2382
+ const root = parse(text);
2383
+ for (const { block } of root.blocks.get("system snmp community")?.entries.values() ?? []) {
2384
+ if (block.settings.has("name"))
2385
+ block.settings.set("name", '"<redacted>"');
2386
+ }
2387
+ const hide = (block) => {
2388
+ for (const [key, value] of block.settings) {
2389
+ if (value.startsWith("ENC "))
2390
+ block.settings.set(key, "ENC <redacted>");
2391
+ if (key === "private-key")
2392
+ block.settings.set(key, '"<redacted>"');
2393
+ }
2394
+ for (const child of block.blocks.values())
2395
+ hide(child);
2396
+ for (const entry of block.entries.values())
2397
+ hide(entry.block);
2398
+ };
2399
+ hide(root);
2400
+ return print(root);
2401
+ }
2402
+ async function readConfig(cli, device) {
2403
+ await checkVersion(cli, device);
2404
+ const parts = [];
2405
+ for (const section of SECTIONS) {
2406
+ const show = FULL.includes(section) ? "show full-configuration" : "show";
2407
+ parts.push(await cli.run(`${show} ${section}`, 120000));
2408
+ }
2409
+ const text = parts.join(`
2410
+ `);
2411
+ if (!/^config system global$/m.test(text))
2412
+ throw new Error(`${device.name} returned no configuration`);
2413
+ return scrub(clean(text));
2414
+ }
2415
+ function read(device) {
2416
+ return session(device, (cli) => readConfig(cli, device));
2417
+ }
2418
+ function withSecrets(line) {
2419
+ const key = line.startsWith("set private-key ");
2420
+ const filled = line.replace(MARKER, (_, reference) => {
2421
+ const value = valueOfReference(reference);
2422
+ const text = key && !value.includes("-----BEGIN") ? Buffer.from(value, "base64").toString("utf8").trim() : value;
2423
+ return text.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
2424
+ });
2425
+ return filled.replace(/\r\n?/g, `
2426
+ `).replace(/\?/g, "\x16?");
2427
+ }
2428
+ async function leave(cli) {
2429
+ for (let level = 0;level < 10 && /\(.*\) [#$] $/.test(cli.prompt); level++)
2430
+ await cli.run("end");
2431
+ }
2432
+ async function saving(cli, mode, seconds) {
2433
+ const lines = ["config system global", `set cfg-save ${mode}`, ...seconds ? [`set cfg-revert-timeout ${seconds}`] : [], "end"];
2434
+ for (const line of lines) {
2435
+ const answer = await cli.run(line);
2436
+ if (REFUSED.test(answer))
2437
+ throw new Error(`the device refused ${line}: ${answer.trim().slice(0, 300)}`);
2438
+ }
2439
+ }
2440
+ async function configure(cli, plan, guard) {
2441
+ const minutes = guard.seconds / 60;
2442
+ await saving(cli, "revert", guard.seconds);
2443
+ const restore = async (reason) => {
2444
+ await leave(cli).catch(() => {
2445
+ return;
2446
+ });
2447
+ await cli.run("execute cfg reload").catch(() => {
2448
+ return;
2449
+ });
2450
+ return new Reloaded(`${reason}
2451
+ the device reloads its saved configuration, which undoes what was sent, at the latest in ${minutes} minutes`);
2452
+ };
2453
+ for (const step of plan.steps) {
2454
+ for (const line of step.send) {
2455
+ const answer = await cli.run(withSecrets(line)).catch((error) => error);
2456
+ if (answer instanceof Error)
2457
+ throw await restore(`${step.title}: ${line}
2458
+ ${answer.message}`);
2459
+ if (/System will reload/.test(answer)) {
2460
+ throw new Reloaded(`${step.title}: ${line}
2461
+ the rollback time ran out while the plan was sent, and the device is reloading its saved configuration`);
2462
+ }
2463
+ if (REFUSED.test(answer))
2464
+ throw await restore(`${step.title}: ${line}
2465
+ device said: ${answer.trim().slice(0, 300)}`);
2466
+ if (QUESTION.test(cli.prompt)) {
2467
+ await cli.answer("n").catch(() => {
2468
+ return;
2469
+ });
2470
+ throw await restore(`${step.title}: ${line}
2471
+ device asked: ${cli.prompt.trim()}`);
2472
+ }
2473
+ }
2474
+ }
2475
+ console.log(` sent; the device reloads its saved configuration in ${minutes} minutes unless access is proved`);
2476
+ try {
2477
+ await guard.prove();
2478
+ } catch (error) {
2479
+ throw await restore(`a fresh login failed: ${error.message}`);
2480
+ }
2481
+ const saved = await cli.run("execute cfg save", 120000);
2482
+ if (!/config saved|no config to be saved/.test(saved)) {
2483
+ throw new Error(`access was proved but the save failed; the device reverts in ${minutes} minutes:
2484
+ ${saved.trim().slice(0, 300)}`);
2485
+ }
2486
+ await saving(cli, "automatic");
2487
+ console.log(" logged in again, saved");
2488
+ }
2489
+
2490
+ class Reloaded extends Error {
2491
+ }
2492
+ var RELOAD = { attempts: 20, interval: 15000 };
2493
+ async function settle(device) {
2494
+ for (let attempt = 0;attempt < RELOAD.attempts; attempt++) {
2495
+ await new Promise((done) => setTimeout(done, RELOAD.interval));
2496
+ const settled = await session(device, async (cli) => {
2497
+ if (!/no config to be reloaded/.test(await cli.run("execute cfg reload")))
2498
+ return false;
2499
+ await saving(cli, "automatic");
2500
+ return true;
2501
+ }).catch(() => false);
2502
+ if (settled)
2503
+ return "the device is back and saves changes on its own again";
2504
+ }
2505
+ return "the device did not come back in time and still reverts every change; set `cfg-save automatic` under `config system global`";
2506
+ }
2507
+ async function apply(device, _network, plan, options) {
2508
+ try {
2509
+ await session(device, async (cli) => {
2510
+ await checkVersion(cli, device);
2511
+ await configure(cli, plan, {
2512
+ seconds: options.rollback * 60,
2513
+ prove: async () => {
2514
+ const proof = await login(device);
2515
+ proof.end();
2516
+ }
2517
+ });
2518
+ });
2519
+ } catch (error) {
2520
+ if (!(error instanceof Reloaded))
2521
+ throw error;
2522
+ throw new Error(`${error.message}
2523
+ ${await settle(device)}`);
2524
+ }
2525
+ }
2526
+ function fillLines(set, family, prefixes) {
2527
+ const list = new Block;
2528
+ prefixes.forEach((prefix, index) => {
2529
+ const rule = list.entry(index + 1);
2530
+ rule.set(family === "ipv4" ? "prefix" : "prefix6", family === "ipv4" ? subnet(network2(prefix)) : network2(prefix));
2531
+ if (split(network2(prefix)).len < BITS[family])
2532
+ rule.set("le", BITS[family]);
2533
+ });
2534
+ return [
2535
+ `config ${PREFIX_LIST[family]}`,
2536
+ `edit ${quote(set)}`,
2537
+ "config rule",
2538
+ "purge",
2539
+ ...linesOf(list).map((line) => line.trim()),
2540
+ "end",
2541
+ "next",
2542
+ "end"
2543
+ ];
2544
+ }
2545
+ async function fill(device, _network, set, family, prefixes) {
2546
+ await session(device, async (cli) => {
2547
+ await checkVersion(cli, device);
2548
+ for (const line of fillLines(set, family, prefixes)) {
2549
+ let answer = await cli.run(line);
2550
+ if (QUESTION.test(cli.prompt) && line === "purge")
2551
+ answer = await cli.answer("y");
2552
+ if (REFUSED.test(answer))
2553
+ throw new Error(`${set}: ${line}
2554
+ device said: ${answer.trim().slice(0, 300)}`);
2555
+ }
2556
+ console.log(` ${set}: ${prefixes.length} prefixes on the device`);
2557
+ });
2558
+ }
2559
+ function unsupported(device) {
2560
+ const found = [];
2561
+ found.push(...systemGaps(device));
2562
+ found.push(...managementGaps(device));
2563
+ found.push(...interfaceGaps(device));
2564
+ found.push(...staticGaps(device));
2565
+ found.push(...routingGaps(device));
2566
+ found.push(...firewallGaps(device));
2567
+ found.push(...serviceGaps(device));
2568
+ if (device.stp)
2569
+ found.push("stp, ports are routed");
2570
+ if (Object.keys(device.vrfs ?? {}).length > VRFS)
2571
+ found.push(`vrfs, at most ${VRFS}`);
2572
+ for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
2573
+ if (vrf.description)
2574
+ found.push(`vrfs.${name}.description, the platform numbers a VRF and keeps no description`);
2575
+ }
2576
+ return found;
2577
+ }
2578
+ function systemGaps(device) {
2579
+ const found = [];
2580
+ const system = device.system ?? {};
2581
+ if (system.timezone && !zoneOf(system.timezone))
2582
+ found.push(`system.timezone ${system.timezone}, the platform has no zone for it`);
2583
+ if (system.release_channel)
2584
+ found.push("system.release_channel, the platform's firmware has no release channel to follow");
2585
+ if (system.transceiver_monitoring !== undefined)
2586
+ found.push("system.transceiver_monitoring, the platform reads optics without being told");
2587
+ if (system.ip?.syn_cookies)
2588
+ found.push("system.ip.syn_cookies, the platform has a SYN proxy per DoS policy, not SYN cookies");
2589
+ const local = system.logging?.local;
2590
+ if (local?.files !== undefined || local?.lines_per_file !== undefined) {
2591
+ found.push("system.logging.local.files and lines_per_file, logs kept in memory do not rotate into files");
2592
+ }
2593
+ if ((system.logging?.remote?.length ?? 0) > COLLECTORS)
2594
+ found.push(`system.logging.remote, at most ${COLLECTORS} collectors`);
2595
+ const servers = system.dns?.servers ?? [];
2596
+ for (const family of ["ipv4", "ipv6"]) {
2597
+ if (servers.filter((server) => familyOf2(server) === family).length > RESOLVERS.device) {
2598
+ found.push(`system.dns.servers, at most ${RESOLVERS.device} of each family`);
2599
+ }
2600
+ }
2601
+ return found;
2602
+ }
2603
+ function managementGaps(device) {
2604
+ const found = [];
2605
+ const management = device.management ?? {};
2606
+ if (management.api || management.api_tls)
2607
+ found.push("management.api and api_tls, the API is served by management.https");
2608
+ if (management.ftp)
2609
+ found.push("management.ftp, the platform has no FTP service");
2610
+ if (management.console)
2611
+ found.push("management.console, the console logs in with the device's users");
2612
+ if (management.privilege_password)
2613
+ found.push("management.privilege_password, the platform has no privileged mode");
2614
+ if (management.native?.interfaces?.length)
2615
+ found.push("management.native.interfaces, FortiManager access runs over IP only");
2616
+ if (management.native?.port)
2617
+ found.push("management.native.port, FortiManager access listens on 541");
2618
+ if (management.ssh?.auth_retries)
2619
+ found.push("management.ssh.auth_retries, the platform counts failed logins per source, not per connection");
2620
+ for (const [name, user] of Object.entries(management.snmp?.users ?? {})) {
2621
+ if (user.auth === "sha256")
2622
+ found.push(`management.snmp.users.${name}.auth sha256, the platform authenticates with SHA1 or MD5`);
2623
+ }
2624
+ for (const [name, user] of Object.entries(device.users ?? {})) {
2625
+ if ((user.ssh_keys?.length ?? 0) > SSH_KEYS)
2626
+ found.push(`users.${name}.ssh_keys, at most ${SSH_KEYS}`);
2627
+ }
2628
+ return found;
2629
+ }
2630
+ function portNames(device) {
2631
+ return new Set(portsOf(device.platform, device.model).map((port) => spell(device.platform, device.model, port)));
2632
+ }
2633
+ function interfaceGaps(device) {
2634
+ const found = [];
2635
+ const vlanInterfaces = new Set(Object.values(device.interfaces ?? {}).flatMap((iface) => iface.type === "vlan" ? [iface.vlan] : []));
2636
+ for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
2637
+ if (vlan.description)
2638
+ found.push(`vlans.${name}.description, a VLAN interface holds its own`);
2639
+ }
2640
+ for (const [name, port] of Object.entries(device.ports ?? {})) {
2641
+ if (!port)
2642
+ continue;
2643
+ const where = `ports.${name}`;
2644
+ const speedClass = name.split("-")[0];
2645
+ if (port.speed && !SPEEDS[speedClass]?.[port.speed])
2646
+ found.push(`${where}.speed ${port.speed}, not in a ${speedClass} cage`);
2647
+ for (const field of ["access_vlan", "native_vlan", "stp", "storm_control"]) {
2648
+ if (port[field] !== undefined)
2649
+ found.push(`${where}.${field}, ports are routed`);
2650
+ }
2651
+ if (port.hardware_offload !== undefined)
2652
+ found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
2653
+ if (port.lag) {
2654
+ for (const field of ["addresses", "mtu", "vrrp", "vrf", "lldp", "trunk_vlans", "ipv6_ra", "acl"]) {
2655
+ if (port[field] !== undefined)
2656
+ found.push(`${where}.${field}, a LAG member takes its settings from the LAG`);
2657
+ }
2658
+ }
2659
+ const acl = port.acl ? device.acls?.[port.acl] : undefined;
2660
+ if (acl)
2661
+ found.push(...aclGaps(`acls.${port.acl}`, acl));
2662
+ found.push(...trunkGaps(where, name, port.trunk_vlans, vlanInterfaces, device));
2663
+ found.push(...routedGaps(where, port, false));
2664
+ }
2665
+ const ports = portNames(device);
2666
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
2667
+ const where = `interfaces.${name}`;
2668
+ if (iface.type === "wireguard") {
2669
+ found.push(`${where}, a wireguard: the platform has no WireGuard`);
2670
+ continue;
2671
+ }
2672
+ if (name.length > NAME_LENGTH.interface)
2673
+ found.push(`${where}, a name longer than ${NAME_LENGTH.interface} characters`);
2674
+ if (ports.has(name) || RESERVED_INTERFACES.has(name))
2675
+ found.push(`${where}, a name the platform gives a port of its own`);
2676
+ if (iface.hardware_offload !== undefined)
2677
+ found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
2678
+ if (iface.type === "lag") {
2679
+ if (iface.id !== undefined)
2680
+ found.push(`${where}.id, the platform names a LAG by its key`);
2681
+ for (const field of ["access_vlan", "native_vlan", "stp", "storm_control"]) {
2682
+ if (iface[field] !== undefined)
2683
+ found.push(`${where}.${field}, LAGs are routed`);
2684
+ }
2685
+ found.push(...trunkGaps(where, name, iface.trunk_vlans, vlanInterfaces, device));
2686
+ }
2687
+ if (iface.type === "vlan" && !vlanParent(device, iface.vlan)) {
2688
+ found.push(`${where}, VLAN ${iface.vlan} must be carried tagged by exactly one port or LAG`);
2689
+ }
2690
+ if (iface.type === "loopback") {
2691
+ if (iface.mtu !== undefined)
2692
+ found.push(`${where}.mtu, a loopback has no MTU of its own`);
2693
+ if (iface.vrrp?.length)
2694
+ found.push(`${where}.vrrp, a loopback runs no VRRP`);
2695
+ }
2696
+ if (iface.type === "gre" || iface.type === "vxlan") {
2697
+ if (!holderOf(device, iface.local))
2698
+ found.push(`${where}.local, no interface holds ${iface.local}`);
2699
+ if (familyOf2(iface.local) !== familyOf2(iface.remote))
2700
+ found.push(`${where}, endpoints of two families`);
2701
+ if (iface.vrrp?.length)
2702
+ found.push(`${where}.vrrp, on a tunnel`);
2703
+ if (iface.ipv6_ra)
2704
+ found.push(`${where}.ipv6_ra, on a tunnel`);
2705
+ }
2706
+ if (iface.type === "gre") {
2707
+ for (const entry of iface.addresses ?? []) {
2708
+ if (typeof entry !== "string" && familyOf2(host2(entry.address)) === "ipv6")
2709
+ found.push(`${where}.addresses, a peer on an IPv6 address`);
2710
+ }
2711
+ const ipv4 = (iface.addresses ?? []).filter((entry) => familyOf2(host2(typeof entry === "string" ? entry : entry.address)) === "ipv4");
2712
+ if (ipv4.some((entry) => typeof entry === "string"))
2713
+ found.push(`${where}.addresses, an IPv4 address on a tunnel takes its peer`);
2714
+ if (ipv4.length > 1)
2715
+ found.push(`${where}.addresses, one IPv4 address`);
2716
+ }
2717
+ found.push(...routedGaps(where, iface, iface.type === "gre"));
2718
+ }
2719
+ if (device.lldp && typeof device.lldp === "object") {
2720
+ for (const name of device.lldp.interfaces) {
2721
+ const settings = device.interfaces?.[name];
2722
+ if (settings && settings.type !== "lag" && settings.type !== "vlan")
2723
+ found.push(`lldp.interfaces ${name}, a ${settings.type}`);
2724
+ }
2725
+ }
2726
+ return found;
2727
+ }
2728
+ function trunkGaps(where, name, vlans, withInterface, device) {
2729
+ const found = [];
2730
+ for (const vlan of vlans ?? []) {
2731
+ if (!withInterface.has(vlan)) {
2732
+ found.push(`${where}.trunk_vlans ${vlan}, a VLAN with no interface here: the platform routes and does not switch`);
2733
+ } else if (vlanParent(device, vlan) !== name) {
2734
+ found.push(`${where}.trunk_vlans ${vlan}, which another port or LAG also carries`);
2735
+ }
2736
+ }
2737
+ return found;
2738
+ }
2739
+ function routedGaps(where, settings, tunnel) {
2740
+ const found = [];
2741
+ if (!tunnel && settings.addresses?.some((entry) => typeof entry !== "string"))
2742
+ found.push(`${where}.addresses with a peer, which only a tunnel takes`);
2743
+ for (const group of settings.vrrp ?? []) {
2744
+ if (familyOf2(group.address) === "ipv6")
2745
+ found.push(`${where}.vrrp ${group.id}, IPv6: the platform runs VRRP for IPv4`);
2746
+ }
2747
+ return found;
2748
+ }
2749
+ function aclGaps(where, rules) {
2750
+ const found = [];
2751
+ if (acceptsBeforeDrop(rules))
2752
+ found.push(`${where}, an accept before a drop: the platform's ACLs only drop`);
2753
+ rules.forEach((rule, index) => {
2754
+ if (rule.rate)
2755
+ found.push(`${where}[${index}].rate`);
2756
+ if (rule.match?.vlan)
2757
+ found.push(`${where}[${index}].match.vlan, a port's ACL sees no VLAN`);
2758
+ });
2759
+ return found;
2760
+ }
2761
+ function staticGaps(device) {
2762
+ const found = [];
2763
+ const seen = new Set;
2764
+ for (const route of device.routing?.static ?? []) {
2765
+ const where = `routing.static ${route.prefix}`;
2766
+ if (route.vrf && route.blackhole)
2767
+ found.push(`${where}, a blackhole in a VRF: a route takes the VRF of the interface it leaves through`);
2768
+ if (route.vrf && route.interface && settingsOf(device, route.interface)?.vrf !== route.vrf) {
2769
+ found.push(`${where}.interface, which is not in VRF ${route.vrf}`);
2770
+ }
2771
+ if (route.via && !route.interface && !route.blackhole && !interfaceFor(device, route.via, route.vrf)) {
2772
+ found.push(`${where} via ${route.via}, no interface's own network in its table holds the next hop`);
2773
+ }
2774
+ const key = [
2775
+ split(route.prefix).addr,
2776
+ split(route.prefix).len,
2777
+ route.via,
2778
+ route.interface,
2779
+ route.blackhole,
2780
+ route.distance,
2781
+ route.vrf
2782
+ ].join(" ");
2783
+ if (seen.has(key))
2784
+ found.push(`${where}, the same route twice`);
2785
+ seen.add(key);
2786
+ }
2787
+ return found;
2788
+ }
2789
+ function firewallGaps(device) {
2790
+ const found = [];
2791
+ const fw = device.firewall;
2792
+ if (!fw)
2793
+ return found;
2794
+ if (fw.filter?.output)
2795
+ found.push("firewall.filter.output, the platform filters no traffic it sends itself");
2796
+ if (fw.untracked)
2797
+ found.push("firewall.untracked, every session is tracked");
2798
+ for (const helper of fw.helpers ?? []) {
2799
+ if (!HELPERS[helper])
2800
+ found.push(`firewall.helpers ${helper}, the platform has no helper for it`);
2801
+ }
2802
+ for (const chain of ["forward", "input"]) {
2803
+ (fw.filter?.[chain]?.rules ?? []).forEach((rule, index) => found.push(...ruleGaps2(`firewall.filter.${chain}[${index}]`, rule, chain, device)));
2804
+ }
2805
+ (fw.nat?.source ?? []).forEach((rule, index) => {
2806
+ const where = `firewall.nat.source[${index}]`;
2807
+ const match = rule.match;
2808
+ if (familiesOf(device, match).includes("ipv6") && (match?.family === "ipv6" || match?.src || match?.dst)) {
2809
+ found.push(`${where}, IPv6: central NAT translates IPv4`);
2810
+ }
2811
+ if (rule.action === "dnat")
2812
+ found.push(`${where}.action dnat, in source NAT`);
2813
+ if (rule.action === "snat" && familyOf2(rule.to) === "ipv6")
2814
+ found.push(`${where}.to, IPv6: central NAT translates IPv4`);
2815
+ if (match?.dst_port !== undefined)
2816
+ found.push(`${where}.match.dst_port, central NAT matches the source port only`);
2817
+ if (Array.isArray(match?.src_port))
2818
+ found.push(`${where}.match.src_port, one port or range`);
2819
+ if (rule.action === "snat" && rule.to_port !== undefined && match?.src_port === undefined)
2820
+ found.push(`${where}.to_port, without match.src_port`);
2821
+ if (match?.src && match.src_set || match?.dst && match.dst_set)
2822
+ found.push(`${where}.match, a prefix and a set on one side`);
2823
+ if (match?.state || match?.tcp_flags)
2824
+ found.push(`${where}.match.state and tcp_flags`);
2825
+ });
2826
+ (fw.nat?.destination ?? []).forEach((rule, index) => {
2827
+ const where = `firewall.nat.destination[${index}]`;
2828
+ const match = rule.match;
2829
+ if (rule.action !== "dnat") {
2830
+ found.push(`${where}.action ${rule.action}, in destination NAT`);
2831
+ return;
2832
+ }
2833
+ if (!match?.dst || split(match.dst.includes("/") ? match.dst : `${match.dst}/32`).len !== 32) {
2834
+ found.push(`${where}.match.dst, one IPv4 address the virtual IP answers on`);
2835
+ }
2836
+ const sources = match?.src ? [match.src] : match?.src_set ? fw.address_sets?.[match.src_set] ?? [] : [];
2837
+ if (familyOf2(rule.to) === "ipv6" || sources.some((source) => familyOf2(host2(source)) === "ipv6"))
2838
+ found.push(`${where}, IPv6`);
2839
+ if (match?.dst_port !== undefined && Array.isArray(match.dst_port))
2840
+ found.push(`${where}.match.dst_port, one port or range`);
2841
+ if (match?.dst_port !== undefined && match.protocol !== "tcp" && match.protocol !== "udp") {
2842
+ found.push(`${where}.match.protocol, tcp or udp with a port`);
2843
+ }
2844
+ if (match?.dst_port === undefined && (match?.protocol || rule.to_port)) {
2845
+ found.push(`${where}.match.dst_port, a protocol or port is forwarded by its port`);
2846
+ }
2847
+ if (match?.src && match.src_set)
2848
+ found.push(`${where}.match, a prefix and a set on one side`);
2849
+ for (const field of ["dst_set", "src_port", "out_interface", "state", "tcp_flags", "family"]) {
2850
+ if (match?.[field] !== undefined)
2851
+ found.push(`${where}.match.${field}`);
2852
+ }
2853
+ });
2854
+ for (const name of Object.keys(fw.address_sets ?? {})) {
2855
+ if (name.length > NAME_LENGTH.object)
2856
+ found.push(`firewall.address_sets.${name}, a name longer than ${NAME_LENGTH.object} characters`);
2857
+ if (name.startsWith("circuit-"))
2858
+ found.push(`firewall.address_sets.${name}, a name Circuit keeps for the sources of management`);
2859
+ }
2860
+ return found;
2861
+ }
2862
+ function ruleGaps2(where, rule, chain, device) {
2863
+ const found = [];
2864
+ const match = rule.match;
2865
+ const state = match?.state ?? [];
2866
+ if (state.some((one) => one === "invalid" || one === "untracked"))
2867
+ found.push(`${where}.match.state ${state.join(", ")}`);
2868
+ else if (state.length && rule.action !== "accept")
2869
+ found.push(`${where}.match.state, on a rule that does not accept`);
2870
+ if (match?.tcp_flags)
2871
+ found.push(`${where}.match.tcp_flags, the platform matches no TCP flags`);
2872
+ if (match?.src && match.src_set || match?.dst && match.dst_set)
2873
+ found.push(`${where}.match, a prefix and a set on one side`);
2874
+ if (isImplied(rule))
2875
+ return found;
2876
+ const families = familiesOf(device, match);
2877
+ if (!families.length)
2878
+ found.push(`${where}, its addresses share no family`);
2879
+ if (match?.src && match.dst && familyOf2(host2(match.src)) !== familyOf2(host2(match.dst))) {
2880
+ found.push(`${where}, a source and destination of two families`);
2881
+ }
2882
+ for (const family of families) {
2883
+ if (serviceOf(match, family).name.length > NAME_LENGTH.object)
2884
+ found.push(`${where}.match ports, too many for one service`);
2885
+ }
2886
+ if (chain === "input") {
2887
+ if (match?.out_interface)
2888
+ found.push(`${where}.match.out_interface, on traffic to the device`);
2889
+ if (rule.action === "reject")
2890
+ found.push(`${where}.action reject, the platform drops traffic to itself silently`);
2891
+ if (rule.offload)
2892
+ found.push(`${where}.offload, on traffic to the device`);
2893
+ }
2894
+ return found;
2895
+ }
2896
+ function serviceGaps(device) {
2897
+ const found = [];
2898
+ for (const [name, server] of Object.entries(device.dhcp ?? {})) {
2899
+ if (familyOf2(host2(server.network)) === "ipv6")
2900
+ found.push(`dhcp.${name}.network, IPv6: DHCPv6 hands out no gateway`);
2901
+ if ((server.dns?.length ?? 0) > RESOLVERS.dhcp)
2902
+ found.push(`dhcp.${name}.dns, at most ${RESOLVERS.dhcp}`);
2903
+ }
2904
+ const flow = device.flow_export;
2905
+ if (flow?.protocol === "ipfix")
2906
+ found.push("flow_export.protocol ipfix, the platform exports sFlow and NetFlow 9");
2907
+ if (flow && flow.collectors.length > 1)
2908
+ found.push("flow_export.collectors, one collector");
2909
+ for (const name of flow?.interfaces ?? []) {
2910
+ const type = device.interfaces?.[name]?.type;
2911
+ if (type === "gre" || type === "vxlan")
2912
+ found.push(`flow_export.interfaces ${name}, a tunnel: the platform samples the interface it leaves through`);
2913
+ }
2914
+ if (flow?.protocol === "netflow-v9" && flow.sampling !== undefined)
2915
+ found.push("flow_export.sampling, NetFlow here exports every flow");
2916
+ if (flow?.protocol === "sflow" && (flow.sampling === undefined || flow.sampling < 10 || flow.sampling > 99999)) {
2917
+ found.push("flow_export.sampling, required and from 10 to 99999: the platform samples");
2918
+ }
2919
+ return found;
2920
+ }
2921
+ var fortios = {
2922
+ extension: ".conf",
2923
+ unsupported,
2924
+ render,
2925
+ read,
2926
+ plan,
2927
+ apply,
2928
+ fill
2929
+ };
2930
+
205
2931
  // src/adapters/devices/derive.ts
206
2932
  function lagIds(device) {
207
2933
  const numbers = new Map;
@@ -262,7 +2988,7 @@ var KEY_STATEMENT = {
262
2988
  var SNMP_AUTH = { sha1: "authentication-sha", sha256: "authentication-sha256" };
263
2989
  var SNMP_PRIVACY = { aes128: "privacy-aes128", des: "privacy-des" };
264
2990
  var SNMP_GROUP = "circuit";
265
- var LEVEL = {
2991
+ var LEVEL2 = {
266
2992
  emergency: "emergency",
267
2993
  alert: "alert",
268
2994
  critical: "critical",
@@ -287,7 +3013,7 @@ var SERVICE_PORTS = {
287
3013
  native: ["tcp", 830]
288
3014
  };
289
3015
  var BFD_INTERVAL = 300;
290
- function quote(value) {
3016
+ function quote2(value) {
291
3017
  if (/^[\w.:/@-]+$/.test(value))
292
3018
  return value;
293
3019
  const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
@@ -383,13 +3109,13 @@ function stormProfile(settings) {
383
3109
  const left = STORM_KINDS.filter(([kind]) => !storm[kind]).map(([, statement]) => statement);
384
3110
  return { name: `${kinds.join("-")}-${first.percent}-percent`, percent: first.percent, left };
385
3111
  }
386
- function render(device) {
3112
+ function render2(device) {
387
3113
  const out = new Statements;
388
3114
  const names = new Names(device);
389
3115
  system(out, device);
390
- users(out, device);
3116
+ users2(out, device);
391
3117
  services(out, device);
392
- snmp(out, device);
3118
+ snmp2(out, device);
393
3119
  vlans(out, device, names);
394
3120
  ports(out, device, names);
395
3121
  lags(out, device, names);
@@ -397,25 +3123,25 @@ function render(device) {
397
3123
  stormProfiles(out, device);
398
3124
  servicesFilter(out, device, names);
399
3125
  vrfs(out, device, names);
400
- staticRoutes(out, device, names);
3126
+ staticRoutes2(out, device, names);
401
3127
  spanningTree(out, device, names);
402
3128
  lldp(out, device, names);
403
- ospf(out, device, names);
3129
+ ospf2(out, device, names);
404
3130
  dhcpRelay(out, device, names);
405
- dhcpServers(out, device, names);
3131
+ dhcpServers2(out, device, names);
406
3132
  routerAdvertisements(out, device, names);
407
- acls(out, device, names);
3133
+ acls2(out, device, names);
408
3134
  renderBgp(out, device);
409
3135
  sflow(out, device, names);
410
3136
  return out.text();
411
3137
  }
412
3138
  function system(out, device) {
413
- out.set("system host-name", quote(device.name));
3139
+ out.set("system host-name", quote2(device.name));
414
3140
  const system = device.system ?? {};
415
3141
  if (system.timezone)
416
3142
  out.set("system time-zone", system.timezone);
417
3143
  if (system.banner)
418
- out.set("system login message", quote(system.banner));
3144
+ out.set("system login message", quote2(system.banner));
419
3145
  for (const server of system.dns?.servers ?? [])
420
3146
  out.set("system name-server", server);
421
3147
  for (const server of system.ntp?.servers ?? [])
@@ -426,27 +3152,27 @@ function system(out, device) {
426
3152
  }
427
3153
  const local = system.logging?.local;
428
3154
  if (local) {
429
- out.set("system syslog file messages any", LEVEL[local.level]);
3155
+ out.set("system syslog file messages any", LEVEL2[local.level]);
430
3156
  if (local.files !== undefined)
431
3157
  out.set("system syslog file messages archive files", local.files);
432
3158
  }
433
3159
  for (const remote of system.logging?.remote ?? []) {
434
- out.set("system syslog host", remote.address, "any", LEVEL[remote.level]);
3160
+ out.set("system syslog host", remote.address, "any", LEVEL2[remote.level]);
435
3161
  if (remote.port !== undefined && remote.port !== 514)
436
3162
  out.set("system syslog host", remote.address, "port", remote.port);
437
3163
  }
438
3164
  }
439
- function users(out, device) {
3165
+ function users2(out, device) {
440
3166
  for (const [name, user] of Object.entries(device.users ?? {})) {
441
3167
  const login = name === "root" ? "system root-authentication" : `system login user ${name} authentication`;
442
3168
  if (name !== "root") {
443
3169
  out.set(`system login user ${name} class`, USER_CLASS[user.role]);
444
3170
  if (user.description)
445
- out.set(`system login user ${name} full-name`, quote(user.description));
3171
+ out.set(`system login user ${name} full-name`, quote2(user.description));
446
3172
  }
447
3173
  out.set(login, "plain-text-password-value", marker(user.password));
448
3174
  for (const key of user.ssh_keys ?? [])
449
- out.set(login, KEY_STATEMENT[key.split(" ")[0]], quote(key));
3175
+ out.set(login, KEY_STATEMENT[key.split(" ")[0]], quote2(key));
450
3176
  }
451
3177
  }
452
3178
  function services(out, device) {
@@ -483,14 +3209,14 @@ function services(out, device) {
483
3209
  out.set(statement, "port", settings.port);
484
3210
  }
485
3211
  }
486
- function snmp(out, device) {
3212
+ function snmp2(out, device) {
487
3213
  const snmp = device.management?.snmp;
488
3214
  if (!snmp)
489
3215
  return;
490
3216
  if (snmp.contact)
491
- out.set("snmp contact", quote(snmp.contact));
3217
+ out.set("snmp contact", quote2(snmp.contact));
492
3218
  if (snmp.location)
493
- out.set("snmp location", quote(snmp.location));
3219
+ out.set("snmp location", quote2(snmp.location));
494
3220
  if (snmp.community)
495
3221
  out.set("snmp community", marker(snmp.community), "authorization read-only");
496
3222
  if (!snmp.users)
@@ -508,7 +3234,7 @@ function vlans(out, device, names) {
508
3234
  for (const [name, vlan] of Object.entries(device.vlans ?? {})) {
509
3235
  out.set(`vlans ${name} vlan-id`, vlan.id);
510
3236
  if (vlan.description)
511
- out.set(`vlans ${name} description`, quote(vlan.description));
3237
+ out.set(`vlans ${name} description`, quote2(vlan.description));
512
3238
  }
513
3239
  for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
514
3240
  if (iface.type === "vlan")
@@ -566,7 +3292,7 @@ function ports(out, device, names) {
566
3292
  continue;
567
3293
  }
568
3294
  if (settings.description)
569
- out.set(`interfaces ${name} description`, quote(settings.description));
3295
+ out.set(`interfaces ${name} description`, quote2(settings.description));
570
3296
  if (settings.lag) {
571
3297
  out.set(`interfaces ${name} ether-options 802.3ad`, `ae${names.lags.get(settings.lag)}`);
572
3298
  continue;
@@ -586,7 +3312,7 @@ function lags(out, device, names) {
586
3312
  const physical = names.physical(name);
587
3313
  highest = Math.max(highest, names.lags.get(name));
588
3314
  if (lag.description)
589
- out.set(`interfaces ${physical} description`, quote(lag.description));
3315
+ out.set(`interfaces ${physical} description`, quote2(lag.description));
590
3316
  if (lag.mode !== "static")
591
3317
  out.set(`interfaces ${physical} aggregated-ether-options lacp active`);
592
3318
  if (lag.mtu !== undefined)
@@ -605,7 +3331,7 @@ function routedInterfaces(out, device, names) {
605
3331
  const [physical, number] = unit.split(".");
606
3332
  const prefix = `interfaces ${physical} unit ${number}`;
607
3333
  if (iface.description)
608
- out.set(`${prefix} description`, quote(iface.description));
3334
+ out.set(`${prefix} description`, quote2(iface.description));
609
3335
  if (iface.type === "gre") {
610
3336
  out.set(`${prefix} tunnel source`, iface.local);
611
3337
  out.set(`${prefix} tunnel destination`, iface.remote);
@@ -700,7 +3426,7 @@ function vrfs(out, device, names) {
700
3426
  for (const [name, vrf] of Object.entries(device.vrfs ?? {})) {
701
3427
  out.set(`routing-instances ${name} instance-type virtual-router`);
702
3428
  if (vrf.description)
703
- out.set(`routing-instances ${name} description`, quote(vrf.description));
3429
+ out.set(`routing-instances ${name} description`, quote2(vrf.description));
704
3430
  }
705
3431
  const routed = [...Object.entries(device.ports ?? {}), ...Object.entries(device.interfaces ?? {})];
706
3432
  for (const [name, settings] of routed) {
@@ -708,7 +3434,7 @@ function vrfs(out, device, names) {
708
3434
  out.set(`routing-instances ${settings.vrf} interface`, names.unit(name));
709
3435
  }
710
3436
  }
711
- function staticRoutes(out, device, names) {
3437
+ function staticRoutes2(out, device, names) {
712
3438
  if (device.routing?.router_id)
713
3439
  out.set("routing-options router-id", device.routing.router_id);
714
3440
  for (const route of device.routing?.static ?? []) {
@@ -777,7 +3503,7 @@ function lldp(out, device, names) {
777
3503
  out.set("protocols lldp interface", names.port(port));
778
3504
  }
779
3505
  }
780
- function ospf(out, device, names) {
3506
+ function ospf2(out, device, names) {
781
3507
  const ospf = device.routing?.ospf;
782
3508
  if (!ospf)
783
3509
  return;
@@ -852,8 +3578,8 @@ function aclFamily(rule) {
852
3578
  return address ? familyOf2(host2(address)) : undefined;
853
3579
  }
854
3580
  var nextHeader = (protocol) => protocol === "icmp" ? "icmp6" : protocol;
855
- var ruleName = (label, description) => quote(description ? `${label} ${description}` : label);
856
- function acls(out, device, names) {
3581
+ var ruleName = (label, description) => quote2(description ? `${label} ${description}` : label);
3582
+ function acls2(out, device, names) {
857
3583
  const switched = new Set;
858
3584
  const routed = new Set;
859
3585
  for (const [port, settings] of Object.entries(device.ports ?? {})) {
@@ -945,7 +3671,7 @@ function routedFilter(out, device, acl, family) {
945
3671
  });
946
3672
  out.set(filter, "term circuit-default then accept");
947
3673
  }
948
- function dhcpServers(out, device, names) {
3674
+ function dhcpServers2(out, device, names) {
949
3675
  for (const [name, server] of Object.entries(device.dhcp ?? {})) {
950
3676
  out.set(`system services dhcp-local-server group ${name} interface`, names.unit(server.interface));
951
3677
  const pool = `access address-assignment pool ${name} family inet`;
@@ -978,17 +3704,17 @@ function routerAdvertisements(out, device, names) {
978
3704
  }
979
3705
 
980
3706
  // src/adapters/devices/junos/bgp.ts
981
- var REJECT_ALL = "circuit-reject-all";
3707
+ var REJECT_ALL2 = "circuit-reject-all";
982
3708
  var ORIGIN = "circuit-bgp-origin";
983
3709
  var RPKI_GROUP = "circuit";
984
3710
  var GENERATED = "# generated ";
985
3711
  var FAMILY = { ipv4: "inet", ipv6: "inet6" };
986
3712
  var VALIDATION = { valid: "valid", invalid: "invalid", "not-found": "unknown" };
987
- function settingsOf(device, neighbor) {
3713
+ function settingsOf2(device, neighbor) {
988
3714
  const group = neighbor.group ? device.routing?.bgp?.groups?.[neighbor.group] : undefined;
989
3715
  return { ...group, ...neighbor };
990
3716
  }
991
- var slug = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
3717
+ var slug2 = (value) => value.replace(/\*/g, "any").replace(/[^A-Za-z0-9]+/g, "-");
992
3718
  var member = (community, large) => large ? `large:${community}` : community;
993
3719
  function renderBgp(out, device) {
994
3720
  prefixSets(out, device);
@@ -1001,12 +3727,12 @@ function renderBgp(out, device) {
1001
3727
  let rejectAll = false;
1002
3728
  let exports = false;
1003
3729
  for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
1004
- const settings = settingsOf(device, neighbor);
3730
+ const settings = settingsOf2(device, neighbor);
1005
3731
  const group = `protocols bgp group ${name}`;
1006
3732
  out.set(group, "type", settings.remote_as === bgp.asn ? "internal" : "external");
1007
3733
  out.set(group, "peer-as", settings.remote_as);
1008
3734
  if (settings.description)
1009
- out.set(group, "description", quote(settings.description));
3735
+ out.set(group, "description", quote2(settings.description));
1010
3736
  if (settings.local_address)
1011
3737
  out.set(group, "local-address", settings.local_address);
1012
3738
  if (settings.multihop)
@@ -1019,13 +3745,13 @@ function renderBgp(out, device) {
1019
3745
  out.set(group, "authentication-key", marker(settings.password));
1020
3746
  if (settings.bfd)
1021
3747
  out.set(group, "bfd-liveness-detection minimum-interval", BFD_INTERVAL);
1022
- out.set(group, "import", settings.import ?? REJECT_ALL);
3748
+ out.set(group, "import", settings.import ?? REJECT_ALL2);
1023
3749
  if (settings.export) {
1024
3750
  exports = true;
1025
3751
  out.set(group, "export", ORIGIN);
1026
3752
  out.set(group, "export", settings.export);
1027
3753
  } else
1028
- out.set(group, "export", REJECT_ALL);
3754
+ out.set(group, "export", REJECT_ALL2);
1029
3755
  for (const policy of [settings.import, settings.export]) {
1030
3756
  if (policy)
1031
3757
  used.add(policy);
@@ -1043,7 +3769,7 @@ function renderBgp(out, device) {
1043
3769
  out.set(group, "neighbor", neighbor.address);
1044
3770
  }
1045
3771
  if (rejectAll)
1046
- out.set(`policy-options policy-statement ${REJECT_ALL} then reject`);
3772
+ out.set(`policy-options policy-statement ${REJECT_ALL2} then reject`);
1047
3773
  if (exports)
1048
3774
  origin(out, device);
1049
3775
  for (const policy of [...used].sort())
@@ -1077,7 +3803,7 @@ function rpki(out, device) {
1077
3803
  out.set(`routing-options validation group ${RPKI_GROUP} session`, server.address, "port", server.port);
1078
3804
  }
1079
3805
  }
1080
- function familyOfRule(device, rule) {
3806
+ function familyOfRule2(device, rule) {
1081
3807
  const match = rule.match;
1082
3808
  if (match?.family)
1083
3809
  return match.family;
@@ -1090,24 +3816,24 @@ function familyOfRule(device, rule) {
1090
3816
  return familyOf2(host2(set.prefixes[0]));
1091
3817
  return;
1092
3818
  }
1093
- function flatten(device, policy, label, seen) {
3819
+ function flatten2(device, policy, label, seen) {
1094
3820
  const rules = device.policies?.[policy] ?? [];
1095
3821
  const flat = [];
1096
3822
  rules.forEach((rule, index) => {
1097
3823
  const here = label ? `${label}.${index + 1}` : `${index + 1}`;
1098
3824
  if (rule.call && !seen.includes(rule.call))
1099
- flat.push(...flatten(device, rule.call, here, [...seen, rule.call]));
3825
+ flat.push(...flatten2(device, rule.call, here, [...seen, rule.call]));
1100
3826
  if (rule.match || rule.set || rule.action)
1101
3827
  flat.push({ label: here, rule });
1102
3828
  });
1103
3829
  return flat;
1104
3830
  }
1105
- var termName = (label, rule) => quote(rule.description ? `${label} ${rule.description}` : label);
3831
+ var termName = (label, rule) => quote2(rule.description ? `${label} ${rule.description}` : label);
1106
3832
  function policyStatement(out, device, policy) {
1107
3833
  const statement = `policy-options policy-statement ${policy}`;
1108
- const flat = flatten(device, policy, "", [policy]);
3834
+ const flat = flatten2(device, policy, "", [policy]);
1109
3835
  for (const { label, rule } of flat) {
1110
- const family = familyOfRule(device, rule);
3836
+ const family = familyOfRule2(device, rule);
1111
3837
  const split = !family && (rule.set?.next_hop || rule.match?.prefix_length) ? ["ipv4", "ipv6"] : [family];
1112
3838
  for (const part of split) {
1113
3839
  const name = split.length > 1 ? termName(`${label}-${part === "ipv4" ? "v4" : "v6"}`, rule) : termName(label, rule);
@@ -1141,7 +3867,7 @@ function term(out, device, term, rule, family, splitFamily) {
1141
3867
  if (match.as_path_length) {
1142
3868
  const { min = 0, max } = match.as_path_length;
1143
3869
  const name = `circuit-path-length-${min}-${max ?? "any"}`;
1144
- out.set(`policy-options as-path ${name}`, quote(`.{${min},${max ?? ""}}`));
3870
+ out.set(`policy-options as-path ${name}`, quote2(`.{${min},${max ?? ""}}`));
1145
3871
  from.push(["as-path", name]);
1146
3872
  }
1147
3873
  if (match.rpki)
@@ -1158,7 +3884,7 @@ function term(out, device, term, rule, family, splitFamily) {
1158
3884
  then.push(["metric", set.med]);
1159
3885
  if (set.prepend !== undefined) {
1160
3886
  const asn = device.routing.bgp.asn;
1161
- then.push(["as-path-prepend", quote(Array(set.prepend).fill(asn).join(" "))]);
3887
+ then.push(["as-path-prepend", quote2(Array(set.prepend).fill(asn).join(" "))]);
1162
3888
  }
1163
3889
  const removed = [
1164
3890
  ...(set.communities ? ["*:*"] : set.remove_communities ?? []).map((pattern) => member(pattern, false)),
@@ -1190,9 +3916,9 @@ function term(out, device, term, rule, family, splitFamily) {
1190
3916
  out.set(term);
1191
3917
  }
1192
3918
  function community(out, purpose, members) {
1193
- const name = `circuit-${purpose}-${slug(members.join(" "))}`;
3919
+ const name = `circuit-${purpose}-${slug2(members.join(" "))}`;
1194
3920
  for (const value of members)
1195
- out.set(`policy-options community ${name} members`, quote(value));
3921
+ out.set(`policy-options community ${name} members`, quote2(value));
1196
3922
  return name;
1197
3923
  }
1198
3924
 
@@ -1200,10 +3926,10 @@ function community(out, purpose, members) {
1200
3926
  var SUPPORTED_MAJOR = 21;
1201
3927
  var DEVICE_FACTS = [/^set version /, /^set system login user \S+ uid \d+$/];
1202
3928
  var SYSTEM_ACCOUNTS = new Set(["bind", "daemon", "kmem", "nobody", "operator", "sshd", "tty", "uucp"]);
1203
- function wordsOf(statement) {
3929
+ function wordsOf2(statement) {
1204
3930
  return statement.match(/"(?:[^"\\]|\\.)*"|\S+/g) ?? [];
1205
3931
  }
1206
- var bare = (word) => word.startsWith('"') ? word.slice(1, -1).replace(/\\(.)/g, "$1") : word;
3932
+ var bare2 = (word) => word.startsWith('"') ? word.slice(1, -1).replace(/\\(.)/g, "$1") : word;
1207
3933
  var SECRETS = [
1208
3934
  [/ (?:encrypted-password|plain-text-password-value) (?:"[^"]*"|\S+)/, " password <secret>"],
1209
3935
  [/ (?:authentication-key|authentication-password) (?:"[^"]*"|\S+)/, " authentication-key <secret>"],
@@ -1214,15 +3940,15 @@ function keyOf2(statement) {
1214
3940
  let canonical = statement;
1215
3941
  for (const [pattern, replacement] of SECRETS)
1216
3942
  canonical = canonical.replace(pattern, replacement);
1217
- return wordsOf(canonical).slice(1).map(bare);
3943
+ return wordsOf2(canonical).slice(1).map(bare2);
1218
3944
  }
1219
- function scrub(text) {
3945
+ function scrub2(text) {
1220
3946
  return text.replace(/ ## SECRET-DATA$/gm, "").replace(/((?:encrypted-password|authentication-key|privacy-key) )(?:"[^"]*"|\S+)/g, "$1<redacted>").replace(/^(set snmp community )(?:"[^"]*"|\S+)/gm, "$1<redacted>").replace(/"\$\d\$[^"]*"/g, "<redacted>");
1221
3947
  }
1222
- function clean(output) {
3948
+ function clean2(output) {
1223
3949
  const statements = output.replace(/\r/g, "").split(`
1224
3950
  `).map((line) => line.trimEnd()).filter((line) => /^(set|deactivate) /.test(line)).filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
1225
- return scrub(statements.join(`
3951
+ return scrub2(statements.join(`
1226
3952
  `) + `
1227
3953
  `);
1228
3954
  }
@@ -1286,13 +4012,13 @@ function orderedBlockOf(path) {
1286
4012
  return;
1287
4013
  }
1288
4014
  var startsWith = (path, prefix) => prefix.every((word, index) => path[index] === word);
1289
- function plan(desired, current, options) {
4015
+ function plan2(desired, current, options) {
1290
4016
  const lines = (text, verb) => text.split(`
1291
4017
  `).filter((line) => line.startsWith(`${verb} `));
1292
4018
  const wanted = lines(desired, "set");
1293
4019
  const deactivated = lines(current, "deactivate");
1294
4020
  const generated = desired.split(`
1295
- `).filter((line) => line.startsWith(GENERATED)).map((line) => wordsOf(line.slice(GENERATED.length)).map(bare));
4021
+ `).filter((line) => line.startsWith(GENERATED)).map((line) => wordsOf2(line.slice(GENERATED.length)).map(bare2));
1296
4022
  const printed = lines(current, "set").filter((line) => !DEVICE_FACTS.some((fact) => fact.test(line)));
1297
4023
  const isGenerated = (line) => generated.some((path) => keyOf2(line).length > path.length && startsWith(keyOf2(line), path));
1298
4024
  const existing = printed.filter((line) => !isGenerated(line));
@@ -1329,7 +4055,7 @@ function plan(desired, current, options) {
1329
4055
  rewritten.push(block);
1330
4056
  const section = sectionFor(block);
1331
4057
  if (existingHere.length)
1332
- section.remove.push(`delete ${wordsOf(existingHere[0]).slice(1, 1 + block.length).join(" ")}`);
4058
+ section.remove.push(`delete ${wordsOf2(existingHere[0]).slice(1, 1 + block.length).join(" ")}`);
1333
4059
  section.show.push(...existingHere.map((line) => `- ${line}`), ...wantedHere.map((line) => `+ ${line}`));
1334
4060
  section.add.push(...wantedHere);
1335
4061
  }
@@ -1351,22 +4077,22 @@ function plan(desired, current, options) {
1351
4077
  const hidden = path.indexOf("<secret>");
1352
4078
  if (hidden >= 0 && hidden < length && !NAMED_LISTS.has(path[hidden - 1]))
1353
4079
  length = hidden;
1354
- removed.push(wordsOf(line).slice(1, 1 + length));
4080
+ removed.push(wordsOf2(line).slice(1, 1 + length));
1355
4081
  }
1356
- const isInside = (path, other) => other.length < path.length && other.every((word, index) => bare(path[index]) === bare(word));
4082
+ const isInside = (path, other) => other.length < path.length && other.every((word, index) => bare2(path[index]) === bare2(word));
1357
4083
  const removals = removed.filter((path, index) => !removed.some((other) => isInside(path, other)) && removed.findIndex((other) => join(other) === join(path)) === index);
1358
- const isRemoved = (line) => removals.some((path) => startsWith(keyOf2(line), path.map(bare)));
4084
+ const isRemoved = (line) => removals.some((path) => startsWith(keyOf2(line), path.map(bare2)));
1359
4085
  for (const path of removals) {
1360
- const section = sectionFor(path.map(bare));
4086
+ const section = sectionFor(path.map(bare2));
1361
4087
  section.remove.push(`delete ${path.join(" ")}`);
1362
- const under = existing.filter((line) => startsWith(keyOf2(line), path.map(bare)));
4088
+ const under = existing.filter((line) => startsWith(keyOf2(line), path.map(bare2)));
1363
4089
  section.show.push(...under.map((line) => `- ${line}`));
1364
4090
  }
1365
4091
  for (const line of deactivated) {
1366
- const path = wordsOf(line).slice(1);
1367
- if (!wantedUnder(path.map(bare)).length)
4092
+ const path = wordsOf2(line).slice(1);
4093
+ if (!wantedUnder(path.map(bare2)).length)
1368
4094
  continue;
1369
- const section = sectionFor(path.map(bare));
4095
+ const section = sectionFor(path.map(bare2));
1370
4096
  section.activate.push(`activate ${path.join(" ")}`);
1371
4097
  section.show.push(`+ activate ${path.join(" ")}`);
1372
4098
  }
@@ -1389,7 +4115,7 @@ function plan(desired, current, options) {
1389
4115
  const steps = [...sections].filter(([, section]) => section.show.length).map(([title, section]) => ({ title, show: section.show, send: [...section.activate, ...section.remove, ...section.add] }));
1390
4116
  return { steps, problems: [] };
1391
4117
  }
1392
- async function login(device) {
4118
+ async function login2(device) {
1393
4119
  const user = device.users?.[device.connection.user];
1394
4120
  if (!user)
1395
4121
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -1400,7 +4126,7 @@ async function login(device) {
1400
4126
  password: resolve(user.password)
1401
4127
  });
1402
4128
  }
1403
- async function checkVersion(client, device) {
4129
+ async function checkVersion2(client, device) {
1404
4130
  const version = await exec(client, "show version | match Junos:");
1405
4131
  const release = /Junos: (\d+)\.(\S+)/.exec(version);
1406
4132
  if (!release)
@@ -1409,23 +4135,23 @@ async function checkVersion(client, device) {
1409
4135
  throw new Error(`${device.name} runs Junos ${release[1]}.${release[2]}; this adapter renders for Junos ${SUPPORTED_MAJOR}`);
1410
4136
  }
1411
4137
  }
1412
- async function read(device) {
1413
- const client = await login(device);
4138
+ async function read2(device) {
4139
+ const client = await login2(device);
1414
4140
  try {
1415
- await checkVersion(client, device);
4141
+ await checkVersion2(client, device);
1416
4142
  const output = await exec(client, "show configuration | display set | no-more", 180000);
1417
4143
  if (!/^set /m.test(output))
1418
4144
  throw new Error(`${device.name} returned no configuration`);
1419
- return clean(output);
4145
+ return clean2(output);
1420
4146
  } finally {
1421
4147
  client.end();
1422
4148
  }
1423
4149
  }
1424
- var PROMPT = /[>#%] $/;
4150
+ var PROMPT2 = /[>#%] $/;
1425
4151
  async function run(channel, line, timeout = 60000) {
1426
4152
  channel.write(`${line}
1427
4153
  `);
1428
- const output = await readUntil(channel, (text) => PROMPT.test(text.replace(/\r/g, "")), timeout, 300);
4154
+ const output = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), timeout, 300);
1429
4155
  return output.replace(/\r/g, "");
1430
4156
  }
1431
4157
  var EDIT_PROMPT = /\[edit[^\]\n]*\]\n[^\n]*# $/;
@@ -1436,7 +4162,7 @@ async function edit(channel, line, timeout = 60000) {
1436
4162
  return output.replace(/\r/g, "");
1437
4163
  }
1438
4164
  async function openCli(channel) {
1439
- const banner = await readUntil(channel, (text) => PROMPT.test(text.replace(/\r/g, "")), 20000, 500);
4165
+ const banner = await readUntil(channel, (text) => PROMPT2.test(text.replace(/\r/g, "")), 20000, 500);
1440
4166
  if (/% $/.test(banner.replace(/\r/g, "")))
1441
4167
  await run(channel, "cli");
1442
4168
  await run(channel, "set cli screen-length 0");
@@ -1444,18 +4170,18 @@ async function openCli(channel) {
1444
4170
  await run(channel, "set cli complete-on-space off");
1445
4171
  }
1446
4172
  async function named(channel, line) {
1447
- const words = wordsOf(line).slice(1);
4173
+ const words = wordsOf2(line).slice(1);
1448
4174
  const at = words.indexOf("<redacted>");
1449
4175
  if (at < 0)
1450
4176
  return [line];
1451
4177
  const shown = await edit(channel, `show ${words.slice(0, at - 1).join(" ")} | display set`);
1452
4178
  const inList = (statement) => statement[0] === "set" && words.slice(0, at).every((word, index) => statement[index + 1] === word);
1453
4179
  const names = new Set(shown.split(`
1454
- `).map(wordsOf).filter(inList).map((statement) => statement[at + 1]));
4180
+ `).map(wordsOf2).filter(inList).map((statement) => statement[at + 1]));
1455
4181
  return [...names].map((name) => `delete ${[...words.slice(0, at), name, ...words.slice(at + 1)].join(" ")}`);
1456
4182
  }
1457
- var REFUSED = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
1458
- async function configure(channel, plan, guard) {
4183
+ var REFUSED2 = /^terminal:\d+:\(\d+\) (?!warning)|^error: /m;
4184
+ async function configure2(channel, plan, guard) {
1459
4185
  const sent = plan.steps.flatMap((step) => step.send);
1460
4186
  const entered = await run(channel, "configure exclusive");
1461
4187
  if (!/Entering configuration mode/.test(entered) || /^error: /m.test(entered)) {
@@ -1486,7 +4212,7 @@ async function configure(channel, plan, guard) {
1486
4212
  `);
1487
4213
  const loaded = await edit(channel, "\x04", 300000);
1488
4214
  const refused = loaded.split(`
1489
- `).filter((line) => REFUSED.test(line));
4215
+ `).filter((line) => REFUSED2.test(line));
1490
4216
  if (refused.length)
1491
4217
  throw await discard(`the device refused the plan, nothing was applied:
1492
4218
  ${refused.join(`
@@ -1537,16 +4263,16 @@ async function configure(channel, plan, guard) {
1537
4263
  await run(channel, "exit configuration-mode");
1538
4264
  console.log(" logged in again, commit confirmed");
1539
4265
  }
1540
- async function apply(device, _network, plan, options) {
1541
- const client = await login(device);
4266
+ async function apply2(device, _network, plan, options) {
4267
+ const client = await login2(device);
1542
4268
  try {
1543
- await checkVersion(client, device);
4269
+ await checkVersion2(client, device);
1544
4270
  const channel = await shell(client);
1545
4271
  await openCli(channel);
1546
- await configure(channel, plan, {
4272
+ await configure2(channel, plan, {
1547
4273
  minutes: options.rollback,
1548
4274
  prove: async () => {
1549
- const proof = await login(device);
4275
+ const proof = await login2(device);
1550
4276
  proof.end();
1551
4277
  }
1552
4278
  });
@@ -1555,7 +4281,7 @@ async function apply(device, _network, plan, options) {
1555
4281
  client.end();
1556
4282
  }
1557
4283
  }
1558
- function unsupported(device) {
4284
+ function unsupported2(device) {
1559
4285
  const found = [];
1560
4286
  const names = new Names(device);
1561
4287
  const users = device.users ?? {};
@@ -1636,7 +4362,7 @@ function unsupported(device) {
1636
4362
  const acl = port.acl ? device.acls?.[port.acl] : undefined;
1637
4363
  if (acl?.some((rule) => rule.match?.vlan))
1638
4364
  found.push(`acls.${port.acl} matching a VLAN, on port ${name}`);
1639
- found.push(...routedGaps(`ports.${name}`, port));
4365
+ found.push(...routedGaps2(`ports.${name}`, port));
1640
4366
  found.push(...switchedGaps(`ports.${name}`, port, device));
1641
4367
  }
1642
4368
  const loopbackTables = new Set;
@@ -1658,7 +4384,7 @@ function unsupported(device) {
1658
4384
  if (iface.mtu > smallest)
1659
4385
  found.push(`interfaces.${name}.mtu ${iface.mtu}, above the ${smallest} its VLAN's ports carry`);
1660
4386
  }
1661
- found.push(...routedGaps(`interfaces.${name}`, iface));
4387
+ found.push(...routedGaps2(`interfaces.${name}`, iface));
1662
4388
  if (iface.type === "lag")
1663
4389
  found.push(...switchedGaps(`interfaces.${name}`, iface, device));
1664
4390
  if (iface.type === "loopback") {
@@ -1741,7 +4467,7 @@ function bgpGaps(device) {
1741
4467
  const found = [];
1742
4468
  const bgp = device.routing?.bgp;
1743
4469
  for (const [name, neighbor] of Object.entries(bgp?.neighbors ?? {})) {
1744
- const settings = settingsOf(device, neighbor);
4470
+ const settings = settingsOf2(device, neighbor);
1745
4471
  if (settings.role)
1746
4472
  found.push(`routing.bgp.neighbors.${name}.role, Junos 21 has no BGP roles`);
1747
4473
  if (familyOf2(neighbor.address) === "ipv6" && settings.families?.includes("ipv4")) {
@@ -1765,7 +4491,7 @@ function bgpGaps(device) {
1765
4491
  }
1766
4492
  return found;
1767
4493
  }
1768
- function routedGaps(where, settings) {
4494
+ function routedGaps2(where, settings) {
1769
4495
  const found = [];
1770
4496
  const addresses = (settings.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
1771
4497
  if (settings.addresses?.some((entry) => typeof entry !== "string"))
@@ -1795,13 +4521,13 @@ function fillPlan(set, prefixes) {
1795
4521
  const send = [`delete ${list}`, `set ${list}`, ...prefixes.map((prefix) => `set ${list} ${prefix}`)];
1796
4522
  return { steps: [{ title: list, show: [], send }], problems: [] };
1797
4523
  }
1798
- async function fill(device, _network, set, _family, prefixes) {
1799
- const client = await login(device);
4524
+ async function fill2(device, _network, set, _family, prefixes) {
4525
+ const client = await login2(device);
1800
4526
  try {
1801
- await checkVersion(client, device);
4527
+ await checkVersion2(client, device);
1802
4528
  const channel = await shell(client);
1803
4529
  await openCli(channel);
1804
- await configure(channel, fillPlan(set, prefixes));
4530
+ await configure2(channel, fillPlan(set, prefixes));
1805
4531
  channel.end();
1806
4532
  console.log(` ${set}: ${prefixes.length} prefixes on the device`);
1807
4533
  } finally {
@@ -1810,12 +4536,12 @@ async function fill(device, _network, set, _family, prefixes) {
1810
4536
  }
1811
4537
  var junos = {
1812
4538
  extension: ".conf",
1813
- unsupported,
1814
- render,
1815
- read,
1816
- plan,
1817
- apply,
1818
- fill
4539
+ unsupported: unsupported2,
4540
+ render: render2,
4541
+ read: read2,
4542
+ plan: plan2,
4543
+ apply: apply2,
4544
+ fill: fill2
1819
4545
  };
1820
4546
 
1821
4547
  // src/core/time.ts
@@ -1874,7 +4600,7 @@ function head(line) {
1874
4600
  const words = line.split(" ");
1875
4601
  return words.slice(0, Math.max(1, words.length - 1)).join(" ");
1876
4602
  }
1877
- function compare(desired, current, dialect) {
4603
+ function compare2(desired, current, dialect) {
1878
4604
  const canon = dialect.canon ?? ((line) => line);
1879
4605
  const keyOf = (entry) => [...entry.path, canon(entry.line)].join("\x00");
1880
4606
  const pathKey = (path) => path.join("\x00");
@@ -1959,7 +4685,7 @@ function steps(groups, dialect, around = nothingAround) {
1959
4685
  });
1960
4686
  }
1961
4687
  var STORED_CREDENTIAL = /(password (?:irreversible-)?cipher|community read cipher|^snmp-server community)( +)\S+/gm;
1962
- var scrub2 = (text) => text.replace(STORED_CREDENTIAL, "$1$2<redacted>");
4688
+ var scrub3 = (text) => text.replace(STORED_CREDENTIAL, "$1$2<redacted>");
1963
4689
 
1964
4690
  // src/adapters/devices/raisecom-ros/index.ts
1965
4691
  var DEVICE_FACTS2 = [
@@ -2046,7 +4772,7 @@ function vlanList(ids) {
2046
4772
  }
2047
4773
  return ranges.join(",");
2048
4774
  }
2049
- function render2(device) {
4775
+ function render3(device) {
2050
4776
  const lines = [];
2051
4777
  const vlans = device.vlans ?? {};
2052
4778
  const vlanId = (name) => vlans[name].id;
@@ -2236,7 +4962,7 @@ async function open(device) {
2236
4962
  await send(channel, "terminal page-break disable", 15000, 1000);
2237
4963
  return { channel, close };
2238
4964
  }
2239
- async function read2(device) {
4965
+ async function read3(device) {
2240
4966
  const { channel, close } = await open(device);
2241
4967
  try {
2242
4968
  const dump = await send(channel, "show running-config", 150000, 6000);
@@ -2248,13 +4974,13 @@ async function read2(device) {
2248
4974
  const body = lines.slice(start).filter((line) => line.startsWith("!") || !/[#>]\s*$/.test(line.trim()));
2249
4975
  const text = body.join(`
2250
4976
  `).replace(/^!(System (up )?time|Boot times):.*\n/gm, "").trim();
2251
- return scrub2(text + `
4977
+ return scrub3(text + `
2252
4978
  `).replace(/(authkey \S+|privkey\s+\S+) \S+/g, "$1 <redacted>");
2253
4979
  } finally {
2254
4980
  close();
2255
4981
  }
2256
4982
  }
2257
- function unsupported2(device) {
4983
+ function unsupported3(device) {
2258
4984
  const found = [];
2259
4985
  for (const [name, port] of Object.entries(device.ports ?? {})) {
2260
4986
  if (!port)
@@ -2369,10 +5095,10 @@ function unsupported2(device) {
2369
5095
  found.push(...switchUnsupported(device));
2370
5096
  return found;
2371
5097
  }
2372
- var REFUSED2 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
5098
+ var REFUSED3 = /^(%|Error|Incomplete|Unrecognized)|can not|cannot |is in use|Wrong parameter|invalid|unsuccessful/im;
2373
5099
  var CONFIRM = /^(portswitch|switchport trunk allowed vlan|switchport mode)/;
2374
5100
  var RELEASE = /^no (\S+ access-list \d+)$/;
2375
- async function apply2(device, _network, plan) {
5101
+ async function apply3(device, _network, plan) {
2376
5102
  const { channel, close } = await open(device);
2377
5103
  const released = [];
2378
5104
  try {
@@ -2383,7 +5109,7 @@ async function apply2(device, _network, plan) {
2383
5109
  const asks = /[[(]y\/n[\])]|input 'y'/i.test(answer) || CONFIRM.test(line) && !/set successfully/i.test(answer);
2384
5110
  if (asks)
2385
5111
  answer += await send(channel, "y", 1e4, 1000);
2386
- if (REFUSED2.test(answer))
5112
+ if (REFUSED3.test(answer))
2387
5113
  throw new Error(`${step.title}: ${line}
2388
5114
  device said: ${answer.trim().slice(0, 300)}`);
2389
5115
  const release = RELEASE.exec(line);
@@ -2431,8 +5157,8 @@ function createdIds(line) {
2431
5157
  }
2432
5158
  return ids;
2433
5159
  }
2434
- function plan2(desired, current) {
2435
- const groups = compare(desired, current, dialect);
5160
+ function plan3(desired, current) {
5161
+ const groups = compare2(desired, current, dialect);
2436
5162
  const existing = entries(current, dialect);
2437
5163
  for (const group of groups) {
2438
5164
  if (!group.path[0]?.startsWith("access-list "))
@@ -2481,7 +5207,7 @@ function plan2(desired, current) {
2481
5207
  }
2482
5208
  return { steps: planned, problems: [] };
2483
5209
  }
2484
- var raisecom = { extension: ".txt", unsupported: unsupported2, render: render2, read: read2, plan: plan2, apply: apply2 };
5210
+ var raisecom = { extension: ".txt", unsupported: unsupported3, render: render3, read: read3, plan: plan3, apply: apply3 };
2485
5211
 
2486
5212
  // src/adapters/devices/routeros/export.ts
2487
5213
  var unwrap = (text) => text.replace(/\r/g, "").replace(/\\\n\s*/g, "");
@@ -2548,7 +5274,7 @@ function parseLine(menu, line) {
2548
5274
  }
2549
5275
  return { menu, verb, selector, flags, attrs };
2550
5276
  }
2551
- function parse(text) {
5277
+ function parse2(text) {
2552
5278
  const commands = [];
2553
5279
  const generated = new Set;
2554
5280
  let menu = "";
@@ -2567,7 +5293,7 @@ function parse(text) {
2567
5293
  }
2568
5294
  return { commands, generated };
2569
5295
  }
2570
- var DEFAULTS = {
5296
+ var DEFAULTS2 = {
2571
5297
  "*": { disabled: "no", comment: "", instance: "default", "routing-table": "main", vrf: "main" },
2572
5298
  "/interface bridge": { "protocol-mode": "rstp", priority: "0x8000", "vlan-filtering": "no", pvid: "1" },
2573
5299
  "/interface bridge port": { pvid: "1", "frame-types": "admit-all", edge: "auto", "path-cost": "10", priority: "0x80" },
@@ -2637,12 +5363,12 @@ function defaultOf(command, property) {
2637
5363
  return SERVICE_DISABLED[command.selector];
2638
5364
  if (command.menu === "/ip firewall service-port" && property === "disabled")
2639
5365
  return HELPER_DISABLED[command.selector] ?? "no";
2640
- return DEFAULTS[command.menu]?.[property] ?? DEFAULTS["*"][property];
5366
+ return DEFAULTS2[command.menu]?.[property] ?? DEFAULTS2["*"][property];
2641
5367
  }
2642
5368
  var CLEARED_WITH = {
2643
5369
  "/interface ethernet": { speed: "auto-negotiation" }
2644
5370
  };
2645
- var SECRET_KEYS = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
5371
+ var SECRET_KEYS2 = new Set(["private-key", "password", "tcp-md5-key", "authentication-password", "encryption-password"]);
2646
5372
  var isSecretMarker = (value) => value.startsWith("<secret:");
2647
5373
  var canonicalRule = (rule) => rule.replace(/\s*([=&|;{}(),<>!]+)\s*/g, "$1").replace(/\s+/g, " ").trim();
2648
5374
  function normalValue(command, property, value) {
@@ -2663,7 +5389,7 @@ function normalValue(command, property, value) {
2663
5389
  function normal(command) {
2664
5390
  const result = {};
2665
5391
  for (const [property, raw] of Object.entries(command.attrs)) {
2666
- if (SECRET_KEYS.has(property) || isSecretMarker(raw))
5392
+ if (SECRET_KEYS2.has(property) || isSecretMarker(raw))
2667
5393
  continue;
2668
5394
  const value = normalValue(command, property, raw);
2669
5395
  if (value === defaultOf(command, property))
@@ -2708,7 +5434,7 @@ function identity(command) {
2708
5434
  }
2709
5435
  return result;
2710
5436
  }
2711
- var ORDERED = new Set([
5437
+ var ORDERED2 = new Set([
2712
5438
  "/routing filter rule",
2713
5439
  "/ip firewall filter",
2714
5440
  "/ipv6 firewall filter",
@@ -2721,7 +5447,7 @@ var ORDERED = new Set([
2721
5447
  "/interface bridge filter",
2722
5448
  "/interface ethernet switch rule"
2723
5449
  ]);
2724
- function canonical(command) {
5450
+ function canonical2(command) {
2725
5451
  const properties = Object.entries(normal(command)).sort(([a], [b]) => a.localeCompare(b)).map(([name, value]) => `${name}=${value}`);
2726
5452
  const flags = [...command.flags].sort();
2727
5453
  return [command.verb, command.selector, ...flags, ...properties].filter(Boolean).join(" ");
@@ -2798,9 +5524,9 @@ var DYNAMIC = new Set([
2798
5524
  "/ip dhcp-server lease",
2799
5525
  "/interface bridge vlan"
2800
5526
  ]);
2801
- var quote2 = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
2802
- var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw : quote2(raw);
2803
- var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw : quote2(raw);
5527
+ var quote3 = (value) => `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\$/g, "\\$")}"`;
5528
+ var value = (raw) => /^[\w.:/@,!-]+$/.test(raw) ? raw : quote3(raw);
5529
+ var findValue = (raw) => /^[\w.:-]+$/.test(raw) ? raw : quote3(raw);
2804
5530
  function find(menu, id) {
2805
5531
  const conditions = Object.entries(id).map(([property, raw]) => `${property}=${findValue(raw)}`);
2806
5532
  if (DYNAMIC.has(menu))
@@ -2844,7 +5570,7 @@ function changes(wanted, existing, sendSecrets, problems, label) {
2844
5570
  }
2845
5571
  if (sendSecrets) {
2846
5572
  for (const [property, raw] of Object.entries(wanted.attrs)) {
2847
- if (SECRET_KEYS.has(property) || isSecretMarker(raw))
5573
+ if (SECRET_KEYS2.has(property) || isSecretMarker(raw))
2848
5574
  result[property] = raw;
2849
5575
  }
2850
5576
  }
@@ -2886,13 +5612,13 @@ function planEntries(menu, wanted, existing, sendSecrets, problems) {
2886
5612
  kept.add(entryKey);
2887
5613
  const current = existingByKey.get(entryKey);
2888
5614
  if (!current) {
2889
- changeStep.show.push(`+ ${canonical(entry)}`);
5615
+ changeStep.show.push(`+ ${canonical2(entry)}`);
2890
5616
  changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
2891
5617
  continue;
2892
5618
  }
2893
5619
  const flagsDiffer = [...entry.flags].sort().join() !== [...current.flags].sort().join();
2894
5620
  if (flagsDiffer) {
2895
- changeStep.show.push(`~ replace ${canonical(entry)}`);
5621
+ changeStep.show.push(`~ replace ${canonical2(entry)}`);
2896
5622
  removeStep.send.push(`${menu} remove ${find(menu, identity(current))}`);
2897
5623
  changeStep.send.push(line(menu, "add", "", entry.flags, entry.attrs));
2898
5624
  continue;
@@ -2915,12 +5641,12 @@ function planEntries(menu, wanted, existing, sendSecrets, problems) {
2915
5641
  for (const list of wholeLists) {
2916
5642
  const count = removed.filter((entry) => entry.attrs.list === list).length;
2917
5643
  removeStep.show.push(`- list ${list}, ${count} entr${count === 1 ? "y" : "ies"}`);
2918
- removeStep.send.push(`${menu} remove [ find list=${quote2(list)} and !dynamic ]`);
5644
+ removeStep.send.push(`${menu} remove [ find list=${quote3(list)} and !dynamic ]`);
2919
5645
  }
2920
5646
  for (const entry of removed) {
2921
5647
  if (wholeLists.has(entry.attrs.list ?? ""))
2922
5648
  continue;
2923
- removeStep.show.push(`- ${canonical(entry)}`);
5649
+ removeStep.show.push(`- ${canonical2(entry)}`);
2924
5650
  removeStep.send.push(`${menu} remove ${find(menu, identity(entry))}`);
2925
5651
  }
2926
5652
  if (changeStep.send.length)
@@ -2940,26 +5666,26 @@ function planCertificates(wanted, existing) {
2940
5666
  for (const entry of wanted) {
2941
5667
  const name = entry.attrs.name;
2942
5668
  const current = existingByName.get(name);
2943
- if (current && canonical(current) === canonical(entry))
5669
+ if (current && canonical2(current) === canonical2(entry))
2944
5670
  continue;
2945
5671
  const sameCertificate = existing.find((item) => !wantedNames.has(item.attrs.name) && !renamed.has(item.attrs.name) && item.attrs.fingerprint === entry.attrs.fingerprint && item.attrs["private-key"] === entry.attrs["private-key"]);
2946
5672
  if (!current && sameCertificate) {
2947
5673
  const oldName = sameCertificate.attrs.name;
2948
5674
  renamed.add(oldName);
2949
5675
  install.show.push(`~ rename ${oldName} to ${name}`);
2950
- install.send.push(`${menu} set [ find name=${quote2(oldName)} ] name=${quote2(name)}`);
5676
+ install.send.push(`${menu} set [ find name=${quote3(oldName)} ] name=${quote3(name)}`);
2951
5677
  continue;
2952
5678
  }
2953
5679
  install.show.push(`${current ? "~" : "+"} install ${name}`);
2954
5680
  if (current)
2955
- install.send.push(`${menu} remove [ find name=${quote2(name)} ]`);
5681
+ install.send.push(`${menu} remove [ find name=${quote3(name)} ]`);
2956
5682
  install.send.push(INSTALL_CERTIFICATE + name);
2957
5683
  }
2958
5684
  for (const entry of existing) {
2959
5685
  if (wantedNames.has(entry.attrs.name) || renamed.has(entry.attrs.name))
2960
5686
  continue;
2961
5687
  remove.show.push(`- ${entry.attrs.name}`);
2962
- remove.send.push(`${menu} remove [ find name=${quote2(entry.attrs.name)} ]`);
5688
+ remove.send.push(`${menu} remove [ find name=${quote3(entry.attrs.name)} ]`);
2963
5689
  }
2964
5690
  if (install.send.length)
2965
5691
  result.first.push(install);
@@ -2970,8 +5696,8 @@ function planCertificates(wanted, existing) {
2970
5696
  var OLD_RULES = "frameworkOldRules";
2971
5697
  function planOrdered(menu, wanted, existing) {
2972
5698
  const result = { first: [], last: [] };
2973
- const same = wanted.map((item) => canonical(item)).join(`
2974
- `) === existing.map((item) => canonical(item)).join(`
5699
+ const same = wanted.map((item) => canonical2(item)).join(`
5700
+ `) === existing.map((item) => canonical2(item)).join(`
2975
5701
  `);
2976
5702
  if (same)
2977
5703
  return result;
@@ -2981,7 +5707,7 @@ function planOrdered(menu, wanted, existing) {
2981
5707
  const droppedChains = [...new Set(existing.map((item) => item.attrs.chain ?? ""))].filter((chain) => !keptChains.has(chain));
2982
5708
  let removeOld = `${menu} remove $${OLD_RULES}`;
2983
5709
  if (hasChains) {
2984
- const keep = [...keptChains].map((chain) => quote2(chain)).join(";");
5710
+ const keep = [...keptChains].map((chain) => quote3(chain)).join(";");
2985
5711
  const inKeptChain = `[:typeof [:find $keep [${menu} get $id chain]]] != "nil"`;
2986
5712
  removeOld = `:local keep {${keep}}; :foreach id in=$${OLD_RULES} do={ :if (${inKeptChain}) do={ ${menu} remove $id } }`;
2987
5713
  }
@@ -2990,19 +5716,19 @@ function planOrdered(menu, wanted, existing) {
2990
5716
  ...wanted.map((item) => line(menu, "add", "", item.flags, item.attrs)),
2991
5717
  `:global ${OLD_RULES}; ${removeOld}; /system script environment remove [find name="${OLD_RULES}"]`
2992
5718
  ];
2993
- const show = [...existing.map((item) => `- ${canonical(item)}`), ...wanted.map((item) => `+ ${canonical(item)}`)];
5719
+ const show = [...existing.map((item) => `- ${canonical2(item)}`), ...wanted.map((item) => `+ ${canonical2(item)}`)];
2994
5720
  result.first.push({ title: `${menu}, in the order of the config`, show, send });
2995
5721
  if (hasChains) {
2996
5722
  for (const chain of droppedChains) {
2997
- const selector = `[ find chain=${quote2(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
5723
+ const selector = `[ find chain=${quote3(chain)}${DYNAMIC.has(menu) ? " and !dynamic" : ""} ]`;
2998
5724
  result.last.push({ title: `${menu} chain ${chain}`, show: [`- chain ${chain}`], send: [`${menu} remove ${selector}`] });
2999
5725
  }
3000
5726
  }
3001
5727
  return result;
3002
5728
  }
3003
- function plan3(desired, current, options) {
3004
- const want = parse(desired);
3005
- const have = parse(current);
5729
+ function plan4(desired, current, options) {
5730
+ const want = parse2(desired);
5731
+ const have = parse2(current);
3006
5732
  const owned = (command) => !(command.menu.endsWith("address-list") && want.generated.has(command.attrs.list ?? ""));
3007
5733
  const wantedByMenu = byMenu(want.commands.filter(owned));
3008
5734
  const existingByMenu = byMenu(have.commands.filter(owned));
@@ -3016,7 +5742,7 @@ function plan3(desired, current, options) {
3016
5742
  let menuPlan;
3017
5743
  if (menu === "/certificate")
3018
5744
  menuPlan = planCertificates(wanted, existing);
3019
- else if (ORDERED.has(menu))
5745
+ else if (ORDERED2.has(menu))
3020
5746
  menuPlan = planOrdered(menu, wanted, existing);
3021
5747
  else {
3022
5748
  menuPlan = planEntries(menu, wanted, existing, options.secrets, problems);
@@ -3047,7 +5773,7 @@ class Output {
3047
5773
  `;
3048
5774
  }
3049
5775
  }
3050
- function quote3(value) {
5776
+ function quote4(value) {
3051
5777
  const plain = /^[\w.:/@-]+$/.test(value);
3052
5778
  if (plain)
3053
5779
  return value;
@@ -3059,7 +5785,7 @@ function command(verb, properties) {
3059
5785
  for (const [name, value] of Object.entries(properties)) {
3060
5786
  if (value === undefined || value === false)
3061
5787
  continue;
3062
- const written = typeof value === "number" ? String(value) : quote3(value);
5788
+ const written = typeof value === "number" ? String(value) : quote4(value);
3063
5789
  words.push(`${name}=${written}`);
3064
5790
  }
3065
5791
  return words.join(" ");
@@ -3067,7 +5793,7 @@ function command(verb, properties) {
3067
5793
  var yesNo = (on) => on ? "yes" : "no";
3068
5794
  var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
3069
5795
  var eachPort2 = (ports) => [ports].flat().map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
3070
- function portList(ports) {
5796
+ function portList2(ports) {
3071
5797
  if (ports === undefined)
3072
5798
  return;
3073
5799
  return eachPort2(ports).join(",");
@@ -3443,7 +6169,7 @@ var NETWORKS = { ipv4: "bgp-networks-v4", ipv6: "bgp-networks-v6" };
3443
6169
  function renderAddressing(out, ctx) {
3444
6170
  addresses(out, ctx);
3445
6171
  dhcp(out, ctx);
3446
- dns(out, ctx);
6172
+ dns2(out, ctx);
3447
6173
  addressLists(out, ctx);
3448
6174
  }
3449
6175
  function addresses(out, ctx) {
@@ -3501,7 +6227,7 @@ function dhcp(out, ctx) {
3501
6227
  out.add("/ip dhcp-relay", command("add", { "dhcp-server": relay.servers.join(","), interface: ctx.name(relay.interface), name }));
3502
6228
  }
3503
6229
  }
3504
- function dns(out, ctx) {
6230
+ function dns2(out, ctx) {
3505
6231
  const dns = ctx.device.system?.dns;
3506
6232
  out.add("/ip dns", command("set", { "allow-remote-requests": yesNo(dns?.serve), servers: dns?.servers.join(",") ?? "" }));
3507
6233
  }
@@ -3531,7 +6257,7 @@ function addressLists(out, ctx) {
3531
6257
  }
3532
6258
  }
3533
6259
  }
3534
- var HELPERS = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"];
6260
+ var HELPERS2 = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"];
3535
6261
  var CHAINS = ["input", "forward", "output"];
3536
6262
  function renderFirewall(out, ctx) {
3537
6263
  for (const family of ["ipv4", "ipv6"]) {
@@ -3539,7 +6265,7 @@ function renderFirewall(out, ctx) {
3539
6265
  filter(out, ctx, family);
3540
6266
  nat(out, ctx, family);
3541
6267
  }
3542
- helpers(out, ctx);
6268
+ helpers2(out, ctx);
3543
6269
  switchAcls(out, ctx);
3544
6270
  }
3545
6271
  function untracked(out, ctx, family) {
@@ -3575,13 +6301,13 @@ function matchProperties(ctx, match, family) {
3575
6301
  "connection-state": match?.state?.join(","),
3576
6302
  "dst-address": match?.dst,
3577
6303
  "dst-address-list": match?.dst_set,
3578
- "dst-port": portList(match?.dst_port),
6304
+ "dst-port": portList2(match?.dst_port),
3579
6305
  "in-interface": match?.in_interface && ctx.name(match.in_interface),
3580
6306
  "out-interface": match?.out_interface && ctx.name(match.out_interface),
3581
6307
  protocol,
3582
6308
  "src-address": match?.src,
3583
6309
  "src-address-list": match?.src_set,
3584
- "src-port": portList(match?.src_port),
6310
+ "src-port": portList2(match?.src_port),
3585
6311
  "tcp-flags": flags
3586
6312
  };
3587
6313
  }
@@ -3623,9 +6349,9 @@ function nat(out, ctx, family) {
3623
6349
  }
3624
6350
  }
3625
6351
  }
3626
- function helpers(out, ctx) {
6352
+ function helpers2(out, ctx) {
3627
6353
  const enabled = ctx.device.firewall?.helpers ?? [];
3628
- for (const helper of HELPERS) {
6354
+ for (const helper of HELPERS2) {
3629
6355
  out.add("/ip firewall service-port", command(`set ${helper}`, { disabled: yesNo(!enabled.includes(helper)) }));
3630
6356
  }
3631
6357
  }
@@ -3660,7 +6386,7 @@ function switchAcls(out, ctx) {
3660
6386
  }
3661
6387
  }
3662
6388
  var RPKI_GROUP2 = "rpki";
3663
- var REJECT_ALL2 = "REJECT-ALL";
6389
+ var REJECT_ALL3 = "REJECT-ALL";
3664
6390
  var ROLES = {
3665
6391
  provider: "ebgp-customer",
3666
6392
  customer: "ebgp-provider",
@@ -3669,17 +6395,17 @@ var ROLES = {
3669
6395
  "rs-client": "ebgp-rs"
3670
6396
  };
3671
6397
  function renderRouting(out, ctx) {
3672
- staticRoutes2(out, ctx);
6398
+ staticRoutes3(out, ctx);
3673
6399
  rpki2(out, ctx);
3674
6400
  if (ctx.device.routing?.bgp) {
3675
6401
  bgpInstance(out, ctx);
3676
6402
  policies(out, ctx);
3677
6403
  bgpConnections(out, ctx);
3678
6404
  }
3679
- ospf2(out, ctx);
6405
+ ospf3(out, ctx);
3680
6406
  bfd(out, ctx);
3681
6407
  }
3682
- function staticRoutes2(out, ctx) {
6408
+ function staticRoutes3(out, ctx) {
3683
6409
  for (const route of ctx.device.routing?.static ?? []) {
3684
6410
  const menu = familyOf2(host2(route.prefix)) === "ipv4" ? "/ip route" : "/ipv6 route";
3685
6411
  const verb = route.blackhole ? "add blackhole" : "add";
@@ -3707,12 +6433,12 @@ function bgpInstance(out, ctx) {
3707
6433
  const bgp = ctx.device.routing.bgp;
3708
6434
  out.add("/routing bgp instance", command("add", { as: bgp.asn, name: "default", "router-id": ctx.device.routing?.router_id }));
3709
6435
  }
3710
- function settingsOf2(ctx, neighbor) {
6436
+ function settingsOf3(ctx, neighbor) {
3711
6437
  const group = neighbor.group ? ctx.device.routing?.bgp?.groups?.[neighbor.group] : undefined;
3712
6438
  return { ...group, ...neighbor };
3713
6439
  }
3714
- function familiesOf(ctx, neighbor) {
3715
- return settingsOf2(ctx, neighbor).families ?? [familyOf2(neighbor.address)];
6440
+ function familiesOf2(ctx, neighbor) {
6441
+ return settingsOf3(ctx, neighbor).families ?? [familyOf2(neighbor.address)];
3716
6442
  }
3717
6443
  var chainName = (policy, family) => `${policy}-${familySuffix(family)}`;
3718
6444
  function appliesTo(ctx, rule, family) {
@@ -3817,8 +6543,8 @@ function policies(out, ctx) {
3817
6543
  };
3818
6544
  let rejectAllNeeded = false;
3819
6545
  for (const neighbor of Object.values(bgp.neighbors ?? {})) {
3820
- const settings = settingsOf2(ctx, neighbor);
3821
- for (const family of familiesOf(ctx, neighbor)) {
6546
+ const settings = settingsOf3(ctx, neighbor);
6547
+ for (const family of familiesOf2(ctx, neighbor)) {
3822
6548
  for (const policy of [settings.import, settings.export]) {
3823
6549
  if (policy)
3824
6550
  want(policy, family);
@@ -3848,7 +6574,7 @@ function policies(out, ctx) {
3848
6574
  if (rejectAllNeeded) {
3849
6575
  for (const family of ["ipv4", "ipv6"])
3850
6576
  out.add("/routing filter rule", command("add", {
3851
- chain: chainName(REJECT_ALL2, family),
6577
+ chain: chainName(REJECT_ALL3, family),
3852
6578
  rule: "reject"
3853
6579
  }));
3854
6580
  }
@@ -3856,8 +6582,8 @@ function policies(out, ctx) {
3856
6582
  function bgpConnections(out, ctx) {
3857
6583
  const bgp = ctx.device.routing.bgp;
3858
6584
  for (const [name, neighbor] of Object.entries(bgp.neighbors ?? {})) {
3859
- const settings = settingsOf2(ctx, neighbor);
3860
- const [family] = familiesOf(ctx, neighbor);
6585
+ const settings = settingsOf3(ctx, neighbor);
6586
+ const [family] = familiesOf2(ctx, neighbor);
3861
6587
  const addressFamily = familyOf2(neighbor.address);
3862
6588
  const internal = settings.remote_as === bgp.asn;
3863
6589
  const originates = bgp.networks?.some((prefix) => familyOf2(host2(prefix)) === family);
@@ -3867,7 +6593,7 @@ function bgpConnections(out, ctx) {
3867
6593
  as: bgp.asn,
3868
6594
  connect: settings.passive ? "no" : undefined,
3869
6595
  "hold-time": settings.hold_time ? duration(settings.hold_time) : undefined,
3870
- "input.filter": chainName(settings.import ?? REJECT_ALL2, family),
6596
+ "input.filter": chainName(settings.import ?? REJECT_ALL3, family),
3871
6597
  instance: "default",
3872
6598
  [`input.limit-process-routes-${family}`]: settings.max_prefixes,
3873
6599
  "keepalive-time": settings.keepalive ? duration(settings.keepalive) : undefined,
@@ -3876,7 +6602,7 @@ function bgpConnections(out, ctx) {
3876
6602
  "local.role": role,
3877
6603
  multihop: settings.multihop ? "yes" : undefined,
3878
6604
  name,
3879
- "output.filter-chain": chainName(settings.export ?? REJECT_ALL2, family),
6605
+ "output.filter-chain": chainName(settings.export ?? REJECT_ALL3, family),
3880
6606
  "output.network": originates ? NETWORKS[family] : undefined,
3881
6607
  "remote.address": `${neighbor.address}/${addressFamily === "ipv4" ? 32 : 128}`,
3882
6608
  "remote.as": settings.remote_as,
@@ -3885,7 +6611,7 @@ function bgpConnections(out, ctx) {
3885
6611
  }));
3886
6612
  }
3887
6613
  }
3888
- function ospf2(out, ctx) {
6614
+ function ospf3(out, ctx) {
3889
6615
  const ospf = ctx.device.routing?.ospf;
3890
6616
  if (!ospf)
3891
6617
  return;
@@ -3916,12 +6642,12 @@ function ospf2(out, ctx) {
3916
6642
  }
3917
6643
  function bfd(out, ctx) {
3918
6644
  const routing = ctx.device.routing;
3919
- const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf2(ctx, neighbor).bfd);
6645
+ const bgpWants = Object.values(routing?.bgp?.neighbors ?? {}).some((neighbor) => settingsOf3(ctx, neighbor).bfd);
3920
6646
  const ospfWants = Object.values(routing?.ospf?.areas ?? {}).some((area) => Object.values(area.interfaces).some((iface) => iface?.bfd));
3921
6647
  if (bgpWants || ospfWants)
3922
6648
  out.add("/routing bfd configuration", command("add", { interfaces: "all" }));
3923
6649
  }
3924
- var SERVICES = {
6650
+ var SERVICES2 = {
3925
6651
  ftp: "ftp",
3926
6652
  ssh: "ssh",
3927
6653
  telnet: "telnet",
@@ -3937,13 +6663,13 @@ var SNMP_PRIVACY2 = { aes128: "AES", des: "DES" };
3937
6663
  function renderManagement(out, ctx) {
3938
6664
  services2(out, ctx);
3939
6665
  layerTwo(out, ctx);
3940
- snmp2(out, ctx);
3941
- users2(out, ctx);
3942
- certificates(out, ctx);
6666
+ snmp3(out, ctx);
6667
+ users3(out, ctx);
6668
+ certificates2(out, ctx);
3943
6669
  }
3944
6670
  function services2(out, ctx) {
3945
6671
  const management = ctx.device.management ?? {};
3946
- for (const [neutral, platform] of Object.entries(SERVICES)) {
6672
+ for (const [neutral, platform] of Object.entries(SERVICES2)) {
3947
6673
  const service = management[neutral];
3948
6674
  if (!service) {
3949
6675
  out.add("/ip service", command(`set ${platform}`, { disabled: "yes" }));
@@ -3973,7 +6699,7 @@ function layerTwo(out, ctx) {
3973
6699
  discovery = "all";
3974
6700
  out.add("/ip neighbor discovery-settings", command("set", { "discover-interface-list": discovery, protocol: "lldp" }));
3975
6701
  }
3976
- function snmp2(out, ctx) {
6702
+ function snmp3(out, ctx) {
3977
6703
  const management = ctx.device.management ?? {};
3978
6704
  const snmp = management.snmp;
3979
6705
  if (!snmp) {
@@ -3999,7 +6725,7 @@ function snmp2(out, ctx) {
3999
6725
  }));
4000
6726
  }
4001
6727
  }
4002
- function users2(out, ctx) {
6728
+ function users3(out, ctx) {
4003
6729
  for (const [name, user] of Object.entries(ctx.device.users ?? {})) {
4004
6730
  out.add("/user", command("add", {
4005
6731
  comment: user.description,
@@ -4011,7 +6737,7 @@ function users2(out, ctx) {
4011
6737
  out.add("/user ssh-keys", command("add", { key, user: name }));
4012
6738
  }
4013
6739
  }
4014
- function certificates(out, ctx) {
6740
+ function certificates2(out, ctx) {
4015
6741
  for (const [name, certificate] of Object.entries(ctx.device.certificates ?? {})) {
4016
6742
  out.add("/certificate", command("add", {
4017
6743
  fingerprint: fingerprint(certificate.certificate),
@@ -4048,12 +6774,12 @@ function renderSystem(out, ctx) {
4048
6774
  out.add("/system ntp server", command("set", { enabled: yesNo(system.ntp?.serve) }));
4049
6775
  for (const server of system.ntp?.servers ?? [])
4050
6776
  out.add("/system ntp client servers", command("add", { address: server }));
4051
- logging(out, ctx);
6777
+ logging2(out, ctx);
4052
6778
  out.add("/system package update", command("set", { channel: system.release_channel ?? "stable" }));
4053
6779
  out.add("/system routerboard settings", command("set", { "enter-setup-on": "delete-key" }));
4054
- flowExport(out, ctx);
6780
+ flowExport2(out, ctx);
4055
6781
  }
4056
- function logging(out, ctx) {
6782
+ function logging2(out, ctx) {
4057
6783
  const logging = ctx.device.system?.logging;
4058
6784
  const local = logging?.local;
4059
6785
  out.add("/system logging action", command("set 1", {
@@ -4074,7 +6800,7 @@ function logging(out, ctx) {
4074
6800
  out.add("/system logging", command("add", { action, topics: topic }));
4075
6801
  });
4076
6802
  }
4077
- function flowExport(out, ctx) {
6803
+ function flowExport2(out, ctx) {
4078
6804
  const flow = ctx.device.flow_export;
4079
6805
  if (!flow) {
4080
6806
  out.add("/ip traffic-flow", command("set", { enabled: "no" }));
@@ -4094,7 +6820,7 @@ function flowExport(out, ctx) {
4094
6820
  out.add("/ip traffic-flow target", command("add", { "dst-address": collector.address, port: collector.port ?? 2055, version }));
4095
6821
  }
4096
6822
  }
4097
- function render3(device) {
6823
+ function render4(device) {
4098
6824
  const out = new Output;
4099
6825
  const ctx = new Context(device);
4100
6826
  renderInterfaces(out, ctx);
@@ -4108,7 +6834,7 @@ function render3(device) {
4108
6834
 
4109
6835
  // src/adapters/devices/routeros/index.ts
4110
6836
  var SUPPORTED_MAJOR2 = 7;
4111
- async function login2(device) {
6837
+ async function login3(device) {
4112
6838
  const user = device.users?.[device.connection.user];
4113
6839
  if (!user)
4114
6840
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -4119,7 +6845,7 @@ async function login2(device) {
4119
6845
  password: resolve(user.password)
4120
6846
  });
4121
6847
  }
4122
- async function checkVersion2(client, device) {
6848
+ async function checkVersion3(client, device) {
4123
6849
  const version = (await exec(client, ":put [/system resource get version]")).trim();
4124
6850
  const major = Number(version.split(".")[0]);
4125
6851
  if (major !== SUPPORTED_MAJOR2) {
@@ -4135,10 +6861,10 @@ var LIST_CERTIFICATES = [
4135
6861
  ':put ("add fingerprint=" . [/certificate get $c fingerprint] . " name=" . [/certificate get $c name] . " private-key=" . $key)',
4136
6862
  "}"
4137
6863
  ].join(" ");
4138
- async function read3(device) {
4139
- const client = await login2(device);
6864
+ async function read4(device) {
6865
+ const client = await login3(device);
4140
6866
  try {
4141
- await checkVersion2(client, device);
6867
+ await checkVersion3(client, device);
4142
6868
  const config = await exec(client, "/export", 300000);
4143
6869
  const users = await exec(client, "/user export");
4144
6870
  const certificates = (await exec(client, LIST_CERTIFICATES)).trim();
@@ -4159,7 +6885,7 @@ ${certificates}` : "";
4159
6885
  client.end();
4160
6886
  }
4161
6887
  }
4162
- function unsupported3(device) {
6888
+ function unsupported4(device) {
4163
6889
  const found = [];
4164
6890
  for (const [name, port] of Object.entries(device.ports ?? {})) {
4165
6891
  if (!port)
@@ -4233,9 +6959,9 @@ async function installCertificate(client, device, name) {
4233
6959
  }
4234
6960
  await exec(client, `/file remove [find name="${name}.crt" or name="${name}.key"]`);
4235
6961
  }
4236
- async function apply3(device, _network, changes, options) {
4237
- const client = await login2(device);
4238
- await checkVersion2(client, device);
6962
+ async function apply4(device, _network, changes, options) {
6963
+ const client = await login3(device);
6964
+ await checkVersion3(client, device);
4239
6965
  const commands = changes.steps.flatMap((step) => step.send.map((command) => ({ title: step.title, command })));
4240
6966
  for (const { title, command } of commands) {
4241
6967
  if (command.startsWith(INSTALL_CERTIFICATE))
@@ -4281,7 +7007,7 @@ async function apply3(device, _network, changes, options) {
4281
7007
  throw new Error("apply failed and the backup is being restored");
4282
7008
  }
4283
7009
  client.end();
4284
- const proof = await login2(device).catch(() => {
7010
+ const proof = await login3(device).catch(() => {
4285
7011
  return;
4286
7012
  });
4287
7013
  if (!proof)
@@ -4291,10 +7017,10 @@ async function apply3(device, _network, changes, options) {
4291
7017
  proof.end();
4292
7018
  console.log(" logged in again, rollback disarmed");
4293
7019
  }
4294
- async function fill2(device, _network, set, family, prefixes) {
7020
+ async function fill3(device, _network, set, family, prefixes) {
4295
7021
  const menu = family === "ipv4" ? "/ip firewall address-list" : "/ipv6 firewall address-list";
4296
7022
  const batchSize = 60;
4297
- const client = await login2(device);
7023
+ const client = await login3(device);
4298
7024
  try {
4299
7025
  await exec(client, `${menu} remove [find list="${set}" and !dynamic]`);
4300
7026
  for (let start = 0;start < prefixes.length; start += batchSize) {
@@ -4311,12 +7037,12 @@ async function fill2(device, _network, set, family, prefixes) {
4311
7037
  }
4312
7038
  var routeros = {
4313
7039
  extension: ".rsc",
4314
- unsupported: unsupported3,
4315
- render: render3,
4316
- read: read3,
4317
- plan: plan3,
4318
- apply: apply3,
4319
- fill: fill2
7040
+ unsupported: unsupported4,
7041
+ render: render4,
7042
+ read: read4,
7043
+ plan: plan4,
7044
+ apply: apply4,
7045
+ fill: fill3
4320
7046
  };
4321
7047
 
4322
7048
  // src/adapters/devices/vrp/index.ts
@@ -4435,7 +7161,7 @@ class Config {
4435
7161
  `;
4436
7162
  }
4437
7163
  }
4438
- function render4(device) {
7164
+ function render5(device) {
4439
7165
  const config = new Config;
4440
7166
  const vlans = device.vlans ?? {};
4441
7167
  const vlanId = (name) => vlans[name].id;
@@ -4452,7 +7178,7 @@ function render4(device) {
4452
7178
  system2(config, device);
4453
7179
  vlanBlocks(config, device);
4454
7180
  managementAcl(config, device);
4455
- users3(config, device);
7181
+ users4(config, device);
4456
7182
  vrfs3(config, device);
4457
7183
  for (const [name, lag] of Object.entries(interfaces)) {
4458
7184
  if (lag.type === "lag")
@@ -4466,16 +7192,16 @@ function render4(device) {
4466
7192
  const settings = device.ports?.[port];
4467
7193
  config.block(`interface ${nameOf(port)}`, physicalPort2(device, port, settings, vlanId, lagNumbers));
4468
7194
  }
4469
- staticRoutes3(config, device, nameOf);
4470
- ospf3(config, device, nameOf);
7195
+ staticRoutes4(config, device, nameOf);
7196
+ ospf4(config, device, nameOf);
4471
7197
  if (wantsBfd(device))
4472
7198
  config.block("bfd", []);
4473
7199
  if (device.dhcp_relay && Object.keys(device.dhcp_relay).length) {
4474
7200
  config.line("dhcp enable");
4475
7201
  config.separator();
4476
7202
  }
4477
- flowExport2(config, device);
4478
- snmp3(config, device);
7203
+ flowExport3(config, device);
7204
+ snmp4(config, device);
4479
7205
  ssh(config, device);
4480
7206
  userInterfaces(config, device);
4481
7207
  return config.text();
@@ -4535,7 +7261,7 @@ function managementAcl(config, device) {
4535
7261
  const rules = allow.map((cidr, index) => `rule ${(index + 1) * 5} permit source ${host2(cidr)} ${wildcard(split(cidr).len)}`);
4536
7262
  config.block(`acl number ${MANAGEMENT_ACL2}`, ["description Management access", ...rules, `rule ${(allow.length + 1) * 5} deny`]);
4537
7263
  }
4538
- function users3(config, device) {
7264
+ function users4(config, device) {
4539
7265
  const lines = [];
4540
7266
  for (const [name, user] of Object.entries(device.users ?? {})) {
4541
7267
  lines.push(`local-user ${name} password irreversible-cipher ${marker(user.password)}`);
@@ -4660,7 +7386,7 @@ function physicalPort2(device, port, settings, vlanId, lagNumbers) {
4660
7386
  }
4661
7387
  return lines;
4662
7388
  }
4663
- function staticRoutes3(config, device, nameOf) {
7389
+ function staticRoutes4(config, device, nameOf) {
4664
7390
  for (const route of device.routing?.static ?? []) {
4665
7391
  const { addr, len } = split(route.prefix);
4666
7392
  const nextHop = route.blackhole ? "NULL0" : route.via ?? nameOf(route.interface);
@@ -4674,7 +7400,7 @@ function staticRoutes3(config, device, nameOf) {
4674
7400
  }
4675
7401
  config.separator();
4676
7402
  }
4677
- function ospf3(config, device, nameOf) {
7403
+ function ospf4(config, device, nameOf) {
4678
7404
  const ospf = device.routing?.ospf;
4679
7405
  if (!ospf)
4680
7406
  return;
@@ -4698,7 +7424,7 @@ function wantsBfd(device) {
4698
7424
  const areas = Object.values(device.routing?.ospf?.areas ?? {});
4699
7425
  return areas.some((area) => Object.values(area.interfaces).some((settings) => settings?.bfd));
4700
7426
  }
4701
- function flowExport2(config, device) {
7427
+ function flowExport3(config, device) {
4702
7428
  const flow = device.flow_export;
4703
7429
  if (flow?.protocol !== "sflow")
4704
7430
  return;
@@ -4709,7 +7435,7 @@ function flowExport2(config, device) {
4709
7435
  });
4710
7436
  config.separator();
4711
7437
  }
4712
- function snmp3(config, device) {
7438
+ function snmp4(config, device) {
4713
7439
  const snmp = device.management?.snmp;
4714
7440
  if (!snmp)
4715
7441
  return;
@@ -4763,7 +7489,7 @@ function userInterfaces(config, device) {
4763
7489
  config.block("user-interface vty 0 4", ["authentication-mode aaa", `protocol inbound ${device.management?.telnet ? "all" : "ssh"}`]);
4764
7490
  }
4765
7491
  var HOST_KEYS2 = ["ssh-rsa"];
4766
- async function login3(device) {
7492
+ async function login4(device) {
4767
7493
  const user = device.users?.[device.connection.user];
4768
7494
  if (!user)
4769
7495
  throw new Error(`${device.name}: connection user ${device.connection.user} is not one of its users`);
@@ -4783,8 +7509,8 @@ async function openShell(client) {
4783
7509
  await send(channel, "screen-length 0 temporary", 1e4, 1500);
4784
7510
  return channel;
4785
7511
  }
4786
- async function read4(device) {
4787
- const client = await login3(device);
7512
+ async function read5(device) {
7513
+ const client = await login4(device);
4788
7514
  try {
4789
7515
  const channel = await openShell(client);
4790
7516
  const dump = await send(channel, "display current-configuration", 120000, 3000);
@@ -4795,14 +7521,14 @@ async function read4(device) {
4795
7521
  if (start < 0)
4796
7522
  throw new Error(`${device.name} returned no configuration`);
4797
7523
  const body = lines.slice(start).filter((line) => !/^<.*>$/.test(line.trim()) && !line.startsWith("!Last configuration"));
4798
- return scrub2(body.join(`
7524
+ return scrub3(body.join(`
4799
7525
  `).trim() + `
4800
7526
  `);
4801
7527
  } finally {
4802
7528
  client.end();
4803
7529
  }
4804
7530
  }
4805
- function unsupported4(device) {
7531
+ function unsupported5(device) {
4806
7532
  const found = [];
4807
7533
  for (const [name, port] of Object.entries(device.ports ?? {})) {
4808
7534
  if (!port)
@@ -4881,7 +7607,7 @@ function unsupported4(device) {
4881
7607
  }
4882
7608
  return found;
4883
7609
  }
4884
- var REFUSED3 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
7610
+ var REFUSED4 = /^\s*Error|Unrecognized command|Incomplete command|Wrong parameter|Too many parameters/m;
4885
7611
  var INTERACTIVE = /^(snmp-agent usm-user v3 \S+ (?:authentication|privacy)-mode \S+) cipher <secret:([A-Za-z0-9_./:-]+)>$/;
4886
7612
  async function sendLine(channel, line) {
4887
7613
  const interactive = INTERACTIVE.exec(line.trim());
@@ -4898,21 +7624,21 @@ async function sendLine(channel, line) {
4898
7624
  answer += await send(channel, "Y", 20000, 800);
4899
7625
  return answer;
4900
7626
  }
4901
- async function apply4(device, _network, plan) {
4902
- const client = await login3(device);
7627
+ async function apply5(device, _network, plan) {
7628
+ const client = await login4(device);
4903
7629
  const channel = await openShell(client);
4904
7630
  try {
4905
7631
  await send(channel, "system-view", 1e4, 1500);
4906
7632
  for (const step of plan.steps) {
4907
7633
  for (const line of step.send) {
4908
7634
  const answer = await sendLine(channel, line);
4909
- if (REFUSED3.test(answer))
7635
+ if (REFUSED4.test(answer))
4910
7636
  throw new Error(`${step.title}: ${line}
4911
7637
  device said: ${answer.trim().slice(0, 300)}`);
4912
7638
  }
4913
7639
  }
4914
7640
  const committed = await send(channel, "commit", 60000, 3000);
4915
- if (REFUSED3.test(committed))
7641
+ if (REFUSED4.test(committed))
4916
7642
  throw new Error(`commit refused: ${committed.trim().slice(0, 300)}`);
4917
7643
  await send(channel, "quit", 1e4);
4918
7644
  } catch (error) {
@@ -4923,7 +7649,7 @@ async function apply4(device, _network, plan) {
4923
7649
  client.end();
4924
7650
  throw error;
4925
7651
  }
4926
- const proof = await login3(device).catch(() => {
7652
+ const proof = await login4(device).catch(() => {
4927
7653
  return;
4928
7654
  });
4929
7655
  if (!proof) {
@@ -4939,8 +7665,8 @@ async function apply4(device, _network, plan) {
4939
7665
  client.end();
4940
7666
  console.log(" committed, login proved, saved");
4941
7667
  }
4942
- function plan4(desired, current) {
4943
- const groups = compare(desired, current, dialect2);
7668
+ function plan5(desired, current) {
7669
+ const groups = compare2(desired, current, dialect2);
4944
7670
  vlanListChange(groups, "vlan batch ", batchIds, (ids) => [`vlan batch ${vlanBatch(ids)}`], (ids) => [`vlan batch ${vlanBatch(ids)}`]);
4945
7671
  const top = groups.find((group) => group.path.length === 0);
4946
7672
  const removedBatch = top?.remove.find((line) => line.startsWith("vlan batch ")) ?? "";
@@ -4961,15 +7687,15 @@ function plan4(desired, current) {
4961
7687
  }
4962
7688
  var vrp = {
4963
7689
  extension: ".cfg",
4964
- unsupported: unsupported4,
4965
- render: render4,
4966
- read: read4,
4967
- plan: plan4,
4968
- apply: apply4
7690
+ unsupported: unsupported5,
7691
+ render: render5,
7692
+ read: read5,
7693
+ plan: plan5,
7694
+ apply: apply5
4969
7695
  };
4970
7696
 
4971
7697
  // src/adapters/devices/index.ts
4972
- var adapters = { routeros, vrp, "raisecom-ros": raisecom, junos };
7698
+ var adapters = { routeros, vrp, "raisecom-ros": raisecom, junos, fortios };
4973
7699
 
4974
7700
  // src/adapters/registries/index.ts
4975
7701
  async function ripeStat(query, family) {
@@ -4990,7 +7716,7 @@ var registries = { "ripe-stat": ripeStat };
4990
7716
  // src/adapters/registries/librenms.ts
4991
7717
  var AUTH = { sha1: "SHA", sha256: "SHA-256" };
4992
7718
  var PRIVACY = { aes128: "AES", des: "DES" };
4993
- function settingsOf3(network) {
7719
+ function settingsOf4(network) {
4994
7720
  if (!network.librenms)
4995
7721
  throw new Error("the network has no librenms settings");
4996
7722
  return {
@@ -5017,7 +7743,7 @@ function credentials(device) {
5017
7743
  return;
5018
7744
  }
5019
7745
  async function planLibreNms(network) {
5020
- const { base, headers } = settingsOf3(network);
7746
+ const { base, headers } = settingsOf4(network);
5021
7747
  const response = await fetch(`${base}/devices?type=all`, { headers, signal: AbortSignal.timeout(30000) });
5022
7748
  if (!response.ok)
5023
7749
  throw new Error(`LibreNMS answered ${response.status} to the device list`);
@@ -5031,7 +7757,7 @@ async function planLibreNms(network) {
5031
7757
  };
5032
7758
  }
5033
7759
  async function sendLibreNms(network, additions) {
5034
- const { base, headers } = settingsOf3(network);
7760
+ const { base, headers } = settingsOf4(network);
5035
7761
  for (const device of additions) {
5036
7762
  const body = { hostname: device.connection.host, display_template: device.name, ...credentials(device) };
5037
7763
  const response = await fetch(`${base}/devices`, {
@@ -5050,10 +7776,10 @@ async function sendLibreNms(network, additions) {
5050
7776
 
5051
7777
  // src/adapters/registries/peeringdb.ts
5052
7778
  var API = "https://www.peeringdb.com/api";
5053
- var addressesOf2 = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
7779
+ var addressesOf3 = (iface) => (iface.addresses ?? []).map((entry) => typeof entry === "string" ? entry : entry.address);
5054
7780
  function peersWithRouteServer(device, iface) {
5055
7781
  const bgp = device.routing?.bgp;
5056
- const subnets = addressesOf2(iface);
7782
+ const subnets = addressesOf3(iface);
5057
7783
  return Object.values(bgp?.neighbors ?? {}).some((neighbor) => {
5058
7784
  const group = neighbor.group ? bgp?.groups?.[neighbor.group] : undefined;
5059
7785
  const role = neighbor.role ?? group?.role;
@@ -5067,7 +7793,7 @@ function wanted(network) {
5067
7793
  const exchange = iface.exchange;
5068
7794
  if (!exchange?.peeringdb_ixlan)
5069
7795
  continue;
5070
- const published = addressesOf2(iface).map(host2).filter(isGlobal2);
7796
+ const published = addressesOf3(iface).map(host2).filter(isGlobal2);
5071
7797
  records.push({
5072
7798
  name: exchange.name,
5073
7799
  ixlan_id: exchange.peeringdb_ixlan,
@@ -5983,7 +8709,7 @@ async function load(path) {
5983
8709
  const { createJiti } = await import("jiti");
5984
8710
  return createJiti(import.meta.url).import(path, { default: true });
5985
8711
  }
5986
- var network2 = await load(configPath);
8712
+ var network3 = await load(configPath);
5987
8713
  var flags = rest.filter((arg) => arg.startsWith("--"));
5988
8714
  var names = rest.filter((arg) => !arg.startsWith("--"));
5989
8715
  var confirmed = flags.includes("--confirm");
@@ -5997,9 +8723,9 @@ function fail(message) {
5997
8723
  }
5998
8724
  function selected(allByDefault) {
5999
8725
  if (!names.length && allByDefault)
6000
- return [...network2.devices];
6001
- const known = network2.devices.map((device) => device.name).join(", ");
6002
- const byName = (name) => network2.devices.find((device) => device.name === name);
8726
+ return [...network3.devices];
8727
+ const known = network3.devices.map((device) => device.name).join(", ");
8728
+ const byName = (name) => network3.devices.find((device) => device.name === name);
6003
8729
  return names.map((name) => byName(name) ?? fail(`no device ${name}; known: ${known}`));
6004
8730
  }
6005
8731
  function one() {
@@ -6023,7 +8749,7 @@ function writeState(device, text) {
6023
8749
  }
6024
8750
  function guard(devices) {
6025
8751
  const relevant = new Set(devices.map((device) => device.name));
6026
- const findings = validate(network2).filter((finding) => !finding.device || relevant.has(finding.device));
8752
+ const findings = validate(network3).filter((finding) => !finding.device || relevant.has(finding.device));
6027
8753
  for (const finding of findings) {
6028
8754
  const where = finding.device ? `${finding.device}: ` : "";
6029
8755
  const level = finding.level === "error" ? "error" : "warn ";
@@ -6048,8 +8774,8 @@ function show(device, plan) {
6048
8774
  return commands + plan.problems.length;
6049
8775
  }
6050
8776
  function validateCommand() {
6051
- guard([...network2.devices]);
6052
- console.log(`${network2.devices.length} device(s), no errors.`);
8777
+ guard([...network3.devices]);
8778
+ console.log(`${network3.devices.length} device(s), no errors.`);
6053
8779
  }
6054
8780
  function buildCommand() {
6055
8781
  const device = one();
@@ -6073,14 +8799,14 @@ function diffCommand() {
6073
8799
  }
6074
8800
  async function snapshotCommand() {
6075
8801
  for (const device of selected(true)) {
6076
- const text = await adapterOf(device).read(device, network2.devices);
8802
+ const text = await adapterOf(device).read(device, network3.devices);
6077
8803
  writeState(device, text);
6078
8804
  console.log(`${device.name}: ${text.split(`
6079
8805
  `).length} lines`);
6080
8806
  }
6081
8807
  if (names.length || !existsSync3(stateDirectory))
6082
8808
  return;
6083
- const current = new Set(network2.devices.map((device) => basename2(statePath(device))));
8809
+ const current = new Set(network3.devices.map((device) => basename2(statePath(device))));
6084
8810
  for (const file of readdirSync2(stateDirectory)) {
6085
8811
  if (current.has(file))
6086
8812
  continue;
@@ -6092,7 +8818,7 @@ async function applyCommand() {
6092
8818
  const device = one();
6093
8819
  guard([device]);
6094
8820
  const adapter = adapterOf(device);
6095
- const live = await adapter.read(device, network2.devices);
8821
+ const live = await adapter.read(device, network3.devices);
6096
8822
  writeState(device, live);
6097
8823
  const plan = adapter.plan(adapter.render(device), live, options);
6098
8824
  if (!show(device, plan))
@@ -6105,8 +8831,8 @@ Not sent: resolve the problems first.`);
6105
8831
  Nothing sent. Re-run with --confirm.`);
6106
8832
  return;
6107
8833
  }
6108
- await adapter.apply(device, network2.devices, plan, options);
6109
- writeState(device, await adapter.read(device, network2.devices));
8834
+ await adapter.apply(device, network3.devices, plan, options);
8835
+ writeState(device, await adapter.read(device, network3.devices));
6110
8836
  console.log("snapshot updated");
6111
8837
  }
6112
8838
  async function refreshCommand() {
@@ -6122,7 +8848,7 @@ async function refreshCommand() {
6122
8848
  }
6123
8849
  console.log(`${name}: ${prefixes.length} prefixes from ${registry} ${query}`);
6124
8850
  if (confirmed)
6125
- await fill(device, network2.devices, name, family, prefixes);
8851
+ await fill(device, network3.devices, name, family, prefixes);
6126
8852
  }
6127
8853
  if (!confirmed)
6128
8854
  console.log(`
@@ -6137,7 +8863,7 @@ function publicKeyOf(privateKey) {
6137
8863
  }
6138
8864
  function wireguardCommand() {
6139
8865
  const [deviceName, interfaceName, peerName] = names;
6140
- const device = network2.devices.find((candidate) => candidate.name === deviceName) ?? fail(HELP);
8866
+ const device = network3.devices.find((candidate) => candidate.name === deviceName) ?? fail(HELP);
6141
8867
  const iface = device.interfaces?.[interfaceName ?? ""];
6142
8868
  if (iface?.type !== "wireguard")
6143
8869
  fail(`${deviceName} has no WireGuard interface ${interfaceName}`);
@@ -6162,7 +8888,7 @@ function wireguardCommand() {
6162
8888
  `));
6163
8889
  }
6164
8890
  function communitiesCommand() {
6165
- const communities = network2.communities ?? fail("the network declares no communities");
8891
+ const communities = network3.communities ?? fail("the network declares no communities");
6166
8892
  for (const { community, description } of communities) {
6167
8893
  if (community.includes(","))
6168
8894
  fail(`community ${community} holds a comma`);
@@ -6170,7 +8896,7 @@ function communitiesCommand() {
6170
8896
  }
6171
8897
  }
6172
8898
  async function peeringdbCommand() {
6173
- const plan = await planPeeringDb(network2);
8899
+ const plan = await planPeeringDb(network3);
6174
8900
  for (const name of plan.unknown) {
6175
8901
  console.log(` PeeringDB lists ${name}, which the config does not. Remove it by hand if the network left.`);
6176
8902
  }
@@ -6185,10 +8911,10 @@ async function peeringdbCommand() {
6185
8911
  Nothing sent. Re-run with --confirm.`);
6186
8912
  return;
6187
8913
  }
6188
- await sendPeeringDb(network2, plan.changes);
8914
+ await sendPeeringDb(network3, plan.changes);
6189
8915
  }
6190
8916
  async function librenmsCommand() {
6191
- const plan = await planLibreNms(network2);
8917
+ const plan = await planLibreNms(network3);
6192
8918
  for (const hostname of plan.unknown)
6193
8919
  console.log(` LibreNMS monitors ${hostname}, which the config does not. Remove it by hand if it is gone.`);
6194
8920
  for (const device of plan.withoutSnmp)
@@ -6204,17 +8930,17 @@ async function librenmsCommand() {
6204
8930
  Nothing sent. Re-run with --confirm.`);
6205
8931
  return;
6206
8932
  }
6207
- await sendLibreNms(network2, plan.additions);
8933
+ await sendLibreNms(network3, plan.additions);
6208
8934
  }
6209
8935
  function prometheusCommand() {
6210
8936
  if (flags.includes("--auths")) {
6211
- const { yaml, notes } = snmpExporterAuths(network2);
8937
+ const { yaml, notes } = snmpExporterAuths(network3);
6212
8938
  for (const note of notes)
6213
8939
  console.error(note);
6214
8940
  process.stdout.write(yaml);
6215
8941
  return;
6216
8942
  }
6217
- const { targets, withoutSnmp } = prometheusTargets(network2);
8943
+ const { targets, withoutSnmp } = prometheusTargets(network3);
6218
8944
  for (const device of withoutSnmp)
6219
8945
  console.error(`${device.name} has no SNMP in its config; left out`);
6220
8946
  console.log(JSON.stringify(targets, null, 4));
@@ -6231,7 +8957,7 @@ function secretsIn(value, found = new Set) {
6231
8957
  return found;
6232
8958
  }
6233
8959
  function secretsCommand() {
6234
- const references = [...secretsIn([network2.devices, network2.peeringdb, network2.librenms])].sort();
8960
+ const references = [...secretsIn([network3.devices, network3.peeringdb, network3.librenms])].sort();
6235
8961
  let missing = 0;
6236
8962
  for (const reference of references) {
6237
8963
  const name = reference.replace(/^file:/, "");
@@ -6265,12 +8991,12 @@ function driftOf(device) {
6265
8991
  }
6266
8992
  }
6267
8993
  function annotatedGraph(layers, deviceNames) {
6268
- const graph = only(graphOf(network2.devices), layers, deviceNames);
8994
+ const graph = only(graphOf(network3.devices), layers, deviceNames);
6269
8995
  const shown = new Set(graph.nodes.map((node) => node.label));
6270
8996
  return {
6271
8997
  ...graph,
6272
- findings: validate(network2).filter((finding) => !finding.device || shown.has(finding.device)),
6273
- drift: Object.fromEntries(network2.devices.filter((device) => shown.has(device.name)).map((device) => [device.name, driftOf(device)]))
8998
+ findings: validate(network3).filter((finding) => !finding.device || shown.has(finding.device)),
8999
+ drift: Object.fromEntries(network3.devices.filter((device) => shown.has(device.name)).map((device) => [device.name, driftOf(device)]))
6274
9000
  };
6275
9001
  }
6276
9002
  function graphCommand() {
@@ -6306,7 +9032,7 @@ function addressesCommand() {
6306
9032
  const length = lengthText === undefined ? undefined : Number(lengthText);
6307
9033
  if (length !== undefined && !(Number.isInteger(length) && length >= 0 && length <= 128))
6308
9034
  fail("--length takes a prefix length, such as 24");
6309
- const report = addressesOf(network2.devices, within);
9035
+ const report = addressesOf(network3.devices, within);
6310
9036
  if (flagValue("format") === "json") {
6311
9037
  console.log(JSON.stringify(report, null, 4));
6312
9038
  return;
@@ -6341,10 +9067,10 @@ function renameCommand() {
6341
9067
  const [from, to] = names;
6342
9068
  if (!from || !to)
6343
9069
  fail("usage: circuit rename <device> <new-name> [--file] [--confirm]");
6344
- const device = network2.devices.find((candidate) => candidate.name === from);
9070
+ const device = network3.devices.find((candidate) => candidate.name === from);
6345
9071
  if (!device)
6346
- fail(`no device ${from}; known: ${network2.devices.map((candidate) => candidate.name).join(", ")}`);
6347
- if (network2.devices.some((candidate) => candidate.name === to))
9072
+ fail(`no device ${from}; known: ${network3.devices.map((candidate) => candidate.name).join(", ")}`);
9073
+ if (network3.devices.some((candidate) => candidate.name === to))
6348
9074
  fail(`${to} is already a device`);
6349
9075
  if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(to))
6350
9076
  fail(`${to} is not a hostname: letters, digits, dots, dashes and underscores`);
@@ -6381,7 +9107,7 @@ Renamed. Run circuit validate, then circuit apply ${to}: the device keeps its ol
6381
9107
  async function studioCommand() {
6382
9108
  if (flags.includes("--json")) {
6383
9109
  const sources = projectSources(root2);
6384
- const data = studioData(network2.devices, annotatedGraph(LAYERS, []), {
9110
+ const data = studioData(network3.devices, annotatedGraph(LAYERS, []), {
6385
9111
  render: (device) => adapterOf(device).render(device),
6386
9112
  snapshot: (device) => existsSync3(statePath(device)) ? readFileSync4(statePath(device), "utf8") : undefined,
6387
9113
  files: (device) => ({
@@ -6395,7 +9121,7 @@ async function studioCommand() {
6395
9121
  const port = Number(flagValue("port") ?? 4600);
6396
9122
  if (!Number.isInteger(port) || port < 1 || port > 65535)
6397
9123
  fail("--port takes a number from 1 to 65535");
6398
- const { isLoopback, studio } = await import("../chunks/server-n2q4tzy4.js");
9124
+ const { isLoopback, studio } = await import("../chunks/server-1ftkqkx5.js");
6399
9125
  const host = flagValue("host") ?? "127.0.0.1";
6400
9126
  const allowHosts = (flagValue("allow-host") ?? "").split(",").filter(Boolean);
6401
9127
  if (!isLoopback(host) && !allowHosts.length) {