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_GENERATED_IGNORE_PATHS, DEFAULT_TEST_IGNORE_PATHS,
13 DEFAULT_VENDOR_IGNORE_PATHS,
14 EffectiveConfig, Fingerprint, GosecProviderConfig, InlineSuppression, OsvEcosystem,
15 OsvProviderConfig, OxcProviderConfig, OxlintProviderConfig, PmdProviderConfig, Profile,
16 ProfileThresholds, ProfilesConfig, ProviderType, ProvidersConfig, RULES_ALWAYS_ENABLED,
17 RmaTomlConfig, RulesConfig, RulesetsConfig, ScanConfig, SuppressionConfig, SuppressionEngine,
18 SuppressionResult, SuppressionSource, SuppressionType, ThresholdOverride, WarningLevel,
19 parse_expiration_days, parse_inline_suppressions,
20};
21
22use serde::{Deserialize, Serialize};
23use std::path::PathBuf;
24use thiserror::Error;
25
26#[derive(Error, Debug)]
28pub enum RmaError {
29 #[error("IO error: {0}")]
30 Io(#[from] std::io::Error),
31
32 #[error("Parse error in {file}: {message}")]
33 Parse { file: PathBuf, message: String },
34
35 #[error("Analysis error: {0}")]
36 Analysis(String),
37
38 #[error("Index error: {0}")]
39 Index(String),
40
41 #[error("Unsupported language: {0}")]
42 UnsupportedLanguage(String),
43
44 #[error("Configuration error: {0}")]
45 Config(String),
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum Language {
52 Rust,
54 C,
55 Cpp,
56 Zig,
57
58 Java,
60 Kotlin,
61 Scala,
62
63 JavaScript,
65 TypeScript,
66 Html,
67 Css,
68 Scss,
69 Vue,
70 Svelte,
71
72 Python,
74 Ruby,
75 Php,
76 Lua,
77 Perl,
78
79 Haskell,
81 OCaml,
82 Elixir,
83 Erlang,
84
85 Go,
87 Swift,
88 CSharp,
89 Dart,
90
91 Json,
93 Yaml,
94 Toml,
95 Sql,
96 GraphQL,
97
98 Bash,
100 Dockerfile,
101 Hcl, Nix,
103
104 Markdown,
106 Latex,
107
108 Solidity, Wasm, Protobuf,
112
113 Unknown,
114}
115
116impl Language {
117 #[inline]
119 pub fn from_extension(ext: &str) -> Self {
120 match ext.to_lowercase().as_str() {
121 "rs" => Language::Rust,
123 "c" | "h" => Language::C,
124 "cc" | "cpp" | "cxx" | "hpp" | "hxx" | "hh" => Language::Cpp,
125 "zig" => Language::Zig,
126
127 "java" => Language::Java,
129 "kt" | "kts" => Language::Kotlin,
130 "scala" | "sc" => Language::Scala,
131
132 "js" | "mjs" | "cjs" | "jsx" => Language::JavaScript,
134 "ts" | "tsx" | "mts" | "cts" => Language::TypeScript,
135 "html" | "htm" => Language::Html,
136 "css" => Language::Css,
137 "scss" | "sass" => Language::Scss,
138 "vue" => Language::Vue,
139 "svelte" => Language::Svelte,
140
141 "py" | "pyi" | "pyw" => Language::Python,
143 "rb" | "erb" | "rake" | "gemspec" => Language::Ruby,
144 "php" | "phtml" | "php3" | "php4" | "php5" | "phps" => Language::Php,
145 "lua" => Language::Lua,
146 "pl" | "pm" | "t" => Language::Perl,
147
148 "hs" | "lhs" => Language::Haskell,
150 "ml" | "mli" => Language::OCaml,
151 "ex" | "exs" => Language::Elixir,
152 "erl" | "hrl" => Language::Erlang,
153
154 "go" => Language::Go,
156 "swift" => Language::Swift,
157 "cs" | "csx" => Language::CSharp,
158 "dart" => Language::Dart,
159
160 "json" | "jsonc" | "json5" => Language::Json,
162 "yaml" | "yml" => Language::Yaml,
163 "toml" => Language::Toml,
164 "sql" | "mysql" | "pgsql" | "plsql" => Language::Sql,
165 "graphql" | "gql" => Language::GraphQL,
166
167 "sh" | "bash" | "zsh" | "fish" => Language::Bash,
169 "dockerfile" => Language::Dockerfile,
170 "tf" | "tfvars" | "hcl" => Language::Hcl,
171 "nix" => Language::Nix,
172
173 "md" | "markdown" | "mdx" => Language::Markdown,
175 "tex" | "latex" | "sty" | "cls" => Language::Latex,
176
177 "sol" => Language::Solidity,
179 "wat" | "wast" => Language::Wasm,
180 "proto" | "proto3" => Language::Protobuf,
181
182 _ => Language::Unknown,
183 }
184 }
185
186 #[inline]
188 pub fn extensions(&self) -> &'static [&'static str] {
189 match self {
190 Language::Rust => &["rs"],
191 Language::C => &["c", "h"],
192 Language::Cpp => &["cc", "cpp", "cxx", "hpp", "hxx", "hh"],
193 Language::Zig => &["zig"],
194 Language::Java => &["java"],
195 Language::Kotlin => &["kt", "kts"],
196 Language::Scala => &["scala", "sc"],
197 Language::JavaScript => &["js", "mjs", "cjs", "jsx"],
198 Language::TypeScript => &["ts", "tsx", "mts", "cts"],
199 Language::Html => &["html", "htm"],
200 Language::Css => &["css"],
201 Language::Scss => &["scss", "sass"],
202 Language::Vue => &["vue"],
203 Language::Svelte => &["svelte"],
204 Language::Python => &["py", "pyi", "pyw"],
205 Language::Ruby => &["rb", "erb", "rake", "gemspec"],
206 Language::Php => &["php", "phtml"],
207 Language::Lua => &["lua"],
208 Language::Perl => &["pl", "pm", "t"],
209 Language::Haskell => &["hs", "lhs"],
210 Language::OCaml => &["ml", "mli"],
211 Language::Elixir => &["ex", "exs"],
212 Language::Erlang => &["erl", "hrl"],
213 Language::Go => &["go"],
214 Language::Swift => &["swift"],
215 Language::CSharp => &["cs", "csx"],
216 Language::Dart => &["dart"],
217 Language::Json => &["json", "jsonc", "json5"],
218 Language::Yaml => &["yaml", "yml"],
219 Language::Toml => &["toml"],
220 Language::Sql => &["sql", "mysql", "pgsql"],
221 Language::GraphQL => &["graphql", "gql"],
222 Language::Bash => &["sh", "bash", "zsh", "fish"],
223 Language::Dockerfile => &["dockerfile"],
224 Language::Hcl => &["tf", "tfvars", "hcl"],
225 Language::Nix => &["nix"],
226 Language::Markdown => &["md", "markdown", "mdx"],
227 Language::Latex => &["tex", "latex", "sty", "cls"],
228 Language::Solidity => &["sol"],
229 Language::Wasm => &["wat", "wast"],
230 Language::Protobuf => &["proto", "proto3"],
231 Language::Unknown => &[],
232 }
233 }
234
235 #[inline]
237 pub fn is_systems_language(&self) -> bool {
238 matches!(
239 self,
240 Language::Rust | Language::C | Language::Cpp | Language::Zig
241 )
242 }
243
244 #[inline]
246 pub fn is_scripting_language(&self) -> bool {
247 matches!(
248 self,
249 Language::JavaScript
250 | Language::TypeScript
251 | Language::Python
252 | Language::Ruby
253 | Language::Php
254 | Language::Lua
255 | Language::Perl
256 )
257 }
258
259 #[inline]
261 pub fn is_jvm_language(&self) -> bool {
262 matches!(self, Language::Java | Language::Kotlin | Language::Scala)
263 }
264
265 #[inline]
267 pub fn is_functional_language(&self) -> bool {
268 matches!(
269 self,
270 Language::Haskell | Language::OCaml | Language::Elixir | Language::Erlang
271 )
272 }
273
274 #[inline]
276 pub fn is_data_language(&self) -> bool {
277 matches!(
278 self,
279 Language::Json | Language::Yaml | Language::Toml | Language::Sql | Language::GraphQL
280 )
281 }
282
283 #[inline]
285 pub fn supports_security_scanning(&self) -> bool {
286 !matches!(
287 self,
288 Language::Unknown | Language::Markdown | Language::Latex | Language::Wasm
289 )
290 }
291}
292
293impl std::fmt::Display for Language {
294 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295 match self {
296 Language::Rust => write!(f, "rust"),
297 Language::C => write!(f, "c"),
298 Language::Cpp => write!(f, "cpp"),
299 Language::Zig => write!(f, "zig"),
300 Language::Java => write!(f, "java"),
301 Language::Kotlin => write!(f, "kotlin"),
302 Language::Scala => write!(f, "scala"),
303 Language::JavaScript => write!(f, "javascript"),
304 Language::TypeScript => write!(f, "typescript"),
305 Language::Html => write!(f, "html"),
306 Language::Css => write!(f, "css"),
307 Language::Scss => write!(f, "scss"),
308 Language::Vue => write!(f, "vue"),
309 Language::Svelte => write!(f, "svelte"),
310 Language::Python => write!(f, "python"),
311 Language::Ruby => write!(f, "ruby"),
312 Language::Php => write!(f, "php"),
313 Language::Lua => write!(f, "lua"),
314 Language::Perl => write!(f, "perl"),
315 Language::Haskell => write!(f, "haskell"),
316 Language::OCaml => write!(f, "ocaml"),
317 Language::Elixir => write!(f, "elixir"),
318 Language::Erlang => write!(f, "erlang"),
319 Language::Go => write!(f, "go"),
320 Language::Swift => write!(f, "swift"),
321 Language::CSharp => write!(f, "csharp"),
322 Language::Dart => write!(f, "dart"),
323 Language::Json => write!(f, "json"),
324 Language::Yaml => write!(f, "yaml"),
325 Language::Toml => write!(f, "toml"),
326 Language::Sql => write!(f, "sql"),
327 Language::GraphQL => write!(f, "graphql"),
328 Language::Bash => write!(f, "bash"),
329 Language::Dockerfile => write!(f, "dockerfile"),
330 Language::Hcl => write!(f, "hcl"),
331 Language::Nix => write!(f, "nix"),
332 Language::Markdown => write!(f, "markdown"),
333 Language::Latex => write!(f, "latex"),
334 Language::Solidity => write!(f, "solidity"),
335 Language::Wasm => write!(f, "wasm"),
336 Language::Protobuf => write!(f, "protobuf"),
337 Language::Unknown => write!(f, "unknown"),
338 }
339 }
340}
341
342#[derive(
344 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
345)]
346#[serde(rename_all = "lowercase")]
347pub enum Severity {
348 Info,
349 #[default]
350 Warning,
351 Error,
352 Critical,
353}
354
355impl std::fmt::Display for Severity {
356 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357 match self {
358 Severity::Info => write!(f, "info"),
359 Severity::Warning => write!(f, "warning"),
360 Severity::Error => write!(f, "error"),
361 Severity::Critical => write!(f, "critical"),
362 }
363 }
364}
365
366#[derive(
368 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
369)]
370#[serde(rename_all = "lowercase")]
371pub enum Confidence {
372 Low,
374 #[default]
376 Medium,
377 High,
379}
380
381impl std::fmt::Display for Confidence {
382 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383 match self {
384 Confidence::Low => write!(f, "low"),
385 Confidence::Medium => write!(f, "medium"),
386 Confidence::High => write!(f, "high"),
387 }
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
393#[serde(rename_all = "lowercase")]
394pub enum FindingCategory {
395 #[default]
397 Security,
398 Quality,
400 Performance,
402 Style,
404}
405
406impl std::fmt::Display for FindingCategory {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 match self {
409 FindingCategory::Security => write!(f, "security"),
410 FindingCategory::Quality => write!(f, "quality"),
411 FindingCategory::Performance => write!(f, "performance"),
412 FindingCategory::Style => write!(f, "style"),
413 }
414 }
415}
416
417#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
419pub struct SourceLocation {
420 pub file: PathBuf,
421 pub start_line: usize,
422 pub start_column: usize,
423 pub end_line: usize,
424 pub end_column: usize,
425}
426
427impl SourceLocation {
428 pub fn new(
429 file: PathBuf,
430 start_line: usize,
431 start_column: usize,
432 end_line: usize,
433 end_column: usize,
434 ) -> Self {
435 Self {
436 file,
437 start_line,
438 start_column,
439 end_line,
440 end_column,
441 }
442 }
443}
444
445impl std::fmt::Display for SourceLocation {
446 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447 write!(
448 f,
449 "{}:{}:{}-{}:{}",
450 self.file.display(),
451 self.start_line,
452 self.start_column,
453 self.end_line,
454 self.end_column
455 )
456 }
457}
458
459#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461pub struct Fix {
462 pub description: String,
464 pub replacement: String,
466 pub start_byte: usize,
468 pub end_byte: usize,
470}
471
472impl Fix {
473 pub fn new(
475 description: impl Into<String>,
476 replacement: impl Into<String>,
477 start_byte: usize,
478 end_byte: usize,
479 ) -> Self {
480 Self {
481 description: description.into(),
482 replacement: replacement.into(),
483 start_byte,
484 end_byte,
485 }
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
491#[serde(rename_all = "lowercase")]
492pub enum FindingSource {
493 #[default]
495 Builtin,
496 Codeql,
498 Pysa,
500 Osv,
502 Rustsec,
504 Oxc,
506 Oxlint,
508 Pmd,
510 Gosec,
512 #[serde(rename = "taint-flow")]
514 TaintFlow,
515 Plugin,
517 Ai,
519}
520
521impl std::fmt::Display for FindingSource {
522 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
523 match self {
524 FindingSource::Builtin => write!(f, "builtin"),
525 FindingSource::Codeql => write!(f, "codeql"),
526 FindingSource::Pysa => write!(f, "pysa"),
527 FindingSource::Osv => write!(f, "osv"),
528 FindingSource::Rustsec => write!(f, "rustsec"),
529 FindingSource::Oxc => write!(f, "oxc"),
530 FindingSource::Oxlint => write!(f, "oxlint"),
531 FindingSource::Pmd => write!(f, "pmd"),
532 FindingSource::Gosec => write!(f, "gosec"),
533 FindingSource::TaintFlow => write!(f, "taint-flow"),
534 FindingSource::Plugin => write!(f, "plugin"),
535 FindingSource::Ai => write!(f, "ai"),
536 }
537 }
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct Finding {
543 pub id: String,
544 pub rule_id: String,
545 pub message: String,
546 pub severity: Severity,
547 pub location: SourceLocation,
548 pub language: Language,
549 #[serde(skip_serializing_if = "Option::is_none")]
550 pub snippet: Option<String>,
551 #[serde(skip_serializing_if = "Option::is_none")]
552 pub suggestion: Option<String>,
553 #[serde(skip_serializing_if = "Option::is_none")]
555 pub fix: Option<Fix>,
556 #[serde(default)]
558 pub confidence: Confidence,
559 #[serde(default)]
561 pub category: FindingCategory,
562 #[serde(skip_serializing_if = "Option::is_none", default)]
564 pub subcategory: Option<Vec<String>>,
565 #[serde(skip_serializing_if = "Option::is_none", default)]
567 pub technology: Option<Vec<String>>,
568 #[serde(skip_serializing_if = "Option::is_none", default)]
570 pub impact: Option<String>,
571 #[serde(skip_serializing_if = "Option::is_none", default)]
573 pub likelihood: Option<String>,
574 #[serde(default)]
576 pub source: FindingSource,
577 #[serde(skip_serializing_if = "Option::is_none")]
579 pub fingerprint: Option<String>,
580 #[serde(skip_serializing_if = "Option::is_none", default)]
582 pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
583 #[serde(skip_serializing_if = "Option::is_none", default)]
586 pub occurrence_count: Option<usize>,
587 #[serde(skip_serializing_if = "Option::is_none", default)]
589 pub additional_locations: Option<Vec<usize>>,
590 #[serde(skip_serializing_if = "Option::is_none", default)]
592 pub ai_verdict: Option<String>,
593 #[serde(skip_serializing_if = "Option::is_none", default)]
595 pub ai_explanation: Option<String>,
596 #[serde(skip_serializing_if = "Option::is_none", default)]
598 pub ai_confidence: Option<f32>,
599}
600
601impl Finding {
602 pub fn compute_fingerprint(&mut self) {
605 use sha2::{Digest, Sha256};
606
607 let mut hasher = Sha256::new();
608 hasher.update(self.rule_id.as_bytes());
609 hasher.update(self.location.file.to_string_lossy().as_bytes());
610
611 if let Some(snippet) = &self.snippet {
613 let normalized: String = snippet.split_whitespace().collect::<Vec<_>>().join(" ");
614 hasher.update(normalized.as_bytes());
615 }
616
617 let hash = hasher.finalize();
618 self.fingerprint = Some(format!("sha256:{:x}", hash)[..23].to_string());
619 }
620}
621
622pub fn deduplicate_findings(findings: Vec<Finding>) -> Vec<Finding> {
635 use std::collections::HashMap;
636
637 let mut grouped: HashMap<(String, String), Vec<Finding>> = HashMap::new();
639
640 for finding in findings {
641 let key = (
642 finding.location.file.to_string_lossy().to_string(),
643 finding.rule_id.clone(),
644 );
645 grouped.entry(key).or_default().push(finding);
646 }
647
648 let mut result = Vec::new();
650 for ((_file, _rule_id), mut group) in grouped {
651 if group.len() == 1 {
652 result.push(group.remove(0));
654 } else {
655 let count = group.len();
657
658 group.sort_by_key(|f| f.location.start_line);
660
661 let mut representative = group.remove(0);
663
664 let additional_lines: Vec<usize> =
666 group.iter().map(|f| f.location.start_line).collect();
667
668 representative.occurrence_count = Some(count);
669 representative.additional_locations = Some(additional_lines);
670
671 representative.message = format!(
673 "{} ({} occurrences in this file)",
674 representative.message, count
675 );
676
677 result.push(representative);
678 }
679 }
680
681 result.sort_by(|a, b| {
683 let file_cmp = a.location.file.cmp(&b.location.file);
684 if file_cmp == std::cmp::Ordering::Equal {
685 a.location.start_line.cmp(&b.location.start_line)
686 } else {
687 file_cmp
688 }
689 });
690
691 result
692}
693
694#[derive(Debug, Clone, Default, Serialize, Deserialize)]
696pub struct CodeMetrics {
697 pub lines_of_code: usize,
698 pub lines_of_comments: usize,
699 pub blank_lines: usize,
700 pub cyclomatic_complexity: usize,
701 pub cognitive_complexity: usize,
702 pub function_count: usize,
703 pub class_count: usize,
704 pub import_count: usize,
705}
706
707#[derive(Debug, Clone, Default, Serialize, Deserialize)]
709pub struct ScanSummary {
710 pub files_scanned: usize,
711 pub files_skipped: usize,
712 pub total_lines: usize,
713 pub findings_by_severity: std::collections::HashMap<String, usize>,
714 pub languages: std::collections::HashMap<String, usize>,
715 pub duration_ms: u64,
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct RmaConfig {
721 #[serde(default)]
723 pub exclude_patterns: Vec<String>,
724
725 #[serde(default)]
727 pub languages: Vec<Language>,
728
729 #[serde(default = "default_min_severity")]
731 pub min_severity: Severity,
732
733 #[serde(default = "default_max_file_size")]
735 pub max_file_size: usize,
736
737 #[serde(default)]
739 pub parallelism: usize,
740
741 #[serde(default)]
743 pub incremental: bool,
744}
745
746fn default_min_severity() -> Severity {
747 Severity::Warning
748}
749
750fn default_max_file_size() -> usize {
751 10 * 1024 * 1024 }
753
754impl Default for RmaConfig {
755 fn default() -> Self {
756 Self {
757 exclude_patterns: vec![
758 "**/node_modules/**".into(),
759 "**/target/**".into(),
760 "**/vendor/**".into(),
761 "**/.git/**".into(),
762 "**/dist/**".into(),
763 "**/build/**".into(),
764 ],
765 languages: vec![],
766 min_severity: default_min_severity(),
767 max_file_size: default_max_file_size(),
768 parallelism: 0,
769 incremental: false,
770 }
771 }
772}
773
774#[cfg(test)]
775mod tests {
776 use super::*;
777
778 #[test]
779 fn test_language_from_extension() {
780 assert_eq!(Language::from_extension("rs"), Language::Rust);
781 assert_eq!(Language::from_extension("js"), Language::JavaScript);
782 assert_eq!(Language::from_extension("py"), Language::Python);
783 assert_eq!(Language::from_extension("unknown"), Language::Unknown);
784 }
785
786 #[test]
787 fn test_severity_ordering() {
788 assert!(Severity::Info < Severity::Warning);
789 assert!(Severity::Warning < Severity::Error);
790 assert!(Severity::Error < Severity::Critical);
791 }
792
793 #[test]
794 fn test_source_location_display() {
795 let loc = SourceLocation::new(PathBuf::from("test.rs"), 10, 5, 10, 15);
796 assert_eq!(loc.to_string(), "test.rs:10:5-10:15");
797 }
798}