This comprehensive documentation explains how the Domain Specific Language (DSL) of the Solana static analyzer works, how to write new rules, and how to extend its functionality.
- General Architecture
- SpanExtractor Integration
- File
query.rs- DSL Core - File
builders.rs- Rule Builder - Modular Rule Filters - Specific Filters
- How to Write a New Rule
The analyzer's DSL consists of three main components:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ query.rs │ │ builders.rs │ │ rules/[rule]/ │
│ │ │ │ │ │
│ • AstQuery │◄───┤ • RuleBuilder │◄───┤ • mod.rs │
│ • AstNode │ │ • Fluent API │ │ • filters.rs │
│ • NodeData │ │ • Integration │ │ • Specific │
│ • Generic │ │ │ │ Helpers │
│ Helpers │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
▲ ▲ ▲
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Modular │
│ Vulnerability │
│ Rules │
│ │
│ high/unsafe_code│
│ medium/division │
│ low/error_handle│
└─────────────────┘
- Parsing:
query.rsparses the Rust AST usingsyn - Filtering: Applies Solana-specific filters
- Construction:
builders.rsfacilitates rule creation - Detection: Rules detect vulnerability patterns
- Location Extraction:
SpanExtractorprovides precise locations - Reporting: Generates findings with exact locations and code snippets
The analyzer includes precise location extraction through the SpanExtractor system, which provides exact file locations and automatic code snippet generation for all vulnerability findings.
- Exact locations like
src/lib.rs:42:15 - Line numbers, column positions, and end positions
- Function signatures:
pub fn initialize(ctx: Context<Initialize>) -> Result<()> - Struct definitions:
pub struct TransferFunds - Code context: Surrounding lines for better understanding
get_spanned_node(): Method to getsyn::spanned::Spannedobjectsto_findings_with_span_extractor(): Method for creating findings- Automatic integration: Works seamlessly with all DSL queries
let extractor = SpanExtractor::new(
source_code.to_string(),
"src/lib.rs".to_string()
);Usage: Create a new extractor for a specific file.
let location = extractor.span_to_location(node_span);Usage: Convert a syn::Span to precise Location.
let snippet = extractor.extract_snippet(&function_node);Usage: Extract code snippet from any Spanned object.
let context = extractor.extract_context(span, 2);Usage: Extract code with surrounding context lines.
let spanned_node = ast_node.get_spanned_node();
if let Some(spanned) = spanned_node {
let location = span_extractor.span_to_location(spanned.span());
let snippet = span_extractor.extract_snippet(spanned);
}Purpose: Get the underlying syn node that implements Spanned trait.
let findings = AstQuery::new(ast)
.functions()
.uses_unsafe()
.to_findings_with_span_extractor(
Severity::High,
"Unsafe Code Detection",
"Detects unsafe code blocks that could lead to memory safety issues",
file_path,
&span_extractor
);Parameters:
severity: Severity- Vulnerability severitytitle: &str- Short title for the findingdescription: &str- Detailed descriptionfile_path: &str- Source file pathspan_extractor: &SpanExtractor- Extractor instance
Returns: Vec<Finding> with precise locations and code snippets.
.dsl_query(|ast, file_path, span_extractor| {
AstQuery::new(ast)
.functions()
.uses_unsafe()
.to_findings_with_span_extractor(
Severity::High,
"Unsafe Code Detection",
"Detects unsafe code blocks that could lead to memory safety issues",
file_path,
span_extractor
)
})Key Points:
- Always use the
span_extractorparameter provided todsl_query() - Provide clear, descriptive titles and detailed descriptions
- The method automatically extracts precise locations and code snippets
This is the heart of the DSL. It contains the fundamental data structures and operators for querying the AST.
pub enum NodeType {
File, // Complete file
Function, // Function (normal or impl)
Struct, // Structure
Enum, // Enumeration
Block, // Code block
Expression, // Expression
Other, // Other elements
}pub enum NodeData<'a> {
File(&'a File), // syn::File
Function(&'a ItemFn), // Normal function
ImplFunction(&'a syn::ImplItemFn), // Impl function (NEW)
Struct(&'a ItemStruct), // Structure
Enum(&'a ItemEnum), // Enumeration
Block(&'a Block), // Block
Expression(&'a Expr), // Expression
Other, // Others
}Function and ImplFunction is crucial for supporting Anchor projects, where functions are inside impl blocks.
pub struct AstNode<'a> {
pub node_type: NodeType, // Logical node type
pub data: NodeData<'a>, // Node-specific data
pub name: Option<String>, // Name (if applicable)
}Main Methods:
let file_node = AstNode::from_file(&ast);Usage: Create a node representing the entire file.
let func_node = AstNode::from_function(&function_item);Usage: For functions defined directly in the file (fn my_function() {}).
let impl_func_node = AstNode::from_impl_function(&impl_function);Usage: For functions inside impl blocks (Anchor pattern).
let struct_node = AstNode::from_struct(&struct_item);Usage: For data structures.
let node_type: NodeType = node.node_type();
match node_type {
NodeType::Function => println!("It's a function"),
NodeType::Struct => println!("It's a structure"),
_ => {}
}let name: String = node.name();
println!("Name: {}", name); // "Name: my_function"Note: Returns "unnamed" if the node has no name.
let code_snippet: String = node.snippet();
// Examples of output:
// "fn initialize(...)"
// "struct MyAccount"
// "enum MyEnum"let spanned_node = node.get_spanned_node();
if let Some(spanned) = spanned_node {
let span = spanned.span();
// Use with SpanExtractor for precise locations
}Usage: Get the underlying syn node that implements Spanned trait for use with SpanExtractor.
Returns: Option<&dyn syn::spanned::Spanned>
Some(spanned)- For nodes with span information (functions, structs, etc.)None- For nodes without span information
pub struct AstQuery<'a> {
results: Vec<AstNode<'a>>, // Nodes that match the query
}// Extracts ALL functions (normal + impl)
let query = AstQuery::new(ast)
.functions(); // Finds functions anywhereInternal Implementation:
- Searches for
syn::Item::Fn(normal functions) - Searches for
syn::Item::Impl→syn::ImplItem::Fn(impl functions) - Recursively searches in nested modules
let query = AstQuery::new(ast)
.structs(); // Finds all structureslet query = AstQuery::new(ast)
.functions()
.with_name("initialize"); // Only "initialize" functionlet query = AstQuery::new(ast)
.functions()
.uses_unsafe(); // Functions with 'unsafe' or unsafe blocksDetects:
- Functions marked as
unsafe fn unsafe { ... }blocks inside functions- Both normal and impl functions
let query = AstQuery::new(ast)
.functions()
.calls_to("panic"); // Functions that call panic!()Detects:
- Function calls:
function_name() - Method calls:
obj.method_name() - Uses visitor pattern to traverse the AST
let unsafe_or_panic = query1.or(query2); // Combines resultslet intersection = query1.and(query2); // Only common elementslet negated = query.not(); // Inverts the queryif query.exists() {
// Results were found
}let num_functions = query.count();let nodes: Vec<AstNode> = query.collect();let findings = query.to_findings_with_span_extractor(
Severity::High,
"Unsafe Code Detection",
"Detects unsafe code blocks that could lead to memory safety issues",
"src/lib.rs",
&span_extractor
);Parameters:
severity: Severity- Vulnerability severity (High/Medium/Low)title: &str- Short, descriptive title for the findingdescription: &str- Detailed explanation of the vulnerabilityfile_path: &str- Path to the source file being analyzedspan_extractor: &SpanExtractor- Extractor for precise location information
Returns: Vec<Finding> with:
- Exact file locations (line:column)
- Automatic code snippets
- Professional descriptions
let custom_filtered = AstQuery::new(ast)
.functions()
.filter(|node| {
// Custom filtering logic
node.name().contains("unsafe")
});Functionality:
- Allows applying custom predicates
- Takes a function that returns
bool - Useful for complex filtering logic
let custom_nodes = vec![node1, node2, node3];
let query = AstQuery::from_nodes(custom_nodes);let single_query = AstQuery::from_node(&my_node);// For internal DSL use
let results = query.results_mut(); // &mut Vec<AstNode>let results = query.results(); // &[AstNode]let nodes = query.nodes(); // Same as results()fn extract_functions_recursive<'b>(
items: &'b [syn::Item],
results: &mut Vec<AstNode<'b>>
)Functionality:
- Normal functions:
syn::Item::Fn→AstNode::from_function() - Modules:
syn::Item::Mod→ Recursion in content - Impl blocks:
syn::Item::Impl→AstNode::from_impl_function()
Example of parsed code:
// 1. Normal function
pub fn normal_function() { } // ← Detected
// 2. Module with functions
pub mod my_module {
pub fn nested_function() { } // ← Detected recursively
}
// 3. Impl block (Anchor pattern)
impl MyProgram {
pub fn instruction_function() { } // ← Detected as ImplFunction
}This file provides a fluent API for creating analysis rules without manually implementing the Rule trait.
pub struct RuleBuilder {
id: String, // Unique rule ID
title: String, // Descriptive title
description: String, // Detailed description
severity: Severity, // Severity (High/Medium/Low)
rule_type: RuleType, // Type (Solana/Rust/General)
query_builder: Option<Box<dyn Fn(&File, &str, &SpanExtractor) -> Vec<Finding> + Send + Sync>>, // Analysis function with SpanExtractor
references: Vec<String>, // Documentation references
tags: Vec<String>, // Classification tags
enabled: bool, // Enabled by default
}let rule = RuleBuilder::new()
.id("solana-unsafe-code")
.title("Unsafe Code Detection")
.description("Detects unsafe code blocks and functions")
.severity(Severity::High);.id("my-custom-rule") // Unique rule ID.title("My Custom Security Rule").description("This rule detects a specific vulnerability pattern").severity(Severity::High) // Critical
.severity(Severity::Medium) // Medium
.severity(Severity::Low) // Low.visitor_rule(|ast: &syn::File| -> Vec<Finding> {
// Manual implementation using visitor pattern
let mut findings = Vec::new();
// ... detection logic
findings
}).dsl_rule(|ast: &syn::File, file_path: &str| -> Vec<Finding> {
// Use DSL for detection
AstQuery::new(ast)
.functions()
.uses_unsafe()
.to_findings(Severity::High, "Unsafe code detected", file_path)
}).dsl_query(|ast: &syn::File, file_path: &str, span_extractor: &SpanExtractor| -> Vec<Finding> {
// Use DSL with SpanExtractor for precise locations
AstQuery::new(ast)
.functions()
.uses_unsafe()
.to_findings_with_span_extractor(
Severity::High,
"Unsafe Code Detection",
"Detects unsafe code blocks that could lead to memory safety issues",
file_path,
span_extractor
)
})Parameters:
ast: &syn::File- The parsed AST of the source filefile_path: &str- Path to the source file being analyzedspan_extractor: &SpanExtractor- Extractor for precise location information
Returns: Vec<Finding> with precise locations and code snippets.
.reference("https://docs.solana.com/security").references(vec![
"https://docs.solana.com/security",
"https://github.com/solana-labs/solana/security"
]).tag("security")
.tag("unsafe")
.tag("solana").tags(vec!["security", "unsafe", "critical"]).enabled(true) // Enabled by default
.enabled(false) // Disabled by defaultlet rule: Arc<dyn Rule> = builder.build();Internal Process:
- Validates all required fields are present
- Creates a
RustRulewith the provided logic - Returns an
Arc<dyn Rule>for use in the engine
pub fn create_unsafe_code_rule() -> Arc<dyn Rule> {
RuleBuilder::new()
.id("solana-unsafe-code")
.title("Unsafe Code Detection")
.description("Detects unsafe code blocks and functions that could lead to memory safety issues")
.severity(Severity::High)
.rule_type(RuleType::Solana)
.dsl_query(|ast: &syn::File, file_path: &str, span_extractor: &SpanExtractor| -> Vec<Finding> {
AstQuery::new(ast)
.functions()
.uses_unsafe()
.to_findings_with_span_extractor(
Severity::High,
"Unsafe Code Detection",
"Detects unsafe code blocks and functions that could lead to memory safety issues in Solana programs",
file_path,
span_extractor
)
})
.references(vec![
"https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html",
"https://docs.solana.com/developing/programming-model/overview"
])
.tags(vec!["security", "unsafe", "memory-safety"])
.enabled(true)
.build()
}The analyzer uses a modular architecture where each vulnerability rule has its own specific filters. This approach provides better maintainability, scalability, and clarity compared to a centralized filter system.
src/analyzer/rules/solana/
├── high/
│ ├── unsafe_code/
│ │ ├── mod.rs (rule implementation)
│ │ └── filters.rs (specific filters)
│ └── missing_signer_check/
│ ├── mod.rs
│ └── filters.rs
├── medium/
│ ├── division_by_zero/
│ │ ├── mod.rs
│ │ └── filters.rs
│ └── duplicate_mutable_accounts/
│ ├── mod.rs
│ └── filters.rs
└── low/
└── missing_error_handling/
├── mod.rs
└── filters.rs
These are generic helpers available to all rules:
.functions() // All functions
.structs() // All structures
.with_name(name) // Filter by name
.uses_unsafe() // Functions/blocks with unsafe code
.derives_accounts() // Structs with #[derive(Accounts)]
.public_functions() // Only public functions
.calls_to(name) // Calls to specific function
.filter(predicate) // Custom predicate
.or()/.and()/.not() // Logical operators
.exists()/.count() // Result queries
.to_findings_with_span_extractor() // Convert to findingsEach rule defines its own specific filters using trait extensions:
use crate::analyzer::dsl::query::AstQuery;
pub trait UnsafeCodeFilters<'a> {
fn uses_unsafe(self) -> AstQuery<'a>;
}
impl<'a> UnsafeCodeFilters<'a> for AstQuery<'a> {
fn uses_unsafe(self) -> AstQuery<'a> {
// Implementation specific to unsafe code detection
self.filter(|node| {
// Custom logic for this vulnerability
})
}
}use crate::analyzer::dsl::query::AstQuery;
pub trait DuplicateMutableAccountsFilters<'a> {
fn has_duplicate_mutable_accounts(self) -> AstQuery<'a>;
}
impl<'a> DuplicateMutableAccountsFilters<'a> for AstQuery<'a> {
fn has_duplicate_mutable_accounts(self) -> AstQuery<'a> {
// Implementation specific to duplicate mutable accounts
self.filter(|node| {
// Custom logic for this vulnerability
})
}
}Each rule follows this unified pattern:
// In mod.rs of each rule
use crate::analyzer::dsl::query::AstQuery;
use crate::analyzer::dsl::builders::RuleBuilder;
use crate::analyzer::{Rule, Severity};
use std::sync::Arc;
// Import the specific filters for this rule
mod filters;
use filters::SpecificFilters; // Trait name varies per rule
pub fn create_rule() -> Arc<dyn Rule> {
RuleBuilder::new()
.id("solana-rule-name")
.title("Rule Title")
.description("Rule description")
.severity(Severity::High) // or Medium/Low
.dsl_query(|ast, file_path, span_extractor| {
AstQuery::new(ast)
.functions() // Generic helper
.specific_filter() // Specific helper
.to_findings_with_span_extractor(
Severity::High,
"Rule Title",
"Detailed description",
file_path,
span_extractor
)
})
.build()
}- Maintainability: Each vulnerability is self-contained
- Scalability: Easy to add new rules without affecting existing ones
- Clarity: Clear separation between generic and specific logic
- Performance: No unused filters loaded
- Encapsulation: Rule-specific logic stays with the rule
-
Create the directory structure:
src/analyzer/rules/solana/[severity]/[rule_name]/ ├── mod.rs └── filters.rs -
Implement specific filters in
filters.rs -
Implement the rule in
mod.rsusing the pattern above -
Register the rule in the parent
mod.rs
// Uses generic helper from query.rs
AstQuery::new(ast)
.functions()
.uses_unsafe() // Uses specific filter from the rule's filters.rs
AstQuery::new(ast)
.structs()
.derives_accounts() // Generic helper
.has_duplicate_mutable_accounts() // Specific filter// Uses specific filter with anchor-syn for advanced analysis
AstQuery::new(ast)
.structs()
.derives_accounts() // Generic helper
.has_missing_signer_checks() // Specific filterThe DSL uses several internal visitors to detect specific patterns. These are helpers that implement the syn Visitor pattern to efficiently traverse the AST.
struct UnsafeDivisionFinder {
found: bool,
safe_variables: HashMap<String, bool>, // Variables marked as safe
}Functionality:
- Safe variable tracking: Identifies variables assigned to non-zero literals
- Division detection: Finds
/and%operators - Danger assessment: Determines if the divisor is potentially unsafe
Main Methods:
// Detects and tracks assignments like:
let safe_divisor = 100; // ← Marked as safe (non-zero literal)
let unsafe_divisor = user_input; // ← Not marked (unknown value)
let zero_divisor = 0; // ← Not marked (zero literal)// Detects division operations:
let result = amount / divisor; // ← Analyzes if 'divisor' is safe
let remainder = value % modulo; // ← Also analyzes modulo// Evaluates different expression types:
let zero = 0; // → true (dangerous)
let safe = 100; // → false (safe)
let variable = x; // → true if not in safe_variables
let call = get_value(); // → true (unknown result)
let field = obj.field; // → true (unknown value)struct OwnerCheckFinder {
found: bool,
}Functionality: Detects owner checks in Solana/Anchor code.
Main Methods:
// Detects comparisons involving "owner":
if account.owner == program_id { } // ← Detected
if ctx.accounts.token.owner == &spl_token::id() { } // ← Detected
if owner_key != expected_owner { } // ← Detected// Detects verification macros with "owner":
require!(account.owner == program_id); // ← Detected
assert_eq!(token.owner, expected_owner); // ← Detected
assert!(ctx.accounts.mint.owner == &spl_token::id()); // ← Detectedstruct CallFinder {
target_function: String, // Target function to find
found: bool,
}Functionality: Searches for specific function or method calls (but NOT macros).
Main Methods:
// Detects direct function calls:
dangerous_function(); // ← If target_function = "dangerous_function"
some_function(); // ← If target_function = "some_function"
// Note: Does NOT detect macros like panic!() or assert!()// Detects method calls:
result.unwrap(); // ← If target_function = "unwrap"
value.dangerous_method(); // ← If target_function = "dangerous_method"
token.transfer(); // ← If target_function = "transfer"Important Limitation: CallFinder currently does NOT support macro detection. For detecting macros like panic!(), assert!(), etc., a custom visitor with visit_expr_macro() would be needed.
// Inside has_unsafe_divisions():
let mut finder = UnsafeDivisionFinder {
found: false,
safe_variables: HashMap::new(),
};
// The visitor traverses the entire function AST
syn::visit::visit_item_fn(&mut finder, func);
// If unsafe divisions were found, include the function in results
if finder.found {
new_results.push(node.clone());
}Advantages of the Visitor Pattern:
- Efficiency: Traverses the AST only once
- Completeness: Doesn't miss nested nodes
- Flexibility: Easy to extend for new patterns
- Reusability: Visitors can be combined
Create a file in the appropriate severity folder:
src/analyzer/rules/solana/high/- High severitysrc/analyzer/rules/solana/medium/- Medium severitysrc/analyzer/rules/solana/low/- Low severity
use crate::analyzer::dsl::builders::RuleBuilder;
use crate::analyzer::dsl::query::AstQuery;
use crate::analyzer::dsl::filters::solana::SolanaFilters;
use crate::analyzer::{Severity, engine::{Rule, RuleType}};
use std::sync::Arc;
pub fn create_my_custom_rule() -> Arc<dyn Rule> {
RuleBuilder::new()
.id("solana-my-custom-rule")
.title("My Custom Security Rule")
.description("Detects a specific vulnerability pattern")
.severity(Severity::Medium)
.rule_type(RuleType::Solana)
.dsl_query(|ast: &syn::File| -> AstQuery {
// Use DSL to define the query
AstQuery::new(ast)
.functions()
.public_functions()
.calls_to("dangerous_function")
})
.references(vec![
"https://docs.solana.com/security"
])
.tags(vec!["security", "solana"])
.build()
}In src/analyzer/rules/solana/[severity]/mod.rs:
mod my_custom_rule;
pub use my_custom_rule::create_my_custom_rule;In src/analyzer/rules/solana/mod.rs:
// In the register_builtin_rules function
engine.register_rule(high::create_my_custom_rule());