@takodotid/circuit 0.43.1 → 1.0.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -37
- package/package.json +21 -58
- package/src/checks/across.ts +402 -0
- package/src/checks/built-in.ts +256 -0
- package/src/checks/device.ts +436 -0
- package/src/checks/index.ts +53 -0
- package/src/cli/advisories.ts +108 -0
- package/src/cli/app.ts +71 -0
- package/src/cli/editor-types.generated.ts +30 -0
- package/src/cli/editor-types.ts +69 -0
- package/src/cli/format.ts +124 -0
- package/src/cli/git-hooks.ts +79 -0
- package/src/cli/index.ts +350 -1170
- package/src/cli/migrate.ts +554 -0
- package/src/cli/project-commands.ts +656 -0
- package/src/cli/releases.ts +374 -0
- package/src/cli/remember.ts +339 -0
- package/src/cli/rename.ts +27 -33
- package/src/cli/secrets-password.ts +60 -0
- package/src/cli/secrets.ts +258 -0
- package/src/cli/setup.ts +300 -0
- package/src/cli/signing.ts +23 -0
- package/src/cli/text-editor.ts +268 -0
- package/src/cli/ui.ts +298 -0
- package/src/cli/version.ts +2 -0
- package/src/cli/versions.ts +158 -0
- package/src/config/address.ts +102 -0
- package/src/config/circuit.d.ts +30 -0
- package/src/config/common.ts +65 -0
- package/src/config/communities.ts +479 -0
- package/src/config/define.ts +684 -0
- package/src/config/device.ts +164 -0
- package/src/config/firewall.ts +150 -0
- package/src/config/index.ts +22 -0
- package/src/{schema → config}/interfaces.ts +121 -85
- package/src/config/keys.ts +434 -0
- package/src/config/management.ts +61 -0
- package/src/config/merge.ts +335 -0
- package/src/config/models.ts +70 -0
- package/src/config/monitoring.ts +64 -0
- package/src/config/network.ts +180 -0
- package/src/config/presets.ts +149 -0
- package/src/config/routing.ts +401 -0
- package/src/config/services.ts +51 -0
- package/src/config/switching.ts +84 -0
- package/src/config/system.ts +103 -0
- package/src/{schema → config}/time-zones.ts +1 -1
- package/src/core/address.ts +1 -1
- package/src/core/cabling.ts +8 -43
- package/src/core/graph.ts +40 -0
- package/src/{integrations → core}/registries.ts +5 -3
- package/src/core/secrets-file.ts +539 -0
- package/src/core/secrets.ts +83 -64
- package/src/core/topology.ts +103 -146
- package/src/core/utils.ts +12 -1
- package/src/model.ts +83 -0
- package/src/platforms/derive.ts +64 -37
- package/src/platforms/finding.ts +9 -0
- package/src/platforms/fortios/firewall.ts +76 -64
- package/src/platforms/fortios/index.ts +189 -125
- package/src/platforms/fortios/names.ts +51 -18
- package/src/platforms/fortios/render.ts +69 -62
- package/src/platforms/fortios/routing.ts +89 -58
- package/src/platforms/fortios/vdoms.ts +47 -16
- package/src/platforms/junos/TRAPS.md +2 -2
- package/src/platforms/junos/bgp.ts +63 -42
- package/src/platforms/junos/index.ts +122 -89
- package/src/platforms/junos/render.ts +139 -104
- package/src/platforms/platform.ts +10 -8
- package/src/platforms/raisecom-ros/TRAPS.md +1 -1
- package/src/platforms/raisecom-ros/index.ts +89 -70
- package/src/platforms/raisecom-ros/render.ts +49 -45
- package/src/platforms/routeros/TRAPS.md +3 -3
- package/src/platforms/routeros/index.ts +121 -60
- package/src/platforms/routeros/render.ts +214 -195
- package/src/platforms/vrp/TRAPS.md +1 -1
- package/src/platforms/vrp/index.ts +74 -50
- package/src/platforms/vrp/render.ts +67 -56
- package/src/project/project.ts +725 -0
- package/src/studio/app/env.d.ts +9 -0
- package/src/studio/app/index.html +18 -0
- package/src/studio/app/src/App.vue +95 -0
- package/src/studio/app/src/components/AddressesView.vue +38 -0
- package/src/studio/app/src/components/CablesView.vue +29 -0
- package/src/studio/app/src/components/CompareView.vue +160 -0
- package/src/studio/app/src/components/DataTable.vue +49 -0
- package/src/studio/app/src/components/DeviceView.vue +237 -0
- package/src/studio/app/src/components/DevicesView.vue +32 -0
- package/src/studio/app/src/components/Faceplate.vue +75 -0
- package/src/studio/app/src/components/GraphView.vue +282 -0
- package/src/studio/app/src/components/LineDiff.vue +24 -0
- package/src/studio/app/src/components/ProblemsView.vue +42 -0
- package/src/studio/app/src/components/SearchView.vue +72 -0
- package/src/studio/app/src/components/SecretsView.vue +112 -0
- package/src/studio/app/src/components/SessionsView.vue +35 -0
- package/src/studio/app/src/components/Settings.vue +95 -0
- package/src/studio/app/src/components/ToolsView.vue +118 -0
- package/src/studio/app/src/components/VlansView.vue +50 -0
- package/src/studio/app/src/lib/csv.ts +19 -0
- package/src/studio/{public/diff.js → app/src/lib/diff.ts} +40 -35
- package/src/studio/app/src/lib/format.ts +38 -0
- package/src/studio/app/src/lib/net.ts +68 -0
- package/src/studio/app/src/lib/secrets-crypto.ts +35 -0
- package/src/studio/app/src/lib/store.ts +71 -0
- package/src/studio/app/src/lib/tools.ts +217 -0
- package/src/studio/app/src/main.ts +9 -0
- package/src/studio/{public → app/src}/style.css +14 -4
- package/src/studio/app/tsconfig.json +17 -0
- package/src/studio/app/vite.config.ts +16 -0
- package/src/studio/assets.generated.ts +3 -0
- package/src/studio/data.ts +96 -188
- package/src/studio/git.ts +27 -43
- package/src/studio/server.ts +196 -107
- package/src/studio/types.ts +141 -0
- package/src/transport/console.ts +3 -3
- package/src/transport/ssh.ts +1 -1
- package/dist/chunks/index-3b343xva.js +0 -177
- package/dist/chunks/index-6wb41q67.js +0 -62
- package/dist/chunks/index-n7w3tg5y.js +0 -149
- package/dist/chunks/index-yzw86rc8.js +0 -921
- package/dist/chunks/new-w3zys5y4.js +0 -172
- package/dist/chunks/server-cxr9ynxc.js +0 -259
- package/dist/cli/index.js +0 -12922
- package/dist/helpers/index.js +0 -881
- package/dist/index.js +0 -28
- package/dist/types/core/address-space.d.ts +0 -62
- package/dist/types/core/address.d.ts +0 -44
- package/dist/types/core/cabling.d.ts +0 -41
- package/dist/types/core/cross-checks.d.ts +0 -28
- package/dist/types/core/define.d.ts +0 -79
- package/dist/types/core/project-files.d.ts +0 -12
- package/dist/types/core/schema-docs.d.ts +0 -25
- package/dist/types/core/secrets.d.ts +0 -62
- package/dist/types/core/topology.d.ts +0 -96
- package/dist/types/core/utils.d.ts +0 -10
- package/dist/types/core/validator.d.ts +0 -23
- package/dist/types/helpers/bgp.d.ts +0 -195
- package/dist/types/helpers/cables.d.ts +0 -69
- package/dist/types/helpers/checks.d.ts +0 -36
- package/dist/types/helpers/exchange.d.ts +0 -55
- package/dist/types/helpers/filters.d.ts +0 -51
- package/dist/types/helpers/index.d.ts +0 -14
- package/dist/types/helpers/route-server.d.ts +0 -52
- package/dist/types/helpers/upstreams.d.ts +0 -192
- package/dist/types/index.d.ts +0 -10
- package/dist/types/integrations/librenms.d.ts +0 -60
- package/dist/types/integrations/netbox.d.ts +0 -89
- package/dist/types/integrations/peeringdb.d.ts +0 -24
- package/dist/types/integrations/prometheus.d.ts +0 -24
- package/dist/types/integrations/registries.d.ts +0 -8
- package/dist/types/platforms/bgp-wait.d.ts +0 -24
- package/dist/types/platforms/catalog.d.ts +0 -109
- package/dist/types/platforms/derive.d.ts +0 -17
- package/dist/types/platforms/fortios/firewall.d.ts +0 -47
- package/dist/types/platforms/fortios/index.d.ts +0 -140
- package/dist/types/platforms/fortios/models.d.ts +0 -14
- package/dist/types/platforms/fortios/names.d.ts +0 -52
- package/dist/types/platforms/fortios/plan.d.ts +0 -114
- package/dist/types/platforms/fortios/render.d.ts +0 -77
- package/dist/types/platforms/fortios/routing.d.ts +0 -56
- package/dist/types/platforms/fortios/tree.d.ts +0 -37
- package/dist/types/platforms/fortios/vdoms.d.ts +0 -50
- package/dist/types/platforms/index.d.ts +0 -13
- package/dist/types/platforms/junos/bgp.d.ts +0 -44
- package/dist/types/platforms/junos/index.d.ts +0 -114
- package/dist/types/platforms/junos/models.d.ts +0 -14
- package/dist/types/platforms/junos/render.d.ts +0 -154
- package/dist/types/platforms/line-diff.d.ts +0 -67
- package/dist/types/platforms/platform.d.ts +0 -135
- package/dist/types/platforms/raisecom-ros/index.d.ts +0 -71
- package/dist/types/platforms/raisecom-ros/models.d.ts +0 -14
- package/dist/types/platforms/raisecom-ros/render.d.ts +0 -41
- package/dist/types/platforms/routeros/export.d.ts +0 -59
- package/dist/types/platforms/routeros/index.d.ts +0 -116
- package/dist/types/platforms/routeros/models.d.ts +0 -18
- package/dist/types/platforms/routeros/plan.d.ts +0 -58
- package/dist/types/platforms/routeros/render.d.ts +0 -250
- package/dist/types/platforms/vrp/index.d.ts +0 -83
- package/dist/types/platforms/vrp/models.d.ts +0 -12
- package/dist/types/platforms/vrp/render.d.ts +0 -96
- package/dist/types/schema/common.d.ts +0 -30
- package/dist/types/schema/device.d.ts +0 -126
- package/dist/types/schema/firewall.d.ts +0 -131
- package/dist/types/schema/index.d.ts +0 -8
- package/dist/types/schema/interfaces.d.ts +0 -205
- package/dist/types/schema/policy.d.ts +0 -107
- package/dist/types/schema/routing.d.ts +0 -161
- package/dist/types/schema/system.d.ts +0 -198
- package/dist/types/schema/time-zones.d.ts +0 -2
- package/dist/types/transport/console.d.ts +0 -115
- package/dist/types/transport/path-check.d.ts +0 -32
- package/dist/types/transport/ssh.d.ts +0 -27
- package/src/cli/graph-format.ts +0 -91
- package/src/cli/new.ts +0 -234
- package/src/core/address-space.ts +0 -320
- package/src/core/cross-checks.ts +0 -369
- package/src/core/define.ts +0 -124
- package/src/core/project-files.ts +0 -35
- package/src/core/schema-docs.ts +0 -174
- package/src/core/validator.ts +0 -508
- package/src/helpers/bgp.ts +0 -473
- package/src/helpers/cables.ts +0 -74
- package/src/helpers/checks.ts +0 -201
- package/src/helpers/exchange.ts +0 -123
- package/src/helpers/filters.ts +0 -107
- package/src/helpers/index.ts +0 -25
- package/src/helpers/route-server.ts +0 -212
- package/src/helpers/upstreams.ts +0 -369
- package/src/index.ts +0 -13
- package/src/integrations/librenms.ts +0 -291
- package/src/integrations/netbox.ts +0 -620
- package/src/integrations/peeringdb.ts +0 -173
- package/src/integrations/prometheus.ts +0 -90
- package/src/schema/common.ts +0 -41
- package/src/schema/device.ts +0 -145
- package/src/schema/firewall.ts +0 -140
- package/src/schema/index.ts +0 -8
- package/src/schema/policy.ts +0 -115
- package/src/schema/routing.ts +0 -173
- package/src/schema/system.ts +0 -203
- package/src/studio/public/app.js +0 -186
- package/src/studio/public/compare.js +0 -250
- package/src/studio/public/dom.js +0 -138
- package/src/studio/public/evaluate.js +0 -687
- package/src/studio/public/export.js +0 -130
- package/src/studio/public/graph.js +0 -1541
- package/src/studio/public/index.html +0 -83
- package/src/studio/public/net.js +0 -134
- package/src/studio/public/tables.js +0 -1289
- package/src/studio/public/tools.js +0 -610
- package/templates/colocation/checks.ts +0 -53
- package/templates/colocation/circuit.config.ts +0 -9
- package/templates/colocation/edge-01.ts +0 -133
- package/templates/colocation/routing.ts +0 -13
- package/templates/colocation/tenancy.ts +0 -76
- package/templates/colocation/tenants/acme.ts +0 -19
- package/templates/colocation/tenants/index.ts +0 -6
- package/templates/colocation/tor-01.ts +0 -33
- package/templates/edge-router/checks.ts +0 -20
- package/templates/edge-router/circuit.config.ts +0 -8
- package/templates/edge-router/edge-01.ts +0 -110
- package/templates/edge-router/routing.ts +0 -13
- package/templates/project/AGENTS.md +0 -51
- package/templates/project/CLAUDE.md +0 -1
- package/templates/project/README.md +0 -16
- package/templates/project/gitignore +0 -5
- package/templates/project/tsconfig.json +0 -13
- package/templates/single-site/checks.ts +0 -26
- package/templates/single-site/circuit.config.ts +0 -8
- package/templates/single-site/router.ts +0 -86
- package/templates/single-site/site.ts +0 -18
- package/templates/single-site/switch.ts +0 -34
- /package/src/studio/{public → app/public}/favicon.svg +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin-ext.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-latin.woff2 +0 -0
- /package/src/studio/{public → app/public}/special-gothic-license.txt +0 -0
|
@@ -6,11 +6,13 @@ import type { Client, ClientChannel } from "ssh2";
|
|
|
6
6
|
import { Address } from "../../core/address";
|
|
7
7
|
import type { Neighbor } from "../../core/cabling";
|
|
8
8
|
import { Secrets } from "../../core/secrets";
|
|
9
|
-
import type { AclRule, Device, Family, FilterRule } from "../../
|
|
9
|
+
import type { AclRule, Device, Family, FilterRule, Gre, Lag, VlanInterface, Vxlan } from "../../model";
|
|
10
10
|
import { SerialConsole, type ConsoleDialect } from "../../transport/console";
|
|
11
11
|
import { Ssh } from "../../transport/ssh";
|
|
12
12
|
import { Catalog } from "../catalog";
|
|
13
13
|
import { Derive } from "../derive";
|
|
14
|
+
import type { Finding } from "../finding";
|
|
15
|
+
|
|
14
16
|
import { type ApplyOptions, type OsSupport, type Plan, type ReadOptions, type ReadSession, Platform } from "../platform";
|
|
15
17
|
import { FortiOsFirewall } from "./firewall";
|
|
16
18
|
import { FortiOsNames } from "./names";
|
|
@@ -129,6 +131,16 @@ export class FortiOsCli {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
/** The fields of a match that may hold a list or an interface set before Circuit writes them out. */
|
|
135
|
+
type ListedMatch = {
|
|
136
|
+
src?: unknown;
|
|
137
|
+
dst?: unknown;
|
|
138
|
+
in_interface?: unknown;
|
|
139
|
+
out_interface?: unknown;
|
|
140
|
+
in_interface_set?: string;
|
|
141
|
+
out_interface_set?: string;
|
|
142
|
+
};
|
|
143
|
+
|
|
132
144
|
/** A failure after which the device reloads its saved configuration, which still says to revert. */
|
|
133
145
|
class Reloaded extends Error {}
|
|
134
146
|
|
|
@@ -182,6 +194,11 @@ export class FortiOs extends Platform {
|
|
|
182
194
|
|
|
183
195
|
override readonly console = FortiOs.CONSOLE;
|
|
184
196
|
|
|
197
|
+
/** A management VDOM that cannot work: named `root`, listing a LAG's port instead of the LAG, or with a route that leaves through an interface outside it or goes nowhere. */
|
|
198
|
+
checks(device: Device): Finding[] {
|
|
199
|
+
return FortiOsVdoms.problems(device).map((message) => ({ level: "error", device: device.name, message }));
|
|
200
|
+
}
|
|
201
|
+
|
|
185
202
|
unsupported(device: Device): string[] {
|
|
186
203
|
const found: string[] = [];
|
|
187
204
|
|
|
@@ -193,21 +210,34 @@ export class FortiOs extends Platform {
|
|
|
193
210
|
found.push(...FortiOs.firewallGaps(device));
|
|
194
211
|
found.push(...FortiOs.serviceGaps(device));
|
|
195
212
|
|
|
196
|
-
if (device.stp) found.push("stp, ports are routed");
|
|
213
|
+
if (device.switching?.stp) found.push("switching.stp, ports are routed");
|
|
197
214
|
found.push(...FortiOs.transparentGaps(device));
|
|
198
215
|
found.push(...FortiOsVdoms.gaps(device));
|
|
199
216
|
|
|
200
|
-
for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
|
|
201
|
-
if (iface
|
|
217
|
+
for (const [name, iface] of Object.entries(device.interfaces?.vlan_interface ?? {})) {
|
|
218
|
+
if (iface?.mac) found.push(`interfaces.vlan_interface.${name}.mac, which only RouterOS sets for now`);
|
|
202
219
|
}
|
|
203
|
-
|
|
220
|
+
const vrfs = device.routing?.vrfs ?? {};
|
|
221
|
+
if (Object.keys(vrfs).length > FortiOsRender.VRFS) found.push(`routing.vrfs, at most ${FortiOsRender.VRFS}`);
|
|
204
222
|
|
|
205
|
-
for (const [name, vrf] of Object.entries(
|
|
206
|
-
if (vrf
|
|
223
|
+
for (const [name, vrf] of Object.entries(vrfs)) {
|
|
224
|
+
if (vrf?.description) found.push(`routing.vrfs.${name}.description, the platform numbers a VRF and keeps no description`);
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
// Proved on a FG-3700D on FortiOS 6.2.2, in NAT mode. BFD on a multihop BGP session is refused in FortiOsRouting.gaps, for the reason the device showed.
|
|
210
|
-
|
|
228
|
+
// A management VDOM is rendered but not proved: `experimental()` gives it.
|
|
229
|
+
found.push(...Derive.unproved(device, ["bfd-timers", "static-route-bfd", "multihop-bfd", "management-vdom"]));
|
|
230
|
+
|
|
231
|
+
return found;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
override experimental(device: Device): string[] {
|
|
235
|
+
const found: string[] = [];
|
|
236
|
+
|
|
237
|
+
if (device.system?.vdoms) found.push("system.vdoms, a management VDOM");
|
|
238
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
239
|
+
if (route.vrf && route.bfd) found.push(`routing.static_routes ${route.prefix}.bfd, BFD in a VRF`);
|
|
240
|
+
}
|
|
211
241
|
|
|
212
242
|
return found;
|
|
213
243
|
}
|
|
@@ -285,7 +315,7 @@ export class FortiOs extends Platform {
|
|
|
285
315
|
await FortiOs.checkVersion(cli, device);
|
|
286
316
|
|
|
287
317
|
const lines = FortiOs.fillLines(set, family, prefixes);
|
|
288
|
-
for (const line of device.vdoms ? FortiOsVdoms.wrap({ kind: "vdom", vdom: FortiOsVdoms.ROOT }, lines) : lines) {
|
|
318
|
+
for (const line of device.system?.vdoms ? FortiOsVdoms.wrap({ kind: "vdom", vdom: FortiOsVdoms.ROOT }, lines) : lines) {
|
|
289
319
|
let answer = await cli.run(line);
|
|
290
320
|
if (FortiOsCli.QUESTION.test(cli.prompt) && line === "purge") answer = await cli.answer("y");
|
|
291
321
|
if (FortiOs.REFUSED.test(answer)) throw new Error(`${set}: ${line}\n device said: ${answer.trim().slice(0, 300)}`);
|
|
@@ -390,7 +420,7 @@ export class FortiOs extends Platform {
|
|
|
390
420
|
private static async openConsole(device: Device) {
|
|
391
421
|
const { console, user, password } = SerialConsole.of(device);
|
|
392
422
|
const opened = await SerialConsole.open(console);
|
|
393
|
-
const login = () => SerialConsole.login(opened.channel, FortiOs.CONSOLE, user, password, console.
|
|
423
|
+
const login = () => SerialConsole.login(opened.channel, FortiOs.CONSOLE, user, password, console.password_only);
|
|
394
424
|
|
|
395
425
|
try {
|
|
396
426
|
await login();
|
|
@@ -454,7 +484,7 @@ export class FortiOs extends Platform {
|
|
|
454
484
|
|
|
455
485
|
/** Run `work` among the device's own settings: inside `config global` on a device with VDOMs, as it is on one without. */
|
|
456
486
|
private static async inGlobal<T>(cli: FortiOsCli, device: Device | boolean, work: () => Promise<T>): Promise<T> {
|
|
457
|
-
const multi = typeof device === "boolean" ? device : Boolean(device.vdoms);
|
|
487
|
+
const multi = typeof device === "boolean" ? device : Boolean(device.system?.vdoms);
|
|
458
488
|
if (!multi) return work();
|
|
459
489
|
|
|
460
490
|
await cli.run("config global");
|
|
@@ -511,7 +541,7 @@ export class FortiOs extends Platform {
|
|
|
511
541
|
*/
|
|
512
542
|
private static async readVdoms(cli: FortiOsCli, device: Device, show: (section: string) => Promise<string>): Promise<string[]> {
|
|
513
543
|
const management = FortiOsVdoms.managementOf(device);
|
|
514
|
-
if (!management) throw new Error(`${device.name} runs several VDOMs and its config has no vdoms; write vdoms.management`);
|
|
544
|
+
if (!management) throw new Error(`${device.name} runs several VDOMs and its config has no vdoms; write system.vdoms.management`);
|
|
515
545
|
|
|
516
546
|
const parts: string[] = [];
|
|
517
547
|
const inVdom = async (vdom: string, sections: readonly string[]) => {
|
|
@@ -624,7 +654,7 @@ export class FortiOs extends Platform {
|
|
|
624
654
|
async (cli) => {
|
|
625
655
|
const reloaded = await FortiOs.inGlobal(cli, device, () => cli.run("execute cfg reload"));
|
|
626
656
|
if (!/no config to be reloaded/.test(reloaded)) return false;
|
|
627
|
-
await FortiOs.saving(cli, "automatic", Boolean(device.vdoms));
|
|
657
|
+
await FortiOs.saving(cli, "automatic", Boolean(device.system?.vdoms));
|
|
628
658
|
return true;
|
|
629
659
|
},
|
|
630
660
|
options
|
|
@@ -643,7 +673,7 @@ export class FortiOs extends Platform {
|
|
|
643
673
|
if (options.console) {
|
|
644
674
|
await FortiOs.consoleSession(device, async (cli, relogin) => {
|
|
645
675
|
await FortiOs.checkVersion(cli, device);
|
|
646
|
-
await FortiOs.configure(cli, plan, { seconds, prove: relogin }, Boolean(device.vdoms));
|
|
676
|
+
await FortiOs.configure(cli, plan, { seconds, prove: relogin }, Boolean(device.system?.vdoms));
|
|
647
677
|
});
|
|
648
678
|
return;
|
|
649
679
|
}
|
|
@@ -661,7 +691,7 @@ export class FortiOs extends Platform {
|
|
|
661
691
|
proof.end();
|
|
662
692
|
},
|
|
663
693
|
},
|
|
664
|
-
Boolean(device.vdoms)
|
|
694
|
+
Boolean(device.system?.vdoms)
|
|
665
695
|
);
|
|
666
696
|
});
|
|
667
697
|
} catch (error) {
|
|
@@ -684,8 +714,10 @@ export class FortiOs extends Platform {
|
|
|
684
714
|
found.push(`connection.host, ${where}: only the management address, one IPv4 address outside the bridges`);
|
|
685
715
|
|
|
686
716
|
const holders = [
|
|
687
|
-
...
|
|
688
|
-
...
|
|
717
|
+
...Derive.ports(device).map(([name, port]) => [`interfaces.ethernet.${name}`, name, port.addresses] as const),
|
|
718
|
+
...FortiOsNames.interfacesOf(device).map(
|
|
719
|
+
({ name, kind, settings }) => [`interfaces.${kind}.${name}`, name, settings.addresses] as const
|
|
720
|
+
),
|
|
689
721
|
];
|
|
690
722
|
|
|
691
723
|
for (const [path, name, addresses] of holders) {
|
|
@@ -699,22 +731,25 @@ export class FortiOs extends Platform {
|
|
|
699
731
|
if (routing?.bgp || routing?.ospf) found.push(`routing, ${where}: static routes only`);
|
|
700
732
|
if (routing?.bfd) found.push(`routing.bfd, ${where}: BFD is proved in NAT mode only`);
|
|
701
733
|
|
|
702
|
-
for (const [index, route] of (routing?.
|
|
703
|
-
const at = `routing.
|
|
734
|
+
for (const [index, route] of (routing?.static_routes ?? []).entries()) {
|
|
735
|
+
const at = `routing.static_routes[${index}]`;
|
|
704
736
|
if (!management) found.push(`${at}, ${where}: a route needs the management address`);
|
|
705
|
-
if (!route.
|
|
737
|
+
if (!route.next_hop || Address.family(route.next_hop) !== "ipv4" || route.interface || route.blackhole || route.vrf)
|
|
706
738
|
found.push(`${at}, ${where}: only an IPv4 next hop, for the management address`);
|
|
707
739
|
if (route.bfd) found.push(`${at}.bfd, ${where}: BFD is proved in NAT mode only`);
|
|
708
740
|
}
|
|
709
741
|
|
|
710
742
|
if (device.firewall?.nat) found.push(`firewall.nat, ${where}`);
|
|
711
|
-
if (device.
|
|
743
|
+
if (device.services?.dhcp_server && Object.keys(device.services.dhcp_server).length) found.push(`services.dhcp_server, ${where}`);
|
|
712
744
|
|
|
713
745
|
if (!management) {
|
|
714
746
|
const services = device.management ?? {};
|
|
715
|
-
for (const service of ["ssh", "telnet", "http", "https", "api", "api_tls"
|
|
747
|
+
for (const service of ["ssh", "telnet", "http", "https", "api", "api_tls"] as const) {
|
|
716
748
|
if (services[service]) found.push(`management.${service}, ${where}: the device has no management address to answer on`);
|
|
717
749
|
}
|
|
750
|
+
if (device.monitoring?.snmp) found.push(`monitoring.snmp, ${where}: the device has no management address to answer on`);
|
|
751
|
+
if (services.native_protocol)
|
|
752
|
+
found.push(`management.native_protocol, ${where}: the device has no management address to answer on`);
|
|
718
753
|
}
|
|
719
754
|
|
|
720
755
|
return found;
|
|
@@ -727,17 +762,18 @@ export class FortiOs extends Platform {
|
|
|
727
762
|
if (system.timezone && !FortiOsRender.zoneOf(system.timezone))
|
|
728
763
|
found.push(`system.timezone ${system.timezone}, the platform has no zone for it`);
|
|
729
764
|
if (system.release_channel) found.push("system.release_channel, the platform's firmware has no release channel to follow");
|
|
730
|
-
if (
|
|
731
|
-
found.push("
|
|
732
|
-
if (system.
|
|
765
|
+
if (device.monitoring?.transceivers !== undefined)
|
|
766
|
+
found.push("monitoring.transceivers, the platform reads optics without being told");
|
|
767
|
+
if (system.ip_stack?.syn_cookies)
|
|
768
|
+
found.push("system.ip_stack.syn_cookies, the platform has a SYN proxy per DoS policy, not SYN cookies");
|
|
733
769
|
|
|
734
770
|
const local = system.logging?.local;
|
|
735
771
|
if (local?.files !== undefined || local?.lines_per_file !== undefined) {
|
|
736
772
|
found.push("system.logging.local.files and lines_per_file, logs kept in memory do not rotate into files");
|
|
737
773
|
}
|
|
738
774
|
|
|
739
|
-
if ((
|
|
740
|
-
found.push(`
|
|
775
|
+
if ((device.monitoring?.syslog?.length ?? 0) > FortiOsRender.COLLECTORS)
|
|
776
|
+
found.push(`monitoring.syslog, at most ${FortiOsRender.COLLECTORS} collectors`);
|
|
741
777
|
|
|
742
778
|
const servers = system.dns?.servers ?? [];
|
|
743
779
|
for (const family of ["ipv4", "ipv6"] as const) {
|
|
@@ -757,19 +793,20 @@ export class FortiOs extends Platform {
|
|
|
757
793
|
if (management.ftp) found.push("management.ftp, the platform has no FTP service");
|
|
758
794
|
if (management.console) found.push("management.console, the console logs in with the device's users");
|
|
759
795
|
if (management.privilege_password) found.push("management.privilege_password, the platform has no privileged mode");
|
|
760
|
-
if (management.
|
|
761
|
-
|
|
796
|
+
if (management.native_protocol?.interfaces?.length)
|
|
797
|
+
found.push("management.native_protocol.interfaces, FortiManager access runs over IP only");
|
|
798
|
+
if (management.native_protocol?.port) found.push("management.native_protocol.port, FortiManager access listens on 541");
|
|
762
799
|
if (management.ssh?.auth_retries)
|
|
763
800
|
found.push("management.ssh.auth_retries, the platform counts failed logins per source, not per connection");
|
|
764
801
|
|
|
765
|
-
for (const [name, user] of Object.entries(
|
|
802
|
+
for (const [name, user] of Object.entries(device.monitoring?.snmp?.users ?? {})) {
|
|
766
803
|
if (user.auth === "sha256")
|
|
767
|
-
found.push(`
|
|
804
|
+
found.push(`monitoring.snmp.users.${name}.auth sha256, the platform authenticates with SHA1 or MD5`);
|
|
768
805
|
}
|
|
769
806
|
|
|
770
|
-
for (const [name, user] of Object.entries(device.users ?? {})) {
|
|
771
|
-
if ((user
|
|
772
|
-
found.push(`users.${name}.ssh_keys, at most ${FortiOsRender.SSH_KEYS}`);
|
|
807
|
+
for (const [name, user] of Object.entries(device.system?.users ?? {})) {
|
|
808
|
+
if ((user?.ssh_keys?.length ?? 0) > FortiOsRender.SSH_KEYS)
|
|
809
|
+
found.push(`system.users.${name}.ssh_keys, at most ${FortiOsRender.SSH_KEYS}`);
|
|
773
810
|
}
|
|
774
811
|
|
|
775
812
|
return found;
|
|
@@ -783,17 +820,13 @@ export class FortiOs extends Platform {
|
|
|
783
820
|
private static interfaceGaps(device: Device): string[] {
|
|
784
821
|
const found: string[] = [];
|
|
785
822
|
const vlanInterfaces = new Set(
|
|
786
|
-
Object.values(device.interfaces ?? {}).flatMap((iface) => (iface
|
|
823
|
+
Object.values(device.interfaces?.vlan_interface ?? {}).flatMap((iface) => (iface ? [iface.vlan] : []))
|
|
787
824
|
);
|
|
788
825
|
|
|
789
|
-
|
|
790
|
-
if (vlan.description) found.push(`vlans.${name}.description, a VLAN interface holds its own`);
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
for (const [name, port] of Object.entries(device.ports ?? {})) {
|
|
794
|
-
if (!port) continue;
|
|
826
|
+
// A VLAN's description is dropped, as `switching.vlans` says: the platform knows a VLAN by its number, and a VLAN interface holds a description of its own.
|
|
795
827
|
|
|
796
|
-
|
|
828
|
+
for (const [name, port] of Derive.ports(device)) {
|
|
829
|
+
const where = `interfaces.ethernet.${name}`;
|
|
797
830
|
const speedClass = name.split("-")[0]!;
|
|
798
831
|
if (port.speed && !FortiOsRender.SPEEDS[speedClass]?.[port.speed])
|
|
799
832
|
found.push(`${where}.speed ${port.speed}, not in a ${speedClass} cage`);
|
|
@@ -805,14 +838,14 @@ export class FortiOs extends Platform {
|
|
|
805
838
|
if (port.hardware_offload !== undefined)
|
|
806
839
|
found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
|
|
807
840
|
|
|
808
|
-
if (port.
|
|
809
|
-
for (const field of ["addresses", "mtu", "vrrp", "vrf", "lldp", "trunk_vlans", "ipv6_ra", "
|
|
841
|
+
if (port.in_lag) {
|
|
842
|
+
for (const field of ["addresses", "mtu", "vrrp", "vrf", "lldp", "trunk_vlans", "ipv6_ra", "ingress_acl"] as const) {
|
|
810
843
|
if (port[field] !== undefined) found.push(`${where}.${field}, a LAG member takes its settings from the LAG`);
|
|
811
844
|
}
|
|
812
845
|
}
|
|
813
846
|
|
|
814
|
-
const acl = port.
|
|
815
|
-
if (acl) found.push(...FortiOs.aclGaps(`acls.${port.
|
|
847
|
+
const acl = port.ingress_acl ? device.switching?.acls?.[port.ingress_acl] : undefined;
|
|
848
|
+
if (acl) found.push(...FortiOs.aclGaps(`switching.acls.${port.ingress_acl}`, acl));
|
|
816
849
|
|
|
817
850
|
found.push(...FortiOs.trunkGaps(where, name, port.trunk_vlans, vlanInterfaces, device));
|
|
818
851
|
found.push(...FortiOs.routedGaps(where, port, false));
|
|
@@ -820,10 +853,10 @@ export class FortiOs extends Platform {
|
|
|
820
853
|
|
|
821
854
|
const ports = FortiOs.portNames(device);
|
|
822
855
|
|
|
823
|
-
for (const
|
|
824
|
-
const where = `interfaces.${name}`;
|
|
856
|
+
for (const { name, kind, settings: iface } of FortiOsNames.interfacesOf(device)) {
|
|
857
|
+
const where = `interfaces.${kind}.${name}`;
|
|
825
858
|
|
|
826
|
-
if (
|
|
859
|
+
if (kind === "wireguard") {
|
|
827
860
|
found.push(`${where}, a wireguard: the platform has no WireGuard`);
|
|
828
861
|
continue;
|
|
829
862
|
}
|
|
@@ -835,32 +868,38 @@ export class FortiOs extends Platform {
|
|
|
835
868
|
if (iface.hardware_offload !== undefined)
|
|
836
869
|
found.push(`${where}.hardware_offload, offload is decided per policy, which can match many interfaces`);
|
|
837
870
|
|
|
838
|
-
if (
|
|
839
|
-
|
|
871
|
+
if (kind === "lag") {
|
|
872
|
+
const lag = iface as Lag;
|
|
873
|
+
if (lag.id !== undefined) found.push(`${where}.id, the platform names a LAG by its key`);
|
|
840
874
|
for (const field of ["access_vlan", "native_vlan", "stp", "storm_control"] as const) {
|
|
841
|
-
if (
|
|
875
|
+
if (lag[field] !== undefined) found.push(`${where}.${field}, LAGs are routed`);
|
|
842
876
|
}
|
|
843
877
|
|
|
844
|
-
found.push(...FortiOs.trunkGaps(where, name,
|
|
878
|
+
found.push(...FortiOs.trunkGaps(where, name, lag.trunk_vlans, vlanInterfaces, device));
|
|
845
879
|
}
|
|
846
880
|
|
|
847
|
-
if (
|
|
848
|
-
|
|
881
|
+
if (kind === "vlan_interface") {
|
|
882
|
+
const vlan = (iface as VlanInterface).vlan;
|
|
883
|
+
if (!FortiOsRender.vlanParent(device, vlan))
|
|
884
|
+
found.push(`${where}, VLAN ${vlan} must be carried tagged by exactly one port or LAG`);
|
|
849
885
|
}
|
|
850
886
|
|
|
851
|
-
if (
|
|
887
|
+
if (kind === "loopback") {
|
|
852
888
|
if (iface.mtu !== undefined) found.push(`${where}.mtu, a loopback has no MTU of its own`);
|
|
853
889
|
if (iface.vrrp?.length) found.push(`${where}.vrrp, a loopback runs no VRRP`);
|
|
854
890
|
}
|
|
855
891
|
|
|
856
|
-
if (
|
|
857
|
-
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
if (
|
|
892
|
+
if (kind === "gre" || kind === "vxlan") {
|
|
893
|
+
const tunnel = iface as Gre | Vxlan;
|
|
894
|
+
if (!FortiOsNames.holderOf(device, tunnel.local_address))
|
|
895
|
+
found.push(`${where}.local_address, no interface holds ${tunnel.local_address}`);
|
|
896
|
+
if (Address.family(tunnel.local_address) !== Address.family(tunnel.remote_address))
|
|
897
|
+
found.push(`${where}, endpoints of two families`);
|
|
898
|
+
if (tunnel.vrrp?.length) found.push(`${where}.vrrp, on a tunnel`);
|
|
899
|
+
if (tunnel.ipv6_ra) found.push(`${where}.ipv6_ra, on a tunnel`);
|
|
861
900
|
}
|
|
862
901
|
|
|
863
|
-
if (
|
|
902
|
+
if (kind === "gre") {
|
|
864
903
|
for (const entry of iface.addresses ?? []) {
|
|
865
904
|
if (typeof entry !== "string" && Address.family(Address.host(entry.address)) === "ipv6")
|
|
866
905
|
found.push(`${where}.addresses, a peer on an IPv6 address`);
|
|
@@ -874,14 +913,15 @@ export class FortiOs extends Platform {
|
|
|
874
913
|
if (ipv4.length > 1) found.push(`${where}.addresses, one IPv4 address`);
|
|
875
914
|
}
|
|
876
915
|
|
|
877
|
-
found.push(...FortiOs.routedGaps(where, iface,
|
|
916
|
+
found.push(...FortiOs.routedGaps(where, iface, kind === "gre"));
|
|
878
917
|
}
|
|
879
918
|
|
|
880
919
|
// LLDP runs on ports and aggregates; a VLAN interface has no LLDP settings.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
920
|
+
const lldp = device.switching?.lldp;
|
|
921
|
+
if (lldp && typeof lldp === "object") {
|
|
922
|
+
for (const name of lldp.interfaces) {
|
|
923
|
+
const kind = FortiOsNames.interfacesOf(device).find((entry) => entry.name === name)?.kind;
|
|
924
|
+
if (kind && kind !== "lag") found.push(`switching.lldp.interfaces ${name}, a ${kind}`);
|
|
885
925
|
}
|
|
886
926
|
}
|
|
887
927
|
|
|
@@ -913,7 +953,7 @@ export class FortiOs extends Platform {
|
|
|
913
953
|
private static routedGaps(
|
|
914
954
|
where: string,
|
|
915
955
|
settings: {
|
|
916
|
-
addresses?: readonly (string | { address: string;
|
|
956
|
+
addresses?: readonly (string | { address: string; peer_address: string })[];
|
|
917
957
|
vrrp?: readonly { id: number; address: string }[];
|
|
918
958
|
},
|
|
919
959
|
tunnel: boolean
|
|
@@ -931,42 +971,57 @@ export class FortiOs extends Platform {
|
|
|
931
971
|
}
|
|
932
972
|
|
|
933
973
|
/** An ACL as the platform's, which only drop: every drop comes before any accept, and no rule limits a rate or matches a VLAN. */
|
|
934
|
-
private static aclGaps(where: string, rules: readonly AclRule
|
|
974
|
+
private static aclGaps(where: string, rules: readonly AclRule[]): string[] {
|
|
935
975
|
const found: string[] = [];
|
|
936
976
|
|
|
937
977
|
if (FortiOsFirewall.acceptsBeforeDrop(rules)) found.push(`${where}, an accept before a drop: the platform's ACLs only drop`);
|
|
938
978
|
|
|
939
979
|
rules.forEach((rule, index) => {
|
|
940
980
|
if (rule.rate) found.push(`${where}[${index}].rate`);
|
|
941
|
-
if (rule.
|
|
981
|
+
if (rule.if?.vlan) found.push(`${where}[${index}].if.vlan, a port's ACL sees no VLAN`);
|
|
982
|
+
found.push(...FortiOs.listGaps(`${where}[${index}]`, rule.if));
|
|
942
983
|
});
|
|
943
984
|
|
|
944
985
|
return found;
|
|
945
986
|
}
|
|
946
987
|
|
|
988
|
+
/** Lists in a rule's match where the platform takes one value, and interface sets: Circuit writes them out as one rule per value before the platform reads them. */
|
|
989
|
+
private static listGaps(where: string, match: ListedMatch | undefined): string[] {
|
|
990
|
+
const found: string[] = [];
|
|
991
|
+
|
|
992
|
+
for (const field of ["src", "dst", "in_interface", "out_interface"] as const) {
|
|
993
|
+
const value = match?.[field];
|
|
994
|
+
if (Array.isArray(value) && value.length > 1) found.push(`${where}.if.${field}, a list: the platform takes one value here`);
|
|
995
|
+
}
|
|
996
|
+
for (const field of ["in_interface_set", "out_interface_set"] as const) {
|
|
997
|
+
if (match?.[field] !== undefined) found.push(`${where}.if.${field}, which Circuit writes out as its interfaces`);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
return found;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
947
1003
|
private static staticGaps(device: Device): string[] {
|
|
948
1004
|
const found: string[] = [];
|
|
949
1005
|
const seen = new Set<string>();
|
|
950
1006
|
|
|
951
|
-
for (const route of device.routing?.
|
|
952
|
-
const where = `routing.
|
|
1007
|
+
for (const route of device.routing?.static_routes ?? []) {
|
|
1008
|
+
const where = `routing.static_routes ${route.prefix}`;
|
|
953
1009
|
|
|
954
|
-
if (route.vrf && route.bfd) found.push(`${where}.bfd, BFD in a VRF is not proved on a device yet`);
|
|
955
1010
|
if (route.vrf && route.blackhole)
|
|
956
1011
|
found.push(`${where}, a blackhole in a VRF: a route takes the VRF of the interface it leaves through`);
|
|
957
1012
|
if (route.vrf && route.interface && FortiOsNames.settingsOf(device, route.interface)?.vrf !== route.vrf) {
|
|
958
1013
|
found.push(`${where}.interface, which is not in VRF ${route.vrf}`);
|
|
959
1014
|
}
|
|
960
1015
|
|
|
961
|
-
if (route.
|
|
962
|
-
found.push(`${where}
|
|
1016
|
+
if (route.next_hop && !route.interface && !route.blackhole && !FortiOsNames.interfaceFor(device, route.next_hop, route.vrf)) {
|
|
1017
|
+
found.push(`${where} next_hop ${route.next_hop}, no interface's own network in its table holds the next hop`);
|
|
963
1018
|
}
|
|
964
1019
|
|
|
965
1020
|
// The device refuses a second route identical to one it has: `Duplicated entry`.
|
|
966
1021
|
const key = [
|
|
967
1022
|
Address.split(route.prefix).addr,
|
|
968
1023
|
Address.split(route.prefix).len,
|
|
969
|
-
route.
|
|
1024
|
+
route.next_hop,
|
|
970
1025
|
route.interface,
|
|
971
1026
|
route.blackhole,
|
|
972
1027
|
route.distance,
|
|
@@ -986,65 +1041,71 @@ export class FortiOs extends Platform {
|
|
|
986
1041
|
|
|
987
1042
|
if (fw.filter?.output) found.push("firewall.filter.output, the platform filters no traffic it sends itself");
|
|
988
1043
|
if (fw.untracked) found.push("firewall.untracked, every session is tracked");
|
|
1044
|
+
if (fw.interface_sets) found.push("firewall.interface_sets, which Circuit writes out as each rule's interfaces");
|
|
989
1045
|
|
|
990
|
-
for (const helper of fw.
|
|
991
|
-
if (!FortiOsRender.HELPERS[helper]) found.push(`firewall.
|
|
1046
|
+
for (const helper of fw.conntrack_helpers ?? []) {
|
|
1047
|
+
if (!FortiOsRender.HELPERS[helper]) found.push(`firewall.conntrack_helpers ${helper}, the platform has no helper for it`);
|
|
992
1048
|
}
|
|
993
1049
|
|
|
994
1050
|
for (const chain of ["forward", "input"] as const) {
|
|
995
1051
|
(fw.filter?.[chain]?.rules ?? []).forEach((rule, index) =>
|
|
996
|
-
found.push(...FortiOs.ruleGaps(`firewall.filter.${chain}[${index}]`, rule, chain, device))
|
|
1052
|
+
found.push(...FortiOs.ruleGaps(`firewall.filter.${chain}.rules[${index}]`, rule, chain, device))
|
|
997
1053
|
);
|
|
998
1054
|
}
|
|
999
1055
|
|
|
1000
1056
|
(fw.nat?.source ?? []).forEach((rule, index) => {
|
|
1001
1057
|
const where = `firewall.nat.source[${index}]`;
|
|
1002
|
-
const match = rule.
|
|
1058
|
+
const match = rule.if;
|
|
1059
|
+
found.push(...FortiOs.listGaps(where, match));
|
|
1003
1060
|
|
|
1004
1061
|
if (FortiOsFirewall.familiesOf(device, match).includes("ipv6") && (match?.family === "ipv6" || match?.src || match?.dst)) {
|
|
1005
1062
|
found.push(`${where}, IPv6: central NAT translates IPv4`);
|
|
1006
1063
|
}
|
|
1007
1064
|
|
|
1008
|
-
if (rule.
|
|
1009
|
-
if (rule.
|
|
1010
|
-
|
|
1011
|
-
if (
|
|
1012
|
-
if (
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1065
|
+
if (rule.then === "dnat") found.push(`${where}.then dnat, in source NAT`);
|
|
1066
|
+
if (rule.then === "snat" && Address.family(rule.to_address) === "ipv6")
|
|
1067
|
+
found.push(`${where}.to_address, IPv6: central NAT translates IPv4`);
|
|
1068
|
+
if (match?.dst_port !== undefined) found.push(`${where}.if.dst_port, central NAT matches the source port only`);
|
|
1069
|
+
if (Array.isArray(match?.src_port)) found.push(`${where}.if.src_port, one port or range`);
|
|
1070
|
+
if (rule.then === "snat" && rule.to_port !== undefined && match?.src_port === undefined)
|
|
1071
|
+
found.push(`${where}.to_port, without if.src_port`);
|
|
1072
|
+
if ((match?.src && match.src_address_set) || (match?.dst && match.dst_address_set))
|
|
1073
|
+
found.push(`${where}.if, a prefix and a set on one side`);
|
|
1074
|
+
if (match?.connection_state || match?.tcp_flags) found.push(`${where}.if.connection_state and tcp_flags`);
|
|
1017
1075
|
});
|
|
1018
1076
|
|
|
1019
1077
|
(fw.nat?.destination ?? []).forEach((rule, index) => {
|
|
1020
1078
|
const where = `firewall.nat.destination[${index}]`;
|
|
1021
|
-
const match = rule.
|
|
1079
|
+
const match = rule.if;
|
|
1080
|
+
found.push(...FortiOs.listGaps(where, match));
|
|
1022
1081
|
|
|
1023
|
-
if (rule.
|
|
1024
|
-
found.push(`${where}.
|
|
1082
|
+
if (rule.then !== "dnat") {
|
|
1083
|
+
found.push(`${where}.then ${rule.then}, in destination NAT`);
|
|
1025
1084
|
return;
|
|
1026
1085
|
}
|
|
1027
1086
|
|
|
1028
|
-
|
|
1029
|
-
|
|
1087
|
+
const dst = FortiOsNames.single(match?.dst);
|
|
1088
|
+
if (!dst || Address.split(dst.includes("/") ? dst : `${dst}/32`).len !== 32) {
|
|
1089
|
+
found.push(`${where}.if.dst, one IPv4 address the virtual IP answers on`);
|
|
1030
1090
|
}
|
|
1031
1091
|
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1092
|
+
const src = FortiOsNames.single(match?.src);
|
|
1093
|
+
const sources = src ? [src] : match?.src_address_set ? (fw.address_sets?.[match.src_address_set] ?? []) : [];
|
|
1094
|
+
if (Address.family(rule.to_address) === "ipv6" || sources.some((source) => Address.family(Address.host(source)) === "ipv6"))
|
|
1034
1095
|
found.push(`${where}, IPv6`);
|
|
1035
|
-
if (match?.dst_port !== undefined && Array.isArray(match.dst_port)) found.push(`${where}.
|
|
1096
|
+
if (match?.dst_port !== undefined && Array.isArray(match.dst_port)) found.push(`${where}.if.dst_port, one port or range`);
|
|
1036
1097
|
if (match?.dst_port !== undefined && match.protocol !== "tcp" && match.protocol !== "udp") {
|
|
1037
|
-
found.push(`${where}.
|
|
1098
|
+
found.push(`${where}.if.protocol, tcp or udp with a port`);
|
|
1038
1099
|
}
|
|
1039
1100
|
|
|
1040
1101
|
if (match?.dst_port === undefined && (match?.protocol || rule.to_port)) {
|
|
1041
|
-
found.push(`${where}.
|
|
1102
|
+
found.push(`${where}.if.dst_port, a protocol or port is forwarded by its port`);
|
|
1042
1103
|
}
|
|
1043
1104
|
|
|
1044
|
-
if (match?.src && match.
|
|
1105
|
+
if (match?.src && match.src_address_set) found.push(`${where}.if, a prefix and a set on one side`);
|
|
1045
1106
|
|
|
1046
|
-
for (const field of ["
|
|
1047
|
-
if (match?.[field] !== undefined) found.push(`${where}.
|
|
1107
|
+
for (const field of ["dst_address_set", "src_port", "out_interface", "connection_state", "tcp_flags", "family"] as const) {
|
|
1108
|
+
if (match?.[field] !== undefined) found.push(`${where}.if.${field}`);
|
|
1048
1109
|
}
|
|
1049
1110
|
});
|
|
1050
1111
|
|
|
@@ -1059,35 +1120,39 @@ export class FortiOs extends Platform {
|
|
|
1059
1120
|
}
|
|
1060
1121
|
|
|
1061
1122
|
/** A filter rule the platform cannot express. */
|
|
1062
|
-
private static ruleGaps(where: string, rule: FilterRule
|
|
1123
|
+
private static ruleGaps(where: string, rule: FilterRule, chain: "forward" | "input", device: Device): string[] {
|
|
1063
1124
|
const found: string[] = [];
|
|
1064
|
-
const match = rule.
|
|
1065
|
-
const state = match?.
|
|
1125
|
+
const match = rule.if;
|
|
1126
|
+
const state = match?.connection_state ?? [];
|
|
1066
1127
|
|
|
1067
1128
|
// The platform is stateful: replies pass on their own, and a rule matches a session's first packet.
|
|
1068
|
-
if (state.some((one) => one === "invalid" || one === "untracked")) found.push(`${where}.
|
|
1069
|
-
else if (state.length && rule.
|
|
1129
|
+
if (state.some((one) => one === "invalid" || one === "untracked")) found.push(`${where}.if.connection_state ${state.join(", ")}`);
|
|
1130
|
+
else if (state.length && rule.then !== "accept") found.push(`${where}.if.connection_state, on a rule that does not accept`);
|
|
1070
1131
|
|
|
1071
|
-
if (match?.tcp_flags) found.push(`${where}.
|
|
1072
|
-
if ((match?.src && match.
|
|
1132
|
+
if (match?.tcp_flags) found.push(`${where}.if.tcp_flags, the platform matches no TCP flags`);
|
|
1133
|
+
if ((match?.src && match.src_address_set) || (match?.dst && match.dst_address_set))
|
|
1134
|
+
found.push(`${where}.if, a prefix and a set on one side`);
|
|
1135
|
+
found.push(...FortiOs.listGaps(where, match));
|
|
1073
1136
|
if (FortiOsFirewall.isImplied(rule)) return found;
|
|
1074
1137
|
|
|
1075
1138
|
const families = FortiOsFirewall.familiesOf(device, match);
|
|
1076
1139
|
if (!families.length) found.push(`${where}, its addresses share no family`);
|
|
1077
1140
|
|
|
1078
|
-
|
|
1141
|
+
const src = FortiOsNames.single(match?.src);
|
|
1142
|
+
const dst = FortiOsNames.single(match?.dst);
|
|
1143
|
+
if (src && dst && Address.family(Address.host(src)) !== Address.family(Address.host(dst))) {
|
|
1079
1144
|
found.push(`${where}, a source and destination of two families`);
|
|
1080
1145
|
}
|
|
1081
1146
|
|
|
1082
1147
|
for (const family of families) {
|
|
1083
1148
|
if (FortiOsFirewall.serviceOf(match, family).name.length > FortiOs.NAME_LENGTH.object)
|
|
1084
|
-
found.push(`${where}.
|
|
1149
|
+
found.push(`${where}.if ports, too many for one service`);
|
|
1085
1150
|
}
|
|
1086
1151
|
|
|
1087
1152
|
if (chain === "input") {
|
|
1088
|
-
if (match?.out_interface) found.push(`${where}.
|
|
1089
|
-
if (rule.
|
|
1090
|
-
if (rule.
|
|
1153
|
+
if (match?.out_interface) found.push(`${where}.if.out_interface, on traffic to the device`);
|
|
1154
|
+
if (rule.then === "reject") found.push(`${where}.then reject, the platform drops traffic to itself silently`);
|
|
1155
|
+
if (rule.fast_path) found.push(`${where}.fast_path, on traffic to the device`);
|
|
1091
1156
|
}
|
|
1092
1157
|
|
|
1093
1158
|
return found;
|
|
@@ -1096,25 +1161,24 @@ export class FortiOs extends Platform {
|
|
|
1096
1161
|
private static serviceGaps(device: Device): string[] {
|
|
1097
1162
|
const found: string[] = [];
|
|
1098
1163
|
|
|
1099
|
-
for (const [name, server] of Object.entries(device.
|
|
1164
|
+
for (const [name, server] of Object.entries(device.services?.dhcp_server ?? {})) {
|
|
1100
1165
|
if (Address.family(Address.host(server.network)) === "ipv6")
|
|
1101
|
-
found.push(`
|
|
1166
|
+
found.push(`services.dhcp_server.${name}.network, IPv6: DHCPv6 hands out no gateway`);
|
|
1102
1167
|
if ((server.dns?.length ?? 0) > FortiOsRender.RESOLVERS.dhcp)
|
|
1103
|
-
found.push(`
|
|
1168
|
+
found.push(`services.dhcp_server.${name}.dns, at most ${FortiOsRender.RESOLVERS.dhcp}`);
|
|
1104
1169
|
}
|
|
1105
1170
|
|
|
1106
|
-
const flow = device.flow_export;
|
|
1107
|
-
if (flow?.protocol === "ipfix") found.push("flow_export.protocol ipfix, the platform exports sFlow and NetFlow 9");
|
|
1108
|
-
if (flow && flow.collectors.length > 1) found.push("flow_export.collectors, one collector");
|
|
1171
|
+
const flow = device.monitoring?.flow_export;
|
|
1172
|
+
if (flow?.protocol === "ipfix") found.push("monitoring.flow_export.protocol ipfix, the platform exports sFlow and NetFlow 9");
|
|
1173
|
+
if (flow && flow.collectors.length > 1) found.push("monitoring.flow_export.collectors, one collector");
|
|
1109
1174
|
for (const name of flow?.interfaces ?? []) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
found.push(`flow_export.interfaces ${name}, a tunnel: the platform samples the interface it leaves through`);
|
|
1175
|
+
if (FortiOsNames.isTunnel(device, name))
|
|
1176
|
+
found.push(`monitoring.flow_export.interfaces ${name}, a tunnel: the platform samples the interface it leaves through`);
|
|
1113
1177
|
}
|
|
1114
1178
|
if (flow?.protocol === "netflow-v9" && flow.sampling !== undefined)
|
|
1115
|
-
found.push("flow_export.sampling, NetFlow here exports every flow");
|
|
1179
|
+
found.push("monitoring.flow_export.sampling, NetFlow here exports every flow");
|
|
1116
1180
|
if (flow?.protocol === "sflow" && (flow.sampling === undefined || flow.sampling < 10 || flow.sampling > 99999)) {
|
|
1117
|
-
found.push("flow_export.sampling, required and from 10 to 99999: the platform samples");
|
|
1181
|
+
found.push("monitoring.flow_export.sampling, required and from 10 to 99999: the platform samples");
|
|
1118
1182
|
}
|
|
1119
1183
|
|
|
1120
1184
|
return found;
|