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:

TypeDescriptionExample
integerA signed 64-bit integer-5, 360
floatA 64-bit float, NaN is disallowed2.001
booleanA true or false value#t, #f
identifierA function name or placeholder value in a lambda functionadd
datetimeA datetime value with timezone information. More info2024-09-17T09:00-05
spana (uniform) duration of time. More infoP5wT1h30m

Datetime

Datetimes use the string format from the jiff crate, which is a lightly modified version of ISO8601. A datetime must include a date in the format <YYYY>-<MM>-<DD>. An optional time in the format T<HH>:[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:

FunctionNameOperand TypesBehavior
(gt <A> <B>)greater thannon-identifier primitivesevaluate A > B
(lt <A> <B>)less thannon-identifier primitivesevaluate A < B
(gte <A> <B>)greater than or equalnon-identifier primitivesevaluate A >= B
(lte <A> <B>)less than or equalnon-identifier primitivesevaluate A <= B
(eq <A> <B>)equalnon-identifier primitivesevaluate A == B
(neq <A> <B>)not equalnon-identifier primitivesevaluate A != B
(add <A> <B>)addintegers, floats, bools, spans, or (datetime + span)evaluate A + B
(sub <A> <B>)subtractintegers, floats, bools, spans, or (datetime + span)evaluate A - B
(divz <A> <B>)divide or zerointegers, floatsif B == 0 return B, else evaluate A / B
(duration <A> <B>)durationdatetimesevaluate A - B to produce a span
(and <A> <B>)andboolsevaluate A & B
(or <A> <B>)orboolsevaluate `A
(not <A>)notboolevaluate !A
(max <A>)maxarray of integers, floats, datetimes, spansfind the largest value in A
(min <A>)minarray of integers, floats, datetimes, spansfind the smallest value in A
(avg <A>)averagearray of integers, floatscalculate the average of A
(median <A>)medianarray of integers, floatscalculate the median of A
(count <A>)countarray of non-identifier primitivesreturn 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 less than 8.0.

Note that for this to work, the array element is inserted as the first operand in a binary operand function.

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 <A> <B>) | all | return #t if A returned #t for all elements of B | | (nall <A> <B>) | not all | return #t if A returned #f for at least one element of B | | (some <A> <B>) | some | return #t if A returned #t for at least one element of B | | (none <A> <B>) | none | return #t if A returned #f for all elements of B | | (filter <A> <B>) | filter | return the subset of elements of B for which A returned #t | | (foreach <A> <B>) | 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 /<FIELD_NAME>. 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 OutputGoalPolicy Expression
A boolean valueForward the value as the pass/fail determination$
A JSON arrayPass 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 (lt 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.