use serde::{Deserialize, Serialize};
use thiserror::Error;
use crate::DedupScope;
pub const DEFAULT_MAX_FILE_SIZE_BYTES: u64 = 100 * 1024 * 1024;
pub const DEFAULT_ENTROPY_THRESHOLD: f64 = 4.5;
pub const DEFAULT_ENTROPY_BPE_MAX_BYTES_PER_TOKEN: f64 = 2.2;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ScanConfig {
pub min_confidence: f64,
pub max_decode_depth: usize,
pub entropy_enabled: bool,
pub entropy_in_source_files: bool,
#[serde(default = "default_entropy_ml_authoritative")]
pub entropy_ml_authoritative: bool,
#[serde(default = "default_generic_keyword_low_entropy")]
pub generic_keyword_low_entropy: bool,
pub entropy_threshold: f64,
#[serde(default = "default_entropy_bpe_max_bytes_per_token")]
pub entropy_bpe_max_bytes_per_token: f64,
pub min_secret_len: usize,
pub max_file_size: u64,
pub dedup: DedupScope,
pub ml_enabled: bool,
pub ml_weight: f64,
pub unicode_normalization: bool,
pub validate_decode: bool,
pub max_decode_bytes: usize,
pub max_matches_per_chunk: usize,
#[serde(default)]
pub scan_comments: bool,
pub known_prefixes: Vec<String>,
pub secret_keywords: Vec<String>,
pub test_keywords: Vec<String>,
pub placeholder_keywords: Vec<String>,
}
pub(crate) const MAX_DECODE_DEPTH_LIMIT: usize = 10;
pub const fn max_decode_depth_limit() -> usize {
MAX_DECODE_DEPTH_LIMIT
}
fn default_entropy_ml_authoritative() -> bool {
true
}
fn default_generic_keyword_low_entropy() -> bool {
true
}
fn default_entropy_bpe_max_bytes_per_token() -> f64 {
DEFAULT_ENTROPY_BPE_MAX_BYTES_PER_TOKEN
}
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("min_confidence must be between 0.0 and 1.0, found {0}")]
InvalidConfidence(f64),
#[error("max_decode_depth exceeds limit of {MAX_DECODE_DEPTH_LIMIT}, found {0}")]
DepthTooHigh(usize),
#[error("ml_weight must be between 0.0 and 1.0, found {0}")]
InvalidMlWeight(f64),
#[error("entropy_bpe_max_bytes_per_token must be a finite value > 0.0, found {0}")]
InvalidBpeBound(f64),
#[error("entropy_threshold must be a finite number, found {0}")]
NonFiniteEntropyThreshold(f64),
#[error("entropy_threshold must be between 0.0 and 8.0 bits per byte, found {0}")]
InvalidEntropyThreshold(f64),
#[error("failed to parse ScanConfig TOML: {0}")]
Parse(String),
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
min_confidence: 0.40,
max_decode_depth: 10,
entropy_enabled: true,
entropy_in_source_files: false,
entropy_ml_authoritative: true,
generic_keyword_low_entropy: true,
entropy_threshold: DEFAULT_ENTROPY_THRESHOLD,
entropy_bpe_max_bytes_per_token: DEFAULT_ENTROPY_BPE_MAX_BYTES_PER_TOKEN,
min_secret_len: 16,
max_file_size: DEFAULT_MAX_FILE_SIZE_BYTES,
dedup: DedupScope::Credential,
ml_enabled: true,
ml_weight: 0.5,
unicode_normalization: true,
validate_decode: true,
max_decode_bytes: 512 * 1024,
max_matches_per_chunk: 1000,
scan_comments: false,
known_prefixes: crate::embedded::CONFIG_KNOWN_PREFIXES
.iter()
.map(|value| (*value).to_string())
.collect(),
secret_keywords: crate::embedded::CONFIG_SECRET_KEYWORDS
.iter()
.map(|value| (*value).to_string())
.collect(),
test_keywords: crate::embedded::CONFIG_TEST_KEYWORDS
.iter()
.map(|value| (*value).to_string())
.collect(),
placeholder_keywords: crate::embedded::CONFIG_PLACEHOLDER_KEYWORDS
.iter()
.map(|value| (*value).to_string())
.collect(),
}
}
}
impl ScanConfig {
pub fn validate(&self) -> Result<(), ConfigError> {
if !(0.0..=1.0).contains(&self.min_confidence) {
return Err(ConfigError::InvalidConfidence(self.min_confidence));
}
if self.max_decode_depth > MAX_DECODE_DEPTH_LIMIT {
return Err(ConfigError::DepthTooHigh(self.max_decode_depth));
}
if !(0.0..=1.0).contains(&self.ml_weight) {
return Err(ConfigError::InvalidMlWeight(self.ml_weight));
}
if !self.entropy_bpe_max_bytes_per_token.is_finite()
|| self.entropy_bpe_max_bytes_per_token <= 0.0
{
return Err(ConfigError::InvalidBpeBound(
self.entropy_bpe_max_bytes_per_token,
));
}
if !self.entropy_threshold.is_finite() {
return Err(ConfigError::NonFiniteEntropyThreshold(
self.entropy_threshold,
));
}
if !(0.0..=8.0).contains(&self.entropy_threshold) {
return Err(ConfigError::InvalidEntropyThreshold(self.entropy_threshold));
}
Ok(())
}
pub fn from_toml_str(raw: &str) -> Result<Self, ConfigError> {
let config: ScanConfig =
toml::from_str(raw).map_err(|error| ConfigError::Parse(error.to_string()))?;
config.validate()?;
Ok(config)
}
}