snyk-docker-plugin
Version:
Snyk CLI docker plugin
168 lines • 7.69 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.getPackagesFromRunInstructions = exports.instructionDigest = exports.getPackagesFromDockerfile = exports.getLayersFromPackages = exports.getDockerfileBaseImageName = void 0;
const types_1 = require("../types");
const types_2 = require("./types");
// Naive regex; see tests for cases
// tslint:disable-next-line:max-line-length
const installRegex = /(rpm\s+-i|rpm\s+--install|apk\s+((--update|-u|--no-cache)\s+)*add(\s+(--update|-u|--no-cache))*|apt-get\s+((--assume-yes|--yes|-y)\s+)*install(\s+(--assume-yes|--yes|-y))*|apt\s+((--assume-yes|--yes|-y)\s+)*install|yum\s+install|aptitude\s+install)\s+/;
function getPackagesFromDockerfile(dockerfile) {
const runInstructions = getRunInstructionsFromDockerfile(dockerfile);
return getPackagesFromRunInstructions(runInstructions);
}
exports.getPackagesFromDockerfile = getPackagesFromDockerfile;
function getRunInstructionsFromDockerfile(dockerfile) {
return dockerfile
.getInstructions()
.filter((instruction) => instruction.getInstruction().toUpperCase() === "RUN")
.map((instruction) => getInstructionExpandVariables(instruction, dockerfile, types_1.UnresolvedDockerfileVariableHandling.Continue));
}
/*
* This is fairly ugly because a single RUN could contain multiple install
* commands, which in turn may install multiple packages, so we've got a
* 3-level nested array (RUN instruction[] -> install[] -> package[])
*
* We also need to account for the multiple ways to split commands, and
* arbitrary whitespace
*/
function getPackagesFromRunInstructions(runInstructions) {
return runInstructions.reduce((dockerfilePackages, instruction) => {
const cleanedInstruction = cleanInstruction(instruction);
const commands = cleanedInstruction.split(/\s?(;|&&)\s?/);
const installCommands = commands.filter((command) => installRegex.test(command));
if (installCommands.length) {
// Get the packages per install command and flatten them
for (const command of installCommands) {
const packages = command
.replace(installRegex, "")
.split(/\s+/)
.filter((arg) => arg && !arg.startsWith("-"));
packages.forEach((pkg) => {
var _a;
// Use package name without version as the key
let name = pkg.split("=")[0];
if (name.startsWith("$")) {
name = name.slice(1);
}
const installCommand = ((_a = installCommands
.find((cmd) => cmd.indexOf(name) !== -1)) === null || _a === void 0 ? void 0 : _a.replace(/\s+/g, " ").trim()) || "Unknown";
dockerfilePackages[name] = {
instruction,
installCommand,
};
});
}
}
return dockerfilePackages;
}, {});
}
exports.getPackagesFromRunInstructions = getPackagesFromRunInstructions;
/**
* Return the instruction text without any of the image prefixes
* @param instruction the full RUN instruction extracted from image
*/
function cleanInstruction(instruction) {
let cleanedInstruction = instruction;
const runDefs = ["RUN ", "/bin/sh ", "-c "];
const argsPrefixRegex = /^\|\d .*?=/;
for (const runDef of runDefs) {
if (cleanedInstruction.startsWith(runDef)) {
cleanedInstruction = cleanedInstruction.slice(runDef.length);
if (runDef === runDefs[0] && argsPrefixRegex.test(cleanedInstruction)) {
const match = installRegex.exec(cleanedInstruction);
if (match) {
cleanedInstruction = cleanedInstruction.slice(match.index);
}
}
}
}
return cleanedInstruction;
}
/**
* Return the specified text with variables expanded
* @param instruction the instruction associated with this string
* @param dockerfile Dockerfile to use for expanding the variables
* @param unresolvedVariableHandling Strategy for reacting to unresolved vars
* @param text a string with variables to expand, if not specified
* the instruction text is used
*/
function getInstructionExpandVariables(instruction, dockerfile, unresolvedVariableHandling, text) {
let str = text || instruction.toString();
const resolvedVariables = {};
for (const variable of instruction.getVariables()) {
const line = variable.getRange().start.line;
const name = variable.getName();
resolvedVariables[name] = dockerfile.resolveVariable(name, line);
}
for (const variable of Object.keys(resolvedVariables)) {
if (unresolvedVariableHandling ===
types_1.UnresolvedDockerfileVariableHandling.Abort &&
!resolvedVariables[variable]) {
str = "";
break;
}
// The $ is a special regexp character that should be escaped with a backslash
// Support both notations either with $variable_name or ${variable_name}
// The global search "g" flag is used to match and replace all occurrences
str = str.replace(RegExp(`\\$\{${variable}\}|\\$${variable}`, "g"), resolvedVariables[variable] || "");
}
return str;
}
/**
* Return the image name of the last from stage, after resolving all aliases
* @param dockerfile Dockerfile to use for retrieving the last stage image name
*/
function getDockerfileBaseImageName(dockerfile) {
const froms = dockerfile.getFROMs();
// collect stages names
const stagesNames = froms.reduce((stagesNames, fromInstruction) => {
const fromName = fromInstruction.getImage();
const args = fromInstruction.getArguments();
// the FROM expanded base name
const expandedName = getInstructionExpandVariables(fromInstruction, dockerfile, types_1.UnresolvedDockerfileVariableHandling.Abort, fromName);
const hasUnresolvedVariables = expandedName.split(":").some((name) => !name) ||
expandedName.split("@").some((name) => !name);
// store the resolved stage name
if (!hasUnresolvedVariables) {
stagesNames.last = stagesNames.aliases[expandedName] || expandedName;
}
if (args.length > 2 && args[1].getValue().toUpperCase() === "AS") {
// the AS alias name
const aliasName = args[2].getValue();
// support nested referral
stagesNames.aliases[aliasName] = stagesNames.last;
}
return stagesNames;
}, { last: undefined, aliases: {} });
if (stagesNames.last) {
return {
baseImage: stagesNames.last,
};
}
if (!froms.length) {
return {
error: {
code: types_2.DockerFileAnalysisErrorCode.BASE_IMAGE_NAME_NOT_FOUND,
},
};
}
return {
error: {
code: types_2.DockerFileAnalysisErrorCode.BASE_IMAGE_NON_RESOLVABLE,
},
};
}
exports.getDockerfileBaseImageName = getDockerfileBaseImageName;
function instructionDigest(instruction) {
return Buffer.from(instruction).toString("base64");
}
exports.instructionDigest = instructionDigest;
function getLayersFromPackages(dockerfilePkgs) {
return Object.keys(dockerfilePkgs).reduce((res, pkg) => {
const { instruction } = dockerfilePkgs[pkg];
res[instructionDigest(instruction)] = { instruction };
return res;
}, {});
}
exports.getLayersFromPackages = getLayersFromPackages;
//# sourceMappingURL=instruction-parser.js.map
;