From c9cd0cedeaff815bf30e838db7c8d7e72c2d06c2 Mon Sep 17 00:00:00 2001 From: jlanson Date: Thu, 12 Sep 2024 12:01:51 -0400 Subject: [PATCH] chore: Added initial plugin system docs. This commit introduces the initial documentation for the plugin system. There is still more to do to improve the docs, especially to document the query API for those who want to write plugins _without_ using an SDK, but this is at least a good starting point. Signed-off-by: Andrew Lilley Brinker Signed-off-by: Julian Lanson --- site/content/docs/guide/_index.md | 1 + .../docs/guide/plugin/for-developers.md | 277 ++++++++++++++++++ site/content/docs/guide/plugin/for-users.md | 178 +++++++++++ site/content/docs/guide/plugin/index.md | 30 ++ site/content/docs/guide/plugin/policy-expr.md | 199 +++++++++++++ 5 files changed, 685 insertions(+) create mode 100644 site/content/docs/guide/plugin/for-developers.md create mode 100644 site/content/docs/guide/plugin/for-users.md create mode 100644 site/content/docs/guide/plugin/index.md create mode 100644 site/content/docs/guide/plugin/policy-expr.md diff --git a/site/content/docs/guide/_index.md b/site/content/docs/guide/_index.md index d99752ab..bc8fe37b 100644 --- a/site/content/docs/guide/_index.md +++ b/site/content/docs/guide/_index.md @@ -22,6 +22,7 @@ by opening an issue on our [issue tracker](https://github.com/mitre/hipcheck/iss - [Why Hipcheck?](@/docs/guide/why.md) - [Key Concepts](@/docs/guide/concepts/index.md) - [How to use Hipcheck](@/docs/guide/how-to-use.md) +- [Plugins](@/docs/guide/plugin/index.md) - [Analyses](@/docs/guide/analyses.md) - [Configuration](@/docs/guide/configuration.md) - [Debugging](@/docs/guide/debugging.md) diff --git a/site/content/docs/guide/plugin/for-developers.md b/site/content/docs/guide/plugin/for-developers.md new file mode 100644 index 00000000..215100fb --- /dev/null +++ b/site/content/docs/guide/plugin/for-developers.md @@ -0,0 +1,277 @@ +--- +title: Developing Plugins +--- + +# Developing Plugins + +## Creating a New Plugin + +A Hipcheck plugin is a separate executable artifact that Hipcheck downloads, +starts, and communicates with over a gRPC protocol to request data. A plugin's +executable artifact is the binary, set of executable program files, Docker +container, or other artifact which can be run as a command line interface +program through a singular "start command" defined in the plugin's +manifest file. + +The benefit of the executable-and-gRPC plugin design is that plugins can be +written in any of the many languages that have a gRPC library. One drawback is +that plugin authors have to at least be aware of the target platform(s) they +compile their plugin for, and more likely will need to support a handful of +target platforms. This can be simplified through the optional use of container +files as the plugin executable artifact. + +Once a plugin author writes their plugin, compiles, packages, and +distribute it, Hipcheck users can specify the plugin in their policy file for +Hipcheck to fetch and use in analysis. + +## Plugin CLI + +Hipcheck requires that plugins provide a CLI which accepts a `--port ` +argument, enabling Hipcheck to centrally manage the ports plugins are listening +on. The port provided via this CLI argument must be the port the running plugin +process listens on for gRPC requests, and on which it returns responses. + +Once started, the plugin should continue running, listening for gRPC requests +from Hipcheck, until shut down by the Hipcheck process. + +## The Rust SDK + +The Hipcheck team maintains a library crate `hipcheck-sdk` which provides +developers with tools for greatly simplifying plugin development in Rust. This +section will describe at a high level how a plugin author can use the SDK, but +for more detailed information please see the [API docs](https://docs.rs/hipcheck-sdk). + +The first step is to add `hipcheck-sdk` as a dependency to your Rust project. +If you plan to use the macro approach described below, please add the `"macros"` +feature. + +Next, the SDK provides `prelude` module which authors can import to get +access to all the essential types it exposes. If you want to manage your imports +to avoid potential type name collisions you may do so, otherwise simply write +`use hipcheck_sdk::prelude::*`. + +### Defining Query Endpoints + +The Hipcheck plugin communication protocol allows a plugin to expose multiple +named query endpoints that can be called by Hipcheck core or other plugins. +Developers may choose to use the `query` [attribute macro](#using-proc-macros) +to mark functions as endpoints, or [manually implement](#manual-implementation) +the `Query` trait. + +#### Using Proc Macros + +The SDK offers an attribute proc macro `query` for marking `async` functions +as query endpoints. As a reminder, you must have enabled the `"macros"` feature +on your `hipcheck_sdk` dependency to use the SDK macros. + +To mark an `async fn` as a query endpoint, The function signature must be of the +form + +```rust +async fn [FUNC_NAME](engine: &mut PluginEngine, input: [INPUT_TYPE]) -> Result<[OUTPUT_TYPE]> +``` + +Where: +- `PluginEngine` and `Result` are from `hipcheck_sdk::prelude` +- `[INPUT_TYPE]` and `[OUTPUT_TYPE]` are Rust types that implement + `serde::Serialize` and `schemars::JsonSchema`. These traits are implemented + already for many standard types. + +To tag this function as a query endpoint, simply (@Todo - how to import?) and +apply the `#[query]` attribute to the function. + +Importantly, this attribute will create a struct with Pascal-case version of +your function name (e.g. `foo_bar()` -> `struct FooBar`). You will need this +struct name to implement `Plugin::queries()` [below](#the-plugin-trait). + +For a description of how the `PluginEngine` is used to query other plugins, see +[below](#querying-other-plugins). + +#### Manual Implementation + +For each query endpoint you want to define, you must create a struct that +implements the `Query` trait from the `prelude`. `Query` is declared as such: + +```rust +#[tonic::async_trait] +trait Query: Send { + fn input_schema(&self) -> JsonSchema; + + fn output_schema(&self) -> JsonSchema; + + async fn run(&self, engine: &mut PluginEngine, input: JsonValue) -> Result; +} +``` + +The `input_schema()` and `output_schema()` function calls allow you to declare +the signature of the query (what type of JSON value it takes and returns, +respectively) as a `schemars::schema::Schema` object. Since schemas are +themselves JSON objects, we recommend you store these as separate `.json` +files that you reference in `include_str!()` macro calls to copy the contents +into your binary at compile time as a `&'static str`. For example: + +```rust +static MY_QUERY_KEY_SCHEMA: &str = include_str!("../schema/my_query_key_schema.json"); +static MY_QUERY_OUTPUT_SCHEMA: &str = include_str!("../schema/my_query_output_schema.json"); +``` + +#### The `Query::run()` Function + +The `run()` function is the place where your actual query logic wil go. Let's +look at it in more detail. It's an `async` function since the underlying SDK +may execute the `run()` functions of different `impl Query` structs in parallel +as queries from Hipcheck come in, and `async` allows for simple and efficient +concurrency. The function takes a (mutable) reference to a `PluginEngine` +struct. We will discuss `PluginEngine` below, but for now just know that +this struct exposes an `async query()` function that allows your +query endpoint to in turn request information from other plugins. With that complexity +out of the way, all that's left is a simple function that takes a JSON object as +input and returns a JSON object of its own, wrapped in a `Result` to allow for failure. + +The first step of your `run()` function implementation will likely be to parse the JSON +value in to primitive typed data that you can manipulate. This could involve +deserializing to a struct or `match`ing on the `JsonValue` variants manually. +If the value of `input` does not match what your query endpoint expects in its +input schema, you can return an `Err(Error::UnexpectedPluginQueryInputFormat)`, +where `Error` is the `enum` type from the SDK `prelude`. For more information on the +different error variants, see the [API docs](https://docs.rs/hipcheck-sdk). + +If your query endpoint can complete with just the input data, then you can +simply perform the calculations, serialize the output type to a JSON value, and +return it wrapped in `Ok`. However, many plugins will rely on additional data from other +plugins. In the next subsection we will describe how to do that in more detail. + +#### Querying Other Plugins + +As mentioned above, the `run()` function receives a handle to a `PluginEngine` instance +which exposes the following generic function: + +```rust +async fn query(&mut self, target: T, input: V) -> Result +where + T: TryInto>, + V: Into; + +struct QueryTarget { + publisher: String, + plugin: String, + query: Option, +} +``` + +At a high-level, this function simply takes a value that identifies the target +plugin and query endpoint, and passes the `input` value to give to that query +endpoint's `run()` function, then returns the forwarded result of that +operation. + +The "target query endpoint" identifier is anything that implements +`TryInto`. The SDK implements this trait for `String`, so you can +pass a string of the format `publisher/plugin[/query]` where the bracketed +substring is optional. Each plugin is allowed to declare an unnamed "default" +query; by omitting the `/query` from your target string, you are targetting the +default query endpoint for the plugin. If you don't want to pass a `String` to +`target`, you can always instantiate a `QueryTarget` yourself and pass that. + +### The `Plugin` Trait + +At this point, you should have one struct that implements `Query` for each +query endpoint you want your plugin to expose. Now, you need to implement the +`Plugin` trait which will tie everything together and expose some additional +information about your plugin to Hipcheck. The `Plugin` trait is as follows: + +```rust +trait Plugin: Send + Sync + 'static { + + const PUBLISHER: &'static str; + + const NAME: &'static str; + + fn set_config(&self, config: JsonValue) -> StdResult<(), ConfigError>; + + fn queries(&self) -> impl Iterator; + + fn explain_default_query(&self) -> Result>; + + fn default_policy_expr(&self) -> Result; +} + +pub struct NamedQuery { + name: &'static str, + inner: DynQuery, +} + +type DynQuery = Box; +``` + +The associated strings `PUBLISHER` and `NAME` allow you to declare the publisher +and name of the plugin, respectively. + +The `set_config()` function allows Hipcheck users to pass a set of `String` +key-value pairs to your plugin as a configuration step before any endpoints are +queried. On success, simply return `Ok(())`. If the contents of the `config` +JSON value do not match what you expect, return a `ConfigError` enum variant to +describe why. + +Your implementation of `queries()` is what actually binds each of your `impl +Query` structs to the plugin. As briefly mentioned above, query endpoints have +names, with up to one query allowed be unnamed (`name` is an empty string) and +thus designated as the "default" query for the plugin. Due to limitations of +Rust, the SDK must introduce a `NamedQuery` struct to bind a name to the query +structs. Your implementation of `queries()` will, for each `impl Query` struct, +instantiate that struct, then use that to create a `NamedQuery` instance with +the appropriate `name` field. Finally, return an iterator of all the +`NamedQuery` instances. + +Plugins are not required to declare a default query endpoint, but plugins +designed for "top-level" analysis (namely those that are not explicitly +designed to provide data to other plugins) are highly encouraged to do so. +Furthermore, it is highly suggested that the default query endpoint is designed +to take the `Target` schema (@Todo - link to it), as this is the object type +passed to the designated query endpoints of all "top-level" plugins declared in +the Hipcheck policy file. + +If you do define a default query endpoint, `Plugin::explain_default_query()` +should return a `Ok(Some(_))` containing a string that explains the default +query. + +Lastly, if yours is an analysis plugin, users will need to write [policy +expressions](policy-expr) to interpret your plugin's output. In many cases, it +may be appropriate to define a default policy expression associated with your +default query endpoint so that users do not have to write one themselves. This +is the purpose of `default_policy_expr()`. This function will only ever be +called by the SDK after `set_config()` has completed, so you can also take +configuration parameters to influence the value returned by +`default_policy_expr().` For example, if the output of your plugin will +generally will be compared against an integer/float threshold, you can return a +`(lte $ )` where `` may be a value received from +`set_config()`. + +### Running Your Plugin + +At this point you now have a struct that implements `Plugin`. The last thing to +do is write some boilerplate code for starting the plugin server. The Rust SDK +exposes a `PluginServer` type as follows: + +```rust +pub struct PluginServer

{ + plugin: Arc

, +} + +impl PluginServer

{ + pub fn register(plugin: P) -> PluginServer

{ + ... + } + + pub async fn listen(self, port: u16) -> Result<()> { + ... + } +} +``` + +So, once you have parsed the port from the CLI `--port ` flag that +Hipcheck passes to your plugin, you simply pass an instance of your `impl +Plugin` struct to `PluginServer::register()`, then call `listen().await` +on the returned `PluginServer` instance. This function will not return until +the gRPC channel with Hipcheck core is closed. + +And that's all there is to it! Happy plugin development! diff --git a/site/content/docs/guide/plugin/for-users.md b/site/content/docs/guide/plugin/for-users.md new file mode 100644 index 00000000..b99ecb0b --- /dev/null +++ b/site/content/docs/guide/plugin/for-users.md @@ -0,0 +1,178 @@ +--- +title: Using Plugins +--- + +# Using Plugins + +When running Hipcheck, users provide a "policy file", which is a +[KDL](https://kdl.dev/)-language configuration file that describes everything +about how to perform the analysis. It specifies which top-level plugins to +execute, how to configure them, how to interpret their output using [policy +expressions](#policy-expressions), and how to weight the pass/fail result of +each analysis when calculating a final score. In this way, Hipcheck provides +users extensive flexibility in both defining risk and the set of measurements +used to evaluate it. + +Let's now look at an example policy file to examine its parts more closely: + +``` +plugins { + plugin "mitre/activity" version="0.1.0" + plugin "mitre/binary" version="0.1.0" + plugin "mitre/fuzz" version="0.1.0" + plugin "mitre/review" version="0.1.0" + plugin "mitre/typo" version="0.1.0" + plugin "mitre/affiliation" version="0.1.0" + plugin "mitre/entropy" version="0.1.0" + plugin "mitre/churn" version="0.1.0" +} + +analyze { + investigate policy="(gt 0.5 $)" + investigate-if-fail "mitre/typo" "mitre/binary" + + category "practices" { + analysis "mitre/activity" policy="(lte $ 52)" weight=3 + analysis "mitre/binary" policy="(eq 0 (count $))" { + binary-file "./config/Binary.toml" + } + analysis "mitre/fuzz" policy="(eq #t $)" + analysis "mitre/review" policy="(lte $ 0.05)" + } + + category "attacks" { + analysis "mitre/typo" policy="(eq 0 (count $))" { + typo-file "./config/Typos.toml" + } + + category "commit" { + analysis "mitre/affiliation" policy="(eq 0 (count $))" { + orgs-file "./config/Orgs.toml" + } + + analysis "mitre/entropy" policy="(eq 0 (count (filter (gt 8.0) $)))" { + langs-file "./config/Langs.toml" + } + analysis "mitre/churn" policy="(lte (divz (count (filter (gt 3) $)) (count $)) 0.02)" { + langs-file "./config/Langs.toml" + } + } + } +} +``` + +As you can see, the file has two main sections: a `plugins` section, and an +`analyze` section. We can explore each of these in turn. + +## The `plugin` Section + +This section defines the plugins that will be used to run the analyses +described in the file. These plugins are defined with a name, version, and an +optional manifest field (not shown in the example above) which provides a link +to the plugin's download manifest. For an example of the manifest field, see +[@Todo - link to For-Developers section]. In the future, when a Hipcheck plugin +registry is established, the manifest field will become optional. In the +immediate term it will be practically required. + +At runtime, each plugin will be downloaded by Hipcheck, its size and checksum +verified, and the plugin contents decompressed and unarchived to produce the +plugin executable artifacts which will be stored in a local plugin cache. +Hipcheck will do the same recursively for all plugins. + +In the future Hipcheck will likely add some form of dependency resolution to +minimize duplication of shared dependencies, similar to what exists in other +more mature package ecosystems. For now the details of this mechanism are left +unspecified. + +## The `analysis` Section + +Whereas the `plugin` section is simply a flat list telling Hipcheck which +plugins to resolve and start up, the `analysis` section composes those +analyses into a score tree. + +The score tree is comprised of a series of nested "categories" that eventually +terminate in analysis leaf nodes. Whether an analysis appears in this tree +determines whether Hipcheck actually queries it, so although you can list +plugins in the `plugins` section that do not appear in the `analysis` section, +they will not be run. On the contrary, specifying a plugin in the `analysis` +section that is not in the `plugins` section is an error. + +### The Score Tree + +Each category and analysis node in the tree has an optional `weight` field, +which is an integer that dictates how much or little that node's final score of +0 or 1 (pass and fail, respectively) should count compared to its neighbors at +the same depth of the tree. If left unspecified, the weight of a node defaults +to `1`. + +Once all the weights are normalized, an individual analysis's contribution to +Hipcheck's final score for a target can be calculated by multiplying its own +weight and the weight of all its parent categories up to the top of the +`analysis` section. As each analysis produces a pass/fail result, the +corresponding `0` or `1` is multiplied with that analysis's contribution +percentage and added to the overall score. + +Users may also run `hc scoring --policy ` to see a version of the +score tree with normalized weights for a given policy file. + +See [the Complete Guide to Hipcheck's section on scoring][hipcheck_scoring] +for more information on how Hipcheck's scoring mechanism works. + +### Configuration + +A plugin author may choose to provide a set of parameters so that users may +configure the plugin's behavior. These can be set inside the corresponding +brackets for each analysis node. For example, see the `binary-file` +configuration inside `mitre/binary`. The provided key-value pairs are passed to +their respective plugins at startup. + +### Policy Expressions + +Hipcheck plugins return data or measurements on data in JSON format, such that +other plugins could be written to consume and process their output. However, +the scoring system of Hipcheck relies on turning the output of each top-level +plugin into a pass/fail evalution. In order to facilitate transforming plugin +data into a boolean value, Hipcheck provides "policy expressions", which are a +small expression language. See [here](policy-expr) for a reference on the policy +expression language. + +Users can define the pass/fail policy for an analysis node in the score tree +with a `policy` key. As described in more detail in the policy expression +reference, a policy used in analysis ought to take one or more JSON pointers +(operands that start with `$`) as entrypoints for part or all of the JSON object +returned by the analysis to be fed into the expression. Additionally, +all policies should ultimately return a boolean value, with `true` +meaning that the analysis passed. + +Instead of users always having to define their own policy expressions, plugins +may define a default pass/fail policy that may depend on configuration items +that the plugin accepts in the `analysis` section of the policy file. If a +plugin's default policy is acceptable, the user does not need to provide a +`policy` key when placing that plugin into a scoring tree in their policy file. +If the default policy is configurable, the user can configure it by setting the +relevant configuration item for the plugin. Note that any user-provided policy +will always override the default policy. + +Finally, if the policy expression language is not powerful enough to express a +desired policy for a given analysis, users may define their own plugin which +takes the analysis output, performs some more complicated computations on it, +and use that as their input for the score tree. + +### Final Scoring and Investigation + +Once the policies for each top-level analysis has been evaluated, the score +tree produces the final score. Hipcheck now looks at the `investigate` field of +the policy file. + +This node accepts a `policy` key-value pair, which takes a policy expression as +a string. The input to the policy expression is the numeric output of the +scoring tree reduction, therefore a floating pointer number between 0.0 and 1.0 +inclusive. This defines the policy used to determine if the "risk score" +produced by the score tree should result in Hipcheck flagging the target of +analysis for further investigation. + +The `investigate-if-fail` node enables users of Hipcheck to additionally mark +specific analyses such that if those analyses produce a failed result, the +overall target of analysis is marked for further investigation regardless of +the risk score. In this case, the risk score is still calculated and all other +analyses are still run. diff --git a/site/content/docs/guide/plugin/index.md b/site/content/docs/guide/plugin/index.md new file mode 100644 index 00000000..0091a5ab --- /dev/null +++ b/site/content/docs/guide/plugin/index.md @@ -0,0 +1,30 @@ +--- +title: Plugins +--- + +# Introduction + +After Hipcheck resolves a user's desired analysis target, it moves to the main +analysis phase. This involves Hipcheck passing the target description to a set of +user-specified, top-level analyses which measure some aspect of the target and +produce a pass/fail result. These tertiary data sources often rely on +lower-level measurements about the target to produce their results. + +To facilitate the integration of third-party data sources and analysis +techniques into Hipcheck's analysis phase, data sources are split out into +plugins that Hipcheck can query. In order to produce their result, plugins can +in turn query information from other plugins, which Hipcheck performs on their +behalf. + +The remainder of this section of the documentation is split in two. The [first +section](for-users) is aimed at users. It covers how they can specify analysis +plugins and control the use of their data in producing a pass/fail determination +for a given target. The [second section](for-developers) is aimed at plugin +developers, and explains how to create and distribute your own plugin. + + +## Table of Contents + +- [Using Plugins](@/docs/guide/plugin/for-users.md) +- [Developing Plugins](@/docs/guide/plugin/for-developers.md) +- [Policy Expressions](@/docs/guide/plugin/policy-expr.md) diff --git a/site/content/docs/guide/plugin/policy-expr.md b/site/content/docs/guide/plugin/policy-expr.md new file mode 100644 index 00000000..b5107e52 --- /dev/null +++ b/site/content/docs/guide/plugin/policy-expr.md @@ -0,0 +1,199 @@ +--- +title: Policy Expressions +--- + +# Policy Expressions + +"Policy expressions" are a small expression language in Hipcheck that allows the +JSON data output by analysis plugins to be reduced to a boolean pass/fail +determination used for scoring. Policy expressions are mostly found in policy +files, as the `policy` node for analyses or the `investigate` node for the +entire analysis. Plugin authors may also want to be familiar with policy +expressions, as one of the gRPC calls they may implement returns a default +policy expression for the analysis implemented by the plugin. + +The policy expression language is limited. It does not permit user-defined +functions, assignment to variables, or the retention of any state. Any policy +expression supplied in a policy file which does not result in a boolean output +will produce an error. + +If the policy expression language is insufficient to represent a desired policy +on the output of a given plugin, users are encouraged to write their own plugin +which takes as input that plugin's output and performs the desired manipulation. + +### Primitives + +Policy expressions have the following primitive types: + +| Type | Description | Example | +| ------- | ----------- | ------- | +| integer | A signed 64-bit integer | `-5`, `360` | +| float | A 64-bit float, NaN is disallowed | `2.001` | +| boolean | A true or false value | `#t`, `#f` | +| identifier | A function name or placeholder value in a lambda function | `add` | +| datetime | A datetime value with timezone information. [More info](#datetime) | `2024-09-17T09:00-05` | +| span | a (uniform) duration of time. [More info](#span) | `P5wT1h30m` | + +#### Datetime + +Datetimes use the string format from the `jiff` [crate][jiff], which is a +lightly modified version of ISO8601. A datetime must include a date in the +format `--

`. An optional time in the format `T:[MM]:[SS]` will +be accepted after the date. Decimal fractions of hours and minutes are not +allowed; use smaller time units instead (e.g. `T10:30` instead of `T10.5`). +Decimal fractions of seconds are allowed. The timezone is always internally +resolved to UTC, but you can specify the datetime's original timezone as an an +offset from UTC by including `+{HH}:[MM]` or `-{HH}:[MM]`. + +#### Span + +Spans represent a duration of time using the `jiff` [crate] `Span` type. Policy +expression spans can include weeks, days, hours, minutes, and seconds. They can +include optional decimal fractions of the smallest unit of time (hours, minutes, +or seconds) used (e.g. `1.5h`). Spans are prefixed with the letter "P" followed +by optional date units. Time units are separated from date units with the letter +"T". All date and time units are specified in single case-agnostic letter +abbreviations after the number. For example, a span of one week, one day, one +hour, one minute, and one-and-a-tenth seconds would be `P1w1dT1h1m1.1s`. + +Although `jiff` day and week spans can be non-uniform depending on timezone +information, policy expression spans always use uniform 24-hour days and 7-day +weeks. + +### Expressions + +#### Arrays + +Arrays are vectors of homogeneously-type primitives. This means that all +elements of an array must be the same type, and that type must be a primitive +(integer, float, boolean, datetime, span). Arrays cannot contain expression +types like other arrays, functions, or lambdas. Square brackets represent the +array boundaries and elements are separated by whitespace. Examples: + + ``` + [1 1 2 3 5 8] + [0.152, -12.482, 0.09] + [#t #t #f #t #f] + ``` + +#### Function + +Functions are Lisp-like expressions, meaning that they are bounded by +parentheses, and the function name comes first followed by whitespace-delimited +operands. Examples: + +``` +(add 2 2) // Add two integers +(min [-3.1, -6.6, 7.8]) // Get the minimum of an array of floats +``` + +##### Primitive Function Reference + +The standard environment for evaluating policy expressions contains the +following functions: + +| Function | Name | Operand Types | Behavior | +| ---------| ---- | -------- | -------- | +| `(gt )`| greater than | non-identifier primitives | evaluate `A > B` | +| `(lt )`| less than | non-identifier primitives | evaluate `A < B` | +| `(gte )`| greater than or equal | non-identifier primitives | evaluate `A >= B` | +| `(lte )`| less than or equal | non-identifier primitives | evaluate `A <= B` | +| `(eq )` | equal | non-identifier primitives | evaluate `A == B` | +| `(neq )` | not equal | non-identifier primitives | evaluate `A != B` | +| `(add )` | add | integers, floats, bools, spans, or (datetime + span) | evaluate `A + B` | +| `(sub )` | subtract | integers, floats, bools, spans, or (datetime + span) | evaluate `A - B` | +| `(divz )` | divide or zero | integers, floats | if `B == 0` return `B`, else evaluate `A / B` | +| `(duration )` | duration | datetimes | evaluate `A - B` to produce a `span` | +| `(and )` | and | bools | evaluate `A & B` | +| `(or )` | or | bools | evaluate `A | B` | +| `(not )` | not | bool | evaluate `!A` | +| `(max )` | max | array of integers, floats, datetimes, spans | find the largest value in `A` | +| `(min )` | min | array of integers, floats, datetimes, spans | find the smallest value in `A` | +| `(avg )` | average | array of integers, floats | calculate the average of `A` | +| `(median )` | median | array of integers, floats | calculate the median of `A` | +| `(count )` | count | array of non-identifier primitives | return the number of elements in `A` | + +#### Lambdas + +A lambda is an incomplete function invocation that is missing an operand. In the +standard policy expression environment, there are multiple functions that take +as operands a lambda and an array, and then evaluate the lambda +for each element of the array. For example, `(lte 8.0)` is an incomplete `lte` +function call. When we do the following: + +``` +(foreach (lte 8.0) [0.3, 9.4, 5.1]) +``` + +It will apply the lambda to each element of the float array, resulting in an +array of three booleans that correspond to whether the element at that index in +the float array was greater than `8.0` ("greater than" because the first operand +of `lte` is `8.0`, not the array element). + +##### Lambda Function Reference + +Each function takes a lambda as the first operand and an array as the second. +The type of the array and the type of the missing operand in the lambda must +match. + +| Function | Name | Behavior | +| `(all )` | all | return `#t` if `A` returned `#t` for all elements of `B` | +| `(nall )` | not all | return `#t` if `A` returned `#f` for at least one element of `B` | +| `(some )` | some | return `#t` if `A` returned `#t` for at least one element of `B` | +| `(none )` | none | return `#t` if `A` returned `#f` for all elements of `B` | +| `(filter )` | filter | return the subset of elements of `B` for which `A` returned `#t` | +| `(foreach )` | for each | apply `A` to each element of `B`, producing a same-size array | + +Some examples: + +``` +(filter (gt 10) [3 11 0]) // Return array of elements less than or equal to 10 +(foreach (not) [#t #f]) // Return an array of inverted booleans +(some (gt 10) [3 11 0]) // Return true if any element is less than or equal to 10 +``` + +#### JSON Pointers + +As a reminder, the purpose of the policy expression language is to allow us to +manipulate data from plugins and produce a boolean pass/fail determination. Each +policy expression in a Hipcheck policy file needs to contain one or more +locations at which to "receive" part or all of the JSON data from a plugin +(otherwise the policy would be independent of the data and could be evaluated +immediately). This is where JSON pointers come in. + +A JSON pointer is a replacement for an expression or function operand in a +policy expression. They are prefixed with a `$`. If the JSON value is an object, +fields can be recursively accessed by appending `/`. For example, +to extract the float at field "baz" below, we would use `$/bar/baz`: + +``` +{ + "foo": [1, 2, 3, 4], + "bar": { + "bee": false, + "baz": 0.01 + } +} +``` + +Examples: + +|Plugin Output | Goal | Policy Expression +|----|----|----| +| A boolean value | Forward the value as the pass/fail determination | `$` | +| A JSON array | Pass if all elements less than 10 | `(all (gt 10) $)` | +| An object containing a boolean field "fail" | Invert the field | `(not $/fail)` | + +As mentioned above, a policy expression can contain multiple JSON +pointers. As an example, this can be useful if you want to calculate the +percentage of elements of an array that pass a filter: + +``` +(lt (divz (count (filter (gt 10) $) (count $))) 0.5) +``` + +This policy expression will check that less than half of the elements in `$` are +less than 10. It uses JSON pointers twice, once to get the total element count, +again to count the number of elements filtered by the lambda. + +[jiff]: https://crates.io/crates/jiff