diff --git a/.github/workflows/cargo.yml b/.github/workflows/cargo.yml index c5ef28d3c..841cf40de 100644 --- a/.github/workflows/cargo.yml +++ b/.github/workflows/cargo.yml @@ -19,7 +19,6 @@ jobs: - name: Run cargo check uses: actions-rs/cargo@v1 - continue-on-error: true # WARNING: only for this example, remove it! with: command: check @@ -39,7 +38,6 @@ jobs: - name: Run cargo test uses: actions-rs/cargo@v1 - continue-on-error: true # WARNING: only for this example, remove it! with: command: test @@ -60,14 +58,12 @@ jobs: - name: Run cargo fmt uses: actions-rs/cargo@v1 - continue-on-error: true # WARNING: only for this example, remove it! with: command: fmt args: --all -- --check - name: Run cargo clippy uses: actions-rs/cargo@v1 - continue-on-error: true # WARNING: only for this example, remove it! with: command: clippy args: -- -D warnings \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf7f..212de442f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.DS_Store \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index da280ab45..97ee0f1f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,10 +4,20 @@ version = 3 [[package]] name = "aderyn" -version = "0.0.6" +version = "0.0.7" dependencies = [ + "aderyn_core", "clap 4.4.6", + "rayon", + "tokei", +] + +[[package]] +name = "aderyn_core" +version = "0.0.7" +dependencies = [ "eyre", + "rayon", "serde", "serde_json", "serde_repr", diff --git a/Cargo.toml b/Cargo.toml index 7fba66a53..dcb56202a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,7 @@ -[package] -name = "aderyn" -version = "0.0.6" -edition = "2021" -authors = ["Alex Roan "] -description = "Rust based Solidity AST analyzer" -license = "MIT" +[workspace] +members = [ + "aderyn", + "aderyn_core" +] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -clap = { version = "4.4.6", features = ["derive"] } -eyre = "0.6.8" -serde = { version = "1.0.160", features = ["derive"] } -serde_json = "1.0.96" -serde_repr = "0.1.12" -tokei = "12.1.2" -toml = "0.8.2" +resolver="1" \ No newline at end of file diff --git a/README.md b/README.md index 7b87e66b6..a511b336f 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ cargo install aderyn ## Quick Start -The project you're running Aderyn on should be either a **Foundry** or compiled **Hardhat** project. +The root path you're running Aderyn on should be either a **Foundry** or compiled **Hardhat** project. ```sh aderyn /path/to/your/foundry/project/root/directory/ @@ -82,9 +82,18 @@ That's it! Aderyn identifies whether the project root is a Foundry or Hardhat re `report.md` will be output **in the directory in which you ran the command.** + ### Arguments -You must provide the root directory of the repo you want to analyze. +Usage: `aderyn [OPTIONS] ` + +Options: + - `-o`, `--output `: Desired file path for the final report (will overwrite existing one) [default: report.md] + - `-h`, `--help`: Print help + - `-V`, `--version`: Print version + + +You must provide the root directory of the repo you want to analyze. Alternatively, you can provide a single Solidity filepath (this mode requires [Foundry](https://book.getfoundry.sh/) to be installed). Examples: @@ -92,15 +101,27 @@ Examples: aderyn /path/to/your/foundry/project/root/directory/ ``` -To run Aderyn in the folder you're currently on, run: - +Run Aderyn in the folder you're currently in: ```sh aderyn . ``` + +Output to a different markdown file: + +```sh +aderyn -o output.md . +``` + +Run on a single Solidity file (requires [Foundry](https://book.getfoundry.sh/) to be installed on your machine): + +```sh +aderyn src/MyContract.sol +``` + ## Supported Development Frameworks -Aderyn automatically detects the development framework so long as it's Foundry or Hardhat. +If the `` is a directory, Aderyn automatically detects the development framework so long as it's Foundry or Hardhat. ### Foundry @@ -110,7 +131,9 @@ If Foundry is detected in the project root, Aderyn will first run `forge build` If Hardhat is detected, Aderyn does not auto-compile. Make sure to run `hardhat compile` BEFORE running Aderyn. +## Single Solidity File Mode +If it is a Solidity file path, then Aderyn will create a temporary Foundry project, copy the contract into it, compile the contract and then analyze the AST generated by that temporary project. # Roadmap @@ -123,6 +146,8 @@ If Hardhat is detected, Aderyn does not auto-compile. Make sure to run `hardhat * [ ] More complex static analysis detectors * [ ] auto-fixes * [ ] installer that doesn't require Rust (aderynup) +* [ ] VSCode Extension +* [ ] Python bindings **Long-term goals - Product** @@ -142,7 +167,7 @@ To build Aderyn locally, [install Rust](https://www.rust-lang.org/tools/install) ## Credits -This project exists thanks to all the people who [contribute](/contributing.md).
+This project exists thanks to all the people who [contribute](/CONTRIBUTING.md).
@@ -163,4 +188,4 @@ This project exists thanks to all the people who [contribute](/contributing.md). [issues-url]: https://github.com/cyfrin/aderyn/issues [license-shield]: https://img.shields.io/github/license/cyfrin/aderyn?logoColor=%23fff&color=blue [license-url]: https://github.com/cyfrin/aderyn/blob/master/LICENSE.txt -[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 +[linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 \ No newline at end of file diff --git a/aderyn/Cargo.toml b/aderyn/Cargo.toml new file mode 100644 index 000000000..02d1172b9 --- /dev/null +++ b/aderyn/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "aderyn" +version = "0.0.7" +edition = "2021" +authors = ["Alex Roan "] +description = "Rust based Solidity AST analyzer" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aderyn_core = { version = "0.0.7", path = "../aderyn_core" } +clap = { version = "4.4.6", features = ["derive"] } +rayon = "1.8.0" +tokei = "12.1.2" \ No newline at end of file diff --git a/aderyn/src/lib.rs b/aderyn/src/lib.rs new file mode 100644 index 000000000..bdb51acf2 --- /dev/null +++ b/aderyn/src/lib.rs @@ -0,0 +1,3 @@ +pub mod process_foundry; +pub mod process_hardhat; +pub mod virtual_foundry; diff --git a/aderyn/src/main.rs b/aderyn/src/main.rs new file mode 100644 index 000000000..2323f9e25 --- /dev/null +++ b/aderyn/src/main.rs @@ -0,0 +1,98 @@ +use aderyn::{process_foundry, process_hardhat, virtual_foundry}; +use aderyn_core::run; +use clap::Parser; +use std::{fs::read_dir, path::PathBuf}; +use tokei::{Config, LanguageType}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +struct Args { + /// Foundry or Hardhat project root directory (or path to single solidity file) + root: String, + + /// Desired file path for the final report (will overwrite existing one) + #[arg(short, long, default_value = "report.md")] + output: String, +} + +enum Framework { + Foundry, + Hardhat, +} + +fn main() { + let args = Args::parse(); + + if !args.output.ends_with(".md") { + eprintln!("Warning: output file lacks the \".md\" extension in its filename."); + } + + let is_single_file = args.root.ends_with(".sol") && PathBuf::from(&args.root).is_file(); + let mut safe_space = PathBuf::new(); + + let (src_path, mut context_loader) = { + if is_single_file { + safe_space = virtual_foundry::build_isolated_workspace_for_file(&args.root); + process_foundry::with_project_root_at(&safe_space) + } else { + println!("Detecting framework..."); + let root_path = PathBuf::from(&args.root); + let framework = detect_framework(root_path.clone()).unwrap_or_else(|| { + // Exit with a non-zero exit code + eprintln!("Error detecting framework"); + std::process::exit(1); + }); + + // This whole block loads the solidity files and ASTs into the context loader + // TODO: move much of this gutsy stuff into the foundry / hardhat modules. + match framework { + Framework::Foundry => process_foundry::with_project_root_at(&root_path), + Framework::Hardhat => process_hardhat::with_project_root_at(&root_path), + } + } + }; + + // Using the source path, get the sloc from tokei + let mut languages = tokei::Languages::new(); + let tokei_config = Config::default(); + languages.get_statistics(&[src_path], &[], &tokei_config); + context_loader.set_sloc_stats(languages[&LanguageType::Solidity].clone()); + + // Load the context loader into the run function, which runs the detectors + run(context_loader, args.output).unwrap_or_else(|err| { + // Exit with a non-zero exit code + eprintln!("Error running aderyn"); + eprintln!("{:?}", err); + std::process::exit(1); + }); + + if is_single_file { + virtual_foundry::delete_safe_space(&safe_space); + } +} + +fn detect_framework(path: PathBuf) -> Option { + // Canonicalize the path + let canonical_path = path.canonicalize().expect("Failed to canonicalize path"); + + // Check if the directory exists + if !canonical_path.is_dir() { + return None; + } + + // Read the contents of the directory + let entries = read_dir(&canonical_path).expect("Failed to read directory"); + + for entry in entries.flatten() { + let filename = entry.file_name(); + match filename.to_str() { + Some("foundry.toml") => return Some(Framework::Foundry), + Some("hardhat.config.js") | Some("hardhat.config.ts") => { + return Some(Framework::Hardhat) + } + _ => {} + } + } + + None +} diff --git a/aderyn/src/process_foundry.rs b/aderyn/src/process_foundry.rs new file mode 100644 index 000000000..d173ac0c0 --- /dev/null +++ b/aderyn/src/process_foundry.rs @@ -0,0 +1,87 @@ +use aderyn_core::{ + context::loader::ContextLoader, + framework::foundry::{load_foundry, read_foundry_output_file}, + read_file_to_string, + visitor::ast_visitor::Node, +}; +use rayon::prelude::*; +use std::path::PathBuf; + +pub fn with_project_root_at(root_path: &PathBuf) -> (String, ContextLoader) { + let mut context_loader = ContextLoader::default(); + + println!("Framework detected: Foundry mode engaged."); + println!("Foundry root path: {:?}", root_path); + let loaded_foundry = load_foundry(root_path).unwrap_or_else(|err| { + // Exit with a non-zero exit code + eprintln!("Error loading Foundry Root"); + eprintln!("{:?}", err); + std::process::exit(1); + }); + let src_path_buf = root_path.join(&loaded_foundry.src_path); + let src_path = src_path_buf.to_str().unwrap().to_string(); + println!("Foundry src path: {:?}", src_path); + + // Load the foundry output files into the context loader using the ASTs + let foundry_intermediates = loaded_foundry + .output_filepaths + .par_iter() + .map(|output_filepath| { + if let Ok(foundry_output) = read_foundry_output_file(output_filepath.to_str().unwrap()) + { + Some(foundry_output.ast) + } else { + eprintln!( + "Error reading Foundry output file: {}", + output_filepath.to_str().unwrap() + ); + None + } + }) + .collect::>(); + + // read_foundry_output_file and print an error message if it fails + foundry_intermediates.into_iter().flatten().for_each(|ast| { + ast.accept(&mut context_loader).unwrap_or_else(|err| { + // Exit with a non-zero exit code + eprintln!("Error loading Foundry AST into ContextLoader"); + eprintln!("{:?}", err); + std::process::exit(1); + }) + }); + + // Load the solidity source files into memory, and assign the content to the source_unit.source + for source_filepath in loaded_foundry.src_filepaths { + match read_file_to_string(&source_filepath) { + Ok(content) => { + // Convert the full_path to a string + let full_path_str = source_filepath.to_str().unwrap_or(""); + + // Find the index where "src/" starts + let src_component = src_path_buf.file_name().unwrap().to_str().unwrap(); + if let Some(start_index) = full_path_str.find(src_component) { + let target_path = &full_path_str[start_index..]; + + // Search for a match and modify + for unit in &context_loader.source_units { + if let Some(ref abs_path) = unit.absolute_path { + if abs_path == target_path { + context_loader.set_source_unit_source_content(unit.id, content); + break; + } + } + } + } + } + Err(err) => { + eprintln!( + "Error reading Solidity source file: {}", + source_filepath.to_str().unwrap() + ); + eprintln!("{:?}", err); + } + } + } + + (src_path, context_loader) +} diff --git a/aderyn/src/process_hardhat.rs b/aderyn/src/process_hardhat.rs new file mode 100644 index 000000000..5d1913b78 --- /dev/null +++ b/aderyn/src/process_hardhat.rs @@ -0,0 +1,54 @@ +use aderyn_core::{ + context::loader::ContextLoader, framework::hardhat::load_hardhat, read_file_to_string, + visitor::ast_visitor::Node, +}; +use std::path::PathBuf; + +pub fn with_project_root_at(root_path: &PathBuf) -> (String, ContextLoader) { + let mut context_loader = ContextLoader::default(); + + println!("Framework detected. Hardhat mode engaged."); + println!("Hardhat root path: {:?}", root_path); + let src_path = root_path.join("contracts").to_str().unwrap().to_string(); + let hardhat_output = load_hardhat(root_path).unwrap_or_else(|err| { + // Exit with a non-zero exit code + eprintln!("Error loading Hardhat build info"); + eprintln!("{:?}", err); + std::process::exit(1); + }); + for (key, contract_source) in hardhat_output.output.sources.iter() { + if key.starts_with("contracts/") { + contract_source + .ast + .accept(&mut context_loader) + .unwrap_or_else(|err| { + // Exit with a non-zero exit code + eprintln!("Error loading Hardhat AST into ContextLoader"); + eprintln!("{:?}", err); + std::process::exit(1); + }); + let source_file_path = root_path.join(key); + match read_file_to_string(&source_file_path) { + Ok(content) => { + for unit in &context_loader.source_units { + if let Some(ref abs_path) = unit.absolute_path { + if abs_path == key { + context_loader.set_source_unit_source_content(unit.id, content); + break; + } + } + } + } + Err(err) => { + eprintln!( + "Error reading Solidity source file: {}", + source_file_path.to_str().unwrap() + ); + eprintln!("{:?}", err); + } + } + } + } + + (src_path, context_loader) +} diff --git a/aderyn/src/virtual_foundry.rs b/aderyn/src/virtual_foundry.rs new file mode 100644 index 000000000..bac2b0850 --- /dev/null +++ b/aderyn/src/virtual_foundry.rs @@ -0,0 +1,50 @@ +use std::{path::PathBuf, process::Stdio}; + +pub fn build_isolated_workspace_for_file(solidity_file: &str) -> PathBuf { + let file = PathBuf::from(solidity_file); + + let forge_folder_name = file.file_stem().unwrap().to_str().unwrap(); + let safe_space = get_or_create_safespace(forge_folder_name); + + let new_name = safe_space.join("src").join(file.file_name().unwrap()); + + _ = std::fs::remove_file(safe_space.join("src").join("Counter.sol")); + _ = std::fs::remove_file(safe_space.join("test").join("Counter.t.sol")); + _ = std::fs::remove_file(safe_space.join("script").join("Counter.s.sol")); + + std::fs::copy(solidity_file, new_name).unwrap_or_else(|x| { + eprint!("Unable to copy your file to safespace! {}", x); + std::process::exit(1); + }); + + safe_space +} + +pub fn delete_safe_space(folder: &PathBuf) { + let _ = std::fs::remove_dir_all(folder); +} + +pub fn get_or_create_safespace(folder_name: &str) -> PathBuf { + let config_home_loc = std::env::var("XDG_CONFIG_HOME") + .or_else(|_| std::env::var("HOME").map(|home| format!("{}/.config", home))) + .unwrap(); + + let mut config_loc = PathBuf::from(config_home_loc); + config_loc.push("aderyn"); + + if !config_loc.exists() { + std::fs::create_dir_all(config_loc.as_path()).expect( + "Couldn't initialize aderyn folder at home directory for analyzing vulenrabilities ", + ); + } + + // Run `forge init ` in the root + let _output = std::process::Command::new("forge") + .args(["init", folder_name]) + .current_dir(&config_loc) + .stdout(Stdio::inherit()) // This will stream the stdout + .stderr(Stdio::inherit()) + .status(); + + config_loc.join(folder_name) +} diff --git a/aderyn_core/Cargo.toml b/aderyn_core/Cargo.toml new file mode 100644 index 000000000..bf23285c9 --- /dev/null +++ b/aderyn_core/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "aderyn_core" +version = "0.0.7" +edition = "2021" +authors = ["Alex Roan "] +description = "Rust based Solidity AST analyzer backend" +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +eyre = "0.6.8" +rayon = "1.8.0" +serde = { version = "1.0.160", features = ["derive"] } +serde_json = "1.0.96" +serde_repr = "0.1.12" +tokei = "12.1.2" +toml = "0.8.2" \ No newline at end of file diff --git a/src/ast/blocks.rs b/aderyn_core/src/ast/blocks.rs similarity index 100% rename from src/ast/blocks.rs rename to aderyn_core/src/ast/blocks.rs diff --git a/src/ast/contracts.rs b/aderyn_core/src/ast/contracts.rs similarity index 100% rename from src/ast/contracts.rs rename to aderyn_core/src/ast/contracts.rs diff --git a/src/ast/documentation.rs b/aderyn_core/src/ast/documentation.rs similarity index 100% rename from src/ast/documentation.rs rename to aderyn_core/src/ast/documentation.rs diff --git a/src/ast/enumerations.rs b/aderyn_core/src/ast/enumerations.rs similarity index 100% rename from src/ast/enumerations.rs rename to aderyn_core/src/ast/enumerations.rs diff --git a/src/ast/errors.rs b/aderyn_core/src/ast/errors.rs similarity index 100% rename from src/ast/errors.rs rename to aderyn_core/src/ast/errors.rs diff --git a/src/ast/events.rs b/aderyn_core/src/ast/events.rs similarity index 100% rename from src/ast/events.rs rename to aderyn_core/src/ast/events.rs diff --git a/src/ast/expressions.rs b/aderyn_core/src/ast/expressions.rs similarity index 100% rename from src/ast/expressions.rs rename to aderyn_core/src/ast/expressions.rs diff --git a/src/ast/functions.rs b/aderyn_core/src/ast/functions.rs similarity index 100% rename from src/ast/functions.rs rename to aderyn_core/src/ast/functions.rs diff --git a/src/ast/identifiers.rs b/aderyn_core/src/ast/identifiers.rs similarity index 100% rename from src/ast/identifiers.rs rename to aderyn_core/src/ast/identifiers.rs diff --git a/src/ast/import_directives.rs b/aderyn_core/src/ast/import_directives.rs similarity index 100% rename from src/ast/import_directives.rs rename to aderyn_core/src/ast/import_directives.rs diff --git a/src/ast/literals.rs b/aderyn_core/src/ast/literals.rs similarity index 100% rename from src/ast/literals.rs rename to aderyn_core/src/ast/literals.rs diff --git a/src/ast/magic.rs b/aderyn_core/src/ast/magic.rs similarity index 100% rename from src/ast/magic.rs rename to aderyn_core/src/ast/magic.rs diff --git a/src/ast/mod.rs b/aderyn_core/src/ast/mod.rs similarity index 100% rename from src/ast/mod.rs rename to aderyn_core/src/ast/mod.rs diff --git a/src/ast/modifiers.rs b/aderyn_core/src/ast/modifiers.rs similarity index 100% rename from src/ast/modifiers.rs rename to aderyn_core/src/ast/modifiers.rs diff --git a/src/ast/node.rs b/aderyn_core/src/ast/node.rs similarity index 100% rename from src/ast/node.rs rename to aderyn_core/src/ast/node.rs diff --git a/src/ast/pragma_directives.rs b/aderyn_core/src/ast/pragma_directives.rs similarity index 100% rename from src/ast/pragma_directives.rs rename to aderyn_core/src/ast/pragma_directives.rs diff --git a/src/ast/source_units.rs b/aderyn_core/src/ast/source_units.rs similarity index 100% rename from src/ast/source_units.rs rename to aderyn_core/src/ast/source_units.rs diff --git a/src/ast/statements.rs b/aderyn_core/src/ast/statements.rs similarity index 100% rename from src/ast/statements.rs rename to aderyn_core/src/ast/statements.rs diff --git a/src/ast/structures.rs b/aderyn_core/src/ast/structures.rs similarity index 100% rename from src/ast/structures.rs rename to aderyn_core/src/ast/structures.rs diff --git a/src/ast/types.rs b/aderyn_core/src/ast/types.rs similarity index 100% rename from src/ast/types.rs rename to aderyn_core/src/ast/types.rs diff --git a/src/ast/user_defined_value_types.rs b/aderyn_core/src/ast/user_defined_value_types.rs similarity index 100% rename from src/ast/user_defined_value_types.rs rename to aderyn_core/src/ast/user_defined_value_types.rs diff --git a/src/ast/using_for_directives.rs b/aderyn_core/src/ast/using_for_directives.rs similarity index 100% rename from src/ast/using_for_directives.rs rename to aderyn_core/src/ast/using_for_directives.rs diff --git a/src/ast/variables.rs b/aderyn_core/src/ast/variables.rs similarity index 100% rename from src/ast/variables.rs rename to aderyn_core/src/ast/variables.rs diff --git a/src/context/loader.rs b/aderyn_core/src/context/loader.rs similarity index 73% rename from src/context/loader.rs rename to aderyn_core/src/context/loader.rs index ce8ad7048..4ee1dbc6a 100644 --- a/src/context/loader.rs +++ b/aderyn_core/src/context/loader.rs @@ -119,62 +119,62 @@ impl ASTNode { #[derive(Default, Debug)] pub struct ContextLoader { - sloc_stats: Language, + pub sloc_stats: Language, pub nodes: HashMap, last_source_unit_id: i64, // Hashmaps of all nodes => source_unit_id - array_type_names: HashMap, - assignments: HashMap, - binary_operations: HashMap, - blocks: HashMap, - conditionals: HashMap, - contract_definitions: HashMap, - elementary_type_names: HashMap, - elementary_type_name_expressions: HashMap, - emit_statements: HashMap, - enum_definitions: HashMap, - enum_values: HashMap, - event_definitions: HashMap, - error_definitions: HashMap, - expression_statements: HashMap, - function_calls: HashMap, - function_call_options: HashMap, - function_definitions: HashMap, - function_type_names: HashMap, - for_statements: HashMap, - identifiers: HashMap, - identifier_paths: HashMap, - if_statements: HashMap, - import_directives: HashMap, - index_accesses: HashMap, - index_range_accesses: HashMap, - inheritance_specifiers: HashMap, - inline_assemblies: HashMap, - literals: HashMap, - member_accesses: HashMap, - new_expressions: HashMap, - mappings: HashMap, - modifier_definitions: HashMap, - modifier_invocations: HashMap, - override_specifiers: HashMap, - parameter_lists: HashMap, - pragma_directives: HashMap, - returns: HashMap, - revert_statements: HashMap, - source_units: Vec, - struct_definitions: HashMap, - structured_documentations: HashMap, - try_statements: HashMap, - try_catch_clauses: HashMap, - tuple_expressions: HashMap, - unary_operations: HashMap, - user_defined_type_names: HashMap, - user_defined_value_type_definitions: HashMap, - using_for_directives: HashMap, - variable_declarations: HashMap, - variable_declaration_statements: HashMap, - while_statements: HashMap, + pub array_type_names: HashMap, + pub assignments: HashMap, + pub binary_operations: HashMap, + pub blocks: HashMap, + pub conditionals: HashMap, + pub contract_definitions: HashMap, + pub elementary_type_names: HashMap, + pub elementary_type_name_expressions: HashMap, + pub emit_statements: HashMap, + pub enum_definitions: HashMap, + pub enum_values: HashMap, + pub event_definitions: HashMap, + pub error_definitions: HashMap, + pub expression_statements: HashMap, + pub function_calls: HashMap, + pub function_call_options: HashMap, + pub function_definitions: HashMap, + pub function_type_names: HashMap, + pub for_statements: HashMap, + pub identifiers: HashMap, + pub identifier_paths: HashMap, + pub if_statements: HashMap, + pub import_directives: HashMap, + pub index_accesses: HashMap, + pub index_range_accesses: HashMap, + pub inheritance_specifiers: HashMap, + pub inline_assemblies: HashMap, + pub literals: HashMap, + pub member_accesses: HashMap, + pub new_expressions: HashMap, + pub mappings: HashMap, + pub modifier_definitions: HashMap, + pub modifier_invocations: HashMap, + pub override_specifiers: HashMap, + pub parameter_lists: HashMap, + pub pragma_directives: HashMap, + pub returns: HashMap, + pub revert_statements: HashMap, + pub source_units: Vec, + pub struct_definitions: HashMap, + pub structured_documentations: HashMap, + pub try_statements: HashMap, + pub try_catch_clauses: HashMap, + pub tuple_expressions: HashMap, + pub unary_operations: HashMap, + pub user_defined_type_names: HashMap, + pub user_defined_value_type_definitions: HashMap, + pub using_for_directives: HashMap, + pub variable_declarations: HashMap, + pub variable_declaration_statements: HashMap, + pub while_statements: HashMap, } impl ContextLoader { @@ -190,220 +190,6 @@ impl ContextLoader { } } - // GETTERS - - pub fn get_sloc_stats(&self) -> &Language { - &self.sloc_stats - } - - pub fn get_node(&self, id: i64) -> Option<&ASTNode> { - self.nodes.get(&id) - } - - pub fn get_array_type_names(&self) -> Vec<&ArrayTypeName> { - self.array_type_names.keys().collect() - } - - pub fn get_assignments(&self) -> Vec<&Assignment> { - self.assignments.keys().collect() - } - - pub fn get_binary_operations(&self) -> Vec<&BinaryOperation> { - self.binary_operations.keys().collect() - } - - pub fn get_blocks(&self) -> Vec<&Block> { - self.blocks.keys().collect() - } - - pub fn get_conditionals(&self) -> Vec<&Conditional> { - self.conditionals.keys().collect() - } - - pub fn get_contract_definitions(&self) -> Vec<&ContractDefinition> { - self.contract_definitions.keys().collect() - } - - pub fn get_elementary_type_names(&self) -> Vec<&ElementaryTypeName> { - self.elementary_type_names.keys().collect() - } - - pub fn get_elementary_type_name_expressions(&self) -> Vec<&ElementaryTypeNameExpression> { - self.elementary_type_name_expressions.keys().collect() - } - - pub fn get_emit_statements(&self) -> Vec<&EmitStatement> { - self.emit_statements.keys().collect() - } - - pub fn get_enum_definitions(&self) -> Vec<&EnumDefinition> { - self.enum_definitions.keys().collect() - } - - pub fn get_enum_values(&self) -> Vec<&EnumValue> { - self.enum_values.keys().collect() - } - - pub fn get_event_definitions(&self) -> Vec<&EventDefinition> { - self.event_definitions.keys().collect() - } - - pub fn get_error_definitions(&self) -> Vec<&ErrorDefinition> { - self.error_definitions.keys().collect() - } - - pub fn get_expression_statements(&self) -> Vec<&ExpressionStatement> { - self.expression_statements.keys().collect() - } - - pub fn get_function_calls(&self) -> Vec<&FunctionCall> { - self.function_calls.keys().collect() - } - - pub fn get_function_call_options(&self) -> Vec<&FunctionCallOptions> { - self.function_call_options.keys().collect() - } - - pub fn get_function_definitions(&self) -> Vec<&FunctionDefinition> { - self.function_definitions.keys().collect() - } - - pub fn get_function_type_names(&self) -> Vec<&FunctionTypeName> { - self.function_type_names.keys().collect() - } - - pub fn get_for_statements(&self) -> Vec<&ForStatement> { - self.for_statements.keys().collect() - } - - pub fn get_identifiers(&self) -> Vec<&Identifier> { - self.identifiers.keys().collect() - } - - pub fn get_identifier_paths(&self) -> Vec<&IdentifierPath> { - self.identifier_paths.keys().collect() - } - - pub fn get_if_statements(&self) -> Vec<&IfStatement> { - self.if_statements.keys().collect() - } - - pub fn get_import_directives(&self) -> Vec<&ImportDirective> { - self.import_directives.keys().collect() - } - - pub fn get_index_accesses(&self) -> Vec<&IndexAccess> { - self.index_accesses.keys().collect() - } - - pub fn get_index_range_accesses(&self) -> Vec<&IndexRangeAccess> { - self.index_range_accesses.keys().collect() - } - - pub fn get_inheritance_specifiers(&self) -> Vec<&InheritanceSpecifier> { - self.inheritance_specifiers.keys().collect() - } - - pub fn get_inline_assemblies(&self) -> Vec<&InlineAssembly> { - self.inline_assemblies.keys().collect() - } - - pub fn get_literals(&self) -> Vec<&Literal> { - self.literals.keys().collect() - } - - pub fn get_member_accesses(&self) -> Vec<&MemberAccess> { - self.member_accesses.keys().collect() - } - - pub fn get_new_expressions(&self) -> Vec<&NewExpression> { - self.new_expressions.keys().collect() - } - - pub fn get_mappings(&self) -> Vec<&Mapping> { - self.mappings.keys().collect() - } - - pub fn get_modifier_definitions(&self) -> Vec<&ModifierDefinition> { - self.modifier_definitions.keys().collect() - } - - pub fn get_modifier_invocations(&self) -> Vec<&ModifierInvocation> { - self.modifier_invocations.keys().collect() - } - - pub fn get_override_specifiers(&self) -> Vec<&OverrideSpecifier> { - self.override_specifiers.keys().collect() - } - - pub fn get_parameter_lists(&self) -> Vec<&ParameterList> { - self.parameter_lists.keys().collect() - } - - pub fn get_pragma_directives(&self) -> Vec<&PragmaDirective> { - self.pragma_directives.keys().collect() - } - - pub fn get_returns(&self) -> Vec<&Return> { - self.returns.keys().collect() - } - - pub fn get_revert_statements(&self) -> Vec<&RevertStatement> { - self.revert_statements.keys().collect() - } - - pub fn get_struct_definitions(&self) -> Vec<&StructDefinition> { - self.struct_definitions.keys().collect() - } - - pub fn get_structured_documentations(&self) -> Vec<&StructuredDocumentation> { - self.structured_documentations.keys().collect() - } - - pub fn get_try_statements(&self) -> Vec<&TryStatement> { - self.try_statements.keys().collect() - } - - pub fn get_try_catch_clauses(&self) -> Vec<&TryCatchClause> { - self.try_catch_clauses.keys().collect() - } - - pub fn get_tuple_expressions(&self) -> Vec<&TupleExpression> { - self.tuple_expressions.keys().collect() - } - - pub fn get_unary_operations(&self) -> Vec<&UnaryOperation> { - self.unary_operations.keys().collect() - } - - pub fn get_user_defined_type_names(&self) -> Vec<&UserDefinedTypeName> { - self.user_defined_type_names.keys().collect() - } - - pub fn get_user_defined_value_type_definitions(&self) -> Vec<&UserDefinedValueTypeDefinition> { - self.user_defined_value_type_definitions.keys().collect() - } - - pub fn get_using_for_directives(&self) -> Vec<&UsingForDirective> { - self.using_for_directives.keys().collect() - } - - pub fn get_variable_declarations(&self) -> Vec<&VariableDeclaration> { - self.variable_declarations.keys().collect() - } - - pub fn get_variable_declaration_statements(&self) -> Vec<&VariableDeclarationStatement> { - self.variable_declaration_statements.keys().collect() - } - - pub fn get_while_statements(&self) -> Vec<&WhileStatement> { - self.while_statements.keys().collect() - } - - pub fn get_source_units(&self) -> Vec<&SourceUnit> { - self.source_units.iter().collect() - } - pub fn get_source_unit_from_child_node(&self, node: &ASTNode) -> Option<&SourceUnit> { let source_unit_id = match node { ASTNode::ArrayTypeName(node) => self.array_type_names.get(node), @@ -482,7 +268,11 @@ impl ContextLoader { pub fn get_node_sort_key(&self, node: &ASTNode) -> (String, usize) { let source_unit = self.get_source_unit_from_child_node(node).unwrap(); let absolute_path = source_unit.absolute_path.as_ref().unwrap().clone(); - let source_line = source_unit.source_line(node.src().unwrap()).unwrap(); + let source_line = node + .src() + .map(|src| source_unit.source_line(src).unwrap_or(0)) // If `src` is `Some`, get the line number, else return 0 + .unwrap_or(0); // If `src` is `None`, default to 0 + (absolute_path, source_line) } } @@ -912,13 +702,13 @@ mod loader_tests { fn test_delegate_call_in_loops() -> Result<()> { let mut loader = ContextLoader::default(); let extended_inheritance = read_compiler_output( - "tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", + "../tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", )?; let inheritance_base = read_compiler_output( - "tests/contract-playground/out/InheritanceBase.sol/InheritanceBase.json", + "../tests/contract-playground/out/InheritanceBase.sol/InheritanceBase.json", )?; let i_contract_inheritance = read_compiler_output( - "tests/contract-playground/out/IContractInheritance.sol/IContractInheritance.json", + "../tests/contract-playground/out/IContractInheritance.sol/IContractInheritance.json", )?; extended_inheritance.ast.accept(&mut loader)?; inheritance_base.ast.accept(&mut loader)?; @@ -926,7 +716,7 @@ mod loader_tests { // Get all for statements, and check if there is a delegate call in the body of each for statement let mut delegate_call_in_loop_detector = DelegateCallInLoopDetector::default(); - let for_statements = loader.get_for_statements(); + let for_statements = loader.for_statements.keys(); for for_statement in for_statements { for_statement.accept(&mut delegate_call_in_loop_detector)?; } diff --git a/src/context/mod.rs b/aderyn_core/src/context/mod.rs similarity index 100% rename from src/context/mod.rs rename to aderyn_core/src/context/mod.rs diff --git a/src/detect/detector.rs b/aderyn_core/src/detect/detector.rs similarity index 59% rename from src/detect/detector.rs rename to aderyn_core/src/detect/detector.rs index 5444c1bb3..0b0e7f374 100644 --- a/src/detect/detector.rs +++ b/aderyn_core/src/detect/detector.rs @@ -1,5 +1,5 @@ use crate::{ - context::loader::{ASTNode, ContextLoader}, + context::loader::ContextLoader, detect::{ high::delegate_call_in_loop::DelegateCallInLoopDetector, low::{ @@ -25,7 +25,7 @@ use crate::{ }, }, }; -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; pub fn get_all_detectors() -> Vec> { vec![ @@ -75,22 +75,67 @@ pub trait Detector { String::from("Description") } - fn instances(&self) -> Vec> { - Vec::new() + // Keys are source file name and line number + // Value is ASTNode.src + fn instances(&self) -> BTreeMap<(String, usize), String> { + BTreeMap::new() } } pub mod detector_test_helpers { + use std::path::PathBuf; + use crate::{ context::loader::ContextLoader, framework::foundry::read_foundry_output_file, - visitor::ast_visitor::Node, + read_file_to_string, visitor::ast_visitor::Node, }; pub fn load_contract(filepath: &str) -> ContextLoader { - let filepath = std::path::PathBuf::from(filepath); + let path_buf_filepath = std::path::PathBuf::from(filepath); let mut context_loader = ContextLoader::default(); - let foundry_output = read_foundry_output_file(filepath.to_str().unwrap()).unwrap(); + let foundry_output = read_foundry_output_file(path_buf_filepath.to_str().unwrap()).unwrap(); let _ = foundry_output.ast.accept(&mut context_loader); + // Get the path of the source file + let mut new_path = PathBuf::new(); + for component in path_buf_filepath.components() { + if component.as_os_str() == "out" { + break; + } + new_path.push(component); + } + new_path.push(foundry_output.ast.absolute_path.unwrap()); + match read_file_to_string(&new_path) { + Ok(content) => { + println!( + "Loaded Solidity source file: {}", + new_path.to_str().unwrap() + ); + // Convert the full_path to a string + let full_path_str = new_path.to_str().unwrap_or(""); + + // Find the index where "src/" starts + if let Some(start_index) = full_path_str.find("src/") { + let target_path = &full_path_str[start_index..]; + + // Search for a match and modify + for unit in context_loader.source_units.iter() { + if let Some(ref abs_path) = unit.absolute_path { + if abs_path == target_path { + context_loader.set_source_unit_source_content(unit.id, content); + break; + } + } + } + } + } + Err(err) => { + eprintln!( + "Error reading Solidity source file: {}", + new_path.to_str().unwrap() + ); + eprintln!("{:?}", err); + } + } context_loader } } diff --git a/src/detect/high/delegate_call_in_loop.rs b/aderyn_core/src/detect/high/delegate_call_in_loop.rs similarity index 71% rename from src/detect/high/delegate_call_in_loop.rs rename to aderyn_core/src/detect/high/delegate_call_in_loop.rs index 40e62e3b3..af876c1b4 100644 --- a/src/detect/high/delegate_call_in_loop.rs +++ b/aderyn_core/src/detect/high/delegate_call_in_loop.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::error::Error; use crate::visitor::ast_visitor::Node; @@ -11,13 +12,16 @@ use eyre::Result; #[derive(Default)] pub struct DelegateCallInLoopDetector { - found_delegate_call_in_loop: Vec>, + found_member_access: Vec>, + + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl ASTConstVisitor for DelegateCallInLoopDetector { fn visit_member_access(&mut self, node: &MemberAccess) -> Result { if node.member_name == "delegatecall" { - self.found_delegate_call_in_loop + self.found_member_access .push(Some(ASTNode::MemberAccess(node.clone()))); } Ok(true) @@ -26,14 +30,22 @@ impl ASTConstVisitor for DelegateCallInLoopDetector { impl Detector for DelegateCallInLoopDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for for_statement in loader.get_for_statements() { + for for_statement in loader.for_statements.keys() { for_statement.accept(self)?; } - for while_statement in loader.get_while_statements() { + for while_statement in loader.for_statements.keys() { while_statement.accept(self)?; } + for member_access in self.found_member_access.clone().into_iter().flatten() { + if let ASTNode::MemberAccess(member_access) = member_access { + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::MemberAccess(member_access.clone())), + member_access.src.clone(), + ); + } + } - Ok(!self.found_delegate_call_in_loop.is_empty()) + Ok(!self.found_instances.is_empty()) } fn severity(&self) -> IssueSeverity { @@ -48,8 +60,8 @@ impl Detector for DelegateCallInLoopDetector { String::from("When calling `delegatecall` the same `msg.value` amount will be accredited multiple times.") } - fn instances(&self) -> Vec> { - self.found_delegate_call_in_loop.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -62,7 +74,7 @@ mod delegate_call_in_loop_detector_tests { #[test] fn test_delegate_call_in_loop_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", + "../tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", ); let mut detector = DelegateCallInLoopDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/high/mod.rs b/aderyn_core/src/detect/high/mod.rs similarity index 100% rename from src/detect/high/mod.rs rename to aderyn_core/src/detect/high/mod.rs diff --git a/src/detect/low/avoid_abi_encode_packed.rs b/aderyn_core/src/detect/low/avoid_abi_encode_packed.rs similarity index 86% rename from src/detect/low/avoid_abi_encode_packed.rs rename to aderyn_core/src/detect/low/avoid_abi_encode_packed.rs index 251d77485..8b842eb05 100644 --- a/src/detect/low/avoid_abi_encode_packed.rs +++ b/aderyn_core/src/detect/low/avoid_abi_encode_packed.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,12 +8,13 @@ use eyre::Result; #[derive(Default)] pub struct AvoidAbiEncodePackedDetector { - found_abi_encode_packed: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for AvoidAbiEncodePackedDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for member_access in loader.get_member_accesses() { + for member_access in loader.member_accesses.keys() { // If the member_access's member_name = "encodePacked", loop through the argument_types and count how many of them contain any of the following in type_strings: // ["bytes ", "[]", "string"] // If the count is greater than 1, add the member_access to the found_abi_encode_packed vector @@ -37,12 +38,14 @@ impl Detector for AvoidAbiEncodePackedDetector { } } if count > 1 { - self.found_abi_encode_packed - .push(Some(ASTNode::MemberAccess(member_access.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::MemberAccess(member_access.clone())), + member_access.src.clone(), + ); } } } - Ok(!self.found_abi_encode_packed.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -59,8 +62,8 @@ impl Detector for AvoidAbiEncodePackedDetector { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_abi_encode_packed.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -72,8 +75,9 @@ mod avoid_abi_encode_packed_tests { #[test] fn test_avoid_abi_encode_packed_detector() { - let context_loader = - load_contract("./tests/contract-playground/out/KeccakContract.sol/KeccakContract.json"); + let context_loader = load_contract( + "../tests/contract-playground/out/KeccakContract.sol/KeccakContract.json", + ); let mut detector = AvoidAbiEncodePackedDetector::default(); let found = detector.detect(&context_loader).unwrap(); // assert that the detector found an abi encode packed diff --git a/src/detect/low/deprecated_oz_functions.rs b/aderyn_core/src/detect/low/deprecated_oz_functions.rs similarity index 78% rename from src/detect/low/deprecated_oz_functions.rs rename to aderyn_core/src/detect/low/deprecated_oz_functions.rs index 0caa2f2c4..363e7cd48 100644 --- a/src/detect/low/deprecated_oz_functions.rs +++ b/aderyn_core/src/detect/low/deprecated_oz_functions.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,12 +8,13 @@ use eyre::Result; #[derive(Default)] pub struct DeprecatedOZFunctionsDetector { - found_deprecated_oz_functions: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for DeprecatedOZFunctionsDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for identifier in loader.get_identifiers() { + for identifier in loader.identifiers.keys() { // if source_unit has any ImportDirectives with absolute_path containing "openzeppelin" // call identifier.accept(self) let source_unit = loader @@ -28,11 +29,13 @@ impl Detector for DeprecatedOZFunctionsDetector { .map_or(false, |path| path.contains("openzeppelin")) }) && identifier.name == "_setupRole" { - self.found_deprecated_oz_functions - .push(Some(ASTNode::Identifier(identifier.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Identifier(identifier.clone())), + identifier.src.clone(), + ); } } - for member_access in loader.get_member_accesses() { + for member_access in loader.member_accesses.keys() { // if source_unit has any ImportDirectives with absolute_path containing "openzeppelin" // call member_access.accept(self) let source_unit = loader @@ -46,11 +49,13 @@ impl Detector for DeprecatedOZFunctionsDetector { .map_or(false, |path| path.contains("openzeppelin")) }) && member_access.member_name == "safeApprove" { - self.found_deprecated_oz_functions - .push(Some(ASTNode::MemberAccess(member_access.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::MemberAccess(member_access.clone())), + member_access.src.clone(), + ); } } - Ok(!self.found_deprecated_oz_functions.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -65,8 +70,8 @@ impl Detector for DeprecatedOZFunctionsDetector { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_deprecated_oz_functions.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -79,7 +84,7 @@ mod deprecated_oz_functions_tests { #[test] fn test_deprecated_oz_functions_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", + "../tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", ); let mut detector = DeprecatedOZFunctionsDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/low/different_storage_conditionals.rs b/aderyn_core/src/detect/low/different_storage_conditionals.rs similarity index 84% rename from src/detect/low/different_storage_conditionals.rs rename to aderyn_core/src/detect/low/different_storage_conditionals.rs index b75737859..33d2e206a 100644 --- a/src/detect/low/different_storage_conditionals.rs +++ b/aderyn_core/src/detect/low/different_storage_conditionals.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, error::Error}; +use std::{ + collections::{BTreeMap, HashMap}, + error::Error, +}; use crate::{ ast::{BinaryOperation, Expression, VariableDeclaration}, @@ -9,15 +12,16 @@ use eyre::Result; #[derive(Default)] pub struct DifferentStorageConditionalDetector { - found_different_storage_conditionals: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for DifferentStorageConditionalDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { // Step 1: Get all state variable declarations let state_variables: Vec<&VariableDeclaration> = loader - .get_variable_declarations() - .into_iter() + .variable_declarations + .keys() .filter(|&var_decl| var_decl.state_variable) .collect(); @@ -30,7 +34,7 @@ impl Detector for DifferentStorageConditionalDetector { Vec<&BinaryOperation>, > = HashMap::new(); - for binary_operation in loader.get_binary_operations() { + for binary_operation in loader.binary_operations.keys() { if let Expression::Identifier(left_expr) = &*binary_operation.left_expression { if state_variable_ids.contains(&left_expr.referenced_declaration) { binary_operations_by_referenced_state_variable @@ -88,11 +92,17 @@ impl Detector for DifferentStorageConditionalDetector { && current_op_operator == mirror_operator); if !is_consistent_or_mirror { - self.found_different_storage_conditionals - .push(Some(ASTNode::BinaryOperation((*op).clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::BinaryOperation((*op).clone())), + op.src.clone(), + ); if !first_added { - self.found_different_storage_conditionals - .push(Some(ASTNode::BinaryOperation((*first_op).clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::BinaryOperation( + (*first_op).clone(), + )), + first_op.src.clone(), + ); first_added = true; } } @@ -100,20 +110,24 @@ impl Detector for DifferentStorageConditionalDetector { } } - Ok(!self.found_different_storage_conditionals.is_empty()) + Ok(!self.found_instances.is_empty()) } + fn title(&self) -> String { String::from("Conditional storage checks are not consistent") } + fn description(&self) -> String { String::from("When writing `require` or `if` conditionals that check storage values, it is important to be consistent to prevent off-by-one errors. \ There are instances found where the same storage variable is checked multiple times, but the conditionals are not consistent.") } + fn severity(&self) -> IssueSeverity { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_different_storage_conditionals.clone() + + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -126,7 +140,7 @@ mod different_storage_conditionals_tests { #[test] fn test_different_storage_conditionals() { let context_loader = load_contract( - "./tests/contract-playground/out/StorageConditionals.sol/StorageConditionals.json", + "../tests/contract-playground/out/StorageConditionals.sol/StorageConditionals.json", ); let mut detector = DifferentStorageConditionalDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/low/ecrecover.rs b/aderyn_core/src/detect/low/ecrecover.rs similarity index 81% rename from src/detect/low/ecrecover.rs rename to aderyn_core/src/detect/low/ecrecover.rs index 21b94ad4f..9bef2d9e3 100644 --- a/src/detect/low/ecrecover.rs +++ b/aderyn_core/src/detect/low/ecrecover.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,18 +8,21 @@ use eyre::Result; #[derive(Default)] pub struct EcrecoverDetector { - found_ecrecover: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for EcrecoverDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for identifier in loader.get_identifiers() { + for identifier in loader.identifiers.keys() { if identifier.name == "ecrecover" { - self.found_ecrecover - .push(Some(ASTNode::Identifier(identifier.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Identifier(identifier.clone())), + identifier.src.clone(), + ); } } - Ok(!self.found_ecrecover.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -41,8 +44,8 @@ impl Detector for EcrecoverDetector { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_ecrecover.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -56,7 +59,7 @@ mod ecrecover_tests { #[test] fn test_ecrecover_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", + "../tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", ); let mut detector = EcrecoverDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/low/mod.rs b/aderyn_core/src/detect/low/mod.rs similarity index 100% rename from src/detect/low/mod.rs rename to aderyn_core/src/detect/low/mod.rs diff --git a/src/detect/low/unsafe_erc20_functions.rs b/aderyn_core/src/detect/low/unsafe_erc20_functions.rs similarity index 77% rename from src/detect/low/unsafe_erc20_functions.rs rename to aderyn_core/src/detect/low/unsafe_erc20_functions.rs index dcc89308d..418e9a0e0 100644 --- a/src/detect/low/unsafe_erc20_functions.rs +++ b/aderyn_core/src/detect/low/unsafe_erc20_functions.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,21 +8,24 @@ use eyre::Result; #[derive(Default)] pub struct UnsafeERC20FunctionsDetector { - found_unsafe_erc20_functions: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for UnsafeERC20FunctionsDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for member_access in loader.get_member_accesses() { + for member_access in loader.member_accesses.keys() { if member_access.member_name == "transferFrom" || member_access.member_name == "approve" || member_access.member_name == "transfer" { - self.found_unsafe_erc20_functions - .push(Some(ASTNode::MemberAccess(member_access.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::MemberAccess(member_access.clone())), + member_access.src.clone(), + ); } } - Ok(!self.found_unsafe_erc20_functions.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -37,8 +40,8 @@ impl Detector for UnsafeERC20FunctionsDetector { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_unsafe_erc20_functions.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -51,7 +54,7 @@ mod unsafe_erc20_functions_tests { #[test] fn test_unsafe_erc20_functions() { let context_loader = load_contract( - "./tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", + "../tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", ); let mut detector = UnsafeERC20FunctionsDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/low/unspecific_solidity_pragma.rs b/aderyn_core/src/detect/low/unspecific_solidity_pragma.rs similarity index 76% rename from src/detect/low/unspecific_solidity_pragma.rs rename to aderyn_core/src/detect/low/unspecific_solidity_pragma.rs index cb17d16c0..5aafb4506 100644 --- a/src/detect/low/unspecific_solidity_pragma.rs +++ b/aderyn_core/src/detect/low/unspecific_solidity_pragma.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,21 +8,25 @@ use eyre::Result; #[derive(Default)] pub struct UnspecificSolidityPragmaDetector { - found_unspecific_solidity_pragma: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for UnspecificSolidityPragmaDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for pragma_directive in loader.get_pragma_directives() { + for pragma_directive in loader.pragma_directives.keys() { for literal in &pragma_directive.literals { if literal.contains('^') || literal.contains('>') { - self.found_unspecific_solidity_pragma - .push(Some(ASTNode::PragmaDirective(pragma_directive.clone()))); + self.found_instances.insert( + loader + .get_node_sort_key(&ASTNode::PragmaDirective(pragma_directive.clone())), + pragma_directive.src.clone(), + ); break; } } } - Ok(!self.found_unspecific_solidity_pragma.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -37,8 +41,8 @@ impl Detector for UnspecificSolidityPragmaDetector { IssueSeverity::Low } - fn instances(&self) -> Vec> { - self.found_unspecific_solidity_pragma.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -52,7 +56,7 @@ mod unspecific_solidity_pragma_tests { #[test] fn test_deprecated_oz_functions_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/IContractInheritance.sol/IContractInheritance.json", + "../tests/contract-playground/out/IContractInheritance.sol/IContractInheritance.json", ); let mut detector = UnspecificSolidityPragmaDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/medium/block_timestamp_deadline.rs b/aderyn_core/src/detect/medium/block_timestamp_deadline.rs similarity index 86% rename from src/detect/medium/block_timestamp_deadline.rs rename to aderyn_core/src/detect/medium/block_timestamp_deadline.rs index cf0f5a99a..0228f5a77 100644 --- a/src/detect/medium/block_timestamp_deadline.rs +++ b/aderyn_core/src/detect/medium/block_timestamp_deadline.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ ast::{Expression, FunctionCallKind}, @@ -9,13 +9,13 @@ use eyre::Result; #[derive(Default)] pub struct BlockTimestampDeadlineDetector { - found_block_timestamp_deadlines: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for BlockTimestampDeadlineDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - let function_calls = loader.get_function_calls(); - for call in function_calls { + for call in loader.function_calls.keys() { // Uniswap V2 - Function Calls // For each FunctionCall, if the Expression is a MemberAccess that is named any of the following: // [ @@ -47,8 +47,12 @@ impl Detector for BlockTimestampDeadlineDetector { *member_access.expression { if identifier.name == "block" { - self.found_block_timestamp_deadlines - .push(Some(ASTNode::FunctionCall(call.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::FunctionCall( + call.clone(), + )), + call.src.clone(), + ); } } } @@ -76,8 +80,12 @@ impl Detector for BlockTimestampDeadlineDetector { *member_access.expression { if identifier.name == "block" { - self.found_block_timestamp_deadlines - .push(Some(ASTNode::FunctionCall(call.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::FunctionCall( + call.clone(), + )), + call.src.clone(), + ); } } } @@ -89,7 +97,7 @@ impl Detector for BlockTimestampDeadlineDetector { } // TODO: Uniswap V3 - Struct definitions - Ok(!self.found_block_timestamp_deadlines.is_empty()) + Ok(!self.found_instances.is_empty()) } fn severity(&self) -> IssueSeverity { @@ -105,8 +113,8 @@ impl Detector for BlockTimestampDeadlineDetector { Consider allowing function caller to specify swap deadline input parameter.".to_string() } - fn instances(&self) -> Vec> { - self.found_block_timestamp_deadlines.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -120,7 +128,7 @@ mod block_timestamp_deadline_detector_tests { #[test] fn test_block_timestamp_deadline_uniswap_v2_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/UniswapV2Swapper.sol/UniswapV2Swapper.json", + "../tests/contract-playground/out/UniswapV2Swapper.sol/UniswapV2Swapper.json", ); let mut detector = BlockTimestampDeadlineDetector::default(); let found = detector.detect(&context_loader).unwrap(); @@ -151,7 +159,7 @@ mod block_timestamp_deadline_detector_tests { #[test] fn test_block_timestamp_deadline_uniswap_v3_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/UniswapV3Swapper.sol/UniswapV3Swapper.json", + "../tests/contract-playground/out/UniswapV3Swapper.sol/UniswapV3Swapper.json", ); let mut detector = BlockTimestampDeadlineDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/medium/centralization_risk.rs b/aderyn_core/src/detect/medium/centralization_risk.rs similarity index 72% rename from src/detect/medium/centralization_risk.rs rename to aderyn_core/src/detect/medium/centralization_risk.rs index c22cfb71b..bc5f49f38 100644 --- a/src/detect/medium/centralization_risk.rs +++ b/aderyn_core/src/detect/medium/centralization_risk.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ ast::ModifierInvocation, @@ -11,6 +11,9 @@ use eyre::Result; #[derive(Default)] pub struct CentralizationRiskDetector { found_centralization_risks: Vec>, + + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl ASTConstVisitor for CentralizationRiskDetector { @@ -57,11 +60,41 @@ impl ASTConstVisitor for CentralizationRiskDetector { impl Detector for CentralizationRiskDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for source_unit in loader.get_source_units() { + for source_unit in loader.source_units.iter() { source_unit.accept(self)?; } + for modifier_invocation in self + .found_centralization_risks + .clone() + .into_iter() + .flatten() + { + if let ASTNode::ModifierInvocation(modifier_invocation) = modifier_invocation { + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::ModifierInvocation( + modifier_invocation.clone(), + )), + modifier_invocation.src.clone(), + ); + } + } + for contract_definition in self + .found_centralization_risks + .clone() + .into_iter() + .flatten() + { + if let ASTNode::ContractDefinition(contract_definition) = contract_definition { + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::ContractDefinition( + contract_definition.clone(), + )), + contract_definition.src.clone(), + ); + } + } - Ok(!self.found_centralization_risks.is_empty()) + Ok(!self.found_instances.is_empty()) } fn severity(&self) -> IssueSeverity { @@ -76,8 +109,8 @@ impl Detector for CentralizationRiskDetector { String::from("Contracts have owners with privileged rights to perform admin tasks and need to be trusted to not perform malicious updates or drain funds.") } - fn instances(&self) -> Vec> { - self.found_centralization_risks.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -90,7 +123,7 @@ mod centralization_risk_detector_tests { #[test] fn test_centralization_risk_detector() { let context_loader = - load_contract("./tests/contract-playground/out/AdminContract.sol/AdminContract.json"); + load_contract("../tests/contract-playground/out/AdminContract.sol/AdminContract.json"); let mut detector = CentralizationRiskDetector::default(); let found = detector.detect(&context_loader).unwrap(); // assert that the detector found a centralization risk diff --git a/src/detect/medium/mod.rs b/aderyn_core/src/detect/medium/mod.rs similarity index 100% rename from src/detect/medium/mod.rs rename to aderyn_core/src/detect/medium/mod.rs diff --git a/src/detect/medium/solmate_safe_transfer_lib.rs b/aderyn_core/src/detect/medium/solmate_safe_transfer_lib.rs similarity index 83% rename from src/detect/medium/solmate_safe_transfer_lib.rs rename to aderyn_core/src/detect/medium/solmate_safe_transfer_lib.rs index dc302fde0..5a9e92002 100644 --- a/src/detect/medium/solmate_safe_transfer_lib.rs +++ b/aderyn_core/src/detect/medium/solmate_safe_transfer_lib.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -9,12 +9,13 @@ use eyre::Result; #[derive(Default)] pub struct SolmateSafeTransferLibDetector { found_solmate_import: bool, - found_transfer_usage: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for SolmateSafeTransferLibDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for import_directive in loader.get_import_directives() { + for import_directive in loader.import_directives.keys() { if !self.found_solmate_import { // If the import directive absolute_path contains the strings "solmate" and "SafeTransferLib", flip the found_solmate_import flag to true if import_directive @@ -33,19 +34,21 @@ impl Detector for SolmateSafeTransferLibDetector { } } - for member_access in loader.get_member_accesses() { + for member_access in loader.member_accesses.keys() { // If the member access member_name is any of the following names, add it to the list of found // found_transfer_usage vector: ["safeTransfer", "safeTransferFrom", "safeApprove"] if member_access.member_name == "safeTransfer" || member_access.member_name == "safeTransferFrom" || member_access.member_name == "safeApprove" { - self.found_transfer_usage - .push(Some(ASTNode::MemberAccess(member_access.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::MemberAccess(member_access.clone())), + member_access.src.clone(), + ); } } - if self.found_solmate_import && !self.found_transfer_usage.is_empty() { + if self.found_solmate_import && !self.found_instances.is_empty() { return Ok(true); } @@ -64,8 +67,8 @@ impl Detector for SolmateSafeTransferLibDetector { String::from("There is a subtle difference between the implementation of solmate's SafeTransferLib and OZ's SafeERC20: OZ's SafeERC20 checks if the token is a contract or not, solmate's SafeTransferLib does not.\nhttps://github.com/transmissions11/solmate/blob/main/src/utils/SafeTransferLib.sol#L9 \n`@dev Note that none of the functions in this library check that a token has code at all! That responsibility is delegated to the caller`\n") } - fn instances(&self) -> Vec> { - self.found_transfer_usage.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -79,7 +82,7 @@ mod solmate_safe_transfer_lib_tests { #[test] fn test_solmate_safe_transfer_lib() { let context_loader = - load_contract("./tests/contract-playground/out/T11sTranferer.sol/T11sTranferer.json"); + load_contract("../tests/contract-playground/out/T11sTranferer.sol/T11sTranferer.json"); let mut detector = SolmateSafeTransferLibDetector::default(); let found = detector.detect(&context_loader).unwrap(); // assert that the detector found a delegate call in a loop diff --git a/src/detect/medium/unsafe_oz_erc721_mint.rs b/aderyn_core/src/detect/medium/unsafe_oz_erc721_mint.rs similarity index 81% rename from src/detect/medium/unsafe_oz_erc721_mint.rs rename to aderyn_core/src/detect/medium/unsafe_oz_erc721_mint.rs index 41d8e9554..77c24f71e 100644 --- a/src/detect/medium/unsafe_oz_erc721_mint.rs +++ b/aderyn_core/src/detect/medium/unsafe_oz_erc721_mint.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,12 +8,13 @@ use eyre::Result; #[derive(Default)] pub struct UnsafeERC721MintDetector { - found_unsafe_erc721_mint: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for UnsafeERC721MintDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - for identifier in loader.get_identifiers() { + for identifier in loader.identifiers.keys() { // if source_unit has any ImportDirectives with absolute_path containing "openzeppelin" // call identifier.accept(self) let source_unit = loader @@ -28,25 +29,31 @@ impl Detector for UnsafeERC721MintDetector { .map_or(false, |path| path.contains("openzeppelin")) }) && identifier.name == "_mint" { - self.found_unsafe_erc721_mint - .push(Some(ASTNode::Identifier(identifier.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Identifier(identifier.clone())), + identifier.src.clone(), + ); } } - Ok(!self.found_unsafe_erc721_mint.is_empty()) + Ok(!self.found_instances.is_empty()) } + fn title(&self) -> String { String::from("Using `ERC721::_mint()` can be dangerous") } + fn description(&self) -> String { String::from( "Using `ERC721::_mint()` can mint ERC721 tokens to addresses which don't support ERC721 tokens. Use `_safeMint()` instead of `_mint()` for ERC721.", ) } + fn severity(&self) -> IssueSeverity { IssueSeverity::Medium } - fn instances(&self) -> Vec> { - self.found_unsafe_erc721_mint.clone() + + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -60,7 +67,7 @@ mod unsafe_erc721_mint_tests { #[test] fn test_unsafe_erc721_mint_detector() { let context_loader = load_contract( - "./tests/contract-playground/out/UnsafeERC721Mint.sol/UnsafeERC721Mint.json", + "../tests/contract-playground/out/UnsafeERC721Mint.sol/UnsafeERC721Mint.json", ); let mut detector = UnsafeERC721MintDetector::default(); let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/mod.rs b/aderyn_core/src/detect/mod.rs similarity index 100% rename from src/detect/mod.rs rename to aderyn_core/src/detect/mod.rs diff --git a/src/detect/nc/constants_instead_of_literals.rs b/aderyn_core/src/detect/nc/constants_instead_of_literals.rs similarity index 76% rename from src/detect/nc/constants_instead_of_literals.rs rename to aderyn_core/src/detect/nc/constants_instead_of_literals.rs index ce3fc4f04..732bd22fb 100644 --- a/src/detect/nc/constants_instead_of_literals.rs +++ b/aderyn_core/src/detect/nc/constants_instead_of_literals.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ ast::{Literal, LiteralKind}, @@ -11,6 +11,9 @@ use eyre::Result; #[derive(Default)] pub struct ConstantsInsteadOfLiteralsDetector { found_literals: Vec>, + + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl ASTConstVisitor for ConstantsInsteadOfLiteralsDetector { @@ -31,11 +34,19 @@ impl Detector for ConstantsInsteadOfLiteralsDetector { // get all function definitions. // for each function definition, find all Literal types // if the literal type is either a Number, HexString or Address, then add it to the list of found literals - for function_definition in loader.get_function_definitions() { + for function_definition in loader.function_definitions.keys() { function_definition.accept(self)?; } + for literal in self.found_literals.clone().into_iter().flatten() { + if let ASTNode::Literal(literal) = literal { + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Literal(literal.clone())), + literal.src.clone(), + ); + } + } - Ok(!self.found_literals.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -50,8 +61,8 @@ impl Detector for ConstantsInsteadOfLiteralsDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_literals.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -64,7 +75,7 @@ mod constants_instead_of_literals_tests { #[test] fn test_constants_instead_of_literals() { let context_loader = - load_contract("./tests/contract-playground/out/Counter.sol/Counter.json"); + load_contract("../tests/contract-playground/out/Counter.sol/Counter.json"); let mut detector = ConstantsInsteadOfLiteralsDetector::default(); // assert that the detector finds the public function let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/nc/mod.rs b/aderyn_core/src/detect/nc/mod.rs similarity index 100% rename from src/detect/nc/mod.rs rename to aderyn_core/src/detect/nc/mod.rs diff --git a/src/detect/nc/non_reentrant_before_others.rs b/aderyn_core/src/detect/nc/non_reentrant_before_others.rs similarity index 75% rename from src/detect/nc/non_reentrant_before_others.rs rename to aderyn_core/src/detect/nc/non_reentrant_before_others.rs index 05c4b1ebd..dff729b63 100644 --- a/src/detect/nc/non_reentrant_before_others.rs +++ b/aderyn_core/src/detect/nc/non_reentrant_before_others.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,23 +8,27 @@ use eyre::Result; #[derive(Default)] pub struct NonReentrantBeforeOthersDetector { - found_non_reentrant_after_others: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for NonReentrantBeforeOthersDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { - let function_definitions = loader.get_function_definitions(); + let function_definitions = loader.function_definitions.keys(); for definition in function_definitions { if definition.modifiers.len() > 1 { for (index, modifier) in definition.modifiers.iter().enumerate() { if modifier.modifier_name.name == "nonReentrant" && index != 0 { - self.found_non_reentrant_after_others - .push(Some(ASTNode::FunctionDefinition(definition.clone()))); + self.found_instances.insert( + loader + .get_node_sort_key(&ASTNode::ModifierInvocation(modifier.clone())), + modifier.src.clone(), + ); } } } } - Ok(!self.found_non_reentrant_after_others.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -39,8 +43,8 @@ impl Detector for NonReentrantBeforeOthersDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_non_reentrant_after_others.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -54,7 +58,7 @@ mod non_reentrant_before_others_tests { #[test] fn test_non_reentrant_before_others() { let context_loader = - load_contract("./tests/contract-playground/out/AdminContract.sol/AdminContract.json"); + load_contract("../tests/contract-playground/out/AdminContract.sol/AdminContract.json"); let mut detector = NonReentrantBeforeOthersDetector::default(); let found = detector.detect(&context_loader).unwrap(); // assert that the detector found something diff --git a/src/detect/nc/require_with_string.rs b/aderyn_core/src/detect/nc/require_with_string.rs similarity index 76% rename from src/detect/nc/require_with_string.rs rename to aderyn_core/src/detect/nc/require_with_string.rs index 33e41e846..7d75ba763 100644 --- a/src/detect/nc/require_with_string.rs +++ b/aderyn_core/src/detect/nc/require_with_string.rs @@ -1,7 +1,6 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ - ast::Identifier, context::loader::{ASTNode, ContextLoader}, detect::detector::{Detector, IssueSeverity}, }; @@ -9,29 +8,30 @@ use eyre::Result; #[derive(Default)] pub struct RequireWithStringDetector { - found_require_without_string: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for RequireWithStringDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { // Collect all require statements without a string literal. - let requires_and_reverts: Vec<&Identifier> = loader - .get_identifiers() - .iter() - .filter(|id| id.name == "revert" || id.name == "require") - .cloned() - .collect(); + let requires_and_reverts = loader + .identifiers + .keys() + .filter(|id| id.name == "revert" || id.name == "require"); for id in requires_and_reverts { if (id.name == "revert" && id.argument_types.as_ref().unwrap().is_empty()) || (id.name == "require" && id.argument_types.as_ref().unwrap().len() == 1) { - self.found_require_without_string - .push(Some(ASTNode::Identifier(id.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Identifier(id.clone())), + id.src.clone(), + ); } } - Ok(!self.found_require_without_string.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -46,8 +46,8 @@ impl Detector for RequireWithStringDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_require_without_string.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -60,7 +60,7 @@ mod require_with_string_tests { #[test] fn test_require_with_string() { let context_loader = load_contract( - "./tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", + "../tests/contract-playground/out/DeprecatedOZFunctions.sol/DeprecatedOZFunctions.json", ); let mut detector = RequireWithStringDetector::default(); // assert that the detector finds something diff --git a/src/detect/nc/unindexed_events.rs b/aderyn_core/src/detect/nc/unindexed_events.rs similarity index 80% rename from src/detect/nc/unindexed_events.rs rename to aderyn_core/src/detect/nc/unindexed_events.rs index df681b422..5ec6584db 100644 --- a/src/detect/nc/unindexed_events.rs +++ b/aderyn_core/src/detect/nc/unindexed_events.rs @@ -1,4 +1,4 @@ -use std::error::Error; +use std::{collections::BTreeMap, error::Error}; use crate::{ context::loader::{ASTNode, ContextLoader}, @@ -8,14 +8,15 @@ use eyre::Result; #[derive(Default)] pub struct UnindexedEventsDetector { - found_unindexed_events: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for UnindexedEventsDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { // for each event definition, check if it has any indexed parameters // if it does not, then add it to the list of found unindexed events - for event_definition in loader.get_event_definitions().iter() { + for event_definition in loader.event_definitions.keys() { let mut indexed_count = 0; let mut non_indexed = false; @@ -28,11 +29,14 @@ impl Detector for UnindexedEventsDetector { } if non_indexed && indexed_count < 3 { - self.found_unindexed_events - .push(Some(ASTNode::EventDefinition((*event_definition).clone()))); + self.found_instances.insert( + loader + .get_node_sort_key(&ASTNode::EventDefinition((*event_definition).clone())), + event_definition.src.clone(), + ); } } - Ok(!self.found_unindexed_events.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -49,8 +53,8 @@ impl Detector for UnindexedEventsDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_unindexed_events.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -63,14 +67,14 @@ mod unindexed_event_tests { #[test] fn test_unindexed_events() { let context_loader = load_contract( - "./tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", + "../tests/contract-playground/out/ExtendedInheritance.sol/ExtendedInheritance.json", ); let mut detector = UnindexedEventsDetector::default(); // assert that the detector finds the public function let found = detector.detect(&context_loader).unwrap(); assert!(found); // assert that the detector finds the correct number of unindexed events - assert_eq!(detector.found_unindexed_events.len(), 1); + assert_eq!(detector.instances().len(), 1); // assert that the detector returns the correct severity assert_eq!( detector.severity(), diff --git a/src/detect/nc/useless_public_function.rs b/aderyn_core/src/detect/nc/useless_public_function.rs similarity index 67% rename from src/detect/nc/useless_public_function.rs rename to aderyn_core/src/detect/nc/useless_public_function.rs index 58dd5e140..c625bd81a 100644 --- a/src/detect/nc/useless_public_function.rs +++ b/aderyn_core/src/detect/nc/useless_public_function.rs @@ -1,4 +1,7 @@ -use std::{collections::HashSet, error::Error}; +use std::{ + collections::{BTreeMap, HashSet}, + error::Error, +}; use crate::{ ast::{FunctionKind, Visibility}, @@ -9,23 +12,23 @@ use eyre::Result; #[derive(Default)] pub struct UselessPublicFunctionDetector { - found_useless_public_function: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl Detector for UselessPublicFunctionDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { // Collect the ids of all functions referenced by identifiers. let referenced_functions: HashSet<_> = loader - .get_identifiers() - .iter() + .identifiers + .keys() .map(|i| i.referenced_declaration) .collect(); - let function_definitions = loader.get_function_definitions(); + let function_definitions = loader.function_definitions.keys(); // Collect all public FunctionDefinitions which are not in the referenced set. let unreferenced_public_functions = function_definitions - .iter() .filter(|f| { f.visibility == Visibility::Public && f.kind != FunctionKind::Constructor @@ -33,10 +36,24 @@ impl Detector for UselessPublicFunctionDetector { }) .map(|f| Some(ASTNode::FunctionDefinition((*f).clone()))); - self.found_useless_public_function - .extend(unreferenced_public_functions); + self.found_instances.extend( + unreferenced_public_functions + .map(|node| { + ( + loader.get_node_sort_key(&node.clone().unwrap()), + // match node as FunctionDefinition + match node.unwrap() { + ASTNode::FunctionDefinition(function_definition) => { + function_definition.src.clone() + } + _ => unreachable!(), + }, + ) + }) + .collect::>(), + ); - Ok(!self.found_useless_public_function.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -51,8 +68,8 @@ impl Detector for UselessPublicFunctionDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_useless_public_function.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -65,7 +82,7 @@ mod useless_public_function_tests { #[test] fn test_useless_public_functions() { let context_loader = - load_contract("./tests/contract-playground/out/Counter.sol/Counter.json"); + load_contract("../tests/contract-playground/out/Counter.sol/Counter.json"); let mut detector = UselessPublicFunctionDetector::default(); // assert that the detector finds the public function let found = detector.detect(&context_loader).unwrap(); diff --git a/src/detect/nc/zero_address_check.rs b/aderyn_core/src/detect/nc/zero_address_check.rs similarity index 89% rename from src/detect/nc/zero_address_check.rs rename to aderyn_core/src/detect/nc/zero_address_check.rs index 79f04d3b1..e0ed64dba 100644 --- a/src/detect/nc/zero_address_check.rs +++ b/aderyn_core/src/detect/nc/zero_address_check.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, error::Error}; +use std::{ + collections::{BTreeMap, HashMap}, + error::Error, +}; use crate::{ ast::{Assignment, BinaryOperation, Expression, Mutability, VariableDeclaration}, @@ -26,8 +29,8 @@ pub struct ZeroAddressCheckDetector { // where the left hand side is a mutable address state variable assignments_to_mutable_address_state_variables: HashMap, - // List of all the assignments without zero address checks found - found_no_zero_address_check: Vec>, + // Keys are source file name and line number + found_instances: BTreeMap<(String, usize), String>, } impl ZeroAddressCheckDetector { @@ -84,8 +87,8 @@ impl Detector for ZeroAddressCheckDetector { fn detect(&mut self, loader: &ContextLoader) -> Result> { // Get all address state variables self.mutable_address_state_variables = loader - .get_variable_declarations() - .into_iter() // We can consume the Vec since it's just references. + .variable_declarations + .keys() .filter_map(|var_decl| { if !var_decl.constant && matches!(var_decl.mutability, Some(Mutability::Mutable)) @@ -111,7 +114,7 @@ impl Detector for ZeroAddressCheckDetector { .collect(); // Get all function definitions - for function_definition in loader.get_function_definitions() { + for function_definition in loader.function_definitions.keys() { // Reset transient variables self.reset_transient_variables(); // Visit the function definition using the BinaryOperator and Assignment visitors @@ -120,13 +123,15 @@ impl Detector for ZeroAddressCheckDetector { // in the binary_checks_against_zero_address, add the assignment to the found_no_zero_address_check for (key, value) in &self.assignments_to_mutable_address_state_variables { if !self.binary_checks_against_zero_address.contains_key(key) { - self.found_no_zero_address_check - .push(Some(ASTNode::Assignment(value.clone()))); + self.found_instances.insert( + loader.get_node_sort_key(&ASTNode::Assignment(value.clone())), + value.src.clone(), + ); } } } - Ok(!self.found_no_zero_address_check.is_empty()) + Ok(!self.found_instances.is_empty()) } fn title(&self) -> String { @@ -145,8 +150,8 @@ impl Detector for ZeroAddressCheckDetector { IssueSeverity::NC } - fn instances(&self) -> Vec> { - self.found_no_zero_address_check.clone() + fn instances(&self) -> BTreeMap<(String, usize), String> { + self.found_instances.clone() } } @@ -159,14 +164,15 @@ mod zero_address_check_tests { #[test] fn test_deprecated_oz_functions_detector() { - let context_loader = - load_contract("./tests/contract-playground/out/StateVariables.sol/StateVariables.json"); + let context_loader = load_contract( + "../tests/contract-playground/out/StateVariables.sol/StateVariables.json", + ); let mut detector = ZeroAddressCheckDetector::default(); let found = detector.detect(&context_loader).unwrap(); // assert that the detector found the issue assert!(found); // assert that the detector found the correct number of issues - assert_eq!(detector.found_no_zero_address_check.len(), 1); + assert_eq!(detector.instances().len(), 1); // assert that the severity is NC assert_eq!( detector.severity(), diff --git a/src/framework/foundry.rs b/aderyn_core/src/framework/foundry.rs similarity index 100% rename from src/framework/foundry.rs rename to aderyn_core/src/framework/foundry.rs diff --git a/src/framework/hardhat.rs b/aderyn_core/src/framework/hardhat.rs similarity index 100% rename from src/framework/hardhat.rs rename to aderyn_core/src/framework/hardhat.rs diff --git a/src/framework/mod.rs b/aderyn_core/src/framework/mod.rs similarity index 100% rename from src/framework/mod.rs rename to aderyn_core/src/framework/mod.rs diff --git a/src/lib.rs b/aderyn_core/src/lib.rs similarity index 77% rename from src/lib.rs rename to aderyn_core/src/lib.rs index b3667a2a0..b153ea475 100644 --- a/src/lib.rs +++ b/aderyn_core/src/lib.rs @@ -8,15 +8,15 @@ pub mod visitor; use eyre::Result; use std::error::Error; use std::fs::{remove_file, File}; -use std::io; -use std::path::Path; +use std::io::{self}; +use std::path::{Path, PathBuf}; use crate::context::loader::ContextLoader; use crate::detect::detector::{get_all_detectors, IssueSeverity}; use crate::report::printer::{MarkdownReportPrinter, ReportPrinter}; use crate::report::reporter::{Issue, Report}; -pub fn run(context_loader: ContextLoader) -> Result<(), Box> { +pub fn run(context_loader: ContextLoader, output_file_path: String) -> Result<(), Box> { println!("Get Detectors"); let detectors = get_all_detectors(); @@ -56,17 +56,28 @@ pub fn run(context_loader: ContextLoader) -> Result<(), Box> { println!("Detectors run, processing found issues"); let printer = MarkdownReportPrinter; - report.post_process(&context_loader); println!("Found issues processed. Printing report"); - printer.print_report(get_markdown_writer("report.md")?, &report, &context_loader)?; + printer.print_report( + get_markdown_writer(&output_file_path)?, + &report, + &context_loader, + )?; - println!("Report printed to ./report.md"); + println!("Report printed to {}", output_file_path); Ok(()) } fn get_markdown_writer(filename: &str) -> io::Result { + let file_path = Path::new(filename); + if let Some(parent_dir) = file_path.parent() { + std::fs::create_dir_all(parent_dir)?; + } if Path::new(filename).exists() { remove_file(filename)?; // If file exists, delete it } File::create(filename) } + +pub fn read_file_to_string(path: &PathBuf) -> Result { + Ok(std::fs::read_to_string(path)?) +} diff --git a/src/report/mod.rs b/aderyn_core/src/report/mod.rs similarity index 100% rename from src/report/mod.rs rename to aderyn_core/src/report/mod.rs diff --git a/src/report/printer.rs b/aderyn_core/src/report/printer.rs similarity index 91% rename from src/report/printer.rs rename to aderyn_core/src/report/printer.rs index d75ecb4c2..51456435d 100644 --- a/src/report/printer.rs +++ b/aderyn_core/src/report/printer.rs @@ -1,6 +1,6 @@ use std::io::{Result, Write}; -use crate::{ast::SourceUnit, context::loader::ContextLoader}; +use crate::context::loader::ContextLoader; use super::reporter::{Issue, Report}; @@ -104,8 +104,8 @@ impl ReportPrinter for MarkdownReportPrinter { // Files Summary writeln!(writer, "## Files Summary\n")?; - let total_source_units = loader.get_source_units().len(); - let total_sloc = loader.get_sloc_stats().code; + let total_source_units = loader.source_units.len(); + let total_sloc = loader.sloc_stats.code; // Start the markdown table writeln!(writer, "| Key | Value |")?; @@ -122,10 +122,12 @@ impl ReportPrinter for MarkdownReportPrinter { writeln!(writer, "| Filepath | nSLOC |")?; writeln!(writer, "| --- | --- |")?; - let sloc_stats = loader.get_sloc_stats(); + let sloc_stats = &loader.sloc_stats; - let mut source_units = loader.get_source_units(); - source_units.sort_by_key(|su| su.absolute_path.as_deref().unwrap_or("")); + let mut source_units = loader.source_units.clone(); + source_units.sort_by_key(|su: &crate::ast::SourceUnit| { + su.absolute_path.as_deref().unwrap_or("").to_string() + }); // Iterate over source units and add each as a row in the markdown table for source_unit in source_units { @@ -259,7 +261,7 @@ impl ReportPrinter for MarkdownReportPrinter { &self, mut writer: W, issue: &Issue, - loader: &ContextLoader, + _loader: &ContextLoader, severity: &str, number: i32, ) -> Result<()> { @@ -268,16 +270,7 @@ impl ReportPrinter for MarkdownReportPrinter { "## {}-{}: {}\n\n{}\n", // is the anchor for the issue title severity, number, issue.title, issue.description )?; - for node in issue.instances.iter().flatten() { - let mut contract_path = "unknown"; - let source_unit: &SourceUnit = loader.get_source_unit_from_child_node(node).unwrap(); - if let Some(path) = source_unit.absolute_path.as_ref() { - contract_path = path; - } - let mut line_number = 0; - if let Some(src) = node.src() { - line_number = source_unit.source_line(src).unwrap(); - } + for (contract_path, line_number) in issue.instances.keys() { writeln!( writer, "- Found in {}: Line: {}", diff --git a/aderyn_core/src/report/reporter.rs b/aderyn_core/src/report/reporter.rs new file mode 100644 index 000000000..f1020fc8e --- /dev/null +++ b/aderyn_core/src/report/reporter.rs @@ -0,0 +1,19 @@ +use std::collections::BTreeMap; + +#[derive(Default, PartialEq)] +pub struct Report { + pub criticals: Vec, + pub highs: Vec, + pub mediums: Vec, + pub lows: Vec, + pub ncs: Vec, +} + +#[derive(Default, PartialEq, Clone, Debug)] +pub struct Issue { + pub title: String, + pub description: String, + // Keys are source file name and line number + // Value is ASTNode.src + pub instances: BTreeMap<(String, usize), String>, +} diff --git a/src/visitor/ast_visitor.rs b/aderyn_core/src/visitor/ast_visitor.rs similarity index 100% rename from src/visitor/ast_visitor.rs rename to aderyn_core/src/visitor/ast_visitor.rs diff --git a/src/visitor/mod.rs b/aderyn_core/src/visitor/mod.rs similarity index 100% rename from src/visitor/mod.rs rename to aderyn_core/src/visitor/mod.rs diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 92b4e19a6..000000000 --- a/src/main.rs +++ /dev/null @@ -1,210 +0,0 @@ -use aderyn::{ - context::loader::ContextLoader, - framework::{ - foundry::{load_foundry, read_foundry_output_file}, - hardhat::load_hardhat, - }, - run, - visitor::ast_visitor::Node, -}; -use clap::Parser; -use std::{ - fs::{read_dir, File}, - io::{Read, Result}, - path::PathBuf, -}; -use tokei::{Config, LanguageType}; - -#[derive(Parser, Debug)] -#[command(author, version, about, long_about = None)] -struct Args { - /// Foundry or Hardhat project root directory - root: String, -} - -enum Framework { - Foundry, - Hardhat, -} - -fn main() { - let args = Args::parse(); - - // Detect the framework - println!("Detecting framework..."); - let root_path = PathBuf::from(&args.root); - let framework = detect_framework(root_path.clone()).unwrap_or_else(|| { - // Exit with a non-zero exit code - eprintln!("Error detecting framework"); - std::process::exit(1); - }); - - let src_path: String; - let mut context_loader = ContextLoader::default(); - - // This whole block loads the solidity files and ASTs into the context loader - // TODO: move much of this gutsy stuff into the foundry / hardhat modules. - match framework { - Framework::Foundry => { - println!("Framework detected: Foundry mode engaged."); - println!("Foundry root path: {:?}", root_path); - let loaded_foundry = load_foundry(&root_path).unwrap_or_else(|err| { - // Exit with a non-zero exit code - eprintln!("Error loading Foundry Root"); - eprintln!("{:?}", err); - std::process::exit(1); - }); - src_path = root_path - .join(loaded_foundry.src_path) - .to_str() - .unwrap() - .to_string(); - // Load the foundry output files into the context loader using the ASTs - for output_filepath in loaded_foundry.output_filepaths { - // read_foundry_output_file and print an error message if it fails - if let Ok(foundry_output) = - read_foundry_output_file(output_filepath.to_str().unwrap()) - { - foundry_output - .ast - .accept(&mut context_loader) - .unwrap_or_else(|err| { - // Exit with a non-zero exit code - eprintln!("Error loading Foundry AST into ContextLoader"); - eprintln!("{:?}", err); - std::process::exit(1); - }) - } else { - eprintln!( - "Error reading Foundry output file: {}", - output_filepath.to_str().unwrap() - ); - } - } - // Load the solidity source files into memory, and assign the content to the source_unit.source - for source_filepath in loaded_foundry.src_filepaths { - match read_file_to_string(&source_filepath) { - Ok(content) => { - // Convert the full_path to a string - let full_path_str = source_filepath.to_str().unwrap_or(""); - - // Find the index where "src/" starts - if let Some(start_index) = full_path_str.find("src/") { - let target_path = &full_path_str[start_index..]; - - // Search for a match and modify - for unit in context_loader.get_source_units() { - if let Some(ref abs_path) = unit.absolute_path { - if abs_path == target_path { - context_loader - .set_source_unit_source_content(unit.id, content); - break; - } - } - } - } - } - Err(err) => { - eprintln!( - "Error reading Solidity source file: {}", - source_filepath.to_str().unwrap() - ); - eprintln!("{:?}", err); - } - } - } - } - Framework::Hardhat => { - println!("Framework detected. Hardhat mode engaged."); - println!("Hardhat root path: {:?}", root_path); - src_path = root_path.join("contracts").to_str().unwrap().to_string(); - let hardhat_output = load_hardhat(&root_path).unwrap_or_else(|err| { - // Exit with a non-zero exit code - eprintln!("Error loading Hardhat build info"); - eprintln!("{:?}", err); - std::process::exit(1); - }); - for (key, contract_source) in hardhat_output.output.sources.iter() { - if key.starts_with("contracts/") { - contract_source - .ast - .accept(&mut context_loader) - .unwrap_or_else(|err| { - // Exit with a non-zero exit code - eprintln!("Error loading Hardhat AST into ContextLoader"); - eprintln!("{:?}", err); - std::process::exit(1); - }); - let source_file_path = root_path.join(key); - match read_file_to_string(&source_file_path) { - Ok(content) => { - for unit in context_loader.get_source_units() { - if let Some(ref abs_path) = unit.absolute_path { - if abs_path == key { - context_loader - .set_source_unit_source_content(unit.id, content); - break; - } - } - } - } - Err(err) => { - eprintln!( - "Error reading Solidity source file: {}", - source_file_path.to_str().unwrap() - ); - eprintln!("{:?}", err); - } - } - } - } - } - } - - // Using the source path, get the sloc from tokei - let mut languages = tokei::Languages::new(); - let tokei_config = Config::default(); - languages.get_statistics(&[src_path], &[], &tokei_config); - context_loader.set_sloc_stats(languages[&LanguageType::Solidity].clone()); - - // Load the context loader into the run function, which runs the detectors - run(context_loader).unwrap_or_else(|err| { - // Exit with a non-zero exit code - eprintln!("Error running aderyn"); - eprintln!("{:?}", err); - std::process::exit(1); - }); -} - -fn read_file_to_string(path: &PathBuf) -> Result { - let mut file = File::open(path)?; - let mut content = String::new(); - file.read_to_string(&mut content)?; - Ok(content) -} - -fn detect_framework(path: PathBuf) -> Option { - // Canonicalize the path - let canonical_path = path.canonicalize().expect("Failed to canonicalize path"); - - // Check if the directory exists - if !canonical_path.is_dir() { - return None; - } - - // Read the contents of the directory - let entries = read_dir(&canonical_path).expect("Failed to read directory"); - - for entry in entries.flatten() { - let filename = entry.file_name(); - match filename.to_str() { - Some("foundry.toml") => return Some(Framework::Foundry), - Some("hardhat.config.js") | Some("hardhat.config.ts") => { - return Some(Framework::Hardhat) - } - _ => {} - } - } - - None -} diff --git a/src/report/reporter.rs b/src/report/reporter.rs deleted file mode 100644 index a657382cb..000000000 --- a/src/report/reporter.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::context::loader::{ASTNode, ContextLoader}; - -#[derive(Default, PartialEq)] -pub struct Report { - pub criticals: Vec, - pub highs: Vec, - pub mediums: Vec, - pub lows: Vec, - pub ncs: Vec, -} - -impl Report { - pub fn post_process(&mut self, loader: &ContextLoader) { - sort_issue_instances(&mut self.criticals, loader); - sort_issue_instances(&mut self.highs, loader); - sort_issue_instances(&mut self.mediums, loader); - sort_issue_instances(&mut self.lows, loader); - sort_issue_instances(&mut self.ncs, loader); - } -} - -fn sort_issue_instances(issues: &mut Vec, loader: &ContextLoader) { - for issue in issues { - issue.sort_instances(loader); - } -} - -#[derive(Default, PartialEq, Clone, Debug)] -pub struct Issue { - pub title: String, - pub description: String, - pub instances: Vec>, -} - -impl Issue { - fn sort_instances(&mut self, loader: &ContextLoader) { - self.instances.sort_by(|a, b| { - let a_key = loader.get_node_sort_key(a.as_ref().unwrap()); - let b_key = loader.get_node_sort_key(b.as_ref().unwrap()); - a_key.cmp(&b_key) - }); - } -}