use crate::{
expr::Expr,
parser::{
error::{DetectError, SpanExt},
typed::{
self, EnumOperator, EnumSelector, NumericOperator, NumericSelector, PathComponent,
StringOperator, StringSelector, TemporalOperator, TemporalSelector, TypedSelector,
},
RawExpr, RawPredicate, RawValue,
},
predicate::{
parse_time_value, Bound, DetectFileType, EnumMatcher, EnumPredicate, MetadataPredicate,
NamePredicate, NumberMatcher, Predicate, StreamingCompiledContentPredicate, StringMatcher,
TimeMatcher,
},
};
fn parse_size_value(s: &str, value_span: pest::Span, source: &str) -> Result<u64, DetectError> {
crate::util::parse_size(s).map_err(|err_msg| DetectError::InvalidValue {
expected: "size with unit (e.g., 1mb, 100kb)".to_string(),
found: err_msg,
span: value_span.to_source_span(),
src: source.to_string(),
})
}
fn is_ordering_operator(op: typed::StructuredOperator) -> bool {
matches!(
op,
typed::StructuredOperator::Greater
| typed::StructuredOperator::GreaterOrEqual
| typed::StructuredOperator::Less
| typed::StructuredOperator::LessOrEqual
)
}
pub struct Typechecker;
impl Typechecker {
pub fn typecheck(
raw_expr: RawExpr<'_>,
source: &str,
config: &crate::RuntimeConfig,
) -> Result<Expr<Predicate>, DetectError> {
Self::typecheck_inner(raw_expr, source, config)
}
fn typecheck_inner(
raw_expr: RawExpr<'_>,
source: &str,
config: &crate::RuntimeConfig,
) -> Result<Expr<Predicate>, DetectError> {
match raw_expr {
RawExpr::Predicate(pred) => Self::typecheck_predicate(pred, source, config),
RawExpr::And(lhs, rhs) => {
let typed_lhs = Self::typecheck_inner(*lhs, source, config)?;
let typed_rhs = Self::typecheck_inner(*rhs, source, config)?;
Ok(Expr::and(typed_lhs, typed_rhs))
}
RawExpr::Or(lhs, rhs) => {
let typed_lhs = Self::typecheck_inner(*lhs, source, config)?;
let typed_rhs = Self::typecheck_inner(*rhs, source, config)?;
Ok(Expr::or(typed_lhs, typed_rhs))
}
RawExpr::Not(expr) => {
let typed_expr = Self::typecheck_inner(*expr, source, config)?;
Ok(Expr::negate(typed_expr))
}
RawExpr::SingleWord(span) => {
let word = span.as_str();
match crate::parser::resolve_alias(word) {
Ok(predicate) => Ok(Expr::Predicate(predicate)),
Err(typed::AliasError::UnknownAlias(_)) => {
let suggestions = crate::parser::suggest_aliases(word);
let suggestions_msg = if suggestions.is_empty() {
Some(format!(
"Valid aliases: {}",
crate::predicate::DetectFileType::all_valid_strings().join(", ")
))
} else {
Some(format!("Did you mean: {}?", suggestions.join(", ")))
};
Err(DetectError::UnknownAlias {
word: word.to_string(),
span: span.to_source_span(),
src: source.to_string(),
suggestions: suggestions_msg,
})
}
Err(typed::AliasError::Structured(
typed::StructuredSelectorError::UnknownFormat { format },
)) => Err(DetectError::UnknownStructuredFormat {
format,
span: span.to_source_span(),
src: source.to_string(),
suggestions: Some("Valid formats: yaml, json, toml".to_string()),
}),
Err(typed::AliasError::Structured(
typed::StructuredSelectorError::InvalidPath {
format,
path,
reason,
},
)) => Err(DetectError::InvalidStructuredPath {
format,
path,
span: span.to_source_span(),
reason,
src: source.to_string(),
}),
}
}
}
}
fn build_synthetic_precondition(
format: typed::DataFormat,
config: &crate::RuntimeConfig,
actual_predicate: Predicate,
) -> Expr<Predicate> {
use std::collections::HashSet;
use typed::DataFormat;
let extensions: Vec<&str> = match format {
DataFormat::Yaml => vec!["yaml", "yml"],
DataFormat::Json => vec!["json"],
DataFormat::Toml => vec!["toml"],
};
let ext_set: HashSet<String> = extensions.iter().map(|s| (*s).to_string()).collect();
let ext_predicate = Predicate::name(NamePredicate::Extension(StringMatcher::In(ext_set)));
let size_predicate = Predicate::meta(MetadataPredicate::Filesize(NumberMatcher::In(
Bound::Right(..config.max_structured_size),
)));
Expr::and(
Expr::and(
Expr::Predicate(ext_predicate),
Expr::Predicate(size_predicate),
),
Expr::Predicate(actual_predicate),
)
}
fn typecheck_predicate(
pred: RawPredicate<'_>,
source: &str,
config: &crate::RuntimeConfig,
) -> Result<Expr<Predicate>, DetectError> {
let typed_selector = typed::parse_selector_operator(
pred.selector,
pred.selector_span,
pred.operator,
pred.operator_span,
source,
)?;
match typed_selector {
TypedSelector::String(selector, operator) => {
let predicate = Self::build_string_predicate(
selector,
operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Expr::Predicate(predicate))
}
TypedSelector::Numeric(selector, operator) => {
let predicate = Self::build_numeric_predicate(
selector,
operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Expr::Predicate(predicate))
}
TypedSelector::Temporal(selector, operator) => {
let predicate = Self::build_temporal_predicate(
selector,
operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Expr::Predicate(predicate))
}
TypedSelector::Enum(selector, operator) => {
let predicate = Self::build_enum_predicate(
selector,
operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Expr::Predicate(predicate))
}
TypedSelector::StructuredData(format, path, operator) => {
let predicate = Self::build_structured_predicate(
format,
path,
operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Self::build_synthetic_precondition(
format, config, predicate,
))
}
TypedSelector::StructuredDataString(format, path, string_operator) => {
let predicate = Self::build_structured_string_predicate(
format,
path,
string_operator,
&pred.value,
pred.value_span,
source,
)?;
Ok(Self::build_synthetic_precondition(
format, config, predicate,
))
}
}
}
fn build_string_predicate(
selector: StringSelector,
operator: StringOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
let string_matcher = Self::parse_string_value(value, operator, value_span, source)?;
match selector {
StringSelector::Path(component) => {
let name_pred = match component {
PathComponent::Full => NamePredicate::FullPath(string_matcher),
PathComponent::Name => NamePredicate::FileName(string_matcher),
PathComponent::Stem => NamePredicate::BaseName(string_matcher),
PathComponent::Extension => NamePredicate::Extension(string_matcher),
PathComponent::Parent => NamePredicate::DirPath(string_matcher),
};
Ok(Predicate::name(name_pred))
}
StringSelector::Contents => {
let pattern = Self::build_content_pattern(value, operator, value_span, source)?;
let content_pred =
StreamingCompiledContentPredicate::new(pattern).map_err(|e| {
DetectError::InvalidValue {
expected: "valid regex pattern".to_string(),
found: format!("{e:?}"),
span: value_span.to_source_span(),
src: source.to_string(),
}
})?;
Ok(Predicate::contents(content_pred))
}
}
}
fn build_numeric_predicate(
selector: NumericSelector,
operator: NumericOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
let number_value = Self::parse_numeric_value(value, &selector, value_span, source)?;
let number_matcher = Self::build_number_matcher(operator, number_value);
match selector {
NumericSelector::Size => {
Ok(Predicate::meta(MetadataPredicate::Filesize(number_matcher)))
}
NumericSelector::Depth => Ok(Predicate::name(NamePredicate::Depth(number_matcher))),
}
}
fn build_temporal_predicate(
selector: TemporalSelector,
operator: TemporalOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
let time_value = Self::parse_temporal_value(value, value_span, source)?;
let time_matcher = Self::build_time_matcher(operator, time_value);
let meta_pred = match selector {
TemporalSelector::Modified => MetadataPredicate::Modified(time_matcher),
TemporalSelector::Created => MetadataPredicate::Created(time_matcher),
TemporalSelector::Accessed => MetadataPredicate::Accessed(time_matcher),
};
Ok(Predicate::meta(meta_pred))
}
fn build_enum_predicate(
selector: EnumSelector,
operator: EnumOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
match selector {
EnumSelector::Type => {
let enum_matcher =
Self::parse_enum_value::<DetectFileType>(value, operator, value_span, source)?;
Ok(Predicate::meta(MetadataPredicate::Type(enum_matcher)))
}
}
}
fn build_structured_predicate(
format: typed::DataFormat,
path: Vec<super::structured_path::PathComponent>,
operator: typed::StructuredOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
use crate::predicate::StructuredDataPredicate;
use typed::DataFormat;
let raw_string = value.as_string().to_string();
let predicate = match format {
DataFormat::Yaml => {
let yaml_value = Self::build_yaml_rhs(value);
if is_ordering_operator(operator) && !Self::is_comparable_yaml(&yaml_value) {
return Err(DetectError::InvalidValue {
expected: "numeric or date value".to_string(),
found: format!("{yaml_value:?}"),
span: value_span.to_source_span(),
src: source.to_string(),
});
}
StructuredDataPredicate::YamlValue {
path,
operator,
value: yaml_value,
raw_string,
}
}
DataFormat::Json => {
let json_value = Self::build_json_rhs(value);
if is_ordering_operator(operator) && !Self::is_comparable_json(&json_value) {
return Err(DetectError::InvalidValue {
expected: "numeric or date value".to_string(),
found: format!("{json_value:?}"),
span: value_span.to_source_span(),
src: source.to_string(),
});
}
StructuredDataPredicate::JsonValue {
path,
operator,
value: json_value,
raw_string,
}
}
DataFormat::Toml => {
let toml_value = Self::build_toml_rhs(value);
if is_ordering_operator(operator) && !Self::is_comparable_toml(&toml_value) {
return Err(DetectError::InvalidValue {
expected: "numeric or date value".to_string(),
found: format!("{toml_value:?}"),
span: value_span.to_source_span(),
src: source.to_string(),
});
}
StructuredDataPredicate::TomlValue {
path,
operator,
value: toml_value,
raw_string,
}
}
};
Ok(Predicate::structured(predicate))
}
fn build_structured_string_predicate(
format: typed::DataFormat,
path: Vec<super::structured_path::PathComponent>,
string_operator: typed::StringOperator,
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<Predicate, DetectError> {
use crate::predicate::StructuredDataPredicate;
use typed::DataFormat;
let matcher = Self::parse_string_value(value, string_operator, value_span, source)?;
let predicate = match format {
DataFormat::Yaml => StructuredDataPredicate::YamlString { path, matcher },
DataFormat::Json => StructuredDataPredicate::JsonString { path, matcher },
DataFormat::Toml => StructuredDataPredicate::TomlString { path, matcher },
};
Ok(Predicate::structured(predicate))
}
fn build_yaml_rhs(value: &RawValue) -> yaml_rust2::Yaml {
let content = value.as_string();
yaml_rust2::YamlLoader::load_from_str(content)
.ok()
.and_then(|mut docs| docs.pop())
.unwrap_or_else(|| yaml_rust2::Yaml::String(content.to_string()))
}
fn build_json_rhs(value: &RawValue) -> serde_json::Value {
let content = value.as_string();
serde_json::from_str(content)
.unwrap_or_else(|_| serde_json::Value::String(content.to_string()))
}
fn build_toml_rhs(value: &RawValue) -> toml::Value {
let content = value.as_string();
let synthetic_doc = format!("_v = {content}");
if let Ok(parsed) = toml::from_str::<toml::Table>(&synthetic_doc) {
if let Some(value) = parsed.get("_v") {
return value.clone();
}
}
if let Ok(value) = toml::from_str::<toml::Value>(content) {
return value;
}
toml::Value::String(content.to_string())
}
fn is_comparable_yaml(value: &yaml_rust2::Yaml) -> bool {
matches!(
value,
yaml_rust2::Yaml::Integer(_) | yaml_rust2::Yaml::Real(_)
)
}
fn is_comparable_json(value: &serde_json::Value) -> bool {
value.is_number()
}
fn is_comparable_toml(value: &toml::Value) -> bool {
matches!(
value,
toml::Value::Integer(_) | toml::Value::Float(_) | toml::Value::Datetime(_)
)
}
fn parse_string_value(
value: &RawValue,
operator: StringOperator,
value_span: pest::Span,
source: &str,
) -> Result<StringMatcher, DetectError> {
let value_str = match value {
RawValue::Quoted(s) | RawValue::Raw(s) => s,
};
if matches!(operator, StringOperator::In) {
return Self::parse_as_set(value_str, value_span, source);
}
match operator {
StringOperator::Equals => Ok(StringMatcher::Equals((*value_str).to_string())),
StringOperator::NotEquals => Ok(StringMatcher::NotEquals((*value_str).to_string())),
StringOperator::Matches => {
StringMatcher::regex(value_str).map_err(|e| DetectError::InvalidValue {
expected: "valid regex pattern".to_string(),
found: format!("{value_str}: {e}"),
span: value_span.to_source_span(),
src: source.to_string(),
})
}
StringOperator::Contains => Ok(StringMatcher::Contains((*value_str).to_string())),
StringOperator::In => unreachable!("Handled above"),
}
}
fn parse_as_set(
value_str: &str,
value_span: pest::Span,
source: &str,
) -> Result<StringMatcher, DetectError> {
let inner = if value_str.starts_with('[') && value_str.ends_with(']') {
&value_str[1..value_str.len() - 1]
} else {
value_str
};
use crate::parser::RawParser;
let items =
RawParser::parse_set_contents(inner).map_err(|e| DetectError::InvalidValue {
expected: "valid set items (e.g., [rs, js] or \"foo, bar\", baz)".to_string(),
found: format!("parse error: {e}"),
span: value_span.to_source_span(),
src: source.to_string(),
})?;
let set: std::collections::HashSet<String> = items.into_iter().collect();
Ok(StringMatcher::In(set))
}
fn build_content_pattern(
value: &RawValue,
operator: StringOperator,
_value_span: pest::Span,
source: &str,
) -> Result<String, DetectError> {
let s = match value {
RawValue::Quoted(s) | RawValue::Raw(s) => s,
};
let pattern = match operator {
StringOperator::Equals => format!("^{}$", regex::escape(s)),
StringOperator::Matches => (*s).to_string(),
StringOperator::Contains => regex::escape(s),
_ => {
return Err(DetectError::Internal {
message: "Invalid operator for contents".to_string(),
src: source.to_string(),
})
}
};
Ok(pattern)
}
fn parse_numeric_value(
value: &RawValue,
selector: &NumericSelector,
value_span: pest::Span,
source: &str,
) -> Result<u64, DetectError> {
let s = match value {
RawValue::Quoted(s) | RawValue::Raw(s) => s,
};
if matches!(selector, NumericSelector::Size) && s.chars().any(char::is_alphabetic) {
parse_size_value(s, value_span, source)
} else {
s.parse().map_err(|_| DetectError::InvalidValue {
expected: "numeric value".to_string(),
found: (*s).to_string(),
span: value_span.to_source_span(),
src: source.to_string(),
})
}
}
fn build_number_matcher(operator: NumericOperator, value: u64) -> NumberMatcher {
match operator {
NumericOperator::Equals => NumberMatcher::Equals(value),
NumericOperator::NotEquals => NumberMatcher::NotEquals(value),
NumericOperator::Greater => NumberMatcher::In(Bound::Left((value + 1)..)),
NumericOperator::GreaterOrEqual => NumberMatcher::In(Bound::Left(value..)),
NumericOperator::Less => NumberMatcher::In(Bound::Right(..value)),
NumericOperator::LessOrEqual => NumberMatcher::In(Bound::Right(..(value + 1))),
}
}
fn parse_temporal_value(
value: &RawValue,
value_span: pest::Span,
source: &str,
) -> Result<chrono::DateTime<chrono::Local>, DetectError> {
let s = match value {
RawValue::Quoted(s) | RawValue::Raw(s) => s,
};
parse_time_value(s).map_err(|e| DetectError::InvalidValue {
expected: "valid time".to_string(),
found: format!("{s}: {e:?}"),
span: value_span.to_source_span(),
src: source.to_string(),
})
}
fn build_time_matcher(
operator: TemporalOperator,
value: chrono::DateTime<chrono::Local>,
) -> TimeMatcher {
match operator {
TemporalOperator::Equals => TimeMatcher::Equals(value),
TemporalOperator::NotEquals => TimeMatcher::NotEquals(value),
TemporalOperator::After => TimeMatcher::After(value),
TemporalOperator::Before => TimeMatcher::Before(value),
TemporalOperator::AfterOrEqual => TimeMatcher::AfterOrEqual(value),
TemporalOperator::BeforeOrEqual => TimeMatcher::BeforeOrEqual(value),
}
}
fn parse_enum_value<E: EnumPredicate>(
value: &RawValue,
operator: EnumOperator,
value_span: pest::Span,
source: &str,
) -> Result<EnumMatcher<E>, DetectError> {
let value_str = match value {
RawValue::Quoted(s) | RawValue::Raw(s) => s,
};
match operator {
EnumOperator::Equals => {
let variant =
E::from_str(value_str).map_err(|_err_msg| DetectError::InvalidValue {
expected: format!("one of: {}", E::all_valid_strings().join(", ")),
found: (*value_str).to_string(),
span: value_span.to_source_span(),
src: source.to_string(),
})?;
Ok(EnumMatcher::Equals(variant))
}
EnumOperator::NotEquals => {
let variant =
E::from_str(value_str).map_err(|_err_msg| DetectError::InvalidValue {
expected: format!("one of: {}", E::all_valid_strings().join(", ")),
found: (*value_str).to_string(),
span: value_span.to_source_span(),
src: source.to_string(),
})?;
Ok(EnumMatcher::NotEquals(variant))
}
EnumOperator::In => {
let string_matcher = Self::parse_as_set(value_str, value_span, source)?;
let items = match string_matcher {
StringMatcher::In(set) => set,
_ => unreachable!("parse_as_set should return StringMatcher::In"),
};
let mut variant_set = std::collections::HashSet::new();
for item in items {
let variant =
E::from_str(&item).map_err(|_err_msg| DetectError::InvalidValue {
expected: format!("one of: {}", E::all_valid_strings().join(", ")),
found: item.clone(),
span: value_span.to_source_span(),
src: source.to_string(),
})?;
variant_set.insert(variant);
}
Ok(EnumMatcher::In(variant_set))
}
}
}
}