online-dsl-forge 0.3.3

A bounded parser and runtime for in-memory DSL expressions.
Documentation
use std::collections::BTreeSet;

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SecurityProfileId {
  GenericSafe,
  GenericTransform,
  WafRequest,
  WafResponse,
  WafStream,
  MitigationField,
  Custom(String),
}

#[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Phase {
  Generic,
  Request,
  Response,
  Stream,
}

#[derive(Debug, Clone, Copy, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BodyTarget {
  Request,
  Response,
  Stream,
}

#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BodyAccess {
  #[default]
  None,
  SizeOnly,
  PrefixBytes,
}

impl BodyAccess {
  pub fn merge(self, other: Self) -> Self {
    self.max(other)
  }

  pub fn allows(self, needed: Self) -> bool {
    self >= needed
  }

  pub fn reads_payload(self) -> bool {
    matches!(self, Self::PrefixBytes)
  }
}

#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum RegexPolicy {
  Forbid,
  LiteralOnlyPrecompiled,
  DynamicWithBudget,
}

#[derive(Debug, Clone, Copy, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Determinism {
  Required,
  BestEffort,
}

#[derive(Debug, Clone, Copy, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct BodyNeedSummary {
  pub request: BodyAccess,
  pub response: BodyAccess,
  pub stream: BodyAccess,
}

impl BodyNeedSummary {
  pub fn none() -> Self {
    Self::default()
  }

  pub fn all(access: BodyAccess) -> Self {
    Self {
      request: access,
      response: access,
      stream: access,
    }
  }

  pub fn merge(self, other: Self) -> Self {
    Self {
      request: self.request.merge(other.request),
      response: self.response.merge(other.response),
      stream: self.stream.merge(other.stream),
    }
  }

  pub fn merge_target(&mut self, target: BodyTarget, access: BodyAccess) {
    match target {
      BodyTarget::Request => self.request = self.request.merge(access),
      BodyTarget::Response => self.response = self.response.merge(access),
      BodyTarget::Stream => self.stream = self.stream.merge(access),
    }
  }

  pub fn reads_payload(self) -> bool {
    self.request.reads_payload() || self.response.reads_payload() || self.stream.reads_payload()
  }

  pub fn allows(self, needed: Self) -> bool {
    self.request.allows(needed.request)
      && self.response.allows(needed.response)
      && self.stream.allows(needed.stream)
  }
}

#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
pub struct SecurityProfile {
  pub id: SecurityProfileId,
  pub allowed_phases: BTreeSet<Phase>,
  pub max_ast_nodes: usize,
  pub max_call_depth: usize,
  pub default_regex_policy: RegexPolicy,
  pub max_cost_units: u64,
  pub determinism: Determinism,
  pub fail_closed: bool,
  #[serde(default)]
  pub body_access_limit: Option<BodyNeedSummary>,
}

impl SecurityProfile {
  pub fn generic_safe() -> Self {
    Self {
      id: SecurityProfileId::GenericSafe,
      allowed_phases: BTreeSet::from([Phase::Generic]),
      max_ast_nodes: 4096,
      max_call_depth: 64,
      default_regex_policy: RegexPolicy::DynamicWithBudget,
      max_cost_units: 100_000,
      determinism: Determinism::Required,
      fail_closed: true,
      body_access_limit: None,
    }
  }

  pub fn generic_transform() -> Self {
    Self {
      id: SecurityProfileId::GenericTransform,
      max_ast_nodes: 8192,
      max_call_depth: 128,
      max_cost_units: 250_000,
      ..Self::generic_safe()
    }
  }

  pub fn waf_request() -> Self {
    Self::waf(SecurityProfileId::WafRequest, Phase::Request)
  }

  pub fn waf_response() -> Self {
    Self::waf(SecurityProfileId::WafResponse, Phase::Response)
  }

  pub fn waf_stream() -> Self {
    Self::waf(SecurityProfileId::WafStream, Phase::Stream)
  }

  pub fn oxirule_waf_request() -> Self {
    Self::oxirule_waf(SecurityProfileId::WafRequest, Phase::Request)
  }

  pub fn oxirule_waf_response() -> Self {
    Self::oxirule_waf(SecurityProfileId::WafResponse, Phase::Response)
  }

  pub fn oxirule_waf_stream() -> Self {
    Self::oxirule_waf(SecurityProfileId::WafStream, Phase::Stream)
  }

  pub fn mitigation_field(phase: Phase) -> Self {
    let allowed_phases = BTreeSet::from([phase]);
    Self {
      id: SecurityProfileId::MitigationField,
      allowed_phases,
      max_ast_nodes: 2048,
      max_call_depth: 32,
      default_regex_policy: RegexPolicy::LiteralOnlyPrecompiled,
      max_cost_units: 50_000,
      determinism: Determinism::Required,
      fail_closed: true,
      body_access_limit: None,
    }
  }

  pub fn with_regex_policy(mut self, policy: RegexPolicy) -> Self {
    self.default_regex_policy = policy;
    self
  }

  pub fn with_body_access_limit(mut self, limit: Option<BodyNeedSummary>) -> Self {
    self.body_access_limit = limit;
    self
  }

  pub fn deny_body_access(self) -> Self {
    self.with_body_access_limit(Some(BodyNeedSummary::none()))
  }

  pub fn allow_body_access(self) -> Self {
    self.with_body_access_limit(None)
  }

  pub fn active_phase(&self) -> Option<Phase> {
    if self.allowed_phases.len() == 1 {
      self.allowed_phases.iter().next().copied()
    } else {
      None
    }
  }

  fn waf(id: SecurityProfileId, phase: Phase) -> Self {
    Self {
      id,
      allowed_phases: BTreeSet::from([phase]),
      max_ast_nodes: 4096,
      max_call_depth: 64,
      default_regex_policy: RegexPolicy::LiteralOnlyPrecompiled,
      max_cost_units: 100_000,
      determinism: Determinism::Required,
      fail_closed: true,
      body_access_limit: None,
    }
  }

  fn oxirule_waf(id: SecurityProfileId, phase: Phase) -> Self {
    Self {
      default_regex_policy: RegexPolicy::DynamicWithBudget,
      ..Self::waf(id, phase)
    }
  }
}

impl Default for SecurityProfile {
  fn default() -> Self {
    Self::generic_safe()
  }
}