tau_engine/
lib.rs

1//! # Tau Engine
2//!
3//! This crate provides a library that tags documents by running and matching rules over them.
4//! The engine makes use of a Pratt parser and a tree solver in order to evaluate the detection
5//! logic of a rule against a document, if the outcome is true the document is considered tagged by
6//! that rule.
7//!
8//!
9//! ## Rules
10//!
11//! A rule is used to tag a document and is made up of three parts:
12//! - `detection`: the logic used to evaluate a document.
13//! - `true positives`: example documents that must evaluate to true for the given detection.
14//! - `true negatives`: example documents that must evaluate to false for the given detection.
15//!
16//! The detection block is made up of a condition, and identifiers. This allows for simple but
17//! expressive rules, below is a brief summary (see [Rules](Rule) for more):
18//!
19//! ### Identifiers
20//!
21//! Identifiers are used to help keep the condition concise and generally contain the core of the
22//! matching logic. They consist of Key/Value pairs which allow for the extraction of data from the
23//! document and the evaluate of its value. It should be noted that mappings are treated as
24//! conjunctions, while sequences are treated as disjunctions.
25//!
26//! Identifiers make use of the following matching logic:
27//! - `foobar`: an exact match of foobar
28//! - `foobar*`: starts with foobar
29//! - `*foobar`: ends with foobar
30//! - `*foobar*`: contains foobar
31//! - `?foobar`: regex foobar
32//!
33//! Any of the above can be made case insensitive with the `i` prefix, for example:
34//! - `ifoobar`
35//! - `ifoobar*`
36//!
37//! Escaping can be achieved with a combination of `'` and `"`.
38//!
39//! ### Condition
40//!
41//! The condition is just a boolean expression and supports the following:
42//! - `and`: logical conjunction
43//! - `or`: logical disjunction
44//! - `==`: equality comparison
45//! - `>`, `>=`, `<`, `<=`: numeric comparisons
46//! - `not`: negate
47//! - `all(i)`: make sequences behave as conjunctions
48//! - `of(i, x)`: ensure a sequence has a minimum number of matches
49//!
50//!
51//! ### Examples
52//!
53//! ```text
54//! detection:
55//!   A:
56//!     foo.bar: foobar
57//!
58//!   condition: A
59//!
60//! true_positives:
61//! - foo:
62//!     bar: foobar
63//!
64//! true_negatives:
65//! - foo:
66//!     bar: foo
67//! ```
68//!
69//! ## Documents
70//!
71//! A document is anything that can provide data to the engine in a meaningful way, usually through Key/Value
72//! pairs, i.e: an event log, json object, yaml file, etc. Implementations are achieved with the
73//! [`Document`](Document) trait.
74//!
75//! ## Solving
76//!
77//! This is an example of how you can tag a document against a provided rule:
78//!
79//! ```
80//! use std::borrow::Cow;
81//!
82//! use tau_engine::{Document, Rule, Value};
83//!
84//! // Define a document.
85//! struct Foo {
86//!     foo: String,
87//! }
88//! impl Document for Foo {
89//!     fn find(&self, key: &str) -> Option<Value<'_>> {
90//!         match key {
91//!             "foo" => Some(Value::String(Cow::Borrowed(&self.foo))),
92//!             _ => None,
93//!         }
94//!     }
95//! }
96//!
97//! // Write a rule.
98//! let rule = r#"
99//! detection:
100//!   A:
101//!     foo: foobar
102//!   condition: A
103//! true_positives:
104//! - foo: foobar
105//! true_negatives:
106//! - foo: foo
107//! "#;
108//!
109//! // Load and validate a rule.
110//! let rule = Rule::from_str(rule).unwrap();
111//! assert_eq!(rule.validate().unwrap(), true);
112//!
113//! // Create a document.
114//! let foo = Foo {
115//!     foo: "foobar".to_owned(),
116//! };
117//!
118//! // Evalute the document with the rule.
119//! assert_eq!(rule.matches(&foo), true);
120//! ```
121//!
122//! ## Features
123//!
124//! The following are a list of features that can be enabled or disabled:
125//! - **core**: Exposes some of Tau Engine's internals.
126//! - **ignore_case**: Force the engine to always be case insensitive, this will ignore
127//!     the `i` prefix and for that reason is not compatible with case sensitive rules.
128//! - **json**: Enable serde json support, this will allow the tau-engine to solve on
129//!     `serde_json::Value`.
130//!
131//!
132//! ### JSON
133//!
134//! When JSON support is enabled for the tau-engine, the result is a solver that can now reason over
135//! any document that can be deserialized into `serde_json::Value`.
136//!
137//! ```ignore
138//! # use serde_json::json;
139//! use tau_engine::{Document, Rule};
140//!
141//! // Write a rule.
142//! let rule = r#"
143//! detection:
144//!   A:
145//!     foo: foobar
146//!   condition: A
147//! true_positives:
148//! - foo: foobar
149//! true_negatives:
150//! - foo: foo
151//! "#;
152//!
153//! // Load and validate a rule.
154//! let rule = Rule::from_str(rule).unwrap();
155//! assert_eq!(rule.validate().unwrap(), true);
156//!
157//! // Create a document.
158//! let foo = json!({
159//!     "foo": "foobar",
160//! });
161//!
162//! // Evalute the document with the rule.
163//! assert_eq!(rule.matches(&foo), true);
164//! ```
165
166#![cfg_attr(feature = "benchmarks", feature(test))]
167
168#[cfg_attr(test, macro_use)]
169#[cfg(feature = "benchmarks")]
170extern crate test;
171
172pub use self::document::Document;
173pub use self::error::{Error, Kind as ErrorKind};
174pub use self::optimiser::Optimisations;
175pub use self::rule::Rule;
176pub use self::solver::solve;
177pub use self::value::{Array, AsValue, Object, Value};
178
179pub(crate) use error::Result;
180
181mod document;
182mod error;
183mod identifier;
184#[cfg(feature = "json")]
185mod json;
186mod optimiser;
187mod parser;
188mod rule;
189mod solver;
190mod tokeniser;
191mod value;
192mod yaml;
193
194#[cfg(feature = "core")]
195/// Exposes some of Tau Engine's internals.
196pub mod core {
197    /// Exposes some of Tau Engine's internal optimisations so that Expressions can be built by hand.
198    pub mod optimiser {
199        pub use crate::optimiser::*;
200    }
201    /// Exposes some of Tau Engine's internal parsing so that Expressions can be built by hand.
202    pub mod parser {
203        pub use crate::identifier::*;
204        pub use crate::parser::*;
205        pub use crate::tokeniser::*;
206    }
207    pub use crate::rule::Detection;
208
209    use std::collections::HashMap;
210
211    use crate::document::Document;
212    use crate::parser::Expression;
213    use crate::solver::SolverResult;
214
215    lazy_static::lazy_static! {
216        static ref IDENTIFIERS: HashMap<String, Expression> = HashMap::new();
217    }
218
219    /// Evaluates a `Document` with the provided expression.
220    ///
221    /// # Panics
222    ///
223    /// This method will panic if an invalid expression is provided
224    pub fn solve(expression: &Expression, document: &dyn Document) -> bool {
225        match super::solver::solve_expression(expression, &IDENTIFIERS, document) {
226            SolverResult::True => true,
227            SolverResult::False | SolverResult::Missing => false,
228        }
229    }
230
231    /// Evaluates a `Document` with the provided expression, and identifiers.
232    ///
233    /// # Panics
234    ///
235    /// This method will panic if an invalid expression is provided
236    pub fn solve_expression(
237        expression: &Expression,
238        identifiers: &HashMap<String, Expression>,
239        document: &dyn Document,
240    ) -> bool {
241        match super::solver::solve_expression(expression, identifiers, document) {
242            SolverResult::True => true,
243            SolverResult::False | SolverResult::Missing => false,
244        }
245    }
246}