@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,12 @@
1
+ // packages/rule-helpers/src/arista/index.ts
2
+ // Re-export all Arista EOS helpers
3
+
4
+ export * from './helpers';
5
+
6
+ // Also re-export commonly used common helpers for convenience
7
+ export {
8
+ hasChildCommand,
9
+ getChildCommand,
10
+ getChildCommands,
11
+ parseIp,
12
+ } from '../common/helpers';
@@ -0,0 +1,637 @@
1
+ // packages/rule-helpers/src/aruba/helpers.ts
2
+ // Aruba-specific helper functions used across Aruba rules
3
+
4
+ import type { ConfigNode } from '../../types/ConfigNode';
5
+ import { hasChildCommand, getChildCommand, getChildCommands } from '../common/helpers';
6
+
7
+ /**
8
+ * Extract the interface name from an interface stanza id.
9
+ * @param node The interface ConfigNode
10
+ * @returns The interface identifier without the leading keyword
11
+ */
12
+ export const getInterfaceName = (node: ConfigNode): string | undefined => {
13
+ const match = node.id.match(/interface\s+(.+)/i);
14
+ const ifName = match?.[1]?.trim();
15
+ return ifName && ifName.length > 0 ? ifName : undefined;
16
+ };
17
+
18
+ // =============================================================================
19
+ // AOS-CX Helpers
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Check if an AOS-CX interface is a physical port (slot/member/port format).
24
+ * @param interfaceName The interface identifier
25
+ * @returns true if it's a physical port (e.g., 1/1/1)
26
+ */
27
+ export const isAosCxPhysicalPort = (interfaceName: string): boolean => {
28
+ return /^\d+\/\d+\/\d+$/.test(interfaceName.trim());
29
+ };
30
+
31
+ /**
32
+ * Check if an AOS-CX interface is a LAG.
33
+ * @param interfaceName The interface identifier
34
+ * @returns true if it's a LAG interface
35
+ */
36
+ export const isAosCxLag = (interfaceName: string): boolean => {
37
+ return /^lag\s*\d+$/i.test(interfaceName.trim());
38
+ };
39
+
40
+ /**
41
+ * Check if an AOS-CX interface is a VLAN interface.
42
+ * @param interfaceName The interface identifier
43
+ * @returns true if it's a VLAN interface
44
+ */
45
+ export const isAosCxVlanInterface = (interfaceName: string): boolean => {
46
+ return /^vlan\s*\d+$/i.test(interfaceName.trim());
47
+ };
48
+
49
+ /**
50
+ * Check if an AOS-CX interface is configured as trunk mode.
51
+ * @param node The interface ConfigNode
52
+ * @returns true if the interface has trunk VLAN configuration
53
+ */
54
+ export const isAosCxTrunk = (node: ConfigNode): boolean => {
55
+ return hasChildCommand(node, 'vlan trunk');
56
+ };
57
+
58
+ /**
59
+ * Check if an AOS-CX interface is configured as access mode.
60
+ * @param node The interface ConfigNode
61
+ * @returns true if the interface has access VLAN configuration
62
+ */
63
+ export const isAosCxAccess = (node: ConfigNode): boolean => {
64
+ return hasChildCommand(node, 'vlan access');
65
+ };
66
+
67
+ /**
68
+ * Get the access VLAN ID from an AOS-CX interface.
69
+ * @param node The interface ConfigNode
70
+ * @returns The VLAN ID, or null if not configured
71
+ */
72
+ export const getAosCxVlanAccess = (node: ConfigNode): number | null => {
73
+ const cmd = getChildCommand(node, 'vlan access');
74
+ if (!cmd) return null;
75
+ const match = cmd.id.match(/vlan\s+access\s+(\d+)/i);
76
+ const vlanId = match?.[1];
77
+ if (!vlanId) {
78
+ return null;
79
+ }
80
+ return parseInt(vlanId, 10);
81
+ };
82
+
83
+ /**
84
+ * Get the native VLAN ID from an AOS-CX trunk interface.
85
+ * @param node The interface ConfigNode
86
+ * @returns The native VLAN ID, or null if not configured
87
+ */
88
+ export const getAosCxTrunkNative = (node: ConfigNode): number | null => {
89
+ const cmd = getChildCommand(node, 'vlan trunk native');
90
+ if (!cmd) return null;
91
+ const match = cmd.id.match(/vlan\s+trunk\s+native\s+(\d+)/i);
92
+ const vlanId = match?.[1];
93
+ if (!vlanId) {
94
+ return null;
95
+ }
96
+ return parseInt(vlanId, 10);
97
+ };
98
+
99
+ /**
100
+ * Get allowed VLANs from an AOS-CX trunk interface.
101
+ * @param node The interface ConfigNode
102
+ * @returns Array of allowed VLAN IDs, or empty array if not configured
103
+ */
104
+ export const getAosCxTrunkAllowed = (node: ConfigNode): number[] => {
105
+ const cmd = getChildCommand(node, 'vlan trunk allowed');
106
+ if (!cmd) return [];
107
+ const match = cmd.id.match(/vlan\s+trunk\s+allowed\s+([\d,]+)/i);
108
+ const vlanList = match?.[1];
109
+ if (!vlanList) return [];
110
+ return vlanList
111
+ .split(',')
112
+ .map((v) => parseInt(v.trim(), 10))
113
+ .filter((v) => !isNaN(v));
114
+ };
115
+
116
+ /**
117
+ * Check if an AOS-CX interface has BPDU guard enabled.
118
+ * @param node The interface ConfigNode
119
+ * @returns true if BPDU guard is configured
120
+ */
121
+ export const hasAosCxBpduGuard = (node: ConfigNode): boolean => {
122
+ return hasChildCommand(node, 'spanning-tree bpdu-guard');
123
+ };
124
+
125
+ /**
126
+ * Check if an AOS-CX interface is an admin-edge port.
127
+ * @param node The interface ConfigNode
128
+ * @returns true if admin-edge is configured
129
+ */
130
+ export const isAosCxEdgePort = (node: ConfigNode): boolean => {
131
+ return hasChildCommand(node, 'spanning-tree port-type admin-edge');
132
+ };
133
+
134
+ /**
135
+ * Check if an AOS-CX interface has root-guard enabled.
136
+ * @param node The interface ConfigNode
137
+ * @returns true if root-guard is configured
138
+ */
139
+ export const hasAosCxRootGuard = (node: ConfigNode): boolean => {
140
+ return hasChildCommand(node, 'spanning-tree root-guard');
141
+ };
142
+
143
+ /**
144
+ * Check if an AOS-CX interface has loop-protect enabled.
145
+ * @param node The interface ConfigNode
146
+ * @returns true if loop-protect is configured
147
+ */
148
+ export const hasAosCxLoopProtect = (node: ConfigNode): boolean => {
149
+ return hasChildCommand(node, 'loop-protect');
150
+ };
151
+
152
+ /**
153
+ * Check if an AOS-CX interface has storm-control configured.
154
+ * @param node The interface ConfigNode
155
+ * @returns true if any storm-control setting is configured
156
+ */
157
+ export const hasAosCxStormControl = (node: ConfigNode): boolean => {
158
+ return hasChildCommand(node, 'storm-control');
159
+ };
160
+
161
+ /**
162
+ * Check if an AOS-CX interface has DHCP snooping trust configured.
163
+ * @param node The interface ConfigNode
164
+ * @returns true if dhcp-snooping trust is configured
165
+ */
166
+ export const hasAosCxDhcpSnooping = (node: ConfigNode): boolean => {
167
+ return hasChildCommand(node, 'dhcp-snooping');
168
+ };
169
+
170
+ /**
171
+ * Check if an AOS-CX interface has ARP inspection trust configured.
172
+ * @param node The interface ConfigNode
173
+ * @returns true if ip arp inspection trust is configured
174
+ */
175
+ export const hasAosCxArpInspection = (node: ConfigNode): boolean => {
176
+ return hasChildCommand(node, 'ip arp inspection');
177
+ };
178
+
179
+ /**
180
+ * Check if an AOS-CX interface has IP source guard (source-binding) configured.
181
+ * @param node The interface ConfigNode
182
+ * @returns true if ip source-binding is configured
183
+ */
184
+ export const hasAosCxIpSourceGuard = (node: ConfigNode): boolean => {
185
+ return hasChildCommand(node, 'ip source-binding');
186
+ };
187
+
188
+ /**
189
+ * Check if an AOS-CX interface has port security configured.
190
+ * @param node The interface ConfigNode
191
+ * @returns true if port-access port-security is configured
192
+ */
193
+ export const hasAosCxPortSecurity = (node: ConfigNode): boolean => {
194
+ return hasChildCommand(node, 'port-access port-security');
195
+ };
196
+
197
+ /**
198
+ * Check if an AOS-CX interface has 802.1X authenticator configured.
199
+ * @param node The interface ConfigNode
200
+ * @returns true if dot1x authenticator is configured
201
+ */
202
+ export const hasAosCxDot1x = (node: ConfigNode): boolean => {
203
+ return hasChildCommand(node, 'aaa authentication port-access dot1x');
204
+ };
205
+
206
+ /**
207
+ * Check if an AOS-CX interface has MAC authentication configured.
208
+ * @param node The interface ConfigNode
209
+ * @returns true if mac-auth is configured
210
+ */
211
+ export const hasAosCxMacAuth = (node: ConfigNode): boolean => {
212
+ return hasChildCommand(node, 'aaa authentication port-access mac-auth');
213
+ };
214
+
215
+ /**
216
+ * Get MSTP region name from global config.
217
+ * @param node The spanning-tree config-name node
218
+ * @returns The region name, or undefined
219
+ */
220
+ export const getAosCxMstpRegionName = (node: ConfigNode): string | undefined => {
221
+ const match = node.id.match(/spanning-tree\s+config-name\s+(\S+)/i);
222
+ return match?.[1];
223
+ };
224
+
225
+ /**
226
+ * Check if an AOS-CX interface has MACsec configured.
227
+ * @param node The interface ConfigNode
228
+ * @returns true if MACsec policy is applied
229
+ */
230
+ export const hasAosCxMacsec = (node: ConfigNode): boolean => {
231
+ return hasChildCommand(node, 'apply macsec policy') || hasChildCommand(node, 'apply mka policy');
232
+ };
233
+
234
+ // =============================================================================
235
+ // AOS-Switch Helpers
236
+ // =============================================================================
237
+
238
+ /**
239
+ * Parse port range string to array of port numbers.
240
+ * Handles formats like "1-24", "25,26,27", "1-24,48"
241
+ * @param portStr The port range string
242
+ * @returns Array of individual port numbers
243
+ */
244
+ export const parsePortRange = (portStr: string): number[] => {
245
+ const ports: number[] = [];
246
+ const parts = portStr.split(',');
247
+
248
+ for (const part of parts) {
249
+ const trimmed = part.trim();
250
+ if (trimmed.includes('-')) {
251
+ const [startRaw, endRaw] = trimmed
252
+ .split('-')
253
+ .map((n) => parseInt(n.trim(), 10));
254
+ if (
255
+ startRaw === undefined ||
256
+ endRaw === undefined ||
257
+ isNaN(startRaw) ||
258
+ isNaN(endRaw)
259
+ ) {
260
+ continue;
261
+ }
262
+ for (let i = startRaw; i <= endRaw; i++) {
263
+ ports.push(i);
264
+ }
265
+ } else {
266
+ const num = parseInt(trimmed, 10);
267
+ if (!isNaN(num)) {
268
+ ports.push(num);
269
+ }
270
+ }
271
+ }
272
+
273
+ return ports;
274
+ };
275
+
276
+ /**
277
+ * Get tagged ports from an AOS-Switch VLAN node.
278
+ * @param node The VLAN ConfigNode
279
+ * @returns Array of tagged port numbers
280
+ */
281
+ export const getVlanTaggedPorts = (node: ConfigNode): (number | string)[] => {
282
+ const cmd = getChildCommand(node, 'tagged');
283
+ if (!cmd) return [];
284
+ const match = cmd.id.match(/tagged\s+(.*)/i);
285
+ const taggedList = match?.[1];
286
+ if (!taggedList) return [];
287
+
288
+ const result: (number | string)[] = [];
289
+ const parts = taggedList.split(',');
290
+
291
+ for (const part of parts) {
292
+ const trimmed = part.trim();
293
+ if (/^trk\d+$/i.test(trimmed)) {
294
+ result.push(trimmed.toLowerCase());
295
+ } else if (trimmed.includes('-')) {
296
+ result.push(...parsePortRange(trimmed));
297
+ } else {
298
+ const num = parseInt(trimmed, 10);
299
+ if (!isNaN(num)) {
300
+ result.push(num);
301
+ }
302
+ }
303
+ }
304
+
305
+ return result;
306
+ };
307
+
308
+ /**
309
+ * Get untagged ports from an AOS-Switch VLAN node.
310
+ * @param node The VLAN ConfigNode
311
+ * @returns Array of untagged port numbers
312
+ */
313
+ export const getVlanUntaggedPorts = (node: ConfigNode): (number | string)[] => {
314
+ const cmd = getChildCommand(node, 'untagged');
315
+ if (!cmd) return [];
316
+ const match = cmd.id.match(/untagged\s+(.*)/i);
317
+ const untaggedList = match?.[1];
318
+ if (!untaggedList) return [];
319
+
320
+ const result: (number | string)[] = [];
321
+ const parts = untaggedList.split(',');
322
+
323
+ for (const part of parts) {
324
+ const trimmed = part.trim();
325
+ if (/^trk\d+$/i.test(trimmed)) {
326
+ result.push(trimmed.toLowerCase());
327
+ } else if (trimmed.includes('-')) {
328
+ result.push(...parsePortRange(trimmed));
329
+ } else {
330
+ const num = parseInt(trimmed, 10);
331
+ if (!isNaN(num)) {
332
+ result.push(num);
333
+ }
334
+ }
335
+ }
336
+
337
+ return result;
338
+ };
339
+
340
+ /**
341
+ * Get the VLAN name from an AOS-Switch VLAN node.
342
+ * @param node The VLAN ConfigNode
343
+ * @returns The VLAN name, or undefined if not set
344
+ */
345
+ export const getAosSwitchVlanName = (node: ConfigNode): string | undefined => {
346
+ const cmd = getChildCommand(node, 'name');
347
+ if (!cmd) return undefined;
348
+ const match = cmd.id.match(/name\s+["']?([^"']+)["']?/i);
349
+ const name = match?.[1];
350
+ return name?.trim();
351
+ };
352
+
353
+ /**
354
+ * Check if AOS-Switch has manager password configured.
355
+ * @param nodes Array of top-level ConfigNodes (AST children)
356
+ * @returns true if manager password is configured
357
+ */
358
+ export const hasManagerPassword = (nodes: ConfigNode[]): boolean => {
359
+ return nodes.some((n) => n.id.toLowerCase().startsWith('password manager'));
360
+ };
361
+
362
+ /**
363
+ * Check if AOS-Switch has operator password configured.
364
+ * @param nodes Array of top-level ConfigNodes (AST children)
365
+ * @returns true if operator password is configured
366
+ */
367
+ export const hasOperatorPassword = (nodes: ConfigNode[]): boolean => {
368
+ return nodes.some((n) => n.id.toLowerCase().startsWith('password operator'));
369
+ };
370
+
371
+ // =============================================================================
372
+ // WLC Helpers
373
+ // =============================================================================
374
+
375
+ /**
376
+ * Get the WLAN encryption mode from an SSID profile.
377
+ * @param node The SSID profile ConfigNode
378
+ * @returns The opmode value (e.g., 'wpa3-sae-aes', 'wpa2-aes', 'opensystem'), or null
379
+ */
380
+ export const getWlanEncryption = (node: ConfigNode): string | null => {
381
+ const cmd = getChildCommand(node, 'opmode');
382
+ if (!cmd) return null;
383
+ const match = cmd.id.match(/opmode\s+(\S+)/i);
384
+ const mode = match?.[1];
385
+ return mode ? mode.toLowerCase() : null;
386
+ };
387
+
388
+ /**
389
+ * Check if a WLAN SSID profile has secure encryption (WPA2/WPA3).
390
+ * @param node The SSID profile ConfigNode
391
+ * @returns true if encryption is WPA2 or WPA3
392
+ */
393
+ export const hasSecureEncryption = (node: ConfigNode): boolean => {
394
+ const opmode = getWlanEncryption(node);
395
+ if (!opmode) return false;
396
+ return opmode.includes('wpa2') || opmode.includes('wpa3') || opmode.includes('aes');
397
+ };
398
+
399
+ /**
400
+ * Check if a WLAN SSID profile is open (no encryption).
401
+ * @param node The SSID profile ConfigNode
402
+ * @returns true if the SSID is open/unencrypted
403
+ */
404
+ export const isOpenSsid = (node: ConfigNode): boolean => {
405
+ const opmode = getWlanEncryption(node);
406
+ return opmode === 'opensystem' || opmode === 'open';
407
+ };
408
+
409
+ /**
410
+ * Get the ESSID from a WLAN SSID profile.
411
+ * @param node The SSID profile ConfigNode
412
+ * @returns The ESSID value, or undefined
413
+ */
414
+ export const getEssid = (node: ConfigNode): string | undefined => {
415
+ const cmd = getChildCommand(node, 'essid');
416
+ if (!cmd) return undefined;
417
+ const match = cmd.id.match(/essid\s+["']?([^"'\n]+)["']?/i);
418
+ const essid = match?.[1];
419
+ return essid?.trim();
420
+ };
421
+
422
+ /**
423
+ * Get the AAA profile reference from a virtual-AP profile.
424
+ * @param node The virtual-AP ConfigNode
425
+ * @returns The AAA profile name, or undefined
426
+ */
427
+ export const getVapAaaProfile = (node: ConfigNode): string | undefined => {
428
+ const cmd = getChildCommand(node, 'aaa-profile');
429
+ if (!cmd) return undefined;
430
+ const match = cmd.id.match(/aaa-profile\s+["']?([^"'\n]+)["']?/i);
431
+ const profile = match?.[1];
432
+ return profile?.trim();
433
+ };
434
+
435
+ /**
436
+ * Get the SSID profile reference from a virtual-AP profile.
437
+ * @param node The virtual-AP ConfigNode
438
+ * @returns The SSID profile name, or undefined
439
+ */
440
+ export const getVapSsidProfile = (node: ConfigNode): string | undefined => {
441
+ const cmd = getChildCommand(node, 'ssid-profile');
442
+ if (!cmd) return undefined;
443
+ const match = cmd.id.match(/ssid-profile\s+["']?([^"'\n]+)["']?/i);
444
+ const profile = match?.[1];
445
+ return profile?.trim();
446
+ };
447
+
448
+ /**
449
+ * Get virtual-APs from an AP group.
450
+ * @param node The AP group ConfigNode
451
+ * @returns Array of virtual-AP names
452
+ */
453
+ export const getApGroupVirtualAps = (node: ConfigNode): string[] => {
454
+ const vaps: string[] = [];
455
+ for (const child of node.children) {
456
+ const match = child.id.match(/virtual-ap\s+["']?([^"'\n]+)["']?/i);
457
+ const vapName = match?.[1];
458
+ if (vapName) {
459
+ vaps.push(vapName);
460
+ }
461
+ }
462
+ return vaps;
463
+ };
464
+
465
+ /**
466
+ * Check if RADIUS server has a key configured.
467
+ * @param node The RADIUS server ConfigNode
468
+ * @returns true if a key is configured
469
+ */
470
+ export const hasRadiusKey = (node: ConfigNode): boolean => {
471
+ return hasChildCommand(node, 'key');
472
+ };
473
+
474
+ /**
475
+ * Get the RADIUS server host address.
476
+ * @param node The RADIUS server ConfigNode
477
+ * @returns The host IP/hostname, or undefined
478
+ */
479
+ export const getRadiusHost = (node: ConfigNode): string | undefined => {
480
+ const cmd = getChildCommand(node, 'host');
481
+ if (!cmd) return undefined;
482
+ const match = cmd.id.match(/host\s+(\S+)/i);
483
+ const host = match?.[1];
484
+ return host?.trim();
485
+ };
486
+
487
+ /**
488
+ * Extract profile name from a profile definition node.
489
+ * Handles both quoted and unquoted names.
490
+ * @param nodeId The node identifier string
491
+ * @returns The profile name, or undefined
492
+ */
493
+ export const extractProfileName = (nodeId: string): string | undefined => {
494
+ // Match patterns like: wlan ssid-profile "Name" or aaa profile "Name"
495
+ const match = nodeId.match(/(?:ssid-profile|virtual-ap|profile|server-group|ap-group|arm-profile)\s+["']?([^"'\n]+)["']?$/i);
496
+ const profile = match?.[1];
497
+ return profile ? profile.trim() : undefined;
498
+ };
499
+
500
+ /**
501
+ * Check if a WLAN SSID profile uses WPA3.
502
+ * @param node The SSID profile ConfigNode
503
+ * @returns true if encryption is WPA3
504
+ */
505
+ export const hasWpa3Encryption = (node: ConfigNode): boolean => {
506
+ const opmode = getWlanEncryption(node);
507
+ if (!opmode) return false;
508
+ return opmode.includes('wpa3');
509
+ };
510
+
511
+ /**
512
+ * Check if a WLAN SSID profile uses WPA3-Enterprise.
513
+ * @param node The SSID profile ConfigNode
514
+ * @returns true if encryption is WPA3-Enterprise
515
+ */
516
+ export const hasWpa3Enterprise = (node: ConfigNode): boolean => {
517
+ const opmode = getWlanEncryption(node);
518
+ if (!opmode) return false;
519
+ return opmode.includes('wpa3') && !opmode.includes('sae');
520
+ };
521
+
522
+ /**
523
+ * Check if a WLAN SSID profile uses WPA3-SAE (Personal).
524
+ * @param node The SSID profile ConfigNode
525
+ * @returns true if encryption is WPA3-SAE
526
+ */
527
+ export const hasWpa3Sae = (node: ConfigNode): boolean => {
528
+ const opmode = getWlanEncryption(node);
529
+ if (!opmode) return false;
530
+ return opmode.includes('wpa3') && opmode.includes('sae');
531
+ };
532
+
533
+ /**
534
+ * Check if Protected Management Frames (PMF/802.11w) is enabled.
535
+ * @param node The SSID profile ConfigNode
536
+ * @returns 'required' | 'optional' | null
537
+ */
538
+ export const getPmfMode = (node: ConfigNode): 'required' | 'optional' | null => {
539
+ const cmd = getChildCommand(node, 'mgmt-frame-protection');
540
+ if (!cmd) return null;
541
+ const id = cmd.id.toLowerCase();
542
+ if (id.includes('required')) return 'required';
543
+ if (id.includes('optional')) return 'optional';
544
+ return null;
545
+ };
546
+
547
+ /**
548
+ * Check if SSID profile is configured for 6 GHz band.
549
+ * @param node The SSID profile ConfigNode
550
+ * @returns true if 6ghz band is configured
551
+ */
552
+ export const is6GhzSsid = (node: ConfigNode): boolean => {
553
+ const cmd = getChildCommand(node, 'band');
554
+ if (!cmd) return false;
555
+ return cmd.id.toLowerCase().includes('6ghz');
556
+ };
557
+
558
+ /**
559
+ * Get max clients limit from SSID profile.
560
+ * @param node The SSID profile ConfigNode
561
+ * @returns The max clients value, or null
562
+ */
563
+ export const getMaxClients = (node: ConfigNode): number | null => {
564
+ const cmd = getChildCommand(node, 'max-clients');
565
+ if (!cmd) return null;
566
+ const match = cmd.id.match(/max-clients\s+(\d+)/i);
567
+ return match?.[1] ? parseInt(match[1], 10) : null;
568
+ };
569
+
570
+ /**
571
+ * Check if CPsec (Control Plane Security) is enabled on WLC.
572
+ * @param node The control-plane-security ConfigNode
573
+ * @returns true if cpsec is enabled
574
+ */
575
+ export const hasCpsecEnabled = (node: ConfigNode): boolean => {
576
+ return !node.id.toLowerCase().includes('disable');
577
+ };
578
+
579
+ /**
580
+ * Check if whitelist-db is enabled for AP authorization.
581
+ * @param node The cpsec ConfigNode
582
+ * @returns true if whitelist-db is enabled
583
+ */
584
+ export const hasWhitelistDb = (node: ConfigNode): boolean => {
585
+ return hasChildCommand(node, 'whitelist-db enable');
586
+ };
587
+
588
+ // =============================================================================
589
+ // Common Aruba Helpers
590
+ // =============================================================================
591
+
592
+ /**
593
+ * Find a child stanza by exact name match.
594
+ * @param node The parent ConfigNode
595
+ * @param stanzaName The stanza name to find
596
+ * @returns The matching child node, or undefined
597
+ */
598
+ export const findStanza = (
599
+ node: ConfigNode,
600
+ stanzaName: string
601
+ ): ConfigNode | undefined => {
602
+ return node.children.find(
603
+ (child) => child.id.toLowerCase() === stanzaName.toLowerCase()
604
+ );
605
+ };
606
+
607
+ /**
608
+ * Find all stanzas matching a pattern within a node's children.
609
+ * @param node The parent ConfigNode
610
+ * @param pattern The regex pattern to match
611
+ * @returns Array of matching child nodes
612
+ */
613
+ export const findStanzas = (node: ConfigNode, pattern: RegExp): ConfigNode[] => {
614
+ return node.children.filter((child) => pattern.test(child.id.toLowerCase()));
615
+ };
616
+
617
+ /**
618
+ * Check if an interface/node has a description configured.
619
+ * @param node The ConfigNode
620
+ * @returns true if a description command exists
621
+ */
622
+ export const hasDescription = (node: ConfigNode): boolean => {
623
+ return hasChildCommand(node, 'description');
624
+ };
625
+
626
+ /**
627
+ * Get the description from a node.
628
+ * @param node The ConfigNode
629
+ * @returns The description text, or undefined
630
+ */
631
+ export const getDescription = (node: ConfigNode): string | undefined => {
632
+ const cmd = getChildCommand(node, 'description');
633
+ if (!cmd) return undefined;
634
+ const match = cmd.id.match(/description\s+["']?(.+?)["']?$/i);
635
+ const description = match?.[1];
636
+ return description?.trim();
637
+ };
@@ -0,0 +1,13 @@
1
+ // packages/rule-helpers/src/aruba/index.ts
2
+ // Re-export all Aruba helpers
3
+
4
+ export * from './helpers';
5
+
6
+ // Also re-export commonly used common helpers for convenience
7
+ export {
8
+ hasChildCommand,
9
+ getChildCommand,
10
+ getChildCommands,
11
+ parseIp,
12
+ isShutdown,
13
+ } from '../common/helpers';