snyk-docker-plugin
Version:
Snyk CLI docker plugin
186 lines (163 loc) • 4.7 kB
text/typescript
import { contentTypes } from "@snyk/docker-registry-v2-client";
import {
DockerPull,
DockerPullOptions,
DockerPullResult,
} from "@snyk/snyk-docker-pull";
import * as Debug from "debug";
import * as Modem from "docker-modem";
import { createWriteStream } from "fs";
import { Stream } from "stream";
import { DockerInspectOutput } from "./analyzer/types";
import * as subProcess from "./sub-process";
export { Docker, DockerOptions };
interface DockerOptions {
host?: string;
tlsVerify?: string;
tlsCert?: string;
tlsCaCert?: string;
tlsKey?: string;
socketPath?: string;
platform?: string;
}
const debug = Debug("snyk");
/**
* Type guard to validate that an object conforms to the DockerInspectOutput interface
*/
function isValidDockerInspectOutput(data: any): data is DockerInspectOutput {
return (
data &&
typeof data === "object" &&
!Array.isArray(data) &&
typeof data.Architecture === "string"
);
}
class Docker {
public static async binaryExists(): Promise<boolean> {
try {
await subProcess.execute("docker", ["version"]);
return true;
} catch (e) {
return false;
}
}
public async pull(
registry: string,
repo: string,
tag: string,
imageSavePath: string,
username?: string,
password?: string,
platform?: string,
): Promise<DockerPullResult> {
const dockerPull = new DockerPull();
const platformTokens = platform ? platform.split("/") : undefined;
let os: string = "linux";
let architecture: string = "amd64";
let variant: string | undefined;
if (platformTokens) {
const platformTokensLen = platformTokens.length;
if (platformTokensLen <= 1) {
throw Error(
"Invalid platform string. Please provide a platform name that follows os/arch[/variant] format.",
);
}
os = platformTokens[0];
architecture = platformTokens[1];
variant = platformTokens[2];
}
const opt: DockerPullOptions = {
username,
password,
loadImage: false,
platform: platformTokens ? { os, architecture, variant } : undefined,
imageSavePath,
reqOptions: {
acceptManifest: [
contentTypes.OCI_MANIFEST_V1,
contentTypes.OCI_INDEX_V1,
contentTypes.MANIFEST_V2,
contentTypes.MANIFEST_LIST_V2,
].join(","),
},
};
return await dockerPull.pull(registry, repo, tag, opt);
}
public async pullCli(
targetImage: string,
options?: DockerOptions,
): Promise<subProcess.CmdOutput> {
const args: string[] = ["pull", targetImage];
if (options?.platform) {
args.push(`--platform=${options.platform}`);
}
return subProcess.execute("docker", args);
}
public async save(targetImage: string, destination: string) {
const request = {
path: `/images/${targetImage}/get?`,
method: "GET",
isStream: true,
statusCodes: {
200: true,
400: "bad request",
404: "not found",
500: "server error",
},
};
debug(
`Docker.save: targetImage: ${targetImage}, destination: ${destination}`,
);
const modem = new Modem();
return new Promise<void>((resolve, reject) => {
modem.dial(request, (err, stream: Stream) => {
if (err) {
return reject(err);
}
const writeStream = createWriteStream(destination);
writeStream.on("error", (err) => {
reject(err);
});
writeStream.on("finish", () => {
resolve();
});
stream.on("error", (err) => {
reject(err);
});
stream.on("end", () => {
writeStream.end();
});
stream.pipe(writeStream);
});
});
}
public async inspectImage(targetImage: string): Promise<DockerInspectOutput> {
const request = {
path: `/images/${targetImage}/json`,
method: "GET",
statusCodes: {
200: true,
404: "not found",
500: "server error",
},
};
debug(`Docker.inspectImage: targetImage: ${targetImage}`);
const modem = new Modem();
return new Promise<DockerInspectOutput>((resolve, reject) => {
modem.dial(request, (err, data) => {
if (err) {
return reject(err);
}
// Validate that the response conforms to DockerInspectOutput interface
if (!isValidDockerInspectOutput(data)) {
return reject(
new Error(
`Invalid Docker inspect response for image ${targetImage}: expected DockerInspectOutput format`,
),
);
}
resolve(data);
});
});
}
}