Skip to content

Commit

Permalink
Add sindri exec|x command with Circomspect support
Browse files Browse the repository at this point in the history
This adds a new `exec` subcommand (or `x` for short) that allows transparently
pulling and executing ZK tools in a Docker container with the local project
mounted. [Circomspect](https://github.com/trailofbits/circomspect) is currently
the only available tool, but we'll expand this with more commands in the
[docker-zkp](https://github.com/sindri-labs/docker-zkp) repository. The `sindri
lint` command was also updated to use a locally installed version of
Circomspect if it's available, and to otherwise fall back to using Docker.

Merges #77
  • Loading branch information
sangaline authored Feb 24, 2024
1 parent 4dedaa0 commit 78b80ff
Show file tree
Hide file tree
Showing 8 changed files with 764 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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` \
Expand Down
17 changes: 17 additions & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
113 changes: 113 additions & 0 deletions src/cli/exec.ts
Original file line number Diff line number Diff line change
@@ -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 <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);
}
});
2 changes: 2 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -25,6 +26,7 @@ export const program = new Command()
false,
)
.addCommand(configCommand)
.addCommand(execCommand)
.addCommand(initCommand)
.addCommand(deployCommand)
.addCommand(lintCommand)
Expand Down
80 changes: 43 additions & 37 deletions src/cli/lint.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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()
Expand Down Expand Up @@ -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}".`,
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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);
}
}

Expand Down
Loading

0 comments on commit 78b80ff

Please sign in to comment.