@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.
Files changed (71) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +86 -0
  3. package/package.json +60 -0
  4. package/src/constants.ts +77 -0
  5. package/src/engine/RuleExecutor.ts +256 -0
  6. package/src/engine/Runner.ts +312 -0
  7. package/src/engine/SandboxedExecutor.ts +208 -0
  8. package/src/errors.ts +88 -0
  9. package/src/helpers/arista/helpers.ts +1220 -0
  10. package/src/helpers/arista/index.ts +12 -0
  11. package/src/helpers/aruba/helpers.ts +637 -0
  12. package/src/helpers/aruba/index.ts +13 -0
  13. package/src/helpers/cisco/helpers.ts +534 -0
  14. package/src/helpers/cisco/index.ts +11 -0
  15. package/src/helpers/common/helpers.ts +265 -0
  16. package/src/helpers/common/index.ts +5 -0
  17. package/src/helpers/common/validation.ts +280 -0
  18. package/src/helpers/cumulus/helpers.ts +676 -0
  19. package/src/helpers/cumulus/index.ts +12 -0
  20. package/src/helpers/extreme/helpers.ts +422 -0
  21. package/src/helpers/extreme/index.ts +12 -0
  22. package/src/helpers/fortinet/helpers.ts +892 -0
  23. package/src/helpers/fortinet/index.ts +12 -0
  24. package/src/helpers/huawei/helpers.ts +790 -0
  25. package/src/helpers/huawei/index.ts +11 -0
  26. package/src/helpers/index.ts +53 -0
  27. package/src/helpers/juniper/helpers.ts +756 -0
  28. package/src/helpers/juniper/index.ts +12 -0
  29. package/src/helpers/mikrotik/helpers.ts +722 -0
  30. package/src/helpers/mikrotik/index.ts +12 -0
  31. package/src/helpers/nokia/helpers.ts +856 -0
  32. package/src/helpers/nokia/index.ts +11 -0
  33. package/src/helpers/paloalto/helpers.ts +939 -0
  34. package/src/helpers/paloalto/index.ts +12 -0
  35. package/src/helpers/vyos/helpers.ts +429 -0
  36. package/src/helpers/vyos/index.ts +12 -0
  37. package/src/index.ts +30 -0
  38. package/src/json-rules/ExpressionEvaluator.ts +292 -0
  39. package/src/json-rules/HelperRegistry.ts +177 -0
  40. package/src/json-rules/JsonRuleCompiler.ts +339 -0
  41. package/src/json-rules/JsonRuleValidator.ts +371 -0
  42. package/src/json-rules/index.ts +97 -0
  43. package/src/json-rules/schema.json +350 -0
  44. package/src/json-rules/types.ts +303 -0
  45. package/src/pack-loader/PackLoader.ts +332 -0
  46. package/src/pack-loader/index.ts +17 -0
  47. package/src/pack-loader/types.ts +135 -0
  48. package/src/parser/IncrementalParser.ts +527 -0
  49. package/src/parser/Sanitizer.ts +104 -0
  50. package/src/parser/SchemaAwareParser.ts +504 -0
  51. package/src/parser/VendorSchema.ts +72 -0
  52. package/src/parser/vendors/arista-eos.ts +206 -0
  53. package/src/parser/vendors/aruba-aoscx.ts +123 -0
  54. package/src/parser/vendors/aruba-aosswitch.ts +113 -0
  55. package/src/parser/vendors/aruba-wlc.ts +173 -0
  56. package/src/parser/vendors/cisco-ios.ts +110 -0
  57. package/src/parser/vendors/cisco-nxos.ts +107 -0
  58. package/src/parser/vendors/cumulus-linux.ts +161 -0
  59. package/src/parser/vendors/extreme-exos.ts +154 -0
  60. package/src/parser/vendors/extreme-voss.ts +167 -0
  61. package/src/parser/vendors/fortinet-fortigate.ts +217 -0
  62. package/src/parser/vendors/huawei-vrp.ts +192 -0
  63. package/src/parser/vendors/index.ts +1521 -0
  64. package/src/parser/vendors/juniper-junos.ts +230 -0
  65. package/src/parser/vendors/mikrotik-routeros.ts +274 -0
  66. package/src/parser/vendors/nokia-sros.ts +251 -0
  67. package/src/parser/vendors/paloalto-panos.ts +264 -0
  68. package/src/parser/vendors/vyos-vyos.ts +454 -0
  69. package/src/types/ConfigNode.ts +72 -0
  70. package/src/types/DeclarativeRule.ts +158 -0
  71. 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';