Skip to main content

rma_analyzer/
lib.rs

1//! Code analysis and security scanning for Rust Monorepo Analyzer
2//!
3//! This crate provides metrics computation, vulnerability detection,
4//! and pattern-based analysis on parsed ASTs.
5//!
6//! NOTE: This crate DETECTS security vulnerabilities - it does not contain them.
7//! The security rules detect dangerous patterns like unsafe code, code injection, etc.
8
9pub mod metrics;
10pub mod rules;
11pub mod security;
12
13use anyhow::Result;
14use rayon::prelude::*;
15use rma_common::{CodeMetrics, Finding, Language, RmaConfig, Severity};
16use rma_parser::ParsedFile;
17use serde::{Deserialize, Serialize};
18use std::sync::Arc;
19use tracing::{debug, info, instrument};
20
21/// Results from analyzing a single file
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct FileAnalysis {
24    pub path: String,
25    pub language: Language,
26    pub metrics: CodeMetrics,
27    pub findings: Vec<Finding>,
28}
29
30/// Summary of analysis results
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32pub struct AnalysisSummary {
33    pub files_analyzed: usize,
34    pub total_findings: usize,
35    pub critical_count: usize,
36    pub error_count: usize,
37    pub warning_count: usize,
38    pub info_count: usize,
39    pub total_complexity: usize,
40    pub total_loc: usize,
41}
42
43/// The main analysis engine
44pub struct AnalyzerEngine {
45    config: Arc<RmaConfig>,
46    rules: Vec<Box<dyn rules::Rule + Send + Sync>>,
47}
48
49impl AnalyzerEngine {
50    /// Create a new analyzer with default rules
51    pub fn new(config: RmaConfig) -> Self {
52        let mut engine = Self {
53            config: Arc::new(config),
54            rules: Vec::new(),
55        };
56        engine.register_default_rules();
57        engine
58    }
59
60    /// Register all default security and quality rules
61    fn register_default_rules(&mut self) {
62        // Rust rules - DETECT dangerous patterns
63        self.rules.push(Box::new(security::rust::UnsafeBlockRule));
64        self.rules.push(Box::new(security::rust::UnwrapRule));
65        self.rules.push(Box::new(security::rust::PanicRule));
66        self.rules.push(Box::new(security::rust::TransmuteRule));
67        self.rules
68            .push(Box::new(security::rust::RawPointerDerefRule));
69        self.rules
70            .push(Box::new(security::rust::CommandInjectionRule));
71        self.rules.push(Box::new(security::rust::SqlInjectionRule));
72        self.rules
73            .push(Box::new(security::rust::UncheckedIndexRule));
74        self.rules.push(Box::new(security::rust::PathTraversalRule));
75
76        // JavaScript rules - DETECT dangerous patterns
77        self.rules
78            .push(Box::new(security::javascript::DynamicCodeExecutionRule));
79        self.rules
80            .push(Box::new(security::javascript::TimerStringRule));
81        self.rules
82            .push(Box::new(security::javascript::InnerHtmlRule));
83        self.rules
84            .push(Box::new(security::javascript::ConsoleLogRule));
85
86        // Python rules - DETECT dangerous patterns
87        self.rules
88            .push(Box::new(security::python::DynamicExecutionRule));
89        self.rules
90            .push(Box::new(security::python::ShellInjectionRule));
91        self.rules
92            .push(Box::new(security::python::HardcodedSecretRule));
93
94        // Generic rules (apply to all languages)
95        self.rules.push(Box::new(security::generic::TodoFixmeRule));
96        self.rules
97            .push(Box::new(security::generic::LongFunctionRule::new(100)));
98        self.rules
99            .push(Box::new(security::generic::HighComplexityRule::new(15)));
100        self.rules
101            .push(Box::new(security::generic::HardcodedSecretRule));
102        self.rules
103            .push(Box::new(security::generic::InsecureCryptoRule));
104    }
105
106    /// Analyze a single parsed file
107    #[instrument(skip(self, parsed), fields(path = %parsed.path.display()))]
108    pub fn analyze_file(&self, parsed: &ParsedFile) -> Result<FileAnalysis> {
109        let metrics = metrics::compute_metrics(parsed);
110
111        let mut findings = Vec::new();
112
113        // Run all applicable rules
114        for rule in &self.rules {
115            if rule.applies_to(parsed.language) {
116                let rule_findings = rule.check(parsed);
117                findings.extend(rule_findings);
118            }
119        }
120
121        // Filter by minimum severity
122        findings.retain(|f| f.severity >= self.config.min_severity);
123
124        debug!(
125            "Analyzed {} - {} findings, complexity {}",
126            parsed.path.display(),
127            findings.len(),
128            metrics.cyclomatic_complexity
129        );
130
131        Ok(FileAnalysis {
132            path: parsed.path.to_string_lossy().to_string(),
133            language: parsed.language,
134            metrics,
135            findings,
136        })
137    }
138
139    /// Analyze multiple parsed files in parallel
140    #[instrument(skip(self, files))]
141    pub fn analyze_files(
142        &self,
143        files: &[ParsedFile],
144    ) -> Result<(Vec<FileAnalysis>, AnalysisSummary)> {
145        info!("Starting parallel analysis of {} files", files.len());
146
147        let results: Vec<FileAnalysis> = files
148            .par_iter()
149            .filter_map(|parsed| self.analyze_file(parsed).ok())
150            .collect();
151
152        let summary = compute_summary(&results);
153
154        info!(
155            "Analysis complete: {} files, {} findings ({} critical)",
156            summary.files_analyzed, summary.total_findings, summary.critical_count
157        );
158
159        Ok((results, summary))
160    }
161}
162
163/// Compute aggregate summary from analysis results
164fn compute_summary(results: &[FileAnalysis]) -> AnalysisSummary {
165    let mut summary = AnalysisSummary {
166        files_analyzed: results.len(),
167        ..Default::default()
168    };
169
170    for result in results {
171        summary.total_loc += result.metrics.lines_of_code;
172        summary.total_complexity += result.metrics.cyclomatic_complexity;
173
174        for finding in &result.findings {
175            summary.total_findings += 1;
176            match finding.severity {
177                Severity::Critical => summary.critical_count += 1,
178                Severity::Error => summary.error_count += 1,
179                Severity::Warning => summary.warning_count += 1,
180                Severity::Info => summary.info_count += 1,
181            }
182        }
183    }
184
185    summary
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use rma_parser::ParserEngine;
192    use std::path::Path;
193
194    #[test]
195    fn test_analyze_rust_file_with_unsafe() {
196        let config = RmaConfig::default();
197        let parser = ParserEngine::new(config.clone());
198        let analyzer = AnalyzerEngine::new(config);
199
200        let content = r#"
201fn safe_function() {
202    println!("Safe!");
203}
204
205fn risky_function() {
206    unsafe {
207        std::ptr::null::<i32>();
208    }
209}
210"#;
211
212        let parsed = parser.parse_file(Path::new("test.rs"), content).unwrap();
213        let analysis = analyzer.analyze_file(&parsed).unwrap();
214
215        // Should detect the unsafe block
216        assert!(
217            analysis
218                .findings
219                .iter()
220                .any(|f| f.rule_id.contains("unsafe"))
221        );
222    }
223}