diff --git a/packages/rules/src/auditability/docs/missing_natspec.rs b/packages/rules/src/auditability/docs/missing_natspec.rs new file mode 100644 index 0000000..384670b --- /dev/null +++ b/packages/rules/src/auditability/docs/missing_natspec.rs @@ -0,0 +1,72 @@ +//! Detect Missing NatSpec Documentation +//! +//! Flags public/external functions that lack NatSpec (`///` or `/** */`) comments. +//! Poor documentation reduces auditability and maintainability. + +use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity}; +use syn::{Item, Visibility}; + +pub struct MissingNatspecRule; + +impl Rule for MissingNatspecRule { + fn name(&self) -> &str { + "missing-natspec" + } + + fn description(&self) -> &str { + "Detects public functions missing NatSpec documentation comments." + } + + fn check(&self, ast: &[Item]) -> Vec { + let mut violations = Vec::new(); + for item in ast { + if let Item::Fn(func) = item { + let is_public = matches!(func.vis, Visibility::Public(_)); + let has_doc = func.attrs.iter().any(|a| a.path().is_ident("doc")); + if is_public && !has_doc { + violations.push(RuleViolation { + rule_name: self.name().to_string(), + description: format!( + "Public function `{}` is missing NatSpec documentation.", + func.sig.ident + ), + severity: ViolationSeverity::Low, + line_number: 0, + column_number: 0, + variable_name: func.sig.ident.to_string(), + suggestion: "Add `/// @notice`, `/// @param`, and `/// @return` \ + NatSpec comments above the function." + .to_string(), + }); + } + } + } + violations + } +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_file; + + fn check(code: &str) -> Vec { + let ast = parse_file(code).expect("parse failed"); + MissingNatspecRule.check(&ast.items) + } + + #[test] + fn flags_undocumented_public_fn() { + assert!(!check("pub fn transfer() {}").is_empty()); + } + + #[test] + fn no_violation_for_documented_fn() { + assert!(check("/// @notice transfers tokens\npub fn transfer() {}").is_empty()); + } + + #[test] + fn no_violation_for_private_fn() { + assert!(check("fn internal_helper() {}").is_empty()); + } +} \ No newline at end of file diff --git a/packages/rules/src/auditability/docs/mod.rs b/packages/rules/src/auditability/docs/mod.rs new file mode 100644 index 0000000..a4010a1 --- /dev/null +++ b/packages/rules/src/auditability/docs/mod.rs @@ -0,0 +1,2 @@ +pub mod missing_natspec; +pub use missing_natspec::MissingNatspecRule; \ No newline at end of file diff --git a/packages/rules/src/auditability/mod.rs b/packages/rules/src/auditability/mod.rs new file mode 100644 index 0000000..e68002c --- /dev/null +++ b/packages/rules/src/auditability/mod.rs @@ -0,0 +1,2 @@ +pub mod docs; +pub use docs::MissingNatspecRule; \ No newline at end of file diff --git a/packages/rules/src/security/external_calls/external_calls_in_loops.rs b/packages/rules/src/security/external_calls/external_calls_in_loops.rs new file mode 100644 index 0000000..fe143a3 --- /dev/null +++ b/packages/rules/src/security/external_calls/external_calls_in_loops.rs @@ -0,0 +1,78 @@ +//! Detect External Calls Inside Loops +//! +//! Flags external function calls that appear inside loop bodies. +//! This pattern increases gas costs and expands the reentrancy attack surface. + +use crate::rule_engine::{Rule, RuleViolation, ViolationSeverity}; +use syn::{Expr, Item, Stmt}; + +pub struct ExternalCallsInLoopsRule; + +impl Rule for ExternalCallsInLoopsRule { + fn name(&self) -> &str { + "external-calls-in-loops" + } + + fn description(&self) -> &str { + "Detects external calls inside loop bodies. \ + This increases gas risk and expands the reentrancy attack surface." + } + + fn check(&self, ast: &[Item]) -> Vec { + let mut violations = Vec::new(); + for item in ast { + if let Item::Fn(func) = item { + self.check_stmts(&func.block.stmts, &mut violations); + } + } + violations + } +} + +impl ExternalCallsInLoopsRule { + fn check_stmts(&self, stmts: &[Stmt], violations: &mut Vec) { + for stmt in stmts { + if let Stmt::Expr(Expr::ForLoop(loop_expr), _) = stmt { + self.check_loop_body(&loop_expr.body.stmts, violations); + } + if let Stmt::Expr(Expr::While(loop_expr), _) = stmt { + self.check_loop_body(&loop_expr.body.stmts, violations); + } + } + } + + fn check_loop_body(&self, stmts: &[Stmt], violations: &mut Vec) { + for stmt in stmts { + if let Stmt::Expr(Expr::MethodCall(call), _) = stmt { + let method = call.method.to_string(); + if matches!(method.as_str(), "transfer" | "call" | "send" | "invoke") { + violations.push(RuleViolation { + rule_name: self.name().to_string(), + description: format!( + "External call `{}` inside a loop increases gas and reentrancy risk.", + method + ), + severity: ViolationSeverity::High, + line_number: 0, + column_number: 0, + variable_name: method, + suggestion: "Collect results in the loop and perform external calls after." + .to_string(), + }); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_file; + + #[test] + fn no_violation_no_loop() { + let ast = parse_file("fn f() { x.transfer(1); }").expect("parse"); + assert!(ExternalCallsInLoopsRule.check(&ast.items).is_empty()); + } +} \ No newline at end of file diff --git a/packages/rules/src/security/external_calls/mod.rs b/packages/rules/src/security/external_calls/mod.rs new file mode 100644 index 0000000..d7585f0 --- /dev/null +++ b/packages/rules/src/security/external_calls/mod.rs @@ -0,0 +1,2 @@ +pub mod external_calls_in_loops; +pub use external_calls_in_loops::ExternalCallsInLoopsRule; \ No newline at end of file