diff --git a/Cargo.lock b/Cargo.lock index b14cd185..d5e416ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -401,6 +401,7 @@ dependencies = [ "pyrometer", "reqwest", "shared", + "solang-parser", "tokio", "tracing", "tracing-subscriber", @@ -413,6 +414,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + [[package]] name = "const-hex" version = "1.10.0" @@ -1879,6 +1890,7 @@ dependencies = [ "ahash", "analyzers", "ariadne", + "colored", "criterion", "ethers-core", "graph", diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 4b76ced0..f170f3d2 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -23,6 +23,7 @@ tracing-subscriber.workspace = true tracing-tree.workspace = true petgraph.workspace = true ethers-core.workspace = true +solang-parser.workspace = true clap = { version = "4.1.4", features = ["derive"] } tokio = { version = "1", features = ["full"] } diff --git a/crates/cli/src/detectors/mod.rs b/crates/cli/src/detectors/mod.rs new file mode 100644 index 00000000..55b5fc47 --- /dev/null +++ b/crates/cli/src/detectors/mod.rs @@ -0,0 +1,41 @@ +use pyrometer::detector::{Detector, DetectorResult}; +use pyrometer::Analyzer; +use pyrometer::reporter::{ReportFormat, get_reporter}; + +pub mod my_detector; +pub mod second_detector; + +// Import other detector modules here + +pub fn get_all_detectors() -> Vec> { + vec![ + Box::new(my_detector::MyDetector), + Box::new(second_detector::SecondDetector), + // Add more detectors here as you create them + // Box::new(your_detector::YourDetector), + // Box::new(his_detector::HisDetector), + ] +} + +pub fn find_detector(name: &str) -> Option> { + get_all_detectors().into_iter().find(|d| d.name() == name) +} + +pub fn run_detectors(analyzer: &Analyzer, detector_names: &[String]) -> Vec { + let mut results = Vec::new(); + + for name in detector_names { + if let Some(detector) = find_detector(name) { + results.extend(detector.run(analyzer)); + } else { + println!("Unknown detector: {}", name); + } + } + + results +} + +pub fn report_results(results: &[DetectorResult], format: ReportFormat) { + let reporter = get_reporter(format); + reporter.report(results); +} \ No newline at end of file diff --git a/crates/cli/src/detectors/my_detector.rs b/crates/cli/src/detectors/my_detector.rs new file mode 100644 index 00000000..b2d17267 --- /dev/null +++ b/crates/cli/src/detectors/my_detector.rs @@ -0,0 +1,38 @@ +// crates/cli/src/detectors/my_detector.rs +use pyrometer::detector::{Confidence, Detector, DetectorResult, Severity}; +use pyrometer::Analyzer; +use solang_parser::pt::Loc; + +pub struct MyDetector; + +impl Detector for MyDetector { + fn name(&self) -> &'static str { + "MyDetector" + } + + fn description(&self) -> String { + "Detects a specific pattern in the code".to_string() + } + + fn severity(&self) -> Severity { + Severity::Medium + } + + fn confidence(&self) -> Confidence { + Confidence::Medium + } + + fn run(&self, analyzer: &Analyzer) -> Vec { + // Implement detection logic here + let mut results = vec![]; + results.push(DetectorResult { + issue_name: "test_issue".to_string(), + location: Loc::File(0, 0, 100), + detector_name: self.name().to_string(), + severity: self.severity(), + confidence: self.confidence(), + message: "This is a test message".to_string(), + }); + results + } +} diff --git a/crates/cli/src/detectors/second_detector.rs b/crates/cli/src/detectors/second_detector.rs new file mode 100644 index 00000000..175b3ee2 --- /dev/null +++ b/crates/cli/src/detectors/second_detector.rs @@ -0,0 +1,28 @@ +// crates/cli/src/detectors/second_detector.rs +use pyrometer::detector::{Detector, DetectorResult, Severity, Confidence}; +use pyrometer::Analyzer; + +pub struct SecondDetector; + +impl Detector for SecondDetector { + fn name(&self) -> &'static str { + "SecondDetector" + } + + fn description(&self) -> String { + "Detects a second pattern in the code".to_string() + } + + fn severity(&self) -> Severity { + Severity::Medium + } + + fn confidence(&self) -> Confidence { + Confidence::Medium + } + + fn run(&self, analyzer: &Analyzer) -> Vec { + // Implement detection logic here + vec![] + } +} \ No newline at end of file diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 4be132b9..92f2fd92 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -10,7 +10,7 @@ use shared::{post_to_site, Search}; use shared::{GraphDot, USE_DEBUG_SITE}; use ariadne::sources; -use clap::{ArgAction, Parser, ValueHint}; +use clap::{ArgAction, Parser, ValueHint, Subcommand}; use tracing::{error, trace}; use tracing_subscriber::{prelude::*, Registry}; @@ -22,10 +22,17 @@ use std::{ path::PathBuf, }; use tokio::runtime::Runtime; +mod detectors; +use detectors::{get_all_detectors, run_detectors}; +use detectors::my_detector::MyDetector; +use pyrometer::reporter::ReportFormat; +use crate::detectors::report_results; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { + #[command(subcommand)] + command: Option, /// The path to the solidity file to process #[clap(value_hint = ValueHint::FilePath, value_name = "PATH")] pub path: String, @@ -115,6 +122,16 @@ struct Args { pub debug_stack: bool, } +#[derive(Subcommand, Debug)] +enum Commands { + Detect { + // --detectors MyDetector,SecondDetector | --detectors="MyDetector,SecondDetector" + #[clap(long, value_delimiter = ',')] + detectors: Option>, + }, + ListDetectors, +} + pub fn subscriber() { tracing_subscriber::Registry::default() .with(tracing_subscriber::filter::EnvFilter::from_default_env()) @@ -569,4 +586,25 @@ fn main() { // } // println!(); // }); + + match args.command { + Some(Commands::Detect { detectors }) => { + // add in * stuff + let detector_names = detectors.unwrap_or_else(|| { + println!("No detectors specified. Running all detectors..."); + get_all_detectors().iter().map(|d| d.name().to_string()).collect() + }); + + println!("Running detectors: {:?}", detector_names); + let results = run_detectors(&analyzer, &detector_names); + report_results(&results, ReportFormat::Stdout); + }, + Some(Commands::ListDetectors) => { + println!("Available detectors:"); + for detector in get_all_detectors() { + println!("- {} : {}", detector.name(), detector.description()); + } + }, + None => (), + } } diff --git a/crates/pyrometer/Cargo.toml b/crates/pyrometer/Cargo.toml index 7a279ba5..8933d268 100644 --- a/crates/pyrometer/Cargo.toml +++ b/crates/pyrometer/Cargo.toml @@ -27,6 +27,7 @@ serde_json = "1" tokio = { version = "1", features = ["full"] } serde = { version = "1", features = ["derive"] } reqwest = { version = "0.12", features = ["json"] } +colored = "2.0" diff --git a/crates/pyrometer/src/detector.rs b/crates/pyrometer/src/detector.rs new file mode 100644 index 00000000..afab45bc --- /dev/null +++ b/crates/pyrometer/src/detector.rs @@ -0,0 +1,88 @@ +use crate::Analyzer; +use serde::{Deserialize, Serialize}; +use solang_parser::pt::Loc; + +/// Represents the severity of an issue detected in the code. +/// +/// The severity levels are ordered from least to most severe: +/// Informational < Low < Medium < High +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Severity { + /// Informational issues are not problematic but might be of interest. + Informational, + /// Low severity issues pose minimal risk. + Low, + /// Medium severity issues pose moderate risk and should be addressed. + Medium, + /// High severity issues pose significant risk and require attention. + High, +} + +/// Indicates the level of confidence in the accuracy of a detected issue. +/// +/// The confidence levels are ordered from least to most certain: +/// Low < Medium < High < Certain +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Confidence { + /// Low confidence suggests a high chance of false positives. + Low, + /// Medium confidence indicates a moderate level of certainty. + Medium, + /// High confidence suggests a low chance of false positives. + High, + /// Certain confidence indicates that the issue is definitely present. + Certain, +} + +/// Represents the result of a detector finding an issue in the code. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DetectorResult { + /// The name or title of the detected issue. + pub issue_name: String, + /// The name of the detector that found the issue. + pub detector_name: String, + /// The location in the source code where the issue was detected. + pub location: Loc, + /// A detailed description of the detected issue. + pub message: String, + /// A list of details about the issue. + // pub details: Vec, + /// The severity level of the detected issue. + pub severity: Severity, + /// The confidence level in the accuracy of the detected issue. + pub confidence: Confidence, +} + + +impl DetectorResult {} + +pub struct DetectorDetail { + pub loc: Loc, + pub description: String, +} + +/// Defines the interface for implementing a code pattern detector. +pub trait Detector { + /// Returns the name of the detector. + fn name(&self) -> &'static str; + + /// Provides a description of what the detector looks for. + fn description(&self) -> String; + + /// Returns the severity level of issues found by this detector. + fn severity(&self) -> Severity; + + /// Returns the confidence level of issues found by this detector. + fn confidence(&self) -> Confidence; + + /// Executes the detector on the provided analyzer and returns a list of detected issues. + /// + /// # Arguments + /// + /// * `analyzer` - A reference to the Analyzer containing the code to be analyzed. + /// + /// # Returns + /// + /// A vector of `DetectorResult` instances, each representing a detected issue. + fn run(&self, analyzer: &Analyzer) -> Vec; +} diff --git a/crates/pyrometer/src/lib.rs b/crates/pyrometer/src/lib.rs index b08cde03..f303bb4e 100644 --- a/crates/pyrometer/src/lib.rs +++ b/crates/pyrometer/src/lib.rs @@ -3,5 +3,7 @@ mod analyzer; mod analyzer_backend; mod builtin_fns; pub mod graph_backend; +pub mod detector; +pub mod reporter; pub use analyzer::*; diff --git a/crates/pyrometer/src/reporter.rs b/crates/pyrometer/src/reporter.rs new file mode 100644 index 00000000..e959c2c1 --- /dev/null +++ b/crates/pyrometer/src/reporter.rs @@ -0,0 +1,126 @@ +use crate::detector::{Confidence, DetectorResult, Severity}; +use colored::*; + +pub trait DetectorResultReporter { + fn report(&self, results: &[DetectorResult]); +} + +pub enum ReportFormat { + Markdown, + Json, + Stdout, +} + +pub struct MarkdownReporter; +pub struct JsonReporter; +pub struct StdoutReporter; + +pub fn get_reporter(format: ReportFormat) -> Box { + match format { + ReportFormat::Markdown => Box::new(MarkdownReporter), + ReportFormat::Json => Box::new(JsonReporter), + ReportFormat::Stdout => Box::new(StdoutReporter), + } +} + +impl DetectorResultReporter for MarkdownReporter { + fn report(&self, results: &[DetectorResult]) { + // Implementation for Markdown output + } +} + +impl DetectorResultReporter for JsonReporter { + fn report(&self, results: &[DetectorResult]) { + match serde_json::to_string_pretty(results) { + Ok(json) => println!("{}", json), + Err(e) => eprintln!("Error serializing to JSON: {}", e), + } + } +} + +impl DetectorResultReporter for StdoutReporter { + fn report(&self, results: &[DetectorResult]) { + if results.is_empty() { + println!("{}", "No issues detected.".green()); + return; + } + + println!("{}", "Detector Results:".bold().underline()); + println!(); + + for (index, result) in results.iter().enumerate() { + self.print_result(index + 1, result); + println!(); + } + + self.print_summary(results); + } +} + +impl StdoutReporter { + fn print_result(&self, index: usize, result: &DetectorResult) { + println!( + "{}. {} ({})", + index, + result.issue_name.bold(), + result.detector_name.italic() + ); + println!( + " {}: {}", + "Severity".yellow(), + self.format_severity(result.severity) + ); + println!( + " {}: {}", + "Confidence".yellow(), + self.format_confidence(result.confidence) + ); + println!( + " {}: {}:{}-{}", + "Location".yellow(), + result.location.file_no(), + result.location.start(), + result.location.end() + ); + println!(" {}: {}", "Description".yellow(), result.message); + } + + fn format_severity(&self, severity: Severity) -> ColoredString { + match severity { + Severity::Informational => "Informational".blue(), + Severity::Low => "Low".green(), + Severity::Medium => "Medium".yellow(), + Severity::High => "High".red(), + } + } + + fn format_confidence(&self, confidence: Confidence) -> ColoredString { + match confidence { + Confidence::Low => "Low".yellow(), + Confidence::Medium => "Medium".yellow(), + Confidence::High => "High".green(), + Confidence::Certain => "Certain".green().bold(), + } + } + + fn print_summary(&self, results: &[DetectorResult]) { + let total = results.len(); + let (high, medium, low, info) = + results + .iter() + .fold((0, 0, 0, 0), |acc, r| match r.severity { + // summing up the counts of each severity + Severity::High => (acc.0 + 1, acc.1, acc.2, acc.3), + Severity::Medium => (acc.0, acc.1 + 1, acc.2, acc.3), + Severity::Low => (acc.0, acc.1, acc.2 + 1, acc.3), + Severity::Informational => (acc.0, acc.1, acc.2, acc.3 + 1), + }); + + println!("{}", "Summary:".bold().underline()); + println!("Total issues: {}", total); + println!(" High: {}", high.to_string().red()); + println!(" Medium: {}", medium.to_string().yellow()); + println!(" Low: {}", low.to_string().green()); + println!(" Info: {}", info.to_string().blue()); + } +}