Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Evaluate JSON Pointers in Policy Expressions #418

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions hipcheck/src/analysis/score.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,8 @@ pub fn score_results(_phase: &SpinnerPhase, db: &dyn ScoringProvider) -> Result<
// Determine if analysis passed by evaluating policy expr
let passed = {
if let Ok(output) = &response {
Executor::std()
.run(analysis.1.as_str(), &output.value)
Executor::std(output.value.clone())
.run(analysis.1.as_str())
.map_err(|e| hc_error!("{}", e))?
} else {
false
Expand Down
11 changes: 8 additions & 3 deletions hipcheck/src/policy_exprs/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use crate::policy_exprs::{eval, Error, Expr, Ident, Primitive, Result, F64};
use itertools::Itertools as _;
use serde_json::Value;
use std::{cmp::Ordering, collections::HashMap, ops::Not as _};
use Expr::*;
use Primitive::*;
Expand All @@ -13,6 +14,8 @@ pub struct Env<'parent> {

/// Possible pointer to parent, for lexical scope.
parent: Option<&'parent Env<'parent>>,

pub context: Value,
}

/// A binding in the environment.
Expand All @@ -30,16 +33,17 @@ type Op = fn(&Env, &[Expr]) -> Result<Expr>;

impl<'parent> Env<'parent> {
/// Create an empty environment.
fn empty() -> Self {
fn empty(context: Value) -> Self {
Env {
bindings: HashMap::new(),
parent: None,
context,
}
}

/// Create the standard environment.
pub fn std() -> Self {
let mut env = Env::empty();
pub fn std(context: Value) -> Self {
let mut env = Env::empty(context);

// Comparison functions.
env.add_fn("gt", gt);
Expand Down Expand Up @@ -87,6 +91,7 @@ impl<'parent> Env<'parent> {
Env {
bindings: HashMap::new(),
parent: Some(self),
context: self.context.clone(),
}
}

Expand Down
4 changes: 2 additions & 2 deletions hipcheck/src/policy_exprs/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ pub struct Ident(pub String);
/// A late-binding for a JSON pointer
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JsonPointer {
pointer: String,
value: Option<serde_json::Value>,
pub pointer: String,
pub value: Option<serde_json::Value>,
}

/// A non-NaN 64-bit floating point number.
Expand Down
12 changes: 8 additions & 4 deletions hipcheck/src/policy_exprs/json_pointer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ fn process_pointer(pointer: &str, context: &Value) -> Result<String> {
}

/// Wrap serde_json's `Value::pointer` method to provide better error handling.
fn lookup_json_pointer<'val>(pointer: &str, context: &'val Value) -> Result<&'val Value> {
pub fn lookup_json_pointer<'val>(pointer: &str, context: &'val Value) -> Result<&'val Value> {
// serde_json's JSON Pointer implementation does not distinguish between
// syntax errors and lookup errors, so we check the syntax ourselves.
// The only syntax error that serde_json currently recognizes is that a
Expand All @@ -101,11 +101,15 @@ fn lookup_json_pointer<'val>(pointer: &str, context: &'val Value) -> Result<&'va
/// Attempt to interpret a JSON Value as a Policy Expression.
/// `pointer` and `context` are only passed in to provide more context in the
/// case of errors.
fn json_to_policy_expr(val: &Value, pointer: &str, context: &Value) -> Result<Expr> {
pub fn json_to_policy_expr(val: &Value, pointer: &str, context: &Value) -> Result<Expr> {
match val {
Value::Number(n) => {
let not_nan = NotNan::new(n.as_f64().unwrap()).unwrap();
Ok(Expr::Primitive(Primitive::Float(not_nan)))
if n.is_i64() {
Ok(Expr::Primitive(Primitive::Int(n.as_i64().unwrap())))
} else {
let not_nan = NotNan::new(n.as_f64().unwrap()).unwrap();
Ok(Expr::Primitive(Primitive::Float(not_nan)))
}
}
Value::Bool(b) => Ok(Expr::Primitive(Primitive::Bool(*b))),
Value::Array(a) => {
Expand Down
80 changes: 59 additions & 21 deletions hipcheck/src/policy_exprs/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub use crate::policy_exprs::{
token::LexingError,
};
use env::Binding;
use expr::JsonPointer;
pub use expr::{parse, Primitive};
use json_pointer::process_json_pointers;
use serde_json::Value;
Expand All @@ -29,22 +30,23 @@ pub struct Executor {

impl Executor {
/// Create an `Executor` with the standard set of functions defined.
pub fn std() -> Self {
Executor { env: Env::std() }
pub fn std(context: Value) -> Self {
Executor {
env: Env::std(context),
}
}

/// Run a `deke` program.
pub fn run(&self, raw_program: &str, context: &Value) -> Result<bool> {
match self.parse_and_eval(raw_program, context)? {
pub fn run(&self, raw_program: &str) -> Result<bool> {
match self.parse_and_eval(raw_program)? {
Expr::Primitive(Primitive::Bool(b)) => Ok(b),
result => Err(Error::DidNotReturnBool(result)),
}
}

/// Run a `deke` program, but don't try to convert the result to a `bool`.
pub fn parse_and_eval(&self, raw_program: &str, context: &Value) -> Result<Expr> {
let processed_program = process_json_pointers(raw_program, context)?;
let program = parse(&processed_program)?;
pub fn parse_and_eval(&self, raw_program: &str) -> Result<Expr> {
let program = parse(raw_program)?;
let expr = eval(&self.env, &program)?;
Ok(expr)
}
Expand All @@ -67,7 +69,11 @@ pub(crate) fn eval(env: &Env, program: &Expr) -> Result<Expr> {
}
}
Expr::Lambda(_, body) => Ok((**body).clone()),
Expr::JsonPointer(_) => unreachable!(),
Expr::JsonPointer(JsonPointer { pointer, .. }) => {
let val = json_pointer::lookup_json_pointer(pointer, &env.context)?;
let expr = json_pointer::json_to_policy_expr(val, pointer, &env.context)?;
Ok(expr)
}
};

log::debug!("input: {program:?}, output: {output:?}");
Expand All @@ -78,37 +84,38 @@ pub(crate) fn eval(env: &Env, program: &Expr) -> Result<Expr> {
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use test_log::test;

#[test]
fn run_bool() {
let program = "#t";
let context = Value::Null;
let is_true = Executor::std().run(program, &context).unwrap();
let is_true = Executor::std(context).run(program).unwrap();
assert!(is_true);
}

#[test]
fn run_basic() {
let program = "(eq (add 1 2) 3)";
let context = Value::Null;
let is_true = Executor::std().run(program, &context).unwrap();
let is_true = Executor::std(context).run(program).unwrap();
assert!(is_true);
}

#[test]
fn eval_basic() {
let program = "(add 1 2)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Int(3)));
}

#[test]
fn eval_divz_int_zero() {
let program = "(divz 1 0)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Primitive(Primitive::Float(F64::new(0.0).unwrap()))
Expand All @@ -119,7 +126,7 @@ mod tests {
fn eval_divz_int() {
let program = "(divz 1 2)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Primitive(Primitive::Float(F64::new(0.5).unwrap()))
Expand All @@ -130,7 +137,7 @@ mod tests {
fn eval_divz_float() {
let program = "(divz 1.0 2.0)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Primitive(Primitive::Float(F64::new(0.5).unwrap()))
Expand All @@ -141,7 +148,7 @@ mod tests {
fn eval_divz_float_zero() {
let program = "(divz 1.0 0.0)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Primitive(Primitive::Float(F64::new(0.0).unwrap()))
Expand All @@ -152,31 +159,31 @@ mod tests {
fn eval_bools() {
let program = "(neq 1 2)";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Bool(true)));
}

#[test]
fn eval_array() {
let program = "(max [1 4 6 10 2 3 0])";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Int(10)));
}

#[test]
fn run_array() {
let program = "(eq 7 (count [1 4 6 10 2 3 0]))";
let context = Value::Null;
let is_true = Executor::std().run(program, &context).unwrap();
let is_true = Executor::std(context).run(program).unwrap();
assert!(is_true);
}

#[test]
fn eval_higher_order_func() {
let program = "(eq 3 (count (filter (gt 8.0) [1.0 2.0 10.0 20.0 30.0])))";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Bool(true)));
}

Expand All @@ -185,15 +192,15 @@ mod tests {
let program =
"(eq 3 (count (filter (gt 8.0) (foreach (sub 1.0) [1.0 2.0 10.0 20.0 30.0]))))";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Bool(true)));
}

#[test]
fn eval_basic_filter() {
let program = "(filter (eq 0) [1 0 1 0 0 1 2])";
let context = Value::Null;
let result = Executor::std().parse_and_eval(program, &context).unwrap();
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Array(vec![
Expand All @@ -203,4 +210,35 @@ mod tests {
])
);
}

#[test]
fn eval_basic_json_pointer_root_bool() {
let program = "$";
let context = Value::Bool(true);
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Bool(true)));
}

#[test]
fn eval_basic_json_pointer_i64() {
let program = "$/answer";
let context = json!({
"answer": 42,
});
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(result, Expr::Primitive(Primitive::Int(42)));
}

#[test]
fn eval_basic_json_pointer_f64() {
let program = "$/number";
let context = json!({
"number": 7.3,
});
let result = Executor::std(context).parse_and_eval(program).unwrap();
assert_eq!(
result,
Expr::Primitive(Primitive::Float(F64::new(7.3).unwrap()))
);
}
}
4 changes: 2 additions & 2 deletions hipcheck/src/report/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -687,8 +687,8 @@ impl RecommendationKind {
fn is(risk_score: RiskScore, risk_policy: RiskPolicy) -> Result<RecommendationKind> {
let value = serde_json::to_value(risk_score.0).unwrap();
Ok(
if Executor::std()
.run(&risk_policy.0, &value)
if Executor::std(value)
.run(&risk_policy.0)
.context("investigate policy expression execution failed")?
{
RecommendationKind::Pass
Expand Down