Skip to main content
Glama
security-utils.ts7.09 kB
/** * Security utilities for sanitizing logs and handling errors safely */ /** * Sensitive field patterns to redact from logs */ const SENSITIVE_PATTERNS = [ /password/i, /token/i, /key/i, /secret/i, /auth/i, /bearer/i, /credential/i, /api[_-]?key/i, /access[_-]?token/i, /refresh[_-]?token/i, /private[_-]?key/i, /client[_-]?secret/i, ]; /** * Sensitive header names to redact */ const SENSITIVE_HEADERS = [ 'authorization', 'x-api-key', 'x-auth-token', 'cookie', 'set-cookie', ]; /** * Sanitize an object by redacting sensitive fields */ export function sanitizeObject(obj: any, depth = 0): any { if (depth > 10) return '[Max Depth Reached]'; // Prevent infinite recursion if (obj === null || obj === undefined) return obj; if (typeof obj === 'string') { return sanitizeString(obj); } if (typeof obj !== 'object') return obj; if (Array.isArray(obj)) { return obj.map((item) => sanitizeObject(item, depth + 1)); } const sanitized: any = {}; for (const [key, value] of Object.entries(obj)) { if (isSensitiveField(key)) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = sanitizeObject(value, depth + 1); } } return sanitized; } /** * Check if a field name is sensitive */ function isSensitiveField(fieldName: string): boolean { const lowerField = fieldName.toLowerCase(); // Check against sensitive headers if (SENSITIVE_HEADERS.includes(lowerField)) { return true; } // Check against sensitive patterns return SENSITIVE_PATTERNS.some((pattern) => pattern.test(fieldName)); } /** * Sanitize a string by masking potential sensitive data */ function sanitizeString(str: string): string { // Mask potential tokens/keys (anything that looks like a long alphanumeric string) return str .replace(/\b[A-Za-z0-9+/]{20,}={0,2}\b/g, '[REDACTED]') .replace(/\b[A-Fa-f0-9]{32,}\b/g, '[REDACTED]') .replace(/Bearer\s+[A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]'); } /** * Sanitize HTTP headers for logging */ export function sanitizeHeaders( headers: Record<string, any>, ): Record<string, any> { const sanitized: Record<string, any> = {}; for (const [key, value] of Object.entries(headers)) { if (isSensitiveField(key)) { sanitized[key] = '[REDACTED]'; } else { sanitized[key] = sanitizeObject(value); } } return sanitized; } /** * Error types for categorization */ export enum ErrorCategory { USER_ERROR = 'user_error', // Safe to show to user SYSTEM_ERROR = 'system_error', // Internal error, don't expose details NETWORK_ERROR = 'network_error', // Connection issues, safe basic info VALIDATION_ERROR = 'validation_error', // Input validation, safe to show } /** * Categorized error interface */ export interface CategorizedError { category: ErrorCategory; publicMessage: string; internalMessage: string; statusCode?: number; originalError?: Error; } /** * Categorize and sanitize errors for safe user exposure */ export function categorizeError( error: any, context?: string, ): CategorizedError { const contextPrefix = context ? `[${context}] ` : ''; // Network/HTTP errors if (error?.response?.status) { const status = error.response.status; if (status === 401) { return { category: ErrorCategory.USER_ERROR, publicMessage: 'Authentication failed. Please check your Grafana token.', internalMessage: `${contextPrefix}HTTP 401: ${error.message}`, statusCode: 401, originalError: error, }; } if (status === 403) { return { category: ErrorCategory.USER_ERROR, publicMessage: 'Permission denied. Insufficient privileges for this operation.', internalMessage: `${contextPrefix}HTTP 403: ${error.message}`, statusCode: 403, originalError: error, }; } if (status === 404) { return { category: ErrorCategory.USER_ERROR, publicMessage: 'Resource not found. Please verify the identifier and try again.', internalMessage: `${contextPrefix}HTTP 404: ${error.message}`, statusCode: 404, originalError: error, }; } if (status >= 400 && status < 500) { return { category: ErrorCategory.USER_ERROR, publicMessage: 'Invalid request. Please check your parameters and try again.', internalMessage: `${contextPrefix}HTTP ${status}: ${error.message}`, statusCode: status, originalError: error, }; } if (status >= 500) { return { category: ErrorCategory.SYSTEM_ERROR, publicMessage: 'Grafana server error. Please try again later.', internalMessage: `${contextPrefix}HTTP ${status}: ${error.message}`, statusCode: status, originalError: error, }; } } // Network connection errors if ( error?.code === 'ECONNREFUSED' || error?.code === 'ENOTFOUND' || error?.code === 'ETIMEDOUT' ) { return { category: ErrorCategory.NETWORK_ERROR, publicMessage: 'Unable to connect to Grafana. Please check the server URL and network connection.', internalMessage: `${contextPrefix}Network error: ${error.message}`, originalError: error, }; } // Validation errors (Zod, etc.) if (error?.name === 'ZodError' || error?.issues) { return { category: ErrorCategory.VALIDATION_ERROR, publicMessage: 'Invalid input parameters. Please check your request and try again.', internalMessage: `${contextPrefix}Validation error: ${error.message}`, originalError: error, }; } // Default system error for unknown issues return { category: ErrorCategory.SYSTEM_ERROR, publicMessage: 'An unexpected error occurred. Please try again.', internalMessage: `${contextPrefix}Unknown error: ${error?.message || 'No error message available'}`, originalError: error, }; } /** * Format error for user display */ export function formatUserError(categorizedError: CategorizedError): string { return categorizedError.publicMessage; } /** * Format error for internal logging */ export function formatInternalError( categorizedError: CategorizedError, ): string { const details = []; details.push(`Category: ${categorizedError.category}`); details.push(`Message: ${categorizedError.internalMessage}`); if (categorizedError.statusCode) { details.push(`Status: ${categorizedError.statusCode}`); } if (categorizedError.originalError?.stack) { details.push(`Stack: ${categorizedError.originalError.stack}`); } return details.join(' | '); } /** * Safe JSON stringify that handles circular references and sanitizes sensitive data */ export function safeStringify(obj: any, sanitize = true): string { try { const processedObj = sanitize ? sanitizeObject(obj) : obj; return JSON.stringify(processedObj, null, 2); } catch (_error) { return '[Unable to serialize object - circular reference or other issue]'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/quanticsoul4772/grafana-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server