@sentriflow/core 0.2.0 → 0.2.1

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.
@@ -52,8 +52,9 @@ export const hasPlaintextPassword = (node: ConfigNode): boolean => {
52
52
  * @returns true if service password-encryption is configured
53
53
  */
54
54
  export const hasServicePasswordEncryption = (ast: ConfigNode[]): boolean => {
55
+ if (!Array.isArray(ast)) return false;
55
56
  return ast.some((node) =>
56
- equalsIgnoreCase(node.id, 'service password-encryption')
57
+ equalsIgnoreCase(node?.id ?? '', 'service password-encryption')
57
58
  );
58
59
  };
59
60
 
@@ -63,8 +64,9 @@ export const hasServicePasswordEncryption = (ast: ConfigNode[]): boolean => {
63
64
  * @returns true if SSH v2 is configured
64
65
  */
65
66
  export const hasSshVersion2 = (ast: ConfigNode[]): boolean => {
67
+ if (!Array.isArray(ast)) return false;
66
68
  return ast.some((node) =>
67
- /^ip\s+ssh\s+version\s+2/i.test(node.id)
69
+ /^ip\s+ssh\s+version\s+2/i.test(node?.id ?? '')
68
70
  );
69
71
  };
70
72
 
@@ -74,13 +76,14 @@ export const hasSshVersion2 = (ast: ConfigNode[]): boolean => {
74
76
  * @returns Array of weak ciphers found
75
77
  */
76
78
  export const getWeakSshCiphers = (ast: ConfigNode[]): string[] => {
79
+ if (!Array.isArray(ast)) return [];
77
80
  const weakCiphers = ['3des-cbc', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc', 'blowfish-cbc'];
78
81
  const found: string[] = [];
79
82
 
80
83
  for (const node of ast) {
81
- if (/^ip\s+ssh\s+ciphers/i.test(node.id)) {
84
+ if (/^ip\s+ssh\s+ciphers/i.test(node?.id ?? '')) {
82
85
  for (const cipher of weakCiphers) {
83
- if (includesIgnoreCase(node.id, cipher)) {
86
+ if (includesIgnoreCase(node?.id ?? '', cipher)) {
84
87
  found.push(cipher);
85
88
  }
86
89
  }
@@ -95,21 +98,22 @@ export const getWeakSshCiphers = (ast: ConfigNode[]): string[] => {
95
98
  * @returns true if telnet is properly disabled
96
99
  */
97
100
  export const isTelnetDisabled = (ast: ConfigNode[]): boolean => {
101
+ if (!Array.isArray(ast)) return true;
98
102
  // Check for 'no management telnet' or management telnet with shutdown
99
103
  const noMgmtTelnet = ast.some((node) =>
100
- /^no\s+management\s+telnet/i.test(node.id)
104
+ node?.id && /^no\s+management\s+telnet/i.test(node.id)
101
105
  );
102
106
 
103
107
  if (noMgmtTelnet) return true;
104
108
 
105
109
  // Check if management telnet section exists and is shutdown
106
110
  const mgmtTelnet = ast.find((node) =>
107
- /^management\s+telnet/i.test(node.id)
111
+ node?.id && /^management\s+telnet/i.test(node.id)
108
112
  );
109
113
 
110
- if (mgmtTelnet) {
114
+ if (mgmtTelnet?.children) {
111
115
  return mgmtTelnet.children.some((child) =>
112
- equalsIgnoreCase(child.id, 'shutdown')
116
+ child?.id && equalsIgnoreCase(child.id, 'shutdown')
113
117
  );
114
118
  }
115
119
 
@@ -123,11 +127,12 @@ export const isTelnetDisabled = (ast: ConfigNode[]): boolean => {
123
127
  * @returns true if HTTP server is disabled
124
128
  */
125
129
  export const isHttpServerDisabled = (ast: ConfigNode[]): boolean => {
130
+ if (!Array.isArray(ast)) return true;
126
131
  const hasNoHttp = ast.some((node) =>
127
- /^no\s+ip\s+http\s+server/i.test(node.id)
132
+ /^no\s+ip\s+http\s+server/i.test(node?.id ?? '')
128
133
  );
129
134
  const hasHttp = ast.some((node) =>
130
- /^ip\s+http\s+server/i.test(node.id) && !/^no\s+/i.test(node.id)
135
+ /^ip\s+http\s+server/i.test(node?.id ?? '') && !/^no\s+/i.test(node?.id ?? '')
131
136
  );
132
137
  return hasNoHttp || !hasHttp;
133
138
  };
@@ -138,12 +143,13 @@ export const isHttpServerDisabled = (ast: ConfigNode[]): boolean => {
138
143
  * @returns Array of insecure community configurations found
139
144
  */
140
145
  export const getInsecureSnmpCommunities = (ast: ConfigNode[]): string[] => {
146
+ if (!Array.isArray(ast)) return [];
141
147
  const insecure: string[] = [];
142
148
  const defaultCommunities = ['public', 'private', 'community'];
143
149
 
144
150
  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);
151
+ if (/^snmp-server\s+community\s+/i.test(node?.id ?? '')) {
152
+ const match = node?.id?.match(/snmp-server\s+community\s+(\S+)/i);
147
153
  if (match?.[1]) {
148
154
  const community = match[1];
149
155
  if (defaultCommunities.some((dc) => equalsIgnoreCase(community, dc))) {
@@ -163,9 +169,10 @@ export const getInsecureSnmpCommunities = (ast: ConfigNode[]): string[] => {
163
169
  * @returns true if SNMPv3 with priv mode is configured
164
170
  */
165
171
  export const hasSnmpV3AuthPriv = (ast: ConfigNode[]): boolean => {
172
+ if (!Array.isArray(ast)) return false;
166
173
  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)
174
+ /^snmp-server\s+group\s+\S+\s+v3\s+priv/i.test(node?.id ?? '') ||
175
+ /^snmp-server\s+user\s+\S+\s+\S+\s+v3\s+auth\s+\S+\s+\S+\s+priv/i.test(node?.id ?? '')
169
176
  );
170
177
  };
171
178
 
@@ -175,11 +182,13 @@ export const hasSnmpV3AuthPriv = (ast: ConfigNode[]): boolean => {
175
182
  * @returns true if NTP authentication is configured
176
183
  */
177
184
  export const hasNtpAuthentication = (ast: ConfigNode[]): boolean => {
185
+ // Guard against collision: other vendors pass single node, Arista expects array
186
+ if (!Array.isArray(ast)) return false;
178
187
  const hasAuthenticate = ast.some((node) =>
179
- /^ntp\s+authenticate$/i.test(node.id)
188
+ /^ntp\s+authenticate$/i.test(node?.id ?? '')
180
189
  );
181
190
  const hasTrustedKey = ast.some((node) =>
182
- /^ntp\s+trusted-key\s+/i.test(node.id)
191
+ /^ntp\s+trusted-key\s+/i.test(node?.id ?? '')
183
192
  );
184
193
  return hasAuthenticate && hasTrustedKey;
185
194
  };
@@ -190,8 +199,9 @@ export const hasNtpAuthentication = (ast: ConfigNode[]): boolean => {
190
199
  * @returns true if AAA authentication login is configured
191
200
  */
192
201
  export const hasAaaAuthenticationLogin = (ast: ConfigNode[]): boolean => {
202
+ if (!Array.isArray(ast)) return false;
193
203
  return ast.some((node) =>
194
- /^aaa\s+authentication\s+login\s+/i.test(node.id)
204
+ /^aaa\s+authentication\s+login\s+/i.test(node?.id ?? '')
195
205
  );
196
206
  };
197
207
 
@@ -201,8 +211,9 @@ export const hasAaaAuthenticationLogin = (ast: ConfigNode[]): boolean => {
201
211
  * @returns true if TACACS+ server is configured
202
212
  */
203
213
  export const hasTacacsServer = (ast: ConfigNode[]): boolean => {
214
+ if (!Array.isArray(ast)) return false;
204
215
  return ast.some((node) =>
205
- /^tacacs-server\s+host\s+/i.test(node.id)
216
+ /^tacacs-server\s+host\s+/i.test(node?.id ?? '')
206
217
  );
207
218
  };
208
219
 
@@ -212,8 +223,9 @@ export const hasTacacsServer = (ast: ConfigNode[]): boolean => {
212
223
  * @returns true if AAA accounting is configured
213
224
  */
214
225
  export const hasAaaAccounting = (ast: ConfigNode[]): boolean => {
226
+ if (!Array.isArray(ast)) return false;
215
227
  return ast.some((node) =>
216
- /^aaa\s+accounting\s+/i.test(node.id)
228
+ /^aaa\s+accounting\s+/i.test(node?.id ?? '')
217
229
  );
218
230
  };
219
231
 
@@ -223,9 +235,10 @@ export const hasAaaAccounting = (ast: ConfigNode[]): boolean => {
223
235
  * @returns true if management VRF is properly configured
224
236
  */
225
237
  export const hasManagementVrf = (ast: ConfigNode[]): boolean => {
238
+ if (!Array.isArray(ast)) return false;
226
239
  return ast.some((node) =>
227
- /^vrf\s+instance\s+MGMT/i.test(node.id) ||
228
- /^vrf\s+instance\s+management/i.test(node.id)
240
+ /^vrf\s+instance\s+MGMT/i.test(node?.id ?? '') ||
241
+ /^vrf\s+instance\s+management/i.test(node?.id ?? '')
229
242
  );
230
243
  };
231
244
 
@@ -235,6 +248,7 @@ export const hasManagementVrf = (ast: ConfigNode[]): boolean => {
235
248
  * @returns Array of information disclosure issues found
236
249
  */
237
250
  export const getBannerInfoDisclosure = (ast: ConfigNode[]): string[] => {
251
+ if (!Array.isArray(ast)) return [];
238
252
  const issues: string[] = [];
239
253
  const sensitivePatterns = [
240
254
  { pattern: /version\s+\d+/i, desc: 'software version' },
@@ -245,8 +259,8 @@ export const getBannerInfoDisclosure = (ast: ConfigNode[]): string[] => {
245
259
  ];
246
260
 
247
261
  for (const node of ast) {
248
- if (/^banner\s+(login|motd)/i.test(node.id)) {
249
- const bannerText = node.rawText || node.id;
262
+ if (/^banner\s+(login|motd)/i.test(node?.id ?? '')) {
263
+ const bannerText = node?.rawText ?? node?.id ?? '';
250
264
  for (const { pattern, desc } of sensitivePatterns) {
251
265
  if (pattern.test(bannerText)) {
252
266
  issues.push(`Banner contains ${desc}`);
@@ -263,10 +277,12 @@ export const getBannerInfoDisclosure = (ast: ConfigNode[]): string[] => {
263
277
  * @returns The timeout value in minutes, or undefined if not set
264
278
  */
265
279
  export const getConsoleIdleTimeout = (ast: ConfigNode[]): number | undefined => {
280
+ if (!Array.isArray(ast)) return undefined;
266
281
  for (const node of ast) {
267
- if (/^management\s+console/i.test(node.id)) {
282
+ if (node?.id && /^management\s+console/i.test(node.id)) {
283
+ if (!node?.children) continue;
268
284
  for (const child of node.children) {
269
- const match = child.id.match(/idle-timeout\s+(\d+)/i);
285
+ const match = child?.id?.match(/idle-timeout\s+(\d+)/i);
270
286
  if (match?.[1]) {
271
287
  return parseInteger(match[1]) ?? undefined;
272
288
  }
@@ -282,9 +298,10 @@ export const getConsoleIdleTimeout = (ast: ConfigNode[]): number | undefined =>
282
298
  * @returns true if ZTP is disabled
283
299
  */
284
300
  export const isZtpDisabled = (ast: ConfigNode[]): boolean => {
301
+ if (!Array.isArray(ast)) return false;
285
302
  return ast.some((node) =>
286
- /^no\s+zerotouch\s+enable/i.test(node.id) ||
287
- equalsIgnoreCase(node.id, 'zerotouch cancel')
303
+ /^no\s+zerotouch\s+enable/i.test(node?.id ?? '') ||
304
+ equalsIgnoreCase(node?.id ?? '', 'zerotouch cancel')
288
305
  );
289
306
  };
290
307
 
@@ -298,8 +315,9 @@ export const isZtpDisabled = (ast: ConfigNode[]): boolean => {
298
315
  * @returns true if system control-plane ACL is configured
299
316
  */
300
317
  export const hasControlPlaneAcl = (ast: ConfigNode[]): boolean => {
318
+ if (!Array.isArray(ast)) return false;
301
319
  return ast.some((node) =>
302
- /^system\s+control-plane/i.test(node.id)
320
+ /^system\s+control-plane/i.test(node?.id ?? '')
303
321
  );
304
322
  };
305
323
 
@@ -309,8 +327,9 @@ export const hasControlPlaneAcl = (ast: ConfigNode[]): boolean => {
309
327
  * @returns true if CoPP policy is customized
310
328
  */
311
329
  export const hasCoppPolicy = (ast: ConfigNode[]): boolean => {
330
+ if (!Array.isArray(ast)) return false;
312
331
  return ast.some((node) =>
313
- /^policy-map\s+type\s+copp/i.test(node.id)
332
+ /^policy-map\s+type\s+copp/i.test(node?.id ?? '')
314
333
  );
315
334
  };
316
335
 
@@ -320,8 +339,9 @@ export const hasCoppPolicy = (ast: ConfigNode[]): boolean => {
320
339
  * @returns true if ip redirects are disabled
321
340
  */
322
341
  export const hasNoIpRedirects = (interfaceNode: ConfigNode): boolean => {
342
+ if (!interfaceNode?.children) return false;
323
343
  return interfaceNode.children.some((child) =>
324
- /^no\s+ip\s+redirects/i.test(child.id)
344
+ child?.id && /^no\s+ip\s+redirects/i.test(child.id)
325
345
  );
326
346
  };
327
347
 
@@ -331,8 +351,9 @@ export const hasNoIpRedirects = (interfaceNode: ConfigNode): boolean => {
331
351
  * @returns true if ip unreachables are disabled
332
352
  */
333
353
  export const hasNoIpUnreachables = (interfaceNode: ConfigNode): boolean => {
354
+ if (!interfaceNode?.children) return false;
334
355
  return interfaceNode.children.some((child) =>
335
- /^no\s+ip\s+unreachables/i.test(child.id)
356
+ child?.id && /^no\s+ip\s+unreachables/i.test(child.id)
336
357
  );
337
358
  };
338
359
 
@@ -342,17 +363,18 @@ export const hasNoIpUnreachables = (interfaceNode: ConfigNode): boolean => {
342
363
  * @returns true if authentication is configured
343
364
  */
344
365
  export const hasRoutingProtocolAuth = (routerNode: ConfigNode): boolean => {
366
+ if (!routerNode?.id || !routerNode?.children) return false;
345
367
  // Check for OSPF authentication
346
368
  if (/^router\s+ospf/i.test(routerNode.id)) {
347
369
  return routerNode.children.some((child) =>
348
- /authentication/i.test(child.id)
370
+ child?.id && /authentication/i.test(child.id)
349
371
  );
350
372
  }
351
373
 
352
374
  // Check for IS-IS authentication
353
375
  if (/^router\s+isis/i.test(routerNode.id)) {
354
376
  return routerNode.children.some((child) =>
355
- /authentication/i.test(child.id)
377
+ child?.id && /authentication/i.test(child.id)
356
378
  );
357
379
  }
358
380
 
@@ -365,8 +387,9 @@ export const hasRoutingProtocolAuth = (routerNode: ConfigNode): boolean => {
365
387
  * @returns true if BFD is configured
366
388
  */
367
389
  export const hasBfd = (ast: ConfigNode[]): boolean => {
390
+ if (!Array.isArray(ast)) return false;
368
391
  return ast.some((node) =>
369
- /^bfd\s+/i.test(node.id)
392
+ /^bfd\s+/i.test(node?.id ?? '')
370
393
  );
371
394
  };
372
395
 
@@ -380,15 +403,16 @@ export const hasBfd = (ast: ConfigNode[]): boolean => {
380
403
  * @returns Object with storm control status for each type
381
404
  */
382
405
  export const getStormControlStatus = (interfaceNode: ConfigNode): { broadcast: boolean; multicast: boolean; unicast: boolean } => {
406
+ if (!interfaceNode?.children) return { broadcast: false, multicast: false, unicast: false };
383
407
  return {
384
408
  broadcast: interfaceNode.children.some((child) =>
385
- /^storm-control\s+broadcast/i.test(child.id)
409
+ child?.id && /^storm-control\s+broadcast/i.test(child.id)
386
410
  ),
387
411
  multicast: interfaceNode.children.some((child) =>
388
- /^storm-control\s+multicast/i.test(child.id)
412
+ child?.id && /^storm-control\s+multicast/i.test(child.id)
389
413
  ),
390
414
  unicast: interfaceNode.children.some((child) =>
391
- /^storm-control\s+unknown-unicast/i.test(child.id)
415
+ child?.id && /^storm-control\s+unknown-unicast/i.test(child.id)
392
416
  ),
393
417
  };
394
418
  };
@@ -399,8 +423,9 @@ export const getStormControlStatus = (interfaceNode: ConfigNode): { broadcast: b
399
423
  * @returns true if DHCP snooping is configured
400
424
  */
401
425
  export const hasDhcpSnooping = (ast: ConfigNode[]): boolean => {
426
+ if (!Array.isArray(ast)) return false;
402
427
  return ast.some((node) =>
403
- /^ip\s+dhcp\s+snooping$/i.test(node.id)
428
+ /^ip\s+dhcp\s+snooping$/i.test(node?.id ?? '')
404
429
  );
405
430
  };
406
431
 
@@ -410,8 +435,9 @@ export const hasDhcpSnooping = (ast: ConfigNode[]): boolean => {
410
435
  * @returns true if interface is DHCP snooping trusted
411
436
  */
412
437
  export const isDhcpSnoopingTrust = (interfaceNode: ConfigNode): boolean => {
438
+ if (!interfaceNode?.children) return false;
413
439
  return interfaceNode.children.some((child) =>
414
- /^ip\s+dhcp\s+snooping\s+trust/i.test(child.id)
440
+ child?.id && /^ip\s+dhcp\s+snooping\s+trust/i.test(child.id)
415
441
  );
416
442
  };
417
443
 
@@ -421,8 +447,9 @@ export const isDhcpSnoopingTrust = (interfaceNode: ConfigNode): boolean => {
421
447
  * @returns true if DAI is configured
422
448
  */
423
449
  export const hasDynamicArpInspection = (ast: ConfigNode[]): boolean => {
450
+ if (!Array.isArray(ast)) return false;
424
451
  return ast.some((node) =>
425
- /^ip\s+arp\s+inspection\s+vlan/i.test(node.id)
452
+ /^ip\s+arp\s+inspection\s+vlan/i.test(node?.id ?? '')
426
453
  );
427
454
  };
428
455
 
@@ -432,8 +459,9 @@ export const hasDynamicArpInspection = (ast: ConfigNode[]): boolean => {
432
459
  * @returns true if interface is ARP inspection trusted
433
460
  */
434
461
  export const isArpInspectionTrust = (interfaceNode: ConfigNode): boolean => {
462
+ if (!interfaceNode?.children) return false;
435
463
  return interfaceNode.children.some((child) =>
436
- /^ip\s+arp\s+inspection\s+trust/i.test(child.id)
464
+ child?.id && /^ip\s+arp\s+inspection\s+trust/i.test(child.id)
437
465
  );
438
466
  };
439
467
 
@@ -443,8 +471,9 @@ export const isArpInspectionTrust = (interfaceNode: ConfigNode): boolean => {
443
471
  * @returns true if IP verify source is configured
444
472
  */
445
473
  export const hasIpSourceGuard = (interfaceNode: ConfigNode): boolean => {
474
+ if (!interfaceNode?.children) return false;
446
475
  return interfaceNode.children.some((child) =>
447
- /^ip\s+verify\s+source/i.test(child.id)
476
+ child?.id && /^ip\s+verify\s+source/i.test(child.id)
448
477
  );
449
478
  };
450
479
 
@@ -454,8 +483,9 @@ export const hasIpSourceGuard = (interfaceNode: ConfigNode): boolean => {
454
483
  * @returns true if port security is configured
455
484
  */
456
485
  export const hasPortSecurity = (interfaceNode: ConfigNode): boolean => {
486
+ if (!interfaceNode?.children) return false;
457
487
  return interfaceNode.children.some((child) =>
458
- /^switchport\s+port-security/i.test(child.id)
488
+ child?.id && /^switchport\s+port-security/i.test(child.id)
459
489
  );
460
490
  };
461
491
 
@@ -471,17 +501,18 @@ export const hasPortSecurity = (interfaceNode: ConfigNode): boolean => {
471
501
  */
472
502
  export const getBgpNeighborsWithoutAuth = (routerBgpNode: ConfigNode, neighborIp?: string): string[] => {
473
503
  const neighborsWithoutAuth: string[] = [];
504
+ if (!routerBgpNode?.children) return neighborsWithoutAuth;
474
505
  const neighborConfigs = new Map<string, { hasPassword: boolean }>();
475
506
 
476
507
  for (const child of routerBgpNode.children) {
477
- const neighborMatch = child.id.match(/^neighbor\s+(\S+)/i);
508
+ const neighborMatch = child?.id?.match(/^neighbor\s+(\S+)/i);
478
509
  if (neighborMatch?.[1]) {
479
510
  const ip = neighborMatch[1];
480
511
  if (!neighborConfigs.has(ip)) {
481
512
  neighborConfigs.set(ip, { hasPassword: false });
482
513
  }
483
514
 
484
- if (/password/i.test(child.id)) {
515
+ if (child?.id && /password/i.test(child.id)) {
485
516
  const config = neighborConfigs.get(ip);
486
517
  if (config) {
487
518
  config.hasPassword = true;
@@ -508,11 +539,12 @@ export const getBgpNeighborsWithoutAuth = (routerBgpNode: ConfigNode, neighborIp
508
539
  */
509
540
  export const getBgpNeighborsWithoutTtlSecurity = (routerBgpNode: ConfigNode): string[] => {
510
541
  const neighborsWithoutTtl: string[] = [];
542
+ if (!routerBgpNode?.children) return neighborsWithoutTtl;
511
543
  const neighborConfigs = new Map<string, { hasTtl: boolean; isEbgp: boolean }>();
512
- const localAs = routerBgpNode.id.match(/router\s+bgp\s+(\d+)/i)?.[1];
544
+ const localAs = routerBgpNode?.id?.match(/router\s+bgp\s+(\d+)/i)?.[1];
513
545
 
514
546
  for (const child of routerBgpNode.children) {
515
- const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as\s+(\d+)/i);
547
+ const neighborMatch = child?.id?.match(/^neighbor\s+(\S+)\s+remote-as\s+(\d+)/i);
516
548
  if (neighborMatch?.[1] && neighborMatch?.[2]) {
517
549
  const ip = neighborMatch[1];
518
550
  const remoteAs = neighborMatch[2];
@@ -522,7 +554,7 @@ export const getBgpNeighborsWithoutTtlSecurity = (routerBgpNode: ConfigNode): st
522
554
  });
523
555
  }
524
556
 
525
- const ttlMatch = child.id.match(/^neighbor\s+(\S+)\s+ttl\s+maximum-hops/i);
557
+ const ttlMatch = child?.id?.match(/^neighbor\s+(\S+)\s+ttl\s+maximum-hops/i);
526
558
  if (ttlMatch?.[1]) {
527
559
  const config = neighborConfigs.get(ttlMatch[1]);
528
560
  if (config) {
@@ -547,15 +579,16 @@ export const getBgpNeighborsWithoutTtlSecurity = (routerBgpNode: ConfigNode): st
547
579
  */
548
580
  export const getBgpNeighborsWithoutMaxRoutes = (routerBgpNode: ConfigNode): string[] => {
549
581
  const neighborsWithoutMax: string[] = [];
582
+ if (!routerBgpNode?.children) return neighborsWithoutMax;
550
583
  const neighborConfigs = new Map<string, boolean>();
551
584
 
552
585
  for (const child of routerBgpNode.children) {
553
- const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as/i);
586
+ const neighborMatch = child?.id?.match(/^neighbor\s+(\S+)\s+remote-as/i);
554
587
  if (neighborMatch?.[1]) {
555
588
  neighborConfigs.set(neighborMatch[1], false);
556
589
  }
557
590
 
558
- const maxMatch = child.id.match(/^neighbor\s+(\S+)\s+maximum-routes/i);
591
+ const maxMatch = child?.id?.match(/^neighbor\s+(\S+)\s+maximum-routes/i);
559
592
  if (maxMatch?.[1]) {
560
593
  neighborConfigs.set(maxMatch[1], true);
561
594
  }
@@ -576,8 +609,9 @@ export const getBgpNeighborsWithoutMaxRoutes = (routerBgpNode: ConfigNode): stri
576
609
  * @returns true if graceful restart is configured
577
610
  */
578
611
  export const hasBgpGracefulRestart = (routerBgpNode: ConfigNode): boolean => {
612
+ if (!routerBgpNode?.children) return false;
579
613
  return routerBgpNode.children.some((child) =>
580
- /^bgp\s+graceful-restart/i.test(child.id)
614
+ child?.id && /^bgp\s+graceful-restart/i.test(child.id)
581
615
  );
582
616
  };
583
617
 
@@ -587,8 +621,9 @@ export const hasBgpGracefulRestart = (routerBgpNode: ConfigNode): boolean => {
587
621
  * @returns true if log-neighbor-changes is configured
588
622
  */
589
623
  export const hasBgpLogNeighborChanges = (routerBgpNode: ConfigNode): boolean => {
624
+ if (!routerBgpNode?.children) return false;
590
625
  return routerBgpNode.children.some((child) =>
591
- /^bgp\s+log-neighbor-changes/i.test(child.id)
626
+ child?.id && /^bgp\s+log-neighbor-changes/i.test(child.id)
592
627
  );
593
628
  };
594
629
 
@@ -602,8 +637,9 @@ export const hasBgpLogNeighborChanges = (routerBgpNode: ConfigNode): boolean =>
602
637
  * @returns true if RPKI cache is configured
603
638
  */
604
639
  export const hasRpkiConfiguration = (routerBgpNode: ConfigNode): boolean => {
640
+ if (!routerBgpNode?.children) return false;
605
641
  return routerBgpNode.children.some((child) =>
606
- /^rpki\s+cache/i.test(child.id)
642
+ child?.id && /^rpki\s+cache/i.test(child.id)
607
643
  );
608
644
  };
609
645
 
@@ -613,8 +649,9 @@ export const hasRpkiConfiguration = (routerBgpNode: ConfigNode): boolean => {
613
649
  * @returns true if origin validation is configured
614
650
  */
615
651
  export const hasRpkiOriginValidation = (routerBgpNode: ConfigNode): boolean => {
652
+ if (!routerBgpNode?.children) return false;
616
653
  return routerBgpNode.children.some((child) =>
617
- /^rpki\s+origin-validation/i.test(child.id)
654
+ child?.id && /^rpki\s+origin-validation/i.test(child.id)
618
655
  );
619
656
  };
620
657
 
@@ -628,11 +665,12 @@ export const hasRpkiOriginValidation = (routerBgpNode: ConfigNode): boolean => {
628
665
  * @returns Object with uRPF mode if configured
629
666
  */
630
667
  export const getUrpfMode = (interfaceNode: ConfigNode): { enabled: boolean; mode?: 'strict' | 'loose' } => {
668
+ if (!interfaceNode?.children) return { enabled: false };
631
669
  for (const child of interfaceNode.children) {
632
- if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+rx/i.test(child.id)) {
670
+ if (child?.id && /^ip\s+verify\s+unicast\s+source\s+reachable-via\s+rx/i.test(child.id)) {
633
671
  return { enabled: true, mode: 'strict' };
634
672
  }
635
- if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+any/i.test(child.id)) {
673
+ if (child?.id && /^ip\s+verify\s+unicast\s+source\s+reachable-via\s+any/i.test(child.id)) {
636
674
  return { enabled: true, mode: 'loose' };
637
675
  }
638
676
  }
@@ -674,14 +712,16 @@ export const getMlagReloadDelays = (mlagNode: ConfigNode): { mlag: boolean; nonM
674
712
  * @returns true if EVPN peer group has password
675
713
  */
676
714
  export const hasEvpnPeerAuth = (routerBgpNode: ConfigNode): boolean => {
715
+ if (!routerBgpNode?.children) return false;
677
716
  // Look for EVPN peer group with password
678
717
  let evpnPeerGroup: string | undefined;
679
718
 
680
719
  for (const child of routerBgpNode.children) {
681
720
  // Find EVPN address family activation
682
- if (/^address-family\s+evpn/i.test(child.id)) {
721
+ if (child?.id && /^address-family\s+evpn/i.test(child.id)) {
722
+ if (!child?.children) continue;
683
723
  for (const subchild of child.children) {
684
- const match = subchild.id.match(/neighbor\s+(\S+)\s+activate/i);
724
+ const match = subchild?.id?.match(/neighbor\s+(\S+)\s+activate/i);
685
725
  if (match?.[1]) {
686
726
  evpnPeerGroup = match[1];
687
727
  }
@@ -693,7 +733,7 @@ export const hasEvpnPeerAuth = (routerBgpNode: ConfigNode): boolean => {
693
733
 
694
734
  // Check if peer group has password
695
735
  return routerBgpNode.children.some((child) =>
696
- includesIgnoreCase(child.id, `neighbor ${evpnPeerGroup}`) &&
736
+ child?.id && includesIgnoreCase(child.id, `neighbor ${evpnPeerGroup}`) &&
697
737
  includesIgnoreCase(child.id, 'password')
698
738
  );
699
739
  };
@@ -709,11 +749,12 @@ export const hasEvpnPeerAuth = (routerBgpNode: ConfigNode): boolean => {
709
749
  * @returns true if logging meets minimum level requirement
710
750
  */
711
751
  export const hasLoggingLevel = (ast: ConfigNode[], minLevel: string): boolean => {
752
+ if (!Array.isArray(ast)) return false;
712
753
  const levels = ['emergencies', 'alerts', 'critical', 'errors', 'warnings', 'notifications', 'informational', 'debugging'];
713
754
  const minIndex = levels.indexOf(minLevel.toLowerCase());
714
755
 
715
756
  for (const node of ast) {
716
- const match = node.id.match(/^logging\s+(?:buffered|trap)\s+(\S+)/i);
757
+ const match = (node?.id ?? '').match(/^logging\s+(?:buffered|trap)\s+(\S+)/i);
717
758
  if (match?.[1]) {
718
759
  const configuredIndex = levels.indexOf(match[1].toLowerCase());
719
760
  if (configuredIndex >= minIndex) {
@@ -730,8 +771,9 @@ export const hasLoggingLevel = (ast: ConfigNode[], minLevel: string): boolean =>
730
771
  * @returns true if logging source-interface is configured
731
772
  */
732
773
  export const hasLoggingSourceInterface = (ast: ConfigNode[]): boolean => {
774
+ if (!Array.isArray(ast)) return false;
733
775
  return ast.some((node) =>
734
- /^logging\s+source-interface/i.test(node.id)
776
+ /^logging\s+source-interface/i.test(node?.id ?? '')
735
777
  );
736
778
  };
737
779
 
@@ -741,8 +783,9 @@ export const hasLoggingSourceInterface = (ast: ConfigNode[]): boolean => {
741
783
  * @returns true if event-monitor is configured
742
784
  */
743
785
  export const hasEventMonitor = (ast: ConfigNode[]): boolean => {
786
+ if (!Array.isArray(ast)) return false;
744
787
  return ast.some((node) =>
745
- /^event-monitor$/i.test(node.id)
788
+ /^event-monitor$/i.test(node?.id ?? '')
746
789
  );
747
790
  };
748
791
 
@@ -756,8 +799,9 @@ export const hasEventMonitor = (ast: ConfigNode[]): boolean => {
756
799
  * @returns true if VRRP authentication is configured
757
800
  */
758
801
  export const hasVrrpAuthentication = (interfaceNode: ConfigNode): boolean => {
802
+ if (!interfaceNode?.children) return false;
759
803
  return interfaceNode.children.some((child) =>
760
- /^vrrp\s+\d+\s+authentication/i.test(child.id)
804
+ child?.id && /^vrrp\s+\d+\s+authentication/i.test(child.id)
761
805
  );
762
806
  };
763
807
 
@@ -767,8 +811,9 @@ export const hasVrrpAuthentication = (interfaceNode: ConfigNode): boolean => {
767
811
  * @returns true if ip virtual-router mac-address is configured
768
812
  */
769
813
  export const hasVirtualRouterMac = (ast: ConfigNode[]): boolean => {
814
+ if (!Array.isArray(ast)) return false;
770
815
  return ast.some((node) =>
771
- /^ip\s+virtual-router\s+mac-address/i.test(node.id)
816
+ /^ip\s+virtual-router\s+mac-address/i.test(node?.id ?? '')
772
817
  );
773
818
  };
774
819
 
@@ -783,11 +828,12 @@ export const hasVirtualRouterMac = (ast: ConfigNode[]): boolean => {
783
828
  * @returns true if interface appears to be external facing
784
829
  */
785
830
  export const isExternalInterface = (interfaceNode: ConfigNode): boolean => {
831
+ if (!interfaceNode?.children) return false;
786
832
  const description = interfaceNode.children.find((child) =>
787
- startsWithIgnoreCase(child.id, 'description ')
833
+ child?.id && startsWithIgnoreCase(child.id, 'description ')
788
834
  );
789
835
 
790
- if (description) {
836
+ if (description?.id) {
791
837
  return (
792
838
  includesIgnoreCase(description.id, 'wan') ||
793
839
  includesIgnoreCase(description.id, 'internet') ||
@@ -807,8 +853,9 @@ export const isExternalInterface = (interfaceNode: ConfigNode): boolean => {
807
853
  * @returns true if interface is configured as access port
808
854
  */
809
855
  export const isAccessPort = (interfaceNode: ConfigNode): boolean => {
856
+ if (!interfaceNode?.children) return false;
810
857
  return interfaceNode.children.some((child) =>
811
- /^switchport\s+mode\s+access/i.test(child.id)
858
+ child?.id && /^switchport\s+mode\s+access/i.test(child.id)
812
859
  );
813
860
  };
814
861
 
@@ -818,8 +865,9 @@ export const isAccessPort = (interfaceNode: ConfigNode): boolean => {
818
865
  * @returns true if interface is configured as trunk port
819
866
  */
820
867
  export const isTrunkPort = (interfaceNode: ConfigNode): boolean => {
868
+ if (!interfaceNode?.children) return false;
821
869
  return interfaceNode.children.some((child) =>
822
- /^switchport\s+mode\s+trunk/i.test(child.id)
870
+ child?.id && /^switchport\s+mode\s+trunk/i.test(child.id)
823
871
  );
824
872
  };
825
873
 
@@ -829,8 +877,9 @@ export const isTrunkPort = (interfaceNode: ConfigNode): boolean => {
829
877
  * @returns true if mlag configuration block exists
830
878
  */
831
879
  export const hasMlagConfiguration = (ast: ConfigNode[]): boolean => {
880
+ if (!Array.isArray(ast)) return false;
832
881
  return ast.some((node) =>
833
- startsWithIgnoreCase(node.id, 'mlag configuration')
882
+ startsWithIgnoreCase(node?.id ?? '', 'mlag configuration')
834
883
  );
835
884
  };
836
885
 
@@ -842,8 +891,9 @@ export const hasMlagConfiguration = (ast: ConfigNode[]): boolean => {
842
891
  export const getMlagConfiguration = (
843
892
  ast: ConfigNode[]
844
893
  ): ConfigNode | undefined => {
894
+ if (!Array.isArray(ast)) return undefined;
845
895
  return ast.find((node) =>
846
- startsWithIgnoreCase(node.id, 'mlag configuration')
896
+ startsWithIgnoreCase(node?.id ?? '', 'mlag configuration')
847
897
  );
848
898
  };
849
899
 
@@ -869,8 +919,9 @@ export const checkMlagRequirements = (
869
919
  * @returns true if management api http-commands is configured
870
920
  */
871
921
  export const hasManagementApi = (ast: ConfigNode[]): boolean => {
922
+ if (!Array.isArray(ast)) return false;
872
923
  return ast.some((node) =>
873
- startsWithIgnoreCase(node.id, 'management api')
924
+ startsWithIgnoreCase(node?.id ?? '', 'management api')
874
925
  );
875
926
  };
876
927
 
@@ -880,8 +931,9 @@ export const hasManagementApi = (ast: ConfigNode[]): boolean => {
880
931
  * @returns Array of management API configuration nodes
881
932
  */
882
933
  export const getManagementApiNodes = (ast: ConfigNode[]): ConfigNode[] => {
934
+ if (!Array.isArray(ast)) return [];
883
935
  return ast.filter((node) =>
884
- startsWithIgnoreCase(node.id, 'management api')
936
+ startsWithIgnoreCase(node?.id ?? '', 'management api')
885
937
  );
886
938
  };
887
939
 
@@ -891,12 +943,13 @@ export const getManagementApiNodes = (ast: ConfigNode[]): ConfigNode[] => {
891
943
  * @returns true if HTTPS transport is configured
892
944
  */
893
945
  export const hasHttpsTransport = (apiNode: ConfigNode): boolean => {
946
+ if (!apiNode?.children) return false;
894
947
  // Check for "protocol https" or "no shutdown" with https
895
948
  const hasProtocolHttps = apiNode.children.some((child) =>
896
- includesIgnoreCase(child.id, 'protocol https')
949
+ child?.id && includesIgnoreCase(child.id, 'protocol https')
897
950
  );
898
951
  const hasTransportHttps = apiNode.children.some((child) =>
899
- includesIgnoreCase(child.id, 'transport https')
952
+ child?.id && includesIgnoreCase(child.id, 'transport https')
900
953
  );
901
954
  return hasProtocolHttps || hasTransportHttps;
902
955
  };
@@ -942,9 +995,10 @@ export const getVxlanVniMappings = (
942
995
  vxlanNode: ConfigNode
943
996
  ): { vni: string; vlan?: string }[] => {
944
997
  const mappings: { vni: string; vlan?: string }[] = [];
998
+ if (!vxlanNode?.children) return mappings;
945
999
 
946
1000
  for (const child of vxlanNode.children) {
947
- const vniMatch = child.id.match(/vxlan\s+vni\s+(\d+)\s+vlan\s+(\d+)/i);
1001
+ const vniMatch = child?.id?.match(/vxlan\s+vni\s+(\d+)\s+vlan\s+(\d+)/i);
948
1002
  if (vniMatch) {
949
1003
  const vni = vniMatch[1];
950
1004
  if (!vni) {
@@ -955,7 +1009,7 @@ export const getVxlanVniMappings = (
955
1009
  continue;
956
1010
  }
957
1011
 
958
- const simpleMatch = child.id.match(/vxlan\s+vni\s+(\d+)/i);
1012
+ const simpleMatch = child?.id?.match(/vxlan\s+vni\s+(\d+)/i);
959
1013
  if (simpleMatch) {
960
1014
  const vni = simpleMatch[1];
961
1015
  if (!vni) {
@@ -983,8 +1037,9 @@ export const hasVxlanSourceInterface = (vxlanNode: ConfigNode): boolean => {
983
1037
  * @returns The MLAG ID if configured, undefined otherwise
984
1038
  */
985
1039
  export const getMlagId = (interfaceNode: ConfigNode): string | undefined => {
1040
+ if (!interfaceNode?.children) return undefined;
986
1041
  const mlagCmd = interfaceNode.children.find((child) =>
987
- /^mlag\s+\d+/i.test(child.id)
1042
+ child?.id && /^mlag\s+\d+/i.test(child.id)
988
1043
  );
989
1044
  if (!mlagCmd) return undefined;
990
1045
 
@@ -1044,12 +1099,13 @@ export const isEthernetInterface = (node: ConfigNode): boolean => {
1044
1099
  * @returns true if daemon(s) are configured
1045
1100
  */
1046
1101
  export const hasDaemon = (ast: ConfigNode[], daemonName?: string): boolean => {
1102
+ if (!Array.isArray(ast)) return false;
1047
1103
  if (daemonName) {
1048
1104
  return ast.some((node) =>
1049
- equalsIgnoreCase(node.id, `daemon ${daemonName}`)
1105
+ equalsIgnoreCase(node?.id ?? '', `daemon ${daemonName}`)
1050
1106
  );
1051
1107
  }
1052
- return ast.some((node) => startsWithIgnoreCase(node.id, 'daemon '));
1108
+ return ast.some((node) => startsWithIgnoreCase(node?.id ?? '', 'daemon '));
1053
1109
  };
1054
1110
 
1055
1111
  /**
@@ -1058,7 +1114,8 @@ export const hasDaemon = (ast: ConfigNode[], daemonName?: string): boolean => {
1058
1114
  * @returns true if event-handler(s) are configured
1059
1115
  */
1060
1116
  export const hasEventHandler = (ast: ConfigNode[]): boolean => {
1061
- return ast.some((node) => startsWithIgnoreCase(node.id, 'event-handler '));
1117
+ if (!Array.isArray(ast)) return false;
1118
+ return ast.some((node) => startsWithIgnoreCase(node?.id ?? '', 'event-handler '));
1062
1119
  };
1063
1120
 
1064
1121
  /**
@@ -1067,8 +1124,9 @@ export const hasEventHandler = (ast: ConfigNode[]): boolean => {
1067
1124
  * @returns Array of VRF instance nodes
1068
1125
  */
1069
1126
  export const getVrfInstances = (ast: ConfigNode[]): ConfigNode[] => {
1127
+ if (!Array.isArray(ast)) return [];
1070
1128
  return ast.filter((node) =>
1071
- /^vrf\s+instance\s+\S+/i.test(node.id)
1129
+ /^vrf\s+instance\s+\S+/i.test(node?.id ?? '')
1072
1130
  );
1073
1131
  };
1074
1132
 
@@ -1078,8 +1136,9 @@ export const getVrfInstances = (ast: ConfigNode[]): ConfigNode[] => {
1078
1136
  * @returns The VRF name if configured, undefined otherwise
1079
1137
  */
1080
1138
  export const getInterfaceVrf = (interfaceNode: ConfigNode): string | undefined => {
1139
+ if (!interfaceNode?.children) return undefined;
1081
1140
  const vrfCmd = interfaceNode.children.find((child) =>
1082
- /^vrf\s+\S+/i.test(child.id)
1141
+ child?.id && /^vrf\s+\S+/i.test(child.id)
1083
1142
  );
1084
1143
  if (!vrfCmd) return undefined;
1085
1144
 
@@ -1093,8 +1152,9 @@ export const getInterfaceVrf = (interfaceNode: ConfigNode): string | undefined =
1093
1152
  * @returns true if EVPN address-family is configured
1094
1153
  */
1095
1154
  export const hasEvpnAddressFamily = (routerBgpNode: ConfigNode): boolean => {
1155
+ if (!routerBgpNode?.children) return false;
1096
1156
  return routerBgpNode.children.some((child) =>
1097
- /^address-family\s+evpn/i.test(child.id)
1157
+ child?.id && /^address-family\s+evpn/i.test(child.id)
1098
1158
  );
1099
1159
  };
1100
1160
 
@@ -1104,8 +1164,9 @@ export const hasEvpnAddressFamily = (routerBgpNode: ConfigNode): boolean => {
1104
1164
  * @returns true if ip virtual-router address is configured
1105
1165
  */
1106
1166
  export const hasVirtualRouterAddress = (interfaceNode: ConfigNode): boolean => {
1167
+ if (!interfaceNode?.children) return false;
1107
1168
  return interfaceNode.children.some((child) =>
1108
- /^ip\s+virtual-router\s+address/i.test(child.id)
1169
+ child?.id && /^ip\s+virtual-router\s+address/i.test(child.id)
1109
1170
  );
1110
1171
  };
1111
1172
 
@@ -1115,8 +1176,9 @@ export const hasVirtualRouterAddress = (interfaceNode: ConfigNode): boolean => {
1115
1176
  * @returns true if ip address is configured
1116
1177
  */
1117
1178
  export const hasIpAddress = (interfaceNode: ConfigNode): boolean => {
1179
+ if (!interfaceNode?.children) return false;
1118
1180
  return interfaceNode.children.some((child) =>
1119
- /^ip\s+address\s+\d+\.\d+\.\d+\.\d+/i.test(child.id)
1181
+ child?.id && /^ip\s+address\s+\d+\.\d+\.\d+\.\d+/i.test(child.id)
1120
1182
  );
1121
1183
  };
1122
1184
 
@@ -1126,11 +1188,12 @@ export const hasIpAddress = (interfaceNode: ConfigNode): boolean => {
1126
1188
  * @returns true if interface is shutdown
1127
1189
  */
1128
1190
  export const isShutdown = (interfaceNode: ConfigNode): boolean => {
1191
+ if (!interfaceNode?.children) return false;
1129
1192
  const hasShutdown = interfaceNode.children.some((child) =>
1130
- equalsIgnoreCase(child.id, 'shutdown')
1193
+ child?.id && equalsIgnoreCase(child.id, 'shutdown')
1131
1194
  );
1132
1195
  const hasNoShutdown = interfaceNode.children.some((child) =>
1133
- equalsIgnoreCase(child.id, 'no shutdown')
1196
+ child?.id && equalsIgnoreCase(child.id, 'no shutdown')
1134
1197
  );
1135
1198
  return hasShutdown && !hasNoShutdown;
1136
1199
  };
@@ -1141,8 +1204,9 @@ export const isShutdown = (interfaceNode: ConfigNode): boolean => {
1141
1204
  * @returns The description if configured, undefined otherwise
1142
1205
  */
1143
1206
  export const getInterfaceDescription = (interfaceNode: ConfigNode): string | undefined => {
1207
+ if (!interfaceNode?.children) return undefined;
1144
1208
  const descCmd = interfaceNode.children.find((child) =>
1145
- startsWithIgnoreCase(child.id, 'description ')
1209
+ child?.id && startsWithIgnoreCase(child.id, 'description ')
1146
1210
  );
1147
1211
  if (!descCmd) return undefined;
1148
1212
 
@@ -1155,8 +1219,9 @@ export const getInterfaceDescription = (interfaceNode: ConfigNode): string | und
1155
1219
  * @returns true if NTP server(s) are configured
1156
1220
  */
1157
1221
  export const hasNtpServer = (ast: ConfigNode[]): boolean => {
1222
+ if (!Array.isArray(ast)) return false;
1158
1223
  return ast.some((node) =>
1159
- /^ntp\s+server\s+/i.test(node.id)
1224
+ /^ntp\s+server\s+/i.test(node?.id ?? '')
1160
1225
  );
1161
1226
  };
1162
1227
 
@@ -1166,8 +1231,9 @@ export const hasNtpServer = (ast: ConfigNode[]): boolean => {
1166
1231
  * @returns true if logging host is configured
1167
1232
  */
1168
1233
  export const hasLoggingHost = (ast: ConfigNode[]): boolean => {
1234
+ if (!Array.isArray(ast)) return false;
1169
1235
  return ast.some((node) =>
1170
- /^logging\s+host\s+/i.test(node.id)
1236
+ /^logging\s+host\s+/i.test(node?.id ?? '')
1171
1237
  );
1172
1238
  };
1173
1239
 
@@ -1177,8 +1243,9 @@ export const hasLoggingHost = (ast: ConfigNode[]): boolean => {
1177
1243
  * @returns true if SNMP is configured
1178
1244
  */
1179
1245
  export const hasSnmpServer = (ast: ConfigNode[]): boolean => {
1246
+ if (!Array.isArray(ast)) return false;
1180
1247
  return ast.some((node) =>
1181
- /^snmp-server\s+/i.test(node.id)
1248
+ /^snmp-server\s+/i.test(node?.id ?? '')
1182
1249
  );
1183
1250
  };
1184
1251
 
@@ -1188,8 +1255,9 @@ export const hasSnmpServer = (ast: ConfigNode[]): boolean => {
1188
1255
  * @returns true if AAA is configured
1189
1256
  */
1190
1257
  export const hasAaa = (ast: ConfigNode[]): boolean => {
1258
+ if (!Array.isArray(ast)) return false;
1191
1259
  return ast.some((node) =>
1192
- /^aaa\s+/i.test(node.id)
1260
+ /^aaa\s+/i.test(node?.id ?? '')
1193
1261
  );
1194
1262
  };
1195
1263
 
@@ -1199,8 +1267,9 @@ export const hasAaa = (ast: ConfigNode[]): boolean => {
1199
1267
  * @returns true if spanning-tree is configured
1200
1268
  */
1201
1269
  export const hasSpanningTree = (ast: ConfigNode[]): boolean => {
1270
+ if (!Array.isArray(ast)) return false;
1202
1271
  return ast.some((node) =>
1203
- /^spanning-tree\s+/i.test(node.id)
1272
+ /^spanning-tree\s+/i.test(node?.id ?? '')
1204
1273
  );
1205
1274
  };
1206
1275
 
@@ -1210,11 +1279,12 @@ export const hasSpanningTree = (ast: ConfigNode[]): boolean => {
1210
1279
  * @returns The spanning-tree mode (mstp, rapid-pvst, none, etc.)
1211
1280
  */
1212
1281
  export const getSpanningTreeMode = (ast: ConfigNode[]): string | undefined => {
1282
+ if (!Array.isArray(ast)) return undefined;
1213
1283
  const stpNode = ast.find((node) =>
1214
- /^spanning-tree\s+mode\s+/i.test(node.id)
1284
+ /^spanning-tree\s+mode\s+/i.test(node?.id ?? '')
1215
1285
  );
1216
1286
  if (!stpNode) return undefined;
1217
1287
 
1218
- const match = stpNode.id.match(/spanning-tree\s+mode\s+(\S+)/i);
1288
+ const match = stpNode?.id?.match(/spanning-tree\s+mode\s+(\S+)/i);
1219
1289
  return match ? match[1] : undefined;
1220
1290
  };