@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,303 @@
1
+ // packages/core/src/json-rules/types.ts
2
+
3
+ /**
4
+ * JSON Rule Types
5
+ *
6
+ * Provides a JSON-serializable rule format that allows third-party customers
7
+ * to write validation rules without TypeScript knowledge. Supports full access
8
+ * to helper functions via the `helper` check type.
9
+ */
10
+
11
+ import type { RuleVendor, RuleMetadata } from '../types/IRule';
12
+ import { MAX_METADATA_LENGTH } from '../constants';
13
+
14
+ /**
15
+ * Argument value for helper function invocation.
16
+ * Can be a literal value or a reference to node properties.
17
+ */
18
+ export type JsonArgValue =
19
+ | string
20
+ | number
21
+ | boolean
22
+ | null
23
+ | { $ref: 'node' | 'node.id' | 'node.type' | 'node.children' | 'node.params' | 'node.rawText' };
24
+
25
+ /**
26
+ * JSON-serializable check conditions for rule evaluation.
27
+ * Extends DeclarativeCheck with helper function invocation and expression support.
28
+ */
29
+ export type JsonCheck =
30
+ // Pattern matching on node.id
31
+ | { type: 'match'; pattern: string; flags?: string }
32
+ | { type: 'not_match'; pattern: string; flags?: string }
33
+
34
+ // Text contains on node.id
35
+ | { type: 'contains'; text: string }
36
+ | { type: 'not_contains'; text: string }
37
+
38
+ // Child node existence (case-insensitive prefix match)
39
+ | { type: 'child_exists'; selector: string }
40
+ | { type: 'child_not_exists'; selector: string }
41
+
42
+ // Child text matching
43
+ | { type: 'child_matches'; selector: string; pattern: string; flags?: string }
44
+ | { type: 'child_contains'; selector: string; text: string }
45
+
46
+ // Helper function invocation (NEW)
47
+ | {
48
+ type: 'helper';
49
+ /** Helper name, optionally namespaced (e.g., "cisco.isTrunkPort", "hasChildCommand") */
50
+ helper: string;
51
+ /** Arguments to pass to the helper function */
52
+ args?: JsonArgValue[];
53
+ /** If true, negate the result */
54
+ negate?: boolean;
55
+ }
56
+
57
+ // Simple expression evaluation (NEW)
58
+ | {
59
+ type: 'expr';
60
+ /** JavaScript expression to evaluate (sandboxed) */
61
+ expr: string;
62
+ }
63
+
64
+ // Logical combinators
65
+ | { type: 'and'; conditions: JsonCheck[] }
66
+ | { type: 'or'; conditions: JsonCheck[] }
67
+ | { type: 'not'; condition: JsonCheck };
68
+
69
+ /**
70
+ * A complete JSON rule definition.
71
+ * Can be serialized to/from JSON for external rule distribution.
72
+ */
73
+ export interface JsonRule {
74
+ /** Unique rule identifier (e.g., "JSON-SEC-001") */
75
+ id: string;
76
+
77
+ /** Optional selector for node filtering (e.g., "interface", "router bgp") */
78
+ selector?: string;
79
+
80
+ /** Optional vendor(s) this rule applies to */
81
+ vendor?: RuleVendor | RuleVendor[];
82
+
83
+ /** Rule metadata including severity, description, remediation */
84
+ metadata: RuleMetadata;
85
+
86
+ /** The check condition to evaluate */
87
+ check: JsonCheck;
88
+
89
+ /**
90
+ * Optional: Custom message template for failures.
91
+ * Supports placeholders: {nodeId}, {ruleId}
92
+ */
93
+ failureMessage?: string;
94
+
95
+ /**
96
+ * Optional: Custom message template for passes.
97
+ * Supports placeholders: {nodeId}, {ruleId}
98
+ */
99
+ successMessage?: string;
100
+ }
101
+
102
+ /**
103
+ * A JSON rule file containing multiple rules with optional metadata.
104
+ */
105
+ export interface JsonRuleFile {
106
+ /** Schema version for forward compatibility */
107
+ version: '1.0';
108
+
109
+ /** Optional metadata about this rule file */
110
+ meta?: {
111
+ /** Name of this rule collection */
112
+ name?: string;
113
+ /** Description of the rule collection */
114
+ description?: string;
115
+ /** Author or organization */
116
+ author?: string;
117
+ /** License for the rules */
118
+ license?: string;
119
+ };
120
+
121
+ /** Array of JSON rules */
122
+ rules: JsonRule[];
123
+ }
124
+
125
+ /**
126
+ * Type guard to check if an object is a valid JsonArgValue.
127
+ */
128
+ export function isJsonArgValue(obj: unknown): obj is JsonArgValue {
129
+ if (obj === null) return true;
130
+ if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
131
+ return true;
132
+ }
133
+ if (typeof obj === 'object' && '$ref' in obj) {
134
+ const ref = (obj as { $ref: unknown }).$ref;
135
+ return (
136
+ ref === 'node' ||
137
+ ref === 'node.id' ||
138
+ ref === 'node.type' ||
139
+ ref === 'node.children' ||
140
+ ref === 'node.params' ||
141
+ ref === 'node.rawText'
142
+ );
143
+ }
144
+ return false;
145
+ }
146
+
147
+ /**
148
+ * Type guard to check if an object is a valid JsonCheck.
149
+ */
150
+ export function isJsonCheck(obj: unknown): obj is JsonCheck {
151
+ if (typeof obj !== 'object' || obj === null) {
152
+ return false;
153
+ }
154
+
155
+ const check = obj as Record<string, unknown>;
156
+
157
+ switch (check.type) {
158
+ case 'match':
159
+ case 'not_match':
160
+ return (
161
+ typeof check.pattern === 'string' &&
162
+ (check.flags === undefined || typeof check.flags === 'string')
163
+ );
164
+
165
+ case 'contains':
166
+ case 'not_contains':
167
+ return typeof check.text === 'string';
168
+
169
+ case 'child_exists':
170
+ case 'child_not_exists':
171
+ return typeof check.selector === 'string';
172
+
173
+ case 'child_matches':
174
+ return (
175
+ typeof check.selector === 'string' &&
176
+ typeof check.pattern === 'string' &&
177
+ (check.flags === undefined || typeof check.flags === 'string')
178
+ );
179
+
180
+ case 'child_contains':
181
+ return typeof check.selector === 'string' && typeof check.text === 'string';
182
+
183
+ case 'helper':
184
+ if (typeof check.helper !== 'string') return false;
185
+ if (check.args !== undefined) {
186
+ if (!Array.isArray(check.args)) return false;
187
+ if (!check.args.every(isJsonArgValue)) return false;
188
+ }
189
+ if (check.negate !== undefined && typeof check.negate !== 'boolean') return false;
190
+ return true;
191
+
192
+ case 'expr':
193
+ return typeof check.expr === 'string';
194
+
195
+ case 'and':
196
+ case 'or':
197
+ return Array.isArray(check.conditions) && check.conditions.every(isJsonCheck);
198
+
199
+ case 'not':
200
+ return isJsonCheck(check.condition);
201
+
202
+ default:
203
+ return false;
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Type guard to check if an object is a valid JsonRule.
209
+ */
210
+ export function isJsonRule(obj: unknown): obj is JsonRule {
211
+ if (typeof obj !== 'object' || obj === null) {
212
+ return false;
213
+ }
214
+
215
+ const rule = obj as Record<string, unknown>;
216
+
217
+ // Check required fields
218
+ if (typeof rule.id !== 'string' || rule.id.length === 0) {
219
+ return false;
220
+ }
221
+
222
+ // Check optional selector
223
+ if (rule.selector !== undefined && typeof rule.selector !== 'string') {
224
+ return false;
225
+ }
226
+
227
+ // Check metadata
228
+ if (typeof rule.metadata !== 'object' || rule.metadata === null) {
229
+ return false;
230
+ }
231
+
232
+ const metadata = rule.metadata as Record<string, unknown>;
233
+ if (!['error', 'warning', 'info'].includes(metadata.level as string)) {
234
+ return false;
235
+ }
236
+ if (typeof metadata.obu !== 'string' || metadata.obu.length > MAX_METADATA_LENGTH) {
237
+ return false;
238
+ }
239
+ if (typeof metadata.owner !== 'string' || metadata.owner.length > MAX_METADATA_LENGTH) {
240
+ return false;
241
+ }
242
+ // Validate optional string fields length
243
+ if (metadata.description !== undefined) {
244
+ if (typeof metadata.description !== 'string' || metadata.description.length > MAX_METADATA_LENGTH) {
245
+ return false;
246
+ }
247
+ }
248
+ if (metadata.remediation !== undefined) {
249
+ if (typeof metadata.remediation !== 'string' || metadata.remediation.length > MAX_METADATA_LENGTH) {
250
+ return false;
251
+ }
252
+ }
253
+
254
+ // Check the check condition
255
+ if (!isJsonCheck(rule.check)) {
256
+ return false;
257
+ }
258
+
259
+ // Check optional message templates
260
+ if (rule.failureMessage !== undefined && typeof rule.failureMessage !== 'string') {
261
+ return false;
262
+ }
263
+ if (rule.successMessage !== undefined && typeof rule.successMessage !== 'string') {
264
+ return false;
265
+ }
266
+
267
+ return true;
268
+ }
269
+
270
+ /**
271
+ * Type guard to check if an object is a valid JsonRuleFile.
272
+ */
273
+ export function isJsonRuleFile(obj: unknown): obj is JsonRuleFile {
274
+ if (typeof obj !== 'object' || obj === null) {
275
+ return false;
276
+ }
277
+
278
+ const file = obj as Record<string, unknown>;
279
+
280
+ // Check version
281
+ if (file.version !== '1.0') {
282
+ return false;
283
+ }
284
+
285
+ // Check optional meta
286
+ if (file.meta !== undefined) {
287
+ if (typeof file.meta !== 'object' || file.meta === null) {
288
+ return false;
289
+ }
290
+ const meta = file.meta as Record<string, unknown>;
291
+ if (meta.name !== undefined && typeof meta.name !== 'string') return false;
292
+ if (meta.description !== undefined && typeof meta.description !== 'string') return false;
293
+ if (meta.author !== undefined && typeof meta.author !== 'string') return false;
294
+ if (meta.license !== undefined && typeof meta.license !== 'string') return false;
295
+ }
296
+
297
+ // Check rules array
298
+ if (!Array.isArray(file.rules)) {
299
+ return false;
300
+ }
301
+
302
+ return file.rules.every(isJsonRule);
303
+ }
@@ -0,0 +1,332 @@
1
+ // packages/core/src/pack-loader/PackLoader.ts
2
+
3
+ /**
4
+ * SEC-012: Encrypted Rule Pack Loader
5
+ *
6
+ * TIERED EXECUTION MODEL:
7
+ * 1. LOAD-TIME (VM Sandboxed): Decrypt, validate expiry/license, extract rule definitions
8
+ * 2. RUNTIME (Native): Compile check functions natively for high-performance execution
9
+ *
10
+ * Security model:
11
+ * - Pack is encrypted with AES-256-GCM (authenticated encryption)
12
+ * - Only valid license key holders can decrypt
13
+ * - Self-validation (expiry check) runs in VM sandbox
14
+ * - After validation, rules are compiled natively for performance
15
+ */
16
+
17
+ import { createDecipheriv, pbkdf2Sync } from 'crypto';
18
+ import { createContext, Script, type Context as VMContext } from 'vm';
19
+ import {
20
+ PackLoadError,
21
+ type PackLoadOptions,
22
+ type LoadedPack,
23
+ type LicenseInfo,
24
+ GRPX_CONSTANTS,
25
+ } from './types';
26
+ import type { IRule, RuleMetadata, RuleVendor, RulePackMetadata } from '../types/IRule';
27
+ import type { ConfigNode } from '../types/ConfigNode';
28
+ import type { Context } from '../types/IRule';
29
+
30
+ // Import all rule helpers for injection into compiled check functions
31
+ import * as helpers from '../helpers';
32
+ import { getAllVendorModules } from '../helpers';
33
+
34
+ /**
35
+ * All rule helpers merged into a single object for injection.
36
+ * This allows compiled check functions to access helpers by name.
37
+ * Dynamically built from the helpers module.
38
+ */
39
+ function buildAllHelpers(): Record<string, unknown> {
40
+ const result: Record<string, unknown> = { ...helpers };
41
+ const vendorModules = getAllVendorModules();
42
+ for (const [_name, module] of Object.entries(vendorModules)) {
43
+ Object.assign(result, module);
44
+ }
45
+ return result;
46
+ }
47
+
48
+ const allHelpers = buildAllHelpers();
49
+
50
+ /**
51
+ * Intermediate result from VM execution.
52
+ * Rules have serialized check functions that will be compiled natively.
53
+ */
54
+ interface VMPackResult {
55
+ metadata: RulePackMetadata;
56
+ rules: Array<{
57
+ id: string;
58
+ selector?: string;
59
+ vendor?: string | string[];
60
+ metadata: RuleMetadata;
61
+ checkSource: string; // Serialized function source from trusted pack
62
+ }>;
63
+ validUntil: string;
64
+ licenseInfo?: LicenseInfo | null;
65
+ }
66
+
67
+ /**
68
+ * Loads and validates an encrypted rule pack (.grpx).
69
+ *
70
+ * @param packData - The encrypted pack file contents
71
+ * @param options - Load options including license key
72
+ * @returns Promise resolving to the loaded pack with rules
73
+ * @throws PackLoadError if loading fails
74
+ */
75
+ export async function loadEncryptedPack(
76
+ packData: Buffer,
77
+ options: PackLoadOptions
78
+ ): Promise<LoadedPack> {
79
+ const { licenseKey, machineId, getActivationCount, timeout = 5000 } = options;
80
+
81
+ // ========== PHASE 1: DECRYPT ==========
82
+ if (packData.length < GRPX_CONSTANTS.HEADER_SIZE) {
83
+ throw new PackLoadError('INVALID_FORMAT', 'Pack file too small');
84
+ }
85
+
86
+ const magic = packData.toString('utf8', 0, 4);
87
+ if (magic !== GRPX_CONSTANTS.MAGIC) {
88
+ throw new PackLoadError('INVALID_FORMAT', 'Invalid pack format (bad magic)');
89
+ }
90
+
91
+ const version = packData.readUInt8(4);
92
+ const algorithm = packData.readUInt8(5);
93
+ const kdf = packData.readUInt8(6);
94
+ // bytes 7-11 are reserved
95
+ const iv = packData.subarray(12, 24);
96
+ const tag = packData.subarray(24, 40);
97
+ const salt = packData.subarray(40, 72);
98
+ const payloadLength = packData.readUInt32BE(72);
99
+ const encryptedPayload = packData.subarray(
100
+ GRPX_CONSTANTS.HEADER_SIZE,
101
+ GRPX_CONSTANTS.HEADER_SIZE + payloadLength
102
+ );
103
+
104
+ if (version !== GRPX_CONSTANTS.CURRENT_VERSION) {
105
+ throw new PackLoadError('INVALID_FORMAT', `Unsupported version: ${version}`);
106
+ }
107
+
108
+ if (algorithm !== GRPX_CONSTANTS.ALG_AES_256_GCM) {
109
+ throw new PackLoadError('INVALID_FORMAT', `Unsupported algorithm: ${algorithm}`);
110
+ }
111
+
112
+ // Derive key from license key
113
+ let key: Buffer;
114
+ if (kdf === GRPX_CONSTANTS.KDF_PBKDF2) {
115
+ key = pbkdf2Sync(
116
+ licenseKey,
117
+ salt,
118
+ GRPX_CONSTANTS.PBKDF2_ITERATIONS,
119
+ GRPX_CONSTANTS.KEY_LENGTH,
120
+ 'sha256'
121
+ );
122
+ } else {
123
+ throw new PackLoadError('INVALID_FORMAT', `Unsupported KDF: ${kdf}`);
124
+ }
125
+
126
+ // Decrypt the payload
127
+ let decryptedSource: string;
128
+ try {
129
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
130
+ decipher.setAuthTag(tag);
131
+ const decrypted = Buffer.concat([
132
+ decipher.update(encryptedPayload),
133
+ decipher.final(),
134
+ ]);
135
+ decryptedSource = decrypted.toString('utf8');
136
+ } catch (error) {
137
+ // Log decryption failure category (not key or payload details)
138
+ const errorType = error instanceof Error ? error.name : 'Unknown';
139
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
140
+
141
+ // Only log in debug mode to avoid information leakage
142
+ if (process.env.DEBUG) {
143
+ console.error(`[PackLoader] Decryption failed: ${errorType} - ${errorMsg}`);
144
+ }
145
+
146
+ throw new PackLoadError(
147
+ 'DECRYPTION_FAILED',
148
+ 'Invalid license key or corrupted pack'
149
+ );
150
+ }
151
+
152
+ // ========== PHASE 2: VM VALIDATION (Sandboxed) ==========
153
+ // Execute factory in sandbox to validate expiry, machine ID, etc.
154
+ const sandbox = createValidationSandbox({ machineId, getActivationCount });
155
+ const context = createContext(sandbox);
156
+
157
+ let vmResult: VMPackResult;
158
+ try {
159
+ const script = new Script(decryptedSource, {
160
+ filename: 'pack.js',
161
+ timeout,
162
+ } as { filename: string; timeout: number });
163
+ const factory = script.runInContext(context) as (ctx: typeof sandbox) => VMPackResult;
164
+ vmResult = factory(sandbox);
165
+
166
+ if (!vmResult || !Array.isArray(vmResult.rules)) {
167
+ throw new PackLoadError('VALIDATION_FAILED', 'Invalid pack structure');
168
+ }
169
+ } catch (error) {
170
+ if (error instanceof PackLoadError) {
171
+ throw error;
172
+ }
173
+ const msg = error instanceof Error ? error.message : String(error);
174
+ if (msg.includes('EXPIRED')) {
175
+ throw new PackLoadError('EXPIRED', msg);
176
+ }
177
+ if (msg.includes('MACHINE_MISMATCH')) {
178
+ throw new PackLoadError('MACHINE_MISMATCH', msg);
179
+ }
180
+ if (msg.includes('ACTIVATION_LIMIT')) {
181
+ throw new PackLoadError('ACTIVATION_LIMIT', msg);
182
+ }
183
+ throw new PackLoadError('VALIDATION_FAILED', msg);
184
+ }
185
+
186
+ // ========== PHASE 3: NATIVE COMPILATION ==========
187
+ // Convert serialized check functions to native functions.
188
+ // This happens OUTSIDE the sandbox for full performance.
189
+ //
190
+ // SECURITY JUSTIFICATION for dynamic function compilation:
191
+ // - The code originated from our trusted pack-builder tool
192
+ // - It was encrypted with AES-256-GCM (authenticated, tamper-proof)
193
+ // - Only holders of the valid license key can decrypt it
194
+ // - The GCM auth tag ensures the payload wasn't modified
195
+ // - This is equivalent to loading a signed plugin/extension
196
+ const nativeRules: IRule[] = vmResult.rules.map(ruleDef => {
197
+ const checkFn = compileNativeCheckFunction(ruleDef.checkSource);
198
+ return {
199
+ id: ruleDef.id,
200
+ selector: ruleDef.selector,
201
+ vendor: ruleDef.vendor as RuleVendor | RuleVendor[] | undefined,
202
+ metadata: ruleDef.metadata,
203
+ check: checkFn,
204
+ };
205
+ });
206
+
207
+ return {
208
+ metadata: vmResult.metadata,
209
+ rules: nativeRules,
210
+ validUntil: vmResult.validUntil,
211
+ licenseInfo: vmResult.licenseInfo ?? undefined,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Generate helper names list for destructuring.
217
+ * Cached to avoid recomputing on every function compilation.
218
+ */
219
+ const helperNames = Object.keys(allHelpers).filter(
220
+ key => typeof allHelpers[key] === 'function'
221
+ );
222
+ const helperDestructure = helperNames.join(', ');
223
+
224
+ /**
225
+ * Compile a serialized check function to a native function.
226
+ *
227
+ * NOTE: This intentionally uses dynamic function compilation for performance.
228
+ * See SECURITY JUSTIFICATION in loadEncryptedPack() above.
229
+ *
230
+ * The function is wrapped to inject all rule helpers into scope, allowing
231
+ * serialized check functions to use helpers like hasChildCommand, findStanza, etc.
232
+ */
233
+ function compileNativeCheckFunction(
234
+ source: string
235
+ ): (node: ConfigNode, ctx: Context) => ReturnType<IRule['check']> {
236
+ // The source is trusted (from authenticated encrypted pack)
237
+ // Wrap the function to inject helpers into scope via destructuring
238
+ // This allows the original function to reference helpers by name
239
+ const wrappedSource = `
240
+ (function(__helpers__) {
241
+ const { ${helperDestructure} } = __helpers__;
242
+ return (${source});
243
+ })
244
+ `;
245
+
246
+ // Compile the wrapper and immediately invoke with helpers
247
+ const wrapperFn = (0, eval)(wrappedSource) as (helpers: Record<string, unknown>) => IRule['check'];
248
+ const compiledFn = wrapperFn(allHelpers);
249
+
250
+ return compiledFn as IRule['check'];
251
+ }
252
+
253
+ /**
254
+ * Create a minimal sandbox for validation phase only.
255
+ * This sandbox is intentionally restricted to prevent malicious code execution.
256
+ */
257
+ function createValidationSandbox(options: {
258
+ machineId?: string;
259
+ getActivationCount?: (packName: string) => number;
260
+ }): Record<string, unknown> {
261
+ const RealDate = Date;
262
+ return Object.freeze({
263
+ // Safe Date access (read-only, can't be mocked)
264
+ Date: Object.freeze({
265
+ now: () => RealDate.now(),
266
+ parse: (s: string) => RealDate.parse(s),
267
+ }),
268
+ // Safe JSON access
269
+ JSON: Object.freeze({
270
+ parse: JSON.parse,
271
+ stringify: JSON.stringify,
272
+ }),
273
+ // Safe Math access
274
+ Math: Object.freeze(Math),
275
+ // No-op console (for debugging in pack factory)
276
+ console: Object.freeze({
277
+ log: () => {},
278
+ warn: () => {},
279
+ error: () => {},
280
+ }),
281
+ // Provided context for validation
282
+ machineId: options.machineId,
283
+ getActivationCount: options.getActivationCount,
284
+ // Basic primitives
285
+ undefined,
286
+ null: null,
287
+ NaN,
288
+ Infinity,
289
+ // Required for error throwing in factory
290
+ Error,
291
+ });
292
+ }
293
+
294
+ /**
295
+ * Validate that a pack file has the correct format without decrypting.
296
+ * Useful for quick format checks before attempting full load.
297
+ *
298
+ * @param packData - The pack file contents
299
+ * @returns true if the format appears valid
300
+ */
301
+ export function validatePackFormat(packData: Buffer): boolean {
302
+ if (packData.length < GRPX_CONSTANTS.HEADER_SIZE) {
303
+ return false;
304
+ }
305
+
306
+ const magic = packData.toString('utf8', 0, 4);
307
+ if (magic !== GRPX_CONSTANTS.MAGIC) {
308
+ return false;
309
+ }
310
+
311
+ const version = packData.readUInt8(4);
312
+ if (version !== GRPX_CONSTANTS.CURRENT_VERSION) {
313
+ return false;
314
+ }
315
+
316
+ const algorithm = packData.readUInt8(5);
317
+ if (algorithm !== GRPX_CONSTANTS.ALG_AES_256_GCM) {
318
+ return false;
319
+ }
320
+
321
+ const kdf = packData.readUInt8(6);
322
+ if (kdf !== GRPX_CONSTANTS.KDF_PBKDF2) {
323
+ return false;
324
+ }
325
+
326
+ const payloadLength = packData.readUInt32BE(72);
327
+ if (packData.length < GRPX_CONSTANTS.HEADER_SIZE + payloadLength) {
328
+ return false;
329
+ }
330
+
331
+ return true;
332
+ }
@@ -0,0 +1,17 @@
1
+ // packages/core/src/pack-loader/index.ts
2
+
3
+ /**
4
+ * SEC-012: Encrypted Rule Pack System (Consumer API)
5
+ *
6
+ * Provides loading and validation of encrypted rule packs:
7
+ * - AES-256-GCM decryption
8
+ * - PBKDF2 key derivation
9
+ * - VM sandboxed validation
10
+ * - Native runtime execution
11
+ *
12
+ * For pack CREATION (buildEncryptedPack, generateLicenseKey, etc.),
13
+ * use the separate @sentriflow/pack-builder package.
14
+ */
15
+
16
+ export * from './types';
17
+ export { loadEncryptedPack, validatePackFormat } from './PackLoader';