Comprehensive security best practices and guidelines for building secure rule-based applications with Rule Engine JS.
Rule Engine JS is designed with security as a core principle. It provides multiple layers of protection against common attacks while maintaining flexibility and performance.
Rule Engine JS automatically protects against prototype pollution attacks:
// β Malicious input attempt
const maliciousData = {
__proto__: { isAdmin: true },
constructor: { prototype: { isAdmin: true } },
'constructor.prototype.isAdmin': true,
user: { name: 'attacker' },
};
// β
Safe path resolution - prototype properties are blocked
const engine = createRuleEngine();
// These return undefined (safe)
engine.resolvePath(maliciousData, '__proto__.isAdmin');
engine.resolvePath(maliciousData, 'constructor.prototype.isAdmin');
engine.resolvePath(maliciousData, 'constructor.prototype');
// This works (safe)
engine.resolvePath(maliciousData, 'user.name'); // Returns "attacker"
// Rules using malicious paths fail safely
const maliciousRule = { eq: ['__proto__.isAdmin', true] };
const result = engine.evaluateExpr(maliciousRule, maliciousData);
console.log(result.success); // false - path resolves to undefined
Functions in data objects are blocked by default to prevent code execution:
const dataWithFunctions = {
user: {
name: 'John',
getName: function () {
return this.name;
},
dangerousFunction: function () {
return eval('process.env'); // Potentially dangerous
},
},
};
// β
Function access is blocked
engine.resolvePath(dataWithFunctions, 'user.getName'); // undefined
engine.resolvePath(dataWithFunctions, 'user.dangerousFunction'); // undefined
// β
Safe data access works
engine.resolvePath(dataWithFunctions, 'user.name'); // "John"
// β
Production-ready secure configuration
const secureEngine = createRuleEngine({
allowPrototypeAccess: false, // Never allow prototype access
strict: true, // Enable strict type checking
maxDepth: 10, // Prevent deep recursion attacks
maxOperators: 100, // Limit rule complexity
enableDebug: false, // Disable debug info in production
maxCacheSize: 1000, // Reasonable cache limits
});
// β Insecure configuration (avoid in production)
const insecureEngine = createRuleEngine({
allowPrototypeAccess: true, // Dangerous!
strict: false, // More permissive
maxDepth: 1000, // Too deep
maxOperators: 10000, // Too many operators
enableDebug: true, // Exposes internal info
});
Always validate rule structures before evaluation:
function validateRuleStructure(rule) {
const errors = [];
// Basic structure validation
if (!rule || typeof rule !== 'object' || Array.isArray(rule)) {
errors.push('Rule must be a non-null object');
return { valid: false, errors };
}
// Check for empty rules
const operators = Object.keys(rule);
if (operators.length === 0) {
errors.push('Rule must contain at least one operator');
return { valid: false, errors };
}
// Validate operator names (whitelist approach)
const allowedOperators = [
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'and',
'or',
'not',
'in',
'notIn',
'contains',
'startsWith',
'endsWith',
'regex',
'between',
'isNull',
'isNotNull',
];
const invalidOperators = operators.filter((op) => !allowedOperators.includes(op));
if (invalidOperators.length > 0) {
errors.push(`Invalid operators: ${invalidOperators.join(', ')}`);
}
// Validate rule depth
const depth = calculateRuleDepth(rule);
if (depth > 10) {
errors.push(`Rule depth ${depth} exceeds maximum of 10`);
}
// Validate operator count
const operatorCount = countOperators(rule);
if (operatorCount > 100) {
errors.push(`Rule has ${operatorCount} operators, maximum is 100`);
}
return { valid: errors.length === 0, errors };
}
function calculateRuleDepth(rule, currentDepth = 0) {
if (typeof rule !== 'object' || rule === null) {
return currentDepth;
}
let maxDepth = currentDepth;
Object.values(rule).forEach((value) => {
if (Array.isArray(value)) {
value.forEach((item) => {
if (typeof item === 'object' && item !== null) {
maxDepth = Math.max(maxDepth, calculateRuleDepth(item, currentDepth + 1));
}
});
}
});
return maxDepth;
}
function countOperators(rule) {
if (typeof rule !== 'object' || rule === null) {
return 0;
}
let count = Object.keys(rule).length;
Object.values(rule).forEach((value) => {
if (Array.isArray(value)) {
value.forEach((item) => {
if (typeof item === 'object' && item !== null) {
count += countOperators(item);
}
});
}
});
return count;
}
// Usage
function safeEvaluateRule(rule, context) {
const validation = validateRuleStructure(rule);
if (!validation.valid) {
return {
success: false,
error: 'Invalid rule structure',
details: { validationErrors: validation.errors },
};
}
try {
return engine.evaluateExpr(rule, context);
} catch (error) {
return {
success: false,
error: 'Rule evaluation failed',
details: { originalError: error.message },
};
}
}
Sanitize context data before evaluation:
function sanitizeContext(context, options = {}) {
const {
maxDepth = 10,
maxStringLength = 1000,
maxArrayLength = 1000,
allowedTypes = ['string', 'number', 'boolean', 'object'],
removeFunctions = true,
removePrototypeProps = true,
} = options;
return sanitizeValue(context, 0, {
maxDepth,
maxStringLength,
maxArrayLength,
allowedTypes,
removeFunctions,
removePrototypeProps,
});
}
function sanitizeValue(value, currentDepth, options) {
// Prevent deep recursion
if (currentDepth > options.maxDepth) {
return '[TRUNCATED: Max depth exceeded]';
}
// Handle null and undefined
if (value === null || value === undefined) {
return value;
}
// Handle primitives
if (typeof value === 'string') {
if (value.length > options.maxStringLength) {
return value.substring(0, options.maxStringLength) + '[TRUNCATED]';
}
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return value;
}
// Remove functions
if (typeof value === 'function') {
return options.removeFunctions ? undefined : '[FUNCTION]';
}
// Handle arrays
if (Array.isArray(value)) {
if (!options.allowedTypes.includes('object')) {
return '[ARRAY: Not allowed]';
}
const sanitizedArray = value
.slice(0, options.maxArrayLength)
.map((item) => sanitizeValue(item, currentDepth + 1, options))
.filter((item) => item !== undefined);
return sanitizedArray;
}
// Handle objects
if (typeof value === 'object') {
if (!options.allowedTypes.includes('object')) {
return '[OBJECT: Not allowed]';
}
const sanitizedObject = {};
for (const [key, val] of Object.entries(value)) {
// Remove prototype-related properties
if (options.removePrototypeProps && ['__proto__', 'constructor', 'prototype'].includes(key)) {
continue;
}
// Sanitize key
const sanitizedKey =
typeof key === 'string' ? key.substring(0, 100) : String(key).substring(0, 100);
// Sanitize value
const sanitizedVal = sanitizeValue(val, currentDepth + 1, options);
if (sanitizedVal !== undefined) {
sanitizedObject[sanitizedKey] = sanitizedVal;
}
}
return sanitizedObject;
}
// Unknown type - convert to string safely
return String(value).substring(0, 100);
}
// Usage
function secureEvaluateRule(rule, rawContext) {
// Validate rule first
const ruleValidation = validateRuleStructure(rule);
if (!ruleValidation.valid) {
throw new Error(`Invalid rule: ${ruleValidation.errors.join(', ')}`);
}
// Sanitize context
const safeContext = sanitizeContext(rawContext, {
maxDepth: 5,
maxStringLength: 500,
maxArrayLength: 100,
});
return engine.evaluateExpr(rule, safeContext);
}
Prototype pollution occurs when attackers modify object prototypes:
// β Dangerous: Direct prototype manipulation
const maliciousInput = {
__proto__: { isAdmin: true },
'constructor.prototype.isAdmin': true,
};
// β What attackers try to achieve
Object.prototype.isAdmin = true;
const normalUser = {};
console.log(normalUser.isAdmin); // true - all objects now have isAdmin!
Rule Engine JS automatically prevents prototype pollution:
// β
Safe: Built-in protection
const engine = createRuleEngine();
const maliciousContext = {
__proto__: { isAdmin: true },
constructor: { prototype: { isAdmin: true } },
user: { name: 'attacker' },
};
// These return undefined (protected)
engine.resolvePath(maliciousContext, '__proto__.isAdmin');
engine.resolvePath(maliciousContext, 'constructor.prototype.isAdmin');
// Safe access still works
engine.resolvePath(maliciousContext, 'user.name'); // "attacker"
// Rules fail safely with malicious paths
const rule = { eq: ['__proto__.isAdmin', true] };
const result = engine.evaluateExpr(rule, maliciousContext);
console.log(result.success); // false
// Enhanced context sanitization
function protectAgainstPrototypePollution(obj) {
if (!obj || typeof obj !== 'object') {
return obj;
}
// Deep clone to avoid reference pollution
const cleaned = JSON.parse(JSON.stringify(obj));
// Recursively remove dangerous properties
function cleanObject(target) {
if (!target || typeof target !== 'object') {
return target;
}
// Remove prototype-related properties
delete target.__proto__;
delete target.constructor;
delete target.prototype;
// Remove properties that could affect prototypes
const dangerousKeys = [
'__defineGetter__',
'__defineSetter__',
'__lookupGetter__',
'__lookupSetter__',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'toString',
'valueOf',
];
dangerousKeys.forEach((key) => {
if (target.hasOwnProperty(key)) {
delete target[key];
}
});
// Recursively clean nested objects
Object.keys(target).forEach((key) => {
if (typeof target[key] === 'object' && target[key] !== null) {
cleanObject(target[key]);
}
});
return target;
}
return cleanObject(cleaned);
}
// Usage
function extraSecureEvaluation(rule, context) {
const protectedContext = protectAgainstPrototypePollution(context);
return engine.evaluateExpr(rule, protectedContext);
}
Validate regex patterns to prevent ReDoS (Regular Expression Denial of Service):
function validateRegexPattern(pattern, flags = '') {
const errors = [];
// Check for dangerous patterns that could cause ReDoS
const dangerousPatterns = [
/\(\?\=.*\)\+/, // Positive lookahead with quantifier
/\(\?\!.*\)\+/, // Negative lookahead with quantifier
/\(\?\<=.*\)\+/, // Positive lookbehind with quantifier
/\(\?\<!.*\)\+/, // Negative lookbehind with quantifier
/\(\w\+\)\+/, // Nested quantifiers
/\(\w\*\)\+/, // Nested quantifiers
/\(\.\+\)\+/, // Nested quantifiers with .+
/\(\.\*\)\+/, // Nested quantifiers with .*
];
dangerousPatterns.forEach((dangerous, index) => {
if (dangerous.test(pattern)) {
errors.push(`Pattern contains potentially dangerous construct #${index + 1}`);
}
});
// Check pattern length
if (pattern.length > 1000) {
errors.push('Pattern too long (max 1000 characters)');
}
// Validate flags
const validFlags = ['g', 'i', 'm', 's', 'u', 'y'];
const flagChars = flags.split('');
const invalidFlags = flagChars.filter((f) => !validFlags.includes(f));
if (invalidFlags.length > 0) {
errors.push(`Invalid regex flags: ${invalidFlags.join(', ')}`);
}
// Test compilation
try {
new RegExp(pattern, flags);
} catch (error) {
errors.push(`Invalid regex pattern: ${error.message}`);
}
return { valid: errors.length === 0, errors };
}
// Safe regex operator wrapper
function createSecureRegexRule(field, pattern, options = {}) {
const validation = validateRegexPattern(pattern, options.flags);
if (!validation.valid) {
throw new Error(`Invalid regex pattern: ${validation.errors.join(', ')}`);
}
return { regex: [field, pattern, options] };
}
// Usage
try {
const safeRule = createSecureRegexRule('email', '^[\\w\\.-]+@[\\w\\.-]+\\.[a-zA-Z]{2,}$');
const result = engine.evaluateExpr(safeRule, { email: 'user@example.com' });
} catch (error) {
console.error('Regex validation failed:', error.message);
}
When generating rules dynamically, always validate inputs:
class SecureRuleBuilder {
constructor() {
this.allowedOperators = new Set([
'eq',
'neq',
'gt',
'gte',
'lt',
'lte',
'and',
'or',
'not',
'in',
'notIn',
'contains',
'startsWith',
'endsWith',
'between',
'isNull',
'isNotNull',
]);
this.allowedFields = new Set();
this.maxRuleDepth = 5;
this.maxOperators = 20;
}
setAllowedFields(fields) {
this.allowedFields = new Set(fields);
return this;
}
validateFieldAccess(field) {
if (typeof field !== 'string') {
throw new Error('Field must be a string');
}
// Check field whitelist
if (this.allowedFields.size > 0 && !this.allowedFields.has(field)) {
throw new Error(`Field '${field}' not in allowed fields list`);
}
// Check for dangerous patterns
if (
field.includes('__proto__') ||
field.includes('constructor') ||
field.includes('prototype')
) {
throw new Error(`Field '${field}' contains dangerous patterns`);
}
// Validate field format
if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(field)) {
throw new Error(`Field '${field}' has invalid format`);
}
return true;
}
validateOperator(operator) {
if (!this.allowedOperators.has(operator)) {
throw new Error(`Operator '${operator}' not allowed`);
}
return true;
}
validateValue(value) {
// Prevent dangerous values
if (typeof value === 'function') {
throw new Error('Function values not allowed');
}
if (typeof value === 'object' && value !== null) {
// Check for prototype pollution attempts
if (
value.hasOwnProperty('__proto__') ||
value.hasOwnProperty('constructor') ||
value.hasOwnProperty('prototype')
) {
throw new Error('Object contains dangerous properties');
}
}
return true;
}
createRule(operator, ...args) {
this.validateOperator(operator);
// Validate field arguments
if (
['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'startsWith', 'endsWith'].includes(
operator
)
) {
if (args.length < 2) {
throw new Error(`Operator '${operator}' requires at least 2 arguments`);
}
this.validateFieldAccess(args[0]);
this.validateValue(args[1]);
}
if (['in', 'notIn'].includes(operator)) {
if (args.length < 2) {
throw new Error(`Operator '${operator}' requires at least 2 arguments`);
}
this.validateFieldAccess(args[0]);
if (!Array.isArray(args[1])) {
throw new Error(`Operator '${operator}' requires array as second argument`);
}
args[1].forEach((value) => this.validateValue(value));
}
if (operator === 'between') {
if (args.length < 2) {
throw new Error('Between operator requires at least 2 arguments');
}
this.validateFieldAccess(args[0]);
if (!Array.isArray(args[1]) || args[1].length !== 2) {
throw new Error('Between operator requires array of 2 values');
}
args[1].forEach((value) => this.validateValue(value));
}
return { [operator]: args };
}
and(...conditions) {
if (conditions.length === 0) {
throw new Error('AND operator requires at least one condition');
}
return { and: conditions };
}
or(...conditions) {
if (conditions.length === 0) {
throw new Error('OR operator requires at least one condition');
}
return { or: conditions };
}
not(condition) {
if (!condition) {
throw new Error('NOT operator requires a condition');
}
return { not: [condition] };
}
}
// Usage
const secureBuilder = new SecureRuleBuilder().setAllowedFields([
'user.name',
'user.email',
'user.age',
'user.role',
]);
try {
const rule = secureBuilder.and(
secureBuilder.createRule('eq', 'user.role', 'admin'),
secureBuilder.createRule('gte', 'user.age', 18)
);
const result = engine.evaluateExpr(rule, context);
} catch (error) {
console.error('Secure rule building failed:', error.message);
}
Implement role-based access control for rule evaluation:
class SecureRuleEngine {
constructor(baseEngine, options = {}) {
this.baseEngine = baseEngine;
this.rolePermissions = new Map();
this.auditLog = [];
this.options = {
enableAudit: options.enableAudit !== false,
maxAuditEntries: options.maxAuditEntries || 1000,
...options,
};
}
defineRole(roleName, permissions) {
this.rolePermissions.set(roleName, {
...permissions,
createdAt: new Date(),
updatedAt: new Date(),
});
}
hasPermission(userRole, permission) {
const rolePerms = this.rolePermissions.get(userRole);
if (!rolePerms) return false;
return rolePerms[permission] === true;
}
evaluateExpr(rule, context, userInfo = {}) {
const { userId, role, sessionId } = userInfo;
// Log access attempt
this.auditLog.push({
timestamp: new Date(),
userId,
role,
sessionId,
action: 'rule_evaluation',
ruleHash: this.hashRule(rule),
contextKeys: Object.keys(context),
});
// Check basic rule evaluation permission
if (!this.hasPermission(role, 'evaluateRules')) {
const error = new Error('Insufficient permissions to evaluate rules');
this.auditLog.push({
timestamp: new Date(),
userId,
role,
action: 'access_denied',
reason: 'missing_evaluate_permission',
});
throw error;
}
// Check for sensitive field access
const sensitiveFields = this.extractSensitiveFields(rule);
if (sensitiveFields.length > 0 && !this.hasPermission(role, 'accessSensitiveFields')) {
const error = new Error('Insufficient permissions to access sensitive fields');
this.auditLog.push({
timestamp: new Date(),
userId,
role,
action: 'access_denied',
reason: 'sensitive_field_access',
sensitiveFields,
});
throw error;
}
try {
const result = this.baseEngine.evaluateExpr(rule, context);
// Log successful evaluation
this.auditLog.push({
timestamp: new Date(),
userId,
role,
action: 'rule_evaluation_success',
ruleHash: this.hashRule(rule),
result: result.success,
});
// Trim audit log if needed
if (this.auditLog.length > this.options.maxAuditEntries) {
this.auditLog = this.auditLog.slice(-this.options.maxAuditEntries);
}
return result;
} catch (error) {
// Log evaluation error
this.auditLog.push({
timestamp: new Date(),
userId,
role,
action: 'rule_evaluation_error',
error: error.message,
});
throw error;
}
}
extractSensitiveFields(rule) {
const sensitivePatterns = [
/password/i,
/ssn/i,
/social.*security/i,
/credit.*card/i,
/bank.*account/i,
/api.*key/i,
/secret/i,
/token/i,
];
const fields = this.extractAllFields(rule);
return fields.filter((field) => sensitivePatterns.some((pattern) => pattern.test(field)));
}
extractAllFields(rule, fields = new Set()) {
if (typeof rule !== 'object' || rule === null) {
return Array.from(fields);
}
Object.entries(rule).forEach(([operator, args]) => {
if (Array.isArray(args)) {
args.forEach((arg) => {
if (typeof arg === 'string' && arg.includes('.')) {
fields.add(arg);
} else if (typeof arg === 'object') {
this.extractAllFields(arg, fields);
}
});
}
});
return Array.from(fields);
}
hashRule(rule) {
// Simple hash for audit purposes
return JSON.stringify(rule).substring(0, 100) + '...';
}
getAuditLog(filters = {}) {
let filtered = this.auditLog;
if (filters.userId) {
filtered = filtered.filter((entry) => entry.userId === filters.userId);
}
if (filters.action) {
filtered = filtered.filter((entry) => entry.action === filters.action);
}
if (filters.since) {
const since = new Date(filters.since);
filtered = filtered.filter((entry) => entry.timestamp >= since);
}
return filtered;
}
}
// Setup roles and permissions
const secureEngine = new SecureRuleEngine(engine);
secureEngine.defineRole('admin', {
evaluateRules: true,
accessSensitiveFields: true,
manageRules: true,
});
secureEngine.defineRole('user', {
evaluateRules: true,
accessSensitiveFields: false,
manageRules: false,
});
secureEngine.defineRole('readonly', {
evaluateRules: true,
accessSensitiveFields: false,
manageRules: false,
});
// Usage with user context
const userInfo = {
userId: 'user123',
role: 'user',
sessionId: 'session456',
};
try {
const result = secureEngine.evaluateExpr(rule, context, userInfo);
} catch (error) {
console.error('Access denied:', error.message);
}
Implement field-level access controls:
class FieldSecurityManager {
constructor() {
this.fieldPermissions = new Map();
this.fieldClassifications = new Map();
}
classifyField(fieldPath, classification, options = {}) {
this.fieldClassifications.set(fieldPath, {
classification, // 'public', 'internal', 'confidential', 'restricted'
description: options.description,
dataType: options.dataType,
retention: options.retention,
createdAt: new Date(),
});
}
setFieldPermission(role, fieldPath, permissions) {
const key = `${role}:${fieldPath}`;
this.fieldPermissions.set(key, {
read: permissions.read !== false,
write: permissions.write === true,
...permissions,
updatedAt: new Date(),
});
}
canAccessField(role, fieldPath, operation = 'read') {
const key = `${role}:${fieldPath}`;
const permission = this.fieldPermissions.get(key);
if (!permission) {
// Check by classification
const classification = this.fieldClassifications.get(fieldPath);
if (classification) {
return this.getDefaultPermissionByClassification(
role,
classification.classification,
operation
);
}
// Default to deny
return false;
}
return permission[operation] === true;
}
getDefaultPermissionByClassification(role, classification, operation) {
const defaultPermissions = {
admin: {
public: { read: true, write: true },
internal: { read: true, write: true },
confidential: { read: true, write: true },
restricted: { read: true, write: false },
},
user: {
public: { read: true, write: false },
internal: { read: true, write: false },
confidential: { read: false, write: false },
restricted: { read: false, write: false },
},
readonly: {
public: { read: true, write: false },
internal: { read: false, write: false },
confidential: { read: false, write: false },
restricted: { read: false, write: false },
},
};
return defaultPermissions[role]?.[classification]?.[operation] === true;
}
filterContextByPermissions(context, role) {
const filtered = {};
const filterObject = (obj, path = '') => {
const result = {};
Object.entries(obj).forEach(([key, value]) => {
const fullPath = path ? `${path}.${key}` : key;
if (this.canAccessField(role, fullPath, 'read')) {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
result[key] = filterObject(value, fullPath);
} else {
result[key] = value;
}
}
});
return result;
};
return filterObject(context);
}
}
// Setup field security
const fieldSecurity = new FieldSecurityManager();
// Classify fields
fieldSecurity.classifyField('user.email', 'internal');
fieldSecurity.classifyField('user.ssn', 'restricted');
fieldSecurity.classifyField('user.name', 'public');
fieldSecurity.classifyField('user.salary', 'confidential');
// Set specific permissions
fieldSecurity.setFieldPermission('hr', 'user.salary', { read: true, write: true });
fieldSecurity.setFieldPermission('manager', 'user.salary', { read: true, write: false });
// Filter context before rule evaluation
function secureEvaluateWithFieldSecurity(rule, context, userRole) {
const filteredContext = fieldSecurity.filterContextByPermissions(context, userRole);
return engine.evaluateExpr(rule, filteredContext);
}
Implement automatic data masking for sensitive fields:
class DataMaskingService {
constructor() {
this.maskingRules = new Map();
this.sensitivePatterns = [
{ pattern: /\b\d{3}-\d{2}-\d{4}\b/g, type: 'ssn' },
{ pattern: /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/g, type: 'credit_card' },
{ pattern: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, type: 'email' },
{ pattern: /\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, type: 'phone' },
];
}
defineMaskingRule(fieldPath, maskingType, options = {}) {
this.maskingRules.set(fieldPath, {
type: maskingType,
maskChar: options.maskChar || '*',
preserveLength: options.preserveLength !== false,
preserveFormat: options.preserveFormat === true,
showLast: options.showLast || 0,
showFirst: options.showFirst || 0,
...options,
});
}
maskValue(value, maskingRule) {
if (typeof value !== 'string') {
return value;
}
switch (maskingRule.type) {
case 'full':
return maskingRule.maskChar.repeat(value.length);
case 'partial':
const showFirst = maskingRule.showFirst;
const showLast = maskingRule.showLast;
const maskLength = Math.max(0, value.length - showFirst - showLast);
return (
value.substring(0, showFirst) +
maskingRule.maskChar.repeat(maskLength) +
value.substring(value.length - showLast)
);
case 'format_preserving':
return this.formatPreservingMask(value, maskingRule);
case 'hash':
return this.hashValue(value);
case 'remove':
return '[REDACTED]';
default:
return value;
}
}
formatPreservingMask(value, rule) {
return value.replace(/[A-Za-z0-9]/g, rule.maskChar);
}
hashValue(value) {
// Simple hash for demonstration - use crypto.createHash in production
let hash = 0;
for (let i = 0; i < value.length; i++) {
const char = value.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash; // Convert to 32-bit integer
}
return `[HASH:${Math.abs(hash).toString(16)}]`;
}
maskContext(context, userRole) {
const masked = JSON.parse(JSON.stringify(context));
const maskObject = (obj, path = '') => {
Object.entries(obj).forEach(([key, value]) => {
const fullPath = path ? `${path}.${key}` : key;
const maskingRule = this.maskingRules.get(fullPath);
if (maskingRule && !this.userCanSeeUnmasked(userRole, fullPath)) {
obj[key] = this.maskValue(value, maskingRule);
} else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
maskObject(value, fullPath);
} else if (typeof value === 'string') {
// Auto-detect and mask sensitive patterns
obj[key] = this.autoMaskSensitiveData(value, userRole);
}
});
};
maskObject(masked);
return masked;
}
userCanSeeUnmasked(userRole, fieldPath) {
// Define roles that can see unmasked data for specific fields
const unmaskingPermissions = {
admin: ['*'], // Admin can see all
hr: ['user.ssn', 'user.salary', 'user.email'],
finance: ['user.salary', 'user.bankAccount'],
manager: ['user.email', 'user.phone'],
};
const permissions = unmaskingPermissions[userRole] || [];
return permissions.includes('*') || permissions.includes(fieldPath);
}
autoMaskSensitiveData(text, userRole) {
if (this.userCanSeeUnmasked(userRole, '*')) {
return text;
}
let maskedText = text;
this.sensitivePatterns.forEach(({ pattern, type }) => {
maskedText = maskedText.replace(pattern, (match) => {
switch (type) {
case 'ssn':
return 'XXX-XX-' + match.slice(-4);
case 'credit_card':
return '**** **** **** ' + match.slice(-4);
case 'email':
const [user, domain] = match.split('@');
return user.charAt(0) + '***@' + domain;
case 'phone':
return '***-***-' + match.slice(-4);
default:
return '[MASKED]';
}
});
});
return maskedText;
}
}
// Setup data masking
const dataMasking = new DataMaskingService();
// Define masking rules
dataMasking.defineMaskingRule('user.ssn', 'partial', {
showLast: 4,
maskChar: 'X',
});
dataMasking.defineMaskingRule('user.creditCard', 'partial', {
showLast: 4,
preserveFormat: true,
});
dataMasking.defineMaskingRule('user.password', 'full');
dataMasking.defineMaskingRule('user.apiKey', 'hash');
// Usage
function secureEvaluateWithMasking(rule, context, userRole) {
const maskedContext = dataMasking.maskContext(context, userRole);
return engine.evaluateExpr(rule, maskedContext);
}
// β
Secure production configuration
const productionEngine = createRuleEngine({
// Security settings
allowPrototypeAccess: false, // Never allow prototype access
strict: true, // Strict type checking
enableDebug: false, // Disable debug info
// Resource limits
maxDepth: 10, // Reasonable nesting limit
maxOperators: 100, // Prevent resource exhaustion
maxCacheSize: 1000, // Controlled memory usage
// Performance settings
enableCache: true, // Enable for performance
});
// Environment-specific configuration
function createSecureEngine(environment) {
const baseConfig = {
allowPrototypeAccess: false,
strict: true,
enableCache: true,
};
switch (environment) {
case 'production':
return createRuleEngine({
...baseConfig,
enableDebug: false,
maxDepth: 10,
maxOperators: 100,
maxCacheSize: 1000,
});
case 'staging':
return createRuleEngine({
...baseConfig,
enableDebug: false,
maxDepth: 15,
maxOperators: 200,
maxCacheSize: 2000,
});
case 'development':
return createRuleEngine({
...baseConfig,
enableDebug: true,
maxDepth: 20,
maxOperators: 500,
maxCacheSize: 500,
});
default:
throw new Error(`Unknown environment: ${environment}`);
}
}
// Configuration validation
function validateEngineConfig(config) {
const errors = [];
if (config.allowPrototypeAccess === true) {
errors.push('allowPrototypeAccess should be false in production');
}
if (config.enableDebug === true && process.env.NODE_ENV === 'production') {
errors.push('Debug mode should be disabled in production');
}
if (config.maxDepth > 20) {
errors.push('maxDepth should not exceed 20 to prevent deep recursion');
}
if (config.maxOperators > 1000) {
errors.push('maxOperators should not exceed 1000 to prevent resource exhaustion');
}
if (config.maxCacheSize > 10000) {
errors.push('maxCacheSize should not exceed 10000 to prevent memory issues');
}
return { valid: errors.length === 0, errors };
}
class SecurityAuditSystem {
constructor(options = {}) {
this.auditLog = [];
this.securityEvents = [];
this.options = {
maxLogEntries: options.maxLogEntries || 10000,
enableRealTimeAlerts: options.enableRealTimeAlerts !== false,
alertThresholds: {
failedEvaluations: options.failedEvaluations || 10,
suspiciousPatterns: options.suspiciousPatterns || 5,
timeWindow: options.timeWindow || 300000, // 5 minutes
},
...options,
};
this.alertHandlers = [];
}
logSecurityEvent(event) {
const auditEntry = {
id: this.generateId(),
timestamp: new Date(),
level: event.level || 'info',
category: event.category,
action: event.action,
userId: event.userId,
sessionId: event.sessionId,
ipAddress: event.ipAddress,
userAgent: event.userAgent,
details: event.details || {},
ruleHash: event.ruleHash,
success: event.success,
error: event.error,
};
this.auditLog.push(auditEntry);
if (auditEntry.level === 'warning' || auditEntry.level === 'error') {
this.securityEvents.push(auditEntry);
}
// Check for suspicious patterns
this.detectSuspiciousActivity(auditEntry);
// Trim logs if needed
if (this.auditLog.length > this.options.maxLogEntries) {
this.auditLog = this.auditLog.slice(-this.options.maxLogEntries);
}
return auditEntry.id;
}
detectSuspiciousActivity(currentEvent) {
const timeWindow = this.options.alertThresholds.timeWindow;
const cutoffTime = new Date(Date.now() - timeWindow);
// Recent events from same user/IP
const recentEvents = this.auditLog.filter(
(event) =>
event.timestamp >= cutoffTime &&
(event.userId === currentEvent.userId || event.ipAddress === currentEvent.ipAddress)
);
// Check for multiple failed evaluations
const failedEvaluations = recentEvents.filter(
(event) => event.success === false && event.category === 'rule_evaluation'
);
if (failedEvaluations.length >= this.options.alertThresholds.failedEvaluations) {
this.triggerAlert({
type: 'multiple_failed_evaluations',
severity: 'high',
userId: currentEvent.userId,
ipAddress: currentEvent.ipAddress,
count: failedEvaluations.length,
timeWindow: timeWindow,
});
}
// Check for suspicious rule patterns
const suspiciousPatterns = [
'__proto__',
'constructor',
'prototype',
'eval',
'Function',
'script',
];
if (currentEvent.ruleHash) {
const hasSuspiciousPattern = suspiciousPatterns.some((pattern) =>
currentEvent.ruleHash.toLowerCase().includes(pattern)
);
if (hasSuspiciousPattern) {
this.triggerAlert({
type: 'suspicious_rule_pattern',
severity: 'high',
userId: currentEvent.userId,
ruleHash: currentEvent.ruleHash,
patterns: suspiciousPatterns.filter((p) =>
currentEvent.ruleHash.toLowerCase().includes(p)
),
});
}
}
// Check for rapid successive requests
const rapidRequests = recentEvents.filter(
(event) => event.timestamp >= new Date(Date.now() - 60000) // Last minute
);
if (rapidRequests.length > 100) {
this.triggerAlert({
type: 'potential_dos_attack',
severity: 'critical',
userId: currentEvent.userId,
ipAddress: currentEvent.ipAddress,
requestCount: rapidRequests.length,
});
}
}
triggerAlert(alert) {
const alertEvent = {
id: this.generateId(),
timestamp: new Date(),
type: alert.type,
severity: alert.severity,
...alert,
};
console.warn('SECURITY ALERT:', alertEvent);
// Notify registered alert handlers
this.alertHandlers.forEach((handler) => {
try {
handler(alertEvent);
} catch (error) {
console.error('Alert handler failed:', error);
}
});
}
onAlert(handler) {
this.alertHandlers.push(handler);
}
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
getSecurityReport(timeRange = 3600000) {
// Default: last hour
const cutoffTime = new Date(Date.now() - timeRange);
const recentEvents = this.auditLog.filter((event) => event.timestamp >= cutoffTime);
const report = {
timeRange: timeRange,
totalEvents: recentEvents.length,
successfulEvaluations: recentEvents.filter((e) => e.success === true).length,
failedEvaluations: recentEvents.filter((e) => e.success === false).length,
uniqueUsers: new Set(recentEvents.map((e) => e.userId)).size,
uniqueIPs: new Set(recentEvents.map((e) => e.ipAddress)).size,
securityEvents: this.securityEvents.filter((e) => e.timestamp >= cutoffTime),
topErrors: this.getTopErrors(recentEvents),
suspiciousActivity: this.getSuspiciousActivity(recentEvents),
};
return report;
}
getTopErrors(events) {
const errorCounts = {};
events
.filter((e) => e.error)
.forEach((event) => {
errorCounts[event.error] = (errorCounts[event.error] || 0) + 1;
});
return Object.entries(errorCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([error, count]) => ({ error, count }));
}
getSuspiciousActivity(events) {
return events.filter(
(event) =>
event.level === 'warning' ||
event.level === 'error' ||
event.category === 'security_violation'
);
}
}
// Setup audit system
const auditSystem = new SecurityAuditSystem({
maxLogEntries: 50000,
alertThresholds: {
failedEvaluations: 5,
suspiciousPatterns: 3,
timeWindow: 300000, // 5 minutes
},
});
// Setup alert handlers
auditSystem.onAlert((alert) => {
// Send to security team
console.error('SECURITY ALERT:', alert);
// Could integrate with external systems:
// - Send email/SMS notifications
// - Log to SIEM system
// - Block IP addresses
// - Disable user accounts
});
// Wrapper for secure evaluation with auditing
function auditedEvaluateExpr(rule, context, userInfo = {}) {
const startTime = Date.now();
try {
auditSystem.logSecurityEvent({
category: 'rule_evaluation',
action: 'started',
userId: userInfo.userId,
sessionId: userInfo.sessionId,
ipAddress: userInfo.ipAddress,
userAgent: userInfo.userAgent,
ruleHash: JSON.stringify(rule).substring(0, 200),
});
const result = engine.evaluateExpr(rule, context);
auditSystem.logSecurityEvent({
category: 'rule_evaluation',
action: 'completed',
userId: userInfo.userId,
sessionId: userInfo.sessionId,
ipAddress: userInfo.ipAddress,
success: result.success,
duration: Date.now() - startTime,
details: {
operatorCount: JSON.stringify(rule).split('"').length,
contextSize: JSON.stringify(context).length,
},
});
return result;
} catch (error) {
auditSystem.logSecurityEvent({
level: 'error',
category: 'rule_evaluation',
action: 'failed',
userId: userInfo.userId,
sessionId: userInfo.sessionId,
ipAddress: userInfo.ipAddress,
success: false,
error: error.message,
duration: Date.now() - startTime,
});
throw error;
}
}
// β Dangerous: Accepting rules directly from user input
app.post('/evaluate-rule', (req, res) => {
const rule = req.body.rule; // Untrusted input!
const result = engine.evaluateExpr(rule, userData);
res.json(result);
});
// β
Safe: Validate and sanitize rules
app.post('/evaluate-rule', (req, res) => {
try {
const validation = validateRuleStructure(req.body.rule);
if (!validation.valid) {
return res.status(400).json({ error: 'Invalid rule', details: validation.errors });
}
const sanitizedContext = sanitizeContext(userData);
const result = secureEngine.evaluateExpr(req.body.rule, sanitizedContext, req.user);
res.json(result);
} catch (error) {
res.status(500).json({ error: 'Evaluation failed' });
}
});
// β Dangerous: Including sensitive data in context
const context = {
user: {
id: user.id,
email: user.email,
password: user.hashedPassword, // Sensitive!
ssn: user.ssn, // Sensitive!
creditCard: user.creditCard, // Sensitive!
apiKey: user.apiKey, // Sensitive!
},
};
// β
Safe: Filter context to only necessary data
const context = {
user: {
id: user.id,
email: user.email.split('@')[1], // Only domain for rules
role: user.role,
isActive: user.isActive,
// No sensitive data included
},
};
// β Dangerous: No validation on regex patterns
const userRule = {
regex: [userField, userPattern], // Could be malicious ReDoS pattern
};
// β
Safe: Validate regex patterns
function createSafeRegexRule(field, pattern, flags) {
const validation = validateRegexPattern(pattern, flags);
if (!validation.valid) {
throw new Error(`Invalid pattern: ${validation.errors.join(', ')}`);
}
return { regex: [field, pattern, { flags }] };
}
// β Dangerous: No rate limiting on rule evaluation
app.post('/evaluate', (req, res) => {
const result = engine.evaluateExpr(req.body.rule, context);
res.json(result);
});
// β
Safe: Implement rate limiting
const rateLimit = require('express-rate-limit');
const evaluationLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // Limit each IP to 100 requests per windowMs
message: 'Too many evaluation requests, please try again later.',
});
app.post('/evaluate', evaluationLimiter, (req, res) => {
// Evaluation logic with rate limiting
});
// β Dangerous: Exposing internal error details
app.post('/evaluate', (req, res) => {
try {
const result = engine.evaluateExpr(rule, context);
res.json(result);
} catch (error) {
res.status(500).json({
error: error.message, // Might expose internals
stack: error.stack, // Definitely exposes internals
context: context, // Exposes data
});
}
});
// β
Safe: Generic error responses
app.post('/evaluate', (req, res) => {
try {
const result = engine.evaluateExpr(rule, context);
res.json(result);
} catch (error) {
// Log detailed error internally
logger.error('Rule evaluation failed', {
error: error.message,
stack: error.stack,
userId: req.user?.id,
});
// Return generic error to client
res.status(500).json({
error: 'Evaluation failed',
requestId: generateRequestId(),
});
}
});
const assert = require('assert');
describe('Security Tests', () => {
let engine;
beforeEach(() => {
engine = createRuleEngine({
allowPrototypeAccess: false,
strict: true,
maxDepth: 10,
maxOperators: 100,
});
});
describe('Prototype Pollution Protection', () => {
it('should block __proto__ access', () => {
const maliciousContext = {
__proto__: { isAdmin: true },
user: { name: 'test' },
};
const rule = { eq: ['__proto__.isAdmin', true] };
const result = engine.evaluateExpr(rule, maliciousContext);
assert.strictEqual(result.success, false);
});
it('should block constructor.prototype access', () => {
const maliciousContext = {
constructor: { prototype: { isAdmin: true } },
user: { name: 'test' },
};
const rule = { eq: ['constructor.prototype.isAdmin', true] };
const result = engine.evaluateExpr(rule, maliciousContext);
assert.strictEqual(result.success, false);
});
it('should allow safe property access', () => {
const safeContext = {
user: { name: 'John', role: 'user' },
};
const rule = { eq: ['user.name', 'John'] };
const result = engine.evaluateExpr(rule, safeContext);
assert.strictEqual(result.success, true);
});
});
describe('Function Access Prevention', () => {
it('should block function property access', () => {
const contextWithFunctions = {
user: {
name: 'John',
getName: function () {
return this.name;
},
dangerousFunc: function () {
return eval('process.env');
},
},
};
const rule1 = { eq: ['user.getName', 'something'] };
const result1 = engine.evaluateExpr(rule1, contextWithFunctions);
assert.strictEqual(result1.success, false);
const rule2 = { eq: ['user.dangerousFunc', 'something'] };
const result2 = engine.evaluateExpr(rule2, contextWithFunctions);
assert.strictEqual(result2.success, false);
});
});
describe('Input Validation', () => {
it('should reject rules exceeding max depth', () => {
// Create deeply nested rule
let deepRule = { eq: ['value', 1] };
for (let i = 0; i < 15; i++) {
deepRule = { and: [deepRule, { eq: ['other', 2] }] };
}
assert.throws(() => {
engine.evaluateExpr(deepRule, { value: 1, other: 2 });
}, /exceeds maximum depth/);
});
it('should reject rules with too many operators', () => {
// Create rule with many operators
const conditions = [];
for (let i = 0; i < 150; i++) {
conditions.push({ eq: [`field${i}`, i] });
}
const bigRule = { and: conditions };
assert.throws(() => {
engine.evaluateExpr(bigRule, {});
}, /exceeds maximum operators/);
});
});
describe('Regex Security', () => {
it('should handle potentially dangerous regex patterns safely', () => {
const dangerousPatterns = [
'(a+)+', // Catastrophic backtracking
'(a|a)*', // Catastrophic backtracking
'a{10000000}', // Resource exhaustion
'(?=.*a)(?=.*b)(?=.*c)(?=.*d)(?=.*e).*', // Many lookaheads
];
dangerousPatterns.forEach((pattern) => {
const rule = { regex: ['text', pattern] };
const context = { text: 'aaaaaaaaaaaaaaaaaaaaX' };
// Should either work quickly or fail safely
const startTime = Date.now();
try {
engine.evaluateExpr(rule, context);
} catch (error) {
// Acceptable to fail with invalid pattern
}
const endTime = Date.now();
// Should not take more than 1 second
assert(endTime - startTime < 1000, `Pattern took too long: ${pattern}`);
});
});
});
describe('Memory Safety', () => {
it('should handle large contexts safely', () => {
const largeContext = {
largeArray: new Array(10000).fill('data'),
largeString: 'x'.repeat(100000),
user: { name: 'test' },
};
const rule = { eq: ['user.name', 'test'] };
// Should not crash or consume excessive memory
const result = engine.evaluateExpr(rule, largeContext);
assert.strictEqual(result.success, true);
});
it('should handle circular references gracefully', () => {
const circularContext = { name: 'test' };
circularContext.self = circularContext;
const rule = { eq: ['name', 'test'] };
// Should handle circular reference without crashing
const result = engine.evaluateExpr(rule, circularContext);
assert.strictEqual(result.success, true);
});
});
});
// Penetration testing helpers
function generateMaliciousPayloads() {
return [
// Prototype pollution attempts
{ __proto__: { isAdmin: true } },
{ 'constructor.prototype.isAdmin': true },
{ 'prototype.polluted': true },
// Code injection attempts
{ eval: 'process.env' },
{ Function: 'return process' },
{ script: "<script>alert('xss')</script>" },
// Path traversal attempts
{ '../../../etc/passwd': 'content' },
{ '..\\..\\..\\windows\\system32': 'content' },
// Buffer overflow attempts
{ longString: 'A'.repeat(1000000) },
{ deepNesting: createDeeplyNested(100) },
// SQL injection style (even though not SQL)
{ "'; DROP TABLE users; --": 'malicious' },
{ "1' OR '1'='1": 'malicious' },
];
}
function createDeeplyNested(depth) {
let obj = { value: 'end' };
for (let i = 0; i < depth; i++) {
obj = { level: i, nested: obj };
}
return obj;
}
// Security regression tests
describe('Security Regression Tests', () => {
const maliciousPayloads = generateMaliciousPayloads();
maliciousPayloads.forEach((payload, index) => {
it(`should handle malicious payload ${index + 1} safely`, () => {
const rule = { eq: ['test', 'value'] };
assert.doesNotThrow(() => {
const result = engine.evaluateExpr(rule, payload);
// Result can be true or false, but should not throw or crash
});
});
});
});
class SecurityScanner {
constructor(engine) {
this.engine = engine;
this.vulnerabilities = [];
}
scanRule(rule) {
const issues = [];
// Check for suspicious patterns
const ruleStr = JSON.stringify(rule);
const suspiciousPatterns = [
/__proto__/,
/constructor\.prototype/,
/eval\(/,
/Function\(/,
/process\./,
/require\(/,
/import\(/,
];
suspiciousPatterns.forEach((pattern, index) => {
if (pattern.test(ruleStr)) {
issues.push({
type: 'suspicious_pattern',
severity: 'high',
pattern: pattern.source,
location: ruleStr.indexOf(pattern.source),
});
}
});
// Check rule complexity
const complexity = this.calculateComplexity(rule);
if (complexity.depth > 10) {
issues.push({
type: 'excessive_depth',
severity: 'medium',
depth: complexity.depth,
maxRecommended: 10,
});
}
if (complexity.operators > 100) {
issues.push({
type: 'excessive_operators',
severity: 'medium',
operators: complexity.operators,
maxRecommended: 100,
});
}
// Check for regex patterns
const regexPatterns = this.extractRegexPatterns(rule);
regexPatterns.forEach((pattern) => {
const regexIssues = this.analyzeRegexPattern(pattern);
issues.push(...regexIssues);
});
return issues;
}
scanContext(context) {
const issues = [];
// Check for sensitive data exposure
const sensitiveFields = this.findSensitiveFields(context);
if (sensitiveFields.length > 0) {
issues.push({
type: 'sensitive_data_exposure',
severity: 'high',
fields: sensitiveFields,
});
}
// Check context size
const contextSize = JSON.stringify(context).length;
if (contextSize > 1000000) {
// 1MB
issues.push({
type: 'large_context',
severity: 'medium',
size: contextSize,
maxRecommended: 1000000,
});
}
return issues;
}
calculateComplexity(rule, depth = 0) {
if (typeof rule !== 'object' || rule === null) {
return { depth, operators: 0 };
}
let maxDepth = depth;
let totalOperators = Object.keys(rule).length;
Object.values(rule).forEach((value) => {
if (Array.isArray(value)) {
value.forEach((item) => {
if (typeof item === 'object' && item !== null) {
const childComplexity = this.calculateComplexity(item, depth + 1);
maxDepth = Math.max(maxDepth, childComplexity.depth);
totalOperators += childComplexity.operators;
}
});
}
});
return { depth: maxDepth, operators: totalOperators };
}
extractRegexPatterns(rule) {
const patterns = [];
const extract = (obj) => {
if (typeof obj !== 'object' || obj === null) return;
Object.entries(obj).forEach(([key, value]) => {
if (key === 'regex' && Array.isArray(value) && value.length >= 2) {
patterns.push(value[1]);
} else if (typeof value === 'object') {
extract(value);
} else if (Array.isArray(value)) {
value.forEach((item) => extract(item));
}
});
};
extract(rule);
return patterns;
}
analyzeRegexPattern(pattern) {
const issues = [];
// Check for ReDoS vulnerabilities
const redosPatterns = [
/\(\w\+\)\+/, // (a+)+
/\(\w\*\)\+/, // (a*)+
/\(\w\+\)\*/, // (a+)*
/\(\.\+\)\+/, // (.+)+
/\(\.\*\)\+/, // (.*)+
];
redosPatterns.forEach((redos) => {
if (redos.test(pattern)) {
issues.push({
type: 'regex_redos_vulnerability',
severity: 'critical',
pattern: pattern,
vulnerability: redos.source,
});
}
});
// Check pattern length
if (pattern.length > 500) {
issues.push({
type: 'regex_too_long',
severity: 'medium',
pattern: pattern,
length: pattern.length,
});
}
return issues;
}
findSensitiveFields(obj, path = '', sensitiveFields = []) {
if (typeof obj !== 'object' || obj === null) return sensitiveFields;
const sensitivePatterns = [
/password/i,
/passwd/i,
/secret/i,
/key/i,
/token/i,
/ssn/i,
/social.?security/i,
/credit.?card/i,
/api.?key/i,
/private/i,
];
Object.entries(obj).forEach(([key, value]) => {
const fullPath = path ? `${path}.${key}` : key;
// Check if field name matches sensitive patterns
if (sensitivePatterns.some((pattern) => pattern.test(key))) {
sensitiveFields.push({
path: fullPath,
field: key,
type: typeof value,
});
}
// Recursively check nested objects
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
this.findSensitiveFields(value, fullPath, sensitiveFields);
}
});
return sensitiveFields;
}
generateSecurityReport() {
return {
timestamp: new Date(),
vulnerabilities: this.vulnerabilities,
summary: {
total: this.vulnerabilities.length,
critical: this.vulnerabilities.filter((v) => v.severity === 'critical').length,
high: this.vulnerabilities.filter((v) => v.severity === 'high').length,
medium: this.vulnerabilities.filter((v) => v.severity === 'medium').length,
low: this.vulnerabilities.filter((v) => v.severity === 'low').length,
},
};
}
}
// Usage
const scanner = new SecurityScanner(engine);
function secureEvaluateWithScanning(rule, context) {
// Scan rule for security issues
const ruleIssues = scanner.scanRule(rule);
if (ruleIssues.some((issue) => issue.severity === 'critical')) {
throw new Error('Rule contains critical security vulnerabilities');
}
// Scan context for sensitive data
const contextIssues = scanner.scanContext(context);
if (contextIssues.some((issue) => issue.severity === 'high')) {
console.warn('Context contains sensitive data:', contextIssues);
}
return engine.evaluateExpr(rule, context);
}
allowPrototypeAccess
set to false
strict
mode enabled for productionenableDebug
disabled in productionmaxDepth
, maxOperators
)class SecurityDashboard {
constructor(auditSystem) {
this.auditSystem = auditSystem;
}
generateDashboard() {
const report = this.auditSystem.getSecurityReport(86400000); // Last 24 hours
return {
summary: {
timestamp: new Date(),
totalEvents: report.totalEvents,
securityAlerts: report.securityEvents.length,
failureRate: ((report.failedEvaluations / report.totalEvents) * 100).toFixed(2) + '%',
uniqueUsers: report.uniqueUsers,
uniqueIPs: report.uniqueIPs,
},
alerts: {
critical: report.securityEvents.filter((e) => e.severity === 'critical'),
high: report.securityEvents.filter((e) => e.severity === 'high'),
medium: report.securityEvents.filter((e) => e.severity === 'medium'),
},
topThreats: {
suspiciousIPs: this.getTopSuspiciousIPs(report),
failedAttempts: this.getTopFailedAttempts(report),
errorPatterns: report.topErrors,
},
recommendations: this.generateRecommendations(report),
};
}
getTopSuspiciousIPs(report) {
const ipCounts = {};
report.securityEvents.forEach((event) => {
if (event.ipAddress) {
ipCounts[event.ipAddress] = (ipCounts[event.ipAddress] || 0) + 1;
}
});
return Object.entries(ipCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([ip, count]) => ({ ip, alertCount: count }));
}
getTopFailedAttempts(report) {
const userCounts = {};
report.securityEvents
.filter((e) => e.action === 'access_denied' || e.action === 'rule_evaluation_error')
.forEach((event) => {
if (event.userId) {
userCounts[event.userId] = (userCounts[event.userId] || 0) + 1;
}
});
return Object.entries(userCounts)
.sort(([, a], [, b]) => b - a)
.slice(0, 10)
.map(([userId, count]) => ({ userId, failureCount: count }));
}
generateRecommendations(report) {
const recommendations = [];
if (report.failedEvaluations / report.totalEvents > 0.1) {
recommendations.push({
type: 'high_failure_rate',
message: 'High failure rate detected. Review rule validation logic.',
priority: 'high',
});
}
if (report.securityEvents.length > 50) {
recommendations.push({
type: 'many_security_events',
message: 'High number of security events. Consider tightening access controls.',
priority: 'medium',
});
}
return recommendations;
}
}
// Usage
const dashboard = new SecurityDashboard(auditSystem);
const securityOverview = dashboard.generateDashboard();
console.log('Security Dashboard:', securityOverview);
allowPrototypeAccess: false
Rule Engine JS implements multiple security layers:
Remember: Security is not optional. Always implement these practices in production environments to protect your application and usersβ data.