mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-09-12 19:51:00 +05:00
Merge 81afaf45e2 into 853ca33659
This commit is contained in:
commit
327471c241
65
apps/dokploy/__test__/registry/redact-secrets.test.ts
Normal file
65
apps/dokploy/__test__/registry/redact-secrets.test.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { redactExecError, redactSecrets } from "@dokploy/server/services/registry";
|
||||
import { ExecError } from "@dokploy/server/utils/process/execAsync";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("redactSecrets", () => {
|
||||
it("replaces a secret with ***", () => {
|
||||
const command = `printf %s 'sup3r-secret' | docker login registry.example.com -u user --password-stdin`;
|
||||
expect(redactSecrets(command, ["sup3r-secret"])).toBe(
|
||||
"printf %s '***' | docker login registry.example.com -u user --password-stdin",
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts every occurrence of a secret", () => {
|
||||
expect(redactSecrets("pw=abc and again abc", ["abc"])).toBe(
|
||||
"pw=*** and again ***",
|
||||
);
|
||||
});
|
||||
|
||||
it("redacts multiple secrets", () => {
|
||||
expect(redactSecrets("a then b", ["a", "b"])).toBe("*** then ***");
|
||||
});
|
||||
|
||||
it("ignores empty, null and undefined secrets", () => {
|
||||
const text = "nothing to redact";
|
||||
expect(redactSecrets(text, ["", null, undefined])).toBe(text);
|
||||
});
|
||||
|
||||
it("leaves the text untouched when the secret is absent", () => {
|
||||
const text = "no secret here";
|
||||
expect(redactSecrets(text, ["other"])).toBe(text);
|
||||
});
|
||||
|
||||
it("redacts a shell-escaped password", () => {
|
||||
const escaped = "'a'\\''b'"; // shEscape("a'b")
|
||||
const command = `printf %s ${escaped} | docker login`;
|
||||
expect(redactSecrets(command, [escaped])).toBe(
|
||||
"printf %s *** | docker login",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactExecError", () => {
|
||||
it("redacts the secret from an ExecError message and command", () => {
|
||||
const error = new ExecError("Command execution failed: pw=hunter2", {
|
||||
command: "docker login -p hunter2",
|
||||
stderr: "auth failed for hunter2",
|
||||
});
|
||||
const redacted = redactExecError(error, ["hunter2"]);
|
||||
expect(redacted).toBeInstanceOf(ExecError);
|
||||
const execError = redacted as ExecError;
|
||||
expect(execError.message).toBe("Command execution failed: pw=***");
|
||||
expect(execError.command).toBe("docker login -p ***");
|
||||
expect(execError.stderr).toBe("auth failed for ***");
|
||||
});
|
||||
|
||||
it("redacts the secret from a plain Error message", () => {
|
||||
const error = new Error("failed with token abc123");
|
||||
const redacted = redactExecError(error, ["abc123"]) as Error;
|
||||
expect(redacted.message).toBe("failed with token ***");
|
||||
});
|
||||
|
||||
it("returns non-error values unchanged", () => {
|
||||
expect(redactExecError("just a string", ["x"])).toBe("just a string");
|
||||
});
|
||||
});
|
||||
@ -39,6 +39,11 @@ import {
|
||||
updateDeploymentStatus,
|
||||
} from "./deployment";
|
||||
import { type Domain, getDomainHost } from "./domain";
|
||||
import {
|
||||
getRegistryPasswords,
|
||||
redactExecError,
|
||||
redactSecrets,
|
||||
} from "./registry";
|
||||
import {
|
||||
createPreviewDeploymentComment,
|
||||
getIssueComment,
|
||||
@ -246,11 +251,17 @@ export const deployApplication = async ({
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
const passwords = await getRegistryPasswords([
|
||||
application.registryId,
|
||||
application.buildRegistryId,
|
||||
application.rollbackRegistryId,
|
||||
]);
|
||||
const rawMessage = error instanceof Error ? error.message : String(error);
|
||||
const safeMessage = redactSecrets(rawMessage, passwords);
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
const encodedMessage = encodeBase64(safeMessage);
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
@ -267,13 +278,12 @@ export const deployApplication = async ({
|
||||
projectName: application.environment.project.name,
|
||||
applicationName: application.name,
|
||||
applicationType: "application",
|
||||
// @ts-ignore
|
||||
errorMessage: error?.message || "Error building",
|
||||
errorMessage: safeMessage || "Error building",
|
||||
buildLink,
|
||||
organizationId: application.environment.project.organizationId,
|
||||
});
|
||||
|
||||
throw error;
|
||||
throw redactExecError(error, passwords);
|
||||
} finally {
|
||||
// Only extract commit info for non-docker sources
|
||||
if (application.sourceType !== "docker") {
|
||||
@ -337,11 +347,16 @@ export const rebuildApplication = async ({
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
const passwords = await getRegistryPasswords([
|
||||
application.registryId,
|
||||
application.buildRegistryId,
|
||||
application.rollbackRegistryId,
|
||||
]);
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
const encodedMessage = encodeBase64(redactSecrets(message, passwords));
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
@ -353,7 +368,7 @@ export const rebuildApplication = async ({
|
||||
}
|
||||
await updateDeploymentStatus(deployment.deploymentId, "error");
|
||||
await updateApplicationStatus(applicationId, "error");
|
||||
throw error;
|
||||
throw redactExecError(error, passwords);
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -579,11 +594,16 @@ export const rebuildPreviewApplication = async ({
|
||||
});
|
||||
} catch (error) {
|
||||
let command = "";
|
||||
const passwords = await getRegistryPasswords([
|
||||
application.registryId,
|
||||
application.buildRegistryId,
|
||||
application.rollbackRegistryId,
|
||||
]);
|
||||
|
||||
// Only log details for non-ExecError errors
|
||||
if (!(error instanceof ExecError)) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const encodedMessage = encodeBase64(message);
|
||||
const encodedMessage = encodeBase64(redactSecrets(message, passwords));
|
||||
command += `echo "${encodedMessage}" | base64 -d >> "${deployment.logPath}";`;
|
||||
}
|
||||
|
||||
@ -604,7 +624,7 @@ export const rebuildPreviewApplication = async ({
|
||||
await updatePreviewDeployment(previewDeploymentId, {
|
||||
previewStatus: "error",
|
||||
});
|
||||
throw error;
|
||||
throw redactExecError(error, passwords);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@ -3,9 +3,10 @@ import { type apiCreateRegistry, registry } from "@dokploy/server/db/schema";
|
||||
import {
|
||||
execAsync,
|
||||
execAsyncRemote,
|
||||
ExecError,
|
||||
} from "@dokploy/server/utils/process/execAsync";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { eq, inArray } from "drizzle-orm";
|
||||
import type { z } from "zod";
|
||||
import { IS_CLOUD } from "../constants";
|
||||
|
||||
@ -37,6 +38,55 @@ function sanitizeRegistryError(
|
||||
return message.split(password).join("***");
|
||||
}
|
||||
|
||||
export const redactSecrets = (
|
||||
text: string,
|
||||
secrets: (string | null | undefined)[],
|
||||
): string => {
|
||||
let result = text;
|
||||
for (const secret of secrets) {
|
||||
if (secret) {
|
||||
result = result.split(secret).join("***");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const getRegistryPasswords = async (
|
||||
registryIds: (string | null | undefined)[],
|
||||
): Promise<(string | null)[]> => {
|
||||
const uniqueIds = [...new Set(registryIds.filter((id): id is string => !!id))];
|
||||
if (uniqueIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const rows = await db.query.registry.findMany({
|
||||
where: inArray(registry.registryId, uniqueIds),
|
||||
columns: { password: true },
|
||||
});
|
||||
return rows.flatMap((row) =>
|
||||
row.password ? [row.password, shEscape(row.password)] : [row.password],
|
||||
);
|
||||
};
|
||||
|
||||
export const redactExecError = (
|
||||
error: unknown,
|
||||
secrets: (string | null | undefined)[],
|
||||
): unknown => {
|
||||
if (error instanceof ExecError) {
|
||||
return new ExecError(redactSecrets(error.message, secrets), {
|
||||
command: redactSecrets(error.command, secrets),
|
||||
stdout: error.stdout ? redactSecrets(error.stdout, secrets) : error.stdout,
|
||||
stderr: error.stderr ? redactSecrets(error.stderr, secrets) : error.stderr,
|
||||
exitCode: error.exitCode,
|
||||
originalError: undefined,
|
||||
serverId: error.serverId,
|
||||
});
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
error.message = redactSecrets(error.message, secrets);
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
export const createRegistry = async (
|
||||
input: z.infer<typeof apiCreateRegistry>,
|
||||
organizationId: string,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user