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
54 changes: 54 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ reqwest = { version = "0.12", features = ["json"] }
toml = "0.8"
# Directory utilities
dirs = "6.0"
# Cross-platform browser opening
opener = "0.7"

[dev-dependencies]
tempfile = "3.8"
Expand Down
44 changes: 34 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ A comprehensive security analysis tool for Solana smart contracts that helps dev
- **Multiple Report Formats**: JSON, HTML, Markdown, and CSV outputs
- **Plugin System**: Extensible architecture for custom security rules
- **CI/CD Integration**: GitHub Actions support with automated security checks
- **Professional Reports**: Beautiful HTML reports with severity rankings and actionable recommendations
- **Professional Reports**: HTML reports with severity rankings and actionable recommendations
- **Smart Error Handling**: Clear, colored error messages with proper path validation
- **Comprehensive Examples**: 8 educational examples demonstrating vulnerabilities and secure patterns

Expand Down Expand Up @@ -52,6 +52,9 @@ solsec scan ./my-program --html-only --output results.html
# Generate multiple formats at once
solsec scan ./my-program --format json,html,markdown,csv

# Don't open browser automatically
solsec scan ./my-program --no-open

# Run fuzz testing
solsec fuzz ./my-solana-program --timeout 300
```
Expand All @@ -60,7 +63,9 @@ solsec fuzz ./my-solana-program --timeout 300

### `solsec scan`

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.
Run static analysis on your Solana smart contracts. Generates both JSON and HTML by default. If no path is provided, it recursively scans the current directory for all `.rs` files, automatically ignoring `target/` and `.git/` folders.

HTML reports automatically open in the default browser when running interactively, but remain closed in CI/automation environments.

```bash
solsec scan [PATH] [OPTIONS]
Expand All @@ -69,27 +74,31 @@ OPTIONS:
-c, --config <FILE> Configuration file path
-o, --output <DIR> Output directory [default: ./solsec-results]
-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)
--json-only Only generate JSON
--html-only Only generate HTML
--no-open Don't automatically open HTML report in browser
--fail-on-critical Exit with non-zero code on critical issues [default: true]

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

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

# Generate only JSON for CI/CD integration
# 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 HTML but don't open browser
solsec scan ./programs --html-only --no-open --output results.html

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

# Legacy: Scan with configuration file
# Scan with configuration file
solsec scan ./programs --config solsec.toml --output ./security-results
```

Expand Down Expand Up @@ -299,17 +308,32 @@ rm -rf ./tmp-security-results
echo "✅ Security scan passed!"
```

## Browser Opening Behavior

HTML reports automatically open in the default browser under the following conditions:

**Opens automatically when:**
- Running in an interactive terminal (not redirected)
- Generating HTML reports (`--html-only` or default formats)
- Not in CI/automation environments

**Remains closed when:**
- Running in CI environments (GitHub Actions, GitLab CI, etc.)
- Output is redirected or piped
- Using `--no-open` flag
- Only generating non-visual formats (JSON, CSV)

## 📊 Report Examples

### HTML Report
Beautiful, interactive HTML reports with:
Interactive HTML reports with:
- Executive summary with issue counts by severity
- Detailed findings with code snippets
- Actionable recommendations
- Responsive design for all devices

### JSON Report
Machine-readable format perfect for:
Machine-readable format for:
- CI/CD pipeline integration
- Custom tooling and analysis
- Data processing and metrics
Expand Down Expand Up @@ -368,7 +392,7 @@ solsec scan examples/unchecked_account/vulnerable.rs # 6 issues found
solsec scan examples/reentrancy/vulnerable.rs # 2 issues found

# Test secure examples (should find 0 issues)
solsec scan examples/*/secure.rs # All pass!
solsec scan examples/*/secure.rs # No issues found

# Comprehensive analysis
solsec scan examples/ # 26 total issues across all vulnerable examples
Expand Down
127 changes: 108 additions & 19 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ use crate::fuzz::FuzzEngine;
use crate::plugin::{PluginAction, PluginManager};
use crate::report::{ReportFormat, ReportGenerator};

#[derive(Debug)]
pub struct ScanConfig {
pub path: PathBuf,
pub config: Option<PathBuf>,
pub output: PathBuf,
pub formats: Vec<ReportFormat>,
pub json_only: bool,
pub html_only: bool,
pub no_open: bool,
pub fail_on_critical: bool,
}

#[derive(Parser)]
#[command(name = "solsec")]
#[command(about = "Solana Smart Contract Security Toolkit")]
Expand Down Expand Up @@ -46,6 +58,10 @@ pub enum Commands {
#[arg(long, conflicts_with = "format")]
html_only: bool,

/// Don't automatically open HTML report in browser (opens by default in interactive mode)
#[arg(long)]
no_open: bool,

/// Fail with non-zero exit code on critical issues
#[arg(long, default_value = "true")]
fail_on_critical: bool,
Expand Down Expand Up @@ -81,29 +97,28 @@ pub enum Commands {
},
}

pub async fn handle_scan_command(
path: PathBuf,
config: Option<PathBuf>,
output: PathBuf,
formats: Vec<ReportFormat>,
json_only: bool,
html_only: bool,
fail_on_critical: bool,
) -> Result<()> {
info!("Starting static analysis scan on: {}", path.display());
pub async fn handle_scan_command(config: ScanConfig) -> Result<()> {
info!(
"Starting static analysis scan on: {}",
config.path.display()
);

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

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

// Check if we should open HTML before generating reports
let should_open = should_open_html(&formats_to_generate, config.no_open);
let mut html_file_path: Option<PathBuf> = None;

// Generate reports in all requested formats
let report_gen = ReportGenerator::new();
for format in formats_to_generate {
Expand All @@ -114,14 +129,19 @@ pub async fn handle_scan_command(
ReportFormat::Csv => "csv",
};

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

// Track HTML file path for opening later
if matches!(format, ReportFormat::Html) {
html_file_path = Some(output_file.clone());
}

report_gen
.generate_report(&results, &output_file, format.clone())
.await?;
Expand All @@ -135,11 +155,18 @@ pub async fn handle_scan_command(
critical_count, high_count
);

if fail_on_critical && critical_count > 0 {
if config.fail_on_critical && critical_count > 0 {
error!("Critical issues found. Failing as requested.");
std::process::exit(1);
}

// Open HTML report in browser if appropriate
if should_open {
if let Some(html_path) = html_file_path {
open_html_file(&html_path)?;
}
}

Ok(())
}

Expand Down Expand Up @@ -199,3 +226,65 @@ pub async fn handle_plugin_command(action: PluginAction, path: Option<PathBuf>)

Ok(())
}

/// Detects if we're running in a CI environment
fn is_ci_environment() -> bool {
// Check common CI environment variables
std::env::var("CI").is_ok()
|| std::env::var("GITHUB_ACTIONS").is_ok()
|| std::env::var("GITLAB_CI").is_ok()
|| std::env::var("JENKINS_URL").is_ok()
|| std::env::var("TRAVIS").is_ok()
|| std::env::var("CIRCLECI").is_ok()
|| std::env::var("BUILDKITE").is_ok()
|| std::env::var("TF_BUILD").is_ok() // Azure DevOps
}

/// Detects if we're in an interactive terminal session
fn is_interactive() -> bool {
// Check if stdout is a terminal and not redirected
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}

/// Determines if we should automatically open the HTML report
fn should_open_html(formats: &[ReportFormat], no_open: bool) -> bool {
// Don't open if user explicitly disabled it
if no_open {
return false;
}

// Don't open in CI environments
if is_ci_environment() {
return false;
}

// Don't open if not in interactive terminal
if !is_interactive() {
return false;
}

// Only open if HTML is being generated
formats.contains(&ReportFormat::Html)
}

/// Opens the HTML file in the default browser
fn open_html_file(file_path: &PathBuf) -> Result<()> {
match opener::open(file_path) {
Ok(()) => {
info!(
"📖 Opening security report in browser: {}",
file_path.display()
);
Ok(())
}
Err(e) => {
warn!(
"Could not open HTML report in browser: {}. You can manually open: {}",
e,
file_path.display()
);
Ok(()) // Don't fail the entire command if browser opening fails
}
}
}
Loading
Loading