Skip to main content

cfn_guard/
lib.rs

1// Copyright Amazon Web Services, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4/* require return types marked as must_use to be used (such as Result types) */
5#![deny(unused_must_use)]
6
7pub mod commands;
8mod rules;
9pub mod utils;
10
11pub use crate::commands::helper::{validate_and_return_json as run_checks, ValidateInput};
12use crate::commands::parse_tree::ParseTree;
13use crate::commands::rulegen::Rulegen;
14use crate::commands::test::Test;
15use crate::commands::validate::{OutputFormatType, ShowSummaryType, Validate};
16use crate::commands::Executable;
17pub use crate::rules::errors::Error;
18
19#[cfg(target_arch = "wasm32")]
20use crate::utils::reader::{ReadBuffer, Reader};
21#[cfg(target_arch = "wasm32")]
22use crate::utils::writer::WriteBuffer::Vec as WBVec;
23#[cfg(target_arch = "wasm32")]
24use std::io::Cursor;
25
26use wasm_bindgen::prelude::*;
27
28pub trait CommandBuilder<T: Executable> {
29    fn try_build(self) -> crate::rules::Result<T>;
30}
31
32#[derive(Default, Debug)]
33/// .
34/// A builder to help construct the `ParseTree` command
35pub struct ParseTreeBuilder {
36    rules: Option<String>,
37    output: Option<String>,
38    print_json: bool,
39    print_yaml: bool,
40}
41
42impl CommandBuilder<ParseTree> for ParseTreeBuilder {
43    /// .
44    /// builds a parse tree command
45    fn try_build(self) -> crate::rules::Result<ParseTree> {
46        let ParseTreeBuilder {
47            rules,
48            output,
49            print_json,
50            print_yaml,
51        } = self;
52
53        Ok(ParseTree {
54            rules,
55            output,
56            print_json,
57            print_yaml,
58        })
59    }
60}
61
62impl ParseTreeBuilder {
63    /// path to the rules file to be evaluated
64    pub fn rules(mut self, rules: Option<String>) -> Self {
65        self.rules = rules;
66
67        self
68    }
69
70    /// path to the output file where the parse tree will be printed to
71    pub fn output(mut self, output: Option<String>) -> Self {
72        self.output = output;
73
74        self
75    }
76
77    /// print parse tree in JSON format
78    pub fn print_json(mut self, arg: bool) -> Self {
79        self.print_json = arg;
80
81        self
82    }
83
84    /// print parse tree in YAML format
85    pub fn print_yaml(mut self, arg: bool) -> Self {
86        self.print_yaml = arg;
87
88        self
89    }
90}
91
92#[derive(Debug)]
93#[wasm_bindgen]
94/// .
95/// A builder to help construct the `Validate` command
96pub struct ValidateBuilder {
97    rules: Vec<String>,
98    data: Vec<String>,
99    input_params: Vec<String>,
100    template_type: Option<String>,
101    output_format: OutputFormatType,
102    show_summary: Vec<ShowSummaryType>,
103    alphabetical: bool,
104    last_modified: bool,
105    verbose: bool,
106    print_json: bool,
107    payload: bool,
108    structured: bool,
109}
110
111impl Default for ValidateBuilder {
112    fn default() -> Self {
113        Self {
114            rules: Default::default(),
115            data: Default::default(),
116            input_params: Default::default(),
117            template_type: Default::default(),
118            output_format: Default::default(),
119            show_summary: vec![Default::default()],
120            alphabetical: Default::default(),
121            last_modified: false,
122            verbose: false,
123            print_json: false,
124            payload: false,
125            structured: false,
126        }
127    }
128}
129
130impl CommandBuilder<Validate> for ValidateBuilder {
131    /// .
132    /// attempts to construct a `Validate` command
133    ///
134    /// This function will return an error if
135    /// - conflicting attributes have been set
136    /// - both rules is empty, and payload is false
137    #[allow(deprecated)]
138    fn try_build(self) -> crate::rules::Result<Validate> {
139        if self.structured {
140            if self.output_format == OutputFormatType::SingleLineSummary {
141                return Err(Error::IllegalArguments(String::from(
142                        "single-line-summary is not able to be used when the `structured` flag is present",
143                    )));
144            }
145
146            if self.print_json {
147                return Err(Error::IllegalArguments(String::from("unable to construct validate command when both structured and print_json are set to true")));
148            }
149
150            if self.verbose {
151                return Err(Error::IllegalArguments(String::from("unable to construct validate command when both structured and verbose are set to true")));
152            }
153
154            if self.show_summary.iter().any(|st| {
155                matches!(
156                    st,
157                    ShowSummaryType::Pass
158                        | ShowSummaryType::Fail
159                        | ShowSummaryType::Skip
160                        | ShowSummaryType::All
161                )
162            }) {
163                return Err(Error::IllegalArguments(String::from(
164                    "Cannot provide a summary-type other than `none` when the `structured` flag is present",
165                )));
166            }
167        } else if matches!(
168            self.output_format,
169            OutputFormatType::Junit | OutputFormatType::Sarif
170        ) {
171            return Err(Error::IllegalArguments(format!(
172                "the structured flag must be set when output is set to {:?}",
173                self.output_format
174            )));
175        }
176
177        if self.payload && (!self.rules.is_empty() || !self.data.is_empty()) {
178            return Err(Error::IllegalArguments(String::from("cannot construct a validate command payload conflicts with both data and rules arguments")));
179        }
180
181        if !self.payload && self.rules.is_empty() {
182            return Err(Error::IllegalArguments(String::from("cannot construct a validate command: either payload must be set to true, or rules must not be empty")));
183        }
184
185        if self.last_modified && self.alphabetical {
186            return Err(Error::IllegalArguments(String::from(
187                "cannot have both last modified, and alphabetical arguments set to true",
188            )));
189        }
190
191        let ValidateBuilder {
192            rules,
193            data,
194            input_params,
195            template_type,
196            output_format,
197            show_summary,
198            alphabetical,
199            last_modified,
200            verbose,
201            print_json,
202            payload,
203            structured,
204        } = self;
205
206        Ok(Validate {
207            rules,
208            data,
209            input_params,
210            template_type,
211            output_format,
212            show_summary,
213            alphabetical,
214            last_modified,
215            verbose,
216            print_json,
217            payload,
218            structured,
219        })
220    }
221}
222
223#[wasm_bindgen]
224impl ValidateBuilder {
225    /// 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
226    /// conflicts with payload
227    pub fn rules(mut self, rules: Vec<String>) -> Self {
228        self.rules = rules;
229
230        self
231    }
232
233    /// 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
234    /// conflicts with payload
235    pub fn data(mut self, data: Vec<String>) -> Self {
236        self.data = data;
237
238        self
239    }
240
241    /// 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)
242    /// default is failed
243    /// must be set to none if used together with the structured flag
244    #[wasm_bindgen(js_name = showSummary)]
245    pub fn show_summary(mut self, args: Vec<ShowSummaryType>) -> Self {
246        self.show_summary = args;
247
248        self
249    }
250
251    /// 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
252    pub fn input_params(mut self, input_params: Vec<String>) -> Self {
253        self.input_params = input_params;
254
255        self
256    }
257
258    /// Specify the format in which the output should be displayed
259    /// default is single-line-summary
260    /// if junit is used, `structured` attributed must be set to true
261    #[wasm_bindgen(js_name = outputFormat)]
262    pub fn output_format(mut self, output: OutputFormatType) -> Self {
263        self.output_format = output;
264
265        self
266    }
267
268    /// Tells the command that rules, and data will be passed via a reader, as a json payload.
269    /// Conflicts with both rules, and data
270    /// default is false
271    pub fn payload(mut self, arg: bool) -> Self {
272        self.payload = arg;
273
274        self
275    }
276
277    /// Validate files in a directory ordered alphabetically, conflicts with `last_modified` field
278    pub fn alphabetical(mut self, arg: bool) -> Self {
279        self.alphabetical = arg;
280
281        self
282    }
283
284    /// Validate files in a directory ordered by last modified times, conflicts with `alphabetical` field
285    pub fn last_modified(mut self, arg: bool) -> Self {
286        self.last_modified = arg;
287
288        self
289    }
290
291    /// Output verbose logging, conflicts with `structured` field
292    /// default is false
293    pub fn verbose(mut self, arg: bool) -> Self {
294        self.verbose = arg;
295
296        self
297    }
298
299    /// Print the parse tree in a json format. This can be used to get more details on how the clauses were evaluated
300    /// conflicts with the `structured` attribute
301    /// default is false
302    pub fn print_json(mut self, arg: bool) -> Self {
303        self.print_json = arg;
304
305        self
306    }
307
308    /// Prints the output which must be specified to JSON/YAML/JUnit in a structured format
309    /// Conflicts with the following attributes `verbose`, `print-json`, `output-format` when set
310    /// to "single-line-summary", show-summary when set to anything other than "none"
311    /// default is false
312    pub fn structured(mut self, arg: bool) -> Self {
313        self.structured = arg;
314
315        self
316    }
317
318    #[cfg(target_arch = "wasm32")]
319    #[wasm_bindgen(constructor)]
320    pub fn new() -> ValidateBuilder {
321        ValidateBuilder {
322            ..Default::default()
323        }
324    }
325
326    #[cfg(target_arch = "wasm32")]
327    #[wasm_bindgen(js_name = tryBuildAndExecute)]
328    pub fn try_build_js_and_execute(self, payload: &str) -> Result<JsValue, JsValue> {
329        let mut reader = Reader::new(ReadBuffer::Cursor(Cursor::new(Vec::from(
330            payload.as_bytes(),
331        ))));
332
333        let mut writer = utils::writer::Writer::new(WBVec(vec![]))
334            .map_err(|e| JsValue::from_str(&e.to_string()))?;
335
336        let cmd = self
337            .try_build()
338            .map_err(|e| JsValue::from_str(&e.to_string()))?;
339
340        cmd.execute(&mut writer, &mut reader)
341            .map_err(|e| JsValue::from_str(&e.to_string()))?;
342
343        Ok(JsValue::from(
344            writer.into_string().unwrap_or("".to_string()),
345        ))
346    }
347}
348/// .
349/// A builder to help construct the `Test` command
350#[derive(Default, Debug)]
351pub struct TestBuilder {
352    rules: Option<String>,
353    test_data: Option<String>,
354    directory: Option<String>,
355    alphabetical: bool,
356    last_modified: bool,
357    verbose: bool,
358    output_format: OutputFormatType,
359}
360
361impl CommandBuilder<Test> for TestBuilder {
362    /// .
363    /// attempts to construct a `Test` command
364    ///
365    /// This function will return an error if
366    /// - conflicting attributes have been set
367    /// - rules, test-data, and directory is set to None
368    fn try_build(self) -> crate::rules::Result<Test> {
369        if self.last_modified && self.alphabetical {
370            return Err(Error::IllegalArguments(String::from("unable to construct a test command: cannot have both last modified, and alphabetical arguments set to true")));
371        }
372
373        if self.directory.is_some() && self.rules.is_some() {
374            return Err(Error::IllegalArguments(String::from("unable to construct a test command: cannot pass both a directory argument, and a rules argument")));
375        }
376
377        if !matches!(self.output_format, OutputFormatType::SingleLineSummary) && self.verbose {
378            return Err(Error::IllegalArguments(String::from("Cannot provide an output_type of JSON, YAML, or JUnit while the verbose flag is set")));
379        }
380
381        let TestBuilder {
382            rules,
383            test_data,
384            directory,
385            alphabetical,
386            last_modified,
387            verbose,
388            output_format,
389        } = self;
390
391        Ok(Test {
392            rules,
393            test_data,
394            directory,
395            alphabetical,
396            last_modified,
397            verbose,
398            output_format,
399        })
400    }
401}
402
403impl TestBuilder {
404    // the path to the rule file
405    // conflicts with directory
406    pub fn rules(mut self, rules: Option<String>) -> Self {
407        self.rules = rules;
408
409        self
410    }
411
412    // the path to the test-data file
413    // conflicts with directory
414    pub fn test_data(mut self, test_data: Option<String>) -> Self {
415        self.test_data = test_data;
416
417        self
418    }
419
420    // A path to a directory containing rule file(s), and a subdirectory called tests containing
421    // data input file(s)
422    // conflicts with rules, and test_data
423    pub fn directory(mut self, directory: Option<String>) -> Self {
424        self.directory = directory;
425
426        self
427    }
428
429    /// Test files in a directory ordered alphabetically, conflicts with `last_modified` field
430    /// default is false
431    pub fn alphabetical(mut self, arg: bool) -> Self {
432        self.alphabetical = arg;
433
434        self
435    }
436
437    /// Test files in a directory ordered by last modified times, conflicts with `alphabetical` field
438    /// default is false
439    pub fn last_modified(mut self, arg: bool) -> Self {
440        self.last_modified = arg;
441
442        self
443    }
444
445    /// Output verbose logging, conflicts with output_format if not single-line-summary
446    /// default is false
447    pub fn verbose(mut self, arg: bool) -> Self {
448        self.verbose = arg;
449
450        self
451    }
452
453    /// Specify the format in which the output should be displayed
454    /// default is single-line-summary
455    /// will conflict with verbose if set to something other than single-line-summary and verbose
456    /// is set to true
457    pub fn output_format(mut self, output: OutputFormatType) -> Self {
458        self.output_format = output;
459
460        self
461    }
462}
463
464#[derive(Debug, Default)]
465/// .
466/// A builder to help construct the `Rulegen` command
467pub struct RulegenBuilder {
468    output: Option<String>,
469    template: String,
470}
471
472impl CommandBuilder<Rulegen> for RulegenBuilder {
473    /// construct a rulegen command
474    fn try_build(self) -> crate::rules::Result<Rulegen> {
475        let RulegenBuilder { output, template } = self;
476        Ok(Rulegen { output, template })
477    }
478}
479
480impl RulegenBuilder {
481    /// path for the output file where the generated rules will be written to
482    /// if no path is specified output will be printed to the stdout
483    pub fn output(mut self, output: Option<String>) -> Self {
484        self.output = output;
485
486        self
487    }
488
489    /// path to the template which the rules will be autogenerated from
490    pub fn template(mut self, template: String) -> Self {
491        self.template = template;
492
493        self
494    }
495}
496
497#[cfg(test)]
498mod cfn_guard_lib_tests {
499    use crate::{
500        commands::validate::ShowSummaryType, CommandBuilder, TestBuilder, ValidateBuilder,
501    };
502
503    #[test]
504    fn validate_with_errors() {
505        // fails cause structured, but show_summary fail
506        let cmd = ValidateBuilder::default()
507            .data(vec![String::from("resources/validate/data-dir")])
508            .rules(vec![String::from("resources/validate/rules-dir")])
509            .structured(true)
510            .output_format(crate::commands::validate::OutputFormatType::JSON)
511            .try_build();
512        assert!(cmd.is_err());
513
514        // fails cause structured, but single-line-summary
515        let cmd = ValidateBuilder::default()
516            .data(vec![String::from("resources/validate/data-dir")])
517            .rules(vec![String::from("resources/validate/rules-dir")])
518            .structured(true)
519            .show_summary(vec![ShowSummaryType::None])
520            .try_build();
521        assert!(cmd.is_err());
522
523        // fails cause structured, but verbose
524        let cmd = ValidateBuilder::default()
525            .data(vec![String::from("resources/validate/data-dir")])
526            .rules(vec![String::from("resources/validate/rules-dir")])
527            .structured(true)
528            .output_format(crate::commands::validate::OutputFormatType::JSON)
529            .verbose(true)
530            .show_summary(vec![ShowSummaryType::None])
531            .try_build();
532        assert!(cmd.is_err());
533
534        // fails cause structured, but print_json
535        let cmd = ValidateBuilder::default()
536            .data(vec![String::from("resources/validate/data-dir")])
537            .rules(vec![String::from("resources/validate/rules-dir")])
538            .structured(true)
539            .output_format(crate::commands::validate::OutputFormatType::JSON)
540            .print_json(true)
541            .show_summary(vec![ShowSummaryType::None])
542            .try_build();
543        assert!(cmd.is_err());
544
545        // fails cause junit or sarif, but not structured
546        vec![
547            crate::commands::validate::OutputFormatType::Junit,
548            crate::commands::validate::OutputFormatType::Sarif,
549        ]
550        .into_iter()
551        .for_each(|output_format| {
552            let cmd = ValidateBuilder::default()
553                .data(vec![String::from("resources/validate/data-dir")])
554                .rules(vec![String::from("resources/validate/rules-dir")])
555                .output_format(output_format)
556                .show_summary(vec![ShowSummaryType::None])
557                .try_build();
558
559            assert!(cmd.is_err());
560        });
561
562        // fails cause no payload, or rules
563        let cmd = ValidateBuilder::default()
564            .output_format(crate::commands::validate::OutputFormatType::Junit)
565            .structured(true)
566            .show_summary(vec![ShowSummaryType::None])
567            .try_build();
568
569        assert!(cmd.is_err());
570
571        // fails cause payload, and rules conflict
572        let cmd = ValidateBuilder::default()
573            .rules(vec![String::from("resources/validate/rules-dir")])
574            .payload(true)
575            .output_format(crate::commands::validate::OutputFormatType::Junit)
576            .structured(true)
577            .show_summary(vec![ShowSummaryType::None])
578            .try_build();
579
580        assert!(cmd.is_err());
581
582        // fails cause payload, and data conflict
583        let cmd = ValidateBuilder::default()
584            .data(vec![String::from("resources/validate/data-dir")])
585            .payload(true)
586            .output_format(crate::commands::validate::OutputFormatType::Junit)
587            .structured(true)
588            .show_summary(vec![ShowSummaryType::None])
589            .try_build();
590
591        assert!(cmd.is_err());
592
593        // fails cause last_modified, and alphabetical conflict
594        let cmd = ValidateBuilder::default()
595            .payload(true)
596            .alphabetical(true)
597            .last_modified(true)
598            .output_format(crate::commands::validate::OutputFormatType::Junit)
599            .structured(true)
600            .show_summary(vec![ShowSummaryType::None])
601            .try_build();
602
603        assert!(cmd.is_err());
604    }
605
606    #[test]
607    fn validate_happy_path() {
608        let data = vec![String::from("resources/validate/data-dir")];
609        let rules = vec![String::from("resources/validate/rules-dir")];
610        let cmd = ValidateBuilder::default()
611            .verbose(true)
612            .output_format(crate::commands::validate::OutputFormatType::JSON)
613            .data(data.clone())
614            .rules(rules.clone())
615            .try_build();
616
617        assert!(cmd.is_ok());
618
619        let cmd = ValidateBuilder::default()
620            .verbose(true)
621            .show_summary(vec![
622                ShowSummaryType::Pass,
623                ShowSummaryType::Fail,
624                ShowSummaryType::Skip,
625            ])
626            .output_format(crate::commands::validate::OutputFormatType::JSON)
627            .data(data.clone())
628            .rules(rules.clone())
629            .try_build();
630
631        assert!(cmd.is_ok());
632
633        let cmd = ValidateBuilder::default()
634            .verbose(true)
635            .output_format(crate::commands::validate::OutputFormatType::JSON)
636            .data(data.clone())
637            .rules(rules.clone())
638            .try_build();
639
640        assert!(cmd.is_ok());
641
642        let cmd = ValidateBuilder::default()
643            .data(data.clone())
644            .rules(rules.clone())
645            .structured(true)
646            .output_format(crate::commands::validate::OutputFormatType::JSON)
647            .show_summary(vec![ShowSummaryType::None])
648            .try_build();
649
650        assert!(cmd.is_ok());
651
652        let cmd = ValidateBuilder::default()
653            .data(data.clone())
654            .rules(rules.clone())
655            .output_format(crate::commands::validate::OutputFormatType::Junit)
656            .structured(true)
657            .show_summary(vec![ShowSummaryType::None])
658            .try_build();
659
660        assert!(cmd.is_ok());
661    }
662
663    #[test]
664    fn build_test_command_happy_path() {
665        let data = String::from("resources/validate/data-dir");
666        let rules = String::from("resources/validate/rules-dir");
667        let cmd = TestBuilder::default()
668            .test_data(Option::from(data.clone()))
669            .rules(Option::from(rules.clone()))
670            .try_build();
671
672        assert!(cmd.is_ok());
673
674        let cmd = TestBuilder::default()
675            .directory(Option::from(data.clone()))
676            .try_build();
677        assert!(cmd.is_ok());
678
679        let cmd = TestBuilder::default()
680            .directory(Option::from(data.clone()))
681            .alphabetical(true)
682            .try_build();
683
684        assert!(cmd.is_ok())
685    }
686
687    #[test]
688    fn build_test_command_with_errors() {
689        let data = String::from("resources/validate/data-dir");
690        let rules = String::from("resources/validate/rules-dir");
691
692        // fails cause rules and dir
693        let cmd = TestBuilder::default()
694            .rules(Option::from(rules.clone()))
695            .directory(Option::from(data.clone()))
696            .try_build();
697
698        assert!(cmd.is_err());
699
700        // fails cause alphabetical and last_modified
701        let cmd = TestBuilder::default()
702            .directory(Option::from(data.clone()))
703            .last_modified(true)
704            .alphabetical(true)
705            .try_build();
706
707        assert!(cmd.is_err());
708    }
709}