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, Serialize, Deserialize)]
219pub struct Finding {
220 pub id: String,
221 pub rule_id: String,
222 pub message: String,
223 pub severity: Severity,
224 pub location: SourceLocation,
225 pub language: Language,
226 #[serde(skip_serializing_if = "Option::is_none")]
227 pub snippet: Option<String>,
228 #[serde(skip_serializing_if = "Option::is_none")]
229 pub suggestion: Option<String>,
230 #[serde(default)]
232 pub confidence: Confidence,
233 #[serde(default)]
235 pub category: FindingCategory,
236 #[serde(skip_serializing_if = "Option::is_none")]
238 pub fingerprint: Option<String>,
239 #[serde(skip_serializing_if = "Option::is_none", default)]
241 pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
242}
243
244impl Finding {
245 pub fn compute_fingerprint(&mut self) {
248 use sha2::{Digest, Sha256};
249
250 let mut hasher = Sha256::new();
251 hasher.update(self.rule_id.as_bytes());
252 hasher.update(self.location.file.to_string_lossy().as_bytes());
253
254 if let Some(snippet) = &self.snippet {
256 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
257 hasher.update(normalized.as_bytes());
258 }
259
260 let hash = hasher.finalize();
261 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
262 }
263}
264
265#[derive(Debug, Clone, Default, Serialize, Deserialize)]
267pub struct CodeMetrics {
268 pub lines_of_code: usize,
269 pub lines_of_comments: usize,
270 pub blank_lines: usize,
271 pub cyclomatic_complexity: usize,
272 pub cognitive_complexity: usize,
273 pub function_count: usize,
274 pub class_count: usize,
275 pub import_count: usize,
276}
277
278#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280pub struct ScanSummary {
281 pub files_scanned: usize,
282 pub files_skipped: usize,
283 pub total_lines: usize,
284 pub findings_by_severity: std::collections::HashMap<String, usize>,
285 pub languages: std::collections::HashMap<String, usize>,
286 pub duration_ms: u64,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct RmaConfig {
292 #[serde(default)]
294 pub exclude_patterns: Vec<String>,
295
296 #[serde(default)]
298 pub languages: Vec<Language>,
299
300 #[serde(default = "default_min_severity")]
302 pub min_severity: Severity,
303
304 #[serde(default = "default_max_file_size")]
306 pub max_file_size: usize,
307
308 #[serde(default)]
310 pub parallelism: usize,
311
312 #[serde(default)]
314 pub incremental: bool,
315}
316
317fn default_min_severity() -> Severity {
318 Severity::Warning
319}
320
321fn default_max_file_size() -> usize {
322 10 * 1024 * 1024 }
324
325impl Default for RmaConfig {
326 fn default() -> Self {
327 Self {
328 exclude_patterns: vec![
329 "**/node_modules/**".into(),
330 "**/target/**".into(),
331 "**/vendor/**".into(),
332 "**/.git/**".into(),
333 "**/dist/**".into(),
334 "**/build/**".into(),
335 ],
336 languages: vec![],
337 min_severity: default_min_severity(),
338 max_file_size: default_max_file_size(),
339 parallelism: 0,
340 incremental: false,
341 }
342 }
343}
344
345#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_language_from_extension() {
351 assert_eq!(Language::from_extension("rs"), Language::Rust);
352 assert_eq!(Language::from_extension("js"), Language::JavaScript);
353 assert_eq!(Language::from_extension("py"), Language::Python);
354 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
355 }
356
357 #[test]
358 fn test_severity_ordering() {
359 assert!(Severity::Info < Severity::Warning);
360 assert!(Severity::Warning < Severity::Error);
361 assert!(Severity::Error < Severity::Critical);
362 }
363
364 #[test]
365 fn test_source_location_display() {
366 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
367 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
368 }
369}