tellaro-query-language 1.3.8

A flexible, human-friendly query language for searching and filtering structured data
Documentation
//! Mutator system for field transformations in TQL.
//!
//! Mutators transform field values during query evaluation, supporting operations like
//! string manipulation, encoding/decoding, network operations, and enrichment lookups.

pub mod dns;
pub mod encoding;
pub mod geoip;
pub mod list;
pub mod network;
pub mod string_mutators;

use crate::error::{Result, TqlError};
use serde_json::Value as JsonValue;
use std::collections::HashMap;

/// Base trait for all mutators.
///
/// ## Parameter convention
///
/// Mutator parameters arrive as a `HashMap<String, JsonValue>`. Named arguments use
/// their declared key (e.g., `"delimiter"`, `"find"`). Positional arguments are stored
/// under their zero-based index as a string key: `"0"`, `"1"`, etc.
///
/// Use [`get_param`] to look up a parameter by named key first, falling back to the
/// positional index. This lets users write either `| split(delimiter=',')` or
/// `| split(',')` with the same result.
pub trait Mutator: Send + Sync {
    /// Apply the mutator to a value
    ///
    /// # Arguments
    ///
    /// * `field_name` - The name of the field being mutated
    /// * `record` - The full record (for enrichment mutators)
    /// * `value` - The value to transform
    ///
    /// # Returns
    ///
    /// The transformed value
    fn apply(&self, field_name: &str, record: &JsonValue, value: &JsonValue) -> Result<JsonValue>;

    /// Get the mutator name
    fn name(&self) -> &str;

    /// Check if this mutator is an enrichment mutator
    ///
    /// Enrichment mutators add data to the record (e.g., nslookup adds DNS data,
    /// geoip adds geo location data). They return a special structure with
    /// `_tql_enrichment` that the evaluator/post-processor uses to enrich records.
    ///
    /// # Returns
    ///
    /// `true` if this is an enrichment mutator, `false` otherwise
    fn is_enrichment(&self) -> bool {
        false
    }
}

/// Mutator parameters type
pub type MutatorParams = HashMap<String, JsonValue>;

/// Apply a sequence of mutators to a value
///
/// # Arguments
///
/// * `value` - The original value
/// * `mutators` - A list of mutator instances
/// * `field_name` - The name of the field being processed
/// * `record` - The entire record (for enrichment mutators)
///
/// # Returns
///
/// The final mutated value
pub fn apply_mutators(
    value: &JsonValue,
    mutators: &[Box<dyn Mutator>],
    field_name: &str,
    record: &JsonValue,
) -> Result<JsonValue> {
    let mut result = value.clone();

    for mutator in mutators {
        result = mutator.apply(field_name, record, &result)?;
    }

    Ok(result)
}

/// Look up a mutator parameter by named key first, then fall back to positional index.
///
/// Mutator parameters can be supplied as named args (`split(delimiter=',')`) or as
/// positional args (`split(',')`). The parser stores positional args under string keys
/// "0", "1", etc. (see `build_mutator_params` in `evaluator.rs`). This helper
/// encapsulates the named-then-positional lookup convention so individual mutators
/// don't have to duplicate the pattern.
///
/// # Arguments
///
/// * `params` - The parameter map
/// * `named_key` - The named parameter key to try first (e.g., "delimiter")
/// * `positional_index` - The positional index to try as fallback (e.g., 0 → key "0")
///
/// # Returns
///
/// A reference to the `JsonValue` if found by either key, or `None`
pub fn get_param<'a>(
    params: &'a HashMap<String, JsonValue>,
    named_key: &str,
    positional_index: usize,
) -> Option<&'a JsonValue> {
    params
        .get(named_key)
        .or_else(|| params.get(&positional_index.to_string()))
}

/// Create a mutator instance from a name and parameters
///
/// # Arguments
///
/// * `name` - The mutator name (case-insensitive)
/// * `params` - Optional parameters as key-value pairs
///
/// # Returns
///
/// A boxed mutator instance
///
/// # Errors
///
/// Returns an error if the mutator is not recognized or parameters are invalid
pub fn create_mutator(name: &str, params: Option<MutatorParams>) -> Result<Box<dyn Mutator>> {
    let params = params.unwrap_or_default();
    let name_lower = name.to_lowercase();

    match name_lower.as_str() {
        // String mutators
        "lowercase" => Ok(Box::new(string_mutators::LowercaseMutator::new(params))),
        "uppercase" => Ok(Box::new(string_mutators::UppercaseMutator::new(params))),
        "trim" => Ok(Box::new(string_mutators::TrimMutator::new(params))),
        "split" => Ok(Box::new(string_mutators::SplitMutator::new(params))),
        "length" => Ok(Box::new(string_mutators::LengthMutator::new(params))),
        "replace" => Ok(Box::new(string_mutators::ReplaceMutator::new(params))),

        // Encoding mutators
        "b64encode" => Ok(Box::new(encoding::Base64EncodeMutator::new(params))),
        "b64decode" => Ok(Box::new(encoding::Base64DecodeMutator::new(params))),
        "urldecode" => Ok(Box::new(encoding::URLDecodeMutator::new(params))),
        "hexencode" => Ok(Box::new(encoding::HexEncodeMutator::new(params))),
        "hexdecode" => Ok(Box::new(encoding::HexDecodeMutator::new(params))),
        "md5" => Ok(Box::new(encoding::MD5Mutator::new(params))),
        "sha256" => Ok(Box::new(encoding::SHA256Mutator::new(params))),

        // Network/security mutators
        "refang" => Ok(Box::new(network::RefangMutator::new(params))),
        "defang" => Ok(Box::new(network::DefangMutator::new(params))),
        "is_private" => Ok(Box::new(network::IsPrivateMutator::new(params))),
        "is_global" => Ok(Box::new(network::IsGlobalMutator::new(params))),
        "is_multicast" => Ok(Box::new(network::IsMulticastMutator::new(params))),
        "is_loopback" => Ok(Box::new(network::IsLoopbackMutator::new(params))),
        "is_link_local" => Ok(Box::new(network::IsLinkLocalMutator::new(params))),

        // DNS mutators
        "nslookup" => Ok(Box::new(dns::NSLookupMutator::new(params))),

        // GeoIP mutators ("geo" is an alias used by Python and JS implementations)
        "geoip" | "geoip_lookup" | "geo" => Ok(Box::new(geoip::GeoIPMutator::new(params))),

        // List mutators
        "any" => Ok(Box::new(list::AnyMutator::new(params))),
        "all" => Ok(Box::new(list::AllMutator::new(params))),
        "avg" => Ok(Box::new(list::AvgMutator::new(params))),
        "average" => Ok(Box::new(list::AverageMutator::new(params))),
        "sum" => Ok(Box::new(list::SumMutator::new(params))),
        "max" => Ok(Box::new(list::MaxMutator::new(params))),
        "min" => Ok(Box::new(list::MinMutator::new(params))),

        // Future mutators will be added here
        _ => Err(TqlError::MutatorError(format!("Unknown mutator: {}", name))),
    }
}