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