use crate::utils::audit::{AuditFinding, AuditSeverity};
use crate::utils::audit_parser::{ParsedCodeAnalysis, RustCodeParser};
use crate::utils::debug_logger::VerbosityLevel;
use crate::{debug_print, debug_warn};
use anyhow::Result;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::RwLock;
mod constants {
pub const SOLANA_PUBKEY_SIZE_BYTES: usize = 32;
pub const SOLANA_PUBKEY_MIN_LENGTH: usize = 32; pub const SOLANA_PUBKEY_MAX_LENGTH: usize = 44;
pub const BASE58_CHARS: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
pub const FALSE_POSITIVE_INDICATORS: &[&str] = &[
"test",
"mock",
"example",
"dummy",
"placeholder",
"lorem",
"ipsum",
"comment",
"doc",
"readme",
"license",
"copyright",
"author",
"guid",
"uuid",
"id64",
"hash",
"checksum",
"base64",
"encoded",
"string",
"sample",
"fake",
"default",
"null",
"zero",
"empty",
"temp",
"benchmark",
"perf",
"stress",
];
}
static FINDING_ID_COUNTER: AtomicUsize = AtomicUsize::new(1);
static SESSION_ID: Lazy<String> = Lazy::new(|| {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_else(|_| {
std::time::Duration::from_secs(1640995200) })
.as_secs();
format!("{:08x}", timestamp & 0xFFFFFFFF) });
fn create_regex(pattern: &str, name: &str) -> regex::Regex {
regex::Regex::new(pattern).unwrap_or_else(|e| {
panic!(
"Failed to compile regex pattern '{}' for {}: {}",
pattern, name, e
)
})
}
static REGEX_CACHE: Lazy<HashMap<&'static str, regex::Regex>> = Lazy::new(|| {
debug_print!(
VerbosityLevel::Detailed,
"Initializing regex cache with lazy loading"
);
HashMap::new() });
static REGEX_CACHE_STORE: Lazy<RwLock<HashMap<&'static str, regex::Regex>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
fn get_regex(pattern_name: &'static str) -> Option<regex::Regex> {
{
let cache = REGEX_CACHE_STORE.read().unwrap();
if let Some(regex) = cache.get(pattern_name) {
return Some(regex.clone());
}
}
let mut cache = REGEX_CACHE_STORE.write().unwrap();
if let Some(regex) = cache.get(pattern_name) {
return Some(regex.clone());
}
let regex = match pattern_name {
"password_pattern" => create_regex(r#"(?i)password\s*=\s*['"'][^'"']+['"']"#, pattern_name),
"api_key_pattern" => create_regex(r#"(?i)api_key\s*=\s*['"'][^'"']+['"']"#, pattern_name),
"secret_pattern" => create_regex(r#"(?i)secret\s*=\s*['"'][^'"']+['"']"#, pattern_name),
"hex_key_pattern" => create_regex(r#"['"'][0-9a-fA-F]{32,}['"']"#, pattern_name),
"base64_pattern" => create_regex(r#"['"'][A-Za-z0-9+/]{20,}={0,2}['"']"#, pattern_name),
"command_injection" => create_regex(
r#"Command::new.*(?:format!|concat!|user_input)"#,
pattern_name,
),
"shell_command" => create_regex(r#"(?:shell|system|exec|cmd|spawn)\("#, pattern_name),
"unsafe_exec" => create_regex(r#"process::Command.*(?:format!|user_input)"#, pattern_name),
"path_traversal" => create_regex(r#"\.\.[\\/]"#, pattern_name), "dynamic_path" => create_regex(r#"Path::new.*(?:format!|user_input)"#, pattern_name),
"http_insecure" => create_regex(r#"http://[^/]+"#, pattern_name),
"tls_bypass" => create_regex(r#"danger_accept_invalid_certs\(true\)"#, pattern_name),
"solana_signer" => create_regex(r#"AccountInfo.*is_signer"#, pattern_name),
"solana_pda" => create_regex(r#"find_program_address"#, pattern_name),
"solana_owner" => create_regex(r#"AccountInfo.*owner"#, pattern_name),
"rent_exempt" => create_regex(r#"rent_exempt"#, pattern_name),
_ => {
debug_warn!("Unknown regex pattern requested: {}", pattern_name);
return None;
}
};
cache.insert(pattern_name, regex.clone());
debug_print!(
VerbosityLevel::Verbose,
"Lazy-loaded regex pattern: {}",
pattern_name
);
Some(regex)
}
pub struct FindingIdAllocator;
impl FindingIdAllocator {
pub fn next_id() -> String {
Self::next_uuid_id()
}
pub fn next_legacy_id() -> String {
let id = FINDING_ID_COUNTER.fetch_add(1, Ordering::SeqCst);
format!("OSVM-{}-{:03}", &*SESSION_ID, id)
}
pub fn next_category_id(category: &str) -> String {
let uuid = uuid::Uuid::new_v4();
match category {
"solana" => format!("OSVM-SOL-{}-{}", &*SESSION_ID, uuid.simple()),
"crypto" => format!("OSVM-CRYPTO-{}-{}", &*SESSION_ID, uuid.simple()),
"network" => format!("OSVM-NET-{}-{}", &*SESSION_ID, uuid.simple()),
"auth" => format!("OSVM-AUTH-{}-{}", &*SESSION_ID, uuid.simple()),
_ => format!("OSVM-{}-{}", &*SESSION_ID, uuid.simple()),
}
}
pub fn next_uuid_id() -> String {
let uuid = uuid::Uuid::new_v4();
format!("OSVM-{}-{}", &*SESSION_ID, uuid.simple())
}
#[cfg(test)]
pub fn reset() {
FINDING_ID_COUNTER.store(1, Ordering::SeqCst);
}
pub fn get_session_id() -> &'static str {
&*SESSION_ID
}
}
pub trait AuditCheck {
fn name(&self) -> &'static str;
fn category(&self) -> &'static str;
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>>;
fn check_content(&self, content: &str, file_path: &str) -> Result<Vec<AuditFinding>> {
let analysis = RustCodeParser::parse_code(content)?;
self.check(&analysis, file_path)
}
}
pub struct MemorySafetyCheck;
impl AuditCheck for MemorySafetyCheck {
fn name(&self) -> &'static str {
"Memory Safety"
}
fn category(&self) -> &'static str {
"Memory"
}
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut findings = Vec::new();
for unsafe_block in &analysis.unsafe_blocks {
findings.push(AuditFinding {
id: FindingIdAllocator::next_id(),
title: "Unsafe code block detected".to_string(),
description: format!(
"File {} contains unsafe code blocks that bypass Rust's memory safety guarantees at line {}",
file_path, unsafe_block.line
),
severity: AuditSeverity::Medium,
category: "Memory Safety".to_string(),
cwe_id: Some("CWE-119".to_string()),
cvss_score: Some(5.5),
impact: "Potential memory safety violations and buffer overflows".to_string(),
recommendation: "Review unsafe code blocks carefully, ensure proper bounds checking and memory management".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html".to_string(),
"https://cwe.mitre.org/data/definitions/119.html".to_string(),
],
});
}
let unwrap_count = analysis.unwrap_usages.len();
if unwrap_count > 5 {
findings.push(AuditFinding {
id: FindingIdAllocator::next_id(),
title: "Excessive unwrap/expect usage".to_string(),
description: format!(
"File {} contains {} instances of unwrap/expect which can cause panics",
file_path, unwrap_count
),
severity: AuditSeverity::Medium,
category: "Error Handling".to_string(),
cwe_id: Some("CWE-248".to_string()),
cvss_score: Some(4.0),
impact: "Application crashes due to unhandled panics, potential denial of service".to_string(),
recommendation: "Replace unwrap/expect with proper error handling using match or if let patterns".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://doc.rust-lang.org/book/ch09-00-error-handling.html".to_string(),
"https://cwe.mitre.org/data/definitions/248.html".to_string(),
],
});
}
Ok(findings)
}
}
pub struct CryptographyCheck;
impl AuditCheck for CryptographyCheck {
fn name(&self) -> &'static str {
"Cryptography"
}
fn category(&self) -> &'static str {
"Cryptography"
}
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut findings = Vec::new();
for secret in &analysis.hardcoded_secrets {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("crypto"),
title: "Hardcoded secret detected".to_string(),
description: format!(
"File {} contains a hardcoded {} at line {}",
file_path, secret.secret_type, secret.line
),
severity: AuditSeverity::High,
category: "Cryptography".to_string(),
cwe_id: Some("CWE-798".to_string()),
cvss_score: Some(8.0),
impact: "Exposed secrets could lead to unauthorized access".to_string(),
recommendation: "Remove hardcoded secrets and use environment variables or secure key management".to_string(),
code_location: Some(file_path.to_string()),
references: vec!["https://cwe.mitre.org/data/definitions/798.html".to_string()],
});
}
for crypto_op in &analysis.crypto_operations {
if crypto_op.operation_type.contains("weak") || crypto_op.operation_type.contains("md5")
{
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("crypto"),
title: "Weak cryptographic algorithm".to_string(),
description: format!(
"File {} uses weak cryptographic algorithm at line {}",
file_path, crypto_op.line
),
severity: AuditSeverity::Medium,
category: "Cryptography".to_string(),
cwe_id: Some("CWE-327".to_string()),
cvss_score: Some(5.0),
impact: "Use of weak cryptographic algorithms may allow attacks".to_string(),
recommendation: "Use strong, modern cryptographic algorithms".to_string(),
code_location: Some(file_path.to_string()),
references: vec!["https://cwe.mitre.org/data/definitions/327.html".to_string()],
});
}
}
Ok(findings)
}
}
pub struct NetworkSecurityCheck;
impl AuditCheck for NetworkSecurityCheck {
fn name(&self) -> &'static str {
"Network Security"
}
fn category(&self) -> &'static str {
"Network"
}
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut findings = Vec::new();
for network_op in &analysis.network_operations {
if !network_op.uses_https {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("network"),
title: "Insecure HTTP usage detected".to_string(),
description: format!(
"File {} uses HTTP instead of HTTPS at line {}",
file_path, network_op.line
),
severity: AuditSeverity::Medium,
category: "Network Security".to_string(),
cwe_id: Some("CWE-319".to_string()),
cvss_score: Some(5.0),
impact: "Data transmitted in plain text, susceptible to interception"
.to_string(),
recommendation: "Use HTTPS for all external network communications".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://cwe.mitre.org/data/definitions/319.html".to_string(),
"https://owasp.org/Top10/A02_2021-Cryptographic_Failures/".to_string(),
],
});
}
}
Ok(findings)
}
}
pub struct SolanaSecurityCheck;
impl AuditCheck for SolanaSecurityCheck {
fn name(&self) -> &'static str {
"Solana Security"
}
fn category(&self) -> &'static str {
"Solana"
}
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut findings = Vec::new();
for solana_op in &analysis.solana_operations {
if !solana_op.signer_check {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Missing signer validation in Solana operation".to_string(),
description: format!(
"File {} contains Solana operation without signer validation at line {}",
file_path, solana_op.line
),
severity: AuditSeverity::Critical,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-862".to_string()),
cvss_score: Some(9.0),
impact: "Unauthorized users could execute privileged operations".to_string(),
recommendation: "Always validate that required accounts are signers using is_signer checks".to_string(),
code_location: Some(format!("{}:{}", file_path, solana_op.line)),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html".to_string(),
"https://solana-labs.github.io/solana-program-library/anchor/lang/macro.Program.html".to_string(),
],
});
}
if !solana_op.owner_check && solana_op.operation_type.contains("account") {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Missing account owner validation".to_string(),
description: format!(
"File {} contains account operation without owner validation at line {}",
file_path, solana_op.line
),
severity: AuditSeverity::High,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-284".to_string()),
cvss_score: Some(7.5),
impact: "Programs could operate on accounts owned by malicious programs".to_string(),
recommendation: "Always verify account ownership before performing operations: account.owner == expected_program_id".to_string(),
code_location: Some(format!("{}:{}", file_path, solana_op.line)),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html".to_string(),
],
});
}
if !solana_op.program_id_check && solana_op.operation_type.contains("invoke") {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Missing program ID validation before CPI".to_string(),
description: format!(
"File {} contains Cross-Program Invocation without program ID validation at line {}",
file_path, solana_op.line
),
severity: AuditSeverity::Critical,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-20".to_string()),
cvss_score: Some(9.0),
impact: "Arbitrary program execution vulnerability - attacker can invoke malicious programs".to_string(),
recommendation: "Always validate the program ID before making cross-program invocations".to_string(),
code_location: Some(format!("{}:{}", file_path, solana_op.line)),
references: vec![
"https://docs.solana.com/developing/programming-model/calling-between-programs".to_string(),
"https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/0-arbitrary-cpi".to_string(),
],
});
}
if !solana_op.pda_seeds.is_empty() && solana_op.pda_seeds.len() < 2 {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Insufficient PDA seed uniqueness".to_string(),
description: format!(
"File {} uses PDA with insufficient seed uniqueness ({} seeds) at line {}",
file_path, solana_op.pda_seeds.len(), solana_op.line
),
severity: AuditSeverity::High,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-330".to_string()),
cvss_score: Some(7.0),
impact: "PDA collision attacks possible, unauthorized access to program accounts".to_string(),
recommendation: "Use multiple unique seeds for PDA creation including user-specific identifiers".to_string(),
code_location: Some(format!("{}:{}", file_path, solana_op.line)),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html".to_string(),
"https://github.com/coral-xyz/sealevel-attacks/tree/master/programs/3-pda-sharing".to_string(),
],
});
}
if !solana_op.account_data_validation
&& solana_op.operation_type.contains("AccountInfo")
{
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Missing account data validation".to_string(),
description: format!(
"File {} accesses account data without proper validation at line {}",
file_path, solana_op.line
),
severity: AuditSeverity::Medium,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-20".to_string()),
cvss_score: Some(6.0),
impact: "Account data confusion vulnerabilities, type safety bypasses".to_string(),
recommendation: "Always deserialize and validate account data before use, check discriminators".to_string(),
code_location: Some(format!("{}:{}", file_path, solana_op.line)),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html".to_string(),
],
});
}
}
self.check_solana_patterns(analysis, file_path, &mut findings);
self.check_mev_protection(analysis, file_path, &mut findings);
self.check_authority_transfer_patterns(analysis, file_path, &mut findings);
self.check_duplicate_mutable_accounts(analysis, file_path, &mut findings);
self.check_precision_arithmetic(analysis, file_path, &mut findings);
self.check_atomic_validations(analysis, file_path, &mut findings);
Ok(findings)
}
}
impl SolanaSecurityCheck {
fn is_valid_base58_pubkey(value: &str) -> bool {
if value.len() < constants::SOLANA_PUBKEY_MIN_LENGTH
|| value.len() > constants::SOLANA_PUBKEY_MAX_LENGTH
{
return false;
}
if !value.chars().all(|c| constants::BASE58_CHARS.contains(c)) {
return false;
}
match bs58::decode(value).into_vec() {
Ok(decoded) => decoded.len() == constants::SOLANA_PUBKEY_SIZE_BYTES,
Err(_) => false,
}
}
fn analyze_solana_key_confidence(
&self,
value: &str,
context: &str,
) -> (bool, f32, AuditSeverity) {
let context_lower = context.to_lowercase();
if context_lower != "string_literal" {
for &indicator in constants::FALSE_POSITIVE_INDICATORS {
if context_lower.contains(indicator) {
return (false, 0.0, AuditSeverity::Info);
}
}
}
let mut confidence: f32 = 0.0;
if self.is_known_solana_program_id(value) {
confidence += 0.9;
}
let strong_indicators = [
("pubkey", 0.8),
("program_id", 0.9),
("account", 0.6),
("signer", 0.7),
("authority", 0.7),
("mint", 0.8),
("pda", 0.9),
("system_program", 0.9),
("spl_token", 0.8),
("metaplex", 0.8),
("anchor", 0.7),
("solana_sdk", 0.9),
("solana_program", 0.9),
("anchor_lang", 0.8),
];
for (indicator, weight) in &strong_indicators {
if context_lower.contains(indicator) {
confidence += weight;
break; }
}
let naming_patterns = [
("_pubkey", 0.7),
("_program", 0.6),
("_account", 0.5),
("pubkey_", 0.7),
("program_", 0.6),
("solana_", 0.6),
];
for (pattern, weight) in &naming_patterns {
if context_lower.contains(pattern) {
confidence += weight * 0.8; break;
}
}
let crypto_indicators = [
("crypto", 0.3),
("blockchain", 0.3),
("defi", 0.4),
("web3", 0.4),
("transaction", 0.2),
("wallet", 0.3),
];
for (indicator, weight) in &crypto_indicators {
if context_lower.contains(indicator) {
confidence += weight;
break;
}
}
let is_likely = confidence > 0.4;
let severity = if confidence > 0.8 {
AuditSeverity::Medium
} else if confidence > 0.6 {
AuditSeverity::Low
} else if confidence > 0.4 {
AuditSeverity::Info
} else {
AuditSeverity::Info
};
(is_likely, confidence.min(1.0), severity)
}
fn is_known_solana_program_id(&self, value: &str) -> bool {
let known_program_ids = [
"11111111111111111111111111111112", "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL", "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", "p1exdMJcjVao65QdewkaZRUnU6VPSXhus9n2GzWfh98", "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", "DjVE6JNiYqPL2QXyCUUh8rNjHrbz9hXHNYt99MQ59qw1", "JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo", ];
known_program_ids.contains(&value)
}
fn check_solana_patterns(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
for string_lit in &analysis.string_literals {
if Self::is_valid_base58_pubkey(&string_lit.value) {
let (is_likely_key, confidence, severity) =
self.analyze_solana_key_confidence(&string_lit.value, &string_lit.context);
if is_likely_key {
let confidence_desc = if confidence > 0.8 {
"high confidence"
} else if confidence > 0.6 {
"medium confidence"
} else {
"low confidence"
};
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: format!("Potential hardcoded Solana public key ({})", confidence_desc),
description: format!(
"File {} contains what appears to be a hardcoded base58-encoded public key at line {} (confidence: {:.1}%): '{}'{}",
file_path,
string_lit.line,
confidence * 100.0,
&string_lit.value[..12], if confidence > 0.8 { "" } else { " - manual review recommended" }
),
severity,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-798".to_string()),
cvss_score: Some(3.0 + (confidence * 4.0)), impact: if confidence > 0.8 {
"Hardcoded keys reduce flexibility and may expose sensitive information".to_string()
} else {
"Potential hardcoded key detected - requires manual verification".to_string()
},
recommendation: format!(
"{}. {}",
if confidence > 0.8 {
"Use environment variables or configuration for public keys"
} else {
"Verify if this is actually a Solana public key"
},
"Consider using the Pubkey::from_str() function with constants if this is a legitimate program ID"
),
code_location: Some(format!("{}:{}", file_path, string_lit.line)),
references: vec![
"https://docs.solana.com/developing/programming-model/accounts".to_string(),
"https://docs.rs/solana-sdk/latest/solana_sdk/pubkey/struct.Pubkey.html".to_string(),
],
});
}
}
}
let code_patterns = [
("rent_exempt", "Missing rent exemption check"),
("lamports", "Potential lamports manipulation"),
("close_account", "Account closure without proper validation"),
];
for (pattern, issue) in code_patterns.iter() {
if let Some(regex) = get_regex(pattern) {
let has_pattern = analysis
.solana_operations
.iter()
.any(|op| op.operation_type.contains(pattern));
if has_pattern {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: issue.to_string(),
description: format!(
"File {} contains Solana operations related to {}",
file_path, pattern
),
severity: AuditSeverity::Medium,
category: "Solana Security".to_string(),
cwe_id: Some("CWE-20".to_string()),
cvss_score: Some(5.5),
impact: "Potential Solana-specific security vulnerability".to_string(),
recommendation: format!(
"Review {} operations for proper validation",
pattern
),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html".to_string()
],
});
}
}
}
}
fn check_mev_protection(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
let mev_indicators = [
("slippage", "Missing slippage protection"),
("deadline", "Missing deadline protection"),
("oracle", "Oracle price manipulation risk"),
("swap", "Unprotected swap operation"),
];
for (pattern, issue) in mev_indicators.iter() {
let has_pattern = analysis
.solana_operations
.iter()
.any(|op| op.operation_type.to_lowercase().contains(pattern));
let has_protection = analysis.string_literals.iter().any(|lit| {
lit.value
.to_lowercase()
.contains(&format!("{}_protection", pattern))
});
if has_pattern && !has_protection {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: format!("MEV Risk: {}", issue),
description: format!(
"File {} contains {} operations without proper protection mechanisms",
file_path, pattern
),
severity: AuditSeverity::High,
category: "Solana MEV Protection".to_string(),
cwe_id: Some("CWE-367".to_string()),
cvss_score: Some(7.5),
impact: format!("Potential {} manipulation and MEV attacks", pattern),
recommendation: format!("Implement {} protection with deadline checks and oracle validation", pattern),
code_location: Some(file_path.to_string()),
references: vec![
"https://docs.solana.com/developing/programming-model/transactions#atomic-transaction-processing".to_string(),
"https://book.anchor-lang.com/anchor_bts/security.html#mev-protection".to_string(),
],
});
}
}
}
fn check_authority_transfer_patterns(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
let has_authority_change = analysis.solana_operations.iter().any(|op| {
op.operation_type.contains("authority") || op.operation_type.contains("owner")
});
if has_authority_change {
let has_two_step = analysis.string_literals.iter().any(|lit| {
lit.value.contains("pending_authority") || lit.value.contains("accept_authority")
});
if !has_two_step {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Unsafe authority transfer pattern".to_string(),
description: format!(
"File {} contains authority transfer operations without two-step verification",
file_path
),
severity: AuditSeverity::High,
category: "Solana Authority Management".to_string(),
cwe_id: Some("CWE-269".to_string()),
cvss_score: Some(8.0),
impact: "Authority could be transferred to incorrect or malicious addresses".to_string(),
recommendation: "Implement two-step authority transfer with pending/accept pattern and proper validation".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#authority-transfer".to_string(),
"https://docs.solana.com/developing/programming-model/accounts#ownership".to_string(),
],
});
}
}
}
fn check_duplicate_mutable_accounts(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
let has_mutable_accounts = analysis
.solana_operations
.iter()
.any(|op| op.operation_type.contains("mutable") || op.operation_type.contains("mut"));
if has_mutable_accounts {
let has_dedup_check = analysis
.string_literals
.iter()
.any(|lit| lit.value.contains("duplicate") || lit.value.contains("unique"));
if !has_dedup_check {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Potential duplicate mutable accounts vulnerability".to_string(),
description: format!(
"File {} contains mutable account operations without duplicate checking",
file_path
),
severity: AuditSeverity::Medium,
category: "Solana Account Management".to_string(),
cwe_id: Some("CWE-694".to_string()),
cvss_score: Some(6.5),
impact: "Same account could be used multiple times in different roles, leading to unexpected behavior".to_string(),
recommendation: "Implement account deduplication checks before processing mutable accounts".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#duplicate-accounts".to_string(),
],
});
}
}
}
fn check_precision_arithmetic(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
let has_float_ops = analysis
.path_operations
.iter()
.any(|op| op.operation_type.contains("f32") || op.operation_type.contains("f64"));
if has_float_ops {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Floating-point arithmetic in financial calculations".to_string(),
description: format!(
"File {} uses floating-point arithmetic which can cause precision loss in financial operations",
file_path
),
severity: AuditSeverity::High,
category: "Solana Financial Math".to_string(),
cwe_id: Some("CWE-682".to_string()),
cvss_score: Some(7.5),
impact: "Precision loss in financial calculations can lead to incorrect token amounts".to_string(),
recommendation: "Use fixed-point arithmetic or integer-based calculations for financial operations".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#numerical-precision".to_string(),
],
});
}
let has_div_before_mul = analysis
.path_operations
.iter()
.zip(analysis.path_operations.iter().skip(1))
.any(|(op1, op2)| {
op1.operation_type == "/" && op2.operation_type == "*" && op2.line == op1.line + 1
});
if has_div_before_mul {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Division before multiplication causing precision loss".to_string(),
description: format!(
"File {} performs division before multiplication, which can cause precision loss",
file_path
),
severity: AuditSeverity::Medium,
category: "Solana Financial Math".to_string(),
cwe_id: Some("CWE-682".to_string()),
cvss_score: Some(5.5),
impact: "Mathematical operations may lose precision, affecting financial calculations".to_string(),
recommendation: "Reorder operations to multiply before dividing, or use higher precision arithmetic".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#numerical-precision".to_string(),
],
});
}
}
fn check_atomic_validations(
&self,
analysis: &ParsedCodeAnalysis,
file_path: &str,
findings: &mut Vec<AuditFinding>,
) {
let has_validation = analysis.solana_operations.iter().any(|op| {
op.operation_type.contains("validate") || op.operation_type.contains("check")
});
let has_cpi = analysis
.solana_operations
.iter()
.any(|op| op.operation_type.contains("cpi") || op.operation_type.contains("invoke"));
let has_reload = analysis
.solana_operations
.iter()
.any(|op| op.operation_type.contains("reload"));
if has_cpi && !has_reload {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Missing account reload after CPI".to_string(),
description: format!(
"File {} contains CPI operations without subsequent account reloading",
file_path
),
severity: AuditSeverity::High,
category: "Solana CPI Safety".to_string(),
cwe_id: Some("CWE-362".to_string()),
cvss_score: Some(7.0),
impact: "Account data may be stale after CPI, leading to incorrect program behavior and potential race conditions".to_string(),
recommendation: "Always reload account data after CPI operations to ensure data consistency and prevent race conditions. Use account.reload() or fetch fresh account data.".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#account-reloading".to_string(),
"https://docs.solana.com/developing/programming-model/calling-between-programs#reentrancy".to_string(),
],
});
}
if has_validation {
let has_toctou_risk = analysis
.path_operations
.iter()
.any(|op| op.operation_type.contains("==") || op.operation_type.contains("!="));
if has_toctou_risk {
findings.push(AuditFinding {
id: FindingIdAllocator::next_category_id("solana"),
title: "Potential TOCTOU race condition in account validation".to_string(),
description: format!(
"File {} contains account validation that may be susceptible to time-of-check-time-of-use attacks",
file_path
),
severity: AuditSeverity::Medium,
category: "Solana Race Conditions".to_string(),
cwe_id: Some("CWE-367".to_string()),
cvss_score: Some(6.0),
impact: "Account state could change between validation and use, leading to security vulnerabilities".to_string(),
recommendation: "Ensure account validations are atomic and cannot be bypassed by concurrent operations. Consider using locks or ensuring operations are performed within the same transaction context.".to_string(),
code_location: Some(file_path.to_string()),
references: vec![
"https://book.anchor-lang.com/anchor_bts/security.html#atomic-operations".to_string(),
"https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use".to_string(),
],
});
}
}
}
}
pub struct InputValidationCheck;
impl AuditCheck for InputValidationCheck {
fn name(&self) -> &'static str {
"Input Validation"
}
fn category(&self) -> &'static str {
"Input"
}
fn check(&self, analysis: &ParsedCodeAnalysis, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut findings = Vec::new();
for cmd_exec in &analysis.command_executions {
if cmd_exec.is_dynamic {
let has_sanitization = analysis.string_literals.iter().any(|lit| {
lit.value.contains("sanitize")
|| lit.value.contains("validate")
|| lit.value.contains("escape")
});
let has_safe_patterns = analysis.string_literals.iter().any(|lit| {
lit.value.contains("shellwords")
|| lit.value.contains("shlex")
|| lit.value.contains("quote")
});
let severity = if has_sanitization || has_safe_patterns {
AuditSeverity::Medium } else {
AuditSeverity::High };
findings.push(AuditFinding {
id: FindingIdAllocator::next_id(),
title: "Potential command injection vulnerability".to_string(),
description: format!(
"File {} contains command execution with potentially unsafe input at line {}{}",
file_path,
cmd_exec.line,
if has_sanitization || has_safe_patterns { " (mitigation patterns detected)" } else { " (no mitigation detected)" }
),
severity,
category: "Input Validation".to_string(),
cwe_id: Some("CWE-78".to_string()),
cvss_score: Some(if has_sanitization || has_safe_patterns { 5.5 } else { 7.5 }),
impact: "Arbitrary command execution on the host system".to_string(),
recommendation: if has_sanitization || has_safe_patterns {
"Review current sanitization logic to ensure it's comprehensive. Consider using allowlists instead of blocklists.".to_string()
} else {
"Validate and sanitize all input before using in commands, use parameterized commands, or consider using safe command execution libraries.".to_string()
},
code_location: Some(format!("{}:{}", file_path, cmd_exec.line)),
references: vec![
"https://cwe.mitre.org/data/definitions/78.html".to_string(),
"https://owasp.org/Top10/A03_2021-Injection/".to_string(),
"https://docs.rs/shellwords/latest/shellwords/".to_string(),
],
});
}
}
for path_op in &analysis.path_operations {
if path_op.is_dynamic {
let has_path_sanitization = analysis.string_literals.iter().any(|lit| {
lit.value.contains("canonicalize")
|| lit.value.contains("Path::normalize")
|| lit.value.contains("path_clean")
|| lit.value.contains("resolve")
});
let has_path_validation = analysis.path_operations.iter().any(|op| {
op.line >= path_op.line.saturating_sub(5)
&& op.line <= path_op.line + 5
&& (op.operation_type.contains("starts_with")
|| op.operation_type.contains("contains"))
});
let severity = if has_path_sanitization || has_path_validation {
AuditSeverity::Low
} else {
AuditSeverity::High
};
findings.push(AuditFinding {
id: FindingIdAllocator::next_id(),
title: "Potential path traversal vulnerability".to_string(),
description: format!(
"File {} contains file operations with potentially unsafe paths at line {}{}",
file_path,
path_op.line,
if has_path_sanitization || has_path_validation { " (validation patterns detected)" } else { " (no validation detected)" }
),
severity,
category: "Input Validation".to_string(),
cwe_id: Some("CWE-22".to_string()),
cvss_score: Some(if has_path_sanitization || has_path_validation { 3.5 } else { 7.0 }),
impact: "Unauthorized access to files outside intended directory".to_string(),
recommendation: if has_path_sanitization || has_path_validation {
"Review path validation logic to ensure it prevents all traversal attempts including encoded sequences.".to_string()
} else {
"Validate and canonicalize file paths, use safe path construction methods, and implement proper bounds checking.".to_string()
},
code_location: Some(format!("{}:{}", file_path, path_op.line)),
references: vec![
"https://cwe.mitre.org/data/definitions/22.html".to_string(),
"https://owasp.org/Top10/A01_2021-Broken_Access_Control/".to_string(),
],
});
}
}
Ok(findings)
}
}
pub struct ModularAuditCoordinator {
checks: Vec<Box<dyn AuditCheck + Send + Sync>>,
}
impl ModularAuditCoordinator {
pub fn new() -> Self {
let checks: Vec<Box<dyn AuditCheck + Send + Sync>> = vec![
Box::new(MemorySafetyCheck),
Box::new(CryptographyCheck),
Box::new(NetworkSecurityCheck),
Box::new(SolanaSecurityCheck),
Box::new(InputValidationCheck),
];
Self { checks }
}
pub fn add_check(mut self, check: Box<dyn AuditCheck + Send + Sync>) -> Self {
self.checks.push(check);
self
}
pub fn audit_file(&self, content: &str, file_path: &str) -> Result<Vec<AuditFinding>> {
let mut all_findings = Vec::new();
let analysis = RustCodeParser::parse_code(content)?;
for check in &self.checks {
match check.check(&analysis, file_path) {
Ok(findings) => all_findings.extend(findings),
Err(e) => {
log::warn!(
"Check '{}' failed for file {}: {}",
check.name(),
file_path,
e
);
}
}
}
Ok(all_findings)
}
pub fn list_checks(&self) -> Vec<(&str, &str)> {
self.checks
.iter()
.map(|check| (check.name(), check.category()))
.collect()
}
}
impl Default for ModularAuditCoordinator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_finding_id_allocator() {
FindingIdAllocator::reset();
let id1 = FindingIdAllocator::next_id();
let id2 = FindingIdAllocator::next_id();
let id3 = FindingIdAllocator::next_category_id("solana");
assert!(id1.starts_with("OSVM-") && id1.contains("-")); assert!(id2.starts_with("OSVM-") && id2.contains("-"));
assert!(id3.starts_with("OSVM-SOL-") && id3.contains("-"));
assert_ne!(id1, id2);
assert_ne!(id1, id3);
assert_ne!(id2, id3);
}
#[test]
fn test_memory_safety_check() {
let code = r#"
fn test() {
unsafe {
let ptr = std::ptr::null_mut();
}
let result = Some(42).unwrap();
}
"#;
let check = MemorySafetyCheck;
let findings = check.check_content(code, "test.rs").unwrap();
assert!(!findings.is_empty());
}
#[test]
fn test_base58_validation() {
assert!(SolanaSecurityCheck::is_valid_base58_pubkey(
"TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
));
assert!(SolanaSecurityCheck::is_valid_base58_pubkey(
"11111111111111111111111111111112"
));
assert!(!SolanaSecurityCheck::is_valid_base58_pubkey(
"1111111111111111111111111111111O"
)); assert!(!SolanaSecurityCheck::is_valid_base58_pubkey(
"1111111111111111111111111111111I"
)); assert!(!SolanaSecurityCheck::is_valid_base58_pubkey(
"111111111111111111111111111111l0"
));
assert!(!SolanaSecurityCheck::is_valid_base58_pubkey(
"SGVsbG8gV29ybGQ="
));
assert!(!SolanaSecurityCheck::is_valid_base58_pubkey("123")); assert!(!SolanaSecurityCheck::is_valid_base58_pubkey(
"1".repeat(100).as_str()
)); }
#[test]
fn test_hardcoded_key_detection() {
let code_with_hardcoded_key = r#"
const PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
const SYSTEM_PROGRAM: &str = "11111111111111111111111111111112";
"#;
let code_with_base64 = r#"
const NOT_A_KEY: &str = "SGVsbG8gV29ybGQ="; // base64
const INVALID_BASE58: &str = "1111111111111111111111111111111O"; // Contains 'O'
"#;
let check = SolanaSecurityCheck;
let findings_hardcoded = check
.check_content(code_with_hardcoded_key, "test.rs")
.unwrap();
let hardcoded_findings: Vec<_> = findings_hardcoded
.iter()
.filter(|f| f.title.contains("hardcoded Solana public key"))
.collect();
assert_eq!(
hardcoded_findings.len(),
2,
"Should detect 2 hardcoded Solana keys"
);
let findings_base64 = check.check_content(code_with_base64, "test.rs").unwrap();
let base64_findings: Vec<_> = findings_base64
.iter()
.filter(|f| f.title.contains("hardcoded Solana public key"))
.collect();
assert_eq!(
base64_findings.len(),
0,
"Should not detect base64 or invalid base58 as Solana keys"
);
}
#[test]
fn test_modular_coordinator() {
let coordinator = ModularAuditCoordinator::new();
let checks = coordinator.list_checks();
assert!(checks.len() >= 5);
assert!(checks.iter().any(|(name, _)| *name == "Memory Safety"));
assert!(checks.iter().any(|(name, _)| *name == "Solana Security"));
}
#[test]
fn test_regex_cache() {
let pattern = get_regex("password_pattern").unwrap();
assert!(pattern.is_match(r#"password = "secret123""#));
assert!(!pattern.is_match("not a password"));
}
#[test]
fn test_lazy_regex_performance() {
use std::time::Instant;
let start = Instant::now();
for _ in 0..100 {
get_regex("password_pattern");
}
let cached_duration = start.elapsed();
let pattern = get_regex("password_pattern").unwrap();
assert!(pattern.is_match(r#"password = "secret123""#));
assert!(
cached_duration.as_millis() < 100,
"Cached regex calls should be fast"
);
let unknown = get_regex("nonexistent_pattern");
assert!(unknown.is_none());
}
}