Redact release deployment failure diagnostics

This commit is contained in:
Jeppe B
2026-06-01 21:27:28 +02:00
parent 3616e197e1
commit 2c01feb3be
2 changed files with 62 additions and 8 deletions
@@ -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.")
);
}
+29
View File
@@ -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", {