UNPKG

snyk-docker-plugin

Version:
265 lines (234 loc) 7.38 kB
import * as varint from "varint"; import { Elf, ElfProgram, GoModulesResult, GoVersionsResult, ReadPtrFunc, } from "./types"; /** * Create same output as `go version -m binary-file` does * @param binary */ export function extractModulesFromBinary(binary: Elf): GoModulesResult { const { version: goVersion, mod } = findVers(binary); const { name, modules } = prepareGoDependencies(mod); return { goVersion, name, modules }; } /** * Normalize versions to align with `snyk-go-parser` * @param mod */ function prepareGoDependencies( mod: string, ): Omit<GoModulesResult, "goVersion"> { if (!mod) { return { name: "", modules: {} }; } const [, mainModuleLine, ...versionsLines] = mod.split("\n"); const [, name] = mainModuleLine.split("\t"); const modules = {}; versionsLines.forEach((versionLine) => { if (!versionLine) { return; } const [, name, ver] = versionLine.split("\t"); if (!name || !ver) { return; } // Versions in Go have leading 'v' let version = ver.substring(1); // In versions with hash, we only care about hash // v0.0.0-20200905004654-be1d3432aa8f => #be1d3432aa8f version = version.includes("-") ? `#${version.substring(version.lastIndexOf("-") + 1)}` : version; modules[name] = version; }); return { name, modules }; } // Source // https://github.com/golang/go/blob/master/src/debug/buildinfo/buildinfo.go#L142 /** * Function finds and returns the Go version and * module version information in the executable binary * @param binary */ function findVers(binary: Elf): GoVersionsResult { const buildInfoMagic = "\xff Go buildinf:"; const result = { version: "", mod: "", }; // 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) { return result; } 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); [result.version, data] = decodeString(data); [result.mod, data] = decodeString(data); } else { const bigEndian = data[15] !== 0; let readPtr: ReadPtrFunc; 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: string = readString( binary, ptrSize, readPtr, readPtr(data.slice(16, 16 + ptrSize)), ); if (version === "") { return result; } result.version = version; // 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: string = 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") { result.mod = mod.slice(16, mod.length - 16); } else { result.mod = ""; } } return result; } function decodeString(data: Buffer): [string, Buffer] { 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: Elf): number { 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: ElfProgram[], addr: number, size: number, ): Buffer | undefined { 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: Elf, ptrSize: number, readPtr: ReadPtrFunc, addr: number, ): string { 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"); }