1pub mod config;
7
8pub use config::{
9 AllowConfig, AllowType, Baseline, BaselineConfig, BaselineEntry, BaselineMode,
10 CURRENT_CONFIG_VERSION, ConfigLoadResult, ConfigSource, ConfigWarning, EffectiveConfig,
11 Fingerprint, InlineSuppression, Profile, ProfileThresholds, ProfilesConfig, RmaTomlConfig,
12 RulesConfig, RulesetsConfig, ScanConfig, SuppressionType, ThresholdOverride, WarningLevel,
13 parse_inline_suppressions,
14};
15
16use serde::{Deserialize, Serialize};
17use std::path::PathBuf;
18use thiserror::Error;
19
20#[derive(Error, Debug)]
22pub enum RmaError {
23 #[error("IO error: {0}")]
24 Io(#[from] std::io::Error),
25
26 #[error("Parse error in {file}: {message}")]
27 Parse { file: PathBuf, message: String },
28
29 #[error("Analysis error: {0}")]
30 Analysis(String),
31
32 #[error("Index error: {0}")]
33 Index(String),
34
35 #[error("Unsupported language: {0}")]
36 UnsupportedLanguage(String),
37
38 #[error("Configuration error: {0}")]
39 Config(String),
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum Language {
46 Rust,
47 JavaScript,
48 TypeScript,
49 Python,
50 Go,
51 Java,
52 Unknown,
53}
54
55impl Language {
56 pub fn from_extension(ext: &str) -> Self {
58 match ext.to_lowercase().as_str() {
59 "rs" => Language::Rust,
60 "js" | "mjs" | "cjs" => Language::JavaScript,
61 "ts" | "tsx" => Language::TypeScript,
62 "py" | "pyi" => Language::Python,
63 "go" => Language::Go,
64 "java" => Language::Java,
65 _ => Language::Unknown,
66 }
67 }
68
69 pub fn extensions(&self) -> &'static [&'static str] {
71 match self {
72 Language::Rust => &["rs"],
73 Language::JavaScript => &["js", "mjs", "cjs"],
74 Language::TypeScript => &["ts", "tsx"],
75 Language::Python => &["py", "pyi"],
76 Language::Go => &["go"],
77 Language::Java => &["java"],
78 Language::Unknown => &[],
79 }
80 }
81}
82
83impl std::fmt::Display for Language {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Language::Rust => write!(f, "rust"),
87 Language::JavaScript => write!(f, "javascript"),
88 Language::TypeScript => write!(f, "typescript"),
89 Language::Python => write!(f, "python"),
90 Language::Go => write!(f, "go"),
91 Language::Java => write!(f, "java"),
92 Language::Unknown => write!(f, "unknown"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
99#[serde(rename_all = "lowercase")]
100pub enum Severity {
101 Info,
102 Warning,
103 Error,
104 Critical,
105}
106
107impl std::fmt::Display for Severity {
108 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109 match self {
110 Severity::Info => write!(f, "info"),
111 Severity::Warning => write!(f, "warning"),
112 Severity::Error => write!(f, "error"),
113 Severity::Critical => write!(f, "critical"),
114 }
115 }
116}
117
118#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
120#[serde(rename_all = "lowercase")]
121pub enum Confidence {
122 Low,
124 #[default]
126 Medium,
127 High,
129}
130
131impl std::fmt::Display for Confidence {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 match self {
134 Confidence::Low => write!(f, "low"),
135 Confidence::Medium => write!(f, "medium"),
136 Confidence::High => write!(f, "high"),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
143#[serde(rename_all = "lowercase")]
144pub enum FindingCategory {
145 #[default]
147 Security,
148 Quality,
150 Performance,
152 Style,
154}
155
156impl std::fmt::Display for FindingCategory {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 match self {
159 FindingCategory::Security => write!(f, "security"),
160 FindingCategory::Quality => write!(f, "quality"),
161 FindingCategory::Performance => write!(f, "performance"),
162 FindingCategory::Style => write!(f, "style"),
163 }
164 }
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
169pub struct SourceLocation {
170 pub file: PathBuf,
171 pub start_line: usize,
172 pub start_column: usize,
173 pub end_line: usize,
174 pub end_column: usize,
175}
176
177impl SourceLocation {
178 pub fn new(
179 file: PathBuf,
180 start_line: usize,
181 start_column: usize,
182 end_line: usize,
183 end_column: usize,
184 ) -> Self {
185 Self {
186 file,
187 start_line,
188 start_column,
189 end_line,
190 end_column,
191 }
192 }
193}
194
195impl std::fmt::Display for SourceLocation {
196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197 write!(
198 f,
199 "{}:{}:{}-{}:{}",
200 self.file.display(),
201 self.start_line,
202 self.start_column,
203 self.end_line,
204 self.end_column
205 )
206 }
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Finding {
212 pub id: String,
213 pub rule_id: String,
214 pub message: String,
215 pub severity: Severity,
216 pub location: SourceLocation,
217 pub language: Language,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub snippet: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
221 pub suggestion: Option<String>,
222 #[serde(default)]
224 pub confidence: Confidence,
225 #[serde(default)]
227 pub category: FindingCategory,
228 #[serde(skip_serializing_if = "Option::is_none")]
230 pub fingerprint: Option<String>,
231}
232
233impl Finding {
234 pub fn compute_fingerprint(&mut self) {
237 use sha2::{Digest, Sha256};
238
239 let mut hasher = Sha256::new();
240 hasher.update(self.rule_id.as_bytes());
241 hasher.update(self.location.file.to_string_lossy().as_bytes());
242
243 if let Some(snippet) = &self.snippet {
245 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
246 hasher.update(normalized.as_bytes());
247 }
248
249 let hash = hasher.finalize();
250 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
251 }
252}
253
254#[derive(Debug, Clone, Default, Serialize, Deserialize)]
256pub struct CodeMetrics {
257 pub lines_of_code: usize,
258 pub lines_of_comments: usize,
259 pub blank_lines: usize,
260 pub cyclomatic_complexity: usize,
261 pub cognitive_complexity: usize,
262 pub function_count: usize,
263 pub class_count: usize,
264 pub import_count: usize,
265}
266
267#[derive(Debug, Clone, Default, Serialize, Deserialize)]
269pub struct ScanSummary {
270 pub files_scanned: usize,
271 pub files_skipped: usize,
272 pub total_lines: usize,
273 pub findings_by_severity: std::collections::HashMap<String, usize>,
274 pub languages: std::collections::HashMap<String, usize>,
275 pub duration_ms: u64,
276}
277
278#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct RmaConfig {
281 #[serde(default)]
283 pub exclude_patterns: Vec<String>,
284
285 #[serde(default)]
287 pub languages: Vec<Language>,
288
289 #[serde(default = "default_min_severity")]
291 pub min_severity: Severity,
292
293 #[serde(default = "default_max_file_size")]
295 pub max_file_size: usize,
296
297 #[serde(default)]
299 pub parallelism: usize,
300
301 #[serde(default)]
303 pub incremental: bool,
304}
305
306fn default_min_severity() -> Severity {
307 Severity::Warning
308}
309
310fn default_max_file_size() -> usize {
311 10 * 1024 * 1024 }
313
314impl Default for RmaConfig {
315 fn default() -> Self {
316 Self {
317 exclude_patterns: vec![
318 "**/node_modules/**".into(),
319 "**/target/**".into(),
320 "**/vendor/**".into(),
321 "**/.git/**".into(),
322 "**/dist/**".into(),
323 "**/build/**".into(),
324 ],
325 languages: vec![],
326 min_severity: default_min_severity(),
327 max_file_size: default_max_file_size(),
328 parallelism: 0,
329 incremental: false,
330 }
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_language_from_extension() {
340 assert_eq!(Language::from_extension("rs"), Language::Rust);
341 assert_eq!(Language::from_extension("js"), Language::JavaScript);
342 assert_eq!(Language::from_extension("py"), Language::Python);
343 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
344 }
345
346 #[test]
347 fn test_severity_ordering() {
348 assert!(Severity::Info < Severity::Warning);
349 assert!(Severity::Warning < Severity::Error);
350 assert!(Severity::Error < Severity::Critical);
351 }
352
353 #[test]
354 fn test_source_location_display() {
355 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
356 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
357 }
358}