1pub 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#[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#[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
43pub struct AnalyzerEngine {
45 config: Arc<RmaConfig>,
46 rules: Vec<Box<dyn rules::Rule + Send + Sync>>,
47}
48
49impl AnalyzerEngine {
50 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 fn register_default_rules(&mut self) {
62 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 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 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 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 #[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 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 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 #[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
163fn 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 assert!(
217 analysis
218 .findings
219 .iter()
220 .any(|f| f.rule_id.contains("unsafe"))
221 );
222 }
223}