@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,1220 @@
1
+ // packages/rule-helpers/src/arista/helpers.ts
2
+ // Arista EOS-specific helper functions
3
+ // Based on Arista Best Practices: docs/Arista-best-practices.md
4
+
5
+ import type { ConfigNode } from '../../types/ConfigNode';
6
+ import {
7
+ hasChildCommand,
8
+ getChildCommand,
9
+ parseIp,
10
+ equalsIgnoreCase,
11
+ includesIgnoreCase,
12
+ startsWithIgnoreCase,
13
+ parseInteger,
14
+ } from '../common/helpers';
15
+
16
+ // Re-export common helpers for convenience
17
+ export { hasChildCommand, getChildCommand, getChildCommands, parseIp } from '../common/helpers';
18
+
19
+ // ============================================================================
20
+ // Management Plane Security Helpers
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Check if password uses SHA-512 encryption (strong)
25
+ * @param node The config node containing password
26
+ * @returns true if using sha512 encryption
27
+ */
28
+ export const hasStrongPasswordEncryption = (node: ConfigNode): boolean => {
29
+ return includesIgnoreCase(node.id, 'sha512') || includesIgnoreCase(node.id, '$6$');
30
+ };
31
+
32
+ /**
33
+ * Check if password is plaintext (cleartext)
34
+ * @param node The config node containing password
35
+ * @returns true if password appears to be plaintext
36
+ */
37
+ export const hasPlaintextPassword = (node: ConfigNode): boolean => {
38
+ // Check for cleartext password patterns
39
+ if (includesIgnoreCase(node.id, 'secret 0 ') || includesIgnoreCase(node.id, 'password 0 ')) {
40
+ return true;
41
+ }
42
+ // Check for enable password without encryption type
43
+ if (/^enable\s+password\s+[^$]/i.test(node.id)) {
44
+ return true;
45
+ }
46
+ return false;
47
+ };
48
+
49
+ /**
50
+ * Check if service password-encryption is enabled
51
+ * @param ast The full AST array
52
+ * @returns true if service password-encryption is configured
53
+ */
54
+ export const hasServicePasswordEncryption = (ast: ConfigNode[]): boolean => {
55
+ return ast.some((node) =>
56
+ equalsIgnoreCase(node.id, 'service password-encryption')
57
+ );
58
+ };
59
+
60
+ /**
61
+ * Check if SSH version 2 is configured
62
+ * @param ast The full AST array
63
+ * @returns true if SSH v2 is configured
64
+ */
65
+ export const hasSshVersion2 = (ast: ConfigNode[]): boolean => {
66
+ return ast.some((node) =>
67
+ /^ip\s+ssh\s+version\s+2/i.test(node.id)
68
+ );
69
+ };
70
+
71
+ /**
72
+ * Check for weak SSH ciphers
73
+ * @param ast The full AST array
74
+ * @returns Array of weak ciphers found
75
+ */
76
+ export const getWeakSshCiphers = (ast: ConfigNode[]): string[] => {
77
+ const weakCiphers = ['3des-cbc', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc', 'blowfish-cbc'];
78
+ const found: string[] = [];
79
+
80
+ for (const node of ast) {
81
+ if (/^ip\s+ssh\s+ciphers/i.test(node.id)) {
82
+ for (const cipher of weakCiphers) {
83
+ if (includesIgnoreCase(node.id, cipher)) {
84
+ found.push(cipher);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return found;
90
+ };
91
+
92
+ /**
93
+ * Check if telnet management is disabled
94
+ * @param ast The full AST array
95
+ * @returns true if telnet is properly disabled
96
+ */
97
+ export const isTelnetDisabled = (ast: ConfigNode[]): boolean => {
98
+ // Check for 'no management telnet' or management telnet with shutdown
99
+ const noMgmtTelnet = ast.some((node) =>
100
+ /^no\s+management\s+telnet/i.test(node.id)
101
+ );
102
+
103
+ if (noMgmtTelnet) return true;
104
+
105
+ // Check if management telnet section exists and is shutdown
106
+ const mgmtTelnet = ast.find((node) =>
107
+ /^management\s+telnet/i.test(node.id)
108
+ );
109
+
110
+ if (mgmtTelnet) {
111
+ return mgmtTelnet.children.some((child) =>
112
+ equalsIgnoreCase(child.id, 'shutdown')
113
+ );
114
+ }
115
+
116
+ // Telnet is disabled by default in EOS
117
+ return true;
118
+ };
119
+
120
+ /**
121
+ * Check if HTTP server is disabled (insecure)
122
+ * @param ast The full AST array
123
+ * @returns true if HTTP server is disabled
124
+ */
125
+ export const isHttpServerDisabled = (ast: ConfigNode[]): boolean => {
126
+ const hasNoHttp = ast.some((node) =>
127
+ /^no\s+ip\s+http\s+server/i.test(node.id)
128
+ );
129
+ const hasHttp = ast.some((node) =>
130
+ /^ip\s+http\s+server/i.test(node.id) && !/^no\s+/i.test(node.id)
131
+ );
132
+ return hasNoHttp || !hasHttp;
133
+ };
134
+
135
+ /**
136
+ * Check for SNMPv1/v2c community strings (insecure)
137
+ * @param ast The full AST array
138
+ * @returns Array of insecure community configurations found
139
+ */
140
+ export const getInsecureSnmpCommunities = (ast: ConfigNode[]): string[] => {
141
+ const insecure: string[] = [];
142
+ const defaultCommunities = ['public', 'private', 'community'];
143
+
144
+ for (const node of ast) {
145
+ if (/^snmp-server\s+community\s+/i.test(node.id)) {
146
+ const match = node.id.match(/snmp-server\s+community\s+(\S+)/i);
147
+ if (match?.[1]) {
148
+ const community = match[1];
149
+ if (defaultCommunities.some((dc) => equalsIgnoreCase(community, dc))) {
150
+ insecure.push(`Default community "${match[1]}"`);
151
+ } else {
152
+ insecure.push(`SNMPv2c community configured`);
153
+ }
154
+ }
155
+ }
156
+ }
157
+ return insecure;
158
+ };
159
+
160
+ /**
161
+ * Check if SNMPv3 is properly configured with auth and priv
162
+ * @param ast The full AST array
163
+ * @returns true if SNMPv3 with priv mode is configured
164
+ */
165
+ export const hasSnmpV3AuthPriv = (ast: ConfigNode[]): boolean => {
166
+ return ast.some((node) =>
167
+ /^snmp-server\s+group\s+\S+\s+v3\s+priv/i.test(node.id) ||
168
+ /^snmp-server\s+user\s+\S+\s+\S+\s+v3\s+auth\s+\S+\s+\S+\s+priv/i.test(node.id)
169
+ );
170
+ };
171
+
172
+ /**
173
+ * Check if NTP authentication is enabled
174
+ * @param ast The full AST array
175
+ * @returns true if NTP authentication is configured
176
+ */
177
+ export const hasNtpAuthentication = (ast: ConfigNode[]): boolean => {
178
+ const hasAuthenticate = ast.some((node) =>
179
+ /^ntp\s+authenticate$/i.test(node.id)
180
+ );
181
+ const hasTrustedKey = ast.some((node) =>
182
+ /^ntp\s+trusted-key\s+/i.test(node.id)
183
+ );
184
+ return hasAuthenticate && hasTrustedKey;
185
+ };
186
+
187
+ /**
188
+ * Check if AAA authentication login is configured
189
+ * @param ast The full AST array
190
+ * @returns true if AAA authentication login is configured
191
+ */
192
+ export const hasAaaAuthenticationLogin = (ast: ConfigNode[]): boolean => {
193
+ return ast.some((node) =>
194
+ /^aaa\s+authentication\s+login\s+/i.test(node.id)
195
+ );
196
+ };
197
+
198
+ /**
199
+ * Check if TACACS+ is configured
200
+ * @param ast The full AST array
201
+ * @returns true if TACACS+ server is configured
202
+ */
203
+ export const hasTacacsServer = (ast: ConfigNode[]): boolean => {
204
+ return ast.some((node) =>
205
+ /^tacacs-server\s+host\s+/i.test(node.id)
206
+ );
207
+ };
208
+
209
+ /**
210
+ * Check if AAA accounting is configured
211
+ * @param ast The full AST array
212
+ * @returns true if AAA accounting is configured
213
+ */
214
+ export const hasAaaAccounting = (ast: ConfigNode[]): boolean => {
215
+ return ast.some((node) =>
216
+ /^aaa\s+accounting\s+/i.test(node.id)
217
+ );
218
+ };
219
+
220
+ /**
221
+ * Check if Management VRF is configured
222
+ * @param ast The full AST array
223
+ * @returns true if management VRF is properly configured
224
+ */
225
+ export const hasManagementVrf = (ast: ConfigNode[]): boolean => {
226
+ return ast.some((node) =>
227
+ /^vrf\s+instance\s+MGMT/i.test(node.id) ||
228
+ /^vrf\s+instance\s+management/i.test(node.id)
229
+ );
230
+ };
231
+
232
+ /**
233
+ * Check if login banner reveals system information (non-compliant)
234
+ * @param ast The full AST array
235
+ * @returns Array of information disclosure issues found
236
+ */
237
+ export const getBannerInfoDisclosure = (ast: ConfigNode[]): string[] => {
238
+ const issues: string[] = [];
239
+ const sensitivePatterns = [
240
+ { pattern: /version\s+\d+/i, desc: 'software version' },
241
+ { pattern: /arista/i, desc: 'vendor name' },
242
+ { pattern: /eos/i, desc: 'OS name' },
243
+ { pattern: /@\S+\.\S+/i, desc: 'email address' },
244
+ { pattern: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/, desc: 'IP address' },
245
+ ];
246
+
247
+ for (const node of ast) {
248
+ if (/^banner\s+(login|motd)/i.test(node.id)) {
249
+ const bannerText = node.rawText || node.id;
250
+ for (const { pattern, desc } of sensitivePatterns) {
251
+ if (pattern.test(bannerText)) {
252
+ issues.push(`Banner contains ${desc}`);
253
+ }
254
+ }
255
+ }
256
+ }
257
+ return issues;
258
+ };
259
+
260
+ /**
261
+ * Check if console idle timeout is configured
262
+ * @param ast The full AST array
263
+ * @returns The timeout value in minutes, or undefined if not set
264
+ */
265
+ export const getConsoleIdleTimeout = (ast: ConfigNode[]): number | undefined => {
266
+ for (const node of ast) {
267
+ if (/^management\s+console/i.test(node.id)) {
268
+ for (const child of node.children) {
269
+ const match = child.id.match(/idle-timeout\s+(\d+)/i);
270
+ if (match?.[1]) {
271
+ return parseInteger(match[1]) ?? undefined;
272
+ }
273
+ }
274
+ }
275
+ }
276
+ return undefined;
277
+ };
278
+
279
+ /**
280
+ * Check if ZTP (Zero Touch Provisioning) is disabled
281
+ * @param ast The full AST array
282
+ * @returns true if ZTP is disabled
283
+ */
284
+ export const isZtpDisabled = (ast: ConfigNode[]): boolean => {
285
+ return ast.some((node) =>
286
+ /^no\s+zerotouch\s+enable/i.test(node.id) ||
287
+ equalsIgnoreCase(node.id, 'zerotouch cancel')
288
+ );
289
+ };
290
+
291
+ // ============================================================================
292
+ // Control Plane Security Helpers
293
+ // ============================================================================
294
+
295
+ /**
296
+ * Check if Control Plane ACL is configured
297
+ * @param ast The full AST array
298
+ * @returns true if system control-plane ACL is configured
299
+ */
300
+ export const hasControlPlaneAcl = (ast: ConfigNode[]): boolean => {
301
+ return ast.some((node) =>
302
+ /^system\s+control-plane/i.test(node.id)
303
+ );
304
+ };
305
+
306
+ /**
307
+ * Check if CoPP (Control Plane Policing) is customized
308
+ * @param ast The full AST array
309
+ * @returns true if CoPP policy is customized
310
+ */
311
+ export const hasCoppPolicy = (ast: ConfigNode[]): boolean => {
312
+ return ast.some((node) =>
313
+ /^policy-map\s+type\s+copp/i.test(node.id)
314
+ );
315
+ };
316
+
317
+ /**
318
+ * Check if interface has ICMP redirects disabled
319
+ * @param interfaceNode The interface ConfigNode
320
+ * @returns true if ip redirects are disabled
321
+ */
322
+ export const hasNoIpRedirects = (interfaceNode: ConfigNode): boolean => {
323
+ return interfaceNode.children.some((child) =>
324
+ /^no\s+ip\s+redirects/i.test(child.id)
325
+ );
326
+ };
327
+
328
+ /**
329
+ * Check if interface has ICMP unreachables disabled
330
+ * @param interfaceNode The interface ConfigNode
331
+ * @returns true if ip unreachables are disabled
332
+ */
333
+ export const hasNoIpUnreachables = (interfaceNode: ConfigNode): boolean => {
334
+ return interfaceNode.children.some((child) =>
335
+ /^no\s+ip\s+unreachables/i.test(child.id)
336
+ );
337
+ };
338
+
339
+ /**
340
+ * Check if routing protocol has authentication configured
341
+ * @param routerNode The router ConfigNode (OSPF, IS-IS, etc.)
342
+ * @returns true if authentication is configured
343
+ */
344
+ export const hasRoutingProtocolAuth = (routerNode: ConfigNode): boolean => {
345
+ // Check for OSPF authentication
346
+ if (/^router\s+ospf/i.test(routerNode.id)) {
347
+ return routerNode.children.some((child) =>
348
+ /authentication/i.test(child.id)
349
+ );
350
+ }
351
+
352
+ // Check for IS-IS authentication
353
+ if (/^router\s+isis/i.test(routerNode.id)) {
354
+ return routerNode.children.some((child) =>
355
+ /authentication/i.test(child.id)
356
+ );
357
+ }
358
+
359
+ return false;
360
+ };
361
+
362
+ /**
363
+ * Check if BFD (Bidirectional Forwarding Detection) is configured
364
+ * @param ast The full AST array
365
+ * @returns true if BFD is configured
366
+ */
367
+ export const hasBfd = (ast: ConfigNode[]): boolean => {
368
+ return ast.some((node) =>
369
+ /^bfd\s+/i.test(node.id)
370
+ );
371
+ };
372
+
373
+ // ============================================================================
374
+ // Data Plane Security Helpers
375
+ // ============================================================================
376
+
377
+ /**
378
+ * Check if interface has storm control configured
379
+ * @param interfaceNode The interface ConfigNode
380
+ * @returns Object with storm control status for each type
381
+ */
382
+ export const getStormControlStatus = (interfaceNode: ConfigNode): { broadcast: boolean; multicast: boolean; unicast: boolean } => {
383
+ return {
384
+ broadcast: interfaceNode.children.some((child) =>
385
+ /^storm-control\s+broadcast/i.test(child.id)
386
+ ),
387
+ multicast: interfaceNode.children.some((child) =>
388
+ /^storm-control\s+multicast/i.test(child.id)
389
+ ),
390
+ unicast: interfaceNode.children.some((child) =>
391
+ /^storm-control\s+unknown-unicast/i.test(child.id)
392
+ ),
393
+ };
394
+ };
395
+
396
+ /**
397
+ * Check if DHCP snooping is enabled
398
+ * @param ast The full AST array
399
+ * @returns true if DHCP snooping is configured
400
+ */
401
+ export const hasDhcpSnooping = (ast: ConfigNode[]): boolean => {
402
+ return ast.some((node) =>
403
+ /^ip\s+dhcp\s+snooping$/i.test(node.id)
404
+ );
405
+ };
406
+
407
+ /**
408
+ * Check if interface is DHCP snooping trusted
409
+ * @param interfaceNode The interface ConfigNode
410
+ * @returns true if interface is DHCP snooping trusted
411
+ */
412
+ export const isDhcpSnoopingTrust = (interfaceNode: ConfigNode): boolean => {
413
+ return interfaceNode.children.some((child) =>
414
+ /^ip\s+dhcp\s+snooping\s+trust/i.test(child.id)
415
+ );
416
+ };
417
+
418
+ /**
419
+ * Check if Dynamic ARP Inspection is enabled
420
+ * @param ast The full AST array
421
+ * @returns true if DAI is configured
422
+ */
423
+ export const hasDynamicArpInspection = (ast: ConfigNode[]): boolean => {
424
+ return ast.some((node) =>
425
+ /^ip\s+arp\s+inspection\s+vlan/i.test(node.id)
426
+ );
427
+ };
428
+
429
+ /**
430
+ * Check if interface is ARP inspection trusted
431
+ * @param interfaceNode The interface ConfigNode
432
+ * @returns true if interface is ARP inspection trusted
433
+ */
434
+ export const isArpInspectionTrust = (interfaceNode: ConfigNode): boolean => {
435
+ return interfaceNode.children.some((child) =>
436
+ /^ip\s+arp\s+inspection\s+trust/i.test(child.id)
437
+ );
438
+ };
439
+
440
+ /**
441
+ * Check if IP Source Guard is enabled on interface
442
+ * @param interfaceNode The interface ConfigNode
443
+ * @returns true if IP verify source is configured
444
+ */
445
+ export const hasIpSourceGuard = (interfaceNode: ConfigNode): boolean => {
446
+ return interfaceNode.children.some((child) =>
447
+ /^ip\s+verify\s+source/i.test(child.id)
448
+ );
449
+ };
450
+
451
+ /**
452
+ * Check if port security is enabled on interface
453
+ * @param interfaceNode The interface ConfigNode
454
+ * @returns true if port security is configured
455
+ */
456
+ export const hasPortSecurity = (interfaceNode: ConfigNode): boolean => {
457
+ return interfaceNode.children.some((child) =>
458
+ /^switchport\s+port-security/i.test(child.id)
459
+ );
460
+ };
461
+
462
+ // ============================================================================
463
+ // BGP Security Helpers
464
+ // ============================================================================
465
+
466
+ /**
467
+ * Check if BGP neighbor has MD5/password authentication
468
+ * @param routerBgpNode The router bgp ConfigNode
469
+ * @param neighborIp Optional specific neighbor IP to check
470
+ * @returns Array of neighbors without authentication
471
+ */
472
+ export const getBgpNeighborsWithoutAuth = (routerBgpNode: ConfigNode, neighborIp?: string): string[] => {
473
+ const neighborsWithoutAuth: string[] = [];
474
+ const neighborConfigs = new Map<string, { hasPassword: boolean }>();
475
+
476
+ for (const child of routerBgpNode.children) {
477
+ const neighborMatch = child.id.match(/^neighbor\s+(\S+)/i);
478
+ if (neighborMatch?.[1]) {
479
+ const ip = neighborMatch[1];
480
+ if (!neighborConfigs.has(ip)) {
481
+ neighborConfigs.set(ip, { hasPassword: false });
482
+ }
483
+
484
+ if (/password/i.test(child.id)) {
485
+ const config = neighborConfigs.get(ip);
486
+ if (config) {
487
+ config.hasPassword = true;
488
+ }
489
+ }
490
+ }
491
+ }
492
+
493
+ for (const [ip, config] of neighborConfigs) {
494
+ if (!config.hasPassword) {
495
+ if (!neighborIp || ip === neighborIp) {
496
+ neighborsWithoutAuth.push(ip);
497
+ }
498
+ }
499
+ }
500
+
501
+ return neighborsWithoutAuth;
502
+ };
503
+
504
+ /**
505
+ * Check if BGP neighbor has TTL security (GTSM) configured
506
+ * @param routerBgpNode The router bgp ConfigNode
507
+ * @returns Array of neighbors without TTL security
508
+ */
509
+ export const getBgpNeighborsWithoutTtlSecurity = (routerBgpNode: ConfigNode): string[] => {
510
+ const neighborsWithoutTtl: string[] = [];
511
+ const neighborConfigs = new Map<string, { hasTtl: boolean; isEbgp: boolean }>();
512
+ const localAs = routerBgpNode.id.match(/router\s+bgp\s+(\d+)/i)?.[1];
513
+
514
+ for (const child of routerBgpNode.children) {
515
+ const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as\s+(\d+)/i);
516
+ if (neighborMatch?.[1] && neighborMatch?.[2]) {
517
+ const ip = neighborMatch[1];
518
+ const remoteAs = neighborMatch[2];
519
+ neighborConfigs.set(ip, {
520
+ hasTtl: false,
521
+ isEbgp: localAs !== remoteAs
522
+ });
523
+ }
524
+
525
+ const ttlMatch = child.id.match(/^neighbor\s+(\S+)\s+ttl\s+maximum-hops/i);
526
+ if (ttlMatch?.[1]) {
527
+ const config = neighborConfigs.get(ttlMatch[1]);
528
+ if (config) {
529
+ config.hasTtl = true;
530
+ }
531
+ }
532
+ }
533
+
534
+ for (const [ip, config] of neighborConfigs) {
535
+ if (config.isEbgp && !config.hasTtl) {
536
+ neighborsWithoutTtl.push(ip);
537
+ }
538
+ }
539
+
540
+ return neighborsWithoutTtl;
541
+ };
542
+
543
+ /**
544
+ * Check if BGP neighbor has maximum-routes configured
545
+ * @param routerBgpNode The router bgp ConfigNode
546
+ * @returns Array of neighbors without max-prefix limit
547
+ */
548
+ export const getBgpNeighborsWithoutMaxRoutes = (routerBgpNode: ConfigNode): string[] => {
549
+ const neighborsWithoutMax: string[] = [];
550
+ const neighborConfigs = new Map<string, boolean>();
551
+
552
+ for (const child of routerBgpNode.children) {
553
+ const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as/i);
554
+ if (neighborMatch?.[1]) {
555
+ neighborConfigs.set(neighborMatch[1], false);
556
+ }
557
+
558
+ const maxMatch = child.id.match(/^neighbor\s+(\S+)\s+maximum-routes/i);
559
+ if (maxMatch?.[1]) {
560
+ neighborConfigs.set(maxMatch[1], true);
561
+ }
562
+ }
563
+
564
+ for (const [ip, hasMax] of neighborConfigs) {
565
+ if (!hasMax) {
566
+ neighborsWithoutMax.push(ip);
567
+ }
568
+ }
569
+
570
+ return neighborsWithoutMax;
571
+ };
572
+
573
+ /**
574
+ * Check if BGP has graceful restart configured
575
+ * @param routerBgpNode The router bgp ConfigNode
576
+ * @returns true if graceful restart is configured
577
+ */
578
+ export const hasBgpGracefulRestart = (routerBgpNode: ConfigNode): boolean => {
579
+ return routerBgpNode.children.some((child) =>
580
+ /^bgp\s+graceful-restart/i.test(child.id)
581
+ );
582
+ };
583
+
584
+ /**
585
+ * Check if BGP has log-neighbor-changes configured
586
+ * @param routerBgpNode The router bgp ConfigNode
587
+ * @returns true if log-neighbor-changes is configured
588
+ */
589
+ export const hasBgpLogNeighborChanges = (routerBgpNode: ConfigNode): boolean => {
590
+ return routerBgpNode.children.some((child) =>
591
+ /^bgp\s+log-neighbor-changes/i.test(child.id)
592
+ );
593
+ };
594
+
595
+ // ============================================================================
596
+ // RPKI Helpers
597
+ // ============================================================================
598
+
599
+ /**
600
+ * Check if RPKI is configured
601
+ * @param routerBgpNode The router bgp ConfigNode
602
+ * @returns true if RPKI cache is configured
603
+ */
604
+ export const hasRpkiConfiguration = (routerBgpNode: ConfigNode): boolean => {
605
+ return routerBgpNode.children.some((child) =>
606
+ /^rpki\s+cache/i.test(child.id)
607
+ );
608
+ };
609
+
610
+ /**
611
+ * Check if RPKI origin validation is enabled
612
+ * @param routerBgpNode The router bgp ConfigNode
613
+ * @returns true if origin validation is configured
614
+ */
615
+ export const hasRpkiOriginValidation = (routerBgpNode: ConfigNode): boolean => {
616
+ return routerBgpNode.children.some((child) =>
617
+ /^rpki\s+origin-validation/i.test(child.id)
618
+ );
619
+ };
620
+
621
+ // ============================================================================
622
+ // Anti-Spoofing Helpers
623
+ // ============================================================================
624
+
625
+ /**
626
+ * Check if interface has uRPF (unicast RPF) configured
627
+ * @param interfaceNode The interface ConfigNode
628
+ * @returns Object with uRPF mode if configured
629
+ */
630
+ export const getUrpfMode = (interfaceNode: ConfigNode): { enabled: boolean; mode?: 'strict' | 'loose' } => {
631
+ for (const child of interfaceNode.children) {
632
+ if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+rx/i.test(child.id)) {
633
+ return { enabled: true, mode: 'strict' };
634
+ }
635
+ if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+any/i.test(child.id)) {
636
+ return { enabled: true, mode: 'loose' };
637
+ }
638
+ }
639
+ return { enabled: false };
640
+ };
641
+
642
+ // ============================================================================
643
+ // MLAG Helpers (existing, enhanced)
644
+ // ============================================================================
645
+
646
+ /**
647
+ * Check if MLAG dual-primary detection is configured
648
+ * @param mlagNode The MLAG configuration node
649
+ * @returns true if dual-primary detection is configured
650
+ */
651
+ export const hasMlagDualPrimaryDetection = (mlagNode: ConfigNode): boolean => {
652
+ return hasChildCommand(mlagNode, 'dual-primary detection');
653
+ };
654
+
655
+ /**
656
+ * Check if MLAG reload delays are configured
657
+ * @param mlagNode The MLAG configuration node
658
+ * @returns Object with reload delay configuration status
659
+ */
660
+ export const getMlagReloadDelays = (mlagNode: ConfigNode): { mlag: boolean; nonMlag: boolean } => {
661
+ return {
662
+ mlag: hasChildCommand(mlagNode, 'reload-delay mlag'),
663
+ nonMlag: hasChildCommand(mlagNode, 'reload-delay non-mlag'),
664
+ };
665
+ };
666
+
667
+ // ============================================================================
668
+ // VXLAN/EVPN Helpers (existing, enhanced)
669
+ // ============================================================================
670
+
671
+ /**
672
+ * Check if EVPN peers have password authentication
673
+ * @param routerBgpNode The router bgp ConfigNode
674
+ * @returns true if EVPN peer group has password
675
+ */
676
+ export const hasEvpnPeerAuth = (routerBgpNode: ConfigNode): boolean => {
677
+ // Look for EVPN peer group with password
678
+ let evpnPeerGroup: string | undefined;
679
+
680
+ for (const child of routerBgpNode.children) {
681
+ // Find EVPN address family activation
682
+ if (/^address-family\s+evpn/i.test(child.id)) {
683
+ for (const subchild of child.children) {
684
+ const match = subchild.id.match(/neighbor\s+(\S+)\s+activate/i);
685
+ if (match?.[1]) {
686
+ evpnPeerGroup = match[1];
687
+ }
688
+ }
689
+ }
690
+ }
691
+
692
+ if (!evpnPeerGroup) return false;
693
+
694
+ // Check if peer group has password
695
+ return routerBgpNode.children.some((child) =>
696
+ includesIgnoreCase(child.id, `neighbor ${evpnPeerGroup}`) &&
697
+ includesIgnoreCase(child.id, 'password')
698
+ );
699
+ };
700
+
701
+ // ============================================================================
702
+ // Logging/Monitoring Helpers
703
+ // ============================================================================
704
+
705
+ /**
706
+ * Check if logging is configured with specific level
707
+ * @param ast The full AST array
708
+ * @param minLevel Minimum required logging level
709
+ * @returns true if logging meets minimum level requirement
710
+ */
711
+ export const hasLoggingLevel = (ast: ConfigNode[], minLevel: string): boolean => {
712
+ const levels = ['emergencies', 'alerts', 'critical', 'errors', 'warnings', 'notifications', 'informational', 'debugging'];
713
+ const minIndex = levels.indexOf(minLevel.toLowerCase());
714
+
715
+ for (const node of ast) {
716
+ const match = node.id.match(/^logging\s+(?:buffered|trap)\s+(\S+)/i);
717
+ if (match?.[1]) {
718
+ const configuredIndex = levels.indexOf(match[1].toLowerCase());
719
+ if (configuredIndex >= minIndex) {
720
+ return true;
721
+ }
722
+ }
723
+ }
724
+ return false;
725
+ };
726
+
727
+ /**
728
+ * Check if logging source interface is configured
729
+ * @param ast The full AST array
730
+ * @returns true if logging source-interface is configured
731
+ */
732
+ export const hasLoggingSourceInterface = (ast: ConfigNode[]): boolean => {
733
+ return ast.some((node) =>
734
+ /^logging\s+source-interface/i.test(node.id)
735
+ );
736
+ };
737
+
738
+ /**
739
+ * Check if event-monitor is enabled
740
+ * @param ast The full AST array
741
+ * @returns true if event-monitor is configured
742
+ */
743
+ export const hasEventMonitor = (ast: ConfigNode[]): boolean => {
744
+ return ast.some((node) =>
745
+ /^event-monitor$/i.test(node.id)
746
+ );
747
+ };
748
+
749
+ // ============================================================================
750
+ // High Availability Helpers
751
+ // ============================================================================
752
+
753
+ /**
754
+ * Check if VRRP has authentication configured
755
+ * @param interfaceNode The interface ConfigNode
756
+ * @returns true if VRRP authentication is configured
757
+ */
758
+ export const hasVrrpAuthentication = (interfaceNode: ConfigNode): boolean => {
759
+ return interfaceNode.children.some((child) =>
760
+ /^vrrp\s+\d+\s+authentication/i.test(child.id)
761
+ );
762
+ };
763
+
764
+ /**
765
+ * Check if virtual-router MAC is configured (for MLAG VARP)
766
+ * @param ast The full AST array
767
+ * @returns true if ip virtual-router mac-address is configured
768
+ */
769
+ export const hasVirtualRouterMac = (ast: ConfigNode[]): boolean => {
770
+ return ast.some((node) =>
771
+ /^ip\s+virtual-router\s+mac-address/i.test(node.id)
772
+ );
773
+ };
774
+
775
+ // ============================================================================
776
+ // Interface Type Helpers (extending existing)
777
+ // ============================================================================
778
+
779
+ /**
780
+ * Check if interface is a WAN/external facing interface
781
+ * Based on description containing WAN, Internet, ISP, External keywords
782
+ * @param interfaceNode The interface ConfigNode
783
+ * @returns true if interface appears to be external facing
784
+ */
785
+ export const isExternalInterface = (interfaceNode: ConfigNode): boolean => {
786
+ const description = interfaceNode.children.find((child) =>
787
+ startsWithIgnoreCase(child.id, 'description ')
788
+ );
789
+
790
+ if (description) {
791
+ return (
792
+ includesIgnoreCase(description.id, 'wan') ||
793
+ includesIgnoreCase(description.id, 'internet') ||
794
+ includesIgnoreCase(description.id, 'isp') ||
795
+ includesIgnoreCase(description.id, 'external') ||
796
+ includesIgnoreCase(description.id, 'uplink') ||
797
+ includesIgnoreCase(description.id, 'peering')
798
+ );
799
+ }
800
+
801
+ return false;
802
+ };
803
+
804
+ /**
805
+ * Check if interface is an access (edge/endpoint) port
806
+ * @param interfaceNode The interface ConfigNode
807
+ * @returns true if interface is configured as access port
808
+ */
809
+ export const isAccessPort = (interfaceNode: ConfigNode): boolean => {
810
+ return interfaceNode.children.some((child) =>
811
+ /^switchport\s+mode\s+access/i.test(child.id)
812
+ );
813
+ };
814
+
815
+ /**
816
+ * Check if interface is a trunk port
817
+ * @param interfaceNode The interface ConfigNode
818
+ * @returns true if interface is configured as trunk port
819
+ */
820
+ export const isTrunkPort = (interfaceNode: ConfigNode): boolean => {
821
+ return interfaceNode.children.some((child) =>
822
+ /^switchport\s+mode\s+trunk/i.test(child.id)
823
+ );
824
+ };
825
+
826
+ /**
827
+ * Check if MLAG is configured
828
+ * @param ast The full AST array
829
+ * @returns true if mlag configuration block exists
830
+ */
831
+ export const hasMlagConfiguration = (ast: ConfigNode[]): boolean => {
832
+ return ast.some((node) =>
833
+ startsWithIgnoreCase(node.id, 'mlag configuration')
834
+ );
835
+ };
836
+
837
+ /**
838
+ * Get MLAG configuration node
839
+ * @param ast The full AST array
840
+ * @returns The MLAG configuration node, or undefined
841
+ */
842
+ export const getMlagConfiguration = (
843
+ ast: ConfigNode[]
844
+ ): ConfigNode | undefined => {
845
+ return ast.find((node) =>
846
+ startsWithIgnoreCase(node.id, 'mlag configuration')
847
+ );
848
+ };
849
+
850
+ /**
851
+ * Check if MLAG has required settings (domain-id, peer-link, peer-address)
852
+ * @param mlagNode The MLAG configuration node
853
+ * @returns Object with status of each MLAG requirement
854
+ */
855
+ export const checkMlagRequirements = (
856
+ mlagNode: ConfigNode
857
+ ): { hasDomainId: boolean; hasPeerLink: boolean; hasPeerAddress: boolean; hasLocalInterface: boolean } => {
858
+ return {
859
+ hasDomainId: hasChildCommand(mlagNode, 'domain-id'),
860
+ hasPeerLink: hasChildCommand(mlagNode, 'peer-link'),
861
+ hasPeerAddress: hasChildCommand(mlagNode, 'peer-address'),
862
+ hasLocalInterface: hasChildCommand(mlagNode, 'local-interface'),
863
+ };
864
+ };
865
+
866
+ /**
867
+ * Check if management API (eAPI) is configured
868
+ * @param ast The full AST array
869
+ * @returns true if management api http-commands is configured
870
+ */
871
+ export const hasManagementApi = (ast: ConfigNode[]): boolean => {
872
+ return ast.some((node) =>
873
+ startsWithIgnoreCase(node.id, 'management api')
874
+ );
875
+ };
876
+
877
+ /**
878
+ * Get management API configuration nodes
879
+ * @param ast The full AST array
880
+ * @returns Array of management API configuration nodes
881
+ */
882
+ export const getManagementApiNodes = (ast: ConfigNode[]): ConfigNode[] => {
883
+ return ast.filter((node) =>
884
+ startsWithIgnoreCase(node.id, 'management api')
885
+ );
886
+ };
887
+
888
+ /**
889
+ * Check if management API has HTTPS enabled (secure)
890
+ * @param apiNode The management api configuration node
891
+ * @returns true if HTTPS transport is configured
892
+ */
893
+ export const hasHttpsTransport = (apiNode: ConfigNode): boolean => {
894
+ // Check for "protocol https" or "no shutdown" with https
895
+ const hasProtocolHttps = apiNode.children.some((child) =>
896
+ includesIgnoreCase(child.id, 'protocol https')
897
+ );
898
+ const hasTransportHttps = apiNode.children.some((child) =>
899
+ includesIgnoreCase(child.id, 'transport https')
900
+ );
901
+ return hasProtocolHttps || hasTransportHttps;
902
+ };
903
+
904
+ /**
905
+ * Check if an interface is a VXLAN interface
906
+ * @param node The interface ConfigNode
907
+ * @returns true if it's a VXLAN interface
908
+ */
909
+ export const isVxlanInterface = (node: ConfigNode): boolean => {
910
+ return /^interface\s+Vxlan\d*/i.test(node.id);
911
+ };
912
+
913
+ /**
914
+ * Check if an interface is an MLAG peer-link (typically Port-Channel)
915
+ * @param node The interface ConfigNode
916
+ * @param mlagNode The MLAG configuration node (optional)
917
+ * @returns true if this interface is configured as MLAG peer-link
918
+ */
919
+ export const isMlagPeerLink = (
920
+ node: ConfigNode,
921
+ mlagNode?: ConfigNode
922
+ ): boolean => {
923
+ if (!mlagNode) return false;
924
+ const peerLink = getChildCommand(mlagNode, 'peer-link');
925
+ if (!peerLink) return false;
926
+
927
+ // Extract interface name from peer-link command
928
+ const match = peerLink.id.match(/peer-link\s+(\S+)/i);
929
+ if (!match) return false;
930
+
931
+ const peerLinkInterface = match[1];
932
+ if (!peerLinkInterface) return false;
933
+ return includesIgnoreCase(node.id, peerLinkInterface);
934
+ };
935
+
936
+ /**
937
+ * Get all VXLAN VNI mappings from a Vxlan interface
938
+ * @param vxlanNode The Vxlan interface ConfigNode
939
+ * @returns Array of VNI mappings
940
+ */
941
+ export const getVxlanVniMappings = (
942
+ vxlanNode: ConfigNode
943
+ ): { vni: string; vlan?: string }[] => {
944
+ const mappings: { vni: string; vlan?: string }[] = [];
945
+
946
+ for (const child of vxlanNode.children) {
947
+ const vniMatch = child.id.match(/vxlan\s+vni\s+(\d+)\s+vlan\s+(\d+)/i);
948
+ if (vniMatch) {
949
+ const vni = vniMatch[1];
950
+ if (!vni) {
951
+ continue;
952
+ }
953
+ const vlan = vniMatch[2];
954
+ mappings.push({ vni, vlan });
955
+ continue;
956
+ }
957
+
958
+ const simpleMatch = child.id.match(/vxlan\s+vni\s+(\d+)/i);
959
+ if (simpleMatch) {
960
+ const vni = simpleMatch[1];
961
+ if (!vni) {
962
+ continue;
963
+ }
964
+ mappings.push({ vni });
965
+ }
966
+ }
967
+
968
+ return mappings;
969
+ };
970
+
971
+ /**
972
+ * Check if VXLAN has source interface configured
973
+ * @param vxlanNode The Vxlan interface ConfigNode
974
+ * @returns true if vxlan source-interface is configured
975
+ */
976
+ export const hasVxlanSourceInterface = (vxlanNode: ConfigNode): boolean => {
977
+ return hasChildCommand(vxlanNode, 'vxlan source-interface');
978
+ };
979
+
980
+ /**
981
+ * Check if interface has MLAG ID configured
982
+ * @param interfaceNode The interface ConfigNode
983
+ * @returns The MLAG ID if configured, undefined otherwise
984
+ */
985
+ export const getMlagId = (interfaceNode: ConfigNode): string | undefined => {
986
+ const mlagCmd = interfaceNode.children.find((child) =>
987
+ /^mlag\s+\d+/i.test(child.id)
988
+ );
989
+ if (!mlagCmd) return undefined;
990
+
991
+ const match = mlagCmd.id.match(/mlag\s+(\d+)/i);
992
+ return match ? match[1] : undefined;
993
+ };
994
+
995
+ /**
996
+ * Check if interface is a Port-Channel
997
+ * @param node The interface ConfigNode
998
+ * @returns true if it's a Port-Channel interface
999
+ */
1000
+ export const isPortChannel = (node: ConfigNode): boolean => {
1001
+ return /^interface\s+Port-Channel\d+/i.test(node.id);
1002
+ };
1003
+
1004
+ /**
1005
+ * Check if interface is a Loopback
1006
+ * @param node The interface ConfigNode
1007
+ * @returns true if it's a Loopback interface
1008
+ */
1009
+ export const isLoopback = (node: ConfigNode): boolean => {
1010
+ return /^interface\s+Loopback\d+/i.test(node.id);
1011
+ };
1012
+
1013
+ /**
1014
+ * Check if interface is an SVI (VLAN interface)
1015
+ * @param node The interface ConfigNode
1016
+ * @returns true if it's a VLAN SVI
1017
+ */
1018
+ export const isSvi = (node: ConfigNode): boolean => {
1019
+ return /^interface\s+Vlan\d+/i.test(node.id);
1020
+ };
1021
+
1022
+ /**
1023
+ * Check if interface is a Management interface
1024
+ * @param node The interface ConfigNode
1025
+ * @returns true if it's a Management interface
1026
+ */
1027
+ export const isManagementInterface = (node: ConfigNode): boolean => {
1028
+ return /^interface\s+Management\d+/i.test(node.id);
1029
+ };
1030
+
1031
+ /**
1032
+ * Check if interface is an Ethernet port
1033
+ * @param node The interface ConfigNode
1034
+ * @returns true if it's an Ethernet interface
1035
+ */
1036
+ export const isEthernetInterface = (node: ConfigNode): boolean => {
1037
+ return /^interface\s+Ethernet\d+/i.test(node.id);
1038
+ };
1039
+
1040
+ /**
1041
+ * Check if daemon is configured
1042
+ * @param ast The full AST array
1043
+ * @param daemonName Optional specific daemon name to check
1044
+ * @returns true if daemon(s) are configured
1045
+ */
1046
+ export const hasDaemon = (ast: ConfigNode[], daemonName?: string): boolean => {
1047
+ if (daemonName) {
1048
+ return ast.some((node) =>
1049
+ equalsIgnoreCase(node.id, `daemon ${daemonName}`)
1050
+ );
1051
+ }
1052
+ return ast.some((node) => startsWithIgnoreCase(node.id, 'daemon '));
1053
+ };
1054
+
1055
+ /**
1056
+ * Check if event-handler is configured
1057
+ * @param ast The full AST array
1058
+ * @returns true if event-handler(s) are configured
1059
+ */
1060
+ export const hasEventHandler = (ast: ConfigNode[]): boolean => {
1061
+ return ast.some((node) => startsWithIgnoreCase(node.id, 'event-handler '));
1062
+ };
1063
+
1064
+ /**
1065
+ * Get all VRF instances
1066
+ * @param ast The full AST array
1067
+ * @returns Array of VRF instance nodes
1068
+ */
1069
+ export const getVrfInstances = (ast: ConfigNode[]): ConfigNode[] => {
1070
+ return ast.filter((node) =>
1071
+ /^vrf\s+instance\s+\S+/i.test(node.id)
1072
+ );
1073
+ };
1074
+
1075
+ /**
1076
+ * Check if interface is in a VRF
1077
+ * @param interfaceNode The interface ConfigNode
1078
+ * @returns The VRF name if configured, undefined otherwise
1079
+ */
1080
+ export const getInterfaceVrf = (interfaceNode: ConfigNode): string | undefined => {
1081
+ const vrfCmd = interfaceNode.children.find((child) =>
1082
+ /^vrf\s+\S+/i.test(child.id)
1083
+ );
1084
+ if (!vrfCmd) return undefined;
1085
+
1086
+ const match = vrfCmd.id.match(/vrf\s+(\S+)/i);
1087
+ return match ? match[1] : undefined;
1088
+ };
1089
+
1090
+ /**
1091
+ * Check if BGP EVPN is configured
1092
+ * @param routerBgpNode The router bgp ConfigNode
1093
+ * @returns true if EVPN address-family is configured
1094
+ */
1095
+ export const hasEvpnAddressFamily = (routerBgpNode: ConfigNode): boolean => {
1096
+ return routerBgpNode.children.some((child) =>
1097
+ /^address-family\s+evpn/i.test(child.id)
1098
+ );
1099
+ };
1100
+
1101
+ /**
1102
+ * Check if interface has IP virtual-router address (VARP)
1103
+ * @param interfaceNode The interface ConfigNode
1104
+ * @returns true if ip virtual-router address is configured
1105
+ */
1106
+ export const hasVirtualRouterAddress = (interfaceNode: ConfigNode): boolean => {
1107
+ return interfaceNode.children.some((child) =>
1108
+ /^ip\s+virtual-router\s+address/i.test(child.id)
1109
+ );
1110
+ };
1111
+
1112
+ /**
1113
+ * Check if interface has ip address configured
1114
+ * @param interfaceNode The interface ConfigNode
1115
+ * @returns true if ip address is configured
1116
+ */
1117
+ export const hasIpAddress = (interfaceNode: ConfigNode): boolean => {
1118
+ return interfaceNode.children.some((child) =>
1119
+ /^ip\s+address\s+\d+\.\d+\.\d+\.\d+/i.test(child.id)
1120
+ );
1121
+ };
1122
+
1123
+ /**
1124
+ * Check if interface is shutdown
1125
+ * @param interfaceNode The interface ConfigNode
1126
+ * @returns true if interface is shutdown
1127
+ */
1128
+ export const isShutdown = (interfaceNode: ConfigNode): boolean => {
1129
+ const hasShutdown = interfaceNode.children.some((child) =>
1130
+ equalsIgnoreCase(child.id, 'shutdown')
1131
+ );
1132
+ const hasNoShutdown = interfaceNode.children.some((child) =>
1133
+ equalsIgnoreCase(child.id, 'no shutdown')
1134
+ );
1135
+ return hasShutdown && !hasNoShutdown;
1136
+ };
1137
+
1138
+ /**
1139
+ * Get interface description
1140
+ * @param interfaceNode The interface ConfigNode
1141
+ * @returns The description if configured, undefined otherwise
1142
+ */
1143
+ export const getInterfaceDescription = (interfaceNode: ConfigNode): string | undefined => {
1144
+ const descCmd = interfaceNode.children.find((child) =>
1145
+ startsWithIgnoreCase(child.id, 'description ')
1146
+ );
1147
+ if (!descCmd) return undefined;
1148
+
1149
+ return descCmd.id.replace(/^description\s+/i, '').trim();
1150
+ };
1151
+
1152
+ /**
1153
+ * Check if NTP is configured
1154
+ * @param ast The full AST array
1155
+ * @returns true if NTP server(s) are configured
1156
+ */
1157
+ export const hasNtpServer = (ast: ConfigNode[]): boolean => {
1158
+ return ast.some((node) =>
1159
+ /^ntp\s+server\s+/i.test(node.id)
1160
+ );
1161
+ };
1162
+
1163
+ /**
1164
+ * Check if syslog/logging is configured
1165
+ * @param ast The full AST array
1166
+ * @returns true if logging host is configured
1167
+ */
1168
+ export const hasLoggingHost = (ast: ConfigNode[]): boolean => {
1169
+ return ast.some((node) =>
1170
+ /^logging\s+host\s+/i.test(node.id)
1171
+ );
1172
+ };
1173
+
1174
+ /**
1175
+ * Check if SNMP is configured
1176
+ * @param ast The full AST array
1177
+ * @returns true if SNMP is configured
1178
+ */
1179
+ export const hasSnmpServer = (ast: ConfigNode[]): boolean => {
1180
+ return ast.some((node) =>
1181
+ /^snmp-server\s+/i.test(node.id)
1182
+ );
1183
+ };
1184
+
1185
+ /**
1186
+ * Check if AAA is configured
1187
+ * @param ast The full AST array
1188
+ * @returns true if AAA is configured
1189
+ */
1190
+ export const hasAaa = (ast: ConfigNode[]): boolean => {
1191
+ return ast.some((node) =>
1192
+ /^aaa\s+/i.test(node.id)
1193
+ );
1194
+ };
1195
+
1196
+ /**
1197
+ * Check if spanning-tree is configured
1198
+ * @param ast The full AST array
1199
+ * @returns true if spanning-tree is configured
1200
+ */
1201
+ export const hasSpanningTree = (ast: ConfigNode[]): boolean => {
1202
+ return ast.some((node) =>
1203
+ /^spanning-tree\s+/i.test(node.id)
1204
+ );
1205
+ };
1206
+
1207
+ /**
1208
+ * Get spanning-tree mode
1209
+ * @param ast The full AST array
1210
+ * @returns The spanning-tree mode (mstp, rapid-pvst, none, etc.)
1211
+ */
1212
+ export const getSpanningTreeMode = (ast: ConfigNode[]): string | undefined => {
1213
+ const stpNode = ast.find((node) =>
1214
+ /^spanning-tree\s+mode\s+/i.test(node.id)
1215
+ );
1216
+ if (!stpNode) return undefined;
1217
+
1218
+ const match = stpNode.id.match(/spanning-tree\s+mode\s+(\S+)/i);
1219
+ return match ? match[1] : undefined;
1220
+ };