use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub general: GeneralConfig,
pub scoring: ScoringConfig,
pub name_filter: NameFilterConfig,
pub scope_filter: ScopeFilterConfig,
pub consequence: ConsequenceConfig,
pub rhs: RhsConfig,
pub output: OutputConfig,
#[serde(default)]
pub rules: Vec<CustomRule>,
}
impl Default for Config {
fn default() -> Self {
Self {
general: GeneralConfig::default(),
scoring: ScoringConfig::default(),
name_filter: NameFilterConfig::default(),
scope_filter: ScopeFilterConfig::default(),
consequence: ConsequenceConfig::default(),
rhs: RhsConfig::default(),
output: OutputConfig::default(),
rules: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GeneralConfig {
pub ignore_paths: Vec<String>,
pub extensions: Vec<String>,
pub max_file_size: usize,
pub follow_symlinks: bool,
}
impl Default for GeneralConfig {
fn default() -> Self {
Self {
ignore_paths: vec![
"**/target/**".into(),
"**/node_modules/**".into(),
"**/.git/**".into(),
],
extensions: vec!["rs".into()],
max_file_size: 1_000_000,
follow_symlinks: false,
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SensitivityPreset {
Paranoid,
High,
#[default]
Balanced,
Low,
Minimal,
}
impl SensitivityPreset {
pub fn threshold(&self) -> i32 {
match self {
Self::Paranoid => 30,
Self::High => 50,
Self::Balanced => 70,
Self::Low => 85,
Self::Minimal => 95,
}
}
pub fn base_comparison_score(&self) -> i32 {
match self {
Self::Paranoid => 60,
Self::High => 55,
Self::Balanced => 50,
Self::Low => 45,
Self::Minimal => 40,
}
}
}
impl std::str::FromStr for SensitivityPreset {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"paranoid" => Ok(Self::Paranoid),
"high" => Ok(Self::High),
"balanced" => Ok(Self::Balanced),
"low" => Ok(Self::Low),
"minimal" => Ok(Self::Minimal),
_ => Err(format!("Unknown sensitivity preset: {}", s)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ScoringConfig {
pub sensitivity: SensitivityPreset,
pub threshold_override: Option<i32>,
pub base_comparison_score: i32,
pub base_definition_score: i32,
pub ignore_values: Vec<String>,
pub ignore_value_prefixes: Vec<String>,
pub ignore_value_suffixes: Vec<String>,
pub ignore_value_contains: Vec<String>,
}
impl Default for ScoringConfig {
fn default() -> Self {
Self {
sensitivity: SensitivityPreset::default(),
threshold_override: None,
base_comparison_score: 50,
base_definition_score: 30,
ignore_values: Vec::new(),
ignore_value_prefixes: Vec::new(),
ignore_value_suffixes: Vec::new(),
ignore_value_contains: Vec::new(),
}
}
}
impl ScoringConfig {
pub fn effective_threshold(&self) -> i32 {
self.threshold_override
.unwrap_or_else(|| self.sensitivity.threshold())
}
pub fn should_ignore_value(&self, value: &str) -> bool {
let lower = value.to_lowercase();
if self.ignore_values.iter().any(|v| v.to_lowercase() == lower) {
return true;
}
if self.ignore_value_prefixes.iter().any(|p| lower.starts_with(&p.to_lowercase())) {
return true;
}
if self.ignore_value_suffixes.iter().any(|s| lower.ends_with(&s.to_lowercase())) {
return true;
}
if self.ignore_value_contains.iter().any(|c| lower.contains(&c.to_lowercase())) {
return true;
}
false
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct NameFilterConfig {
pub additional_benign_terms: Vec<TermConfig>,
pub additional_suspicious_terms: Vec<TermConfig>,
pub ignore_names: Vec<String>,
pub ignore_patterns: Vec<String>,
}
impl Default for NameFilterConfig {
fn default() -> Self {
Self {
additional_benign_terms: Vec::new(),
additional_suspicious_terms: Vec::new(),
ignore_names: Vec::new(),
ignore_patterns: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TermConfig {
pub term: String,
pub score: i32,
#[serde(default = "default_match_mode")]
pub mode: String,
}
fn default_match_mode() -> String {
"contains".into()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ScopeFilterConfig {
pub ignore_tests: bool,
pub ignore_examples: bool,
pub ignore_benchmarks: bool,
pub ignore_paths: Vec<String>,
}
impl Default for ScopeFilterConfig {
fn default() -> Self {
Self {
ignore_tests: true,
ignore_examples: true,
ignore_benchmarks: true,
ignore_paths: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ConsequenceConfig {
pub additional_logging_functions: Vec<String>,
pub additional_auth_functions: Vec<String>,
pub additional_auth_fields: Vec<String>,
}
impl Default for ConsequenceConfig {
fn default() -> Self {
Self {
additional_logging_functions: Vec::new(),
additional_auth_functions: Vec::new(),
additional_auth_fields: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct RhsConfig {
pub additional_command_names: Vec<String>,
pub additional_auth_names: Vec<String>,
}
impl Default for RhsConfig {
fn default() -> Self {
Self {
additional_command_names: Vec::new(),
additional_auth_names: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct OutputConfig {
pub format: String,
pub show_scores: bool,
pub show_remediation: bool,
pub color: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
format: "text".into(),
show_scores: false,
show_remediation: true,
color: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomRule {
pub id: String,
pub description: String,
pub name_pattern: Option<String>,
pub value_pattern: Option<String>,
pub score: i32,
pub severity: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = Config::default();
assert_eq!(config.scoring.sensitivity, SensitivityPreset::Balanced);
assert!(config.scope_filter.ignore_tests);
assert!(config.general.ignore_paths.contains(&"**/target/**".to_string()));
}
#[test]
fn test_sensitivity_preset_threshold() {
assert_eq!(SensitivityPreset::Paranoid.threshold(), 30);
assert_eq!(SensitivityPreset::Balanced.threshold(), 70);
assert_eq!(SensitivityPreset::Minimal.threshold(), 95);
}
#[test]
fn test_effective_threshold() {
let mut config = ScoringConfig::default();
assert_eq!(config.effective_threshold(), 70);
config.threshold_override = Some(80);
assert_eq!(config.effective_threshold(), 80); }
#[test]
fn test_preset_from_str() {
assert_eq!(
"paranoid".parse::<SensitivityPreset>().unwrap(),
SensitivityPreset::Paranoid
);
assert_eq!(
"BALANCED".parse::<SensitivityPreset>().unwrap(),
SensitivityPreset::Balanced
);
assert!("invalid".parse::<SensitivityPreset>().is_err());
}
#[test]
fn test_config_serialization() {
let config = Config::default();
let toml_str = toml::to_string(&config).unwrap();
assert!(toml_str.contains("[general]"));
assert!(toml_str.contains("[scoring]"));
let parsed: Config = toml::from_str(&toml_str).unwrap();
assert_eq!(parsed.scoring.sensitivity, config.scoring.sensitivity);
}
}