snyk-docker-plugin
Version:
Snyk CLI docker plugin
252 lines • 10 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.pullIfNotLocal = exports.extractImageDetails = exports.getImageArchive = void 0;
const Debug = require("debug");
const fs = require("fs");
const mkdirp = require("mkdirp");
const path = require("path");
const docker_1 = require("../docker");
const image_1 = require("../extractor/image");
const debug = Debug("snyk");
async function getInspectResult(docker, targetImage) {
const info = await docker.inspectImage(targetImage);
return JSON.parse(info.stdout)[0];
}
function cleanupCallback(imageFolderPath, imageName) {
return () => {
const fullImagePath = path.join(imageFolderPath, imageName);
if (fs.existsSync(fullImagePath)) {
fs.unlinkSync(fullImagePath);
}
try {
fs.rmdirSync(imageFolderPath);
}
catch (err) {
debug(`Can't remove folder ${imageFolderPath}, got error ${err.message}`);
}
};
}
async function pullWithDockerBinary(docker, targetImage, saveLocation, username, password, platform) {
let pullAndSaveSuccessful = false;
try {
if (username || password) {
debug("using local docker binary credentials. the credentials you provided will be ignored");
}
await docker.pullCli(targetImage, { platform });
await docker.save(targetImage, saveLocation);
return (pullAndSaveSuccessful = true);
}
catch (err) {
debug(`couldn't pull ${targetImage} using docker binary: ${err.message}`);
handleDockerPullError(err.stderr, platform);
return pullAndSaveSuccessful;
}
}
function handleDockerPullError(err, platform) {
if (err && err.includes("unknown operating system or architecture")) {
throw new Error("Unknown operating system or architecture");
}
if (err.includes("operating system is not supported")) {
throw new Error(`Operating system is not supported`);
}
const unknownManifestConditions = [
"no matching manifest for",
"manifest unknown",
];
if (unknownManifestConditions.some((value) => err.includes(value))) {
if (platform) {
throw new Error(`The image does not exist for ${platform}`);
}
throw new Error(`The image does not exist for the current platform`);
}
if (err.includes("invalid reference format")) {
throw new Error(`invalid image format`);
}
if (err.includes("unknown flag: --platform")) {
throw new Error('"--platform" is only supported on a Docker daemon with version later than 17.09');
}
if (err ===
'"--platform" is only supported on a Docker daemon with experimental features enabled') {
throw new Error(err);
}
}
async function pullFromContainerRegistry(docker, targetImage, imageSavePath, username, password, platform) {
const { hostname, imageName, tag } = extractImageDetails(targetImage);
debug(`Attempting to pull: registry: ${hostname}, image: ${imageName}, tag: ${tag}`);
try {
return await docker.pull(hostname, imageName, tag, imageSavePath, username, password, platform);
}
catch (err) {
handleDockerPullError(err.message);
throw err;
}
}
async function pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform) {
if (await docker_1.Docker.binaryExists()) {
const pullAndSaveSuccessful = await pullWithDockerBinary(docker, targetImage, saveLocation, username, password, platform);
if (pullAndSaveSuccessful) {
return new image_1.ImageName(targetImage);
}
}
const { indexDigest, manifestDigest } = await pullFromContainerRegistry(docker, targetImage, imageSavePath, username, password, platform);
const imageName = new image_1.ImageName(targetImage, {
manifest: manifestDigest,
index: indexDigest,
});
return imageName;
}
/**
* In the case that an `ImageType.Identifier` is detected we need to produce
* an image archive, either by saving the image if it's already loaded into
* the local docker daemon, or by pulling the image from a remote registry and
* saving it to the filesystem directly.
*
* Users may also provide us with a URL to an image in a Docker compatible
* remote registry.
*
* @param {string} targetImage - The image to test, this could be in one of
* the following forms:
* * [registry/]<repo>/<image>[:tag]
* * <repo>/<image>[:tag]
* * <image>[:tag]
* In the case that a registry is not provided, the plugin will default
* this to Docker Hub. If a tag is not provided this will default to
* `latest`.
* @param {string} [username] - Optional username for private repo auth.
* @param {string} [password] - Optional password for private repo auth.
* @param {string} [platform] - Optional platform parameter to pull specific image arch.
*/
async function getImageArchive(targetImage, imageSavePath, username, password, platform) {
const docker = new docker_1.Docker();
mkdirp.sync(imageSavePath);
const destination = {
name: imageSavePath,
removeCallback: cleanupCallback(imageSavePath, "image.tar"),
};
const saveLocation = path.join(destination.name, "image.tar");
let inspectResult;
try {
inspectResult = await getInspectResult(docker, targetImage);
}
catch (_a) {
debug(`${targetImage} does not exist locally, proceeding to pull image.`);
}
if (inspectResult === undefined) {
const imageName = await pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform);
return {
imageName,
path: saveLocation,
removeArchive: destination.removeCallback,
};
}
if (platform !== undefined &&
inspectResult &&
!isLocalImageSameArchitecture(platform, inspectResult.Architecture)) {
const imageName = await pullImage(docker, targetImage, saveLocation, imageSavePath, username, password, platform);
return {
imageName,
path: saveLocation,
removeArchive: destination.removeCallback,
};
}
else {
await docker.save(targetImage, saveLocation);
const imageName = new image_1.ImageName(targetImage);
return {
imageName,
path: saveLocation,
removeArchive: destination.removeCallback,
};
}
}
exports.getImageArchive = getImageArchive;
function isImagePartOfURL(targetImage) {
// Based on the Docker spec, if the image contains a hostname, then the hostname should contain
// a `.` or `:` before the first instance of a `/`. ref: https://stackoverflow.com/a/37867949
if (!targetImage.includes("/")) {
return false;
}
const partBeforeFirstForwardSlash = targetImage.split("/")[0];
return (partBeforeFirstForwardSlash.includes(".") ||
partBeforeFirstForwardSlash.includes(":") ||
partBeforeFirstForwardSlash === "localhost");
}
function extractHostnameFromTargetImage(targetImage) {
// We need to detect if the `targetImage` is part of a URL. If not, the default hostname will be
// used (registry-1.docker.io). ref: https://stackoverflow.com/a/37867949
const defaultHostname = "registry-1.docker.io";
if (!isImagePartOfURL(targetImage)) {
return { hostname: defaultHostname, remainder: targetImage };
}
const dockerFriendlyRegistryHostname = "docker.io/";
if (targetImage.startsWith(dockerFriendlyRegistryHostname)) {
return {
hostname: defaultHostname,
remainder: targetImage.substring(dockerFriendlyRegistryHostname.length),
};
}
const i = targetImage.indexOf("/");
return {
hostname: targetImage.substring(0, i),
remainder: targetImage.substring(i + 1),
};
}
function extractImageNameAndTag(remainder, targetImage) {
const defaultTag = "latest";
if (!remainder.includes("@")) {
const [imageName, tag] = remainder.split(":");
return {
imageName: appendDefaultRepoPrefixIfRequired(imageName, targetImage),
tag: tag || defaultTag,
};
}
const [imageName, tag] = remainder.split("@");
return {
imageName: appendDefaultRepoPrefixIfRequired(dropTagIfSHAIsPresent(imageName), targetImage),
tag: tag || defaultTag,
};
}
function appendDefaultRepoPrefixIfRequired(imageName, targetImage) {
const defaultRepoPrefix = "library/";
if (isImagePartOfURL(targetImage) || imageName.includes("/")) {
return imageName;
}
return defaultRepoPrefix + imageName;
}
function dropTagIfSHAIsPresent(imageName) {
if (!imageName.includes(":")) {
return imageName;
}
return imageName.split(":")[0];
}
function extractImageDetails(targetImage) {
const { hostname, remainder } = extractHostnameFromTargetImage(targetImage);
const { imageName, tag } = extractImageNameAndTag(remainder, targetImage);
return { hostname, imageName, tag };
}
exports.extractImageDetails = extractImageDetails;
function isLocalImageSameArchitecture(platformOption, inspectResultArchitecture) {
let platformArchitecture;
try {
// Note: this is using the same flag/input pattern as the new Docker buildx: eg. linux/arm64/v8
platformArchitecture = platformOption.split("/")[1];
}
catch (error) {
debug(`Error parsing platform flag: '${error.message}'`);
return false;
}
return platformArchitecture === inspectResultArchitecture;
}
async function pullIfNotLocal(targetImage, options) {
const docker = new docker_1.Docker();
try {
await docker.inspectImage(targetImage);
return;
}
catch (err) {
// image doesn't exist locally
}
await docker.pullCli(targetImage);
}
exports.pullIfNotLocal = pullIfNotLocal;
//# sourceMappingURL=image-inspector.js.map
;