snyk-docker-plugin
Version:
Snyk CLI docker plugin
113 lines (102 loc) • 3.7 kB
text/typescript
import * as Debug from "debug";
import { normalize as normalizePath } from "path";
import { ChiselPackage } from "../../analyzer/types";
import { decompressZstd } from "../../compression-utils";
import { getContentAsBuffer } from "../../extractor";
import { ExtractAction, ExtractedLayers } from "../../extractor/types";
import { streamToBuffer } from "../../stream-utils";
const debug = Debug("snyk");
/**
* Path to the Chisel manifest file within container images.
* This file contains package and slice information for Chisel-built images.
*/
const CHISEL_MANIFEST_PATH = "/var/lib/chisel/manifest.wall";
/**
* Extract action for Ubuntu Chisel manifest files.
*
* Chisel is Ubuntu's tool for creating minimal container images by installing
* only specific "slices" of Debian packages. The manifest.wall file is a
* zstd-compressed NDJSON (newline-delimited JSON) file that records all
* installed packages, slices, and files for integrity verification and SBOM generation.
*
* See: https://documentation.ubuntu.com/chisel/en/latest/reference/manifest/
*/
export const getChiselManifestAction: ExtractAction = {
actionName: "chisel-manifest",
filePathMatches: (filePath) =>
filePath === normalizePath(CHISEL_MANIFEST_PATH),
callback: streamToBuffer,
};
/**
* Extracts and parses Chisel package information from Docker image layers.
*
* Searches for the Chisel manifest file (/var/lib/chisel/manifest.wall), decompresses it,
* and extracts package entries. The manifest uses NDJSON format where each line is a
* separate JSON object with a "kind" field indicating the entry type.
*
* @param extractedLayers - Layers extracted from the Docker image
* @returns Array of Chisel packages found in the manifest, or empty array if not found
*/
export function getChiselManifestContent(
extractedLayers: ExtractedLayers,
): ChiselPackage[] {
const compressedManifest = getContentAsBuffer(
extractedLayers,
getChiselManifestAction,
);
if (!compressedManifest) {
return [];
}
try {
const decompressed = decompressZstd(compressedManifest);
const manifestText = decompressed.toString("utf8");
const packages: ChiselPackage[] = [];
const lines = manifestText.split("\n");
for (const line of lines) {
if (!line.trim()) {
continue;
}
try {
const entry = JSON.parse(line);
// Only extract package entries; manifest also contains "slice", "path", and "content" entries
if (entry.kind === "package") {
// Validate required fields exist before creating package object
if (!entry.name || !entry.version || !entry.sha256 || !entry.arch) {
debug(
`Skipping package entry with missing required fields: ${JSON.stringify(
entry,
)}`,
);
continue;
}
packages.push({
kind: entry.kind,
name: entry.name,
version: entry.version,
sha256: entry.sha256,
arch: entry.arch,
});
}
} catch (parseError) {
// Skip malformed JSON lines - manifest may be corrupted or have trailing newlines
debug(
`Failed to parse Chisel manifest line: ${
parseError instanceof Error
? parseError.message
: String(parseError)
}`,
);
continue;
}
}
debug(`Found ${packages.length} Chisel packages in manifest`);
return packages;
} catch (error) {
debug(
`Failed to process Chisel manifest: ${
error instanceof Error ? error.message : String(error)
}`,
);
return [];
}
}