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