@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,312 @@
|
|
|
1
|
+
// packages/core/src/engine/Runner.ts
|
|
2
|
+
|
|
3
|
+
import type { ConfigNode } from '../types/ConfigNode';
|
|
4
|
+
import type { IRule, RuleResult, Context } from '../types/IRule';
|
|
5
|
+
import { RuleExecutor } from './RuleExecutor';
|
|
6
|
+
import type { ExecutionOptions } from './RuleExecutor';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Index structure for fast rule lookup.
|
|
10
|
+
* Reduces selector matching from O(N×R) to O(N×k) where k << R.
|
|
11
|
+
*/
|
|
12
|
+
interface RuleIndex {
|
|
13
|
+
/** Rules indexed by first keyword of selector (lowercase) */
|
|
14
|
+
byPrefix: Map<string, IRule[]>;
|
|
15
|
+
/** Rules with no selector (global rules that run on all nodes) */
|
|
16
|
+
global: IRule[];
|
|
17
|
+
/** Rules indexed by exact selector match (lowercase) */
|
|
18
|
+
exact: Map<string, IRule[]>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Options for RuleEngine.
|
|
23
|
+
*/
|
|
24
|
+
export interface EngineOptions {
|
|
25
|
+
/** Enable timeout protection for rule execution */
|
|
26
|
+
enableTimeoutProtection?: boolean;
|
|
27
|
+
/** Options for the rule executor (timeout settings) */
|
|
28
|
+
executionOptions?: ExecutionOptions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Rule Engine with selector indexing for high-performance rule evaluation.
|
|
33
|
+
*
|
|
34
|
+
* Performance characteristics:
|
|
35
|
+
* - Index build: O(R) where R = number of rules
|
|
36
|
+
* - Per-node lookup: O(1) average case (hash map lookup)
|
|
37
|
+
* - Total scan: O(N×k) where N = nodes, k = average matching rules per node
|
|
38
|
+
*
|
|
39
|
+
* For 500 rules with good selector distribution, k is typically 5-20,
|
|
40
|
+
* giving ~25-100× improvement over naive O(N×R) scanning.
|
|
41
|
+
*/
|
|
42
|
+
export class RuleEngine {
|
|
43
|
+
private index: RuleIndex | null = null;
|
|
44
|
+
private indexedRules: IRule[] = [];
|
|
45
|
+
private indexVersion = 0;
|
|
46
|
+
private executor: RuleExecutor | null = null;
|
|
47
|
+
private options: EngineOptions;
|
|
48
|
+
|
|
49
|
+
constructor(options: EngineOptions = {}) {
|
|
50
|
+
this.options = {
|
|
51
|
+
enableTimeoutProtection: options.enableTimeoutProtection ?? false,
|
|
52
|
+
executionOptions: options.executionOptions,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (this.options.enableTimeoutProtection) {
|
|
56
|
+
this.executor = new RuleExecutor(this.options.executionOptions);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build an index for fast rule lookup.
|
|
62
|
+
* Call this once when rules change, not on every scan.
|
|
63
|
+
*
|
|
64
|
+
* @param rules The rules to index
|
|
65
|
+
*/
|
|
66
|
+
public buildIndex(rules: IRule[]): void {
|
|
67
|
+
this.index = {
|
|
68
|
+
byPrefix: new Map(),
|
|
69
|
+
global: [],
|
|
70
|
+
exact: new Map(),
|
|
71
|
+
};
|
|
72
|
+
this.indexedRules = rules;
|
|
73
|
+
this.indexVersion++;
|
|
74
|
+
|
|
75
|
+
for (const rule of rules) {
|
|
76
|
+
if (!rule.selector) {
|
|
77
|
+
// No selector = global rule, runs on everything
|
|
78
|
+
this.index.global.push(rule);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const selector = rule.selector.toLowerCase();
|
|
83
|
+
|
|
84
|
+
// Index by exact selector
|
|
85
|
+
const exactBucket = this.index.exact.get(selector);
|
|
86
|
+
if (exactBucket) {
|
|
87
|
+
exactBucket.push(rule);
|
|
88
|
+
} else {
|
|
89
|
+
this.index.exact.set(selector, [rule]);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Index by first word (prefix) for partial matches
|
|
93
|
+
const prefix = selector.split(/\s+/)[0] ?? selector;
|
|
94
|
+
const prefixBucket = this.index.byPrefix.get(prefix);
|
|
95
|
+
if (prefixBucket) {
|
|
96
|
+
prefixBucket.push(rule);
|
|
97
|
+
} else {
|
|
98
|
+
this.index.byPrefix.set(prefix, [rule]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get rules that might match a node.
|
|
105
|
+
* Returns a small subset of total rules based on prefix matching.
|
|
106
|
+
*
|
|
107
|
+
* @param node The configuration node to find candidate rules for
|
|
108
|
+
* @returns Array of rules that may match the node
|
|
109
|
+
*/
|
|
110
|
+
private getCandidateRules(node: ConfigNode): IRule[] {
|
|
111
|
+
if (!this.index) {
|
|
112
|
+
return this.indexedRules; // Fallback to all rules if no index
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const nodeId = node.id.toLowerCase();
|
|
116
|
+
const nodePrefix = nodeId.split(/\s+/)[0] ?? nodeId;
|
|
117
|
+
|
|
118
|
+
// Start with global rules (always run)
|
|
119
|
+
const candidates: IRule[] = [...this.index.global];
|
|
120
|
+
|
|
121
|
+
// Add rules matching the node's prefix
|
|
122
|
+
const prefixRules = this.index.byPrefix.get(nodePrefix);
|
|
123
|
+
if (prefixRules) {
|
|
124
|
+
candidates.push(...prefixRules);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return candidates;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Run rules against nodes using the index for fast lookup.
|
|
132
|
+
*
|
|
133
|
+
* @param nodes The root nodes of the configuration AST
|
|
134
|
+
* @param rules Optional rules array - if provided and different from indexed rules, rebuilds index
|
|
135
|
+
* @param context Optional global context to pass to rules
|
|
136
|
+
* @returns Array of RuleResult objects
|
|
137
|
+
*/
|
|
138
|
+
public run(
|
|
139
|
+
nodes: ConfigNode[],
|
|
140
|
+
rules?: IRule[],
|
|
141
|
+
context: Partial<Context> = {}
|
|
142
|
+
): RuleResult[] {
|
|
143
|
+
// Rebuild index if rules provided and different
|
|
144
|
+
if (rules && rules !== this.indexedRules) {
|
|
145
|
+
this.buildIndex(rules);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If no rules indexed, return empty results
|
|
149
|
+
if (this.indexedRules.length === 0) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const results: RuleResult[] = [];
|
|
154
|
+
|
|
155
|
+
// Create context once with lazy AST getter
|
|
156
|
+
const ruleContext: Context = {
|
|
157
|
+
...context,
|
|
158
|
+
getAst: () => nodes,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const visit = (node: ConfigNode): void => {
|
|
162
|
+
// Only check candidate rules, not all rules
|
|
163
|
+
const candidates = this.getCandidateRules(node);
|
|
164
|
+
|
|
165
|
+
for (const rule of candidates) {
|
|
166
|
+
if (this.matchesSelector(node, rule.selector)) {
|
|
167
|
+
// Use executor if timeout protection is enabled
|
|
168
|
+
if (this.executor) {
|
|
169
|
+
const result = this.executor.execute(rule, node, ruleContext);
|
|
170
|
+
if (result) {
|
|
171
|
+
results.push(result);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// Direct execution without timeout protection
|
|
175
|
+
try {
|
|
176
|
+
const result = rule.check(node, ruleContext);
|
|
177
|
+
if (result) {
|
|
178
|
+
results.push(result);
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
results.push({
|
|
182
|
+
passed: false,
|
|
183
|
+
message: `Rule execution error: ${
|
|
184
|
+
error instanceof Error ? error.message : String(error)
|
|
185
|
+
}`,
|
|
186
|
+
ruleId: rule.id,
|
|
187
|
+
nodeId: node.id,
|
|
188
|
+
level: 'error',
|
|
189
|
+
loc: node.loc,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Recurse into children
|
|
197
|
+
for (const child of node.children) {
|
|
198
|
+
visit(child);
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
for (const node of nodes) {
|
|
203
|
+
visit(node);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Checks if a node matches a rule's selector.
|
|
211
|
+
* Uses case-insensitive prefix matching.
|
|
212
|
+
*
|
|
213
|
+
* @param node The configuration node
|
|
214
|
+
* @param selector The selector string (e.g., "interface", "router bgp")
|
|
215
|
+
* @returns True if the node matches the selector
|
|
216
|
+
*/
|
|
217
|
+
private matchesSelector(node: ConfigNode, selector?: string): boolean {
|
|
218
|
+
if (!selector) return true;
|
|
219
|
+
return node.id.toLowerCase().startsWith(selector.toLowerCase());
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if the index needs rebuilding.
|
|
224
|
+
* Useful for determining when to call buildIndex().
|
|
225
|
+
*
|
|
226
|
+
* @param rules The rules to check against
|
|
227
|
+
* @returns True if the rules differ from the indexed rules
|
|
228
|
+
*/
|
|
229
|
+
public needsReindex(rules: IRule[]): boolean {
|
|
230
|
+
return rules !== this.indexedRules;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the current index version.
|
|
235
|
+
* Increments each time buildIndex() is called.
|
|
236
|
+
*/
|
|
237
|
+
public getIndexVersion(): number {
|
|
238
|
+
return this.indexVersion;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get statistics about the current index.
|
|
243
|
+
* Useful for debugging and performance monitoring.
|
|
244
|
+
*/
|
|
245
|
+
public getIndexStats(): {
|
|
246
|
+
totalRules: number;
|
|
247
|
+
globalRules: number;
|
|
248
|
+
prefixBuckets: number;
|
|
249
|
+
exactBuckets: number;
|
|
250
|
+
avgRulesPerPrefix: number;
|
|
251
|
+
} {
|
|
252
|
+
if (!this.index) {
|
|
253
|
+
return {
|
|
254
|
+
totalRules: 0,
|
|
255
|
+
globalRules: 0,
|
|
256
|
+
prefixBuckets: 0,
|
|
257
|
+
exactBuckets: 0,
|
|
258
|
+
avgRulesPerPrefix: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const prefixBuckets = this.index.byPrefix.size;
|
|
263
|
+
let totalPrefixRules = 0;
|
|
264
|
+
for (const bucket of this.index.byPrefix.values()) {
|
|
265
|
+
totalPrefixRules += bucket.length;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
totalRules: this.indexedRules.length,
|
|
270
|
+
globalRules: this.index.global.length,
|
|
271
|
+
prefixBuckets,
|
|
272
|
+
exactBuckets: this.index.exact.size,
|
|
273
|
+
avgRulesPerPrefix: prefixBuckets > 0 ? totalPrefixRules / prefixBuckets : 0,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Clear the index and release memory.
|
|
279
|
+
*/
|
|
280
|
+
public clearIndex(): void {
|
|
281
|
+
this.index = null;
|
|
282
|
+
this.indexedRules = [];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Get the rule executor (if timeout protection is enabled).
|
|
287
|
+
*/
|
|
288
|
+
public getExecutor(): RuleExecutor | null {
|
|
289
|
+
return this.executor;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Get list of auto-disabled rules (if timeout protection is enabled).
|
|
294
|
+
*/
|
|
295
|
+
public getDisabledRules(): string[] {
|
|
296
|
+
return this.executor?.getDisabledRules() ?? [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Re-enable a previously disabled rule.
|
|
301
|
+
*/
|
|
302
|
+
public enableRule(ruleId: string): void {
|
|
303
|
+
this.executor?.enableRule(ruleId);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Reset executor state (timeout counts, disabled rules).
|
|
308
|
+
*/
|
|
309
|
+
public resetExecutor(): void {
|
|
310
|
+
this.executor?.resetAll();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// packages/core/src/engine/SandboxedExecutor.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SEC-001: Sandboxed Executor for Declarative Rules
|
|
5
|
+
*
|
|
6
|
+
* Provides safe evaluation of declarative rules without executing
|
|
7
|
+
* arbitrary JavaScript. Custom code blocks run in a VM sandbox
|
|
8
|
+
* with strict timeouts and limited API access.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createContext, Script, type Context as VMContext } from 'vm';
|
|
12
|
+
import type { ConfigNode } from '../types/ConfigNode';
|
|
13
|
+
import type { Context } from '../types/IRule';
|
|
14
|
+
import type { IRule, RuleResult, RuleVendor } from '../types/IRule';
|
|
15
|
+
import type { DeclarativeRule, DeclarativeCheck } from '../types/DeclarativeRule';
|
|
16
|
+
|
|
17
|
+
/** Timeout for custom code execution in milliseconds */
|
|
18
|
+
const CUSTOM_CODE_TIMEOUT_MS = 100;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Compiles a DeclarativeRule into an IRule with a safe check function.
|
|
22
|
+
*
|
|
23
|
+
* For most declarative checks, this produces a native function that
|
|
24
|
+
* executes without any sandboxing overhead. Only 'custom' type checks
|
|
25
|
+
* require VM sandboxing.
|
|
26
|
+
*/
|
|
27
|
+
export class SandboxedExecutor {
|
|
28
|
+
private readonly sandbox: Record<string, unknown>;
|
|
29
|
+
private readonly vmContext: VMContext;
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.sandbox = this.createSandbox();
|
|
33
|
+
this.vmContext = createContext(this.sandbox);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Compiles a declarative rule into an executable IRule.
|
|
38
|
+
*/
|
|
39
|
+
compileRule(decl: DeclarativeRule): IRule {
|
|
40
|
+
return {
|
|
41
|
+
id: decl.id,
|
|
42
|
+
selector: decl.selector,
|
|
43
|
+
vendor: decl.vendor as RuleVendor | RuleVendor[] | undefined,
|
|
44
|
+
metadata: decl.metadata,
|
|
45
|
+
check: (node: ConfigNode, ctx: Context): RuleResult => {
|
|
46
|
+
return this.evaluate(decl.check, node, ctx, decl);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evaluates a declarative check condition against a node.
|
|
53
|
+
*/
|
|
54
|
+
private evaluate(
|
|
55
|
+
check: DeclarativeCheck,
|
|
56
|
+
node: ConfigNode,
|
|
57
|
+
ctx: Context,
|
|
58
|
+
rule: DeclarativeRule
|
|
59
|
+
): RuleResult {
|
|
60
|
+
const passed = this.evaluateCondition(check, node);
|
|
61
|
+
return {
|
|
62
|
+
passed,
|
|
63
|
+
message: passed
|
|
64
|
+
? `${rule.id}: Check passed`
|
|
65
|
+
: `${rule.id}: Check failed - ${rule.metadata.remediation ?? 'See rule documentation'}`,
|
|
66
|
+
ruleId: rule.id,
|
|
67
|
+
nodeId: node.id,
|
|
68
|
+
level: rule.metadata.level,
|
|
69
|
+
loc: node.loc,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Recursively evaluates a declarative check condition.
|
|
75
|
+
* Returns true if the condition is satisfied.
|
|
76
|
+
*/
|
|
77
|
+
private evaluateCondition(check: DeclarativeCheck, node: ConfigNode): boolean {
|
|
78
|
+
switch (check.type) {
|
|
79
|
+
case 'match':
|
|
80
|
+
return new RegExp(check.pattern, check.flags).test(node.id);
|
|
81
|
+
|
|
82
|
+
case 'not_match':
|
|
83
|
+
return !new RegExp(check.pattern, check.flags).test(node.id);
|
|
84
|
+
|
|
85
|
+
case 'contains':
|
|
86
|
+
return node.id.includes(check.text);
|
|
87
|
+
|
|
88
|
+
case 'not_contains':
|
|
89
|
+
return !node.id.includes(check.text);
|
|
90
|
+
|
|
91
|
+
case 'child_exists':
|
|
92
|
+
return node.children.some(c =>
|
|
93
|
+
c.id.toLowerCase().startsWith(check.selector.toLowerCase())
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
case 'child_not_exists':
|
|
97
|
+
return !node.children.some(c =>
|
|
98
|
+
c.id.toLowerCase().startsWith(check.selector.toLowerCase())
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
case 'child_matches': {
|
|
102
|
+
const matchingChildren = node.children.filter(c =>
|
|
103
|
+
c.id.toLowerCase().startsWith(check.selector.toLowerCase())
|
|
104
|
+
);
|
|
105
|
+
const regex = new RegExp(check.pattern, check.flags);
|
|
106
|
+
return matchingChildren.some(c => regex.test(c.id));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
case 'child_contains': {
|
|
110
|
+
const containsChildren = node.children.filter(c =>
|
|
111
|
+
c.id.toLowerCase().startsWith(check.selector.toLowerCase())
|
|
112
|
+
);
|
|
113
|
+
return containsChildren.some(c => c.id.includes(check.text));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'and':
|
|
117
|
+
return check.conditions.every(c => this.evaluateCondition(c, node));
|
|
118
|
+
|
|
119
|
+
case 'or':
|
|
120
|
+
return check.conditions.some(c => this.evaluateCondition(c, node));
|
|
121
|
+
|
|
122
|
+
case 'not':
|
|
123
|
+
return !this.evaluateCondition(check.condition, node);
|
|
124
|
+
|
|
125
|
+
case 'custom':
|
|
126
|
+
return this.executeCustomCode(check.code, node);
|
|
127
|
+
|
|
128
|
+
default:
|
|
129
|
+
// Unknown check type - fail closed for safety
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Executes custom code in a VM sandbox.
|
|
136
|
+
* Returns the boolean result of the code execution.
|
|
137
|
+
*/
|
|
138
|
+
private executeCustomCode(code: string, node: ConfigNode): boolean {
|
|
139
|
+
// Prepare a frozen copy of the node for the sandbox
|
|
140
|
+
this.sandbox.node = Object.freeze({
|
|
141
|
+
id: node.id,
|
|
142
|
+
type: node.type,
|
|
143
|
+
children: node.children.map(c => Object.freeze({
|
|
144
|
+
id: c.id,
|
|
145
|
+
type: c.type,
|
|
146
|
+
})),
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const wrappedCode = `(function() { ${code} })()`;
|
|
151
|
+
const script = new Script(wrappedCode, {
|
|
152
|
+
filename: 'custom-check.js',
|
|
153
|
+
timeout: CUSTOM_CODE_TIMEOUT_MS,
|
|
154
|
+
} as { filename: string; timeout: number });
|
|
155
|
+
const result = script.runInContext(this.vmContext);
|
|
156
|
+
return Boolean(result);
|
|
157
|
+
} catch {
|
|
158
|
+
// Any error (timeout, syntax, runtime) = fail closed
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Creates a minimal sandbox for custom code execution.
|
|
165
|
+
* Only provides safe, read-only access to basic JavaScript features.
|
|
166
|
+
*/
|
|
167
|
+
private createSandbox(): Record<string, unknown> {
|
|
168
|
+
return Object.freeze({
|
|
169
|
+
// The node being checked (set dynamically)
|
|
170
|
+
node: null,
|
|
171
|
+
|
|
172
|
+
// Safe built-ins (frozen)
|
|
173
|
+
Boolean,
|
|
174
|
+
Number,
|
|
175
|
+
String,
|
|
176
|
+
Array,
|
|
177
|
+
Object,
|
|
178
|
+
RegExp,
|
|
179
|
+
|
|
180
|
+
// Safe JSON access
|
|
181
|
+
JSON: Object.freeze({
|
|
182
|
+
parse: JSON.parse,
|
|
183
|
+
stringify: JSON.stringify,
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
// Safe Math access
|
|
187
|
+
Math: Object.freeze(Math),
|
|
188
|
+
|
|
189
|
+
// Primitives
|
|
190
|
+
true: true,
|
|
191
|
+
false: false,
|
|
192
|
+
undefined,
|
|
193
|
+
null: null,
|
|
194
|
+
NaN,
|
|
195
|
+
Infinity,
|
|
196
|
+
|
|
197
|
+
// No console, no require, no import, no process, no global
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Creates a SandboxedExecutor instance.
|
|
204
|
+
* Use this for compiling declarative rules.
|
|
205
|
+
*/
|
|
206
|
+
export function createSandboxedExecutor(): SandboxedExecutor {
|
|
207
|
+
return new SandboxedExecutor();
|
|
208
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// packages/core/src/errors.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base error class for all SentriFlow errors.
|
|
5
|
+
* Provides structured error handling with error codes.
|
|
6
|
+
*/
|
|
7
|
+
export class SentriflowError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly code: string,
|
|
10
|
+
message: string,
|
|
11
|
+
public readonly details?: Record<string, unknown>
|
|
12
|
+
) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'SentriflowError';
|
|
15
|
+
// Maintains proper stack trace for where error was thrown (V8 only)
|
|
16
|
+
if (Error.captureStackTrace) {
|
|
17
|
+
Error.captureStackTrace(this, this.constructor);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns a user-friendly error message without internal details.
|
|
23
|
+
*/
|
|
24
|
+
toUserMessage(): string {
|
|
25
|
+
return `[${this.code}] ${this.message}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns a JSON representation for logging.
|
|
30
|
+
*/
|
|
31
|
+
toJSON(): Record<string, unknown> {
|
|
32
|
+
return {
|
|
33
|
+
code: this.code,
|
|
34
|
+
message: this.message,
|
|
35
|
+
details: this.details,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Error thrown when configuration loading fails.
|
|
42
|
+
*/
|
|
43
|
+
export class SentriflowConfigError extends SentriflowError {
|
|
44
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
45
|
+
super('CONFIG_ERROR', message, details);
|
|
46
|
+
this.name = 'SentriflowConfigError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Error thrown when path validation fails.
|
|
52
|
+
*/
|
|
53
|
+
export class SentriflowPathError extends SentriflowError {
|
|
54
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
55
|
+
super('PATH_ERROR', message, details);
|
|
56
|
+
this.name = 'SentriflowPathError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Error thrown when parsing fails.
|
|
62
|
+
*/
|
|
63
|
+
export class SentriflowParseError extends SentriflowError {
|
|
64
|
+
constructor(message: string, line?: number) {
|
|
65
|
+
super('PARSE_ERROR', message, line !== undefined ? { line } : undefined);
|
|
66
|
+
this.name = 'SentriflowParseError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Error thrown when rule validation or execution fails.
|
|
72
|
+
*/
|
|
73
|
+
export class SentriflowRuleError extends SentriflowError {
|
|
74
|
+
constructor(message: string, ruleId?: string) {
|
|
75
|
+
super('RULE_ERROR', message, ruleId ? { ruleId } : undefined);
|
|
76
|
+
this.name = 'SentriflowRuleError';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Error thrown when input size exceeds limits.
|
|
82
|
+
*/
|
|
83
|
+
export class SentriflowSizeLimitError extends SentriflowError {
|
|
84
|
+
constructor(message: string, details?: Record<string, unknown>) {
|
|
85
|
+
super('SIZE_LIMIT_ERROR', message, details);
|
|
86
|
+
this.name = 'SentriflowSizeLimitError';
|
|
87
|
+
}
|
|
88
|
+
}
|