@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,892 @@
1
+ // packages/rule-helpers/src/fortinet/helpers.ts
2
+ // Fortinet FortiGate (FortiOS) specific helper functions
3
+
4
+ import type { ConfigNode } from '../../types/ConfigNode';
5
+ import { parseIp } from '../common/helpers';
6
+
7
+ /**
8
+ * Find a config section by name within a node's children.
9
+ * FortiOS uses "config <section>" format.
10
+ * @param node The parent ConfigNode
11
+ * @param sectionName The section name (e.g., "system global", "firewall policy")
12
+ * @returns The matching child node, or undefined
13
+ */
14
+ export const findConfigSection = (
15
+ node: ConfigNode,
16
+ sectionName: string
17
+ ): ConfigNode | undefined => {
18
+ const normalizedName = sectionName.toLowerCase();
19
+ return node.children.find((child) => {
20
+ const childId = child.id.toLowerCase();
21
+ // Match "config <sectionName>" or just "<sectionName>"
22
+ return (
23
+ childId === `config ${normalizedName}` ||
24
+ childId === normalizedName ||
25
+ childId.startsWith(`config ${normalizedName} `) ||
26
+ childId.startsWith(`${normalizedName} `)
27
+ );
28
+ });
29
+ };
30
+
31
+ /**
32
+ * Find all config sections matching a pattern within a node's children
33
+ * @param node The parent ConfigNode
34
+ * @param pattern The regex pattern to match
35
+ * @returns Array of matching child nodes
36
+ */
37
+ export const findConfigSections = (node: ConfigNode, pattern: RegExp): ConfigNode[] => {
38
+ return node.children.filter((child) => pattern.test(child.id.toLowerCase()));
39
+ };
40
+
41
+ /**
42
+ * Find an edit entry by name within a config section.
43
+ * FortiOS uses "edit <name>" for entries.
44
+ * @param configSection The config section ConfigNode
45
+ * @param entryName The entry name to find
46
+ * @returns The matching edit entry, or undefined
47
+ */
48
+ export const findEditEntry = (
49
+ configSection: ConfigNode,
50
+ entryName: string
51
+ ): ConfigNode | undefined => {
52
+ const normalizedName = entryName.toLowerCase().replace(/^["']|["']$/g, '');
53
+ return configSection.children.find((child) => {
54
+ const childId = child.id.toLowerCase();
55
+ // Match "edit <name>" with or without quotes
56
+ const editMatch = childId.match(/^edit\s+["']?([^"']+)["']?$/i);
57
+ const editName = editMatch?.[1];
58
+ if (editName) {
59
+ return editName.toLowerCase() === normalizedName;
60
+ }
61
+ return false;
62
+ });
63
+ };
64
+
65
+ /**
66
+ * Get all edit entries within a config section
67
+ * @param configSection The config section ConfigNode
68
+ * @returns Array of edit entry nodes
69
+ */
70
+ export const getEditEntries = (configSection: ConfigNode): ConfigNode[] => {
71
+ return configSection.children.filter((child) =>
72
+ child.id.toLowerCase().startsWith('edit ')
73
+ );
74
+ };
75
+
76
+ /**
77
+ * Extract the name from an edit entry.
78
+ * FortiOS uses "edit <name>" format.
79
+ * @param editEntry The edit entry ConfigNode
80
+ * @returns The entry name
81
+ */
82
+ export const getEditEntryName = (editEntry: ConfigNode): string => {
83
+ const match = editEntry.id.match(/^edit\s+["']?([^"']+)["']?$/i);
84
+ const entryName = match?.[1];
85
+ return entryName ?? editEntry.id;
86
+ };
87
+
88
+ /**
89
+ * Get a "set" command value from a FortiOS config entry.
90
+ * FortiOS uses "set <param> <value>" format.
91
+ * @param node The ConfigNode
92
+ * @param paramName The parameter name
93
+ * @returns The value, or undefined
94
+ */
95
+ export const getSetValue = (node: ConfigNode, paramName: string): string | undefined => {
96
+ const normalizedParam = paramName.toLowerCase();
97
+ for (const child of node.children) {
98
+ const childId = child.id.toLowerCase();
99
+ const match = childId.match(new RegExp(`^set\\s+${normalizedParam}\\s+(.+)$`, 'i'));
100
+ const value = match?.[1];
101
+ if (value) {
102
+ // Remove quotes if present
103
+ return value.replace(/^["']|["']$/g, '').trim();
104
+ }
105
+ }
106
+ return undefined;
107
+ };
108
+
109
+ /**
110
+ * Check if a "set" command exists for a parameter
111
+ * @param node The ConfigNode
112
+ * @param paramName The parameter name
113
+ * @returns true if the set command exists
114
+ */
115
+ export const hasSetValue = (node: ConfigNode, paramName: string): boolean => {
116
+ return getSetValue(node, paramName) !== undefined;
117
+ };
118
+
119
+ /**
120
+ * Get all "set" command values for a parameter that may appear multiple times
121
+ * (e.g., set member "obj1", set member "obj2")
122
+ * @param node The ConfigNode
123
+ * @param paramName The parameter name
124
+ * @returns Array of values
125
+ */
126
+ export const getSetValues = (node: ConfigNode, paramName: string): string[] => {
127
+ const normalizedParam = paramName.toLowerCase();
128
+ const values: string[] = [];
129
+ for (const child of node.children) {
130
+ const childId = child.id.toLowerCase();
131
+ const match = childId.match(new RegExp(`^set\\s+${normalizedParam}\\s+(.+)$`, 'i'));
132
+ const matchValue = match?.[1];
133
+ if (matchValue) {
134
+ // Handle space-separated values (e.g., set allowaccess ping https ssh)
135
+ const valueStr = matchValue.replace(/^["']|["']$/g, '').trim();
136
+ // Split by space, keeping quoted values together
137
+ const parts = valueStr.match(/["'][^"']+["']|\S+/g) || [];
138
+ values.push(...parts.map((p) => p.replace(/^["']|["']$/g, '')));
139
+ }
140
+ }
141
+ return values;
142
+ };
143
+
144
+ /**
145
+ * Check if a firewall policy action is "accept" (allow)
146
+ * @param policyNode The firewall policy ConfigNode
147
+ * @returns true if the action is accept
148
+ */
149
+ export const isPolicyAccept = (policyNode: ConfigNode): boolean => {
150
+ const action = getSetValue(policyNode, 'action');
151
+ return action?.toLowerCase() === 'accept';
152
+ };
153
+
154
+ /**
155
+ * Check if a firewall policy action is "deny" or "drop"
156
+ * @param policyNode The firewall policy ConfigNode
157
+ * @returns true if the action is deny
158
+ */
159
+ export const isPolicyDeny = (policyNode: ConfigNode): boolean => {
160
+ const action = getSetValue(policyNode, 'action');
161
+ if (!action) return false;
162
+ const actionLower = action.toLowerCase();
163
+ return actionLower === 'deny' || actionLower === 'drop';
164
+ };
165
+
166
+ /**
167
+ * Check if a firewall policy is disabled (status disable)
168
+ * @param policyNode The firewall policy ConfigNode
169
+ * @returns true if the policy is disabled
170
+ */
171
+ export const isPolicyDisabled = (policyNode: ConfigNode): boolean => {
172
+ const status = getSetValue(policyNode, 'status');
173
+ return status?.toLowerCase() === 'disable';
174
+ };
175
+
176
+ /**
177
+ * Check if a firewall policy has logging enabled
178
+ * @param policyNode The firewall policy ConfigNode
179
+ * @returns Object indicating logtraffic status
180
+ */
181
+ export const hasLogging = (policyNode: ConfigNode): { logtraffic: string | undefined; logtrafficStart: boolean } => {
182
+ const logtraffic = getSetValue(policyNode, 'logtraffic');
183
+ const logtrafficStart = getSetValue(policyNode, 'logtraffic-start');
184
+ return {
185
+ logtraffic,
186
+ logtrafficStart: logtrafficStart?.toLowerCase() === 'enable',
187
+ };
188
+ };
189
+
190
+ /**
191
+ * Check if a policy uses "all" (any) source address
192
+ * @param policyNode The firewall policy ConfigNode
193
+ * @returns true if srcaddr includes "all"
194
+ */
195
+ export const hasAnySrcAddr = (policyNode: ConfigNode): boolean => {
196
+ const srcaddr = getSetValues(policyNode, 'srcaddr');
197
+ return srcaddr.some((addr) => addr.toLowerCase() === 'all');
198
+ };
199
+
200
+ /**
201
+ * Check if a policy uses "all" (any) destination address
202
+ * @param policyNode The firewall policy ConfigNode
203
+ * @returns true if dstaddr includes "all"
204
+ */
205
+ export const hasAnyDstAddr = (policyNode: ConfigNode): boolean => {
206
+ const dstaddr = getSetValues(policyNode, 'dstaddr');
207
+ return dstaddr.some((addr) => addr.toLowerCase() === 'all');
208
+ };
209
+
210
+ /**
211
+ * Check if a policy uses "ALL" service (any service)
212
+ * @param policyNode The firewall policy ConfigNode
213
+ * @returns true if service includes "ALL"
214
+ */
215
+ export const hasAnyService = (policyNode: ConfigNode): boolean => {
216
+ const service = getSetValues(policyNode, 'service');
217
+ return service.some((svc) => svc.toUpperCase() === 'ALL');
218
+ };
219
+
220
+ /**
221
+ * Check if a policy has UTM/security profiles attached
222
+ * @param policyNode The firewall policy ConfigNode
223
+ * @returns Object with profile statuses
224
+ */
225
+ export const getSecurityProfiles = (policyNode: ConfigNode): {
226
+ avProfile: string | undefined;
227
+ webfilterProfile: string | undefined;
228
+ ipsProfile: string | undefined;
229
+ applicationList: string | undefined;
230
+ dnsfilterProfile: string | undefined;
231
+ emailfilterProfile: string | undefined;
232
+ dlpSensor: string | undefined;
233
+ sslSshProfile: string | undefined;
234
+ profileProtocolOptions: string | undefined;
235
+ utmStatus: string | undefined;
236
+ inspectionMode: string | undefined;
237
+ } => {
238
+ return {
239
+ avProfile: getSetValue(policyNode, 'av-profile'),
240
+ webfilterProfile: getSetValue(policyNode, 'webfilter-profile'),
241
+ ipsProfile: getSetValue(policyNode, 'ips-sensor'),
242
+ applicationList: getSetValue(policyNode, 'application-list'),
243
+ dnsfilterProfile: getSetValue(policyNode, 'dnsfilter-profile'),
244
+ emailfilterProfile: getSetValue(policyNode, 'emailfilter-profile'),
245
+ dlpSensor: getSetValue(policyNode, 'dlp-sensor'),
246
+ sslSshProfile: getSetValue(policyNode, 'ssl-ssh-profile'),
247
+ profileProtocolOptions: getSetValue(policyNode, 'profile-protocol-options'),
248
+ utmStatus: getSetValue(policyNode, 'utm-status'),
249
+ inspectionMode: getSetValue(policyNode, 'inspection-mode'),
250
+ };
251
+ };
252
+
253
+ /**
254
+ * Check if a policy has any UTM profile attached
255
+ * @param policyNode The firewall policy ConfigNode
256
+ * @returns true if any security profile is configured
257
+ */
258
+ export const hasSecurityProfile = (policyNode: ConfigNode): boolean => {
259
+ const profiles = getSecurityProfiles(policyNode);
260
+ return !!(
261
+ profiles.avProfile ||
262
+ profiles.webfilterProfile ||
263
+ profiles.ipsProfile ||
264
+ profiles.applicationList ||
265
+ profiles.dnsfilterProfile ||
266
+ profiles.emailfilterProfile ||
267
+ profiles.dlpSensor
268
+ );
269
+ };
270
+
271
+ /**
272
+ * Get the interface IP address and mask from a system interface entry
273
+ * @param interfaceNode The interface edit entry ConfigNode
274
+ * @returns Object with ip and mask, or undefined
275
+ */
276
+ export const getInterfaceIp = (
277
+ interfaceNode: ConfigNode
278
+ ): { ip: string; mask: string } | undefined => {
279
+ const ipValue = getSetValue(interfaceNode, 'ip');
280
+ if (!ipValue) return undefined;
281
+
282
+ // FortiOS format: "set ip 192.168.1.1 255.255.255.0" or "set ip 192.168.1.1/24"
283
+ const parts = ipValue.split(/\s+/);
284
+ if (parts.length >= 2) {
285
+ const [ip, mask] = parts;
286
+ if (!ip || !mask) {
287
+ return undefined;
288
+ }
289
+ return { ip, mask };
290
+ }
291
+ const firstPart = parts[0];
292
+ if (firstPart && firstPart.includes('/')) {
293
+ const [ip, prefix] = firstPart.split('/');
294
+ if (!ip || !prefix) {
295
+ return undefined;
296
+ }
297
+ return { ip, mask: prefix };
298
+ }
299
+ return undefined;
300
+ };
301
+
302
+ /**
303
+ * Get allowed access methods on an interface
304
+ * @param interfaceNode The interface edit entry ConfigNode
305
+ * @returns Array of allowed access methods
306
+ */
307
+ export const getInterfaceAllowAccess = (interfaceNode: ConfigNode): string[] => {
308
+ return getSetValues(interfaceNode, 'allowaccess');
309
+ };
310
+
311
+ /**
312
+ * Check if HTTP(S) management is allowed on an interface
313
+ * @param interfaceNode The interface edit entry ConfigNode
314
+ * @returns true if HTTP or HTTPS access is allowed
315
+ */
316
+ export const hasHttpManagement = (interfaceNode: ConfigNode): boolean => {
317
+ const access = getInterfaceAllowAccess(interfaceNode);
318
+ return access.some((a) => a.toLowerCase() === 'http' || a.toLowerCase() === 'https');
319
+ };
320
+
321
+ /**
322
+ * Check if SSH is allowed on an interface
323
+ * @param interfaceNode The interface edit entry ConfigNode
324
+ * @returns true if SSH access is allowed
325
+ */
326
+ export const hasSshAccess = (interfaceNode: ConfigNode): boolean => {
327
+ const access = getInterfaceAllowAccess(interfaceNode);
328
+ return access.some((a) => a.toLowerCase() === 'ssh');
329
+ };
330
+
331
+ /**
332
+ * Check if Telnet is allowed on an interface (insecure)
333
+ * @param interfaceNode The interface edit entry ConfigNode
334
+ * @returns true if Telnet access is allowed
335
+ */
336
+ export const hasTelnetAccess = (interfaceNode: ConfigNode): boolean => {
337
+ const access = getInterfaceAllowAccess(interfaceNode);
338
+ return access.some((a) => a.toLowerCase() === 'telnet');
339
+ };
340
+
341
+ /**
342
+ * Get the schedule for a firewall policy
343
+ * @param policyNode The firewall policy ConfigNode
344
+ * @returns The schedule name
345
+ */
346
+ export const getPolicySchedule = (policyNode: ConfigNode): string | undefined => {
347
+ return getSetValue(policyNode, 'schedule');
348
+ };
349
+
350
+ /**
351
+ * Check if the schedule is "always" (always active)
352
+ * @param policyNode The firewall policy ConfigNode
353
+ * @returns true if schedule is "always"
354
+ */
355
+ export const isAlwaysSchedule = (policyNode: ConfigNode): boolean => {
356
+ const schedule = getPolicySchedule(policyNode);
357
+ return schedule?.toLowerCase() === 'always';
358
+ };
359
+
360
+ /**
361
+ * Get NAT settings for a policy
362
+ * @param policyNode The firewall policy ConfigNode
363
+ * @returns Object with NAT settings
364
+ */
365
+ export const getNatSettings = (policyNode: ConfigNode): {
366
+ nat: boolean;
367
+ ippool: boolean;
368
+ poolname: string[];
369
+ } => {
370
+ const nat = getSetValue(policyNode, 'nat');
371
+ const ippool = getSetValue(policyNode, 'ippool');
372
+ const poolname = getSetValues(policyNode, 'poolname');
373
+ return {
374
+ nat: nat?.toLowerCase() === 'enable',
375
+ ippool: ippool?.toLowerCase() === 'enable',
376
+ poolname,
377
+ };
378
+ };
379
+
380
+ /**
381
+ * Check if HA (High Availability) is configured
382
+ * @param systemHaNode The system ha config section
383
+ * @returns true if HA is enabled
384
+ */
385
+ export const isHAEnabled = (systemHaNode: ConfigNode): boolean => {
386
+ const mode = getSetValue(systemHaNode, 'mode');
387
+ return mode !== undefined && mode.toLowerCase() !== 'standalone';
388
+ };
389
+
390
+ /**
391
+ * Get the HA mode
392
+ * @param systemHaNode The system ha config section
393
+ * @returns The HA mode (standalone, a-a, a-p, etc.)
394
+ */
395
+ export const getHAMode = (systemHaNode: ConfigNode): string | undefined => {
396
+ return getSetValue(systemHaNode, 'mode');
397
+ };
398
+
399
+ /**
400
+ * Check if admin user has strong password policy
401
+ * @param adminNode The admin user edit entry
402
+ * @returns Object with password policy info
403
+ */
404
+ export const getAdminPasswordPolicy = (adminNode: ConfigNode): {
405
+ forcePasswordChange: boolean;
406
+ twoFactorAuth: string | undefined;
407
+ } => {
408
+ const forcePasswordChange = getSetValue(adminNode, 'force-password-change');
409
+ const twoFactorAuth = getSetValue(adminNode, 'two-factor');
410
+ return {
411
+ forcePasswordChange: forcePasswordChange?.toLowerCase() === 'enable',
412
+ twoFactorAuth,
413
+ };
414
+ };
415
+
416
+ /**
417
+ * Get the admin profile (permission level) for an admin user
418
+ * @param adminNode The admin user edit entry
419
+ * @returns The profile name
420
+ */
421
+ export const getAdminProfile = (adminNode: ConfigNode): string | undefined => {
422
+ return getSetValue(adminNode, 'accprofile');
423
+ };
424
+
425
+ /**
426
+ * Check if admin is a super_admin
427
+ * @param adminNode The admin user edit entry
428
+ * @returns true if super_admin profile
429
+ */
430
+ export const isSuperAdmin = (adminNode: ConfigNode): boolean => {
431
+ const profile = getAdminProfile(adminNode);
432
+ return profile?.toLowerCase() === 'super_admin';
433
+ };
434
+
435
+ /**
436
+ * Get trusted hosts for admin access restriction
437
+ * @param adminNode The admin user edit entry
438
+ * @returns Array of trusted host entries
439
+ */
440
+ export const getAdminTrustedHosts = (adminNode: ConfigNode): string[] => {
441
+ const trustedHosts: string[] = [];
442
+ // FortiOS uses trusthost1, trusthost2, ... trusthost10
443
+ for (let i = 1; i <= 10; i++) {
444
+ const host = getSetValue(adminNode, `trusthost${i}`);
445
+ if (host && host !== '0.0.0.0 0.0.0.0') {
446
+ trustedHosts.push(host);
447
+ }
448
+ }
449
+ return trustedHosts;
450
+ };
451
+
452
+ /**
453
+ * Check if admin has any trusted host restriction
454
+ * @param adminNode The admin user edit entry
455
+ * @returns true if trusted hosts are configured
456
+ */
457
+ export const hasAdminTrustedHosts = (adminNode: ConfigNode): boolean => {
458
+ return getAdminTrustedHosts(adminNode).length > 0;
459
+ };
460
+
461
+ /**
462
+ * Parse FortiOS IP address format (e.g., "10.0.0.1 255.255.255.0" or "10.0.0.0/24")
463
+ * @param address The address string
464
+ * @returns Object with parsed address info, or null if invalid
465
+ */
466
+ export const parseFortiAddress = (
467
+ address: string
468
+ ): { ip: number; mask: string } | null => {
469
+ const parts = address.trim().split(/\s+/);
470
+
471
+ // IP + netmask format: "10.0.0.1 255.255.255.0"
472
+ if (parts.length === 2) {
473
+ const [ipStr, maskStr] = parts;
474
+ if (!ipStr || !maskStr) {
475
+ return null;
476
+ }
477
+ const ip = parseIp(ipStr);
478
+ if (ip === null) return null;
479
+ return { ip, mask: maskStr };
480
+ }
481
+
482
+ // CIDR format: "10.0.0.1/24"
483
+ const singlePart = parts[0];
484
+ if (parts.length === 1 && singlePart && singlePart.includes('/')) {
485
+ const [ipStr, prefix] = singlePart.split('/');
486
+ if (!ipStr || !prefix) {
487
+ return null;
488
+ }
489
+ const ip = parseIp(ipStr);
490
+ if (ip === null) return null;
491
+ return { ip, mask: `/${prefix}` };
492
+ }
493
+
494
+ // Single IP
495
+ if (parts.length === 1 && singlePart) {
496
+ const ip = parseIp(singlePart);
497
+ if (ip === null) return null;
498
+ return { ip, mask: '255.255.255.255' };
499
+ }
500
+
501
+ return null;
502
+ };
503
+
504
+ // ============================================================================
505
+ // System Hardening Helpers (FGT-HARD-*)
506
+ // ============================================================================
507
+
508
+ /**
509
+ * Check if USB auto-install is enabled (security risk)
510
+ * @param globalNode The system global config section
511
+ * @returns true if USB auto-install is enabled
512
+ */
513
+ export const isUsbAutoInstallEnabled = (globalNode: ConfigNode): boolean => {
514
+ const usbAutoInstall = getSetValue(globalNode, 'usb-auto-install');
515
+ return usbAutoInstall?.toLowerCase() === 'enable';
516
+ };
517
+
518
+ /**
519
+ * Check if admin-maintainer account is enabled
520
+ * @param globalNode The system global config section
521
+ * @returns true if maintainer account is enabled
522
+ */
523
+ export const isAdminMaintainerEnabled = (globalNode: ConfigNode): boolean => {
524
+ const maintainer = getSetValue(globalNode, 'admin-maintainer');
525
+ // Default is enable, so if not explicitly disabled, it's enabled
526
+ return maintainer?.toLowerCase() !== 'disable';
527
+ };
528
+
529
+ /**
530
+ * Check if private data encryption is enabled
531
+ * @param globalNode The system global config section
532
+ * @returns true if private data encryption is enabled
533
+ */
534
+ export const isPrivateDataEncryptionEnabled = (globalNode: ConfigNode): boolean => {
535
+ const encryption = getSetValue(globalNode, 'private-data-encryption');
536
+ return encryption?.toLowerCase() === 'enable';
537
+ };
538
+
539
+ /**
540
+ * Get admin lockout threshold
541
+ * @param globalNode The system global config section
542
+ * @returns The lockout threshold number, or undefined
543
+ */
544
+ export const getAdminLockoutThreshold = (globalNode: ConfigNode): number | undefined => {
545
+ const threshold = getSetValue(globalNode, 'admin-lockout-threshold');
546
+ return threshold ? parseInt(threshold, 10) : undefined;
547
+ };
548
+
549
+ /**
550
+ * Get admin lockout duration
551
+ * @param globalNode The system global config section
552
+ * @returns The lockout duration in seconds, or undefined
553
+ */
554
+ export const getAdminLockoutDuration = (globalNode: ConfigNode): number | undefined => {
555
+ const duration = getSetValue(globalNode, 'admin-lockout-duration');
556
+ return duration ? parseInt(duration, 10) : undefined;
557
+ };
558
+
559
+ // ============================================================================
560
+ // Password Policy Helpers (FGT-MGMT-004)
561
+ // ============================================================================
562
+
563
+ /**
564
+ * Get password policy settings
565
+ * @param passwordPolicyNode The system password-policy config section
566
+ * @returns Object with password policy settings
567
+ */
568
+ export const getPasswordPolicySettings = (passwordPolicyNode: ConfigNode): {
569
+ status: boolean;
570
+ minimumLength: number | undefined;
571
+ minLowerCase: number | undefined;
572
+ minUpperCase: number | undefined;
573
+ minNonAlphanumeric: number | undefined;
574
+ minNumber: number | undefined;
575
+ expireStatus: boolean;
576
+ expireDays: number | undefined;
577
+ reusePassword: boolean;
578
+ } => {
579
+ const status = getSetValue(passwordPolicyNode, 'status');
580
+ const minimumLength = getSetValue(passwordPolicyNode, 'minimum-length');
581
+ const minLowerCase = getSetValue(passwordPolicyNode, 'min-lower-case-letter');
582
+ const minUpperCase = getSetValue(passwordPolicyNode, 'min-upper-case-letter');
583
+ const minNonAlphanumeric = getSetValue(passwordPolicyNode, 'min-non-alphanumeric');
584
+ const minNumber = getSetValue(passwordPolicyNode, 'min-number');
585
+ const expireStatus = getSetValue(passwordPolicyNode, 'expire-status');
586
+ const expireDays = getSetValue(passwordPolicyNode, 'expire-day');
587
+ const reusePassword = getSetValue(passwordPolicyNode, 'reuse-password');
588
+
589
+ return {
590
+ status: status?.toLowerCase() === 'enable',
591
+ minimumLength: minimumLength ? parseInt(minimumLength, 10) : undefined,
592
+ minLowerCase: minLowerCase ? parseInt(minLowerCase, 10) : undefined,
593
+ minUpperCase: minUpperCase ? parseInt(minUpperCase, 10) : undefined,
594
+ minNonAlphanumeric: minNonAlphanumeric ? parseInt(minNonAlphanumeric, 10) : undefined,
595
+ minNumber: minNumber ? parseInt(minNumber, 10) : undefined,
596
+ expireStatus: expireStatus?.toLowerCase() === 'enable',
597
+ expireDays: expireDays ? parseInt(expireDays, 10) : undefined,
598
+ reusePassword: reusePassword?.toLowerCase() !== 'disable',
599
+ };
600
+ };
601
+
602
+ // ============================================================================
603
+ // SNMP Helpers (FGT-MGMT-009)
604
+ // ============================================================================
605
+
606
+ /**
607
+ * Check if SNMP community has default/weak name
608
+ * @param communityNode The SNMP community edit entry
609
+ * @returns true if the community name is weak/default
610
+ */
611
+ export const hasWeakSnmpCommunity = (communityNode: ConfigNode): boolean => {
612
+ const name = getSetValue(communityNode, 'name');
613
+ if (!name) return false;
614
+ const weakNames = ['public', 'private', 'community', 'snmp', 'default'];
615
+ return weakNames.includes(name.toLowerCase());
616
+ };
617
+
618
+ /**
619
+ * Get SNMP user security level
620
+ * @param snmpUserNode The SNMP user edit entry
621
+ * @returns The security level (no-auth-no-priv, auth-no-priv, auth-priv)
622
+ */
623
+ export const getSnmpSecurityLevel = (snmpUserNode: ConfigNode): string | undefined => {
624
+ return getSetValue(snmpUserNode, 'security-level');
625
+ };
626
+
627
+ // ============================================================================
628
+ // SSL/SSH Profile Helpers (FGT-SSL-*)
629
+ // ============================================================================
630
+
631
+ /**
632
+ * Get SSL inspection profile settings
633
+ * @param sslProfileNode The SSL-SSH profile edit entry
634
+ * @returns Object with SSL settings
635
+ */
636
+ export const getSslProfileSettings = (sslProfileNode: ConfigNode): {
637
+ minSslVersion: string | undefined;
638
+ unsupportedSslVersion: string | undefined;
639
+ expiredServerCert: string | undefined;
640
+ revokedServerCert: string | undefined;
641
+ untrustedServerCert: string | undefined;
642
+ certValidationFailure: string | undefined;
643
+ } => {
644
+ // Find the ssl config section within the profile
645
+ const sslSection = findConfigSection(sslProfileNode, 'ssl');
646
+ if (!sslSection) {
647
+ return {
648
+ minSslVersion: undefined,
649
+ unsupportedSslVersion: undefined,
650
+ expiredServerCert: undefined,
651
+ revokedServerCert: undefined,
652
+ untrustedServerCert: undefined,
653
+ certValidationFailure: undefined,
654
+ };
655
+ }
656
+
657
+ return {
658
+ minSslVersion: getSetValue(sslSection, 'min-allowed-ssl-version'),
659
+ unsupportedSslVersion: getSetValue(sslSection, 'unsupported-ssl-version'),
660
+ expiredServerCert: getSetValue(sslSection, 'expired-server-cert'),
661
+ revokedServerCert: getSetValue(sslSection, 'revoked-server-cert'),
662
+ untrustedServerCert: getSetValue(sslSection, 'untrusted-server-cert'),
663
+ certValidationFailure: getSetValue(sslSection, 'cert-validation-failure'),
664
+ };
665
+ };
666
+
667
+ /**
668
+ * Check if SSL profile uses weak SSL version
669
+ * @param minSslVersion The minimum SSL version string
670
+ * @returns true if the version is considered weak
671
+ */
672
+ export const isWeakSslVersion = (minSslVersion: string | undefined): boolean => {
673
+ if (!minSslVersion) return false;
674
+ const weakVersions = ['ssl-3.0', 'tls-1.0', 'tls-1.1'];
675
+ return weakVersions.includes(minSslVersion.toLowerCase());
676
+ };
677
+
678
+ // ============================================================================
679
+ // DoS Policy Helpers (FGT-DOS-*)
680
+ // ============================================================================
681
+
682
+ /**
683
+ * Get DoS anomaly settings from a DoS policy
684
+ * @param dosPolicyNode The DoS policy edit entry
685
+ * @returns Array of anomaly configurations
686
+ */
687
+ export const getDosAnomalySettings = (dosPolicyNode: ConfigNode): Array<{
688
+ name: string;
689
+ status: boolean;
690
+ action: string | undefined;
691
+ threshold: number | undefined;
692
+ log: boolean;
693
+ }> => {
694
+ const anomalySection = findConfigSection(dosPolicyNode, 'anomaly');
695
+ if (!anomalySection) return [];
696
+
697
+ const anomalies = getEditEntries(anomalySection);
698
+ return anomalies.map((anomaly) => {
699
+ const name = getEditEntryName(anomaly);
700
+ const status = getSetValue(anomaly, 'status');
701
+ const action = getSetValue(anomaly, 'action');
702
+ const threshold = getSetValue(anomaly, 'threshold');
703
+ const log = getSetValue(anomaly, 'log');
704
+
705
+ return {
706
+ name,
707
+ status: status?.toLowerCase() === 'enable',
708
+ action,
709
+ threshold: threshold ? parseInt(threshold, 10) : undefined,
710
+ log: log?.toLowerCase() === 'enable',
711
+ };
712
+ });
713
+ };
714
+
715
+ // ============================================================================
716
+ // SD-WAN Helpers (FGT-SDW-*)
717
+ // ============================================================================
718
+
719
+ /**
720
+ * Check if SD-WAN is enabled
721
+ * @param sdwanNode The system sdwan config section
722
+ * @returns true if SD-WAN is enabled
723
+ */
724
+ export const isSdwanEnabled = (sdwanNode: ConfigNode): boolean => {
725
+ const status = getSetValue(sdwanNode, 'status');
726
+ return status?.toLowerCase() === 'enable';
727
+ };
728
+
729
+ /**
730
+ * Get SD-WAN health check configurations
731
+ * @param sdwanNode The system sdwan config section
732
+ * @returns Array of health check names
733
+ */
734
+ export const getSdwanHealthChecks = (sdwanNode: ConfigNode): ConfigNode[] => {
735
+ const healthCheckSection = findConfigSection(sdwanNode, 'health-check');
736
+ if (!healthCheckSection) return [];
737
+ return getEditEntries(healthCheckSection);
738
+ };
739
+
740
+ /**
741
+ * Get SD-WAN members
742
+ * @param sdwanNode The system sdwan config section
743
+ * @returns Array of member configurations
744
+ */
745
+ export const getSdwanMembers = (sdwanNode: ConfigNode): ConfigNode[] => {
746
+ const membersSection = findConfigSection(sdwanNode, 'members');
747
+ if (!membersSection) return [];
748
+ return getEditEntries(membersSection);
749
+ };
750
+
751
+ // ============================================================================
752
+ // VPN Helpers (Extended for FGT-VPN-*)
753
+ // ============================================================================
754
+
755
+ /**
756
+ * Get IKE version from IPsec phase1
757
+ * @param phase1Node The IPsec phase1-interface edit entry
758
+ * @returns The IKE version (1 or 2), or undefined
759
+ */
760
+ export const getIkeVersion = (phase1Node: ConfigNode): number | undefined => {
761
+ const version = getSetValue(phase1Node, 'ike-version');
762
+ return version ? parseInt(version, 10) : undefined;
763
+ };
764
+
765
+ /**
766
+ * Get DH groups from IPsec configuration
767
+ * @param phaseNode The IPsec phase1 or phase2 edit entry
768
+ * @returns Array of DH group numbers
769
+ */
770
+ export const getDhGroups = (phaseNode: ConfigNode): number[] => {
771
+ const dhgrp = getSetValues(phaseNode, 'dhgrp');
772
+ return dhgrp.map((g) => parseInt(g, 10)).filter((n) => !isNaN(n));
773
+ };
774
+
775
+ /**
776
+ * Check if weak DH groups are used
777
+ * @param dhGroups Array of DH group numbers
778
+ * @returns true if any weak DH group is found
779
+ */
780
+ export const hasWeakDhGroup = (dhGroups: number[]): boolean => {
781
+ const weakGroups = [1, 2, 5]; // DH groups 1, 2, 5 are considered weak
782
+ return dhGroups.some((g) => weakGroups.includes(g));
783
+ };
784
+
785
+ /**
786
+ * Check if PFS is enabled in phase2
787
+ * @param phase2Node The IPsec phase2-interface edit entry
788
+ * @returns true if PFS is enabled
789
+ */
790
+ export const isPfsEnabled = (phase2Node: ConfigNode): boolean => {
791
+ const pfs = getSetValue(phase2Node, 'pfs');
792
+ return pfs?.toLowerCase() === 'enable';
793
+ };
794
+
795
+ /**
796
+ * Get key lifetime from IPsec phase2
797
+ * @param phase2Node The IPsec phase2-interface edit entry
798
+ * @returns Key lifetime in seconds, or undefined
799
+ */
800
+ export const getKeyLifetime = (phase2Node: ConfigNode): number | undefined => {
801
+ const lifetime = getSetValue(phase2Node, 'keylifeseconds');
802
+ return lifetime ? parseInt(lifetime, 10) : undefined;
803
+ };
804
+
805
+ // ============================================================================
806
+ // SSL VPN Helpers (FGT-VPN-006)
807
+ // ============================================================================
808
+
809
+ /**
810
+ * Get SSL VPN settings
811
+ * @param sslSettingsNode The vpn ssl settings config section
812
+ * @returns Object with SSL VPN settings
813
+ */
814
+ export const getSslVpnSettings = (sslSettingsNode: ConfigNode): {
815
+ sslMinProtoVer: string | undefined;
816
+ sslMaxProtoVer: string | undefined;
817
+ idleTimeout: number | undefined;
818
+ authTimeout: number | undefined;
819
+ loginAttemptLimit: number | undefined;
820
+ loginBlockTime: number | undefined;
821
+ reqClientCert: boolean;
822
+ checkReferer: boolean;
823
+ } => {
824
+ return {
825
+ sslMinProtoVer: getSetValue(sslSettingsNode, 'ssl-min-proto-ver'),
826
+ sslMaxProtoVer: getSetValue(sslSettingsNode, 'ssl-max-proto-ver'),
827
+ idleTimeout: parseInt(getSetValue(sslSettingsNode, 'idle-timeout') || '0', 10) || undefined,
828
+ authTimeout: parseInt(getSetValue(sslSettingsNode, 'auth-timeout') || '0', 10) || undefined,
829
+ loginAttemptLimit: parseInt(getSetValue(sslSettingsNode, 'login-attempt-limit') || '0', 10) || undefined,
830
+ loginBlockTime: parseInt(getSetValue(sslSettingsNode, 'login-block-time') || '0', 10) || undefined,
831
+ reqClientCert: getSetValue(sslSettingsNode, 'reqclientcert')?.toLowerCase() === 'enable',
832
+ checkReferer: getSetValue(sslSettingsNode, 'check-referer')?.toLowerCase() === 'enable',
833
+ };
834
+ };
835
+
836
+ // ============================================================================
837
+ // Admin 2FA Helpers (FGT-MGMT-006)
838
+ // ============================================================================
839
+
840
+ /**
841
+ * Check if admin has two-factor authentication enabled
842
+ * @param adminNode The admin user edit entry
843
+ * @returns true if 2FA is enabled
844
+ */
845
+ export const hasAdmin2FA = (adminNode: ConfigNode): boolean => {
846
+ const twoFactor = getSetValue(adminNode, 'two-factor');
847
+ return twoFactor !== undefined && twoFactor.toLowerCase() !== 'disable';
848
+ };
849
+
850
+ /**
851
+ * Get admin two-factor authentication type
852
+ * @param adminNode The admin user edit entry
853
+ * @returns The 2FA type (fortitoken, email, sms, etc.) or undefined
854
+ */
855
+ export const getAdmin2FAType = (adminNode: ConfigNode): string | undefined => {
856
+ return getSetValue(adminNode, 'two-factor');
857
+ };
858
+
859
+ // ============================================================================
860
+ // Interface Role Helpers (FGT-NET-003)
861
+ // ============================================================================
862
+
863
+ /**
864
+ * Get interface role
865
+ * @param interfaceNode The interface edit entry
866
+ * @returns The interface role (wan, lan, dmz, undefined)
867
+ */
868
+ export const getInterfaceRole = (interfaceNode: ConfigNode): string | undefined => {
869
+ return getSetValue(interfaceNode, 'role');
870
+ };
871
+
872
+ /**
873
+ * Check if interface is WAN-facing
874
+ * @param interfaceNode The interface edit entry
875
+ * @returns true if interface has WAN role
876
+ */
877
+ export const isWanInterface = (interfaceNode: ConfigNode): boolean => {
878
+ const role = getInterfaceRole(interfaceNode);
879
+ return role?.toLowerCase() === 'wan';
880
+ };
881
+
882
+ /**
883
+ * Check if interface has management access on WAN
884
+ * @param interfaceNode The interface edit entry
885
+ * @returns true if WAN interface has management protocols enabled
886
+ */
887
+ export const hasWanManagementAccess = (interfaceNode: ConfigNode): boolean => {
888
+ if (!isWanInterface(interfaceNode)) return false;
889
+ const access = getInterfaceAllowAccess(interfaceNode);
890
+ const mgmtProtocols = ['https', 'http', 'ssh', 'telnet', 'snmp'];
891
+ return access.some((a) => mgmtProtocols.includes(a.toLowerCase()));
892
+ };