use crate::ai::{
consensus_to_core_finding, create_provider, AuditContext, ConsensusConfig, ConsensusEngine,
GasAuditor, LlmProvider, LogicAuditor, SecurityAuditor,
};
use crate::chains::ChainRegistry;
use crate::core::{
AuditResult, AuditSummary, OutputFormat, ProjectConfig, RiskLevel, SecurityScores,
};
use crate::exploit;
use crate::gas;
use crate::plugins::PluginRegistry;
use crate::reports;
use crate::security::SecurityEngine;
use crate::utils::Cache;
use super::AuditArgs;
use anyhow::Result;
use std::time::Instant;
pub fn run(args: &AuditArgs) -> Result<()> {
let start = Instant::now();
let mut config = ProjectConfig::from_default_location();
apply_args(&mut config, args);
let cache = Cache::new(&config)?;
let chain_registry = ChainRegistry::default();
let plugin_registry = PluginRegistry::new(&config)?;
let security_engine = SecurityEngine::new(&config, &plugin_registry)?;
let source_files = discover_sources(&config)?;
if source_files.is_empty() {
anyhow::bail!("No Solidity source files found in {:?}", config.src_dirs);
}
let changed_files = cache.filter_changed_files(&source_files);
let skipped_count = source_files.len() - changed_files.len();
let files_to_analyze = if changed_files.is_empty() && !source_files.is_empty() {
source_files.clone()
} else {
changed_files
};
if skipped_count > 0 && !files_to_analyze.is_empty() {
eprintln!(
"⏭️ Skipping {} unchanged files (content hash matched)",
skipped_count
);
}
if args.quick {
eprintln!(
"⚡ Running quick audit on {} files (chain: {})...",
files_to_analyze.len(),
args.shared.chain
);
} else {
eprintln!(
"🔍 Running security audit on {} files (chain: {})...",
files_to_analyze.len(),
args.shared.chain
);
}
let findings = if args.quick {
security_engine.analyze_files_quick(&files_to_analyze, &chain_registry)?
} else {
security_engine.analyze_files(&files_to_analyze, &chain_registry)?
};
if !args.quick {
for file in &files_to_analyze {
let _ = cache.record_file_hash(file);
}
}
let exploit_findings = if !args.quick && (args.exploit || args.full) {
eprintln!("💥 Running exploit path analysis...");
exploit::analyze_exploit_paths(&findings, &files_to_analyze)?
} else {
Vec::new()
};
let gas_findings = if !args.quick && (args.gas || args.full) {
eprintln!("⛽ Running gas analysis...");
gas::analyze_gas(&files_to_analyze, &config)?
} else {
Vec::new()
};
let ai_findings = if !args.quick && args.ai {
run_ai_analysis(&files_to_analyze, args, &config)?
} else {
Vec::new()
};
let mut all_findings = findings;
all_findings.extend(exploit_findings);
all_findings.extend(gas_findings);
all_findings.extend(ai_findings);
let scores = security_engine.calculate_scores(&all_findings);
let overall_score = calculate_overall(&scores);
let risk_level = determine_risk_level(overall_score, &all_findings);
let production_ready = overall_score >= config.min_deployment_score;
let deployment_approved = !has_blocking_findings(&all_findings) && production_ready;
let duration = start.elapsed().as_secs_f64();
let result = AuditResult {
project_name: config.project_root.to_string_lossy().to_string(),
chain: args.shared.chain.clone(),
timestamp: chrono::Utc::now().to_rfc3339(),
duration_seconds: duration,
findings: all_findings.clone(),
scores,
overall_score,
risk_level,
production_ready,
deployment_approved,
summary: build_summary(&all_findings, &source_files),
};
match config.output {
OutputFormat::Json => {
println!("{}", serde_json::to_string_pretty(&result)?);
}
OutputFormat::Markdown => {
let md = reports::markdown::generate_report(&result)?;
println!("{}", md);
}
OutputFormat::Terminal => {
if args.quick || args.summary {
let summary = reports::generate_executive_summary(&result);
println!("{}", summary);
} else {
print_terminal_report(&result, args);
}
}
}
if args.shared.report {
let report_dir = std::path::PathBuf::from("reports");
std::fs::create_dir_all(&report_dir)?;
reports::json::write_report(&result, &report_dir.join("audit.json"))?;
let md = reports::markdown::generate_report(&result)?;
std::fs::write(report_dir.join("audit.md"), md)?;
eprintln!("📄 Reports saved to reports/");
}
cache.store("last_audit", &result)?;
if args.shared.strict && all_findings.iter().any(|f| f.severity.score() >= 3) {
anyhow::bail!("Strict mode: findings detected with severity MEDIUM or higher");
}
Ok(())
}
fn apply_args(config: &mut ProjectConfig, args: &AuditArgs) {
config.chain = args.shared.chain.clone();
config.project_root = args.shared.project.clone();
config.strict = args.shared.strict;
config.offline = args.shared.offline;
config.production = args.shared.production;
config.parallelism = args.shared.parallelism;
config.output = if args.shared.json {
OutputFormat::Json
} else if args.shared.markdown {
OutputFormat::Markdown
} else {
OutputFormat::Terminal
};
if let Some(exclude) = &args.exclude {
config.exclude = exclude.split(',').map(String::from).collect();
}
}
fn discover_sources(config: &ProjectConfig) -> Result<Vec<std::path::PathBuf>> {
let mut files = Vec::new();
for dir in &config.src_dirs {
let dir_path = if dir.is_absolute() {
dir.clone()
} else {
config.project_root.join(dir)
};
if !dir_path.exists() {
continue;
}
for entry in walkdir::WalkDir::new(&dir_path)
.into_iter()
.filter_entry(|e| {
!config
.exclude
.iter()
.any(|p| e.file_name().to_string_lossy().contains(p))
})
.filter_map(|e| e.ok())
{
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "sol") {
files.push(path.to_path_buf());
}
}
}
Ok(files)
}
fn calculate_overall(scores: &SecurityScores) -> u8 {
let vals = [
scores.access_control,
scores.security,
scores.fuzzing,
scores.gas,
scores.architecture,
scores.upgradeability,
scores.dependencies,
scores.deployment,
scores.proxy_safety,
scores.chain_compatibility,
scores.production_readiness,
scores.exploit_resistance,
];
(vals.iter().copied().map(u16::from).sum::<u16>() / vals.len() as u16) as u8
}
fn determine_risk_level(score: u8, findings: &[crate::core::Finding]) -> RiskLevel {
let has_critical = findings
.iter()
.any(|f| f.severity == crate::core::Severity::Critical);
let has_high = findings
.iter()
.any(|f| f.severity == crate::core::Severity::High);
if has_critical || score < 30 {
RiskLevel::Critical
} else if has_high || score < 50 {
RiskLevel::High
} else if score < 70 {
RiskLevel::Medium
} else if score < 85 {
RiskLevel::Low
} else {
RiskLevel::Minimal
}
}
fn has_blocking_findings(findings: &[crate::core::Finding]) -> bool {
findings.iter().any(|f| f.blocks_deployment)
}
fn build_summary(
findings: &[crate::core::Finding],
sources: &[std::path::PathBuf],
) -> AuditSummary {
let mut summary = AuditSummary {
total_findings: findings.len(),
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
info_count: 0,
files_analyzed: sources.len(),
lines_analyzed: 0,
contracts_analyzed: sources.len(),
};
for f in findings {
match f.severity {
crate::core::Severity::Critical => summary.critical_count += 1,
crate::core::Severity::High => summary.high_count += 1,
crate::core::Severity::Medium => summary.medium_count += 1,
crate::core::Severity::Low => summary.low_count += 1,
crate::core::Severity::Informational => summary.info_count += 1,
}
}
for src in sources {
if let Ok(content) = std::fs::read_to_string(src) {
summary.lines_analyzed += content.lines().count();
}
}
summary
}
fn print_terminal_report(result: &AuditResult, _args: &AuditArgs) {
use colored::*;
println!(
"\n{}",
"═══════════════════════════════════════════════".bright_blue()
);
println!(
"{}",
" FORGE AUDIT — SECURITY REPORT "
.bright_blue()
.bold()
);
println!(
"{}",
"═══════════════════════════════════════════════".bright_blue()
);
println!("\n📋 Project: {}", result.project_name);
println!("⛓️ Chain: {}", result.chain);
println!("🕐 Duration: {:.2}s", result.duration_seconds);
println!("📁 Files: {}", result.summary.files_analyzed);
println!("\n{}", "── Findings ──".bold());
println!(
" 🛑 Critical: {}",
result.summary.critical_count.to_string().red().bold()
);
println!(
" 🔴 High: {}",
result.summary.high_count.to_string().red()
);
println!(
" 🟡 Medium: {}",
result.summary.medium_count.to_string().yellow()
);
println!(
" 🔵 Low: {}",
result.summary.low_count.to_string().blue()
);
println!(
" ⚪ Info: {}",
result.summary.info_count.to_string().dimmed()
);
println!("\n{}", "── Scores ──".bold());
println!(
" 🔐 Access Control: {:>3}/100",
result.scores.access_control
);
println!(
" 🛡️ Security: {:>3}/100",
result.scores.security
);
println!(" 🎯 Fuzzing: {:>3}/100", result.scores.fuzzing);
println!(" ⛽ Gas: {:>3}/100", result.scores.gas);
println!(
" 🏗️ Architecture: {:>3}/100",
result.scores.architecture
);
println!(
" ⬆️ Upgradeability: {:>3}/100",
result.scores.upgradeability
);
println!(
" 📦 Dependencies: {:>3}/100",
result.scores.dependencies
);
println!(
" 🚀 Deployment: {:>3}/100",
result.scores.deployment
);
println!(
" 🔗 Proxy Safety: {:>3}/100",
result.scores.proxy_safety
);
println!(
" ⛓️ Chain Compat: {:>3}/100",
result.scores.chain_compatibility
);
println!(
" ✅ Production Ready: {:>3}/100",
result.scores.production_readiness
);
println!(
" 💥 Exploit Resistance: {:>3}/100",
result.scores.exploit_resistance
);
let overall_color = if result.overall_score >= 85 {
"green"
} else if result.overall_score >= 70 {
"yellow"
} else {
"red"
};
println!(
"\n{} {}",
"Overall Score:".bold(),
result.overall_score.to_string().color(overall_color).bold()
);
println!(
"{} {}",
"Risk Level:".bold(),
format!("{}", result.risk_level)
.color(match result.risk_level {
RiskLevel::Critical => "red",
RiskLevel::High => "red",
RiskLevel::Medium => "yellow",
RiskLevel::Low => "green",
RiskLevel::Minimal => "green",
})
.bold()
);
println!(
"{} {}",
"Production Ready:".bold(),
if result.production_ready {
"✅ YES".green().bold()
} else {
"❌ NO".red().bold()
}
);
println!(
"{} {}",
"Deployment:".bold(),
if result.deployment_approved {
"✅ APPROVED".green().bold()
} else {
"❌ BLOCKED".red().bold()
}
);
let critical_high: Vec<_> = result
.findings
.iter()
.filter(|f| {
f.severity == crate::core::Severity::Critical
|| f.severity == crate::core::Severity::High
})
.collect();
if !critical_high.is_empty() {
println!("\n{}", "── Top Findings ──".bold().red());
for f in critical_high.iter().take(5) {
println!(
"\n [{}] {}",
f.severity.to_string().red().bold(),
f.title.bold()
);
println!(
" 📄 {}:{}",
f.file.as_deref().unwrap_or("?"),
f.line.map_or("?".into(), |l| l.to_string())
);
println!(" 💡 {}", f.recommendation);
}
if critical_high.len() > 5 {
println!(
"\n ... and {} more critical/high findings",
critical_high.len() - 5
);
}
}
println!(
"\n{}",
"═══════════════════════════════════════════════".bright_blue()
);
}
fn run_ai_analysis(
files: &[std::path::PathBuf],
args: &AuditArgs,
config: &ProjectConfig,
) -> Result<Vec<crate::core::Finding>> {
use std::sync::Arc;
eprintln!(
"🤖 Running AI analysis (provider: {}, model: {})...",
args.ai_provider, args.ai_model
);
let boxed: Box<dyn LlmProvider> = create_provider(
&args.ai_provider,
&args.ai_model,
config.ai.temperature,
config.ai.max_tokens,
args.ai_api_key.clone(),
args.ollama_endpoint.clone(),
)?;
let provider: Arc<dyn LlmProvider> = Arc::from(boxed);
let mut engine = ConsensusEngine::new(ConsensusConfig {
min_confidence: config.ai.min_confidence,
..Default::default()
});
engine.register(Box::new(SecurityAuditor::new(Arc::clone(&provider))));
if args.ai_full || args.full {
engine.register(Box::new(GasAuditor::new(Arc::clone(&provider))));
engine.register(Box::new(LogicAuditor::new(Arc::clone(&provider))));
}
let mut all_ai_findings = Vec::new();
for file_path in files {
let source_code = match std::fs::read_to_string(file_path) {
Ok(c) => c,
Err(e) => {
eprintln!(" ⚠️ Could not read {}: {e}", file_path.display());
continue;
}
};
let file_name = file_path.to_string_lossy();
let ctx = AuditContext::new(&source_code, &file_name, &config.chain);
let report = engine.analyze(&ctx);
if report.auditor_count > 0 {
eprintln!(
" 🤖 {}: {} findings ({} consensus, {} filtered)",
file_name,
report.findings.len(),
report.deduplicated_count,
report.filtered_count
);
}
for cf in &report.findings {
all_ai_findings.push(consensus_to_core_finding(cf, &file_name));
}
}
eprintln!(
"🤖 AI analysis complete — {} findings reported",
all_ai_findings.len()
);
Ok(all_ai_findings)
}