UNPKG

@proguardian/cli

Version:

Guardian supervision layer for AI coding assistants

196 lines (166 loc) 6.15 kB
/** * Custom error classes for ProGuardian CLI * These provide specific error types for better error handling and security */ import path from 'path' import { error as logError } from './logger.js' /** * Sanitize error messages to prevent information disclosure */ export function sanitizeErrorMessage(message) { if (typeof message !== 'string') { return String(message) } let safe = message // Remove IPv6 addresses FIRST (before ID replacement) // Full IPv6 pattern safe = safe.replace(/([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}/g, '<ip>') // Compressed IPv6 patterns safe = safe.replace(/([0-9a-fA-F]{1,4}:){1,7}:/g, '<ip>') safe = safe.replace(/:([0-9a-fA-F]{1,4}:){1,7}/g, '<ip>') // Any remaining IPv6-like patterns safe = safe.replace(/([0-9a-fA-F]{1,4}:){2,}[0-9a-fA-F]{0,4}/g, '<ip>') // Remove paths - handle relative paths FIRST to avoid partial matches // Relative paths: ../path or ./path (must have at least one slash) safe = safe.replace(/\.{1,2}([\\/][^\s"'<>|]*)+/g, '<path>') // Then handle absolute paths // Windows absolute paths: C:\path or C:/path safe = safe.replace(/[a-zA-Z]:[\\//][^\s"'<>|]+/g, '<path>') // Unix absolute paths: /path (but not if it's already <path>) safe = safe.replace(/(?<!<path>)\/[^\s"'<>|]+/g, '<path>') // Windows UNC paths: \\server\share safe = safe.replace(/\\\\[^\s"'<>|]+/g, '<path>') // Remove UUIDs BEFORE other ID replacements safe = safe.replace( /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, '<uuid>', ) // Remove potential sensitive data patterns safe = safe.replace(/\b\d{4,}\b/g, '<id>') // IDs (4+ digits) safe = safe.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, '<email>') // Emails safe = safe.replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, '<ip>') // IPv4 addresses // Remove potential tokens/secrets (long hex strings, base64-like strings) safe = safe.replace(/\b[a-fA-F0-9]{32,}\b/g, '<token>') // Hex tokens (32+ chars) safe = safe.replace(/[A-Za-z0-9+/]{20,}={0,2}/g, '<token>') // Base64-like tokens return safe } export class ProGuardianError extends Error { constructor(message, code) { super(message) this.name = this.constructor.name this.code = code Error.captureStackTrace(this, this.constructor) } } export class SecurityError extends ProGuardianError { constructor(message, details = {}) { // Never expose sensitive details in error messages const safeMessage = sanitizeErrorMessage(message) super(safeMessage, 'SECURITY_ERROR') this.details = details } } export class ValidationError extends ProGuardianError { constructor(field, requirement, value = undefined) { // Include sanitized value in message if provided let message = `Invalid ${field}: ${requirement}` if (value !== undefined) { const sanitizedValue = sanitizeErrorMessage(String(value)) message += ` (got: ${sanitizedValue})` } super(message, 'VALIDATION_ERROR') this.field = field this.requirement = requirement this.value = value } } export class PermissionError extends ProGuardianError { constructor(operation, filePath) { // Don't expose full paths in error messages // Use path.basename for cross-platform compatibility const safePath = filePath ? path.basename(filePath) : 'file' const message = `Permission denied: Cannot ${operation} ${safePath}` super(message, 'PERMISSION_ERROR') this.operation = operation } } export class PathTraversalError extends SecurityError { constructor(attemptedPath) { super('Invalid path: Path traversal attempt detected') this.attemptedPath = attemptedPath } } export class CommandInjectionError extends SecurityError { constructor(command) { super('Invalid command: Potentially unsafe characters detected') this.command = command // Store for debugging, but don't expose in message } } export class FileOperationError extends ProGuardianError { constructor(operation, filePath, details) { const safePath = filePath ? path.basename(filePath) : 'file' const message = `File operation failed: Cannot ${operation} ${safePath}${details ? `: ${details}` : ''}` super(message, 'FILE_OPERATION_ERROR') this.operation = operation this.filePath = filePath this.details = details } } export class CLINotFoundError extends ProGuardianError { constructor(cliName) { super(`${cliName} CLI not found. Please install it first.`, 'CLI_NOT_FOUND') this.cliName = cliName } } /** * Format error for display */ export function formatError(error, verbose = false) { if (!error) { return 'Unknown error' } if (typeof error === 'string') { return error } if (typeof error === 'number') { return String(error) } let result = error.message || 'Unknown error' if (error.code) { result = `[${error.code}] ${result}` } if (verbose && error.cause) { result += `\nCaused by: ${formatError(error.cause, verbose)}` } if (verbose && error.stack && process.env.NODE_ENV === 'development') { result += `\n${error.stack}` } return result } /** * Handle errors consistently across the application */ export function handleError(error, options = {}) { const { exit = true, verbose = false } = options if (error instanceof SecurityError) { logError(`Security violation: ${error.message}`) if (verbose && process.env.NODE_ENV === 'development') { logError(`Details: ${JSON.stringify(error.details)}`) } } else if (error instanceof ValidationError) { logError(`Validation failed: ${error.message}`) } else if (error instanceof PermissionError) { logError(`Permission denied: ${error.message}`) } else if (error instanceof ProGuardianError) { logError(`${error.code}: ${error.message}`) } else { // Unknown errors - don't expose stack traces in production logError('An unexpected error occurred') if (verbose || process.env.NODE_ENV === 'development') { logError(error.message || error.toString(), error) } } if (exit) { process.exit(1) } }