From 327e8181af94af5f8a5d61c40ba7f9abeb22a491 Mon Sep 17 00:00:00 2001 From: Duane Adam Date: Tue, 16 Dec 2025 10:43:18 +0800 Subject: [PATCH] Add copy logs button with PII/secret sanitization Add a copy button to individual container logs that strips sensitive data before copying to clipboard. Includes sanitization for emails, database URLs with passwords, JWT tokens, API keys, private key blocks, and git access tokens. --- app/Livewire/Project/Shared/GetLogs.php | 5 + bootstrap/helpers/shared.php | 24 ++++ .../project/shared/get-logs.blade.php | 15 +++ tests/Unit/SanitizeLogsForExportTest.php | 103 ++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 tests/Unit/SanitizeLogsForExportTest.php diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index f57563330..c0ced26ef 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -179,6 +179,11 @@ public function getLogs($refresh = false) } } + public function copyLogs(): string + { + return sanitizeLogsForExport($this->outputs); + } + public function render() { return view('livewire.project.shared.get-logs'); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 1066f1a63..2943432ac 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -653,6 +653,30 @@ function removeAnsiColors($text) return preg_replace('/\e[[][A-Za-z0-9];?[0-9]*m?/', '', $text); } +function sanitizeLogsForExport(string $text): string +{ + // Use existing helper for tokens and ANSI codes + $text = remove_iip($text); + + // Database URLs with passwords - must run before email regex to prevent false matches + // (postgres://user:password@host → postgres://user:@host) + $text = preg_replace('/((?:postgres|mysql|mongodb|rediss?|mariadb):\/\/[^:]+:)[^@]+(@)/i', '$1'.REDACTED.'$2', $text); + + // Email addresses + $text = preg_replace('/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/', REDACTED, $text); + + // Bearer/JWT tokens + $text = preg_replace('/Bearer\s+[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+\.[A-Za-z0-9\-_]+/i', 'Bearer '.REDACTED, $text); + + // API keys (common patterns) + $text = preg_replace('/(api[_-]?key|apikey|api[_-]?secret|secret[_-]?key)[=:]\s*[\'"]?[A-Za-z0-9\-_]{16,}[\'"]?/i', '$1='.REDACTED, $text); + + // Private key blocks + $text = preg_replace('/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/', REDACTED, $text); + + return $text; +} + function getTopLevelNetworks(Service|Application $resource) { if ($resource->getMorphClass() === \App\Models\Service::class) { diff --git a/resources/views/livewire/project/shared/get-logs.blade.php b/resources/views/livewire/project/shared/get-logs.blade.php index 8504a160f..f9a804aeb 100644 --- a/resources/views/livewire/project/shared/get-logs.blade.php +++ b/resources/views/livewire/project/shared/get-logs.blade.php @@ -235,6 +235,21 @@ class="p-1 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text- d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99" /> +