@sentriflow/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +86 -0
  3. package/package.json +60 -0
  4. package/src/constants.ts +77 -0
  5. package/src/engine/RuleExecutor.ts +256 -0
  6. package/src/engine/Runner.ts +312 -0
  7. package/src/engine/SandboxedExecutor.ts +208 -0
  8. package/src/errors.ts +88 -0
  9. package/src/helpers/arista/helpers.ts +1220 -0
  10. package/src/helpers/arista/index.ts +12 -0
  11. package/src/helpers/aruba/helpers.ts +637 -0
  12. package/src/helpers/aruba/index.ts +13 -0
  13. package/src/helpers/cisco/helpers.ts +534 -0
  14. package/src/helpers/cisco/index.ts +11 -0
  15. package/src/helpers/common/helpers.ts +265 -0
  16. package/src/helpers/common/index.ts +5 -0
  17. package/src/helpers/common/validation.ts +280 -0
  18. package/src/helpers/cumulus/helpers.ts +676 -0
  19. package/src/helpers/cumulus/index.ts +12 -0
  20. package/src/helpers/extreme/helpers.ts +422 -0
  21. package/src/helpers/extreme/index.ts +12 -0
  22. package/src/helpers/fortinet/helpers.ts +892 -0
  23. package/src/helpers/fortinet/index.ts +12 -0
  24. package/src/helpers/huawei/helpers.ts +790 -0
  25. package/src/helpers/huawei/index.ts +11 -0
  26. package/src/helpers/index.ts +53 -0
  27. package/src/helpers/juniper/helpers.ts +756 -0
  28. package/src/helpers/juniper/index.ts +12 -0
  29. package/src/helpers/mikrotik/helpers.ts +722 -0
  30. package/src/helpers/mikrotik/index.ts +12 -0
  31. package/src/helpers/nokia/helpers.ts +856 -0
  32. package/src/helpers/nokia/index.ts +11 -0
  33. package/src/helpers/paloalto/helpers.ts +939 -0
  34. package/src/helpers/paloalto/index.ts +12 -0
  35. package/src/helpers/vyos/helpers.ts +429 -0
  36. package/src/helpers/vyos/index.ts +12 -0
  37. package/src/index.ts +30 -0
  38. package/src/json-rules/ExpressionEvaluator.ts +292 -0
  39. package/src/json-rules/HelperRegistry.ts +177 -0
  40. package/src/json-rules/JsonRuleCompiler.ts +339 -0
  41. package/src/json-rules/JsonRuleValidator.ts +371 -0
  42. package/src/json-rules/index.ts +97 -0
  43. package/src/json-rules/schema.json +350 -0
  44. package/src/json-rules/types.ts +303 -0
  45. package/src/pack-loader/PackLoader.ts +332 -0
  46. package/src/pack-loader/index.ts +17 -0
  47. package/src/pack-loader/types.ts +135 -0
  48. package/src/parser/IncrementalParser.ts +527 -0
  49. package/src/parser/Sanitizer.ts +104 -0
  50. package/src/parser/SchemaAwareParser.ts +504 -0
  51. package/src/parser/VendorSchema.ts +72 -0
  52. package/src/parser/vendors/arista-eos.ts +206 -0
  53. package/src/parser/vendors/aruba-aoscx.ts +123 -0
  54. package/src/parser/vendors/aruba-aosswitch.ts +113 -0
  55. package/src/parser/vendors/aruba-wlc.ts +173 -0
  56. package/src/parser/vendors/cisco-ios.ts +110 -0
  57. package/src/parser/vendors/cisco-nxos.ts +107 -0
  58. package/src/parser/vendors/cumulus-linux.ts +161 -0
  59. package/src/parser/vendors/extreme-exos.ts +154 -0
  60. package/src/parser/vendors/extreme-voss.ts +167 -0
  61. package/src/parser/vendors/fortinet-fortigate.ts +217 -0
  62. package/src/parser/vendors/huawei-vrp.ts +192 -0
  63. package/src/parser/vendors/index.ts +1521 -0
  64. package/src/parser/vendors/juniper-junos.ts +230 -0
  65. package/src/parser/vendors/mikrotik-routeros.ts +274 -0
  66. package/src/parser/vendors/nokia-sros.ts +251 -0
  67. package/src/parser/vendors/paloalto-panos.ts +264 -0
  68. package/src/parser/vendors/vyos-vyos.ts +454 -0
  69. package/src/types/ConfigNode.ts +72 -0
  70. package/src/types/DeclarativeRule.ts +158 -0
  71. package/src/types/IRule.ts +270 -0
@@ -0,0 +1,939 @@
1
+ // packages/rule-helpers/src/paloalto/helpers.ts
2
+ // Palo Alto PAN-OS-specific helper functions
3
+
4
+ import type { ConfigNode } from '../../types/ConfigNode';
5
+ import { hasChildCommand, getChildCommand, parseIp } from '../common/helpers';
6
+
7
+ /**
8
+ * Find a stanza by name within a node's children (case-insensitive)
9
+ * @param node The parent ConfigNode
10
+ * @param stanzaName The stanza name to find
11
+ * @returns The matching child node, or undefined
12
+ */
13
+ export const findStanza = (
14
+ node: ConfigNode,
15
+ stanzaName: string
16
+ ): ConfigNode | undefined => {
17
+ return node.children.find(
18
+ (child) => child.id.toLowerCase() === stanzaName.toLowerCase()
19
+ );
20
+ };
21
+
22
+ /**
23
+ * Find all stanzas matching a pattern within a node's children
24
+ * @param node The parent ConfigNode
25
+ * @param pattern The regex pattern to match
26
+ * @returns Array of matching child nodes
27
+ */
28
+ export const findStanzas = (node: ConfigNode, pattern: RegExp): ConfigNode[] => {
29
+ return node.children.filter((child) => pattern.test(child.id.toLowerCase()));
30
+ };
31
+
32
+ /**
33
+ * Check if a security rule has logging enabled
34
+ * @param ruleNode The security rule ConfigNode
35
+ * @returns Object indicating log-start and log-end status
36
+ */
37
+ export const hasLogging = (
38
+ ruleNode: ConfigNode
39
+ ): { logStart: boolean; logEnd: boolean } => {
40
+ const logStart = hasChildCommand(ruleNode, 'log-start');
41
+ const logEnd = hasChildCommand(ruleNode, 'log-end');
42
+ return { logStart, logEnd };
43
+ };
44
+
45
+ /**
46
+ * Check if a security rule has a security profile attached
47
+ * @param ruleNode The security rule ConfigNode
48
+ * @returns true if any security profile is attached
49
+ */
50
+ export const hasSecurityProfile = (ruleNode: ConfigNode): boolean => {
51
+ // Check for profile-setting stanza
52
+ const profileSetting = findStanza(ruleNode, 'profile-setting');
53
+ if (profileSetting && profileSetting.children.length > 0) {
54
+ return true;
55
+ }
56
+
57
+ // Check for individual profile commands
58
+ const profileKeywords = [
59
+ 'virus',
60
+ 'spyware',
61
+ 'vulnerability',
62
+ 'url-filtering',
63
+ 'file-blocking',
64
+ 'wildfire-analysis',
65
+ 'data-filtering',
66
+ ];
67
+
68
+ return profileKeywords.some((keyword) => hasChildCommand(ruleNode, keyword));
69
+ };
70
+
71
+ /**
72
+ * Check if a rule action is "allow" (vs deny/drop/reset)
73
+ * @param ruleNode The rule ConfigNode
74
+ * @returns true if the action is allow
75
+ */
76
+ export const isAllowRule = (ruleNode: ConfigNode): boolean => {
77
+ const action = getChildCommand(ruleNode, 'action');
78
+ if (!action) return false;
79
+ return action.id.toLowerCase().includes('allow');
80
+ };
81
+
82
+ /**
83
+ * Check if a rule action is "deny" or "drop" or "reset"
84
+ * @param ruleNode The rule ConfigNode
85
+ * @returns true if the action is deny/drop/reset
86
+ */
87
+ export const isDenyRule = (ruleNode: ConfigNode): boolean => {
88
+ const action = getChildCommand(ruleNode, 'action');
89
+ if (!action) return false;
90
+ const actionId = action.id.toLowerCase();
91
+ return (
92
+ actionId.includes('deny') ||
93
+ actionId.includes('drop') ||
94
+ actionId.includes('reset')
95
+ );
96
+ };
97
+
98
+ /**
99
+ * Get the source zones from a rule
100
+ * @param ruleNode The rule ConfigNode
101
+ * @returns Array of source zone names
102
+ */
103
+ export const getSourceZones = (ruleNode: ConfigNode): string[] => {
104
+ const from = findStanza(ruleNode, 'from');
105
+ if (!from) return [];
106
+ return from.children.map((child) => child.id.trim());
107
+ };
108
+
109
+ /**
110
+ * Get the destination zones from a rule
111
+ * @param ruleNode The rule ConfigNode
112
+ * @returns Array of destination zone names
113
+ */
114
+ export const getDestinationZones = (ruleNode: ConfigNode): string[] => {
115
+ const to = findStanza(ruleNode, 'to');
116
+ if (!to) return [];
117
+ return to.children.map((child) => child.id.trim());
118
+ };
119
+
120
+ /**
121
+ * Get the applications from a rule
122
+ * @param ruleNode The rule ConfigNode
123
+ * @returns Array of application names
124
+ */
125
+ export const getApplications = (ruleNode: ConfigNode): string[] => {
126
+ // Check for "application" stanza with children
127
+ const application = findStanza(ruleNode, 'application');
128
+ if (application && application.children.length > 0) {
129
+ return application.children.map((child) => child.id.trim());
130
+ }
131
+
132
+ // Also check for inline "application <value>" commands
133
+ const appCommands = ruleNode.children.filter((child) =>
134
+ child.id.toLowerCase().startsWith('application ')
135
+ );
136
+ if (appCommands.length > 0) {
137
+ return appCommands.map((cmd) => {
138
+ const parts = cmd.id.split(/\s+/);
139
+ return parts.slice(1).join(' ').replace(/;$/, '').trim();
140
+ });
141
+ }
142
+
143
+ return [];
144
+ };
145
+
146
+ /**
147
+ * Check if a rule uses "any" application (risky)
148
+ * @param ruleNode The rule ConfigNode
149
+ * @returns true if application is "any"
150
+ */
151
+ export const hasAnyApplication = (ruleNode: ConfigNode): boolean => {
152
+ const apps = getApplications(ruleNode);
153
+ return apps.some((app) => app.toLowerCase() === 'any');
154
+ };
155
+
156
+ /**
157
+ * Check if a rule uses "any" source (0.0.0.0/0 or "any")
158
+ * @param ruleNode The rule ConfigNode
159
+ * @returns true if source is "any"
160
+ */
161
+ export const hasAnySource = (ruleNode: ConfigNode): boolean => {
162
+ // Check for "source" stanza with children
163
+ const source = findStanza(ruleNode, 'source');
164
+ if (source && source.children.length > 0) {
165
+ return source.children.some((child) => {
166
+ const id = child.id.toLowerCase().trim().replace(/;$/, '');
167
+ return id === 'any' || id === '0.0.0.0/0';
168
+ });
169
+ }
170
+
171
+ // Also check for inline "source any" or "source <value>" commands
172
+ const sourceCommands = ruleNode.children.filter((child) =>
173
+ child.id.toLowerCase().startsWith('source ')
174
+ );
175
+ if (sourceCommands.length > 0) {
176
+ return sourceCommands.some((cmd) => {
177
+ const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
178
+ return value === 'any' || value === '0.0.0.0/0';
179
+ });
180
+ }
181
+
182
+ return false;
183
+ };
184
+
185
+ /**
186
+ * Check if a rule uses "any" destination
187
+ * @param ruleNode The rule ConfigNode
188
+ * @returns true if destination is "any"
189
+ */
190
+ export const hasAnyDestination = (ruleNode: ConfigNode): boolean => {
191
+ // Check for "destination" stanza with children
192
+ const destination = findStanza(ruleNode, 'destination');
193
+ if (destination && destination.children.length > 0) {
194
+ return destination.children.some((child) => {
195
+ const id = child.id.toLowerCase().trim().replace(/;$/, '');
196
+ return id === 'any' || id === '0.0.0.0/0';
197
+ });
198
+ }
199
+
200
+ // Also check for inline "destination any" or "destination <value>" commands
201
+ const destCommands = ruleNode.children.filter((child) =>
202
+ child.id.toLowerCase().startsWith('destination ')
203
+ );
204
+ if (destCommands.length > 0) {
205
+ return destCommands.some((cmd) => {
206
+ const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
207
+ return value === 'any' || value === '0.0.0.0/0';
208
+ });
209
+ }
210
+
211
+ return false;
212
+ };
213
+
214
+ /**
215
+ * Check if a rule uses "any" service (all TCP/UDP ports)
216
+ * @param ruleNode The rule ConfigNode
217
+ * @returns true if service is "any"
218
+ */
219
+ export const hasAnyService = (ruleNode: ConfigNode): boolean => {
220
+ // Check for "service" stanza with children
221
+ const service = findStanza(ruleNode, 'service');
222
+ if (service && service.children.length > 0) {
223
+ return service.children.some((child) => {
224
+ const id = child.id.toLowerCase().trim().replace(/;$/, '');
225
+ return id === 'any';
226
+ });
227
+ }
228
+
229
+ // Also check for inline "service any" or "service <value>" commands
230
+ const serviceCommands = ruleNode.children.filter((child) =>
231
+ child.id.toLowerCase().startsWith('service ')
232
+ );
233
+ if (serviceCommands.length > 0) {
234
+ return serviceCommands.some((cmd) => {
235
+ const value = cmd.id.split(/\s+/).slice(1).join(' ').toLowerCase().replace(/;$/, '').trim();
236
+ return value === 'any';
237
+ });
238
+ }
239
+
240
+ return false;
241
+ };
242
+
243
+ /**
244
+ * Check if a rule is disabled
245
+ * @param ruleNode The rule ConfigNode
246
+ * @returns true if the rule is disabled
247
+ */
248
+ export const isRuleDisabled = (ruleNode: ConfigNode): boolean => {
249
+ const disabled = getChildCommand(ruleNode, 'disabled');
250
+ if (!disabled) return false;
251
+ return disabled.id.toLowerCase().includes('yes') || disabled.id.toLowerCase().includes('true');
252
+ };
253
+
254
+ /**
255
+ * Get all security rules from a rulebase
256
+ * @param rulebaseNode The rulebase ConfigNode
257
+ * @returns Array of security rule nodes
258
+ */
259
+ export const getSecurityRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
260
+ const security = findStanza(rulebaseNode, 'security');
261
+ if (!security) return [];
262
+
263
+ const rules = findStanza(security, 'rules');
264
+ if (!rules) return [];
265
+
266
+ return rules.children;
267
+ };
268
+
269
+ /**
270
+ * Get all NAT rules from a rulebase
271
+ * @param rulebaseNode The rulebase ConfigNode
272
+ * @returns Array of NAT rule nodes
273
+ */
274
+ export const getNatRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
275
+ const nat = findStanza(rulebaseNode, 'nat');
276
+ if (!nat) return [];
277
+
278
+ const rules = findStanza(nat, 'rules');
279
+ if (!rules) return [];
280
+
281
+ return rules.children;
282
+ };
283
+
284
+ /**
285
+ * Check if HA (High Availability) is configured
286
+ * @param deviceconfigNode The deviceconfig ConfigNode
287
+ * @returns true if HA is configured
288
+ */
289
+ export const isHAConfigured = (deviceconfigNode: ConfigNode): boolean => {
290
+ const ha = findStanza(deviceconfigNode, 'high-availability');
291
+ if (!ha) return false;
292
+ return ha.children.length > 0;
293
+ };
294
+
295
+ /**
296
+ * Check if an interface is a physical Ethernet port
297
+ * @param interfaceName The interface name
298
+ * @returns true if it's a physical ethernet port
299
+ */
300
+ export const isPhysicalEthernetPort = (interfaceName: string): boolean => {
301
+ return /^ethernet\d+\/\d+$/i.test(interfaceName);
302
+ };
303
+
304
+ /**
305
+ * Check if an interface is a loopback
306
+ * @param interfaceName The interface name
307
+ * @returns true if it's a loopback interface
308
+ */
309
+ export const isLoopbackInterface = (interfaceName: string): boolean => {
310
+ return /^loopback\.\d+$/i.test(interfaceName);
311
+ };
312
+
313
+ /**
314
+ * Check if an interface is a tunnel
315
+ * @param interfaceName The interface name
316
+ * @returns true if it's a tunnel interface
317
+ */
318
+ export const isTunnelInterface = (interfaceName: string): boolean => {
319
+ return /^tunnel\.\d+$/i.test(interfaceName);
320
+ };
321
+
322
+ /**
323
+ * Check if an interface is an aggregate (LACP)
324
+ * @param interfaceName The interface name
325
+ * @returns true if it's an aggregate interface
326
+ */
327
+ export const isAggregateInterface = (interfaceName: string): boolean => {
328
+ return /^ae\d+$/i.test(interfaceName);
329
+ };
330
+
331
+ /**
332
+ * Extract zone name from a zone configuration node
333
+ * @param zoneNode The zone ConfigNode
334
+ * @returns The zone name
335
+ */
336
+ export const getZoneName = (zoneNode: ConfigNode): string => {
337
+ // Zone node ID is typically the zone name itself
338
+ return zoneNode.id.split(/\s+/)[0] || zoneNode.id;
339
+ };
340
+
341
+ /**
342
+ * Check if zone protection profile is applied to a zone
343
+ * @param zoneNode The zone ConfigNode
344
+ * @returns true if zone protection profile is configured
345
+ */
346
+ export const hasZoneProtectionProfile = (zoneNode: ConfigNode): boolean => {
347
+ return hasChildCommand(zoneNode, 'zone-protection-profile');
348
+ };
349
+
350
+ /**
351
+ * Check if user identification is enabled on a zone
352
+ * @param zoneNode The zone ConfigNode
353
+ * @returns true if user identification is enabled
354
+ */
355
+ export const hasUserIdentification = (zoneNode: ConfigNode): boolean => {
356
+ const network = findStanza(zoneNode, 'network');
357
+ if (!network) return false;
358
+ return hasChildCommand(network, 'enable-user-identification');
359
+ };
360
+
361
+ /**
362
+ * Parse PAN-OS address format (e.g., "10.0.0.1/24" or "10.0.0.1-10.0.0.255")
363
+ * @param address The address string
364
+ * @returns Object with parsed address info, or null if invalid
365
+ */
366
+ export const parsePanosAddress = (
367
+ address: string
368
+ ): { ip: number; prefix?: number; rangeEnd?: number } | null => {
369
+ // CIDR format: 10.0.0.1/24
370
+ if (address.includes('/')) {
371
+ const parts = address.split('/');
372
+ if (parts.length !== 2) return null;
373
+
374
+ const [ipStr, prefixStr] = parts;
375
+ if (!ipStr || !prefixStr) {
376
+ return null;
377
+ }
378
+
379
+ const ip = parseIp(ipStr);
380
+ const prefix = parseInt(prefixStr, 10);
381
+
382
+ if (ip === null || isNaN(prefix) || prefix < 0 || prefix > 32) {
383
+ return null;
384
+ }
385
+
386
+ return { ip, prefix };
387
+ }
388
+
389
+ // Range format: 10.0.0.1-10.0.0.255
390
+ if (address.includes('-')) {
391
+ const parts = address.split('-');
392
+ if (parts.length !== 2) return null;
393
+
394
+ const [startStr, endStr] = parts;
395
+ if (!startStr || !endStr) {
396
+ return null;
397
+ }
398
+
399
+ const ip = parseIp(startStr);
400
+ const rangeEnd = parseIp(endStr);
401
+
402
+ if (ip === null || rangeEnd === null) {
403
+ return null;
404
+ }
405
+
406
+ return { ip, rangeEnd };
407
+ }
408
+
409
+ // Single IP
410
+ const ip = parseIp(address);
411
+ if (ip === null) return null;
412
+
413
+ return { ip };
414
+ };
415
+
416
+ /**
417
+ * Check if WildFire is configured
418
+ * @param profilesNode The profiles ConfigNode
419
+ * @returns true if WildFire analysis is configured
420
+ */
421
+ export const hasWildfireProfile = (profilesNode: ConfigNode): boolean => {
422
+ const wildfire = findStanza(profilesNode, 'wildfire-analysis');
423
+ return wildfire !== undefined && wildfire.children.length > 0;
424
+ };
425
+
426
+ /**
427
+ * Check if URL Filtering is configured
428
+ * @param profilesNode The profiles ConfigNode
429
+ * @returns true if URL filtering is configured
430
+ */
431
+ export const hasUrlFilteringProfile = (profilesNode: ConfigNode): boolean => {
432
+ const urlFiltering = findStanza(profilesNode, 'url-filtering');
433
+ return urlFiltering !== undefined && urlFiltering.children.length > 0;
434
+ };
435
+
436
+ /**
437
+ * Check if Anti-Virus profile is configured
438
+ * @param profilesNode The profiles ConfigNode
439
+ * @returns true if AV profile is configured
440
+ */
441
+ export const hasAntiVirusProfile = (profilesNode: ConfigNode): boolean => {
442
+ const virus = findStanza(profilesNode, 'virus');
443
+ return virus !== undefined && virus.children.length > 0;
444
+ };
445
+
446
+ /**
447
+ * Check if Anti-Spyware profile is configured
448
+ * @param profilesNode The profiles ConfigNode
449
+ * @returns true if Anti-Spyware profile is configured
450
+ */
451
+ export const hasAntiSpywareProfile = (profilesNode: ConfigNode): boolean => {
452
+ const spyware = findStanza(profilesNode, 'spyware');
453
+ return spyware !== undefined && spyware.children.length > 0;
454
+ };
455
+
456
+ /**
457
+ * Check if Vulnerability Protection profile is configured
458
+ * @param profilesNode The profiles ConfigNode
459
+ * @returns true if Vulnerability Protection profile is configured
460
+ */
461
+ export const hasVulnerabilityProfile = (profilesNode: ConfigNode): boolean => {
462
+ const vuln = findStanza(profilesNode, 'vulnerability');
463
+ return vuln !== undefined && vuln.children.length > 0;
464
+ };
465
+
466
+ /**
467
+ * Check if File Blocking profile is configured
468
+ * @param profilesNode The profiles ConfigNode
469
+ * @returns true if File Blocking profile is configured
470
+ */
471
+ export const hasFileBlockingProfile = (profilesNode: ConfigNode): boolean => {
472
+ const fileBlocking = findStanza(profilesNode, 'file-blocking');
473
+ return fileBlocking !== undefined && fileBlocking.children.length > 0;
474
+ };
475
+
476
+ /**
477
+ * Check if password complexity is configured
478
+ * @param systemNode The system ConfigNode
479
+ * @returns true if password complexity is configured
480
+ */
481
+ export const hasPasswordComplexity = (systemNode: ConfigNode): boolean => {
482
+ const passwordComplexity = findStanza(systemNode, 'password-complexity');
483
+ if (!passwordComplexity) return false;
484
+ return hasChildCommand(passwordComplexity, 'enabled');
485
+ };
486
+
487
+ /**
488
+ * Get password complexity settings
489
+ * @param systemNode The system ConfigNode
490
+ * @returns Object with password complexity settings
491
+ */
492
+ export const getPasswordComplexitySettings = (
493
+ systemNode: ConfigNode
494
+ ): {
495
+ enabled: boolean;
496
+ minLength: number | null;
497
+ minUppercase: number | null;
498
+ minLowercase: number | null;
499
+ minNumeric: number | null;
500
+ minSpecial: number | null;
501
+ } => {
502
+ const defaults = {
503
+ enabled: false,
504
+ minLength: null as number | null,
505
+ minUppercase: null as number | null,
506
+ minLowercase: null as number | null,
507
+ minNumeric: null as number | null,
508
+ minSpecial: null as number | null,
509
+ };
510
+
511
+ const passwordComplexity = findStanza(systemNode, 'password-complexity');
512
+ if (!passwordComplexity) return defaults;
513
+
514
+ const enabledCmd = getChildCommand(passwordComplexity, 'enabled');
515
+ const enabled = enabledCmd?.id.toLowerCase().includes('yes') ?? false;
516
+
517
+ const getNumericValue = (key: string): number | null => {
518
+ const cmd = getChildCommand(passwordComplexity, key);
519
+ if (!cmd) return null;
520
+ const match = cmd.id.match(/(\d+)/);
521
+ return match?.[1] ? parseInt(match[1], 10) : null;
522
+ };
523
+
524
+ return {
525
+ enabled,
526
+ minLength: getNumericValue('minimum-length'),
527
+ minUppercase: getNumericValue('minimum-uppercase-letters'),
528
+ minLowercase: getNumericValue('minimum-lowercase-letters'),
529
+ minNumeric: getNumericValue('minimum-numeric-letters'),
530
+ minSpecial: getNumericValue('minimum-special-characters'),
531
+ };
532
+ };
533
+
534
+ /**
535
+ * Check if SNMP is configured with v3 (secure) or v2c (less secure)
536
+ * @param systemNode The system ConfigNode
537
+ * @returns Object indicating SNMP configuration status
538
+ */
539
+ export const getSnmpConfiguration = (
540
+ systemNode: ConfigNode
541
+ ): { configured: boolean; hasV3: boolean; hasV2c: boolean; hasCommunityPublic: boolean } => {
542
+ const snmpSetting = findStanza(systemNode, 'snmp-setting');
543
+ if (!snmpSetting) {
544
+ return { configured: false, hasV3: false, hasV2c: false, hasCommunityPublic: false };
545
+ }
546
+
547
+ const accessSetting = findStanza(snmpSetting, 'access-setting');
548
+ if (!accessSetting) {
549
+ return { configured: false, hasV3: false, hasV2c: false, hasCommunityPublic: false };
550
+ }
551
+
552
+ const version = findStanza(accessSetting, 'version');
553
+ if (!version) {
554
+ return { configured: false, hasV3: false, hasV2c: false, hasCommunityPublic: false };
555
+ }
556
+
557
+ const hasV3 = findStanza(version, 'v3') !== undefined;
558
+ const v2c = findStanza(version, 'v2c');
559
+ const hasV2c = v2c !== undefined;
560
+
561
+ // Check for default/weak community strings
562
+ let hasCommunityPublic = false;
563
+ if (v2c) {
564
+ const communityString = getChildCommand(v2c, 'snmp-community-string');
565
+ if (communityString) {
566
+ const value = communityString.id.toLowerCase();
567
+ hasCommunityPublic =
568
+ value.includes('public') || value.includes('private') || value.includes('community');
569
+ }
570
+ }
571
+
572
+ return { configured: true, hasV3, hasV2c, hasCommunityPublic };
573
+ };
574
+
575
+ /**
576
+ * Check if decryption profile has secure TLS settings
577
+ * @param decryptionProfileNode The decryption profile ConfigNode
578
+ * @returns Object with TLS security assessment
579
+ */
580
+ export const getDecryptionTlsSettings = (
581
+ decryptionProfileNode: ConfigNode
582
+ ): { hasMinVersion: boolean; minVersion: string | null; hasWeakCiphers: boolean } => {
583
+ const sslProtocolSettings = findStanza(decryptionProfileNode, 'ssl-protocol-settings');
584
+ if (!sslProtocolSettings) {
585
+ return { hasMinVersion: false, minVersion: null, hasWeakCiphers: false };
586
+ }
587
+
588
+ const minVersionCmd = getChildCommand(sslProtocolSettings, 'min-version');
589
+ let minVersion: string | null = null;
590
+ let hasMinVersion = false;
591
+
592
+ if (minVersionCmd) {
593
+ hasMinVersion = true;
594
+ const match = minVersionCmd.id.match(/min-version\s+(\S+)/i);
595
+ minVersion = match?.[1]?.replace(/;$/, '') ?? null;
596
+ }
597
+
598
+ // Check for weak ciphers
599
+ const encAlgoCmd = getChildCommand(sslProtocolSettings, 'enc-algo');
600
+ let hasWeakCiphers = false;
601
+ if (encAlgoCmd) {
602
+ const value = encAlgoCmd.id.toLowerCase();
603
+ hasWeakCiphers =
604
+ value.includes('rc4') ||
605
+ value.includes('3des') ||
606
+ value.includes('des') ||
607
+ value.includes('null');
608
+ }
609
+
610
+ return { hasMinVersion, minVersion, hasWeakCiphers };
611
+ };
612
+
613
+ /**
614
+ * Get IKE crypto profile settings for security assessment
615
+ * @param ikeProfileNode The IKE crypto profile ConfigNode
616
+ * @returns Object with security assessment
617
+ */
618
+ export const getIkeCryptoSettings = (
619
+ ikeProfileNode: ConfigNode
620
+ ): {
621
+ hasWeakDH: boolean;
622
+ hasWeakHash: boolean;
623
+ hasWeakEncryption: boolean;
624
+ dhGroups: string[];
625
+ hashes: string[];
626
+ encryptions: string[];
627
+ } => {
628
+ const dhGroups: string[] = [];
629
+ const hashes: string[] = [];
630
+ const encryptions: string[] = [];
631
+
632
+ // Extract DH groups
633
+ const dhGroupCmd = getChildCommand(ikeProfileNode, 'dh-group');
634
+ if (dhGroupCmd) {
635
+ const match = dhGroupCmd.id.match(/dh-group\s+\[([^\]]+)\]/i);
636
+ if (match?.[1]) {
637
+ dhGroups.push(...match[1].split(/\s+/).filter((g) => g.length > 0));
638
+ } else {
639
+ const singleMatch = dhGroupCmd.id.match(/dh-group\s+(\S+)/i);
640
+ if (singleMatch?.[1]) {
641
+ dhGroups.push(singleMatch[1].replace(/;$/, ''));
642
+ }
643
+ }
644
+ }
645
+
646
+ // Extract hash algorithms
647
+ const hashCmd = getChildCommand(ikeProfileNode, 'hash');
648
+ if (hashCmd) {
649
+ const match = hashCmd.id.match(/hash\s+\[([^\]]+)\]/i);
650
+ if (match?.[1]) {
651
+ hashes.push(...match[1].split(/\s+/).filter((h) => h.length > 0));
652
+ } else {
653
+ const singleMatch = hashCmd.id.match(/hash\s+(\S+)/i);
654
+ if (singleMatch?.[1]) {
655
+ hashes.push(singleMatch[1].replace(/;$/, ''));
656
+ }
657
+ }
658
+ }
659
+
660
+ // Extract encryption algorithms
661
+ const encCmd = getChildCommand(ikeProfileNode, 'encryption');
662
+ if (encCmd) {
663
+ const match = encCmd.id.match(/encryption\s+\[([^\]]+)\]/i);
664
+ if (match?.[1]) {
665
+ encryptions.push(...match[1].split(/\s+/).filter((e) => e.length > 0));
666
+ } else {
667
+ const singleMatch = encCmd.id.match(/encryption\s+(\S+)/i);
668
+ if (singleMatch?.[1]) {
669
+ encryptions.push(singleMatch[1].replace(/;$/, ''));
670
+ }
671
+ }
672
+ }
673
+
674
+ // Check for weak settings
675
+ const weakDHGroups = ['group1', 'group2', 'group5'];
676
+ const weakHashes = ['md5', 'sha1'];
677
+ const weakEncryptions = ['des', '3des'];
678
+
679
+ const hasWeakDH = dhGroups.some((g) => weakDHGroups.includes(g.toLowerCase()));
680
+ const hasWeakHash = hashes.some((h) => weakHashes.includes(h.toLowerCase()));
681
+ const hasWeakEncryption = encryptions.some((e) => weakEncryptions.includes(e.toLowerCase()));
682
+
683
+ return { hasWeakDH, hasWeakHash, hasWeakEncryption, dhGroups, hashes, encryptions };
684
+ };
685
+
686
+ /**
687
+ * Check if zone protection profile has flood protection configured
688
+ * @param zppNode The zone protection profile ConfigNode
689
+ * @returns Object indicating flood protection status
690
+ */
691
+ export const hasFloodProtection = (
692
+ zppNode: ConfigNode
693
+ ): { hasSyn: boolean; hasUdp: boolean; hasIcmp: boolean; hasOtherIp: boolean } => {
694
+ const flood = findStanza(zppNode, 'flood');
695
+ if (!flood) {
696
+ return { hasSyn: false, hasUdp: false, hasIcmp: false, hasOtherIp: false };
697
+ }
698
+
699
+ const tcpSyn = findStanza(flood, 'tcp-syn');
700
+ const udp = findStanza(flood, 'udp');
701
+ const icmp = findStanza(flood, 'icmp');
702
+ const otherIp = findStanza(flood, 'other-ip');
703
+
704
+ const isEnabled = (stanza: ConfigNode | undefined): boolean => {
705
+ if (!stanza) return false;
706
+ const enableCmd = getChildCommand(stanza, 'enable');
707
+ return enableCmd?.id.toLowerCase().includes('yes') ?? false;
708
+ };
709
+
710
+ return {
711
+ hasSyn: isEnabled(tcpSyn),
712
+ hasUdp: isEnabled(udp),
713
+ hasIcmp: isEnabled(icmp),
714
+ hasOtherIp: isEnabled(otherIp),
715
+ };
716
+ };
717
+
718
+ /**
719
+ * Check if zone protection profile has reconnaissance protection
720
+ * @param zppNode The zone protection profile ConfigNode
721
+ * @returns true if scan/reconnaissance protection is configured
722
+ */
723
+ export const hasReconProtection = (zppNode: ConfigNode): boolean => {
724
+ const scan = findStanza(zppNode, 'scan');
725
+ if (!scan) return false;
726
+
727
+ // Check for at least one scan protection type
728
+ const tcpPortScan = findStanza(scan, 'tcp-port-scan');
729
+ const hostSweep = findStanza(scan, 'host-sweep');
730
+ const udpPortScan = findStanza(scan, 'udp-port-scan');
731
+
732
+ return tcpPortScan !== undefined || hostSweep !== undefined || udpPortScan !== undefined;
733
+ };
734
+
735
+ /**
736
+ * Check if User-ID is enabled on a zone (for untrust zone check)
737
+ * @param zoneNode The zone ConfigNode
738
+ * @returns true if User-ID is enabled
739
+ */
740
+ export const isUserIdEnabled = (zoneNode: ConfigNode): boolean => {
741
+ // Check direct enable-user-identification command
742
+ if (hasChildCommand(zoneNode, 'enable-user-identification')) {
743
+ const cmd = getChildCommand(zoneNode, 'enable-user-identification');
744
+ return cmd?.id.toLowerCase().includes('yes') ?? false;
745
+ }
746
+ return false;
747
+ };
748
+
749
+ /**
750
+ * Check if HA has backup links configured
751
+ * @param haNode The high-availability ConfigNode
752
+ * @returns Object indicating backup link status
753
+ */
754
+ export const getHABackupStatus = (
755
+ haNode: ConfigNode
756
+ ): { hasHa1Backup: boolean; hasHa2Backup: boolean; hasHeartbeatBackup: boolean } => {
757
+ const interfaceStanza = findStanza(haNode, 'interface');
758
+ if (!interfaceStanza) {
759
+ return { hasHa1Backup: false, hasHa2Backup: false, hasHeartbeatBackup: false };
760
+ }
761
+
762
+ const hasHa1Backup = findStanza(interfaceStanza, 'ha1-backup') !== undefined;
763
+ const hasHa2Backup = findStanza(interfaceStanza, 'ha2-backup') !== undefined;
764
+
765
+ // Check for heartbeat backup in election-option
766
+ const group = findStanza(haNode, 'group');
767
+ let hasHeartbeatBackup = false;
768
+ if (group) {
769
+ const electionOption = findStanza(group, 'election-option');
770
+ if (electionOption) {
771
+ const heartbeatCmd = getChildCommand(electionOption, 'heartbeat-backup');
772
+ hasHeartbeatBackup = heartbeatCmd?.id.toLowerCase().includes('yes') ?? false;
773
+ }
774
+ }
775
+
776
+ return { hasHa1Backup, hasHa2Backup, hasHeartbeatBackup };
777
+ };
778
+
779
+ /**
780
+ * Check if HA has link monitoring configured
781
+ * @param haNode The high-availability ConfigNode
782
+ * @returns true if link monitoring is configured
783
+ */
784
+ export const hasHALinkMonitoring = (haNode: ConfigNode): boolean => {
785
+ const linkMonitoring = findStanza(haNode, 'link-monitoring');
786
+ if (!linkMonitoring) return false;
787
+
788
+ const linkGroup = findStanza(linkMonitoring, 'link-group');
789
+ return linkGroup !== undefined && linkGroup.children.length > 0;
790
+ };
791
+
792
+ /**
793
+ * Check if HA has path monitoring configured
794
+ * @param haNode The high-availability ConfigNode
795
+ * @returns true if path monitoring is configured
796
+ */
797
+ export const hasHAPathMonitoring = (haNode: ConfigNode): boolean => {
798
+ const pathMonitoring = findStanza(haNode, 'path-monitoring');
799
+ if (!pathMonitoring) return false;
800
+
801
+ const pathGroup = findStanza(pathMonitoring, 'path-group');
802
+ return pathGroup !== undefined && pathGroup.children.length > 0;
803
+ };
804
+
805
+ /**
806
+ * Check if log forwarding is configured
807
+ * @param logSettingsNode The log-settings ConfigNode
808
+ * @returns Object indicating log forwarding status
809
+ */
810
+ export const getLogForwardingStatus = (
811
+ logSettingsNode: ConfigNode
812
+ ): { hasSyslog: boolean; hasPanorama: boolean; hasEmail: boolean } => {
813
+ const profiles = findStanza(logSettingsNode, 'profiles');
814
+ if (!profiles) {
815
+ return { hasSyslog: false, hasPanorama: false, hasEmail: false };
816
+ }
817
+
818
+ let hasSyslog = false;
819
+ let hasPanorama = false;
820
+ let hasEmail = false;
821
+
822
+ // Check each profile for forwarding destinations
823
+ for (const profile of profiles.children) {
824
+ const matchList = findStanza(profile, 'match-list');
825
+ if (matchList) {
826
+ for (const match of matchList.children) {
827
+ if (findStanza(match, 'send-syslog')) hasSyslog = true;
828
+ if (hasChildCommand(match, 'send-to-panorama')) {
829
+ const cmd = getChildCommand(match, 'send-to-panorama');
830
+ if (cmd?.id.toLowerCase().includes('yes')) hasPanorama = true;
831
+ }
832
+ if (findStanza(match, 'send-email')) hasEmail = true;
833
+ }
834
+ }
835
+ }
836
+
837
+ return { hasSyslog, hasPanorama, hasEmail };
838
+ };
839
+
840
+ /**
841
+ * Check if dynamic content updates are scheduled
842
+ * @param systemNode The system ConfigNode
843
+ * @returns Object indicating update schedule status
844
+ */
845
+ export const getUpdateScheduleStatus = (
846
+ systemNode: ConfigNode
847
+ ): {
848
+ hasThreats: boolean;
849
+ hasAntivirus: boolean;
850
+ hasWildfire: boolean;
851
+ wildfireRealtime: boolean;
852
+ } => {
853
+ const updateSchedule = findStanza(systemNode, 'update-schedule');
854
+ if (!updateSchedule) {
855
+ return { hasThreats: false, hasAntivirus: false, hasWildfire: false, wildfireRealtime: false };
856
+ }
857
+
858
+ const threats = findStanza(updateSchedule, 'threats');
859
+ const antivirus = findStanza(updateSchedule, 'anti-virus');
860
+ const wildfire = findStanza(updateSchedule, 'wildfire');
861
+
862
+ let wildfireRealtime = false;
863
+ if (wildfire) {
864
+ const recurring = findStanza(wildfire, 'recurring');
865
+ if (recurring) {
866
+ wildfireRealtime = hasChildCommand(recurring, 'real-time');
867
+ }
868
+ }
869
+
870
+ 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,
874
+ wildfireRealtime,
875
+ };
876
+ };
877
+
878
+ /**
879
+ * Get all decryption rules from a rulebase
880
+ * @param rulebaseNode The rulebase ConfigNode
881
+ * @returns Array of decryption rule nodes
882
+ */
883
+ export const getDecryptionRules = (rulebaseNode: ConfigNode): ConfigNode[] => {
884
+ const decryption = findStanza(rulebaseNode, 'decryption');
885
+ if (!decryption) return [];
886
+
887
+ const rules = findStanza(decryption, 'rules');
888
+ if (!rules) return [];
889
+
890
+ return rules.children;
891
+ };
892
+
893
+ /**
894
+ * Check if a decryption rule uses "decrypt" action
895
+ * @param ruleNode The decryption rule ConfigNode
896
+ * @returns true if the action is decrypt
897
+ */
898
+ export const isDecryptRule = (ruleNode: ConfigNode): boolean => {
899
+ const action = getChildCommand(ruleNode, 'action');
900
+ if (!action) return false;
901
+ return action.id.toLowerCase().includes('decrypt');
902
+ };
903
+
904
+ /**
905
+ * Get interface management profile settings
906
+ * @param profileNode The interface-management-profile ConfigNode
907
+ * @returns Object indicating enabled services
908
+ */
909
+ export const getInterfaceManagementServices = (
910
+ profileNode: ConfigNode
911
+ ): {
912
+ https: boolean;
913
+ http: boolean;
914
+ ssh: boolean;
915
+ telnet: boolean;
916
+ ping: boolean;
917
+ snmp: boolean;
918
+ } => {
919
+ // Use exact matching with word boundary to avoid "https" matching "http"
920
+ const isServiceEnabled = (serviceName: string): boolean => {
921
+ // Look for exact service name followed by space/end (e.g., "http yes" not "https yes")
922
+ const cmd = profileNode.children.find((child) => {
923
+ const lowerId = child.id.toLowerCase();
924
+ // Match exact service name: "http yes", "http no", etc.
925
+ return lowerId === serviceName || lowerId.startsWith(serviceName + ' ');
926
+ });
927
+ if (!cmd) return false;
928
+ return cmd.id.toLowerCase().includes('yes');
929
+ };
930
+
931
+ return {
932
+ https: isServiceEnabled('https'),
933
+ http: isServiceEnabled('http'),
934
+ ssh: isServiceEnabled('ssh'),
935
+ telnet: isServiceEnabled('telnet'),
936
+ ping: isServiceEnabled('ping'),
937
+ snmp: isServiceEnabled('snmp'),
938
+ };
939
+ };