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

[Enhancement]: Adding support for calling functions inline #400

Merged
merged 6 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 34 additions & 0 deletions guard/resources/validate/functions/output/failing_complex_rule.out
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
template.yaml Status = FAIL
FAILED rules
failing_complex_rule.guard/PARAMETERIZED_RULE_WITH_FUNCTION_CALL_IN_PARAMS FAIL
---
Evaluating data template.yaml against rules failing_complex_rule.guard
Number of non-compliant resources 1
Resource = newServer {
Type = AWS::New::Service
Rule = PARAMETERIZED_RULE_WITH_FUNCTION_CALL_IN_PARAMS {
ALL {
Rule = compare_result_of_regex_replace {
ALL {
Check = %expected EQUALS %replaced {
ComparisonError {
Error = Check was not compliant as property [/Resources/newServer/Properties/Arn[L:9,C:11]] was not present in [(resolved, Path=/Resources/newServer/Properties/Arn[L:9,C:11] Value="aws/123456789012/us-west-2/newservice-Table/extracted")]
}
PropertyPath = /Resources/newServer/Properties/Arn[L:9,C:11]
Operator = EQUAL
Value = "aws/123456789012/us-west-2/newservice-Table/extracted"
ComparedWith = ["aws/123456789012/us-west-2/newservice-Table/extracted"]
Code:
7. "Principal": "*",
8. "Actions": ["s3*", "ec2*"]
9. }
10. Arn: arn:aws:newservice:us-west-2:123456789012:Table/extracted
11. Encoded: This%20string%20will%20be%20URL%20encoded
12. Collection:

}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Resource = newServer {
ALL {
Check = %res EQUALS "a,b" {
ComparisonError {
Message = Violation: The joined value does not match the expected result
Error = Check was not compliant as property value [Path=/Resources/newServer/Collection/0[L:12,C:8] Value="a,b,c"] not equal to value [Path=[L:0,C:0] Value="a,b"].
PropertyPath = /Resources/newServer/Collection/0[L:12,C:8]
Operator = EQUAL
Expand All @@ -26,6 +25,24 @@ Resource = newServer {

}
}
Check = a,b EQUALS join(%collection, ",") {
ComparisonError {
Message = Violation: The joined value does not match the expected result
Error = Check was not compliant as property [/Resources/newServer/Collection/0[L:12,C:8]] was not present in [(resolved, Path=/Resources/newServer/Collection/0[L:12,C:8] Value="a,b,c")]
}
PropertyPath = /Resources/newServer/Collection/0[L:12,C:8]
Operator = EQUAL
Value = "a,b,c"
ComparedWith = ["a,b,c"]
Code:
10. Arn: arn:aws:newservice:us-west-2:123456789012:Table/extracted
11. Encoded: This%20string%20will%20be%20URL%20encoded
12. Collection:
13. - a
14. - b
15. - c

}
}
}
}
22 changes: 22 additions & 0 deletions guard/resources/validate/functions/rules/complex_rules.guard
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
rule compare_number_of_buckets(expected) {
%expected == 5
}

let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ]

rule COMBINED_FUNCTION_AND_PARAMETERIZED_RULES when %buckets !empty {
let number_of_buckets = count(%buckets)
compare_number_of_buckets(%number_of_buckets)
joshfried-aws marked this conversation as resolved.
Show resolved Hide resolved
}

rule compare_result_of_regex_replace(replaced, expected) {
%replaced == %expected
}

let template = Resources.*[ Type == 'AWS::New::Service']

rule PARAMETERIZED_RULE_WITH_FUNCTION_CALL_IN_PARAMS when %template exists {
let arn = %template.Properties.Arn
let expected = "aws/123456789012/us-west-2/newservice-Table/extracted"
compare_result_of_regex_replace(regex_replace(%arn, "^arn:(\w+):(\w+):([\w0-9-]+):(\d+):(.+)$", "${1}/${4}/${3}/${2}-${5}"), %expected)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
rule compare_result_of_regex_replace(replaced, expected) {
%expected == %replaced
}

let template = Resources.*[ Type == 'AWS::New::Service']

rule PARAMETERIZED_RULE_WITH_FUNCTION_CALL_IN_PARAMS when %template exists {
let arn = %template.Properties.Arn
compare_result_of_regex_replace(regex_replace(%arn, "^arn:(\w+):(\w+):([\w0-9-]+):(\d+):(.+)$", "${1}/${4}/${3}/${2}-${5}"), "random_str")
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@ rule TEST_COLLECTION when %template !empty {

let res = join(%collection, ",")
%res == "a,b"

"a,b" == join(%collection, ",")
joshfried-aws marked this conversation as resolved.
Show resolved Hide resolved
<< Violation: The joined value does not match the expected result >>
}
}

3 changes: 3 additions & 0 deletions guard/resources/validate/functions/rules/json_parse.guard
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ rule SOME_RULE when %template !empty {

let res = json_parse(%policy)

%expected == json_parse(%policy)

%res !empty
%res == %expected

Expand All @@ -23,3 +25,4 @@ rule SOME_RULE when %template !empty {
}
}


28 changes: 23 additions & 5 deletions guard/src/rules/eval.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use super::exprs::*;
use super::*;
use crate::rules::eval::operators::Comparator;
use crate::rules::eval_context::{block_scope, ValueScope};
use crate::rules::eval_context::{block_scope, resolve_function, ValueScope};
use crate::rules::path_value::compare_eq;
use std::collections::HashMap;

Expand Down Expand Up @@ -1112,9 +1112,23 @@ pub(in crate::rules) fn eval_guard_access_clause<'value, 'loc: 'value>(
return Err(e);
}
},
LetValue::FunctionCall(_) => todo!(),
LetValue::FunctionCall(FunctionExpr {
parameters, name, ..
}) => match resolve_function(name, parameters, resolver) {
Ok(result) => (result, false),
Err(e) => {
resolver.end_record(
&blk_context,
RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::FAIL,
at_least_one_matches: !all,
message: Some(format!("Error {e} when handling clause, bailing")),
}),
)?;
return Err(e);
}
},
},

None => {
resolver.end_record(
&blk_context,
Expand Down Expand Up @@ -1587,8 +1601,12 @@ pub(in crate::rules) fn eval_parameterized_rule_call<'value, 'loc: 'value>(
resolver.query(&query.query)?,
);
}
// TODO: when we add inline function support
LetValue::FunctionCall(_) => unimplemented!(),
LetValue::FunctionCall(FunctionExpr {
parameters, name, ..
}) => {
let result = resolve_function(name, parameters, resolver)?;
resolved_parameters.insert((param_rule.parameter_names[idx]).as_str(), result);
}
}
}
let mut eval = ResolvedParameterContext {
Expand Down
97 changes: 42 additions & 55 deletions guard/src/rules/eval_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -840,7 +840,9 @@ fn query_retrieval_with_converter<'value, 'loc: 'value>(
vec![QueryResult::Literal(Rc::new(path_value.clone()))]
}

LetValue::FunctionCall(_) => todo!(),
LetValue::FunctionCall(FunctionExpr {
parameters, name, ..
}) => resolve_function(name, parameters, resolver)?,
};

let lhs = map
Expand Down Expand Up @@ -1122,33 +1124,7 @@ impl<'value, 'loc: 'value> EvalContext<'value, 'loc> for RootScope<'value, 'loc>
parameters, name, ..
}) = self.scope.function_expressions.get(variable_name)
{
validate_number_of_params(name, parameters.len())?;
let args = parameters.iter().try_fold(
vec![],
|mut args, param| -> Result<Vec<Vec<QueryResult>>> {
match param {
LetValue::Value(value) => {
args.push(vec![QueryResult::Literal(Rc::new(value.clone()))])
}
LetValue::AccessClause(clause) => {
let resolved_query = self.query(&clause.query)?;
args.push(resolved_query);
}
// TODO: when we add inline function call support
_ => unimplemented!(),
}

Ok(args)
},
)?;

let result = try_handle_function_call(name, &args)?
.into_iter()
.flatten()
.map(Rc::new)
.map(QueryResult::Resolved)
.collect::<Vec<_>>();

let result = resolve_function(name, parameters, self)?;
self.scope
.resolved_variables
.insert(variable_name, result.clone());
Expand Down Expand Up @@ -1405,33 +1381,7 @@ impl<'value, 'loc: 'value, 'eval> EvalContext<'value, 'loc> for BlockScope<'valu
parameters, name, ..
}) = self.scope.function_expressions.get(variable_name)
{
validate_number_of_params(name, parameters.len())?;
let args = parameters.iter().try_fold(
vec![],
|mut args, param| -> Result<Vec<Vec<QueryResult>>> {
match param {
LetValue::Value(value) => {
args.push(vec![QueryResult::Literal(Rc::new(value.clone()))])
}
LetValue::AccessClause(clause) => {
let resolved_query = self.query(&clause.query)?;
args.push(resolved_query);
}
// TODO: when we add inline function call support
_ => unimplemented!(),
}

Ok(args)
},
)?;

let result = try_handle_function_call(name, &args)?
.into_iter()
.flatten()
.map(Rc::new)
.map(QueryResult::Resolved)
.collect::<Vec<_>>();

let result = resolve_function(name, parameters, self)?;
self.scope
.resolved_variables
.insert(variable_name, result.clone());
Expand Down Expand Up @@ -2264,6 +2214,43 @@ pub(crate) fn simplifed_json_from_root<'value>(
})
}

pub(crate) fn resolve_function<'value, 'eval, 'loc: 'value>(
name: &str,
parameters: &'value Vec<LetValue<'loc>>,
resolver: &'eval mut dyn EvalContext<'value, 'loc>,
) -> Result<Vec<QueryResult>> {
validate_number_of_params(name, parameters.len())?;
let args =
parameters
.iter()
.try_fold(vec![], |mut args, param| -> Result<Vec<Vec<QueryResult>>> {
match param {
LetValue::Value(value) => {
args.push(vec![QueryResult::Literal(Rc::new(value.clone()))])
}
LetValue::AccessClause(clause) => {
let resolved_query = resolver.query(&clause.query)?;
args.push(resolved_query);
}
LetValue::FunctionCall(FunctionExpr {
parameters, name, ..
}) => {
let result = resolve_function(name, parameters, resolver)?;
args.push(result);
}
}

Ok(args)
})?;

Ok(try_handle_function_call(name, &args)?
.into_iter()
.flatten()
.map(Rc::new)
.map(QueryResult::Resolved)
.collect::<Vec<_>>())
}

#[cfg(test)]
#[path = "eval_context_tests.rs"]
pub(super) mod eval_context_tests;
9 changes: 8 additions & 1 deletion guard/src/rules/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -986,12 +986,19 @@ where
move |(rhs, msg)| {
(Some(LetValue::Value(PathAwareValue::try_from(rhs).unwrap())), msg.map(String::from).or(None))
}),
map(tuple((
preceded(zero_or_more_ws_or_comment, function_expr),
preceded(zero_or_more_ws_or_comment, opt(custom_message)))),
|(rhs, msg)| {
(Some(LetValue::FunctionCall(rhs)), msg.map(String::from).or(None))
}),
map(tuple((
preceded(zero_or_more_ws_or_comment, access),
preceded(zero_or_more_ws_or_comment, opt(custom_message)))),
|(rhs, msg)| {
(Some(LetValue::AccessClause(rhs)), msg.map(String::from).or(None))
}),

))))(rest)?;
Ok((
rest,
Expand Down Expand Up @@ -1067,8 +1074,8 @@ pub(crate) fn let_value(input: Span) -> IResult<Span, LetValue> {
map(parse_value, |val| {
LetValue::Value(PathAwareValue::try_from(val).unwrap())
}),
map(access, LetValue::AccessClause),
map(function_expr, LetValue::FunctionCall),
map(access, LetValue::AccessClause),
)),
)(input)
}
Expand Down