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)]
147pub struct Validate {
150 #[arg(short, long, help=RULES_HELP, num_args=0.., conflicts_with=PAYLOAD.0)]
151 pub(crate) rules: Vec<String>,
154 #[arg(short, long, help=DATA_HELP, num_args=0.., conflicts_with=PAYLOAD.0)]
155 pub(crate) data: Vec<String>,
158 #[arg(short, long, help=INPUT_PARAMETERS_HELP, num_args=0..)]
159 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 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 pub(crate) show_summary: Vec<ShowSummaryType>,
174 #[arg(short, long, help=ALPHABETICAL_HELP, conflicts_with=LAST_MODIFIED.0)]
175 pub(crate) alphabetical: bool,
177 #[arg(name="last-modified", short=LAST_MODIFIED.1, long, help=LAST_MODIFIED_HELP, conflicts_with=ALPHABETICAL.0)]
178 pub(crate) last_modified: bool,
180 #[arg(short, long, help=VERBOSE_HELP)]
181 pub(crate) verbose: bool,
184 #[arg(name="print-json", short=PRINT_JSON.1, long, help=PRINT_JSON_HELP)]
185 pub(crate) print_json: bool,
189 #[arg(short=PAYLOAD.1, long, help=PAYLOAD_HELP)]
190 pub(crate) payload: bool,
194 #[arg(short=STRUCTURED.1, long, help=STRUCTURED_HELP, conflicts_with_all=vec![PRINT_JSON.0, VERBOSE.0])]
195 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 #[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 .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 } }
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
525const 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#[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;