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,
18 parse_expiration_days, 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(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
104#[serde(rename_all = "lowercase")]
105pub enum Severity {
106 Info,
107 #[default]
108 Warning,
109 Error,
110 Critical,
111}
112
113impl std::fmt::Display for Severity {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Severity::Info => write!(f, "info"),
117 Severity::Warning => write!(f, "warning"),
118 Severity::Error => write!(f, "error"),
119 Severity::Critical => write!(f, "critical"),
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
126#[serde(rename_all = "lowercase")]
127pub enum Confidence {
128 Low,
130 #[default]
132 Medium,
133 High,
135}
136
137impl std::fmt::Display for Confidence {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 match self {
140 Confidence::Low => write!(f, "low"),
141 Confidence::Medium => write!(f, "medium"),
142 Confidence::High => write!(f, "high"),
143 }
144 }
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
149#[serde(rename_all = "lowercase")]
150pub enum FindingCategory {
151 #[default]
153 Security,
154 Quality,
156 Performance,
158 Style,
160}
161
162impl std::fmt::Display for FindingCategory {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 match self {
165 FindingCategory::Security => write!(f, "security"),
166 FindingCategory::Quality => write!(f, "quality"),
167 FindingCategory::Performance => write!(f, "performance"),
168 FindingCategory::Style => write!(f, "style"),
169 }
170 }
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
175pub struct SourceLocation {
176 pub file: PathBuf,
177 pub start_line: usize,
178 pub start_column: usize,
179 pub end_line: usize,
180 pub end_column: usize,
181}
182
183impl SourceLocation {
184 pub fn new(
185 file: PathBuf,
186 start_line: usize,
187 start_column: usize,
188 end_line: usize,
189 end_column: usize,
190 ) -> Self {
191 Self {
192 file,
193 start_line,
194 start_column,
195 end_line,
196 end_column,
197 }
198 }
199}
200
201impl std::fmt::Display for SourceLocation {
202 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203 write!(
204 f,
205 "{}:{}:{}-{}:{}",
206 self.file.display(),
207 self.start_line,
208 self.start_column,
209 self.end_line,
210 self.end_column
211 )
212 }
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct Finding {
218 pub id: String,
219 pub rule_id: String,
220 pub message: String,
221 pub severity: Severity,
222 pub location: SourceLocation,
223 pub language: Language,
224 #[serde(skip_serializing_if = "Option::is_none")]
225 pub snippet: Option<String>,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub suggestion: Option<String>,
228 #[serde(default)]
230 pub confidence: Confidence,
231 #[serde(default)]
233 pub category: FindingCategory,
234 #[serde(skip_serializing_if = "Option::is_none")]
236 pub fingerprint: Option<String>,
237 #[serde(skip_serializing_if = "Option::is_none", default)]
239 pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
240}
241
242impl Finding {
243 pub fn compute_fingerprint(&mut self) {
246 use sha2::{Digest, Sha256};
247
248 let mut hasher = Sha256::new();
249 hasher.update(self.rule_id.as_bytes());
250 hasher.update(self.location.file.to_string_lossy().as_bytes());
251
252 if let Some(snippet) = &self.snippet {
254 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
255 hasher.update(normalized.as_bytes());
256 }
257
258 let hash = hasher.finalize();
259 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
260 }
261}
262
263#[derive(Debug, Clone, Default, Serialize, Deserialize)]
265pub struct CodeMetrics {
266 pub lines_of_code: usize,
267 pub lines_of_comments: usize,
268 pub blank_lines: usize,
269 pub cyclomatic_complexity: usize,
270 pub cognitive_complexity: usize,
271 pub function_count: usize,
272 pub class_count: usize,
273 pub import_count: usize,
274}
275
276#[derive(Debug, Clone, Default, Serialize, Deserialize)]
278pub struct ScanSummary {
279 pub files_scanned: usize,
280 pub files_skipped: usize,
281 pub total_lines: usize,
282 pub findings_by_severity: std::collections::HashMap<String, usize>,
283 pub languages: std::collections::HashMap<String, usize>,
284 pub duration_ms: u64,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct RmaConfig {
290 #[serde(default)]
292 pub exclude_patterns: Vec<String>,
293
294 #[serde(default)]
296 pub languages: Vec<Language>,
297
298 #[serde(default = "default_min_severity")]
300 pub min_severity: Severity,
301
302 #[serde(default = "default_max_file_size")]
304 pub max_file_size: usize,
305
306 #[serde(default)]
308 pub parallelism: usize,
309
310 #[serde(default)]
312 pub incremental: bool,
313}
314
315fn default_min_severity() -> Severity {
316 Severity::Warning
317}
318
319fn default_max_file_size() -> usize {
320 10 * 1024 * 1024 }
322
323impl Default for RmaConfig {
324 fn default() -> Self {
325 Self {
326 exclude_patterns: vec![
327 "**/node_modules/**".into(),
328 "**/target/**".into(),
329 "**/vendor/**".into(),
330 "**/.git/**".into(),
331 "**/dist/**".into(),
332 "**/build/**".into(),
333 ],
334 languages: vec![],
335 min_severity: default_min_severity(),
336 max_file_size: default_max_file_size(),
337 parallelism: 0,
338 incremental: false,
339 }
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_language_from_extension() {
349 assert_eq!(Language::from_extension("rs"), Language::Rust);
350 assert_eq!(Language::from_extension("js"), Language::JavaScript);
351 assert_eq!(Language::from_extension("py"), Language::Python);
352 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
353 }
354
355 #[test]
356 fn test_severity_ordering() {
357 assert!(Severity::Info < Severity::Warning);
358 assert!(Severity::Warning < Severity::Error);
359 assert!(Severity::Error < Severity::Critical);
360 }
361
362 #[test]
363 fn test_source_location_display() {
364 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
365 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
366 }
367}