Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,29 @@

GasGuard supports command-line autocompletion for `bash`, `zsh`, and `fish`.

## Scan resource limits

Local scans check the number and combined size of supported source files before
starting analysis. The defaults are 10,000 files and 100 MiB of source. Files
under `node_modules`, `.git`, `target`, `dist`, and `build` are excluded by
default. Configure limits in `gasguard.config.json` or override them per run:

```json
{
"scan": {
"maxFiles": 10000,
"maxBytes": 104857600
}
}
```

```sh
gasguard scan ./contracts --max-files 5000 --max-bytes 52428800
```

Discovery stops as soon as a configured limit is exceeded. The CLI reports
`FILE_LIMIT_EXCEEDED` or `SIZE_LIMIT_EXCEEDED` and does not start analysis.

## Installation

To load completions in your current shell session, add the following to your profile (`~/.bashrc`, `~/.zshrc`, or your fish config):
Expand Down
38 changes: 38 additions & 0 deletions packages/cli/src/commands/scan.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,42 @@ describe("runScan", () => {
code: "SIZE_LIMIT_EXCEEDED",
});
});

it("accepts repositories exactly at the file and source-byte limits", async () => {
const sourceFiles = ["contract A {}", "contract B {}"];
await fs.writeFile(path.join(directory, "A.sol"), sourceFiles[0]);
await fs.writeFile(path.join(directory, "B.sol"), sourceFiles[1]);
const totalBytes = sourceFiles.reduce(
(total, source) => total + Buffer.byteLength(source),
0,
);

await expect(
collectScannableFiles(directory, {
maxFiles: sourceFiles.length,
maxBytes: totalBytes,
}),
).resolves.toHaveLength(sourceFiles.length);
});

it("rejects the first file or byte beyond the configured preflight limit", async () => {
const sourceFiles = ["contract A {}", "contract B {}"];
await fs.writeFile(path.join(directory, "A.sol"), sourceFiles[0]);
await fs.writeFile(path.join(directory, "B.sol"), sourceFiles[1]);
const totalBytes = sourceFiles.reduce(
(total, source) => total + Buffer.byteLength(source),
0,
);

await expect(
collectScannableFiles(directory, { maxFiles: sourceFiles.length - 1 }),
).rejects.toMatchObject<Partial<RepositoryAnalysisError>>({
code: "FILE_LIMIT_EXCEEDED",
});
await expect(
collectScannableFiles(directory, { maxBytes: totalBytes - 1 }),
).rejects.toMatchObject<Partial<RepositoryAnalysisError>>({
code: "SIZE_LIMIT_EXCEEDED",
});
});
});
19 changes: 12 additions & 7 deletions packages/cli/src/commands/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
Finding as EngineFinding,
} from "../../../../libs/engine/core/analyzer-interface";
import { ScanWatcher } from "../../../../src/analysis/watch/watcher";
import { glob } from "glob";
import { globIterate } from "glob";
import { loadCliConfig } from "../config/config-loader";
import {
finishProgress,
Expand Down Expand Up @@ -132,7 +132,11 @@ export const scanCommand = new Command("scan")
process.exitCode = 130;
return;
}
console.error(chalk.red(`Error during scan: ${error}`));
const message =
error instanceof RepositoryAnalysisError
? `${error.code}: ${error.message}`
: String(error);
console.error(chalk.red(`Error during scan: ${message}`));
process.exitCode = 1;
} finally {
process.removeListener("SIGINT", abort);
Expand Down Expand Up @@ -298,16 +302,16 @@ export async function collectScannableFiles(
const patterns = limits.include?.length
? limits.include
: ["**/*.sol", "**/*.vy", "**/*.rs"];
const discovered = await glob(patterns, {
const discovered = globIterate(patterns, {
cwd: path.resolve(dirPath),
absolute: true,
nodir: true,
ignore: [...defaultExcludes, ...(limits.exclude ?? [])],
});
for (const file of discovered
.filter((candidate) => extensions.includes(path.extname(candidate)))
.sort()) {
const seen = new Set<string>();
for await (const file of discovered) {
checkCancelled();
if (!extensions.includes(path.extname(file)) || seen.has(file)) continue;
if (files.length >= maxFiles) {
throw new RepositoryAnalysisError(
`Repository exceeds the ${maxFiles} file limit.`,
Expand All @@ -323,8 +327,9 @@ export async function collectScannableFiles(
}
totalBytes += fileStats.size;
files.push(file);
seen.add(file);
}
return files;
return files.sort();
}

function asConfigObject(value: unknown): Record<string, unknown> {
Expand Down
Loading