UNPKG

snyk-docker-plugin

Version:
175 lines (156 loc) 5.29 kB
import * as Debug from "debug"; import { createReadStream } from "fs"; import * as gunzip from "gunzip-maybe"; import { basename, normalize as normalizePath } from "path"; import { Readable } from "stream"; import { extract, Extract } from "tar-stream"; import { getErrorMessage } from "../error-utils"; import { streamToJson } from "../stream-utils"; export class InvalidArchiveError extends Error { constructor(message: string) { super(); this.name = "InvalidArchiveError"; this.message = message; } } import { HashAlgorithm, PluginOptions } from "../types"; import { extractImageLayer } from "./layer"; import { ExtractAction, ExtractedLayers, ExtractedLayersAndManifest, ImageConfig, TarArchiveManifest, } from "./types"; const debug = Debug("snyk"); export interface ArchiveConfig { isLayerFile: (name: string) => boolean; isImageConfigFile: (name: string) => boolean; formatLabel: string; layerErrorType: string; extractImageId: (configValue: string) => string; } export const dockerArchiveConfig: ArchiveConfig = { isLayerFile: (name) => basename(name).endsWith(".tar"), isImageConfigFile: (name) => new RegExp("[A-Fa-f0-9]{64}\\.json").test(name), formatLabel: "Docker", layerErrorType: "tar", extractImageId: (configValue) => configValue.split(".")[0], }; export const kanikoArchiveConfig: ArchiveConfig = { isLayerFile: (name) => basename(name).endsWith(".tar.gz"), isImageConfigFile: (name) => new RegExp("sha256:[A-Fa-f0-9]{64}").test(name), formatLabel: "Kaniko", layerErrorType: "tar.gz", extractImageId: (configValue) => configValue, }; export function createExtractArchive( config: ArchiveConfig, ): ( archiveFilesystemPath: string, extractActions: ExtractAction[], options: Partial<PluginOptions>, ) => Promise<ExtractedLayersAndManifest> { return (archiveFilesystemPath, extractActions, _options) => new Promise((resolve, reject) => { const tarExtractor: Extract = extract(); const layers: Record<string, ExtractedLayers> = {}; let manifest: TarArchiveManifest; let imageConfig: ImageConfig; tarExtractor.on("entry", async (header, stream, next) => { if (header.type === "file") { const normalizedName = normalizePath(header.name); if (config.isLayerFile(normalizedName)) { try { layers[normalizedName] = await extractImageLayer( stream, extractActions, ); } catch (error) { debug( `Error extracting layer content from: '${getErrorMessage( error, )}'`, ); reject( new Error(`Error reading ${config.layerErrorType} archive`), ); } } else if (isManifestFile(normalizedName)) { const manifestArray = await getManifestFile<TarArchiveManifest[]>( stream, ); manifest = manifestArray[0]; } else if (config.isImageConfigFile(normalizedName)) { imageConfig = await getManifestFile<ImageConfig>(stream); } } stream.resume(); next(); }); tarExtractor.on("finish", () => { try { resolve(assembleLayersAndManifest(manifest, imageConfig, layers)); } catch (error) { debug( `Error getting layers and manifest content from ${ config.formatLabel } archive: ${getErrorMessage(error)}`, ); reject( new InvalidArchiveError(`Invalid ${config.formatLabel} archive`), ); } }); tarExtractor.on("error", (error) => reject(error)); createReadStream(archiveFilesystemPath) .on("error", (error) => reject(error)) .pipe(gunzip()) .pipe(tarExtractor); }); } function assembleLayersAndManifest( manifest: TarArchiveManifest, imageConfig: ImageConfig, layers: Record<string, ExtractedLayers>, ): ExtractedLayersAndManifest { const layersWithNormalizedNames = manifest.Layers.map((layerName) => normalizePath(layerName), ); const filteredLayers = layersWithNormalizedNames .filter((layerName) => layers[layerName]) .map((layerName) => layers[layerName]) .reverse(); if (filteredLayers.length === 0) { throw new Error("We found no layers in the provided image"); } return { layers: filteredLayers, manifest, imageConfig, }; } async function getManifestFile<T>(stream: Readable): Promise<T> { return streamToJson<T>(stream); } function isManifestFile(name: string): boolean { return name === "manifest.json"; } export function createGetImageIdFromManifest( config: ArchiveConfig, ): (manifest: TarArchiveManifest) => string { return (manifest) => { try { const imageId = config.extractImageId(manifest.Config); if (imageId.includes(":")) { return imageId; } return `${HashAlgorithm.Sha256}:${imageId}`; } catch (err) { throw new Error("Failed to extract image ID from archive manifest"); } }; } export function getManifestLayers(manifest: TarArchiveManifest): string[] { return manifest.Layers.map((layer) => normalizePath(layer)); }