pub mod analyzers;
pub mod models;
use anyhow::{Result, anyhow};
use colored::*;
use std::path::PathBuf;
use std::collections::HashMap;
use crate::analyzers::Analyzer;
use crate::analyzers::missing_owner::MissingOwnerCheck;
use crate::analyzers::account_data_matching::AccountDataMatching;
use crate::analyzers::account_initialization::AccountInitialization;
use crate::analyzers::initialization_frontrunning::InitializationFrontrunning;
use crate::analyzers::arbitrary_cpi::ArbitraryCpi;
use crate::analyzers::closing_accounts::ClosingAccounts;
use crate::analyzers::duplicate_mutable_accounts::DuplicateMutableAccounts;
use crate::analyzers::bump_seed_canonicalization::MissingBumpSeedCanonicalization;
use crate::analyzers::pda_sharing::PdaSharing;
use crate::analyzers::type_cosplay::TypeCosplay;
use crate::analyzers::reentrancy::ReentrancyAnalyzer;
use crate::analyzers::unauthorized_access::UnauthorizedAccessAnalyzer;
use crate::analyzers::integer_overflow::IntegerOverflowAnalyzer;
use crate::analyzers::invalid_sysvar_accounts::InvalidSysvarAccounts;
use crate::analyzers::improper_instruction_introspection::ImproperInstructionIntrospection;
use crate::analyzers::account_reloading::AccountReloading;
use crate::analyzers::precision_loss::PrecisionLossAnalyzer;
use crate::analyzers::insecure_randomness::InsecureRandomnessAnalyzer;
use crate::analyzers::seed_collision::SeedCollision;
use crate::models::Program;
pub use crate::analyzers::{Finding, Severity, Certainty, Location};
pub use crate::models::markdown;
pub fn analyze_program_dir(program_path: PathBuf) -> Result<Vec<Finding>> {
let program = Program::new(program_path)?;
run_analyzers(&program)
}
pub fn analyze_program<T>(_program_module: T) -> Result<Vec<Finding>> {
let module_name = std::any::type_name::<T>();
let suppress_output = std::env::var("SOLANA_FENDER_SUPPRESS_OUTPUT")
.unwrap_or_else(|_| "false".to_string())
.parse::<bool>()
.unwrap_or(false);
if !suppress_output {
println!("Analyzing module: {}", module_name);
}
let mut asts = std::collections::HashMap::new();
let file_path = PathBuf::from(format!("{}.rs", module_name));
let simple_module_name = module_name.split("::").last().unwrap_or("synthetic_module");
let file_content = format!(r#"
use anchor_lang::prelude::*;
#[program]
pub mod {} {{
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {{
Ok(())
}}
}}
#[derive(Accounts)]
pub struct Initialize {{}}
"#, simple_module_name);
let file = syn::parse_file(&file_content)
.map_err(|e| anyhow!("Failed to parse synthetic module: {}", e))?;
asts.insert(file_path, file);
let program = Program {
asts,
root_path: PathBuf::from("."),
};
run_analyzers(&program)
}
pub fn analyze_program_by_name(module_name: &str) -> Result<Vec<Finding>> {
let suppress_output = std::env::var("SOLANA_FENDER_SUPPRESS_OUTPUT")
.unwrap_or_else(|_| "false".to_string())
.parse::<bool>()
.unwrap_or(false);
if !suppress_output {
println!("Analyzing module by name: {}", module_name);
}
let file_path = locate_module_file(module_name)
.ok_or_else(|| anyhow!("Could not locate source file for module: {}", module_name))?;
if !suppress_output {
println!("Found module source at: {}", file_path.display());
}
analyze_program_file(file_path)
}
fn locate_module_file(module_name: &str) -> Option<PathBuf> {
let path_parts: Vec<&str> = module_name.split("::").collect();
let relative_path = path_parts.join(std::path::MAIN_SEPARATOR.to_string().as_str());
let search_paths = vec![
PathBuf::from(format!("{}.rs", relative_path)),
PathBuf::from(format!("{}/mod.rs", relative_path)),
PathBuf::from(format!("src/{}.rs", relative_path)),
PathBuf::from(format!("src/{}/mod.rs", relative_path)),
];
for path in search_paths {
if path.exists() {
return Some(path);
}
}
let lib_path = PathBuf::from("src/lib.rs");
if lib_path.exists() {
if let Ok(content) = std::fs::read_to_string(&lib_path) {
let simple_name = path_parts.last().unwrap_or(&"");
if content.contains(&format!("mod {}", simple_name)) {
return Some(lib_path);
}
}
}
None
}
pub fn analyze_program_file(file_path: PathBuf) -> Result<Vec<Finding>> {
if !file_path.exists() {
return Err(anyhow!("File not found: {}", file_path.display()));
}
if file_path.extension().map_or(true, |ext| ext != "rs") {
return Err(anyhow!("Not a Rust file: {}", file_path.display()));
}
let program = Program::from_file(file_path)?;
run_analyzers(&program)
}
fn run_analyzers(program: &Program) -> Result<Vec<Finding>> {
let analyzers: Vec<Box<dyn Analyzer>> = vec![
Box::new(MissingOwnerCheck),
Box::new(AccountDataMatching),
Box::new(AccountInitialization),
Box::new(InitializationFrontrunning),
Box::new(ArbitraryCpi),
Box::new(ClosingAccounts),
Box::new(DuplicateMutableAccounts),
Box::new(MissingBumpSeedCanonicalization),
Box::new(PdaSharing),
Box::new(TypeCosplay),
Box::new(InvalidSysvarAccounts),
Box::new(ReentrancyAnalyzer),
Box::new(UnauthorizedAccessAnalyzer),
Box::new(IntegerOverflowAnalyzer),
Box::new(ImproperInstructionIntrospection),
Box::new(AccountReloading),
Box::new(PrecisionLossAnalyzer),
Box::new(InsecureRandomnessAnalyzer),
Box::new(SeedCollision),
];
let mut all_findings = Vec::new();
let debug_mode = std::env::var("SOLANA_FENDER_DEBUG")
.unwrap_or_else(|_| "false".to_string())
.parse::<bool>()
.unwrap_or(false);
let suppress_output = std::env::var("SOLANA_FENDER_SUPPRESS_OUTPUT")
.unwrap_or_else(|_| "false".to_string())
.parse::<bool>()
.unwrap_or(false);
for analyzer in analyzers {
let analyzer_name = analyzer.name();
if debug_mode && !suppress_output {
println!("\nRunning {}", analyzer_name.bold());
println!("{}", analyzer.description());
}
match analyzer.analyze(program) {
Ok(findings) => {
if findings.is_empty() {
if debug_mode && !suppress_output {
println!("{}", "✓ No issues found".green());
} else if !suppress_output {
println!("{} {}: {}", "✓".green(), analyzer_name, "No issues found".green());
}
} else {
if debug_mode && !suppress_output {
for finding in &findings {
println!("\n{} ({:?}, {:?})", "Issue found:".yellow(), finding.severity, finding.certainty);
println!(" → {}", finding.message);
println!(" at {}:{}:{}", finding.location.file, finding.location.line, finding.location.column);
}
} else if !suppress_output {
println!("{} {}: {} issues found", "❌".red(), analyzer_name, findings.len());
for (i, finding) in findings.iter().enumerate() {
let severity_colored = match finding.severity {
Severity::Low => format!("[{}]", finding.severity).yellow(),
Severity::Medium => format!("[{}]", finding.severity).truecolor(255, 165, 0), Severity::High => format!("[{}]", finding.severity).red(),
Severity::Critical => format!("[{}]", finding.severity).red().bold(),
};
println!(" {}. {} {} at {}:{}:{}",
i+1,
severity_colored,
finding.message,
finding.location.file,
finding.location.line,
finding.location.column);
}
}
all_findings.extend(findings);
}
}
Err(e) => {
let error_msg = format!("Error running analyzer {}: {}", analyzer_name, e);
if debug_mode && !suppress_output {
println!("{}: {}", "Error running analyzer".red(), e);
} else if !suppress_output {
println!("{} {}: Error - {}", "❌".red(), analyzer_name, e);
}
return Err(anyhow!(error_msg));
}
}
}
Ok(all_findings)
}
pub fn filter_findings_by_severity(
findings: Vec<Finding>,
ignore_low: bool,
ignore_medium: bool,
ignore_high: bool,
ignore_critical: bool,
) -> Vec<Finding> {
findings.into_iter()
.filter(|finding| {
match finding.severity {
Severity::Low => !ignore_low,
Severity::Medium => !ignore_medium,
Severity::High => !ignore_high,
Severity::Critical => !ignore_critical,
}
})
.collect()
}
pub fn findings_to_markdown(
findings: Vec<Finding>,
program_name: &str,
output_path: Option<&std::path::Path>,
) -> Result<String> {
let suppress_output = std::env::var("SOLANA_FENDER_SUPPRESS_OUTPUT")
.unwrap_or_else(|_| "false".to_string())
.parse::<bool>()
.unwrap_or(false);
let mut findings_map: HashMap<PathBuf, Vec<models::markdown::Finding>> = HashMap::new();
for finding in findings {
let file_path = PathBuf::from(&finding.location.file);
let mut markdown_finding = models::markdown::Finding::new(
&format!("{:?} Severity Issue", finding.severity),
&format!("{}", finding.severity),
finding.location.line,
&finding.message,
);
if let Ok(file_content) = std::fs::read_to_string(&file_path) {
let lines: Vec<&str> = file_content.lines().collect();
let start_line = finding.location.line.saturating_sub(2);
let end_line = std::cmp::min(finding.location.line + 2, lines.len());
if start_line < end_line && start_line < lines.len() {
let snippet = lines[start_line..end_line].join("\n");
markdown_finding.code_snippet = Some(snippet);
}
} else if !suppress_output {
eprintln!("Warning: Could not read file {} for code snippet", file_path.display());
}
let recommendation = match finding.severity {
Severity::Critical => format!("This is a critical issue that must be fixed immediately. {}", get_recommendation_for_finding(&finding)),
Severity::High => format!("This is a high severity issue that should be addressed promptly. {}", get_recommendation_for_finding(&finding)),
Severity::Medium => format!("This is a medium severity issue that should be reviewed. {}", get_recommendation_for_finding(&finding)),
Severity::Low => format!("This is a low severity issue. {}", get_recommendation_for_finding(&finding)),
};
markdown_finding.recommendation = Some(recommendation);
findings_map.entry(file_path)
.or_insert_with(Vec::new)
.push(markdown_finding);
}
models::markdown::create_analysis_report(
program_name,
findings_map,
output_path,
)
}
fn get_recommendation_for_finding(finding: &Finding) -> String {
if finding.message.contains("owner check") {
"Implement proper owner checks to ensure account ownership is validated before use."
} else if finding.message.contains("data matching") || finding.message.contains("account data") {
"Ensure account data is properly validated and matches expected types."
} else if finding.message.contains("initialization") {
"Verify that accounts are properly initialized before use."
} else if finding.message.contains("CPI") {
"Review Cross-Program Invocation (CPI) calls to ensure they are secure and authorized."
} else if finding.message.contains("closing") {
"Ensure accounts are properly closed and funds are transferred to the correct destination."
} else if finding.message.contains("duplicate") || finding.message.contains("mutable") {
"Check for duplicate mutable accounts to prevent unintended data modification."
} else if finding.message.contains("bump seed") || finding.message.contains("canonicalization") {
"Use canonical bump seeds for PDA derivation to ensure consistent account addressing."
} else if finding.message.contains("PDA sharing") {
"Avoid sharing PDAs between different logical entities."
} else if finding.message.contains("type cosplay") {
"Ensure account types are properly validated to prevent type confusion attacks."
} else if finding.message.contains("reentrancy") {
"Implement reentrancy guards to prevent reentrancy attacks."
} else if finding.message.contains("unauthorized") || finding.message.contains("access") {
"Implement proper access controls to prevent unauthorized access."
} else if finding.message.contains("integer") || finding.message.contains("overflow") {
"Use checked arithmetic operations to prevent integer overflow/underflow."
} else if finding.message.contains("sysvar") {
"Validate sysvar accounts against their proper sysvar::*::ID."
} else if finding.message.contains("randomness") {
"Use a secure randomness source like an Oracle (e.g. Switchboard, Chainlink) instead of predictable on-chain data."
} else {
"Review the code carefully and implement appropriate security measures."
}.to_string()
}