@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.
- package/LICENSE +190 -0
- package/README.md +86 -0
- package/package.json +60 -0
- package/src/constants.ts +77 -0
- package/src/engine/RuleExecutor.ts +256 -0
- package/src/engine/Runner.ts +312 -0
- package/src/engine/SandboxedExecutor.ts +208 -0
- package/src/errors.ts +88 -0
- package/src/helpers/arista/helpers.ts +1220 -0
- package/src/helpers/arista/index.ts +12 -0
- package/src/helpers/aruba/helpers.ts +637 -0
- package/src/helpers/aruba/index.ts +13 -0
- package/src/helpers/cisco/helpers.ts +534 -0
- package/src/helpers/cisco/index.ts +11 -0
- package/src/helpers/common/helpers.ts +265 -0
- package/src/helpers/common/index.ts +5 -0
- package/src/helpers/common/validation.ts +280 -0
- package/src/helpers/cumulus/helpers.ts +676 -0
- package/src/helpers/cumulus/index.ts +12 -0
- package/src/helpers/extreme/helpers.ts +422 -0
- package/src/helpers/extreme/index.ts +12 -0
- package/src/helpers/fortinet/helpers.ts +892 -0
- package/src/helpers/fortinet/index.ts +12 -0
- package/src/helpers/huawei/helpers.ts +790 -0
- package/src/helpers/huawei/index.ts +11 -0
- package/src/helpers/index.ts +53 -0
- package/src/helpers/juniper/helpers.ts +756 -0
- package/src/helpers/juniper/index.ts +12 -0
- package/src/helpers/mikrotik/helpers.ts +722 -0
- package/src/helpers/mikrotik/index.ts +12 -0
- package/src/helpers/nokia/helpers.ts +856 -0
- package/src/helpers/nokia/index.ts +11 -0
- package/src/helpers/paloalto/helpers.ts +939 -0
- package/src/helpers/paloalto/index.ts +12 -0
- package/src/helpers/vyos/helpers.ts +429 -0
- package/src/helpers/vyos/index.ts +12 -0
- package/src/index.ts +30 -0
- package/src/json-rules/ExpressionEvaluator.ts +292 -0
- package/src/json-rules/HelperRegistry.ts +177 -0
- package/src/json-rules/JsonRuleCompiler.ts +339 -0
- package/src/json-rules/JsonRuleValidator.ts +371 -0
- package/src/json-rules/index.ts +97 -0
- package/src/json-rules/schema.json +350 -0
- package/src/json-rules/types.ts +303 -0
- package/src/pack-loader/PackLoader.ts +332 -0
- package/src/pack-loader/index.ts +17 -0
- package/src/pack-loader/types.ts +135 -0
- package/src/parser/IncrementalParser.ts +527 -0
- package/src/parser/Sanitizer.ts +104 -0
- package/src/parser/SchemaAwareParser.ts +504 -0
- package/src/parser/VendorSchema.ts +72 -0
- package/src/parser/vendors/arista-eos.ts +206 -0
- package/src/parser/vendors/aruba-aoscx.ts +123 -0
- package/src/parser/vendors/aruba-aosswitch.ts +113 -0
- package/src/parser/vendors/aruba-wlc.ts +173 -0
- package/src/parser/vendors/cisco-ios.ts +110 -0
- package/src/parser/vendors/cisco-nxos.ts +107 -0
- package/src/parser/vendors/cumulus-linux.ts +161 -0
- package/src/parser/vendors/extreme-exos.ts +154 -0
- package/src/parser/vendors/extreme-voss.ts +167 -0
- package/src/parser/vendors/fortinet-fortigate.ts +217 -0
- package/src/parser/vendors/huawei-vrp.ts +192 -0
- package/src/parser/vendors/index.ts +1521 -0
- package/src/parser/vendors/juniper-junos.ts +230 -0
- package/src/parser/vendors/mikrotik-routeros.ts +274 -0
- package/src/parser/vendors/nokia-sros.ts +251 -0
- package/src/parser/vendors/paloalto-panos.ts +264 -0
- package/src/parser/vendors/vyos-vyos.ts +454 -0
- package/src/types/ConfigNode.ts +72 -0
- package/src/types/DeclarativeRule.ts +158 -0
- package/src/types/IRule.ts +270 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// packages/core/src/parser/vendors/vyos-vyos.ts
|
|
2
|
+
|
|
3
|
+
import type { VendorSchema } from '../VendorSchema';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* VyOS/Ubiquiti EdgeOS configuration schema.
|
|
7
|
+
*
|
|
8
|
+
* VyOS (and Ubiquiti EdgeOS which is based on Vyatta/VyOS) uses a hierarchical
|
|
9
|
+
* configuration model that can be displayed in multiple formats:
|
|
10
|
+
* 1. Set command format (CLI style): "set interfaces ethernet eth0 address 192.168.1.1/24"
|
|
11
|
+
* 2. Hierarchical display format with braces: "interfaces { ethernet eth0 { ... } }"
|
|
12
|
+
*
|
|
13
|
+
* Key characteristics:
|
|
14
|
+
* - Brace-based hierarchy in display mode (like JunOS)
|
|
15
|
+
* - Set-style commands: "set interfaces ethernet eth0 address 192.168.1.1/24"
|
|
16
|
+
* - Delete commands: "delete interfaces ethernet eth0 address 192.168.1.1/24"
|
|
17
|
+
* - Comments: Block comments or added via "comment" command
|
|
18
|
+
* - Interface naming: eth0, eth1, bond0, br0, wg0, vti0, pppoe0, tun0
|
|
19
|
+
* - Zone-based firewall with named rulesets
|
|
20
|
+
* - VPN support: IPsec, OpenVPN, WireGuard, L2TP
|
|
21
|
+
*
|
|
22
|
+
* Configuration structure (hierarchical format):
|
|
23
|
+
* ```
|
|
24
|
+
* interfaces {
|
|
25
|
+
* ethernet eth0 {
|
|
26
|
+
* address 192.168.1.1/24
|
|
27
|
+
* description "WAN Interface"
|
|
28
|
+
* hw-id 00:0c:29:xx:xx:xx
|
|
29
|
+
* }
|
|
30
|
+
* loopback lo {
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
* system {
|
|
34
|
+
* host-name vyos-router
|
|
35
|
+
* login {
|
|
36
|
+
* user vyos {
|
|
37
|
+
* authentication {
|
|
38
|
+
* encrypted-password "$6$..."
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
45
|
+
* Set command format:
|
|
46
|
+
* ```
|
|
47
|
+
* set interfaces ethernet eth0 address '192.168.1.1/24'
|
|
48
|
+
* set interfaces ethernet eth0 description 'WAN Interface'
|
|
49
|
+
* set system host-name vyos-router
|
|
50
|
+
* set firewall name WAN_IN default-action drop
|
|
51
|
+
* set firewall name WAN_IN rule 10 action accept
|
|
52
|
+
* set firewall name WAN_IN rule 10 state established enable
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export const VyOSSchema: VendorSchema = {
|
|
56
|
+
id: 'vyos',
|
|
57
|
+
name: 'VyOS/EdgeOS',
|
|
58
|
+
useBraceHierarchy: true,
|
|
59
|
+
|
|
60
|
+
commentPatterns: [
|
|
61
|
+
/^\/\*.*\*\/$/, // Block comments /* ... */
|
|
62
|
+
/^\/\*.*$/, // Multi-line comment start /* ...
|
|
63
|
+
/^.*\*\/$/, // Multi-line comment end ... */
|
|
64
|
+
/^#/, // Hash comments (some modes)
|
|
65
|
+
],
|
|
66
|
+
sectionDelimiter: '}',
|
|
67
|
+
|
|
68
|
+
blockStarters: [
|
|
69
|
+
// ============ DEPTH 0: Top-level configuration stanzas ============
|
|
70
|
+
|
|
71
|
+
// System configuration (hostname, login, dns, ntp, syslog)
|
|
72
|
+
{ pattern: /^system\s*\{?$/i, depth: 0 },
|
|
73
|
+
|
|
74
|
+
// Interfaces (ethernet, loopback, bonding, bridge, wireguard, etc.)
|
|
75
|
+
{ pattern: /^interfaces\s*\{?$/i, depth: 0 },
|
|
76
|
+
|
|
77
|
+
// Firewall configuration (zones, groups, rules)
|
|
78
|
+
{ pattern: /^firewall\s*\{?$/i, depth: 0 },
|
|
79
|
+
|
|
80
|
+
// NAT configuration
|
|
81
|
+
{ pattern: /^nat\s*\{?$/i, depth: 0 },
|
|
82
|
+
|
|
83
|
+
// Routing protocols
|
|
84
|
+
{ pattern: /^protocols\s*\{?$/i, depth: 0 },
|
|
85
|
+
|
|
86
|
+
// Policy (route-map, prefix-list, as-path, community-list)
|
|
87
|
+
{ pattern: /^policy\s*\{?$/i, depth: 0 },
|
|
88
|
+
|
|
89
|
+
// Service configuration (dhcp-server, dns, ssh, https, etc.)
|
|
90
|
+
{ pattern: /^service\s*\{?$/i, depth: 0 },
|
|
91
|
+
|
|
92
|
+
// VPN configuration (ipsec, openvpn, l2tp, pptp, wireguard)
|
|
93
|
+
{ pattern: /^vpn\s*\{?$/i, depth: 0 },
|
|
94
|
+
|
|
95
|
+
// High availability (VRRP, conntrack-sync)
|
|
96
|
+
{ pattern: /^high-availability\s*\{?$/i, depth: 0 },
|
|
97
|
+
|
|
98
|
+
// QoS (traffic shaping, policies)
|
|
99
|
+
{ pattern: /^traffic-policy\s*\{?$/i, depth: 0 },
|
|
100
|
+
{ pattern: /^qos\s*\{?$/i, depth: 0 },
|
|
101
|
+
|
|
102
|
+
// Container (podman/docker containers in VyOS 1.4+)
|
|
103
|
+
{ pattern: /^container\s*\{?$/i, depth: 0 },
|
|
104
|
+
|
|
105
|
+
// Load balancing (WAN load balancing, reverse proxy)
|
|
106
|
+
{ pattern: /^load-balancing\s*\{?$/i, depth: 0 },
|
|
107
|
+
|
|
108
|
+
// VRRP (standalone in older versions)
|
|
109
|
+
{ pattern: /^vrrp\s*\{?$/i, depth: 0 },
|
|
110
|
+
|
|
111
|
+
// PKI (certificates, CA)
|
|
112
|
+
{ pattern: /^pki\s*\{?$/i, depth: 0 },
|
|
113
|
+
|
|
114
|
+
// Set command format (flat configuration)
|
|
115
|
+
{ pattern: /^set\s+system\s+/i, depth: 0 },
|
|
116
|
+
{ pattern: /^set\s+interfaces\s+/i, depth: 0 },
|
|
117
|
+
{ pattern: /^set\s+firewall\s+/i, depth: 0 },
|
|
118
|
+
{ pattern: /^set\s+nat\s+/i, depth: 0 },
|
|
119
|
+
{ pattern: /^set\s+protocols\s+/i, depth: 0 },
|
|
120
|
+
{ pattern: /^set\s+policy\s+/i, depth: 0 },
|
|
121
|
+
{ pattern: /^set\s+service\s+/i, depth: 0 },
|
|
122
|
+
{ pattern: /^set\s+vpn\s+/i, depth: 0 },
|
|
123
|
+
{ pattern: /^set\s+high-availability\s+/i, depth: 0 },
|
|
124
|
+
{ pattern: /^set\s+traffic-policy\s+/i, depth: 0 },
|
|
125
|
+
{ pattern: /^set\s+qos\s+/i, depth: 0 },
|
|
126
|
+
{ pattern: /^set\s+container\s+/i, depth: 0 },
|
|
127
|
+
{ pattern: /^set\s+load-balancing\s+/i, depth: 0 },
|
|
128
|
+
{ pattern: /^set\s+pki\s+/i, depth: 0 },
|
|
129
|
+
|
|
130
|
+
// Delete commands (same structure as set)
|
|
131
|
+
{ pattern: /^delete\s+/i, depth: 0 },
|
|
132
|
+
|
|
133
|
+
// ============ DEPTH 1: Inside top-level stanzas ============
|
|
134
|
+
|
|
135
|
+
// Inside system
|
|
136
|
+
{ pattern: /^host-name\s+/i, depth: 1 },
|
|
137
|
+
{ pattern: /^login\s*\{?$/i, depth: 1 },
|
|
138
|
+
{ pattern: /^name-server\s+/i, depth: 1 },
|
|
139
|
+
{ pattern: /^ntp\s*\{?$/i, depth: 1 },
|
|
140
|
+
{ pattern: /^syslog\s*\{?$/i, depth: 1 },
|
|
141
|
+
{ pattern: /^time-zone\s+/i, depth: 1 },
|
|
142
|
+
{ pattern: /^console\s*\{?$/i, depth: 1 },
|
|
143
|
+
{ pattern: /^config-management\s*\{?$/i, depth: 1 },
|
|
144
|
+
{ pattern: /^conntrack\s*\{?$/i, depth: 1 },
|
|
145
|
+
{ pattern: /^domain-name\s+/i, depth: 1 },
|
|
146
|
+
{ pattern: /^flow-accounting\s*\{?$/i, depth: 1 },
|
|
147
|
+
{ pattern: /^options\s*\{?$/i, depth: 1 },
|
|
148
|
+
{ pattern: /^static-host-mapping\s*\{?$/i, depth: 1 },
|
|
149
|
+
{ pattern: /^task-scheduler\s*\{?$/i, depth: 1 },
|
|
150
|
+
|
|
151
|
+
// Inside interfaces - interface types
|
|
152
|
+
{ pattern: /^ethernet\s+\S+\s*\{?$/i, depth: 1 },
|
|
153
|
+
{ pattern: /^loopback\s+\S+\s*\{?$/i, depth: 1 },
|
|
154
|
+
{ pattern: /^bonding\s+\S+\s*\{?$/i, depth: 1 },
|
|
155
|
+
{ pattern: /^bridge\s+\S+\s*\{?$/i, depth: 1 },
|
|
156
|
+
{ pattern: /^wireguard\s+\S+\s*\{?$/i, depth: 1 },
|
|
157
|
+
{ pattern: /^openvpn\s+\S+\s*\{?$/i, depth: 1 },
|
|
158
|
+
{ pattern: /^vti\s+\S+\s*\{?$/i, depth: 1 },
|
|
159
|
+
{ pattern: /^tunnel\s+\S+\s*\{?$/i, depth: 1 },
|
|
160
|
+
{ pattern: /^l2tpv3\s+\S+\s*\{?$/i, depth: 1 },
|
|
161
|
+
{ pattern: /^pppoe\s+\S+\s*\{?$/i, depth: 1 },
|
|
162
|
+
{ pattern: /^vxlan\s+\S+\s*\{?$/i, depth: 1 },
|
|
163
|
+
{ pattern: /^macsec\s+\S+\s*\{?$/i, depth: 1 },
|
|
164
|
+
{ pattern: /^pseudo-ethernet\s+\S+\s*\{?$/i, depth: 1 },
|
|
165
|
+
{ pattern: /^wireless\s+\S+\s*\{?$/i, depth: 1 },
|
|
166
|
+
{ pattern: /^wwan\s+\S+\s*\{?$/i, depth: 1 },
|
|
167
|
+
{ pattern: /^dummy\s+\S+\s*\{?$/i, depth: 1 },
|
|
168
|
+
|
|
169
|
+
// Inside firewall
|
|
170
|
+
{ pattern: /^all-ping\s+/i, depth: 1 },
|
|
171
|
+
{ pattern: /^broadcast-ping\s+/i, depth: 1 },
|
|
172
|
+
{ pattern: /^config-trap\s+/i, depth: 1 },
|
|
173
|
+
{ pattern: /^group\s*\{?$/i, depth: 1 },
|
|
174
|
+
{ pattern: /^ipv6-name\s+\S+\s*\{?$/i, depth: 1 },
|
|
175
|
+
{ pattern: /^ipv6-src-route\s+/i, depth: 1 },
|
|
176
|
+
{ pattern: /^ip-src-route\s+/i, depth: 1 },
|
|
177
|
+
{ pattern: /^log-martians\s+/i, depth: 1 },
|
|
178
|
+
{ pattern: /^name\s+\S+\s*\{?$/i, depth: 1 },
|
|
179
|
+
{ pattern: /^options\s*\{?$/i, depth: 1 },
|
|
180
|
+
{ pattern: /^receive-redirects\s+/i, depth: 1 },
|
|
181
|
+
{ pattern: /^send-redirects\s+/i, depth: 1 },
|
|
182
|
+
{ pattern: /^source-validation\s+/i, depth: 1 },
|
|
183
|
+
{ pattern: /^state-policy\s*\{?$/i, depth: 1 },
|
|
184
|
+
{ pattern: /^syn-cookies\s+/i, depth: 1 },
|
|
185
|
+
{ pattern: /^twa-hazards-protection\s+/i, depth: 1 },
|
|
186
|
+
{ pattern: /^zone\s+\S+\s*\{?$/i, depth: 1 },
|
|
187
|
+
// VyOS 1.4+ firewall structure
|
|
188
|
+
{ pattern: /^ipv4\s*\{?$/i, depth: 1 },
|
|
189
|
+
{ pattern: /^ipv6\s*\{?$/i, depth: 1 },
|
|
190
|
+
|
|
191
|
+
// Inside nat
|
|
192
|
+
{ pattern: /^source\s*\{?$/i, depth: 1 },
|
|
193
|
+
{ pattern: /^destination\s*\{?$/i, depth: 1 },
|
|
194
|
+
{ pattern: /^nptv6\s*\{?$/i, depth: 1 },
|
|
195
|
+
|
|
196
|
+
// Inside protocols
|
|
197
|
+
{ pattern: /^bgp\s*\{?$/i, depth: 1 },
|
|
198
|
+
{ pattern: /^ospf\s*\{?$/i, depth: 1 },
|
|
199
|
+
{ pattern: /^ospfv3\s*\{?$/i, depth: 1 },
|
|
200
|
+
{ pattern: /^rip\s*\{?$/i, depth: 1 },
|
|
201
|
+
{ pattern: /^ripng\s*\{?$/i, depth: 1 },
|
|
202
|
+
{ pattern: /^isis\s*\{?$/i, depth: 1 },
|
|
203
|
+
{ pattern: /^static\s*\{?$/i, depth: 1 },
|
|
204
|
+
{ pattern: /^bfd\s*\{?$/i, depth: 1 },
|
|
205
|
+
{ pattern: /^igmp-proxy\s*\{?$/i, depth: 1 },
|
|
206
|
+
{ pattern: /^mpls\s*\{?$/i, depth: 1 },
|
|
207
|
+
{ pattern: /^nhrp\s*\{?$/i, depth: 1 },
|
|
208
|
+
{ pattern: /^pim\s*\{?$/i, depth: 1 },
|
|
209
|
+
{ pattern: /^rpki\s*\{?$/i, depth: 1 },
|
|
210
|
+
{ pattern: /^segment-routing\s*\{?$/i, depth: 1 },
|
|
211
|
+
|
|
212
|
+
// Inside policy
|
|
213
|
+
{ pattern: /^access-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
214
|
+
{ pattern: /^access-list6\s+\S+\s*\{?$/i, depth: 1 },
|
|
215
|
+
{ pattern: /^as-path-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
216
|
+
{ pattern: /^community-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
217
|
+
{ pattern: /^extcommunity-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
218
|
+
{ pattern: /^large-community-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
219
|
+
{ pattern: /^prefix-list\s+\S+\s*\{?$/i, depth: 1 },
|
|
220
|
+
{ pattern: /^prefix-list6\s+\S+\s*\{?$/i, depth: 1 },
|
|
221
|
+
{ pattern: /^route-map\s+\S+\s*\{?$/i, depth: 1 },
|
|
222
|
+
{ pattern: /^local-route\s*\{?$/i, depth: 1 },
|
|
223
|
+
{ pattern: /^local-route6\s*\{?$/i, depth: 1 },
|
|
224
|
+
|
|
225
|
+
// Inside service
|
|
226
|
+
{ pattern: /^dhcp-server\s*\{?$/i, depth: 1 },
|
|
227
|
+
{ pattern: /^dhcpv6-server\s*\{?$/i, depth: 1 },
|
|
228
|
+
{ pattern: /^dhcp-relay\s*\{?$/i, depth: 1 },
|
|
229
|
+
{ pattern: /^dhcpv6-relay\s*\{?$/i, depth: 1 },
|
|
230
|
+
{ pattern: /^dns\s*\{?$/i, depth: 1 },
|
|
231
|
+
{ pattern: /^https\s*\{?$/i, depth: 1 },
|
|
232
|
+
{ pattern: /^ssh\s*\{?$/i, depth: 1 },
|
|
233
|
+
{ pattern: /^snmp\s*\{?$/i, depth: 1 },
|
|
234
|
+
{ pattern: /^lldp\s*\{?$/i, depth: 1 },
|
|
235
|
+
{ pattern: /^ntp\s*\{?$/i, depth: 1 },
|
|
236
|
+
{ pattern: /^router-advert\s*\{?$/i, depth: 1 },
|
|
237
|
+
{ pattern: /^tftp-server\s*\{?$/i, depth: 1 },
|
|
238
|
+
{ pattern: /^mdns\s*\{?$/i, depth: 1 },
|
|
239
|
+
{ pattern: /^monitoring\s*\{?$/i, depth: 1 },
|
|
240
|
+
{ pattern: /^webproxy\s*\{?$/i, depth: 1 },
|
|
241
|
+
{ pattern: /^broadcast-relay\s*\{?$/i, depth: 1 },
|
|
242
|
+
{ pattern: /^ids\s*\{?$/i, depth: 1 },
|
|
243
|
+
{ pattern: /^ipoe-server\s*\{?$/i, depth: 1 },
|
|
244
|
+
{ pattern: /^pppoe-server\s*\{?$/i, depth: 1 },
|
|
245
|
+
{ pattern: /^console-server\s*\{?$/i, depth: 1 },
|
|
246
|
+
|
|
247
|
+
// Inside vpn
|
|
248
|
+
{ pattern: /^ipsec\s*\{?$/i, depth: 1 },
|
|
249
|
+
{ pattern: /^l2tp\s*\{?$/i, depth: 1 },
|
|
250
|
+
{ pattern: /^pptp\s*\{?$/i, depth: 1 },
|
|
251
|
+
{ pattern: /^openconnect\s*\{?$/i, depth: 1 },
|
|
252
|
+
{ pattern: /^sstp\s*\{?$/i, depth: 1 },
|
|
253
|
+
|
|
254
|
+
// Inside high-availability
|
|
255
|
+
{ pattern: /^vrrp\s*\{?$/i, depth: 1 },
|
|
256
|
+
|
|
257
|
+
// ============ DEPTH 2: Nested inside depth-1 blocks ============
|
|
258
|
+
|
|
259
|
+
// Inside login
|
|
260
|
+
{ pattern: /^user\s+\S+\s*\{?$/i, depth: 2 },
|
|
261
|
+
{ pattern: /^radius\s*\{?$/i, depth: 2 },
|
|
262
|
+
{ pattern: /^tacacs\s*\{?$/i, depth: 2 },
|
|
263
|
+
|
|
264
|
+
// Inside NTP
|
|
265
|
+
{ pattern: /^server\s+\S+\s*\{?$/i, depth: 2 },
|
|
266
|
+
{ pattern: /^allow-client\s*\{?$/i, depth: 2 },
|
|
267
|
+
|
|
268
|
+
// Inside syslog
|
|
269
|
+
{ pattern: /^global\s*\{?$/i, depth: 2 },
|
|
270
|
+
{ pattern: /^host\s+\S+\s*\{?$/i, depth: 2 },
|
|
271
|
+
{ pattern: /^console\s*\{?$/i, depth: 2 },
|
|
272
|
+
|
|
273
|
+
// Inside interface definitions (vif, address, firewall bindings)
|
|
274
|
+
{ pattern: /^vif\s+\d+\s*\{?$/i, depth: 2 },
|
|
275
|
+
{ pattern: /^vif-s\s+\d+\s*\{?$/i, depth: 2 }, // QinQ outer VLAN
|
|
276
|
+
|
|
277
|
+
// Inside firewall group
|
|
278
|
+
{ pattern: /^address-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
279
|
+
{ pattern: /^ipv6-address-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
280
|
+
{ pattern: /^network-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
281
|
+
{ pattern: /^ipv6-network-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
282
|
+
{ pattern: /^interface-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
283
|
+
{ pattern: /^mac-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
284
|
+
{ pattern: /^port-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
285
|
+
{ pattern: /^domain-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
286
|
+
|
|
287
|
+
// Inside firewall ruleset (name/ipv6-name)
|
|
288
|
+
{ pattern: /^default-action\s+/i, depth: 2 },
|
|
289
|
+
{ pattern: /^enable-default-log\s*$/i, depth: 2 },
|
|
290
|
+
{ pattern: /^rule\s+\d+\s*\{?$/i, depth: 2 },
|
|
291
|
+
|
|
292
|
+
// Inside firewall zone
|
|
293
|
+
{ pattern: /^default-action\s+/i, depth: 2 },
|
|
294
|
+
{ pattern: /^from\s+\S+\s*\{?$/i, depth: 2 },
|
|
295
|
+
{ pattern: /^interface\s+/i, depth: 2 },
|
|
296
|
+
{ pattern: /^local-zone\s*$/i, depth: 2 },
|
|
297
|
+
|
|
298
|
+
// Inside NAT source/destination
|
|
299
|
+
{ pattern: /^rule\s+\d+\s*\{?$/i, depth: 2 },
|
|
300
|
+
|
|
301
|
+
// Inside BGP (VyOS 1.4+ uses 'system-as' inside bgp block)
|
|
302
|
+
{ pattern: /^address-family\s*\{?$/i, depth: 2 },
|
|
303
|
+
{ pattern: /^neighbor\s+\S+\s*\{?$/i, depth: 2 },
|
|
304
|
+
{ pattern: /^peer-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
305
|
+
{ pattern: /^parameters\s*\{?$/i, depth: 2 },
|
|
306
|
+
{ pattern: /^listen\s*\{?$/i, depth: 2 },
|
|
307
|
+
|
|
308
|
+
// Inside OSPF
|
|
309
|
+
{ pattern: /^area\s+\S+\s*\{?$/i, depth: 2 },
|
|
310
|
+
{ pattern: /^default-information\s*\{?$/i, depth: 2 },
|
|
311
|
+
{ pattern: /^interface\s+\S+\s*\{?$/i, depth: 2 },
|
|
312
|
+
{ pattern: /^neighbor\s+\S+\s*\{?$/i, depth: 2 },
|
|
313
|
+
{ pattern: /^passive-interface\s+/i, depth: 2 },
|
|
314
|
+
{ pattern: /^parameters\s*\{?$/i, depth: 2 },
|
|
315
|
+
{ pattern: /^redistribute\s*\{?$/i, depth: 2 },
|
|
316
|
+
{ pattern: /^refresh\s*\{?$/i, depth: 2 },
|
|
317
|
+
{ pattern: /^timers\s*\{?$/i, depth: 2 },
|
|
318
|
+
|
|
319
|
+
// Inside static routes
|
|
320
|
+
{ pattern: /^route\s+\S+\s*\{?$/i, depth: 2 },
|
|
321
|
+
{ pattern: /^route6\s+\S+\s*\{?$/i, depth: 2 },
|
|
322
|
+
|
|
323
|
+
// Inside DHCP server
|
|
324
|
+
{ pattern: /^shared-network-name\s+\S+\s*\{?$/i, depth: 2 },
|
|
325
|
+
{ pattern: /^high-availability\s*\{?$/i, depth: 2 },
|
|
326
|
+
{ pattern: /^hostfile-update\s*\{?$/i, depth: 2 },
|
|
327
|
+
|
|
328
|
+
// Inside DNS
|
|
329
|
+
{ pattern: /^forwarding\s*\{?$/i, depth: 2 },
|
|
330
|
+
{ pattern: /^dynamic\s*\{?$/i, depth: 2 },
|
|
331
|
+
|
|
332
|
+
// Inside IPsec
|
|
333
|
+
{ pattern: /^esp-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
334
|
+
{ pattern: /^ike-group\s+\S+\s*\{?$/i, depth: 2 },
|
|
335
|
+
{ pattern: /^interface\s+\S+\s*\{?$/i, depth: 2 },
|
|
336
|
+
{ pattern: /^site-to-site\s*\{?$/i, depth: 2 },
|
|
337
|
+
{ pattern: /^profile\s+\S+\s*\{?$/i, depth: 2 },
|
|
338
|
+
{ pattern: /^remote-access\s*\{?$/i, depth: 2 },
|
|
339
|
+
|
|
340
|
+
// Inside L2TP/PPTP remote access VPN
|
|
341
|
+
{ pattern: /^remote-access\s*\{?$/i, depth: 2 },
|
|
342
|
+
|
|
343
|
+
// Inside VRRP
|
|
344
|
+
{ pattern: /^group\s+\S+\s*\{?$/i, depth: 2 },
|
|
345
|
+
|
|
346
|
+
// Inside route-map
|
|
347
|
+
{ pattern: /^rule\s+\d+\s*\{?$/i, depth: 2 },
|
|
348
|
+
|
|
349
|
+
// Inside prefix-list
|
|
350
|
+
{ pattern: /^rule\s+\d+\s*\{?$/i, depth: 2 },
|
|
351
|
+
|
|
352
|
+
// Inside SSH
|
|
353
|
+
{ pattern: /^access-control\s*\{?$/i, depth: 2 },
|
|
354
|
+
{ pattern: /^dynamic-protection\s*\{?$/i, depth: 2 },
|
|
355
|
+
|
|
356
|
+
// ============ DEPTH 3: Deeply nested blocks ============
|
|
357
|
+
|
|
358
|
+
// Inside user authentication
|
|
359
|
+
{ pattern: /^authentication\s*\{?$/i, depth: 3 },
|
|
360
|
+
{ pattern: /^public-keys\s+\S+\s*\{?$/i, depth: 3 },
|
|
361
|
+
|
|
362
|
+
// Inside VIF (VLAN subinterface)
|
|
363
|
+
{ pattern: /^vif-c\s+\d+\s*\{?$/i, depth: 3 }, // QinQ inner VLAN
|
|
364
|
+
|
|
365
|
+
// Inside DHCP shared-network
|
|
366
|
+
{ pattern: /^subnet\s+\S+\s*\{?$/i, depth: 3 },
|
|
367
|
+
|
|
368
|
+
// Inside BGP address-family
|
|
369
|
+
{ pattern: /^ipv4-unicast\s*\{?$/i, depth: 3 },
|
|
370
|
+
{ pattern: /^ipv6-unicast\s*\{?$/i, depth: 3 },
|
|
371
|
+
{ pattern: /^l2vpn-evpn\s*\{?$/i, depth: 3 },
|
|
372
|
+
|
|
373
|
+
// Inside BGP neighbor
|
|
374
|
+
{ pattern: /^address-family\s*\{?$/i, depth: 3 },
|
|
375
|
+
|
|
376
|
+
// Inside OSPF area
|
|
377
|
+
{ pattern: /^area-type\s*\{?$/i, depth: 3 },
|
|
378
|
+
{ pattern: /^network\s+\S+\s*$/i, depth: 3 },
|
|
379
|
+
{ pattern: /^range\s+\S+\s*\{?$/i, depth: 3 },
|
|
380
|
+
{ pattern: /^virtual-link\s+\S+\s*\{?$/i, depth: 3 },
|
|
381
|
+
|
|
382
|
+
// Inside static route
|
|
383
|
+
{ pattern: /^next-hop\s+\S+\s*\{?$/i, depth: 3 },
|
|
384
|
+
{ pattern: /^blackhole\s*\{?$/i, depth: 3 },
|
|
385
|
+
|
|
386
|
+
// Inside IPsec esp-group/ike-group
|
|
387
|
+
{ pattern: /^proposal\s+\d+\s*\{?$/i, depth: 3 },
|
|
388
|
+
|
|
389
|
+
// Inside IPsec site-to-site
|
|
390
|
+
{ pattern: /^peer\s+\S+\s*\{?$/i, depth: 3 },
|
|
391
|
+
|
|
392
|
+
// Inside firewall rule (match conditions and actions)
|
|
393
|
+
{ pattern: /^source\s*\{?$/i, depth: 3 },
|
|
394
|
+
{ pattern: /^destination\s*\{?$/i, depth: 3 },
|
|
395
|
+
{ pattern: /^state\s*\{?$/i, depth: 3 },
|
|
396
|
+
{ pattern: /^tcp\s*\{?$/i, depth: 3 },
|
|
397
|
+
{ pattern: /^icmp\s*\{?$/i, depth: 3 },
|
|
398
|
+
{ pattern: /^time\s*\{?$/i, depth: 3 },
|
|
399
|
+
{ pattern: /^recent\s*\{?$/i, depth: 3 },
|
|
400
|
+
{ pattern: /^log\s*\{?$/i, depth: 3 },
|
|
401
|
+
{ pattern: /^limit\s*\{?$/i, depth: 3 },
|
|
402
|
+
|
|
403
|
+
// Inside NAT rule
|
|
404
|
+
{ pattern: /^source\s*\{?$/i, depth: 3 },
|
|
405
|
+
{ pattern: /^destination\s*\{?$/i, depth: 3 },
|
|
406
|
+
{ pattern: /^translation\s*\{?$/i, depth: 3 },
|
|
407
|
+
|
|
408
|
+
// Inside zone from
|
|
409
|
+
{ pattern: /^firewall\s*\{?$/i, depth: 3 },
|
|
410
|
+
|
|
411
|
+
// Inside route-map rule
|
|
412
|
+
{ pattern: /^match\s*\{?$/i, depth: 3 },
|
|
413
|
+
{ pattern: /^set\s*\{?$/i, depth: 3 },
|
|
414
|
+
{ pattern: /^on-match\s*\{?$/i, depth: 3 },
|
|
415
|
+
|
|
416
|
+
// Inside SNMP
|
|
417
|
+
{ pattern: /^community\s+\S+\s*\{?$/i, depth: 3 },
|
|
418
|
+
{ pattern: /^trap-target\s+\S+\s*\{?$/i, depth: 3 },
|
|
419
|
+
{ pattern: /^v3\s*\{?$/i, depth: 3 },
|
|
420
|
+
|
|
421
|
+
// ============ DEPTH 4: Very deeply nested ============
|
|
422
|
+
|
|
423
|
+
// Inside DHCP subnet
|
|
424
|
+
{ pattern: /^static-mapping\s+\S+\s*\{?$/i, depth: 4 },
|
|
425
|
+
{ pattern: /^range\s+\d+\s*\{?$/i, depth: 4 },
|
|
426
|
+
|
|
427
|
+
// Inside IPsec peer
|
|
428
|
+
{ pattern: /^tunnel\s+\d+\s*\{?$/i, depth: 4 },
|
|
429
|
+
{ pattern: /^vti\s*\{?$/i, depth: 4 },
|
|
430
|
+
{ pattern: /^authentication\s*\{?$/i, depth: 4 },
|
|
431
|
+
{ pattern: /^connection-type\s+/i, depth: 4 },
|
|
432
|
+
|
|
433
|
+
// Inside BGP neighbor address-family
|
|
434
|
+
{ pattern: /^ipv4-unicast\s*\{?$/i, depth: 4 },
|
|
435
|
+
{ pattern: /^ipv6-unicast\s*\{?$/i, depth: 4 },
|
|
436
|
+
|
|
437
|
+
// Inside SNMPv3
|
|
438
|
+
{ pattern: /^user\s+\S+\s*\{?$/i, depth: 4 },
|
|
439
|
+
{ pattern: /^group\s+\S+\s*\{?$/i, depth: 4 },
|
|
440
|
+
{ pattern: /^view\s+\S+\s*\{?$/i, depth: 4 },
|
|
441
|
+
|
|
442
|
+
// ============ DEPTH 5: Deepest nesting ============
|
|
443
|
+
|
|
444
|
+
// Inside IPsec tunnel
|
|
445
|
+
{ pattern: /^local\s*\{?$/i, depth: 5 },
|
|
446
|
+
{ pattern: /^remote\s*\{?$/i, depth: 5 },
|
|
447
|
+
{ pattern: /^protocol\s*\{?$/i, depth: 5 },
|
|
448
|
+
],
|
|
449
|
+
|
|
450
|
+
blockEnders: [
|
|
451
|
+
/^\}$/,
|
|
452
|
+
/^\}\s*$/,
|
|
453
|
+
],
|
|
454
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// packages/core/src/types/ConfigNode.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defines the type of a configuration node in the Abstract Syntax Tree (AST).
|
|
5
|
+
* - 'section': Represents a configuration block (e.g., interface, router bgp).
|
|
6
|
+
* - 'command': Represents a single configuration command within a section or globally.
|
|
7
|
+
* - 'comment': Represents a comment line in the configuration.
|
|
8
|
+
* - 'virtual_root': A synthetic node used to wrap orphan commands for rule validation.
|
|
9
|
+
*/
|
|
10
|
+
export type NodeType = 'section' | 'command' | 'comment' | 'virtual_root';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Represents a node in the Abstract Syntax Tree (AST) of a configuration file.
|
|
14
|
+
* This structure normalizes flattened text into a hierarchical tree.
|
|
15
|
+
*/
|
|
16
|
+
export interface ConfigNode {
|
|
17
|
+
/**
|
|
18
|
+
* A unique identifier for the node, typically derived from its raw text or path.
|
|
19
|
+
* Example: "interface GigabitEthernet1"
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The type of the configuration node.
|
|
25
|
+
*/
|
|
26
|
+
type: NodeType;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The original raw text line(s) that this node represents.
|
|
30
|
+
*/
|
|
31
|
+
rawText: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parameters extracted from the rawText, typically the command and its arguments.
|
|
35
|
+
* Example: for "interface Gi0/1", params might be ["interface", "Gi0/1"].
|
|
36
|
+
*/
|
|
37
|
+
params: string[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Child configuration nodes, forming the hierarchical structure.
|
|
41
|
+
*/
|
|
42
|
+
children: ConfigNode[];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Critical for "Snippet Resilience": Indicates if the node originated from
|
|
46
|
+
* the base configuration or a partial snippet.
|
|
47
|
+
*/
|
|
48
|
+
source: 'base' | 'snippet';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Location in the original source file for error reporting and context.
|
|
52
|
+
*/
|
|
53
|
+
loc: {
|
|
54
|
+
startLine: number;
|
|
55
|
+
endLine: number;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The indentation level of the rawText (number of leading whitespace characters).
|
|
60
|
+
* This is crucial for indentation-based parsing.
|
|
61
|
+
*/
|
|
62
|
+
indent: number;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* For section nodes, the block depth from BlockStarterDefs.
|
|
66
|
+
* - 0: Top-level blocks (interface, router, vlan)
|
|
67
|
+
* - 1: Nested blocks (address-family inside router)
|
|
68
|
+
* - 2: Deeply nested (vrf inside address-family)
|
|
69
|
+
* - undefined: For non-section nodes (commands, comments)
|
|
70
|
+
*/
|
|
71
|
+
blockDepth?: number;
|
|
72
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// packages/core/src/types/DeclarativeRule.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SEC-001: Declarative Rule Types
|
|
5
|
+
*
|
|
6
|
+
* Provides a declarative rule format that can be evaluated without
|
|
7
|
+
* executing arbitrary JavaScript code. This is the preferred format
|
|
8
|
+
* for external/untrusted rules as it executes natively with zero overhead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { RuleVendor, RuleMetadata } from './IRule';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Declarative check conditions that can be evaluated safely.
|
|
15
|
+
* These map to common pattern matching and node inspection operations.
|
|
16
|
+
*/
|
|
17
|
+
export type DeclarativeCheck =
|
|
18
|
+
// Pattern matching
|
|
19
|
+
| { type: 'match'; pattern: string; flags?: string }
|
|
20
|
+
| { type: 'not_match'; pattern: string; flags?: string }
|
|
21
|
+
|
|
22
|
+
// Text contains
|
|
23
|
+
| { type: 'contains'; text: string }
|
|
24
|
+
| { type: 'not_contains'; text: string }
|
|
25
|
+
|
|
26
|
+
// Child node existence
|
|
27
|
+
| { type: 'child_exists'; selector: string }
|
|
28
|
+
| { type: 'child_not_exists'; selector: string }
|
|
29
|
+
|
|
30
|
+
// Child text matching
|
|
31
|
+
| { type: 'child_matches'; selector: string; pattern: string; flags?: string }
|
|
32
|
+
| { type: 'child_contains'; selector: string; text: string }
|
|
33
|
+
|
|
34
|
+
// Logical combinators
|
|
35
|
+
| { type: 'and'; conditions: DeclarativeCheck[] }
|
|
36
|
+
| { type: 'or'; conditions: DeclarativeCheck[] }
|
|
37
|
+
| { type: 'not'; condition: DeclarativeCheck }
|
|
38
|
+
|
|
39
|
+
// SEC-001: Custom code (sandboxed execution)
|
|
40
|
+
// Use sparingly - only when declarative checks are insufficient
|
|
41
|
+
| { type: 'custom'; code: string };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A declarative rule definition that can be safely evaluated.
|
|
45
|
+
*
|
|
46
|
+
* Unlike IRule with its JavaScript check function, DeclarativeRule
|
|
47
|
+
* uses a JSON-serializable check condition that can be evaluated
|
|
48
|
+
* without running arbitrary code.
|
|
49
|
+
*/
|
|
50
|
+
export interface DeclarativeRule {
|
|
51
|
+
/** Unique rule identifier */
|
|
52
|
+
id: string;
|
|
53
|
+
|
|
54
|
+
/** Optional selector for node filtering */
|
|
55
|
+
selector?: string;
|
|
56
|
+
|
|
57
|
+
/** Optional vendor(s) this rule applies to */
|
|
58
|
+
vendor?: RuleVendor | RuleVendor[];
|
|
59
|
+
|
|
60
|
+
/** Rule metadata */
|
|
61
|
+
metadata: RuleMetadata;
|
|
62
|
+
|
|
63
|
+
/** The declarative check condition */
|
|
64
|
+
check: DeclarativeCheck;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Type guard to check if an object is a valid DeclarativeCheck.
|
|
69
|
+
*/
|
|
70
|
+
export function isDeclarativeCheck(obj: unknown): obj is DeclarativeCheck {
|
|
71
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const check = obj as Record<string, unknown>;
|
|
76
|
+
|
|
77
|
+
switch (check.type) {
|
|
78
|
+
case 'match':
|
|
79
|
+
case 'not_match':
|
|
80
|
+
return typeof check.pattern === 'string' &&
|
|
81
|
+
(check.flags === undefined || typeof check.flags === 'string');
|
|
82
|
+
|
|
83
|
+
case 'contains':
|
|
84
|
+
case 'not_contains':
|
|
85
|
+
return typeof check.text === 'string';
|
|
86
|
+
|
|
87
|
+
case 'child_exists':
|
|
88
|
+
case 'child_not_exists':
|
|
89
|
+
return typeof check.selector === 'string';
|
|
90
|
+
|
|
91
|
+
case 'child_matches':
|
|
92
|
+
return typeof check.selector === 'string' &&
|
|
93
|
+
typeof check.pattern === 'string' &&
|
|
94
|
+
(check.flags === undefined || typeof check.flags === 'string');
|
|
95
|
+
|
|
96
|
+
case 'child_contains':
|
|
97
|
+
return typeof check.selector === 'string' &&
|
|
98
|
+
typeof check.text === 'string';
|
|
99
|
+
|
|
100
|
+
case 'and':
|
|
101
|
+
case 'or':
|
|
102
|
+
return Array.isArray(check.conditions) &&
|
|
103
|
+
check.conditions.every(isDeclarativeCheck);
|
|
104
|
+
|
|
105
|
+
case 'not':
|
|
106
|
+
return isDeclarativeCheck(check.condition);
|
|
107
|
+
|
|
108
|
+
case 'custom':
|
|
109
|
+
return typeof check.code === 'string';
|
|
110
|
+
|
|
111
|
+
default:
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Type guard to check if an object is a valid DeclarativeRule.
|
|
118
|
+
*/
|
|
119
|
+
export function isDeclarativeRule(obj: unknown): obj is DeclarativeRule {
|
|
120
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const rule = obj as Record<string, unknown>;
|
|
125
|
+
|
|
126
|
+
// Check required fields
|
|
127
|
+
if (typeof rule.id !== 'string' || rule.id.length === 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check optional selector
|
|
132
|
+
if (rule.selector !== undefined && typeof rule.selector !== 'string') {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check metadata
|
|
137
|
+
if (typeof rule.metadata !== 'object' || rule.metadata === null) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const metadata = rule.metadata as Record<string, unknown>;
|
|
142
|
+
if (!['error', 'warning', 'info'].includes(metadata.level as string)) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (typeof metadata.obu !== 'string') {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
if (typeof metadata.owner !== 'string') {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check declarative check
|
|
153
|
+
if (!isDeclarativeCheck(rule.check)) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
}
|