feat(telemetry): capture error messages with security hardening

## Summary
Enhanced telemetry system to capture actual error messages for debugging
while implementing comprehensive security hardening to protect sensitive data.

## Changes
- Added optional errorMessage parameter to trackError() method
- Implemented sanitizeErrorMessage() with 7-layer security protection
- Updated all production and test call sites (atomic change)
- Added 18 new security-focused tests

## Security Fixes
- ReDoS Prevention: Early truncation + simplified regex patterns
- Full URL Redaction: Changed [URL]/path → [URL] to prevent leakage
- Credential Detection: AWS keys, GitHub tokens, JWT, Bearer tokens
- Correct Sanitization Order: URLs → credentials → emails → generic
- Error Handling: Try-catch wrapper with [SANITIZATION_FAILED] fallback

## Impact
- Resolves 272+ weekly errors with no error messages
- Protects against ReDoS attacks
- Prevents API structure and credential leakage
- 90.75% test coverage, 269 tests passing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-10-03 15:44:04 +02:00
parent 2a9a3b9410
commit c3b691cedf
9 changed files with 268 additions and 16 deletions

View File

@@ -127,7 +127,7 @@ export class TelemetryEventTracker {
/**
* Track an error event
*/
trackError(errorType: string, context: string, toolName?: string): void {
trackError(errorType: string, context: string, toolName?: string, errorMessage?: string): void {
if (!this.isEnabled()) return;
// Don't rate limit error tracking - we want to see all errors
@@ -135,6 +135,7 @@ export class TelemetryEventTracker {
errorType: this.sanitizeErrorType(errorType),
context: this.sanitizeContext(context),
tool: toolName ? toolName.replace(/[^a-zA-Z0-9_-]/g, '_') : undefined,
error: errorMessage ? this.sanitizeErrorMessage(errorMessage) : undefined,
}, false); // Skip rate limiting for errors
}
@@ -428,4 +429,56 @@ export class TelemetryEventTracker {
}
return sanitized;
}
/**
* Sanitize error message
*/
private sanitizeErrorMessage(errorMessage: string): string {
try {
// Early truncate to prevent ReDoS and performance issues
const maxLength = 1500;
const trimmed = errorMessage.length > maxLength
? errorMessage.substring(0, maxLength)
: errorMessage;
// Handle stack traces - keep only first 3 lines (message + top stack frames)
const lines = trimmed.split('\n');
let sanitized = lines.slice(0, 3).join('\n');
// Sanitize sensitive data in correct order to prevent leakage
// 1. URLs first (most encompassing) - fully redact to prevent path leakage
sanitized = sanitized.replace(/https?:\/\/\S+/gi, '[URL]');
// 2. Specific credential patterns (before generic patterns)
sanitized = sanitized
.replace(/AKIA[A-Z0-9]{16}/g, '[AWS_KEY]')
.replace(/ghp_[a-zA-Z0-9]{36,}/g, '[GITHUB_TOKEN]')
.replace(/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g, '[JWT]')
.replace(/Bearer\s+[^\s]+/gi, 'Bearer [TOKEN]');
// 3. Emails (after URLs to avoid partial matches)
sanitized = sanitized.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, '[EMAIL]');
// 4. Long keys and quoted tokens
sanitized = sanitized
.replace(/\b[a-zA-Z0-9_-]{32,}\b/g, '[KEY]')
.replace(/(['"])[a-zA-Z0-9_-]{16,}\1/g, '$1[TOKEN]$1');
// 5. Generic credential patterns (after specific ones to avoid conflicts)
sanitized = sanitized
.replace(/password\s*[=:]\s*\S+/gi, 'password=[REDACTED]')
.replace(/api[_-]?key\s*[=:]\s*\S+/gi, 'api_key=[REDACTED]')
.replace(/(?<!Bearer\s)token\s*[=:]\s*\S+/gi, 'token=[REDACTED]'); // Negative lookbehind to avoid Bearer tokens
// Final truncate to 500 chars
if (sanitized.length > 500) {
sanitized = sanitized.substring(0, 500) + '...';
}
return sanitized;
} catch (error) {
logger.debug('Error message sanitization failed:', error);
return '[SANITIZATION_FAILED]';
}
}
}