@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,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';
|