@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,534 @@
1
+ // packages/rule-helpers/src/cisco/helpers.ts
2
+ // Cisco IOS/IOS-XE specific helper functions
3
+ // Based on Cisco Best Practices: docs/Cisco-best-practices.md
4
+
5
+ import type { ConfigNode } from '../../types/ConfigNode';
6
+ import {
7
+ hasChildCommand,
8
+ getChildCommand,
9
+ equalsIgnoreCase,
10
+ includesIgnoreCase,
11
+ startsWithIgnoreCase,
12
+ parseInteger,
13
+ } from '../common/helpers';
14
+
15
+ // Re-export common helpers for convenience
16
+ export { hasChildCommand, getChildCommand, getChildCommands } from '../common/helpers';
17
+
18
+ /**
19
+ * Check if interface is shutdown
20
+ */
21
+ export const isShutdown = (node: ConfigNode): boolean => {
22
+ return node.children.some((child) => {
23
+ return equalsIgnoreCase(child.id.trim(), 'shutdown');
24
+ });
25
+ };
26
+
27
+ /**
28
+ * Check if interface is a physical port (not Loopback, Vlan, Null, etc.)
29
+ */
30
+ export const isPhysicalPort = (interfaceName: string): boolean => {
31
+ return (
32
+ !includesIgnoreCase(interfaceName, 'loopback') &&
33
+ !includesIgnoreCase(interfaceName, 'null') &&
34
+ !includesIgnoreCase(interfaceName, 'vlan') &&
35
+ !includesIgnoreCase(interfaceName, 'tunnel') &&
36
+ !includesIgnoreCase(interfaceName, 'port-channel') &&
37
+ !includesIgnoreCase(interfaceName, 'bvi') &&
38
+ !includesIgnoreCase(interfaceName, 'nve')
39
+ );
40
+ };
41
+
42
+ /**
43
+ * Check if interface name suggests it's a trunk/uplink based on description
44
+ */
45
+ export const isLikelyTrunk = (node: ConfigNode): boolean => {
46
+ const desc = getChildCommand(node, 'description');
47
+ if (desc) {
48
+ const descText = desc.rawText;
49
+ return (
50
+ includesIgnoreCase(descText, 'uplink') ||
51
+ includesIgnoreCase(descText, 'downlink') ||
52
+ includesIgnoreCase(descText, 'isl') ||
53
+ includesIgnoreCase(descText, 'trunk') ||
54
+ includesIgnoreCase(descText, 'po-member')
55
+ );
56
+ }
57
+ return hasChildCommand(node, 'switchport mode trunk');
58
+ };
59
+
60
+ /**
61
+ * Check if interface is configured as trunk
62
+ */
63
+ export const isTrunkPort = (node: ConfigNode): boolean => {
64
+ return hasChildCommand(node, 'switchport mode trunk');
65
+ };
66
+
67
+ /**
68
+ * Check if interface is configured as access
69
+ */
70
+ export const isAccessPort = (node: ConfigNode): boolean => {
71
+ return hasChildCommand(node, 'switchport mode access');
72
+ };
73
+
74
+ /**
75
+ * Check if description suggests external-facing interface
76
+ */
77
+ export const isExternalFacing = (node: ConfigNode): boolean => {
78
+ const desc = getChildCommand(node, 'description');
79
+ if (desc) {
80
+ const descText = desc.rawText;
81
+ return (
82
+ includesIgnoreCase(descText, 'wan:') ||
83
+ includesIgnoreCase(descText, 'external') ||
84
+ includesIgnoreCase(descText, 'internet') ||
85
+ includesIgnoreCase(descText, 'isp') ||
86
+ includesIgnoreCase(descText, 'dmz') ||
87
+ includesIgnoreCase(descText, 'perimeter')
88
+ );
89
+ }
90
+ return false;
91
+ };
92
+
93
+ /**
94
+ * Check if description suggests user endpoint port
95
+ */
96
+ export const isEndpointPort = (node: ConfigNode): boolean => {
97
+ const desc = getChildCommand(node, 'description');
98
+ if (desc) {
99
+ const descText = desc.rawText;
100
+ return (
101
+ includesIgnoreCase(descText, 'endpoint:') ||
102
+ includesIgnoreCase(descText, 'user:') ||
103
+ includesIgnoreCase(descText, 'workstation') ||
104
+ includesIgnoreCase(descText, 'desktop') ||
105
+ includesIgnoreCase(descText, 'desk')
106
+ );
107
+ }
108
+ // Also check if it's an access port without uplink indicators
109
+ return isAccessPort(node) && !isLikelyTrunk(node);
110
+ };
111
+
112
+ /**
113
+ * Check if description suggests phone or AP
114
+ */
115
+ export const isPhoneOrAP = (node: ConfigNode): boolean => {
116
+ const desc = getChildCommand(node, 'description');
117
+ if (desc) {
118
+ const descText = desc.rawText;
119
+ return (
120
+ includesIgnoreCase(descText, 'phone') ||
121
+ includesIgnoreCase(descText, 'voice') ||
122
+ includesIgnoreCase(descText, 'cisco-ap') ||
123
+ includesIgnoreCase(descText, 'aruba-ap') ||
124
+ includesIgnoreCase(descText, 'ap-') ||
125
+ includesIgnoreCase(descText, '-ap')
126
+ );
127
+ }
128
+ // Check for voice vlan which indicates phone
129
+ return hasChildCommand(node, 'switchport voice vlan');
130
+ };
131
+
132
+ /**
133
+ * Check if trunk is connected to a non-Cisco device (server, storage, non-Cisco switch)
134
+ * These require switchport nonegotiate since the other end doesn't speak DTP
135
+ */
136
+ export const isTrunkToNonCisco = (node: ConfigNode): boolean => {
137
+ const desc = getChildCommand(node, 'description');
138
+ if (desc) {
139
+ const descText = desc.rawText;
140
+ // Non-Cisco endpoints that need nonegotiate
141
+ if (
142
+ includesIgnoreCase(descText, 'server:') ||
143
+ includesIgnoreCase(descText, 'storage:') ||
144
+ includesIgnoreCase(descText, 'esx') ||
145
+ includesIgnoreCase(descText, 'vmware') ||
146
+ includesIgnoreCase(descText, 'hyperv') ||
147
+ includesIgnoreCase(descText, 'hyper-v') ||
148
+ includesIgnoreCase(descText, 'linux') ||
149
+ includesIgnoreCase(descText, 'appliance') ||
150
+ includesIgnoreCase(descText, 'firewall') ||
151
+ includesIgnoreCase(descText, 'loadbalancer') ||
152
+ includesIgnoreCase(descText, 'lb:') ||
153
+ includesIgnoreCase(descText, 'nas:') ||
154
+ includesIgnoreCase(descText, 'san:')
155
+ ) {
156
+ return true;
157
+ }
158
+ // Cisco switch connections - DTP is fine
159
+ if (
160
+ includesIgnoreCase(descText, 'uplink:') ||
161
+ includesIgnoreCase(descText, 'downlink:') ||
162
+ includesIgnoreCase(descText, 'isl:') ||
163
+ includesIgnoreCase(descText, 'po-member:')
164
+ ) {
165
+ return false;
166
+ }
167
+ }
168
+ // No description - can't determine, don't flag
169
+ return false;
170
+ };
171
+
172
+ // ============================================================================
173
+ // Management Plane Helpers
174
+ // ============================================================================
175
+
176
+ /**
177
+ * Check if AAA new-model is configured (global command)
178
+ */
179
+ export const isAaaNewModel = (node: ConfigNode): boolean => {
180
+ return equalsIgnoreCase(node.id.trim(), 'aaa new-model');
181
+ };
182
+
183
+ /**
184
+ * Check if password uses strong encryption type (Type 8/9, scrypt, sha256)
185
+ * Type 7 is easily reversible, Type 5 (MD5) is deprecated
186
+ */
187
+ export const hasStrongPasswordType = (node: ConfigNode): boolean => {
188
+ const rawText = node.rawText;
189
+ // Strong: algorithm-type sha256, algorithm-type scrypt, secret (type 5+)
190
+ // Weak: password (type 0 or 7)
191
+ if (includesIgnoreCase(rawText, 'algorithm-type sha256') || includesIgnoreCase(rawText, 'algorithm-type scrypt')) {
192
+ return true;
193
+ }
194
+ if (includesIgnoreCase(rawText, ' secret ')) {
195
+ // secret uses MD5 (type 5) minimum, better than password
196
+ return true;
197
+ }
198
+ return false;
199
+ };
200
+
201
+ /**
202
+ * Check if username uses weak password type (Type 7 or plaintext)
203
+ */
204
+ export const hasWeakUsernamePassword = (node: ConfigNode): boolean => {
205
+ const rawText = node.rawText;
206
+ // Check for "password 7" or just "password" without algorithm-type
207
+ if (includesIgnoreCase(rawText, ' password ')) {
208
+ if (includesIgnoreCase(rawText, 'algorithm-type sha256') || includesIgnoreCase(rawText, 'algorithm-type scrypt')) {
209
+ return false;
210
+ }
211
+ return true;
212
+ }
213
+ return false;
214
+ };
215
+
216
+ /**
217
+ * Get SSH version from configuration
218
+ */
219
+ export const getSshVersion = (node: ConfigNode): number | null => {
220
+ if (includesIgnoreCase(node.rawText, 'ip ssh version')) {
221
+ const match = node.params.find((p) => p === '1' || p === '2');
222
+ if (match) {
223
+ return parseInteger(match);
224
+ }
225
+ }
226
+ return null;
227
+ };
228
+
229
+ /**
230
+ * Check if SNMP community is a well-known default
231
+ */
232
+ export const isDefaultSnmpCommunity = (community: string): boolean => {
233
+ const defaultCommunities = [
234
+ 'public',
235
+ 'private',
236
+ 'community',
237
+ 'snmp',
238
+ 'admin',
239
+ 'cisco',
240
+ 'secret',
241
+ 'test',
242
+ 'default',
243
+ ];
244
+ return defaultCommunities.some((dc) => equalsIgnoreCase(community, dc));
245
+ };
246
+
247
+ /**
248
+ * Check if SNMP v3 is configured
249
+ */
250
+ export const isSnmpV3User = (node: ConfigNode): boolean => {
251
+ return startsWithIgnoreCase(node.id, 'snmp-server user');
252
+ };
253
+
254
+ /**
255
+ * Check if SNMP v3 uses auth-priv
256
+ */
257
+ export const hasSnmpV3AuthPriv = (node: ConfigNode): boolean => {
258
+ return includesIgnoreCase(node.rawText, 'auth') && includesIgnoreCase(node.rawText, 'priv');
259
+ };
260
+
261
+ /**
262
+ * Get VTY line range from node
263
+ */
264
+ export const getVtyLineRange = (node: ConfigNode): { start: number; end: number } | null => {
265
+ const params = node.params;
266
+ // line vty 0 15
267
+ if (params.length >= 4) {
268
+ const startStr = params[2];
269
+ const endStr = params[3];
270
+ const start = startStr ? parseInteger(startStr) : null;
271
+ const end = endStr ? parseInteger(endStr) : null;
272
+ if (start !== null && end !== null) {
273
+ return { start, end };
274
+ }
275
+ }
276
+ // line vty 0
277
+ if (params.length >= 3) {
278
+ const startStr = params[2];
279
+ const start = startStr ? parseInteger(startStr) : null;
280
+ if (start !== null) {
281
+ return { start, end: start };
282
+ }
283
+ }
284
+ return null;
285
+ };
286
+
287
+ /**
288
+ * Check if VTY has access-class configured
289
+ */
290
+ export const hasVtyAccessClass = (node: ConfigNode): boolean => {
291
+ return hasChildCommand(node, 'access-class');
292
+ };
293
+
294
+ /**
295
+ * Check if NTP authentication is enabled
296
+ */
297
+ export const hasNtpAuthentication = (node: ConfigNode): boolean => {
298
+ return includesIgnoreCase(node.rawText, 'ntp authenticate') || includesIgnoreCase(node.rawText, 'ntp authentication-key');
299
+ };
300
+
301
+ // ============================================================================
302
+ // Control Plane Helpers
303
+ // ============================================================================
304
+
305
+ /**
306
+ * Check if OSPF authentication is configured on interface
307
+ */
308
+ export const hasOspfAuthentication = (node: ConfigNode): boolean => {
309
+ return hasChildCommand(node, 'ip ospf authentication') ||
310
+ hasChildCommand(node, 'ip ospf message-digest-key');
311
+ };
312
+
313
+ /**
314
+ * Check if EIGRP authentication is configured on interface
315
+ */
316
+ export const hasEigrpAuthentication = (node: ConfigNode): boolean => {
317
+ return hasChildCommand(node, 'ip authentication mode eigrp') &&
318
+ hasChildCommand(node, 'ip authentication key-chain eigrp');
319
+ };
320
+
321
+ /**
322
+ * Check if BGP neighbor has password configured
323
+ */
324
+ export const hasBgpNeighborPassword = (neighborCommands: ConfigNode[]): boolean => {
325
+ return neighborCommands.some((cmd) =>
326
+ includesIgnoreCase(cmd.id, 'password')
327
+ );
328
+ };
329
+
330
+ /**
331
+ * Check if BGP neighbor has TTL security (GTSM) configured
332
+ */
333
+ export const hasBgpTtlSecurity = (neighborCommands: ConfigNode[]): boolean => {
334
+ return neighborCommands.some((cmd) =>
335
+ includesIgnoreCase(cmd.id, 'ttl-security')
336
+ );
337
+ };
338
+
339
+ /**
340
+ * Check if BGP neighbor has maximum-prefix configured
341
+ */
342
+ export const hasBgpMaxPrefix = (neighborCommands: ConfigNode[]): boolean => {
343
+ return neighborCommands.some((cmd) =>
344
+ includesIgnoreCase(cmd.id, 'maximum-prefix')
345
+ );
346
+ };
347
+
348
+ /**
349
+ * Check if BGP has log-neighbor-changes enabled
350
+ */
351
+ export const hasBgpLogNeighborChanges = (node: ConfigNode): boolean => {
352
+ return hasChildCommand(node, 'bgp log-neighbor-changes');
353
+ };
354
+
355
+ /**
356
+ * Get all BGP neighbors from router bgp section
357
+ */
358
+ export const getBgpNeighbors = (node: ConfigNode): Map<string, ConfigNode[]> => {
359
+ const neighbors = new Map<string, ConfigNode[]>();
360
+
361
+ for (const child of node.children) {
362
+ if (startsWithIgnoreCase(child.id, 'neighbor')) {
363
+ const neighborIp = child.params[1];
364
+ if (neighborIp) {
365
+ const existing = neighbors.get(neighborIp) || [];
366
+ existing.push(child);
367
+ neighbors.set(neighborIp, existing);
368
+ }
369
+ }
370
+ }
371
+
372
+ return neighbors;
373
+ };
374
+
375
+ /**
376
+ * Check if HSRP has MD5 authentication
377
+ */
378
+ export const hasHsrpMd5Auth = (node: ConfigNode): boolean => {
379
+ return node.children.some((child) => {
380
+ return startsWithIgnoreCase(child.id, 'standby') && includesIgnoreCase(child.id, 'authentication md5');
381
+ });
382
+ };
383
+
384
+ /**
385
+ * Check if VRRP has authentication
386
+ */
387
+ export const hasVrrpAuthentication = (node: ConfigNode): boolean => {
388
+ return node.children.some((child) => {
389
+ return startsWithIgnoreCase(child.id, 'vrrp') && includesIgnoreCase(child.id, 'authentication');
390
+ });
391
+ };
392
+
393
+ // ============================================================================
394
+ // Data Plane Helpers
395
+ // ============================================================================
396
+
397
+ /**
398
+ * Check if interface has uRPF (unicast RPF) enabled
399
+ */
400
+ export const hasUrpf = (node: ConfigNode): boolean => {
401
+ return hasChildCommand(node, 'ip verify unicast source reachable-via');
402
+ };
403
+
404
+ /**
405
+ * Get uRPF mode (rx = strict, any = loose)
406
+ */
407
+ export const getUrpfMode = (node: ConfigNode): 'strict' | 'loose' | null => {
408
+ const cmd = getChildCommand(node, 'ip verify unicast source reachable-via');
409
+ if (cmd) {
410
+ if (includesIgnoreCase(cmd.rawText, 'reachable-via rx')) {
411
+ return 'strict';
412
+ }
413
+ if (includesIgnoreCase(cmd.rawText, 'reachable-via any')) {
414
+ return 'loose';
415
+ }
416
+ }
417
+ return null;
418
+ };
419
+
420
+ /**
421
+ * Check if IP redirects are disabled
422
+ */
423
+ export const hasNoIpRedirects = (node: ConfigNode): boolean => {
424
+ return hasChildCommand(node, 'no ip redirects');
425
+ };
426
+
427
+ /**
428
+ * Check if IP unreachables are disabled
429
+ */
430
+ export const hasNoIpUnreachables = (node: ConfigNode): boolean => {
431
+ return hasChildCommand(node, 'no ip unreachables');
432
+ };
433
+
434
+ /**
435
+ * Check if IP proxy-arp is disabled
436
+ */
437
+ export const hasNoProxyArp = (node: ConfigNode): boolean => {
438
+ return hasChildCommand(node, 'no ip proxy-arp');
439
+ };
440
+
441
+ /**
442
+ * Check if IP directed-broadcast is disabled
443
+ */
444
+ export const hasNoDirectedBroadcast = (node: ConfigNode): boolean => {
445
+ return hasChildCommand(node, 'no ip directed-broadcast');
446
+ };
447
+
448
+ /**
449
+ * Check if interface is a WAN/Internet-facing interface
450
+ */
451
+ export const isWanInterface = (node: ConfigNode): boolean => {
452
+ const desc = getChildCommand(node, 'description');
453
+ if (desc) {
454
+ const descText = desc.rawText;
455
+ return (
456
+ includesIgnoreCase(descText, 'wan') ||
457
+ includesIgnoreCase(descText, 'internet') ||
458
+ includesIgnoreCase(descText, 'isp') ||
459
+ includesIgnoreCase(descText, 'external') ||
460
+ includesIgnoreCase(descText, 'outside') ||
461
+ includesIgnoreCase(descText, 'border') ||
462
+ includesIgnoreCase(descText, 'edge')
463
+ );
464
+ }
465
+ return false;
466
+ };
467
+
468
+ /**
469
+ * Check if interface is a loopback
470
+ */
471
+ export const isLoopbackInterface = (interfaceName: string): boolean => {
472
+ return includesIgnoreCase(interfaceName, 'loopback');
473
+ };
474
+
475
+ /**
476
+ * Check if interface is a tunnel
477
+ */
478
+ export const isTunnelInterface = (interfaceName: string): boolean => {
479
+ return includesIgnoreCase(interfaceName, 'tunnel');
480
+ };
481
+
482
+ /**
483
+ * Check if interface is a VLAN SVI
484
+ */
485
+ export const isVlanInterface = (interfaceName: string): boolean => {
486
+ return startsWithIgnoreCase(interfaceName, 'interface vlan');
487
+ };
488
+
489
+ // ============================================================================
490
+ // Service Hardening Helpers
491
+ // ============================================================================
492
+
493
+ /**
494
+ * Check if service password-encryption is enabled
495
+ */
496
+ export const hasPasswordEncryption = (node: ConfigNode): boolean => {
497
+ return equalsIgnoreCase(node.id.trim(), 'service password-encryption');
498
+ };
499
+
500
+ /**
501
+ * Check if TCP keepalives are enabled
502
+ */
503
+ export const hasTcpKeepalives = (node: ConfigNode): boolean => {
504
+ const cmd = node.id.trim();
505
+ return equalsIgnoreCase(cmd, 'service tcp-keepalives-in') || equalsIgnoreCase(cmd, 'service tcp-keepalives-out');
506
+ };
507
+
508
+ /**
509
+ * Check if service is a dangerous/unnecessary service that should be disabled
510
+ */
511
+ export const isDangerousService = (node: ConfigNode): boolean => {
512
+ const cmd = node.id.trim();
513
+ const dangerousServices = [
514
+ 'service tcp-small-servers',
515
+ 'service udp-small-servers',
516
+ 'ip finger',
517
+ 'service finger',
518
+ 'ip bootp server',
519
+ 'service config',
520
+ 'ip http server',
521
+ 'service pad',
522
+ 'boot network',
523
+ 'service call-home',
524
+ ];
525
+ return dangerousServices.some((svc) => equalsIgnoreCase(cmd, svc));
526
+ };
527
+
528
+ /**
529
+ * Check if Smart Install (vstack) is enabled
530
+ */
531
+ export const isSmartInstallEnabled = (node: ConfigNode): boolean => {
532
+ // "vstack" without "no" means it's enabled
533
+ return equalsIgnoreCase(node.id.trim(), 'vstack');
534
+ };
@@ -0,0 +1,11 @@
1
+ // packages/rule-helpers/src/cisco/index.ts
2
+ // Re-export all Cisco 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
+ } from '../common/helpers';