Skip to content
Open
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
17 changes: 17 additions & 0 deletions packages/cli/src/commander-compat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Command } from "commander";

declare module "commander" {
interface Command {
addCommand(command: Command): this;
}
}

if (typeof (Command.prototype as any).addCommand !== "function") {
(Command.prototype as any).addCommand = function (command: Command) {
this.commands = this.commands || [];
this.commands.push(command);
return this;
};
}

export {};
2 changes: 1 addition & 1 deletion packages/cli/src/commands/annotate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { annotateFile, Annotation } from "../../../src/reporting/annotator";

export const annotateCommand = new Command("annotate")
.description("Annotate source files with inline issue comments")
.argument("<file>", "Source file to annotate")
.arguments("<file>")
.option("-o, --output <file>", "Output file path (default: <file>.annotated)")
.option("--line <n>", "Line number for a demo annotation", "1")
.action((file: string, options) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {

export const astCommand = new Command("ast")
.description("Inspect the AST of a smart contract source file")
.argument("<file>", "Path to a .sol, .rs, or .vy source file")
.arguments("<file>")
.option("--json", "Output full snapshot as JSON instead of the tree view")
.option("--compact", "Use compact (single-line) JSON (implies --json)")
.option("-o, --output <file>", "Write output to a file instead of stdout")
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import "../commander-compat";
import "../commander-compat";
import { Command } from "commander";
import chalk from "chalk";
import fs from "fs-extra";
Expand Down Expand Up @@ -29,8 +31,7 @@ const showConfigCommand = new Command("show")

const setConfigCommand = new Command("set")
.description("Set a configuration value")
.argument("<key>", "Configuration key (e.g., scan.maxFiles)")
.argument("<value>", "Configuration value")
.arguments("<key> <value>")
.action(async (key: string, value: string) => {
try {
const configPath = path.join(process.cwd(), "gasguard.config.json");
Expand Down
123 changes: 67 additions & 56 deletions packages/cli/src/commands/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@ import { Command } from "commander";
import chalk from "chalk";
import fs from "fs-extra";
import path from "path";
import { generateJsonReport } from "../../reporting/json-reporter";
import { generateSarifReport } from "../../reporting/sarif-reporter";
import { printSummary } from "../../reporting/summary-printer";
import { generateJsonReport, type ScanResult } from "../reporting/json-reporter";
import { generateSarifReport } from "../reporting/sarif-reporter";
import { printSummary } from "../reporting/summary-printer";
import { ScanWatcher } from "../../../../src/analysis/watch/watcher";

export interface ScanCommandOptions {
output?: string;
format: "json" | "sarif" | "text" | "both";
summary?: boolean;
fixPreview?: boolean;
watch?: boolean;
confidence: string;
}

export const scanCommand = new Command("scan")
.description("Scan smart contracts for gas optimization opportunities")
.argument("[path]", "Path to scan (default: current directory)", ".")
.arguments("[path]")
.option("-o, --output <file>", "Output file for JSON report")
.option(
"-f, --format <format>",
Expand All @@ -27,68 +36,26 @@ export const scanCommand = new Command("scan")
"Minimum confidence threshold (0.0-1.0)",
"0.7",
)
.action(async (scanPath: string, options) => {
.action(async (scanPath: string = ".", options: ScanCommandOptions) => {
try {
const runScan = async () => {
console.log(chalk.blue(`\n🔍 Scanning ${scanPath}...`));

// Collect scannable files
const files = await collectScannableFiles(scanPath);

if (files.length === 0) {
console.log(chalk.yellow("No scannable files found."));
return;
}

console.log(chalk.green(`Found ${files.length} file(s) to scan.`));

// Simulate scanning (in real implementation, this would use the actual scanner)
const scanResults = await simulateScan(files);

// Generate reports
if (options.format === "json" || options.format === "both") {
const outputPath =
options.output || path.join(process.cwd(), "gasguard-report.json");
await generateJsonReport(scanResults, outputPath);
console.log(chalk.green(`✓ JSON report saved to ${outputPath}`));
}
await runScan(scanPath, options);

if (options.format === "sarif") {
const outputPath =
options.output ||
path.join(process.cwd(), "gasguard-report.sarif.json");
await generateSarifReport(scanResults, outputPath);
console.log(chalk.green(`✓ SARIF report saved to ${outputPath}`));
}

if (
options.summary !== false &&
(options.format === "text" || options.format === "both")
) {
printSummary(scanResults, options);
}
};

// Perform initial scan
await runScan();

// Setup Watch Mode if requested
if (options.watch) {
console.log(
chalk.cyan(
`\n👀 Watch mode enabled. Listening for changes in ${scanPath}...`,
`\nWatch mode enabled. Listening for changes in ${scanPath}...`,
),
);

const watcher = new ScanWatcher(scanPath, {
ignored: (p) => p.includes("node_modules") || p.includes(".git"),
});

watcher.watch(async (filePath) => {
console.log(chalk.cyan(`\n[File Changed] ${filePath}`));
await runScan();
await runScan(scanPath, options);
});

// Keep the process alive
process.on("SIGINT", () => {
watcher.stop();
process.exit(0);
Expand All @@ -100,18 +67,64 @@ export const scanCommand = new Command("scan")
}
});

export async function runScan(
scanPath: string,
options: ScanCommandOptions,
): Promise<void> {
console.log(chalk.blue(`\nScanning ${scanPath}...`));

const files = await collectScannableFiles(scanPath);

if (files.length === 0) {
console.log(chalk.yellow("No scannable files found."));
return;
}

console.log(chalk.green(`Found ${files.length} file(s) to scan.`));

const scanResults = await simulateScan(files);

if (options.format === "json" || options.format === "both") {
const outputPath =
options.output || path.join(process.cwd(), "gasguard-report.json");
await generateJsonReport(scanResults, outputPath);
console.log(chalk.green(`JSON report saved to ${outputPath}`));
}

if (options.format === "sarif") {
const outputPath =
options.output || path.join(process.cwd(), "gasguard-report.sarif.json");
await generateSarifReport(scanResults, outputPath);
console.log(chalk.green(`SARIF report saved to ${outputPath}`));
}

if (
options.summary !== false &&
(options.format === "text" || options.format === "both")
) {
printSummary(scanResults, {
fixPreview: options.fixPreview,
confidence: Number(options.confidence),
});
}
}

async function collectScannableFiles(dirPath: string): Promise<string[]> {
const files: string[] = [];
const extensions = [".sol", ".vy", ".rs"];

const stats = await fs.stat(dirPath);
if (stats.isFile()) {
return extensions.includes(path.extname(dirPath)) ? [dirPath] : [];
}

async function walk(currentPath: string) {
const entries = await fs.readdir(currentPath, { withFileTypes: true });

for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);

if (entry.isDirectory()) {
// Skip node_modules and .git
if (
!["node_modules", ".git", "target", "dist", "build"].includes(
entry.name,
Expand All @@ -132,9 +145,8 @@ async function collectScannableFiles(dirPath: string): Promise<string[]> {
return files;
}

async function simulateScan(files: string[]): Promise<any> {
// This is a placeholder - in real implementation, this would use the actual scanner
const results = {
async function simulateScan(files: string[]): Promise<ScanResult> {
const results: ScanResult = {
timestamp: new Date().toISOString(),
scanPath: files[0] || ".",
totalFiles: files.length,
Expand All @@ -154,7 +166,6 @@ async function simulateScan(files: string[]): Promise<any> {
},
};

// Simulate some findings for demonstration
if (files.length > 0) {
results.findings.push({
file: files[0],
Expand Down
11 changes: 7 additions & 4 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#!/usr/bin/env node

import "./commander-compat";
import { Command } from "commander";
import chalk from "chalk";
import { scanCommand } from "./commands/scan";
Expand All @@ -19,10 +20,12 @@ program
.option("--no-color", "Disable colored output");

// Global error handling
program.configureOutput({
writeErr: (str: string) => process.stderr.write(chalk.red(str)),
writeOut: (str: string) => process.stdout.write(str),
});
if (typeof (program as any).configureOutput === "function") {
program.configureOutput({
writeErr: (str: string) => process.stderr.write(chalk.red(str)),
writeOut: (str: string) => process.stdout.write(str),
});
}

// Add commands
program.addCommand(scanCommand);
Expand Down
17 changes: 14 additions & 3 deletions packages/cli/src/reporting/summary-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,20 @@ export function printSummary(
);
}

function printSeverity(label: string, count: number, color: string): void {
const coloredCount =
count > 0 ? chalk[color](count.toString()) : chalk.gray("0");
function printSeverity(
label: string,
count: number,
color: "red" | "yellow" | "blue" | "gray",
): void {
const colorize =
color === "red"
? chalk.red
: color === "yellow"
? chalk.yellow
: color === "blue"
? chalk.blue
: chalk.gray;
const coloredCount = count > 0 ? colorize(count.toString()) : chalk.gray("0");
console.log(` ${label.padEnd(10)}: ${coloredCount}`);
}

Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/types/fs-extra.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "fs-extra";
4 changes: 4 additions & 0 deletions packages/rules/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ pub mod unused_state_variables;
pub mod vyper;

// Explicitly export core types to avoid ambiguity
pub use optimization::arrays::detect_dynamic_array_deletions;
pub use optimization::deployment::{estimate_bytecode_size, ExcessiveContractSizeRule};
pub use optimization::storage::detect_mapping_iteration;
pub use optimization::storage::{
detect_packing_opportunities, find_consecutive_packable_groups, get_type_size,
is_packable_type, PackingOpportunity, VariableInfo,
};
pub use rule_engine::{
extract_struct_fields, find_variable_usage, Rule, RuleEngine, RuleViolation, ViolationSeverity,
};
pub use security::{HardcodedAddressesRule, MissingDomainSeparationRule};
pub use solidity::{DynamicArrayDeletionRule, MappingIterationRule, StateVariablePackingRule};
pub use security::{HardcodedAddressesRule, MissingDomainSeparationRule, defi::MissingSlippageValidationRule};
pub use solidity::{StateVariablePackingRule, MappingIterationRule};
pub use optimization::storage::detect_mapping_iteration;
Expand Down
Loading
Loading