use crate::utils::writer::Writer;
use grep_searcher::SearcherBuilder;
use indoc::formatdoc;
use pretty_assertions::{assert_eq, assert_ne};
use std::collections::HashMap;
use crate::rules::eval_context::eval_context_tests::BasicQueryTesting;
use crate::rules::eval_context::{root_scope, EventRecord, RecordTracker};
use super::*;
#[test]
fn test_all_unary_functions() -> Result<()> {
let path_value = PathAwareValue::try_from("{}")?;
let non_empty_path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
ec2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-123456789012
Tags: []
"#,
)?)?;
let list_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(r#"[1, 2, 3]"#)?)?;
let empty_list_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(r#"[]"#)?)?;
let string_value = PathAwareValue::try_from(r#""String""#)?;
let empty_string_value = PathAwareValue::try_from(r#""""#)?;
let int_value = PathAwareValue::try_from(r#"10"#)?;
let bool_value = PathAwareValue::try_from(r#"true"#)?;
let float_value = PathAwareValue::try_from(r#"10.2"#)?;
let char_range_value = PathAwareValue::try_from(r#"r[a, d)"#)?;
let int_range_value = PathAwareValue::try_from(r#"r(10, 20)"#)?;
let float_range_value = PathAwareValue::try_from(r#"r(10.0, 20.5]"#)?;
let null_value = PathAwareValue::Null(path_value::Path::root());
type UnaryTest<'test> = Vec<(
Box<dyn Fn(&QueryResult) -> Result<bool>>,
Vec<QueryResult>,
Vec<QueryResult>,
)>;
let tests: UnaryTest = vec![
(
Box::new(exists_operation),
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
],
vec![QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
})],
),
(
Box::new(element_empty_operation),
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(empty_string_value)), QueryResult::Resolved(Rc::new(empty_list_value.clone())),
QueryResult::UnResolved(UnResolved {
remaining_query: "".to_string(),
reason: None,
traversed_to: Rc::new(path_value.clone()),
}),
],
vec![
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
],
),
(
Box::new(is_string_operation),
vec![QueryResult::Resolved(Rc::new(string_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_int_operation),
vec![QueryResult::Resolved(Rc::new(int_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_list_operation),
vec![
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(empty_list_value.clone())),
],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(int_range_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_struct_operation),
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
],
vec![
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(empty_list_value)),
QueryResult::Resolved(Rc::new(float_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_bool_operation),
vec![QueryResult::Resolved(Rc::new(bool_value))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_float_operation),
vec![QueryResult::Resolved(Rc::new(float_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_char_range_operation),
vec![QueryResult::Resolved(Rc::new(char_range_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::Resolved(Rc::new(float_range_value.clone())),
QueryResult::Resolved(Rc::new(int_range_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_int_range_operation),
vec![QueryResult::Resolved(Rc::new(int_range_value))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::Resolved(Rc::new(float_range_value.clone())),
QueryResult::Resolved(Rc::new(char_range_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_float_range_operation),
vec![QueryResult::Resolved(Rc::new(float_range_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value.clone())),
QueryResult::Resolved(Rc::new(string_value.clone())),
QueryResult::Resolved(Rc::new(int_value.clone())),
QueryResult::Resolved(Rc::new(non_empty_path_value.clone())),
QueryResult::Resolved(Rc::new(char_range_value.clone())),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value.clone()),
reason: None,
remaining_query: "".to_string(),
}),
],
),
(
Box::new(is_null_operation),
vec![QueryResult::Resolved(Rc::new(null_value.clone()))],
vec![
QueryResult::Resolved(Rc::new(path_value.clone())),
QueryResult::Resolved(Rc::new(list_value)),
QueryResult::Resolved(Rc::new(string_value)),
QueryResult::Resolved(Rc::new(int_value)),
QueryResult::Resolved(Rc::new(non_empty_path_value)),
QueryResult::Resolved(Rc::new(char_range_value)),
QueryResult::Resolved(Rc::new(float_value)),
QueryResult::Resolved(Rc::new(float_range_value)),
QueryResult::UnResolved(UnResolved {
traversed_to: Rc::new(path_value),
reason: None,
remaining_query: "".to_string(),
}),
],
),
];
for (index, (func, successes, failures)) in tests.iter().enumerate() {
println!("Testing Case #{}", index);
for (idx, each_success) in successes.iter().enumerate() {
println!("Testing Success Case {}#{}", index, idx);
assert!((*func)(each_success)?);
}
for (idx, each_failure) in failures.iter().enumerate() {
println!("Testing Failure Case {}#{}", index, idx);
assert!(!(*func)(each_failure)?);
}
}
Ok(())
}
#[test]
fn query_empty_and_non_empty() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
s3:
Type: AWS::S3::Bucket
ec2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-123456789012
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let query = AccessQuery::try_from("Resources.*[ Type == /Bucket/ ]")?.query;
let status = unary_operation(
&query,
(CmpOperator::Empty, true),
false,
"".to_string(),
None,
&mut eval,
)?;
match status {
EvaluationResult::QueryValueResult(expected) => {
assert_eq!(expected.len(), 1);
let matched = &expected[0].0;
match matched {
QueryResult::Resolved(res) => {
assert_eq!(res.self_path().0.as_str(), "/Resources/s3");
}
_ => unreachable!(),
}
}
EvaluationResult::EmptyQueryResult(_) => unreachable!(),
}
let query = AccessQuery::try_from("Resources.*[ Type == /Broker/ ]")?.query;
let status = unary_operation(
&query,
(CmpOperator::Empty, true),
false,
"".to_string(),
None,
&mut eval,
)?;
match status {
EvaluationResult::QueryValueResult(_) => unreachable!(),
EvaluationResult::EmptyQueryResult(status) => {
assert_eq!(status, Status::FAIL);
}
}
Ok(())
}
#[test]
fn each_lhs_value_not_comparable() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Parameters:
allowed_images: [ami-123456789012, ami-01234567890]
Resources:
s3:
Type: AWS::S3::Bucket
ec2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-123456789012
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let query_ec2 = AccessQuery::try_from("Resources.ec2.Properties.ImageId")?.query;
let lhs = eval.query(&query_ec2)?;
assert_eq!(lhs.len(), 1);
let lhs = match &lhs[0] {
QueryResult::Resolved(val) => val,
_ => unreachable!(),
};
let rhs_query = AccessQuery::try_from("Parameters.allowed_images")?.query;
let rhs = eval.query(&rhs_query)?;
let result = each_lhs_compare(compare_eq, Rc::clone(lhs), &rhs)?;
assert_eq!(result.len(), 1);
let cmp_result = &result[0];
match cmp_result {
ComparisonResult::NotComparable(NotComparableWithRhs {
pair: LhsRhsPair { rhs: value, .. },
..
}) => {
let rhs_ptr = match &rhs[0] {
QueryResult::Resolved(ptr) => &**ptr,
_ => unreachable!(),
};
assert_eq!(rhs_ptr, &**value);
}
_ => unreachable!(),
}
let result = each_lhs_compare(
in_cmp(true), Rc::clone(lhs),
&rhs,
)?;
assert_eq!(result.len(), 1);
let cmp_result = &result[0];
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs { outcome, .. }) => {
assert!(!(*outcome));
}
_ => unreachable!(),
}
let result = each_lhs_compare(
in_cmp(false), Rc::clone(lhs),
&rhs,
)?;
assert_eq!(result.len(), 1);
let cmp_result = &result[0];
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs { outcome, .. }) => {
assert!(*outcome);
}
_ => unreachable!(),
}
Ok(())
}
#[test]
fn each_lhs_value_eq_compare() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Parameters:
allowed_images: [ami-123456789012, ami-01234567890]
Resources:
s3:
Type: AWS::S3::Bucket
ec2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-123456789012
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let query_ec2 = AccessQuery::try_from("Resources.ec2.Properties.ImageId")?.query;
let lhs = eval.query(&query_ec2)?;
assert_eq!(lhs.len(), 1);
let lhs = match &lhs[0] {
QueryResult::Resolved(val) => val,
_ => unreachable!(),
};
let rhs_query = AccessQuery::try_from("Parameters.allowed_images[*]")?.query;
let rhs = eval.query(&rhs_query)?;
assert_eq!(rhs.len(), 2);
let result = each_lhs_compare(compare_eq, Rc::clone(lhs), &rhs)?;
assert_eq!(result.len(), 2);
for cmp_result in result {
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs {
pair: LhsRhsPair { rhs, .. },
outcome,
}) => {
if outcome {
match (&**lhs, &*rhs) {
(PathAwareValue::String((_, s1)), PathAwareValue::String((_, s2))) => {
assert_eq!(s1, s2);
assert!(!std::ptr::eq(s1, s2));
assert_eq!(s1.as_str(), "ami-123456789012")
}
(_, _) => unreachable!(),
}
} else {
match (&**lhs, &*rhs) {
(PathAwareValue::String((_, s1)), PathAwareValue::String((_, s2))) => {
assert_ne!(s1, s2);
assert!(!std::ptr::eq(s1, s2));
assert_eq!(s1.as_str(), "ami-123456789012");
assert_eq!(s2.as_str(), "ami-01234567890");
}
(_, _) => unreachable!(),
}
}
}
_ => unreachable!(),
}
}
Ok(())
}
#[test]
fn each_lhs_value_eq_compare_mixed_comparable() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Parameters:
allowed_images: [ami-123456789012, ami-01234567890]
Resources:
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
- Principal: '*'
Effect: Allow
Resource: ['s3*']
- Principal: [aws-123, aws-345]
Effect: Allow
Resource: '*'
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let lhs_query =
AccessQuery::try_from("Resources.*.Properties.PolicyDocument.Statement[*].Principal")?
.query;
let selected_lhs = eval.query(&lhs_query)?;
assert_eq!(selected_lhs.len(), 2);
let rhs_value = PathAwareValue::try_from(r#""*""#)?;
let rhs_query_result = vec![QueryResult::Resolved(Rc::new(rhs_value))];
for each_lhs in selected_lhs {
match &each_lhs {
QueryResult::Resolved(lhs) => {
for cmp_result in each_lhs_compare(
not_compare(compare_eq, true),
Rc::clone(lhs),
&rhs_query_result,
)? {
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs { outcome, .. }) => {
if !outcome {
assert_eq!(lhs.self_path().0.as_str(), "/Resources/iam/Properties/PolicyDocument/Statement/0/Principal");
} else {
assert!(lhs.self_path().0.starts_with("/Resources/iam/Properties/PolicyDocument/Statement/1/Principal"));
}
}
_ => unreachable!(),
}
}
}
_ => unreachable!(),
}
}
Ok(())
}
#[test]
fn each_lhs_value_eq_compare_mixed_single_plus_array_form_correct_exec() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Parameters:
allowed_images: [ami-123456789012, ami-01234567890]
Resources:
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
- Principal: '*'
Effect: Allow
Resource: ['s3*']
- Principal: [aws-123, aws-345]
Effect: Allow
Resource: '*'
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let lhs_query =
AccessQuery::try_from("Resources.*.Properties.PolicyDocument.Statement[*].Principal[*]")?
.query;
let selected_lhs = eval.query(&lhs_query)?;
assert_eq!(selected_lhs.len(), 3);
let rhs_value = PathAwareValue::try_from(r#""*""#)?;
let rhs_query_result = vec![QueryResult::Resolved(Rc::new(rhs_value))];
for each_lhs in selected_lhs {
match each_lhs {
QueryResult::Resolved(lhs) => {
for cmp_result in each_lhs_compare(compare_eq, Rc::clone(&lhs), &rhs_query_result)?
{
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs { outcome, .. }) => {
if outcome {
assert_eq!(lhs.self_path().0.as_str(), "/Resources/iam/Properties/PolicyDocument/Statement/0/Principal");
} else {
match lhs.self_path().0.as_str() {
"/Resources/iam/Properties/PolicyDocument/Statement/1/Principal/0" |
"/Resources/iam/Properties/PolicyDocument/Statement/1/Principal/1" => {},
_ => unreachable!()
}
}
}
_ => unreachable!(),
}
}
}
_ => unreachable!(),
}
}
Ok(())
}
macro_rules! test_case {
($rhs_value:expr, $lhs:expr, $eval:ident, $func:expr, $assert:expr) => {
let lhs_gt_query = AccessQuery::try_from($lhs)?.query;
let rhs_value = $rhs_value;
let values = $eval.query(&lhs_gt_query)?;
for each_lhs in values {
match each_lhs {
QueryResult::Resolved(res) => {
for cmp_result in each_lhs_compare(
$func,
res,
&[QueryResult::Resolved(Rc::new(rhs_value.clone()))],
)? {
match cmp_result {
ComparisonResult::Comparable(ComparisonWithRhs { outcome, .. }) => {
assert_eq!(outcome, $assert);
}
_ => {}
}
}
}
_ => unreachable!(),
}
}
};
}
#[test]
fn binary_comparisons_gt_ge() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
values:
int: 10
ints: [20, 10]
float: 1.0
array: [1 ,2]
string: Hi
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
test_case!(
PathAwareValue::try_from("8")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_gt,
true
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_gt,
true
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_ge,
true
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_ge,
true
);
test_case!(
PathAwareValue::try_from("10")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_ge,
true
);
test_case!(
PathAwareValue::try_from("15")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_gt,
false
);
test_case!(
PathAwareValue::try_from("0.5")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_gt,
true
);
test_case!(
PathAwareValue::try_from("1.5")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_gt,
false
);
test_case!(
PathAwareValue::try_from("1.0")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_ge,
true
);
test_case!(
PathAwareValue::try_from(r#""Hi""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_ge,
true
);
test_case!(
PathAwareValue::try_from(r#""Di""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_gt,
true
);
test_case!(
PathAwareValue::try_from(r#""Ji""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_gt,
false
);
Ok(())
}
#[test]
fn binary_comparisons_lt_le() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
values:
int: 10
ints: [20, 10]
float: 1.0
array: [1 ,2]
string: Hi
"#,
)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
test_case!(
PathAwareValue::try_from("8")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_lt,
false
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_lt,
false
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_le,
false
);
test_case!(
PathAwareValue::try_from("8")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_le,
false
);
test_case!(
PathAwareValue::try_from("20")?,
r#"values.ints"#,
eval,
crate::rules::path_value::compare_le,
true
);
test_case!(
PathAwareValue::try_from("15")?,
r#"values.int"#,
eval,
crate::rules::path_value::compare_lt,
true
);
test_case!(
PathAwareValue::try_from("0.5")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_lt,
false
);
test_case!(
PathAwareValue::try_from("1.0")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_le,
true
);
test_case!(
PathAwareValue::try_from("1.5")?,
r#"values.float"#,
eval,
crate::rules::path_value::compare_lt,
true
);
test_case!(
PathAwareValue::try_from(r#""Hi""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_le,
true
);
test_case!(
PathAwareValue::try_from(r#""Di""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_lt,
false
);
test_case!(
PathAwareValue::try_from(r#""Ji""#)?,
r#"values.string"#,
eval,
crate::rules::path_value::compare_lt,
true
);
Ok(())
}
#[test]
fn test_compare_rulegen() -> Result<()> {
let rulegen_created = r#"
let aws_ec2_securitygroup_resources = Resources.*[ Type == 'AWS::EC2::SecurityGroup' ]
rule aws_ec2_securitygroup when %aws_ec2_securitygroup_resources !empty {
%aws_ec2_securitygroup_resources.Properties.SecurityGroupEgress == [{"CidrIp":"0.0.0.0/0","IpProtocol":-1},{"CidrIpv6":"::/0","IpProtocol":-1}]
}"#;
let template = r#"
Resources:
# SecurityGroups
## Alb Security Groups
rFrontendAppSpecificSg:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Frontend Security Group
GroupName: secgrp-frontend
SecurityGroupEgress:
- CidrIp: "0.0.0.0/0"
IpProtocol: -1
- CidrIpv6: "::/0"
IpProtocol: -1
VpcId: vpc-123abc
"#;
let rules = RulesFile::try_from(rulegen_created)?;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(template)?)?;
let mut root = root_scope(&rules, Rc::new(value));
let status = eval_rules_file(&rules, &mut root, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn block_guard_pass() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
- Principal: '*'
Effect: Allow
Resource: ['s3*']
- Principal: [aws-123, aws-345]
Effect: Allow
Resource: '*'
ecs:
Type: AWS::ECS::Task
Properties:
Role:
Ref: iam
"#,
)?)?;
let block_clauses = GuardClause::try_from(
r#"Resources[ Type == /Role/ ].Properties.PolicyDocument {
Statement[*] {
Principal != '*' <<No wildcard allowed for Principals>>
}
}
"#,
)?;
let mut tracker = RecordTracker::new();
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: Some(&mut tracker),
};
let status = eval_guard_clause(&block_clauses, &mut eval)?;
assert_eq!(status, Status::FAIL);
let top = tracker.extract();
match top.container.as_ref() {
Some(record) => {
assert!(matches!(
record,
RecordType::BlockGuardCheck(BlockCheck {
status: Status::FAIL,
..
})
),);
assert_eq!(top.children.len(), 3);
let top_child = &top.children[2];
assert!(matches!(
top_child.container.as_ref().unwrap(),
RecordType::BlockGuardCheck(BlockCheck {
status: Status::FAIL,
..
})
),);
assert_eq!(top_child.children.len(), 2); for (idx, each) in top_child.children.iter().enumerate() {
match each.container.as_ref() {
Some(inner) => {
if idx == 0 {
assert!(matches!(
inner,
RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::FAIL,
..
})
),);
assert_eq!(each.children.len(), 1); let guard_rec = &each.children[0];
match guard_rec.container.as_ref().unwrap() {
RecordType::ClauseValueCheck(ClauseCheck::Comparison(
ComparisonClauseCheck {
status: Status::FAIL,
custom_message: Some(msg),
message: None,
comparison: (CmpOperator::Eq, true),
from: QueryResult::Resolved(from_q),
to: Some(QueryResult::Resolved(_)),
},
)) => {
assert_eq!(msg, "No wildcard allowed for Principals");
assert_eq!(from_q.self_path().0.as_str(), "/Resources/iam/Properties/PolicyDocument/Statement/0/Principal");
}
_ => unreachable!(),
}
} else {
assert!(matches!(
inner,
RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::PASS,
..
})
),);
assert_eq!(each.children.len(), 2); for each_clause_check in &each.children {
match &each_clause_check.container {
Some(RecordType::ClauseValueCheck(ClauseCheck::Success)) => {}
_ => unreachable!(),
}
}
}
}
None => unreachable!(),
}
}
}
None => unreachable!(),
}
Ok(())
}
#[test]
fn test_guard_10_compatibility_and_diff() -> Result<()> {
let value_str = r###"
Statement:
- Principal: ['*', 's3:*']
"###;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let clause_str = r#"Statement.*.Principal == '*'"#;
let clause = GuardClause::try_from(clause_str)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause_str = r#"SOME Statement.*.Principal == '*'"#;
let clause = GuardClause::try_from(clause_str)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::PASS);
let value_str = r###"
Statement:
- Principal: aws
- Principal: ['*', 's3:*']
"###;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn block_evaluation() -> Result<()> {
let value_str = r#"
Resources:
apiGw:
Type: 'AWS::ApiGateway::RestApi'
Properties:
EndpointConfiguration: ["PRIVATE"]
Policy:
Statement:
- Action: Allow
Resource: ['*', "aws:"]
Condition:
'aws:IsSecure': true
'aws:sourceVpc': ['vpc-1234']
- Action: Allow
Resource: ['*', "aws:"]
"#;
let value = serde_yaml::from_str::<serde_yaml::Value>(value_str)?;
let value = PathAwareValue::try_from(value)?;
let clause_str = r#"Resources.*[ Type == 'AWS::ApiGateway::RestApi' ].Properties {
EndpointConfiguration == ["PRIVATE"]
some Policy.Statement[*] {
Action == 'Allow'
Condition[ keys == 'aws:IsSecure' ] !empty
}
}
"#;
let clause = GuardClause::try_from(clause_str)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn block_evaluation_fail() -> Result<()> {
let value_str = r#"
Resources:
apiGw:
Type: 'AWS::ApiGateway::RestApi'
Properties:
EndpointConfiguration: ["PRIVATE"]
Policy:
Statement:
- Action: Allow
Resource: ['*', "aws:"]
Condition:
'aws:IsSecure': true
'aws:sourceVpc': ['vpc-1234']
- Action: Allow
Resource: ['*', "aws:"]
apiGw2:
Type: 'AWS::ApiGateway::RestApi'
Properties:
EndpointConfiguration: ["PRIVATE"]
Policy:
Statement:
- Action: Allow
Resource: ['*', "aws:"]
"#;
let value = serde_yaml::from_str::<serde_yaml::Value>(value_str)?;
let value = PathAwareValue::try_from(value)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let clause_str = r#"Resources.*[ Type == 'AWS::ApiGateway::RestApi' ].Properties {
EndpointConfiguration == ["PRIVATE"]
some Policy.Statement[*] {
Action == 'Allow'
Condition[ keys == 'aws:IsSecure' ] !empty
}
}
"#;
let clause = GuardClause::try_from(clause_str)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn variable_projections() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
s3_bucket:
Type: AWS::S3::Bucket
s3_bucket_policy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: s3_bucket
s3_bucket_policy_2:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: aws:arn
"#,
)?)?;
let rules_file = RulesFile::try_from(
r#"
let policies = Resources[ Type == /BucketPolicy$/ ]
rule policies_check when %policies not empty { # testing no view projection check
%policies.Properties.Bucket exists
%policies.Properties.Bucket not empty # checks both Map not empty/ string not empty
#
# checks Ref's value is not empty. This has 2 results, one FAILure for s3_bucket_policy_2
# one PASS for s3_bucket_policy. Due to some keyword it does PASS
#
some %policies.Properties.Bucket.Ref not empty
}
"#,
)?;
let mut root_scope = root_scope(&rules_file, Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut root_scope, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn variable_projections_failures() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
s3_bucket:
Type: AWS::S3::Bucket
s3_bucket_policy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: s3_bucket
s3_bucket_policy_2:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: ""
"#,
)?)?;
let rules_file = RulesFile::try_from(
r#"
let policies = Resources[ Type == /BucketPolicy$/ ]
rule policies_check when %policies not empty { # testing no view projection check
%policies.Properties.Bucket exists
%policies.Properties.Bucket not empty # checks both Map not empty/ string not empty
#
# checks Ref's value is not empty. This has 2 results, one FAILure for s3_bucket_policy_2
# one PASS for s3_bucket_policy. Due to some keyword it does PASS
#
some %policies.Properties.Bucket.Ref not empty
}
"#,
)?;
let mut root_scope = root_scope(&rules_file, Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut root_scope, None)?;
assert_eq!(status, Status::FAIL);
let top = root_scope.reset_recorder().extract();
assert_eq!(top.children.len(), 1); let rule = &top.children[0];
assert_eq!(rule.children.len(), 4); for (idx, each_rule_clause) in rule.children.iter().enumerate() {
if idx == 0 {
assert!(matches!(
each_rule_clause.container,
Some(RecordType::RuleCondition(Status::PASS))
),);
assert_eq!(each_rule_clause.children.len(), 1); let gbc = &each_rule_clause.children[0];
assert!(matches!(
gbc.container,
Some(RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::PASS,
..
}))
),);
} else if idx == 2 {
assert!(matches!(
each_rule_clause.container,
Some(RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::FAIL,
..
}))
),);
assert_eq!(each_rule_clause.children.len(), 2); let failed_clause = &each_rule_clause.children[1];
assert!(matches!(
failed_clause.container,
Some(RecordType::ClauseValueCheck(ClauseCheck::Unary(
UnaryValueCheck {
comparison: (CmpOperator::Empty, true),
value: ValueCheck {
status: Status::FAIL,
..
}
}
)))
),);
} else {
assert!(matches!(
each_rule_clause.container,
Some(RecordType::GuardClauseBlockCheck(BlockCheck {
status: Status::PASS,
..
}))
),);
}
}
Ok(())
}
#[test]
fn query_cross_joins() -> Result<()> {
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
s3_bucket:
Type: AWS::S3::Bucket
s3_bucket_policy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: s3_bucket
"#,
)?)?;
let rules_files = RulesFile::try_from(
r#"
rule s3_cross_query_join {
let policies = Resources[ Type == /BucketPolicy$/ ].Properties.Bucket.Ref
Resources.%policies {
Type == 'AWS::S3::Bucket'
}
}
"#,
)?;
let mut root_scope = root_scope(&rules_files, Rc::new(path_value.clone()));
let status = eval_rules_file(&rules_files, &mut root_scope, None)?;
assert_eq!(status, Status::PASS);
let rules_files = RulesFile::try_from(
r#"
rule s3_cross_query_join {
let policies = Resources[ Type == /NotBucketPolicy$/ ].Properties.Bucket.Ref
Resources.%policies {
Type == 'AWS::S3::Bucket'
}
}
"#,
)?;
let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value));
let status = eval_rules_file(&rules_files, &mut root_scope, None)?;
assert_eq!(status, Status::SKIP);
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(
r#"
Resources:
s3_bucket:
Type: AWS::S3::Bucket
s3_bucket_policy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: s3_bucket
s3_bucket_policy_2:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: aws:arn...
"#,
)?)?;
let rules_files = RulesFile::try_from(
r#"
rule s3_cross_query_join {
let policies = Resources[ Type == /BucketPolicy$/ ].Properties.Bucket.Ref
Resources.%policies {
Type == 'AWS::S3::Bucket'
}
}
"#,
)?;
let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value.clone()));
let status = eval_rules_file(&rules_files, &mut root_scope, None)?;
assert_eq!(status, Status::FAIL);
let rules_files = RulesFile::try_from(
r#"
rule s3_cross_query_join {
let policies = some Resources[ Type == /BucketPolicy$/ ].Properties.Bucket.Ref
Resources.%policies {
Type == 'AWS::S3::Bucket'
}
}
"#,
)?;
let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value.clone()));
let status = eval_rules_file(&rules_files, &mut root_scope, None)?;
assert_eq!(status, Status::PASS);
let rules_files = RulesFile::try_from(
r#"
rule s3_cross_query_join {
let policies = Resources[ Type == /BucketPolicy$/ ].Properties.Bucket.Ref
some Resources.%policies {
Type == 'AWS::S3::Bucket'
}
}
"#,
)?;
let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value));
let status = eval_rules_file(&rules_files, &mut root_scope, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn cross_rule_clause_when_checks() -> Result<()> {
let rules_skipped = r#"
rule skipped when skip !exists {
Resources.*.Properties.Tags !empty
}
rule dependent_on_skipped when skipped {
Resources.*.Properties exists
}
rule dependent_on_dependent when dependent_on_skipped {
Resources.*.Properties exists
}
rule dependent_on_not_skipped when !skipped {
Resources.*.Properties exists
}
"#;
let input = r#"
{
skip: true,
Resources: {
first: {
Type: 'WhackWhat',
Properties: {
Tags: [{ hi: "there" }, { right: "way" }]
}
}
}
}
"#;
let resources = PathAwareValue::try_from(input)?;
let rules = RulesFile::try_from(rules_skipped)?;
let mut root = root_scope(&rules, Rc::new(resources));
let status = eval_rules_file(&rules, &mut root, None)?;
assert_eq!(status, Status::PASS);
let mut expectations = HashMap::with_capacity(4);
expectations.insert("skipped".to_string(), Status::SKIP);
expectations.insert("dependent_on_skipped".to_string(), Status::SKIP);
expectations.insert("dependent_on_dependent".to_string(), Status::SKIP);
expectations.insert("dependent_on_not_skipped".to_string(), Status::PASS);
let rules_results = root.reset_recorder().extract().children;
assert_eq!(rules_results.len(), 4);
for each in rules_results {
match each.container {
Some(RecordType::RuleCheck(status)) => {
assert_eq!(expectations.get(status.name), Some(&status.status));
}
_ => unreachable!(),
}
}
let input = r#"
{
Resources: {
first: {
Type: 'WhackWhat',
Properties: {
Tags: [{ hi: "there" }, { right: "way" }]
}
}
}
}
"#;
let resources = PathAwareValue::try_from(input)?;
let mut root = root_scope(&rules, Rc::new(resources));
let status = eval_rules_file(&rules, &mut root, None)?;
assert_eq!(status, Status::PASS);
expectations.clear();
expectations.insert("skipped".to_string(), Status::PASS);
expectations.insert("dependent_on_skipped".to_string(), Status::PASS);
expectations.insert("dependent_on_dependent".to_string(), Status::PASS);
expectations.insert("dependent_on_not_skipped".to_string(), Status::SKIP);
let rules_results = root.reset_recorder().extract().children;
assert_eq!(rules_results.len(), 4);
for each in rules_results {
match each.container {
Some(RecordType::RuleCheck(status)) => {
assert_eq!(expectations.get(status.name), Some(&status.status));
}
_ => unreachable!(),
}
}
Ok(())
}
#[test]
fn test_field_type_array_or_single() -> Result<()> {
let statements = r#"{
Statement: [{
Action: '*',
Effect: 'Allow',
Resources: '*'
}, {
Action: ['api:Get', 'api2:Set'],
Effect: 'Allow',
Resources: '*'
}]
}
"#;
let path_value = PathAwareValue::try_from(statements)?;
let clause = GuardClause::try_from(r#"Statement[*].Action != '*'"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let statements = r#"{
Statement: {
Action: '*',
Effect: 'Allow',
Resources: '*'
}
}
"#;
let path_value = PathAwareValue::try_from(statements)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause = GuardClause::try_from(r#"Statement[*].Action[*] != '*'"#)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause = GuardClause::try_from(r#"Statement.*.Action.* != '*'"#)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause = GuardClause::try_from(r#"some Statement[*].Action == '*'"#)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::PASS);
let clause = GuardClause::try_from(r#"some Statement[*].Action != '*'"#)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn test_for_in_and_not_in() -> Result<()> {
let statements = r#"
{
"mainSteps": [
{
"action": "aws:updateAgent"
},
{
"action": "aws:configurePackage"
}
]
}"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(statements)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let clause = GuardClause::try_from(
r#"mainSteps[*].action !IN ["aws:updateSsmAgent", "aws:updateAgent"]"#,
)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause = GuardClause::try_from(
r#"mainSteps[*].action IN ["aws:updateSsmAgent", "aws:updateAgent"]"#,
)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause = GuardClause::try_from(
r#"some mainSteps[*].action IN ["aws:updateSsmAgent", "aws:updateAgent"]"#,
)?;
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn test_rule_with_range_test_and_this() -> Result<()> {
let rule_str = r#"rule check_parameter_validity {
InputParameter.TcpBlockedPorts[*] {
this in r[0, 65535] <<[NON_COMPLIANT] Parameter TcpBlockedPorts has invalid value.>>
}
}"#;
let rule = Rule::try_from(rule_str)?;
let value_str = r#"
InputParameter:
TcpBlockedPorts:
- 21
- 22
- 101
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::PASS);
let value_str = r#"
InputParameter:
TcpBlockedPorts:
- 21
- 22
- 101
- 100000
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn test_inner_when_skipped() -> Result<()> {
let rule_str = r#"
rule no_wild_card_in_managed_policy {
Resources[ Type == /ManagedPolicy/ ] {
when Properties.ManagedPolicyName != /Admin/ {
Properties.PolicyDocument.Statement[*].Action[*] != '*'
}
}
}
"#;
let rule = Rule::try_from(rule_str)?;
let value_str = r#"
Resources:
ReadOnlyAdminPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
PolicyDocument:
Statement:
- Action: '*'
Effect: Allow
Resource: '*'
Version: 2012-10-17
Description: ''
ManagedPolicyName: AdminPolicy
ReadOnlyPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
PolicyDocument:
Statement:
- Action:
- 'cloudwatch:*'
- '*'
Effect: Allow
Resource: '*'
Version: 2013-10-17
Description: ''
ManagedPolicyName: OperatorPolicy
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::FAIL);
let value_str = r#"
Resources:
ReadOnlyAdminPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
PolicyDocument:
Statement:
- Action: '*'
Effect: Allow
Resource: '*'
Version: 2012-10-17
Description: ''
ManagedPolicyName: AdminPolicy
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::SKIP);
let value_str = r#"
Resources: {}
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::SKIP);
let value_str = r#"{}"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value_str)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn test_multiple_valued_clause_reporting() -> Result<()> {
struct ReportAssertions {}
impl<'value> RecordTracer<'value> for ReportAssertions {
fn start_record(&mut self, _context: &str) -> Result<()> {
Ok(())
}
fn end_record(&mut self, _context: &str, record: RecordType<'value>) -> Result<()> {
match record {
RecordType::GuardClauseBlockCheck(BlockCheck {
message,
status,
at_least_one_matches,
}) => {
assert_eq!(message, None);
assert_eq!(status, Status::FAIL);
assert!(!at_least_one_matches);
}
RecordType::ClauseValueCheck(ClauseCheck::Comparison(ComparisonClauseCheck {
status,
from,
to,
..
})) => {
assert!(to.is_some());
assert_eq!(status, Status::FAIL);
match from {
QueryResult::Resolved(res) => {
assert!(
res.self_path().0.as_str() == "/Resources/second/Properties/Name"
|| res.self_path().0.as_str()
== "/Resources/failed/Properties/Name",
);
}
_ => unreachable!(),
}
}
RecordType::ClauseValueCheck(ClauseCheck::Success) => {}
RecordType::RuleCheck(NamedStatus { name, status, .. }) => {
assert_eq!(name, "name_check");
assert_eq!(status, Status::FAIL);
}
RecordType::FileCheck(NamedStatus { status, .. }) => {
assert_eq!(status, Status::FAIL);
}
_ => unreachable!(),
}
Ok(())
}
}
let rule = r###"
rule name_check { Resources.*.Properties.Name == /NAME/ }
"###;
let value = r###"
Resources:
second:
Properties:
Name: FAILEDMatch
first:
Properties:
Name: MatchNAME
matches:
Properties:
Name: MatchNAME
failed:
Properties:
Name: FAILEDMatch
"###;
let rules = Rule::try_from(rule)?;
let values = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value)?)?;
let mut asserter = ReportAssertions {};
let mut root = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: Some(&mut asserter),
};
let status = eval_rule(&rules, &mut root)?;
assert_eq!(status, Status::FAIL);
let rule = r###"
let resources = Resources.*
rule name_check { %resources.Properties.Name == /NAME/ }
"###;
let rules = RulesFile::try_from(rule)?;
let mut root = root_scope(&rules, Rc::new(values));
let status = eval_rules_file(&rules, &mut root, None)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[rstest::rstest]
#[case("SubdomainMaster", "Master.PrivateIp", Status::PASS)]
#[case("SubdomainInternal", "Master.PrivateIp", Status::PASS)]
#[case("SubdomainDefault", "Infra1.PrivateIp", Status::PASS)]
#[case("SubdomainDefault", "Infra1.PrivateIp", Status::PASS)]
#[case("Subdomain", "Infra1.PrivateIp", Status::FAIL)]
#[case("SubdomainDefault", "Infra1.PublicIp", Status::FAIL)]
#[case("Subdomain", "Master.PrivateIp", Status::FAIL)]
#[case("SubdomainDefault", "Master.PublicIp", Status::FAIL)]
fn test_in_comparison_operator_for_list_of_lists(
#[case] name_arg: &str,
#[case] resource_records_arg: &str,
#[case] status_arg: Status,
) -> Result<()> {
let template = formatdoc! {
r###"
Resources:
MasterRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneName: !Ref 'HostedZoneName'
Comment: DNS name for my instance.
Name: !Join ['', [!Ref '{}', ., !Ref 'HostedZoneName']]
Type: A
TTL: "900"
ResourceRecords:
- !GetAtt '{}'"###,
name_arg,
resource_records_arg,
};
let rules = r#"
let aws_route53_recordset_resources = Resources.*[ Type == 'AWS::Route53::RecordSet' ]
rule aws_route53_recordset when %aws_route53_recordset_resources !empty {
let targets = [{"Fn::Join": ["",[{"Ref": "SubdomainMaster"},".", {"Ref": "HostedZoneName"}]]}, {"Fn::Join": ["",[{"Ref": "SubdomainWild"},".", {"Ref": "HostedZoneName"}]]}, {"Fn::Join": ["",[{"Ref": 'SubdomainInternal'},".", {"Ref": "HostedZoneName"}]]}, {"Fn::Join": ["",[{"Ref": "SubdomainDefault"},".", {"Ref": "HostedZoneName"}]]}]
%aws_route53_recordset_resources.Properties.Comment == "DNS name for my instance."
%aws_route53_recordset_resources.Properties.ResourceRecords IN [[{"Fn::GetAtt": "Master.PrivateIp"}], [{"Fn::GetAtt": "Infra1.PrivateIp"}]]
%aws_route53_recordset_resources.Properties.Name IN %targets
%aws_route53_recordset_resources.Properties.Type == "A"
%aws_route53_recordset_resources.Properties.TTL == "900"
%aws_route53_recordset_resources.Properties.HostedZoneName == {"Ref": "HostedZoneName"}
}
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(&template)?)?;
let rule_eval = RulesFile::try_from(rules)?;
let mut context = root_scope(&rule_eval, Rc::new(value));
let status = eval_rules_file(&rule_eval, &mut context, None)?;
assert_eq!(status, status_arg);
Ok(())
}
#[rstest::rstest]
#[case(r#"'900'"#, Status::PASS)]
#[case(r#"!!str 900"#, Status::PASS)]
#[case(r#"900"#, Status::FAIL)]
#[case(r#"!!int "900""#, Status::FAIL)]
#[case(r#"!!float "900""#, Status::FAIL)]
fn test_type_conversions(#[case] ttl_arg: &str, #[case] status_arg: Status) -> Result<()> {
let template = formatdoc! {
r###"
Resources:
MasterRecord:
Type: AWS::Route53::RecordSet
Properties:
TTL: {}
"###,
ttl_arg,
};
let rules = r#"
let aws_route53_recordset_resources = Resources.*[ Type == 'AWS::Route53::RecordSet' ]
rule aws_route53_recordset when %aws_route53_recordset_resources !empty {
%aws_route53_recordset_resources.Properties.TTL == "900"
}
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(&template)?)?;
let rule_eval = RulesFile::try_from(rules)?;
let mut context = root_scope(&rule_eval, Rc::new(value));
let status = eval_rules_file(&rule_eval, &mut context, None)?;
assert_eq!(status, status_arg);
Ok(())
}
#[test]
fn is_bool() -> Result<()> {
let rule_str = r###"
rule check_is_bool{
foo is_bool
}
"###;
let resources_str = r###"
{
foo: false
}
"###;
let value = PathAwareValue::try_from(resources_str)?;
let rules_file = RulesFile::try_from(rule_str)?;
println!("{:?}", rules_file);
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::PASS);
let resources_str = r#"
{
foo: "false"
}
"#;
let value = PathAwareValue::try_from(resources_str)?;
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn is_int() -> Result<()> {
let rule_str = r###"
rule check_is_int{
foo is_int
}
"###;
let resources_str = r###"
{
foo: 1
}
"###;
let value = PathAwareValue::try_from(resources_str)?;
let rules_file = RulesFile::try_from(rule_str)?;
println!("{:?}", rules_file);
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::PASS);
let resources_str = r#"
{
foo: "1"
}
"#;
let value = PathAwareValue::try_from(resources_str)?;
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn double_projection_tests() -> Result<()> {
let rule_str = r###"
rule check_ecs_against_local_or_metadata {
let ecs_tasks = Resources.*[
Type == 'AWS::ECS::TaskDefinition'
Properties.TaskRoleArn exists
]
let iam_references = some %ecs_tasks.Properties.TaskRoleArn.'Fn::GetAtt'[0]
when %iam_references !empty {
let iam_local = Resources.%iam_references
%iam_local.Type == 'AWS::IAM::Role'
%iam_local.Properties.PermissionsBoundary exists
}
let ecs_task_role_is_string = %ecs_tasks[
Properties.TaskRoleArn is_string
]
when %ecs_task_role_is_string !empty {
%ecs_task_role_is_string.Metadata.NotRestricted exists
}
}
"###;
let resources_str = r#"
{
Resources: {
ecs: {
Type: 'AWS::ECS::TaskDefinition',
Metadata: {
NotRestricted: true
},
Properties: {
TaskRoleArn: "aws:arn..."
}
},
ecs2: {
Type: 'AWS::ECS::TaskDefinition',
Properties: {
TaskRoleArn: { 'Fn::GetAtt': ["iam", "arn"] }
}
},
iam: {
Type: 'AWS::IAM::Role',
Properties: {
PermissionsBoundary: "aws:arn"
}
}
}
}
"#;
let value = PathAwareValue::try_from(resources_str)?;
let rules_file = RulesFile::try_from(rule_str)?;
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::PASS);
let resources_str = r#"
{
Resources: {
ecs2: {
Type: 'AWS::ECS::TaskDefinition',
Properties: {
TaskRoleArn: { 'Fn::GetAtt': ["iam", "arn"] }
}
}
}
}
"#;
let value = PathAwareValue::try_from(resources_str)?;
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn test_rules_with_some_clauses() -> Result<()> {
let query = r#"let x = some Resources.*[ Type == 'AWS::IAM::Role' ].Properties.Tags[ Key == /[A-Za-z0-9]+Role/ ]"#;
let resources = r#" {
"Resources": {
"CounterTaskDefExecutionRole5959CB2D": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}],
"Version": "2012-10-17"
},
"PermissionsBoundary": {"Fn::Sub" : "arn::aws::iam::${AWS::AccountId}:policy/my-permission-boundary"},
"Tags": [{ "Key": "TestRole", "Value": ""}]
},
"Metadata": {
"aws:cdk:path": "foo/Counter/TaskDef/ExecutionRole/Resource"
}
},
"BlankRole001": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}],
"Version": "2012-10-17"
},
"Tags": [{ "Key": "FooBar", "Value": ""}]
},
"Metadata": {
"aws:cdk:path": "foo/Counter/TaskDef/ExecutionRole/Resource"
}
},
"BlankRole002": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}],
"Version": "2012-10-17"
}
},
"Metadata": {
"aws:cdk:path": "foo/Counter/TaskDef/ExecutionRole/Resource"
}
}
}
}
"#;
let value = PathAwareValue::try_from(resources)?;
let parsed = RulesFile::try_from(query)?;
let mut eval = root_scope(&parsed, Rc::new(value));
let selected = eval.resolve_variable("x")?;
println!("{:?}", selected);
assert_eq!(selected.len(), 1);
Ok(())
}
#[test]
fn test_support_for_atleast_one_match_clause() -> Result<()> {
let clause_some_str = r#"some Tags[*].Key == /PROD/"#;
let clause_some = GuardClause::try_from(clause_some_str)?;
let clause_str = r#"Tags[*].Key == /PROD/"#;
let clause = GuardClause::try_from(clause_str)?;
let values_str = r#"{
Tags: [
{
Key: "InPROD",
Value: "ProdApp"
},
{
Key: "NoP",
Value: "NoQ"
}
]
}
"#;
let values = PathAwareValue::try_from(values_str)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_guard_clause(&clause_some, &mut eval)?;
assert_eq!(status, Status::PASS);
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let values_str = r#"{ Tags: [] }"#;
let values = PathAwareValue::try_from(values_str)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_guard_clause(&clause_some, &mut eval)?;
assert_eq!(status, Status::FAIL);
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let values_str = r#"{ }"#;
let values = PathAwareValue::try_from(values_str)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&clause_some, &mut eval)?;
assert_eq!(status, Status::FAIL);
let status = eval_guard_clause(&clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
let selection_str = r#"Resources[
Type == 'AWS::DynamoDB::Table'
some Properties.Tags[*].Key == /PROD/
]"#;
let resources_str = r#"{
Resources: {
ddbSelected: {
Type: 'AWS::DynamoDB::Table',
Properties: {
Tags: [
{
Key: "PROD",
Value: "ProdApp"
}
]
}
},
ddbNotSelected: {
Type: 'AWS::DynamoDB::Table'
}
}
}"#;
let _resources = PathAwareValue::try_from(resources_str)?;
let selection_query = AccessQuery::try_from(selection_str)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let selected = eval.query(&selection_query.query)?;
println!("Selected = {:?}", selected);
assert_eq!(selected.len(), 1);
Ok(())
}
#[test]
fn test_map_keys_function() -> Result<()> {
let value_str = r#"
Resources:
apiGw:
Type: 'AWS::ApiGateway::RestApi'
Properties:
EndpointConfiguration: ["PRIVATE"]
Policy:
Statement:
- Action: Allow
Resource: ['*', "aws:"]
Condition:
'aws:IsSecure': true
"#;
let value = serde_yaml::from_str::<serde_yaml::Value>(value_str)?;
let value = PathAwareValue::try_from(value)?;
let rule_str = r#"
let api_gw = Resources[ Type == 'AWS::ApiGateway::RestApi' ]
rule check_rest_api_is_private_and_has_access {
%api_gw {
Properties.EndpointConfiguration == ["PRIVATE"]
some Properties.Policy.Statement[*].Condition[ keys == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] !empty
}
}"#;
let rule = RulesFile::try_from(rule_str)?;
let mut root = root_scope(&rule, Rc::new(value));
let status = eval_rules_file(&rule, &mut root, None)?;
assert_eq!(status, Status::FAIL);
let value_str = r#"
Resources:
apiGw:
Type: 'AWS::ApiGateway::RestApi'
Properties:
EndpointConfiguration: ["PRIVATE"]
Policy:
Statement:
- Action: Allow
Resource: ['*', "aws:"]
Condition:
'aws:IsSecure': true
'aws:sourceVpc': ['vpc-1234']
"#;
let value = serde_yaml::from_str::<serde_yaml::Value>(value_str)?;
let value = PathAwareValue::try_from(value)?;
let mut root = root_scope(&rule, Rc::new(value));
let status = eval_rules_file(&rule, &mut root, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn ensure_all_list_value_access_on_empty_fails() -> Result<()> {
let resources = r#"Tags: []"#;
let values = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let claused_failure_spec = GuardClause::try_from(r#"Tags[*].Key == /Name/"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"some Tags[*].Key == /Name/"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"Tags[*] { Key == /Name/ }"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"some Tags[*] { Key == /Name/ }"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"Tags !empty"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"Tags empty"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::PASS);
let claused_failure_spec = GuardClause::try_from(r#"Tags[*] !empty"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let claused_failure_spec = GuardClause::try_from(r#"Tags[*] empty"#)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_guard_clause(&claused_failure_spec, &mut eval)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn ensure_all_map_values_access_on_empty_fails() -> Result<()> {
let resources = r#"Resources: {}"#;
let values = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values.clone()),
recorder: None,
};
let clause_failure_spec = GuardClause::try_from(r#"Resources.*.Properties exists"#)?;
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause_failure_spec = GuardClause::try_from(r#"Resources.* { Properties exists }"#)?;
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
let clause_failure_spec = GuardClause::try_from(r#"Resources exists"#)?;
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::PASS);
let clause_failure_spec =
GuardClause::try_from(r#"Resources[ Type == /Bucket/ ].Properties exists"#)?;
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::SKIP);
let resources = r#"
Resources:
ec2:
Type: AWS::EC2::Instance
Properties:
ImageId: ami-1234554657
"#;
let _value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::SKIP);
let resources = r#"{}"#;
let values = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let clause_failure_spec = GuardClause::try_from(r#"Resources exists"#)?;
let status = eval_guard_clause(&clause_failure_spec, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
fn find_failed_clauses<'event, 'value>(
root: &'event EventRecord<'value>,
) -> Vec<&'event EventRecord<'value>> {
match &root.container {
Some(RecordType::Filter(_)) | Some(RecordType::ClauseValueCheck(ClauseCheck::Success)) => {
vec![]
}
Some(RecordType::ClauseValueCheck(_)) => vec![root],
_ => {
let mut acc = Vec::new();
for child in &root.children {
acc.extend(find_failed_clauses(child));
}
acc
}
}
}
#[test]
fn filter_based_join_clauses_failures_and_skips() -> Result<()> {
let resources = r#"
Resources:
function:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role:
Ref: iam
function2:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role: aws:arn
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: ['*']
iam2:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: '*'
"#;
let rules = r###"
rule ensure_lambda_role_local_stack {
let with_refs = some Resources[ Type == 'AWS::Lambda::Function' ].Properties.Role.Ref
Resources.%with_refs {
Type == 'AWS::IAM::Role'
Properties.PolicyDocument.Statement[*] {
Action != '*'
Principal != '*'
}
}
}
"###;
let rules_file = RulesFile::try_from(rules)?;
let path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = root_scope(&rules_file, Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
let top = eval.reset_recorder().extract();
let failed_clauses = find_failed_clauses(&top);
assert_eq!(failed_clauses.len(), 2);
for each in failed_clauses {
if let Some(RecordType::ClauseValueCheck(check)) = &each.container {
match check {
ClauseCheck::Comparison(ComparisonClauseCheck { status, from, .. }) => {
assert_eq!(*status, Status::FAIL);
assert!(each.context.contains("Action") || each.context.contains("Principal"),);
assert!(from.resolved().map_or(false, |res| {
let path = res.self_path().0.as_str();
path == "/Resources/iam/Properties/PolicyDocument/Statement/Action"
|| path
== "/Resources/iam/Properties/PolicyDocument/Statement/Principal/0"
}))
}
_ => unreachable!(),
}
}
}
let resources = r#"
Resources:
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: ['*']
iam2:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: '*'
"#;
let path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = root_scope(&rules_file, Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::SKIP);
let resources = r#"
Resources:
function2:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role: aws:arn
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: ['*']
"#;
let path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = eval.reset_root(Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::SKIP);
let resources = r###"
Resources:
function:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role:
Ref: iamNotThere # dangling reference
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: ['*']
"###;
let path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let mut eval = eval.reset_root(Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
let top = eval.reset_recorder().extract();
let failed_clauses = find_failed_clauses(&top);
assert_eq!(failed_clauses.len(), 1);
match &failed_clauses[0].container {
Some(RecordType::ClauseValueCheck(ClauseCheck::MissingBlockValue(check))) => {
assert_eq!(check.status, Status::FAIL);
assert_eq!(check.from.resolved(), None);
}
_ => unreachable!(),
}
Ok(())
}
#[test]
fn filter_based_with_join_pass_use_cases() -> Result<()> {
let resources = r#"
Resources:
function:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role:
Ref: iam
function2:
Type: AWS::Lambda::Function
Properties:
Code: ''
Role: aws:arn
iam:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: ['*']
iam2:
Type: AWS::IAM::Role
Properties:
PolicyDocument:
Statement:
Action: '*'
Resource: '*'
Effect: Allow
Principal: '*'
"#;
let rules = r###"
rule ensure_lambda_role_local_stack {
let with_refs = some Resources[ Type == 'AWS::Lambda::Function' ].Properties.Role.Ref
Resources.%with_refs {
Type == 'AWS::IAM::Role'
Properties.PolicyDocument.Statement[*] {
Action == '*'
Principal == '*'
}
}
}
"###;
let path_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let rules_file = RulesFile::try_from(rules)?;
let mut eval = root_scope(&rules_file, Rc::new(path_value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn rule_clause_tests() -> Result<()> {
let r = r###"
rule check_all_resources_have_tags_present {
let all_resources = Resources.*.Properties
%all_resources.Tags EXISTS
%all_resources.Tags !EMPTY
}
"###;
let rule = RulesFile::try_from(r)?;
let v = r#"
{
"Resources": {
"vpc": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": "10.0.0.0/25",
"Tags": [
{
"Key": "my-vpc",
"Value": "my-vpc"
}
]
}
}
}
}
"#;
let value = PathAwareValue::try_from(v)?;
let mut eval = root_scope(&rule, Rc::new(value));
let status = eval_rules_file(&rule, &mut eval, None)?;
assert_eq!(Status::PASS, status);
let v = r#"
{
"Resources": {
"vpc": {
"Type": "AWS::EC2::VPC",
"Properties": {
"CidrBlock": "10.0.0.0/25",
"Tags": []
}
}
}
}
"#;
let value = PathAwareValue::try_from(v)?;
let mut eval = eval.reset_root(Rc::new(value));
let status = eval_rules_file(&rule, &mut eval, None)?;
assert_eq!(Status::FAIL, status);
Ok(())
}
#[test]
fn rule_test_type_blocks() -> Result<()> {
let r = r"
rule iam_basic_checks {
AWS::IAM::Role {
Properties.AssumeRolePolicyDocument.Version == /(\d{4})-(\d{2})-(\d{2})/
Properties.PermissionsBoundary == /arn:aws:iam::(\d{12}):policy/
Properties.Tags[*].Value == /[a-zA-Z0-9]+/
Properties.Tags[*].Key == /[a-zA-Z0-9]+/
}
}";
let value = r#"
{
"Resources": {
"iamrole": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789012:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
}
}
},
"iamRole2": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789112:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
},
"Tags": [
{ "Key": "Key", "Value": "Value" }
]
}
}
}
}
"#;
let root = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value)?)?;
let rules_file = RulesFile::try_from(r)?;
let mut root_context = root_scope(&rules_file, Rc::new(root));
let status = eval_rules_file(&rules_file, &mut root_context, None)?;
assert_eq!(Status::FAIL, status);
let top = root_context.reset_recorder().extract();
let failed_clause = find_failed_clauses(&top);
assert_eq!(failed_clause.len(), 2); for each in failed_clause {
match &each.container {
Some(RecordType::ClauseValueCheck(ClauseCheck::Comparison(
ComparisonClauseCheck {
from, status, to, ..
},
))) => {
assert_eq!(*status, Status::FAIL);
assert_eq!(from.resolved(), None);
assert_eq!(*to, None);
}
_ => unreachable!(),
}
}
Ok(())
}
#[test]
fn rules_file_tests_the_unituitive_all_clause_that_skips() -> Result<()> {
let file = r#"
let iam_resources = Resources.*[ Type == "AWS::IAM::Role" ]
rule iam_resources_exists {
%iam_resources !EMPTY
}
rule iam_basic_checks when iam_resources_exists {
%iam_resources.Properties.AssumeRolePolicyDocument.Version == /(\d{4})-(\d{2})-(\d{2})/
%iam_resources.Properties.PermissionsBoundary == /arn:aws:iam::(\d{12}):policy/
#
# This check FAILs as it tests using a conjoined call. It is testing that ALL
# IAM resources have non empty Tags. This FAILs as "iamrole" does not have Tags
# property specified. Therefore this check overall leads to PASS, which is the
# correct outcome as specified. See next test on the right way to use this
#
when %iam_resources.Properties.Tags EXISTS
%iam_resources.Properties.Tags !EMPTY {
%iam_resources.Properties.Tags[*].Value == /[a-zA-Z0-9]+/
%iam_resources.Properties.Tags[*].Key == /[a-zA-Z0-9]+/
}
}"#;
let value = r#"
{
"Resources": {
"iamrole": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789012:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
}
}
},
"iamRole2": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789112:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
},
"Tags": [
{ "Key": "Key", "Value": "Value" }
]
}
}
}
}
"#;
let root = PathAwareValue::try_from(value)?;
let rules_file = RulesFile::try_from(file)?;
let mut root_context = root_scope(&rules_file, Rc::new(root));
let status = eval_rules_file(&rules_file, &mut root_context, None)?;
assert_eq!(Status::PASS, status);
Ok(())
}
#[test]
fn rules_file_tests_simpler_correct_form_using_newer_constructs() -> Result<()> {
let file = r"
rule iam_basic_checks {
Resources[ Type == 'AWS::IAM::Role' ] {
Properties {
AssumeRolePolicyDocument.Version == /(\d{4})-(\d{2})-(\d{2})/
PermissionsBoundary == /arn:aws:iam::(\d{12}):policy/
Tags[*] {
Key == /[a-zA-Z0-9]+/
Value == /[a-zA-Z0-9]+/
}
}
}
}";
let value = r#"
{
"Resources": {
"iamrole": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789012:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
}
}
},
"iamRole2": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789112:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
},
"Tags": [
{ "Key": "Key", "Value": "Value" }
]
}
}
}
}
"#;
let root = PathAwareValue::try_from(value)?;
let rules_file = RulesFile::try_from(file)?;
let mut root_context = root_scope(&rules_file, Rc::new(root));
let status = eval_rules_file(&rules_file, &mut root_context, None)?;
assert_eq!(Status::FAIL, status);
let top = root_context.reset_recorder().extract();
let failed_clause = find_failed_clauses(&top);
assert_eq!(failed_clause.len(), 1); for each in failed_clause {
match &each.container {
Some(RecordType::ClauseValueCheck(ClauseCheck::MissingBlockValue(ValueCheck {
status,
from,
..
}))) => {
assert_eq!(*status, Status::FAIL);
assert_eq!(from.resolved(), None);
}
_ => unreachable!(),
}
}
let value = r#"
{
"Resources": {
"iamrole": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789012:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
},
Tags: []
}
},
"iamRole2": {
"Type": "AWS::IAM::Role",
"Properties": {
"PermissionsBoundary": "arn:aws:iam::123456789112:policy/permboundary",
"AssumeRolePolicyDocument": {
"Version": "2021-01-10",
"Statement": {
"Effect": "Allow",
"Principal": "*",
"Action": "*",
"Resource": "*"
}
},
"Tags": [
{ "Key": "Key", "Value": "Value" }
]
}
}
}
}
"#;
let root = PathAwareValue::try_from(value)?;
let mut root_context = root_context.reset_root(Rc::new(root));
let status = eval_rules_file(&rules_file, &mut root_context, None)?;
assert_eq!(Status::FAIL, status);
let top = root_context.reset_recorder().extract();
let failed_clause = find_failed_clauses(&top);
assert_eq!(failed_clause.len(), 1); for each in failed_clause {
match &each.container {
Some(RecordType::ClauseValueCheck(ClauseCheck::MissingBlockValue(ValueCheck {
status,
from,
..
}))) => {
assert_eq!(*status, Status::FAIL);
assert_eq!(from.resolved(), None);
match from.unresolved_traversed_to() {
Some(val) => {
assert_eq!(
val.self_path().0.as_str(),
"/Resources/iamrole/Properties/Tags"
);
}
None => unreachable!(),
}
}
_ => unreachable!(),
}
}
Ok(())
}
const SAMPLE: &str = r#"
{
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "false"}
}
},
{
"Sid": "ServicePutObject",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "true"}
}
}
]
}
"#;
#[test]
fn test_iam_statement_clauses() -> Result<()> {
let sample = r#"
{
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "false"},
"StringEquals": {"aws:SourceVpc": "vpc-12243sc"}
}
},
{
"Sid": "ServicePutObject",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "true"}
}
}
]
}
"#;
let values = PathAwareValue::try_from(sample)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let clause = r#"Statement[
Condition EXISTS ].Condition.*[
this is_struct ][ KEYS == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] NOT EMPTY"#;
let parsed = GuardClause::try_from(clause)?;
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(Status::PASS, status);
let clause = r#"Statement[ Condition EXISTS
Condition.*[ KEYS == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] !EMPTY ] NOT EMPTY
"#;
let parsed = GuardClause::try_from(clause)?;
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(Status::PASS, status);
let parsed = GuardClause::try_from(
r#"SOME Statement[*].Condition.*[ THIS IS_STRUCT ][ KEYS == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] NOT EMPTY"#,
)?;
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(Status::PASS, status);
let sample = r#"
{
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject"
}
]
}"#;
let value = PathAwareValue::try_from(sample)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(status, Status::FAIL);
let sample = r#"
{
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Condition": {
"array": [1, 3, 4]
}
}
]
}"#;
let value = PathAwareValue::try_from(sample)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(status, Status::FAIL);
let sample = r#"
{
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Condition": {
"array": [1, 3, 4],
"StringEquals": {"aws:SourceVpc": "vpc-12243sc"}
}
}
]
}"#;
let value = PathAwareValue::try_from(sample)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(status, Status::PASS);
let value = PathAwareValue::try_from(SAMPLE)?;
let parsed = GuardClause::try_from(clause)?;
let mut eval = BasicQueryTesting {
root: Rc::new(value),
recorder: None,
};
let status = eval_guard_clause(&parsed, &mut eval)?;
assert_eq!(Status::FAIL, status);
Ok(())
}
#[test]
fn test_api_gateway() -> Result<()> {
let rule = r#"
rule check_rest_api_private {
AWS::ApiGateway::RestApi {
# Endpoint configuration must only be private
Properties.EndpointConfiguration == ["PRIVATE"]
# At least one statement in the resource policy must contain a condition with the key of "aws:sourceVpc" or "aws:sourceVpce"
Properties.Policy.Statement[ Condition.*[ KEYS == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] !EMPTY ] !EMPTY
}
}
"#;
let rule = Rule::try_from(rule)?;
let resources = r#"
{
"Resources": {
"apigatewayapi": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Policy": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "false"},
"StringEquals": {"aws:SourceVpc": "vpc-12243sc"}
}
},
{
"Sid": "ServicePutObject",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "true"}
}
}
]
},
"EndpointConfiguration": ["PRIVATE"]
}
}
}
}"#;
let values = PathAwareValue::try_from(resources)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn test_api_gateway_cleaner_model() -> Result<()> {
let rule = r#"
rule check_rest_api_private {
AWS::ApiGateway::RestApi {
Properties {
# Endpoint configuration must only be private
EndpointConfiguration == ["PRIVATE"]
some Policy.Statement[*] {
Condition.*[ keys == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] not empty
}
}
}
}
"#;
let rule = Rule::try_from(rule)?;
let resources = r#"
{
"Resources": {
"apigatewayapi": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Policy": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "false"},
"StringEquals": {"aws:SourceVpc": "vpc-12243sc"}
}
},
{
"Sid": "ServicePutObject",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "true"}
}
}
]
},
"EndpointConfiguration": ["PRIVATE"]
}
}
}
}"#;
let values = PathAwareValue::try_from(resources)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::PASS);
let resources = r#"
{
"Resources": {
"apigatewayapi": {
"Type": "AWS::ApiGateway::RestApi",
"Properties": {
"Policy": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PrincipalPutObjectIfIpAddress",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "false"},
"Bool": {"aws:SecureTransport": "true"}
}
},
{
"Sid": "ServicePutObject",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-service-bucket/*",
"Condition": {
"Bool": {"aws:ViaAWSService": "true"}
}
}
]
},
"EndpointConfiguration": ["PRIVATE"]
}
}
}
}"#;
let values = PathAwareValue::try_from(resources)?;
let mut eval = BasicQueryTesting {
root: Rc::new(values),
recorder: None,
};
let status = eval_rule(&rule, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn testing_iam_role_prov_serve() -> Result<()> {
let resources = r#"
{
"Resources": {
"CounterTaskDefExecutionRole5959CB2D": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
}
}],
"Version": "2012-10-17"
},
"PermissionBoundary": {"Fn::Sub" : "arn::aws::iam::${AWS::AccountId}:policy/my-permission-boundary"},
"Tags": [{ "Key": "TestRole", "Value": ""}]
},
"Metadata": {
"aws:cdk:path": "foo/Counter/TaskDef/ExecutionRole/Resource"
}
}
}
}
"#;
let rules = r#"
let iam_roles = Resources.*[ Type == "AWS::IAM::Role" ]
let ecs_tasks = Resources.*[ Type == "AWS::ECS::TaskDefinition" ]
rule deny_permissions_boundary_iam_role when %iam_roles !EMPTY {
# atleast one Tags contains a Key "TestRole"
%iam_roles.Properties.Tags[ Key == "TestRole" ] NOT EMPTY
%iam_roles.Properties.PermissionBoundary !EXISTS
}
rule deny_task_role_no_permission_boundary when %ecs_tasks !EMPTY {
let task_role = %ecs_tasks.Properties.TaskRoleArn
when %task_role.'Fn::GetAtt' EXISTS {
let role_name = %task_role.'Fn::GetAtt'[0]
let iam_roles_by_name = Resources.*[ KEYS == %role_name ]
%iam_roles_by_name !EMPTY
iam_roles_by_name.Properties.Tags !EMPTY
} or
%task_role == /aws:arn/ # either a direct string or
}
"#;
let rules_file = RulesFile::try_from(rules)?;
let value = PathAwareValue::try_from(resources)?;
let mut eval = root_scope(&rules_file, Rc::new(value));
let status = eval_rules_file(&rules_file, &mut eval, None)?;
println!("{}", status);
Ok(())
}
#[test]
fn testing_sg_rules_pro_serve() -> Result<()> {
let sgs = r#"
[{
"Resources": {
"CounterServiceSecurityGroupF41A3908": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "foo/Counter/Service/SecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "0.0.0.0/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"VpcId": {
"Ref": "Vpc8378EB38"
}
},
"Metadata": {
"aws:cdk:path": "foo/Counter/Service/SecurityGroup/Resource"
}
}
}
},
{
"Resources": {
"CounterServiceSecurityGroupF41A3908": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "foo/Counter/Service/SecurityGroup",
"SecurityGroupEgress": [
{
"CidrIpv6": "::/0",
"Description": "Allow all outbound traffic by default",
"IpProtocol": "-1"
}
],
"VpcId": {
"Ref": "Vpc8378EB38"
}
},
"Metadata": {
"aws:cdk:path": "foo/Counter/Service/SecurityGroup/Resource"
}
}
}
}, {
"Resources": {
"CounterServiceSecurityGroupF41A3908": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "foo/Counter/Service/SecurityGroup",
"SecurityGroupEgress": [
{
"CidrIp": "10.0.0.0/16",
"Description": "",
"IpProtocol": "-1"
}
],
"VpcId": {
"Ref": "Vpc8378EB38"
}
},
"Metadata": {
"aws:cdk:path": "foo/Counter/Service/SecurityGroup/Resource"
}
}
}
},
{ "Resources": {
"CounterServiceSecurityGroupF41A3908": {
"Type": "AWS::EC2::SecurityGroup",
"Properties": {
"GroupDescription": "foo/Counter/Service/SecurityGroup",
"VpcId": {
"Ref": "Vpc8378EB38"
}
},
"Metadata": {
"aws:cdk:path": "foo/Counter/Service/SecurityGroup/Resource"
}
}
}
}]
"#;
let rules = r#"
let sgs = Resources.*[ Type == "AWS::EC2::SecurityGroup" ]
rule deny_egress when %sgs NOT EMPTY {
# Ensure that none of the security group contain a rule
# that has Cidr Ip set to any
%sgs.Properties.SecurityGroupEgress[ CidrIp == "0.0.0.0/0" or
CidrIpv6 == "::/0" ] EMPTY
}
"#;
let rules_file = RulesFile::try_from(rules)?;
let values = PathAwareValue::try_from(sgs)?;
let samples = match values {
PathAwareValue::List((_p, v)) => v,
_ => unreachable!(),
};
for (index, each) in samples.iter().enumerate() {
let mut root_context = root_scope(&rules_file, Rc::new(each.clone()));
let status = eval_rules_file(&rules_file, &mut root_context, None)?;
println!("{}", format!("Status {} = {}", index, status).underline());
}
Ok(())
}
#[test]
fn test_s3_bucket_pro_serv() -> Result<()> {
let values = r#"
[
{
"Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : true,
"IgnorePublicAcls" : true,
"RestrictPublicBuckets" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : false,
"BlockPublicPolicy" : true,
"IgnorePublicAcls" : true,
"RestrictPublicBuckets" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : false,
"IgnorePublicAcls" : true,
"RestrictPublicBuckets" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : true,
"IgnorePublicAcls" : false,
"RestrictPublicBuckets" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : true,
"IgnorePublicAcls" : true,
"RestrictPublicBuckets" : false
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : false,
"BlockPublicPolicy" : false,
"IgnorePublicAcls" : false,
"RestrictPublicBuckets" : false
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
},
{ "Resources": {
"S3Bucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"BlockPublicAcls" : true,
"BlockPublicPolicy" : true,
"RestrictPublicBuckets" : true
},
"Metadata": {
"aws:cdk:path": "foo/Counter/S3/Resource"
}
}
}
}]
"#;
let parsed_values = match PathAwareValue::try_from(values)? {
PathAwareValue::List((_, v)) => v,
_ => unreachable!(),
};
let rule = r#"
rule deny_s3_public_bucket {
AWS::S3::Bucket { # this is just a short form notation for Resources.*[ Type == "AWS::S3::Bucket" ]
Properties.BlockPublicAcls NOT EXISTS or
Properties.BlockPublicPolicy NOT EXISTS or
Properties.IgnorePublicAcls NOT EXISTS or
Properties.RestrictPublicBuckets NOT EXISTS or
Properties.BlockPublicAcls == false or
Properties.BlockPublicPolicy == false or
Properties.IgnorePublicAcls == false or
Properties.RestrictPublicBuckets == false
}
}
"#;
let s3_rule = RulesFile::try_from(rule)?;
let expectations = [
Status::FAIL,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
Status::PASS,
];
for (idx, each) in parsed_values.iter().enumerate() {
let mut root_scope = root_scope(&s3_rule, Rc::new(each.clone()));
let status = eval_rules_file(&s3_rule, &mut root_scope, None)?;
assert_eq!(status, expectations[idx]);
}
Ok(())
}
#[test]
fn match_lhs_with_rhs_single_element_pass() -> Result<()> {
let clause = r#"algorithms == ["KMS"]"#;
let value = r#"algorithms: KMS"#;
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value)?)?;
let guard_clause = GuardClause::try_from(clause)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let status = eval_guard_clause(&guard_clause, &mut eval)?;
assert_eq!(status, Status::PASS);
let clause = r#"algorithms == ["KMS", "SSE"]"#;
let value = r#"algorithms: KMS"#;
let path_value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(value)?)?;
let guard_clause = GuardClause::try_from(clause)?;
let mut eval = BasicQueryTesting {
root: Rc::new(path_value),
recorder: None,
};
let status = eval_guard_clause(&guard_clause, &mut eval)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
fn parameterized_evaluations() -> Result<()> {
let parameterized = r###"
rule check_iam_statements(statements) {
%statements {
when Effect == 'Allow' {
Action != '*'
}
}
}
rule iam_checks {
when Resources exists {
Resources[ Type == /IAM::Role/ ] {
check_iam_statements(Properties.AssumeRolePolicyDocument.Statement[*])
}
}
when resourceType == /IAM::Role/ {
check_iam_statements(configuration.assumeRolePolicyDocument.Statement[*])
}
}
"###;
let rules_files = RulesFile::try_from(parameterized)?;
let template_value = r###"
Resources:
iamRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: '*'
Principal: '*'
Resource: '*'
Effect: Allow
"###;
let template =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(template_value)?)?;
let mut eval = root_scope(&rules_files, Rc::new(template));
let status = eval_rules_file(&rules_files, &mut eval, None)?;
let top = eval.reset_recorder().extract();
let mut writer = Writer::default();
crate::commands::validate::print_verbose_tree(&top, &mut writer);
assert_eq!(status, Status::FAIL);
let aws_config_value = r###"
version: 1.2
resourceType: AWS::IAM::Role
configuration:
assumeRolePolicyDocument:
Statement:
- Action: 'sts:AssumeRole'
Principal: '*'
Resource: '*'
Effect: Allow
"###;
let config_value =
PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(aws_config_value)?)?;
let mut eval = root_scope(&rules_files, Rc::new(config_value));
let status = eval_rules_file(&rules_files, &mut eval, None)?;
let top = eval.reset_recorder().extract();
crate::commands::validate::print_verbose_tree(&top, &mut writer);
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn using_resource_names_for_assessment() -> Result<()> {
let resources = r###"
Resources:
s3:
Type: AWS::S3::Bucket
s3Policy:
Type: AWS::S3::BucketPolicy
Properties:
BucketName:
Ref: s3
s3Fail:
Type: AWS::S3::Bucket
"###;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let rules_file = r###"
rule check_s3_has_bucket_policy {
let s3_buckets = Resources[ s3_name | Type == 'AWS::S3::Bucket' ]
let s3_bucket_policy_associations =
some Resources[ Type == 'AWS::S3::BucketPolicy' ].Properties.BucketName.Ref
when %s3_buckets not empty {
# %s3_name == %s3_bucket_policy_associations
%s3_bucket_policy_associations == %s3_name
<<ALL S3 buckets do not have a bucket policy associated>>
}
}
"###;
let rules = RulesFile::try_from(rules_file)?;
let mut eval = root_scope(&rules, Rc::new(value));
let status = eval_rules_file(&rules, &mut eval, None)?;
assert_eq!(status, Status::FAIL);
Ok(())
}
#[test]
#[ignore]
fn test_string_in_comparison() -> Result<()> {
let resources = r#"
Resources:
s3:
Type: AWS::S3::Bucket
s3Policy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
Resource:
Fn::Sub: "aws:arn:s3::${s3}"
"#;
let value = PathAwareValue::try_from(serde_yaml::from_str::<serde_yaml::Value>(resources)?)?;
let rules = r###"
let s3_buckets = Resources[ bucket_names | Type == 'AWS::S3::Bucket' ]
rule s3_policies {
when %s3_buckets not empty {
Resources[ Type == 'AWS::S3::BucketPolicy' ] {
some %bucket_names[*] in Properties.PolicyDocument.Statement.Resource.'Fn::Sub'
}
}
}
"###;
let rules_files = RulesFile::try_from(rules)?;
let mut eval = root_scope(&rules_files, Rc::new(value));
let status = eval_rules_file(&rules_files, &mut eval, None)?;
assert_eq!(status, Status::PASS);
Ok(())
}
#[test]
fn test_searcher() -> Result<()> {
let resources = r#"
Resources:
s3:
Type: AWS::S3::Bucket
s3Policy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Statement:
Resource:
Fn::Sub: "aws:arn:s3::${s3}"
"#;
use grep_matcher::Matcher;
use grep_regex::RegexMatcher;
let matcher = RegexMatcher::new("\\s+(s3):$|\\s+(s3Policy):$").unwrap();
SearcherBuilder::new()
.line_number(true)
.build()
.search_slice(
&matcher,
resources.as_bytes(),
grep_searcher::sinks::UTF8(|_, line| {
let mut captures = matcher.new_captures()?;
let _matched = matcher.captures(line.trim_end().as_bytes(), &mut captures)?;
Ok(true)
}),
)?;
Ok(())
}
#[test]
fn status_combinator() {
let skip: Status = Status::SKIP;
let pass: Status = Status::PASS;
let fail: Status = Status::FAIL;
assert_eq!(skip.and(skip), Status::SKIP);
assert_eq!(skip.and(pass), Status::PASS);
assert_eq!(pass.and(skip), Status::PASS);
assert_eq!(pass.and(pass), Status::PASS);
assert_eq!(fail.and(fail), Status::FAIL);
assert_eq!(fail.and(skip), Status::FAIL);
assert_eq!(skip.and(fail), Status::FAIL);
assert_eq!(pass.and(fail), Status::FAIL);
assert_eq!(fail.and(pass), Status::FAIL);
}