@codai/memorai-core
Version:
Simplified advanced memory engine - no tiers, just powerful semantic search with persistence
465 lines (464 loc) • 16.2 kB
JavaScript
/**
* Security utilities and enhancements for Memorai
* Includes input validation, rate limiting, and security auditing
*/
import { createHash, randomBytes, createCipheriv, createDecipheriv, timingSafeEqual, } from 'crypto';
import { logger } from '../utils/logger.js';
export class InputValidator {
/**
* Validate input against a set of rules
*/
static validate(input, rules) {
const errors = [];
for (const rule of rules) {
const value = input[rule.field];
// Check required fields
if (rule.required && (value === undefined || value === null)) {
errors.push(`Field '${rule.field}' is required`);
continue;
}
// Skip validation for optional empty fields
if (!rule.required && (value === undefined || value === null)) {
continue;
}
// Type validation
if (!this.validateType(value, rule.type)) {
errors.push(`Field '${rule.field}' must be of type ${rule.type}`);
continue;
}
// String-specific validations
if (rule.type === 'string' && typeof value === 'string') {
if (rule.minLength && value.length < rule.minLength) {
errors.push(`Field '${rule.field}' must be at least ${rule.minLength} characters`);
}
if (rule.maxLength && value.length > rule.maxLength) {
errors.push(`Field '${rule.field}' must be no more than ${rule.maxLength} characters`);
}
if (rule.pattern && !rule.pattern.test(value)) {
errors.push(`Field '${rule.field}' format is invalid`);
}
}
// Array-specific validations
if (rule.type === 'array' && Array.isArray(value)) {
if (rule.minLength && value.length < rule.minLength) {
errors.push(`Field '${rule.field}' must have at least ${rule.minLength} items`);
}
if (rule.maxLength && value.length > rule.maxLength) {
errors.push(`Field '${rule.field}' must have no more than ${rule.maxLength} items`);
}
} // Allowed values validation
if (rule.allowedValues && rule.allowedValues.length > 0) {
const allowedValues = rule.allowedValues;
if (!allowedValues.includes(value)) {
errors.push(`Field '${rule.field}' must be one of: ${rule.allowedValues.join(', ')}`);
}
}
// Custom validation
if (rule.customValidator) {
const customResult = rule.customValidator(value);
if (customResult !== true) {
errors.push(typeof customResult === 'string'
? customResult
: `Field '${rule.field}' is invalid`);
}
}
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* Sanitize string input to prevent injection attacks
*/
static sanitizeString(input) {
if (typeof input !== 'string') {
return '';
}
return input
.replace(/[<>]/g, '') // Remove potential HTML tags
.replace(/['"]/g, '') // Remove quotes that could break SQL/JS
.replace(/[\\]/g, '') // Remove backslashes
.trim();
}
/**
* Validate and sanitize memory content
*/
static validateMemoryContent(content) {
const errors = [];
if (!content || typeof content !== 'string') {
errors.push('Content must be a non-empty string');
return { isValid: false, sanitizedContent: '', errors };
}
if (content.length > 10000) {
errors.push('Content exceeds maximum length of 10,000 characters');
}
if (content.length < 1) {
errors.push('Content cannot be empty');
}
// Check for suspicious patterns
const suspiciousPatterns = [
/<script[^>]*>/i,
/javascript:/i,
/on\w+\s*=/i,
/expression\s*\(/i,
];
for (const pattern of suspiciousPatterns) {
if (pattern.test(content)) {
errors.push('Content contains potentially malicious code');
break;
}
}
const sanitizedContent = this.sanitizeString(content);
return {
isValid: errors.length === 0,
sanitizedContent,
errors,
};
}
static validateType(value, expectedType) {
switch (expectedType) {
case 'string':
return typeof value === 'string';
case 'number':
return typeof value === 'number' && !isNaN(value);
case 'boolean':
return typeof value === 'boolean';
case 'object':
return (typeof value === 'object' && value !== null && !Array.isArray(value));
case 'array':
return Array.isArray(value);
default:
return false;
}
}
}
export class RateLimiter {
constructor(config) {
this.config = config;
this.requests = new Map();
}
/**
* Check if a request should be allowed
*/
isAllowed(tenantId, agentId) {
const key = this.config.keyGenerator
? this.config.keyGenerator(tenantId, agentId)
: `${tenantId}:${agentId || 'default'}`;
const now = Date.now();
const windowStart = Math.floor(now / this.config.windowMs) * this.config.windowMs;
let requestData = this.requests.get(key);
// Initialize or reset window
if (!requestData || requestData.windowStart < windowStart) {
requestData = { count: 0, windowStart };
this.requests.set(key, requestData);
}
const allowed = requestData.count < this.config.maxRequests;
if (allowed) {
requestData.count++;
}
return {
allowed,
remainingRequests: Math.max(0, this.config.maxRequests - requestData.count),
resetTime: new Date(windowStart + this.config.windowMs),
};
}
/**
* Get current rate limit status for a tenant/agent
*/
getStatus(tenantId, agentId) {
const result = this.isAllowed(tenantId, agentId);
const key = this.config.keyGenerator
? this.config.keyGenerator(tenantId, agentId)
: `${tenantId}:${agentId || 'default'}`;
const requestData = this.requests.get(key);
return {
requestCount: requestData?.count || 0,
remainingRequests: result.remainingRequests,
resetTime: result.resetTime,
};
}
/**
* Clear rate limit data (useful for testing)
*/
clear() {
this.requests.clear();
}
}
export class SecurityAuditor {
constructor() {
this.auditLog = [];
this.maxLogSize = 10000;
}
/**
* Log a security audit event
*/
logEvent(event) {
const auditEvent = {
timestamp: new Date(),
...event,
};
this.auditLog.push(auditEvent);
// Rotate log if it gets too large
if (this.auditLog.length > this.maxLogSize) {
this.auditLog = this.auditLog.slice(-Math.floor(this.maxLogSize * 0.8));
} // Log critical events immediately
if (event.severity === 'critical') {
logger.error('CRITICAL SECURITY EVENT:', auditEvent);
}
}
/**
* Get audit events for a specific tenant
*/
getTenantAuditLog(tenantId, options = {}) {
let events = this.auditLog.filter(event => event.tenantId === tenantId);
if (options.startDate) {
events = events.filter(event => event.timestamp >= options.startDate);
}
if (options.endDate) {
events = events.filter(event => event.timestamp <= options.endDate);
}
if (options.eventType) {
events = events.filter(event => event.eventType === options.eventType);
}
if (options.severity) {
events = events.filter(event => event.severity === options.severity);
}
// Sort by timestamp descending (newest first)
events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
if (options.limit) {
events = events.slice(0, options.limit);
}
return events;
}
/**
* Get security statistics
*/
getSecurityStats(tenantId) {
const events = tenantId
? this.auditLog.filter(event => event.tenantId === tenantId)
: this.auditLog;
const now = new Date();
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const recentEvents = events.filter(event => event.timestamp >= oneDayAgo);
const recentCriticalEvents = recentEvents.filter(event => event.severity === 'critical').length;
const eventsBySeverity = events.reduce((acc, event) => {
acc[event.severity] = (acc[event.severity] || 0) + 1;
return acc;
}, {});
const eventsByType = events.reduce((acc, event) => {
acc[event.eventType] = (acc[event.eventType] || 0) + 1;
return acc;
}, {});
const failedEvents = events.filter(event => !event.success).length;
const failureRate = events.length > 0 ? failedEvents / events.length : 0;
return {
totalEvents: events.length,
eventsBySeverity,
eventsByType,
failureRate,
recentCriticalEvents,
};
}
/**
* Export audit log for external security systems
*/
exportAuditLog(format = 'json') {
if (format === 'csv') {
const headers = [
'timestamp',
'eventType',
'severity',
'tenantId',
'agentId',
'action',
'resource',
'success',
'errorMessage',
];
const rows = this.auditLog.map(event => [
event.timestamp.toISOString(),
event.eventType,
event.severity,
event.tenantId,
event.agentId || '',
event.action,
event.resource || '',
event.success.toString(),
event.errorMessage || '',
]);
return [headers, ...rows].map(row => row.join(',')).join('\n');
}
return JSON.stringify(this.auditLog, null, 2);
}
}
export class EncryptionManager {
constructor(encryptionKey) {
this.encryptionKey = encryptionKey;
this.algorithm = 'aes-256-cbc';
if (!encryptionKey || encryptionKey.length < 32) {
throw new Error('Encryption key must be at least 32 characters long');
}
// Create a 32-byte key from the input string
this.key = createHash('sha256').update(encryptionKey).digest();
} /**
* Encrypt sensitive data
*/
encrypt(text) {
const iv = randomBytes(16);
const cipher = createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
return iv.toString('hex') + ':' + encrypted;
}
/**
* Decrypt sensitive data
*/
decrypt(encryptedText) {
const parts = encryptedText.split(':');
if (parts.length !== 2) {
throw new Error('Invalid encrypted text format');
}
const ivPart = parts[0];
const encryptedPart = parts[1];
if (!ivPart || !encryptedPart) {
throw new Error('Invalid encrypted text format - missing parts');
}
const iv = Buffer.from(ivPart, 'hex');
const decipher = createDecipheriv(this.algorithm, this.key, iv);
let decrypted = decipher.update(encryptedPart, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
/**
* Hash data for secure comparison
*/
hash(data, salt) {
const actualSalt = salt || randomBytes(16).toString('hex');
const hash = createHash('sha256');
hash.update(data + actualSalt);
return actualSalt + ':' + hash.digest('hex');
}
/**
* Verify hashed data
*/
verifyHash(data, hashedData) {
const parts = hashedData.split(':');
if (parts.length !== 2) {
return false;
}
const salt = parts[0];
const expectedHash = parts[1];
if (!salt || !expectedHash) {
return false;
}
const actualHashWithSalt = this.hash(data, salt);
const actualHashParts = actualHashWithSalt.split(':');
if (actualHashParts.length !== 2) {
return false;
}
const actualHash = actualHashParts[1];
if (!actualHash) {
return false;
}
return timingSafeEqual(Buffer.from(expectedHash, 'hex'), Buffer.from(actualHash, 'hex'));
}
}
export class SecurityManager {
constructor(config) {
this.config = config;
this.encryptionManager = new EncryptionManager(config.encryptionKey);
this.auditor = new SecurityAuditor();
const rateLimitConfig = config.rateLimiting || {
windowMs: 60000, // 1 minute
maxRequests: 100, // 100 requests per minute
};
this.rateLimiter = new RateLimiter(rateLimitConfig);
}
/**
* Encrypt sensitive data
*/
encrypt(data) {
return this.encryptionManager.encrypt(data);
}
/**
* Decrypt sensitive data
*/
decrypt(encryptedData) {
return this.encryptionManager.decrypt(encryptedData);
}
/**
* Hash data for secure comparison
*/
hash(data, salt) {
return this.encryptionManager.hash(data, salt);
}
/**
* Verify hashed data
*/
verifyHash(data, hashedData) {
return this.encryptionManager.verifyHash(data, hashedData);
}
/**
* Validate input data
*/
validateInput(input, rules) {
return InputValidator.validate(input, rules);
}
/**
* Check if operation is rate limited
*/
checkRateLimit(tenantId, agentId) {
const result = this.rateLimiter.isAllowed(tenantId, agentId);
return result.allowed;
}
/**
* Record security audit event
*/
auditEvent(event) {
if (this.config.auditLog) {
this.auditor.logEvent(event);
}
}
/**
* Get security audit events
*/
getAuditEvents(filter) {
if (!filter?.tenantId) {
return [];
}
const auditOptions = {};
if (filter.startDate) {
auditOptions.startDate = filter.startDate;
}
if (filter.endDate) {
auditOptions.endDate = filter.endDate;
}
if (filter.eventType) {
auditOptions.eventType =
filter.eventType;
}
if (filter.severity) {
auditOptions.severity = filter.severity;
}
return this.auditor.getTenantAuditLog(filter.tenantId, auditOptions);
}
/**
* Get security statistics
*/
getStats() {
const securityStats = this.auditor.getSecurityStats();
return {
totalEvents: securityStats.totalEvents,
eventsByType: securityStats.eventsByType,
eventsBySeverity: securityStats.eventsBySeverity,
rateLimitStatus: {}, // Rate limiter doesn't have persistent stats
};
}
}
// Global instances for convenience
export const globalRateLimiter = new RateLimiter({
windowMs: 60000, // 1 minute
maxRequests: 100, // 100 requests per minute per tenant
});
export const securityAuditor = new SecurityAuditor();