diff --git a/Dockerfile b/Dockerfile index 9845e28..d1f7732 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN if [ "$UID" != "1000" ]; then \ ; fi RUN apt-get update -RUN apt-get install --yes git \ +RUN apt-get install --yes git python3 \ `# Chromium installation dependencies` \ curl unzip \ `# Chromium runtime dependencies` \ diff --git a/compose.yaml b/compose.yaml index b58462c..4d53275 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,11 @@ services: GID: "${GID:-1000}" UID: "${UID:-1000}" command: ["/bin/sh", "-c", "yarn install && yarn build:watch"] + depends_on: + - socat-docker-bridge + environment: + - DOCKER_HOST=tcp://localhost:2375 + - SINDRI_DEVELOPMENT_HOST_ROOT=${PWD} init: true network_mode: host extra_hosts: @@ -17,7 +22,19 @@ services: volumes: - ./:/sindri/ - ~/.gitconfig:/home/node/.gitconfig + - /tmp/sindri:/tmp/sindri + - /var/run/docker.sock:/var/run/docker.sock - yarn-cache:/home/node/.cache/yarn/ + # Expose the host's `/var/run/docker.sock` socket as TCP port 2375 using socat as the bridge. + # This allows the `sindri-js` container to access the host's docker daemon without root. + socat-docker-bridge: + image: alpine/socat + command: tcp-listen:2375,fork,reuseaddr unix-connect:/var/run/docker.sock + user: root + volumes: + - /var/run/docker.sock:/var/run/docker.sock + network_mode: host + volumes: yarn-cache: diff --git a/package.json b/package.json index 1d69cdd..bf9da4a 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/tar-js": "^0.3.5", "axios": "^1.6.2", "commander": "^11.1.0", + "dockerode": "^4.0.2", "env-paths": "^2.2.1", "formdata-node": "^6.0.3", "gzip-js": "^0.3.2", @@ -106,6 +107,7 @@ }, "devDependencies": { "@ava/typescript": "^4.1.0", + "@types/dockerode": "^3.3.23", "@types/sarif": "^2.1.7", "@typescript-eslint/eslint-plugin": "^6.11.0", "@typescript-eslint/parser": "^6.11.0", diff --git a/src/cli/exec.ts b/src/cli/exec.ts new file mode 100644 index 0000000..768cc9e --- /dev/null +++ b/src/cli/exec.ts @@ -0,0 +1,113 @@ +import assert from "assert"; +import path from "path"; +import process from "process"; + +import { Command } from "@commander-js/extra-typings"; + +import { + checkDockerAvailability, + execDockerCommand, + findFileUpwards, + getDockerImageTags, +} from "cli/utils"; +import sindri from "lib"; +import { print } from "lib/logging"; + +// Shared globals between the different subcommands. +let listTags: boolean; +let rootDirectory: string; +let tag: string; + +const circomspectCommand = new Command() + .name("circomspect") + .description( + "Trail of Bit's Circomspect static analysis tool for Circom circuits.", + ) + .helpOption(false) + .addHelpCommand(false) + .allowUnknownOption() + .passThroughOptions() + .argument("[args...]", "Arguments to pass to the tool.") + .action(async (args) => { + if (listTags) return; // Don't run the command if we're just listing tags. + + try { + const code = await execDockerCommand("circomspect", args, { + logger: sindri.logger, + rootDirectory, + tag, + tty: true, + }); + process.exit(code); + } catch (error) { + sindri.logger.error("Failed to run the circomspect command."); + sindri.logger.debug(error); + return process.exit(1); + } + }); + +export const execCommand = new Command() + .name("exec") + .alias("x") + .description( + "Run a ZKP tool in your project root inside of an optimized docker container.", + ) + .passThroughOptions() + .option( + "-l, --list-tags", + "List the available docker image tags for a given tool.", + ) + .option( + "-t, --tag ", + "The version tag of the docker image to use.", + "auto", + ) + .addCommand(circomspectCommand) + .hook("preAction", async (command) => { + // Store the options in globals for subcommands to access them. + const opts = command.opts(); + listTags = !!opts.listTags; + tag = opts.tag; + + // Handle the `--list-tags` option. + if (listTags) { + const repository = command.args[0]; + assert( + repository, + "The preAction hook should only run if there's a subcommand.", + ); + try { + const tags = await getDockerImageTags(repository); + tags.forEach((tag) => print(tag)); + } catch (error) { + sindri.logger.fatal("Error listing available docker image tags."); + sindri.logger.error(error); + return process.exit(1); + } + return process.exit(0); + } + + // Find the project root. + const cwd = process.cwd(); + const sindriJsonPath = findFileUpwards(/^sindri.json$/i, cwd); + if (sindriJsonPath) { + rootDirectory = path.dirname(sindriJsonPath); + } else { + rootDirectory = cwd; + sindri.logger.warn( + `No "sindri.json" file was found in or above "${cwd}", ` + + "using the current directory as the project root.", + ); + } + rootDirectory = path.normalize(path.resolve(rootDirectory)); + + // Check that docker is installed. + if (!(await checkDockerAvailability(sindri.logger))) { + sindri.logger.fatal( + "Docker is either not installed or the daemon isn't currently running, but it is " + + 'required by "sindri exec".\nPlease install Docker by following the instructions at: ' + + "https://docs.docker.com/get-docker/", + ); + process.exit(1); + } + }); diff --git a/src/cli/index.ts b/src/cli/index.ts index 3244a29..5418544 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -4,6 +4,7 @@ import { argv, exit } from "process"; import { Command } from "@commander-js/extra-typings"; import { configCommand } from "cli/config"; +import { execCommand } from "cli/exec"; import { initCommand } from "cli/init"; import { deployCommand } from "cli/deploy"; import { lintCommand } from "cli/lint"; @@ -25,6 +26,7 @@ export const program = new Command() false, ) .addCommand(configCommand) + .addCommand(execCommand) .addCommand(initCommand) .addCommand(deployCommand) .addCommand(lintCommand) diff --git a/src/cli/lint.ts b/src/cli/lint.ts index 492fca3..52defe7 100644 --- a/src/cli/lint.ts +++ b/src/cli/lint.ts @@ -1,5 +1,4 @@ import assert from "assert"; -import { execSync } from "child_process"; import { randomUUID } from "crypto"; import { existsSync, readFileSync, unlinkSync } from "fs"; import os from "os"; @@ -11,7 +10,11 @@ import type { Schema } from "jsonschema"; import { Validator as JsonValidator } from "jsonschema"; import type { Log as SarifLog, Result as SarifResult } from "sarif"; -import { findFileUpwards, loadSindriManifestJsonSchema } from "cli/utils"; +import { + execCommand, + findFileUpwards, + loadSindriManifestJsonSchema, +} from "cli/utils"; import sindri from "lib"; export const lintCommand = new Command() @@ -186,55 +189,48 @@ export const lintCommand = new Command() // Run Circomspect for Circom circuits. if (circuitType === "circom") { - let circomspectInstalled: boolean = false; try { - execSync("circomspect --help"); - circomspectInstalled = true; - } catch { - sindri.logger.warn( - "Circomspect is not installed, skipping circomspect static analysis.\n" + - "Please install circomspect by following the directions at: " + - "https://github.com/trailofbits/circomspect#installing-circomspect", - ); - warningCount += 1; - } - if (circomspectInstalled) { // Run Circomspect and parse the results. sindri.logger.info( "Running static analysis with Circomspect by Trail of Bits...", ); const sarifFile = path.join( os.tmpdir(), - `sindri-circomspect-${randomUUID()}.sarif`, + "sindri", + `circomspect-${randomUUID()}.sarif`, ); let sarif: SarifLog | undefined; try { - const circuitPath = + const circuitPath: string = "circuitPath" in sindriJson && sindriJson.circuitPath - ? sindriJson.circuitPath + ? (sindriJson.circuitPath as string) : "circuit.circom"; - try { - execSync( - `circomspect --level INFO --sarif-file ${sarifFile} ${circuitPath}`, - { - cwd: rootDirectory, - }, + const code = await execCommand( + "circomspect", + ["--level", "INFO", "--sarif-file", sarifFile, circuitPath], + { + cwd: rootDirectory, + logger: sindri.logger, + rootDirectory, + tty: false, + }, + ); + if (code !== null) { + sindri.logger.debug("Parsing Circomspect SARIF results."); + const sarifContent = readFileSync(sarifFile, { + encoding: "utf-8", + }); + sarif = JSON.parse(sarifContent); + } else { + sindri.logger.warn( + "Circomspect is not installed, skipping circomspect static analysis.\n" + + "Please install Docker by following the directions at: " + + "https://docs.docker.com/get-docker/\n" + + "Or install Circomspect by following the directions at: " + + "https://github.com/trailofbits/circomspect#installing-circomspect", ); - } catch (error) { - // It's expected that circomspect will return a non-zero exit code if it finds issues, - // so we silently squash those errors and only throw if it's something else. - if ( - !(error instanceof Error) || - !("status" in error) || - !error.status - ) { - throw error; - } + warningCount += 1; } - const sarifContent = readFileSync(sarifFile, { - encoding: "utf-8", - }); - sarif = JSON.parse(sarifContent); } catch (error) { sindri.logger.fatal( `Error running Circomspect in "${rootDirectory}".`, @@ -284,6 +280,7 @@ export const lintCommand = new Command() }); // Log out the circomspect results. + let circomspectIssueFound = false; results.forEach((result: SarifResult) => { if ( !result?.locations?.length || @@ -314,15 +311,24 @@ export const lintCommand = new Command() `${result.message.text} [Circomspect: ${result.ruleId}]`; if (result.level === "error") { sindri.logger.error(logMessage); + circomspectIssueFound = true; errorCount += 1; } else if (result.level === "warning") { sindri.logger.warn(logMessage); + circomspectIssueFound = true; warningCount += 1; } else { sindri.logger.debug(logMessage); } }); + if (!circomspectIssueFound) { + sindri.logger.info("No issues found with Circomspect, good job!"); + } } + } catch (error) { + sindri.logger.fatal("Error running Circomspect, aborting."); + sindri.logger.debug(error); + return process.exit(1); } } diff --git a/src/cli/utils.ts b/src/cli/utils.ts index d21dedf..d13b575 100644 --- a/src/cli/utils.ts +++ b/src/cli/utils.ts @@ -1,8 +1,15 @@ +import assert from "assert"; +import { spawn } from "child_process"; import { constants as fsConstants, readdirSync, readFileSync } from "fs"; import { access, mkdir, readdir, readFile, stat, writeFile } from "fs/promises"; +import os from "os"; import path from "path"; +import process from "process"; +import { type Duplex, Writable } from "stream"; import { fileURLToPath } from "url"; +import axios from "axios"; +import Docker from "dockerode"; import type { Schema } from "jsonschema"; import nunjucks from "nunjucks"; import type { PackageJson } from "type-fest"; @@ -12,6 +19,387 @@ import type { Logger } from "lib/logging"; const currentFilePath = fileURLToPath(import.meta.url); const currentDirectoryPath = path.dirname(currentFilePath); +/** + * Checks if a given command exists in the system's PATH. + * + * This function attempts to spawn the command with the `--version` flag, assuming that most + * commands will support it or at least not have side effects when it is passed. + * + * @param command - The name of the command to check. + * + * @returns A boolean indicating whether the command exists. + */ +export function checkCommandExists(command: string): Promise { + return new Promise((resolve) => { + const process = spawn(command, ["--version"]); + + process.on("error", () => { + // Command could not be spawned or was not found in the PATH + resolve(false); + }); + + process.on("exit", (code) => { + // Command exists if there are no errors or the exit code isn't 127. + resolve(code !== 127 && code !== null); + }); + }); +} + +/** + * Checks whether we can connect to the Docker daemon. + * + * @returns A boolean value indicating whether the Docker daemon is accessible. + */ +export async function checkDockerAvailability( + logger?: Logger, +): Promise { + const docker = new Docker(); + try { + await docker.ping(); + } catch (error) { + logger?.debug("Failed to connect to the Docker daemon."); + logger?.debug(error); + return false; + } + logger?.debug("Docker daemon is accessible."); + return true; +} + +/** + * Supported external commands, each must correspond to a `docker-zkp` image repository. + */ +type ExternalCommand = "circomspect"; + +/** + * A writable stream that discards all input. + */ +export const devNull = new Writable({ + write(_chunk, _encoding, callback) { + callback(); + }, +}); + +/** + * Executes an external command, either locally or in a Docker container. + * + * @param command - The command to execute, corresponds to a `docker-zkp` image. + * @param args - The arguments to pass to the command. + * @param options - Additional options for the command. + * @param options.cwd - The current working directory for the executed command. + * @param options.docker - The `Docker` instance to use for running the command. Defaults to a new + * `Docker` instance with default options. + * @param options.logger - The logger to use for logging messages. There will be no logging if not + * specified. + * @param options.rootDirectory - The project root directory on the host. Will be determined by + * searching up the directory tree for a `sindri.json` file if not specified. This directory is + * mounted into the Docker container at `/sindri/` if the command is executed in Docker. + * @param options.tag - The tag of the Docker image to use. Defaults to `auto`, which will map to + * the `latest` tag unless a version specifier is found in `sindri.json` that supersedes it. + * @param options.tty - Whether to use a TTY for the command. Defaults to `false` which means that + * the command's output will be ignored. + * + * @returns The exit code of the command, or `null` if the command is not available locally or in + * Docker. + */ +export async function execCommand( + command: ExternalCommand, + args: string[] = [], + { + cwd = process.cwd(), + docker = new Docker(), + logger, + rootDirectory, + tag = "auto", + tty = false, + }: { + cwd?: string; + docker?: Docker; + logger?: Logger; + rootDirectory?: string; + tag?: string; + tty?: boolean; + }, +): Promise { + // Try using a local command first (unless `SINDRI_FORCE_DOCKER` is set). + if (isTruthy(process.env.SINDRI_FORCE_DOCKER ?? "false")) { + logger?.debug( + `Forcing docker usage for command "${command}" because "SINDRI_FORCE_DOCKER" is set to ` + + `"${process.env.SINDRI_FORCE_DOCKER}".`, + ); + } else if (await checkCommandExists(command)) { + logger?.debug(`Executing the "${command}" command locally.`); + return await execLocalCommand(command, args, { cwd, logger, tty }); + } else { + logger?.debug( + `The "${command}" command was not found locally, trying Docker instead.`, + ); + } + + // Fall back to using Docker if possible. + if (await checkDockerAvailability(logger)) { + logger?.debug(`Executing the "${command}" command in a Docker container.`); + return await execDockerCommand(command, args, { + cwd, + docker, + logger, + rootDirectory, + tag, + tty, + }); + } + + // There's no way to run the command. + logger?.debug( + `The "${command}" command is not available locally or in Docker.`, + ); + return null; +} + +/** + * Executes an external command in a Docker container. + * + * @param command - The command to execute, corresponds to a `docker-zkp` image. + * @param args - The arguments to pass to the command. + * @param options - Additional options for the command. + * @param options.cwd - The current working directory on the host for the executed command. + * @param options.docker - The `Docker` instance to use for running the command. Defaults to a new + * `Docker` instance with default options. + * @param options.logger - The logger to use for logging messages. There will be no logging if not + * specified. + * @param options.rootDirectory - The project root directory on the host. Will be determined by + * searching up the directory tree for a `sindri.json` file if not specified. This directory is + * mounted into the Docker container at `/sindri/`. + * @param options.tag - The tag of the Docker image to use. Defaults to `auto`, which will map to + * the `latest` tag unless a version specifier is found in `sindri.json` that supersedes it. + * @param options.tty - Whether to use a TTY for the command. Defaults to `false` which means that + * the command's output will be ignored. + * + * @returns The exit code of the command. + */ +export async function execDockerCommand( + command: ExternalCommand, + args: string[] = [], + { + cwd = process.cwd(), + docker = new Docker(), + logger, + rootDirectory, + tag = "auto", + tty = false, + }: { + cwd?: string; + docker?: Docker; + logger?: Logger; + rootDirectory?: string; + tag?: string; + tty?: boolean; + }, +): Promise { + // Determine the image to use. + const image = + command === "circomspect" + ? `sindrilabs/circomspect:${tag === "auto" ? "latest" : tag}` + : null; + if (!image) { + throw new Error(`The command "${command}" is not supported.`); + } + + // Find the project root if one wasn't specified. + if (!rootDirectory) { + const cwd = process.cwd(); + const sindriJsonPath = findFileUpwards(/^sindri.json$/i, cwd); + if (sindriJsonPath) { + rootDirectory = path.dirname(sindriJsonPath); + } else { + rootDirectory = cwd; + logger?.warn( + `No "sindri.json" file was found in or above "${cwd}", ` + + `using the current directory as the project root.`, + ); + } + } + rootDirectory = path.normalize(path.resolve(rootDirectory)); + + // Pull the appropriate image. + logger?.debug(`Pulling the "${image}" image.`); + try { + await new Promise((resolve, reject) => { + docker.pull( + image, + (error: Error | null, stream: NodeJS.ReadableStream) => { + if (error) { + reject(error); + } else { + docker.modem.followProgress(stream, (error, result) => + error ? reject(error) : resolve(result), + ); + } + }, + ); + }); + } catch (error) { + logger?.error(`Failed to pull the "${image}" image.`); + logger?.error(error); + return process.exit(1); + } + + // Remap the root directory to its location on the host system when running in development mode. + // This is because the development container has the project root mounted at `/sindri/`, but the + // mounts are performed on the host system so the paths need to exist there. + let mountDirectory: string = rootDirectory; + if (process.env.SINDRI_DEVELOPMENT_HOST_ROOT) { + if (rootDirectory === "/sindri" || rootDirectory.startsWith("/sindri/")) { + mountDirectory = rootDirectory.replace( + "/sindri", + process.env.SINDRI_DEVELOPMENT_HOST_ROOT, + ); + logger?.debug( + `Remapped "${rootDirectory}" to "${mountDirectory}" for bind mount on the Docker host.`, + ); + } else { + logger?.fatal( + `The root directory path "${rootDirectory}" must be under "/sindri/"` + + 'when using "SINDRI_DEVELOPMENT_HOST_ROOT".', + ); + return process.exit(1); + } + } + + // Remap the current working directory to its location inside the container. If the user is in a + // subdirectory of the project root, we need to remap the current working directory to the same + // subdirectory inside the container. + const relativeCwd = path.relative(rootDirectory, cwd); + let internalCwd: string; + if (relativeCwd.startsWith("..")) { + internalCwd = "/sindri/"; + logger?.warn( + `The current working directory ("${cwd}") is not under the project root ` + + `("${rootDirectory}"), will use the project root as the current working directory.`, + ); + } else { + internalCwd = path.join("/sindri/", relativeCwd); + } + logger?.debug( + `Remapped the "${cwd}" working directory to "${internalCwd}" in the Docker container.`, + ); + + // Run circomspect with the project root mounted and pipe the output to stdout. + const data: { StatusCode: number } = await new Promise((resolve, reject) => { + docker + .run( + image, + args, + tty ? [process.stdout, process.stderr] : devNull, + { + AttachStderr: tty, + AttachStdin: tty, + AttachStdout: tty, + HostConfig: { + Binds: [ + // Circuit project root. + `${mountDirectory}:/sindri`, + // Shared temporary directory. + `${os.tmpdir()}/sindri/:/tmp/sindri/`, + ], + }, + OpenStdin: tty, + StdinOnce: false, + Tty: tty, + WorkingDir: internalCwd, + }, + (error, data) => { + if (error) { + reject(error); + } else { + resolve(data); + } + }, + ) + .on("container", (container) => { + if (!tty) return; + + // Attach stdin/stdout/stderr if we're running in TTY mode. + const stream = container.attach( + { + stream: true, + stdin: true, + stdout: true, + stderr: true, + }, + function (error: Error, stream: Duplex) { + if (error) { + reject(error); + } + + // Connect stdin and stdout. + // Note that stderr is redirected into stdout because this is the normal TTY behavior. + stream.pipe(process.stdout); + }, + ); + + // Ensure the stream is resumed because streams start paused. + if (stream) { + stream.resume(); + } + }); + }); + return data.StatusCode; +} + +/** + * Executes a command locally. + * + * @param command - The command to execute. + * @param args - The arguments to pass to the command. + * @param options - Additional options for the command. + * @param options.cwd - The current working directory for the executed command. + * @param options.logger - The logger to use for logging messages. There will be no logging if not + * specified. + * @param options.tty - Whether to use a TTY for the command. Defaults to `false` which means that + * the command's output will be ignored. + * + * @returns The exit code of the command. + */ +export async function execLocalCommand( + command: ExternalCommand, + args: string[] = [], + { + cwd = process.cwd(), + logger, + tty = false, + }: { + cwd?: string; + logger?: Logger; + tty?: boolean; + }, +): Promise { + const child = spawn(command, args, { + cwd, + stdio: tty ? "inherit" : "ignore", + }); + try { + const code: number = await new Promise((resolve, reject) => { + child.on("error", (error) => { + reject(error); + }); + child.on("close", (code, signal) => { + // If the command exits with a signal (e.g. `SIGABRT`), then follow the common convention of + // mapping this to an exit code of: 128 + (the signal number). + if (code == null && signal != null) { + code = 128 + os.constants.signals[signal]; + } + assert(code != null); + resolve(code); + }); + }); + return code; + } catch (error) { + logger?.error(`Failed to execute the "${command}" command.`); + logger?.error(error); + return process.exit(1); + } +} + /** * Checks whether or not a file (including directories) exists. * @@ -60,6 +448,47 @@ export function findFileUpwards( return findFileUpwards(filename, parentDirectory); } +/** + * Retrieves the available tags for a Docker image from DockerHub, ordered from oldest to newest. + * + * @param repository - The name of the Docker image repository. + * @param username - The DockerHub username of the repository owner (default: "sindrilabs"). + * + * @returns An array of available tags for the Docker image. + */ +export async function getDockerImageTags( + repository: string, + username: string = "sindrilabs", +): Promise { + const url = `https://hub.docker.com/v2/repositories/${username}/${repository}/tags/`; + const { + data: { results }, + } = await axios.get<{ + results: Array<{ + last_updated: string; + name: string; + tag_status: string; + }>; + }>(url); + return results + .filter(({ tag_status }) => tag_status === "active") + .filter(({ name }) => name !== "dev") + .sort((a, b) => a.last_updated.localeCompare(b.last_updated)) + .map(({ name }) => name); +} + +/** + * Determines if a string represents a truthy value. + * + * @param {string} str - The string to check for truthiness. + * + * @returns {boolean} `true` if the string represents a truthy value, otherwise `false`. + */ +export function isTruthy(str: string): boolean { + const truthyValues = ["1", "true", "t", "yes", "y", "on"]; + return truthyValues.includes(str.toLowerCase()); +} + /** * Loads the project's `package.json` file. * diff --git a/yarn.lock b/yarn.lock index c212a64..8b16ae6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -47,6 +47,11 @@ chalk "^2.4.2" js-tokens "^4.0.0" +"@balena/dockerignore@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@balena/dockerignore/-/dockerignore-1.0.2.tgz#9ffe4726915251e8eb69f44ef3547e0da2c03e0d" + integrity sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q== + "@commander-js/extra-typings@^11.1.0": version "11.1.0" resolved "https://registry.npmjs.org/@commander-js/extra-typings/-/extra-typings-11.1.0.tgz" @@ -809,6 +814,22 @@ resolved "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz" integrity sha512-d6McJeGsuoRlwWZmVIeE8CUA27lu6jLjvv1JzqmpsytOYYbVi1tHZEnwCNVOXnj4pyLvneZlFlpXUK+X9wBWyw== +"@types/docker-modem@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/docker-modem/-/docker-modem-3.0.6.tgz#1f9262fcf85425b158ca725699a03eb23cddbf87" + integrity sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg== + dependencies: + "@types/node" "*" + "@types/ssh2" "*" + +"@types/dockerode@^3.3.23": + version "3.3.23" + resolved "https://registry.yarnpkg.com/@types/dockerode/-/dockerode-3.3.23.tgz#07b2084013d01e14d5d97856446f4d9c9f27c223" + integrity sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw== + dependencies: + "@types/docker-modem" "*" + "@types/node" "*" + "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz" @@ -852,6 +873,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^18.11.18": + version "18.19.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.19.18.tgz#7526471b28828d1fef1f7e4960fb9477e6e4369c" + integrity sha512-80CP7B8y4PzZF0GWx15/gVWRrB5y/bIjNI84NK3cmQJu0WZwvmj2WMA5LcofQFVfLqqCSp545+U2LsrVzX36Zg== + dependencies: + undici-types "~5.26.4" + "@types/nunjucks@^3.2.6": version "3.2.6" resolved "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz" @@ -874,6 +902,13 @@ resolved "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz" integrity sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg== +"@types/ssh2@*": + version "1.11.19" + resolved "https://registry.yarnpkg.com/@types/ssh2/-/ssh2-1.11.19.tgz#4f2ec691b0674ea1590915fe5114a9aeae0eb41d" + integrity sha512-ydbQAqEcdNVy2t1w7dMh6eWMr+iOgtEkqM/3K9RMijMaok/ER7L8GW6PwsOypHCN++M+c8S/UR9SgMqNIFstbA== + dependencies: + "@types/node" "^18.11.18" + "@types/tar-js@^0.3.5": version "0.3.5" resolved "https://registry.npmjs.org/@types/tar-js/-/tar-js-0.3.5.tgz" @@ -1171,6 +1206,13 @@ asap@^2.0.3: resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" integrity sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA== +asn1@^0.2.6: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + ast-types@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.4.tgz#ee0d77b343263965ecc3fb62da16e7222b2b6782" @@ -1305,6 +1347,13 @@ basic-ftp@^5.0.2: resolved "https://registry.yarnpkg.com/basic-ftp/-/basic-ftp-5.0.4.tgz#28aeab7bfbbde5f5d0159cd8bb3b8e633bbb091d" integrity sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA== +bcrypt-pbkdf@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + big-integer@^1.6.44: version "1.6.51" resolved "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz" @@ -1322,6 +1371,15 @@ bindings@^1.4.0: dependencies: file-uri-to-path "1.0.0" +bl@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + blueimp-md5@^2.10.0: version "2.19.0" resolved "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz" @@ -1361,7 +1419,7 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer@^5.2.1: +buffer@^5.2.1, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -1377,6 +1435,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +buildcheck@~0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.6.tgz#89aa6e417cfd1e2196e3f8fe915eb709d2fe4238" + integrity sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A== + bundle-name@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz" @@ -1474,6 +1537,11 @@ chokidar@^3.5.1, chokidar@^3.5.2: optionalDependencies: fsevents "~2.3.2" +chownr@^1.1.1: + version "1.1.4" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + chownr@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" @@ -1641,6 +1709,14 @@ cosmiconfig@9.0.0: js-yaml "^4.1.0" parse-json "^5.2.0" +cpu-features@~0.0.9: + version "0.0.9" + resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.9.tgz#5226b92f0f1c63122b0a3eb84cb8335a4de499fc" + integrity sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ== + dependencies: + buildcheck "~0.0.6" + nan "^2.17.0" + "crc32@>= 0.2.2": version "0.2.2" resolved "https://registry.npmjs.org/crc32/-/crc32-0.2.2.tgz" @@ -1783,6 +1859,25 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +docker-modem@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/docker-modem/-/docker-modem-5.0.3.tgz#50c06f11285289f58112b5c4c4d89824541c41d0" + integrity sha512-89zhop5YVhcPEt5FpUFGr3cDyceGhq/F9J+ZndQ4KfqNvfbJpPMfgeixFgUj5OjCYAboElqODxY5Z1EBsSa6sg== + dependencies: + debug "^4.1.1" + readable-stream "^3.5.0" + split-ca "^1.0.1" + ssh2 "^1.15.0" + +dockerode@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/dockerode/-/dockerode-4.0.2.tgz#dedc8529a1db3ac46d186f5912389899bc309f7d" + integrity sha512-9wM1BVpVMFr2Pw3eJNXrYYt6DT9k0xMcsSCjtPvyQ+xa1iPg/Mo3T/gUcwI0B2cczqCeCYRPF8yFYDwtFXT0+w== + dependencies: + "@balena/dockerignore" "^1.0.2" + docker-modem "^5.0.3" + tar-fs "~2.0.1" + doctrine@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" @@ -1805,7 +1900,7 @@ emoji-regex@^8.0.0: resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -end-of-stream@^1.1.0: +end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== @@ -2270,6 +2365,11 @@ formdata-node@^6.0.3: resolved "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz" integrity sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg== +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== + fs-extra@^11.1.1: version "11.1.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz" @@ -2661,7 +2761,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3101,6 +3201,11 @@ mitt@3.0.1: resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== +mkdirp-classic@^0.5.2: + version "0.5.3" + resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" + integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== + mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" @@ -3135,6 +3240,11 @@ mz@^2.7.0: object-assign "^4.0.1" thenify-all "^1.0.0" +nan@^2.17.0, nan@^2.18.0: + version "2.18.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.18.0.tgz#26a6faae7ffbeb293a39660e88a76b82e30b7554" + integrity sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" @@ -3680,7 +3790,7 @@ rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -readable-stream@^3.6.0: +readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.5.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -3828,7 +3938,7 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz" integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== -"safer-buffer@>= 2.1.2 < 3": +"safer-buffer@>= 2.1.2 < 3", safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -3977,6 +4087,11 @@ source-map@^0.6.1, source-map@~0.6.1: resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +split-ca@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split-ca/-/split-ca-1.0.1.tgz#6c83aff3692fa61256e0cd197e05e9de157691a6" + integrity sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ== + split2@^4.0.0: version "4.2.0" resolved "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz" @@ -3992,6 +4107,17 @@ sprintf-js@~1.0.2: resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssh2@^1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.15.0.tgz#2f998455036a7f89e0df5847efb5421748d9871b" + integrity sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw== + dependencies: + asn1 "^0.2.6" + bcrypt-pbkdf "^1.0.2" + optionalDependencies: + cpu-features "~0.0.9" + nan "^2.18.0" + stack-utils@^2.0.6: version "2.0.6" resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz" @@ -4132,11 +4258,32 @@ tar-fs@3.0.5: bare-fs "^2.1.1" bare-path "^2.1.0" +tar-fs@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.0.1.tgz#e44086c1c60d31a4f0cf893b1c4e155dabfae9e2" + integrity sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA== + dependencies: + chownr "^1.1.1" + mkdirp-classic "^0.5.2" + pump "^3.0.0" + tar-stream "^2.0.0" + tar-js@^0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/tar-js/-/tar-js-0.3.0.tgz" integrity sha512-9uqP2hJUZNKRkwPDe5nXxXdzo6w+BFBPq9x/tyi5/U/DneuSesO/HMb0y5TeWpfcv49YDJTs7SrrZeeu8ZHWDA== +tar-stream@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" + integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== + dependencies: + bl "^4.0.3" + end-of-stream "^1.4.1" + fs-constants "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.1.1" + tar-stream@^3.1.5: version "3.1.7" resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-3.1.7.tgz#24b3fb5eabada19fe7338ed6d26e5f7c482e792b" @@ -4292,6 +4439,11 @@ tunnel@0.0.6: resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +tweetnacl@^0.14.3: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + type-check@^0.4.0, type-check@~0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"