diff --git a/lib/analyzer/layer-attribution.ts b/lib/analyzer/layer-attribution.ts new file mode 100644 index 000000000..7c863dd50 --- /dev/null +++ b/lib/analyzer/layer-attribution.ts @@ -0,0 +1,257 @@ +import { ExtractedLayers, HistoryEntry } from "../extractor/types"; +import { LayerAttributionEntry } from "../facts"; +import { + getApkDbFileContent, + getApkDbFileContentAction, +} from "../inputs/apk/static"; +import { + getAptDbFileContent, + getDpkgFileContentAction, +} from "../inputs/apt/static"; +import { + getChiselManifestAction, + getChiselManifestContent, +} from "../inputs/chisel/static"; +import { + getRpmDbFileContent, + getRpmDbFileContentAction, + getRpmNdbFileContent, + getRpmNdbFileContentAction, + getRpmSqliteDbFileContent, + getRpmSqliteDbFileContentAction, +} from "../inputs/rpm/static"; +import { analyze as apkAnalyze } from "./package-managers/apk"; +import { analyze as aptAnalyze } from "./package-managers/apt"; +import { analyze as chiselAnalyze } from "./package-managers/chisel"; +import { analyze as rpmAnalyze } from "./package-managers/rpm"; +import { AnalysisType } from "./types"; + +export interface LayerAttributionResult { + entries: LayerAttributionEntry[]; + pkgLayerMap: Map; +} + +/** + * Returns the Dockerfile instruction strings for all non-empty history entries, + * in order. The resulting array aligns 1:1 with `rootFsLayers` — each index + * corresponds to the real filesystem layer at that position. + * + * This differs intentionally from `getUserInstructionLayersFromConfig` in + * extractor/index.ts, which uses a timestamp heuristic to select only the + * *user-added* layers for Dockerfile attribution. Here we need instructions + * for every layer so we can annotate each attribution entry correctly. + */ +function buildHistoryInstructions( + history: HistoryEntry[] | null | undefined, +): string[] { + if (!history) { + return []; + } + return history.filter((h) => !h.empty_layer).map((h) => h.created_by ?? ""); +} + +function pkgKey(name: string, version: string): string { + return `${name}@${version}`; +} + +/** + * Returns true if the layer contains a file that was processed by the given + * extract action. Used to distinguish "layer has no package DB" (return null + * → skip) from "layer has an empty package DB" (return empty Set → track). + */ +function layerHasAction(layer: ExtractedLayers, actionName: string): boolean { + return Object.values(layer).some((fileContent) => actionName in fileContent); +} + +/** + * Parses the package DB for a single layer and returns the set of + * "name@version" keys present in that layer. + * + * Returns null when the layer does not contain the package DB file at all + * (e.g. a COPY or ENV instruction). An empty Set means the DB file exists + * but is empty (e.g. all packages were removed in this layer). + */ +async function parseLayerPackages( + layer: ExtractedLayers, + analysisType: AnalysisType, + targetImage: string, +): Promise | null> { + if (analysisType === AnalysisType.Apk) { + if (!layerHasAction(layer, getApkDbFileContentAction.actionName)) { + return null; + } + const content = getApkDbFileContent(layer); + const analysis = await apkAnalyze(targetImage, content); + const result = new Set(); + for (const pkg of analysis.Analysis) { + result.add(pkgKey(pkg.Name, pkg.Version)); + } + return result; + } + + if (analysisType === AnalysisType.Apt) { + if (!layerHasAction(layer, getDpkgFileContentAction.actionName)) { + return null; + } + const aptFiles = getAptDbFileContent(layer); + const analysis = await aptAnalyze(targetImage, aptFiles); + const result = new Set(); + for (const pkg of analysis.Analysis) { + result.add(pkgKey(pkg.Name, pkg.Version)); + } + return result; + } + + if (analysisType === AnalysisType.Rpm) { + const hasBdb = layerHasAction(layer, getRpmDbFileContentAction.actionName); + const hasNdb = layerHasAction(layer, getRpmNdbFileContentAction.actionName); + const hasSqlite = layerHasAction( + layer, + getRpmSqliteDbFileContentAction.actionName, + ); + if (!hasBdb && !hasNdb && !hasSqlite) { + return null; + } + const [bdbPkgs, ndbPkgs, sqlitePkgs] = await Promise.all([ + hasBdb ? getRpmDbFileContent(layer) : Promise.resolve([]), + hasNdb ? getRpmNdbFileContent(layer) : Promise.resolve([]), + hasSqlite ? getRpmSqliteDbFileContent(layer) : Promise.resolve([]), + ]); + const analysis = await rpmAnalyze( + targetImage, + [...bdbPkgs, ...ndbPkgs, ...sqlitePkgs], + [], + ); + const result = new Set(); + for (const pkg of analysis.Analysis) { + result.add(pkgKey(pkg.Name, pkg.Version)); + } + return result; + } + + if (analysisType === AnalysisType.Chisel) { + if (!layerHasAction(layer, getChiselManifestAction.actionName)) { + return null; + } + const pkgs = getChiselManifestContent(layer); + const analysis = await chiselAnalyze(targetImage, pkgs); + const result = new Set(); + for (const pkg of analysis.Analysis) { + result.add(pkgKey(pkg.Name, pkg.Version)); + } + return result; + } + + return null; +} + +export async function computeLayerAttribution( + orderedLayers: ExtractedLayers[], + analysisType: AnalysisType, + rootFsLayers: string[], + manifestLayers: string[], + history: HistoryEntry[] | null | undefined, + targetImage: string, +): Promise { + const instructions = buildHistoryInstructions(history); + const entries: LayerAttributionEntry[] = []; + const pkgLayerMap = new Map(); + const limit = Math.min(orderedLayers.length, rootFsLayers.length); + + let previousPkgs = new Set(); + + for (let i = 0; i < limit; i++) { + const diffID = rootFsLayers[i]; + // Explicit bounds guard: manifestLayers and instructions may be shorter + // than rootFsLayers for malformed or partially-described images. + const digest = i < manifestLayers.length ? manifestLayers[i] : undefined; + const instruction = i < instructions.length ? instructions[i] : undefined; + + const currentPkgs = await parseLayerPackages( + orderedLayers[i], + analysisType, + targetImage, + ); + if (currentPkgs === null) { + // Layer has no package DB file (e.g. COPY/ENV/LABEL instruction). + // Do not update previousPkgs — the package state has not changed. + continue; + } + + const newPkgs: string[] = []; + for (const key of currentPkgs) { + if (!previousPkgs.has(key)) { + newPkgs.push(key); + pkgLayerMap.set(key, { layerIndex: i, diffID }); + } + } + + const removedPkgs: string[] = []; + for (const key of previousPkgs) { + if (!currentPkgs.has(key)) { + removedPkgs.push(key); + } + } + + if (newPkgs.length > 0 || removedPkgs.length > 0) { + const entry: LayerAttributionEntry = { + layerIndex: i, + diffID, + packages: newPkgs, + }; + if (digest) { + entry.digest = digest; + } + if (instruction) { + entry.instruction = instruction; + } + if (removedPkgs.length > 0) { + entry.removedPackages = removedPkgs; + } + entries.push(entry); + } + + previousPkgs = currentPkgs; + } + + return { entries, pkgLayerMap }; +} + +/** + * Merges attribution entries produced by multiple package managers into a + * single list sorted by layer index. When two managers both write entries for + * the same layer (e.g. APT and Chisel in a mixed image), their package lists + * and removedPackages lists are combined. Layer metadata (diffID, digest, + * instruction) is taken from the first entry seen for that layer index. + */ +export function mergeLayerAttributionEntries( + entries: LayerAttributionEntry[], +): LayerAttributionEntry[] { + const byLayer = new Map(); + + for (const entry of entries) { + const existing = byLayer.get(entry.layerIndex); + if (!existing) { + byLayer.set(entry.layerIndex, { + ...entry, + packages: [...entry.packages], + removedPackages: entry.removedPackages + ? [...entry.removedPackages] + : undefined, + }); + } else { + existing.packages.push(...entry.packages); + if (entry.removedPackages && entry.removedPackages.length > 0) { + if (!existing.removedPackages) { + existing.removedPackages = [...entry.removedPackages]; + } else { + existing.removedPackages.push(...entry.removedPackages); + } + } + } + } + + return Array.from(byLayer.values()).sort( + (a, b) => a.layerIndex - b.layerIndex, + ); +} diff --git a/lib/analyzer/static-analyzer.ts b/lib/analyzer/static-analyzer.ts index 7c32dac6c..06ef28fac 100644 --- a/lib/analyzer/static-analyzer.ts +++ b/lib/analyzer/static-analyzer.ts @@ -2,6 +2,7 @@ import * as Debug from "debug"; import { DockerFileAnalysis } from "../dockerfile"; import { getErrorMessage } from "../error-utils"; import * as archiveExtractor from "../extractor"; +import { LayerAttributionEntry } from "../facts"; import { getGoModulesContentAction, goModulesToScannedProjects, @@ -70,6 +71,10 @@ import { pipFilesToScannedProjects } from "./applications/python"; import { getApplicationFiles } from "./applications/runtime-common"; import { AppDepsScanResultWithoutTarget } from "./applications/types"; import { detectJavaRuntime } from "./base-runtimes"; +import { + computeLayerAttribution, + mergeLayerAttributionEntries, +} from "./layer-attribution"; import * as osReleaseDetector from "./os-release"; import { analyze as apkAnalyze } from "./package-managers/apk"; import { @@ -82,6 +87,7 @@ import { mapRpmSqlitePackages, } from "./package-managers/rpm"; import { + AnalysisType, ImagePackagesAnalysis, OSRelease, StaticPackagesAnalysis, @@ -159,6 +165,7 @@ export async function analyze( imageId, manifestLayers, extractedLayers, + orderedLayers, rootFsLayers, autoDetectedUserInstructions, platform, @@ -236,6 +243,55 @@ export async function analyze( throw new Error("Failed to detect installed OS packages"); } + let layerPackageAttribution: LayerAttributionEntry[] | undefined; + if ( + isTrue(options["layer-attribution"]) && + rootFsLayers && + orderedLayers && + orderedLayers.length > 0 + ) { + const resultsWithPackages = results.filter((r) => r.Analysis.length > 0); + if (resultsWithPackages.length > 0) { + const allEntries: LayerAttributionEntry[] = []; + const attributionCache = new Map< + AnalysisType, + Map + >(); + for (const result of resultsWithPackages) { + try { + let pkgLayerMap: Map; + if (attributionCache.has(result.AnalyzeType)) { + pkgLayerMap = attributionCache.get(result.AnalyzeType)!; + } else { + const { entries, pkgLayerMap: computed } = + await computeLayerAttribution( + orderedLayers, + result.AnalyzeType, + rootFsLayers, + manifestLayers, + history, + targetImage, + ); + allEntries.push(...entries); + attributionCache.set(result.AnalyzeType, computed); + pkgLayerMap = computed; + } + for (const pkg of result.Analysis) { + const key = `${pkg.Name}@${pkg.Version}`; + const attr = pkgLayerMap.get(key); + if (attr) { + pkg.layerIndex = attr.layerIndex; + pkg.layerDiffId = attr.diffID; + } + } + } catch (err) { + debug(`Could not compute layer attribution: ${getErrorMessage(err)}`); + } + } + layerPackageAttribution = mergeLayerAttributionEntries(allEntries); + } + } + const binaries = getBinariesHashes(extractedLayers); const javaRuntime = detectJavaRuntime(extractedLayers); const baseRuntimes = javaRuntime ? [javaRuntime] : undefined; @@ -318,6 +374,7 @@ export async function analyze( baseRuntimes, imageLayers: manifestLayers, rootFsLayers, + layerPackageAttribution, applicationDependenciesScanResults, manifestFiles, autoDetectedUserInstructions, diff --git a/lib/analyzer/types.ts b/lib/analyzer/types.ts index 78a1874ba..abe27a639 100644 --- a/lib/analyzer/types.ts +++ b/lib/analyzer/types.ts @@ -1,5 +1,5 @@ import { ImageName } from "../extractor/image"; -import { BaseRuntime } from "../facts"; +import { BaseRuntime, LayerAttributionEntry } from "../facts"; import { AutoDetectedUserInstructions, ManifestFile } from "../types"; import { AppDepsScanResultWithoutTarget, @@ -17,6 +17,8 @@ export interface AnalyzedPackage { }; Purl?: string; AutoInstalled?: boolean; + layerIndex?: number; + layerDiffId?: string; } export interface AnalyzedPackageWithVersion extends AnalyzedPackage { Version: string; @@ -79,6 +81,7 @@ export interface StaticAnalysis { baseRuntimes?: BaseRuntime[]; imageLayers: string[]; rootFsLayers?: string[]; + layerPackageAttribution?: LayerAttributionEntry[]; autoDetectedUserInstructions?: AutoDetectedUserInstructions; applicationDependenciesScanResults: AppDepsScanResultWithoutTarget[]; manifestFiles: ManifestFile[]; diff --git a/lib/dependency-tree/index.ts b/lib/dependency-tree/index.ts index 53e95f132..895e03d16 100644 --- a/lib/dependency-tree/index.ts +++ b/lib/dependency-tree/index.ts @@ -121,11 +121,19 @@ export function buildTree( }; for (const depInfo of tooFrequentDeps) { + const freqLabels: { [key: string]: string } = {}; + if (depInfo.layerDiffId !== undefined) { + freqLabels.layerDiffId = depInfo.layerDiffId; + } + if (depInfo.layerIndex !== undefined) { + freqLabels.layerIndex = String(depInfo.layerIndex); + } const pkg: DepTreeDep = { name: depFullName(depInfo), version: depInfo.Version, sourceVersion: depInfo.SourceVersion, dependencies: {}, + ...(Object.keys(freqLabels).length > 0 ? { labels: freqLabels } : {}), }; // The existence of the "meta" package breaks upgrade @@ -172,11 +180,20 @@ function buildTreeRecursive( return null; } + const labels: { [key: string]: string } = {}; + if (depInfo.layerDiffId !== undefined) { + labels.layerDiffId = depInfo.layerDiffId; + } + if (depInfo.layerIndex !== undefined) { + labels.layerIndex = String(depInfo.layerIndex); + } + const tree: DepTreeDep = { name: fullName, version: depInfo.Version, purl: depInfo.Purl, dependencies: {}, + ...(Object.keys(labels).length > 0 ? { labels } : {}), }; if (depInfo._visited) { return tree; diff --git a/lib/extractor/index.ts b/lib/extractor/index.ts index c5cd0a278..98fbe77a9 100644 --- a/lib/extractor/index.ts +++ b/lib/extractor/index.ts @@ -5,6 +5,7 @@ import { getPackagesFromRunInstructions, } from "../dockerfile/instruction-parser"; import { getErrorMessage } from "../error-utils"; +import { isTrue } from "../option-utils"; import { AutoDetectedUserInstructions, ImageType } from "../types"; import { PluginOptions } from "../types"; import * as dockerExtractor from "./docker-archive"; @@ -147,6 +148,9 @@ export async function extractImageContent( manifestLayers: extractor.getManifestLayers(archiveContent.manifest), imageCreationTime: archiveContent.imageConfig.created, extractedLayers: layersWithLatestFileModifications(archiveContent.layers), + orderedLayers: isTrue(options?.["layer-attribution"]) + ? archiveContent.layers + : undefined, rootFsLayers: getRootFsLayersFromConfig(archiveContent.imageConfig), autoDetectedUserInstructions: getDetectedLayersInfoFromConfig( archiveContent.imageConfig, diff --git a/lib/extractor/types.ts b/lib/extractor/types.ts index d07b1ea2f..0949526e6 100644 --- a/lib/extractor/types.ts +++ b/lib/extractor/types.ts @@ -29,6 +29,7 @@ export interface ExtractionResult { imageId: string; manifestLayers: string[]; extractedLayers: ExtractedLayers; + orderedLayers?: ExtractedLayers[]; rootFsLayers?: string[]; autoDetectedUserInstructions?: AutoDetectedUserInstructions; platform?: string; diff --git a/lib/facts.ts b/lib/facts.ts index d8902705b..76bb55595 100644 --- a/lib/facts.ts +++ b/lib/facts.ts @@ -162,3 +162,17 @@ export interface BaseRuntimesFact { type: "baseRuntimes"; data: BaseRuntime[]; } + +export interface LayerAttributionEntry { + layerIndex: number; + diffID: string; + digest?: string; + instruction?: string; + packages: string[]; + removedPackages?: string[]; +} + +export interface LayerPackageAttributionFact { + type: "layerPackageAttribution"; + data: LayerAttributionEntry[]; +} diff --git a/lib/response-builder.ts b/lib/response-builder.ts index 1acbf95cd..540ca6a88 100644 --- a/lib/response-builder.ts +++ b/lib/response-builder.ts @@ -186,6 +186,17 @@ async function buildResponse( additionalFacts.push(rootFsFact); } + if ( + depsAnalysis.layerPackageAttribution && + depsAnalysis.layerPackageAttribution.length > 0 + ) { + const layerPackageAttributionFact: facts.LayerPackageAttributionFact = { + type: "layerPackageAttribution", + data: depsAnalysis.layerPackageAttribution, + }; + additionalFacts.push(layerPackageAttributionFact); + } + if (depsAnalysis.depTree.targetOS.prettyName) { const imageOsReleasePrettyNameFact: facts.ImageOsReleasePrettyNameFact = { type: "imageOsReleasePrettyName", diff --git a/lib/types.ts b/lib/types.ts index d4220560e..3f50afdd4 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -82,7 +82,9 @@ export type FactType = // Used for application dependencies scanning; shows which files were used in the analysis of the dependencies. | "testedFiles" // Application files observed in the image - | "applicationFiles"; + | "applicationFiles" + // Per-layer package attribution: which layer introduced each OS package + | "layerPackageAttribution"; export interface PluginResponse { /** The first result is guaranteed to be the OS dependencies scan result. */ @@ -238,6 +240,9 @@ export interface PluginOptions { /** Include system-level JARs and WARs from /usr/lib in scan results. The default is "false". */ "include-system-jars": boolean | string; + /** Compute and emit per-layer package attribution. The default is "false". */ + "layer-attribution": boolean | string; + "target-reference": string; parameterWarnings?: string[]; diff --git a/test/harness/run.ts b/test/harness/run.ts new file mode 100644 index 000000000..bb2264d78 --- /dev/null +++ b/test/harness/run.ts @@ -0,0 +1,190 @@ +#!/usr/bin/env ts-node +/** + * Plugin harness — exercise the scan() API from the command line. + * + * Usage: + * ts-node test/harness/run.ts [options] + * + * Examples: + * ts-node test/harness/run.ts docker-archive:test/fixtures/docker-archives/docker-save/nginx.tar + * ts-node test/harness/run.ts --output /tmp/out.json --fact layerPackageAttribution docker-archive:test/fixtures/... + * ts-node test/harness/run.ts --platform linux/arm64 alpine:3.19 + * ts-node test/harness/run.ts --username user --password pass registry.example.com/image:tag + */ + +import * as fs from "fs"; +import * as path from "path"; +import { scan } from "../../lib/scan"; +import { Fact, PluginOptions } from "../../lib/types"; + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +interface Args { + image: string; + output?: string; + facts: string[]; + compact: boolean; + pluginOptions: Partial; +} + +function printHelp(): void { + console.log(` +Usage: ts-node test/harness/run.ts [options] + + Image identifier (e.g. "alpine:3.19") or a prefixed + archive path: + docker-archive:/path/to/image.tar + oci-archive:/path/to/image.tar + kaniko-archive:/path/to/image.tar + +Options: + --file Path to a Dockerfile for Dockerfile analysis + --platform Target platform, e.g. linux/amd64 (default) + --username Registry username (or set SNYK_REGISTRY_USERNAME) + --password Registry password (or set SNYK_REGISTRY_PASSWORD) + --image-name Override image name/tag for archive scans + --exclude-app-vulns Exclude application vulnerability scanning + --exclude-base-image-vulns Exclude base image packages from results + --exclude-node-modules Skip node_modules scanning + --nested-jars-depth Depth for nested JAR unpacking (default: 1) + --collect-application-files Collect application file metadata + --layer-attribution Compute per-layer package attribution (adds layerPackageAttribution fact) + + --output Write JSON output to file instead of stdout + --compact Compact JSON (default: pretty-printed) + --fact Show only this fact type (repeatable). + e.g. --fact depGraph --fact layerPackageAttribution + --help Show this message +`); +} + +function parseArgs(argv: string[]): Args { + const args = argv.slice(2); + const pluginOptions: Partial = {}; + const facts: string[] = []; + let output: string | undefined; + let compact = false; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + const next = () => { + if (i + 1 >= args.length || args[i + 1].startsWith("--")) { + throw new Error(`${arg} requires a value`); + } + return args[++i]; + }; + + switch (arg) { + case "--help": + case "-h": + printHelp(); + process.exit(0); + break; + case "--file": + pluginOptions.file = next(); + break; + case "--platform": + pluginOptions.platform = next(); + break; + case "--username": + pluginOptions.username = next(); + break; + case "--password": + pluginOptions.password = next(); + break; + case "--image-name": + pluginOptions.imageNameAndTag = next(); + break; + case "--exclude-app-vulns": + pluginOptions["exclude-app-vulns"] = true; + break; + case "--exclude-base-image-vulns": + pluginOptions["exclude-base-image-vulns"] = true; + break; + case "--exclude-node-modules": + pluginOptions["exclude-node-modules"] = true; + break; + case "--nested-jars-depth": + pluginOptions["nested-jars-depth"] = next(); + break; + case "--collect-application-files": + pluginOptions["collect-application-files"] = true; + break; + case "--layer-attribution": + pluginOptions["layer-attribution"] = true; + break; + case "--output": + output = next(); + break; + case "--compact": + compact = true; + break; + case "--fact": + facts.push(next()); + break; + default: + if (arg.startsWith("--")) { + console.error(`Unknown option: ${arg}`); + printHelp(); + process.exit(1); + } + // Positional: the image/archive path + if (pluginOptions.path) { + console.error("Unexpected extra argument: " + arg); + process.exit(1); + } + pluginOptions.path = arg; + } + } + + if (!pluginOptions.path) { + console.error("Error: is required\n"); + printHelp(); + process.exit(1); + } + + return { image: pluginOptions.path, output, facts, compact, pluginOptions }; +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main(): Promise { + const { output, facts, compact, pluginOptions } = parseArgs(process.argv); + + let result; + try { + result = await scan(pluginOptions); + } catch (err: any) { + console.error("Scan failed:", err.message || err); + process.exit(1); + } + + // Filter to requested fact types if --fact was given + if (facts.length > 0) { + const factSet = new Set(facts); + result = { + scanResults: result.scanResults.map((sr) => ({ + ...sr, + facts: sr.facts.filter((f: Fact) => factSet.has(f.type)), + })), + }; + } + + const json = compact + ? JSON.stringify(result) + : JSON.stringify(result, null, 2); + + if (output) { + const dest = path.resolve(output); + fs.writeFileSync(dest, json, "utf8"); + console.error(`Output written to ${dest}`); + } else { + process.stdout.write(json + "\n"); + } +} + +main(); diff --git a/test/lib/analyzer/layer-attribution.spec.ts b/test/lib/analyzer/layer-attribution.spec.ts new file mode 100644 index 000000000..3a13c0d91 --- /dev/null +++ b/test/lib/analyzer/layer-attribution.spec.ts @@ -0,0 +1,503 @@ +import { + computeLayerAttribution, + LayerAttributionResult, + mergeLayerAttributionEntries, +} from "../../../lib/analyzer/layer-attribution"; +import { AnalysisType } from "../../../lib/analyzer/types"; +import { ExtractedLayers } from "../../../lib/extractor/types"; +import { LayerAttributionEntry } from "../../../lib/facts"; + +// Minimal APK DB stanza format: "P:\nV:\n\n" +function makeApkDb(...pkgs: Array<{ name: string; version: string }>): string { + return pkgs.map((p) => `P:${p.name}\nV:${p.version}\n`).join("\n") + "\n"; +} + +function makeApkLayer(content: string): ExtractedLayers { + return { + "/lib/apk/db/installed": { "apk-db": content }, + }; +} + +const emptyLayer: ExtractedLayers = {}; + +describe("computeLayerAttribution", () => { + const image = "test-image:latest"; + + describe("APK package manager", () => { + it("attributes all packages to layer 0 when there is only one layer", async () => { + const pkgs = [ + { name: "curl", version: "7.0.0-r0" }, + { name: "libc", version: "2.35-r1" }, + ]; + const orderedLayers = [makeApkLayer(makeApkDb(...pkgs))]; + const rootFsLayers = ["sha256:aaa"]; + const manifestLayers = ["sha256:aaa-compressed"]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(1); + expect(result.entries[0].layerIndex).toBe(0); + expect(result.entries[0].diffID).toBe("sha256:aaa"); + expect(result.entries[0].digest).toBe("sha256:aaa-compressed"); + expect(result.entries[0].packages).toContain("curl@7.0.0-r0"); + expect(result.entries[0].packages).toContain("libc@2.35-r1"); + + expect(result.pkgLayerMap.get("curl@7.0.0-r0")).toEqual({ + layerIndex: 0, + diffID: "sha256:aaa", + }); + }); + + it("attributes new packages to the layer where they first appear", async () => { + const basePkgs = [ + { name: "libc", version: "2.35-r1" }, + { name: "curl", version: "7.0.0-r0" }, + ]; + const allPkgs = [...basePkgs, { name: "nginx", version: "1.24.0-r0" }]; + + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + makeApkLayer(makeApkDb(...allPkgs)), + ]; + const rootFsLayers = ["sha256:base", "sha256:nginx-layer"]; + const manifestLayers = ["sha256:base-c", "sha256:nginx-c"]; + const history = [ + { created_by: "FROM alpine:3.19", empty_layer: false }, + { created_by: "RUN apk add nginx", empty_layer: false }, + ]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + history, + image, + ); + + expect(result.entries).toHaveLength(2); + + const layer0 = result.entries[0]; + expect(layer0.layerIndex).toBe(0); + expect(layer0.diffID).toBe("sha256:base"); + expect(layer0.instruction).toBe("FROM alpine:3.19"); + expect(layer0.packages).toContain("libc@2.35-r1"); + expect(layer0.packages).toContain("curl@7.0.0-r0"); + expect(layer0.packages).not.toContain("nginx@1.24.0-r0"); + + const layer1 = result.entries[1]; + expect(layer1.layerIndex).toBe(1); + expect(layer1.diffID).toBe("sha256:nginx-layer"); + expect(layer1.instruction).toBe("RUN apk add nginx"); + expect(layer1.packages).toEqual(["nginx@1.24.0-r0"]); + + expect(result.pkgLayerMap.get("nginx@1.24.0-r0")).toEqual({ + layerIndex: 1, + diffID: "sha256:nginx-layer", + }); + }); + + it("skips layers that do not write the package DB", async () => { + const basePkgs = [{ name: "libc", version: "2.35-r1" }]; + const finalPkgs = [...basePkgs, { name: "nginx", version: "1.24.0-r0" }]; + + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + emptyLayer, // COPY or ENV instruction — no package DB + makeApkLayer(makeApkDb(...finalPkgs)), + ]; + const rootFsLayers = ["sha256:a", "sha256:b", "sha256:c"]; + const manifestLayers = ["sha256:a", "sha256:b", "sha256:c"]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(2); + expect(result.entries[0].layerIndex).toBe(0); + expect(result.entries[1].layerIndex).toBe(2); + expect(result.entries[1].packages).toEqual(["nginx@1.24.0-r0"]); + }); + + it("treats a layer with an empty DB file as clearing all packages", async () => { + // `emptyLayer` (no DB file at all) is a COPY/ENV layer — skipped entirely. + // `makeApkLayer("")` (DB file present but empty) means all packages were + // explicitly removed (e.g. `apk del $(apk info)`). It must be tracked. + const basePkgs = [{ name: "libc", version: "2.35-r1" }]; + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + makeApkLayer(""), // APK DB file exists but is empty — all packages deleted + ]; + const rootFsLayers = ["sha256:a", "sha256:b"]; + const manifestLayers = rootFsLayers; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(2); + expect(result.entries[0].packages).toContain("libc@2.35-r1"); + expect(result.entries[1].packages).toHaveLength(0); + expect(result.entries[1].removedPackages).toEqual(["libc@2.35-r1"]); + }); + + it("records a deletion in the layer where the package disappears", async () => { + // Layer 0: base image with curl + libc + // Layer 1: curl deleted (apk del curl) — DB rewritten without it + // Expected: curl attributed to layer 0; layer 1 has no new packages but + // records curl in removedPackages + const basePkgs = [ + { name: "curl", version: "7.0.0-r0" }, + { name: "libc", version: "2.35-r1" }, + ]; + const afterDeletionPkgs = [{ name: "libc", version: "2.35-r1" }]; + + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + makeApkLayer(makeApkDb(...afterDeletionPkgs)), + ]; + const rootFsLayers = ["sha256:base", "sha256:del-curl"]; + const manifestLayers = ["sha256:base", "sha256:del-curl"]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(2); + + expect(result.entries[0].layerIndex).toBe(0); + expect(result.entries[0].packages).toContain("curl@7.0.0-r0"); + expect(result.entries[0].packages).toContain("libc@2.35-r1"); + expect(result.entries[0].removedPackages).toBeUndefined(); + + expect(result.entries[1].layerIndex).toBe(1); + expect(result.entries[1].packages).toHaveLength(0); + expect(result.entries[1].removedPackages).toEqual(["curl@7.0.0-r0"]); + + // pkgLayerMap still records original attribution for both packages + expect(result.pkgLayerMap.get("curl@7.0.0-r0")).toEqual({ + layerIndex: 0, + diffID: "sha256:base", + }); + expect(result.pkgLayerMap.get("libc@2.35-r1")).toEqual({ + layerIndex: 0, + diffID: "sha256:base", + }); + }); + + it("re-attributes a package reinstalled after deletion", async () => { + // Layer 0: curl@7.0 + libc + // Layer 1: curl deleted → removedPackages: [curl@7.0] + // Layer 2: curl@8.0 reinstalled → packages: [curl@8.0] + const basePkgs = [ + { name: "curl", version: "7.0.0-r0" }, + { name: "libc", version: "2.35-r1" }, + ]; + const afterDeletionPkgs = [{ name: "libc", version: "2.35-r1" }]; + const afterReinstallPkgs = [ + { name: "libc", version: "2.35-r1" }, + { name: "curl", version: "8.0.0-r0" }, + ]; + + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + makeApkLayer(makeApkDb(...afterDeletionPkgs)), + makeApkLayer(makeApkDb(...afterReinstallPkgs)), + ]; + const rootFsLayers = ["sha256:base", "sha256:del", "sha256:reinstall"]; + const manifestLayers = rootFsLayers; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(3); + + expect(result.entries[0].layerIndex).toBe(0); + expect(result.entries[0].packages).toContain("curl@7.0.0-r0"); + expect(result.entries[0].packages).toContain("libc@2.35-r1"); + expect(result.entries[0].removedPackages).toBeUndefined(); + + expect(result.entries[1].layerIndex).toBe(1); + expect(result.entries[1].packages).toHaveLength(0); + expect(result.entries[1].removedPackages).toEqual(["curl@7.0.0-r0"]); + + expect(result.entries[2].layerIndex).toBe(2); + expect(result.entries[2].packages).toEqual(["curl@8.0.0-r0"]); + expect(result.entries[2].removedPackages).toBeUndefined(); + + expect(result.pkgLayerMap.get("curl@8.0.0-r0")).toEqual({ + layerIndex: 2, + diffID: "sha256:reinstall", + }); + expect(result.pkgLayerMap.get("curl@7.0.0-r0")).toEqual({ + layerIndex: 0, + diffID: "sha256:base", + }); + }); + + it("returns empty entries when no layer has a package DB", async () => { + const result = await computeLayerAttribution( + [emptyLayer, emptyLayer], + AnalysisType.Apk, + ["sha256:a", "sha256:b"], + ["sha256:a", "sha256:b"], + null, + image, + ); + + expect(result.entries).toHaveLength(0); + expect(result.pkgLayerMap.size).toBe(0); + }); + + it("caps iteration at rootFsLayers length when orderedLayers is longer", async () => { + const pkgs = [{ name: "libc", version: "2.35-r1" }]; + const orderedLayers = [ + makeApkLayer(makeApkDb(...pkgs)), + makeApkLayer(makeApkDb(...pkgs, { name: "extra", version: "1.0-r0" })), + ]; + const rootFsLayers = ["sha256:a"]; // only one — second layer should be ignored + const manifestLayers = ["sha256:a"]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + manifestLayers, + null, + image, + ); + + expect(result.entries).toHaveLength(1); + expect(result.entries[0].layerIndex).toBe(0); + expect(result.pkgLayerMap.has("extra@1.0-r0")).toBe(false); + }); + + it("omits instruction when history entry is absent", async () => { + const orderedLayers = [ + makeApkLayer(makeApkDb({ name: "curl", version: "7.0.0-r0" })), + ]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + ["sha256:aaa"], + ["sha256:aaa"], + null, + image, + ); + + expect(result.entries[0].instruction).toBeUndefined(); + }); + + it("skips empty_layer history entries when aligning with rootFsLayers", async () => { + const basePkgs = [{ name: "libc", version: "2.35-r1" }]; + const finalPkgs = [...basePkgs, { name: "curl", version: "7.0.0-r0" }]; + + const orderedLayers = [ + makeApkLayer(makeApkDb(...basePkgs)), + makeApkLayer(makeApkDb(...finalPkgs)), + ]; + const rootFsLayers = ["sha256:a", "sha256:b"]; + const history = [ + { created_by: "FROM alpine:3.19", empty_layer: false }, + { created_by: "ENV PATH=/usr/local/bin:$PATH", empty_layer: true }, + { created_by: "RUN apk add curl", empty_layer: false }, + ]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apk, + rootFsLayers, + ["sha256:a", "sha256:b"], + history, + image, + ); + + expect(result.entries[0].instruction).toBe("FROM alpine:3.19"); + expect(result.entries[1].instruction).toBe("RUN apk add curl"); + }); + }); + + describe("APT package manager", () => { + function makeAptLayer(dpkgContent: string): ExtractedLayers { + return { + "/var/lib/dpkg/status": { dpkg: dpkgContent }, + }; + } + + function makeDpkgStatus( + ...pkgs: Array<{ name: string; version: string }> + ): string { + return pkgs + .map( + (p) => + `Package: ${p.name}\nStatus: install ok installed\nVersion: ${p.version}\n`, + ) + .join("\n"); + } + + it("attributes packages to layers for APT package manager", async () => { + const basePkgs = [{ name: "libc6", version: "2.35-0ubuntu3" }]; + const allPkgs = [ + ...basePkgs, + { name: "nginx", version: "1.18.0-6ubuntu14" }, + ]; + + const orderedLayers = [ + makeAptLayer(makeDpkgStatus(...basePkgs)), + makeAptLayer(makeDpkgStatus(...allPkgs)), + ]; + + const result = await computeLayerAttribution( + orderedLayers, + AnalysisType.Apt, + ["sha256:base", "sha256:nginx-layer"], + ["sha256:base", "sha256:nginx-layer"], + null, + image, + ); + + expect(result.entries).toHaveLength(2); + expect(result.entries[0].packages).toContain("libc6@2.35-0ubuntu3"); + expect(result.entries[1].packages).toContain("nginx@1.18.0-6ubuntu14"); + }); + }); +}); + +describe("mergeLayerAttributionEntries", () => { + it("returns an empty array for no entries", () => { + expect(mergeLayerAttributionEntries([])).toEqual([]); + }); + + it("returns a single entry unchanged", () => { + const entry: LayerAttributionEntry = { + layerIndex: 0, + diffID: "sha256:aaa", + digest: "sha256:aaa-c", + instruction: "FROM alpine:3.19", + packages: ["libc@2.35-r1"], + }; + const result = mergeLayerAttributionEntries([entry]); + expect(result).toHaveLength(1); + expect(result[0]).toEqual(entry); + }); + + it("sorts entries by layerIndex", () => { + const entries: LayerAttributionEntry[] = [ + { layerIndex: 2, diffID: "sha256:c", packages: ["c@1.0"] }, + { layerIndex: 0, diffID: "sha256:a", packages: ["a@1.0"] }, + { layerIndex: 1, diffID: "sha256:b", packages: ["b@1.0"] }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result.map((e) => e.layerIndex)).toEqual([0, 1, 2]); + }); + + it("merges packages from two managers into the same layer entry", () => { + const entries: LayerAttributionEntry[] = [ + { layerIndex: 0, diffID: "sha256:a", packages: ["apt-pkg@1.0"] }, + { layerIndex: 0, diffID: "sha256:a", packages: ["chisel-pkg@2.0"] }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result).toHaveLength(1); + expect(result[0].packages).toContain("apt-pkg@1.0"); + expect(result[0].packages).toContain("chisel-pkg@2.0"); + }); + + it("merges removedPackages from two managers into the same layer entry", () => { + const entries: LayerAttributionEntry[] = [ + { + layerIndex: 1, + diffID: "sha256:b", + packages: [], + removedPackages: ["apt-pkg@1.0"], + }, + { + layerIndex: 1, + diffID: "sha256:b", + packages: [], + removedPackages: ["chisel-pkg@2.0"], + }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result).toHaveLength(1); + expect(result[0].removedPackages).toContain("apt-pkg@1.0"); + expect(result[0].removedPackages).toContain("chisel-pkg@2.0"); + }); + + it("handles a second entry with no removedPackages when first entry has some", () => { + const entries: LayerAttributionEntry[] = [ + { + layerIndex: 0, + diffID: "sha256:a", + packages: ["a@1.0"], + removedPackages: ["old@1.0"], + }, + { layerIndex: 0, diffID: "sha256:a", packages: ["b@1.0"] }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result).toHaveLength(1); + expect(result[0].removedPackages).toEqual(["old@1.0"]); + expect(result[0].packages).toContain("a@1.0"); + expect(result[0].packages).toContain("b@1.0"); + }); + + it("preserves layer metadata (diffID, digest, instruction) from the first entry", () => { + const entries: LayerAttributionEntry[] = [ + { + layerIndex: 0, + diffID: "sha256:aaa", + digest: "sha256:aaa-compressed", + instruction: "FROM ubuntu:22.04", + packages: ["libc6@2.35"], + }, + { layerIndex: 0, diffID: "sha256:aaa", packages: ["chisel-pkg@1.0"] }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result[0].digest).toBe("sha256:aaa-compressed"); + expect(result[0].instruction).toBe("FROM ubuntu:22.04"); + }); + + it("keeps entries for different layers independent", () => { + const entries: LayerAttributionEntry[] = [ + { layerIndex: 0, diffID: "sha256:a", packages: ["a@1.0"] }, + { layerIndex: 1, diffID: "sha256:b", packages: ["b@1.0"] }, + { layerIndex: 0, diffID: "sha256:a", packages: ["c@1.0"] }, + ]; + const result = mergeLayerAttributionEntries(entries); + expect(result).toHaveLength(2); + expect(result[0].layerIndex).toBe(0); + expect(result[0].packages).toEqual( + expect.arrayContaining(["a@1.0", "c@1.0"]), + ); + expect(result[1].layerIndex).toBe(1); + expect(result[1].packages).toEqual(["b@1.0"]); + }); +}); diff --git a/test/system/app-os/__snapshots__/globs.spec.ts.snap b/test/system/app-os/__snapshots__/globs.spec.ts.snap index 91cb9062c..fe4fe6c81 100644 --- a/test/system/app-os/__snapshots__/globs.spec.ts.snap +++ b/test/system/app-os/__snapshots__/globs.spec.ts.snap @@ -1928,7 +1928,7 @@ Object { }, Object { "data": Array [ - "sha256:114ca5b7280f3b49e94a67659890aadde83d58a8bde0d9020b2bc8c902c3b9de", + "sha256:756975cb9c7e7933d824af9319b512dd72a50894232761d06ef3be59981df838", ], "type": "imageLayers", }, diff --git a/test/system/docker.spec.ts b/test/system/docker.spec.ts index db62e0e03..1693d0bd9 100644 --- a/test/system/docker.spec.ts +++ b/test/system/docker.spec.ts @@ -1,15 +1,6 @@ -import * as crypto from "crypto"; -import { - createReadStream, - createWriteStream, - existsSync, - mkdirSync, - rmdirSync, - unlinkSync, -} from "fs"; +import { existsSync, mkdirSync, rmdirSync, unlinkSync } from "fs"; import * as os from "os"; import * as path from "path"; -import * as tar from "tar-stream"; import { Docker } from "../../lib/docker"; import { CmdOutput } from "../../lib/sub-process"; import * as subProcess from "../../lib/sub-process"; @@ -121,8 +112,6 @@ describe("docker", () => { const TEST_TARGET_IMAGE_DESTINATION = path.join(os.tmpdir(), "image.tar"); const docker = new Docker(); - let expectedChecksum; - let tempFilesToCleanup: string[] = []; beforeAll(async () => { const loadImage = path.join( @@ -130,8 +119,6 @@ describe("docker", () => { "../fixtures/docker-archives", "docker-save/hello-world.tar", ); - const normalizedLoadImage = await normalizeImageTar(loadImage); - expectedChecksum = await calculateImageSHA256(normalizedLoadImage); await subProcess.execute("docker", ["load", "--input", loadImage]); }); @@ -139,71 +126,8 @@ describe("docker", () => { if (existsSync(TEST_TARGET_IMAGE_DESTINATION)) { unlinkSync(TEST_TARGET_IMAGE_DESTINATION); } - for (const file of tempFilesToCleanup) { - if (existsSync(file)) { - unlinkSync(file); - } - } - tempFilesToCleanup = []; }); - async function calculateImageSHA256(tarFilePath: string): Promise { - return new Promise((resolve, reject) => { - const hash = crypto.createHash("sha256"); - const stream = createReadStream(tarFilePath); - - stream.on("data", (data) => { - hash.update(data); - }); - - stream.on("end", () => { - resolve(hash.digest("hex")); - }); - - stream.on("error", (err) => { - reject(err); - }); - }); - } - - async function normalizeImageTar(tarFilePath: string): Promise { - return new Promise((resolve, reject) => { - const extract = tar.extract(); - const pack = tar.pack(); - const tempFilePath = path.join( - os.tmpdir(), - `snyk-docker-plugin-test-${crypto.randomUUID()}.tar`, - ); - tempFilesToCleanup.push(tempFilePath); - const output = createWriteStream(tempFilePath); - extract.on("entry", (header, stream, next) => { - // Normalize the header - header.mtime = new Date(0); // Set modification time to the epoch - header.uid = 0; // Set user ID to 0 - header.gid = 0; // Set group ID to 0 - - // Add entry to the new tar file - const entry = pack.entry(header, next); - stream.pipe(entry); - }); - - extract.on("finish", () => { - pack.finalize(); - }); - - output.on("finish", () => { - resolve(tempFilePath); - }); - - extract.on("error", (err) => { - reject(err); - }); - - pack.pipe(output); - - createReadStream(tarFilePath).pipe(extract); - }); - } test("image saved to specified location", async () => { const targetImage = TEST_TARGET_IMAGE; const targetImageDestination = TEST_TARGET_IMAGE_DESTINATION; @@ -211,22 +135,16 @@ describe("docker", () => { await docker.save(targetImage, targetImageDestination); expect(existsSync(targetImageDestination)).toBeTruthy(); - const normalizedTargetImage = await normalizeImageTar( - targetImageDestination, - ); - - const checksum = await calculateImageSHA256(normalizedTargetImage); - expect(checksum).toEqual(expectedChecksum); }); test("promise rejects when image doesn't exist", async () => { - const image = "someImage:latest"; + const image = "nonexistent-image-for-test:latest"; const destination = "/tmp/image.tar"; const result = docker.save(image, destination); // rejects with expected error - await expect(result).rejects.toThrowError("server error"); + await expect(result).rejects.toThrowError("not found"); expect(existsSync(destination)).toBeFalsy(); }); diff --git a/test/system/operating-systems/__snapshots__/alpine3.7.spec.ts.snap b/test/system/operating-systems/__snapshots__/alpine3.7.spec.ts.snap index badce1000..08ef8205f 100644 --- a/test/system/operating-systems/__snapshots__/alpine3.7.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/alpine3.7.spec.ts.snap @@ -346,7 +346,7 @@ Object { }, Object { "data": Array [ - "sha256:3fc64803ca2de7279269048fe2b8b3c73d4536448c87c32375b2639ac168a48b", + "sha256:5d20c808ce198565ff70b3ed23a991dd49afac45dece63474b27ce6ed036adc6", ], "type": "imageLayers", }, @@ -771,7 +771,7 @@ Object { }, Object { "data": Array [ - "sha256:3fc64803ca2de7279269048fe2b8b3c73d4536448c87c32375b2639ac168a48b", + "sha256:5d20c808ce198565ff70b3ed23a991dd49afac45dece63474b27ce6ed036adc6", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/busybox1.32.spec.ts.snap b/test/system/operating-systems/__snapshots__/busybox1.32.spec.ts.snap index 1d957a10f..3b3ebef49 100644 --- a/test/system/operating-systems/__snapshots__/busybox1.32.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/busybox1.32.spec.ts.snap @@ -44,7 +44,7 @@ Object { }, Object { "data": Array [ - "sha256:e8e544649a797bb64853d4b70feaea366aedb53658277861b685dd0a3e76343a", + "sha256:5360e8f7191161ccd0b6ee792b5fe816481f0d5af19aca5c4116b593b7bc1f46", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap b/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap index cb2ef007b..61da2ed71 100644 --- a/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/centos6.spec.ts.snap @@ -3181,11 +3181,11 @@ Object { }, Object { "data": Array [ - "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9", - "sha256:282064e8c74ac642a58047db969497eeae247fb78e8e9c7c7cd0452b3063a11b", - "sha256:91d67088c5c5c582ba1740d680281e579a6b79a11a722a1099cedbd6eacc90b4", - "sha256:33080feb7575643f4ba24fe41c38d4bed2c9507fb54eb80f49a65ed068cc37be", - "sha256:2b7c38b09edb3ce271c3f5d11560644cb8d153917529c1d28a2818fc8cc0d7aa", + "sha256:ff50d722b38227ec8f2bbf0cdbce428b66745077c173d8117d91376128fa532e", + "sha256:84e0d1671716528b6f2cfa3c5117b59929e4493c9cc92f4028366303efa35c39", + "sha256:826574c5b389816ae84b73341ceff34764f1b11e014afad7abb1ff212ef31896", + "sha256:113f5299e5454fd22e73bedc486d6e0373666daf01bd803854f767f8649c2ee2", + "sha256:8d33272ddf389194f2812716f8617b947c2950dd0d8ae4a71c26319410ac77c7", ], "type": "imageLayers", }, @@ -6555,11 +6555,11 @@ Object { }, Object { "data": Array [ - "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9", - "sha256:282064e8c74ac642a58047db969497eeae247fb78e8e9c7c7cd0452b3063a11b", - "sha256:91d67088c5c5c582ba1740d680281e579a6b79a11a722a1099cedbd6eacc90b4", - "sha256:33080feb7575643f4ba24fe41c38d4bed2c9507fb54eb80f49a65ed068cc37be", - "sha256:2b7c38b09edb3ce271c3f5d11560644cb8d153917529c1d28a2818fc8cc0d7aa", + "sha256:ff50d722b38227ec8f2bbf0cdbce428b66745077c173d8117d91376128fa532e", + "sha256:84e0d1671716528b6f2cfa3c5117b59929e4493c9cc92f4028366303efa35c39", + "sha256:826574c5b389816ae84b73341ceff34764f1b11e014afad7abb1ff212ef31896", + "sha256:113f5299e5454fd22e73bedc486d6e0373666daf01bd803854f767f8649c2ee2", + "sha256:8d33272ddf389194f2812716f8617b947c2950dd0d8ae4a71c26319410ac77c7", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/centos7.spec.ts.snap b/test/system/operating-systems/__snapshots__/centos7.spec.ts.snap index ae927a856..1fb9a1c28 100644 --- a/test/system/operating-systems/__snapshots__/centos7.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/centos7.spec.ts.snap @@ -2397,7 +2397,7 @@ Object { }, Object { "data": Array [ - "sha256:fb82b029bea0a2a3b6a62a9c1e47e57fae2a82f629b2d1a346da4fc8fb53a0b6", + "sha256:9b4ebb48de8dbb85a3d9fbdec4d28a3e2e14912f27b7234c10c1658a05c320a5", ], "type": "imageLayers", }, @@ -4892,7 +4892,7 @@ Object { }, Object { "data": Array [ - "sha256:fb82b029bea0a2a3b6a62a9c1e47e57fae2a82f629b2d1a346da4fc8fb53a0b6", + "sha256:9b4ebb48de8dbb85a3d9fbdec4d28a3e2e14912f27b7234c10c1658a05c320a5", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/debian9.spec.ts.snap b/test/system/operating-systems/__snapshots__/debian9.spec.ts.snap index d9e163674..397192dc7 100644 --- a/test/system/operating-systems/__snapshots__/debian9.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/debian9.spec.ts.snap @@ -1696,7 +1696,7 @@ Object { }, Object { "data": Array [ - "sha256:b323b70996e4f6d603c331669ac44cf6234a2e22002d3686e9c88398c6911c25", + "sha256:4f250268ed6a0b777b9a3d9e0659754a8c97f28420f30cb78c184c3eead07d14", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/distroless.spec.ts.snap b/test/system/operating-systems/__snapshots__/distroless.spec.ts.snap index bc50d9aca..30a610bb3 100644 --- a/test/system/operating-systems/__snapshots__/distroless.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/distroless.spec.ts.snap @@ -157,8 +157,8 @@ Object { }, Object { "data": Array [ - "sha256:79d541cda6cb9a0c0e4aaa62aaea1f85b6b56544b5ad25e1e3369525ec0bf670", - "sha256:236f427c513a1d6f359c493154ef41bb0d768e0d0396599e0625a5f6fee476d7", + "sha256:b9cd0ea6c874f41c5c0ce7710de3f77e4c62988612c5e389b3b2b08ee356d8be", + "sha256:e2745900642c76d22dd0947e689689bba500cf937f75e3e335d1068221f31251", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/oraclelinux8.2.spec.ts.snap b/test/system/operating-systems/__snapshots__/oraclelinux8.2.spec.ts.snap index 4dd47b190..0d955cef2 100644 --- a/test/system/operating-systems/__snapshots__/oraclelinux8.2.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/oraclelinux8.2.spec.ts.snap @@ -2957,7 +2957,7 @@ Object { }, Object { "data": Array [ - "sha256:8f5b0a4c155316ae86167c983db7048fe2db2e9691093b35377eaf9d9b28ecc9", + "sha256:6c3a3ba2973ac77d6356db438301d73aff2750671490e8bdf69a77d550140d72", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/scratch.spec.ts.snap b/test/system/operating-systems/__snapshots__/scratch.spec.ts.snap index 6498b4ca6..6404e0613 100644 --- a/test/system/operating-systems/__snapshots__/scratch.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/scratch.spec.ts.snap @@ -44,7 +44,7 @@ Object { }, Object { "data": Array [ - "sha256:030010b308e3e918de7f8e174a741c7e2f780e37e83fb529c27bcfbaceebfe45", + "sha256:55be39592db47a8e28bcccf7301f9dd34689c64f62a6d91715aca35d2c10fa3a", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/sles15.spec.ts.snap b/test/system/operating-systems/__snapshots__/sles15.spec.ts.snap index abc608c12..c1e76c9d3 100644 --- a/test/system/operating-systems/__snapshots__/sles15.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/sles15.spec.ts.snap @@ -2109,7 +2109,7 @@ Object { }, Object { "data": Array [ - "sha256:fc378ab30d60a2ec107ad2a83226050b29ca6dca2d93cd6220e902375bcf3813", + "sha256:67e592d59ffe1b47aa2eddc186fa103b39552366c59fd4cecbd528e1ae9163cd", ], "type": "imageLayers", }, @@ -4320,7 +4320,7 @@ Object { }, Object { "data": Array [ - "sha256:0f350a1ede685e83e2220dd2ad17b7b1247cfa80eb6a9f0bbfed83d1e544ed45", + "sha256:57bc8f9e21de243ded5af46c274b1fe18d5a72b7f7acd1cc3bb24f69f17b78f8", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap b/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap index c4fb7421d..eabcdb7f6 100644 --- a/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/ubi8.spec.ts.snap @@ -6035,11 +6035,11 @@ Object { }, Object { "data": Array [ - "sha256:76e525e1b16d0a56473d529790d3f1696078799abf41357103298a09f744f0fe", - "sha256:7a1f8cd94aa1e8ef907cb7bb71cb968cb907a283947aa28440159f1564bb12b0", - "sha256:47cf2e444f5a66d9e1b16a55aca8880d5a3329f4e0ed3976b0ea625471116b70", - "sha256:9d4685268f61f7e07d355dba7e314ea6b596fe5c371863d9860861db0d282d94", - "sha256:06d2bfb4c255ce6202421662c358af167a7dfb1dd4b63278c73ddae4ebae007f", + "sha256:a905c078265c2d3a1f959f8ca1d79b30453a16aed4de47dec5ed1c048b26a103", + "sha256:121d5409a427be35e19a57dbaa3d29e62baaf98a5f4296288b646aeb282fd554", + "sha256:a50500db3d1b335828f1a31bf32652ecf422beb7f12bd28f5ab052af90b81428", + "sha256:97b9bf14b9b756916dfa15569caa9183ae65a53d9f438356d2ba9540b64f9f78", + "sha256:01561dd6d6562de4cce2ee7e7ec077227b0133305d6f2b41fa1793ad9044e572", ], "type": "imageLayers", }, @@ -19046,8 +19046,8 @@ Object { }, Object { "data": Array [ - "sha256:ccf04fbd6e1943f648d1c2980e96038edc02b543c597556098ab2bcaa4fd1fa8", - "sha256:b7b591e3443f17f9d8272b8d118b6c031ca826deb09d4b44f296ba934f1b6e57", + "sha256:ec1681b6a383e4ecedbeddd5abc596f3de835aed6db39a735f62395c8edbff30", + "sha256:c4d668e229cd131e0a8e4f8218dca628d9cf9697572875e355fe4b247b6aa9f0", ], "type": "imageLayers", }, diff --git a/test/system/operating-systems/__snapshots__/ubi9.spec.ts.snap b/test/system/operating-systems/__snapshots__/ubi9.spec.ts.snap index 8b9f6651d..f361c755c 100644 --- a/test/system/operating-systems/__snapshots__/ubi9.spec.ts.snap +++ b/test/system/operating-systems/__snapshots__/ubi9.spec.ts.snap @@ -365,7 +365,7 @@ Object { }, Object { "data": Array [ - "sha256:f6e375c0d5ad6710500dc4fe0f0e6c5c43c65465c50ac3ce5c152cc9627f5134", + "sha256:82e56b4fb992cf3a0171fb51b07ac179b0a5ea5ad3ef058f33d333cac1e0acd3", ], "type": "imageLayers", }, diff --git a/test/system/package-managers/__snapshots__/apk.spec.ts.snap b/test/system/package-managers/__snapshots__/apk.spec.ts.snap index 545d7cc46..ad45c642e 100644 --- a/test/system/package-managers/__snapshots__/apk.spec.ts.snap +++ b/test/system/package-managers/__snapshots__/apk.spec.ts.snap @@ -364,7 +364,7 @@ Object { }, Object { "data": Array [ - "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", + "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c", ], "type": "imageLayers", }, @@ -1202,17 +1202,17 @@ Object { }, Object { "data": Array [ - "sha256:62c4934e5eb385ee358a5be9db09d577da7b69f693630964b6d4951f45fd48f6", - "sha256:a5f6da52dd2b6a924287e3c2005bf2e825c0e95226332fcba63bbc188392ac2d", - "sha256:4f399afdd814d166d5cb3c2d4ca4c3753047ecc5f5a5ba9109c95f6ce6831481", - "sha256:0630b0839bcfcd0808ffc4f92c9ef660b58710eed55492dd04fe98aea4639f9f", - "sha256:cf85318006d0c750f3336ef6c8cdb997bc36bb5a654e78b499e61485a0eb4b2d", - "sha256:a698ccf181e248fad4715fb089927614853e32b41b5c8f734d549aea626eaebd", - "sha256:3974f70b2b33dcaf636e092ec8b3793f99482acafa9199ccde34de03c334a7d5", - "sha256:c20be7ba028069931ca5cfd8760cfba74f63ac8154fb9e1ac91cc2c603747961", - "sha256:e082dc465d6818b184733427de33e9a8ef668db3dedca267da84edf9a371fc3d", - "sha256:eda34620677dd351ef90e4fa8f6351252759d9cdd328497c9230132b8cc628b5", - "sha256:0cbb60e4dc94da630bcf48f42035626de5235dee85d0a154513ecf0271ef2cfd", + "sha256:57d32fd75729e1b177c3c7264caab5b0fec4522243a6703b9868abe0504dcbe6", + "sha256:8be87bfc8298139041b58d538f353c1168dbd8177ea0f615aca6882717b6858a", + "sha256:71ab714698cc723e7f156b3f8bc2d9ac7929dd9163760c376b026e00698b24b0", + "sha256:d50eaf0c0521d665b222fef18dfbd459e3da4b69573d931c7a25cc833cf36261", + "sha256:ce380be41cd0e6a3416220a5d17ee4c6074fb04e008039649dd0249a6cb7d272", + "sha256:ea43bad4a7680ded1753d924b4bfa0a30d927d94f10870a8ec42e7b120ea8f4c", + "sha256:b54115c7ceb292b133fc4f7c8d4af3b9bab5e2409d2c807781cae0066894a540", + "sha256:64aea272e7402c2b6d8a674f9239949f33fabf96db02d771a233fda3bb2624a7", + "sha256:d6a08dee0508a79f468b8cace027815ab051e8533251b8f958947136b2399598", + "sha256:154340e32930149af1da2d413a2b1951479dde8b146a2996dbdf66965f081c47", + "sha256:36405d9ceadaa54dc799328413dac44fd61a7100a348e70f1fd40bae519898c9", ], "type": "imageLayers", }, diff --git a/test/system/package-managers/__snapshots__/deb.spec.ts.snap b/test/system/package-managers/__snapshots__/deb.spec.ts.snap index bc7c9113c..a0e6bda89 100644 --- a/test/system/package-managers/__snapshots__/deb.spec.ts.snap +++ b/test/system/package-managers/__snapshots__/deb.spec.ts.snap @@ -1810,7 +1810,7 @@ Object { }, Object { "data": Array [ - "sha256:69215f34c115201c526c8a6552b88e87d18e24d5387322154838f64ed6029683", + "sha256:696098ac4087a4218b99a8bfac2f54d56ebc4f4ecaa69c5174623a8292a942ae", ], "type": "imageLayers", }, diff --git a/test/system/platforms/__snapshots__/amd.spec.ts.snap b/test/system/platforms/__snapshots__/amd.spec.ts.snap index 673dbd48d..acdb8022a 100644 --- a/test/system/platforms/__snapshots__/amd.spec.ts.snap +++ b/test/system/platforms/__snapshots__/amd.spec.ts.snap @@ -1842,12 +1842,12 @@ Object { }, Object { "data": Array [ - "sha256:d0fe97fa8b8cefdffcef1d62b65aba51a6c87b6679628a2b50fc6a7a579f764c", - "sha256:832f21763c8e6b070314e619ebb9ba62f815580da6d0eaec8a1b080bd01575f7", - "sha256:223b15010c47044b6bab9611c7a322e8da7660a8268949e18edde9c6e3ea3700", - "sha256:a1a4c1d0c191cee2ac3912696fe57cf9ad84cbfaf3d13f314061bd89fe73cabb", - "sha256:e9acf707b7fe3eef2f7c6bd127b3b34b262889cb670013adfb46883a49a4ac8e", - "sha256:b854b24feeee6cbd8fc5d6401f5f1b4a3d40c483444e6c4b06a2b7b31f347b40", + "sha256:bb79b6b2107fea8e8a47133a660b78e3a546998fcf0427be39ac9a0af4a97e90", + "sha256:1ed3521a5dcbd05214eb7f35b952ecf018d5a6610c32ba4e315028c556f45e94", + "sha256:5999b99cee8f2875d391d64df20b6296b63f23951a7d41749f028375e887cd05", + "sha256:f99a38f44786b9434af817ff56fd90f9d75238546317c49c46b3c9759ccba941", + "sha256:d6fc863042e2c63ac7bdca3ce8d5b88ef6d96ab1a827bad3ff97c4dedd2e4dd5", + "sha256:9bd1af4eae13aa3ef99901fcb1fb86ee8b778f0ead82a6aac53573b0b236568b", ], "type": "imageLayers", }, @@ -3896,12 +3896,12 @@ Object { }, Object { "data": Array [ - "sha256:d0fe97fa8b8cefdffcef1d62b65aba51a6c87b6679628a2b50fc6a7a579f764c", - "sha256:832f21763c8e6b070314e619ebb9ba62f815580da6d0eaec8a1b080bd01575f7", - "sha256:223b15010c47044b6bab9611c7a322e8da7660a8268949e18edde9c6e3ea3700", - "sha256:a1a4c1d0c191cee2ac3912696fe57cf9ad84cbfaf3d13f314061bd89fe73cabb", - "sha256:e9acf707b7fe3eef2f7c6bd127b3b34b262889cb670013adfb46883a49a4ac8e", - "sha256:b854b24feeee6cbd8fc5d6401f5f1b4a3d40c483444e6c4b06a2b7b31f347b40", + "sha256:bb79b6b2107fea8e8a47133a660b78e3a546998fcf0427be39ac9a0af4a97e90", + "sha256:1ed3521a5dcbd05214eb7f35b952ecf018d5a6610c32ba4e315028c556f45e94", + "sha256:5999b99cee8f2875d391d64df20b6296b63f23951a7d41749f028375e887cd05", + "sha256:f99a38f44786b9434af817ff56fd90f9d75238546317c49c46b3c9759ccba941", + "sha256:d6fc863042e2c63ac7bdca3ce8d5b88ef6d96ab1a827bad3ff97c4dedd2e4dd5", + "sha256:9bd1af4eae13aa3ef99901fcb1fb86ee8b778f0ead82a6aac53573b0b236568b", ], "type": "imageLayers", }, diff --git a/test/system/platforms/__snapshots__/arm.spec.ts.snap b/test/system/platforms/__snapshots__/arm.spec.ts.snap index 7dbb89527..9382eb770 100644 --- a/test/system/platforms/__snapshots__/arm.spec.ts.snap +++ b/test/system/platforms/__snapshots__/arm.spec.ts.snap @@ -1034,11 +1034,11 @@ Object { }, Object { "data": Array [ - "sha256:e2f13739ad415e6f8d9f73253910e4984563a1ec98bd0e0af715fc2c74dfe84b", - "sha256:00e7d2cc2a5924431554f4bf5f0661d4c10e2b83b7cc7b3ae49273a219b16f45", - "sha256:bc622f3d17f980364ca082b5d9a29bb42d67c95422090b3c42f63a2361e9e5da", - "sha256:badb783a0ee342fd46af0cc96b7fcc11d210270e5d6ee042d7ce0d43c32d2b3e", - "sha256:f7c06e5938e23ff2912b3147d72ed0d2496d260e24a9b1de2c0b479a5b0e4b79", + "sha256:b538f80385f9b48122e3da068c932a96ea5018afa3c7be79da00437414bd18cd", + "sha256:66942047c7b3b1e4cfa170855e791fffb10e70e4061f5d20cdf3d712b1d9ba79", + "sha256:03513b8933dacfb95cb1d00fc4846b0c28686aba484b00ef5409ae368e83c922", + "sha256:d81823b5c9b536108c620f3c7b371517e997a25caa8495ef8cb040a17a1eb8be", + "sha256:86ade662cda571f878ef3c7f9a69bb8649b60974fa25680948cad5ab16cac36b", ], "type": "imageLayers", }, diff --git a/test/system/platforms/__snapshots__/ppc64le.spec.ts.snap b/test/system/platforms/__snapshots__/ppc64le.spec.ts.snap index 4692307ab..e31a077cf 100644 --- a/test/system/platforms/__snapshots__/ppc64le.spec.ts.snap +++ b/test/system/platforms/__snapshots__/ppc64le.spec.ts.snap @@ -775,16 +775,16 @@ Object { }, Object { "data": Array [ - "sha256:5a8b4ad4d2fdb56c18f5b1e532f17ef33e6404c83dc1581a3adddf99dcd9d51c", - "sha256:76daf675c598f6bb12a85310d25f6cc070e3c4fd460b4b396ba9ee1dc52356ef", - "sha256:cb0f674aa825b8a225b4f2c34828aa63cf0f5d1a7440f2dc68c9d3549c03fad8", - "sha256:0c9a3fd39ef3b7c4aba9a5f2ac9f91f060f312f47e0dccc4614b4c9ce96213f3", - "sha256:8587af6bdad52b7756699d14d8f55b340c5fe3a0b0c0c75636f60c6b86ab0856", - "sha256:1a514508a2a4053c7156d393288e658201f4a9526423cac2ab5c48d23cd46546", - "sha256:78838c1f02b21ed98005126845be133aa3e1c8921599714c5b7950c3d1630e02", - "sha256:9f334011825722d45a7811645310ae610f85369ac89b1133bdf2bc558f499cd5", - "sha256:0a43bfef3e6f9c1725b5049f5a4ada18329f4e3ebb6d0105aaab2995b34b0cb9", - "sha256:7786a29a020c1d1a81c931d2cdf4ae73bab1227b66ccf08c0ebbc3eadfbdde30", + "sha256:5077f8601dceb5744d875d7740ebc203f674b108a0188f3a31e292b21a4bee64", + "sha256:46f98eb9049c4fcbaa1cf16ddb44aaf910a89a5471dc303198251fa25bc2a1ef", + "sha256:e50cdb8e03fc1b19217cd9f80b86142d090ad3e818795b339ea4d9ef9b74555f", + "sha256:8b4466e085b12b265fb47397c7c105fd7d78a5883beef49ab9f386393c4c3dae", + "sha256:3da6cf5673f6c68a98cb0fff683ae97cadd61fc243467e67f8b7ce6f431da154", + "sha256:82fa271059acb56e3b8e98a6da57f60c4221ed049884ea76885b168864cd084a", + "sha256:056a460c2e9c1813b18c006e67241dd2aab28d6163009bc2dc6707415c70ee55", + "sha256:fdfa909abe06743a2e2e8472539d26f7169a54a08f5808ffbf60499893e0ca29", + "sha256:821c123f6ebfd3e30fd422ad888483dfe8b325dc85556e655af53a6fb389762f", + "sha256:05f53b369b658f1bdcfb30c89c3fef3d68db543600596375f862fbcf3c083557", ], "type": "imageLayers", }, @@ -1851,16 +1851,16 @@ Object { }, Object { "data": Array [ - "sha256:5a8b4ad4d2fdb56c18f5b1e532f17ef33e6404c83dc1581a3adddf99dcd9d51c", - "sha256:76daf675c598f6bb12a85310d25f6cc070e3c4fd460b4b396ba9ee1dc52356ef", - "sha256:cb0f674aa825b8a225b4f2c34828aa63cf0f5d1a7440f2dc68c9d3549c03fad8", - "sha256:0c9a3fd39ef3b7c4aba9a5f2ac9f91f060f312f47e0dccc4614b4c9ce96213f3", - "sha256:8587af6bdad52b7756699d14d8f55b340c5fe3a0b0c0c75636f60c6b86ab0856", - "sha256:1a514508a2a4053c7156d393288e658201f4a9526423cac2ab5c48d23cd46546", - "sha256:78838c1f02b21ed98005126845be133aa3e1c8921599714c5b7950c3d1630e02", - "sha256:9f334011825722d45a7811645310ae610f85369ac89b1133bdf2bc558f499cd5", - "sha256:0a43bfef3e6f9c1725b5049f5a4ada18329f4e3ebb6d0105aaab2995b34b0cb9", - "sha256:7786a29a020c1d1a81c931d2cdf4ae73bab1227b66ccf08c0ebbc3eadfbdde30", + "sha256:5077f8601dceb5744d875d7740ebc203f674b108a0188f3a31e292b21a4bee64", + "sha256:46f98eb9049c4fcbaa1cf16ddb44aaf910a89a5471dc303198251fa25bc2a1ef", + "sha256:e50cdb8e03fc1b19217cd9f80b86142d090ad3e818795b339ea4d9ef9b74555f", + "sha256:8b4466e085b12b265fb47397c7c105fd7d78a5883beef49ab9f386393c4c3dae", + "sha256:3da6cf5673f6c68a98cb0fff683ae97cadd61fc243467e67f8b7ce6f431da154", + "sha256:82fa271059acb56e3b8e98a6da57f60c4221ed049884ea76885b168864cd084a", + "sha256:056a460c2e9c1813b18c006e67241dd2aab28d6163009bc2dc6707415c70ee55", + "sha256:fdfa909abe06743a2e2e8472539d26f7169a54a08f5808ffbf60499893e0ca29", + "sha256:821c123f6ebfd3e30fd422ad888483dfe8b325dc85556e655af53a6fb389762f", + "sha256:05f53b369b658f1bdcfb30c89c3fef3d68db543600596375f862fbcf3c083557", ], "type": "imageLayers", }, diff --git a/test/system/plugin.spec.ts b/test/system/plugin.spec.ts index fc43891bb..c21c880a9 100644 --- a/test/system/plugin.spec.ts +++ b/test/system/plugin.spec.ts @@ -178,11 +178,11 @@ describe("plugin", () => { // layers are read correctly expect(imageLayers).toEqual([ - "sha256:13cb14c2acd34e45446a50af25cb05095a17624678dbafbcc9e26086547c1d74", - "sha256:d4cf327d8ef50eb2e31b646f17217a3baf455391bfd59bce47df50c770ff8c07", - "sha256:7c7d7f4461826dd22f9234a81f9bed9c0bdb0b70b3ce66228bfc87418a9b8313", - "sha256:9040af41bb6677b114134de15ddeb10b070eb1f940dbbe277574ee154d89f6b9", - "sha256:f978b9ed3f26a49b55cf4849e4cadb29335be45a633cbe95a2f4e445e70086bf", + "sha256:8559a31e96f442f2c7b6da49d6c84705f98a39d8be10b3f5f14821d0ee8417df", + "sha256:8d69e59170f7dac013ef436408ed9ddc688dd9ad3bc030bd868add55a77e25f8", + "sha256:3f9f1ec1d262b2889a5fc19bf295f48346dbd8238e22f3eb3dd8a907ca002372", + "sha256:d1f5ff4f210df5d5f6bf48438d33ba0d086c4e08a803acf22292ccd4ede92bd2", + "sha256:1e22bfa8652e0db3a316e2c946ea048b60560630d4faa58405da4c5fcdb3ff07", ]); // correct platform detected