Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions apps/dokploy/__test__/admission/cosign-args.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import {
buildCosignArgs,
buildCosignDockerArgv,
DEFAULT_COSIGN_IMAGE,
} from "@dokploy/server/utils/admission/verify-signature";
import { describe, expect, it } from "vitest";

const REF = `ghcr.io/org/app@sha256:${"a".repeat(64)}`;
const MIRROR = `mirror.example.com/cosign@sha256:${"b".repeat(64)}`;

describe("buildCosignArgs", () => {
it("keyed mode uses --key env://COSIGN_KEY and ends with the ref", () => {
const args = buildCosignArgs(REF, {
mode: "keyed",
publicKey: "-----BEGIN PUBLIC KEY-----\n…",
});
expect(args).toEqual(["verify", "--key", "env://COSIGN_KEY", REF]);
});

it("keyless mode uses identity + issuer flags", () => {
const args = buildCosignArgs(REF, {
mode: "keyless",
certificateIdentityRegexp: "^https://github.com/org/.+$",
certificateOidcIssuer: "https://token.actions.githubusercontent.com",
});
expect(args).toEqual([
"verify",
"--certificate-identity-regexp",
"^https://github.com/org/.+$",
"--certificate-oidc-issuer",
"https://token.actions.githubusercontent.com",
REF,
]);
});

it("appends --insecure-ignore-tlog when ignoreTlog is set", () => {
const args = buildCosignArgs(REF, {
mode: "keyed",
publicKey: "k",
ignoreTlog: true,
});
expect(args).toContain("--insecure-ignore-tlog");
expect(args[args.length - 1]).toBe(REF);
});

it("throws when keyed mode has no public key", () => {
expect(() => buildCosignArgs(REF, { mode: "keyed" })).toThrow();
});

it("throws when keyless mode is missing identity or issuer", () => {
expect(() =>
buildCosignArgs(REF, { mode: "keyless", certificateOidcIssuer: "x" }),
).toThrow();
});

it("rejects a non-digest pinned ref (argv flag-smuggling guard)", () => {
expect(() =>
buildCosignArgs("nginx:latest", { mode: "keyed", publicKey: "k" }),
).toThrow();
});
});

describe("buildCosignDockerArgv", () => {
it("mounts the docker config read-only, passes the key via env, never uses a shell", () => {
const argv = buildCosignDockerArgv(
REF,
{ mode: "keyed", publicKey: "PEMDATA" },
{ dockerConfigDir: "/etc/dokploy/cosign-auth/x" },
);
expect(argv[0]).toBe("run");
expect(argv).toContain("--rm");
expect(argv).toContain("-v");
expect(argv).toContain("/etc/dokploy/cosign-auth/x:/root/.docker:ro");
expect(argv).toContain("-e");
expect(argv).toContain("COSIGN_KEY=PEMDATA");
expect(argv).toContain(DEFAULT_COSIGN_IMAGE);
// the cosign sub-args follow the image, ending with the ref
expect(argv[argv.length - 1]).toBe(REF);
// no shell wrapper
expect(argv).not.toContain("sh");
expect(argv).not.toContain("-c");
});

it("uses a custom cosignImage override when provided", () => {
const argv = buildCosignDockerArgv(
REF,
{
mode: "keyless",
certificateIdentityRegexp: "a",
certificateOidcIssuer: "b",
},
{
dockerConfigDir: "/d",
cosignImage: MIRROR,
},
);
expect(argv).toContain(MIRROR);
expect(argv).not.toContain("-e"); // keyless: no COSIGN_KEY env
});

it("rejects a flag-like cosignImage override", () => {
expect(() =>
buildCosignDockerArgv(
REF,
{
mode: "keyless",
certificateIdentityRegexp: "a",
certificateOidcIssuer: "b",
},
{ dockerConfigDir: "/d", cosignImage: "-v/etc/passwd:/x" },
),
).toThrow();
});

it("rejects a dockerConfigDir containing a colon", () => {
expect(() =>
buildCosignDockerArgv(
REF,
{ mode: "keyed", publicKey: "k" },
{ dockerConfigDir: "/etc/dokploy:evil" },
),
).toThrow();
});
});
82 changes: 82 additions & 0 deletions apps/dokploy/__test__/admission/image-ref.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
buildPinnedRef,
extractImageName,
extractImageTag,
isDigestRef,
normalizeRepo,
parseImageRef,
} from "@dokploy/server/utils/admission/image-ref";
import { describe, expect, it } from "vitest";

describe("image-ref parsing", () => {
it("extracts name without tag (and handles registry port)", () => {
expect(extractImageName("nginx:1.27")).toBe("nginx");
expect(extractImageName("nginx")).toBe("nginx");
expect(extractImageName("registry:5000/app")).toBe("registry:5000/app");
expect(extractImageName("registry:5000/app:v1")).toBe("registry:5000/app");
});

it("extracts name for a digest ref (splits on @, not :)", () => {
expect(extractImageName("nginx@sha256:abc")).toBe("nginx");
expect(extractImageName("registry:5000/app@sha256:abc")).toBe(
"registry:5000/app",
);
});

it("extracts tag, defaulting to latest", () => {
expect(extractImageTag("nginx")).toBe("latest");
expect(extractImageTag("nginx:1.27")).toBe("1.27");
expect(extractImageTag("myhost:5000/fedora/httpd:v1")).toBe("v1");
});

it("detects digest refs", () => {
expect(isDigestRef("nginx@sha256:abc")).toBe(true);
expect(isDigestRef("nginx:1.27")).toBe(false);
});

it("parses into name/tag/digest", () => {
expect(parseImageRef("nginx:1.27")).toEqual({
name: "nginx",
tag: "1.27",
digest: null,
});
expect(parseImageRef("nginx@sha256:abc")).toEqual({
name: "nginx",
tag: null,
digest: "sha256:abc",
});
expect(parseImageRef("r:5000/app:v1@sha256:def")).toEqual({
name: "r:5000/app",
tag: "v1",
digest: "sha256:def",
});
});

it("normalizes Docker Hub shorthand to a canonical repo", () => {
expect(normalizeRepo("alpine")).toBe("docker.io/library/alpine");
expect(normalizeRepo("library/alpine")).toBe("docker.io/library/alpine");
expect(normalizeRepo("user/repo")).toBe("docker.io/user/repo");
expect(normalizeRepo("ghcr.io/org/app")).toBe("ghcr.io/org/app");
expect(normalizeRepo("localhost:5000/app")).toBe("localhost:5000/app");
});

it("builds a pinned ref preserving the operator's registry/repo", () => {
expect(buildPinnedRef("ghcr.io/org/app:v1", "sha256:abc")).toBe(
"ghcr.io/org/app@sha256:abc",
);
expect(buildPinnedRef("alpine", "sha256:def")).toBe("alpine@sha256:def");
});

it("treats a bare numeric segment as a tag, not a port", () => {
expect(extractImageName("nginx:123")).toBe("nginx");
expect(extractImageTag("nginx:123")).toBe("123");
expect(extractImageName("ghcr.io/org/app:8080")).toBe("ghcr.io/org/app");
expect(parseImageRef("ghcr.io/org/app:8080")).toEqual({
name: "ghcr.io/org/app",
tag: "8080",
digest: null,
});
// host:port/path with no tag is still NOT split (true port case)
expect(extractImageName("registry:5000/app")).toBe("registry:5000/app");
});
});
44 changes: 44 additions & 0 deletions apps/dokploy/__test__/admission/select-repo-digest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { selectRepoDigest } from "@dokploy/server/utils/admission/resolve-digest";
import { describe, expect, it } from "vitest";

describe("selectRepoDigest", () => {
it("returns the operator's repo with the matched digest", () => {
const repoDigests = ["nginx@sha256:aaa"];
expect(selectRepoDigest(repoDigests, "nginx:1.27")).toBe(
"nginx@sha256:aaa",
);
});

it("picks the entry whose repository matches the deployed ref (not index 0)", () => {
const repoDigests = [
"alpine@sha256:hub",
"myreg.example.com/alpine@sha256:priv",
];
expect(selectRepoDigest(repoDigests, "myreg.example.com/alpine:3.19")).toBe(
"myreg.example.com/alpine@sha256:priv",
);
});

it("normalizes Docker Hub shorthand when matching", () => {
const repoDigests = ["alpine@sha256:aaa"];
expect(selectRepoDigest(repoDigests, "docker.io/library/alpine:3.19")).toBe(
"docker.io/library/alpine@sha256:aaa",
);
});

it("throws when RepoDigests is empty (unpushed/local-only)", () => {
expect(() => selectRepoDigest([], "nginx:1.27")).toThrow();
});

it("throws when no entry matches the deployed repository", () => {
expect(() =>
selectRepoDigest(["other/image@sha256:zzz"], "nginx:1.27"),
).toThrow();
});

it("pins a bare matched ref — local-only protection lives in resolveDigest's docker pull, not in this pure function", () => {
expect(selectRepoDigest(["nginx@sha256:aaa"], "nginx")).toBe(
"nginx@sha256:aaa",
);
});
});
Loading