diff --git a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue index a28c1dc7..26c4111d 100644 --- a/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue +++ b/src/views/dashboards/superUserDashboard/configuration/ConfigurationReleaseManager.vue @@ -1150,8 +1150,10 @@ function normalizeReleaseStatusService(service) { status: service?.status || service?.state || "ready", state: service?.state || service?.status || "ready", severity: service?.severity || "ok", - message: service?.message || trFallback("status.service_ready", "Release service is ready."), - next_action: service?.next_action || "", + message: redactSensitiveDiagnostics( + service?.message || trFallback("status.service_ready", "Release service is ready.") + ), + next_action: redactSensitiveDiagnostics(service?.next_action || ""), target_tab: service?.target_tab || "overview", target_id: service?.target_id ?? null, deployment_id: service?.deployment_id ?? null, @@ -1175,8 +1177,8 @@ function normalizeReleaseStatusIssue(issue) { channel_slug: issue?.channel_slug || "", service_key: issue?.service_key || null, label: issue?.label || releaseStatusServiceLabel(issue?.service_key), - message: issue?.message || "", - next_action: issue?.next_action || "", + message: redactSensitiveDiagnostics(issue?.message || ""), + next_action: redactSensitiveDiagnostics(issue?.next_action || ""), target_tab: issue?.target_tab || "overview", target_id: issue?.target_id ?? null, deployment_id: issue?.deployment_id ?? null, @@ -4076,12 +4078,35 @@ function deploymentFailureSummary(deployment) { return summary && typeof summary === "object" ? summary : null; } +function redactSensitiveDiagnostics(value) { + let text = String(value || "").trim(); + if (!text) { + return ""; + } + + text = text.replace(/(https?:\/\/)([^\s/@:]+):([^\s/@]+)@/gi, "$1[redacted]@"); + text = text.replace(/\b(?:gh[opsru]_|github_pat_|glpat-|xox[baprs]-)[A-Za-z0-9_\-]{8,}\b/g, "[redacted-token]"); + text = text.replace(/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/gi, "$1 [redacted]"); + text = text.replace( + /\b((?:[A-Z][A-Z0-9_]*_)?(?:PASSWORD|PASSWD|SECRET|TOKEN|API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|AUTH)[A-Z0-9_]*)\s*=\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, + "$1=[redacted]" + ); + text = text.replace( + /\b(?:10\.\d{1,3}\.\d{1,3}\.\d{1,3}|172\.(?:1[6-9]|2\d|3[01])\.\d{1,3}\.\d{1,3}|192\.168\.\d{1,3}\.\d{1,3})\b/g, + "[redacted-private-ip]" + ); + text = text.replace(/\b[a-z0-9.-]+\.internal\b/gi, "[redacted-internal-host]"); + text = text.replace(/(?:^|\s)(?:\/[A-Za-z0-9._-]+){2,}\/\.env\b/g, " [redacted-env-path]"); + + return text; +} + function deploymentFailureCause(deployment) { - return String(deploymentFailureSummary(deployment)?.root_cause || deployment?.error_message || "").trim(); + return redactSensitiveDiagnostics(deploymentFailureSummary(deployment)?.root_cause || deployment?.error_message || ""); } function deploymentFailureNextAction(deployment) { - return String(deploymentFailureSummary(deployment)?.next_action || "").trim(); + return redactSensitiveDiagnostics(deploymentFailureSummary(deployment)?.next_action || ""); } function canPromoteDeployment(deployment) { @@ -4101,9 +4126,9 @@ function promotionBlockedReason(deployment) { return tr("actions.promote"); } - return ( + return redactSensitiveDiagnostics( deployment?.promotion_blocked_reason || - trFallback("deployments.promotion_blocked", "Only successfully deployed release deployments can be promoted.") + trFallback("deployments.promotion_blocked", "Only successfully deployed release deployments can be promoted.") ); } diff --git a/tests/e2e/release-manager.spec.js b/tests/e2e/release-manager.spec.js index 0aaadda5..bde9afd8 100644 --- a/tests/e2e/release-manager.spec.js +++ b/tests/e2e/release-manager.spec.js @@ -2231,6 +2231,35 @@ test("view-only release managers cannot search assignment subjects", async ({ pa expect(state.assignmentSubjectRequests || []).toEqual([]); }); +test("view-only release managers see redacted deployment failure diagnostics", async ({ page }) => { + const state = createReleaseState(); + state.deployments[0].error_message = + "Clone failed for https://deploy:ghp_SECRET_TOKEN_123456789@github.internal/acme/api.git DB_PASSWORD=s3cr3t internal-host=10.0.4.12"; + state.deployments[0].failure_summary.root_cause = state.deployments[0].error_message; + state.deployments[0].failure_summary.next_action = + "Rotate ghp_SECRET_TOKEN_123456789 and inspect /var/lib/coolify/apps/api/.env before retrying."; + + await boot(page, state, { + permissions: ["superuser_release_manager_view"], + }); + await page.goto("/superuser/configuration/releases/overview?channel=canary&app=api&branch=master", { + waitUntil: "domcontentloaded", + }); + + await expectReleaseManagerReady(page); + await expect(page.getByTestId("release-manager-page")).not.toContainText("ghp_SECRET_TOKEN_123456789"); + await expect(page.getByTestId("release-manager-page")).not.toContainText("DB_PASSWORD=s3cr3t"); + await expect(page.getByTestId("release-manager-page")).not.toContainText("10.0.4.12"); + await expect(page.getByTestId("release-manager-page")).not.toContainText("github.internal"); + + await selectReleaseTab(page, "Deployments"); + await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted]"); + await expect(page.getByTestId("release-deployment-failure-2")).toContainText("DB_PASSWORD=[redacted]"); + await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-private-ip]"); + await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-internal-host]"); + await expect(page.getByTestId("release-deployment-failure-2")).toContainText("[redacted-env-path]"); +}); + test("superusers manage release settings, assignments, integrations, and sync operations", async ({ page }) => { const state = await boot(page); await page.goto("/superuser/configuration/releases/overview?channel=canary&app=api&branch=canary", {