/**
* postgres-mcp - Code Mode Security
*
* Input validation, rate limiting, and audit logging for code execution.
*/
import { logger } from '../utils/logger.js';
import {
DEFAULT_SECURITY_CONFIG,
type SecurityConfig,
type ValidationResult,
type ExecutionRecord,
type SandboxResult
} from './types.js';
/**
* Security manager for Code Mode executions
*/
export class CodeModeSecurityManager {
private readonly config: SecurityConfig;
private readonly rateLimitMap = new Map<string, { count: number; resetTime: number }>();
constructor(config?: Partial<SecurityConfig>) {
this.config = { ...DEFAULT_SECURITY_CONFIG, ...config };
}
/**
* Validate code before execution
*/
validateCode(code: string): ValidationResult {
const errors: string[] = [];
// Check code length
if (!code || typeof code !== 'string') {
errors.push('Code must be a non-empty string');
return { valid: false, errors };
}
if (code.length > this.config.maxCodeLength) {
errors.push(`Code exceeds maximum length of ${String(this.config.maxCodeLength)} bytes`);
return { valid: false, errors };
}
// Check for blocked patterns
for (const pattern of this.config.blockedPatterns) {
if (pattern.test(code)) {
errors.push(`Blocked pattern detected: ${pattern.source}`);
}
}
return {
valid: errors.length === 0,
errors
};
}
/**
* Check rate limit for a client
* @returns true if within limits, false if rate limited
*/
checkRateLimit(clientId: string): boolean {
const now = Date.now();
const windowMs = 60000; // 1 minute window
const existing = this.rateLimitMap.get(clientId);
if (!existing || now >= existing.resetTime) {
// Start new window
this.rateLimitMap.set(clientId, {
count: 1,
resetTime: now + windowMs
});
return true;
}
if (existing.count >= this.config.maxExecutionsPerMinute) {
return false;
}
existing.count++;
return true;
}
/**
* Get remaining rate limit for a client
*/
getRateLimitRemaining(clientId: string): number {
const existing = this.rateLimitMap.get(clientId);
if (!existing || Date.now() >= existing.resetTime) {
return this.config.maxExecutionsPerMinute;
}
return Math.max(0, this.config.maxExecutionsPerMinute - existing.count);
}
/**
* Sanitize and truncate result if too large
*/
sanitizeResult(result: unknown): unknown {
try {
const serialized = JSON.stringify(result);
if (serialized.length > this.config.maxResultSize) {
return {
_truncated: true,
_originalSize: serialized.length,
_maxSize: this.config.maxResultSize,
preview: serialized.substring(0, 1000) + '...'
};
}
return result;
} catch {
return {
_error: 'Result could not be serialized',
_type: typeof result
};
}
}
/**
* Log execution for audit purposes
*/
auditLog(execution: ExecutionRecord): void {
const { id, clientId, codePreview, result, readonly } = execution;
const logContext = {
module: 'CODEMODE' as const,
operation: 'execute',
entityId: id,
clientId: clientId ?? 'anonymous',
readonly,
success: result.success,
wallTimeMs: result.metrics.wallTimeMs,
memoryUsedMb: result.metrics.memoryUsedMb
};
if (result.success) {
logger.info(`Code execution completed: ${codePreview.substring(0, 50)}...`, logContext);
} else {
const errorContext = {
...logContext,
...(result.error !== undefined ? { error: result.error } : {}),
...(result.stack !== undefined ? { stack: result.stack } : {})
};
logger.warning(`Code execution failed: ${result.error ?? 'unknown error'}`, errorContext);
}
}
/**
* Create execution record for audit
*/
createExecutionRecord(
code: string,
result: SandboxResult,
readonly: boolean,
clientId?: string
): ExecutionRecord {
return {
id: crypto.randomUUID(),
clientId,
timestamp: new Date(),
codePreview: code.length > 200 ? code.substring(0, 200) + '...' : code,
result,
readonly
};
}
/**
* Clean up old rate limit entries
*/
cleanupRateLimits(): void {
const now = Date.now();
for (const [clientId, entry] of this.rateLimitMap) {
if (now >= entry.resetTime) {
this.rateLimitMap.delete(clientId);
}
}
}
}