use thiserror::Error;
pub const DEFAULT_SUSPICIOUS_ENTROPY: f64 = 4.5;
pub const DEFAULT_MAX_OCCURRENCES_PER_STRING: usize = 1_000;
pub const DEFAULT_MAX_UNIQUE_STRINGS: usize = 100_000;
pub const DEFAULT_MAX_INPUT_BYTES: usize = 1_048_576;
pub const DEFAULT_MAX_SOURCE_BYTES: usize = 16_384;
pub const DEFAULT_MAX_UNIQUE_FILE_IDENTITIES_PER_STRING: usize = 1_024;
pub const DEFAULT_MAX_CATEGORIES_PER_STRING: usize = 64;
pub const DEFAULT_MAX_INDICATORS_PER_STRING: usize = 64;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct AnalysisConfig {
pub min_suspicious_entropy: f64,
pub max_occurrences_per_string: usize,
pub max_unique_strings: usize,
pub max_input_bytes: usize,
pub max_source_bytes: usize,
pub max_unique_file_identities_per_string: usize,
pub max_categories_per_string: usize,
pub max_indicators_per_string: usize,
}
impl AnalysisConfig {
pub fn validate(&self) -> AnalysisResult<()> {
if !self.min_suspicious_entropy.is_finite()
|| !(0.0..=8.0).contains(&self.min_suspicious_entropy)
{
return Err(AnalysisError::InvalidConfiguration {
field: "min_suspicious_entropy",
reason: "must be finite and between 0 and 8 inclusive".to_string(),
});
}
for (field, value) in [
(
"max_occurrences_per_string",
self.max_occurrences_per_string,
),
("max_unique_strings", self.max_unique_strings),
("max_input_bytes", self.max_input_bytes),
("max_source_bytes", self.max_source_bytes),
(
"max_unique_file_identities_per_string",
self.max_unique_file_identities_per_string,
),
("max_categories_per_string", self.max_categories_per_string),
("max_indicators_per_string", self.max_indicators_per_string),
] {
if value == 0 {
return Err(AnalysisError::InvalidConfiguration {
field,
reason: "must be greater than zero".to_string(),
});
}
}
Ok(())
}
}
impl Default for AnalysisConfig {
fn default() -> Self {
Self {
min_suspicious_entropy: DEFAULT_SUSPICIOUS_ENTROPY,
max_occurrences_per_string: DEFAULT_MAX_OCCURRENCES_PER_STRING,
max_unique_strings: DEFAULT_MAX_UNIQUE_STRINGS,
max_input_bytes: DEFAULT_MAX_INPUT_BYTES,
max_source_bytes: DEFAULT_MAX_SOURCE_BYTES,
max_unique_file_identities_per_string: DEFAULT_MAX_UNIQUE_FILE_IDENTITIES_PER_STRING,
max_categories_per_string: DEFAULT_MAX_CATEGORIES_PER_STRING,
max_indicators_per_string: DEFAULT_MAX_INDICATORS_PER_STRING,
}
}
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum AnalysisError {
#[error("invalid configuration `{field}`: {reason}")]
InvalidConfiguration {
field: &'static str,
reason: String,
},
#[error("invalid filter `{field}`: {reason}")]
InvalidFilter {
field: &'static str,
reason: String,
},
#[error("invalid component output `{field}`: {reason}")]
InvalidComponentOutput {
field: &'static str,
reason: String,
},
#[error("`{field}` is {actual} bytes; configured maximum is {limit}")]
InputTooLarge {
field: &'static str,
actual: usize,
limit: usize,
},
#[error("capacity exceeded for {resource}: configured maximum is {limit}")]
CapacityExceeded {
resource: &'static str,
limit: usize,
},
#[error("invalid {kind} identifier: {reason}")]
InvalidIdentifier {
kind: &'static str,
name: String,
reason: &'static str,
},
#[error("invalid severity {severity}; expected a value from 0 through 10")]
InvalidSeverity {
severity: u8,
},
#[error("duplicate {kind} name")]
DuplicateName {
kind: &'static str,
name: String,
},
#[error("{kind} was not found")]
NotFound {
kind: &'static str,
name: String,
},
#[error("invalid regular expression for {context}: {reason}")]
InvalidRegex {
context: &'static str,
reason: &'static str,
},
}
pub type AnalysisResult<T> = Result<T, AnalysisError>;
pub(crate) const MAX_IDENTIFIER_BYTES: usize = 256;
pub(crate) const MAX_DESCRIPTION_BYTES: usize = 4_096;
pub(crate) const MAX_REGEX_BYTES: usize = 65_536;
pub(crate) fn validate_identifier(kind: &'static str, value: &str) -> AnalysisResult<()> {
if value.len() > MAX_IDENTIFIER_BYTES {
return Err(AnalysisError::InputTooLarge {
field: kind,
actual: value.len(),
limit: MAX_IDENTIFIER_BYTES,
});
}
let reason = if value.trim().is_empty() {
Some("must not be empty or whitespace-only")
} else if value.chars().any(char::is_control) {
Some("must not contain control characters")
} else {
None
};
if let Some(reason) = reason {
return Err(AnalysisError::InvalidIdentifier {
kind,
name: value.to_string(),
reason,
});
}
Ok(())
}
pub(crate) fn compact_string(value: String) -> String {
value.into_boxed_str().into_string()
}
pub(crate) fn regex_error_reason(error: ®ex::Error) -> &'static str {
match error {
regex::Error::Syntax(_) => "syntax error",
regex::Error::CompiledTooBig(_) => "compiled expression exceeds the size limit",
_ => "regular expression compilation failed",
}
}