@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,790 @@
1
+ // packages/rule-helpers/src/huawei/helpers.ts
2
+ // Huawei VRP-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 interface is shutdown (using Huawei's 'undo shutdown' pattern)
12
+ * In Huawei, interfaces are shutdown by default; 'undo shutdown' enables them
13
+ */
14
+ export const isShutdown = (node: ConfigNode): boolean => {
15
+ // Check if there's a 'shutdown' command
16
+ const hasShutdown = node.children.some((child) => {
17
+ const id = child.id.toLowerCase().trim();
18
+ return id === 'shutdown';
19
+ });
20
+
21
+ // Check if there's an 'undo shutdown' command
22
+ const hasUndoShutdown = node.children.some((child) => {
23
+ const rawText = child.rawText?.toLowerCase().trim();
24
+ return rawText === 'undo shutdown';
25
+ });
26
+
27
+ // Shutdown if explicitly shutdown OR no undo shutdown (Huawei default is shutdown)
28
+ return hasShutdown || !hasUndoShutdown;
29
+ };
30
+
31
+ /**
32
+ * Check if interface is explicitly enabled (has 'undo shutdown')
33
+ */
34
+ export const isEnabled = (node: ConfigNode): boolean => {
35
+ return node.children.some((child) => {
36
+ const rawText = child.rawText?.toLowerCase().trim();
37
+ return rawText === 'undo shutdown';
38
+ });
39
+ };
40
+
41
+ /**
42
+ * Check if interface is a physical port (not Vlanif, LoopBack, NULL, Tunnel, etc.)
43
+ */
44
+ export const isPhysicalPort = (interfaceName: string): boolean => {
45
+ const name = interfaceName.toLowerCase();
46
+ return (
47
+ !name.includes('vlanif') &&
48
+ !name.includes('loopback') &&
49
+ !name.includes('null') &&
50
+ !name.includes('tunnel') &&
51
+ !name.includes('eth-trunk') &&
52
+ !name.includes('nve') &&
53
+ !name.includes('vbdif')
54
+ );
55
+ };
56
+
57
+ /**
58
+ * Check if interface is a VLAN interface (Vlanif)
59
+ */
60
+ export const isVlanInterface = (interfaceName: string): boolean => {
61
+ return interfaceName.toLowerCase().includes('vlanif');
62
+ };
63
+
64
+ /**
65
+ * Check if interface is a loopback interface
66
+ */
67
+ export const isLoopbackInterface = (interfaceName: string): boolean => {
68
+ return interfaceName.toLowerCase().includes('loopback');
69
+ };
70
+
71
+ /**
72
+ * Check if interface is an Eth-Trunk (LAG)
73
+ */
74
+ export const isEthTrunk = (interfaceName: string): boolean => {
75
+ return interfaceName.toLowerCase().includes('eth-trunk');
76
+ };
77
+
78
+ /**
79
+ * Check if interface is configured as trunk port
80
+ */
81
+ export const isTrunkPort = (node: ConfigNode): boolean => {
82
+ return node.children.some((child) => {
83
+ const rawText = child.rawText?.toLowerCase().trim();
84
+ return rawText?.includes('port link-type trunk') ?? false;
85
+ });
86
+ };
87
+
88
+ /**
89
+ * Check if interface is configured as access port
90
+ */
91
+ export const isAccessPort = (node: ConfigNode): boolean => {
92
+ return node.children.some((child) => {
93
+ const rawText = child.rawText?.toLowerCase().trim();
94
+ return rawText?.includes('port link-type access') ?? false;
95
+ });
96
+ };
97
+
98
+ /**
99
+ * Check if interface is configured as hybrid port
100
+ */
101
+ export const isHybridPort = (node: ConfigNode): boolean => {
102
+ return node.children.some((child) => {
103
+ const rawText = child.rawText?.toLowerCase().trim();
104
+ return rawText?.includes('port link-type hybrid') ?? false;
105
+ });
106
+ };
107
+
108
+ /**
109
+ * Get the default VLAN for an access port
110
+ */
111
+ export const getDefaultVlan = (node: ConfigNode): string | undefined => {
112
+ const vlanCmd = node.children.find((child) => {
113
+ const rawText = child.rawText?.toLowerCase().trim();
114
+ return rawText?.startsWith('port default vlan') ?? false;
115
+ });
116
+
117
+ if (vlanCmd?.rawText) {
118
+ const match = vlanCmd.rawText.match(/port\s+default\s+vlan\s+(\d+)/i);
119
+ const vlan = match?.[1];
120
+ if (vlan) {
121
+ return vlan;
122
+ }
123
+ }
124
+ return undefined;
125
+ };
126
+
127
+ /**
128
+ * Get allowed VLANs for trunk port
129
+ */
130
+ export const getTrunkAllowedVlans = (node: ConfigNode): string | undefined => {
131
+ const vlanCmd = node.children.find((child) => {
132
+ const rawText = child.rawText?.toLowerCase().trim();
133
+ return rawText?.startsWith('port trunk allow-pass vlan') ?? false;
134
+ });
135
+
136
+ if (vlanCmd?.rawText) {
137
+ const match = vlanCmd.rawText.match(/port\s+trunk\s+allow-pass\s+vlan\s+(.+)/i);
138
+ const vlans = match?.[1];
139
+ if (vlans) {
140
+ return vlans.trim();
141
+ }
142
+ }
143
+ return undefined;
144
+ };
145
+
146
+ /**
147
+ * Check if interface has description
148
+ */
149
+ export const hasDescription = (node: ConfigNode): boolean => {
150
+ return hasChildCommand(node, 'description');
151
+ };
152
+
153
+ /**
154
+ * Get interface description
155
+ */
156
+ export const getDescription = (node: ConfigNode): string | undefined => {
157
+ const descCmd = getChildCommand(node, 'description');
158
+ if (descCmd?.rawText) {
159
+ const match = descCmd.rawText.match(/description\s+(.+)/i);
160
+ const description = match?.[1];
161
+ if (description) {
162
+ return description.trim();
163
+ }
164
+ }
165
+ return undefined;
166
+ };
167
+
168
+ /**
169
+ * Check if STP edge port is enabled (stp edged-port enable)
170
+ */
171
+ export const hasStpEdgedPort = (node: ConfigNode): boolean => {
172
+ return node.children.some((child) => {
173
+ const rawText = child.rawText?.toLowerCase().trim();
174
+ return rawText?.includes('stp edged-port enable') ?? false;
175
+ });
176
+ };
177
+
178
+ /**
179
+ * Check if port security is enabled
180
+ */
181
+ export const hasPortSecurity = (node: ConfigNode): boolean => {
182
+ return node.children.some((child) => {
183
+ const rawText = child.rawText?.toLowerCase().trim();
184
+ return rawText?.includes('port-security enable') ?? false;
185
+ });
186
+ };
187
+
188
+ /**
189
+ * Check if BPDU protection is enabled
190
+ */
191
+ export const hasBpduProtection = (node: ConfigNode): boolean => {
192
+ return node.children.some((child) => {
193
+ const rawText = child.rawText?.toLowerCase().trim();
194
+ if (!rawText) {
195
+ return false;
196
+ }
197
+ return rawText.includes('stp bpdu-protection') || rawText.includes('bpdu-protection enable');
198
+ });
199
+ };
200
+
201
+ /**
202
+ * Get child command value for 'set <key> <value>' style commands
203
+ */
204
+ export const getCommandValue = (node: ConfigNode, command: string): string | undefined => {
205
+ const cmd = node.children.find((child) => {
206
+ const text = child.rawText?.toLowerCase().trim();
207
+ return text?.startsWith(command.toLowerCase()) ?? false;
208
+ });
209
+
210
+ if (cmd?.rawText) {
211
+ const rest = cmd.rawText.substring(command.length).trim();
212
+ return rest || undefined;
213
+ }
214
+ return undefined;
215
+ };
216
+
217
+ /**
218
+ * Check if SSH is enabled
219
+ */
220
+ export const isSshEnabled = (node: ConfigNode): boolean => {
221
+ if (node.id.toLowerCase().includes('user-interface')) {
222
+ return node.children.some((child) => {
223
+ const rawText = child.rawText?.toLowerCase().trim();
224
+ return rawText?.includes('protocol inbound ssh') || rawText === 'protocol inbound all';
225
+ });
226
+ }
227
+ return false;
228
+ };
229
+
230
+ /**
231
+ * Check if Telnet is enabled (security concern)
232
+ */
233
+ export const isTelnetEnabled = (node: ConfigNode): boolean => {
234
+ if (node.id.toLowerCase().includes('user-interface')) {
235
+ return node.children.some((child) => {
236
+ const rawText = child.rawText.toLowerCase().trim();
237
+ return (
238
+ rawText.includes('protocol inbound telnet') ||
239
+ rawText === 'protocol inbound all' ||
240
+ // Default for VTY is often telnet
241
+ (!rawText.includes('protocol inbound'))
242
+ );
243
+ });
244
+ }
245
+ return false;
246
+ };
247
+
248
+ /**
249
+ * Check if authentication mode AAA is configured
250
+ */
251
+ export const hasAaaAuthentication = (node: ConfigNode): boolean => {
252
+ return node.children.some((child) => {
253
+ const rawText = child.rawText.toLowerCase().trim();
254
+ return rawText.includes('authentication-mode aaa');
255
+ });
256
+ };
257
+
258
+ /**
259
+ * Check if password authentication is configured (less secure)
260
+ */
261
+ export const hasPasswordAuthentication = (node: ConfigNode): boolean => {
262
+ return node.children.some((child) => {
263
+ const rawText = child.rawText.toLowerCase().trim();
264
+ return rawText.includes('authentication-mode password');
265
+ });
266
+ };
267
+
268
+ /**
269
+ * Check if idle timeout is configured
270
+ */
271
+ export const hasIdleTimeout = (node: ConfigNode): boolean => {
272
+ return hasChildCommand(node, 'idle-timeout');
273
+ };
274
+
275
+ /**
276
+ * Get idle timeout value in minutes
277
+ */
278
+ export const getIdleTimeout = (node: ConfigNode): number | undefined => {
279
+ const timeoutCmd = getChildCommand(node, 'idle-timeout');
280
+ if (timeoutCmd?.rawText) {
281
+ const match = timeoutCmd.rawText.match(/idle-timeout\s+(\d+)/i);
282
+ const timeout = match?.[1];
283
+ if (timeout) {
284
+ return parseInt(timeout, 10);
285
+ }
286
+ }
287
+ return undefined;
288
+ };
289
+
290
+ /**
291
+ * Check if ACL is applied inbound on user-interface
292
+ */
293
+ export const hasAclInbound = (node: ConfigNode): boolean => {
294
+ return node.children.some((child) => {
295
+ const rawText = child.rawText.toLowerCase().trim();
296
+ return rawText.match(/acl\s+\d+\s+inbound/);
297
+ });
298
+ };
299
+
300
+ /**
301
+ * Find a stanza by name in the configuration tree
302
+ */
303
+ export const findStanza = (node: ConfigNode, stanzaName: string): ConfigNode | undefined => {
304
+ if (node.id.toLowerCase().startsWith(stanzaName.toLowerCase())) {
305
+ return node;
306
+ }
307
+ for (const child of node.children) {
308
+ const found = findStanza(child, stanzaName);
309
+ if (found) return found;
310
+ }
311
+ return undefined;
312
+ };
313
+
314
+ /**
315
+ * Find all stanzas by name in the configuration tree
316
+ */
317
+ export const findStanzas = (node: ConfigNode, stanzaName: string): ConfigNode[] => {
318
+ const results: ConfigNode[] = [];
319
+ if (node.id.toLowerCase().startsWith(stanzaName.toLowerCase())) {
320
+ results.push(node);
321
+ }
322
+ for (const child of node.children) {
323
+ results.push(...findStanzas(child, stanzaName));
324
+ }
325
+ return results;
326
+ };
327
+
328
+ /**
329
+ * Check if local-user has password configured with cipher (encrypted)
330
+ */
331
+ export const hasEncryptedPassword = (node: ConfigNode): boolean => {
332
+ return node.children.some((child) => {
333
+ const rawText = child.rawText.toLowerCase().trim();
334
+ return (
335
+ rawText.includes('password cipher') ||
336
+ rawText.includes('password irreversible-cipher')
337
+ );
338
+ });
339
+ };
340
+
341
+ /**
342
+ * Check if local-user has plaintext password (security concern)
343
+ */
344
+ export const hasPlaintextPassword = (node: ConfigNode): boolean => {
345
+ return node.children.some((child) => {
346
+ const rawText = child.rawText.toLowerCase().trim();
347
+ return rawText.includes('password simple');
348
+ });
349
+ };
350
+
351
+ /**
352
+ * Get privilege level for local-user
353
+ */
354
+ export const getPrivilegeLevel = (node: ConfigNode): number | undefined => {
355
+ const privCmd = node.children.find((child) => {
356
+ const rawText = child.rawText?.toLowerCase();
357
+ return rawText?.includes('privilege level');
358
+ });
359
+
360
+ if (privCmd?.rawText) {
361
+ const match = privCmd.rawText.match(/privilege\s+level\s+(\d+)/i);
362
+ const level = match?.[1];
363
+ if (level) {
364
+ return parseInt(level, 10);
365
+ }
366
+ }
367
+ return undefined;
368
+ };
369
+
370
+ // ============================================================================
371
+ // BGP Helper Functions
372
+ // ============================================================================
373
+
374
+ /**
375
+ * Check if BGP peer has password authentication configured
376
+ */
377
+ export const hasBgpPeerPassword = (node: ConfigNode, peerIp: string): boolean => {
378
+ return node.children.some((child) => {
379
+ const rawText = child.rawText?.toLowerCase();
380
+ return rawText?.includes(`peer ${peerIp.toLowerCase()}`) && rawText?.includes('password');
381
+ });
382
+ };
383
+
384
+ /**
385
+ * Check if BGP peer has keychain authentication configured
386
+ */
387
+ export const hasBgpPeerKeychain = (node: ConfigNode, peerIp: string): boolean => {
388
+ return node.children.some((child) => {
389
+ const rawText = child.rawText?.toLowerCase();
390
+ return rawText?.includes(`peer ${peerIp.toLowerCase()}`) && rawText?.includes('keychain');
391
+ });
392
+ };
393
+
394
+ /**
395
+ * Check if BGP peer has GTSM (valid-ttl-hops) configured
396
+ */
397
+ export const hasBgpPeerGtsm = (node: ConfigNode, peerIp: string): boolean => {
398
+ return node.children.some((child) => {
399
+ const rawText = child.rawText?.toLowerCase();
400
+ return rawText?.includes(`peer ${peerIp.toLowerCase()}`) && rawText?.includes('valid-ttl-hops');
401
+ });
402
+ };
403
+
404
+ /**
405
+ * Check if BGP peer has route-limit (maximum prefix) configured
406
+ */
407
+ export const hasBgpPeerRouteLimit = (node: ConfigNode, peerIp: string): boolean => {
408
+ return node.children.some((child) => {
409
+ const rawText = child.rawText?.toLowerCase();
410
+ return rawText?.includes(`peer ${peerIp.toLowerCase()}`) && rawText?.includes('route-limit');
411
+ });
412
+ };
413
+
414
+ /**
415
+ * Check if BGP peer has prefix filtering configured (ip-prefix or route-policy)
416
+ */
417
+ export const hasBgpPeerPrefixFilter = (node: ConfigNode, peerIp: string): boolean => {
418
+ return node.children.some((child) => {
419
+ const rawText = child.rawText?.toLowerCase();
420
+ return (
421
+ rawText?.includes(`peer ${peerIp.toLowerCase()}`) &&
422
+ (rawText?.includes('ip-prefix') || rawText?.includes('route-policy') || rawText?.includes('filter-policy'))
423
+ );
424
+ });
425
+ };
426
+
427
+ /**
428
+ * Get all BGP peer IP addresses from config
429
+ */
430
+ export const getBgpPeers = (node: ConfigNode): string[] => {
431
+ const peers: string[] = [];
432
+ for (const child of node.children) {
433
+ if (child.rawText) {
434
+ const match = child.rawText.match(/peer\s+([\d.]+)\s+as-number/i);
435
+ if (match?.[1]) {
436
+ peers.push(match[1]);
437
+ }
438
+ }
439
+ }
440
+ return peers;
441
+ };
442
+
443
+ /**
444
+ * Check if BGP has graceful-restart enabled
445
+ */
446
+ export const hasBgpGracefulRestart = (node: ConfigNode): boolean => {
447
+ return node.children.some((child) => {
448
+ const rawText = child.rawText?.toLowerCase().trim();
449
+ return rawText === 'graceful-restart' || rawText?.startsWith('graceful-restart ');
450
+ });
451
+ };
452
+
453
+ // ============================================================================
454
+ // OSPF/IS-IS Helper Functions
455
+ // ============================================================================
456
+
457
+ /**
458
+ * Check if OSPF area has authentication configured
459
+ */
460
+ export const hasOspfAreaAuthentication = (node: ConfigNode): boolean => {
461
+ return node.children.some((child) => {
462
+ const rawText = child.rawText?.toLowerCase();
463
+ return rawText?.includes('authentication-mode');
464
+ });
465
+ };
466
+
467
+ /**
468
+ * Check if interface has OSPF authentication configured
469
+ */
470
+ export const hasInterfaceOspfAuth = (node: ConfigNode): boolean => {
471
+ return node.children.some((child) => {
472
+ const rawText = child.rawText?.toLowerCase();
473
+ return rawText?.includes('ospf authentication-mode');
474
+ });
475
+ };
476
+
477
+ /**
478
+ * Check if IS-IS has area authentication configured
479
+ */
480
+ export const hasIsisAreaAuth = (node: ConfigNode): boolean => {
481
+ return node.children.some((child) => {
482
+ const rawText = child.rawText?.toLowerCase();
483
+ return rawText?.includes('area-authentication-mode');
484
+ });
485
+ };
486
+
487
+ /**
488
+ * Check if IS-IS has domain authentication configured
489
+ */
490
+ export const hasIsisDomainAuth = (node: ConfigNode): boolean => {
491
+ return node.children.some((child) => {
492
+ const rawText = child.rawText?.toLowerCase();
493
+ return rawText?.includes('domain-authentication-mode');
494
+ });
495
+ };
496
+
497
+ /**
498
+ * Check if interface has IS-IS authentication configured
499
+ */
500
+ export const hasInterfaceIsisAuth = (node: ConfigNode): boolean => {
501
+ return node.children.some((child) => {
502
+ const rawText = child.rawText?.toLowerCase();
503
+ return rawText?.includes('isis authentication-mode');
504
+ });
505
+ };
506
+
507
+ // ============================================================================
508
+ // VRRP Helper Functions
509
+ // ============================================================================
510
+
511
+ /**
512
+ * Check if interface has VRRP configured
513
+ */
514
+ export const hasVrrp = (node: ConfigNode): boolean => {
515
+ return node.children.some((child) => {
516
+ const rawText = child.rawText?.toLowerCase();
517
+ return rawText?.includes('vrrp vrid');
518
+ });
519
+ };
520
+
521
+ /**
522
+ * Check if VRRP has authentication configured
523
+ * In Huawei VRP, authentication can be on a single line: "vrrp vrid 1 authentication-mode md5 <key>"
524
+ */
525
+ export const hasVrrpAuthentication = (node: ConfigNode): boolean => {
526
+ return node.children.some((child) => {
527
+ const rawText = child.rawText?.toLowerCase();
528
+ // Check if line contains both vrrp vrid and authentication-mode
529
+ return rawText?.includes('vrrp vrid') && rawText?.includes('authentication-mode');
530
+ });
531
+ };
532
+
533
+ /**
534
+ * Get VRRP VRID from interface
535
+ */
536
+ export const getVrrpVrid = (node: ConfigNode): string | undefined => {
537
+ const vrrpCmd = node.children.find((child) => {
538
+ return child.rawText?.toLowerCase().includes('vrrp vrid');
539
+ });
540
+
541
+ if (vrrpCmd?.rawText) {
542
+ const match = vrrpCmd.rawText.match(/vrrp\s+vrid\s+(\d+)/i);
543
+ if (match?.[1]) {
544
+ return match[1];
545
+ }
546
+ }
547
+ return undefined;
548
+ };
549
+
550
+ // ============================================================================
551
+ // Interface Security Helper Functions
552
+ // ============================================================================
553
+
554
+ /**
555
+ * Check if ICMP redirect is disabled on interface
556
+ */
557
+ export const hasIcmpRedirectDisabled = (node: ConfigNode): boolean => {
558
+ return node.children.some((child) => {
559
+ const rawText = child.rawText?.toLowerCase().trim();
560
+ return rawText === 'undo icmp redirect send';
561
+ });
562
+ };
563
+
564
+ /**
565
+ * Check if directed broadcast is disabled on interface
566
+ */
567
+ export const hasDirectedBroadcastDisabled = (node: ConfigNode): boolean => {
568
+ return node.children.some((child) => {
569
+ const rawText = child.rawText?.toLowerCase().trim();
570
+ return rawText === 'undo ip directed-broadcast enable' || rawText === 'undo ip directed-broadcast';
571
+ });
572
+ };
573
+
574
+ /**
575
+ * Check if ARP proxy is disabled on interface
576
+ */
577
+ export const hasArpProxyDisabled = (node: ConfigNode): boolean => {
578
+ return node.children.some((child) => {
579
+ const rawText = child.rawText?.toLowerCase().trim();
580
+ return rawText === 'undo arp proxy enable' || rawText === 'undo proxy-arp';
581
+ });
582
+ };
583
+
584
+ /**
585
+ * Check if uRPF is enabled on interface
586
+ */
587
+ export const hasUrpf = (node: ConfigNode): boolean => {
588
+ return node.children.some((child) => {
589
+ const rawText = child.rawText?.toLowerCase();
590
+ return rawText?.includes('urpf strict') || rawText?.includes('urpf loose');
591
+ });
592
+ };
593
+
594
+ /**
595
+ * Get uRPF mode (strict or loose)
596
+ */
597
+ export const getUrpfMode = (node: ConfigNode): 'strict' | 'loose' | undefined => {
598
+ const urpfCmd = node.children.find((child) => {
599
+ const rawText = child.rawText?.toLowerCase();
600
+ return rawText?.includes('urpf');
601
+ });
602
+
603
+ if (urpfCmd?.rawText) {
604
+ const rawText = urpfCmd.rawText.toLowerCase();
605
+ if (rawText.includes('urpf strict')) return 'strict';
606
+ if (rawText.includes('urpf loose')) return 'loose';
607
+ }
608
+ return undefined;
609
+ };
610
+
611
+ /**
612
+ * Check if LLDP is disabled on interface
613
+ */
614
+ export const hasLldpDisabled = (node: ConfigNode): boolean => {
615
+ return node.children.some((child) => {
616
+ const rawText = child.rawText?.toLowerCase().trim();
617
+ return rawText === 'undo lldp enable';
618
+ });
619
+ };
620
+
621
+ // ============================================================================
622
+ // NTP Helper Functions
623
+ // ============================================================================
624
+
625
+ /**
626
+ * Check if NTP authentication is enabled
627
+ */
628
+ export const hasNtpAuthentication = (node: ConfigNode): boolean => {
629
+ return node.children.some((child) => {
630
+ const rawText = child.rawText?.toLowerCase().trim();
631
+ return rawText === 'authentication enable';
632
+ });
633
+ };
634
+
635
+ /**
636
+ * Check if NTP has authentication key configured
637
+ */
638
+ export const hasNtpAuthKey = (node: ConfigNode): boolean => {
639
+ return node.children.some((child) => {
640
+ const rawText = child.rawText?.toLowerCase();
641
+ return rawText?.includes('authentication-keyid');
642
+ });
643
+ };
644
+
645
+ // ============================================================================
646
+ // SSH Server Helper Functions
647
+ // ============================================================================
648
+
649
+ /**
650
+ * Check if SSH server has strong ciphers configured
651
+ */
652
+ export const hasSshStrongCiphers = (node: ConfigNode): boolean => {
653
+ // Check global config for ssh server cipher settings
654
+ const rawText = node.rawText?.toLowerCase();
655
+ if (rawText?.includes('ssh server cipher')) {
656
+ // Check for strong ciphers (aes256, aes128)
657
+ return rawText.includes('aes256') || rawText.includes('aes128');
658
+ }
659
+ return false;
660
+ };
661
+
662
+ /**
663
+ * Check for weak SSH algorithms
664
+ */
665
+ export const hasWeakSshAlgorithms = (node: ConfigNode): boolean => {
666
+ const rawText = node.rawText?.toLowerCase();
667
+ // Check for weak algorithms like 3des, des, arcfour
668
+ return (
669
+ rawText?.includes('3des') ||
670
+ rawText?.includes('arcfour') ||
671
+ (rawText?.includes('des') && !rawText.includes('aes') && !rawText.includes('3des'))
672
+ ) ?? false;
673
+ };
674
+
675
+ /**
676
+ * Check if SSH uses strong HMAC algorithms
677
+ */
678
+ export const hasSshStrongHmac = (node: ConfigNode): boolean => {
679
+ const rawText = node.rawText?.toLowerCase();
680
+ if (rawText?.includes('ssh server hmac')) {
681
+ return rawText.includes('sha2-256') || rawText.includes('sha2-512');
682
+ }
683
+ return false;
684
+ };
685
+
686
+ /**
687
+ * Check if SSH uses strong key exchange
688
+ */
689
+ export const hasSshStrongKeyExchange = (node: ConfigNode): boolean => {
690
+ const rawText = node.rawText?.toLowerCase();
691
+ if (rawText?.includes('ssh server key-exchange')) {
692
+ return rawText.includes('dh-group14') || rawText.includes('dh-group16') || rawText.includes('dh-group18');
693
+ }
694
+ return false;
695
+ };
696
+
697
+ // ============================================================================
698
+ // CPU-Defend Helper Functions
699
+ // ============================================================================
700
+
701
+ /**
702
+ * Check if CPU-defend policy is configured
703
+ */
704
+ export const hasCpuDefendPolicy = (node: ConfigNode): boolean => {
705
+ return node.id.toLowerCase().startsWith('cpu-defend policy');
706
+ };
707
+
708
+ /**
709
+ * Check if CPU-defend policy has auto-defend enabled
710
+ */
711
+ export const hasCpuDefendAutoDefend = (node: ConfigNode): boolean => {
712
+ return node.children.some((child) => {
713
+ const rawText = child.rawText?.toLowerCase().trim();
714
+ return rawText === 'auto-defend enable';
715
+ });
716
+ };
717
+
718
+ /**
719
+ * Check if CPU-defend policy is applied
720
+ */
721
+ export const isCpuDefendPolicyApplied = (rawText: string): boolean => {
722
+ return rawText.toLowerCase().startsWith('cpu-defend-policy');
723
+ };
724
+
725
+ // ============================================================================
726
+ // Login Banner Helper Functions
727
+ // ============================================================================
728
+
729
+ /**
730
+ * Check if login banner is configured
731
+ */
732
+ export const hasLoginBanner = (node: ConfigNode): boolean => {
733
+ return node.id.toLowerCase().startsWith('header login');
734
+ };
735
+
736
+ // ============================================================================
737
+ // Service Status Helper Functions
738
+ // ============================================================================
739
+
740
+ /**
741
+ * Check if FTP server is disabled
742
+ */
743
+ export const isFtpDisabled = (rawText: string): boolean => {
744
+ return rawText.toLowerCase().trim() === 'undo ftp server enable';
745
+ };
746
+
747
+ /**
748
+ * Check if HTTP server is disabled
749
+ */
750
+ export const isHttpDisabled = (rawText: string): boolean => {
751
+ return rawText.toLowerCase().trim() === 'undo http server enable';
752
+ };
753
+
754
+ /**
755
+ * Check if TFTP server is disabled
756
+ */
757
+ export const isTftpDisabled = (rawText: string): boolean => {
758
+ return rawText.toLowerCase().trim() === 'undo tftp-server enable';
759
+ };
760
+
761
+ /**
762
+ * Check if IP source route is disabled
763
+ */
764
+ export const isIpSourceRouteDisabled = (rawText: string): boolean => {
765
+ return rawText.toLowerCase().trim() === 'undo ip source-route';
766
+ };
767
+
768
+ // ============================================================================
769
+ // HWTACACS Helper Functions
770
+ // ============================================================================
771
+
772
+ /**
773
+ * Check if HWTACACS server template has shared-key configured
774
+ */
775
+ export const hasHwtacacsSharedKey = (node: ConfigNode): boolean => {
776
+ return node.children.some((child) => {
777
+ const rawText = child.rawText?.toLowerCase();
778
+ return rawText?.includes('shared-key cipher');
779
+ });
780
+ };
781
+
782
+ /**
783
+ * Check if HWTACACS has secondary server configured
784
+ */
785
+ export const hasHwtacacsSecondary = (node: ConfigNode): boolean => {
786
+ return node.children.some((child) => {
787
+ const rawText = child.rawText?.toLowerCase();
788
+ return rawText?.includes('secondary');
789
+ });
790
+ };