llm-checker 3.2.1 → 3.2.3
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/README.md +93 -8
- package/bin/enhanced_cli.js +447 -0
- package/package.json +7 -1
- package/src/index.js +84 -20
- package/src/models/deterministic-selector.js +406 -22
- package/src/models/intelligent-selector.js +89 -4
- package/src/policy/audit-reporter.js +420 -0
- package/src/policy/cli-policy.js +403 -0
- package/src/policy/policy-engine.js +497 -0
- package/src/policy/policy-manager.js +324 -0
- package/src/provenance/model-provenance.js +176 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
const NOOP_POLICY = {
|
|
2
|
+
version: 1,
|
|
3
|
+
org: 'default',
|
|
4
|
+
mode: 'audit',
|
|
5
|
+
rules: {}
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
function isPlainObject(value) {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function asArray(value) {
|
|
13
|
+
return Array.isArray(value) ? value : [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function toLowerString(value) {
|
|
17
|
+
if (value === undefined || value === null) return null;
|
|
18
|
+
const normalized = String(value).trim();
|
|
19
|
+
return normalized.length > 0 ? normalized.toLowerCase() : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizePattern(pattern) {
|
|
23
|
+
return typeof pattern === 'string' ? pattern.trim() : '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
class PolicyEngine {
|
|
27
|
+
constructor(policy = null) {
|
|
28
|
+
this.policy = isPlainObject(policy) ? policy : NOOP_POLICY;
|
|
29
|
+
this.patternCache = new Map();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
getMode() {
|
|
33
|
+
return this.policy.mode || 'audit';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
hasActiveRules() {
|
|
37
|
+
const rules = this.policy.rules || {};
|
|
38
|
+
|
|
39
|
+
return Boolean(
|
|
40
|
+
(isPlainObject(rules.models) && Object.keys(rules.models).length > 0) ||
|
|
41
|
+
(isPlainObject(rules.runtime) && Object.keys(rules.runtime).length > 0) ||
|
|
42
|
+
(isPlainObject(rules.compliance) && Object.keys(rules.compliance).length > 0)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
evaluateModel(model, context = {}) {
|
|
47
|
+
const target = isPlainObject(model) ? model : {};
|
|
48
|
+
const violations = [];
|
|
49
|
+
const rules = this.policy.rules || {};
|
|
50
|
+
|
|
51
|
+
this.evaluateModelRules(target, rules.models || {}, violations);
|
|
52
|
+
this.evaluateRuntimeRules(target, context, rules.runtime || {}, violations);
|
|
53
|
+
this.evaluateComplianceRules(target, rules.compliance || {}, violations);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
pass: violations.length === 0,
|
|
57
|
+
mode: this.getMode(),
|
|
58
|
+
violationCount: violations.length,
|
|
59
|
+
violations,
|
|
60
|
+
rationale: this.buildRationale(violations)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
evaluateModels(models, context = {}) {
|
|
65
|
+
return asArray(models).map((model) => {
|
|
66
|
+
const policyResult = this.evaluateModel(model, context);
|
|
67
|
+
return { ...model, policyResult };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
evaluateScoredVariants(scoredVariants, context = {}) {
|
|
72
|
+
return asArray(scoredVariants).map((item) => {
|
|
73
|
+
if (isPlainObject(item) && isPlainObject(item.variant)) {
|
|
74
|
+
const policyResult = this.evaluateModel(item.variant, context);
|
|
75
|
+
return {
|
|
76
|
+
...item,
|
|
77
|
+
policyResult,
|
|
78
|
+
variant: {
|
|
79
|
+
...item.variant,
|
|
80
|
+
policyResult
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const policyResult = this.evaluateModel(item, context);
|
|
86
|
+
return {
|
|
87
|
+
...item,
|
|
88
|
+
policyResult
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
evaluateModelRules(model, modelRules, violations) {
|
|
94
|
+
if (!isPlainObject(modelRules)) return;
|
|
95
|
+
|
|
96
|
+
const modelTargets = this.getModelTargets(model);
|
|
97
|
+
const modelIdentifier = this.getModelIdentifier(model);
|
|
98
|
+
|
|
99
|
+
const denyPatterns = asArray(modelRules.deny).map(normalizePattern).filter(Boolean);
|
|
100
|
+
const allowPatterns = asArray(modelRules.allow).map(normalizePattern).filter(Boolean);
|
|
101
|
+
|
|
102
|
+
const denyMatch = denyPatterns.find((pattern) => this.matchesAnyPattern(pattern, modelTargets));
|
|
103
|
+
if (denyMatch) {
|
|
104
|
+
this.pushViolation(
|
|
105
|
+
violations,
|
|
106
|
+
'MODEL_DENIED',
|
|
107
|
+
'rules.models.deny',
|
|
108
|
+
`Model is denied by pattern "${denyMatch}".`,
|
|
109
|
+
denyMatch,
|
|
110
|
+
modelIdentifier
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (allowPatterns.length > 0) {
|
|
115
|
+
const allowMatch = allowPatterns.find((pattern) => this.matchesAnyPattern(pattern, modelTargets));
|
|
116
|
+
if (!allowMatch) {
|
|
117
|
+
this.pushViolation(
|
|
118
|
+
violations,
|
|
119
|
+
'MODEL_NOT_ALLOWED',
|
|
120
|
+
'rules.models.allow',
|
|
121
|
+
'Model did not match any allow pattern.',
|
|
122
|
+
allowPatterns,
|
|
123
|
+
modelIdentifier
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof modelRules.max_size_gb === 'number') {
|
|
129
|
+
const sizeGB = this.getModelSizeGB(model);
|
|
130
|
+
if (sizeGB === null) {
|
|
131
|
+
this.pushViolation(
|
|
132
|
+
violations,
|
|
133
|
+
'MODEL_SIZE_UNKNOWN',
|
|
134
|
+
'rules.models.max_size_gb',
|
|
135
|
+
'Model size is missing and cannot be evaluated.',
|
|
136
|
+
`<= ${modelRules.max_size_gb}`,
|
|
137
|
+
null
|
|
138
|
+
);
|
|
139
|
+
} else if (sizeGB > modelRules.max_size_gb) {
|
|
140
|
+
this.pushViolation(
|
|
141
|
+
violations,
|
|
142
|
+
'MODEL_TOO_LARGE',
|
|
143
|
+
'rules.models.max_size_gb',
|
|
144
|
+
'Model exceeds maximum allowed size.',
|
|
145
|
+
`<= ${modelRules.max_size_gb}`,
|
|
146
|
+
sizeGB
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof modelRules.max_params_b === 'number') {
|
|
152
|
+
const paramsB = this.getModelParamsB(model);
|
|
153
|
+
if (paramsB === null) {
|
|
154
|
+
this.pushViolation(
|
|
155
|
+
violations,
|
|
156
|
+
'MODEL_PARAMS_UNKNOWN',
|
|
157
|
+
'rules.models.max_params_b',
|
|
158
|
+
'Model parameter count is missing and cannot be evaluated.',
|
|
159
|
+
`<= ${modelRules.max_params_b}`,
|
|
160
|
+
null
|
|
161
|
+
);
|
|
162
|
+
} else if (paramsB > modelRules.max_params_b) {
|
|
163
|
+
this.pushViolation(
|
|
164
|
+
violations,
|
|
165
|
+
'MODEL_TOO_MANY_PARAMS',
|
|
166
|
+
'rules.models.max_params_b',
|
|
167
|
+
'Model exceeds maximum allowed parameters.',
|
|
168
|
+
`<= ${modelRules.max_params_b}`,
|
|
169
|
+
paramsB
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const allowedQuants = asArray(modelRules.allowed_quantizations)
|
|
175
|
+
.map((quant) => toLowerString(quant))
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
|
|
178
|
+
if (allowedQuants.length > 0) {
|
|
179
|
+
const quant = this.getModelQuantization(model);
|
|
180
|
+
if (!quant) {
|
|
181
|
+
this.pushViolation(
|
|
182
|
+
violations,
|
|
183
|
+
'QUANTIZATION_UNKNOWN',
|
|
184
|
+
'rules.models.allowed_quantizations',
|
|
185
|
+
'Model quantization is missing and cannot be evaluated.',
|
|
186
|
+
allowedQuants,
|
|
187
|
+
null
|
|
188
|
+
);
|
|
189
|
+
} else if (!allowedQuants.includes(quant)) {
|
|
190
|
+
this.pushViolation(
|
|
191
|
+
violations,
|
|
192
|
+
'QUANTIZATION_NOT_ALLOWED',
|
|
193
|
+
'rules.models.allowed_quantizations',
|
|
194
|
+
'Model quantization is not in allowlist.',
|
|
195
|
+
allowedQuants,
|
|
196
|
+
quant
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
evaluateRuntimeRules(model, context, runtimeRules, violations) {
|
|
203
|
+
if (!isPlainObject(runtimeRules)) return;
|
|
204
|
+
|
|
205
|
+
const backend = this.resolveBackend(model, context);
|
|
206
|
+
const requiredBackends = asArray(runtimeRules.required_backends)
|
|
207
|
+
.map((item) => toLowerString(item))
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
|
|
210
|
+
if (requiredBackends.length > 0) {
|
|
211
|
+
if (!backend) {
|
|
212
|
+
this.pushViolation(
|
|
213
|
+
violations,
|
|
214
|
+
'BACKEND_UNKNOWN',
|
|
215
|
+
'rules.runtime.required_backends',
|
|
216
|
+
'Runtime backend is missing and cannot be evaluated.',
|
|
217
|
+
requiredBackends,
|
|
218
|
+
null
|
|
219
|
+
);
|
|
220
|
+
} else if (!requiredBackends.includes(backend)) {
|
|
221
|
+
this.pushViolation(
|
|
222
|
+
violations,
|
|
223
|
+
'BACKEND_NOT_ALLOWED',
|
|
224
|
+
'rules.runtime.required_backends',
|
|
225
|
+
'Runtime backend is not in the required backend list.',
|
|
226
|
+
requiredBackends,
|
|
227
|
+
backend
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof runtimeRules.min_ram_gb === 'number') {
|
|
233
|
+
const availableRam = this.resolveSystemRamGB(context);
|
|
234
|
+
if (availableRam === null) {
|
|
235
|
+
this.pushViolation(
|
|
236
|
+
violations,
|
|
237
|
+
'RAM_UNKNOWN',
|
|
238
|
+
'rules.runtime.min_ram_gb',
|
|
239
|
+
'System RAM is missing and cannot be evaluated.',
|
|
240
|
+
`>= ${runtimeRules.min_ram_gb}`,
|
|
241
|
+
null
|
|
242
|
+
);
|
|
243
|
+
} else if (availableRam < runtimeRules.min_ram_gb) {
|
|
244
|
+
this.pushViolation(
|
|
245
|
+
violations,
|
|
246
|
+
'INSUFFICIENT_RAM',
|
|
247
|
+
'rules.runtime.min_ram_gb',
|
|
248
|
+
'System RAM is below policy minimum.',
|
|
249
|
+
`>= ${runtimeRules.min_ram_gb}`,
|
|
250
|
+
availableRam
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (runtimeRules.local_only === true) {
|
|
256
|
+
const isLocal = this.resolveIsLocal(model, context);
|
|
257
|
+
if (!isLocal) {
|
|
258
|
+
this.pushViolation(
|
|
259
|
+
violations,
|
|
260
|
+
'MODEL_NOT_LOCAL',
|
|
261
|
+
'rules.runtime.local_only',
|
|
262
|
+
'Policy requires local execution, but model source is non-local.',
|
|
263
|
+
true,
|
|
264
|
+
isLocal
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
evaluateComplianceRules(model, complianceRules, violations) {
|
|
271
|
+
if (!isPlainObject(complianceRules)) return;
|
|
272
|
+
|
|
273
|
+
const approvedLicenses = asArray(complianceRules.approved_licenses)
|
|
274
|
+
.map((license) => toLowerString(license))
|
|
275
|
+
.filter(Boolean);
|
|
276
|
+
|
|
277
|
+
if (approvedLicenses.length === 0) return;
|
|
278
|
+
|
|
279
|
+
const license = this.getModelLicense(model);
|
|
280
|
+
if (!license) {
|
|
281
|
+
this.pushViolation(
|
|
282
|
+
violations,
|
|
283
|
+
'LICENSE_MISSING',
|
|
284
|
+
'rules.compliance.approved_licenses',
|
|
285
|
+
'Model license metadata is missing.',
|
|
286
|
+
approvedLicenses,
|
|
287
|
+
null
|
|
288
|
+
);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!approvedLicenses.includes(license)) {
|
|
293
|
+
this.pushViolation(
|
|
294
|
+
violations,
|
|
295
|
+
'LICENSE_NOT_APPROVED',
|
|
296
|
+
'rules.compliance.approved_licenses',
|
|
297
|
+
'Model license is not in approved allowlist.',
|
|
298
|
+
approvedLicenses,
|
|
299
|
+
license
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
buildRationale(violations) {
|
|
305
|
+
if (!Array.isArray(violations) || violations.length === 0) {
|
|
306
|
+
return ['Policy evaluation passed with zero violations.'];
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return violations.map((violation) => `${violation.code}: ${violation.message}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
pushViolation(violations, code, rulePath, message, expected, actual) {
|
|
313
|
+
violations.push({
|
|
314
|
+
code,
|
|
315
|
+
path: rulePath,
|
|
316
|
+
message,
|
|
317
|
+
expected,
|
|
318
|
+
actual
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
getModelTargets(model) {
|
|
323
|
+
const values = [
|
|
324
|
+
this.getModelIdentifier(model),
|
|
325
|
+
toLowerString(model.model_id),
|
|
326
|
+
toLowerString(model.modelId),
|
|
327
|
+
toLowerString(model.tag),
|
|
328
|
+
toLowerString(model.name),
|
|
329
|
+
toLowerString(model.model_name),
|
|
330
|
+
toLowerString(model.family)
|
|
331
|
+
].filter(Boolean);
|
|
332
|
+
|
|
333
|
+
return Array.from(new Set(values));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
getModelIdentifier(model) {
|
|
337
|
+
const identifier =
|
|
338
|
+
toLowerString(model.model_identifier) ||
|
|
339
|
+
toLowerString(model.modelIdentifier) ||
|
|
340
|
+
toLowerString(model.tag) ||
|
|
341
|
+
toLowerString(model.name) ||
|
|
342
|
+
toLowerString(model.model_name);
|
|
343
|
+
|
|
344
|
+
if (identifier && identifier.includes(':')) return identifier;
|
|
345
|
+
|
|
346
|
+
const modelId = toLowerString(model.model_id) || toLowerString(model.modelId);
|
|
347
|
+
if (modelId && identifier) {
|
|
348
|
+
return `${modelId}:${identifier}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return modelId || identifier || 'unknown:model';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
getModelSizeGB(model) {
|
|
355
|
+
const explicit = this.toNumber(model.size_gb) ?? this.toNumber(model.sizeGB);
|
|
356
|
+
if (explicit !== null) return explicit;
|
|
357
|
+
|
|
358
|
+
const rawSize = toLowerString(model.size);
|
|
359
|
+
if (rawSize) {
|
|
360
|
+
const mbMatch = rawSize.match(/([0-9]+(?:\.[0-9]+)?)\s*mb/);
|
|
361
|
+
if (mbMatch) return parseFloat(mbMatch[1]) / 1024;
|
|
362
|
+
|
|
363
|
+
const gbMatch = rawSize.match(/([0-9]+(?:\.[0-9]+)?)\s*gb/);
|
|
364
|
+
if (gbMatch) return parseFloat(gbMatch[1]);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const paramsB = this.getModelParamsB(model);
|
|
368
|
+
if (paramsB !== null) {
|
|
369
|
+
const quant = this.getModelQuantization(model) || 'q4_k_m';
|
|
370
|
+
if (quant.includes('q8') || quant.includes('f16') || quant.includes('fp16')) {
|
|
371
|
+
return paramsB;
|
|
372
|
+
}
|
|
373
|
+
if (quant.includes('q6')) {
|
|
374
|
+
return paramsB * 0.75;
|
|
375
|
+
}
|
|
376
|
+
if (quant.includes('q5')) {
|
|
377
|
+
return paramsB * 0.6;
|
|
378
|
+
}
|
|
379
|
+
if (quant.includes('q3')) {
|
|
380
|
+
return paramsB * 0.4;
|
|
381
|
+
}
|
|
382
|
+
if (quant.includes('q2')) {
|
|
383
|
+
return paramsB * 0.3;
|
|
384
|
+
}
|
|
385
|
+
return paramsB * 0.5;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
getModelParamsB(model) {
|
|
392
|
+
const explicit = this.toNumber(model.params_b) ?? this.toNumber(model.paramsB);
|
|
393
|
+
if (explicit !== null) return explicit;
|
|
394
|
+
|
|
395
|
+
const identifier = this.getModelIdentifier(model);
|
|
396
|
+
const match = identifier.match(/([0-9]+(?:\.[0-9]+)?)b\b/i);
|
|
397
|
+
if (match) return parseFloat(match[1]);
|
|
398
|
+
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
getModelQuantization(model) {
|
|
403
|
+
const raw =
|
|
404
|
+
toLowerString(model.quant) ||
|
|
405
|
+
toLowerString(model.quantization) ||
|
|
406
|
+
toLowerString(model.quant_type);
|
|
407
|
+
|
|
408
|
+
return raw;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
getModelLicense(model) {
|
|
412
|
+
const raw =
|
|
413
|
+
toLowerString(model.license) ||
|
|
414
|
+
toLowerString(model.license_id) ||
|
|
415
|
+
toLowerString(model.licenseId);
|
|
416
|
+
|
|
417
|
+
return raw;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
resolveBackend(model, context) {
|
|
421
|
+
const fromContext = toLowerString(context.backend) || toLowerString(context.runtimeBackend);
|
|
422
|
+
if (fromContext) return fromContext;
|
|
423
|
+
|
|
424
|
+
const fromHardware =
|
|
425
|
+
toLowerString(context?.hardware?.summary?.bestBackend) ||
|
|
426
|
+
toLowerString(context?.hardware?.backend);
|
|
427
|
+
if (fromHardware) return fromHardware;
|
|
428
|
+
|
|
429
|
+
return toLowerString(model.backend);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
resolveSystemRamGB(context) {
|
|
433
|
+
const direct =
|
|
434
|
+
this.toNumber(context.ramGB) ??
|
|
435
|
+
this.toNumber(context.totalRamGB) ??
|
|
436
|
+
this.toNumber(context?.hardware?.memory?.total) ??
|
|
437
|
+
this.toNumber(context?.hardware?.summary?.systemRAM) ??
|
|
438
|
+
this.toNumber(context?.hardware?.summary?.effectiveMemory);
|
|
439
|
+
|
|
440
|
+
return direct;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
resolveIsLocal(model, context) {
|
|
444
|
+
if (typeof context.isLocal === 'boolean') {
|
|
445
|
+
return context.isLocal;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if (typeof model.is_local === 'boolean') return model.is_local;
|
|
449
|
+
if (typeof model.isLocal === 'boolean') return model.isLocal;
|
|
450
|
+
if (typeof model.local === 'boolean') return model.local;
|
|
451
|
+
|
|
452
|
+
const source = toLowerString(model.source);
|
|
453
|
+
if (source) {
|
|
454
|
+
return source === 'local' || source === 'ollama';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const type = toLowerString(model.type);
|
|
458
|
+
if (type) {
|
|
459
|
+
return type !== 'cloud' && type !== 'remote' && type !== 'hosted';
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
toNumber(value) {
|
|
466
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
467
|
+
if (typeof value === 'string') {
|
|
468
|
+
const normalized = value.trim();
|
|
469
|
+
if (!normalized) return null;
|
|
470
|
+
const parsed = Number.parseFloat(normalized);
|
|
471
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
472
|
+
}
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
matchesAnyPattern(pattern, targets) {
|
|
477
|
+
if (!pattern || !Array.isArray(targets)) return false;
|
|
478
|
+
const regex = this.getPatternRegex(pattern);
|
|
479
|
+
return targets.some((target) => regex.test(target));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
getPatternRegex(pattern) {
|
|
483
|
+
if (this.patternCache.has(pattern)) {
|
|
484
|
+
return this.patternCache.get(pattern);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const escaped = pattern
|
|
488
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
489
|
+
.replace(/\*/g, '.*');
|
|
490
|
+
|
|
491
|
+
const regex = new RegExp(`^${escaped}$`, 'i');
|
|
492
|
+
this.patternCache.set(pattern, regex);
|
|
493
|
+
return regex;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
module.exports = PolicyEngine;
|