@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,856 @@
1
+ // packages/rule-helpers/src/nokia/helpers.ts
2
+ // Nokia SR OS specific helper functions
3
+
4
+ import type { ConfigNode } from '../../types/ConfigNode';
5
+ import { hasChildCommand, getChildCommand, getChildCommands } from '../common/helpers';
6
+
7
+ // Re-export common helpers for convenience
8
+ export { hasChildCommand, getChildCommand, getChildCommands } from '../common/helpers';
9
+
10
+ /**
11
+ * Check if admin-state is enabled (admin-state enable or admin-state up)
12
+ * Nokia SR OS uses admin-state for enabling/disabling most components
13
+ */
14
+ export const isAdminStateEnabled = (node: ConfigNode): boolean => {
15
+ // Check direct children first (more common case)
16
+ const directCheck = node.children.some((child) => {
17
+ const rawText = child.rawText.toLowerCase().trim();
18
+ return rawText === 'admin-state enable' || rawText === 'admin-state up';
19
+ });
20
+ if (directCheck) return true;
21
+
22
+ // Also check if admin-state is in the node's own rawText (for compact configs)
23
+ const nodeText = node.rawText.toLowerCase();
24
+ return nodeText.includes('admin-state enable') || nodeText.includes('admin-state up');
25
+ };
26
+
27
+ /**
28
+ * Check if admin-state is disabled (admin-state disable or no admin-state command)
29
+ */
30
+ export const isAdminStateDisabled = (node: ConfigNode): boolean => {
31
+ // Check direct children first
32
+ const directCheck = node.children.some((child) => {
33
+ const rawText = child.rawText.toLowerCase().trim();
34
+ return rawText === 'admin-state disable';
35
+ });
36
+ if (directCheck) return true;
37
+
38
+ // Also check node's own rawText
39
+ return node.rawText.toLowerCase().includes('admin-state disable');
40
+ };
41
+
42
+ /**
43
+ * Check if component is shutdown (has shutdown command)
44
+ */
45
+ export const isShutdown = (node: ConfigNode): boolean => {
46
+ return node.children.some((child) => {
47
+ const rawText = child.rawText.toLowerCase().trim();
48
+ return rawText === 'shutdown';
49
+ });
50
+ };
51
+
52
+ /**
53
+ * Check if interface/component is enabled (has admin-state enable and no shutdown)
54
+ */
55
+ export const isEnabled = (node: ConfigNode): boolean => {
56
+ const hasAdminStateEnabled = isAdminStateEnabled(node);
57
+ const hasShutdown = isShutdown(node);
58
+ return hasAdminStateEnabled && !hasShutdown;
59
+ };
60
+
61
+ /**
62
+ * Check if a port is a physical port (not LAG, loopback, or system)
63
+ * Nokia port format: slot/mda/port (e.g., 1/1/1, 1/2/3)
64
+ */
65
+ export const isPhysicalPort = (portName: string): boolean => {
66
+ const name = portName.toLowerCase();
67
+ // Match slot/mda/port pattern
68
+ return /^\d+\/\d+\/\d+/.test(name);
69
+ };
70
+
71
+ /**
72
+ * Check if a port is a LAG (Link Aggregation Group)
73
+ * Nokia LAG format: lag-N or lag N
74
+ */
75
+ export const isLagPort = (portName: string): boolean => {
76
+ const name = portName.toLowerCase();
77
+ return name.includes('lag');
78
+ };
79
+
80
+ /**
81
+ * Check if interface is a system interface (loopback, system)
82
+ */
83
+ export const isSystemInterface = (interfaceName: string): boolean => {
84
+ const name = interfaceName.toLowerCase();
85
+ return (
86
+ name.includes('system') ||
87
+ name.includes('loopback') ||
88
+ name === '"system"'
89
+ );
90
+ };
91
+
92
+ /**
93
+ * Get port mode (network or access)
94
+ */
95
+ export const getPortMode = (node: ConfigNode): 'network' | 'access' | undefined => {
96
+ // Look for ethernet mode configuration
97
+ const ethernetNode = node.children.find((child) =>
98
+ child.id.toLowerCase() === 'ethernet'
99
+ );
100
+
101
+ if (ethernetNode) {
102
+ const modeCmd = ethernetNode.children.find((child) => {
103
+ const rawText = child.rawText.toLowerCase().trim();
104
+ return rawText.startsWith('mode');
105
+ });
106
+
107
+ if (modeCmd) {
108
+ if (modeCmd.rawText.toLowerCase().includes('network')) {
109
+ return 'network';
110
+ }
111
+ if (modeCmd.rawText.toLowerCase().includes('access')) {
112
+ return 'access';
113
+ }
114
+ }
115
+ }
116
+ return undefined;
117
+ };
118
+
119
+ /**
120
+ * Check if port is in network mode
121
+ */
122
+ export const isNetworkPort = (node: ConfigNode): boolean => {
123
+ return getPortMode(node) === 'network';
124
+ };
125
+
126
+ /**
127
+ * Check if port is in access mode
128
+ */
129
+ export const isAccessPort = (node: ConfigNode): boolean => {
130
+ return getPortMode(node) === 'access';
131
+ };
132
+
133
+ /**
134
+ * Check if port has description configured
135
+ */
136
+ export const hasDescription = (node: ConfigNode): boolean => {
137
+ return hasChildCommand(node, 'description');
138
+ };
139
+
140
+ /**
141
+ * Get port/interface description
142
+ */
143
+ export const getDescription = (node: ConfigNode): string | undefined => {
144
+ const descCmd = getChildCommand(node, 'description');
145
+ if (descCmd) {
146
+ // Nokia descriptions are often quoted
147
+ const match = descCmd.rawText.match(/description\s+"([^"]+)"|description\s+(\S+)/i);
148
+ if (match) {
149
+ return match[1] || match[2];
150
+ }
151
+ }
152
+ return undefined;
153
+ };
154
+
155
+ /**
156
+ * Get system name from system block
157
+ */
158
+ export const getSystemName = (node: ConfigNode): string | undefined => {
159
+ const nameCmd = node.children.find((child) => {
160
+ return child.id.toLowerCase().startsWith('name');
161
+ });
162
+
163
+ if (nameCmd) {
164
+ // Nokia system name is quoted: name "Router-Name"
165
+ const match = nameCmd.rawText.match(/name\s+"([^"]+)"/i);
166
+ if (match) {
167
+ return match[1];
168
+ }
169
+ }
170
+ return undefined;
171
+ };
172
+
173
+ /**
174
+ * Check if interface has IP address configured
175
+ */
176
+ export const hasIpAddress = (node: ConfigNode): boolean => {
177
+ return node.children.some((child) => {
178
+ const rawText = child.rawText.toLowerCase().trim();
179
+ return rawText.startsWith('address');
180
+ });
181
+ };
182
+
183
+ /**
184
+ * Get interface IP address
185
+ */
186
+ export const getIpAddress = (node: ConfigNode): string | undefined => {
187
+ const addrCmd = node.children.find((child) => {
188
+ const rawText = child.rawText.toLowerCase().trim();
189
+ return rawText.startsWith('address');
190
+ });
191
+
192
+ if (addrCmd) {
193
+ // Match IPv4 or IPv6 address with optional prefix
194
+ const match = addrCmd.rawText.match(/address\s+([\d./:a-fA-F]+)/i);
195
+ if (match) {
196
+ return match[1];
197
+ }
198
+ }
199
+ return undefined;
200
+ };
201
+
202
+ /**
203
+ * Check if port is assigned to interface
204
+ */
205
+ export const hasPortAssignment = (node: ConfigNode): boolean => {
206
+ return node.children.some((child) => {
207
+ const rawText = child.rawText.toLowerCase().trim();
208
+ return rawText.startsWith('port');
209
+ });
210
+ };
211
+
212
+ /**
213
+ * Get port assignment
214
+ */
215
+ export const getPortAssignment = (node: ConfigNode): string | undefined => {
216
+ const portCmd = node.children.find((child) => {
217
+ const rawText = child.rawText.toLowerCase().trim();
218
+ return rawText.startsWith('port');
219
+ });
220
+
221
+ if (portCmd) {
222
+ const match = portCmd.rawText.match(/port\s+([\d/]+)/i);
223
+ if (match) {
224
+ return match[1];
225
+ }
226
+ }
227
+ return undefined;
228
+ };
229
+
230
+ /**
231
+ * Check if BGP has router-id configured
232
+ */
233
+ export const hasBgpRouterId = (node: ConfigNode): boolean => {
234
+ return hasChildCommand(node, 'router-id');
235
+ };
236
+
237
+ /**
238
+ * Get BGP router-id
239
+ */
240
+ export const getBgpRouterId = (node: ConfigNode): string | undefined => {
241
+ const routerIdCmd = getChildCommand(node, 'router-id');
242
+ if (routerIdCmd) {
243
+ const match = routerIdCmd.rawText.match(/router-id\s+([\d.]+)/i);
244
+ if (match) {
245
+ return match[1];
246
+ }
247
+ }
248
+ return undefined;
249
+ };
250
+
251
+ /**
252
+ * Find a stanza by name in the configuration tree
253
+ */
254
+ export const findStanza = (node: ConfigNode, stanzaName: string): ConfigNode | undefined => {
255
+ if (node.id.toLowerCase().startsWith(stanzaName.toLowerCase())) {
256
+ return node;
257
+ }
258
+ for (const child of node.children) {
259
+ const found = findStanza(child, stanzaName);
260
+ if (found) return found;
261
+ }
262
+ return undefined;
263
+ };
264
+
265
+ /**
266
+ * Find all stanzas by name in the configuration tree
267
+ */
268
+ export const findStanzas = (node: ConfigNode, stanzaName: string): ConfigNode[] => {
269
+ const results: ConfigNode[] = [];
270
+ if (node.id.toLowerCase().startsWith(stanzaName.toLowerCase())) {
271
+ results.push(node);
272
+ }
273
+ for (const child of node.children) {
274
+ results.push(...findStanzas(child, stanzaName));
275
+ }
276
+ return results;
277
+ };
278
+
279
+ /**
280
+ * Check if SAP (Service Access Point) is configured
281
+ */
282
+ export const hasSap = (node: ConfigNode): boolean => {
283
+ return node.children.some((child) => {
284
+ const rawText = child.rawText.toLowerCase().trim();
285
+ return rawText.startsWith('sap');
286
+ });
287
+ };
288
+
289
+ /**
290
+ * Get SAP identifier
291
+ */
292
+ export const getSapId = (node: ConfigNode): string | undefined => {
293
+ const sapCmd = node.children.find((child) => {
294
+ const rawText = child.rawText.toLowerCase().trim();
295
+ return rawText.startsWith('sap');
296
+ });
297
+
298
+ if (sapCmd) {
299
+ // SAP format: sap port:vlan (e.g., sap 1/1/1:100)
300
+ const match = sapCmd.rawText.match(/sap\s+([\d/:]+)/i);
301
+ if (match) {
302
+ return match[1];
303
+ }
304
+ }
305
+ return undefined;
306
+ };
307
+
308
+ /**
309
+ * Check if SNMP is configured (snmp block with admin-state)
310
+ */
311
+ export const isSnmpEnabled = (node: ConfigNode): boolean => {
312
+ if (node.id.toLowerCase() === 'snmp') {
313
+ return isAdminStateEnabled(node);
314
+ }
315
+ return false;
316
+ };
317
+
318
+ /**
319
+ * Check if NTP is configured
320
+ */
321
+ export const hasNtpServer = (node: ConfigNode): boolean => {
322
+ return node.children.some((child) => {
323
+ const rawText = child.rawText.toLowerCase().trim();
324
+ return rawText.includes('ntp-server') || rawText.includes('server');
325
+ });
326
+ };
327
+
328
+ /**
329
+ * Check if SSH is enabled in security settings
330
+ */
331
+ export const isSshEnabled = (node: ConfigNode): boolean => {
332
+ if (node.id.toLowerCase().includes('security') || node.id.toLowerCase().includes('management-interface')) {
333
+ return node.children.some((child) => {
334
+ const rawText = child.rawText.toLowerCase().trim();
335
+ return rawText.includes('ssh') && !rawText.includes('no ssh');
336
+ });
337
+ }
338
+ return false;
339
+ };
340
+
341
+ /**
342
+ * Check if Telnet is enabled (security concern)
343
+ */
344
+ export const isTelnetEnabled = (node: ConfigNode): boolean => {
345
+ if (node.id.toLowerCase().includes('security') || node.id.toLowerCase().includes('management-interface')) {
346
+ return node.children.some((child) => {
347
+ const rawText = child.rawText.toLowerCase().trim();
348
+ return rawText.includes('telnet') && !rawText.includes('no telnet');
349
+ });
350
+ }
351
+ return false;
352
+ };
353
+
354
+ /**
355
+ * Check if authentication is configured
356
+ */
357
+ export const hasAuthentication = (node: ConfigNode): boolean => {
358
+ return node.children.some((child) => {
359
+ const rawText = child.rawText?.toLowerCase().trim();
360
+ if (!rawText) {
361
+ return false;
362
+ }
363
+ return (
364
+ rawText.includes('authentication') ||
365
+ rawText.includes('auth-key') ||
366
+ rawText.includes('password')
367
+ );
368
+ });
369
+ };
370
+
371
+ /**
372
+ * Get interface name from quoted or unquoted format
373
+ * Nokia uses: interface "name" or interface name
374
+ */
375
+ export const getInterfaceName = (node: ConfigNode): string => {
376
+ const match = node.id.match(/interface\s+"([^"]+)"|interface\s+(\S+)/i);
377
+ const quoted = match?.[1];
378
+ const unquoted = match?.[2];
379
+ if (quoted) return quoted;
380
+ if (unquoted) return unquoted;
381
+ return node.id.replace(/^interface\s+/i, '').trim();
382
+ };
383
+
384
+ /**
385
+ * Get router name from router block
386
+ * Nokia uses: router "Base" or router vprn-name
387
+ */
388
+ export const getRouterName = (node: ConfigNode): string => {
389
+ const match = node.id.match(/router\s+"([^"]+)"|router\s+(\S+)/i);
390
+ const quoted = match?.[1];
391
+ const unquoted = match?.[2];
392
+ if (quoted) return quoted;
393
+ if (unquoted) return unquoted;
394
+ return 'Base';
395
+ };
396
+
397
+ /**
398
+ * Check if BGP peer has description
399
+ */
400
+ export const hasPeerDescription = (node: ConfigNode): boolean => {
401
+ return hasChildCommand(node, 'description');
402
+ };
403
+
404
+ /**
405
+ * Get service type from service block
406
+ */
407
+ export const getServiceType = (node: ConfigNode): 'vpls' | 'vprn' | 'epipe' | 'ies' | undefined => {
408
+ const id = node.id.toLowerCase();
409
+ if (id.includes('vpls')) return 'vpls';
410
+ if (id.includes('vprn')) return 'vprn';
411
+ if (id.includes('epipe')) return 'epipe';
412
+ if (id.includes('ies')) return 'ies';
413
+ return undefined;
414
+ };
415
+
416
+ /**
417
+ * Get service ID from service block
418
+ */
419
+ export const getServiceId = (node: ConfigNode): string | undefined => {
420
+ const match = node.id.match(/(vpls|vprn|epipe|ies)\s+(\d+)/i);
421
+ const serviceId = match?.[2];
422
+ if (serviceId) {
423
+ return serviceId;
424
+ }
425
+ return undefined;
426
+ };
427
+
428
+ /**
429
+ * Check if customer is assigned to service
430
+ */
431
+ export const hasCustomer = (node: ConfigNode): boolean => {
432
+ return node.children.some((child) => {
433
+ const rawText = child.rawText?.toLowerCase().trim();
434
+ return rawText?.startsWith('customer') ?? false;
435
+ });
436
+ };
437
+
438
+ /**
439
+ * Get customer ID
440
+ */
441
+ export const getCustomerId = (node: ConfigNode): string | undefined => {
442
+ const customerCmd = node.children.find((child) => {
443
+ const rawText = child.rawText.toLowerCase().trim();
444
+ return rawText.startsWith('customer');
445
+ });
446
+
447
+ if (customerCmd) {
448
+ const match = customerCmd.rawText.match(/customer\s+(\d+)/i);
449
+ if (match) {
450
+ return match[1];
451
+ }
452
+ }
453
+ return undefined;
454
+ };
455
+
456
+ // ============================================================================
457
+ // Management Plane Security Helpers
458
+ // ============================================================================
459
+
460
+ /**
461
+ * Recursively search for a pattern in node and its descendants
462
+ */
463
+ const searchNodeRecursively = (
464
+ node: ConfigNode,
465
+ predicate: (rawText: string) => boolean
466
+ ): boolean => {
467
+ const rawText = node.rawText.toLowerCase().trim();
468
+ if (predicate(rawText)) {
469
+ return true;
470
+ }
471
+ return node.children.some((child) => searchNodeRecursively(child, predicate));
472
+ };
473
+
474
+ /**
475
+ * Check if TACACS+ is configured for AAA
476
+ */
477
+ export const hasTacacsConfig = (node: ConfigNode): boolean => {
478
+ // Check for tacplus in aaa remote-servers
479
+ return searchNodeRecursively(node, (rawText) =>
480
+ rawText.includes('tacplus') || rawText.includes('tacacs')
481
+ );
482
+ };
483
+
484
+ /**
485
+ * Check if RADIUS is configured for AAA
486
+ */
487
+ export const hasRadiusConfig = (node: ConfigNode): boolean => {
488
+ return searchNodeRecursively(node, (rawText) => rawText.includes('radius'));
489
+ };
490
+
491
+ /**
492
+ * Check if SSH version 2 is configured
493
+ */
494
+ export const hasSshV2 = (node: ConfigNode): boolean => {
495
+ return searchNodeRecursively(node, (rawText) =>
496
+ rawText === 'version 2' || rawText.includes('version 2')
497
+ );
498
+ };
499
+
500
+ /**
501
+ * Check if SSHv1 is explicitly enabled (security concern)
502
+ */
503
+ export const hasSshV1 = (node: ConfigNode): boolean => {
504
+ return searchNodeRecursively(node, (rawText) =>
505
+ rawText === 'version 1' ||
506
+ (rawText.includes('version') && rawText.includes('1') && !rawText.includes('2'))
507
+ );
508
+ };
509
+
510
+ /**
511
+ * Check if weak SSH ciphers are configured
512
+ */
513
+ export const hasWeakSshCipher = (node: ConfigNode): boolean => {
514
+ const weakCiphers = ['3des-cbc', 'blowfish-cbc', 'cast128-cbc', 'arcfour', 'rijndael-cbc'];
515
+ return searchNodeRecursively(node, (rawText) =>
516
+ weakCiphers.some((cipher) => rawText.includes(cipher))
517
+ );
518
+ };
519
+
520
+ /**
521
+ * Check if SNMPv3 with privacy is configured
522
+ */
523
+ export const hasSnmpV3Privacy = (node: ConfigNode): boolean => {
524
+ return searchNodeRecursively(node, (rawText) =>
525
+ rawText.includes('security-level privacy') ||
526
+ rawText.includes('usm') ||
527
+ (rawText.includes('snmpv3') && rawText.includes('privacy'))
528
+ );
529
+ };
530
+
531
+ /**
532
+ * Check if default SNMP community strings are used
533
+ */
534
+ export const hasDefaultSnmpCommunity = (node: ConfigNode): boolean => {
535
+ return searchNodeRecursively(node, (rawText) =>
536
+ rawText.includes('community public') ||
537
+ rawText.includes('community private') ||
538
+ rawText.includes('community "public"') ||
539
+ rawText.includes('community "private"')
540
+ );
541
+ };
542
+
543
+ /**
544
+ * Check if NTP authentication is enabled
545
+ */
546
+ export const hasNtpAuthentication = (node: ConfigNode): boolean => {
547
+ return searchNodeRecursively(node, (rawText) =>
548
+ rawText.includes('authentication-check') ||
549
+ rawText.includes('authentication-key') ||
550
+ rawText.includes('message-digest')
551
+ );
552
+ };
553
+
554
+ /**
555
+ * Check if management access filter is configured
556
+ */
557
+ export const hasManagementAccessFilter = (node: ConfigNode): boolean => {
558
+ return searchNodeRecursively(node, (rawText) =>
559
+ rawText.includes('management-access-filter')
560
+ );
561
+ };
562
+
563
+ /**
564
+ * Check if MAF has default-action deny
565
+ */
566
+ export const hasMafDefaultDeny = (node: ConfigNode): boolean => {
567
+ return searchNodeRecursively(node, (rawText) =>
568
+ rawText.includes('default-action deny') || rawText === 'default-action deny'
569
+ );
570
+ };
571
+
572
+ // ============================================================================
573
+ // Control Plane Security Helpers
574
+ // ============================================================================
575
+
576
+ /**
577
+ * Check if OSPF authentication is configured (auth-keychain or authentication-key)
578
+ */
579
+ export const hasOspfAuthentication = (node: ConfigNode): boolean => {
580
+ return searchNodeRecursively(node, (rawText) =>
581
+ rawText.includes('auth-keychain') ||
582
+ rawText.includes('authentication-key') ||
583
+ rawText.includes('message-digest-key')
584
+ );
585
+ };
586
+
587
+ /**
588
+ * Check if IS-IS authentication is configured
589
+ */
590
+ export const hasIsisAuthentication = (node: ConfigNode): boolean => {
591
+ return searchNodeRecursively(node, (rawText) =>
592
+ rawText.includes('auth-keychain') || rawText.includes('authentication-key')
593
+ );
594
+ };
595
+
596
+ /**
597
+ * Check if LDP authentication is configured
598
+ */
599
+ export const hasLdpAuthentication = (node: ConfigNode): boolean => {
600
+ return searchNodeRecursively(node, (rawText) =>
601
+ rawText.includes('auth-keychain') || rawText.includes('authentication-key')
602
+ );
603
+ };
604
+
605
+ /**
606
+ * Check if RSVP authentication is configured
607
+ */
608
+ export const hasRsvpAuthentication = (node: ConfigNode): boolean => {
609
+ return searchNodeRecursively(node, (rawText) =>
610
+ rawText.includes('auth-keychain') || rawText.includes('authentication-key')
611
+ );
612
+ };
613
+
614
+ // ============================================================================
615
+ // BGP Security Helpers
616
+ // ============================================================================
617
+
618
+ /**
619
+ * Check if BGP authentication is configured (auth-keychain or authentication-key)
620
+ */
621
+ export const hasBgpAuthentication = (node: ConfigNode): boolean => {
622
+ return searchNodeRecursively(node, (rawText) =>
623
+ rawText.includes('auth-keychain') ||
624
+ rawText.includes('authentication-key') ||
625
+ rawText.includes('password')
626
+ );
627
+ };
628
+
629
+ /**
630
+ * Check if BGP TTL security (GTSM) is configured
631
+ */
632
+ export const hasBgpTtlSecurity = (node: ConfigNode): boolean => {
633
+ return searchNodeRecursively(node, (rawText) => rawText.includes('ttl-security'));
634
+ };
635
+
636
+ /**
637
+ * Check if BGP prefix-limit is configured
638
+ */
639
+ export const hasBgpPrefixLimit = (node: ConfigNode): boolean => {
640
+ return searchNodeRecursively(node, (rawText) => rawText.includes('prefix-limit'));
641
+ };
642
+
643
+ /**
644
+ * Check if BGP graceful restart is configured
645
+ */
646
+ export const hasBgpGracefulRestart = (node: ConfigNode): boolean => {
647
+ return searchNodeRecursively(node, (rawText) => rawText.includes('graceful-restart'));
648
+ };
649
+
650
+ /**
651
+ * Check if BGP import/export policies are configured
652
+ */
653
+ export const hasBgpPolicies = (node: ConfigNode): boolean => {
654
+ return searchNodeRecursively(node, (rawText) =>
655
+ rawText.includes('import') || rawText.includes('export')
656
+ );
657
+ };
658
+
659
+ /**
660
+ * Check if BGP group is external type
661
+ */
662
+ export const isBgpExternalGroup = (node: ConfigNode): boolean => {
663
+ return searchNodeRecursively(node, (rawText) => rawText.includes('type external'));
664
+ };
665
+
666
+ // ============================================================================
667
+ // CPM Filter Helpers
668
+ // ============================================================================
669
+
670
+ /**
671
+ * Check if CPM filter is configured
672
+ */
673
+ export const hasCpmFilter = (node: ConfigNode): boolean => {
674
+ return searchNodeRecursively(node, (rawText) => rawText.includes('cpm-filter'));
675
+ };
676
+
677
+ /**
678
+ * Check if CPM filter has default-action drop
679
+ */
680
+ export const hasCpmFilterDefaultDrop = (node: ConfigNode): boolean => {
681
+ return searchNodeRecursively(node, (rawText) => rawText.includes('default-action drop'));
682
+ };
683
+
684
+ /**
685
+ * Check if protocol protection is enabled
686
+ */
687
+ export const hasProtocolProtection = (node: ConfigNode): boolean => {
688
+ return searchNodeRecursively(node, (rawText) =>
689
+ rawText.includes('protocol-protection') || rawText.includes('cpu-protection')
690
+ );
691
+ };
692
+
693
+ // ============================================================================
694
+ // Data Plane Security Helpers
695
+ // ============================================================================
696
+
697
+ /**
698
+ * Check if uRPF is configured
699
+ */
700
+ export const hasUrpf = (node: ConfigNode): boolean => {
701
+ return searchNodeRecursively(node, (rawText) => rawText.includes('urpf-check'));
702
+ };
703
+
704
+ /**
705
+ * Get uRPF mode (strict, loose, or undefined)
706
+ */
707
+ export const getUrpfMode = (node: ConfigNode): 'strict' | 'loose' | undefined => {
708
+ // Search recursively for urpf-check and mode
709
+ let mode: 'strict' | 'loose' | undefined;
710
+ const findMode = (n: ConfigNode): void => {
711
+ const rawText = n.rawText.toLowerCase();
712
+ if (rawText.includes('urpf-check')) {
713
+ if (rawText.includes('strict')) {
714
+ mode = 'strict';
715
+ } else if (rawText.includes('loose')) {
716
+ mode = 'loose';
717
+ }
718
+ }
719
+ if (rawText.includes('mode strict')) {
720
+ mode = 'strict';
721
+ } else if (rawText.includes('mode loose')) {
722
+ mode = 'loose';
723
+ }
724
+ n.children.forEach(findMode);
725
+ };
726
+ findMode(node);
727
+ return mode;
728
+ };
729
+
730
+ /**
731
+ * Check if IP filter is applied
732
+ */
733
+ export const hasIpFilter = (node: ConfigNode): boolean => {
734
+ return searchNodeRecursively(node, (rawText) =>
735
+ rawText.includes('ip-filter') ||
736
+ rawText.includes('ingress filter') ||
737
+ rawText.includes('egress filter')
738
+ );
739
+ };
740
+
741
+ // ============================================================================
742
+ // Logging Helpers
743
+ // ============================================================================
744
+
745
+ /**
746
+ * Check if syslog is configured
747
+ */
748
+ export const hasSyslog = (node: ConfigNode): boolean => {
749
+ return searchNodeRecursively(node, (rawText) =>
750
+ rawText.startsWith('syslog') || rawText.includes('syslog')
751
+ );
752
+ };
753
+
754
+ /**
755
+ * Check if SNMP trap group is configured
756
+ */
757
+ export const hasSnmpTrapGroup = (node: ConfigNode): boolean => {
758
+ return searchNodeRecursively(node, (rawText) => rawText.includes('snmp-trap-group'));
759
+ };
760
+
761
+ /**
762
+ * Check if event-control is configured
763
+ */
764
+ export const hasEventControl = (node: ConfigNode): boolean => {
765
+ return searchNodeRecursively(node, (rawText) => rawText.includes('event-control'));
766
+ };
767
+
768
+ /**
769
+ * Check if accounting policy is configured
770
+ */
771
+ export const hasAccountingPolicy = (node: ConfigNode): boolean => {
772
+ return searchNodeRecursively(node, (rawText) => rawText.includes('accounting-policy'));
773
+ };
774
+
775
+ // ============================================================================
776
+ // High Availability Helpers
777
+ // ============================================================================
778
+
779
+ /**
780
+ * Check if BFD is enabled on interface
781
+ */
782
+ export const hasBfd = (node: ConfigNode): boolean => {
783
+ return searchNodeRecursively(node, (rawText) =>
784
+ rawText.includes('bfd-liveness') || rawText.includes('bfd')
785
+ );
786
+ };
787
+
788
+ /**
789
+ * Check if MC-LAG is configured
790
+ */
791
+ export const hasMcLag = (node: ConfigNode): boolean => {
792
+ return searchNodeRecursively(node, (rawText) =>
793
+ rawText.includes('mc-lag') || rawText.includes('multi-chassis')
794
+ );
795
+ };
796
+
797
+ /**
798
+ * Check if MC-LAG authentication is configured
799
+ */
800
+ export const hasMcLagAuthentication = (node: ConfigNode): boolean => {
801
+ return searchNodeRecursively(node, (rawText) => rawText.includes('authentication-key'));
802
+ };
803
+
804
+ /**
805
+ * Check if LACP is configured
806
+ */
807
+ export const hasLacp = (node: ConfigNode): boolean => {
808
+ return searchNodeRecursively(node, (rawText) => rawText.includes('lacp'));
809
+ };
810
+
811
+ // ============================================================================
812
+ // Service Security Helpers
813
+ // ============================================================================
814
+
815
+ /**
816
+ * Check if VPRN has route-distinguisher configured
817
+ */
818
+ export const hasRouteDistinguisher = (node: ConfigNode): boolean => {
819
+ return searchNodeRecursively(node, (rawText) => rawText.includes('route-distinguisher'));
820
+ };
821
+
822
+ /**
823
+ * Check if VPRN has vrf-target configured
824
+ */
825
+ export const hasVrfTarget = (node: ConfigNode): boolean => {
826
+ return searchNodeRecursively(node, (rawText) => rawText.includes('vrf-target'));
827
+ };
828
+
829
+ /**
830
+ * Check if GRT leaking is configured (potential security concern)
831
+ */
832
+ export const hasGrtLeaking = (node: ConfigNode): boolean => {
833
+ return searchNodeRecursively(node, (rawText) => rawText.includes('grt-leaking'));
834
+ };
835
+
836
+ /**
837
+ * Get BGP neighbor IP address from neighbor node
838
+ */
839
+ export const getBgpNeighborIp = (node: ConfigNode): string => {
840
+ const match = node.id.match(/neighbor\s+"?([^"]+)"?|neighbor\s+([\d.:a-fA-F]+)/i);
841
+ if (match) {
842
+ return match[1] ?? match[2] ?? node.id.replace(/^neighbor\s+/i, '').trim();
843
+ }
844
+ return node.id.replace(/^neighbor\s+/i, '').trim();
845
+ };
846
+
847
+ /**
848
+ * Get BGP group name from group node
849
+ */
850
+ export const getBgpGroupName = (node: ConfigNode): string => {
851
+ const match = node.id.match(/group\s+"([^"]+)"/i);
852
+ if (match?.[1]) {
853
+ return match[1];
854
+ }
855
+ return node.id.replace(/^group\s+/i, '').replace(/"/g, '').trim();
856
+ };