UNPKG

@cyclonedx/cdxgen

Version:

Creates CycloneDX Software Bill of Materials (SBOM) from source or container image

1,691 lines (1,596 loc) 59.4 kB
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import process from "node:process"; import esmock from "esmock"; import { assert, beforeEach, describe, it } from "poku"; import sinon from "sinon"; import { create as createTar } from "tar"; import { addSkippedSrcFiles, exportArchive, exportImage, extractFromManifest, isWin, parseImageName, } from "./docker.js"; it("parseImageName tests", () => { if (isWin && process.env.CI === "true") { return; } assert.deepStrictEqual(parseImageName("debian"), { registry: "", repo: "debian", tag: "", digest: "", platform: "", group: "", name: "debian", }); assert.deepStrictEqual(parseImageName("debian:latest"), { registry: "", repo: "debian", tag: "latest", digest: "", platform: "", group: "", name: "debian", }); assert.deepStrictEqual(parseImageName("library/debian:latest"), { registry: "", repo: "library/debian", tag: "latest", digest: "", platform: "", group: "library", name: "debian", }); assert.deepStrictEqual(parseImageName("shiftleft/scan:v1.15.6"), { registry: "", repo: "shiftleft/scan", tag: "v1.15.6", digest: "", platform: "", group: "shiftleft", name: "scan", }); assert.deepStrictEqual( parseImageName("localhost:5000/shiftleft/scan:v1.15.6"), { registry: "localhost:5000", repo: "shiftleft/scan", tag: "v1.15.6", digest: "", platform: "", group: "shiftleft", name: "scan", }, ); assert.deepStrictEqual(parseImageName("localhost:5000/shiftleft/scan"), { registry: "localhost:5000", repo: "shiftleft/scan", tag: "", digest: "", platform: "", group: "shiftleft", name: "scan", }); assert.deepStrictEqual( parseImageName("foocorp.jfrog.io/docker/library/eclipse-temurin:latest"), { registry: "foocorp.jfrog.io", repo: "docker/library/eclipse-temurin", tag: "latest", digest: "", platform: "", group: "docker/library", name: "eclipse-temurin", }, ); assert.deepStrictEqual( parseImageName( "--platform=linux/amd64 foocorp.jfrog.io/docker/library/eclipse-temurin:latest", ), { registry: "foocorp.jfrog.io", repo: "docker/library/eclipse-temurin", tag: "latest", digest: "", platform: "linux/amd64", group: "docker/library", name: "eclipse-temurin", }, ); assert.deepStrictEqual( parseImageName( "quay.io/shiftleft/scan-java@sha256:5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1", ), { registry: "quay.io", repo: "shiftleft/scan-java", tag: "", digest: "5d008306a7c5d09ba0161a3408fa3839dc2c9dd991ffb68adecc1040399fe9e1", platform: "", group: "shiftleft", name: "scan-java", }, ); }); async function loadDockerModule({ clientResponse, fsOverrides, streamOverrides, tarOverrides, utilsOverrides, } = {}) { const dockerClient = sinon.stub().resolves( clientResponse || { Id: "sha256:hello-world", RepoTags: ["hello-world:latest"], }, ); dockerClient.stream = sinon.stub(); const fsStub = { createReadStream: sinon.stub(), lstatSync: sinon.stub(), readdirSync: sinon.stub().returns([]), readFileSync: sinon.stub(), ...fsOverrides, }; const gotStub = { extend: sinon.stub().returns(dockerClient), get: sinon.stub().resolves({ body: "OK" }), }; const utilsStub = { DEBUG_MODE: false, createDryRunError: sinon.stub(), extractPathEnv: sinon.stub().returns([]), getAllFiles: sinon.stub().returns([]), getTmpDir: sinon.stub().returns("/tmp"), isDryRun: false, readEnvironmentVariable: sinon .stub() .callsFake((varName) => process.env[varName]), recordActivity: sinon.stub(), recordDecisionActivity: sinon.stub(), recordSensitiveFileRead: sinon.stub(), safeExtractArchive: sinon.stub().resolves(true), safeExistsSync: sinon.stub().returns(false), safeMkdirSync: sinon.stub(), safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"), safeRmSync: sinon.stub(), safeSpawnSync: sinon.stub().returns({ status: 1, stdout: "", stderr: "" }), safeWriteSync: sinon.stub(), ...utilsOverrides, }; const dockerModule = await esmock("./docker.js", { "node:fs": fsStub, "node:stream/promises": { pipeline: sinon.stub().resolves(), ...streamOverrides, }, got: { default: gotStub }, tar: { x: sinon.stub().returns("extractor"), ...tarOverrides, }, "../helpers/utils.js": utilsStub, }); return { dockerClient, dockerModule, fsStub, gotStub, utilsStub }; } const decodeRegistryAuthHeader = (header) => JSON.parse(Buffer.from(header, "base64url").toString("utf-8")); const dockerConfigExistsStub = () => sinon.stub().callsFake((filePath) => filePath.endsWith("config.json")); const encodedAuth = Buffer.from("trusted-user:trusted-pass").toString("base64"); const authConfigData = (configuredRegistry) => JSON.stringify({ auths: { [configuredRegistry]: { auth: encodedAuth, }, }, }); const credHelperConfigData = (configuredRegistry) => JSON.stringify({ credHelpers: { [configuredRegistry]: "osxkeychain", }, }); const credHelperExe = (helperSuffix) => isWin ? `docker-credential-${helperSuffix}.exe` : `docker-credential-${helperSuffix}`; async function loadDockerModuleWithAuths(configuredRegistry) { return await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(authConfigData(configuredRegistry)), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), }, }); } async function loadDockerModuleWithCredHelpers( configuredRegistry, safeSpawnSync, ) { return await loadDockerModule({ fsOverrides: { readFileSync: sinon .stub() .returns(credHelperConfigData(configuredRegistry)), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), safeSpawnSync, }, }); } const withDockerConfig = async (callback) => { const originalDockerConfig = process.env.DOCKER_CONFIG; process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config"; try { await callback(); } finally { if (originalDockerConfig === undefined) { delete process.env.DOCKER_CONFIG; } else { process.env.DOCKER_CONFIG = originalDockerConfig; } } }; const withEnv = async (updates, callback) => { const originalEnv = {}; for (const envKey of Object.keys(updates)) { originalEnv[envKey] = process.env[envKey]; if (updates[envKey] === undefined) { delete process.env[envKey]; } else { process.env[envKey] = updates[envKey]; } } try { await callback(); } finally { for (const envKey of Object.keys(updates)) { if (originalEnv[envKey] === undefined) { delete process.env[envKey]; } else { process.env[envKey] = originalEnv[envKey]; } } } }; await it("docker connection uses the detected daemon client", async () => { const { dockerModule, gotStub, dockerClient } = await loadDockerModule(); const dockerConn = await dockerModule.getConnection(); assert.strictEqual(dockerConn, dockerClient); sinon.assert.calledOnce(gotStub.get); sinon.assert.calledOnce(gotStub.extend); }); await it("docker getImage returns inspect data from the daemon client", async () => { const inspectData = { Id: "sha256:hello-world", RepoTags: ["hello-world:latest"], }; const { dockerModule, dockerClient } = await loadDockerModule({ clientResponse: inspectData, }); const imageData = await dockerModule.getImage("hello-world:latest"); assert.deepStrictEqual(imageData, inspectData); sinon.assert.calledWith( dockerClient, "images/hello-world:latest/json", sinon.match.has("method", "GET"), ); }); await it("docker getImage falls back to the daemon client when cli inspect fails", async () => { const originalDockerUseCli = process.env.DOCKER_USE_CLI; process.env.DOCKER_USE_CLI = "1"; try { const inspectData = { Id: "sha256:hello-world", RepoTags: ["hello-world:latest"], }; const { dockerModule, dockerClient } = await loadDockerModule({ clientResponse: inspectData, }); const imageData = await dockerModule.getImage("hello-world:latest"); assert.deepStrictEqual(imageData, inspectData); sinon.assert.calledWith( dockerClient, "images/hello-world:latest/json", sinon.match.has("method", "GET"), ); } finally { if (originalDockerUseCli === undefined) { delete process.env.DOCKER_USE_CLI; } else { process.env.DOCKER_USE_CLI = originalDockerUseCli; } } }); await it("docker getImage uses nerdctl when DOCKER_CMD is configured", async () => { const originalDockerCmd = process.env.DOCKER_CMD; const originalDockerUseCli = process.env.DOCKER_USE_CLI; process.env.DOCKER_CMD = "nerdctl"; delete process.env.DOCKER_USE_CLI; try { const inspectData = { Id: "sha256:hello-world", RepoTags: ["hello-world:latest"], }; const safeSpawnSync = sinon.stub(); safeSpawnSync .onCall(0) .returns({ status: 0, stdout: '{"Repository":"hello-world","Tag":"latest"}\n', stderr: "", }) .onCall(1) .returns({ status: 0, stdout: JSON.stringify([inspectData]), stderr: "", }); const { dockerModule, utilsStub } = await loadDockerModule({ clientResponse: inspectData, utilsOverrides: { safeSpawnSync, }, }); const imageData = await dockerModule.getImage("hello-world:latest"); assert.deepStrictEqual(imageData, inspectData); sinon.assert.calledWithExactly(safeSpawnSync, "nerdctl", [ "images", "--format=json", ]); sinon.assert.calledWithExactly(safeSpawnSync, "nerdctl", [ "inspect", "hello-world:latest", ]); sinon.assert.notCalled(utilsStub.safeMkdirSync); } finally { if (originalDockerCmd === undefined) { delete process.env.DOCKER_CMD; } else { process.env.DOCKER_CMD = originalDockerCmd; } if (originalDockerUseCli === undefined) { delete process.env.DOCKER_USE_CLI; } else { process.env.DOCKER_USE_CLI = originalDockerUseCli; } } }); await it("docker getConnection reports blocked network activity in dry-run mode", async () => { const recordActivity = sinon.stub(); const { dockerModule } = await loadDockerModule({ utilsOverrides: { isDryRun: true, recordActivity, }, }); const conn = await dockerModule.getConnection({}, "docker.io"); assert.strictEqual(conn, undefined); sinon.assert.calledWithMatch(recordActivity, { kind: "network", status: "blocked", target: "docker.io", }); }); await it("docker getConnection skips dry-run tracing on containerd runtimes", async () => { const recordActivity = sinon.stub(); const recordSensitiveFileRead = sinon.stub(); await withEnv( { CONTAINERD_ADDRESS: "/run/containerd/containerd.sock", }, async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(authConfigData("docker.io")), }, utilsOverrides: { isDryRun: true, recordActivity, recordSensitiveFileRead, safeExistsSync: dockerConfigExistsStub(), }, }); const conn = await dockerModule.getConnection({}, "docker.io"); assert.strictEqual(conn, undefined); }, ); sinon.assert.notCalled(recordActivity); sinon.assert.notCalled(recordSensitiveFileRead); }); await it("docker getConnection traces docker credential file reads in dry-run mode", async () => { const recordActivity = sinon.stub(); const recordSensitiveFileRead = sinon.stub(); await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(authConfigData("docker.io")), }, utilsOverrides: { isDryRun: true, recordActivity, recordSensitiveFileRead, safeExistsSync: dockerConfigExistsStub(), }, }); await dockerModule.getConnection({}, "docker.io"); }); sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, { label: "Docker credential file", }); sinon.assert.calledWithMatch(recordActivity, { kind: "network", status: "blocked", target: "docker.io", }); }); await it("docker makeRequest does not trace docker credential file reads when the read fails", async () => { const recordSensitiveFileRead = sinon.stub(); await withEnv( { DOCKER_AUTH_CONFIG: undefined, DOCKER_EMAIL: undefined, DOCKER_PASSWORD: undefined, DOCKER_USER: undefined, }, async () => { await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().throws(new Error("read failed")), }, utilsOverrides: { recordSensitiveFileRead, safeExistsSync: dockerConfigExistsStub(), }, }); await assert.rejects(() => dockerModule.makeRequest( "images/create?fromImage=docker.io/library/alpine:latest", "POST", "docker.io/library/alpine:latest", ), ); }); }, ); sinon.assert.notCalled(recordSensitiveFileRead); }); await it("docker getConnection does not trace TLS client files when reading them fails", async () => { const recordSensitiveFileRead = sinon.stub(); await withEnv( { DOCKER_AUTH_CONFIG: undefined, DOCKER_CERT_PATH: "/tmp/docker-certs", DOCKER_EMAIL: undefined, DOCKER_HOST: "tcp://docker.example.test:2376", DOCKER_PASSWORD: undefined, DOCKER_USER: undefined, }, async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon .stub() .onFirstCall() .throws(new Error("cert read failed")), }, utilsOverrides: { recordSensitiveFileRead, }, }); await assert.rejects(() => dockerModule.getConnection({}, "docker.io")); }, ); sinon.assert.notCalled(recordSensitiveFileRead); }); await it("docker makeRequest does not trace TLS client files when reading them fails", async () => { const recordSensitiveFileRead = sinon.stub(); await withEnv( { DOCKER_AUTH_CONFIG: undefined, DOCKER_CERT_PATH: "/tmp/docker-certs", DOCKER_EMAIL: undefined, DOCKER_HOST: "tcp://docker.example.test:2376", DOCKER_PASSWORD: undefined, DOCKER_USER: undefined, }, async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon .stub() .onFirstCall() .throws(new Error("cert read failed")), }, utilsOverrides: { recordSensitiveFileRead, }, }); await assert.rejects(() => dockerModule.makeRequest( "images/create?fromImage=docker.io/library/alpine:latest", "POST", "docker.io/library/alpine:latest", ), ); }, ); sinon.assert.notCalled(recordSensitiveFileRead); }); await it("docker getConnection records which credential source was selected", async () => { const recordDecisionActivity = sinon.stub(); await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(authConfigData("docker.io")), }, utilsOverrides: { isDryRun: true, recordDecisionActivity, safeExistsSync: dockerConfigExistsStub(), }, }); await dockerModule.getConnection({}, "docker.io"); }); sinon.assert.calledWithMatch( recordDecisionActivity, "docker-auth:docker.io", { metadata: sinon.match({ decisionType: "credential-source-selection", selectedSource: "docker-config-auth", }), }, ); }); await it("docker getConnection traces credential helper resolution in dry-run mode", async () => { const safeSpawnSync = sinon.stub().returns({ status: 1, stdout: "", stderr: "", }); await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(credHelperConfigData("docker.io")), }, utilsOverrides: { isDryRun: true, safeExistsSync: dockerConfigExistsStub(), safeSpawnSync, }, }); await dockerModule.getConnection({}, "docker.io"); }); sinon.assert.calledWithExactly( safeSpawnSync, credHelperExe("osxkeychain"), ["get"], { input: "docker.io", }, ); }); await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => { const safeExtractArchive = sinon.stub().resolves(false); const { dockerModule } = await loadDockerModule({ utilsOverrides: { safeExtractArchive, }, }); const result = await dockerModule.extractTar( "/tmp/image.tar", "/tmp/out", {}, ); assert.strictEqual(result, false); sinon.assert.calledWithMatch( safeExtractArchive, "/tmp/image.tar", "/tmp/out", sinon.match.func, "untar", { blockedReason: "Dry run mode blocks untar and layer extraction operations because they create files on disk.", metadata: { archiveFormat: "tar", }, }, ); }); await it("docker extractTar delegates successful untar tracing to safeExtractArchive", async () => { const safeExtractArchive = sinon.stub().resolves(true); const { dockerModule } = await loadDockerModule({ utilsOverrides: { safeExtractArchive, }, }); const result = await dockerModule.extractTar( "/tmp/image.tar", "/tmp/out", {}, ); assert.strictEqual(result, true); sinon.assert.calledOnce(safeExtractArchive); }); await it("docker extractTar preserves failure handling after safeExtractArchive rejects", async () => { const extractionError = new Error("permission denied"); extractionError.code = "EACCES"; const safeExtractArchive = sinon.stub().rejects(extractionError); const { dockerModule } = await loadDockerModule({ utilsOverrides: { safeExtractArchive, }, }); const result = await dockerModule.extractTar( "/tmp/image.tar", "/tmp/out", {}, ); assert.strictEqual(result, false); sinon.assert.calledOnce(safeExtractArchive); }); await it("docker exportImage reports a blocked container activity in dry-run mode", async () => { const recordActivity = sinon.stub(); const recordSensitiveFileRead = sinon.stub(); await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns(authConfigData("docker.io")), }, utilsOverrides: { isDryRun: true, recordActivity, recordSensitiveFileRead, safeExistsSync: dockerConfigExistsStub(), }, }); const result = await dockerModule.exportImage("alpine:3.20", {}); assert.strictEqual(result, undefined); }); sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, { label: "Docker credential file", }); sinon.assert.calledWithMatch(recordActivity, { kind: "container", status: "blocked", target: "alpine:3.20", }); }); await it("docker exportImage preserves scoped registry refs for dry-run auth tracing", async () => { const recordDecisionActivity = sinon.stub(); await withDockerConfig(async () => { const { dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon .stub() .returns(authConfigData("registry.example.com/team")), }, utilsOverrides: { isDryRun: true, recordDecisionActivity, safeExistsSync: dockerConfigExistsStub(), }, }); const result = await dockerModule.exportImage( "registry.example.com/team/app:latest", {}, ); assert.strictEqual(result, undefined); }); sinon.assert.calledWithMatch( recordDecisionActivity, "docker-auth:registry.example.com/team/app", { metadata: sinon.match({ selectedSource: "docker-config-auth", }), }, ); }); await it("docker exportImage skips dry-run tracing for local paths", async () => { const recordActivity = sinon.stub(); const recordSensitiveFileRead = sinon.stub(); const { dockerModule } = await loadDockerModule({ utilsOverrides: { isDryRun: true, recordActivity, recordSensitiveFileRead, safeExistsSync: sinon.stub().returns(true), }, }); const result = await dockerModule.exportImage("/tmp/image.tar", {}); assert.strictEqual(result, undefined); sinon.assert.notCalled(recordActivity); sinon.assert.notCalled(recordSensitiveFileRead); }); await it("docker exportImage ignores local directories", async () => { const imageData = await exportImage("."); assert.strictEqual(imageData, undefined); }); await it("docker makeRequest prefers DOCKER_AUTH_CONFIG over config.json entries for all registries", async () => { await withDockerConfig(async () => { await withEnv( { DOCKER_AUTH_CONFIG: "opaque-global-auth-token", }, async () => { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "helper-user", Secret: "helper-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "registry.example.com": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, credHelpers: { "registry.example.com": "osxkeychain", }, }), ), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), safeSpawnSync, }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com/team/app", ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual( requestOptions.headers["X-Registry-Auth"], "opaque-global-auth-token", ); sinon.assert.notCalled(safeSpawnSync); }, ); }); }); await it("docker makeRequest prefers DOCKER_USER credentials over matching config.json entries", async () => { await withDockerConfig(async () => { await withEnv( { DOCKER_USER: "env-user", DOCKER_PASSWORD: "env-pass", DOCKER_EMAIL: "env@example.com", }, async () => { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "registry.example.com": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, }), ), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com/team/app", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "env-user", password: "env-pass", email: "env@example.com", serveraddress: "registry.example.com", }); }, ); }); }); await it("docker makeRequest applies DOCKER_USER credentials regardless of configured registry entries", async () => { await withDockerConfig(async () => { await withEnv( { DOCKER_USER: "env-user", DOCKER_PASSWORD: "env-pass", DOCKER_EMAIL: "env@example.com", }, async () => { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "helper-user", Secret: "helper-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "other-registry.example.com": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, credHelpers: { "other-registry.example.com": "osxkeychain", }, }), ), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), safeSpawnSync, }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com/team/app", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "env-user", password: "env-pass", email: "env@example.com", serveraddress: "registry.example.com", }); sinon.assert.notCalled(safeSpawnSync); }, ); }); }); await it("docker makeRequest does not forward auth for substring-matched registries", async () => { const originalDockerConfig = process.env.DOCKER_CONFIG; process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config"; try { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "private-registry.example.com": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com", ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual(requestOptions.headers, undefined); } finally { if (originalDockerConfig === undefined) { delete process.env.DOCKER_CONFIG; } else { process.env.DOCKER_CONFIG = originalDockerConfig; } } }); await it("docker makeRequest accepts exact normalized registry matches from config auths", async () => { const originalDockerConfig = process.env.DOCKER_CONFIG; process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config"; try { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "https://registry.example.com/v2/": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com/team/app", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "trusted-user", password: "trusted-pass", serveraddress: "https://registry.example.com/v2/", }); } finally { if (originalDockerConfig === undefined) { delete process.env.DOCKER_CONFIG; } else { process.env.DOCKER_CONFIG = originalDockerConfig; } } }); await it("docker makeRequest accepts normalized exact matches across ipv4 ipv6 explicit ports and scoped subpaths from config auths", async () => { const cases = [ { configuredRegistry: "127.0.0.1:5000", requestedRegistry: "127.0.0.1:5000/team/app", expectedServerAddress: "127.0.0.1:5000", }, { configuredRegistry: "[::1]:5000", requestedRegistry: "[::1]:5000/team/app", expectedServerAddress: "[::1]:5000", }, { configuredRegistry: "https://[2001:db8::1]:5000/v2/", requestedRegistry: "[2001:db8::1]:5000/team/app", expectedServerAddress: "https://[2001:db8::1]:5000/v2/", }, { configuredRegistry: "HTTPS://REGISTRY.EXAMPLE.COM/V2/", requestedRegistry: "registry.example.com/team/app", expectedServerAddress: "HTTPS://REGISTRY.EXAMPLE.COM/V2/", }, { configuredRegistry: "https://registry.example.com:443/v2/", requestedRegistry: "registry.example.com:443/team/app", expectedServerAddress: "https://registry.example.com:443/v2/", }, { configuredRegistry: "http://registry.example.com:80/v2/", requestedRegistry: "registry.example.com:80/team/app", expectedServerAddress: "http://registry.example.com:80/v2/", }, { configuredRegistry: "https://registry.example.com/custom/subpath", requestedRegistry: "registry.example.com/custom/subpath/team/app", expectedServerAddress: "https://registry.example.com/custom/subpath", }, { configuredRegistry: "https://registry.example.com/custom/subpath/v2/", requestedRegistry: "registry.example.com/custom/subpath/team/app", expectedServerAddress: "https://registry.example.com/custom/subpath/v2/", }, ]; await withDockerConfig(async () => { for (const testCase of cases) { const { dockerClient, dockerModule } = await loadDockerModuleWithAuths( testCase.configuredRegistry, ); await dockerModule.makeRequest( `images/create?fromImage=${testCase.requestedRegistry}:latest`, "POST", testCase.requestedRegistry, ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "trusted-user", password: "trusted-pass", serveraddress: testCase.expectedServerAddress, }); } }); }); await it("docker makeRequest rejects wildcard unicode bidi explicit-default-port port-boundary and unrelated scoped-path mismatches from config auths", async () => { const bidiRegistry = "reg\u202eistry.example.com"; const unicodeConfusableRegistry = "reg\u0456stry.example.com"; const cases = [ { configuredRegistry: "*.example.com", requestedRegistry: "team.example.com/app", }, { configuredRegistry: "registry.example.com", requestedRegistry: "registry.example.com:80/team/app", }, { configuredRegistry: "registry.example.com:443", requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: "127.0.0.1:5001", requestedRegistry: "127.0.0.1:5000/team/app", }, { configuredRegistry: "[::1]:5001", requestedRegistry: "[::1]:5000/team/app", }, { configuredRegistry: "https://registry.example.com.evil.invalid/v2/", requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: "https://registry.example.com/custom/subpath", requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: "https://registry.example.com/custom/subpath", requestedRegistry: "registry.example.com/custom/subpathology/team/app", }, { configuredRegistry: "https://registry.example.com:443/v2/", requestedRegistry: "registry.example.com:444/team/app", }, { configuredRegistry: unicodeConfusableRegistry, requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: bidiRegistry, requestedRegistry: "registry.example.com/team/app", }, ]; await withDockerConfig(async () => { for (const testCase of cases) { const { dockerClient, dockerModule } = await loadDockerModuleWithAuths( testCase.configuredRegistry, ); await dockerModule.makeRequest( `images/create?fromImage=${testCase.requestedRegistry}:latest`, "POST", testCase.requestedRegistry, ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual(requestOptions.headers, undefined); } }); }); await it("docker makeRequest accepts raw host:port registry matches from config auths", async () => { await withDockerConfig(async () => { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "localhost:5000": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), }, }); await dockerModule.makeRequest( "images/create?fromImage=localhost:5000/team/app:latest", "POST", "localhost:5000/team/app", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "trusted-user", password: "trusted-pass", serveraddress: "localhost:5000", }); }); }); await it("docker makeRequest keeps raw host:port registries separated by port", async () => { await withDockerConfig(async () => { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "localhost:5001": { auth: Buffer.from("trusted-user:trusted-pass").toString( "base64", ), }, }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), }, }); await dockerModule.makeRequest( "images/create?fromImage=localhost:5000/team/app:latest", "POST", "localhost:5000/team/app", ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual(requestOptions.headers, undefined); }); }); await it("docker makeRequest preserves Docker Hub auth aliases without substring matching", async () => { const originalDockerConfig = process.env.DOCKER_CONFIG; process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config"; try { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "https://index.docker.io/v1/": { auth: Buffer.from("hub-user:hub-pass").toString("base64"), }, }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), }, }); await dockerModule.makeRequest( "images/create?fromImage=docker.io/library/alpine:latest", "POST", "docker.io", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "hub-user", password: "hub-pass", serveraddress: "https://index.docker.io/v1/", }); } finally { if (originalDockerConfig === undefined) { delete process.env.DOCKER_CONFIG; } else { process.env.DOCKER_CONFIG = originalDockerConfig; } } }); await it("docker makeRequest resolves unqualified image pulls to Docker Hub auth entries", async () => { const requestedImages = ["myorg/app:latest", "alpine:latest"]; await withDockerConfig(async () => { for (const requestedImage of requestedImages) { const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ auths: { "https://index.docker.io/v1/": { auth: Buffer.from("hub-user:hub-pass").toString("base64"), }, }, }), ), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), }, }); await dockerModule.makeRequest( `images/create?fromImage=${requestedImage}`, "POST", "", ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "hub-user", password: "hub-pass", serveraddress: "https://index.docker.io/v1/", }); } }); }); await it("docker makeRequest skips credHelpers for substring-matched registries", async () => { const originalDockerConfig = process.env.DOCKER_CONFIG; process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config"; try { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ Username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ credHelpers: { "private-registry.example.com": "osxkeychain", }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), safeSpawnSync, }, }); await dockerModule.makeRequest( "images/create?fromImage=registry.example.com/team/app:latest", "POST", "registry.example.com", ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual(requestOptions.headers, undefined); sinon.assert.notCalled(safeSpawnSync); } finally { if (originalDockerConfig === undefined) { delete process.env.DOCKER_CONFIG; } else { process.env.DOCKER_CONFIG = originalDockerConfig; } } }); await it("docker makeRequest accepts raw host:port registry matches from credHelpers", async () => { await withDockerConfig(async () => { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ credHelpers: { "localhost:5000": "osxkeychain", }, }), ), }, utilsOverrides: { safeExistsSync: sinon .stub() .callsFake((filePath) => filePath.endsWith("config.json")), safeSpawnSync, }, }); await dockerModule.makeRequest( "images/create?fromImage=localhost:5000/team/app:latest", "POST", "localhost:5000/team/app", ); sinon.assert.calledOnceWithExactly( safeSpawnSync, credHelperExe("osxkeychain"), ["get"], { input: "localhost:5000", }, ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "trusted-user", password: "trusted-pass", email: "trusted-user", serveraddress: "localhost:5000", }); }); }); await it("docker getCredsFromHelper normalizes cache keys for equivalent registry hosts", async () => { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerModule } = await loadDockerModule({ utilsOverrides: { safeSpawnSync, }, }); const firstToken = dockerModule.getCredsFromHelper( "osxkeychain", "registry.example.com", ); const secondToken = dockerModule.getCredsFromHelper( "osxkeychain", "https://registry.example.com/v2/", ); assert.strictEqual(firstToken, secondToken); sinon.assert.calledOnceWithExactly( safeSpawnSync, credHelperExe("osxkeychain"), ["get"], { input: "registry.example.com", }, ); }); await it("docker getCredsFromHelper keeps scoped path cache keys isolated", async () => { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerModule } = await loadDockerModule({ utilsOverrides: { safeSpawnSync, }, }); const firstToken = dockerModule.getCredsFromHelper( "osxkeychain", "https://registry.example.com/custom/subpath/v2/", ); const secondToken = dockerModule.getCredsFromHelper( "osxkeychain", "https://registry.example.com/custom/subpath/v2/", ); const thirdToken = dockerModule.getCredsFromHelper( "osxkeychain", "https://registry.example.com/other/subpath/v2/", ); assert.strictEqual(firstToken, secondToken); assert.notStrictEqual(firstToken, thirdToken); assert.deepStrictEqual(decodeRegistryAuthHeader(firstToken), { username: "trusted-user", password: "trusted-pass", email: "trusted-user", serveraddress: "https://registry.example.com/custom/subpath/v2/", }); sinon.assert.calledTwice(safeSpawnSync); }); await it("docker makeRequest accepts ipv4 ipv6 explicit-port and scoped-subpath registry matches from credHelpers", async () => { const cases = [ { configuredRegistry: "127.0.0.1:5000", requestedRegistry: "127.0.0.1:5000/team/app", }, { configuredRegistry: "[::1]:5000", requestedRegistry: "[::1]:5000/team/app", }, { configuredRegistry: "https://registry.example.com:443/v2/", requestedRegistry: "registry.example.com:443/team/app", }, { configuredRegistry: "http://registry.example.com:80/v2/", requestedRegistry: "registry.example.com:80/team/app", }, { configuredRegistry: "https://registry.example.com/custom/subpath/v2/", requestedRegistry: "registry.example.com/custom/subpath/team/app", }, ]; await withDockerConfig(async () => { for (const testCase of cases) { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModuleWithCredHelpers( testCase.configuredRegistry, safeSpawnSync, ); await dockerModule.makeRequest( `images/create?fromImage=${testCase.requestedRegistry}:latest`, "POST", testCase.requestedRegistry, ); sinon.assert.calledOnceWithExactly( safeSpawnSync, credHelperExe("osxkeychain"), ["get"], { input: testCase.configuredRegistry, }, ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "trusted-user", password: "trusted-pass", email: "trusted-user", serveraddress: testCase.configuredRegistry, }); } }); }); await it("docker makeRequest does not invoke credHelpers for wildcard unicode bidi explicit-default-port or port-boundary mismatches", async () => { const bidiRegistry = "reg\u202eistry.example.com"; const unicodeConfusableRegistry = "reg\u0456stry.example.com"; const cases = [ { configuredRegistry: "*.example.com", requestedRegistry: "team.example.com/app", }, { configuredRegistry: "registry.example.com", requestedRegistry: "registry.example.com:80/team/app", }, { configuredRegistry: "registry.example.com:443", requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: "127.0.0.1:5001", requestedRegistry: "127.0.0.1:5000/team/app", }, { configuredRegistry: "[::1]:5001", requestedRegistry: "[::1]:5000/team/app", }, { configuredRegistry: "https://registry.example.com/custom/subpath/v2/", requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: "https://registry.example.com/custom/subpath/v2/", requestedRegistry: "registry.example.com/custom/subpathology/team/app", }, { configuredRegistry: "https://registry.example.com:443/v2/", requestedRegistry: "registry.example.com:444/team/app", }, { configuredRegistry: unicodeConfusableRegistry, requestedRegistry: "registry.example.com/team/app", }, { configuredRegistry: bidiRegistry, requestedRegistry: "registry.example.com/team/app", }, ]; await withDockerConfig(async () => { for (const testCase of cases) { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "trusted-user", Secret: "trusted-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModuleWithCredHelpers( testCase.configuredRegistry, safeSpawnSync, ); await dockerModule.makeRequest( `images/create?fromImage=${testCase.requestedRegistry}:latest`, "POST", testCase.requestedRegistry, ); const requestOptions = dockerClient.firstCall.args[1]; assert.strictEqual(requestOptions.headers, undefined); sinon.assert.notCalled(safeSpawnSync); } }); }); await it("docker makeRequest resolves unqualified image pulls to Docker Hub credHelpers", async () => { const requestedImages = ["myorg/app:latest", "alpine:latest"]; await withDockerConfig(async () => { for (const requestedImage of requestedImages) { const safeSpawnSync = sinon.stub().returns({ status: 0, stdout: JSON.stringify({ username: "hub-user", Secret: "hub-pass", }), stderr: "", }); const { dockerClient, dockerModule } = await loadDockerModule({ fsOverrides: { readFileSync: sinon.stub().returns( JSON.stringify({ credHelpers: { "docker.io": "osxkeychain", }, }), ), }, utilsOverrides: { safeExistsSync: dockerConfigExistsStub(), safeSpawnSync, }, }); await dockerModule.makeRequest( `images/create?fromImage=${requestedImage}`, "POST", "", ); sinon.assert.calledOnceWithExactly( safeSpawnSync, credHelperExe("osxkeychain"), ["get"], { input: "docker.io", }, ); const registryAuthHeader = dockerClient.firstCall.args[1].headers["X-Registry-Auth"]; assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), { username: "hub-user", password: "hub-pass", email: "hub-user", serveraddress: "docker.io", }); } }); }); await it("docker makeRequest accepts normalized exact matches for common public registries without aliasing hosts", async () => { const cases = [ { configuredRegistry: "https://ghcr.io/v2/", requestedRegistry: "ghcr.io/org/image", }, { configuredRegistry: "https://quay.io/v2/", requestedRegistry: "quay.io/org/image", }, { configuredRegistry: "https://public.ecr.aws/v2/", requestedRegistry: "public.ecr.aws/alias/image", }, { configuredRegistry: "https://gcr.io/v2/", requestedRegistry: "gcr.io/project/image", }, ]; await