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