@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,104 @@
1
+ // packages/core/src/parser/Sanitizer.ts
2
+
3
+ import { MAX_LINE_LENGTH } from '../constants';
4
+
5
+ /**
6
+ * Control characters to remove (M-3 fix: Insufficient Input Sanitization).
7
+ * Includes ASCII control characters except tab (\x09) which is valid whitespace.
8
+ * - 0x00-0x08: NUL, SOH, STX, ETX, EOT, ENQ, ACK, BEL, BS
9
+ * - 0x0B: VT (Vertical Tab)
10
+ * - 0x0C: FF (Form Feed)
11
+ * - 0x0E-0x1F: SO, SI, DLE, DC1-DC4, NAK, SYN, ETB, CAN, EM, SUB, ESC, FS, GS, RS, US
12
+ * - 0x7F: DEL
13
+ */
14
+ const CONTROL_CHARS = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g;
15
+
16
+ /**
17
+ * Unicode whitespace characters to normalize to ASCII space.
18
+ * - \u00A0: No-Break Space
19
+ * - \u2000-\u200A: Various width spaces (En Quad through Hair Space)
20
+ * - \u202F: Narrow No-Break Space
21
+ * - \u205F: Medium Mathematical Space
22
+ * - \u3000: Ideographic Space
23
+ */
24
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
25
+
26
+ /**
27
+ * Sanitizes input text by:
28
+ * 1. Removing control characters (security hardening)
29
+ * 2. Replacing Unicode space characters with standard ASCII space
30
+ * 3. Trimming leading/trailing whitespace
31
+ *
32
+ * This function is crucial for ensuring consistent parsing of configuration files
33
+ * that might contain non-standard characters, which could otherwise lead to
34
+ * parsing errors, security issues, or inconsistent matching by regexes.
35
+ *
36
+ * @param text The input string potentially containing various control/whitespace characters.
37
+ * @returns The sanitized string with uniform ASCII spaces and no leading/trailing whitespace.
38
+ */
39
+ export function sanitizeText(text: string): string {
40
+ if (typeof text !== 'string') {
41
+ // Handle non-string input gracefully, though type-checking should prevent this.
42
+ return String(text).trim();
43
+ }
44
+
45
+ return text
46
+ .replace(CONTROL_CHARS, '') // Remove control characters (M-3 fix)
47
+ .replace(UNICODE_SPACES, ' ') // Normalize Unicode spaces
48
+ .trim();
49
+ }
50
+
51
+ /**
52
+ * Parses a line into parameters, handling quoted strings properly.
53
+ * This addresses M-3 where simple split(/\s+/) breaks on quoted passwords/descriptions.
54
+ *
55
+ * SEC-006: Added explicit length check to prevent DoS via very long strings.
56
+ * While upstream MAX_LINE_LENGTH provides protection, direct calls could bypass it.
57
+ *
58
+ * @param line The sanitized line to parse into parameters.
59
+ * @returns An array of parameter strings.
60
+ */
61
+ export function parseParameters(line: string): string[] {
62
+ // SEC-006: Explicit length check to prevent DoS on direct calls
63
+ // that might bypass upstream line length validation
64
+ if (line.length > MAX_LINE_LENGTH) {
65
+ // Return truncated line as single parameter rather than processing
66
+ return [line.slice(0, MAX_LINE_LENGTH)];
67
+ }
68
+
69
+ const params: string[] = [];
70
+ let current = '';
71
+ let inQuote = false;
72
+ let quoteChar = '';
73
+
74
+ for (let i = 0; i < line.length; i++) {
75
+ const char = line[i];
76
+ if (!char) continue;
77
+
78
+ if (!inQuote && (char === '"' || char === "'")) {
79
+ // Start of quoted string
80
+ inQuote = true;
81
+ quoteChar = char;
82
+ } else if (inQuote && char === quoteChar) {
83
+ // End of quoted string
84
+ inQuote = false;
85
+ quoteChar = '';
86
+ } else if (!inQuote && /\s/.test(char)) {
87
+ // Whitespace outside quotes - end of parameter
88
+ if (current.length > 0) {
89
+ params.push(current);
90
+ current = '';
91
+ }
92
+ } else {
93
+ // Regular character - add to current parameter
94
+ current += char;
95
+ }
96
+ }
97
+
98
+ // Don't forget the last parameter
99
+ if (current.length > 0) {
100
+ params.push(current);
101
+ }
102
+
103
+ return params;
104
+ }
@@ -0,0 +1,504 @@
1
+ // packages/core/src/parser/SchemaAwareParser.ts
2
+
3
+ import type { ConfigNode, NodeType } from '../types/ConfigNode';
4
+ import type { VendorSchema, BlockStarterDef } from './VendorSchema';
5
+ import { defaultVendor } from './vendors';
6
+ import { sanitizeText, parseParameters } from './Sanitizer';
7
+ import {
8
+ MAX_LINE_LENGTH,
9
+ MAX_CONFIG_SIZE,
10
+ MAX_NESTING_DEPTH,
11
+ MAX_LINE_COUNT,
12
+ } from '../constants';
13
+ import { SentriflowParseError, SentriflowSizeLimitError } from '../errors';
14
+
15
+ /**
16
+ * Options for the SchemaAwareParser.
17
+ */
18
+ export interface ParserOptions {
19
+ /**
20
+ * The starting line number for the input text, useful for snippets.
21
+ * @default 0
22
+ */
23
+ startLine?: number;
24
+ /**
25
+ * Whether the input is a full configuration ('base') or a partial snippet ('snippet').
26
+ * @default 'base'
27
+ */
28
+ source?: 'base' | 'snippet';
29
+ /**
30
+ * Vendor schema to use for parsing. If not specified, defaults to Cisco IOS.
31
+ * Use detectVendor() to auto-detect the vendor from config content.
32
+ */
33
+ vendor?: VendorSchema;
34
+ }
35
+
36
+ /**
37
+ * Represents a line processed during parsing, including its content and indentation level.
38
+ */
39
+ interface ParsedLine {
40
+ original: string;
41
+ sanitized: string;
42
+ lineNumber: number;
43
+ indent: number;
44
+ isBlockStarter: boolean;
45
+ blockStarterDepth: number;
46
+ isBlockEnder: boolean;
47
+ hasBraceOpen: boolean;
48
+ hasBraceClose: boolean;
49
+ }
50
+
51
+ /**
52
+ * Implements a permissive parser that can interpret hierarchical configuration
53
+ * structures even from flattened text or snippets, using both indentation
54
+ * and schema-aware block starters.
55
+ *
56
+ * Supports both indentation-based hierarchy (Cisco IOS) and brace-based
57
+ * hierarchy (Juniper JunOS) through the VendorSchema system.
58
+ */
59
+ export class SchemaAwareParser {
60
+ private readonly options: Required<Omit<ParserOptions, 'vendor'>>;
61
+ private readonly vendor: VendorSchema;
62
+
63
+ constructor(options?: ParserOptions) {
64
+ this.options = {
65
+ startLine: options?.startLine ?? 0,
66
+ source: options?.source ?? 'base',
67
+ };
68
+ this.vendor = options?.vendor ?? defaultVendor;
69
+ }
70
+
71
+ /**
72
+ * Get the vendor schema being used by this parser instance.
73
+ */
74
+ public getVendor(): VendorSchema {
75
+ return this.vendor;
76
+ }
77
+
78
+ /**
79
+ * Parses the input configuration text into an Abstract Syntax Tree (AST) of ConfigNodes.
80
+ * It attempts to infer hierarchy using indentation and predefined block-starting keywords.
81
+ *
82
+ * For brace-based vendors (Juniper), tracks brace depth to determine hierarchy.
83
+ * For indentation-based vendors (Cisco), uses schema-defined block starters and depth.
84
+ *
85
+ * Security: Validates input size and line lengths to prevent DoS attacks.
86
+ *
87
+ * @param configText The raw configuration text to parse.
88
+ * @returns An array of top-level ConfigNodes representing the parsed configuration.
89
+ * @throws SentriflowSizeLimitError if input exceeds size limits
90
+ */
91
+ public parse(configText: string): ConfigNode[] {
92
+ if (configText.length > MAX_CONFIG_SIZE) {
93
+ throw new SentriflowSizeLimitError(
94
+ `Configuration exceeds maximum size of ${
95
+ MAX_CONFIG_SIZE / 1024 / 1024
96
+ }MB`
97
+ );
98
+ }
99
+
100
+ const lines = configText.split('\n');
101
+
102
+ if (lines.length > MAX_LINE_COUNT) {
103
+ throw new SentriflowSizeLimitError(
104
+ `Configuration exceeds maximum line count of ${MAX_LINE_COUNT}`
105
+ );
106
+ }
107
+
108
+ // Use brace-based parsing for Juniper-style configs
109
+ if (this.vendor.useBraceHierarchy) {
110
+ return this.parseBraceHierarchy(lines);
111
+ }
112
+
113
+ // Use indentation-based parsing for Cisco-style configs
114
+ return this.parseIndentationHierarchy(lines);
115
+ }
116
+
117
+ /**
118
+ * Parses configuration using brace-based hierarchy (Juniper JunOS style).
119
+ * Tracks opening and closing braces to determine block depth.
120
+ */
121
+ private parseBraceHierarchy(lines: string[]): ConfigNode[] {
122
+ const rootNodes: ConfigNode[] = [];
123
+ const parentStack: ConfigNode[] = [];
124
+ let braceDepth = 0;
125
+
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const originalLine = lines[i];
128
+ if (!originalLine) continue;
129
+
130
+ if (originalLine.length > MAX_LINE_LENGTH) {
131
+ continue;
132
+ }
133
+
134
+ const sanitizedLine = sanitizeText(originalLine);
135
+
136
+ // Skip empty lines and comments
137
+ if (sanitizedLine.length === 0 || this.isComment(sanitizedLine)) {
138
+ continue;
139
+ }
140
+
141
+ // Check for braces in the line
142
+ const hasBraceOpen = sanitizedLine.includes('{');
143
+ const hasBraceClose = sanitizedLine.includes('}');
144
+
145
+ // Handle closing brace - pop from stack
146
+ if (hasBraceClose) {
147
+ const closeCount = (sanitizedLine.match(/\}/g) || []).length;
148
+ for (let j = 0; j < closeCount; j++) {
149
+ if (parentStack.length > 0) {
150
+ parentStack.pop();
151
+ }
152
+ braceDepth = Math.max(0, braceDepth - 1);
153
+ }
154
+
155
+ // If line is just a closing brace, continue to next line
156
+ if (sanitizedLine.trim() === '}') {
157
+ continue;
158
+ }
159
+ }
160
+
161
+ // Process content (before opening brace if present)
162
+ let contentLine = sanitizedLine;
163
+ if (hasBraceOpen) {
164
+ // Extract content before the brace
165
+ contentLine = sanitizedLine.replace(/\s*\{.*$/, '').trim();
166
+ }
167
+
168
+ // Skip if no meaningful content
169
+ if (!contentLine || contentLine === '}') {
170
+ // Handle opening brace on its own line or after content
171
+ if (hasBraceOpen) {
172
+ const openCount = (sanitizedLine.match(/\{/g) || []).length;
173
+ braceDepth += openCount;
174
+ }
175
+ continue;
176
+ }
177
+
178
+ // Remove trailing semicolon for cleaner node ID
179
+ const nodeId = contentLine.replace(/;$/, '').trim();
180
+
181
+ // Determine node type - if followed by brace or matches block starter, it's a section
182
+ const blockStarterDepth = this.getBlockStarterDepth(contentLine);
183
+ const isSection = hasBraceOpen || blockStarterDepth >= 0;
184
+ const nodeType: NodeType = isSection ? 'section' : 'command';
185
+
186
+ // Prevent excessive nesting
187
+ if (parentStack.length >= MAX_NESTING_DEPTH) {
188
+ while (parentStack.length >= MAX_NESTING_DEPTH) {
189
+ parentStack.pop();
190
+ }
191
+ }
192
+
193
+ const newNode = this.createConfigNode(
194
+ {
195
+ original: originalLine,
196
+ sanitized: nodeId,
197
+ lineNumber: this.options.startLine + i,
198
+ indent: originalLine.search(/\S|$/),
199
+ isBlockStarter: isSection,
200
+ blockStarterDepth:
201
+ blockStarterDepth >= 0 ? blockStarterDepth : braceDepth,
202
+ isBlockEnder: false,
203
+ hasBraceOpen,
204
+ hasBraceClose,
205
+ },
206
+ nodeType
207
+ );
208
+
209
+ // Add to parent or root
210
+ const currentParent = parentStack.at(-1);
211
+ if (currentParent) {
212
+ currentParent.children.push(newNode);
213
+ } else {
214
+ rootNodes.push(newNode);
215
+ }
216
+
217
+ // If this starts a new block (has opening brace), push to stack
218
+ if (hasBraceOpen && isSection) {
219
+ parentStack.push(newNode);
220
+ const openCount = (sanitizedLine.match(/\{/g) || []).length;
221
+ braceDepth += openCount;
222
+ }
223
+ }
224
+
225
+ return this.applyVirtualContext(rootNodes);
226
+ }
227
+
228
+ /**
229
+ * Parses configuration using indentation-based hierarchy (Cisco IOS style).
230
+ * Uses schema-defined block starters and depth for nesting.
231
+ */
232
+ private parseIndentationHierarchy(lines: string[]): ConfigNode[] {
233
+ const lineContexts: ParsedLine[] = [];
234
+
235
+ for (let i = 0; i < lines.length; i++) {
236
+ const originalLine = lines[i];
237
+ if (!originalLine) continue;
238
+
239
+ if (originalLine.length > MAX_LINE_LENGTH) {
240
+ continue;
241
+ }
242
+
243
+ const sanitizedLine = sanitizeText(originalLine);
244
+
245
+ // Skip empty lines and comments
246
+ if (sanitizedLine.length === 0 || this.isComment(sanitizedLine)) {
247
+ continue;
248
+ }
249
+
250
+ const blockStarterDepth = this.getBlockStarterDepth(sanitizedLine);
251
+ lineContexts.push({
252
+ original: originalLine,
253
+ sanitized: sanitizedLine,
254
+ lineNumber: this.options.startLine + i,
255
+ indent: originalLine.search(/\S|$/),
256
+ isBlockStarter: blockStarterDepth >= 0,
257
+ blockStarterDepth,
258
+ isBlockEnder: this.isSchemaBlockEnder(sanitizedLine),
259
+ hasBraceOpen: false,
260
+ hasBraceClose: false,
261
+ });
262
+ }
263
+
264
+ const rootNodes: ConfigNode[] = [];
265
+ const parentStack: ConfigNode[] = [];
266
+
267
+ for (const currentLine of lineContexts) {
268
+ // SEC-FIX: Context-aware block starter override (CUMULUS_FIX.md)
269
+ // If a depth-0 pattern matches but the line is indented AND we're inside
270
+ // an iface/auto block, treat it as a child command, not a new block.
271
+ // This fixes the issue where "vrf mgmt" inside "iface eth0" was incorrectly
272
+ // parsed as a new top-level section instead of a child command.
273
+ let isBlockStarter = currentLine.isBlockStarter;
274
+ let blockStarterDepth = currentLine.blockStarterDepth;
275
+
276
+ if (isBlockStarter && currentLine.indent > 0) {
277
+ let sectionParent: ConfigNode | undefined;
278
+ for (let i = parentStack.length - 1; i >= 0; i--) {
279
+ const candidate = parentStack[i];
280
+ if (candidate?.blockDepth !== undefined) {
281
+ sectionParent = candidate;
282
+ break;
283
+ }
284
+ }
285
+
286
+ if (sectionParent) {
287
+ const parentType = sectionParent.id.split(/\s+/)[0];
288
+
289
+ // For iface/auto blocks specifically, depth-0 patterns should be commands
290
+ // This must be checked FIRST before multi-depth logic
291
+ if (
292
+ (parentType === 'iface' || parentType === 'auto') &&
293
+ blockStarterDepth === 0
294
+ ) {
295
+ // Override: treat as child command, not new block
296
+ isBlockStarter = false;
297
+ blockStarterDepth = -1;
298
+ } else {
299
+ if (currentLine.indent > sectionParent.indent) {
300
+ const uniqueDepths = [
301
+ ...new Set(
302
+ this.getAllBlockStarterDepths(currentLine.sanitized)
303
+ ),
304
+ ];
305
+ const parentDepth = sectionParent.blockDepth ?? 0;
306
+ const expectedChildDepth = parentDepth + 1;
307
+
308
+ if (uniqueDepths.length > 1) {
309
+ if (uniqueDepths.includes(expectedChildDepth)) {
310
+ blockStarterDepth = expectedChildDepth;
311
+ } else {
312
+ const deeperDepth = Math.min(
313
+ ...uniqueDepths.filter((depth) => depth > parentDepth)
314
+ );
315
+ if (Number.isFinite(deeperDepth)) {
316
+ blockStarterDepth = deeperDepth;
317
+ }
318
+ }
319
+ } else if (
320
+ sectionParent.blockDepth !== undefined &&
321
+ blockStarterDepth <= parentDepth
322
+ ) {
323
+ // Support nested Fortinet config/edit tables by forcing child depth
324
+ blockStarterDepth = expectedChildDepth;
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ const newNodeType: NodeType = isBlockStarter ? 'section' : 'command';
332
+ const modifiedLine: ParsedLine = {
333
+ ...currentLine,
334
+ isBlockStarter,
335
+ blockStarterDepth,
336
+ };
337
+ const newNode = this.createConfigNode(modifiedLine, newNodeType);
338
+
339
+ if (parentStack.length >= MAX_NESTING_DEPTH) {
340
+ while (parentStack.length >= MAX_NESTING_DEPTH) {
341
+ parentStack.pop();
342
+ }
343
+ }
344
+
345
+ while (parentStack.length > 0) {
346
+ const topOfStack = parentStack.at(-1)!;
347
+
348
+ if (isBlockStarter) {
349
+ if (topOfStack.type !== 'section') {
350
+ parentStack.pop();
351
+ } else {
352
+ const parentDepth = topOfStack.blockDepth ?? 0;
353
+ const currentDepth = blockStarterDepth;
354
+
355
+ if (currentDepth > parentDepth) {
356
+ break;
357
+ } else {
358
+ parentStack.pop();
359
+ }
360
+ }
361
+ } else if (currentLine.isBlockEnder) {
362
+ if (topOfStack.type === 'section') {
363
+ parentStack.pop();
364
+ break;
365
+ } else {
366
+ parentStack.pop();
367
+ }
368
+ } else {
369
+ if (topOfStack.type === 'section') {
370
+ break;
371
+ } else if (currentLine.indent <= topOfStack.indent) {
372
+ parentStack.pop();
373
+ } else {
374
+ break;
375
+ }
376
+ }
377
+ }
378
+
379
+ const currentParent = parentStack.at(-1);
380
+
381
+ if (currentParent) {
382
+ currentParent.children.push(newNode);
383
+ } else {
384
+ rootNodes.push(newNode);
385
+ }
386
+
387
+ parentStack.push(newNode);
388
+ }
389
+
390
+ return this.applyVirtualContext(rootNodes);
391
+ }
392
+
393
+ /**
394
+ * Checks if a line is a comment according to the vendor's comment patterns.
395
+ */
396
+ private isComment(sanitizedLine: string): boolean {
397
+ return this.vendor.commentPatterns.some((pattern) =>
398
+ pattern.test(sanitizedLine)
399
+ );
400
+ }
401
+
402
+ /**
403
+ * Returns the block starter depth for a line, or -1 if not a block starter.
404
+ *
405
+ * Returns the FIRST (shallowest) matching pattern. For context-aware decisions
406
+ * (e.g., preferring depth-1 over depth-0 based on parent), see the override logic
407
+ * in parseIndentationHierarchy().
408
+ */
409
+ private getBlockStarterDepth(sanitizedLine: string): number {
410
+ for (const def of this.vendor.blockStarters) {
411
+ if (def.pattern.test(sanitizedLine)) {
412
+ return def.depth;
413
+ }
414
+ }
415
+ return -1;
416
+ }
417
+
418
+ /**
419
+ * Returns ALL matching block starter depths for a line.
420
+ * Used for context-aware parsing where multiple depths may match.
421
+ */
422
+ private getAllBlockStarterDepths(sanitizedLine: string): number[] {
423
+ const depths: number[] = [];
424
+ for (const def of this.vendor.blockStarters) {
425
+ if (def.pattern.test(sanitizedLine)) {
426
+ depths.push(def.depth);
427
+ }
428
+ }
429
+ return depths;
430
+ }
431
+
432
+ /**
433
+ * Checks if a sanitized line matches any of the defined BlockEnders regexes.
434
+ */
435
+ private isSchemaBlockEnder(sanitizedLine: string): boolean {
436
+ return this.vendor.blockEnders.some((regex) => regex.test(sanitizedLine));
437
+ }
438
+
439
+ /**
440
+ * Creates a ConfigNode object from a ParsedLine.
441
+ */
442
+ private createConfigNode(parsedLine: ParsedLine, type: NodeType): ConfigNode {
443
+ const params = parseParameters(parsedLine.sanitized);
444
+ const id = parsedLine.sanitized;
445
+
446
+ const node: ConfigNode = {
447
+ id,
448
+ type,
449
+ rawText: parsedLine.original,
450
+ params,
451
+ children: [],
452
+ source: this.options.source,
453
+ loc: {
454
+ startLine: parsedLine.lineNumber,
455
+ endLine: parsedLine.lineNumber,
456
+ },
457
+ indent: parsedLine.indent,
458
+ };
459
+
460
+ if (type === 'section' && parsedLine.blockStarterDepth >= 0) {
461
+ node.blockDepth = parsedLine.blockStarterDepth;
462
+ }
463
+
464
+ return node;
465
+ }
466
+
467
+ /**
468
+ * Logic to detect "Orphan" commands and wrap them in a `virtual_root`.
469
+ */
470
+ private applyVirtualContext(nodes: ConfigNode[]): ConfigNode[] {
471
+ const processedNodes: ConfigNode[] = [];
472
+ let currentVirtualRoot: ConfigNode | null = null;
473
+
474
+ for (const node of nodes) {
475
+ if (node.type === 'command') {
476
+ if (!currentVirtualRoot) {
477
+ currentVirtualRoot = {
478
+ id: `virtual_root_line_${node.loc.startLine}`,
479
+ type: 'virtual_root',
480
+ rawText: 'virtual_root',
481
+ params: ['virtual_root'],
482
+ children: [],
483
+ source: this.options.source,
484
+ loc: {
485
+ startLine: node.loc.startLine,
486
+ endLine: node.loc.endLine,
487
+ },
488
+ indent: 0,
489
+ };
490
+ processedNodes.push(currentVirtualRoot);
491
+ }
492
+ currentVirtualRoot!.children.push(node);
493
+ currentVirtualRoot!.loc.endLine = Math.max(
494
+ currentVirtualRoot!.loc.endLine,
495
+ node.loc.endLine
496
+ );
497
+ } else {
498
+ currentVirtualRoot = null;
499
+ processedNodes.push(node);
500
+ }
501
+ }
502
+ return processedNodes;
503
+ }
504
+ }
@@ -0,0 +1,72 @@
1
+ // packages/core/src/parser/VendorSchema.ts
2
+
3
+ /**
4
+ * Represents a block-starting pattern with its nesting depth.
5
+ * Depth determines where a block can appear in the hierarchy:
6
+ * - depth 0: Top-level blocks (interface, router, vlan, system, interfaces)
7
+ * - depth 1: Must be inside a depth-0 block (address-family inside router, ge-0/0/0 inside interfaces)
8
+ * - depth 2: Must be inside a depth-1 block (vrf inside address-family, unit inside interface)
9
+ * - depth 3: Deeply nested (family inet inside unit)
10
+ */
11
+ export interface BlockStarterDef {
12
+ pattern: RegExp;
13
+ depth: number;
14
+ }
15
+
16
+ /**
17
+ * Defines a vendor's configuration syntax schema.
18
+ * Each vendor (Cisco IOS, Juniper JunOS, etc.) has different
19
+ * block structures, comment styles, and hierarchy rules.
20
+ *
21
+ * This abstraction allows the parser to handle multiple vendor
22
+ * configuration formats using the same parsing engine.
23
+ */
24
+ export interface VendorSchema {
25
+ /**
26
+ * Unique vendor identifier.
27
+ * Used for programmatic lookup and configuration.
28
+ * @example 'cisco-ios', 'cisco-nxos', 'juniper-junos', 'arista-eos'
29
+ */
30
+ id: string;
31
+
32
+ /**
33
+ * Human-readable display name.
34
+ * @example 'Cisco IOS/IOS-XE', 'Juniper JunOS'
35
+ */
36
+ name: string;
37
+
38
+ /**
39
+ * Block starters with depth information for this vendor.
40
+ * Order matters: more specific patterns should come before generic ones.
41
+ */
42
+ blockStarters: BlockStarterDef[];
43
+
44
+ /**
45
+ * Block enders (exit commands or closing braces).
46
+ * These patterns indicate the end of a configuration block.
47
+ * @example Cisco: /^exit$/i, /^exit-address-family$/i
48
+ * @example Junos: /^\}$/
49
+ */
50
+ blockEnders: RegExp[];
51
+
52
+ /**
53
+ * Comment patterns for this vendor.
54
+ * Lines matching these patterns are treated as comments and skipped.
55
+ * @example Cisco: /^!/, Junos: /^#/, /^\/\*.*\*\/$/
56
+ */
57
+ commentPatterns: RegExp[];
58
+
59
+ /**
60
+ * Optional section delimiter character.
61
+ * Used to separate logical sections in the configuration.
62
+ * @example Cisco: '!', Junos: '}'
63
+ */
64
+ sectionDelimiter?: string;
65
+
66
+ /**
67
+ * Whether this vendor uses braces for hierarchy.
68
+ * - true: Juniper JunOS style with { } blocks
69
+ * - false: Cisco style with indentation-based blocks
70
+ */
71
+ useBraceHierarchy: boolean;
72
+ }