use std::path::Path;
use crate::config::defaults::default_config_toml;
use crate::config::schema::Config;
use crate::error::{Result, SecretraceError};
pub fn load_from_file(path: &Path) -> Result<Config> {
let content = std::fs::read_to_string(path).map_err(|e| SecretraceError::FileRead {
path: path.to_path_buf(),
source: e,
})?;
toml::from_str(&content).map_err(|e| SecretraceError::Config {
message: format!("Failed to parse config file: {}", path.display()),
source: Some(Box::new(e)),
})
}
pub fn load_or_default(start_dir: &Path) -> Result<Config> {
let config_names = [".vsec.toml", "vsec.toml"];
let mut current = start_dir.to_path_buf();
if current.is_file() {
current = current.parent().map(|p| p.to_path_buf()).unwrap_or(current);
}
loop {
for name in &config_names {
let config_path = current.join(name);
if config_path.exists() {
return load_from_file(&config_path);
}
}
match current.parent() {
Some(parent) => current = parent.to_path_buf(),
None => break,
}
}
Ok(Config::default())
}
pub fn generate_default_config(minimal: bool) -> String {
default_config_toml(minimal).to_string()
}
pub fn merge_configs(base: Config, overlay: Config) -> Config {
Config {
general: if overlay.general.ignore_paths.is_empty() {
base.general
} else {
overlay.general
},
scoring: overlay.scoring,
name_filter: NameFilterConfig {
additional_benign_terms: merge_vec(
base.name_filter.additional_benign_terms,
overlay.name_filter.additional_benign_terms,
),
additional_suspicious_terms: merge_vec(
base.name_filter.additional_suspicious_terms,
overlay.name_filter.additional_suspicious_terms,
),
ignore_names: merge_vec(
base.name_filter.ignore_names,
overlay.name_filter.ignore_names,
),
ignore_patterns: merge_vec(
base.name_filter.ignore_patterns,
overlay.name_filter.ignore_patterns,
),
},
scope_filter: overlay.scope_filter,
consequence: ConsequenceConfig {
additional_logging_functions: merge_vec(
base.consequence.additional_logging_functions,
overlay.consequence.additional_logging_functions,
),
additional_auth_functions: merge_vec(
base.consequence.additional_auth_functions,
overlay.consequence.additional_auth_functions,
),
additional_auth_fields: merge_vec(
base.consequence.additional_auth_fields,
overlay.consequence.additional_auth_fields,
),
},
rhs: RhsConfig {
additional_command_names: merge_vec(
base.rhs.additional_command_names,
overlay.rhs.additional_command_names,
),
additional_auth_names: merge_vec(
base.rhs.additional_auth_names,
overlay.rhs.additional_auth_names,
),
},
output: overlay.output,
rules: merge_vec(base.rules, overlay.rules),
}
}
use crate::config::schema::{ConsequenceConfig, NameFilterConfig, RhsConfig};
fn merge_vec<T>(mut base: Vec<T>, overlay: Vec<T>) -> Vec<T> {
base.extend(overlay);
base
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::TempDir;
#[test]
fn test_load_from_file() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join(".vsec.toml");
let mut file = std::fs::File::create(&config_path).unwrap();
writeln!(file, r#"
[scoring]
sensitivity = "high"
threshold_override = 60
"#).unwrap();
let config = load_from_file(&config_path).unwrap();
assert_eq!(
config.scoring.sensitivity,
crate::config::SensitivityPreset::High
);
assert_eq!(config.scoring.threshold_override, Some(60));
}
#[test]
fn test_load_or_default_finds_config() {
let dir = TempDir::new().unwrap();
let config_path = dir.path().join(".vsec.toml");
let mut file = std::fs::File::create(&config_path).unwrap();
writeln!(file, r#"
[scoring]
sensitivity = "paranoid"
"#).unwrap();
let config = load_or_default(dir.path()).unwrap();
assert_eq!(
config.scoring.sensitivity,
crate::config::SensitivityPreset::Paranoid
);
}
#[test]
fn test_load_or_default_uses_default() {
let dir = TempDir::new().unwrap();
let config = load_or_default(dir.path()).unwrap();
assert_eq!(
config.scoring.sensitivity,
crate::config::SensitivityPreset::Balanced
);
}
#[test]
fn test_generate_default_config() {
let minimal = generate_default_config(true);
assert!(minimal.contains("sensitivity"));
assert!(!minimal.contains("additional_benign_terms"));
let full = generate_default_config(false);
assert!(full.contains("sensitivity"));
assert!(full.contains("additional_benign_terms"));
}
#[test]
fn test_merge_configs() {
let base = Config {
name_filter: NameFilterConfig {
ignore_names: vec!["BASE_NAME".to_string()],
..Default::default()
},
..Default::default()
};
let overlay = Config {
name_filter: NameFilterConfig {
ignore_names: vec!["OVERLAY_NAME".to_string()],
..Default::default()
},
..Default::default()
};
let merged = merge_configs(base, overlay);
assert!(merged.name_filter.ignore_names.contains(&"BASE_NAME".to_string()));
assert!(merged.name_filter.ignore_names.contains(&"OVERLAY_NAME".to_string()));
}
}