Skip to main content

rma_common/
lib.rs

1//! Common types and utilities for Qryon
2//!
3//! This crate provides shared data structures, error types, and utilities
4//! used across all Qryon components.
5
6pub 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/// Core error types for RMA operations
27#[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/// Supported programming languages (30+ tree-sitter grammars)
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
50#[serde(rename_all = "lowercase")]
51pub enum Language {
52    // Systems languages
53    Rust,
54    C,
55    Cpp,
56    Zig,
57
58    // JVM languages
59    Java,
60    Kotlin,
61    Scala,
62
63    // Web languages
64    JavaScript,
65    TypeScript,
66    Html,
67    Css,
68    Scss,
69    Vue,
70    Svelte,
71
72    // Scripting languages
73    Python,
74    Ruby,
75    Php,
76    Lua,
77    Perl,
78
79    // Functional languages
80    Haskell,
81    OCaml,
82    Elixir,
83    Erlang,
84
85    // Other compiled languages
86    Go,
87    Swift,
88    CSharp,
89    Dart,
90
91    // Data/Config languages
92    Json,
93    Yaml,
94    Toml,
95    Sql,
96    GraphQL,
97
98    // Infrastructure
99    Bash,
100    Dockerfile,
101    Hcl, // Terraform
102    Nix,
103
104    // Markup
105    Markdown,
106    Latex,
107
108    // Other
109    Solidity, // Smart contracts
110    Wasm,     // WebAssembly text format
111    Protobuf,
112
113    Unknown,
114}
115
116impl Language {
117    /// Detect language from file extension
118    #[inline]
119    pub fn from_extension(ext: &str) -> Self {
120        match ext.to_lowercase().as_str() {
121            // Systems
122            "rs" => Language::Rust,
123            "c" | "h" => Language::C,
124            "cc" | "cpp" | "cxx" | "hpp" | "hxx" | "hh" => Language::Cpp,
125            "zig" => Language::Zig,
126
127            // JVM
128            "java" => Language::Java,
129            "kt" | "kts" => Language::Kotlin,
130            "scala" | "sc" => Language::Scala,
131
132            // Web
133            "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            // Scripting
142            "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            // Functional
149            "hs" | "lhs" => Language::Haskell,
150            "ml" | "mli" => Language::OCaml,
151            "ex" | "exs" => Language::Elixir,
152            "erl" | "hrl" => Language::Erlang,
153
154            // Other compiled
155            "go" => Language::Go,
156            "swift" => Language::Swift,
157            "cs" | "csx" => Language::CSharp,
158            "dart" => Language::Dart,
159
160            // Data/Config
161            "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            // Infrastructure
168            "sh" | "bash" | "zsh" | "fish" => Language::Bash,
169            "dockerfile" => Language::Dockerfile,
170            "tf" | "tfvars" | "hcl" => Language::Hcl,
171            "nix" => Language::Nix,
172
173            // Markup
174            "md" | "markdown" | "mdx" => Language::Markdown,
175            "tex" | "latex" | "sty" | "cls" => Language::Latex,
176
177            // Other
178            "sol" => Language::Solidity,
179            "wat" | "wast" => Language::Wasm,
180            "proto" | "proto3" => Language::Protobuf,
181
182            _ => Language::Unknown,
183        }
184    }
185
186    /// Get file extensions for this language
187    #[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    /// Check if this language is a systems language (for memory safety analysis)
236    #[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    /// Check if this language is a scripting language
245    #[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    /// Check if this language is a JVM language
260    #[inline]
261    pub fn is_jvm_language(&self) -> bool {
262        matches!(self, Language::Java | Language::Kotlin | Language::Scala)
263    }
264
265    /// Check if this language is a functional language
266    #[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    /// Check if this language is a data/config language
275    #[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    /// Check if this language supports security scanning (has security-relevant constructs)
284    #[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/// Severity levels for findings
343#[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/// Confidence level for findings (how certain we are this is a real issue)
367#[derive(
368    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
369)]
370#[serde(rename_all = "lowercase")]
371pub enum Confidence {
372    /// Low confidence - may be a false positive, requires manual review
373    Low,
374    /// Medium confidence - likely an issue but context-dependent
375    #[default]
376    Medium,
377    /// High confidence - almost certainly a real issue
378    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/// Category of finding
392#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
393#[serde(rename_all = "lowercase")]
394pub enum FindingCategory {
395    /// Security vulnerabilities
396    #[default]
397    Security,
398    /// Code quality and maintainability
399    Quality,
400    /// Performance issues
401    Performance,
402    /// Style and formatting
403    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/// A source code location
418#[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/// A suggested fix for a finding with precise byte offsets for auto-fix.
460#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
461pub struct Fix {
462    /// Human-readable description of the fix (e.g., "Replace yaml.load with yaml.safe_load")
463    pub description: String,
464    /// The replacement text to apply
465    pub replacement: String,
466    /// Start byte offset in the source
467    pub start_byte: usize,
468    /// End byte offset in the source (exclusive)
469    pub end_byte: usize,
470}
471
472impl Fix {
473    /// Create a new Fix with the given parameters
474    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/// Source engine that produced a finding
490#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
491#[serde(rename_all = "lowercase")]
492pub enum FindingSource {
493    /// Built-in Semgrep-style pattern rules (compiled into binary)
494    #[default]
495    Builtin,
496    /// CodeQL Models-as-Data generated profiles
497    Codeql,
498    /// Pysa taint stub generated profiles
499    Pysa,
500    /// OSV open-source vulnerability database
501    Osv,
502    /// RustSec advisory database
503    Rustsec,
504    /// Oxc native JS/TS linter
505    Oxc,
506    /// Oxlint CLI JS/TS linter
507    Oxlint,
508    /// PMD Java static analysis
509    Pmd,
510    /// Gosec Go security checker
511    Gosec,
512    /// Cross-file taint flow analysis
513    #[serde(rename = "taint-flow")]
514    TaintFlow,
515    /// WASM plugin system
516    Plugin,
517    /// AI-powered analysis
518    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/// A security or code quality finding
541#[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    /// Structured fix for auto-fix with precise byte offsets
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub fix: Option<Fix>,
556    /// Confidence level (how certain we are this is a real issue)
557    #[serde(default)]
558    pub confidence: Confidence,
559    /// Category of finding (security, quality, performance, style)
560    #[serde(default)]
561    pub category: FindingCategory,
562    /// Security subcategory (vuln, audit, style) — normalized from YAML metadata
563    #[serde(skip_serializing_if = "Option::is_none", default)]
564    pub subcategory: Option<Vec<String>>,
565    /// Technology tags from rule metadata
566    #[serde(skip_serializing_if = "Option::is_none", default)]
567    pub technology: Option<Vec<String>>,
568    /// Impact level from rule metadata (HIGH/MEDIUM/LOW)
569    #[serde(skip_serializing_if = "Option::is_none", default)]
570    pub impact: Option<String>,
571    /// Likelihood level from rule metadata (HIGH/MEDIUM/LOW)
572    #[serde(skip_serializing_if = "Option::is_none", default)]
573    pub likelihood: Option<String>,
574    /// Source engine that produced this finding
575    #[serde(default)]
576    pub source: FindingSource,
577    /// Stable fingerprint for baseline comparison (sha256 hash)
578    #[serde(skip_serializing_if = "Option::is_none")]
579    pub fingerprint: Option<String>,
580    /// Additional properties (e.g., import_hits, import_files_sample for OSV findings)
581    #[serde(skip_serializing_if = "Option::is_none", default)]
582    pub properties: Option<std::collections::HashMap<String, serde_json::Value>>,
583    /// Number of occurrences when deduplicated (same rule in same file)
584    /// None or 1 means single occurrence, >1 means multiple occurrences consolidated
585    #[serde(skip_serializing_if = "Option::is_none", default)]
586    pub occurrence_count: Option<usize>,
587    /// Additional line numbers when occurrence_count > 1
588    #[serde(skip_serializing_if = "Option::is_none", default)]
589    pub additional_locations: Option<Vec<usize>>,
590    /// AI triage verdict: "true_positive", "false_positive", or "needs_review"
591    #[serde(skip_serializing_if = "Option::is_none", default)]
592    pub ai_verdict: Option<String>,
593    /// AI explanation of the finding
594    #[serde(skip_serializing_if = "Option::is_none", default)]
595    pub ai_explanation: Option<String>,
596    /// AI confidence in its verdict (0.0-1.0)
597    #[serde(skip_serializing_if = "Option::is_none", default)]
598    pub ai_confidence: Option<f32>,
599}
600
601impl Finding {
602    /// Compute a stable fingerprint for this finding
603    /// Based on: rule_id + relative path + normalized snippet
604    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        // Normalize snippet by removing whitespace
612        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
622/// Deduplicate findings by grouping same rule in same file
623///
624/// When the same rule fires multiple times in the same file, consolidates them
625/// into a single finding with `occurrence_count` set to the total count.
626/// The first occurrence is kept as the representative, with additional line
627/// numbers stored in `additional_locations`.
628///
629/// # Arguments
630/// * `findings` - Vector of findings to deduplicate
631///
632/// # Returns
633/// * Deduplicated vector of findings with occurrence counts
634pub fn deduplicate_findings(findings: Vec<Finding>) -> Vec<Finding> {
635    use std::collections::HashMap;
636
637    // Group by (file, rule_id)
638    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    // Consolidate each group
649    let mut result = Vec::new();
650    for ((_file, _rule_id), mut group) in grouped {
651        if group.len() == 1 {
652            // Single occurrence - no deduplication needed
653            result.push(group.remove(0));
654        } else {
655            // Multiple occurrences - consolidate
656            let count = group.len();
657
658            // Sort by line number to get the first occurrence
659            group.sort_by_key(|f| f.location.start_line);
660
661            // Take the first as representative
662            let mut representative = group.remove(0);
663
664            // Collect additional line numbers
665            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            // Update message to indicate deduplication
672            representative.message = format!(
673                "{} ({} occurrences in this file)",
674                representative.message, count
675            );
676
677            result.push(representative);
678        }
679    }
680
681    // Sort by file and line for consistent output
682    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/// Code metrics for a file or function
695#[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/// Summary of a scan operation
708#[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/// Configuration for RMA operations
719#[derive(Debug, Clone, Serialize, Deserialize)]
720pub struct RmaConfig {
721    /// Paths to exclude from scanning
722    #[serde(default)]
723    pub exclude_patterns: Vec<String>,
724
725    /// Languages to scan (empty = all supported)
726    #[serde(default)]
727    pub languages: Vec<Language>,
728
729    /// Minimum severity to report
730    #[serde(default = "default_min_severity")]
731    pub min_severity: Severity,
732
733    /// Maximum file size in bytes
734    #[serde(default = "default_max_file_size")]
735    pub max_file_size: usize,
736
737    /// Number of parallel workers (0 = auto)
738    #[serde(default)]
739    pub parallelism: usize,
740
741    /// Enable incremental mode
742    #[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 // 10MB
752}
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}