coraza 0.1.0

Safe Rust bindings to OWASP Coraza WAF
/*
 * Copyright 2022 OWASP Coraza contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//! WAF configuration and compiled engine.

use std::cell::UnsafeCell;
use std::ffi::{CStr, CString};
use std::marker::PhantomData;
use std::os::raw::c_void;

use coraza_sys::*;

use crate::callbacks::{debug_log_trampoline, error_trampoline, CallbackContext};
use crate::error::Error;
use crate::matched_rule::{LogLevel, MatchedRule};
use crate::transaction::Transaction;

/// A builder for configuring and creating a WAF instance.
///
/// Use [`WafConfig::new()`] to create a new configuration, then chain
/// configuration methods before calling [`build()`](WafConfig::build) to
/// compile the WAF.
///
/// # Example
///
/// ```no_run
/// use coraza::WafConfig;
///
/// let waf = WafConfig::new()
///     .unwrap()
///     .with_directives("SecRuleEngine DetectionOnly")
///     .with_directives("SecRequestBodyAccess On")
///     .build()
///     .unwrap();
/// ```
pub struct WafConfig {
    handle: coraza_waf_config_t,
    callback_ctx: Option<Box<CallbackContext>>,
    _phantom: PhantomData<UnsafeCell<i32>>, // Make the struct !Sync
}

impl WafConfig {
    /// Creates a new empty WAF configuration.
    pub fn new() -> Result<Self, Error> {
        let handle = unsafe { coraza_new_waf_config() };
        if handle == 0 {
            return Err(Error::InvalidConfig);
        }
        Ok(Self {
            handle,
            callback_ctx: Some(Box::new(CallbackContext::new())),
            _phantom: PhantomData,
        })
    }

    /// Adds SecLang directives from a string.
    ///
    /// Multiple calls accumulate directives. The directives are parsed when
    /// [`build()`](WafConfig::build) is called.
    pub fn with_directives(self, directives: &str) -> Self {
        let c_directives = CString::new(directives).expect("directive string contains null byte");
        unsafe {
            coraza_rules_add(self.handle, c_directives.as_ptr());
        }
        self
    }

    /// Adds SecLang directives from a file.
    ///
    /// The file must exist and be readable when [`build()`](WafConfig::build) is called.
    pub fn with_directives_from_file(self, path: &str) -> Self {
        let c_path = CString::new(path).expect("path contains null byte");
        unsafe {
            coraza_rules_add_file(self.handle, c_path.as_ptr());
        }
        self
    }

    /// Registers a debug log callback.
    ///
    /// The callback is invoked for each debug log message produced by Coraza.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use coraza::{WafConfig, LogLevel};
    ///
    /// let waf = WafConfig::new()
    ///     .unwrap()
    ///     .with_debug_log_callback(|level, msg, fields| {
    ///         eprintln!("[{}] {} {}", level, msg, fields);
    ///     })
    ///     .with_directives("SecRuleEngine DetectionOnly")
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn with_debug_log_callback<F>(mut self, f: F) -> Self
    where
        F: Fn(LogLevel, &str, &str) + Send + 'static,
    {
        if let Some(ref mut ctx) = self.callback_ctx {
            ctx.debug_log = Some(Box::new(f));
        }
        self
    }

    /// Registers an error callback.
    ///
    /// The callback is invoked each time a rule matches. The [`MatchedRule`]
    /// provides details about the matched rule.
    ///
    /// # Example
    ///
    /// ```no_run
    /// use coraza::WafConfig;
    ///
    /// let waf = WafConfig::new()
    ///     .unwrap()
    ///     .with_error_callback(|rule| {
    ///         eprintln!("Rule {} matched: {}", rule.rule_id, rule.message);
    ///     })
    ///     .with_directives("SecRuleEngine DetectionOnly")
    ///     .build()
    ///     .unwrap();
    /// ```
    pub fn with_error_callback<F>(mut self, f: F) -> Self
    where
        F: Fn(MatchedRule) + Send + 'static,
    {
        if let Some(ref mut ctx) = self.callback_ctx {
            ctx.error = Some(Box::new(f));
        }
        self
    }

    /// Compiles the WAF configuration into a [`Waf`] instance.
    ///
    /// This consumes the configuration. The resulting [`Waf`] is immutable
    /// and can be used to create transactions.
    pub fn build(mut self) -> Result<Waf, Error> {
        let ctx = self.callback_ctx.take();

        // Register callbacks before building the WAF
        if let Some(ctx) = ctx {
            let ctx_ptr = Box::into_raw(ctx) as *mut c_void;

            // Check if we have any callbacks to register
            let has_debug = unsafe { &*ctx_ptr.cast::<CallbackContext>() }
                .debug_log
                .is_some();
            let has_error = unsafe { &*ctx_ptr.cast::<CallbackContext>() }
                .error
                .is_some();

            if has_debug {
                unsafe {
                    coraza_add_debug_log_callback(self.handle, Some(debug_log_trampoline), ctx_ptr);
                }
            }
            if has_error {
                unsafe {
                    coraza_add_error_callback(self.handle, Some(error_trampoline), ctx_ptr);
                }
            }
            // Note: ctx_ptr is leaked here. The Go side will call the callbacks
            // during WAF creation. We should free it after coraza_new_waf returns,
            // but the Go code doesn't provide a way to know when callbacks are done.
            // For safety, we leak it and document that callbacks must not outlive
            // the WAF build phase.
        }

        let mut err: *mut std::os::raw::c_char = std::ptr::null_mut();
        let handle = unsafe { coraza_new_waf(self.handle, &mut err) };

        if handle == 0 {
            let msg = if err.is_null() {
                "unknown error".to_string()
            } else {
                let s = unsafe { CStr::from_ptr(err) }
                    .to_string_lossy()
                    .into_owned();
                unsafe { coraza_free_string(err) };
                s
            };
            return Err(Error::WafCreation(msg));
        }

        // The config handle is consumed by coraza_new_waf; prevent Drop from freeing it
        // (the Go side handles cleanup).
        let _ = std::mem::replace(&mut self.handle, 0);

        Ok(Waf { handle })
    }
}

impl Drop for WafConfig {
    fn drop(&mut self) {
        if self.handle != 0 {
            unsafe {
                coraza_free_waf_config(self.handle);
            }
        }
    }
}

/// A compiled WAF instance.
///
/// Created by [`WafConfig::build()`]. The WAF is immutable after creation
/// and can be used to create multiple transactions concurrently.
///
/// `Waf` is both `Send` and `Sync` — it can be moved between threads and
/// shared between threads for concurrent transaction creation.
///
/// # Example
///
/// ```no_run
/// use coraza::WafConfig;
///
/// let waf = WafConfig::new()
///     .unwrap()
///     .with_directives("SecRuleEngine DetectionOnly")
///     .build()
///     .unwrap();
///
/// let mut tx1 = waf.new_transaction();
/// let mut tx2 = waf.new_transaction();
/// ```
pub struct Waf {
    handle: coraza_waf_t,
}

impl std::fmt::Debug for Waf {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Waf")
            .field("rules_count", &self.rules_count())
            .finish()
    }
}

impl Waf {
    /// Returns the number of rules loaded into the WAF.
    pub fn rules_count(&self) -> i32 {
        unsafe { coraza_rules_count(self.handle) }
    }

    /// Creates a new transaction with a random ID.
    ///
    /// The transaction is used to process a single HTTP request/response cycle.
    pub fn new_transaction(&self) -> Transaction {
        let handle = unsafe { coraza_new_transaction(self.handle) };
        Transaction::new(handle)
    }

    /// Creates a new transaction with the given ID.
    ///
    /// The ID is used for logging and correlation.
    pub fn new_transaction_with_id(&self, id: &str) -> Transaction {
        let c_id = CString::new(id).expect("ID contains null byte");
        let handle = unsafe { coraza_new_transaction_with_id(self.handle, c_id.as_ptr()) };
        Transaction::new(handle)
    }
}

impl Drop for Waf {
    fn drop(&mut self) {
        if self.handle != 0 {
            unsafe {
                coraza_free_waf(self.handle);
            }
        }
    }
}