Skip to main content

cfn_guard/commands/
validate.rs

1use std::cmp;
2use std::convert::TryFrom;
3use std::fmt::Debug;
4use std::fs::File;
5use std::io::{BufReader, Read, Write};
6use std::path::{Path, PathBuf};
7use std::rc::Rc;
8use std::str::FromStr;
9
10use clap::{Args, ValueEnum};
11use colored::*;
12use enumflags2::BitFlags;
13use serde::{Deserialize, Serialize};
14
15use crate::commands::files::{alphabetical, iterate_over, last_modified, walk_dir};
16use crate::commands::reporters::validate::structured::StructuredEvaluator;
17use crate::commands::reporters::validate::summary_table::{self, SummaryType};
18use crate::commands::reporters::validate::tf::TfAware;
19use crate::commands::reporters::validate::{cfn, generic_summary};
20use crate::commands::tracker::StatusContext;
21use crate::commands::{
22    Executable, ALPHABETICAL, DATA_FILE_SUPPORTED_EXTENSIONS, ERROR_STATUS_CODE,
23    FAILURE_STATUS_CODE, LAST_MODIFIED, PAYLOAD, PRINT_JSON, REQUIRED_FLAGS, RULES,
24    RULE_FILE_SUPPORTED_EXTENSIONS, SHOW_SUMMARY, STRUCTURED, SUCCESS_STATUS_CODE, TYPE, VERBOSE,
25};
26use crate::rules::errors::{Error, InternalError};
27use crate::rules::eval::eval_rules_file;
28use crate::rules::eval_context::{root_scope, EventRecord};
29use crate::rules::exprs::RulesFile;
30use crate::rules::path_value::traversal::Traversal;
31use crate::rules::path_value::PathAwareValue;
32use crate::rules::{Result, Status};
33use crate::utils::reader::Reader;
34use crate::utils::writer::Writer;
35use wasm_bindgen::prelude::*;
36
37#[derive(Eq, Clone, Debug, PartialEq)]
38pub(crate) struct DataFile {
39    pub(crate) content: String,
40    pub(crate) path_value: PathAwareValue,
41    pub(crate) name: String,
42}
43
44#[derive(Copy, Eq, Clone, Debug, PartialEq)]
45pub(crate) enum Type {
46    CFNTemplate,
47    Generic,
48}
49
50impl From<&str> for Type {
51    fn from(value: &str) -> Self {
52        match value {
53            "CFNTemplate" => Type::CFNTemplate,
54            _ => Type::Generic,
55        }
56    }
57}
58
59#[wasm_bindgen]
60#[allow(clippy::upper_case_acronyms)]
61#[derive(Copy, Eq, Clone, Debug, PartialEq, ValueEnum, Serialize, Default, Deserialize)]
62pub enum OutputFormatType {
63    #[default]
64    SingleLineSummary,
65    JSON,
66    YAML,
67    Junit,
68    Sarif,
69}
70
71#[wasm_bindgen]
72#[derive(Copy, Eq, Clone, Debug, PartialEq, ValueEnum, Serialize, Default, Deserialize)]
73pub enum ShowSummaryType {
74    All,
75    Pass,
76    #[default]
77    Fail,
78    Skip,
79    None,
80}
81
82impl From<&str> for ShowSummaryType {
83    fn from(value: &str) -> Self {
84        match value {
85            "all" => ShowSummaryType::All,
86            "fail" => ShowSummaryType::Fail,
87            "pass" => ShowSummaryType::Pass,
88            "none" => ShowSummaryType::None,
89            "skip" => ShowSummaryType::Skip,
90            _ => unreachable!(),
91        }
92    }
93}
94
95impl OutputFormatType {
96    pub(crate) fn is_structured(&self) -> bool {
97        !matches!(self, Self::SingleLineSummary)
98    }
99}
100
101impl From<&str> for OutputFormatType {
102    fn from(value: &str) -> Self {
103        match value {
104            "single-line-summary" => OutputFormatType::SingleLineSummary,
105            "json" => OutputFormatType::JSON,
106            "junit" => OutputFormatType::Junit,
107            "sarif" => OutputFormatType::Sarif,
108            _ => OutputFormatType::YAML,
109        }
110    }
111}
112
113#[allow(clippy::too_many_arguments)]
114pub(crate) trait Reporter: Debug {
115    fn report(
116        &self,
117        writer: &mut dyn Write,
118        status: Option<Status>,
119        failed_rules: &[&StatusContext],
120        passed_or_skipped: &[&StatusContext],
121        longest_rule_name: usize,
122        rules_file: &str,
123        data_file: &str,
124        data: &Traversal<'_>,
125        output_type: OutputFormatType,
126    ) -> Result<()>;
127
128    fn report_eval<'value>(
129        &self,
130        _write: &mut dyn Write,
131        _status: Status,
132        _root_record: &EventRecord<'value>,
133        _rules_file: &str,
134        _data_file: &str,
135        _data_file_bytes: &str,
136        _data: &Traversal<'value>,
137        _output_type: OutputFormatType,
138    ) -> Result<()> {
139        Ok(())
140    }
141}
142
143#[derive(Debug, Clone, Eq, PartialEq, Args)]
144#[clap(group=clap::ArgGroup::new(REQUIRED_FLAGS).args([RULES.0, PAYLOAD.0]).required(true))]
145#[clap(about=ABOUT)]
146#[clap(arg_required_else_help = true)]
147/// .
148/// The Validate command evaluates rules against data files to determine success or failure
149pub struct Validate {
150    #[arg(short, long, help=RULES_HELP, num_args=0.., conflicts_with=PAYLOAD.0)]
151    /// a list of paths that point to rule files, or a directory containing rule files on a local machine. Only files that end with .guard or .ruleset will be evaluated
152    /// conflicts with payload
153    pub(crate) rules: Vec<String>,
154    #[arg(short, long, help=DATA_HELP, num_args=0.., conflicts_with=PAYLOAD.0)]
155    /// a list of paths that point to data files, or a directory containing data files  for the rules to be evaluated against. Only JSON, or YAML files will be used
156    /// conflicts with payload
157    pub(crate) data: Vec<String>,
158    #[arg(short, long, help=INPUT_PARAMETERS_HELP, num_args=0..)]
159    /// a list of paths that point to data files, or a directory containing data files to be merged with the data argument and then the  rules will be evaluated against them. Only JSON, or YAML files will be used
160    pub(crate) input_params: Vec<String>,
161    #[arg(name=TYPE.0, short, long, help=TEMPLATE_TYPE_HELP, value_parser=TEMPLATE_TYPE)]
162    #[deprecated(since = "3.0.0", note = "this field does not get evaluated")]
163    pub(crate) template_type: Option<String>,
164    #[arg(short, long, help=OUTPUT_FORMAT_HELP, value_enum, default_value_t=OutputFormatType::SingleLineSummary)]
165    /// Specify the format in which the output should be displayed
166    /// default is single-line-summary
167    /// if junit is used, `structured` attributed must be set to true
168    pub(crate) output_format: OutputFormatType,
169    #[arg(short=SHOW_SUMMARY.1, long, help=SHOW_SUMMARY_HELP, value_enum, default_values_t=vec![ShowSummaryType::Fail], value_delimiter=',')]
170    /// Controls if the summary table needs to be displayed. --show-summary fail (default) or --show-summary pass,fail (only show rules that did pass/fail) or --show-summary none (to turn it off) or --show-summary all (to show all the rules that pass, fail or skip)
171    /// default is failed
172    /// must be set to none if used together with the structured flag
173    pub(crate) show_summary: Vec<ShowSummaryType>,
174    #[arg(short, long, help=ALPHABETICAL_HELP, conflicts_with=LAST_MODIFIED.0)]
175    /// Validate files in a directory ordered alphabetically, conflicts with `last_modified` field
176    pub(crate) alphabetical: bool,
177    #[arg(name="last-modified", short=LAST_MODIFIED.1, long, help=LAST_MODIFIED_HELP, conflicts_with=ALPHABETICAL.0)]
178    /// Validate files in a directory ordered by last modified times, conflicts with `alphabetical` field
179    pub(crate) last_modified: bool,
180    #[arg(short, long, help=VERBOSE_HELP)]
181    /// Output verbose logging, conflicts with `structured` field
182    /// default is false
183    pub(crate) verbose: bool,
184    #[arg(name="print-json", short=PRINT_JSON.1, long, help=PRINT_JSON_HELP)]
185    /// Print the parse tree in a json format. This can be used to get more details on how the clauses were evaluated
186    /// conflicts with the `structured` attribute
187    /// default is false
188    pub(crate) print_json: bool,
189    #[arg(short=PAYLOAD.1, long, help=PAYLOAD_HELP)]
190    /// Tells the command that rules, and data will be passed via a reader, as a json payload.
191    /// Conflicts with both rules, and data
192    /// default is false
193    pub(crate) payload: bool,
194    #[arg(short=STRUCTURED.1, long, help=STRUCTURED_HELP, conflicts_with_all=vec![PRINT_JSON.0, VERBOSE.0])]
195    /// Prints the output which must be specified to JSON/YAML/JUnit in a structured format
196    /// Conflicts with the following attributes `verbose`, `print-json`, `output-format` when set
197    /// to "single-line-summary", show-summary when set to anything other than "none"
198    /// default is false
199    pub(crate) structured: bool,
200}
201
202impl Validate {
203    fn validate_construct(
204        &self,
205        summary_type: &BitFlags<SummaryType, u8>,
206    ) -> crate::rules::Result<()> {
207        if self.structured && !summary_type.is_empty() {
208            return Err(Error::IllegalArguments(String::from(
209                "Cannot provide a summary-type other than `none` when the `structured` flag is present",
210            )));
211        } else if self.structured
212            && matches!(self.output_format, OutputFormatType::SingleLineSummary)
213        {
214            return Err(Error::IllegalArguments(String::from(
215                "single-line-summary is not able to be used when the `structured` flag is present",
216            )));
217        }
218
219        if matches!(self.output_format, OutputFormatType::Junit) && !self.structured {
220            return Err(Error::IllegalArguments(String::from(
221                "the structured flag must be set when output is set to junit",
222            )));
223        }
224
225        if matches!(self.output_format, OutputFormatType::Sarif) && !self.structured {
226            return Err(Error::IllegalArguments(String::from(
227                "the structured flag must be set when output is set to sarif",
228            )));
229        }
230
231        Ok(())
232    }
233
234    fn get_comparator(&self) -> fn(&walkdir::DirEntry, &walkdir::DirEntry) -> cmp::Ordering {
235        match self.last_modified {
236            true => last_modified,
237            false => alphabetical,
238        }
239    }
240}
241
242impl Executable for Validate {
243    /// .
244    /// evaluates the given rules, against the given data files.
245    ///
246    /// This function will return an error if
247    /// - conflicting attributes have been set
248    /// - any of the specified paths do not exist
249    /// - parse errors occur in the rule file
250    /// - illegal json or yaml syntax present in any of the data/input parameter files
251    /// - both rules is empty, and payload is false
252    #[allow(deprecated)]
253    fn execute(&self, writer: &mut Writer, reader: &mut Reader) -> Result<i32> {
254        let summary_type = self
255            .show_summary
256            .iter()
257            .fold(BitFlags::empty(), |mut st, elem| {
258                match elem {
259                    ShowSummaryType::Pass => st.insert(SummaryType::PASS),
260                    ShowSummaryType::Fail => st.insert(SummaryType::FAIL),
261                    ShowSummaryType::Skip => st.insert(SummaryType::SKIP),
262                    ShowSummaryType::None => return BitFlags::empty(),
263                    ShowSummaryType::All => {
264                        st.insert(SummaryType::PASS | SummaryType::FAIL | SummaryType::SKIP)
265                    }
266                };
267                st
268            });
269
270        self.validate_construct(&summary_type)?;
271
272        let cmp = self.get_comparator();
273
274        let data_files = match self.data.is_empty() {
275            false => {
276                let mut streams = Vec::new();
277
278                for file_or_dir in &self.data {
279                    validate_path(file_or_dir)?;
280                    let base = resolve_path(file_or_dir)?;
281                    for file in walk_dir(base, cmp) {
282                        if file.path().is_file() {
283                            let name = file
284                                .path()
285                                // path output occasionally includes double slashes '//'
286                                // without calling canonicalize()
287                                .canonicalize()?
288                                .to_str()
289                                .map_or("".to_string(), String::from);
290                            if has_a_supported_extension(&name, &DATA_FILE_SUPPORTED_EXTENSIONS) {
291                                let mut content = String::new();
292                                let mut reader = BufReader::new(File::open(file.path())?);
293                                reader.read_to_string(&mut content)?;
294
295                                let data_file = build_data_file(content, name)?;
296                                streams.push(data_file);
297                            }
298                        }
299                    }
300                }
301                streams
302            }
303            true => {
304                if !self.rules.is_empty() {
305                    let mut content = String::new();
306                    reader.read_to_string(&mut content)?;
307
308                    let data_file = build_data_file(content, "STDIN".to_string())?;
309
310                    vec![data_file]
311                } else {
312                    vec![]
313                } // expect Payload, since rules aren't specified
314            }
315        };
316
317        let extra_data = match self.input_params.is_empty() {
318            false => {
319                let mut primary_path_value: Option<PathAwareValue> = None;
320
321                for file_or_dir in &self.input_params {
322                    validate_path(file_or_dir)?;
323                    let base = resolve_path(file_or_dir)?;
324
325                    for file in walk_dir(base, cmp) {
326                        if file.path().is_file() {
327                            let name = file
328                                .file_name()
329                                .to_str()
330                                .map_or("".to_string(), String::from);
331
332                            if has_a_supported_extension(&name, &DATA_FILE_SUPPORTED_EXTENSIONS) {
333                                let mut content = String::new();
334                                let mut reader = BufReader::new(File::open(file.path())?);
335                                reader.read_to_string(&mut content)?;
336
337                                let DataFile { path_value, .. } = build_data_file(content, name)?;
338
339                                primary_path_value = match primary_path_value {
340                                    Some(current) => Some(current.merge(path_value)?),
341                                    None => Some(path_value),
342                                };
343                            }
344                        }
345                    }
346                }
347                primary_path_value
348            }
349            true => None,
350        };
351
352        let mut exit_code = SUCCESS_STATUS_CODE;
353
354        let data_type = self
355            .template_type
356            .as_ref()
357            .map_or(Type::Generic, |t| Type::from(t.as_str()));
358
359        let cmp = if self.last_modified {
360            last_modified
361        } else {
362            alphabetical
363        };
364
365        if !self.rules.is_empty() {
366            let mut rules = Vec::new();
367
368            for file_or_dir in &self.rules {
369                validate_path(file_or_dir)?;
370                let base = resolve_path(file_or_dir)?;
371
372                if base.is_file() {
373                    rules.push(base.clone())
374                } else {
375                    for entry in walk_dir(base, cmp) {
376                        if entry.path().is_file()
377                            && entry
378                                .path()
379                                .file_name()
380                                .and_then(|s| s.to_str())
381                                .map_or(false, |s| {
382                                    has_a_supported_extension(s, &RULE_FILE_SUPPORTED_EXTENSIONS)
383                                })
384                        {
385                            rules.push(entry.path().to_path_buf());
386                        }
387                    }
388                }
389            }
390
391            exit_code = match self.structured {
392                true => {
393                    let rule_info = get_rule_info(&rules, writer)?;
394                    let mut evaluator = StructuredEvaluator {
395                        rule_info: &rule_info,
396                        input_params: extra_data,
397                        data: data_files,
398                        output: self.output_format,
399                        writer,
400                        exit_code,
401                    };
402                    evaluator.evaluate()?
403                }
404
405                false => {
406                    for each_file_content in iterate_over(&rules, |content, file| {
407                        Ok(RuleFileInfo {
408                            content,
409                            file_name: get_file_name(file, file),
410                        })
411                    }) {
412                        match each_file_content {
413                            Err(e) => {
414                                writer.write_err(format!("Unable read content from file {e}"))?
415                            }
416                            Ok(rule) => {
417                                let status = evaluate_rule(
418                                    data_type,
419                                    self.output_format,
420                                    &extra_data,
421                                    &data_files,
422                                    rule,
423                                    self.verbose,
424                                    self.print_json,
425                                    summary_type,
426                                    writer,
427                                )?;
428
429                                if status != SUCCESS_STATUS_CODE {
430                                    exit_code = status
431                                }
432                            }
433                        }
434                    }
435                    exit_code
436                }
437            };
438        } else if self.payload {
439            let mut context = String::new();
440            reader.read_to_string(&mut context)?;
441            let payload = deserialize_payload(&context)?;
442
443            let data_collection = payload.list_of_data.iter().enumerate().try_fold(
444                vec![],
445                |mut data_collection, (i, data)| -> Result<Vec<DataFile>> {
446                    let content = data.to_string();
447                    let name = format!("DATA_STDIN[{}]", i + 1);
448                    let data_file = build_data_file(content, name)?;
449
450                    data_collection.push(data_file);
451
452                    Ok(data_collection)
453                },
454            )?;
455
456            let rule_info = payload
457                .list_of_rules
458                .iter()
459                .enumerate()
460                .map(|(i, rules)| RuleFileInfo {
461                    content: rules.to_string(),
462                    file_name: format!("RULES_STDIN[{}]", i + 1),
463                })
464                .collect::<Vec<_>>();
465
466            exit_code = match self.structured {
467                true => {
468                    let mut evaluator = StructuredEvaluator {
469                        rule_info: &rule_info,
470                        input_params: extra_data,
471                        data: data_collection,
472                        output: self.output_format,
473                        writer,
474                        exit_code,
475                    };
476                    evaluator.evaluate()?
477                }
478                false => {
479                    for rule in rule_info {
480                        let status = evaluate_rule(
481                            data_type,
482                            self.output_format,
483                            &None,
484                            &data_collection,
485                            rule,
486                            self.verbose,
487                            self.print_json,
488                            summary_type,
489                            writer,
490                        )?;
491
492                        if status != SUCCESS_STATUS_CODE {
493                            exit_code = status;
494                        }
495                    }
496                    exit_code
497                }
498            };
499        } else {
500            unreachable!()
501        }
502
503        Ok(exit_code)
504    }
505}
506
507#[derive(Deserialize, Debug)]
508pub(crate) struct Payload {
509    #[serde(rename = "rules")]
510    list_of_rules: Vec<String>,
511    #[serde(rename = "data")]
512    list_of_data: Vec<String>,
513}
514
515const ABOUT: &str = r#"Evaluates rules against the data files to determine success or failure.
516You can point rules flag to a rules directory and point data flag to a data directory.
517When pointed to a directory it will read all rules in the directory file and evaluate
518them against the data files found in the directory. The command can also point to a
519single file and it would work as well.
520Note - When pointing the command to a directory, the directory may not contain a mix of
521rules and data files. The directory being pointed to must contain only data files,
522or rules files.
523"#;
524
525// const SHOW_SUMMARY_VALUE_TYPE: [&str; 5] = ["none", "all", "pass", "fail", "skip"];
526const TEMPLATE_TYPE: [&str; 1] = ["CFNTemplate"];
527const RULES_HELP: &str = "Provide a rules file or a directory of rules files. Supports passing multiple values by using this option repeatedly.\
528                          \nExample:\n --rules rule1.guard --rules ./rules-dir1 --rules rule2.guard\
529                          \nFor directory arguments such as `rules-dir1` above, scanning is only supported for files with following extensions: .guard, .ruleset";
530const DATA_HELP: &str = "Provide a data file or directory of data files in JSON or YAML. Supports passing multiple values by using this option repeatedly.\
531                          \nExample:\n --data template1.yaml --data ./data-dir1 --data template2.yaml\
532                          \nFor directory arguments such as `data-dir1` above, scanning is only supported for files with following extensions: .yaml, .yml, .json, .jsn, .template";
533const INPUT_PARAMETERS_HELP: &str = "Provide a parameter file or directory of parameter files in JSON or YAML that specifies any additional parameters to use along with data files to be used as a combined context. \
534                           All the parameter files passed as input get merged and this combined context is again merged with each file passed as an argument for `data`. Due to this, every file is \
535                           expected to contain mutually exclusive properties, without any overlap. Supports passing multiple values by using this option repeatedly.\
536                          \nExample:\n --input-parameters param1.yaml --input-parameters ./param-dir1 --input-parameters param2.yaml\
537                          \nFor directory arguments such as `param-dir1` above, scanning is only supported for files with following extensions: .yaml, .yml, .json, .jsn, .template";
538const TEMPLATE_TYPE_HELP: &str =
539    "Specify the type of data file used for improved messaging - ex: CFNTemplate";
540pub(crate) const OUTPUT_FORMAT_HELP: &str =
541    "Specify the format in which the output should be displayed";
542const SHOW_SUMMARY_HELP: &str = "Controls if the summary table needs to be displayed. --show-summary fail (default) or --show-summary pass,fail (only show rules that did pass/fail) or --show-summary none (to turn it off) or --show-summary all (to show all the rules that pass, fail or skip)";
543const ALPHABETICAL_HELP: &str = "Validate files in a directory ordered alphabetically";
544const LAST_MODIFIED_HELP: &str = "Validate files in a directory ordered by last modified times";
545const VERBOSE_HELP: &str = "Verbose logging";
546const PRINT_JSON_HELP: &str = "Print the parse tree in a json format. This can be used to get more details on how the clauses were evaluated";
547const PAYLOAD_HELP: &str = "Provide rules and data in the following JSON format via STDIN,\n{\"rules\":[\"<rules 1>\", \"<rules 2>\", ...], \"data\":[\"<data 1>\", \"<data 2>\", ...]}, where,\n- \"rules\" takes a list of string \
548                version of rules files as its value and\n- \"data\" takes a list of string version of data files as it value.\nWhen --payload is specified --rules and --data cannot be specified.";
549const STRUCTURED_HELP: &str = "Print out a list of structured and valid JSON/YAML. This argument conflicts with the following arguments: \nverbose \n print-json \n show-summary: all/fail/pass/skip \noutput-format: single-line-summary";
550
551#[allow(clippy::too_many_arguments)]
552fn evaluate_rule(
553    data_type: Type,
554    output: OutputFormatType,
555    extra_data: &Option<PathAwareValue>,
556    data_files: &Vec<DataFile>,
557    rule: RuleFileInfo,
558    verbose: bool,
559    print_json: bool,
560    summary_type: BitFlags<SummaryType>,
561    writer: &mut Writer,
562) -> Result<i32> {
563    let RuleFileInfo { content, file_name } = &rule;
564    match parse_rules(content, file_name) {
565        Err(e) => {
566            writer.write_err(format!(
567                "Parsing error handling rule file = {}, Error = {e}\n---",
568                file_name.underline(),
569            ))?;
570
571            return Ok(ERROR_STATUS_CODE);
572        }
573
574        Ok(Some(rule)) => {
575            let status = evaluate_against_data_input(
576                data_type,
577                output,
578                extra_data,
579                data_files,
580                &rule,
581                file_name,
582                verbose,
583                print_json,
584                summary_type,
585                writer,
586            )?;
587
588            if status == Status::FAIL {
589                return Ok(FAILURE_STATUS_CODE);
590            }
591        }
592        Ok(None) => return Ok(SUCCESS_STATUS_CODE),
593    }
594
595    Ok(SUCCESS_STATUS_CODE)
596}
597
598pub(crate) fn validate_path(base: &str) -> Result<()> {
599    #[cfg(target_arch = "wasm32")]
600    {
601        path_exists(base).map_err(Error::from).and_then(|exists| {
602            if exists {
603                Ok(())
604            } else {
605                Err(Error::FileNotFoundError(base.to_string()))
606            }
607        })
608    }
609
610    #[cfg(not(target_arch = "wasm32"))]
611    {
612        if Path::new(base).exists() {
613            Ok(())
614        } else {
615            Err(Error::FileNotFoundError(base.to_string()))
616        }
617    }
618}
619
620#[wasm_bindgen]
621extern "C" {
622    type Buffer;
623}
624
625#[wasm_bindgen(module = "fs")]
626extern "C" {
627    #[wasm_bindgen(js_name = existsSync, catch)]
628    fn path_exists(path: &str) -> Result<bool>;
629    #[wasm_bindgen(js_name = readDirSync, catch)]
630    fn read_dir(path: &str) -> Result<String>;
631}
632
633#[wasm_bindgen(module = "path")]
634extern "C" {
635    #[wasm_bindgen(js_name = resolve, catch)]
636    fn path_resolve(path: &str) -> Result<String>;
637}
638
639pub fn resolve_path(file_or_dir: &str) -> Result<PathBuf> {
640    #[cfg(target_arch = "wasm32")]
641    {
642        Ok(PathBuf::from_str(&path_resolve(file_or_dir)?)?)
643    }
644
645    #[cfg(not(target_arch = "wasm32"))]
646    {
647        Ok(PathBuf::from_str(file_or_dir)?)
648    }
649}
650
651fn deserialize_payload(payload: &str) -> Result<Payload> {
652    match serde_json::from_str::<Payload>(payload) {
653        Ok(value) => Ok(value),
654        Err(e) => Err(Error::ParseError(e.to_string())),
655    }
656}
657
658pub fn parse_rules<'r>(
659    rules_file_content: &'r str,
660    rules_file_name: &'r str,
661) -> Result<Option<RulesFile<'r>>> {
662    let span = crate::rules::parser::Span::new_extra(rules_file_content, rules_file_name);
663    crate::rules::parser::rules_file(span)
664}
665
666//
667// https://vallentin.dev/2019/05/14/pretty-print-tree
668//
669#[allow(clippy::uninlined_format_args)]
670fn pprint_tree(current: &EventRecord<'_>, prefix: String, last: bool, writer: &mut Writer) {
671    let prefix_current = if last { "`- " } else { "|- " };
672    writeln!(writer, "{}{}{}", prefix, prefix_current, current)
673        .expect("Unable to write to the output");
674
675    let prefix_child = if last { "   " } else { "|  " };
676    let prefix = prefix + prefix_child;
677    if !current.children.is_empty() {
678        let last_child = current.children.len() - 1;
679        for (i, child) in current.children.iter().enumerate() {
680            pprint_tree(child, prefix.clone(), i == last_child, writer);
681        }
682    }
683}
684
685pub(crate) fn print_verbose_tree(root: &EventRecord<'_>, writer: &mut Writer) {
686    pprint_tree(root, "".to_string(), true, writer);
687}
688
689#[allow(clippy::too_many_arguments)]
690fn evaluate_against_data_input<'r>(
691    _data_type: Type,
692    output: OutputFormatType,
693    extra_data: &Option<PathAwareValue>,
694    data_files: &'r Vec<DataFile>,
695    rules: &RulesFile<'_>,
696    rules_file_name: &'r str,
697    verbose: bool,
698    print_json: bool,
699    summary_table: BitFlags<SummaryType>,
700    mut write_output: &mut Writer,
701) -> Result<Status> {
702    let mut overall = Status::PASS;
703    let generic: Box<dyn Reporter> =
704        Box::new(generic_summary::GenericSummary::new(summary_table)) as Box<dyn Reporter>;
705    let tf: Box<dyn Reporter> = Box::new(TfAware::new_with(generic.as_ref())) as Box<dyn Reporter>;
706    let cfn: Box<dyn Reporter> =
707        Box::new(cfn::CfnAware::new_with(tf.as_ref())) as Box<dyn Reporter>;
708
709    let reporter: Box<dyn Reporter> = if summary_table.is_empty() {
710        cfn
711    } else {
712        Box::new(summary_table::SummaryTable::new(
713            summary_table,
714            cfn.as_ref(),
715        )) as Box<dyn Reporter>
716    };
717
718    for file in data_files {
719        let each = match &extra_data {
720            Some(data) => data.clone().merge(file.path_value.clone())?,
721            None => file.path_value.clone(),
722        };
723        let traversal = Traversal::from(&each);
724        let mut root_scope = root_scope(rules, Rc::new(each.clone()));
725        let status = eval_rules_file(rules, &mut root_scope, Some(&file.name))?;
726
727        let root_record = root_scope.reset_recorder().extract();
728
729        reporter.report_eval(
730            &mut write_output,
731            status,
732            &root_record,
733            rules_file_name,
734            &file.name,
735            &file.content,
736            &traversal,
737            output,
738        )?;
739
740        if verbose {
741            print_verbose_tree(&root_record, write_output);
742        }
743
744        if print_json {
745            writeln!(
746                write_output,
747                "{}",
748                serde_json::to_string_pretty(&root_record)?
749            )
750            .expect("Unable to write to the output");
751        }
752
753        if status == Status::FAIL {
754            overall = Status::FAIL
755        }
756    }
757    Ok(overall)
758}
759
760fn build_data_file(content: String, name: String) -> Result<DataFile> {
761    if content.trim().is_empty() {
762        return Err(Error::ParseError(format!(
763            "Unable to parse a template from data file: {name} is empty"
764        )));
765    }
766
767    let path_value = match crate::rules::values::read_from(&content) {
768        Ok(value) => PathAwareValue::try_from(value)?,
769        Err(e) => {
770            if matches!(e, Error::InternalError(InternalError::InvalidKeyType(..))) {
771                return Err(Error::ParseError(e.to_string()));
772            }
773
774            let str_len: usize = cmp::min(content.len(), 100);
775            return Err(Error::ParseError(format!(
776                "Error encountered while parsing data file: {name}, data beginning with \n{}\n ...",
777                &content[..str_len]
778            )));
779        }
780    };
781
782    Ok(DataFile {
783        name,
784        path_value,
785        content,
786    })
787}
788
789fn has_a_supported_extension(name: &str, extensions: &[&str]) -> bool {
790    extensions.iter().any(|extension| name.ends_with(extension))
791}
792
793fn get_file_name(file: &Path, base: &Path) -> String {
794    let empty_path = Path::new("");
795    match file.strip_prefix(base) {
796        Ok(path) => {
797            if path == empty_path {
798                file.file_name().unwrap().to_str().unwrap().to_string()
799            } else {
800                format!("{}", path.display())
801            }
802        }
803        Err(_) => format!("{}", file.display()),
804    }
805}
806
807fn get_rule_info(rules: &[PathBuf], writer: &mut Writer) -> Result<Vec<RuleFileInfo>> {
808    iterate_over(rules, |content, file| {
809        Ok(RuleFileInfo {
810            content,
811            file_name: get_file_name(file, file),
812        })
813    })
814    .try_fold(vec![], |mut res, rule| -> Result<Vec<RuleFileInfo>> {
815        if let Err(e) = rule {
816            writer.write_err(format!("Unable to read content from file {e}"))?;
817            return Err(e);
818        }
819
820        res.push(rule?);
821        Ok(res)
822    })
823}
824
825pub(crate) struct RuleFileInfo {
826    pub(crate) content: String,
827    pub(crate) file_name: String,
828}
829
830#[cfg(test)]
831#[path = "validate_tests.rs"]
832mod validate_tests;