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
5 changes: 2 additions & 3 deletions .cursor/rules/rules.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ solana-smart-contract-security-toolkit/

### CLI Commands

1. **`solsec scan`** - Static analysis of Solana programs
1. **`solsec scan`** - Static analysis of Solana programs (now generates JSON & HTML by default!)
2. **`solsec fuzz`** - Automated fuzz testing with IDL generation
3. **`solsec report`** - Generate reports in multiple formats (JSON, HTML, Markdown, CSV)
4. **`solsec plugin`** - Manage security rule plugins
3. **`solsec plugin`** - Manage security rule plugins

### Built-in Security Rules

Expand Down
12 changes: 2 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -85,22 +85,14 @@ jobs:

- name: Run security scan on self
run: |
mkdir -p ./security-results
./target/release/solsec scan ./src --output ./security-results/analysis.json --format json
continue-on-error: true

- name: Generate security report
run: |
./target/release/solsec report ./security-results --output ./security-report.html --format html
./target/release/solsec scan ./src --output ./security-results
continue-on-error: true

- name: Upload security report
uses: actions/upload-artifact@v4
with:
name: security-report
path: |
./security-results/
./security-report.html
path: ./security-results/
retention-days: 30

- name: Comment PR with security results
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "solsec"
version = "0.1.5"
version = "0.1.6"
edition = "2021"
description = "Solana Smart Contract Security Toolkit - Find security bugs before deployment"
authors = ["Hasip Timurtas"]
Expand Down
65 changes: 33 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# 🛡️ Solana Smart Contract Security Toolkit (solsec)

[![Crates.io](https://img.shields.io/crates/v/solsec.svg)](https://crates.io/crates/solsec)
[![Downloads](https://img.shields.io/crates/d/solsec.svg)](https://crates.io/crates/solsec)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Rust](https://img.shields.io/badge/rust-2021-orange.svg)](https://www.rust-lang.org)

A comprehensive security analysis tool for Solana smart contracts that helps developers identify vulnerabilities before deployment through static analysis and fuzz testing.

## ✨ Features
Expand Down Expand Up @@ -32,46 +37,60 @@ cargo install --path .
### Basic Usage

```bash
# Scan the current project for security issues (recursively)
# Scan the current project and generates both JSON and HTML
solsec scan

# Scan a specific Solana program and set an output directory
solsec scan ./my-solana-program --output ./results

# Generate only JSON
solsec scan ./my-program --json-only --output results.json

# Generate only HTML
solsec scan ./my-program --html-only --output results.html

# Generate multiple formats at once
solsec scan ./my-program --format json,html,markdown,csv

# Run fuzz testing
solsec fuzz ./my-solana-program --timeout 300

# Generate an HTML report
solsec report ./results --output report.html --format html
```

## 📖 Commands

### `solsec scan`

Run static analysis on your Solana smart contracts. If no path is provided, it recursively scans the current directory for all `.rs` files, automatically ignoring `target/` and `.git/` folders.
Run static analysis on your Solana smart contracts. Generates both JSON and HTML If no path is provided, it recursively scans the current directory for all `.rs` files, automatically ignoring `target/` and `.git/` folders.

```bash
solsec scan [PATH] [OPTIONS]

OPTIONS:
-c, --config <FILE> Configuration file path
-o, --output <DIR> Output directory [default: ./solsec-results]
-f, --format <FORMAT> Output format [default: json] [possible values: json, html, markdown, csv]
-f, --format <FORMATS> Output formats (comma-separated) [default: json,html] [possible values: json, html, markdown, csv]
--json-only Only generate JSON (perfect for CI/CD)
--html-only Only generate HTML (perfect for humans)
--fail-on-critical Exit with non-zero code on critical issues [default: true]

EXAMPLES:
# Scan the entire project recursively (default behavior)
# Scan the entire project (generates both JSON and HTML!)
solsec scan

# Scan a specific directory
# Scan a specific directory with default formats
solsec scan ./programs/my-program

# Scan with a configuration file and custom output directory
solsec scan ./programs --config solsec.toml --output ./security-results
# Generate only JSON for CI/CD integration
solsec scan ./programs --json-only --output results.json

# Generate only HTML for manual review
solsec scan ./programs --html-only --output results.html

# Generate all available formats
solsec scan ./programs --format json,html,markdown,csv

# Scan a single file and output as HTML
solsec scan ./src/main.rs --format html
# Legacy: Scan with configuration file
solsec scan ./programs --config solsec.toml --output ./security-results
```

### `solsec fuzz`
Expand All @@ -91,22 +110,7 @@ EXAMPLES:
solsec fuzz ./programs --output ./custom-fuzz-results
```

### `solsec report`

Generate human-readable reports from analysis results.

```bash
solsec report <RESULTS> [OPTIONS]

OPTIONS:
-o, --output <FILE> Output file path [default: ./report.html]
-f, --format <FORMAT> Report format [default: html] [possible values: json, html, markdown, csv]

EXAMPLES:
solsec report ./solsec-results
solsec report ./results --output security-report.md --format markdown
solsec report ./results --format csv > issues.csv
```

### `solsec plugin`

Expand Down Expand Up @@ -253,16 +257,13 @@ jobs:

- name: Run security scan
run: |
solsec scan ./programs --output ./security-results --format json
solsec report ./security-results --output ./security-report.html
solsec scan ./programs --output ./security-results

- name: Upload security report
uses: actions/upload-artifact@v3
with:
name: security-report
path: |
./security-results/
./security-report.html
path: ./security-results/

- name: Fail on critical issues
run: |
Expand Down
84 changes: 44 additions & 40 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,17 @@ pub enum Commands {
#[arg(short, long, default_value = "./solsec-results")]
output: PathBuf,

/// Output format
#[arg(short, long, default_value = "json")]
format: ReportFormat,
/// Output format(s) - can specify multiple: json,html,markdown,csv
#[arg(short, long, default_value = "json,html", value_delimiter = ',')]
format: Vec<ReportFormat>,

/// Only generate JSON output (for CI/CD integration)
#[arg(long, conflicts_with = "format")]
json_only: bool,

/// Only generate HTML output (for human review)
#[arg(long, conflicts_with = "format")]
html_only: bool,

/// Fail with non-zero exit code on critical issues
#[arg(long, default_value = "true")]
Expand All @@ -62,21 +70,6 @@ pub enum Commands {
output: PathBuf,
},

/// Generate reports from analysis results
Report {
/// Path to results directory
#[arg(value_name = "RESULTS")]
results: PathBuf,

/// Output path for generated report
#[arg(short, long, default_value = "./report.html")]
output: PathBuf,

/// Report format
#[arg(short, long, default_value = "html")]
format: ReportFormat,
},

/// Manage security rule plugins
Plugin {
/// Plugin action
Expand All @@ -92,19 +85,47 @@ pub async fn handle_scan_command(
path: PathBuf,
config: Option<PathBuf>,
output: PathBuf,
format: ReportFormat,
formats: Vec<ReportFormat>,
json_only: bool,
html_only: bool,
fail_on_critical: bool,
) -> Result<()> {
info!("Starting static analysis scan on: {}", path.display());

let mut analyzer = StaticAnalyzer::new(config)?;
let results = analyzer.analyze_path(&path).await?;

// Generate report
// Determine which formats to generate
let formats_to_generate = if json_only {
vec![ReportFormat::Json]
} else if html_only {
vec![ReportFormat::Html]
} else {
formats
};

// Generate reports in all requested formats
let report_gen = ReportGenerator::new();
report_gen
.generate_report(&results, &output, format)
.await?;
for format in formats_to_generate {
let extension = match format {
ReportFormat::Json => "json",
ReportFormat::Html => "html",
ReportFormat::Markdown => "md",
ReportFormat::Csv => "csv",
};

let output_file = if output.extension().is_some() {
// If user provided a specific filename, respect it for the first format
output.clone()
} else {
// Generate appropriate filename based on format
output.join(format!("security-report.{}", extension))
};

report_gen
.generate_report(&results, &output_file, format.clone())
.await?;
}

let critical_count = results.iter().filter(|r| r.severity == "critical").count();
let high_count = results.iter().filter(|r| r.severity == "high").count();
Expand Down Expand Up @@ -145,23 +166,6 @@ pub async fn handle_fuzz_command(
Ok(())
}

pub async fn handle_report_command(
results: PathBuf,
output: PathBuf,
format: ReportFormat,
) -> Result<()> {
info!("Generating report from: {}", results.display());

let report_gen = ReportGenerator::new();
report_gen
.generate_from_directory(&results, &output, format)
.await?;

info!("Report generated: {}", output.display());

Ok(())
}

pub async fn handle_plugin_command(action: PluginAction, path: Option<PathBuf>) -> Result<()> {
let mut plugin_manager = PluginManager::new()?;

Expand Down
21 changes: 15 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,28 @@ async fn main() -> Result<()> {
config,
output,
format,
json_only,
html_only,
fail_on_critical,
} => cli::handle_scan_command(path, config, output, format, fail_on_critical).await,
} => {
cli::handle_scan_command(
path,
config,
output,
format,
json_only,
html_only,
fail_on_critical,
)
.await
}
Commands::Fuzz {
path,
timeout,
jobs,
output,
} => cli::handle_fuzz_command(path, timeout, jobs, output).await,
Commands::Report {
results,
output,
format,
} => cli::handle_report_command(results, output, format).await,

Commands::Plugin { action, path } => cli::handle_plugin_command(action, path).await,
}
}
56 changes: 0 additions & 56 deletions src/report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,62 +90,6 @@ impl ReportGenerator {
self.write_report(&report, output_path, format).await
}

pub async fn generate_from_directory(
&self,
results_dir: &Path,
output_path: &Path,
format: ReportFormat,
) -> Result<()> {
info!(
"Generating report from directory: {}",
results_dir.display()
);

// Load analysis results
let mut analysis_results = Vec::new();
let mut fuzz_results = None;

if results_dir.exists() {
for entry in fs::read_dir(results_dir)? {
let entry = entry?;
let path = entry.path();

if path.extension().is_some_and(|ext| ext == "json") {
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");

if filename.contains("analysis") || filename.contains("scan") {
let content = fs::read_to_string(&path)?;

// Try to parse as SecurityReport first (from scan command)
if let Ok(security_report) =
serde_json::from_str::<SecurityReport>(&content)
{
analysis_results.extend(security_report.analysis_results);
} else {
// Fallback to parsing as Vec<AnalysisResult> (legacy format)
let results: Vec<AnalysisResult> = serde_json::from_str(&content)
.with_context(|| {
format!(
"Failed to parse analysis results from: {}",
path.display()
)
})?;
analysis_results.extend(results);
}
} else if filename.contains("fuzz") {
let content = fs::read_to_string(&path)?;
fuzz_results = Some(serde_json::from_str(&content).with_context(|| {
format!("Failed to parse fuzz results from: {}", path.display())
})?);
}
}
}
}

let report = self.build_report(&analysis_results, fuzz_results).await?;
self.write_report(&report, output_path, format).await
}

async fn build_report(
&self,
analysis_results: &[AnalysisResult],
Expand Down
Loading
Loading