diff --git a/lib/analyzer/image-inspector.ts b/lib/analyzer/image-inspector.ts index ec4a3f232..8f7f5f3c7 100644 --- a/lib/analyzer/image-inspector.ts +++ b/lib/analyzer/image-inspector.ts @@ -5,6 +5,7 @@ import * as path from "path"; import { Docker, DockerOptions } from "../docker"; import { ImageName } from "../extractor/image"; +import { parseImageReference } from "../image-reference"; import type { DockerPullResult } from "@snyk/snyk-docker-pull"; import type { @@ -265,102 +266,15 @@ async function getImageArchive( } } -function isImagePartOfURL(targetImage): boolean { - // Based on the Docker spec, if the image contains a hostname, then the hostname should contain - // a `.` or `:` before the first instance of a `/`. ref: https://stackoverflow.com/a/37867949 - if (!targetImage.includes("/")) { - return false; - } - - const partBeforeFirstForwardSlash = targetImage.split("/")[0]; - - return ( - partBeforeFirstForwardSlash.includes(".") || - partBeforeFirstForwardSlash.includes(":") || - partBeforeFirstForwardSlash === "localhost" - ); -} - -function extractHostnameFromTargetImage(targetImage: string): { - hostname: string; - remainder: string; -} { - // We need to detect if the `targetImage` is part of a URL. If not, the default hostname will be - // used (registry-1.docker.io). ref: https://stackoverflow.com/a/37867949 - const defaultHostname = "registry-1.docker.io"; - - if (!isImagePartOfURL(targetImage)) { - return { hostname: defaultHostname, remainder: targetImage }; - } - - const dockerFriendlyRegistryHostname = "docker.io/"; - if (targetImage.startsWith(dockerFriendlyRegistryHostname)) { - return { - hostname: defaultHostname, - remainder: targetImage.substring(dockerFriendlyRegistryHostname.length), - }; - } - - const i = targetImage.indexOf("/"); - return { - hostname: targetImage.substring(0, i), - remainder: targetImage.substring(i + 1), - }; -} - -function extractImageNameAndTag( - remainder: string, - targetImage: string, -): { imageName: string; tag: string } { - const defaultTag = "latest"; - - if (!remainder.includes("@")) { - const [imageName, tag] = remainder.split(":"); - - return { - imageName: appendDefaultRepoPrefixIfRequired(imageName, targetImage), - tag: tag || defaultTag, - }; - } - - const [imageName, tag] = remainder.split("@"); - +function extractImageDetails(targetImage: string): ImageDetails { + const parsed = parseImageReference(targetImage); return { - imageName: appendDefaultRepoPrefixIfRequired( - dropTagIfSHAIsPresent(imageName), - targetImage, - ), - tag: tag || defaultTag, + hostname: parsed.registryForPull, + imageName: parsed.normalizedRepository, + tag: parsed.tailReferenceForPull, }; } -function appendDefaultRepoPrefixIfRequired( - imageName: string, - targetImage: string, -): string { - const defaultRepoPrefix = "library/"; - - if (isImagePartOfURL(targetImage) || imageName.includes("/")) { - return imageName; - } - - return defaultRepoPrefix + imageName; -} - -function dropTagIfSHAIsPresent(imageName: string): string { - if (!imageName.includes(":")) { - return imageName; - } - - return imageName.split(":")[0]; -} - -function extractImageDetails(targetImage: string): ImageDetails { - const { hostname, remainder } = extractHostnameFromTargetImage(targetImage); - const { imageName, tag } = extractImageNameAndTag(remainder, targetImage); - return { hostname, imageName, tag }; -} - function isLocalImageSameArchitecture( platformOption: string, inspectResultArchitecture: string, diff --git a/lib/dependency-tree/index.ts b/lib/dependency-tree/index.ts index 53e95f132..1a5f5a096 100644 --- a/lib/dependency-tree/index.ts +++ b/lib/dependency-tree/index.ts @@ -1,13 +1,31 @@ import { AnalyzedPackageWithVersion, OSRelease } from "../analyzer/types"; +import { parseImageReference } from "../image-reference"; import { DepTree, DepTreeDep } from "../types"; -/** @deprecated Should implement a new function to build a dependency graph instead. */ -export function buildTree( - targetImage: string, - packageFormat: string, - depInfosList: AnalyzedPackageWithVersion[], - targetOS: OSRelease, -): DepTree { +export function nameAndVersionFromTargetImage(targetImage: string): { + name: string; + version: string; +} { + let imageName: string; + let imageVersion: string; + + // TODO: this logic is not sufficient for OCI image references that end + // in .tar (e.g. image:version.tar), which is a valid image name. + // Additionally, the fallback parsing does not fully capture the information + // availble in a valid archive name. + // ref: https://github.com/containers/container-libs/blob/main/image/docs/containers-transports.5.md#containers-storagestorage-specifierimage-iddocker-referenceimage-id + if (!targetImage.endsWith(".tar")) { + try { + const parsed = parseImageReference(targetImage); + return { + name: parsed.fullName, + version: parsed.tag ? parsed.tag : parsed.digest ? "" : "latest", + }; + } catch { + // Fallback to manual parsing + } + } + // A tag can only occur in the last section of a docker image name, so // check any colon separator after the final '/'. If there are no '/', // which is common when using Docker's official images such as @@ -18,8 +36,8 @@ export function buildTree( targetImage.includes(":"); // Defaults for simple images from dockerhub, like "node" or "centos" - let imageName = targetImage; - let imageVersion = "latest"; + imageName = targetImage; + imageVersion = "latest"; // If we have a version, split on the last ':' to avoid the optional // port on a hostname (i.e. localhost:5000) @@ -40,6 +58,19 @@ export function buildTree( imageVersion = ""; } + return { name: imageName, version: imageVersion }; +} + +/** @deprecated Should implement a new function to build a dependency graph instead. */ +export function buildTree( + targetImage: string, + packageFormat: string, + depInfosList: AnalyzedPackageWithVersion[], + targetOS: OSRelease, +): DepTree { + const { name: imageName, version: imageVersion } = + nameAndVersionFromTargetImage(targetImage); + const root: DepTree = { // don't use the real image name to avoid scanning it as an issue name: "docker-image|" + imageName, diff --git a/lib/extractor/image.ts b/lib/extractor/image.ts index b454b4870..6fcf01e96 100644 --- a/lib/extractor/image.ts +++ b/lib/extractor/image.ts @@ -1,3 +1,4 @@ +import { parseImageReference } from "../image-reference"; import { PluginOptions } from "../types"; export { ImageName, ImageDigest, getImageNames }; @@ -10,50 +11,14 @@ class ImageName { targetImage: string, digests: { manifest?: string; index?: string } = {}, ) { - // this regex has been copied from - // https://github.com/distribution/distribution/blob/fb2188868d771aa27e5781a32bf78d4c113c18a6/reference/regexp.go#L101 - // (code has been modified to print the regex), and then adjusted to - // Javascript. The required modifications were replacing `[:xdigit:]` with - // `[a-fA-F0-9]` and escaping the slashes. - // Note that the digest matched in this Regex will match digests that have - // uppercase-letters, while the regex used in validateDigest does NOT match - // uppercase-letters. This simply matches the behaviour from the upstream - // `reference` and `go-digest `packages. - // - // we're matching pattern: (optional)/(mandatory):(optional)@(optional) - // This Regex contains three capture groups: - // 1) The repository / image name - // 2) tag - // 3) digest - const re = - /^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+)?(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?(?:(?:\/[a-z0-9]+(?:(?:(?:[._]|__|[-]*)[a-z0-9]+)+)?)+)?)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][A-Fa-f0-9]{32,}))?$/gi; - - const groups = re.exec(targetImage); - if (groups === null) { - if (targetImage === "") { - throw new Error("image name is empty"); - } - if (re.exec(targetImage.toLowerCase()) !== null) { - throw new Error("image repository contains uppercase letter"); - } - throw new Error("invalid image reference format"); - } - - const parsedGroups = { - name: groups[1], - tag: groups[2], - digest: groups[3], - }; - - this.name = parsedGroups.name; - - const NameTotalLengthMax = 255; - if (this.name.length > NameTotalLengthMax) { - throw new Error("image repository name is more than 255 characters"); - } + const parsed = parseImageReference(targetImage); + this.name = parsed.registry + ? parsed.registry + "/" + parsed.repository + : parsed.repository; - this.tag = - parsedGroups.tag || parsedGroups.digest ? parsedGroups.tag : "latest"; + // If the image name has a tag, use it. If the image name has + // nether a tag nor a digest, use "latest". + this.tag = parsed.tag || parsed.digest ? parsed.tag : "latest"; this.digests = {}; if (digests.index) { @@ -63,11 +28,13 @@ class ImageName { this.digests.manifest = new ImageDigest(digests.manifest); } - if (parsedGroups.digest) { - const digest = new ImageDigest(parsedGroups.digest); - if (this.digests.manifest !== digest && this.digests.index !== digest) { - this.digests.unknown = digest; - } + // If the image name has a digest, and it's not the same as the manifest or index digest, add it as the unknown digest. + if ( + parsed.digest && + !this.digests.manifest?.equals(parsed.digest) && + !this.digests.index?.equals(parsed.digest) + ) { + this.digests.unknown = new ImageDigest(parsed.digest); } } @@ -131,6 +98,13 @@ class ImageDigest { public toString(): string { return this.alg + ":" + this.hex; } + + public equals(other: ImageDigest | string): boolean { + if (typeof other === "string") { + return this.toString() === other; + } + return this.alg === other.alg && this.hex === other.hex; + } } function getImageNames( diff --git a/lib/extractor/oci-distribution-metadata.ts b/lib/extractor/oci-distribution-metadata.ts index 1dc5c35b0..4f43865be 100644 --- a/lib/extractor/oci-distribution-metadata.ts +++ b/lib/extractor/oci-distribution-metadata.ts @@ -1,4 +1,4 @@ -import { parseAll } from "@swimlane/docker-reference"; +import { isValidDigest, parseImageReference } from "../image-reference"; export interface OCIDistributionMetadata { // Must be a valid host, including port if one was used to pull the image. @@ -32,17 +32,16 @@ export function constructOCIDisributionMetadata({ | OCIDistributionMetadata | undefined { try { - const ref = parseAll(imageName); - if (!ref.domain || !ref.repository) { - return; - } - + const parsed = parseImageReference(imageName); + // Extract the registry hostname, using "docker.io" as the default for Docker Hub images. + // Note this is different from registryForPull, which defaults to "registry-1.docker.io" for Docker Hub images. + const hostname = parsed.registry ? parsed.registry : "docker.io"; const metadata: OCIDistributionMetadata = { - registryHost: ref.domain, - repository: ref.repository, + registryHost: hostname, + repository: parsed.normalizedRepository, manifestDigest, indexDigest, - imageTag: ref.tag, + imageTag: parsed.tag, }; if (!ociDistributionMetadataIsValid(metadata)) { @@ -65,40 +64,17 @@ function ociDistributionMetadataIsValid( // 2048 byte limit is enforced by Snyk for platform stability. // Longer strings may be valid, but nothing close to this limit has been observed by Snyk at time of writing. - if ( - Buffer.byteLength(data.repository) > 2048 || - !repositoryNameIsValid(data.repository) - ) { + if (Buffer.byteLength(data.repository) > 2048) { return false; } - if (!digestIsValid(data.manifestDigest)) { + if (!isValidDigest(data.manifestDigest)) { return false; } - if (data.indexDigest && !digestIsValid(data.indexDigest)) { - return false; - } - - if (data.imageTag && !tagIsValid(data.imageTag)) { + if (data.indexDigest && !isValidDigest(data.indexDigest)) { return false; } return true; } - -// Regular Expression Source: OCI Distribution Spec V1 -// https://github.com/opencontainers/distribution-spec/blob/570d0262abe8ec5e59d8e3fbbd7be4bd784b200e/spec.md?plain=1#L141 -const repositoryNameIsValid = (name: string) => - /^[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*(\/[a-z0-9]+((\.|_|__|-+)[a-z0-9]+)*)*$/.test( - name, - ); - -// Regular Expression Source: OCI Image Spec V1 -// https://github.com/opencontainers/image-spec/blob/d60099175f88c47cd379c4738d158884749ed235/descriptor.md?plain=1#L143 -const digestIsValid = (digest: string) => /^sha256:[a-f0-9]{64}$/.test(digest); - -// Regular Expression Source: OCI Image Spec V1 -// https://github.com/opencontainers/distribution-spec/blob/3940529fe6c0a068290b27fb3cd797cf0528bed6/spec.md?plain=1#L160 -const tagIsValid = (tag: string) => - /^[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}$/.test(tag); diff --git a/lib/image-reference.ts b/lib/image-reference.ts new file mode 100644 index 000000000..004283689 --- /dev/null +++ b/lib/image-reference.ts @@ -0,0 +1,195 @@ +/** + * Centralized image reference parsing for OCI image names. + * Uses OCI distribution-style regexes to parse name, registry, tag, and digest. + */ + +// Full reference: optional registry, repository path, optional tag, optional digest. +// Capture groups: 1 = name (repo path including optional registry), 2 = tag, 3 = digest. +const imageReferencePattern = String.raw`^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][a-fA-F0-9]{32,}))?$`; +const imageReferenceRegex = new RegExp(imageReferencePattern); + +// Registry prefix only. Requires '.' or localhost to distinguish registry from repository. +// Capture group 1 = registry hostname (no trailing '/' or '@'). +const imageRegistryPattern = String.raw`^((?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))+|\[(?:[a-fA-F0-9:]+)\]|localhost)(?::[0-9]+)?)(?:/|@)`; +const imageRegistryRegex = new RegExp(imageRegistryPattern); + +// Digest regex. Anchored to the start and end of the string. +const digestPattern = String.raw`^[A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][a-fA-F0-9]{32,}$`; +const digestRegex = new RegExp(digestPattern); + +export class ParsedImageReference { + /** Repository path (e.g. nginx, library/nginx) */ + public readonly repository: string; + /** Registry hostname (e.g. gcr.io, registry-1.docker.io); undefined if none */ + public readonly registry?: string; + /** Tag (e.g. latest, 1.23.0); undefined if only digest or neither */ + public readonly tag?: string; + /** Inline digest (e.g. sha256:abc...); undefined if not present */ + public readonly digest?: string; + + constructor(params: { + repository: string; + registry?: string; + tag?: string; + digest?: string; + }) { + this.repository = params.repository; + this.registry = params.registry; + this.tag = params.tag; + this.digest = params.digest; + } + + /** + * Rebuilds the image reference string from repository, registry, tag, and digest. + * Format: [registry/]repository[:tag][@digest] + * + * Note: This does not normalize the reference for pulling. Specifically, it does not + * add the default "library/" namespace or the "registry-1.docker.io" registry for + * Docker Hub images. To get the components for pulling, use `registryForPull`, + * `normalizedRepository`, and `tailReferenceForPull`. + */ + public toString(): string { + let ref = ""; + if (this.registry) { + ref += this.registry + "/"; + } + ref += this.repository; + if (this.tag) { + ref += ":" + this.tag; + } + if (this.digest) { + ref += "@" + this.digest; + } + return ref; + } + + /** + * The qualified repository name. + * This is the registry and repository combined. + */ + get fullName(): string { + return this.registry + ? this.registry + "/" + this.repository + : this.repository; + } + + /** + * Whether the image is from Docker Hub. + * This is true if the registry is "registry-1.docker.io" or "docker.io" or undefined. + */ + get isDockerHub(): boolean { + return ( + this.registry === "registry-1.docker.io" || + this.registry === "docker.io" || + this.registry === undefined + ); + } + + /** + * The registry to use for pulling the image. + * If the registry is not set, use Docker Hub. + */ + get registryForPull(): string { + if (this.isDockerHub || this.registry === undefined) { + return "registry-1.docker.io"; + } + return this.registry; + } + + /** + * The tail reference to use for pulling the image. + * If the digest is set, use the digest. + * If the tag is set, use the tag. + * If neither are set, use "latest". + */ + get tailReferenceForPull(): string { + return this.digest ?? this.tag ?? "latest"; + } + + /** + * The normalized repository name. + * If the image is from Docker Hub and the repository does not have a namespace, add the default namespace "library". + */ + get normalizedRepository(): string { + if (this.isDockerHub && !this.repository.includes("/")) { + return "library/" + this.repository; + } + return this.repository; + } +} + +/** + * Parse an OCI image reference into repository, registry, tag, and digest. + * + * @param reference - Image reference string (e.g. nginx:1.23.0@sha256:..., gcr.io/nginx:latest) + * @returns ParsedImageReference + * @throws "image name is empty" if reference is empty + * @throws "image repository contains uppercase letter" if repo path has uppercase + * @throws "invalid image reference format" if format is invalid + */ +export function parseImageReference(reference: string): ParsedImageReference { + if (reference === "") { + throw new Error("image name is empty"); + } + + const groups = imageReferenceRegex.exec(reference); + if (groups === null) { + const lowerMatch = imageReferenceRegex.exec(reference.toLowerCase()); + if (lowerMatch !== null) { + throw new Error("image repository contains uppercase letter"); + } + throw new Error("invalid image reference format"); + } + + let repository = groups[1]; + const tag = groups[2]; + const digest = groups[3]; + + let registry: string | undefined; + const registryMatch = imageRegistryRegex.exec(repository); + if (registryMatch !== null) { + registry = registryMatch[1]; + repository = repository.slice(registry.length + 1); + } + + const parsed = new ParsedImageReference({ + repository, + registry, + tag: tag ?? undefined, + digest: digest ?? undefined, + }); + + // According to the OCI distribution specification: "Many clients impose a limit of + // 255 characters on the length of the concatenation of the registry hostname + // (and optional port), `/`, and `` value." + if (parsed.fullName.length > 255) { + throw new Error("image repository name is more than 255 characters"); + } + + return parsed; +} + +/** + * Validate a Docker/OCI image reference without throwing. + * + * @param reference - Image reference string to validate + * @returns true if parseImageReference would succeed, false otherwise + */ +export function isValidImageReference(reference: string): boolean { + try { + parseImageReference(reference); + return true; + } catch { + return false; + } +} + +/** + * Validate a digest string. + * + * @param digest - Digest string to validate + * @returns true if the digest is valid, false otherwise + */ +export function isValidDigest(digest: string): boolean { + return digestRegex.test(digest); +} diff --git a/lib/scan.ts b/lib/scan.ts index 6575ac632..d37f7b2e5 100644 --- a/lib/scan.ts +++ b/lib/scan.ts @@ -7,6 +7,7 @@ import { DockerFileAnalysis } from "./dockerfile/types"; import { extractImageContent } from "./extractor"; import { ImageName } from "./extractor/image"; import { ExtractAction, ExtractionResult } from "./extractor/types"; +import { isValidImageReference, parseImageReference } from "./image-reference"; import { fullImageSavePath } from "./image-save-path"; import { getArchivePath, getImageType } from "./image-type"; import { @@ -18,7 +19,6 @@ import { } from "./option-utils"; import * as staticModule from "./static"; import { ImageType, PluginOptions, PluginResponse } from "./types"; -import { isValidDockerImageReference } from "./utils"; // Registry credentials may also be provided by env vars. When both are set, flags take precedence. export function mergeEnvVarsIntoCredentials( @@ -199,7 +199,7 @@ async function imageIdentifierAnalysis( // Validate Docker image reference format to catch malformed references early. We implement initial validation here // in lieu of simply sending to the docker daemon since some invalid references can result in unknown or invalid API // paths to the Docker daemon, sometimes producing confusing error results (like redirects) instead of the not found response. - if (!isValidDockerImageReference(targetImage)) { + if (!isValidImageReference(targetImage)) { throw new Error(`invalid image reference format: ${targetImage}`); } @@ -235,13 +235,18 @@ async function imageIdentifierAnalysis( } export function appendLatestTagIfMissing(targetImage: string): string { - if ( - getImageType(targetImage) === ImageType.Identifier && - !targetImage.includes(":") - ) { - return `${targetImage}:latest`; + if (getImageType(targetImage) !== ImageType.Identifier) { + return targetImage; + } + try { + const parsed = parseImageReference(targetImage); + if (parsed.tag !== undefined || parsed.digest !== undefined) { + return parsed.toString(); + } + return parsed.toString() + ":latest"; + } catch { + return targetImage; } - return targetImage; } export async function extractContent( diff --git a/lib/utils.ts b/lib/utils.ts index c7c11d1a0..4e8165a8c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,21 +1,5 @@ import { PluginWarningsFact } from "./facts"; -/** - * Validates a Docker image reference format using the official Docker reference regex. - * @param imageReference The Docker image reference to validate - * @returns true if valid, false if invalid - */ -export function isValidDockerImageReference(imageReference: string): boolean { - // Docker image reference validation regex from the official Docker packages: - // https://github.com/distribution/reference/blob/ff14fafe2236e51c2894ac07d4bdfc778e96d682/regexp.go#L9 - // Original regex: ^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][[:xdigit:]]{32,}))?$ - // Note: Converted [[:xdigit:]] to [a-fA-F0-9] and escaped the forward slashes for JavaScript compatibility. - const dockerImageRegex = - /^((?:(?:(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])(?:\.(?:[a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]))*|\[(?:[a-fA-F0-9:]+)\])(?::[0-9]+)?\/)?[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|[-]+)[a-z0-9]+)*)*)(?::([\w][\w.-]{0,127}))?(?:@([A-Za-z][A-Za-z0-9]*(?:[-_+.][A-Za-z][A-Za-z0-9]*)*[:][a-fA-F0-9]{32,}))?$/; - - return dockerImageRegex.test(imageReference); -} - // array[*] indicates to truncate each element to the indicated size export const RESPONSE_SIZE_LIMITS = { "containerConfig.data.user": { type: "string", limit: 1024 }, diff --git a/package-lock.json b/package-lock.json index 94a1dbf41..71ac8c97a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,6 @@ "@snyk/docker-registry-v2-client": "^2.24.2", "@snyk/rpm-parser": "^3.4.1", "@snyk/snyk-docker-pull": "^3.15.1", - "@swimlane/docker-reference": "^2.0.1", "adm-zip": "^0.5.17", "chalk": "^2.4.2", "debug": "^4.4.3", @@ -2682,11 +2681,6 @@ "node": ">=12" } }, - "node_modules/@swimlane/docker-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@swimlane/docker-reference/-/docker-reference-2.0.1.tgz", - "integrity": "sha512-+mxOe4TwtZJHmvVwq8CnY21wYpYbLXS3IXOWQbgolyfwJhS2vEsNKmNHGdWHd7KUkJTYLs7nmJFVx9OZ2qEVbA==" - }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", @@ -17304,11 +17298,6 @@ "tar-fs": "^3.0.9" } }, - "@swimlane/docker-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@swimlane/docker-reference/-/docker-reference-2.0.1.tgz", - "integrity": "sha512-+mxOe4TwtZJHmvVwq8CnY21wYpYbLXS3IXOWQbgolyfwJhS2vEsNKmNHGdWHd7KUkJTYLs7nmJFVx9OZ2qEVbA==" - }, "@szmarczak/http-timer": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", diff --git a/package.json b/package.json index ec732373e..1d56a09b6 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,6 @@ "@snyk/docker-registry-v2-client": "^2.24.2", "@snyk/rpm-parser": "^3.4.1", "@snyk/snyk-docker-pull": "^3.15.1", - "@swimlane/docker-reference": "^2.0.1", "adm-zip": "^0.5.17", "chalk": "^2.4.2", "debug": "^4.4.3", @@ -64,7 +63,6 @@ "@commitlint/config-conventional": "^17.0.2", "@mongodb-js/zstd": "^2.0.1", "@semantic-release/exec": "^6.0.3", - "semantic-release": "^19.0.5", "@types/adm-zip": "^0.4.34", "@types/debug": "^4.1.5", "@types/jest": "^29.5.5", @@ -74,6 +72,7 @@ "jest": "^29.7.0", "npm-run-all": "^4.1.5", "prettier": "^2.7.1", + "semantic-release": "^19.0.5", "ts-jest": "^29.1.1", "ts-node": "^10.2.1", "tsc-watch": "^4.2.8", diff --git a/test/lib/dependency-tree/index.spec.ts b/test/lib/dependency-tree/index.spec.ts index 6722c8d64..b1d5b17e1 100644 --- a/test/lib/dependency-tree/index.spec.ts +++ b/test/lib/dependency-tree/index.spec.ts @@ -1,5 +1,8 @@ import { AnalyzedPackageWithVersion } from "../../../lib/analyzer/types"; -import { buildTree } from "../../../lib/dependency-tree"; +import { + buildTree, + nameAndVersionFromTargetImage, +} from "../../../lib/dependency-tree"; const targetImage = "snyk/deptree-test@1.0.0"; const targetOS = { @@ -76,3 +79,161 @@ describe("dependency-tree", () => { }); }); }); + +const validSha256 = + "sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1"; + +describe("nameAndVersionFromTargetImage", () => { + describe("handles valid image references", () => { + it("with repository only", () => { + expect(nameAndVersionFromTargetImage("nginx")).toEqual({ + name: "nginx", + version: "latest", + }); + }); + + it("with tag", () => { + expect(nameAndVersionFromTargetImage("nginx:1.23")).toEqual({ + name: "nginx", + version: "1.23", + }); + }); + + it("with digest", () => { + expect(nameAndVersionFromTargetImage(`nginx@${validSha256}`)).toEqual({ + name: "nginx", + version: "", + }); + }); + + it("with tag and digest", () => { + expect( + nameAndVersionFromTargetImage(`nginx:1.23@${validSha256}`), + ).toEqual({ + name: "nginx", + version: "1.23", + }); + }); + + it("with registry", () => { + expect(nameAndVersionFromTargetImage("gcr.io/project/nginx")).toEqual({ + name: "gcr.io/project/nginx", + version: "latest", + }); + }); + + it("with registry and port", () => { + expect(nameAndVersionFromTargetImage("localhost:5000/foo/bar")).toEqual({ + name: "localhost:5000/foo/bar", + version: "latest", + }); + }); + + it("with registry and port and tag", () => { + expect( + nameAndVersionFromTargetImage("localhost:5000/foo/bar:tag"), + ).toEqual({ + name: "localhost:5000/foo/bar", + version: "tag", + }); + }); + + it("with registry and port and digest", () => { + expect( + nameAndVersionFromTargetImage(`localhost:5000/foo/bar@${validSha256}`), + ).toEqual({ + name: "localhost:5000/foo/bar", + version: "", + }); + }); + + it("with registry, port, digest and tag", () => { + expect( + nameAndVersionFromTargetImage( + `localhost:5000/foo/bar:tag@${validSha256}`, + ), + ).toEqual({ + name: "localhost:5000/foo/bar", + version: "tag", + }); + }); + + it("with library/ namespace", () => { + expect(nameAndVersionFromTargetImage("library/nginx:latest")).toEqual({ + name: "library/nginx", + version: "latest", + }); + }); + + it("with dots and dashes in the tag", () => { + expect(nameAndVersionFromTargetImage("nginx:1.23.0-alpha")).toEqual({ + name: "nginx", + version: "1.23.0-alpha", + }); + }); + }); + + // These tests are to verify that the previous logic is still working as expected for + // references that cannot be parsed by the new parseImageReference function. + // They are not necessarily asserting that this is the correct parsing logic. + describe("handles file-based reference strings", () => { + it("with a simple image name", () => { + expect(nameAndVersionFromTargetImage("image.tar")).toEqual({ + name: "image.tar", + version: "", + }); + }); + + it("with a longer path", () => { + expect(nameAndVersionFromTargetImage("path/to/archive.tar")).toEqual({ + name: "path/to/archive.tar", + version: "", + }); + }); + + it("with a tag", () => { + expect(nameAndVersionFromTargetImage("path/to/archive.tar:tag")).toEqual({ + name: "path/to/archive.tar", + version: "tag", + }); + }); + + it("with a digest", () => { + expect( + nameAndVersionFromTargetImage(`archive.tar@${validSha256}`), + ).toEqual({ + name: "archive.tar", + version: "", + }); + }); + + it("with a tag and digest", () => { + expect( + nameAndVersionFromTargetImage(`path/to/archive.tar:tag@${validSha256}`), + ).toEqual({ + name: "path/to/archive.tar", + version: "tag", + }); + }); + + it("with a tag and specific image name", () => { + expect( + nameAndVersionFromTargetImage("path/to/archive.tar:image:tag"), + ).toEqual({ + name: "path/to/archive.tar:image", + version: "tag", + }); + }); + + it("with a tag and specific image name and digest", () => { + expect( + nameAndVersionFromTargetImage( + `path/to/archive.tar:image:tag@${validSha256}`, + ), + ).toEqual({ + name: "path/to/archive.tar:image:tag", + version: "", + }); + }); + }); +}); diff --git a/test/lib/image-reference.spec.ts b/test/lib/image-reference.spec.ts new file mode 100644 index 000000000..b7cb81368 --- /dev/null +++ b/test/lib/image-reference.spec.ts @@ -0,0 +1,363 @@ +import { + isValidImageReference, + ParsedImageReference, + parseImageReference, +} from "../../lib/image-reference"; + +const validSha256 = + "sha256:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234"; + +describe("image-reference", () => { + describe("parseImageReference", () => { + describe("valid image references", () => { + it("parses image with tag only", () => { + expect(parseImageReference("nginx:latest")).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: "latest", + digest: undefined, + }); + }); + + it("parses image with semantic tag", () => { + expect(parseImageReference("nginx:1.23.0")).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: "1.23.0", + digest: undefined, + }); + }); + + it("parses image without tag or digest (repository only)", () => { + expect(parseImageReference("nginx")).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: undefined, + digest: undefined, + }); + }); + + it("parses image with digest only", () => { + expect(parseImageReference(`nginx@${validSha256}`)).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: undefined, + digest: validSha256, + }); + }); + + it("parses image with tag and digest (name:tag@digest)", () => { + expect( + parseImageReference(`nginx:1.23.0@${validSha256}`), + ).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: "1.23.0", + digest: validSha256, + }); + }); + + it("parses image with registry (gcr.io)", () => { + expect( + parseImageReference("gcr.io/project/nginx:latest"), + ).toMatchObject({ + repository: "project/nginx", + registry: "gcr.io", + tag: "latest", + digest: undefined, + }); + }); + + it("parses image with registry and digest", () => { + expect( + parseImageReference(`gcr.io/project/nginx:1.23.0@${validSha256}`), + ).toMatchObject({ + repository: "project/nginx", + registry: "gcr.io", + tag: "1.23.0", + digest: validSha256, + }); + }); + + it("parses localhost registry with port", () => { + expect(parseImageReference("localhost:5000/foo/bar:tag")).toMatchObject( + { + repository: "foo/bar", + registry: "localhost:5000", + tag: "tag", + digest: undefined, + }, + ); + }); + + it("parses docker.io style registry", () => { + expect( + parseImageReference("docker.io/calico/cni:release-v3.14"), + ).toMatchObject({ + repository: "calico/cni", + registry: "docker.io", + tag: "release-v3.14", + digest: undefined, + }); + }); + + it("parses library/ prefix (Docker Hub official images)", () => { + expect(parseImageReference("library/nginx:latest")).toMatchObject({ + repository: "library/nginx", + registry: undefined, + tag: "latest", + digest: undefined, + }); + }); + + it("parses docker.io/library/ prefix (Docker Hub official images)", () => { + expect( + parseImageReference("registry-1.docker.io/library/nginx:latest"), + ).toMatchObject({ + repository: "library/nginx", + registry: "registry-1.docker.io", + tag: "latest", + digest: undefined, + }); + }); + + it("parses repository with dots and dashes", () => { + expect(parseImageReference("my.repo/image-name:tag")).toMatchObject({ + repository: "image-name", + registry: "my.repo", + tag: "tag", + digest: undefined, + }); + }); + + it("parses IPv6 registry", () => { + expect(parseImageReference("[::1]:5000/foo/bar:latest")).toMatchObject({ + repository: "foo/bar", + registry: "[::1]:5000", + tag: "latest", + digest: undefined, + }); + }); + + it("parses tag with dots and dashes", () => { + expect(parseImageReference("nginx:1.23.0-alpha")).toMatchObject({ + repository: "nginx", + registry: undefined, + tag: "1.23.0-alpha", + digest: undefined, + }); + }); + + it("returns ParsedImageReference instance", () => { + expect(parseImageReference("nginx:latest")).toBeInstanceOf( + ParsedImageReference, + ); + }); + }); + + describe("invalid image references", () => { + it("throws for empty string", () => { + expect(() => parseImageReference("")).toThrow("image name is empty"); + }); + + it("throws for invalid format (no repository)", () => { + expect(() => parseImageReference(":tag")).toThrow( + "invalid image reference format", + ); + }); + + it("throws for invalid format (leading slash)", () => { + expect(() => parseImageReference("/test:unknown")).toThrow( + "invalid image reference format", + ); + }); + + it("throws for uppercase in repository", () => { + expect(() => parseImageReference("UPPERCASE")).toThrow( + "image repository contains uppercase letter", + ); + }); + + it("throws for uppercase in repository path with registry", () => { + expect(() => parseImageReference("gcr.io/Project/nginx")).toThrow( + "image repository contains uppercase letter", + ); + }); + + it("throws for registry and repository name exceeding 255 characters", () => { + const registry = "example.com/"; + const longName = "a".repeat(256 - registry.length); + expect(() => parseImageReference(registry + longName)).toThrow( + "image repository name is more than 255 characters", + ); + }); + + it("allows registry and repository name of exactly 255 characters", () => { + const registry = "example.com/"; + const name = "a".repeat(255 - registry.length); + expect(parseImageReference(registry + name)).toMatchObject({ + registry: "example.com", + repository: name, + }); + }); + + it("allows registry and repository name of exactly 255 characters with a digest", () => { + const registry = "example.com/"; + const name = "a".repeat(255 - registry.length); + const digest = "@" + validSha256; + expect(parseImageReference(registry + name + digest)).toMatchObject({ + registry: "example.com", + repository: name, + }); + }); + + it("throws for invalid digest (too short)", () => { + expect(() => parseImageReference("nginx@sha256:abc")).toThrow( + "invalid image reference format", + ); + }); + + it("throws for malformed reference", () => { + expect(() => parseImageReference("image:")).toThrow( + "invalid image reference format", + ); + }); + }); + }); + + describe("ParsedImageReference#toString", () => { + it("rebuilds reference with repository only", () => { + const parsed = parseImageReference("nginx"); + expect(parsed.toString()).toBe("nginx"); + }); + + it("rebuilds reference with repository and tag", () => { + const parsed = parseImageReference("nginx:latest"); + expect(parsed.toString()).toBe("nginx:latest"); + }); + + it("rebuilds reference with repository and digest", () => { + const parsed = parseImageReference(`nginx@${validSha256}`); + expect(parsed.toString()).toBe(`nginx@${validSha256}`); + }); + + it("rebuilds reference with repository, tag and digest", () => { + const ref = `nginx:1.23.0@${validSha256}`; + const parsed = parseImageReference(ref); + expect(parsed.toString()).toBe(ref); + }); + + it("rebuilds reference with registry, repository and tag", () => { + const parsed = parseImageReference("gcr.io/project/nginx:latest"); + expect(parsed.toString()).toBe("gcr.io/project/nginx:latest"); + }); + + it("rebuilds reference with registry, repository, tag and digest", () => { + const ref = `gcr.io/project/nginx:1.23.0@${validSha256}`; + const parsed = parseImageReference(ref); + expect(parsed.toString()).toBe(ref); + }); + + it("round-trips for various valid references", () => { + const refs = [ + "nginx", + "nginx:latest", + "nginx:1.23.0", + "library/nginx:latest", + "localhost:5000/foo/bar:tag", + "[::1]:5000/foo/bar:latest", + `nginx@${validSha256}`, + `nginx:1.23.0@${validSha256}`, + ]; + for (const ref of refs) { + expect(parseImageReference(ref).toString()).toBe(ref); + } + }); + }); +}); + +describe("isValidImageReference", () => { + describe("valid image references", () => { + const validImages = [ + "nginx", + "ubuntu", + "alpine", + "nginx:latest", + "ubuntu:20.04", + "alpine:3.14", + "library/nginx", + "library/ubuntu:20.04", + "docker.io/nginx", + "docker.io/library/nginx:latest", + "gcr.io/project-id/image-name", + "gcr.io/project-id/image-name:tag", + "registry.hub.docker.com/library/nginx", + "localhost:5000/myimage", + "localhost:5000/myimage:latest", + "registry.example.com/path/to/image", + "registry.example.com:8080/path/to/image:v1.0", + "my-registry.com/my-namespace/my-image", + "my-registry.com/my-namespace/my-image:v2.1.0", + "nginx@sha256:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234", + "ubuntu:20.04@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12", + "image_name", + "image.name", + "image-name", + "namespace/image_name.with-dots", + "registry.com/namespace/image__double_underscore", + "127.0.0.1:5000/test", + "[::1]:5000/test", + "registry.com/a/b/c/d/e/f/image", + "a.b.c/namespace/image:tag", + ]; + + it.each(validImages)( + "should return true for valid image reference: %s", + (imageName) => { + expect(isValidImageReference(imageName)).toBe(true); + }, + ); + }); + + describe("invalid image references", () => { + const invalidImages = [ + "/test:unknown", + "//invalid", + "invalid//path", + "UPPERCASE", + "Invalid:Tag", + "registry.com/UPPERCASE/image", + "registry.com/namespace/UPPERCASE", + "", + "image:", + ":tag", + "image::", + "registry.com:", + "registry.com:/image", + "image@", + "image@sha256:", + "image@invalid:digest", + "registry.com//namespace/image", + "registry.com/namespace//image", + ".image", + "image.", + "-image", + "image-", + "_image", + "image_", + "registry-.com/image", + "registry.com-/image", + "image:tag@", + "image:tag@sha256", + "registry.com:abc/image", + "registry.com:-1/image", + ]; + + it.each(invalidImages)( + "should return false for invalid image reference: %s", + (imageName) => { + expect(isValidImageReference(imageName)).toBe(false); + }, + ); + }); +}); diff --git a/test/lib/scan.spec.ts b/test/lib/scan.spec.ts index 2714caab9..39b39be35 100644 --- a/test/lib/scan.spec.ts +++ b/test/lib/scan.spec.ts @@ -30,22 +30,22 @@ describe("mergeEnvVarsIntoCredentials", () => { ${FLAG_USER} | ${ENV_VAR_USER} | ${FLAG_USER} `("should set username to $expectedUsername when flag is $usernameFlag and envvar is $usernameEnvVar", - ({ - usernameFlag, - usernameEnvVar, - expectedUsername, - }) => { - if (usernameEnvVar) { - process.env.SNYK_REGISTRY_USERNAME = usernameEnvVar; - } - const options = { - username: usernameFlag, - }; - - mergeEnvVarsIntoCredentials(options); - - expect(options.username).toEqual(expectedUsername); - }); + ({ + usernameFlag, + usernameEnvVar, + expectedUsername, + }) => { + if (usernameEnvVar) { + process.env.SNYK_REGISTRY_USERNAME = usernameEnvVar; + } + const options = { + username: usernameFlag, + }; + + mergeEnvVarsIntoCredentials(options); + + expect(options.username).toEqual(expectedUsername); + }); // prettier-ignore it.each` @@ -59,10 +59,10 @@ describe("mergeEnvVarsIntoCredentials", () => { `("should set password to $expectedPassword when flag is $passwordFlag and envvar is $passwordEnvVar", ({ - passwordFlag, - passwordEnvVar, - expectedPassword, - }) => { + passwordFlag, + passwordEnvVar, + expectedPassword, + }) => { if (passwordEnvVar) { process.env.SNYK_REGISTRY_PASSWORD = passwordEnvVar; } @@ -84,26 +84,64 @@ describe("appendLatestTagIfMissing", () => { ); }); - it("does not append latest to docker archive path", () => { + it("does not append latest to oci archive path", () => { const ociArchivePath = "oci-archive:some/path/image.tar"; expect(appendLatestTagIfMissing(ociArchivePath)).toEqual(ociArchivePath); }); + it("does not append latest to kaniko-archive path", () => { + const path = "kaniko-archive:some/path/image.tar"; + expect(appendLatestTagIfMissing(path)).toEqual(path); + }); + + it("does not append latest to unspecified archive path (.tar)", () => { + const path = "/tmp/nginx.tar"; + expect(appendLatestTagIfMissing(path)).toEqual(path); + }); + it("does not append latest if tag exists", () => { const imageWithTag = "image:sometag"; expect(appendLatestTagIfMissing(imageWithTag)).toEqual(imageWithTag); }); - it("does not modify targetImage with sha", () => { - const imageWithSha = - "snyk container test nginx@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1"; - expect(appendLatestTagIfMissing(imageWithSha)).toEqual(imageWithSha); + it("does not append latest if digest exists (digest-only reference)", () => { + const imageWithDigest = + "nginx@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1"; + expect(appendLatestTagIfMissing(imageWithDigest)).toEqual(imageWithDigest); }); - it("appends latest if no tag exists", () => { - const imageWithoutTag = "image"; - expect(appendLatestTagIfMissing(imageWithoutTag)).toEqual( - `${imageWithoutTag}:latest`, + it("does not append latest if both tag and digest exist", () => { + const imageWithTagAndDigest = + "nginx:1.23@sha256:56ea7092e72db3e9f84d58d583370d59b842de02ea9e1f836c3f3afc7ce408c1"; + expect(appendLatestTagIfMissing(imageWithTagAndDigest)).toEqual( + imageWithTagAndDigest, ); }); + + it("appends latest for repository-only reference", () => { + expect(appendLatestTagIfMissing("image")).toEqual("image:latest"); + }); + + it("appends latest for repository with namespace", () => { + expect(appendLatestTagIfMissing("library/nginx")).toEqual( + "library/nginx:latest", + ); + }); + + it("appends latest for registry with port and no tag", () => { + expect(appendLatestTagIfMissing("localhost:5000/foo/bar")).toEqual( + "localhost:5000/foo/bar:latest", + ); + }); + + it("appends latest for custom registry without tag", () => { + expect(appendLatestTagIfMissing("gcr.io/project/nginx")).toEqual( + "gcr.io/project/nginx:latest", + ); + }); + + it("returns unchanged for invalid image reference", () => { + const invalid = "/test:unknown"; + expect(appendLatestTagIfMissing(invalid)).toEqual(invalid); + }); }); diff --git a/test/lib/utils.spec.ts b/test/lib/utils.spec.ts index 29f29156e..a85d57a46 100644 --- a/test/lib/utils.spec.ts +++ b/test/lib/utils.spec.ts @@ -1,94 +1,4 @@ -import { - isValidDockerImageReference, - RESPONSE_SIZE_LIMITS, - truncateAdditionalFacts, -} from "../../lib/utils"; - -describe("isValidDockerImageReference", () => { - describe("valid image references", () => { - const validImages = [ - "nginx", - "ubuntu", - "alpine", - "nginx:latest", - "ubuntu:20.04", - "alpine:3.14", - "library/nginx", - "library/ubuntu:20.04", - "docker.io/nginx", - "docker.io/library/nginx:latest", - "gcr.io/project-id/image-name", - "gcr.io/project-id/image-name:tag", - "registry.hub.docker.com/library/nginx", - "localhost:5000/myimage", - "localhost:5000/myimage:latest", - "registry.example.com/path/to/image", - "registry.example.com:8080/path/to/image:v1.0", - "my-registry.com/my-namespace/my-image", - "my-registry.com/my-namespace/my-image:v2.1.0", - "nginx@sha256:abcd1234567890abcd1234567890abcd1234567890abcd1234567890abcd1234", - "ubuntu:20.04@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12", - "image_name", - "image.name", - "image-name", - "namespace/image_name.with-dots", - "registry.com/namespace/image__double_underscore", - "127.0.0.1:5000/test", - "[::1]:5000/test", - "registry.com/a/b/c/d/e/f/image", - "a.b.c/namespace/image:tag", - ]; - - it.each(validImages)( - "should return true for valid image reference: %s", - (imageName) => { - expect(isValidDockerImageReference(imageName)).toBe(true); - }, - ); - }); - - describe("invalid image references", () => { - const invalidImages = [ - "/test:unknown", - "//invalid", - "invalid//path", - "UPPERCASE", - "Invalid:Tag", - "registry.com/UPPERCASE/image", - "registry.com/namespace/UPPERCASE", - "", - "image:", - ":tag", - "image::", - "registry.com:", - "registry.com:/image", - "image@", - "image@sha256:", - "image@invalid:digest", - "registry.com//namespace/image", - "registry.com/namespace//image", - ".image", - "image.", - "-image", - "image-", - "_image", - "image_", - "registry-.com/image", - "registry.com-/image", - "image:tag@", - "image:tag@sha256", - "registry.com:abc/image", - "registry.com:-1/image", - ]; - - it.each(invalidImages)( - "should return false for invalid image reference: %s", - (imageName) => { - expect(isValidDockerImageReference(imageName)).toBe(false); - }, - ); - }); -}); +import { RESPONSE_SIZE_LIMITS, truncateAdditionalFacts } from "../../lib/utils"; describe("truncateAdditionalFacts", () => { describe("should handle edge cases", () => { diff --git a/test/system/docker.spec.ts b/test/system/docker.spec.ts index ddab6d15b..72035d11b 100644 --- a/test/system/docker.spec.ts +++ b/test/system/docker.spec.ts @@ -211,6 +211,17 @@ describe("docker", () => { }); test("promise rejects when image doesn't exist", async () => { + const image = "some-non-existent-image:latest"; + const destination = "/tmp/image.tar"; + + const result = docker.save(image, destination); + + // rejects with expected error + await expect(result).rejects.toThrowError("not found"); + expect(existsSync(destination)).toBeFalsy(); + }); + + test("promise rejects when image is invalid", async () => { const image = "someImage:latest"; const destination = "/tmp/image.tar"; diff --git a/test/system/image-inspector.spec.ts b/test/system/image-inspector.spec.ts index 98492c2ba..716f6136a 100644 --- a/test/system/image-inspector.spec.ts +++ b/test/system/image-inspector.spec.ts @@ -30,9 +30,9 @@ describe("extractImageDetails", () => { imageName: "library/hello-world", tag: "latest", }} - ${"gcr.io/kubernetes/someImage:alpine"} | ${{ + ${"gcr.io/kubernetes/some-image:alpine"} | ${{ hostname: "gcr.io", - imageName: "kubernetes/someImage", + imageName: "kubernetes/some-image", tag: "alpine", }} ${"nginx:1.18"} | ${{ @@ -50,9 +50,9 @@ describe("extractImageDetails", () => { imageName: "calico/cni", tag: "release-v3.14", }} - ${"gcr.io:3000/kubernetes/someImage:alpine"} | ${{ + ${"gcr.io:3000/kubernetes/some-image:alpine"} | ${{ hostname: "gcr.io:3000", - imageName: "kubernetes/someImage", + imageName: "kubernetes/some-image", tag: "alpine", }} ${"localhost/alpine"} | ${{ @@ -60,9 +60,9 @@ describe("extractImageDetails", () => { imageName: "alpine", tag: "latest", }} - ${"localhost:1337/kubernetes/someImage:alpine"} | ${{ + ${"localhost:1337/kubernetes/some-image:alpine"} | ${{ hostname: "localhost:1337", - imageName: "kubernetes/someImage", + imageName: "kubernetes/some-image", tag: "alpine", }} ${"gcr.io/distroless/base-debian10@sha256:8756a25c4c5e902c4fe20322cc69d510a0517b51eab630c614efbd612ed568bf"} | ${{ diff --git a/test/system/package-managers/__snapshots__/rpm.spec.ts.snap b/test/system/package-managers/__snapshots__/rpm.spec.ts.snap index d30da9753..9af7ac31b 100644 --- a/test/system/package-managers/__snapshots__/rpm.spec.ts.snap +++ b/test/system/package-managers/__snapshots__/rpm.spec.ts.snap @@ -8221,7 +8221,6 @@ Object { "data": Object { "names": Array [ "registry.access.redhat.com/ubi9/ubi@sha256:c113f67e8e70940af28116d75e32f0aa4ffd3bf6fab30e970850475ab1de697f", - "registry.access.redhat.com/ubi9/ubi@sha256:c113f67e8e70940af28116d75e32f0aa4ffd3bf6fab30e970850475ab1de697f", ], }, "type": "imageNames", @@ -11208,7 +11207,6 @@ Object { "data": Object { "names": Array [ "registry.access.redhat.com/ubi10-beta/ubi@sha256:4b4976d86eefeedab6884c9d2923206c6c3c2e2471206f97fd9d7aaaecbc04ac", - "registry.access.redhat.com/ubi10-beta/ubi@sha256:4b4976d86eefeedab6884c9d2923206c6c3c2e2471206f97fd9d7aaaecbc04ac", ], }, "type": "imageNames",