@sentriflow/core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +86 -0
- package/package.json +60 -0
- package/src/constants.ts +77 -0
- package/src/engine/RuleExecutor.ts +256 -0
- package/src/engine/Runner.ts +312 -0
- package/src/engine/SandboxedExecutor.ts +208 -0
- package/src/errors.ts +88 -0
- package/src/helpers/arista/helpers.ts +1220 -0
- package/src/helpers/arista/index.ts +12 -0
- package/src/helpers/aruba/helpers.ts +637 -0
- package/src/helpers/aruba/index.ts +13 -0
- package/src/helpers/cisco/helpers.ts +534 -0
- package/src/helpers/cisco/index.ts +11 -0
- package/src/helpers/common/helpers.ts +265 -0
- package/src/helpers/common/index.ts +5 -0
- package/src/helpers/common/validation.ts +280 -0
- package/src/helpers/cumulus/helpers.ts +676 -0
- package/src/helpers/cumulus/index.ts +12 -0
- package/src/helpers/extreme/helpers.ts +422 -0
- package/src/helpers/extreme/index.ts +12 -0
- package/src/helpers/fortinet/helpers.ts +892 -0
- package/src/helpers/fortinet/index.ts +12 -0
- package/src/helpers/huawei/helpers.ts +790 -0
- package/src/helpers/huawei/index.ts +11 -0
- package/src/helpers/index.ts +53 -0
- package/src/helpers/juniper/helpers.ts +756 -0
- package/src/helpers/juniper/index.ts +12 -0
- package/src/helpers/mikrotik/helpers.ts +722 -0
- package/src/helpers/mikrotik/index.ts +12 -0
- package/src/helpers/nokia/helpers.ts +856 -0
- package/src/helpers/nokia/index.ts +11 -0
- package/src/helpers/paloalto/helpers.ts +939 -0
- package/src/helpers/paloalto/index.ts +12 -0
- package/src/helpers/vyos/helpers.ts +429 -0
- package/src/helpers/vyos/index.ts +12 -0
- package/src/index.ts +30 -0
- package/src/json-rules/ExpressionEvaluator.ts +292 -0
- package/src/json-rules/HelperRegistry.ts +177 -0
- package/src/json-rules/JsonRuleCompiler.ts +339 -0
- package/src/json-rules/JsonRuleValidator.ts +371 -0
- package/src/json-rules/index.ts +97 -0
- package/src/json-rules/schema.json +350 -0
- package/src/json-rules/types.ts +303 -0
- package/src/pack-loader/PackLoader.ts +332 -0
- package/src/pack-loader/index.ts +17 -0
- package/src/pack-loader/types.ts +135 -0
- package/src/parser/IncrementalParser.ts +527 -0
- package/src/parser/Sanitizer.ts +104 -0
- package/src/parser/SchemaAwareParser.ts +504 -0
- package/src/parser/VendorSchema.ts +72 -0
- package/src/parser/vendors/arista-eos.ts +206 -0
- package/src/parser/vendors/aruba-aoscx.ts +123 -0
- package/src/parser/vendors/aruba-aosswitch.ts +113 -0
- package/src/parser/vendors/aruba-wlc.ts +173 -0
- package/src/parser/vendors/cisco-ios.ts +110 -0
- package/src/parser/vendors/cisco-nxos.ts +107 -0
- package/src/parser/vendors/cumulus-linux.ts +161 -0
- package/src/parser/vendors/extreme-exos.ts +154 -0
- package/src/parser/vendors/extreme-voss.ts +167 -0
- package/src/parser/vendors/fortinet-fortigate.ts +217 -0
- package/src/parser/vendors/huawei-vrp.ts +192 -0
- package/src/parser/vendors/index.ts +1521 -0
- package/src/parser/vendors/juniper-junos.ts +230 -0
- package/src/parser/vendors/mikrotik-routeros.ts +274 -0
- package/src/parser/vendors/nokia-sros.ts +251 -0
- package/src/parser/vendors/paloalto-panos.ts +264 -0
- package/src/parser/vendors/vyos-vyos.ts +454 -0
- package/src/types/ConfigNode.ts +72 -0
- package/src/types/DeclarativeRule.ts +158 -0
- package/src/types/IRule.ts +270 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
// packages/rule-helpers/src/cumulus/helpers.ts
|
|
2
|
+
// NVIDIA Cumulus Linux-specific helper functions
|
|
3
|
+
|
|
4
|
+
import type { ConfigNode } from '../../types/ConfigNode';
|
|
5
|
+
import { hasChildCommand, getChildCommand, parseIp, prefixToMask } from '../common/helpers';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a node represents an NCLU command (net add/del)
|
|
9
|
+
*/
|
|
10
|
+
export const isNcluCommand = (node: ConfigNode): boolean => {
|
|
11
|
+
return node.id.toLowerCase().startsWith('net ');
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if a node represents an NVUE command (nv set/unset)
|
|
16
|
+
*/
|
|
17
|
+
export const isNvueCommand = (node: ConfigNode): boolean => {
|
|
18
|
+
return node.id.toLowerCase().startsWith('nv ');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Check if a node represents an ifupdown2 interface stanza
|
|
23
|
+
*/
|
|
24
|
+
export const isIfaceStanza = (node: ConfigNode): boolean => {
|
|
25
|
+
return node.id.toLowerCase().startsWith('iface ');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if a node represents an auto interface stanza
|
|
30
|
+
*/
|
|
31
|
+
export const isAutoStanza = (node: ConfigNode): boolean => {
|
|
32
|
+
return node.id.toLowerCase().startsWith('auto ');
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if interface is a switch port (swpN)
|
|
37
|
+
*/
|
|
38
|
+
export const isSwitchPort = (interfaceName: string): boolean => {
|
|
39
|
+
const name = interfaceName.toLowerCase();
|
|
40
|
+
return /swp\d+/.test(name);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if interface is a bond interface
|
|
45
|
+
*/
|
|
46
|
+
export const isBondInterface = (interfaceName: string): boolean => {
|
|
47
|
+
const name = interfaceName.toLowerCase();
|
|
48
|
+
return /bond\d+/.test(name);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if interface is a bridge interface
|
|
53
|
+
*/
|
|
54
|
+
export const isBridgeInterface = (interfaceName: string): boolean => {
|
|
55
|
+
const name = interfaceName.toLowerCase();
|
|
56
|
+
return name.includes('bridge') || name === 'br_default' || /^br\d+$/.test(name);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if interface is a VLAN interface (SVI)
|
|
61
|
+
*/
|
|
62
|
+
export const isVlanInterface = (interfaceName: string): boolean => {
|
|
63
|
+
const name = interfaceName.toLowerCase();
|
|
64
|
+
return /vlan\d+/.test(name) || /_vlan\d+$/.test(name);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if interface is the management interface
|
|
69
|
+
*/
|
|
70
|
+
export const isManagementInterface = (interfaceName: string): boolean => {
|
|
71
|
+
const name = interfaceName.toLowerCase();
|
|
72
|
+
return name === 'eth0' || name === 'mgmt';
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if interface is a loopback
|
|
77
|
+
*/
|
|
78
|
+
export const isLoopback = (interfaceName: string): boolean => {
|
|
79
|
+
const name = interfaceName.toLowerCase();
|
|
80
|
+
return name === 'lo' || name.startsWith('loopback');
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if interface is a peerlink (MLAG)
|
|
85
|
+
*/
|
|
86
|
+
export const isPeerlink = (interfaceName: string): boolean => {
|
|
87
|
+
const name = interfaceName.toLowerCase();
|
|
88
|
+
return name.includes('peerlink');
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Check if an iface stanza has VLAN-aware bridge configuration
|
|
93
|
+
*/
|
|
94
|
+
export const isVlanAwareBridge = (node: ConfigNode): boolean => {
|
|
95
|
+
return node.children.some((child) =>
|
|
96
|
+
child.id.toLowerCase().includes('bridge-vlan-aware') &&
|
|
97
|
+
child.id.toLowerCase().includes('yes')
|
|
98
|
+
);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get interface name from an iface or auto stanza
|
|
103
|
+
*/
|
|
104
|
+
export const getInterfaceName = (node: ConfigNode): string => {
|
|
105
|
+
const parts = node.id.split(/\s+/);
|
|
106
|
+
return parts[1] || node.id;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Check if interface has an IP address configured
|
|
111
|
+
*/
|
|
112
|
+
export const hasAddress = (node: ConfigNode): boolean => {
|
|
113
|
+
return node.children.some((child) =>
|
|
114
|
+
child.id.toLowerCase().startsWith('address ')
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Check if interface has a description/alias configured
|
|
120
|
+
*/
|
|
121
|
+
export const hasDescription = (node: ConfigNode): boolean => {
|
|
122
|
+
return node.children.some((child) =>
|
|
123
|
+
child.id.toLowerCase().startsWith('alias ')
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check if bridge has bridge-ports configured
|
|
129
|
+
*/
|
|
130
|
+
export const hasBridgePorts = (node: ConfigNode): boolean => {
|
|
131
|
+
return node.children.some((child) =>
|
|
132
|
+
child.id.toLowerCase().startsWith('bridge-ports ')
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if bridge has bridge-vids (VLANs) configured
|
|
138
|
+
*/
|
|
139
|
+
export const hasBridgeVids = (node: ConfigNode): boolean => {
|
|
140
|
+
return node.children.some((child) =>
|
|
141
|
+
child.id.toLowerCase().startsWith('bridge-vids ')
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if interface has MTU configured
|
|
147
|
+
*/
|
|
148
|
+
export const hasMtu = (node: ConfigNode): boolean => {
|
|
149
|
+
return node.children.some((child) =>
|
|
150
|
+
child.id.toLowerCase().startsWith('mtu ')
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Check if interface has link-speed configured
|
|
156
|
+
*/
|
|
157
|
+
export const hasLinkSpeed = (node: ConfigNode): boolean => {
|
|
158
|
+
return node.children.some((child) =>
|
|
159
|
+
child.id.toLowerCase().startsWith('link-speed ')
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check if bond has bond-slaves configured
|
|
165
|
+
*/
|
|
166
|
+
export const hasBondSlaves = (node: ConfigNode): boolean => {
|
|
167
|
+
return node.children.some((child) =>
|
|
168
|
+
child.id.toLowerCase().startsWith('bond-slaves ')
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if bond has clag-id configured
|
|
174
|
+
*/
|
|
175
|
+
export const hasClagId = (node: ConfigNode): boolean => {
|
|
176
|
+
return node.children.some((child) =>
|
|
177
|
+
child.id.toLowerCase().startsWith('clag-id ')
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Check if interface has STP bpdu-guard enabled
|
|
183
|
+
*/
|
|
184
|
+
export const hasBpduGuard = (node: ConfigNode): boolean => {
|
|
185
|
+
return node.children.some((child) =>
|
|
186
|
+
child.id.toLowerCase().includes('bpduguard') &&
|
|
187
|
+
child.id.toLowerCase().includes('yes')
|
|
188
|
+
);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Check if interface has STP portadminedge (portfast equivalent)
|
|
193
|
+
*/
|
|
194
|
+
export const hasPortAdminEdge = (node: ConfigNode): boolean => {
|
|
195
|
+
return node.children.some((child) =>
|
|
196
|
+
child.id.toLowerCase().includes('portadminedge') &&
|
|
197
|
+
child.id.toLowerCase().includes('yes')
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Find all iface stanzas in a configuration tree
|
|
203
|
+
*/
|
|
204
|
+
export const findIfaceStanzas = (root: ConfigNode): ConfigNode[] => {
|
|
205
|
+
const result: ConfigNode[] = [];
|
|
206
|
+
const traverse = (node: ConfigNode) => {
|
|
207
|
+
if (isIfaceStanza(node)) {
|
|
208
|
+
result.push(node);
|
|
209
|
+
}
|
|
210
|
+
for (const child of node.children) {
|
|
211
|
+
traverse(child);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
traverse(root);
|
|
215
|
+
return result;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Find a stanza by name within a node's children
|
|
220
|
+
*/
|
|
221
|
+
export const findStanza = (
|
|
222
|
+
node: ConfigNode,
|
|
223
|
+
stanzaName: string
|
|
224
|
+
): ConfigNode | undefined => {
|
|
225
|
+
return node.children.find(
|
|
226
|
+
(child) => child.id.toLowerCase() === stanzaName.toLowerCase()
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Find all stanzas starting with a prefix
|
|
232
|
+
*/
|
|
233
|
+
export const findStanzasByPrefix = (node: ConfigNode, prefix: string): ConfigNode[] => {
|
|
234
|
+
return node.children.filter((child) =>
|
|
235
|
+
child.id.toLowerCase().startsWith(prefix.toLowerCase())
|
|
236
|
+
);
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parse Cumulus address format (e.g., "10.0.0.1/24")
|
|
241
|
+
*/
|
|
242
|
+
export const parseCumulusAddress = (
|
|
243
|
+
address: string
|
|
244
|
+
): { ip: number; prefix: number; mask: number } | null => {
|
|
245
|
+
const parts = address.split('/');
|
|
246
|
+
if (parts.length !== 2) return null;
|
|
247
|
+
const [ipStr, prefixStr] = parts;
|
|
248
|
+
if (!ipStr || !prefixStr) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const ip = parseIp(ipStr);
|
|
253
|
+
const prefix = parseInt(prefixStr, 10);
|
|
254
|
+
|
|
255
|
+
if (ip === null || isNaN(prefix) || prefix < 0 || prefix > 32) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
ip,
|
|
261
|
+
prefix,
|
|
262
|
+
mask: prefixToMask(prefix),
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Check if a router bgp block has router-id configured
|
|
268
|
+
*/
|
|
269
|
+
export const hasBgpRouterId = (node: ConfigNode): boolean => {
|
|
270
|
+
return node.children.some((child) =>
|
|
271
|
+
child.id.toLowerCase().startsWith('bgp router-id ')
|
|
272
|
+
);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Check if a router bgp block has neighbors configured
|
|
277
|
+
*/
|
|
278
|
+
export const hasBgpNeighbors = (node: ConfigNode): boolean => {
|
|
279
|
+
return node.children.some((child) =>
|
|
280
|
+
child.id.toLowerCase().startsWith('neighbor ')
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Get BGP neighbor address/interface from a neighbor command
|
|
286
|
+
*/
|
|
287
|
+
export const getBgpNeighborAddress = (neighborCmd: string): string => {
|
|
288
|
+
const parts = neighborCmd.split(/\s+/);
|
|
289
|
+
// Format: "neighbor <addr|interface> ..."
|
|
290
|
+
return parts[1] || '';
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Check if CLAG/MLAG is configured in an interface
|
|
295
|
+
*/
|
|
296
|
+
export const hasClagConfig = (node: ConfigNode): boolean => {
|
|
297
|
+
return node.children.some((child) =>
|
|
298
|
+
child.id.toLowerCase().includes('clag')
|
|
299
|
+
);
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Check if EVPN is configured
|
|
304
|
+
*/
|
|
305
|
+
export const hasEvpnConfig = (node: ConfigNode): boolean => {
|
|
306
|
+
return node.children.some((child) =>
|
|
307
|
+
child.id.toLowerCase().includes('l2vpn evpn') ||
|
|
308
|
+
child.id.toLowerCase().includes('advertise-all-vni')
|
|
309
|
+
);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Management Plane Helpers
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Check if management interface is in management VRF
|
|
318
|
+
* After the CUMULUS_FIX.md parser fix, vrf mgmt is correctly parsed as a child command of iface stanzas.
|
|
319
|
+
*/
|
|
320
|
+
export const hasManagementVrf = (node: ConfigNode): boolean => {
|
|
321
|
+
return node.children.some((child) =>
|
|
322
|
+
child.id.toLowerCase().includes('vrf mgmt') ||
|
|
323
|
+
child.id.toLowerCase() === 'vrf mgmt'
|
|
324
|
+
);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Check if a VRF stanza is management VRF
|
|
329
|
+
*/
|
|
330
|
+
export const isManagementVrf = (interfaceName: string): boolean => {
|
|
331
|
+
return interfaceName.toLowerCase() === 'mgmt';
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// ============================================================================
|
|
335
|
+
// MLAG/CLAG Helpers
|
|
336
|
+
// ============================================================================
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Check if peerlink.4094 sub-interface for CLAG control
|
|
340
|
+
*/
|
|
341
|
+
export const isPeerlinkSubinterface = (interfaceName: string): boolean => {
|
|
342
|
+
return interfaceName.toLowerCase().includes('peerlink.4094');
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Check if clagd-peer-ip is configured
|
|
347
|
+
*/
|
|
348
|
+
export const hasClagdPeerIp = (node: ConfigNode): boolean => {
|
|
349
|
+
return node.children.some((child) =>
|
|
350
|
+
child.id.toLowerCase().startsWith('clagd-peer-ip ')
|
|
351
|
+
);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check if clagd-backup-ip is configured
|
|
356
|
+
*/
|
|
357
|
+
export const hasClagdBackupIp = (node: ConfigNode): boolean => {
|
|
358
|
+
return node.children.some((child) =>
|
|
359
|
+
child.id.toLowerCase().startsWith('clagd-backup-ip ')
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check if clagd-sys-mac is configured
|
|
365
|
+
*/
|
|
366
|
+
export const hasClagdSysMac = (node: ConfigNode): boolean => {
|
|
367
|
+
return node.children.some((child) =>
|
|
368
|
+
child.id.toLowerCase().startsWith('clagd-sys-mac ')
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check if clagd-priority is configured
|
|
374
|
+
*/
|
|
375
|
+
export const hasClagdPriority = (node: ConfigNode): boolean => {
|
|
376
|
+
return node.children.some((child) =>
|
|
377
|
+
child.id.toLowerCase().startsWith('clagd-priority ')
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Validate clagd-sys-mac is in reserved range 44:38:39:ff:xx:xx
|
|
383
|
+
*/
|
|
384
|
+
export const isValidClagdSysMac = (node: ConfigNode): boolean => {
|
|
385
|
+
const sysMacCmd = node.children.find((child) =>
|
|
386
|
+
child.id.toLowerCase().startsWith('clagd-sys-mac ')
|
|
387
|
+
);
|
|
388
|
+
if (!sysMacCmd) return false;
|
|
389
|
+
|
|
390
|
+
const match = sysMacCmd.id.match(/clagd-sys-mac\s+([0-9a-fA-F:]+)/i);
|
|
391
|
+
if (!match?.[1]) return false;
|
|
392
|
+
|
|
393
|
+
const mac = match[1].toLowerCase();
|
|
394
|
+
// Reserved range: 44:38:39:ff:00:00 to 44:38:39:ff:ff:ff
|
|
395
|
+
// Also accept 44:38:39:be:ef:xx for legacy
|
|
396
|
+
return mac.startsWith('44:38:39:ff:') || mac.startsWith('44:38:39:be:ef:');
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Check if VRR (Virtual Router Redundancy) is configured
|
|
401
|
+
*/
|
|
402
|
+
export const hasVrrConfig = (node: ConfigNode): boolean => {
|
|
403
|
+
return node.children.some((child) =>
|
|
404
|
+
child.id.toLowerCase().startsWith('address-virtual ')
|
|
405
|
+
);
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// ============================================================================
|
|
409
|
+
// VLAN Helpers
|
|
410
|
+
// ============================================================================
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Get bridge-access VLAN ID from interface
|
|
414
|
+
*/
|
|
415
|
+
export const getBridgeAccessVlan = (node: ConfigNode): number | null => {
|
|
416
|
+
const accessCmd = node.children.find((child) =>
|
|
417
|
+
child.id.toLowerCase().startsWith('bridge-access ')
|
|
418
|
+
);
|
|
419
|
+
if (!accessCmd) return null;
|
|
420
|
+
|
|
421
|
+
const match = accessCmd.id.match(/bridge-access\s+(\d+)/i);
|
|
422
|
+
if (!match?.[1]) return null;
|
|
423
|
+
|
|
424
|
+
return parseInt(match[1], 10);
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get bridge-vids VLANs from bridge interface
|
|
429
|
+
*/
|
|
430
|
+
export const getBridgeVids = (node: ConfigNode): number[] => {
|
|
431
|
+
const vidsCmd = node.children.find((child) =>
|
|
432
|
+
child.id.toLowerCase().startsWith('bridge-vids ')
|
|
433
|
+
);
|
|
434
|
+
if (!vidsCmd) return [];
|
|
435
|
+
|
|
436
|
+
const match = vidsCmd.id.match(/bridge-vids\s+(.+)/i);
|
|
437
|
+
if (!match?.[1]) return [];
|
|
438
|
+
|
|
439
|
+
return match[1]
|
|
440
|
+
.split(/\s+/)
|
|
441
|
+
.map((v) => parseInt(v, 10))
|
|
442
|
+
.filter((v) => !isNaN(v));
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Get bridge-pvid (native VLAN) from bridge interface
|
|
447
|
+
*/
|
|
448
|
+
export const getBridgePvid = (node: ConfigNode): number | null => {
|
|
449
|
+
const pvidCmd = node.children.find((child) =>
|
|
450
|
+
child.id.toLowerCase().startsWith('bridge-pvid ')
|
|
451
|
+
);
|
|
452
|
+
if (!pvidCmd) return null;
|
|
453
|
+
|
|
454
|
+
const match = pvidCmd.id.match(/bridge-pvid\s+(\d+)/i);
|
|
455
|
+
if (!match?.[1]) return null;
|
|
456
|
+
|
|
457
|
+
return parseInt(match[1], 10);
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// ============================================================================
|
|
461
|
+
// VNI/VXLAN Helpers
|
|
462
|
+
// ============================================================================
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check if interface is a VNI (VXLAN) interface
|
|
466
|
+
*/
|
|
467
|
+
export const isVniInterface = (interfaceName: string): boolean => {
|
|
468
|
+
return /^vni\d+$/i.test(interfaceName) || /^vni[a-zA-Z]+$/i.test(interfaceName);
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Check if vxlan-local-tunnelip is configured on loopback
|
|
473
|
+
*/
|
|
474
|
+
export const hasVxlanLocalTunnelip = (node: ConfigNode): boolean => {
|
|
475
|
+
return node.children.some((child) =>
|
|
476
|
+
child.id.toLowerCase().startsWith('vxlan-local-tunnelip ')
|
|
477
|
+
);
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Check if clagd-vxlan-anycast-ip is configured for MLAG+VXLAN
|
|
482
|
+
*/
|
|
483
|
+
export const hasVxlanAnycastIp = (node: ConfigNode): boolean => {
|
|
484
|
+
return node.children.some((child) =>
|
|
485
|
+
child.id.toLowerCase().startsWith('clagd-vxlan-anycast-ip ')
|
|
486
|
+
);
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Check if bridge-arp-nd-suppress is enabled on VNI
|
|
491
|
+
*/
|
|
492
|
+
export const hasArpNdSuppress = (node: ConfigNode): boolean => {
|
|
493
|
+
return node.children.some((child) =>
|
|
494
|
+
child.id.toLowerCase().includes('bridge-arp-nd-suppress') &&
|
|
495
|
+
child.id.toLowerCase().includes('on')
|
|
496
|
+
);
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Check if bridge-learning is disabled on VNI
|
|
501
|
+
*/
|
|
502
|
+
export const hasBridgeLearningOff = (node: ConfigNode): boolean => {
|
|
503
|
+
return node.children.some((child) =>
|
|
504
|
+
child.id.toLowerCase().includes('bridge-learning') &&
|
|
505
|
+
child.id.toLowerCase().includes('off')
|
|
506
|
+
);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Check if mstpctl-portbpdufilter is enabled on VNI
|
|
511
|
+
*/
|
|
512
|
+
export const hasPortBpduFilter = (node: ConfigNode): boolean => {
|
|
513
|
+
return node.children.some((child) =>
|
|
514
|
+
child.id.toLowerCase().includes('portbpdufilter') &&
|
|
515
|
+
child.id.toLowerCase().includes('yes')
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Check if vxlan-id is configured
|
|
521
|
+
*/
|
|
522
|
+
export const hasVxlanId = (node: ConfigNode): boolean => {
|
|
523
|
+
return node.children.some((child) =>
|
|
524
|
+
child.id.toLowerCase().startsWith('vxlan-id ')
|
|
525
|
+
);
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// ============================================================================
|
|
529
|
+
// BGP Helpers
|
|
530
|
+
// ============================================================================
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Check if BGP authentication (password) is configured for a neighbor
|
|
534
|
+
*/
|
|
535
|
+
export const hasBgpNeighborPassword = (node: ConfigNode, neighborAddr: string): boolean => {
|
|
536
|
+
return node.children.some((child) => {
|
|
537
|
+
const id = child.id.toLowerCase();
|
|
538
|
+
return id.startsWith(`neighbor ${neighborAddr.toLowerCase()} password`);
|
|
539
|
+
});
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Check if BGP peer-group has password configured
|
|
544
|
+
*/
|
|
545
|
+
export const hasBgpPeerGroupPassword = (node: ConfigNode, peerGroup: string): boolean => {
|
|
546
|
+
return node.children.some((child) => {
|
|
547
|
+
const id = child.id.toLowerCase();
|
|
548
|
+
return id.startsWith(`neighbor ${peerGroup.toLowerCase()} password`);
|
|
549
|
+
});
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Check if BGP maximum-prefix is configured for neighbor
|
|
554
|
+
*/
|
|
555
|
+
export const hasBgpMaximumPrefix = (node: ConfigNode, neighborAddr: string): boolean => {
|
|
556
|
+
return node.children.some((child) => {
|
|
557
|
+
const id = child.id.toLowerCase();
|
|
558
|
+
return id.includes(`neighbor ${neighborAddr.toLowerCase()}`) && id.includes('maximum-prefix');
|
|
559
|
+
});
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Check if BFD is enabled for BGP neighbor
|
|
564
|
+
*/
|
|
565
|
+
export const hasBgpBfd = (node: ConfigNode): boolean => {
|
|
566
|
+
return node.children.some((child) =>
|
|
567
|
+
child.id.toLowerCase().includes(' bfd')
|
|
568
|
+
);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Check if BGP multipath is configured
|
|
573
|
+
*/
|
|
574
|
+
export const hasBgpMultipath = (node: ConfigNode): boolean => {
|
|
575
|
+
return node.children.some((child) =>
|
|
576
|
+
child.id.toLowerCase().includes('bgp bestpath as-path multipath-relax') ||
|
|
577
|
+
child.id.toLowerCase().includes('maximum-paths')
|
|
578
|
+
);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Get BGP peer groups from router bgp block
|
|
583
|
+
*/
|
|
584
|
+
export const getBgpPeerGroups = (node: ConfigNode): string[] => {
|
|
585
|
+
const groups: string[] = [];
|
|
586
|
+
for (const child of node.children) {
|
|
587
|
+
const match = child.id.match(/neighbor\s+(\S+)\s+peer-group\s*$/i);
|
|
588
|
+
if (match?.[1]) {
|
|
589
|
+
groups.push(match[1]);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return groups;
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Check if prefix-list is applied to BGP neighbor (inbound)
|
|
597
|
+
*/
|
|
598
|
+
export const hasBgpPrefixListIn = (node: ConfigNode, neighborOrGroup: string): boolean => {
|
|
599
|
+
return node.children.some((child) => {
|
|
600
|
+
const id = child.id.toLowerCase();
|
|
601
|
+
return (
|
|
602
|
+
id.includes(`neighbor ${neighborOrGroup.toLowerCase()}`) &&
|
|
603
|
+
id.includes('prefix-list') &&
|
|
604
|
+
id.includes(' in')
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// ============================================================================
|
|
610
|
+
// Interface MTU Helpers
|
|
611
|
+
// ============================================================================
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get MTU value from interface
|
|
615
|
+
*/
|
|
616
|
+
export const getMtu = (node: ConfigNode): number | null => {
|
|
617
|
+
const mtuCmd = node.children.find((child) =>
|
|
618
|
+
child.id.toLowerCase().startsWith('mtu ')
|
|
619
|
+
);
|
|
620
|
+
if (!mtuCmd) return null;
|
|
621
|
+
|
|
622
|
+
const match = mtuCmd.id.match(/mtu\s+(\d+)/i);
|
|
623
|
+
if (!match?.[1]) return null;
|
|
624
|
+
|
|
625
|
+
return parseInt(match[1], 10);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Check if interface is an uplink (swp5x pattern common for uplinks)
|
|
630
|
+
*/
|
|
631
|
+
export const isUplinkInterface = (interfaceName: string): boolean => {
|
|
632
|
+
const name = interfaceName.toLowerCase();
|
|
633
|
+
// Common patterns: swp51, swp52, swp53, swp54 for uplinks to spine
|
|
634
|
+
return /swp5[0-9]/.test(name);
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// ============================================================================
|
|
638
|
+
// Storm Control Helpers
|
|
639
|
+
// ============================================================================
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Check if storm control is configured on interface
|
|
643
|
+
*/
|
|
644
|
+
export const hasStormControl = (node: ConfigNode): boolean => {
|
|
645
|
+
return node.children.some((child) =>
|
|
646
|
+
child.id.toLowerCase().includes('storm-control')
|
|
647
|
+
);
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
// ============================================================================
|
|
651
|
+
// Port Isolation Helpers
|
|
652
|
+
// ============================================================================
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Check if bridge-port-isolation is enabled
|
|
656
|
+
*/
|
|
657
|
+
export const hasPortIsolation = (node: ConfigNode): boolean => {
|
|
658
|
+
return node.children.some((child) =>
|
|
659
|
+
child.id.toLowerCase().includes('bridge-port-isolation') &&
|
|
660
|
+
child.id.toLowerCase().includes('on')
|
|
661
|
+
);
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
// ============================================================================
|
|
665
|
+
// Root Guard Helpers
|
|
666
|
+
// ============================================================================
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Check if root guard (portrestrictedtcn) is enabled
|
|
670
|
+
*/
|
|
671
|
+
export const hasRootGuard = (node: ConfigNode): boolean => {
|
|
672
|
+
return node.children.some((child) =>
|
|
673
|
+
child.id.toLowerCase().includes('portrestrictedtcn') &&
|
|
674
|
+
child.id.toLowerCase().includes('yes')
|
|
675
|
+
);
|
|
676
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// packages/rule-helpers/src/cumulus/index.ts
|
|
2
|
+
// Re-export all Cumulus Linux helpers
|
|
3
|
+
|
|
4
|
+
export * from './helpers';
|
|
5
|
+
|
|
6
|
+
// Also re-export commonly used common helpers for convenience
|
|
7
|
+
export {
|
|
8
|
+
hasChildCommand,
|
|
9
|
+
getChildCommand,
|
|
10
|
+
getChildCommands,
|
|
11
|
+
parseIp,
|
|
12
|
+
} from '../common/helpers';
|