@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.
@@ -14,8 +14,9 @@ export const findStanza = (
14
14
  node: ConfigNode,
15
15
  stanzaName: string
16
16
  ): ConfigNode | undefined => {
17
+ if (!node?.children) return undefined;
17
18
  return node.children.find(
18
- (child) => child.id.toLowerCase() === stanzaName.toLowerCase()
19
+ (child) => child?.id?.toLowerCase() === stanzaName.toLowerCase()
19
20
  );
20
21
  };
21
22
 
@@ -26,7 +27,8 @@ export const findStanza = (
26
27
  * @returns Array of matching child nodes
27
28
  */
28
29
  export const findStanzas = (node: ConfigNode, pattern: RegExp): ConfigNode[] => {
29
- return node.children.filter((child) => pattern.test(child.id.toLowerCase()));
30
+ if (!node?.children) return [];
31
+ return node.children.filter((child) => child?.id && pattern.test(child.id.toLowerCase()));
30
32
  };
31
33
 
32
34
  /**
@@ -102,8 +104,8 @@ export const isDenyRule = (ruleNode: ConfigNode): boolean => {
102
104
  */
103
105
  export const getSourceZones = (ruleNode: ConfigNode): string[] => {
104
106
  const from = findStanza(ruleNode, 'from');
105
- if (!from) return [];
106
- return from.children.map((child) => child.id.trim());
107
+ if (!from?.children) return [];
108
+ return from.children.map((child) => child?.id?.trim() ?? '').filter(Boolean);
107
109
  };
108
110
 
109
111
  /**
@@ -113,8 +115,8 @@ export const getSourceZones = (ruleNode: ConfigNode): string[] => {
113
115
  */
114
116
  export const getDestinationZones = (ruleNode: ConfigNode): string[] => {
115
117
  const to = findStanza(ruleNode, 'to');
116
- if (!to) return [];
117
- return to.children.map((child) => child.id.trim());
118
+ if (!to?.children) return [];
119
+ return to.children.map((child) => child?.id?.trim() ?? '').filter(Boolean);
118
120
  };
119
121
 
120
122
  /**
@@ -123,21 +125,22 @@ export const getDestinationZones = (ruleNode: ConfigNode): string[] => {
123
125
  * @returns Array of application names
124
126
  */
125
127
  export const getApplications = (ruleNode: ConfigNode): string[] => {
128
+ if (!ruleNode?.children) return [];
126
129
  // Check for "application" stanza with children
127
130
  const application = findStanza(ruleNode, 'application');
128
- if (application && application.children.length > 0) {
129
- return application.children.map((child) => child.id.trim());
131
+ if (application?.children && application.children.length > 0) {
132
+ return application.children.map((child) => child?.id?.trim() ?? '').filter(Boolean);
130
133
  }
131
134
 
132
135
  // Also check for inline "application <value>" commands
133
136
  const appCommands = ruleNode.children.filter((child) =>
134
- child.id.toLowerCase().startsWith('application ')
137
+ child?.id?.toLowerCase().startsWith('application ')
135
138
  );
136
139
  if (appCommands.length > 0) {
137
140
  return appCommands.map((cmd) => {
138
- const parts = cmd.id.split(/\s+/);
141
+ const parts = cmd?.id?.split(/\s+/) ?? [];
139
142
  return parts.slice(1).join(' ').replace(/;$/, '').trim();
140
- });
143
+ }).filter(Boolean);
141
144
  }
142
145
 
143
146
  return [];
@@ -159,22 +162,23 @@ export const hasAnyApplication = (ruleNode: ConfigNode): boolean => {
159
162
  * @returns true if source is "any"
160
163
  */
161
164
  export const hasAnySource = (ruleNode: ConfigNode): boolean => {
165
+ if (!ruleNode?.children) return false;
162
166
  // Check for "source" stanza with children
163
167
  const source = findStanza(ruleNode, 'source');
164
- if (source && source.children.length > 0) {
168
+ if (source?.children && source.children.length > 0) {
165
169
  return source.children.some((child) => {
166
- const id = child.id.toLowerCase().trim().replace(/;$/, '');
170
+ const id = child?.id?.toLowerCase().trim().replace(/;$/, '');
167
171
  return id === 'any' || id === '0.0.0.0/0';
168
172
  });
169
173
  }
170
174
 
171
175
  // Also check for inline "source any" or "source <value>" commands
172
176
  const sourceCommands = ruleNode.children.filter((child) =>
173
- child.id.toLowerCase().startsWith('source ')
177
+ child?.id?.toLowerCase().startsWith('source ')
174
178
  );
175
179
  if (sourceCommands.length > 0) {
176
180
  return sourceCommands.some((cmd) => {
177
- const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
181
+ const value = cmd?.id?.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
178
182
  return value === 'any' || value === '0.0.0.0/0';
179
183
  });
180
184
  }
@@ -188,22 +192,23 @@ export const hasAnySource = (ruleNode: ConfigNode): boolean => {
188
192
  * @returns true if destination is "any"
189
193
  */
190
194
  export const hasAnyDestination = (ruleNode: ConfigNode): boolean => {
195
+ if (!ruleNode?.children) return false;
191
196
  // Check for "destination" stanza with children
192
197
  const destination = findStanza(ruleNode, 'destination');
193
- if (destination && destination.children.length > 0) {
198
+ if (destination?.children && destination.children.length > 0) {
194
199
  return destination.children.some((child) => {
195
- const id = child.id.toLowerCase().trim().replace(/;$/, '');
200
+ const id = child?.id?.toLowerCase().trim().replace(/;$/, '');
196
201
  return id === 'any' || id === '0.0.0.0/0';
197
202
  });
198
203
  }
199
204
 
200
205
  // Also check for inline "destination any" or "destination <value>" commands
201
206
  const destCommands = ruleNode.children.filter((child) =>
202
- child.id.toLowerCase().startsWith('destination ')
207
+ child?.id?.toLowerCase().startsWith('destination ')
203
208
  );
204
209
  if (destCommands.length > 0) {
205
210
  return destCommands.some((cmd) => {
206
- const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
211
+ const value = cmd?.id?.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
207
212
  return value === 'any' || value === '0.0.0.0/0';
208
213
  });
209
214
  }
@@ -217,22 +222,23 @@ export const hasAnyDestination = (ruleNode: ConfigNode): boolean => {
217
222
  * @returns true if service is "any"
218
223
  */
219
224
  export const hasAnyService = (ruleNode: ConfigNode): boolean => {
225
+ if (!ruleNode?.children) return false;
220
226
  // Check for "service" stanza with children
221
227
  const service = findStanza(ruleNode, 'service');
222
- if (service && service.children.length > 0) {
228
+ if (service?.children && service.children.length > 0) {
223
229
  return service.children.some((child) => {
224
- const id = child.id.toLowerCase().trim().replace(/;$/, '');
230
+ const id = child?.id?.toLowerCase().trim().replace(/;$/, '');
225
231
  return id === 'any';
226
232
  });
227
233
  }
228
234
 
229
235
  // Also check for inline "service any" or "service <value>" commands
230
236
  const serviceCommands = ruleNode.children.filter((child) =>
231
- child.id.toLowerCase().startsWith('service ')
237
+ child?.id?.toLowerCase().startsWith('service ')
232
238
  );
233
239
  if (serviceCommands.length > 0) {
234
240
  return serviceCommands.some((cmd) => {
235
- const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
241
+ const value = cmd?.id?.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
236
242
  return value === 'any';
237
243
  });
238
244
  }
@@ -261,7 +267,7 @@ export const getSecurityRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
261
267
  if (!security) return [];
262
268
 
263
269
  const rules = findStanza(security, 'rules');
264
- if (!rules) return [];
270
+ if (!rules?.children) return [];
265
271
 
266
272
  return rules.children;
267
273
  };
@@ -276,7 +282,7 @@ export const getNatRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
276
282
  if (!nat) return [];
277
283
 
278
284
  const rules = findStanza(nat, 'rules');
279
- if (!rules) return [];
285
+ if (!rules?.children) return [];
280
286
 
281
287
  return rules.children;
282
288
  };
@@ -288,7 +294,7 @@ export const getNatRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
288
294
  */
289
295
  export const isHAConfigured = (deviceconfigNode: ConfigNode): boolean => {
290
296
  const ha = findStanza(deviceconfigNode, 'high-availability');
291
- if (!ha) return false;
297
+ if (!ha?.children) return false;
292
298
  return ha.children.length > 0;
293
299
  };
294
300
 
@@ -420,7 +426,7 @@ export const parsePanosAddress = (
420
426
  */
421
427
  export const hasWildfireProfile = (profilesNode: ConfigNode): boolean => {
422
428
  const wildfire = findStanza(profilesNode, 'wildfire-analysis');
423
- return wildfire !== undefined && wildfire.children.length > 0;
429
+ return wildfire !== undefined && (wildfire?.children?.length ?? 0) > 0;
424
430
  };
425
431
 
426
432
  /**
@@ -430,7 +436,7 @@ export const hasWildfireProfile = (profilesNode: ConfigNode): boolean => {
430
436
  */
431
437
  export const hasUrlFilteringProfile = (profilesNode: ConfigNode): boolean => {
432
438
  const urlFiltering = findStanza(profilesNode, 'url-filtering');
433
- return urlFiltering !== undefined && urlFiltering.children.length > 0;
439
+ return urlFiltering !== undefined && (urlFiltering?.children?.length ?? 0) > 0;
434
440
  };
435
441
 
436
442
  /**
@@ -440,7 +446,7 @@ export const hasUrlFilteringProfile = (profilesNode: ConfigNode): boolean => {
440
446
  */
441
447
  export const hasAntiVirusProfile = (profilesNode: ConfigNode): boolean => {
442
448
  const virus = findStanza(profilesNode, 'virus');
443
- return virus !== undefined && virus.children.length > 0;
449
+ return virus !== undefined && (virus?.children?.length ?? 0) > 0;
444
450
  };
445
451
 
446
452
  /**
@@ -450,7 +456,7 @@ export const hasAntiVirusProfile = (profilesNode: ConfigNode): boolean => {
450
456
  */
451
457
  export const hasAntiSpywareProfile = (profilesNode: ConfigNode): boolean => {
452
458
  const spyware = findStanza(profilesNode, 'spyware');
453
- return spyware !== undefined && spyware.children.length > 0;
459
+ return spyware !== undefined && (spyware?.children?.length ?? 0) > 0;
454
460
  };
455
461
 
456
462
  /**
@@ -460,7 +466,7 @@ export const hasAntiSpywareProfile = (profilesNode: ConfigNode): boolean => {
460
466
  */
461
467
  export const hasVulnerabilityProfile = (profilesNode: ConfigNode): boolean => {
462
468
  const vuln = findStanza(profilesNode, 'vulnerability');
463
- return vuln !== undefined && vuln.children.length > 0;
469
+ return vuln !== undefined && (vuln?.children?.length ?? 0) > 0;
464
470
  };
465
471
 
466
472
  /**
@@ -470,7 +476,7 @@ export const hasVulnerabilityProfile = (profilesNode: ConfigNode): boolean => {
470
476
  */
471
477
  export const hasFileBlockingProfile = (profilesNode: ConfigNode): boolean => {
472
478
  const fileBlocking = findStanza(profilesNode, 'file-blocking');
473
- return fileBlocking !== undefined && fileBlocking.children.length > 0;
479
+ return fileBlocking !== undefined && (fileBlocking?.children?.length ?? 0) > 0;
474
480
  };
475
481
 
476
482
  /**
@@ -811,7 +817,7 @@ export const getLogForwardingStatus = (
811
817
  logSettingsNode: ConfigNode
812
818
  ): { hasSyslog: boolean; hasPanorama: boolean; hasEmail: boolean } => {
813
819
  const profiles = findStanza(logSettingsNode, 'profiles');
814
- if (!profiles) {
820
+ if (!profiles?.children) {
815
821
  return { hasSyslog: false, hasPanorama: false, hasEmail: false };
816
822
  }
817
823
 
@@ -822,12 +828,12 @@ export const getLogForwardingStatus = (
822
828
  // Check each profile for forwarding destinations
823
829
  for (const profile of profiles.children) {
824
830
  const matchList = findStanza(profile, 'match-list');
825
- if (matchList) {
831
+ if (matchList?.children) {
826
832
  for (const match of matchList.children) {
827
833
  if (findStanza(match, 'send-syslog')) hasSyslog = true;
828
834
  if (hasChildCommand(match, 'send-to-panorama')) {
829
835
  const cmd = getChildCommand(match, 'send-to-panorama');
830
- if (cmd?.id.toLowerCase().includes('yes')) hasPanorama = true;
836
+ if (cmd?.id?.toLowerCase().includes('yes')) hasPanorama = true;
831
837
  }
832
838
  if (findStanza(match, 'send-email')) hasEmail = true;
833
839
  }
@@ -868,9 +874,9 @@ export const getUpdateScheduleStatus = (
868
874
  }
869
875
 
870
876
  return {
871
- hasThreats: threats !== undefined && threats.children.length > 0,
872
- hasAntivirus: antivirus !== undefined && antivirus.children.length > 0,
873
- hasWildfire: wildfire !== undefined && wildfire.children.length > 0,
877
+ hasThreats: threats !== undefined && (threats?.children?.length ?? 0) > 0,
878
+ hasAntivirus: antivirus !== undefined && (antivirus?.children?.length ?? 0) > 0,
879
+ hasWildfire: wildfire !== undefined && (wildfire?.children?.length ?? 0) > 0,
874
880
  wildfireRealtime,
875
881
  };
876
882
  };
@@ -885,7 +891,7 @@ export const getDecryptionRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
885
891
  if (!decryption) return [];
886
892
 
887
893
  const rules = findStanza(decryption, 'rules');
888
- if (!rules) return [];
894
+ if (!rules?.children) return [];
889
895
 
890
896
  return rules.children;
891
897
  };
@@ -916,15 +922,19 @@ export const getInterfaceManagementServices = (
916
922
  ping: boolean;
917
923
  snmp: boolean;
918
924
  } => {
925
+ if (!profileNode?.children) {
926
+ return { https: false, http: false, ssh: false, telnet: false, ping: false, snmp: false };
927
+ }
919
928
  // Use exact matching with word boundary to avoid "https" matching "http"
920
929
  const isServiceEnabled = (serviceName: string): boolean => {
921
930
  // Look for exact service name followed by space/end (e.g., "http yes" not "https yes")
922
931
  const cmd = profileNode.children.find((child) => {
923
- const lowerId = child.id.toLowerCase();
932
+ const lowerId = child?.id?.toLowerCase();
933
+ if (!lowerId) return false;
924
934
  // Match exact service name: "http yes", "http no", etc.
925
935
  return lowerId === serviceName || lowerId.startsWith(serviceName + ' ');
926
936
  });
927
- if (!cmd) return false;
937
+ if (!cmd?.id) return false;
928
938
  return cmd.id.toLowerCase().includes('yes');
929
939
  };
930
940
 
@@ -11,7 +11,8 @@ export { hasChildCommand, getChildCommand, getChildCommands, parseIp } from '../
11
11
  * Check if a VyOS interface is disabled (has "disable" statement)
12
12
  */
13
13
  export const isDisabled = (node: ConfigNode): boolean => {
14
- return node.children.some((child) => child.id.toLowerCase().trim() === 'disable');
14
+ if (!node?.children) return false;
15
+ return node.children.some((child) => child?.id?.toLowerCase().trim() === 'disable');
15
16
  };
16
17
 
17
18
  /**
@@ -114,8 +115,9 @@ export const findStanza = (
114
115
  node: ConfigNode,
115
116
  stanzaName: string
116
117
  ): ConfigNode | undefined => {
118
+ if (!node?.children) return undefined;
117
119
  return node.children.find(
118
- (child) => child.id.toLowerCase() === stanzaName.toLowerCase()
120
+ (child) => child?.id?.toLowerCase() === stanzaName.toLowerCase()
119
121
  );
120
122
  };
121
123
 
@@ -129,8 +131,9 @@ export const findStanzaByPrefix = (
129
131
  node: ConfigNode,
130
132
  prefix: string
131
133
  ): ConfigNode | undefined => {
134
+ if (!node?.children) return undefined;
132
135
  return node.children.find((child) =>
133
- child.id.toLowerCase().startsWith(prefix.toLowerCase())
136
+ child?.id?.toLowerCase().startsWith(prefix.toLowerCase())
134
137
  );
135
138
  };
136
139
 
@@ -141,7 +144,8 @@ export const findStanzaByPrefix = (
141
144
  * @returns Array of matching child nodes
142
145
  */
143
146
  export const findStanzas = (node: ConfigNode, pattern: RegExp): ConfigNode[] => {
144
- return node.children.filter((child) => pattern.test(child.id.toLowerCase()));
147
+ if (!node?.children) return [];
148
+ return node.children.filter((child) => child?.id && pattern.test(child.id.toLowerCase()));
145
149
  };
146
150
 
147
151
  /**
@@ -151,8 +155,9 @@ export const findStanzas = (node: ConfigNode, pattern: RegExp): ConfigNode[] =>
151
155
  * @returns Array of matching child nodes
152
156
  */
153
157
  export const findStanzasByPrefix = (node: ConfigNode, prefix: string): ConfigNode[] => {
158
+ if (!node?.children) return [];
154
159
  return node.children.filter((child) =>
155
- child.id.toLowerCase().startsWith(prefix.toLowerCase())
160
+ child?.id?.toLowerCase().startsWith(prefix.toLowerCase())
156
161
  );
157
162
  };
158
163
 
@@ -162,8 +167,9 @@ export const findStanzasByPrefix = (node: ConfigNode, prefix: string): ConfigNod
162
167
  * @returns Array of ethernet interface nodes
163
168
  */
164
169
  export const getEthernetInterfaces = (interfacesNode: ConfigNode): ConfigNode[] => {
170
+ if (!interfacesNode?.children) return [];
165
171
  return interfacesNode.children.filter((child) =>
166
- child.id.toLowerCase().startsWith('ethernet eth')
172
+ child?.id?.toLowerCase().startsWith('ethernet eth')
167
173
  );
168
174
  };
169
175
 
@@ -173,8 +179,9 @@ export const getEthernetInterfaces = (interfacesNode: ConfigNode): ConfigNode[]
173
179
  * @returns Array of vif nodes
174
180
  */
175
181
  export const getVifInterfaces = (interfaceNode: ConfigNode): ConfigNode[] => {
182
+ if (!interfaceNode?.children) return [];
176
183
  return interfaceNode.children.filter((child) =>
177
- child.id.toLowerCase().startsWith('vif')
184
+ child?.id?.toLowerCase().startsWith('vif')
178
185
  );
179
186
  };
180
187
 
@@ -186,8 +193,10 @@ export const getVifInterfaces = (interfaceNode: ConfigNode): ConfigNode[] => {
186
193
  export const getFirewallDefaultAction = (
187
194
  rulesetNode: ConfigNode
188
195
  ): 'drop' | 'accept' | 'reject' | undefined => {
196
+ if (!rulesetNode?.children) return undefined;
189
197
  for (const child of rulesetNode.children) {
190
- const id = child.id.toLowerCase().trim();
198
+ const id = child?.id?.toLowerCase().trim();
199
+ if (!id) continue;
191
200
  if (id.startsWith('default-action')) {
192
201
  if (id.includes('drop')) return 'drop';
193
202
  if (id.includes('accept')) return 'accept';
@@ -203,8 +212,9 @@ export const getFirewallDefaultAction = (
203
212
  * @returns Array of rule nodes
204
213
  */
205
214
  export const getFirewallRules = (rulesetNode: ConfigNode): ConfigNode[] => {
215
+ if (!rulesetNode?.children) return [];
206
216
  return rulesetNode.children.filter((child) =>
207
- child.id.toLowerCase().startsWith('rule')
217
+ child?.id?.toLowerCase().startsWith('rule')
208
218
  );
209
219
  };
210
220
 
@@ -216,8 +226,10 @@ export const getFirewallRules = (rulesetNode: ConfigNode): ConfigNode[] => {
216
226
  export const getFirewallRuleAction = (
217
227
  ruleNode: ConfigNode
218
228
  ): 'drop' | 'accept' | 'reject' | undefined => {
229
+ if (!ruleNode?.children) return undefined;
219
230
  for (const child of ruleNode.children) {
220
- const id = child.id.toLowerCase().trim();
231
+ const id = child?.id?.toLowerCase().trim();
232
+ if (!id) continue;
221
233
  if (id.startsWith('action')) {
222
234
  if (id.includes('drop')) return 'drop';
223
235
  if (id.includes('accept')) return 'accept';
@@ -233,8 +245,9 @@ export const getFirewallRuleAction = (
233
245
  * @returns true if translation is configured
234
246
  */
235
247
  export const hasNatTranslation = (ruleNode: ConfigNode): boolean => {
248
+ if (!ruleNode?.children) return false;
236
249
  return ruleNode.children.some((child) =>
237
- child.id.toLowerCase().startsWith('translation')
250
+ child?.id?.toLowerCase().startsWith('translation')
238
251
  );
239
252
  };
240
253
 
@@ -244,8 +257,9 @@ export const hasNatTranslation = (ruleNode: ConfigNode): boolean => {
244
257
  * @returns true if SSH is configured
245
258
  */
246
259
  export const hasSshService = (serviceNode: ConfigNode): boolean => {
260
+ if (!serviceNode?.children) return false;
247
261
  return serviceNode.children.some((child) =>
248
- child.id.toLowerCase().startsWith('ssh')
262
+ child?.id?.toLowerCase().startsWith('ssh')
249
263
  );
250
264
  };
251
265
 
@@ -255,8 +269,9 @@ export const hasSshService = (serviceNode: ConfigNode): boolean => {
255
269
  * @returns The SSH configuration node, or undefined
256
270
  */
257
271
  export const getSshConfig = (serviceNode: ConfigNode): ConfigNode | undefined => {
272
+ if (!serviceNode?.children) return undefined;
258
273
  return serviceNode.children.find((child) =>
259
- child.id.toLowerCase().startsWith('ssh')
274
+ child?.id?.toLowerCase().startsWith('ssh')
260
275
  );
261
276
  };
262
277
 
@@ -266,8 +281,9 @@ export const getSshConfig = (serviceNode: ConfigNode): ConfigNode | undefined =>
266
281
  * @returns true if DHCP server is configured
267
282
  */
268
283
  export const hasDhcpServer = (serviceNode: ConfigNode): boolean => {
284
+ if (!serviceNode?.children) return false;
269
285
  return serviceNode.children.some((child) =>
270
- child.id.toLowerCase().startsWith('dhcp-server')
286
+ child?.id?.toLowerCase().startsWith('dhcp-server')
271
287
  );
272
288
  };
273
289
 
@@ -277,8 +293,9 @@ export const hasDhcpServer = (serviceNode: ConfigNode): boolean => {
277
293
  * @returns The DNS configuration node, or undefined
278
294
  */
279
295
  export const getDnsConfig = (serviceNode: ConfigNode): ConfigNode | undefined => {
296
+ if (!serviceNode?.children) return undefined;
280
297
  return serviceNode.children.find((child) =>
281
- child.id.toLowerCase().startsWith('dns')
298
+ child?.id?.toLowerCase().startsWith('dns')
282
299
  );
283
300
  };
284
301
 
@@ -288,8 +305,9 @@ export const getDnsConfig = (serviceNode: ConfigNode): ConfigNode | undefined =>
288
305
  * @returns true if NTP is configured
289
306
  */
290
307
  export const hasNtpConfig = (systemNode: ConfigNode): boolean => {
308
+ if (!systemNode?.children) return false;
291
309
  return systemNode.children.some((child) =>
292
- child.id.toLowerCase().startsWith('ntp')
310
+ child?.id?.toLowerCase().startsWith('ntp')
293
311
  );
294
312
  };
295
313
 
@@ -299,8 +317,9 @@ export const hasNtpConfig = (systemNode: ConfigNode): boolean => {
299
317
  * @returns true if syslog is configured
300
318
  */
301
319
  export const hasSyslogConfig = (systemNode: ConfigNode): boolean => {
320
+ if (!systemNode?.children) return false;
302
321
  return systemNode.children.some((child) =>
303
- child.id.toLowerCase().startsWith('syslog')
322
+ child?.id?.toLowerCase().startsWith('syslog')
304
323
  );
305
324
  };
306
325
 
@@ -310,8 +329,9 @@ export const hasSyslogConfig = (systemNode: ConfigNode): boolean => {
310
329
  * @returns The login configuration node, or undefined
311
330
  */
312
331
  export const getLoginConfig = (systemNode: ConfigNode): ConfigNode | undefined => {
332
+ if (!systemNode?.children) return undefined;
313
333
  return systemNode.children.find((child) =>
314
- child.id.toLowerCase().startsWith('login')
334
+ child?.id?.toLowerCase().startsWith('login')
315
335
  );
316
336
  };
317
337
 
@@ -321,8 +341,9 @@ export const getLoginConfig = (systemNode: ConfigNode): ConfigNode | undefined =
321
341
  * @returns Array of user nodes
322
342
  */
323
343
  export const getUserConfigs = (loginNode: ConfigNode): ConfigNode[] => {
344
+ if (!loginNode?.children) return [];
324
345
  return loginNode.children.filter((child) =>
325
- child.id.toLowerCase().startsWith('user')
346
+ child?.id?.toLowerCase().startsWith('user')
326
347
  );
327
348
  };
328
349
 
@@ -334,22 +355,24 @@ export const getUserConfigs = (loginNode: ConfigNode): ConfigNode[] => {
334
355
  */
335
356
  export const getSwitchPortMembers = (interfacesNode: ConfigNode): Set<string> => {
336
357
  const members = new Set<string>();
358
+ if (!interfacesNode?.children) return members;
337
359
 
338
360
  // Find all switch interfaces (switch switchX)
339
361
  const switches = interfacesNode.children.filter((child) =>
340
- child.id.toLowerCase().startsWith('switch ')
362
+ child?.id?.toLowerCase().startsWith('switch ')
341
363
  );
342
364
 
343
365
  for (const switchNode of switches) {
366
+ if (!switchNode?.children) continue;
344
367
  // Find switch-port section
345
368
  const switchPort = switchNode.children.find((child) =>
346
- child.id.toLowerCase() === 'switch-port'
369
+ child?.id?.toLowerCase() === 'switch-port'
347
370
  );
348
371
 
349
- if (switchPort) {
372
+ if (switchPort?.children) {
350
373
  // Find all interface members (interface ethX)
351
374
  for (const child of switchPort.children) {
352
- const match = child.id.toLowerCase().match(/^interface\s+(eth\d+)$/);
375
+ const match = child?.id?.toLowerCase().match(/^interface\s+(eth\d+)$/);
353
376
  if (match?.[1]) {
354
377
  members.add(match[1]);
355
378
  }
@@ -368,22 +391,24 @@ export const getSwitchPortMembers = (interfacesNode: ConfigNode): Set<string> =>
368
391
  */
369
392
  export const getBridgeMembers = (interfacesNode: ConfigNode): Set<string> => {
370
393
  const members = new Set<string>();
394
+ if (!interfacesNode?.children) return members;
371
395
 
372
396
  // Find all bridge interfaces (bridge brX)
373
397
  const bridges = interfacesNode.children.filter((child) =>
374
- child.id.toLowerCase().startsWith('bridge ')
398
+ child?.id?.toLowerCase().startsWith('bridge ')
375
399
  );
376
400
 
377
401
  for (const bridgeNode of bridges) {
402
+ if (!bridgeNode?.children) continue;
378
403
  // Find member section
379
404
  const memberSection = bridgeNode.children.find((child) =>
380
- child.id.toLowerCase() === 'member'
405
+ child?.id?.toLowerCase() === 'member'
381
406
  );
382
407
 
383
- if (memberSection) {
408
+ if (memberSection?.children) {
384
409
  // Find all interface members within the member section
385
410
  for (const child of memberSection.children) {
386
- const match = child.id.toLowerCase().match(/^interface\s+(\S+)$/);
411
+ const match = child?.id?.toLowerCase().match(/^interface\s+(\S+)$/);
387
412
  if (match?.[1]) {
388
413
  members.add(match[1]);
389
414
  }
@@ -402,22 +427,24 @@ export const getBridgeMembers = (interfacesNode: ConfigNode): Set<string> => {
402
427
  */
403
428
  export const getBondingMembers = (interfacesNode: ConfigNode): Set<string> => {
404
429
  const members = new Set<string>();
430
+ if (!interfacesNode?.children) return members;
405
431
 
406
432
  // Find all bonding interfaces (bonding bondX)
407
433
  const bonds = interfacesNode.children.filter((child) =>
408
- child.id.toLowerCase().startsWith('bonding ')
434
+ child?.id?.toLowerCase().startsWith('bonding ')
409
435
  );
410
436
 
411
437
  for (const bondNode of bonds) {
438
+ if (!bondNode?.children) continue;
412
439
  // Find member section
413
440
  const memberSection = bondNode.children.find((child) =>
414
- child.id.toLowerCase() === 'member'
441
+ child?.id?.toLowerCase() === 'member'
415
442
  );
416
443
 
417
- if (memberSection) {
444
+ if (memberSection?.children) {
418
445
  // Find all interface members within the member section
419
446
  for (const child of memberSection.children) {
420
- const match = child.id.toLowerCase().match(/^interface\s+(\S+)$/);
447
+ const match = child?.id?.toLowerCase().match(/^interface\s+(\S+)$/);
421
448
  if (match?.[1]) {
422
449
  members.add(match[1]);
423
450
  }
package/src/index.ts CHANGED
@@ -18,6 +18,9 @@ export * from './pack-loader';
18
18
  // Pack Provider abstraction for cloud licensing extension
19
19
  export * from './pack-provider';
20
20
 
21
+ // GRX2 Extended Pack Loader - for CLI and VS Code extension
22
+ export * from './grx2-loader';
23
+
21
24
  // SEC-001: Declarative rules and sandboxed execution
22
25
  export * from './types/DeclarativeRule';
23
26
  export * from './engine/SandboxedExecutor';
@@ -35,12 +35,31 @@ import { getAllVendorModules } from '../helpers';
35
35
  * All rule helpers merged into a single object for injection.
36
36
  * This allows compiled check functions to access helpers by name.
37
37
  * Dynamically built from the helpers module.
38
+ *
39
+ * IMPORTANT: Vendor modules may have colliding helper names (e.g., both Cisco
40
+ * and Cumulus export `hasBgpNeighborPassword` with different signatures).
41
+ * To handle this:
42
+ * 1. Vendor namespaces are added (e.g., `cisco.hasBgpNeighborPassword`)
43
+ * 2. For flat/short names, FIRST vendor wins (no overwrites)
44
+ *
45
+ * Rules should use namespaced helpers for vendor-specific functions.
38
46
  */
39
47
  function buildAllHelpers(): Record<string, unknown> {
40
48
  const result: Record<string, unknown> = { ...helpers };
41
49
  const vendorModules = getAllVendorModules();
42
- for (const [_name, module] of Object.entries(vendorModules)) {
43
- Object.assign(result, module);
50
+
51
+ for (const [name, module] of Object.entries(vendorModules)) {
52
+ // Add the entire vendor module under its namespace
53
+ // e.g., result.cisco = { hasBgpNeighborPassword, getBgpNeighbors, ... }
54
+ result[name] = module;
55
+
56
+ // Add flat/short names ONLY if not already present (first vendor wins)
57
+ // This prevents Cumulus from overwriting Cisco's hasBgpNeighborPassword
58
+ for (const [key, value] of Object.entries(module as Record<string, unknown>)) {
59
+ if (!(key in result)) {
60
+ result[key] = value;
61
+ }
62
+ }
44
63
  }
45
64
  return result;
46
65
  }
@@ -215,9 +234,13 @@ export async function loadEncryptedPack(
215
234
  /**
216
235
  * Generate helper names list for destructuring.
217
236
  * Cached to avoid recomputing on every function compilation.
237
+ *
238
+ * Includes:
239
+ * - All function helpers (flat names)
240
+ * - All vendor namespace objects (e.g., cisco, cumulus)
218
241
  */
219
242
  const helperNames = Object.keys(allHelpers).filter(
220
- key => typeof allHelpers[key] === 'function'
243
+ key => typeof allHelpers[key] === 'function' || typeof allHelpers[key] === 'object'
221
244
  );
222
245
  const helperDestructure = helperNames.join(', ');
223
246
 
@@ -229,8 +252,10 @@ const helperDestructure = helperNames.join(', ');
229
252
  *
230
253
  * The function is wrapped to inject all rule helpers into scope, allowing
231
254
  * serialized check functions to use helpers like hasChildCommand, findStanza, etc.
255
+ *
256
+ * @public Exported for use by GRX2ExtendedLoader
232
257
  */
233
- function compileNativeCheckFunction(
258
+ export function compileNativeCheckFunction(
234
259
  source: string
235
260
  ): (node: ConfigNode, ctx: Context) => ReturnType<IRule['check']> {
236
261
  // The source is trusted (from authenticated encrypted pack)