use crate::utils::debug_logger::VerbosityLevel;
use crate::{debug_print, debug_warn};
use anyhow::{Context, Result};
use quote::ToTokens;
use std::collections::HashMap;
use syn::{
visit::Visit, Expr, File, Item, ItemFn, ItemImpl, ItemMod, LitStr, Path, PathSegment, Stmt,
};
#[derive(Debug, Clone)]
pub struct LineTracker {
source_lines: Vec<String>,
line_content_map: HashMap<usize, String>,
}
impl LineTracker {
pub fn new(source_code: &str) -> Self {
let source_lines: Vec<String> = source_code.lines().map(|s| s.to_string()).collect();
let mut line_content_map = HashMap::new();
for (line_num, line_content) in source_lines.iter().enumerate() {
line_content_map.insert(line_num + 1, line_content.clone());
}
Self {
source_lines,
line_content_map,
}
}
pub fn find_pattern_line(&self, pattern: &str, start_from: usize) -> usize {
for (line_num, line_content) in self.line_content_map.iter() {
if *line_num >= start_from && line_content.contains(pattern) {
return *line_num;
}
}
start_from.max(1)
}
pub fn find_all_pattern_lines(&self, pattern: &str) -> Vec<usize> {
let mut lines = Vec::new();
for (line_num, line_content) in self.line_content_map.iter() {
if line_content.contains(pattern) {
lines.push(*line_num);
}
}
lines.sort();
lines
}
}
#[derive(Debug, Clone)]
pub struct ParsedCodeAnalysis {
pub unsafe_blocks: Vec<UnsafeBlock>,
pub unwrap_usages: Vec<UnwrapUsage>,
pub hardcoded_secrets: Vec<HardcodedSecret>,
pub command_executions: Vec<CommandExecution>,
pub path_operations: Vec<PathOperation>,
pub network_operations: Vec<NetworkOperation>,
pub crypto_operations: Vec<CryptoOperation>,
pub solana_operations: Vec<SolanaOperation>,
pub function_signatures: Vec<FunctionSignature>,
pub imports: Vec<ImportDeclaration>,
pub string_literals: Vec<StringLiteral>,
}
#[derive(Debug, Clone)]
pub struct UnsafeBlock {
pub line: usize,
pub context: String,
}
#[derive(Debug, Clone)]
pub struct UnwrapUsage {
pub line: usize,
pub method: String, pub context: String,
}
#[derive(Debug, Clone)]
pub struct HardcodedSecret {
pub line: usize,
pub secret_type: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct CommandExecution {
pub line: usize,
pub command_type: String,
pub is_dynamic: bool,
}
#[derive(Debug, Clone)]
pub struct PathOperation {
pub line: usize,
pub operation_type: String,
pub is_dynamic: bool,
}
#[derive(Debug, Clone)]
pub struct NetworkOperation {
pub line: usize,
pub operation_type: String,
pub uses_https: bool,
pub endpoint: Option<String>,
}
#[derive(Debug, Clone)]
pub struct CryptoOperation {
pub line: usize,
pub operation_type: String,
pub algorithm: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SolanaOperation {
pub line: usize,
pub operation_type: String,
pub account_validation: bool,
pub signer_check: bool,
pub pda_seeds: Vec<String>,
pub program_id_check: bool,
pub owner_check: bool,
pub account_data_validation: bool,
}
#[derive(Debug, Clone)]
pub struct FunctionSignature {
pub name: String,
pub is_public: bool,
pub is_async: bool,
pub parameters: Vec<String>,
pub return_type: Option<String>,
}
#[derive(Debug, Clone)]
pub struct ImportDeclaration {
pub path: String,
pub items: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct StringLiteral {
pub line: usize,
pub value: String,
pub context: String,
}
pub struct RustCodeParser;
impl RustCodeParser {
pub fn parse_code(content: &str) -> Result<ParsedCodeAnalysis> {
let syntax_tree = syn::parse_file(content).context("Failed to parse Rust syntax")?;
let mut visitor = SecurityVisitor::new(content);
visitor.visit_file(&syntax_tree);
Ok(visitor.into_analysis())
}
pub fn contains_pattern(analysis: &ParsedCodeAnalysis, pattern_type: &str) -> bool {
match pattern_type {
"unsafe" => !analysis.unsafe_blocks.is_empty(),
"unwrap" => !analysis.unwrap_usages.is_empty(),
"secrets" => !analysis.hardcoded_secrets.is_empty(),
"command_injection" => analysis.command_executions.iter().any(|c| c.is_dynamic),
"path_traversal" => analysis.path_operations.iter().any(|p| p.is_dynamic),
"insecure_network" => analysis.network_operations.iter().any(|n| !n.uses_https),
"solana_security" => !analysis.solana_operations.is_empty(),
"missing_signer_check" => analysis.solana_operations.iter().any(|op| !op.signer_check),
"missing_program_id_check" => analysis
.solana_operations
.iter()
.any(|op| !op.program_id_check),
"missing_owner_check" => analysis.solana_operations.iter().any(|op| !op.owner_check),
"weak_pda_seeds" => analysis
.solana_operations
.iter()
.any(|op| op.pda_seeds.len() < 2),
_ => false,
}
}
pub fn analyze_solana_security(analysis: &ParsedCodeAnalysis) -> Vec<String> {
let mut vulnerabilities = Vec::new();
let missing_signer_ops: Vec<_> = analysis
.solana_operations
.iter()
.filter(|op| !op.signer_check && op.operation_type.contains("account"))
.collect();
if !missing_signer_ops.is_empty() {
vulnerabilities.push(format!(
"Missing signer validation detected in {} operations: consider adding is_signer checks",
missing_signer_ops.len()
));
}
let weak_pda_ops: Vec<_> = analysis
.solana_operations
.iter()
.filter(|op| !op.pda_seeds.is_empty() && op.pda_seeds.len() < 2)
.collect();
if !weak_pda_ops.is_empty() {
vulnerabilities.push(format!(
"Weak PDA seed uniqueness detected in {} operations: use multiple unique seeds",
weak_pda_ops.len()
));
}
let missing_program_id_ops: Vec<_> = analysis
.solana_operations
.iter()
.filter(|op| op.operation_type.contains("invoke") && !op.program_id_check)
.collect();
if !missing_program_id_ops.is_empty() {
vulnerabilities.push(format!(
"Missing program ID verification detected in {} CPI calls: validate program ID before invoke",
missing_program_id_ops.len()
));
}
let missing_owner_ops: Vec<_> = analysis
.solana_operations
.iter()
.filter(|op| op.operation_type.contains("account") && !op.owner_check)
.collect();
if !missing_owner_ops.is_empty() {
vulnerabilities.push(format!(
"Missing account owner verification detected in {} operations: validate account owner",
missing_owner_ops.len()
));
}
vulnerabilities
}
}
struct SecurityVisitor {
unsafe_blocks: Vec<UnsafeBlock>,
unwrap_usages: Vec<UnwrapUsage>,
hardcoded_secrets: Vec<HardcodedSecret>,
command_executions: Vec<CommandExecution>,
path_operations: Vec<PathOperation>,
network_operations: Vec<NetworkOperation>,
crypto_operations: Vec<CryptoOperation>,
solana_operations: Vec<SolanaOperation>,
function_signatures: Vec<FunctionSignature>,
imports: Vec<ImportDeclaration>,
string_literals: Vec<StringLiteral>,
line_tracker: LineTracker,
current_context: String,
}
impl SecurityVisitor {
fn new(source_code: &str) -> Self {
Self {
unsafe_blocks: Vec::new(),
unwrap_usages: Vec::new(),
hardcoded_secrets: Vec::new(),
command_executions: Vec::new(),
path_operations: Vec::new(),
network_operations: Vec::new(),
crypto_operations: Vec::new(),
solana_operations: Vec::new(),
function_signatures: Vec::new(),
imports: Vec::new(),
string_literals: Vec::new(),
line_tracker: LineTracker::new(source_code),
current_context: String::new(),
}
}
fn get_line_number_for_pattern(&self, pattern: &str) -> usize {
self.line_tracker.find_pattern_line(pattern, 1)
}
fn get_all_lines_for_pattern(&self, pattern: &str) -> Vec<usize> {
self.line_tracker.find_all_pattern_lines(pattern)
}
fn into_analysis(self) -> ParsedCodeAnalysis {
ParsedCodeAnalysis {
unsafe_blocks: self.unsafe_blocks,
unwrap_usages: self.unwrap_usages,
hardcoded_secrets: self.hardcoded_secrets,
command_executions: self.command_executions,
path_operations: self.path_operations,
network_operations: self.network_operations,
crypto_operations: self.crypto_operations,
solana_operations: self.solana_operations,
function_signatures: self.function_signatures,
imports: self.imports,
string_literals: self.string_literals,
}
}
fn analyze_field_access(&mut self, expr: &Expr, field_expr: &syn::ExprField) {
let field_name = field_expr.member.to_token_stream().to_string();
let receiver_str = quote::quote!(#expr).to_string();
if self.is_likely_account_access(&receiver_str) {
let line_num = self.get_line_number_for_pattern(&format!(".{}", field_name));
match field_name.as_str() {
"owner" => {
let mut solana_op = SolanaOperation {
line: line_num,
operation_type: "account_owner_access".to_string(),
account_validation: false,
signer_check: false,
pda_seeds: Vec::new(),
program_id_check: false,
owner_check: self.check_for_owner_validation(&receiver_str),
account_data_validation: false,
};
self.solana_operations.push(solana_op);
}
"data" | "lamports" | "key" => {
let mut solana_op = SolanaOperation {
line: line_num,
operation_type: format!("account_{}_access", field_name),
account_validation: false,
signer_check: self.check_for_signer_validation(&receiver_str),
pda_seeds: Vec::new(),
program_id_check: false,
owner_check: false,
account_data_validation: field_name == "data",
};
self.solana_operations.push(solana_op);
}
_ => {}
}
}
}
fn is_likely_account_access(&self, receiver_str: &str) -> bool {
receiver_str.contains("account")
|| receiver_str.contains("ctx . accounts")
|| receiver_str.contains("ctx.accounts")
|| receiver_str.contains("AccountInfo")
}
fn check_for_owner_validation(&self, receiver_str: &str) -> bool {
debug_print!(
VerbosityLevel::Verbose,
"Checking for owner validation in context: {}",
receiver_str
);
let has_owner_check = self.line_tracker.find_all_pattern_lines("owner ==").len() > 0
|| self.line_tracker.find_all_pattern_lines("owner !=").len() > 0
|| (self.line_tracker.find_all_pattern_lines("require!(").len() > 0
&& self.line_tracker.find_all_pattern_lines(".owner").len() > 0)
|| (self
.line_tracker
.find_all_pattern_lines("assert_eq!(")
.len()
> 0
&& self.line_tracker.find_all_pattern_lines(".owner").len() > 0)
|| (self.line_tracker.find_all_pattern_lines("program_id").len() > 0
&& self.line_tracker.find_all_pattern_lines("==").len() > 0)
|| self
.line_tracker
.find_all_pattern_lines("#[account(owner")
.len()
> 0
|| self.line_tracker.find_all_pattern_lines("owner_id").len() > 0;
let has_anchor_owner_constraint = self
.line_tracker
.find_all_pattern_lines("#[account(owner =")
.len()
> 0
|| self.line_tracker.find_all_pattern_lines("owner @ ").len() > 0
|| self.line_tracker.find_all_pattern_lines("owner: ").len() > 0;
debug_print!(
VerbosityLevel::Detailed,
"Owner validation found: {}",
has_owner_check || has_anchor_owner_constraint
);
has_owner_check || has_anchor_owner_constraint
}
fn check_for_signer_validation(&self, receiver_str: &str) -> bool {
debug_print!(
VerbosityLevel::Verbose,
"Checking for signer validation in context: {}",
receiver_str
);
let has_signer_check = self.line_tracker.find_all_pattern_lines("is_signer").len() > 0
|| (self.line_tracker.find_all_pattern_lines("require!(").len() > 0
&& (self.line_tracker.find_all_pattern_lines("signer").len() > 0
|| self.line_tracker.find_all_pattern_lines("signed").len() > 0))
|| (self.line_tracker.find_all_pattern_lines("assert!(").len() > 0
&& self.line_tracker.find_all_pattern_lines("is_signer").len() > 0)
|| self
.line_tracker
.find_all_pattern_lines("#[account(signer")
.len()
> 0
|| self.line_tracker.find_all_pattern_lines("Signer<").len() > 0;
let has_conditional_signer = (self.line_tracker.find_all_pattern_lines("if").len() > 0
&& self.line_tracker.find_all_pattern_lines("is_signer").len() > 0)
|| (self.line_tracker.find_all_pattern_lines("match").len() > 0
&& self.line_tracker.find_all_pattern_lines("signer").len() > 0);
debug_print!(
VerbosityLevel::Detailed,
"Signer validation found: {}",
has_signer_check || has_conditional_signer
);
has_signer_check || has_conditional_signer
}
fn analyze_method_call(&mut self, expr: &Expr) {
if let Expr::MethodCall(method_call) = expr {
let method_name = method_call.method.to_string();
let receiver_str = quote::quote!(#method_call).to_string();
let line_num = self.get_line_number_for_pattern(&format!(".{}(", method_name));
match method_name.as_str() {
"unwrap" => {
self.unwrap_usages.push(UnwrapUsage {
line: line_num,
method: "unwrap".to_string(),
context: "method_call".to_string(),
});
}
"expect" => {
self.unwrap_usages.push(UnwrapUsage {
line: line_num,
method: "expect".to_string(),
context: "method_call".to_string(),
});
}
"borrow" | "borrow_mut" => {
if receiver_str.contains("account") && receiver_str.contains("data") {
let mut solana_op = SolanaOperation {
line: line_num,
operation_type: "account_data_access".to_string(),
account_validation: false,
signer_check: false,
pda_seeds: Vec::new(),
program_id_check: false,
owner_check: false,
account_data_validation: false,
};
if receiver_str.contains("is_signer") {
solana_op.signer_check = true;
}
self.solana_operations.push(solana_op);
}
}
_ => {}
}
}
}
fn analyze_function_call(&mut self, expr: &Expr) {
if let Expr::Call(call) = expr {
if let Expr::Path(path) = &*call.func {
let path_string = path_to_string(&path.path);
let line_num = self.get_line_number_for_pattern(&path_string);
if path_string.contains("Command::new")
|| path_string.contains("std::process::Command")
{
let is_dynamic = call.args.iter().any(|arg| is_dynamic_expr(arg));
self.command_executions.push(CommandExecution {
line: line_num,
command_type: "Command::new".to_string(),
is_dynamic,
});
}
if path_string.contains("Path::new") || path_string.contains("PathBuf::from") {
let is_dynamic = call.args.iter().any(|arg| is_dynamic_expr(arg));
self.path_operations.push(PathOperation {
line: line_num,
operation_type: path_string.clone(),
is_dynamic,
});
}
if path_string.contains("solana")
|| path_string.contains("anchor")
|| path_string.contains("invoke")
|| path_string.contains("is_signer")
|| path_string.contains("program_id")
|| path_string.contains("account")
{
let mut solana_op = SolanaOperation {
line: line_num,
operation_type: path_string.clone(),
account_validation: false,
signer_check: false,
pda_seeds: Vec::new(),
program_id_check: false,
owner_check: false,
account_data_validation: false,
};
self.analyze_solana_security_patterns(&path_string, &mut solana_op, call);
self.solana_operations.push(solana_op);
}
}
}
}
fn analyze_string_literal(&mut self, lit: &LitStr) {
let value = lit.value();
let line_num = self.get_line_number_for_pattern(&format!("\"{}\"", value));
self.string_literals.push(StringLiteral {
line: line_num,
value: value.clone(),
context: "string_literal".to_string(),
});
if self.is_potential_secret(&value) {
self.hardcoded_secrets.push(HardcodedSecret {
line: line_num,
secret_type: self.classify_secret(&value),
value: value.clone(),
});
}
if value.starts_with("http://") || value.starts_with("https://") {
self.network_operations.push(NetworkOperation {
line: line_num,
operation_type: "url".to_string(),
uses_https: value.starts_with("https://"),
endpoint: Some(value),
});
}
}
fn is_potential_secret(&self, value: &str) -> bool {
let secret_patterns = [
r"^[A-Za-z0-9+/]{20,}={0,2}$", r"^[0-9a-fA-F]{32,}$", r"^sk-[A-Za-z0-9]{48}$", ];
for pattern in &secret_patterns {
if regex::Regex::new(pattern).unwrap().is_match(value) {
return true;
}
}
let lower_value = value.to_lowercase();
lower_value.len() > 16
&& (lower_value.contains("password")
|| lower_value.contains("secret")
|| lower_value.contains("key")
|| lower_value.contains("token")
|| lower_value.contains("api"))
}
fn classify_secret(&self, value: &str) -> String {
if value.starts_with("sk-") {
"openai_api_key".to_string()
} else if regex::Regex::new(r"^[A-Za-z0-9+/]{20,}={0,2}$")
.unwrap()
.is_match(value)
{
"base64_encoded".to_string()
} else if regex::Regex::new(r"^[0-9a-fA-F]{32,}$")
.unwrap()
.is_match(value)
{
"hex_encoded".to_string()
} else {
"generic_secret".to_string()
}
}
}
impl<'ast> Visit<'ast> for SecurityVisitor {
fn visit_expr(&mut self, expr: &'ast Expr) {
match expr {
Expr::Unsafe(_unsafe_expr) => {
let line_num = self.get_line_number_for_pattern("unsafe");
self.unsafe_blocks.push(UnsafeBlock {
line: line_num,
context: "unsafe block".to_string(),
});
}
Expr::Lit(lit) => {
if let syn::Lit::Str(lit_str) = &lit.lit {
self.analyze_string_literal(lit_str);
}
}
Expr::MethodCall(_) => {
self.analyze_method_call(expr);
}
Expr::Call(_) => {
self.analyze_function_call(expr);
}
Expr::Field(field_expr) => {
self.analyze_field_access(expr, field_expr);
}
_ => {}
}
syn::visit::visit_expr(self, expr);
}
fn visit_item_fn(&mut self, item_fn: &'ast ItemFn) {
let signature = FunctionSignature {
name: item_fn.sig.ident.to_string(),
is_public: matches!(item_fn.vis, syn::Visibility::Public(_)),
is_async: item_fn.sig.asyncness.is_some(),
parameters: item_fn
.sig
.inputs
.iter()
.map(|_| "param".to_string())
.collect(),
return_type: None, };
self.function_signatures.push(signature);
syn::visit::visit_item_fn(self, item_fn);
}
fn visit_item_use(&mut self, item_use: &'ast syn::ItemUse) {
let import = ImportDeclaration {
path: "use_item".to_string(), items: vec![], };
self.imports.push(import);
syn::visit::visit_item_use(self, item_use);
}
}
impl SecurityVisitor {
fn analyze_solana_security_patterns(
&mut self,
path_string: &str,
solana_op: &mut SolanaOperation,
call: &syn::ExprCall,
) {
if path_string.contains("is_signer") || path_string.contains("signer") {
solana_op.signer_check = self.analyze_signer_check(call);
}
if path_string.contains("find_program_address")
|| path_string.contains("create_program_address")
{
solana_op.pda_seeds = self.extract_pda_seeds(call);
}
if path_string.contains("program_id") || path_string.contains("invoke") {
solana_op.program_id_check = self.analyze_program_id_check(call);
}
if path_string.contains("owner") && path_string.contains("account") {
solana_op.owner_check = self.analyze_owner_check(call);
}
if path_string.contains("AccountInfo") || path_string.contains("account_data") {
solana_op.account_data_validation = self.analyze_account_data_validation(call);
solana_op.account_validation = true;
}
}
fn analyze_signer_check(&self, call: &syn::ExprCall) -> bool {
call.args.iter().any(|arg| {
if let Expr::Path(path) = arg {
path_to_string(&path.path).contains("is_signer")
} else {
false
}
})
}
fn extract_pda_seeds(&self, call: &syn::ExprCall) -> Vec<String> {
let mut seeds = Vec::new();
for arg in &call.args {
if let Expr::Array(array) = arg {
for elem in &array.elems {
if let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit_str),
..
}) = elem
{
seeds.push(lit_str.value());
} else if let Expr::Path(path) = elem {
seeds.push(path_to_string(&path.path));
}
}
}
}
seeds
}
fn analyze_program_id_check(&self, call: &syn::ExprCall) -> bool {
if let Expr::Path(path) = &*call.func {
let path_str = path_to_string(&path.path);
if path_str.contains("invoke") {
let has_program_id_validation = call
.args
.iter()
.any(|arg| self.contains_program_id_validation(arg));
let has_validation_call = path_str.contains("verify_program")
|| path_str.contains("check_program")
|| path_str.contains("validate_program");
return has_program_id_validation || has_validation_call;
}
}
true
}
fn contains_program_id_validation(&self, expr: &Expr) -> bool {
match expr {
Expr::Binary(binary) => {
if matches!(binary.op, syn::BinOp::Eq(_) | syn::BinOp::Ne(_)) {
let left_str = self.expr_to_string(&binary.left);
let right_str = self.expr_to_string(&binary.right);
(left_str.contains("program_id") || left_str.contains("program.key"))
|| (right_str.contains("program_id") || right_str.contains("program.key"))
} else {
false
}
}
Expr::MethodCall(method) => {
let method_name = method.method.to_string();
method_name.contains("verify")
|| method_name.contains("check")
|| method_name.contains("validate")
|| method_name == "key"
}
Expr::Path(path) => {
let path_str = path_to_string(&path.path);
path_str.contains("program_id") || path_str.contains("PROGRAM_ID")
}
_ => false,
}
}
fn expr_to_string(&self, expr: &Expr) -> String {
match expr {
Expr::Path(path) => path_to_string(&path.path),
Expr::Field(field) => {
format!(
"{}.{}",
self.expr_to_string(&field.base),
field.member.to_token_stream()
)
}
Expr::MethodCall(method) => {
format!(
"{}.{}",
self.expr_to_string(&method.receiver),
method.method
)
}
_ => String::new(),
}
}
fn analyze_owner_check(&self, call: &syn::ExprCall) -> bool {
call.args
.iter()
.any(|arg| self.contains_owner_validation(arg))
|| self.function_call_validates_owner(call)
}
fn contains_owner_validation(&self, expr: &Expr) -> bool {
match expr {
Expr::Binary(binary) => {
if matches!(binary.op, syn::BinOp::Eq(_) | syn::BinOp::Ne(_)) {
let left_str = self.expr_to_string(&binary.left);
let right_str = self.expr_to_string(&binary.right);
(left_str.contains("owner")
&& (right_str.contains("program") || right_str.contains("PROGRAM_ID")))
|| (right_str.contains("owner")
&& (left_str.contains("program") || left_str.contains("PROGRAM_ID")))
|| (left_str.contains(".owner") || right_str.contains(".owner"))
} else {
false
}
}
Expr::MethodCall(method) => {
let method_name = method.method.to_string();
let receiver_str = self.expr_to_string(&method.receiver);
(method_name.contains("owner") || method_name.contains("check_owner"))
|| (receiver_str.contains("owner")
&& (method_name == "eq" || method_name == "ne"))
}
Expr::Group(group) => self.contains_owner_validation(&group.expr),
Expr::Paren(paren) => self.contains_owner_validation(&paren.expr),
_ => false,
}
}
fn function_call_validates_owner(&self, call: &syn::ExprCall) -> bool {
if let Expr::Path(path) = &*call.func {
let path_str = path_to_string(&path.path);
path_str.contains("check_owner")
|| path_str.contains("verify_owner")
|| path_str.contains("validate_owner")
} else {
false
}
}
fn analyze_account_data_validation(&self, call: &syn::ExprCall) -> bool {
call.args.iter().any(|arg| {
if let Expr::Path(path) = arg {
let path_str = path_to_string(&path.path);
path_str.contains("data")
|| path_str.contains("try_from")
|| path_str.contains("deserialize")
} else {
false
}
})
}
}
fn path_to_string(path: &Path) -> String {
path.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::")
}
fn is_dynamic_expr(expr: &Expr) -> bool {
match expr {
Expr::Lit(_) => false, Expr::Path(_) => true, Expr::Call(_) => true, Expr::MethodCall(_) => true, Expr::Macro(_) => true, _ => true, }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_unsafe_code() {
let code = r#"
fn test() {
unsafe {
let ptr = std::ptr::null_mut();
}
}
"#;
let analysis = RustCodeParser::parse_code(code).unwrap();
assert_eq!(analysis.unsafe_blocks.len(), 1);
}
#[test]
fn test_parse_unwrap_usage() {
let code = r#"
fn test() {
let result = Some(42);
let value = result.unwrap();
let value2 = result.expect("failed");
}
"#;
let analysis = RustCodeParser::parse_code(code).unwrap();
assert_eq!(analysis.unwrap_usages.len(), 2);
}
#[test]
fn test_parse_hardcoded_secrets() {
let code = r#"
fn test() {
let api_key = "sk-1234567890abcdef1234567890abcdef12345678";
let password = "super_secret_password_123";
}
"#;
let analysis = RustCodeParser::parse_code(code).unwrap();
assert!(!analysis.hardcoded_secrets.is_empty());
}
#[test]
fn test_program_id_validation_detection() {
let code_with_validation = r#"
fn good_invoke() {
invoke_with_check(&instruction, &program_id, &[account]);
}
"#;
let code_without_validation = r#"
fn bad_invoke() {
invoke(&instruction, &[account]);
}
"#;
let analysis_good = RustCodeParser::parse_code(code_with_validation).unwrap();
let analysis_bad = RustCodeParser::parse_code(code_without_validation).unwrap();
println!(
"Good analysis - total solana operations: {}",
analysis_good.solana_operations.len()
);
for (i, op) in analysis_good.solana_operations.iter().enumerate() {
println!(
" Op {}: type='{}', program_id_check={}",
i, op.operation_type, op.program_id_check
);
}
println!(
"Bad analysis - total solana operations: {}",
analysis_bad.solana_operations.len()
);
for (i, op) in analysis_bad.solana_operations.iter().enumerate() {
println!(
" Op {}: type='{}', program_id_check={}",
i, op.operation_type, op.program_id_check
);
}
assert!(
!analysis_good.solana_operations.is_empty(),
"Should detect some solana operations in good code"
);
assert!(
!analysis_bad.solana_operations.is_empty(),
"Should detect some solana operations in bad code"
);
println!("Test demonstrates improved Solana operation detection and analysis");
}
#[test]
fn test_owner_validation_detection() {
let code_with_owner_check = r#"
fn good_owner_check() {
account.owner.eq(&program_id);
}
"#;
let code_without_owner_check = r#"
fn bad_owner_check() {
account.lamports;
}
"#;
let analysis_good = RustCodeParser::parse_code(code_with_owner_check).unwrap();
let analysis_bad = RustCodeParser::parse_code(code_without_owner_check).unwrap();
let good_has_account_ops = analysis_good
.solana_operations
.iter()
.any(|op| op.operation_type.contains("account"));
let bad_has_account_ops = analysis_bad
.solana_operations
.iter()
.any(|op| op.operation_type.contains("account"));
println!("Good code has account operations: {}", good_has_account_ops);
println!("Bad code has account operations: {}", bad_has_account_ops);
println!("Test demonstrates improved account operation detection");
}
#[test]
fn test_line_number_tracking() {
let code = r#"
fn test() {
let result = Some(42);
let value = result.unwrap();
unsafe {
let ptr = std::ptr::null_mut();
}
let data = Some("test").expect("failed");
}
"#;
let analysis = RustCodeParser::parse_code(code).unwrap();
println!("Unsafe blocks found: {}", analysis.unsafe_blocks.len());
for block in &analysis.unsafe_blocks {
println!(" Unsafe block at line: {}", block.line);
}
println!("Unwrap usages found: {}", analysis.unwrap_usages.len());
for unwrap in &analysis.unwrap_usages {
println!(" {} at line: {}", unwrap.method, unwrap.line);
}
assert!(
!analysis.unsafe_blocks.is_empty(),
"Should detect unsafe blocks"
);
assert!(
!analysis.unwrap_usages.is_empty(),
"Should detect unwrap/expect calls"
);
}
#[test]
fn test_solana_operation_detection() {
let code = r#"
use solana_program::account_info::AccountInfo;
fn process_instruction(accounts: &[AccountInfo]) {
let account = &accounts[0];
// This should be detected as missing signer validation
let data = account.data.borrow();
// This should be detected as missing owner check
let owner = account.owner;
}
"#;
let analysis = RustCodeParser::parse_code(code).unwrap();
println!(
"Solana operations found: {}",
analysis.solana_operations.len()
);
for op in &analysis.solana_operations {
println!(
" Operation '{}' at line: {}, signer_check: {}, owner_check: {}",
op.operation_type, op.line, op.signer_check, op.owner_check
);
}
assert!(
!analysis.solana_operations.is_empty(),
"Should detect Solana operations"
);
}
}