@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.
- package/README.md +66 -0
- package/package.json +7 -2
- package/src/constants.ts +4 -1
- package/src/engine/RuleExecutor.ts +7 -1
- package/src/grx2-loader/GRX2ExtendedLoader.ts +645 -0
- package/src/grx2-loader/MachineId.ts +51 -0
- package/src/grx2-loader/index.ts +47 -0
- package/src/grx2-loader/types.ts +277 -0
- package/src/helpers/arista/helpers.ts +165 -95
- package/src/helpers/aruba/helpers.ts +11 -5
- package/src/helpers/cisco/helpers.ts +16 -8
- package/src/helpers/common/helpers.ts +19 -13
- package/src/helpers/common/validation.ts +6 -6
- package/src/helpers/cumulus/helpers.ts +11 -7
- package/src/helpers/extreme/helpers.ts +8 -5
- package/src/helpers/fortinet/helpers.ts +16 -6
- package/src/helpers/huawei/helpers.ts +112 -61
- package/src/helpers/juniper/helpers.ts +36 -20
- package/src/helpers/mikrotik/helpers.ts +10 -3
- package/src/helpers/nokia/helpers.ts +71 -42
- package/src/helpers/paloalto/helpers.ts +51 -41
- package/src/helpers/vyos/helpers.ts +58 -31
- package/src/index.ts +3 -0
- package/src/pack-loader/PackLoader.ts +29 -4
- package/src/parser/SchemaAwareParser.ts +84 -0
- package/src/parser/vendors/cisco-ios.ts +19 -5
- package/src/parser/vendors/cisco-nxos.ts +10 -2
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
137
|
+
child?.id?.toLowerCase().startsWith('application ')
|
|
135
138
|
);
|
|
136
139
|
if (appCommands.length > 0) {
|
|
137
140
|
return appCommands.map((cmd) => {
|
|
138
|
-
const parts = cmd
|
|
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
|
|
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
|
|
177
|
+
child?.id?.toLowerCase().startsWith('source ')
|
|
174
178
|
);
|
|
175
179
|
if (sourceCommands.length > 0) {
|
|
176
180
|
return sourceCommands.some((cmd) => {
|
|
177
|
-
const value = cmd
|
|
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
|
|
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
|
|
207
|
+
child?.id?.toLowerCase().startsWith('destination ')
|
|
203
208
|
);
|
|
204
209
|
if (destCommands.length > 0) {
|
|
205
210
|
return destCommands.some((cmd) => {
|
|
206
|
-
const value = cmd
|
|
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
|
|
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
|
|
237
|
+
child?.id?.toLowerCase().startsWith('service ')
|
|
232
238
|
);
|
|
233
239
|
if (serviceCommands.length > 0) {
|
|
234
240
|
return serviceCommands.some((cmd) => {
|
|
235
|
-
const value = cmd
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
872
|
-
hasAntivirus: antivirus !== undefined && antivirus
|
|
873
|
-
hasWildfire: wildfire !== undefined && wildfire
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
43
|
-
|
|
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)
|