snyk-docker-plugin
Version:
Snyk CLI docker plugin
379 lines • 16.4 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.determinePaths = exports.readRawBuildInfo = exports.extractModuleInformation = exports.GoBinary = void 0;
const depGraph = require("@snyk/dep-graph");
const event_loop_spinner_1 = require("event-loop-spinner");
// NOTE: Paths will always be normalized to POSIX even on Windows.
// This makes it easier to ignore differences between Linux and Windows.
const path_1 = require("path");
const varint = require("varint");
const _1 = require("./");
const go_module_1 = require("./go-module");
const pclntab_1 = require("./pclntab");
class GoBinary {
constructor(goElfBinary) {
[this.name, this.modules] = extractModuleInformation(goElfBinary);
const pclnTab = goElfBinary.body.sections.find((section) => section.name === ".gopclntab");
// some CGo built binaries might not contain a pclnTab, which means we
// cannot scan the files.
// TODO: from a technical perspective, it would be enough to only report the
// modules, as the only remediation path is to upgrade a full module
// anyways. From a product perspective, it's not clear (yet).
if (pclnTab === undefined) {
throw Error("no pcln table present in Go binary");
}
this.matchFilesToModules(new pclntab_1.LineTable(pclnTab.data).go12MapFiles());
}
async depGraph() {
const goModulesDepGraph = new depGraph.DepGraphBuilder({ name: _1.DEP_GRAPH_TYPE }, { name: this.name });
for (const module of this.modules) {
for (const pkg of module.packages) {
if (event_loop_spinner_1.eventLoopSpinner.isStarving()) {
await event_loop_spinner_1.eventLoopSpinner.spin();
}
const nodeId = `${pkg}@${module.version}`;
goModulesDepGraph.addPkgNode({ name: pkg, version: module.version }, nodeId);
goModulesDepGraph.connectDep(goModulesDepGraph.rootNodeId, nodeId);
}
}
return goModulesDepGraph.build();
}
// matchFilesToModules goes through all files, extracts the package name and
// adds it to the relevant module in the GoBinary.
matchFilesToModules(files) {
const normalizedFiles = files.map((file) => path_1.posix.normalize(file));
const { modCachePath, vendorPath } = determinePaths(this.modules, normalizedFiles);
for (const fileName of normalizedFiles) {
if (fileName === "<autogenerated>") {
continue;
}
let moduleName = (mod) => mod.fullName();
let pkgFile = "";
if (vendorPath && fileName.startsWith(vendorPath)) {
moduleName = (mod) => mod.name;
pkgFile = trimPrefix(fileName, vendorPath);
}
else if (modCachePath && fileName.startsWith(modCachePath)) {
moduleName = (mod) => mod.fullName();
pkgFile = trimPrefix(fileName, modCachePath);
}
else if (!vendorPath && !modCachePath) {
// is trimmed
pkgFile = fileName;
}
else {
// skip file, probably a file from the Go source.
continue;
}
// Try to find the module that matches our file name, and if found,
// extract the package name out of it.
// Go source files will not be matched by any module, so they will be
// skipped automatically.
for (const module of this.modules) {
const modFullName = moduleName(module);
if (pkgFile.startsWith(modFullName)) {
// For example, the filename "github.com/my/pkg@v0.0.1/a/a.go" will be
// split into "github.com/my/pkg@v0.0.1/" and "a/a.go". We then get
// the package name from the package and file section, and add the
// normalized module name (without the version) in front. This will
// result in the package name "github.com/my/pkg/a".
const parts = pkgFile.split(modFullName);
if (parts.length !== 2 || parts[0] !== "") {
throw {
fileName: pkgFile,
moduleName: modFullName,
};
}
// for files in the "root" of a module
// (github.com/my/pkg@v0.0.1/a.go), the path.parse expression returns
// just a slash. This would result in a package name with a trailing
// slash, which is incorrect.
let dirName = path_1.posix.parse(parts[1]).dir;
if (dirName === path_1.posix.sep) {
dirName = "";
}
const pkgName = module.name + dirName;
if (!module.packages.includes(pkgName)) {
module.packages.push(pkgName);
}
}
}
}
}
}
exports.GoBinary = GoBinary;
function extractModuleInformation(binary) {
const mod = readRawBuildInfo(binary);
if (!mod) {
throw Error("binary contains empty module info");
}
const [pathDirective, mainModuleLine, ...versionsLines] = mod
.replace("\r", "")
.split("\n");
const lineSplit = mainModuleLine.split("\t");
let name = lineSplit[1];
if (lineSplit[0] !== "mod") {
// If the binary has no mod directive, it is a binary from the Go
// distribution, like the "go" command, "vet", "gofmt" or others. In that
// case, we use "go-distribution@" plus the path directive ("cmd/vet" for
// example) as the name. Using the "@" ensures that customers cannot create
// name-clashes with these as "@" is an invalid character in Go modules.
name = "go-distribution@" + pathDirective.split("\t")[1];
}
const modules = [];
versionsLines.forEach((versionLine) => {
const [depType, name, ver] = versionLine.split("\t");
if (!name || !ver) {
return;
}
if (depType === "dep") {
modules.push(new go_module_1.GoModule(name, ver));
}
else if (depType === "=>") {
// we've found a replace directive. These are always for the previous
// line/ module, so we simply need to replace the last module we added.
const last = modules.length - 1;
modules[last].name = name;
modules[last].version = ver;
}
});
return [name, modules];
}
exports.extractModuleInformation = extractModuleInformation;
// Source
// https://cs.opensource.google/go/go/+/refs/tags/go1.18.5:src/debug/buildinfo/buildinfo.go;l=142
/**
* Function finds and returns the Go version and
* module version information in the executable binary
* @param binary
*/
function readRawBuildInfo(binary) {
const buildInfoMagic = "\xff Go buildinf:";
// Read the first 64kB of dataAddr to find the build info blob.
// On some platforms, the blob will be in its own section, and DataStart
// returns the address of that section. On others, it's somewhere in the
// data segment; the linker puts it near the beginning.
const dataAddr = dataStart(binary);
let data = readData(binary.body.programs, dataAddr, 64 * 1024) || Buffer.from([]);
const buildInfoAlign = 16;
const buildInfoSize = 32;
while (true) {
const i = data.toString("binary").indexOf(buildInfoMagic);
if (i < 0 || data.length - i < buildInfoSize) {
throw Error("not a Go executable");
}
if (i % buildInfoAlign === 0 && data.length - i >= buildInfoSize) {
data = data.subarray(i);
break;
}
data = data.subarray((i + buildInfoAlign - 1) & ~buildInfoAlign);
}
// Decode the blob.
// The first 14 bytes are buildInfoMagic.
// The next two bytes indicate pointer size in bytes (4 or 8) and endianness
// (0 for little, 1 for big).
// Two virtual addresses to Go strings follow that: runtime.buildVersion,
// and runtime.modinfo.
// On 32-bit platforms, the last 8 bytes are unused.
// If the endianness has the 2 bit set, then the pointers are zero
// and the 32-byte header is followed by varint-prefixed string data
// for the two string values we care about.
const ptrSize = data[14];
if ((data[15] & 2) !== 0) {
data = data.subarray(32);
[, data] = decodeString(data);
const [mod] = decodeString(data);
return mod;
}
else {
const bigEndian = data[15] !== 0;
let readPtr;
if (ptrSize === 4) {
if (bigEndian) {
readPtr = (buffer) => buffer.readUInt32BE(0);
}
else {
readPtr = (buffer) => buffer.readUInt32LE(0);
}
}
else {
if (bigEndian) {
readPtr = (buffer) => Number(buffer.readBigUInt64BE());
}
else {
readPtr = (buffer) => Number(buffer.readBigUInt64LE());
}
}
// The build info blob left by the linker is identified by
// a 16-byte header, consisting of buildInfoMagic (14 bytes),
// the binary's pointer size (1 byte),
// and whether the binary is big endian (1 byte).
// Now we attempt to read info after metadata.
// From 16th byte to 16th + ptrSize there is a header that points
// to go version
const version = readString(binary, ptrSize, readPtr, readPtr(data.slice(16, 16 + ptrSize)));
if (version === "") {
throw Error("no version found in go binary");
}
// Go version header was right after metadata.
// Modules header right after go version
// Read next `ptrSize` bytes, this point to the
// place where modules info is stored
const mod = readString(binary, ptrSize, readPtr, readPtr(data.slice(16 + ptrSize, 16 + 2 * ptrSize)));
// This verifies that what we got are actually go modules
// First 16 bytes are unicodes as last 16
// Mirrors go version source code
if (mod.length >= 33 && mod[mod.length - 17] === "\n") {
return mod.slice(16, mod.length - 16);
}
else {
throw Error("binary is not built with go module support");
}
}
}
exports.readRawBuildInfo = readRawBuildInfo;
function decodeString(data) {
const num = varint.decode(data);
const size = varint.decode.bytes;
if (size <= 0 || num >= data.length - size) {
return ["", Buffer.from([])];
}
const res = data.subarray(size, num + size);
const rest = data.subarray(num + size);
return [res.toString("binary"), rest];
}
// Source
// https://github.com/golang/go/blob/46f99ce7ea97d11b0a1a079da8dda0f51df2a2d2/src/cmd/go/internal/version/exe.go#L105
/**
* Find start of section that contains module version data
* @param binary
*/
function dataStart(binary) {
for (const section of binary.body.sections) {
if (section.name === ".go.buildinfo") {
return section.addr;
}
}
for (const program of binary.body.programs) {
if (program.type === "load" && program.flags.w === true) {
return program.vaddr;
}
}
return 0;
}
// Source
// https://github.com/golang/go/blob/46f99ce7ea97d11b0a1a079da8dda0f51df2a2d2/src/cmd/go/internal/version/exe.go#L87
/**
* Read at most `size` of bytes from `program` that contains byte at `addr`
* @param programs
* @param addr
* @param size
*/
function readData(programs, addr, size) {
for (const program of programs) {
const vaddr = program.vaddr;
const filesz = program.filesz;
if (vaddr <= addr && addr <= vaddr + filesz - 1) {
let n = vaddr + filesz - addr;
if (n > size) {
n = size;
}
const from = addr - vaddr; // offset from the beginning of the program
return program.data.slice(from, from + n);
}
}
return undefined;
}
// Source
// https://github.com/golang/go/blob/46f99ce7ea97d11b0a1a079da8dda0f51df2a2d2/src/cmd/go/internal/version/version.go#L189
/**
* Function returns the string at address addr in the executable x
* @param binaryFile
* @param ptrSize
* @param readPtr
* @param addr
*/
function readString(binaryFile, ptrSize, readPtr, addr) {
const hdr = readData(binaryFile.body.programs, addr, 2 * ptrSize);
if (!hdr || hdr.length < 2 * ptrSize) {
return "";
}
const dataAddr = readPtr(hdr);
const dataLen = readPtr(hdr.slice(ptrSize));
const data = readData(binaryFile.body.programs, dataAddr, dataLen);
if (!data || data.length < dataLen) {
return "";
}
return data.toString("binary");
}
function isTrimmed(files) {
// the easiest way to detect trimmed binaries: the filenames will all be
// relative.
// There usually is a `build` line in the Go binary's metadata that denotes
// whether `trimpath` has been used or not, but there are binaries out there
// that have trimmed paths without that annotation (for example
// kyverno@v1.8.1).
return files.every((file) => !file.startsWith(path_1.posix.sep));
}
// determinePaths returns the modCachePath and vendorPath for a binary.
// goModCachePath is the path at which the modules are downloaded to. When
// building a Go binary, this is usually either $GOMODCACHE or
// $GOROOT/pkg/mod.
// The vendorPath is the path where vendored files are located, which is usually
// the main module's location + "/vendor".
//
// Binaries built with `-trimpath` will have all paths trimmed away, meaning
// that both returned values will be empty.
function determinePaths(modules, files) {
const normalizedFiles = files.map((file) => path_1.posix.normalize(file));
if (isTrimmed(normalizedFiles)) {
return { modCachePath: "", vendorPath: "" };
}
return {
modCachePath: determineGoModCachePath(modules, normalizedFiles),
vendorPath: determineVendorPath(modules, normalizedFiles),
};
}
exports.determinePaths = determinePaths;
function determineVendorPath(modules, files) {
// to determine the vendor path, we search for a file that contains
// `vendor/<module name>`. This file also contains the "root" of the
// repository, e.g. `/app/vendor/<module-name>`. This means that the `main`
// package is located somewhere in `/app/...`.
// We check for other files in that root to make sure that we really got the
// right vendor folder, and not just a random folder named `vendor` somewhere.
for (const [, mod] of Object.entries(modules)) {
// use path.join so that we will always get linux-style paths even if
// the plugin runs on Windows. This is necessary because the Go binaries
// always contain linux-style path separators.
const vendoredModulePath = path_1.posix.join("vendor", mod.name) + path_1.posix.sep;
const file = files.find((file) => file.includes(vendoredModulePath));
if (file) {
// make sure that we find other files in that path not in the vendor
// folder.
const mainModulePath = file.split(vendoredModulePath)[0];
const success = files.find((file) => file.includes(mainModulePath) && !file.includes(vendoredModulePath));
if (success) {
return path_1.posix.join(mainModulePath, "vendor") + path_1.posix.sep;
}
}
}
return "";
}
function determineGoModCachePath(modules, files) {
// files in the go mod cache path always contain the module name and version.
for (const [, mod] of Object.entries(modules)) {
const file = files.find((file) => file.includes(path_1.posix.sep + mod.fullName()));
if (file) {
return file.split(mod.fullName())[0];
}
}
return "";
}
function trimPrefix(s, prefix) {
if (s.startsWith(prefix)) {
return s.substring(prefix.length);
}
return s;
}
//# sourceMappingURL=go-binary.js.map
;