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, DEFAULT_VENDOR_IGNORE_PATHS,
13 EffectiveConfig, Fingerprint, GosecProviderConfig, InlineSuppression, OsvEcosystem,
14 OsvProviderConfig, OxcProviderConfig, OxlintProviderConfig, PmdProviderConfig, Profile,
15 ProfileThresholds, ProfilesConfig, ProviderType, ProvidersConfig, RULES_ALWAYS_ENABLED,
16 RmaTomlConfig, RulesConfig, RulesetsConfig, ScanConfig, SuppressionConfig, SuppressionEngine,
17 SuppressionResult, 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,
53 C,
54 Cpp,
55 Zig,
56
57 Java,
59 Kotlin,
60 Scala,
61
62 JavaScript,
64 TypeScript,
65 Html,
66 Css,
67 Scss,
68 Vue,
69 Svelte,
70
71 Python,
73 Ruby,
74 Php,
75 Lua,
76 Perl,
77
78 Haskell,
80 OCaml,
81 Elixir,
82 Erlang,
83
84 Go,
86 Swift,
87 CSharp,
88 Dart,
89
90 Json,
92 Yaml,
93 Toml,
94 Sql,
95 GraphQL,
96
97 Bash,
99 Dockerfile,
100 Hcl, Nix,
102
103 Markdown,
105 Latex,
106
107 Solidity, Wasm, Protobuf,
111
112 Unknown,
113}
114
115impl Language {
116 #[inline]
118 pub fn from_extension(ext: &str) -> Self {
119 match ext.to_lowercase().as_str() {
120 "rs" => Language::Rust,
122 "c" | "h" => Language::C,
123 "cc" | "cpp" | "cxx" | "hpp" | "hxx" | "hh" => Language::Cpp,
124 "zig" => Language::Zig,
125
126 "java" => Language::Java,
128 "kt" | "kts" => Language::Kotlin,
129 "scala" | "sc" => Language::Scala,
130
131 "js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
133 "ts" | "tsx" | "mts" | "cts" => Language::TypeScript,
134 "html" | "htm" => Language::Html,
135 "css" => Language::Css,
136 "scss" | "sass" => Language::Scss,
137 "vue" => Language::Vue,
138 "svelte" => Language::Svelte,
139
140 "py" | "pyi" | "pyw" => Language::Python,
142 "rb" | "erb" | "rake" | "gemspec" => Language::Ruby,
143 "php" | "phtml" | "php3" | "php4" | "php5" | "phps" => Language::Php,
144 "lua" => Language::Lua,
145 "pl" | "pm" | "t" => Language::Perl,
146
147 "hs" | "lhs" => Language::Haskell,
149 "ml" | "mli" => Language::OCaml,
150 "ex" | "exs" => Language::Elixir,
151 "erl" | "hrl" => Language::Erlang,
152
153 "go" => Language::Go,
155 "swift" => Language::Swift,
156 "cs" | "csx" => Language::CSharp,
157 "dart" => Language::Dart,
158
159 "json" | "jsonc" | "json5" => Language::Json,
161 "yaml" | "yml" => Language::Yaml,
162 "toml" => Language::Toml,
163 "sql" | "mysql" | "pgsql" | "plsql" => Language::Sql,
164 "graphql" | "gql" => Language::GraphQL,
165
166 "sh" | "bash" | "zsh" | "fish" => Language::Bash,
168 "dockerfile" => Language::Dockerfile,
169 "tf" | "tfvars" | "hcl" => Language::Hcl,
170 "nix" => Language::Nix,
171
172 "md" | "markdown" | "mdx" => Language::Markdown,
174 "tex" | "latex" | "sty" | "cls" => Language::Latex,
175
176 "sol" => Language::Solidity,
178 "wat" | "wast" => Language::Wasm,
179 "proto" | "proto3" => Language::Protobuf,
180
181 _ => Language::Unknown,
182 }
183 }
184
185 #[inline]
187 pub fn extensions(&self) -> &'static [&'static str] {
188 match self {
189 Language::Rust => &["rs"],
190 Language::C => &["c", "h"],
191 Language::Cpp => &["cc", "cpp", "cxx", "hpp", "hxx", "hh"],
192 Language::Zig => &["zig"],
193 Language::Java => &["java"],
194 Language::Kotlin => &["kt", "kts"],
195 Language::Scala => &["scala", "sc"],
196 Language::JavaScript => &["js", "mjs", "cjs", "jsx"],
197 Language::TypeScript => &["ts", "tsx", "mts", "cts"],
198 Language::Html => &["html", "htm"],
199 Language::Css => &["css"],
200 Language::Scss => &["scss", "sass"],
201 Language::Vue => &["vue"],
202 Language::Svelte => &["svelte"],
203 Language::Python => &["py", "pyi", "pyw"],
204 Language::Ruby => &["rb", "erb", "rake", "gemspec"],
205 Language::Php => &["php", "phtml"],
206 Language::Lua => &["lua"],
207 Language::Perl => &["pl", "pm", "t"],
208 Language::Haskell => &["hs", "lhs"],
209 Language::OCaml => &["ml", "mli"],
210 Language::Elixir => &["ex", "exs"],
211 Language::Erlang => &["erl", "hrl"],
212 Language::Go => &["go"],
213 Language::Swift => &["swift"],
214 Language::CSharp => &["cs", "csx"],
215 Language::Dart => &["dart"],
216 Language::Json => &["json", "jsonc", "json5"],
217 Language::Yaml => &["yaml", "yml"],
218 Language::Toml => &["toml"],
219 Language::Sql => &["sql", "mysql", "pgsql"],
220 Language::GraphQL => &["graphql", "gql"],
221 Language::Bash => &["sh", "bash", "zsh", "fish"],
222 Language::Dockerfile => &["dockerfile"],
223 Language::Hcl => &["tf", "tfvars", "hcl"],
224 Language::Nix => &["nix"],
225 Language::Markdown => &["md", "markdown", "mdx"],
226 Language::Latex => &["tex", "latex", "sty", "cls"],
227 Language::Solidity => &["sol"],
228 Language::Wasm => &["wat", "wast"],
229 Language::Protobuf => &["proto", "proto3"],
230 Language::Unknown => &[],
231 }
232 }
233
234 #[inline]
236 pub fn is_systems_language(&self) -> bool {
237 matches!(
238 self,
239 Language::Rust | Language::C | Language::Cpp | Language::Zig
240 )
241 }
242
243 #[inline]
245 pub fn is_scripting_language(&self) -> bool {
246 matches!(
247 self,
248 Language::JavaScript
249 | Language::TypeScript
250 | Language::Python
251 | Language::Ruby
252 | Language::Php
253 | Language::Lua
254 | Language::Perl
255 )
256 }
257
258 #[inline]
260 pub fn is_jvm_language(&self) -> bool {
261 matches!(self, Language::Java | Language::Kotlin | Language::Scala)
262 }
263
264 #[inline]
266 pub fn is_functional_language(&self) -> bool {
267 matches!(
268 self,
269 Language::Haskell | Language::OCaml | Language::Elixir | Language::Erlang
270 )
271 }
272
273 #[inline]
275 pub fn is_data_language(&self) -> bool {
276 matches!(
277 self,
278 Language::Json | Language::Yaml | Language::Toml | Language::Sql | Language::GraphQL
279 )
280 }
281
282 #[inline]
284 pub fn supports_security_scanning(&self) -> bool {
285 !matches!(
286 self,
287 Language::Unknown | Language::Markdown | Language::Latex | Language::Wasm
288 )
289 }
290}
291
292impl std::fmt::Display for Language {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 match self {
295 Language::Rust => write!(f, "rust"),
296 Language::C => write!(f, "c"),
297 Language::Cpp => write!(f, "cpp"),
298 Language::Zig => write!(f, "zig"),
299 Language::Java => write!(f, "java"),
300 Language::Kotlin => write!(f, "kotlin"),
301 Language::Scala => write!(f, "scala"),
302 Language::JavaScript => write!(f, "javascript"),
303 Language::TypeScript => write!(f, "typescript"),
304 Language::Html => write!(f, "html"),
305 Language::Css => write!(f, "css"),
306 Language::Scss => write!(f, "scss"),
307 Language::Vue => write!(f, "vue"),
308 Language::Svelte => write!(f, "svelte"),
309 Language::Python => write!(f, "python"),
310 Language::Ruby => write!(f, "ruby"),
311 Language::Php => write!(f, "php"),
312 Language::Lua => write!(f, "lua"),
313 Language::Perl => write!(f, "perl"),
314 Language::Haskell => write!(f, "haskell"),
315 Language::OCaml => write!(f, "ocaml"),
316 Language::Elixir => write!(f, "elixir"),
317 Language::Erlang => write!(f, "erlang"),
318 Language::Go => write!(f, "go"),
319 Language::Swift => write!(f, "swift"),
320 Language::CSharp => write!(f, "csharp"),
321 Language::Dart => write!(f, "dart"),
322 Language::Json => write!(f, "json"),
323 Language::Yaml => write!(f, "yaml"),
324 Language::Toml => write!(f, "toml"),
325 Language::Sql => write!(f, "sql"),
326 Language::GraphQL => write!(f, "graphql"),
327 Language::Bash => write!(f, "bash"),
328 Language::Dockerfile => write!(f, "dockerfile"),
329 Language::Hcl => write!(f, "hcl"),
330 Language::Nix => write!(f, "nix"),
331 Language::Markdown => write!(f, "markdown"),
332 Language::Latex => write!(f, "latex"),
333 Language::Solidity => write!(f, "solidity"),
334 Language::Wasm => write!(f, "wasm"),
335 Language::Protobuf => write!(f, "protobuf"),
336 Language::Unknown => write!(f, "unknown"),
337 }
338 }
339}
340
341#[derive(
343 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
344)]
345#[serde(rename_all = "lowercase")]
346pub enum Severity {
347 Info,
348 #[default]
349 Warning,
350 Error,
351 Critical,
352}
353
354impl std::fmt::Display for Severity {
355 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
356 match self {
357 Severity::Info => write!(f, "info"),
358 Severity::Warning => write!(f, "warning"),
359 Severity::Error => write!(f, "error"),
360 Severity::Critical => write!(f, "critical"),
361 }
362 }
363}
364
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
367#[serde(rename_all = "lowercase")]
368pub enum Confidence {
369 Low,
371 #[default]
373 Medium,
374 High,
376}
377
378impl std::fmt::Display for Confidence {
379 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 match self {
381 Confidence::Low => write!(f, "low"),
382 Confidence::Medium => write!(f, "medium"),
383 Confidence::High => write!(f, "high"),
384 }
385 }
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
390#[serde(rename_all = "lowercase")]
391pub enum FindingCategory {
392 #[default]
394 Security,
395 Quality,
397 Performance,
399 Style,
401}
402
403impl std::fmt::Display for FindingCategory {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 match self {
406 FindingCategory::Security => write!(f, "security"),
407 FindingCategory::Quality => write!(f, "quality"),
408 FindingCategory::Performance => write!(f, "performance"),
409 FindingCategory::Style => write!(f, "style"),
410 }
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
416pub struct SourceLocation {
417 pub file: PathBuf,
418 pub start_line: usize,
419 pub start_column: usize,
420 pub end_line: usize,
421 pub end_column: usize,
422}
423
424impl SourceLocation {
425 pub fn new(
426 file: PathBuf,
427 start_line: usize,
428 start_column: usize,
429 end_line: usize,
430 end_column: usize,
431 ) -> Self {
432 Self {
433 file,
434 start_line,
435 start_column,
436 end_line,
437 end_column,
438 }
439 }
440}
441
442impl std::fmt::Display for SourceLocation {
443 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
444 write!(
445 f,
446 "{}:{}:{}-{}:{}",
447 self.file.display(),
448 self.start_line,
449 self.start_column,
450 self.end_line,
451 self.end_column
452 )
453 }
454}
455
456#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
458pub struct Fix {
459 pub description: String,
461 pub replacement: String,
463 pub start_byte: usize,
465 pub end_byte: usize,
467}
468
469impl Fix {
470 pub fn new(
472 description: impl Into<String>,
473 replacement: impl Into<String>,
474 start_byte: usize,
475 end_byte: usize,
476 ) -> Self {
477 Self {
478 description: description.into(),
479 replacement: replacement.into(),
480 start_byte,
481 end_byte,
482 }
483 }
484}
485
486#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
488#[serde(rename_all = "lowercase")]
489pub enum FindingSource {
490 #[default]
492 Builtin,
493 Codeql,
495 Pysa,
497 Osv,
499 Rustsec,
501 Oxc,
503 Oxlint,
505 Pmd,
507 Gosec,
509 #[serde(rename = "taint-flow")]
511 TaintFlow,
512 Plugin,
514 Ai,
516}
517
518impl std::fmt::Display for FindingSource {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 match self {
521 FindingSource::Builtin => write!(f, "builtin"),
522 FindingSource::Codeql => write!(f, "codeql"),
523 FindingSource::Pysa => write!(f, "pysa"),
524 FindingSource::Osv => write!(f, "osv"),
525 FindingSource::Rustsec => write!(f, "rustsec"),
526 FindingSource::Oxc => write!(f, "oxc"),
527 FindingSource::Oxlint => write!(f, "oxlint"),
528 FindingSource::Pmd => write!(f, "pmd"),
529 FindingSource::Gosec => write!(f, "gosec"),
530 FindingSource::TaintFlow => write!(f, "taint-flow"),
531 FindingSource::Plugin => write!(f, "plugin"),
532 FindingSource::Ai => write!(f, "ai"),
533 }
534 }
535}
536
537#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct Finding {
540 pub id: String,
541 pub rule_id: String,
542 pub message: String,
543 pub severity: Severity,
544 pub location: SourceLocation,
545 pub language: Language,
546 #[serde(skip_serializing_if = "Option::is_none")]
547 pub snippet: Option<String>,
548 #[serde(skip_serializing_if = "Option::is_none")]
549 pub suggestion: Option<String>,
550 #[serde(skip_serializing_if = "Option::is_none")]
552 pub fix: Option<Fix>,
553 #[serde(default)]
555 pub confidence: Confidence,
556 #[serde(default)]
558 pub category: FindingCategory,
559 #[serde(default)]
561 pub source: FindingSource,
562 #[serde(skip_serializing_if = "Option::is_none")]
564 pub fingerprint: Option<String>,
565 #[serde(skip_serializing_if = "Option::is_none", default)]
567 pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
568 #[serde(skip_serializing_if = "Option::is_none", default)]
571 pub occurrence_count: Option<usize>,
572 #[serde(skip_serializing_if = "Option::is_none", default)]
574 pub additional_locations: Option<Vec<usize>>,
575}
576
577impl Finding {
578 pub fn compute_fingerprint(&mut self) {
581 use sha2::{Digest, Sha256};
582
583 let mut hasher = Sha256::new();
584 hasher.update(self.rule_id.as_bytes());
585 hasher.update(self.location.file.to_string_lossy().as_bytes());
586
587 if let Some(snippet) = &self.snippet {
589 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
590 hasher.update(normalized.as_bytes());
591 }
592
593 let hash = hasher.finalize();
594 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
595 }
596}
597
598pub fn deduplicate_findings(findings: Vec<Finding>) -> Vec<Finding> {
611 use std::collections::HashMap;
612
613 let mut grouped: HashMap<(String, String), Vec<Finding>> = HashMap::new();
615
616 for finding in findings {
617 let key = (
618 finding.location.file.to_string_lossy().to_string(),
619 finding.rule_id.clone(),
620 );
621 grouped.entry(key).or_default().push(finding);
622 }
623
624 let mut result = Vec::new();
626 for ((_file, _rule_id), mut group) in grouped {
627 if group.len() == 1 {
628 result.push(group.remove(0));
630 } else {
631 let count = group.len();
633
634 group.sort_by_key(|f| f.location.start_line);
636
637 let mut representative = group.remove(0);
639
640 let additional_lines: Vec<usize> =
642 group.iter().map(|f| f.location.start_line).collect();
643
644 representative.occurrence_count = Some(count);
645 representative.additional_locations = Some(additional_lines);
646
647 representative.message = format!(
649 "{} ({} occurrences in this file)",
650 representative.message, count
651 );
652
653 result.push(representative);
654 }
655 }
656
657 result.sort_by(|a, b| {
659 let file_cmp = a.location.file.cmp(&b.location.file);
660 if file_cmp == std::cmp::Ordering::Equal {
661 a.location.start_line.cmp(&b.location.start_line)
662 } else {
663 file_cmp
664 }
665 });
666
667 result
668}
669
670#[derive(Debug, Clone, Default, Serialize, Deserialize)]
672pub struct CodeMetrics {
673 pub lines_of_code: usize,
674 pub lines_of_comments: usize,
675 pub blank_lines: usize,
676 pub cyclomatic_complexity: usize,
677 pub cognitive_complexity: usize,
678 pub function_count: usize,
679 pub class_count: usize,
680 pub import_count: usize,
681}
682
683#[derive(Debug, Clone, Default, Serialize, Deserialize)]
685pub struct ScanSummary {
686 pub files_scanned: usize,
687 pub files_skipped: usize,
688 pub total_lines: usize,
689 pub findings_by_severity: std::collections::HashMap<String, usize>,
690 pub languages: std::collections::HashMap<String, usize>,
691 pub duration_ms: u64,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize)]
696pub struct RmaConfig {
697 #[serde(default)]
699 pub exclude_patterns: Vec<String>,
700
701 #[serde(default)]
703 pub languages: Vec<Language>,
704
705 #[serde(default = "default_min_severity")]
707 pub min_severity: Severity,
708
709 #[serde(default = "default_max_file_size")]
711 pub max_file_size: usize,
712
713 #[serde(default)]
715 pub parallelism: usize,
716
717 #[serde(default)]
719 pub incremental: bool,
720}
721
722fn default_min_severity() -> Severity {
723 Severity::Warning
724}
725
726fn default_max_file_size() -> usize {
727 10 * 1024 * 1024 }
729
730impl Default for RmaConfig {
731 fn default() -> Self {
732 Self {
733 exclude_patterns: vec![
734 "**/node_modules/**".into(),
735 "**/target/**".into(),
736 "**/vendor/**".into(),
737 "**/.git/**".into(),
738 "**/dist/**".into(),
739 "**/build/**".into(),
740 ],
741 languages: vec![],
742 min_severity: default_min_severity(),
743 max_file_size: default_max_file_size(),
744 parallelism: 0,
745 incremental: false,
746 }
747 }
748}
749
750#[cfg(test)]
751mod tests {
752 use super::*;
753
754 #[test]
755 fn test_language_from_extension() {
756 assert_eq!(Language::from_extension("rs"), Language::Rust);
757 assert_eq!(Language::from_extension("js"), Language::JavaScript);
758 assert_eq!(Language::from_extension("py"), Language::Python);
759 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
760 }
761
762 #[test]
763 fn test_severity_ordering() {
764 assert!(Severity::Info < Severity::Warning);
765 assert!(Severity::Warning < Severity::Error);
766 assert!(Severity::Error < Severity::Critical);
767 }
768
769 #[test]
770 fn test_source_location_display() {
771 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
772 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
773 }
774}