cfn-guard 3.2.0

AWS CloudFormation Guard is an open-source general-purpose policy-as-code evaluation tool. It provides developers with a simple-to-use, yet powerful and expressive domain-specific language (DSL) to define policies and enables developers to validate JSON- or YAML- formatted structured data with those policies.
Documentation
use crate::commands::tracker::StatusContext;
use crate::commands::validate::{OutputFormatType, Reporter};
use crate::rules::eval_context::EventRecord;
use crate::rules::path_value::traversal::Traversal;
use crate::rules::values::CmpOperator;
use crate::rules::{
    BlockCheck, ClauseCheck, ComparisonClauseCheck, NamedStatus, QueryResult, RecordType, Status,
    TypeBlockCheck, UnaryValueCheck, ValueCheck,
};
use std::io::Write;
use std::rc::Rc;

#[derive(Debug)]
pub(crate) struct ConsoleReporter {}

//
// https://vallentin.dev/2019/05/14/pretty-print-tree
//
fn pprint_failed_sub_tree(
    current: &EventRecord<'_>,
    prefix: String,
    last: bool,
    rules_file_name: &str,
    data_file_name: &str,
    writer: &mut dyn Write,
) -> crate::rules::Result<()> {
    let prefix_current = if last { "`- " } else { "|- " };
    let increment_prefix = match &current.container {
        Some(RecordType::TypeBlock(Status::FAIL))
        | Some(RecordType::BlockGuardCheck(BlockCheck {
            status: Status::FAIL,
            ..
        }))
        | Some(RecordType::GuardClauseBlockCheck(BlockCheck {
            status: Status::FAIL,
            ..
        }))
        | Some(RecordType::TypeCheck(TypeBlockCheck {
            block:
                BlockCheck {
                    status: Status::FAIL,
                    ..
                },
            ..
        }))
        | Some(RecordType::WhenCheck(BlockCheck {
            status: Status::FAIL,
            ..
        })) => false,
        Some(RecordType::FileCheck(NamedStatus {
            status: Status::FAIL,
            ..
        }))
        | Some(RecordType::RuleCheck(NamedStatus {
            status: Status::FAIL,
            ..
        }))
        | Some(RecordType::Disjunction(BlockCheck {
            status: Status::FAIL,
            ..
        })) => {
            writeln!(writer, "{}{}{}", prefix, prefix_current, current)?;
            true
        }

        Some(RecordType::ClauseValueCheck(check)) => {
            match check {
                ClauseCheck::NoValueForEmptyCheck(msg) => {
                    let custom_message = msg
                        .as_ref()
                        .map_or("".to_string(), |s| s.replace('\n', ";"));

                    writeln!(
                        writer,
                        "{}{}Check was not compliant as variable in context [{}] was not empty. Message [{}]",
                        prefix,
                        prefix_current,
                        current.context,
                        custom_message
                    )?;
                }

                ClauseCheck::Success => {}

                ClauseCheck::DependentRule(missing) => {
                    writeln!(
                        writer,
                        "{prefix}{prefix_current}Check was not compliant as dependent rule [{rule}] evaluated to FAIL in [{file}]. Context [{cxt}]",
                        prefix=prefix,
                        prefix_current=prefix_current,
                        rule=missing.rule,
                        file=rules_file_name,
                        cxt=current.context
                    )?;
                }

                ClauseCheck::MissingBlockValue(missing) => {
                    let (property, far) = match &missing.from {
                        QueryResult::UnResolved(ur) => {
                            (ur.remaining_query.as_str(), Rc::clone(&ur.traversed_to))
                        }
                        _ => unreachable!(),
                    };
                    writeln!(
                        writer,
                        "{}{}Check was not compliant as property [{}] is missing in data [{}]. Value traversed to [{}]",
                        prefix,
                        prefix_current,
                        property,
                        data_file_name,
                        far
                    )?;
                }

                ClauseCheck::Unary(UnaryValueCheck {
                    comparison: (cmp, not),
                    value:
                        ValueCheck {
                            status: Status::FAIL,
                            from,
                            message,
                            custom_message,
                        },
                }) => {
                    let cmp_msg = match cmp {
                        CmpOperator::Exists => {
                            if *not {
                                "existed"
                            } else {
                                "did not exist"
                            }
                        }
                        CmpOperator::Empty => {
                            if *not {
                                "was empty"
                            } else {
                                "was not empty"
                            }
                        }
                        CmpOperator::IsList => {
                            if *not {
                                "was a list "
                            } else {
                                "was not list"
                            }
                        }
                        CmpOperator::IsMap => {
                            if *not {
                                "was a struct"
                            } else {
                                "was not struct"
                            }
                        }
                        CmpOperator::IsString => {
                            if *not {
                                "was a string "
                            } else {
                                "was not string"
                            }
                        }
                        _ => unreachable!(),
                    };

                    let custom_message = custom_message.as_ref().map_or("".to_string(), |s| {
                        format!(" Message = [{}]", s.replace('\n', ";"))
                    });

                    let error_message = message
                        .as_ref()
                        .map_or("".to_string(), |s| format!(" Error = [{}]", s));

                    match from {
                        QueryResult::Literal(_) => unreachable!(),
                        QueryResult::Resolved(res) => {
                            writeln!(
                                writer,
                                "{}{}Check was not compliant as property [{prop}] {cmp_msg}.{err}{msg}",
                                prefix,
                                prefix_current,
                                prop=res.self_path(),
                                cmp_msg=cmp_msg,
                                err=error_message,
                                msg=custom_message
                            )?;
                        }

                        QueryResult::UnResolved(unres) => {
                            writeln!(
                                writer,
                                "{}{}Check was not compliant as property [{remain}] is missing. Value traversed to [{tr}].{err}{msg}",
                                prefix,
                                prefix_current,
                                remain=unres.remaining_query,
                                tr=unres.traversed_to,
                                err=error_message,
                                msg=custom_message
                            )?;
                        }
                    }
                }

                ClauseCheck::Comparison(ComparisonClauseCheck {
                    custom_message,
                    message,
                    comparison: (cmp, not),
                    from,
                    status: Status::FAIL,
                    to,
                }) => {
                    let custom_message = custom_message.as_ref().map_or("".to_string(), |s| {
                        format!(" Message = [{}]", s.replace('\n', ";"))
                    });

                    let error_message = message
                        .as_ref()
                        .map_or("".to_string(), |s| format!(" Error = [{}]", s));

                    let to_result = match to {
                        Some(to) => match to {
                            QueryResult::Literal(_) => unreachable!(),
                            QueryResult::Resolved(to_res) => Some(Rc::clone(to_res)),

                            QueryResult::UnResolved(to_unres) => {
                                writeln!(
                                        writer,
                                        "{}{}Check was not compliant as property [{remain}] to compare to is missing. Value traversed to [{to}].{err}{msg}",
                                        prefix,
                                        prefix_current,
                                        remain=to_unres.remaining_query,
                                        to=to_unres.traversed_to,
                                        err=error_message,
                                        msg=custom_message
                                    )?;
                                return Ok(());
                            }
                        },

                        None => None,
                    };

                    match from {
                        QueryResult::Literal(_) => unreachable!(),
                        QueryResult::UnResolved(to_unres) => {
                            writeln!(
                                writer,
                                "{}{}Check was not compliant as property [{remain}] to compare from is missing. Value traversed to [{to}].{err}{msg}",
                                prefix,
                                prefix_current,
                                remain=to_unres.remaining_query,
                                to=to_unres.traversed_to,
                                err=error_message,
                                msg=custom_message
                            )?;
                        }

                        QueryResult::Resolved(res) => {
                            writeln!(
                                writer,
                                "{}{}Check was not compliant as property value [{from}] {op_msg} value [{to}].{err}{msg}",
                                prefix,
                                prefix_current,
                                from=res,
                                to=to_result.map_or("NULL".to_string(), |t| format!("{}", t)),
                                op_msg=match cmp {
                                    CmpOperator::Eq => if *not { "equal to" } else { "not equal to" },
                                    CmpOperator::Le => if *not { "less than equal to" } else { "not less than equal to" },
                                    CmpOperator::Lt => if *not { "less than" } else { "not less than" },
                                    CmpOperator::Ge => if *not { "greater than equal to" } else { "not greater than equal" },
                                    CmpOperator::Gt => if *not { "greater than" } else { "not greater than" },
                                    CmpOperator::In => if *not { "in" } else { "not in" },
                                    _ => unreachable!()
                                },
                                err=error_message,
                                msg=custom_message
                            )?;
                        }
                    }
                }

                _ => return Ok(()), // Success skip
            }
            false
        }

        _ => return Ok(()),
    };

    let prefix = if increment_prefix {
        let prefix_child = if last { "   " } else { "|  " };
        prefix + prefix_child
    } else {
        prefix
    };

    if !current.children.is_empty() {
        let last_child = current.children.len() - 1;
        for (i, child) in current.children.iter().enumerate() {
            pprint_failed_sub_tree(
                child,
                prefix.clone(),
                i == last_child,
                rules_file_name,
                data_file_name,
                writer,
            )?;
        }
    }
    Ok(())
}

impl Reporter for ConsoleReporter {
    fn report(
        &self,
        _writer: &mut dyn Write,
        _status: Option<Status>,
        _failed_rules: &[&StatusContext],
        _passed_or_skipped: &[&StatusContext],
        _longest_rule_name: usize,
        _rules_file: &str,
        _data_file: &str,
        _data: &Traversal<'_>,
        _output_format_type: OutputFormatType,
    ) -> crate::rules::Result<()> {
        Ok(())
    }

    fn report_eval<'value>(
        &self,
        _write: &mut dyn Write,
        _status: Status,
        _root_record: &EventRecord<'value>,
        _rules_file: &str,
        _data_file: &str,
        _data_file_bytes: &str,
        _data: &Traversal<'value>,
        _output_type: OutputFormatType,
    ) -> crate::rules::Result<()> {
        pprint_failed_sub_tree(
            _root_record,
            "".to_string(),
            true,
            _rules_file,
            _data_file,
            _write,
        )
    }
}