@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,1220 @@
|
|
|
1
|
+
// packages/rule-helpers/src/arista/helpers.ts
|
|
2
|
+
// Arista EOS-specific helper functions
|
|
3
|
+
// Based on Arista Best Practices: docs/Arista-best-practices.md
|
|
4
|
+
|
|
5
|
+
import type { ConfigNode } from '../../types/ConfigNode';
|
|
6
|
+
import {
|
|
7
|
+
hasChildCommand,
|
|
8
|
+
getChildCommand,
|
|
9
|
+
parseIp,
|
|
10
|
+
equalsIgnoreCase,
|
|
11
|
+
includesIgnoreCase,
|
|
12
|
+
startsWithIgnoreCase,
|
|
13
|
+
parseInteger,
|
|
14
|
+
} from '../common/helpers';
|
|
15
|
+
|
|
16
|
+
// Re-export common helpers for convenience
|
|
17
|
+
export { hasChildCommand, getChildCommand, getChildCommands, parseIp } from '../common/helpers';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Management Plane Security Helpers
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if password uses SHA-512 encryption (strong)
|
|
25
|
+
* @param node The config node containing password
|
|
26
|
+
* @returns true if using sha512 encryption
|
|
27
|
+
*/
|
|
28
|
+
export const hasStrongPasswordEncryption = (node: ConfigNode): boolean => {
|
|
29
|
+
return includesIgnoreCase(node.id, 'sha512') || includesIgnoreCase(node.id, '$6$');
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if password is plaintext (cleartext)
|
|
34
|
+
* @param node The config node containing password
|
|
35
|
+
* @returns true if password appears to be plaintext
|
|
36
|
+
*/
|
|
37
|
+
export const hasPlaintextPassword = (node: ConfigNode): boolean => {
|
|
38
|
+
// Check for cleartext password patterns
|
|
39
|
+
if (includesIgnoreCase(node.id, 'secret 0 ') || includesIgnoreCase(node.id, 'password 0 ')) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
// Check for enable password without encryption type
|
|
43
|
+
if (/^enable\s+password\s+[^$]/i.test(node.id)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check if service password-encryption is enabled
|
|
51
|
+
* @param ast The full AST array
|
|
52
|
+
* @returns true if service password-encryption is configured
|
|
53
|
+
*/
|
|
54
|
+
export const hasServicePasswordEncryption = (ast: ConfigNode[]): boolean => {
|
|
55
|
+
return ast.some((node) =>
|
|
56
|
+
equalsIgnoreCase(node.id, 'service password-encryption')
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if SSH version 2 is configured
|
|
62
|
+
* @param ast The full AST array
|
|
63
|
+
* @returns true if SSH v2 is configured
|
|
64
|
+
*/
|
|
65
|
+
export const hasSshVersion2 = (ast: ConfigNode[]): boolean => {
|
|
66
|
+
return ast.some((node) =>
|
|
67
|
+
/^ip\s+ssh\s+version\s+2/i.test(node.id)
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check for weak SSH ciphers
|
|
73
|
+
* @param ast The full AST array
|
|
74
|
+
* @returns Array of weak ciphers found
|
|
75
|
+
*/
|
|
76
|
+
export const getWeakSshCiphers = (ast: ConfigNode[]): string[] => {
|
|
77
|
+
const weakCiphers = ['3des-cbc', 'aes128-cbc', 'aes192-cbc', 'aes256-cbc', 'blowfish-cbc'];
|
|
78
|
+
const found: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const node of ast) {
|
|
81
|
+
if (/^ip\s+ssh\s+ciphers/i.test(node.id)) {
|
|
82
|
+
for (const cipher of weakCiphers) {
|
|
83
|
+
if (includesIgnoreCase(node.id, cipher)) {
|
|
84
|
+
found.push(cipher);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return found;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Check if telnet management is disabled
|
|
94
|
+
* @param ast The full AST array
|
|
95
|
+
* @returns true if telnet is properly disabled
|
|
96
|
+
*/
|
|
97
|
+
export const isTelnetDisabled = (ast: ConfigNode[]): boolean => {
|
|
98
|
+
// Check for 'no management telnet' or management telnet with shutdown
|
|
99
|
+
const noMgmtTelnet = ast.some((node) =>
|
|
100
|
+
/^no\s+management\s+telnet/i.test(node.id)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (noMgmtTelnet) return true;
|
|
104
|
+
|
|
105
|
+
// Check if management telnet section exists and is shutdown
|
|
106
|
+
const mgmtTelnet = ast.find((node) =>
|
|
107
|
+
/^management\s+telnet/i.test(node.id)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (mgmtTelnet) {
|
|
111
|
+
return mgmtTelnet.children.some((child) =>
|
|
112
|
+
equalsIgnoreCase(child.id, 'shutdown')
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Telnet is disabled by default in EOS
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if HTTP server is disabled (insecure)
|
|
122
|
+
* @param ast The full AST array
|
|
123
|
+
* @returns true if HTTP server is disabled
|
|
124
|
+
*/
|
|
125
|
+
export const isHttpServerDisabled = (ast: ConfigNode[]): boolean => {
|
|
126
|
+
const hasNoHttp = ast.some((node) =>
|
|
127
|
+
/^no\s+ip\s+http\s+server/i.test(node.id)
|
|
128
|
+
);
|
|
129
|
+
const hasHttp = ast.some((node) =>
|
|
130
|
+
/^ip\s+http\s+server/i.test(node.id) && !/^no\s+/i.test(node.id)
|
|
131
|
+
);
|
|
132
|
+
return hasNoHttp || !hasHttp;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check for SNMPv1/v2c community strings (insecure)
|
|
137
|
+
* @param ast The full AST array
|
|
138
|
+
* @returns Array of insecure community configurations found
|
|
139
|
+
*/
|
|
140
|
+
export const getInsecureSnmpCommunities = (ast: ConfigNode[]): string[] => {
|
|
141
|
+
const insecure: string[] = [];
|
|
142
|
+
const defaultCommunities = ['public', 'private', 'community'];
|
|
143
|
+
|
|
144
|
+
for (const node of ast) {
|
|
145
|
+
if (/^snmp-server\s+community\s+/i.test(node.id)) {
|
|
146
|
+
const match = node.id.match(/snmp-server\s+community\s+(\S+)/i);
|
|
147
|
+
if (match?.[1]) {
|
|
148
|
+
const community = match[1];
|
|
149
|
+
if (defaultCommunities.some((dc) => equalsIgnoreCase(community, dc))) {
|
|
150
|
+
insecure.push(`Default community "${match[1]}"`);
|
|
151
|
+
} else {
|
|
152
|
+
insecure.push(`SNMPv2c community configured`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return insecure;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if SNMPv3 is properly configured with auth and priv
|
|
162
|
+
* @param ast The full AST array
|
|
163
|
+
* @returns true if SNMPv3 with priv mode is configured
|
|
164
|
+
*/
|
|
165
|
+
export const hasSnmpV3AuthPriv = (ast: ConfigNode[]): boolean => {
|
|
166
|
+
return ast.some((node) =>
|
|
167
|
+
/^snmp-server\s+group\s+\S+\s+v3\s+priv/i.test(node.id) ||
|
|
168
|
+
/^snmp-server\s+user\s+\S+\s+\S+\s+v3\s+auth\s+\S+\s+\S+\s+priv/i.test(node.id)
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if NTP authentication is enabled
|
|
174
|
+
* @param ast The full AST array
|
|
175
|
+
* @returns true if NTP authentication is configured
|
|
176
|
+
*/
|
|
177
|
+
export const hasNtpAuthentication = (ast: ConfigNode[]): boolean => {
|
|
178
|
+
const hasAuthenticate = ast.some((node) =>
|
|
179
|
+
/^ntp\s+authenticate$/i.test(node.id)
|
|
180
|
+
);
|
|
181
|
+
const hasTrustedKey = ast.some((node) =>
|
|
182
|
+
/^ntp\s+trusted-key\s+/i.test(node.id)
|
|
183
|
+
);
|
|
184
|
+
return hasAuthenticate && hasTrustedKey;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Check if AAA authentication login is configured
|
|
189
|
+
* @param ast The full AST array
|
|
190
|
+
* @returns true if AAA authentication login is configured
|
|
191
|
+
*/
|
|
192
|
+
export const hasAaaAuthenticationLogin = (ast: ConfigNode[]): boolean => {
|
|
193
|
+
return ast.some((node) =>
|
|
194
|
+
/^aaa\s+authentication\s+login\s+/i.test(node.id)
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check if TACACS+ is configured
|
|
200
|
+
* @param ast The full AST array
|
|
201
|
+
* @returns true if TACACS+ server is configured
|
|
202
|
+
*/
|
|
203
|
+
export const hasTacacsServer = (ast: ConfigNode[]): boolean => {
|
|
204
|
+
return ast.some((node) =>
|
|
205
|
+
/^tacacs-server\s+host\s+/i.test(node.id)
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if AAA accounting is configured
|
|
211
|
+
* @param ast The full AST array
|
|
212
|
+
* @returns true if AAA accounting is configured
|
|
213
|
+
*/
|
|
214
|
+
export const hasAaaAccounting = (ast: ConfigNode[]): boolean => {
|
|
215
|
+
return ast.some((node) =>
|
|
216
|
+
/^aaa\s+accounting\s+/i.test(node.id)
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check if Management VRF is configured
|
|
222
|
+
* @param ast The full AST array
|
|
223
|
+
* @returns true if management VRF is properly configured
|
|
224
|
+
*/
|
|
225
|
+
export const hasManagementVrf = (ast: ConfigNode[]): boolean => {
|
|
226
|
+
return ast.some((node) =>
|
|
227
|
+
/^vrf\s+instance\s+MGMT/i.test(node.id) ||
|
|
228
|
+
/^vrf\s+instance\s+management/i.test(node.id)
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Check if login banner reveals system information (non-compliant)
|
|
234
|
+
* @param ast The full AST array
|
|
235
|
+
* @returns Array of information disclosure issues found
|
|
236
|
+
*/
|
|
237
|
+
export const getBannerInfoDisclosure = (ast: ConfigNode[]): string[] => {
|
|
238
|
+
const issues: string[] = [];
|
|
239
|
+
const sensitivePatterns = [
|
|
240
|
+
{ pattern: /version\s+\d+/i, desc: 'software version' },
|
|
241
|
+
{ pattern: /arista/i, desc: 'vendor name' },
|
|
242
|
+
{ pattern: /eos/i, desc: 'OS name' },
|
|
243
|
+
{ pattern: /@\S+\.\S+/i, desc: 'email address' },
|
|
244
|
+
{ pattern: /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/, desc: 'IP address' },
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
for (const node of ast) {
|
|
248
|
+
if (/^banner\s+(login|motd)/i.test(node.id)) {
|
|
249
|
+
const bannerText = node.rawText || node.id;
|
|
250
|
+
for (const { pattern, desc } of sensitivePatterns) {
|
|
251
|
+
if (pattern.test(bannerText)) {
|
|
252
|
+
issues.push(`Banner contains ${desc}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return issues;
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if console idle timeout is configured
|
|
262
|
+
* @param ast The full AST array
|
|
263
|
+
* @returns The timeout value in minutes, or undefined if not set
|
|
264
|
+
*/
|
|
265
|
+
export const getConsoleIdleTimeout = (ast: ConfigNode[]): number | undefined => {
|
|
266
|
+
for (const node of ast) {
|
|
267
|
+
if (/^management\s+console/i.test(node.id)) {
|
|
268
|
+
for (const child of node.children) {
|
|
269
|
+
const match = child.id.match(/idle-timeout\s+(\d+)/i);
|
|
270
|
+
if (match?.[1]) {
|
|
271
|
+
return parseInteger(match[1]) ?? undefined;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return undefined;
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Check if ZTP (Zero Touch Provisioning) is disabled
|
|
281
|
+
* @param ast The full AST array
|
|
282
|
+
* @returns true if ZTP is disabled
|
|
283
|
+
*/
|
|
284
|
+
export const isZtpDisabled = (ast: ConfigNode[]): boolean => {
|
|
285
|
+
return ast.some((node) =>
|
|
286
|
+
/^no\s+zerotouch\s+enable/i.test(node.id) ||
|
|
287
|
+
equalsIgnoreCase(node.id, 'zerotouch cancel')
|
|
288
|
+
);
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// Control Plane Security Helpers
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Check if Control Plane ACL is configured
|
|
297
|
+
* @param ast The full AST array
|
|
298
|
+
* @returns true if system control-plane ACL is configured
|
|
299
|
+
*/
|
|
300
|
+
export const hasControlPlaneAcl = (ast: ConfigNode[]): boolean => {
|
|
301
|
+
return ast.some((node) =>
|
|
302
|
+
/^system\s+control-plane/i.test(node.id)
|
|
303
|
+
);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Check if CoPP (Control Plane Policing) is customized
|
|
308
|
+
* @param ast The full AST array
|
|
309
|
+
* @returns true if CoPP policy is customized
|
|
310
|
+
*/
|
|
311
|
+
export const hasCoppPolicy = (ast: ConfigNode[]): boolean => {
|
|
312
|
+
return ast.some((node) =>
|
|
313
|
+
/^policy-map\s+type\s+copp/i.test(node.id)
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Check if interface has ICMP redirects disabled
|
|
319
|
+
* @param interfaceNode The interface ConfigNode
|
|
320
|
+
* @returns true if ip redirects are disabled
|
|
321
|
+
*/
|
|
322
|
+
export const hasNoIpRedirects = (interfaceNode: ConfigNode): boolean => {
|
|
323
|
+
return interfaceNode.children.some((child) =>
|
|
324
|
+
/^no\s+ip\s+redirects/i.test(child.id)
|
|
325
|
+
);
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Check if interface has ICMP unreachables disabled
|
|
330
|
+
* @param interfaceNode The interface ConfigNode
|
|
331
|
+
* @returns true if ip unreachables are disabled
|
|
332
|
+
*/
|
|
333
|
+
export const hasNoIpUnreachables = (interfaceNode: ConfigNode): boolean => {
|
|
334
|
+
return interfaceNode.children.some((child) =>
|
|
335
|
+
/^no\s+ip\s+unreachables/i.test(child.id)
|
|
336
|
+
);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if routing protocol has authentication configured
|
|
341
|
+
* @param routerNode The router ConfigNode (OSPF, IS-IS, etc.)
|
|
342
|
+
* @returns true if authentication is configured
|
|
343
|
+
*/
|
|
344
|
+
export const hasRoutingProtocolAuth = (routerNode: ConfigNode): boolean => {
|
|
345
|
+
// Check for OSPF authentication
|
|
346
|
+
if (/^router\s+ospf/i.test(routerNode.id)) {
|
|
347
|
+
return routerNode.children.some((child) =>
|
|
348
|
+
/authentication/i.test(child.id)
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Check for IS-IS authentication
|
|
353
|
+
if (/^router\s+isis/i.test(routerNode.id)) {
|
|
354
|
+
return routerNode.children.some((child) =>
|
|
355
|
+
/authentication/i.test(child.id)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return false;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Check if BFD (Bidirectional Forwarding Detection) is configured
|
|
364
|
+
* @param ast The full AST array
|
|
365
|
+
* @returns true if BFD is configured
|
|
366
|
+
*/
|
|
367
|
+
export const hasBfd = (ast: ConfigNode[]): boolean => {
|
|
368
|
+
return ast.some((node) =>
|
|
369
|
+
/^bfd\s+/i.test(node.id)
|
|
370
|
+
);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Data Plane Security Helpers
|
|
375
|
+
// ============================================================================
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Check if interface has storm control configured
|
|
379
|
+
* @param interfaceNode The interface ConfigNode
|
|
380
|
+
* @returns Object with storm control status for each type
|
|
381
|
+
*/
|
|
382
|
+
export const getStormControlStatus = (interfaceNode: ConfigNode): { broadcast: boolean; multicast: boolean; unicast: boolean } => {
|
|
383
|
+
return {
|
|
384
|
+
broadcast: interfaceNode.children.some((child) =>
|
|
385
|
+
/^storm-control\s+broadcast/i.test(child.id)
|
|
386
|
+
),
|
|
387
|
+
multicast: interfaceNode.children.some((child) =>
|
|
388
|
+
/^storm-control\s+multicast/i.test(child.id)
|
|
389
|
+
),
|
|
390
|
+
unicast: interfaceNode.children.some((child) =>
|
|
391
|
+
/^storm-control\s+unknown-unicast/i.test(child.id)
|
|
392
|
+
),
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if DHCP snooping is enabled
|
|
398
|
+
* @param ast The full AST array
|
|
399
|
+
* @returns true if DHCP snooping is configured
|
|
400
|
+
*/
|
|
401
|
+
export const hasDhcpSnooping = (ast: ConfigNode[]): boolean => {
|
|
402
|
+
return ast.some((node) =>
|
|
403
|
+
/^ip\s+dhcp\s+snooping$/i.test(node.id)
|
|
404
|
+
);
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Check if interface is DHCP snooping trusted
|
|
409
|
+
* @param interfaceNode The interface ConfigNode
|
|
410
|
+
* @returns true if interface is DHCP snooping trusted
|
|
411
|
+
*/
|
|
412
|
+
export const isDhcpSnoopingTrust = (interfaceNode: ConfigNode): boolean => {
|
|
413
|
+
return interfaceNode.children.some((child) =>
|
|
414
|
+
/^ip\s+dhcp\s+snooping\s+trust/i.test(child.id)
|
|
415
|
+
);
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check if Dynamic ARP Inspection is enabled
|
|
420
|
+
* @param ast The full AST array
|
|
421
|
+
* @returns true if DAI is configured
|
|
422
|
+
*/
|
|
423
|
+
export const hasDynamicArpInspection = (ast: ConfigNode[]): boolean => {
|
|
424
|
+
return ast.some((node) =>
|
|
425
|
+
/^ip\s+arp\s+inspection\s+vlan/i.test(node.id)
|
|
426
|
+
);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Check if interface is ARP inspection trusted
|
|
431
|
+
* @param interfaceNode The interface ConfigNode
|
|
432
|
+
* @returns true if interface is ARP inspection trusted
|
|
433
|
+
*/
|
|
434
|
+
export const isArpInspectionTrust = (interfaceNode: ConfigNode): boolean => {
|
|
435
|
+
return interfaceNode.children.some((child) =>
|
|
436
|
+
/^ip\s+arp\s+inspection\s+trust/i.test(child.id)
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if IP Source Guard is enabled on interface
|
|
442
|
+
* @param interfaceNode The interface ConfigNode
|
|
443
|
+
* @returns true if IP verify source is configured
|
|
444
|
+
*/
|
|
445
|
+
export const hasIpSourceGuard = (interfaceNode: ConfigNode): boolean => {
|
|
446
|
+
return interfaceNode.children.some((child) =>
|
|
447
|
+
/^ip\s+verify\s+source/i.test(child.id)
|
|
448
|
+
);
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Check if port security is enabled on interface
|
|
453
|
+
* @param interfaceNode The interface ConfigNode
|
|
454
|
+
* @returns true if port security is configured
|
|
455
|
+
*/
|
|
456
|
+
export const hasPortSecurity = (interfaceNode: ConfigNode): boolean => {
|
|
457
|
+
return interfaceNode.children.some((child) =>
|
|
458
|
+
/^switchport\s+port-security/i.test(child.id)
|
|
459
|
+
);
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// BGP Security Helpers
|
|
464
|
+
// ============================================================================
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check if BGP neighbor has MD5/password authentication
|
|
468
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
469
|
+
* @param neighborIp Optional specific neighbor IP to check
|
|
470
|
+
* @returns Array of neighbors without authentication
|
|
471
|
+
*/
|
|
472
|
+
export const getBgpNeighborsWithoutAuth = (routerBgpNode: ConfigNode, neighborIp?: string): string[] => {
|
|
473
|
+
const neighborsWithoutAuth: string[] = [];
|
|
474
|
+
const neighborConfigs = new Map<string, { hasPassword: boolean }>();
|
|
475
|
+
|
|
476
|
+
for (const child of routerBgpNode.children) {
|
|
477
|
+
const neighborMatch = child.id.match(/^neighbor\s+(\S+)/i);
|
|
478
|
+
if (neighborMatch?.[1]) {
|
|
479
|
+
const ip = neighborMatch[1];
|
|
480
|
+
if (!neighborConfigs.has(ip)) {
|
|
481
|
+
neighborConfigs.set(ip, { hasPassword: false });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (/password/i.test(child.id)) {
|
|
485
|
+
const config = neighborConfigs.get(ip);
|
|
486
|
+
if (config) {
|
|
487
|
+
config.hasPassword = true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
for (const [ip, config] of neighborConfigs) {
|
|
494
|
+
if (!config.hasPassword) {
|
|
495
|
+
if (!neighborIp || ip === neighborIp) {
|
|
496
|
+
neighborsWithoutAuth.push(ip);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return neighborsWithoutAuth;
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Check if BGP neighbor has TTL security (GTSM) configured
|
|
506
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
507
|
+
* @returns Array of neighbors without TTL security
|
|
508
|
+
*/
|
|
509
|
+
export const getBgpNeighborsWithoutTtlSecurity = (routerBgpNode: ConfigNode): string[] => {
|
|
510
|
+
const neighborsWithoutTtl: string[] = [];
|
|
511
|
+
const neighborConfigs = new Map<string, { hasTtl: boolean; isEbgp: boolean }>();
|
|
512
|
+
const localAs = routerBgpNode.id.match(/router\s+bgp\s+(\d+)/i)?.[1];
|
|
513
|
+
|
|
514
|
+
for (const child of routerBgpNode.children) {
|
|
515
|
+
const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as\s+(\d+)/i);
|
|
516
|
+
if (neighborMatch?.[1] && neighborMatch?.[2]) {
|
|
517
|
+
const ip = neighborMatch[1];
|
|
518
|
+
const remoteAs = neighborMatch[2];
|
|
519
|
+
neighborConfigs.set(ip, {
|
|
520
|
+
hasTtl: false,
|
|
521
|
+
isEbgp: localAs !== remoteAs
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const ttlMatch = child.id.match(/^neighbor\s+(\S+)\s+ttl\s+maximum-hops/i);
|
|
526
|
+
if (ttlMatch?.[1]) {
|
|
527
|
+
const config = neighborConfigs.get(ttlMatch[1]);
|
|
528
|
+
if (config) {
|
|
529
|
+
config.hasTtl = true;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
for (const [ip, config] of neighborConfigs) {
|
|
535
|
+
if (config.isEbgp && !config.hasTtl) {
|
|
536
|
+
neighborsWithoutTtl.push(ip);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return neighborsWithoutTtl;
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Check if BGP neighbor has maximum-routes configured
|
|
545
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
546
|
+
* @returns Array of neighbors without max-prefix limit
|
|
547
|
+
*/
|
|
548
|
+
export const getBgpNeighborsWithoutMaxRoutes = (routerBgpNode: ConfigNode): string[] => {
|
|
549
|
+
const neighborsWithoutMax: string[] = [];
|
|
550
|
+
const neighborConfigs = new Map<string, boolean>();
|
|
551
|
+
|
|
552
|
+
for (const child of routerBgpNode.children) {
|
|
553
|
+
const neighborMatch = child.id.match(/^neighbor\s+(\S+)\s+remote-as/i);
|
|
554
|
+
if (neighborMatch?.[1]) {
|
|
555
|
+
neighborConfigs.set(neighborMatch[1], false);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const maxMatch = child.id.match(/^neighbor\s+(\S+)\s+maximum-routes/i);
|
|
559
|
+
if (maxMatch?.[1]) {
|
|
560
|
+
neighborConfigs.set(maxMatch[1], true);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
for (const [ip, hasMax] of neighborConfigs) {
|
|
565
|
+
if (!hasMax) {
|
|
566
|
+
neighborsWithoutMax.push(ip);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return neighborsWithoutMax;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Check if BGP has graceful restart configured
|
|
575
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
576
|
+
* @returns true if graceful restart is configured
|
|
577
|
+
*/
|
|
578
|
+
export const hasBgpGracefulRestart = (routerBgpNode: ConfigNode): boolean => {
|
|
579
|
+
return routerBgpNode.children.some((child) =>
|
|
580
|
+
/^bgp\s+graceful-restart/i.test(child.id)
|
|
581
|
+
);
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Check if BGP has log-neighbor-changes configured
|
|
586
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
587
|
+
* @returns true if log-neighbor-changes is configured
|
|
588
|
+
*/
|
|
589
|
+
export const hasBgpLogNeighborChanges = (routerBgpNode: ConfigNode): boolean => {
|
|
590
|
+
return routerBgpNode.children.some((child) =>
|
|
591
|
+
/^bgp\s+log-neighbor-changes/i.test(child.id)
|
|
592
|
+
);
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
// ============================================================================
|
|
596
|
+
// RPKI Helpers
|
|
597
|
+
// ============================================================================
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Check if RPKI is configured
|
|
601
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
602
|
+
* @returns true if RPKI cache is configured
|
|
603
|
+
*/
|
|
604
|
+
export const hasRpkiConfiguration = (routerBgpNode: ConfigNode): boolean => {
|
|
605
|
+
return routerBgpNode.children.some((child) =>
|
|
606
|
+
/^rpki\s+cache/i.test(child.id)
|
|
607
|
+
);
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Check if RPKI origin validation is enabled
|
|
612
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
613
|
+
* @returns true if origin validation is configured
|
|
614
|
+
*/
|
|
615
|
+
export const hasRpkiOriginValidation = (routerBgpNode: ConfigNode): boolean => {
|
|
616
|
+
return routerBgpNode.children.some((child) =>
|
|
617
|
+
/^rpki\s+origin-validation/i.test(child.id)
|
|
618
|
+
);
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// ============================================================================
|
|
622
|
+
// Anti-Spoofing Helpers
|
|
623
|
+
// ============================================================================
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Check if interface has uRPF (unicast RPF) configured
|
|
627
|
+
* @param interfaceNode The interface ConfigNode
|
|
628
|
+
* @returns Object with uRPF mode if configured
|
|
629
|
+
*/
|
|
630
|
+
export const getUrpfMode = (interfaceNode: ConfigNode): { enabled: boolean; mode?: 'strict' | 'loose' } => {
|
|
631
|
+
for (const child of interfaceNode.children) {
|
|
632
|
+
if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+rx/i.test(child.id)) {
|
|
633
|
+
return { enabled: true, mode: 'strict' };
|
|
634
|
+
}
|
|
635
|
+
if (/^ip\s+verify\s+unicast\s+source\s+reachable-via\s+any/i.test(child.id)) {
|
|
636
|
+
return { enabled: true, mode: 'loose' };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return { enabled: false };
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
// ============================================================================
|
|
643
|
+
// MLAG Helpers (existing, enhanced)
|
|
644
|
+
// ============================================================================
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Check if MLAG dual-primary detection is configured
|
|
648
|
+
* @param mlagNode The MLAG configuration node
|
|
649
|
+
* @returns true if dual-primary detection is configured
|
|
650
|
+
*/
|
|
651
|
+
export const hasMlagDualPrimaryDetection = (mlagNode: ConfigNode): boolean => {
|
|
652
|
+
return hasChildCommand(mlagNode, 'dual-primary detection');
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Check if MLAG reload delays are configured
|
|
657
|
+
* @param mlagNode The MLAG configuration node
|
|
658
|
+
* @returns Object with reload delay configuration status
|
|
659
|
+
*/
|
|
660
|
+
export const getMlagReloadDelays = (mlagNode: ConfigNode): { mlag: boolean; nonMlag: boolean } => {
|
|
661
|
+
return {
|
|
662
|
+
mlag: hasChildCommand(mlagNode, 'reload-delay mlag'),
|
|
663
|
+
nonMlag: hasChildCommand(mlagNode, 'reload-delay non-mlag'),
|
|
664
|
+
};
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// ============================================================================
|
|
668
|
+
// VXLAN/EVPN Helpers (existing, enhanced)
|
|
669
|
+
// ============================================================================
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Check if EVPN peers have password authentication
|
|
673
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
674
|
+
* @returns true if EVPN peer group has password
|
|
675
|
+
*/
|
|
676
|
+
export const hasEvpnPeerAuth = (routerBgpNode: ConfigNode): boolean => {
|
|
677
|
+
// Look for EVPN peer group with password
|
|
678
|
+
let evpnPeerGroup: string | undefined;
|
|
679
|
+
|
|
680
|
+
for (const child of routerBgpNode.children) {
|
|
681
|
+
// Find EVPN address family activation
|
|
682
|
+
if (/^address-family\s+evpn/i.test(child.id)) {
|
|
683
|
+
for (const subchild of child.children) {
|
|
684
|
+
const match = subchild.id.match(/neighbor\s+(\S+)\s+activate/i);
|
|
685
|
+
if (match?.[1]) {
|
|
686
|
+
evpnPeerGroup = match[1];
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (!evpnPeerGroup) return false;
|
|
693
|
+
|
|
694
|
+
// Check if peer group has password
|
|
695
|
+
return routerBgpNode.children.some((child) =>
|
|
696
|
+
includesIgnoreCase(child.id, `neighbor ${evpnPeerGroup}`) &&
|
|
697
|
+
includesIgnoreCase(child.id, 'password')
|
|
698
|
+
);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// Logging/Monitoring Helpers
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Check if logging is configured with specific level
|
|
707
|
+
* @param ast The full AST array
|
|
708
|
+
* @param minLevel Minimum required logging level
|
|
709
|
+
* @returns true if logging meets minimum level requirement
|
|
710
|
+
*/
|
|
711
|
+
export const hasLoggingLevel = (ast: ConfigNode[], minLevel: string): boolean => {
|
|
712
|
+
const levels = ['emergencies', 'alerts', 'critical', 'errors', 'warnings', 'notifications', 'informational', 'debugging'];
|
|
713
|
+
const minIndex = levels.indexOf(minLevel.toLowerCase());
|
|
714
|
+
|
|
715
|
+
for (const node of ast) {
|
|
716
|
+
const match = node.id.match(/^logging\s+(?:buffered|trap)\s+(\S+)/i);
|
|
717
|
+
if (match?.[1]) {
|
|
718
|
+
const configuredIndex = levels.indexOf(match[1].toLowerCase());
|
|
719
|
+
if (configuredIndex >= minIndex) {
|
|
720
|
+
return true;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Check if logging source interface is configured
|
|
729
|
+
* @param ast The full AST array
|
|
730
|
+
* @returns true if logging source-interface is configured
|
|
731
|
+
*/
|
|
732
|
+
export const hasLoggingSourceInterface = (ast: ConfigNode[]): boolean => {
|
|
733
|
+
return ast.some((node) =>
|
|
734
|
+
/^logging\s+source-interface/i.test(node.id)
|
|
735
|
+
);
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Check if event-monitor is enabled
|
|
740
|
+
* @param ast The full AST array
|
|
741
|
+
* @returns true if event-monitor is configured
|
|
742
|
+
*/
|
|
743
|
+
export const hasEventMonitor = (ast: ConfigNode[]): boolean => {
|
|
744
|
+
return ast.some((node) =>
|
|
745
|
+
/^event-monitor$/i.test(node.id)
|
|
746
|
+
);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// ============================================================================
|
|
750
|
+
// High Availability Helpers
|
|
751
|
+
// ============================================================================
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Check if VRRP has authentication configured
|
|
755
|
+
* @param interfaceNode The interface ConfigNode
|
|
756
|
+
* @returns true if VRRP authentication is configured
|
|
757
|
+
*/
|
|
758
|
+
export const hasVrrpAuthentication = (interfaceNode: ConfigNode): boolean => {
|
|
759
|
+
return interfaceNode.children.some((child) =>
|
|
760
|
+
/^vrrp\s+\d+\s+authentication/i.test(child.id)
|
|
761
|
+
);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Check if virtual-router MAC is configured (for MLAG VARP)
|
|
766
|
+
* @param ast The full AST array
|
|
767
|
+
* @returns true if ip virtual-router mac-address is configured
|
|
768
|
+
*/
|
|
769
|
+
export const hasVirtualRouterMac = (ast: ConfigNode[]): boolean => {
|
|
770
|
+
return ast.some((node) =>
|
|
771
|
+
/^ip\s+virtual-router\s+mac-address/i.test(node.id)
|
|
772
|
+
);
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
// ============================================================================
|
|
776
|
+
// Interface Type Helpers (extending existing)
|
|
777
|
+
// ============================================================================
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Check if interface is a WAN/external facing interface
|
|
781
|
+
* Based on description containing WAN, Internet, ISP, External keywords
|
|
782
|
+
* @param interfaceNode The interface ConfigNode
|
|
783
|
+
* @returns true if interface appears to be external facing
|
|
784
|
+
*/
|
|
785
|
+
export const isExternalInterface = (interfaceNode: ConfigNode): boolean => {
|
|
786
|
+
const description = interfaceNode.children.find((child) =>
|
|
787
|
+
startsWithIgnoreCase(child.id, 'description ')
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (description) {
|
|
791
|
+
return (
|
|
792
|
+
includesIgnoreCase(description.id, 'wan') ||
|
|
793
|
+
includesIgnoreCase(description.id, 'internet') ||
|
|
794
|
+
includesIgnoreCase(description.id, 'isp') ||
|
|
795
|
+
includesIgnoreCase(description.id, 'external') ||
|
|
796
|
+
includesIgnoreCase(description.id, 'uplink') ||
|
|
797
|
+
includesIgnoreCase(description.id, 'peering')
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return false;
|
|
802
|
+
};
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Check if interface is an access (edge/endpoint) port
|
|
806
|
+
* @param interfaceNode The interface ConfigNode
|
|
807
|
+
* @returns true if interface is configured as access port
|
|
808
|
+
*/
|
|
809
|
+
export const isAccessPort = (interfaceNode: ConfigNode): boolean => {
|
|
810
|
+
return interfaceNode.children.some((child) =>
|
|
811
|
+
/^switchport\s+mode\s+access/i.test(child.id)
|
|
812
|
+
);
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Check if interface is a trunk port
|
|
817
|
+
* @param interfaceNode The interface ConfigNode
|
|
818
|
+
* @returns true if interface is configured as trunk port
|
|
819
|
+
*/
|
|
820
|
+
export const isTrunkPort = (interfaceNode: ConfigNode): boolean => {
|
|
821
|
+
return interfaceNode.children.some((child) =>
|
|
822
|
+
/^switchport\s+mode\s+trunk/i.test(child.id)
|
|
823
|
+
);
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Check if MLAG is configured
|
|
828
|
+
* @param ast The full AST array
|
|
829
|
+
* @returns true if mlag configuration block exists
|
|
830
|
+
*/
|
|
831
|
+
export const hasMlagConfiguration = (ast: ConfigNode[]): boolean => {
|
|
832
|
+
return ast.some((node) =>
|
|
833
|
+
startsWithIgnoreCase(node.id, 'mlag configuration')
|
|
834
|
+
);
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Get MLAG configuration node
|
|
839
|
+
* @param ast The full AST array
|
|
840
|
+
* @returns The MLAG configuration node, or undefined
|
|
841
|
+
*/
|
|
842
|
+
export const getMlagConfiguration = (
|
|
843
|
+
ast: ConfigNode[]
|
|
844
|
+
): ConfigNode | undefined => {
|
|
845
|
+
return ast.find((node) =>
|
|
846
|
+
startsWithIgnoreCase(node.id, 'mlag configuration')
|
|
847
|
+
);
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Check if MLAG has required settings (domain-id, peer-link, peer-address)
|
|
852
|
+
* @param mlagNode The MLAG configuration node
|
|
853
|
+
* @returns Object with status of each MLAG requirement
|
|
854
|
+
*/
|
|
855
|
+
export const checkMlagRequirements = (
|
|
856
|
+
mlagNode: ConfigNode
|
|
857
|
+
): { hasDomainId: boolean; hasPeerLink: boolean; hasPeerAddress: boolean; hasLocalInterface: boolean } => {
|
|
858
|
+
return {
|
|
859
|
+
hasDomainId: hasChildCommand(mlagNode, 'domain-id'),
|
|
860
|
+
hasPeerLink: hasChildCommand(mlagNode, 'peer-link'),
|
|
861
|
+
hasPeerAddress: hasChildCommand(mlagNode, 'peer-address'),
|
|
862
|
+
hasLocalInterface: hasChildCommand(mlagNode, 'local-interface'),
|
|
863
|
+
};
|
|
864
|
+
};
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Check if management API (eAPI) is configured
|
|
868
|
+
* @param ast The full AST array
|
|
869
|
+
* @returns true if management api http-commands is configured
|
|
870
|
+
*/
|
|
871
|
+
export const hasManagementApi = (ast: ConfigNode[]): boolean => {
|
|
872
|
+
return ast.some((node) =>
|
|
873
|
+
startsWithIgnoreCase(node.id, 'management api')
|
|
874
|
+
);
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Get management API configuration nodes
|
|
879
|
+
* @param ast The full AST array
|
|
880
|
+
* @returns Array of management API configuration nodes
|
|
881
|
+
*/
|
|
882
|
+
export const getManagementApiNodes = (ast: ConfigNode[]): ConfigNode[] => {
|
|
883
|
+
return ast.filter((node) =>
|
|
884
|
+
startsWithIgnoreCase(node.id, 'management api')
|
|
885
|
+
);
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
/**
|
|
889
|
+
* Check if management API has HTTPS enabled (secure)
|
|
890
|
+
* @param apiNode The management api configuration node
|
|
891
|
+
* @returns true if HTTPS transport is configured
|
|
892
|
+
*/
|
|
893
|
+
export const hasHttpsTransport = (apiNode: ConfigNode): boolean => {
|
|
894
|
+
// Check for "protocol https" or "no shutdown" with https
|
|
895
|
+
const hasProtocolHttps = apiNode.children.some((child) =>
|
|
896
|
+
includesIgnoreCase(child.id, 'protocol https')
|
|
897
|
+
);
|
|
898
|
+
const hasTransportHttps = apiNode.children.some((child) =>
|
|
899
|
+
includesIgnoreCase(child.id, 'transport https')
|
|
900
|
+
);
|
|
901
|
+
return hasProtocolHttps || hasTransportHttps;
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Check if an interface is a VXLAN interface
|
|
906
|
+
* @param node The interface ConfigNode
|
|
907
|
+
* @returns true if it's a VXLAN interface
|
|
908
|
+
*/
|
|
909
|
+
export const isVxlanInterface = (node: ConfigNode): boolean => {
|
|
910
|
+
return /^interface\s+Vxlan\d*/i.test(node.id);
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Check if an interface is an MLAG peer-link (typically Port-Channel)
|
|
915
|
+
* @param node The interface ConfigNode
|
|
916
|
+
* @param mlagNode The MLAG configuration node (optional)
|
|
917
|
+
* @returns true if this interface is configured as MLAG peer-link
|
|
918
|
+
*/
|
|
919
|
+
export const isMlagPeerLink = (
|
|
920
|
+
node: ConfigNode,
|
|
921
|
+
mlagNode?: ConfigNode
|
|
922
|
+
): boolean => {
|
|
923
|
+
if (!mlagNode) return false;
|
|
924
|
+
const peerLink = getChildCommand(mlagNode, 'peer-link');
|
|
925
|
+
if (!peerLink) return false;
|
|
926
|
+
|
|
927
|
+
// Extract interface name from peer-link command
|
|
928
|
+
const match = peerLink.id.match(/peer-link\s+(\S+)/i);
|
|
929
|
+
if (!match) return false;
|
|
930
|
+
|
|
931
|
+
const peerLinkInterface = match[1];
|
|
932
|
+
if (!peerLinkInterface) return false;
|
|
933
|
+
return includesIgnoreCase(node.id, peerLinkInterface);
|
|
934
|
+
};
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Get all VXLAN VNI mappings from a Vxlan interface
|
|
938
|
+
* @param vxlanNode The Vxlan interface ConfigNode
|
|
939
|
+
* @returns Array of VNI mappings
|
|
940
|
+
*/
|
|
941
|
+
export const getVxlanVniMappings = (
|
|
942
|
+
vxlanNode: ConfigNode
|
|
943
|
+
): { vni: string; vlan?: string }[] => {
|
|
944
|
+
const mappings: { vni: string; vlan?: string }[] = [];
|
|
945
|
+
|
|
946
|
+
for (const child of vxlanNode.children) {
|
|
947
|
+
const vniMatch = child.id.match(/vxlan\s+vni\s+(\d+)\s+vlan\s+(\d+)/i);
|
|
948
|
+
if (vniMatch) {
|
|
949
|
+
const vni = vniMatch[1];
|
|
950
|
+
if (!vni) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
const vlan = vniMatch[2];
|
|
954
|
+
mappings.push({ vni, vlan });
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const simpleMatch = child.id.match(/vxlan\s+vni\s+(\d+)/i);
|
|
959
|
+
if (simpleMatch) {
|
|
960
|
+
const vni = simpleMatch[1];
|
|
961
|
+
if (!vni) {
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
mappings.push({ vni });
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return mappings;
|
|
969
|
+
};
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Check if VXLAN has source interface configured
|
|
973
|
+
* @param vxlanNode The Vxlan interface ConfigNode
|
|
974
|
+
* @returns true if vxlan source-interface is configured
|
|
975
|
+
*/
|
|
976
|
+
export const hasVxlanSourceInterface = (vxlanNode: ConfigNode): boolean => {
|
|
977
|
+
return hasChildCommand(vxlanNode, 'vxlan source-interface');
|
|
978
|
+
};
|
|
979
|
+
|
|
980
|
+
/**
|
|
981
|
+
* Check if interface has MLAG ID configured
|
|
982
|
+
* @param interfaceNode The interface ConfigNode
|
|
983
|
+
* @returns The MLAG ID if configured, undefined otherwise
|
|
984
|
+
*/
|
|
985
|
+
export const getMlagId = (interfaceNode: ConfigNode): string | undefined => {
|
|
986
|
+
const mlagCmd = interfaceNode.children.find((child) =>
|
|
987
|
+
/^mlag\s+\d+/i.test(child.id)
|
|
988
|
+
);
|
|
989
|
+
if (!mlagCmd) return undefined;
|
|
990
|
+
|
|
991
|
+
const match = mlagCmd.id.match(/mlag\s+(\d+)/i);
|
|
992
|
+
return match ? match[1] : undefined;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
/**
|
|
996
|
+
* Check if interface is a Port-Channel
|
|
997
|
+
* @param node The interface ConfigNode
|
|
998
|
+
* @returns true if it's a Port-Channel interface
|
|
999
|
+
*/
|
|
1000
|
+
export const isPortChannel = (node: ConfigNode): boolean => {
|
|
1001
|
+
return /^interface\s+Port-Channel\d+/i.test(node.id);
|
|
1002
|
+
};
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Check if interface is a Loopback
|
|
1006
|
+
* @param node The interface ConfigNode
|
|
1007
|
+
* @returns true if it's a Loopback interface
|
|
1008
|
+
*/
|
|
1009
|
+
export const isLoopback = (node: ConfigNode): boolean => {
|
|
1010
|
+
return /^interface\s+Loopback\d+/i.test(node.id);
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Check if interface is an SVI (VLAN interface)
|
|
1015
|
+
* @param node The interface ConfigNode
|
|
1016
|
+
* @returns true if it's a VLAN SVI
|
|
1017
|
+
*/
|
|
1018
|
+
export const isSvi = (node: ConfigNode): boolean => {
|
|
1019
|
+
return /^interface\s+Vlan\d+/i.test(node.id);
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
/**
|
|
1023
|
+
* Check if interface is a Management interface
|
|
1024
|
+
* @param node The interface ConfigNode
|
|
1025
|
+
* @returns true if it's a Management interface
|
|
1026
|
+
*/
|
|
1027
|
+
export const isManagementInterface = (node: ConfigNode): boolean => {
|
|
1028
|
+
return /^interface\s+Management\d+/i.test(node.id);
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
/**
|
|
1032
|
+
* Check if interface is an Ethernet port
|
|
1033
|
+
* @param node The interface ConfigNode
|
|
1034
|
+
* @returns true if it's an Ethernet interface
|
|
1035
|
+
*/
|
|
1036
|
+
export const isEthernetInterface = (node: ConfigNode): boolean => {
|
|
1037
|
+
return /^interface\s+Ethernet\d+/i.test(node.id);
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Check if daemon is configured
|
|
1042
|
+
* @param ast The full AST array
|
|
1043
|
+
* @param daemonName Optional specific daemon name to check
|
|
1044
|
+
* @returns true if daemon(s) are configured
|
|
1045
|
+
*/
|
|
1046
|
+
export const hasDaemon = (ast: ConfigNode[], daemonName?: string): boolean => {
|
|
1047
|
+
if (daemonName) {
|
|
1048
|
+
return ast.some((node) =>
|
|
1049
|
+
equalsIgnoreCase(node.id, `daemon ${daemonName}`)
|
|
1050
|
+
);
|
|
1051
|
+
}
|
|
1052
|
+
return ast.some((node) => startsWithIgnoreCase(node.id, 'daemon '));
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Check if event-handler is configured
|
|
1057
|
+
* @param ast The full AST array
|
|
1058
|
+
* @returns true if event-handler(s) are configured
|
|
1059
|
+
*/
|
|
1060
|
+
export const hasEventHandler = (ast: ConfigNode[]): boolean => {
|
|
1061
|
+
return ast.some((node) => startsWithIgnoreCase(node.id, 'event-handler '));
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Get all VRF instances
|
|
1066
|
+
* @param ast The full AST array
|
|
1067
|
+
* @returns Array of VRF instance nodes
|
|
1068
|
+
*/
|
|
1069
|
+
export const getVrfInstances = (ast: ConfigNode[]): ConfigNode[] => {
|
|
1070
|
+
return ast.filter((node) =>
|
|
1071
|
+
/^vrf\s+instance\s+\S+/i.test(node.id)
|
|
1072
|
+
);
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Check if interface is in a VRF
|
|
1077
|
+
* @param interfaceNode The interface ConfigNode
|
|
1078
|
+
* @returns The VRF name if configured, undefined otherwise
|
|
1079
|
+
*/
|
|
1080
|
+
export const getInterfaceVrf = (interfaceNode: ConfigNode): string | undefined => {
|
|
1081
|
+
const vrfCmd = interfaceNode.children.find((child) =>
|
|
1082
|
+
/^vrf\s+\S+/i.test(child.id)
|
|
1083
|
+
);
|
|
1084
|
+
if (!vrfCmd) return undefined;
|
|
1085
|
+
|
|
1086
|
+
const match = vrfCmd.id.match(/vrf\s+(\S+)/i);
|
|
1087
|
+
return match ? match[1] : undefined;
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
/**
|
|
1091
|
+
* Check if BGP EVPN is configured
|
|
1092
|
+
* @param routerBgpNode The router bgp ConfigNode
|
|
1093
|
+
* @returns true if EVPN address-family is configured
|
|
1094
|
+
*/
|
|
1095
|
+
export const hasEvpnAddressFamily = (routerBgpNode: ConfigNode): boolean => {
|
|
1096
|
+
return routerBgpNode.children.some((child) =>
|
|
1097
|
+
/^address-family\s+evpn/i.test(child.id)
|
|
1098
|
+
);
|
|
1099
|
+
};
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Check if interface has IP virtual-router address (VARP)
|
|
1103
|
+
* @param interfaceNode The interface ConfigNode
|
|
1104
|
+
* @returns true if ip virtual-router address is configured
|
|
1105
|
+
*/
|
|
1106
|
+
export const hasVirtualRouterAddress = (interfaceNode: ConfigNode): boolean => {
|
|
1107
|
+
return interfaceNode.children.some((child) =>
|
|
1108
|
+
/^ip\s+virtual-router\s+address/i.test(child.id)
|
|
1109
|
+
);
|
|
1110
|
+
};
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Check if interface has ip address configured
|
|
1114
|
+
* @param interfaceNode The interface ConfigNode
|
|
1115
|
+
* @returns true if ip address is configured
|
|
1116
|
+
*/
|
|
1117
|
+
export const hasIpAddress = (interfaceNode: ConfigNode): boolean => {
|
|
1118
|
+
return interfaceNode.children.some((child) =>
|
|
1119
|
+
/^ip\s+address\s+\d+\.\d+\.\d+\.\d+/i.test(child.id)
|
|
1120
|
+
);
|
|
1121
|
+
};
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* Check if interface is shutdown
|
|
1125
|
+
* @param interfaceNode The interface ConfigNode
|
|
1126
|
+
* @returns true if interface is shutdown
|
|
1127
|
+
*/
|
|
1128
|
+
export const isShutdown = (interfaceNode: ConfigNode): boolean => {
|
|
1129
|
+
const hasShutdown = interfaceNode.children.some((child) =>
|
|
1130
|
+
equalsIgnoreCase(child.id, 'shutdown')
|
|
1131
|
+
);
|
|
1132
|
+
const hasNoShutdown = interfaceNode.children.some((child) =>
|
|
1133
|
+
equalsIgnoreCase(child.id, 'no shutdown')
|
|
1134
|
+
);
|
|
1135
|
+
return hasShutdown && !hasNoShutdown;
|
|
1136
|
+
};
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Get interface description
|
|
1140
|
+
* @param interfaceNode The interface ConfigNode
|
|
1141
|
+
* @returns The description if configured, undefined otherwise
|
|
1142
|
+
*/
|
|
1143
|
+
export const getInterfaceDescription = (interfaceNode: ConfigNode): string | undefined => {
|
|
1144
|
+
const descCmd = interfaceNode.children.find((child) =>
|
|
1145
|
+
startsWithIgnoreCase(child.id, 'description ')
|
|
1146
|
+
);
|
|
1147
|
+
if (!descCmd) return undefined;
|
|
1148
|
+
|
|
1149
|
+
return descCmd.id.replace(/^description\s+/i, '').trim();
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Check if NTP is configured
|
|
1154
|
+
* @param ast The full AST array
|
|
1155
|
+
* @returns true if NTP server(s) are configured
|
|
1156
|
+
*/
|
|
1157
|
+
export const hasNtpServer = (ast: ConfigNode[]): boolean => {
|
|
1158
|
+
return ast.some((node) =>
|
|
1159
|
+
/^ntp\s+server\s+/i.test(node.id)
|
|
1160
|
+
);
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
/**
|
|
1164
|
+
* Check if syslog/logging is configured
|
|
1165
|
+
* @param ast The full AST array
|
|
1166
|
+
* @returns true if logging host is configured
|
|
1167
|
+
*/
|
|
1168
|
+
export const hasLoggingHost = (ast: ConfigNode[]): boolean => {
|
|
1169
|
+
return ast.some((node) =>
|
|
1170
|
+
/^logging\s+host\s+/i.test(node.id)
|
|
1171
|
+
);
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Check if SNMP is configured
|
|
1176
|
+
* @param ast The full AST array
|
|
1177
|
+
* @returns true if SNMP is configured
|
|
1178
|
+
*/
|
|
1179
|
+
export const hasSnmpServer = (ast: ConfigNode[]): boolean => {
|
|
1180
|
+
return ast.some((node) =>
|
|
1181
|
+
/^snmp-server\s+/i.test(node.id)
|
|
1182
|
+
);
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
/**
|
|
1186
|
+
* Check if AAA is configured
|
|
1187
|
+
* @param ast The full AST array
|
|
1188
|
+
* @returns true if AAA is configured
|
|
1189
|
+
*/
|
|
1190
|
+
export const hasAaa = (ast: ConfigNode[]): boolean => {
|
|
1191
|
+
return ast.some((node) =>
|
|
1192
|
+
/^aaa\s+/i.test(node.id)
|
|
1193
|
+
);
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* Check if spanning-tree is configured
|
|
1198
|
+
* @param ast The full AST array
|
|
1199
|
+
* @returns true if spanning-tree is configured
|
|
1200
|
+
*/
|
|
1201
|
+
export const hasSpanningTree = (ast: ConfigNode[]): boolean => {
|
|
1202
|
+
return ast.some((node) =>
|
|
1203
|
+
/^spanning-tree\s+/i.test(node.id)
|
|
1204
|
+
);
|
|
1205
|
+
};
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* Get spanning-tree mode
|
|
1209
|
+
* @param ast The full AST array
|
|
1210
|
+
* @returns The spanning-tree mode (mstp, rapid-pvst, none, etc.)
|
|
1211
|
+
*/
|
|
1212
|
+
export const getSpanningTreeMode = (ast: ConfigNode[]): string | undefined => {
|
|
1213
|
+
const stpNode = ast.find((node) =>
|
|
1214
|
+
/^spanning-tree\s+mode\s+/i.test(node.id)
|
|
1215
|
+
);
|
|
1216
|
+
if (!stpNode) return undefined;
|
|
1217
|
+
|
|
1218
|
+
const match = stpNode.id.match(/spanning-tree\s+mode\s+(\S+)/i);
|
|
1219
|
+
return match ? match[1] : undefined;
|
|
1220
|
+
};
|