From c7bcce2d2c662ed34969b1ef1e219c007e526e2a Mon Sep 17 00:00:00 2001 From: Josh Fried <112121129+joshfried-aws@users.noreply.github.com> Date: Thu, 29 Jun 2023 15:05:46 -0400 Subject: [PATCH] 3.0.0 (#382) * bumping up to 3.0.0-alpha (#347) * bumping up to 3.0.0 alpha * typo * updating workflow * Populated filename in the output (#358) * Populated filename in the output * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct --------- Co-authored-by: Akshay Rane * Support for some function expressions / stateful rules (#361) * init commit for function resolution / stateful rules * more tests + making test rules better * fixed bugs with validation of functions * small cleanup * fixes as per comments * cleanup * add todo * typos * fixed logical error breaking tests * added test for test command with a function * fixed unit test * added comment to clearly explain whats happening in regex_replace cause no one actually understands regex * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- Co-authored-by: Akshay Rane * Clap Autocompletions (#340) * temp * fixing commands * cleanup * temp * cleanup * adding valuehints * adding valuehints * removed powershell * cleanup * removing derive * removed unecessary ArgActions * bumping up version * refactor to use a function to generate commands * removing unused imports * removed random println * updating readme * fixes * typo * cleanup * typo * adding documentation for functions (#362) * adding documentation for functions * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- Co-authored-by: Akshay Rane * clarifying docs --------- Co-authored-by: Akshay Rane Co-authored-by: Akshay Rane * Deprecated migrate and previous engine (#364) * Deprecated migrate and previous engine * Removed a unit test for old engine --------- Co-authored-by: Akshay Rane * 3.0.0 beta release changes (#365) * Bump up version to 3.0.0-beta * Updated README.md * Add instances to rules integration tests (#351) * Added 2 runners to integration tests for rules registry * Fixed indent * Added explicit shell name * Moved shell to job parameters * Added powershell commands for windows * Removed test branch * Updated README.md (#352) * Updated README for Guard 3.0 * Update README.md Co-authored-by: Ben Bridts --------- Co-authored-by: Ben Bridts --------- Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts * feat: Add cfn-guard-lambda deployment with SAM CLI (#354) * feat: Add cfn-guard-lambda deployment with SAM CLI * Renamed the logical ID for lambda in template & updated README.md * Updated the instructions and added least privileged IAM access policy --------- Co-authored-by: Ben Bridts Co-authored-by: Akshay Rane * Revert "Added deprecated short flag for print-json in parse-tree" This reverts commit 93548a49 * Updated names of binaries to reflect v3 * Updated README.md to add new features * Added rogue_one branch to docker workflow * Bump enumflags2 to 0.7.7 --------- Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts * 3.0.0 Beta release (#366) (#369) * bumping up to 3.0.0-alpha (#347) * bumping up to 3.0.0 alpha * typo * updating workflow * Populated filename in the output (#358) * Populated filename in the output * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct --------- * Support for some function expressions / stateful rules (#361) * init commit for function resolution / stateful rules * more tests + making test rules better * fixed bugs with validation of functions * small cleanup * fixes as per comments * cleanup * add todo * typos * fixed logical error breaking tests * added test for test command with a function * fixed unit test * added comment to clearly explain whats happening in regex_replace cause no one actually understands regex * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- * Clap Autocompletions (#340) * temp * fixing commands * cleanup * temp * cleanup * adding valuehints * adding valuehints * removed powershell * cleanup * removing derive * removed unecessary ArgActions * bumping up version * refactor to use a function to generate commands * removing unused imports * removed random println * updating readme * fixes * typo * cleanup * typo * adding documentation for functions (#362) * adding documentation for functions * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- * clarifying docs --------- * Deprecated migrate and previous engine (#364) * Deprecated migrate and previous engine * Removed a unit test for old engine --------- * 3.0.0 beta release changes (#365) * Bump up version to 3.0.0-beta * Updated README.md * Add instances to rules integration tests (#351) * Added 2 runners to integration tests for rules registry * Fixed indent * Added explicit shell name * Moved shell to job parameters * Added powershell commands for windows * Removed test branch * Updated README.md (#352) * Updated README for Guard 3.0 * Update README.md --------- --------- * feat: Add cfn-guard-lambda deployment with SAM CLI (#354) * feat: Add cfn-guard-lambda deployment with SAM CLI * Renamed the logical ID for lambda in template & updated README.md * Updated the instructions and added least privileged IAM access policy --------- * Revert "Added deprecated short flag for print-json in parse-tree" This reverts commit 93548a49 * Updated names of binaries to reflect v3 * Updated README.md to add new features * Added rogue_one branch to docker workflow * Bump enumflags2 to 0.7.7 --------- --------- Co-authored-by: Akshay Rane Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts * removed uneeded printing of error parser error on parse-tree command (#368) * improve error message for templates that cause an error (#370) * improve erro message for empty templates * addressing clippy lints for validate.rs * addressed comment, removed uneeded code paths, and cleaned some stuff up * Clippy lints + ci (#371) * init * aws_meta_appender_tests.rs clippy lints * parser.rs clippy lints * cfn_reporter.rs clippy lints * files.rs clippy lints * tf.rs clippy lints * tracker.rs clippy lints * operator.rs clippy lints * values.rs clippy lints * traversal.rs clippy lints * path_value.rs clippy lints * rules/mod.rs clippy lints * eval.rs clippy lints * rulegen.rs clippy lints * summary_table.rs clippy lints * aws_meta_appender.rs clippy lints * path_value_tests.rs clippy lints * eval_tests.rs clippy lints * utils.mod.rs clippy lints * parser_tests.rs clippy lints * traversal_tests.rs clippy lints * generic_summary.rs clippy lints * a bunch of misc clippy lints * tests/utils.rs clippy lints * test_command.rs clippy lints * main.rs clippy lints * tests/validate.rs clippy lints * tests/parse_tree.rs clippy lints * functional.rs clippy lints * helper.rs clippy lints * eval_context clippy lints * cfn.rs clippy lints * value_tests.rs clippy lints * last of the lints * adding linting to ci * last few lints * evaluate_tests.rs lints * fix for bug when introduced when rule fails and resource is not the parent of the node where the failure occurs + misc tests (#372) * Updating reporters to all use serde for both json and yaml + misc improvements (#373) * modifying json/yaml responses to ALL use serde_yaml/json for serialization * adding type information to error message * [Bugfix] Fixing improper console output when using single line summary (#378) * changing count.rs to return a pathawarevalue instead of a primitive * temp * adding unit test for show summary all when failing using count fn * removing unecesssary file * [Enhancement] creating a new error code for rule failures (#379) * temp * adding new exit code for when a rule fails * added integration test to validate error code for a failing test * cleanup * rebase + fix test * removed unecessary double 0 * Refined documentation for functions, join path bugfix & version bump (#381) * Updated table of contents and added a writeup for functions * Bug fix for set path for returned PathAwareValue for join function * Refined documentation for functions * Version bump to 3.0.0 * Added more detailed explanation for function usage limitation * Added integration test for join path bugfix --------- Co-authored-by: Akshay Rane * 3.0.0 release changes (#383) * 3.0.0 Beta release (#366) * bumping up to 3.0.0-alpha (#347) * bumping up to 3.0.0 alpha * typo * updating workflow * Populated filename in the output (#358) * Populated filename in the output * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct --------- Co-authored-by: Akshay Rane * Support for some function expressions / stateful rules (#361) * init commit for function resolution / stateful rules * more tests + making test rules better * fixed bugs with validation of functions * small cleanup * fixes as per comments * cleanup * add todo * typos * fixed logical error breaking tests * added test for test command with a function * fixed unit test * added comment to clearly explain whats happening in regex_replace cause no one actually understands regex * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- Co-authored-by: Akshay Rane * Clap Autocompletions (#340) * temp * fixing commands * cleanup * temp * cleanup * adding valuehints * adding valuehints * removed powershell * cleanup * removing derive * removed unecessary ArgActions * bumping up version * refactor to use a function to generate commands * removing unused imports * removed random println * updating readme * fixes * typo * cleanup * typo * adding documentation for functions (#362) * adding documentation for functions * Combined structured output and updated default rule clause name to include file name (#360) * Populated filename in the output * Structured combine * Changed FileData into DataFile and handle error differently * Resolved lifetime issue with FileReport combine method * Updated status and method * Refactored to use existing DataFile struct * Changed FileData into DataFile and handle error differently * Refactored to use existing DataFile struct * Merged file report * Interim commit for structured * Resolved unit tests * Temporary commit for default rule names * Working prototype for formatting issue --------- Co-authored-by: Akshay Rane * clarifying docs --------- Co-authored-by: Akshay Rane Co-authored-by: Akshay Rane * Deprecated migrate and previous engine (#364) * Deprecated migrate and previous engine * Removed a unit test for old engine --------- Co-authored-by: Akshay Rane * 3.0.0 beta release changes (#365) * Bump up version to 3.0.0-beta * Updated README.md * Add instances to rules integration tests (#351) * Added 2 runners to integration tests for rules registry * Fixed indent * Added explicit shell name * Moved shell to job parameters * Added powershell commands for windows * Removed test branch * Updated README.md (#352) * Updated README for Guard 3.0 * Update README.md Co-authored-by: Ben Bridts --------- Co-authored-by: Ben Bridts --------- Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts * feat: Add cfn-guard-lambda deployment with SAM CLI (#354) * feat: Add cfn-guard-lambda deployment with SAM CLI * Renamed the logical ID for lambda in template & updated README.md * Updated the instructions and added least privileged IAM access policy --------- Co-authored-by: Ben Bridts Co-authored-by: Akshay Rane * Revert "Added deprecated short flag for print-json in parse-tree" This reverts commit 93548a49 * Updated names of binaries to reflect v3 * Updated README.md to add new features * Added rogue_one branch to docker workflow * Bump enumflags2 to 0.7.7 --------- Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts --------- Co-authored-by: Akshay Rane Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts * Removed unused import --------- Co-authored-by: Josh Fried <112121129+joshfried-aws@users.noreply.github.com> Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts * removing unused import --------- Co-authored-by: Akshay Rane Co-authored-by: Akshay Rane Co-authored-by: razcloud <34892703+razcloud@users.noreply.github.com> Co-authored-by: Ben Bridts Co-authored-by: Ben Bridts --- .github/workflows/pr.yml | 50 ++-- Cargo.lock | 6 +- README.md | 52 +++- docs/FUNCTIONS.md | 160 ++++++++--- docs/KNOWN_ISSUES.md | 31 ++ guard-ffi/Cargo.toml | 4 +- guard-ffi/src/errors.rs | 1 + guard-lambda/Cargo.toml | 4 +- guard-lambda/src/lib.rs | 1 + guard-lambda/src/main.rs | 7 +- guard-lambda/tests/lambda-handler.rs | 1 - guard/Cargo.toml | 2 +- guard/README.md | 2 +- .../output-dir/parse_tree_functions.yaml | 158 ++++++++++ ...ket_server_side_encryption_parse_tree.json | 267 +++++++++++++++++ .../test-command/data-dir/failing_test.yaml | 14 + .../test-command/functions/data/template.yaml | 32 ++- .../functions/rules/json_parse.guard | 13 +- .../test-command/output-dir/functions.out | 15 + guard/resources/validate/blank.yaml | 0 .../failing_template_with_slash_in_key.yaml | 12 + .../output/failing_count_show_summary_all.out | 30 ++ .../output/failing_join_show_summary_all.out | 31 ++ .../functions/rules/count_with_message.guard | 9 + .../functions/rules/join_with_message.guard | 9 + .../failing_template_with_slash_in_key.out | 46 +++ ...ing_template_without_resources_at_root.out | 37 +++ .../template_where_resources_isnt_root.json | 48 ++++ guard/resources/validate/workshop.guard | 62 ++++ guard/src/commands/aws_meta_appender.rs | 3 +- guard/src/commands/aws_meta_appender_tests.rs | 23 +- guard/src/commands/files.rs | 53 +--- guard/src/commands/helper.rs | 8 +- guard/src/commands/parse_tree.rs | 18 +- guard/src/commands/rulegen.rs | 7 +- guard/src/commands/test.rs | 6 +- guard/src/commands/tracker.rs | 44 +-- guard/src/commands/validate.rs | 272 +++--------------- guard/src/commands/validate/cfn.rs | 104 +++---- guard/src/commands/validate/cfn_reporter.rs | 23 +- guard/src/commands/validate/common.rs | 135 ++------- .../src/commands/validate/console_reporter.rs | 2 +- .../src/commands/validate/generic_summary.rs | 63 ++-- guard/src/commands/validate/structured.rs | 3 +- guard/src/commands/validate/summary_table.rs | 9 +- guard/src/commands/validate/tf.rs | 34 +-- guard/src/main.rs | 5 +- guard/src/rules/display.rs | 2 +- guard/src/rules/errors.rs | 3 + guard/src/rules/eval.rs | 93 +++--- guard/src/rules/eval/operators.rs | 78 ++--- guard/src/rules/eval/operators_tests.rs | 48 ++-- guard/src/rules/eval_context.rs | 20 +- guard/src/rules/eval_context_tests.rs | 2 +- guard/src/rules/eval_tests.rs | 148 +++++----- guard/src/rules/evaluate.rs | 84 +----- guard/src/rules/evaluate_tests.rs | 51 ++-- guard/src/rules/functions/collections.rs | 27 +- .../src/rules/functions/collections_tests.rs | 27 +- guard/src/rules/functions/strings.rs | 14 +- guard/src/rules/functions/strings_tests.rs | 22 +- guard/src/rules/libyaml/event.rs | 4 + guard/src/rules/libyaml/mod.rs | 1 + guard/src/rules/libyaml/util.rs | 1 + guard/src/rules/mod.rs | 16 +- guard/src/rules/parser.rs | 24 +- guard/src/rules/parser_tests.rs | 43 ++- guard/src/rules/path_value.rs | 160 ++--------- guard/src/rules/path_value/traversal.rs | 26 +- guard/src/rules/path_value/traversal_tests.rs | 10 +- guard/src/rules/path_value_tests.rs | 82 +++--- guard/src/rules/values.rs | 29 +- guard/src/rules/values_tests.rs | 12 +- guard/src/utils/mod.rs | 21 +- guard/tests/functional.rs | 6 +- guard/tests/parse_tree.rs | 18 +- guard/tests/rulegen.rs | 7 +- guard/tests/test_command.rs | 36 ++- guard/tests/utils.rs | 22 +- guard/tests/validate.rs | 89 ++++-- 80 files changed, 1762 insertions(+), 1380 deletions(-) create mode 100644 guard/resources/parse-tree/output-dir/parse_tree_functions.yaml create mode 100644 guard/resources/parse-tree/output-dir/s3_bucket_server_side_encryption_parse_tree.json create mode 100644 guard/resources/test-command/data-dir/failing_test.yaml create mode 100644 guard/resources/test-command/output-dir/functions.out create mode 100644 guard/resources/validate/blank.yaml create mode 100644 guard/resources/validate/failing_template_with_slash_in_key.yaml create mode 100644 guard/resources/validate/functions/output/failing_count_show_summary_all.out create mode 100644 guard/resources/validate/functions/output/failing_join_show_summary_all.out create mode 100644 guard/resources/validate/functions/rules/count_with_message.guard create mode 100644 guard/resources/validate/functions/rules/join_with_message.guard create mode 100644 guard/resources/validate/output-dir/failing_template_with_slash_in_key.out create mode 100644 guard/resources/validate/output-dir/failing_template_without_resources_at_root.out create mode 100644 guard/resources/validate/template_where_resources_isnt_root.json create mode 100644 guard/resources/validate/workshop.guard diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 9b207afe7..792452574 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [ main, development, rogue_one ] + branches: [main, development, rogue_one] pull_request: - branches: [ main, development, rogue_one ] + branches: [main, development, rogue_one] env: CARGO_TERM_COLOR: always @@ -14,19 +14,19 @@ jobs: name: Build all crates & run unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Build all crates - run: cargo build --release --verbose - - name: Run unit tests - run: cargo test --verbose + - uses: actions/checkout@v2 + - name: Build all crates + run: cargo build --release --verbose + - name: Run unit tests + run: cargo test --verbose shellcheck: name: Shellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Shellcheck - run: shellcheck install-guard.sh + - uses: actions/checkout@v2 + - name: Shellcheck + run: shellcheck install-guard.sh formatting: name: Formatting check (cargo fmt) @@ -39,10 +39,26 @@ jobs: - name: Rustfmt Check uses: actions-rust-lang/rustfmt@v1 + linting: + name: Linting check (clippy) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + components: clippy + - uses: actions-rs/clippy-check@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + args: -- -D warnings + aws-guard-rules-registry-integration-tests-linux: strategy: matrix: - os: [ ubuntu-latest, macos-latest ] + os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} name: Integration tests against aws-guard-rules-registry steps: @@ -141,10 +157,10 @@ jobs: - name: Run integration tests using parse-tree command run: | cd aws-guard-rules-registry/rules - + $FAILED_RULES = @() $SKIPPED_RULES = @() - + $rules = @(Get-ChildItem -Path .\ -Filter *.guard -Recurse -File) Foreach ($rule in $rules) { @@ -158,19 +174,19 @@ jobs: $FAILED_RULES += "$rule" } } - + $SKIPPED_RULE_COUNT = $SKIPPED_RULES.Length if ($SKIPPED_RULE_COUNT -gt 0) { echo "The following `$SKIPPED_RULE_COUNT.Length` rule(s) were skipped because they contained only comments:" echo $SKIPPED_RULES } - + $FAILED_RULE_COUNT = $FAILED_RULES.Length - + if ($FAILED_RULE_COUNT -gt 0) { echo "The following $FAILED_RULE_COUNT rule(s) have failed the parse-tree integration tests with a non-zero error code:" echo $FAILED_RULES exit 1 } else { echo "All the rules have succeeded the parse-tree integration tests." - } \ No newline at end of file + } diff --git a/Cargo.lock b/Cargo.lock index 36f9381b7..4c58f9418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -124,7 +124,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfn-guard" -version = "3.0.0-beta" +version = "3.0.0" dependencies = [ "Inflector", "clap", @@ -157,7 +157,7 @@ dependencies = [ [[package]] name = "cfn-guard-ffi" -version = "3.0.0-beta" +version = "3.0.0" dependencies = [ "cfn-guard", "ffi-support", @@ -165,7 +165,7 @@ dependencies = [ [[package]] name = "cfn-guard-lambda" -version = "3.0.0-beta" +version = "3.0.0" dependencies = [ "cfn-guard", "lambda_runtime", diff --git a/README.md b/README.md index b71e6ab7f..6c8cfc584 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,10 @@ Guard can be used for the following domains: * [Guard CLI](#guard-cli) * [Installation](#installation) * [How does Guard CLI work?](#how-does-guard-cli-work?) +* [Rule authoring references](#references) +* [Built-in functions & stateful rules](#functions) +* [AWS Rule Registry](#registry) +* [Use Guard as a Docker Image](#docker) * [License](#license) ## FAQs @@ -222,7 +226,7 @@ Check `help` to see if it is working. ```bash $ cfn-guard help -cfn-guard 3.0.0-beta +cfn-guard 3.0.0 Guard is a general-purpose tool that provides a simple declarative syntax to define policy-as-code as rules to validate against any structured hierarchical data (like JSON/YAML). @@ -489,7 +493,7 @@ cfn-guard test -r api_gateway_private_access.guard -t api_gateway_private_access Read [Guard: Unit Testing](docs/UNIT_TESTING.md) for more information on unit testing. To know about other commands read the [Readme in the guard directory](guard/README.md). -## Rule authoring references +## Rule authoring references As a starting point for writing Guard rules for yourself or your organisation we recommend following [this official guide](https://docs.aws.amazon.com/cfn-guard/latest/ug/writing-rules.html) @@ -507,15 +511,54 @@ As a starting point for writing Guard rules for yourself or your organisation we 9. [Composing named-rule blocks in AWS CloudFormation Guard](https://docs.aws.amazon.com/cfn-guard/latest/ug/named-rule-block-composition.html) 10. [Writing clauses to perform context-aware evaluations](https://docs.aws.amazon.com/cfn-guard/latest/ug/context-aware-evaluations.html) +## Built-in functions & stateful rules -## AWS Rule Registry +Guard 3.0 introduces support for functions, allowing for stateful rules that can run on a value that's evaluated based +on some properties extracted out of a data template. + +### Sample template + +Imagine we have a property in our template which consists of a list called as `Collection` and we need to ensure +it has at least 3 items in it. + +```yaml +Resources: + newServer: + Type: AWS::New::Service + Collection: + - a + - b +``` +### Sample rule + +We can write a rule to check this condition as follows: + +``` +let server = Resources.*[ Type == 'AWS::New::Service' ] +rule COUNT_CHECK when %server !empty { + let collection = %server.Collection.* + let count_of_items = count(%collection) + %count_of_items >= 3 + << + Violation: Collection should contain at least 3 items + >> +} +``` + +Expected outcome is that rule fails showing us the violation message since our template is non-compliant. + +For detailed documentation regarding all supported functions, please [follow this link](./docs/FUNCTIONS.md). For limitations of functions usage, please read [this note](./docs/KNOWN_ISSUES.md#function-limitation). + +## AWS Rule Registry As a reference for Guard rules and rule-sets that contain (on a best-effort basis) the compliance policies that adhere to the industry best practices around usages across AWS resources, we have recently launched [AWS Guard Rules Registry](https://github.com/aws-cloudformation/aws-guard-rules-registry). -## Guard Docker Image launched on [ECR public gallery](https://gallery.ecr.aws/aws-cloudformation/cloudformation-guard) +## Use Guard as a Docker Image + +Guard is also published as an ECR image in [ECR public gallery](https://gallery.ecr.aws/aws-cloudformation/cloudformation-guard) and can be used as an image in a docker container. ### Prerequisites @@ -541,6 +584,7 @@ We should see the evaluation result emitted out on the console. * We use the tag `latest` for the most recent docker image that gets published in sync with `main` branch of the `cloudformation-guard` GitHub repository. * We use the convention `.` for tags of historical docker images + ## License This project is licensed under the Apache-2.0 License. diff --git a/docs/FUNCTIONS.md b/docs/FUNCTIONS.md index bc295ff93..37825bd42 100644 --- a/docs/FUNCTIONS.md +++ b/docs/FUNCTIONS.md @@ -1,6 +1,10 @@ # Guard built-in functions and stateful rules -As of version 3.0.0 guard now supplies some builtin functions, allowing for stateful rules +As of version 3.0.0 guard now supplies some builtin functions, allowing for stateful rules. + +Built-in functions are supported only through assignment to a variable at the moment. + +There are some limitations with the current implementation of functions. We **do not** support inline usage yet. Please [read through more about this limitation here](./KNOWN_ISSUES.md#function-limitation). NOTE: all examples are operating off the following yaml template @@ -47,19 +51,27 @@ The following functions all operate on queries that resolve to string values ### json_parse -The json_parse function adds support for parsing inline json strings from a given template. After parsing the string into an object, -you can now evaluate certain properties of this struct just like with a normal json/yaml object +The `json_parse` function adds support for parsing inline JSON strings from a given template. After parsing the string into an object, +you can now evaluate certain properties of this struct just like with a normal JSON/YAML object + +#### Argument(s) -This function accepts a single argument: +1. `json_string`: Either be a query that resolves to a string or a string literal. Example, `'{"a": "basic", "json": "object"}'` -- this argument can either be a query that resolves to a string or a string literal. +#### Return value -The return value for this function is a query where each string that was resolved from the input is parsed into its json value +Query of JSON value(s) corresponding to every string literal resolved from input query -The following example shows how you could parse 2 fields on the above template and then write clauses on the results +#### Example + +The following example shows how you could parse 2 fields on the above template and then write clauses on the results: ``` let template = Resources.*[ Type == 'AWS::New::Service'] +let expected = { + "Principal": "*", + "Actions": ["s3*", "ec2*"] + } rule TEST_JSON_PARSE when %template !empty { let policy = %template.Properties.Policy @@ -67,6 +79,9 @@ rule TEST_JSON_PARSE when %template !empty { %res !empty %res == %expected + << + Violation: the IAM policy does not match with the recommended policy + >> let policy_text = %template.BucketPolicy.PolicyText let res2 = json_parse(%policy_text) @@ -76,23 +91,29 @@ rule TEST_JSON_PARSE when %template !empty { Effect == "Deny" Resource == "arn:aws:s3:::s3-test-123/*" } + } ``` ### regex_replace -The regex_replace function adds support for replacing one regular expression with another +The `regex_replace` function adds support for replacing one regular expression with another -This function accepts 3 arguments: +#### Argument(s) -- The first argument is a query, each string that is resolved from this query will be operated on -- The second argument is either a query that resolves to a string or a string literal, this is the expression we are looking for to extract +1. `base_string`: A query, each string that is resolved from this query will be operated on. Example, `%s3_resource.Properties.BucketName` +2. `regex_to_extract`: A regular expression that we are looking for to extract from the `base_string` - Note: if this string does not resolve to a valid regular expression an error will occur -- The third argument is either a query that resolves to a string or a string literal, this is the expression we are going to use replace the extracted part of the string +3. `regex_replacement` A regular expression that will replace the part we extracted, also supports capture groups -The return value for this function is a query where each string that was resolved from the input that contains the the regex from our 2nd argument is replaced with the regex in the 3rd argument +#### Return value + +A query where each string from the input has gone through the replacements + +#### Example In this simple example, we will re-format an ARN by moving around some sections in it. + We will start with a normal ARN that has the following pattern: `arn:::::/` and we will try to convert it to: `///-/` @@ -108,19 +129,24 @@ rule TEST_REGEX_REPLACE when %template !empty { let res = regex_replace(%arn, %arn_partition_regex, %capture_group_reordering) %res == "aws/123456789012/us-west-2/newservice-Table/extracted" + << Violation: Resulting reformatted ARN does not match the expected format >> } ``` ### join -The join function adds support to collect a query, and then join their values using the provided delimiter. +The `join` function adds support to collect a query, and then join their values using the provided delimiter. + +#### Argument(s) -This function accepts 2 arguments: +1. `collection`: A query, all string values resolved from this query are candidates of elements to be joined +2. `delimiter`: A query or a literal value that resolves to a string or character to be used as delimiter -- The first argument is a query, all string values resolved from this query will then be joined using the delimter argument -- The second argument is either a query that resolves to a string/character, or a literal value that is either a string or character +#### Return value -The return value for this function is query where each string that was resolved from the input is joined with the provided delimiter +Query where each string that was resolved from the input is joined with the provided delimiter + +#### Example The following example queries the template for a Collection field on a given resource, it then provides a join on ONLY the string values that this query resolves to with a `,` delimiter @@ -132,43 +158,75 @@ rule TEST_COLLECTION when %template !empty { let res = join(%collection, ",") %res == "a,b,c" + << Violation: The joined value does not match the expected result >> } ``` -### to_lower and to_upper +### to_lower + +This function can be used to change the casing of the all characters in the string passed to all lowercase. + +#### Argument(s) -Both functions accept a single argument: +1. `base_string`: A query that resolves to string(s) -- This argument is a query that resolves to a string(s) - all strings resolved will have the operation applied on them +#### Return value -Both these functions are very similar, one manipulates all resolved strings from a query to lower case, and the other to upper case +Returns the `base_string` in all lowercase ``` let type = Resources.newServer.Type rule STRING_MANIPULATION when %type !empty { let lower = to_lower(%type) - %lower == "aws::new::service" + %lower == /aws::new::service/ + << Violation: expected a value to be all lowercase >> +} +``` + +### to_upper + +This function can be used to change the casing of the all characters in the string passed to all uppercase. + +#### Argument(s) +1. `base_string`: A query that resolves to string(s) + +#### Return value + +Returns capitalized version of the `base_string` + +#### Example + +``` +let type = Resources.newServer.Type + +rule STRING_MANIPULATION when %type !empty { let upper = to_upper(%type) + %upper == "AWS::NEW::SERVICE" - %upper == /AWS::NEW::SERVICE/ + << Violation: expected a value to be all uppercase >> } ``` ### substring -The substring function adds support to collect a part of all strings resolved from a query +The `substring` function allows to extract a part of string(s) resolved from a query -This function accepts 3 arguments: +#### Argument(s) -- The first argument is a query, each string that is resolved from this query will be operated on -- The second argument is either a query that resolves to an int or a literal int, this is the starting index for the substring (inclusive) -- The third argument is either a query that resolves to an int or a literal int, this is the ending index for the substring (exclusive) +1. `base_string`: A query that resolves to string(s) +2. `start_index`: A query that resolves to an int or a literal int, this is the starting index for the substring (inclusive) +3. `end_index`: A query that resolves to an int or a literal int, this is the ending index for the substring (exclusive) -The return value for this function takes the strings resolved from the first argument, and returns a result of substrings for each one of them: -Note: Any string that would result in an index out of bounds from the 2nd or 3rd argument is skipped +#### Return value + +A result of substrings for each `base_string` passed as input + + - Note: Any string that would result in an index out of bounds from the 2nd or 3rd argument is skipped + +#### Example ``` let template = Resources.*[ Type == 'AWS::New::Service'] @@ -180,18 +238,25 @@ rule TEST_SUBSTRING when %template !empty { let res = substring(%arn, 0, 3) %res == "arn" + << Violation: Substring extracted does not match with the expected outcome >> } ``` ### url_decode -This function accepts a single argument: +This function can be used to transform URL encoded strings into their decoded versions + +#### Argument(s) + +1. `base_string`: A query that resolves to a string or a string literal -- this argument can either be a query that resolves to a string or a string literal. +#### Return value -The return value for this function is a query that contains each url decoded version of every string value from the input +A query containing URL decoded version of every string value from `base_string` -The following rule shows how you could url_decode the string `This%20string%20will%20be%20URL%20encoded` +#### Example + +The following rule shows how you could `url_decode` the string `This%20string%20will%20be%20URL%20encoded` ``` let template = Resources.*[ Type == 'AWS::New::Service'] @@ -202,6 +267,10 @@ rule SOME_RULE when %template !empty { let res = url_decode(%encoded) %res == "This string will be URL encoded" + << + Violation: The result of URL decoding does not + match with the expected outcome + >> } ``` @@ -209,11 +278,15 @@ rule SOME_RULE when %template !empty { ### count -The count function adds support to count the number of items that a query resolves to +This function can be used to count the number of items that a query resolves to + +#### Argument(s) -This function accepts a single argument: +1. `collection`: A query that can resolves to any type -- This argument is a query that can resolve to any type - the number of resolved values from this query is returned as the result +#### Return value + +The number of resolved values from `collection` is returned as the result The following rules show different ways we can use the count function. @@ -221,21 +294,26 @@ The following rules show different ways we can use the count function. - The second queries a list object, and counts the elements in the list - The third queries for all resources that are s3 buckets and have a PublicAcessBlockConfiguration property +#### Example + ``` let template = Resources.*[ Type == 'AWS::New::Service' ] rule SOME_RULE when %template !empty { let props = %template.Properties.* let res = count(%props) - %res == 3 + %res >= 3 + << Violation: There must be at least 3 properties set for this service >> let collection = %template.Collection.* let res2 = count(%collection) - %res2 == 3 + %res2 >= 3 + << Violation: Collection should contain at least 3 items >> let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ] let b = %buckets[ Properties.PublicAccessBlockConfiguration exists ] let res3 = count(%b) - %res3 == 2 + %res3 >= 2 + << Violation: At least 2 buckets should have PublicAccessBlockConfiguration set >> } ``` diff --git a/docs/KNOWN_ISSUES.md b/docs/KNOWN_ISSUES.md index 479728695..da097d8b2 100644 --- a/docs/KNOWN_ISSUES.md +++ b/docs/KNOWN_ISSUES.md @@ -35,3 +35,34 @@ let api_gws = Resources.*[ Type == 'AWS::ApiGateway::RestApi' ] 3. When performing `!=` comparison, if the values are incompatible like comparing a `string` to `int`, an error is thrown internally but currently suppressed and converted to `false` to satisfy the requirements of Rust’s [PartialEq](https://doc.rust-lang.org/std/cmp/trait.PartialEq.html). We are tracking to release a fix for this issue soon. 4. `exists` and `empty` checks do not display the JSON pointer path inside the document in the error messages. Both these clauses often have retrieval errors which does not maintain this traversal information today. We are tracking to resolve this issue. 5. Currently, for `string` literals, Guard does not support embedded escaped strings. We are tracking to resolve this issue soon. +6. **No support for inline functions** + + We **do not** support inline usage of functions at the moment. The support for built-in functions is currently limited to assignment of the return value to a variable. + + Consider an example wherein our template has a node named `Instances` which is a collection. We need to author a rule that checks to ensure this collection contains a certain number of minimum items, say 2. + + This is currently **NOT SUPPORTED**: + ``` + # Not supported at the moment + + rule INSTANCES_COUNT_CHECK { + count(Instances.*) < 2 + << Violation: We should have at least 2 instances >> + } + ``` + While the above code snippet might be tempting to use as it's more intuitive, we haven't made the changes required to support it in our grammar yet. + + > **Workaround**: Assign function value to a variable and then use this variable thereafter in all clauses that follow including the conditions. + + So, our example rule now becomes: + ``` + # Use this instead + + rule INSTANCES_COUNT_CHECK { + let no_of_instances = count(Instances.*) + + %no_of_instances < 2 + << Violation: We should have at least 2 instances >> + } + ``` + \ No newline at end of file diff --git a/guard-ffi/Cargo.toml b/guard-ffi/Cargo.toml index f8da6780d..4cd1810de 100644 --- a/guard-ffi/Cargo.toml +++ b/guard-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard-ffi" -version = "3.0.0-beta" +version = "3.0.0" edition = "2018" authors = ["Diwakar Chakravarthy", "John Tompkins", "Omkar Hegde", "Priya Padmanaban", "aws-cloudformation-developers ", "Tyler Southwick"] description = "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." @@ -14,5 +14,5 @@ keywords = ["policy-as-code", "guard", "cfn-guard", "security", "compliance"] crate-type = ["rlib", "dylib"] [dependencies] -cfn-guard = { version = "3.0.0-beta", path = "../guard" } +cfn-guard = { version = "3.0.0", path = "../guard" } ffi-support = "0.4.4" diff --git a/guard-ffi/src/errors.rs b/guard-ffi/src/errors.rs index a872698e8..a86d6632b 100644 --- a/guard-ffi/src/errors.rs +++ b/guard-ffi/src/errors.rs @@ -29,6 +29,7 @@ fn get_code(e: &Error) -> ErrorCode { Error::MissingValue(_err) => 16, Error::FileNotFoundError(_) => 17, Error::IllegalArguments(_) => 18, + Error::InternalError(_) => unreachable!(), }; ErrorCode::new(code) } diff --git a/guard-lambda/Cargo.toml b/guard-lambda/Cargo.toml index e7c9000f5..663ff398e 100644 --- a/guard-lambda/Cargo.toml +++ b/guard-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard-lambda" -version = "3.0.0-beta" +version = "3.0.0" authors = ["Diwakar Chakravarthy", "John Tompkins", "Omkar Hegde", "Priya Padmanaban", "Bryan Ayala", "Kexiang Wang", "Akshay Rane", "Josh Fried", "aws-cloudformation-developers "] description = "Lambda version of cfn-guard. Checks JSON- or YAML- formatted structured data for policy compliance using a simple, policy-as-code, declarative syntax" @@ -17,4 +17,4 @@ serde_derive = "1.0.92" simple_logger = "4.0.0" log = "0.4.6" tokio = "1.24.2" -cfn-guard = { version = "3.0.0-beta", path = "../guard" } +cfn-guard = { version = "3.0.0", path = "../guard" } diff --git a/guard-lambda/src/lib.rs b/guard-lambda/src/lib.rs index 2a043412b..e36eff06b 100644 --- a/guard-lambda/src/lib.rs +++ b/guard-lambda/src/lib.rs @@ -1 +1,2 @@ +#![allow(special_module_name)] pub mod main; diff --git a/guard-lambda/src/main.rs b/guard-lambda/src/main.rs index 07523d11b..39937843b 100644 --- a/guard-lambda/src/main.rs +++ b/guard-lambda/src/main.rs @@ -27,6 +27,7 @@ pub struct CustomOutput { } #[tokio::main] +#[allow(dead_code)] async fn main() -> Result<(), Error> { SimpleLogger::new() .with_level(LevelFilter::Info) @@ -48,13 +49,13 @@ pub async fn call_cfn_guard(e: CustomEvent, _c: Context) -> Result t, - Err(e) => (e.to_string()), + Err(e) => e.to_string(), }; let json_value: serde_json::Value = serde_json::from_str(&result)?; results_vec.push(json_value) @@ -66,7 +67,7 @@ pub async fn call_cfn_guard(e: CustomEvent, _c: Context) -> Result) -> Result<(), std::fmt::Error> { - write!(f, "{}", serde_json::to_string_pretty(&self).unwrap()); + write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())?; Ok(()) } } diff --git a/guard-lambda/tests/lambda-handler.rs b/guard-lambda/tests/lambda-handler.rs index f0420babb..81c080fa1 100644 --- a/guard-lambda/tests/lambda-handler.rs +++ b/guard-lambda/tests/lambda-handler.rs @@ -1,6 +1,5 @@ #[cfg(test)] mod tests { - use cfn_guard_lambda; use cfn_guard_lambda::main::{call_cfn_guard, CustomEvent, CustomOutput}; use lambda_runtime::Context; diff --git a/guard/Cargo.toml b/guard/Cargo.toml index 3c8a0aaf3..f262647e1 100644 --- a/guard/Cargo.toml +++ b/guard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard" -version = "3.0.0-beta" +version = "3.0.0" edition = "2018" authors = ["Diwakar Chakravarthy", "John Tompkins", "Omkar Hegde", "Priya Padmanaban", "Bryan Ayala", "Kexiang Wang", "Akshay Rane", "Tyler Southwick", "Josh Fried", "aws-cloudformation-developers "] diff --git a/guard/README.md b/guard/README.md index 27b46a109..0131890f0 100644 --- a/guard/README.md +++ b/guard/README.md @@ -1,4 +1,4 @@ -# AWS CloudFormation Guard 2.0's Modes of Operation +# AWS CloudFormation Guard's Modes of Operation 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. diff --git a/guard/resources/parse-tree/output-dir/parse_tree_functions.yaml b/guard/resources/parse-tree/output-dir/parse_tree_functions.yaml new file mode 100644 index 000000000..0c10cf959 --- /dev/null +++ b/guard/resources/parse-tree/output-dir/parse_tree_functions.yaml @@ -0,0 +1,158 @@ +assignments: +- var: template + value: + AccessClause: + query: + - Key: Resources + - AllValues: null + - Filter: + - null + - - - Clause: + access_clause: + query: + query: + - Key: Type + match_all: true + comparator: + - Eq + - false + compare_with: + Value: + path: '' + value: AWS::New::Service + custom_message: null + location: + line: 1 + column: 29 + negation: false + match_all: true +- var: type + value: + AccessClause: + query: + - Key: Resources + - Key: newServer + - Key: Type + match_all: true +guard_rules: +- rule_name: SOME_RULE + conditions: + - - Clause: + access_clause: + query: + query: + - Key: '%type' + match_all: true + comparator: + - Empty + - true + compare_with: null + custom_message: null + location: + line: 5 + column: 21 + negation: false + block: + assignments: + - var: lower + value: + FunctionCall: + parameters: + - AccessClause: + query: + - Key: '%type' + match_all: true + name: to_lower + location: + line: 6 + column: 17 + - var: upper + value: + FunctionCall: + parameters: + - AccessClause: + query: + - Key: '%type' + match_all: true + name: to_upper + location: + line: 10 + column: 17 + conjunctions: + - - Clause: + Clause: + access_clause: + query: + query: + - Key: '%lower' + match_all: true + comparator: + - Eq + - false + compare_with: + Value: + path: '' + value: aws::new::service + custom_message: null + location: + line: 7 + column: 5 + negation: false + - - Clause: + Clause: + access_clause: + query: + query: + - Key: '%lower' + match_all: true + comparator: + - Eq + - false + compare_with: + Value: + path: '' + value: /aws::new::service/ + custom_message: null + location: + line: 8 + column: 5 + negation: false + - - Clause: + Clause: + access_clause: + query: + query: + - Key: '%upper' + match_all: true + comparator: + - Eq + - false + compare_with: + Value: + path: '' + value: AWS::NEW::SERVICE + custom_message: null + location: + line: 11 + column: 5 + negation: false + - - Clause: + Clause: + access_clause: + query: + query: + - Key: '%upper' + match_all: true + comparator: + - Eq + - false + compare_with: + Value: + path: '' + value: /AWS::NEW::SERVICE/ + custom_message: null + location: + line: 12 + column: 5 + negation: false +parameterized_rules: [] diff --git a/guard/resources/parse-tree/output-dir/s3_bucket_server_side_encryption_parse_tree.json b/guard/resources/parse-tree/output-dir/s3_bucket_server_side_encryption_parse_tree.json new file mode 100644 index 000000000..fc9f6a8d9 --- /dev/null +++ b/guard/resources/parse-tree/output-dir/s3_bucket_server_side_encryption_parse_tree.json @@ -0,0 +1,267 @@ +{ + "assignments": [ + { + "var": "s3_buckets_server_side_encryption", + "value": { + "AccessClause": { + "query": [ + { + "Key": "Resources" + }, + { + "AllValues": null + }, + { + "Filter": [ + null, + [ + [ + { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "Type" + } + ], + "match_all": true + }, + "comparator": [ + "Eq", + false + ], + "compare_with": { + "Value": { + "path": "", + "value": "AWS::S3::Bucket" + } + }, + "custom_message": null, + "location": { + "line": 1, + "column": 54 + } + }, + "negation": false + } + } + ], + [ + { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "Metadata" + }, + { + "Key": "guard" + }, + { + "Key": "SuppressedRules" + } + ], + "match_all": true + }, + "comparator": [ + "Exists", + true + ], + "compare_with": null, + "custom_message": null, + "location": { + "line": 2, + "column": 3 + } + }, + "negation": false + } + }, + { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "Metadata" + }, + { + "Key": "guard" + }, + { + "Key": "SuppressedRules" + }, + { + "AllValues": null + } + ], + "match_all": true + }, + "comparator": [ + "Eq", + true + ], + "compare_with": { + "Value": { + "path": "", + "value": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED" + } + }, + "custom_message": null, + "location": { + "line": 3, + "column": 3 + } + }, + "negation": false + } + } + ] + ] + ] + } + ], + "match_all": true + } + } + } + ], + "guard_rules": [ + { + "rule_name": "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED", + "conditions": [ + [ + { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "%s3_buckets_server_side_encryption" + } + ], + "match_all": true + }, + "comparator": [ + "Empty", + true + ], + "compare_with": null, + "custom_message": null, + "location": { + "line": 6, + "column": 52 + } + }, + "negation": false + } + } + ] + ], + "block": { + "assignments": [], + "conjunctions": [ + [ + { + "Clause": { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "%s3_buckets_server_side_encryption" + }, + { + "AllIndices": null + }, + { + "Key": "Properties" + }, + { + "Key": "BucketEncryption" + } + ], + "match_all": true + }, + "comparator": [ + "Exists", + false + ], + "compare_with": null, + "custom_message": null, + "location": { + "line": 7, + "column": 3 + } + }, + "negation": false + } + } + } + ], + [ + { + "Clause": { + "Clause": { + "access_clause": { + "query": { + "query": [ + { + "Key": "%s3_buckets_server_side_encryption" + }, + { + "AllIndices": null + }, + { + "Key": "Properties" + }, + { + "Key": "BucketEncryption" + }, + { + "Key": "ServerSideEncryptionConfiguration" + }, + { + "AllIndices": null + }, + { + "Key": "ServerSideEncryptionByDefault" + }, + { + "Key": "SSEAlgorithm" + } + ], + "match_all": true + }, + "comparator": [ + "In", + false + ], + "compare_with": { + "Value": { + "path": "", + "value": [ + "aws:kms", + "AES256" + ] + } + }, + "custom_message": "\n Violation: S3 Bucket must enable server-side encryption.\n Fix: Set the S3 Bucket property BucketEncryption.ServerSideEncryptionConfiguration.ServerSideEncryptionByDefault.SSEAlgorithm to either \"aws:kms\" or \"AES256\"\n ", + "location": { + "line": 8, + "column": 3 + } + }, + "negation": false + } + } + } + ] + ] + } + } + ], + "parameterized_rules": [] +} \ No newline at end of file diff --git a/guard/resources/test-command/data-dir/failing_test.yaml b/guard/resources/test-command/data-dir/failing_test.yaml new file mode 100644 index 000000000..1fdefafc4 --- /dev/null +++ b/guard/resources/test-command/data-dir/failing_test.yaml @@ -0,0 +1,14 @@ +- name: S3 Bucket Encryption set to SSE AES 256, PASS + input: + Resources: + ExampleS3: + Type: AWS::S3::Bucket + Properties: + BucketName: my-bucket + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + expectations: + rules: + S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED: FAIL diff --git a/guard/resources/test-command/functions/data/template.yaml b/guard/resources/test-command/functions/data/template.yaml index 89e74c70d..bdae71b71 100644 --- a/guard/resources/test-command/functions/data/template.yaml +++ b/guard/resources/test-command/functions/data/template.yaml @@ -5,17 +5,27 @@ newServer: Type: AWS::New::Service Properties: - Policy: | - { - "Principal": "*", - "Actions": ["s3*", "ec2*"] - } - Arn: arn:aws:newservice:us-west-2:123456789012:Table/extracted - Encoded: This%20string%20will%20be%20URL%20encoded - Collection: - - a - - b - - c + BucketPolicy: + PolicyText: '{"Version":"2012-10-17","Statement":[{"Sid":"DenyReducedReliabilityStorage","Effect":"Deny","Principal":"*","Action":"s3:*","Resource":"arn:aws:s3:::s3-test-123/*","Condition":{"StringEquals":{"s3:x-amz-storage-class-123":["ONEZONE_IA","REDUCED_REDUNDANCY"]}}}]}' expectations: rules: SOME_RULE: PASS + +- name: Fail + input: + Resources: + newServer: + Type: AWS::New::Service + Properties: + BucketPolicy: + PolicyText: '{"Version":"2012-10-17","Statement":[{"Sid":"DenyReducedReliabilityStorage","Effect":"Allow","Principal":"*","Action":"s3:*","Resource":"arn:aws:s3:::s3-test-123/*","Condition":{"StringEquals":{"s3:x-amz-storage-class-123":["ONEZONE_IA","REDUCED_REDUNDANCY"]}}}]}' + expectations: + rules: + SOME_RULE: FAIL + +- name: Skip + input: + Resources: {} + expectations: + rules: + SOME_RULE: SKIP diff --git a/guard/resources/test-command/functions/rules/json_parse.guard b/guard/resources/test-command/functions/rules/json_parse.guard index 6bb7dca39..cd24046fc 100644 --- a/guard/resources/test-command/functions/rules/json_parse.guard +++ b/guard/resources/test-command/functions/rules/json_parse.guard @@ -1,16 +1,15 @@ let template = Resources.*[ Type == 'AWS::New::Service'] -let expected = { - "Principal": "*", - "Actions": ["s3*", "ec2*"] -} - rule SOME_RULE when %template !empty { - let policy = %template.Properties.Policy + let policy = %template.Properties.BucketPolicy.PolicyText let res = json_parse(%policy) %res !empty - %res == %expected + %res.Statement[*] + { + Effect == "Deny" + Resource == "arn:aws:s3:::s3-test-123/*" + } } diff --git a/guard/resources/test-command/output-dir/functions.out b/guard/resources/test-command/output-dir/functions.out new file mode 100644 index 000000000..007f64bbd --- /dev/null +++ b/guard/resources/test-command/output-dir/functions.out @@ -0,0 +1,15 @@ +Test Case #1 +Name: Pass + PASS Rules: + SOME_RULE: Expected = PASS + +Test Case #2 +Name: Fail + PASS Rules: + SOME_RULE: Expected = FAIL + +Test Case #3 +Name: Skip + PASS Rules: + SOME_RULE: Expected = SKIP + diff --git a/guard/resources/validate/blank.yaml b/guard/resources/validate/blank.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/guard/resources/validate/failing_template_with_slash_in_key.yaml b/guard/resources/validate/failing_template_with_slash_in_key.yaml new file mode 100644 index 000000000..14c0e863e --- /dev/null +++ b/guard/resources/validate/failing_template_with_slash_in_key.yaml @@ -0,0 +1,12 @@ +Resources: + A/Resource/Name/With/Slash: + Type: AWS::S3::Bucket + Properties: + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + BucketEncryption: + VersioningConfiguration: + Status: Enabled diff --git a/guard/resources/validate/functions/output/failing_count_show_summary_all.out b/guard/resources/validate/functions/output/failing_count_show_summary_all.out new file mode 100644 index 000000000..082d5089d --- /dev/null +++ b/guard/resources/validate/functions/output/failing_count_show_summary_all.out @@ -0,0 +1,30 @@ +template.yaml Status = FAIL +FAILED rules +count_with_message.guard/SOME_RULE FAIL +--- +Evaluating data template.yaml against rules count_with_message.guard +Number of non-compliant resources 1 +Resource = newServer { + Type = AWS::New::Service + Rule = SOME_RULE { + ALL { + Check = %res EQUALS 3 { + ComparisonError { + Error = Check was not compliant as property value [Path=/Resources/newServer/Properties[L:4,C:6] Value=0] not equal to value [Path=[L:0,C:0] Value=3]. + PropertyPath = /Resources/newServer/Properties[L:4,C:6] + Operator = EQUAL + Value = 0 + ComparedWith = 3 + Code: + 2. newServer: + 3. Type: AWS::New::Service + 4. Properties: + 5. Policy: | + 6. { + 7. "Principal": "*", + + } + } + } + } +} diff --git a/guard/resources/validate/functions/output/failing_join_show_summary_all.out b/guard/resources/validate/functions/output/failing_join_show_summary_all.out new file mode 100644 index 000000000..1341bd44d --- /dev/null +++ b/guard/resources/validate/functions/output/failing_join_show_summary_all.out @@ -0,0 +1,31 @@ +template.yaml Status = FAIL +FAILED rules +join_with_message.guard/TEST_COLLECTION FAIL +--- +Evaluating data template.yaml against rules join_with_message.guard +Number of non-compliant resources 1 +Resource = newServer { + Type = AWS::New::Service + Rule = TEST_COLLECTION { + ALL { + Check = %res EQUALS "a,b" { + ComparisonError { + Message = Violation: The joined value does not match the expected result + Error = Check was not compliant as property value [Path=/Resources/newServer/Collection/0[L:12,C:8] Value="a,b,c"] not equal to value [Path=[L:0,C:0] Value="a,b"]. + PropertyPath = /Resources/newServer/Collection/0[L:12,C:8] + Operator = EQUAL + Value = "a,b,c" + ComparedWith = "a,b" + Code: + 10. Arn: arn:aws:newservice:us-west-2:123456789012:Table/extracted + 11. Encoded: This%20string%20will%20be%20URL%20encoded + 12. Collection: + 13. - a + 14. - b + 15. - c + + } + } + } + } +} diff --git a/guard/resources/validate/functions/rules/count_with_message.guard b/guard/resources/validate/functions/rules/count_with_message.guard new file mode 100644 index 000000000..804e787c7 --- /dev/null +++ b/guard/resources/validate/functions/rules/count_with_message.guard @@ -0,0 +1,9 @@ +let template = Resources.*[ Type == 'AWS::New::Service' ] +rule SOME_RULE when %template !empty { + let props = %template.Properties + let stuff = %props.stuff + let other = %stuff.other + let collection = %other.dne.* + let res = count(%collection) + %res == 3 +} diff --git a/guard/resources/validate/functions/rules/join_with_message.guard b/guard/resources/validate/functions/rules/join_with_message.guard new file mode 100644 index 000000000..42446d482 --- /dev/null +++ b/guard/resources/validate/functions/rules/join_with_message.guard @@ -0,0 +1,9 @@ +let template = Resources.*[ Type == 'AWS::New::Service'] + +rule TEST_COLLECTION when %template !empty { + let collection = %template.Collection.* + + let res = join(%collection, ",") + %res == "a,b" + << Violation: The joined value does not match the expected result >> +} \ No newline at end of file diff --git a/guard/resources/validate/output-dir/failing_template_with_slash_in_key.out b/guard/resources/validate/output-dir/failing_template_with_slash_in_key.out new file mode 100644 index 000000000..134fe5a11 --- /dev/null +++ b/guard/resources/validate/output-dir/failing_template_with_slash_in_key.out @@ -0,0 +1,46 @@ +failing_template_with_slash_in_key.yaml Status = FAIL +FAILED rules +s3_bucket_server_side_encryption_enabled.guard/S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED FAIL +--- +Evaluating data failing_template_with_slash_in_key.yaml against rules s3_bucket_server_side_encryption_enabled.guard +Number of non-compliant resources 1 +Resource = A/Resource/Name/With/Slash { + Type = AWS::S3::Bucket + Rule = S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED { + ALL { + Check = %s3_buckets_server_side_encryption[*].Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm IN ["aws:kms","AES256"] { + Message { + Violation: S3 Bucket must enable server-side encryption. + Fix: Set the S3 Bucket property BucketEncryption.ServerSideEncryptionConfiguration.ServerSideEncryptionByDefault.SSEAlgorithm to either "aws:kms" or "AES256" + } + RequiredPropertyError { + PropertyPath = /Resources/A/Resource/Name/With/Slash/Properties/BucketEncryption[L:9,C:23] + MissingProperty = ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm + Reason = Attempting to retrieve from key ServerSideEncryptionConfiguration but type is not an struct type at path /Resources/A/Resource/Name/With/Slash/Properties/BucketEncryption[L:9,C:23], Type = String, Value = String((Path("/Resources/A/Resource/Name/With/Slash/Properties/BucketEncryption", Location { line: 9, col: 23 }), "")) + Code: + 7. BlockPublicPolicy: true + 8. IgnorePublicAcls: true + 9. RestrictPublicBuckets: true + 10. BucketEncryption: + 11. VersioningConfiguration: + 12. Status: Enabled + } + } + } + } +} +`- File(failing_template_with_slash_in_key.yaml, Status=FAIL)[Context=File(rules=1)] + `- Rule(S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED, Status=FAIL)[Context=S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED] + |- Rule/When(Status=PASS)[Context=Rule#S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED/When] + | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block %s3_buckets_server_side_encryption not EMPTY ] + | |- Filter/ConjunctionsBlock(Status=PASS)[Context=Filter/Map#2] + | | |- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block Type EQUALS "AWS::S3::Bucket"] + | | | `- GuardClauseValueCheck(Status=PASS)[Context= Type EQUALS "AWS::S3::Bucket"] + | | `- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::GuardClause#disjunction] + | | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block Metadata.guard.SuppressedRules not EXISTS ] + | | `- GuardClauseValueCheck(Status=PASS)[Context= Metadata.guard.SuppressedRules not EXISTS ] + | `- GuardClauseValueCheck(Status=PASS)[Context= %s3_buckets_server_side_encryption not EMPTY ] + |- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block %s3_buckets_server_side_encryption[*].Properties.BucketEncryption EXISTS ] + | `- GuardClauseValueCheck(Status=PASS)[Context= %s3_buckets_server_side_encryption[*].Properties.BucketEncryption EXISTS ] + `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block %s3_buckets_server_side_encryption[*].Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm IN ["aws:kms","AES256"]] + `- GuardClauseBinaryCheck(Status=FAIL, Comparison= IN, from=(unresolved, Path=/Resources/A/Resource/Name/With/Slash/Properties/BucketEncryption[L:9,C:23] Value=""), to=)[Context= %s3_buckets_server_side_encryption[*].Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm IN ["aws:kms","AES256"]] diff --git a/guard/resources/validate/output-dir/failing_template_without_resources_at_root.out b/guard/resources/validate/output-dir/failing_template_without_resources_at_root.out new file mode 100644 index 000000000..6f148c108 --- /dev/null +++ b/guard/resources/validate/output-dir/failing_template_without_resources_at_root.out @@ -0,0 +1,37 @@ +template_where_resources_isnt_root.json Status = FAIL +FAILED rules +workshop.guard/assert_no_wildcard_actions FAIL +--- +Evaluation of rules workshop.guard against data template_where_resources_isnt_root.json +-- +Property [/Roles/0] in data [template_where_resources_isnt_root.json] is not compliant with [assert_no_wildcard_actions] because needed value at [{"RoleName":"MyRole","RolePath":"/","TrustPolicy":{"Statement":[{"Effect":"Allow","Principal":{"AWS":["314595678785"]},"Action":["sts:AssumeRole"]}]},"Policies":[{"Name":"root","Policy":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]},"Path":"/","IsAWSManagedPolicy":false}]}] was not empty. Error Message [] +Property [/Roles/0/Policies/0] in data [template_where_resources_isnt_root.json] is not compliant with [assert_no_wildcard_actions] because needed value at [{"Name":"root","Policy":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]},"Path":"/","IsAWSManagedPolicy":false}] was not empty. Error Message [] +Property [/Roles/0/Policies/0/Policy/Statement/0/Action] in data [template_where_resources_isnt_root.json] is not compliant with [assert_no_wildcard_actions] because provided value ["*"] did match expected value ["*"]. Error Message [] +-- +`- File(template_where_resources_isnt_root.json, Status=FAIL)[Context=File(rules=1)] + `- Rule(assert_no_wildcard_actions, Status=FAIL)[Context=assert_no_wildcard_actions] + |- Disjunction(Status = FAIL)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + | |- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block Roles[*] EMPTY ] + | | `- GuardClauseUnaryCheck(Status=FAIL, Comparison= EMPTY, Value-At=(resolved, Path=/Roles/0[L:5,C:8] Value={"RoleName":"MyRole","RolePath":"/","TrustPolicy":{"Statement":[{"Effect":"Allow","Principal":{"AWS":["314595678785"]},"Action":["sts:AssumeRole"]}]},"Policies":[{"Name":"root","Policy":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]},"Path":"/","IsAWSManagedPolicy":false}]}))[Context= Roles[*] EMPTY ] + | `- GuardValueBlockCheck(Status = FAIL)[Context=BlockGuardClause#Location[file:workshop.guard, line:7, column:3]] + | `- Disjunction(Status = FAIL)[Context=cfn_guard::rules::exprs::GuardClause#disjunction] + | |- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block Policies[*] EMPTY ] + | | `- GuardClauseUnaryCheck(Status=FAIL, Comparison= EMPTY, Value-At=(resolved, Path=/Roles/0/Policies/0[L:24,C:16] Value={"Name":"root","Policy":{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]},"Path":"/","IsAWSManagedPolicy":false}))[Context= Policies[*] EMPTY ] + | `- GuardValueBlockCheck(Status = FAIL)[Context=BlockGuardClause#Location[file:workshop.guard, line:10, column:7]] + | `- GuardClauseBlock(Status = FAIL)[Context=GuardAccessClause#block Action[*] not EQUALS "*"] + | `- GuardClauseBinaryCheck(Status=FAIL, Comparison=not EQUALS, from=(resolved, Path=/Roles/0/Policies/0/Policy/Statement/0/Action[L:31,C:42] Value="*"), to=(resolved, Path=[L:0,C:0] Value="*"))[Context= Action[*] not EQUALS "*"] + |- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block Users[*] EMPTY ] + | `- GuardClauseValueCheck(Status=PASS)[Context= Users[*] EMPTY ] + |- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block Groups[*] EMPTY ] + | `- GuardClauseValueCheck(Status=PASS)[Context= Groups[*] EMPTY ] + |- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block Resources[*] EMPTY ] + | `- GuardClauseValueCheck(Status=PASS)[Context= Resources[*] EMPTY ] + |- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + | `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block PermissionSets[*] EMPTY ] + | `- GuardClauseValueCheck(Status=PASS)[Context= PermissionSets[*] EMPTY ] + `- Disjunction(Status = PASS)[Context=cfn_guard::rules::exprs::RuleClause#disjunction] + `- GuardClauseBlock(Status = PASS)[Context=GuardAccessClause#block OrphanedPolicies[*] EMPTY ] + `- GuardClauseValueCheck(Status=PASS)[Context= OrphanedPolicies[*] EMPTY ] diff --git a/guard/resources/validate/template_where_resources_isnt_root.json b/guard/resources/validate/template_where_resources_isnt_root.json new file mode 100644 index 000000000..2f8fb166b --- /dev/null +++ b/guard/resources/validate/template_where_resources_isnt_root.json @@ -0,0 +1,48 @@ +{ + "Region": "us-east-1", + "Account": "314595678785", + "Partition": "aws", + "Roles": [ + { + "RoleName": "MyRole", + "RolePath": "/", + "TrustPolicy": { + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "AWS": [ + "314595678785" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Policies": [ + { + "Name": "root", + "Policy": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "*", + "Resource": "*" + } + ] + }, + "Path": "/", + "IsAWSManagedPolicy": false + } + ] + } + ], + "PermissionSets": [], + "Users": [], + "Groups": [], + "Resources": [], + "OrphanedPolicies": [] +} diff --git a/guard/resources/validate/workshop.guard b/guard/resources/validate/workshop.guard new file mode 100644 index 000000000..874e2dd94 --- /dev/null +++ b/guard/resources/validate/workshop.guard @@ -0,0 +1,62 @@ +# This rule will return +# 1) FAIL if any IAM policy has a wildcard action +# 2) PASS if there are no IAM policies or IAM policies have no wildcard actions +# +rule assert_no_wildcard_actions { + Roles[*] empty or + Roles[*] { + # if there are no policies - PASS + Policies[*] empty or + Policies[*].Policy.Statement[*] { + Action[*] != '*' + } + } + + # if there are no users - PASS + Users[*] empty or + Users[*] { + # if there are no policies - PASS + Policies[*] empty or + Policies[*].Policy.Statement[*] { + Action[*] != '*' + } + } + + # if there are no groups - PASS + Groups[*] empty or + Groups[*] { + # if there are no policies - PASS + Policies[*] empty or + Policies[*].Policy.Statement[*] { + Action[*] != '*' + } + } + + # if there are no resources - PASS + Resources[*] empty or + Resources[*] { + # resources only have a single policy + Policy.Policy.Statement[*] { + Action[*] != '*' + } + } + + # if there are no permission sets - PASS + PermissionSets[*] empty or + PermissionSets[*] { + # if there are no policies - PASS + Policies[*] empty or + Policies[*].Policy.Statement[*] { + Action[*] != '*' + } + } + + # if there are no orphaned policies - PASS + OrphanedPolicies[*] empty or + OrphanedPolicies[*] { + # orphaned policies have direct policy elements, no need to traverse to Policies[*] + Policy.Statement[*] { + Action[*] != '*' + } + } +} diff --git a/guard/src/commands/aws_meta_appender.rs b/guard/src/commands/aws_meta_appender.rs index bf9f43f67..e72d3a0a1 100644 --- a/guard/src/commands/aws_meta_appender.rs +++ b/guard/src/commands/aws_meta_appender.rs @@ -18,6 +18,7 @@ impl<'d> EvaluationContext for MetadataAppender<'d> { self.delegate.rule_status(rule_name) } + #[allow(clippy::never_loop)] fn end_evaluation( &self, eval_type: EvaluationType, @@ -42,7 +43,7 @@ impl<'d> EvaluationContext for MetadataAppender<'d> { parts[2] ); let AccessQuery { - query: query, + query, match_all: all, } = AccessQuery::try_from(query.as_str()).unwrap(); if let Ok(selected) = diff --git a/guard/src/commands/aws_meta_appender_tests.rs b/guard/src/commands/aws_meta_appender_tests.rs index 386c0058c..5e4649703 100644 --- a/guard/src/commands/aws_meta_appender_tests.rs +++ b/guard/src/commands/aws_meta_appender_tests.rs @@ -1,4 +1,3 @@ -use super::super::common_test_helpers::DummyEval; use super::*; #[test] @@ -37,33 +36,33 @@ fn append_cdk_metadata_test() -> Result<()> { let query = AccessQuery::try_from( "Resources['table1F1EAFA30'].Properties.ProvisionedThroughput.ReadCapacityUnits", )?; - struct Capture {}; + struct Capture {} impl EvaluationContext for Capture { - fn resolve_variable(&self, variable: &str) -> Result> { + fn resolve_variable(&self, _: &str) -> Result> { unimplemented!() } - fn rule_status(&self, rule_name: &str) -> Result { + fn rule_status(&self, _: &str) -> Result { unimplemented!() } fn end_evaluation( &self, - eval_type: EvaluationType, - context: &str, + _: EvaluationType, + _: &str, msg: String, - from: Option, - to: Option, - status: Option, + _: Option, + _: Option, + _: Option, _cmp: Option<(CmpOperator, bool)>, ) { assert_ne!(msg.as_str(), ""); - assert_eq!(msg.starts_with("FIRST PART"), true); - assert_eq!(msg.len() > "FIRST PART".len(), true); + assert!(msg.starts_with("FIRST PART")); + assert!(msg.len() > "FIRST PART".len()); println!("{}", msg); } - fn start_evaluation(&self, eval_type: EvaluationType, context: &str) { + fn start_evaluation(&self, _: EvaluationType, _: &str) { unimplemented!() } } diff --git a/guard/src/commands/files.rs b/guard/src/commands/files.rs index b407ced9f..d3399de25 100644 --- a/guard/src/commands/files.rs +++ b/guard/src/commands/files.rs @@ -2,10 +2,9 @@ use std::cmp::Ordering; use std::fs::File; use std::io::{BufReader, Read}; use std::path::PathBuf; -use std::str::FromStr; use crate::rules::errors::Error; -use walkdir::{DirEntry, WalkDir}; +use walkdir::WalkDir; pub(crate) fn read_file_content(file: File) -> Result { let mut file_content = String::new(); @@ -14,27 +13,6 @@ pub(crate) fn read_file_content(file: File) -> Result { Ok(file_content) } -pub(crate) fn get_files(file: &str, sort: F) -> Result, Error> -where - F: FnMut(&walkdir::DirEntry, &walkdir::DirEntry) -> Ordering + Send + Sync + 'static, -{ - let path = PathBuf::from_str(file)?; - let input_file = File::open(file)?; - let metadata = input_file.metadata()?; - Ok(if metadata.is_file() { - vec![path] - } else { - let result = get_files_with_filter(file, sort, |entry| { - entry - .file_name() - .to_str() - .map(|name| !name.ends_with("/")) - .unwrap_or(false) - })?; - result - }) -} - pub(crate) fn get_files_with_filter( file: &str, sort: S, @@ -44,25 +22,15 @@ where S: FnMut(&walkdir::DirEntry, &walkdir::DirEntry) -> Ordering + Send + Sync + 'static, F: Fn(&walkdir::DirEntry) -> bool, { - let mut selected = Vec::with_capacity(10); let walker = WalkDir::new(file).sort_by(sort).into_iter(); - let dir_check = |entry: &DirEntry| { - // select directories to traverse - if entry.path().is_dir() { - return true; - } - filter(entry) - }; - for each in walker.filter_entry(dir_check) { - // - // We are ignoring errors here. TODO fix this later - // - if let Ok(entry) = each { - if entry.path().is_file() { - selected.push(entry.into_path()); - } - } - } + + let selected = walker + .filter_entry(|entry| entry.path().is_dir() || filter(entry)) + .flatten() + .filter(|entry| entry.path().is_file()) + .map(|entry| entry.into_path()) + .collect::>(); + Ok(selected) } @@ -125,7 +93,8 @@ pub(crate) fn last_modified(first: &walkdir::DirEntry, second: &walkdir::DirEntr } } } - return Ordering::Equal; + + Ordering::Equal } pub(crate) fn regular_ordering( diff --git a/guard/src/commands/helper.rs b/guard/src/commands/helper.rs index 57593c761..f5e8b358c 100644 --- a/guard/src/commands/helper.rs +++ b/guard/src/commands/helper.rs @@ -13,20 +13,22 @@ use std::convert::TryFrom; use std::io::BufWriter; use std::rc::Rc; +#[allow(dead_code)] pub struct ValidateInput<'a> { pub content: &'a str, pub file_name: &'a str, } +#[allow(dead_code)] pub fn validate_and_return_json( data: ValidateInput, rules: ValidateInput, verbose: bool, ) -> Result { - let path_value = match serde_json::from_str::(&data.content) { + let path_value = match serde_json::from_str::(data.content) { Ok(value) => PathAwareValue::try_from(value), Err(_) => { - let value = serde_yaml::from_str::(&data.content)?; + let value = serde_yaml::from_str::(data.content)?; PathAwareValue::try_from(value) } } @@ -43,7 +45,7 @@ pub fn validate_and_return_json( name: data.file_name.to_owned(), }; - let span = crate::rules::parser::Span::new_extra(&rules.content, rules.file_name); + let span = crate::rules::parser::Span::new_extra(rules.content, rules.file_name); let rules_file_name = rules.file_name; return match crate::rules::parser::rules_file(span) { diff --git a/guard/src/commands/parse_tree.rs b/guard/src/commands/parse_tree.rs index 2ff1d4971..0bdc423b3 100644 --- a/guard/src/commands/parse_tree.rs +++ b/guard/src/commands/parse_tree.rs @@ -64,23 +64,15 @@ impl Command for ParseTree { None => Box::new(reader), }; - let yaml = !app.get_flag(PRINT_JSON.0); let mut content = String::new(); file.read_to_string(&mut content)?; let span = crate::rules::parser::Span::new_extra(&content, ""); - match crate::rules::parser::rules_file(span) { - Err(e) => { - writer.write_err(format!("Parsing error handling rule, Error = {e}"))?; - return Err(e); - } - Ok(rules) => { - if yaml { - serde_yaml::to_writer(writer, &rules)?; - } else { - serde_json::to_writer(writer, &rules)?; - } - } + let rules = crate::rules::parser::rules_file(span)?; + + match app.get_flag(PRINT_JSON.0) { + true => serde_json::to_writer_pretty(writer, &rules)?, + false => serde_yaml::to_writer(writer, &rules)?, } Ok(0_i32) diff --git a/guard/src/commands/rulegen.rs b/guard/src/commands/rulegen.rs index ed7f24aeb..647edf64a 100644 --- a/guard/src/commands/rulegen.rs +++ b/guard/src/commands/rulegen.rs @@ -96,6 +96,7 @@ pub fn parse_template_and_call_gen( gen_rules(cfn_resources) } +#[allow(clippy::map_entry)] fn gen_rules( cfn_resources: HashMap, ) -> HashMap>> { @@ -145,14 +146,14 @@ fn gen_rules( None => prop_val.to_string(), }; - let mut no_newline_stripped_val = stripped_val.trim().replace("\n", ""); + let mut no_newline_stripped_val = stripped_val.trim().replace('\n', ""); // Preserve double quotes for strings. if prop_val.is_string() { let test_str = format!("{}{}{}", "\"", no_newline_stripped_val, "\""); no_newline_stripped_val = test_str; } - let resource_name = format!("{}", &cfn_resource["Type"].as_str().unwrap()); + let resource_name = (&cfn_resource["Type"].as_str().unwrap()).to_string(); if !rule_map.contains_key(&resource_name) { let value_set: HashSet = @@ -176,7 +177,7 @@ fn gen_rules( } } - return rule_map; + rule_map } // Prints the generated rules data structure to stdout. If there are properties mapping to diff --git a/guard/src/commands/test.rs b/guard/src/commands/test.rs index eb390626f..6adf1f1df 100644 --- a/guard/src/commands/test.rs +++ b/guard/src/commands/test.rs @@ -1,4 +1,4 @@ -use clap::{Arg, ArgAction, ArgGroup, ArgMatches, ValueHint}; +use clap::{Arg, ArgAction, ArgGroup, ArgMatches}; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::convert::TryFrom; @@ -15,18 +15,16 @@ use crate::commands::files::{ alpabetical, get_files_with_filter, iterate_over, last_modified, read_file_content, regular_ordering, }; -use crate::commands::tracker::StackTracker; use crate::commands::{ validate, ALPHABETICAL, DIRECTORY, DIRECTORY_ONLY, LAST_MODIFIED, RULES_AND_TEST_FILE, RULES_FILE, TEST, TEST_DATA, VERBOSE, }; use crate::rules::errors::Error; use crate::rules::eval::eval_rules_file; -use crate::rules::evaluate::RootScope; use crate::rules::exprs::RulesFile; use crate::rules::path_value::PathAwareValue; use crate::rules::Status::SKIP; -use crate::rules::{Evaluate, NamedStatus, RecordType, Result, Status}; +use crate::rules::{NamedStatus, RecordType, Result, Status}; use crate::utils::reader::Reader; use crate::utils::writer::Writer; diff --git a/guard/src/commands/tracker.rs b/guard/src/commands/tracker.rs index cb6d2636b..462c462e2 100644 --- a/guard/src/commands/tracker.rs +++ b/guard/src/commands/tracker.rs @@ -41,19 +41,6 @@ impl<'r> std::fmt::Debug for StackTracker<'r> { } } -impl<'r> StackTracker<'r> { - pub(super) fn new(delegate: &'r dyn EvaluationContext) -> Self { - StackTracker { - root_context: delegate, - stack: std::cell::RefCell::new(Vec::new()), - } - } - - pub(super) fn stack(self) -> Vec { - self.stack.into_inner() - } -} - impl<'r> EvaluationContext for StackTracker<'r> { fn resolve_variable(&self, variable: &str) -> Result> { self.root_context.resolve_variable(variable) @@ -76,11 +63,11 @@ impl<'r> EvaluationContext for StackTracker<'r> { if self.stack.borrow().len() == 1 { match self.stack.borrow_mut().get_mut(0) { Some(top) => { - top.status = status.clone(); - top.from = from.clone(); - top.to = to.clone(); - top.msg = Some(msg.clone()); - top.comparator = cmp.clone(); + top.status = status; + top.from = from; + top.to = to; + top.msg = Some(msg); + top.comparator = cmp; } None => unreachable!(), } @@ -88,20 +75,17 @@ impl<'r> EvaluationContext for StackTracker<'r> { } let stack = self.stack.borrow_mut().pop(); - match stack { - Some(mut stack) => { - stack.status = status.clone(); - stack.from = from.clone(); - stack.to = to.clone(); - stack.msg = Some(msg.clone()); - stack.comparator = cmp.clone(); + if let Some(mut stack) = stack { + stack.status = status; + stack.from = from.clone(); + stack.to = to.clone(); + stack.msg = Some(msg.clone()); + stack.comparator = cmp; - match self.stack.borrow_mut().last_mut() { - Some(cxt) => cxt.children.push(stack), - None => unreachable!(), - } + match self.stack.borrow_mut().last_mut() { + Some(cxt) => cxt.children.push(stack), + None => unreachable!(), } - None => {} } self.root_context .end_evaluation(eval_type, context, msg, from, to, status, cmp); diff --git a/guard/src/commands/validate.rs b/guard/src/commands/validate.rs index ae8d1059b..b7256d074 100644 --- a/guard/src/commands/validate.rs +++ b/guard/src/commands/validate.rs @@ -13,9 +13,8 @@ use enumflags2::BitFlags; use serde::Deserialize; use crate::command::Command; -use crate::commands::aws_meta_appender::MetadataAppender; use crate::commands::files::{alpabetical, iterate_over, last_modified}; -use crate::commands::tracker::{StackTracker, StatusContext}; +use crate::commands::tracker::StatusContext; use crate::commands::validate::structured::StructuredEvaluator; use crate::commands::validate::summary_table::SummaryType; use crate::commands::validate::tf::TfAware; @@ -27,13 +26,10 @@ use crate::commands::{ use crate::rules::errors::Error; use crate::rules::eval::eval_rules_file; use crate::rules::eval_context::{root_scope, EventRecord}; -use crate::rules::evaluate::RootScope; use crate::rules::exprs::RulesFile; -use crate::rules::parser::get_rule_name; use crate::rules::path_value::traversal::Traversal; use crate::rules::path_value::PathAwareValue; -use crate::rules::values::CmpOperator; -use crate::rules::{Evaluate, EvaluationContext, EvaluationType, Result, Status}; +use crate::rules::{Result, Status}; use crate::utils::reader::Reader; use crate::utils::writer::Writer; @@ -289,10 +285,10 @@ or rules files. ))); } - let mut streams = Vec::new(); - let data_files = match app.get_many::(DATA.0) { Some(list_of_file_or_dir) => { + let mut streams = Vec::new(); + for file_or_dir in list_of_file_or_dir { validate_path(file_or_dir)?; let base = PathBuf::from_str(file_or_dir)?; @@ -308,20 +304,9 @@ or rules files. let mut reader = BufReader::new(File::open(file.path())?); reader.read_to_string(&mut content)?; - let path = file.path(); - - let file_name = get_file_name(path, &base); - - let path_value = match get_path_aware_value_from_data(&content) { - Ok(t) => t, - Err(e) => return Err(e), - }; + let data_file = build_data_file(content, name)?; - streams.push(DataFile { - name: file_name, - path_value, - content, - }); + streams.push(data_file); } } } @@ -332,16 +317,10 @@ or rules files. if app.contains_id(RULES.0) { let mut content = String::new(); reader.read_to_string(&mut content)?; - let path_value = match get_path_aware_value_from_data(&content) { - Ok(t) => t, - Err(e) => return Err(e), - }; - streams.push(DataFile { - name: "STDIN".to_string(), - path_value, - content, - }); - streams + + let data_file = build_data_file(content, "STDIN".to_string())?; + + vec![data_file] } else { vec![] } // expect Payload, since rules aren't specified @@ -367,7 +346,7 @@ or rules files. let mut reader = BufReader::new(File::open(file.path())?); reader.read_to_string(&mut content)?; - let path_value = get_path_aware_value_from_data(&content)?; + let DataFile { path_value, .. } = build_data_file(content, name)?; primary_path_value = match primary_path_value { Some(current) => Some(current.merge(path_value)?), @@ -431,7 +410,7 @@ or rules files. data: data_files, output: output_type, writer, - exit_code: 0, + exit_code, }; evaluator.evaluate()? } @@ -460,8 +439,8 @@ or rules files. writer, )?; - if status == 5 { - exit_code = 5 + if status != 0 { + exit_code = status } } } @@ -478,13 +457,10 @@ or rules files. vec![], |mut data_collection, (i, data)| -> Result> { let content = data.to_string(); - let path_value = get_path_aware_value_from_data(&content)?; + let name = format!("DATA_STDIN[{}]", i + 1); + let data_file = build_data_file(content, name)?; - data_collection.push(DataFile { - name: format!("DATA_STDIN[{}]", i + 1), - path_value, - content, - }); + data_collection.push(data_file); Ok(data_collection) }, @@ -508,7 +484,7 @@ or rules files. data: data_collection, output: output_type, writer, - exit_code: 0, + exit_code, }; evaluator.evaluate()? } @@ -526,8 +502,8 @@ or rules files. writer, )?; - if status == 5 { - exit_code = 5; + if status != 0 { + exit_code = status; } } exit_code @@ -579,7 +555,7 @@ fn evaluate_rule( )?; if status == Status::FAIL { - return Ok(5); + return Ok(19); } } } @@ -606,23 +582,6 @@ fn parse_rules<'r>(rules_file_content: &'r str, rules_file_name: &'r str) -> Res crate::rules::parser::rules_file(span) } -#[derive(Debug)] -pub(crate) struct ConsoleReporter<'r> { - root_context: StackTracker<'r>, - reporters: &'r Vec<&'r dyn Reporter>, - rules_file_name: &'r str, - data_file_name: &'r str, - verbose: bool, - print_json: bool, - writer: &'r mut Writer, -} - -fn indent_spaces(indent: usize, writer: &mut Writer) { - for _idx in 0..indent { - write!(writer, "{INDENT}").expect("Unable to write to the output"); - } -} - // // https://vallentin.dev/2019/05/14/pretty-print-tree // @@ -646,141 +605,6 @@ pub(crate) fn print_verbose_tree(root: &EventRecord<'_>, writer: &mut Writer) { pprint_tree(root, "".to_string(), true, writer); } -pub(super) fn print_context(cxt: &StatusContext, depth: usize, writer: &mut Writer) { - let header = format!( - "{}({}, {})", - cxt.eval_type, - cxt.context, - common::colored_string(cxt.status) - ) - .underline(); - //let depth = cxt.indent; - let _sub_indent = depth + 1; - indent_spaces(depth - 1, writer); - writeln!(writer, "{header}").expect("Unable to write to the output"); - match &cxt.from { - Some(v) => { - indent_spaces(depth, writer); - write!(writer, "| ").expect("Unable to write to the output"); - writeln!(writer, "From: {v:?}").expect("Unable to write to the output"); - } - None => {} - } - match &cxt.to { - Some(v) => { - indent_spaces(depth, writer); - write!(writer, "| ").expect("Unable to write to the output"); - writeln!(writer, "To: {v:?}").expect("Unable to write to the output"); - } - None => {} - } - match &cxt.msg { - Some(message) => { - indent_spaces(depth, writer); - write!(writer, "| ").expect("Unable to write to the output"); - writeln!(writer, "Message: {message}").expect("Unable to write to the output"); - } - None => {} - } - - for child in &cxt.children { - print_context(child, depth + 1, writer) - } -} - -#[allow(clippy::too_many_arguments)] -impl<'r> ConsoleReporter<'r> { - pub(crate) fn new( - root: StackTracker<'r>, - renderers: &'r Vec<&'r dyn Reporter>, - rules_file_name: &'r str, - data_file_name: &'r str, - verbose: bool, - print_json: bool, - writer: &'r mut Writer, - ) -> ConsoleReporter<'r> { - ConsoleReporter { - root_context: root, - reporters: renderers, - rules_file_name, - data_file_name, - verbose, - print_json, - writer, - } - } - - fn report(mut self, root: &PathAwareValue, output_format_type: OutputFormatType) -> Result<()> { - let stack = self.root_context.stack(); - let top = stack.first().unwrap(); - - if self.verbose && self.print_json { - let serialized_user = serde_json::to_string_pretty(&top.children).unwrap(); - writeln!(self.writer, "{serialized_user}").expect("Unable to write to the output"); - } else { - let longest = get_longest(top, self.rules_file_name); - - let (failed, rest): (Vec<&StatusContext>, Vec<&StatusContext>) = - partition_failed_and_rest(top); - - let traversal = Traversal::from(root); - - for each_reporter in self.reporters { - each_reporter.report( - &mut self.writer, - top.status, - &failed, - &rest, - longest, - self.rules_file_name, - self.data_file_name, - &traversal, - output_format_type, - )?; - } - - if self.verbose { - writeln!(self.writer, "Evaluation Tree").expect("Unable to write to the output"); - for each in &top.children { - print_context(each, 1, self.writer); - } - } - } - - Ok(()) - } -} - -const INDENT: &str = " "; - -impl<'r> EvaluationContext for ConsoleReporter<'r> { - fn resolve_variable(&self, variable: &str) -> Result> { - self.root_context.resolve_variable(variable) - } - - fn rule_status(&self, rule_name: &str) -> Result { - self.root_context.rule_status(rule_name) - } - - fn end_evaluation( - &self, - eval_type: EvaluationType, - context: &str, - msg: String, - from: Option, - to: Option, - status: Option, - cmp: Option<(CmpOperator, bool)>, - ) { - self.root_context - .end_evaluation(eval_type, context, msg, from, to, status, cmp); - } - - fn start_evaluation(&self, eval_type: EvaluationType, context: &str) { - self.root_context.start_evaluation(eval_type, context); - } -} - #[allow(clippy::too_many_arguments)] fn evaluate_against_data_input<'r>( _data_type: Type, @@ -851,45 +675,33 @@ fn evaluate_against_data_input<'r>( } Ok(overall) } - -fn get_path_aware_value_from_data(content: &String) -> Result { +fn build_data_file(content: String, name: String) -> Result { if content.trim().is_empty() { - Err(Error::ParseError("blank data".to_string())) - } else { - let path_value = match crate::rules::values::read_from(content) { - Ok(value) => PathAwareValue::try_from(value)?, - Err(_) => { - let str_len: usize = cmp::min(content.len(), 100); - return Err(Error::ParseError(format!( - "data beginning with \n{}\n ...", - &content[..str_len] - ))); - } - }; - Ok(path_value) + return Err(Error::ParseError(format!( + "Unable to parse a template from data file: {name} is empty" + ))); } -} -fn has_a_supported_extension(name: &str, extensions: &[&str]) -> bool { - extensions.iter().any(|extension| name.ends_with(extension)) -} + let path_value = match crate::rules::values::read_from(&content) { + Ok(value) => PathAwareValue::try_from(value)?, + Err(_) => { + let str_len: usize = cmp::min(content.len(), 100); + return Err(Error::ParseError(format!( + "Error encountered while parsing data file: {name}, data beginning with \n{}\n ...", + &content[..str_len] + ))); + } + }; -fn partition_failed_and_rest(top: &StatusContext) -> (Vec<&StatusContext>, Vec<&StatusContext>) { - top.children - .iter() - .partition(|ctx| matches!(ctx.status, Some(Status::FAIL))) + Ok(DataFile { + name, + path_value, + content, + }) } -fn get_longest(top: &StatusContext, rule_file_name: &str) -> usize { - top.children - .iter() - .max_by(|f, s| { - get_rule_name(rule_file_name, &f.context) - .len() - .cmp(&get_rule_name(rule_file_name, &s.context).len()) - }) - .map(|elem| get_rule_name(rule_file_name, &elem.context).len()) - .unwrap_or(20) +fn has_a_supported_extension(name: &str, extensions: &[&str]) -> bool { + extensions.iter().any(|extension| name.ends_with(extension)) } fn get_file_name(file: &Path, base: &Path) -> String { diff --git a/guard/src/commands/validate/cfn.rs b/guard/src/commands/validate/cfn.rs index 581073636..778a52d55 100644 --- a/guard/src/commands/validate/cfn.rs +++ b/guard/src/commands/validate/cfn.rs @@ -48,10 +48,6 @@ pub(crate) struct CfnAware<'reporter> { } impl<'reporter> CfnAware<'reporter> { - pub(crate) fn new() -> CfnAware<'reporter> { - CfnAware { next: None } - } - pub(crate) fn new_with(next: &'reporter dyn Reporter) -> CfnAware { CfnAware { next: Some(next) } } @@ -85,20 +81,42 @@ impl<'reporter> Reporter for CfnAware<'reporter> { output_type: OutputFormatType, ) -> rules::Result<()> { let root = data.root().unwrap(); - if let Ok(_) = data.at("/Resources", root) { + + if data.at("/Resources", root).is_ok() { let failure_report = simplifed_json_from_root(root_record)?; - Ok(match output_type { + match output_type { OutputFormatType::YAML => serde_yaml::to_writer(write, &failure_report)?, OutputFormatType::JSON => serde_json::to_writer_pretty(write, &failure_report)?, - OutputFormatType::SingleLineSummary => single_line( - write, - data_file, - data_file_bytes, - rules_file, - data, - failure_report, - )?, - }) + OutputFormatType::SingleLineSummary => { + match single_line( + write, + data_file, + data_file_bytes, + rules_file, + data, + failure_report, + ) { + Err(crate::Error::InternalError(_)) => { + self.next.map_or(Ok(()), |next| { + next.report_eval( + write, + status, + root_record, + rules_file, + data_file, + data_file_bytes, + data, + output_type, + ) + })? + } + Ok(_) => {} + Err(e) => return Err(e), + } + } + }; + + Ok(()) } else { self.next.map_or(Ok(()), |next| { next.report_eval( @@ -116,30 +134,6 @@ impl<'reporter> Reporter for CfnAware<'reporter> { } } -fn binary_err_msg( - writer: &mut dyn Write, - _clause: &ClauseReport<'_>, - bc: &BinaryComparison, - prefix: &str, -) -> rules::Result { - let width = "PropertyPath".len() + 4; - writeln!( - writer, - "{prefix}{pp:, @@ -184,7 +178,7 @@ fn single_line( let root = data.root().unwrap(); let mut by_resources = HashMap::new(); for (key, value) in path_tree.range(String::from("/Resources")..) { - let matches = key.matches("/").count(); + let matches = key.matches('/').count(); let mut count = 1; if matches > 2 { @@ -192,7 +186,7 @@ fn single_line( if matches - count == 0 { unreachable!() } - let resource_name = get_resource_name(&key, count, matches); + let resource_name = get_resource_name(key, count, matches); match handle_resource_aggr(data, root, resource_name, &mut by_resources, value) { Some(_) => break, @@ -200,11 +194,12 @@ fn single_line( }; } } else { - let resource_name = match CFN_RESOURCES.captures(&key) { + let resource_name = match CFN_RESOURCES.captures(key) { Ok(Some(cap)) => cap.get(1).unwrap().as_str(), _ => { - writeln!(writer, "key: {}", key)?; - unreachable!() + return Err(crate::Error::InternalError(String::from( + "Unable to resolve key {key} for single line-summary when expecting a cloudformation template, falling back on next reporter" + ))); } }; @@ -401,21 +396,12 @@ fn single_line( writeln!(writer, "{prefix}{line}", prefix = new_prefix, line = line)?; } let mut context = 5; - loop { - match self.code_segment.next() { - Some((num, line)) => { - let line = format!("{num:>5}.{line}", num = num, line = line) - .bright_green(); - writeln!( - writer, - "{prefix}{line}", - prefix = new_prefix, - line = line - )?; - } - None => break, - } + while let Some((num, line)) = self.code_segment.next() { + let line = + format!("{num:>5}.{line}", num = num, line = line).bright_green(); + writeln!(writer, "{prefix}{line}", prefix = new_prefix, line = line)?; context -= 1; + if context <= 0 { break; } @@ -465,7 +451,7 @@ fn handle_resource_aggr<'record, 'value: 'record>( root: &'value Node<'_>, name: String, by_resources: &mut HashMap>, - value: &Vec>>, + value: &[Rc>], ) -> Option<()> { let path = format!("/Resources/{}", name); let resource = match data.at(&path, root) { diff --git a/guard/src/commands/validate/cfn_reporter.rs b/guard/src/commands/validate/cfn_reporter.rs index 497623692..c406ed13a 100644 --- a/guard/src/commands/validate/cfn_reporter.rs +++ b/guard/src/commands/validate/cfn_reporter.rs @@ -12,9 +12,9 @@ use crate::commands::validate::common::{ use crate::commands::validate::{OutputFormatType, Reporter}; use crate::rules::errors::Error; -use super::EvaluationType; use crate::rules::eval_context::EventRecord; use crate::rules::path_value::traversal::Traversal; +use crate::rules::EvaluationType; use crate::rules::Status; lazy_static! { @@ -26,12 +26,6 @@ lazy_static! { #[derive(Debug)] pub(crate) struct CfnReporter {} -impl CfnReporter { - pub(crate) fn new() -> Self { - CfnReporter {} - } -} - impl Reporter for CfnReporter { fn report( &self, @@ -82,7 +76,7 @@ impl Reporter for CfnReporter { let (resource_name, property_path) = match CFN_RESOURCES.captures(&resource_info.path) { Ok(Some(caps)) => { - (caps["name"].to_string(), caps["rest"].replace("/", ".")) + (caps["name"].to_string(), caps["rest"].replace('/', ".")) } Ok(None) => ( format!( @@ -108,10 +102,7 @@ impl Reporter for CfnReporter { } else { HashMap::new() }; - let as_vec = passed_or_skipped - .iter() - .map(|s| *s) - .collect::>(); + let as_vec = passed_or_skipped.to_vec(); let (skipped, passed): (Vec<&StatusContext>, Vec<&StatusContext>) = as_vec.iter().partition(|status| match status.status { // This uses the dereference deep trait of Rust @@ -200,7 +191,7 @@ impl super::common::GenericReporter for SingleLineReporter { for (resource, info) in by_resource_name.iter() { super::common::print_name_info( writer, - &info, + info, longest_rule_len, rules_file_name, data_file_name, @@ -210,7 +201,7 @@ impl super::common::GenericReporter for SingleLineReporter { info.path, data_file_name, info.rule, - info.message.replace("\n", ";") + info.message.replace('\n', ";") )) }, |_, _, op_msg, info| { @@ -221,7 +212,7 @@ impl super::common::GenericReporter for SingleLineReporter { op_msg=op_msg, template=data_file_name, rule= info.rule, - msg=info.message.replace("\n", ";") + msg=info.message.replace('\n', ";") )) }, |_, _, msg, info| { @@ -233,7 +224,7 @@ impl super::common::GenericReporter for SingleLineReporter { expected=info.expected.as_ref().map_or(&serde_json::Value::Null, std::convert::identity), template=data_file_name, rule=info.rule, - msg=info.message.replace("\n", ";") + msg=info.message.replace('\n', ";") )) }, )?; diff --git a/guard/src/commands/validate/common.rs b/guard/src/commands/validate/common.rs index e79c3ba3e..502f54f83 100644 --- a/guard/src/commands/validate/common.rs +++ b/guard/src/commands/validate/common.rs @@ -2,11 +2,9 @@ use colored::*; use serde::Serialize; use crate::commands::tracker::StatusContext; -use crate::commands::validate::OutputFormatType; use crate::rules::eval_context::{ - simplifed_json_from_root, BinaryCheck, BinaryComparison, ClauseReport, EventRecord, FileReport, - GuardClauseReport, InComparison, UnaryCheck, UnaryComparison, ValueComparisons, - ValueUnResolved, + BinaryCheck, BinaryComparison, ClauseReport, EventRecord, FileReport, GuardClauseReport, + InComparison, UnaryCheck, UnaryComparison, ValueComparisons, ValueUnResolved, }; use crate::rules::values::CmpOperator; @@ -62,6 +60,7 @@ impl<'a> Default for NameInfo<'a> { } pub(super) trait GenericReporter: Debug { + #[allow(clippy::too_many_arguments)] fn report( &self, writer: &mut dyn Write, @@ -75,6 +74,7 @@ pub(super) trait GenericReporter: Debug { } #[derive(Debug)] +#[allow(clippy::upper_case_acronyms)] pub(super) enum StructureType { JSON, YAML, @@ -109,7 +109,7 @@ impl GenericReporter for StructuredSummary { failed: HashMap>>, passed: HashSet, skipped: HashSet, - longest_rule_len: usize, + _: usize, ) -> crate::rules::Result<()> { let value = DataOutput { rules_from: rules_file_name, @@ -156,9 +156,9 @@ pub(super) fn find_failing_clauses<'record, 'value>( } } -pub(super) fn extract_name_info_from_record<'record, 'value>( +pub(super) fn extract_name_info_from_record<'record>( rule_name: &'record str, - clause: &'record EventRecord<'value>, + clause: &'record EventRecord<'_>, ) -> crate::rules::Result> { Ok(match &clause.container { Some(RecordType::RuleCheck(NamedStatus { @@ -167,7 +167,7 @@ pub(super) fn extract_name_info_from_record<'record, 'value>( .. })) => NameInfo { message: msg.clone(), - rule: *name, + rule: name, ..Default::default() }, @@ -250,10 +250,7 @@ pub(super) fn extract_name_info_from_record<'record, 'value>( }, None => None, }; - let expected = match expected { - Some((_, ex)) => Some(ex), - None => None, - }; + let expected = expected.map(|(_, ex)| ex); NameInfo { rule: rule_name, comparison: Some(check.comparison.into()), @@ -265,7 +262,6 @@ pub(super) fn extract_name_info_from_record<'record, 'value>( provided: Some(provided), expected, path, - ..Default::default() } } @@ -321,7 +317,7 @@ pub(super) fn extract_name_info_from_record<'record, 'value>( rule: rule_name, comparison: Some(Comparison { not_operator_exists: incomp.comparison.1, - operator: incomp.comparison.0.clone(), + operator: incomp.comparison.0, }), provided, expected: Some(serde_json::Value::Array(to)), @@ -334,50 +330,6 @@ pub(super) fn extract_name_info_from_record<'record, 'value>( }) } -pub(crate) fn extract_event_records<'value>( - root_record: EventRecord<'value>, -) -> ( - Vec>, - Vec>, - Vec>, -) { - let mut failed = Vec::with_capacity(root_record.children.len()); - let mut skipped = Vec::with_capacity(root_record.children.len()); - let mut passed = Vec::with_capacity(root_record.children.len()); - for each_rule in root_record.children { - match &each_rule.container { - Some(RecordType::RuleCheck(NamedStatus { - status: Status::FAIL, - name, - message, - })) => { - let mut failed = EventRecord { - container: Some(RecordType::RuleCheck(NamedStatus { - status: Status::FAIL, - name, - message: message.clone(), - })), - children: vec![], - context: each_rule.context, - }; - //add_failed_children(&mut failed, each_rule.children) - } - - Some(RecordType::RuleCheck(NamedStatus { - status: Status::SKIP, - .. - })) => { - skipped.push(each_rule); - } - - rest => { - skipped.push(each_rule); - } - } - } - (failed, skipped, passed) -} - pub(super) fn report_from_events( root_record: &EventRecord<'_>, writer: &mut dyn Write, @@ -390,11 +342,7 @@ pub(super) fn report_from_events( let mut skipped = HashSet::new(); let mut success = HashSet::new(); for each_rule in &root_record.children { - if let Some(RecordType::RuleCheck(NamedStatus { - status, - name, - message, - })) = &each_rule.container + if let Some(RecordType::RuleCheck(NamedStatus { status, name, .. })) = &each_rule.container { if name.len() > longest_rule_length { longest_rule_length = name.len(); @@ -403,7 +351,7 @@ pub(super) fn report_from_events( Status::FAIL => { let mut clauses = Vec::new(); for each_clause in find_failing_clauses(each_rule) { - clauses.push(extract_name_info_from_record(*name, each_clause)?); + clauses.push(extract_name_info_from_record(name, each_clause)?); } failed.insert(name.to_string(), clauses); } @@ -449,10 +397,7 @@ pub(super) fn extract_name_info<'a>( } None => None, }, - comparison: match each_failing_clause.comparator { - Some(input) => Some(input.into()), - None => None, - }, + comparison: each_failing_clause.comparator.map(|input| input.into()), message: each_failing_clause .msg .as_ref() @@ -538,7 +483,7 @@ pub(super) fn print_compliant_skipped_info( writer: &mut dyn Write, passed: &HashSet, skipped: &HashSet, - rules_file_name: &str, + _: &str, data_file_name: &str, ) -> crate::rules::Result<()> { if !passed.is_empty() { @@ -564,10 +509,11 @@ pub(super) fn print_compliant_skipped_info( Ok(()) } +#[allow(clippy::too_many_arguments)] pub(super) fn print_name_info( writer: &mut dyn Write, info: &[NameInfo<'_>], - longest_rule_len: usize, + _: usize, rules_file_name: &str, data_file_name: &str, retrieval_error: R, @@ -580,7 +526,7 @@ where B: Fn(&str, &str, &str, &NameInfo<'_>) -> crate::rules::Result, { for each in info { - let (cmp, not) = match &each.comparison { + let _ = match &each.comparison { Some(cmp) => (Some(cmp.operator), cmp.not_operator_exists), None => (None, false), }; @@ -662,12 +608,15 @@ where } else { "was int" }, - IsFloat => - if !not { - "was not a float" - } else { - "was float" - }, + // NOTE: This enum actually doesnt exist and this is why we are + // seeing a warning for unreachable pattern underneath....Need to figure out what + // happenned here... + // IsFloat => + // if !not { + // "was not a float" + // } else { + // "was float" + // }, Eq | In | Gt | Lt | Le | Ge => unreachable!(), }, each @@ -700,25 +649,6 @@ struct DataOutputNewForm<'a, 'v> { report: FileReport<'v>, } -pub(super) fn report_structured<'value>( - root: &EventRecord<'value>, - data_from: &str, - rules_from: &str, - type_output: OutputFormatType, -) -> crate::rules::Result { - let mut report = simplifed_json_from_root(root)?; - let output = DataOutputNewForm { - report, - data_from, - rules_from, - }; - Ok(match type_output { - OutputFormatType::JSON => serde_json::to_string(&output)?, - OutputFormatType::YAML => serde_yaml::to_string(&output)?, - _ => unreachable!(), - }) -} - #[derive(Clone, Debug)] pub(super) struct LocalResourceAggr<'record, 'value: 'record> { pub(super) name: String, @@ -747,6 +677,7 @@ impl<'key, T> PartialEq for IdentityHash<'key, T> { } #[derive(Clone, Debug)] +#[allow(dead_code)] pub(super) struct Node<'report, 'value: 'report> { pub(super) parent: std::rc::Rc, pub(super) path: std::rc::Rc, @@ -825,16 +756,6 @@ pub(super) fn populate_hierarchy_path_trees<'report, 'value: 'report>( } } -pub(super) type BinaryComparisonErrorFn = dyn Fn( - &mut dyn Write, - &ClauseReport<'_>, - &BinaryComparison, - String, -) -> crate::rules::Result<()>; - -pub(super) type UnaryComparisonErrorFn = - dyn Fn(&mut dyn Write, &ClauseReport<'_>, &UnaryComparison, String) -> crate::rules::Result<()>; - fn emit_messages( writer: &mut dyn Write, prefix: &str, @@ -1080,7 +1001,7 @@ pub(super) fn pprint_clauses<'report, 'value: 'report>( err_writer.missing_property_msg( &mut post_message, clause, - blk.unresolved.as_ref().map(|ur| ur), + blk.unresolved.as_ref(), &prefix, )?, ); diff --git a/guard/src/commands/validate/console_reporter.rs b/guard/src/commands/validate/console_reporter.rs index cd8e23da7..1021dc255 100644 --- a/guard/src/commands/validate/console_reporter.rs +++ b/guard/src/commands/validate/console_reporter.rs @@ -263,7 +263,7 @@ fn pprint_failed_sub_tree( 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 { "less than 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" }, diff --git a/guard/src/commands/validate/generic_summary.rs b/guard/src/commands/validate/generic_summary.rs index a7c664f32..1550ad0b2 100644 --- a/guard/src/commands/validate/generic_summary.rs +++ b/guard/src/commands/validate/generic_summary.rs @@ -8,7 +8,7 @@ use crate::commands::validate::{OutputFormatType, Reporter}; use crate::rules::{EvaluationType, Status}; use super::common::*; -use crate::rules::eval_context::EventRecord; +use crate::rules::eval_context::{simplifed_json_from_root, EventRecord}; use crate::rules::path_value::traversal::Traversal; use crate::rules::values::CmpOperator; @@ -25,13 +25,13 @@ impl Reporter for GenericSummary { fn report( &self, writer: &mut dyn Write, - status: Option, + _: Option, failed_rules: &[&StatusContext], passed_or_skipped: &[&StatusContext], longest_rule_name: usize, rules_file: &str, data_file: &str, - data: &Traversal<'_>, + _: &Traversal<'_>, output_format_type: OutputFormatType, ) -> crate::rules::Result<()> { let renderer = @@ -81,10 +81,7 @@ impl Reporter for GenericSummary { HashMap::new() }; - let as_vec = passed_or_skipped - .iter() - .map(|s| *s) - .collect::>(); + let as_vec = passed_or_skipped.to_vec(); let (skipped, passed): (Vec<&StatusContext>, Vec<&StatusContext>) = as_vec.iter().partition(|status| match status.status { // This uses the dereference deep trait of Rust @@ -113,32 +110,30 @@ impl Reporter for GenericSummary { fn report_eval<'value>( &self, - _write: &mut dyn Write, + writer: &mut dyn Write, _status: Status, - _root_record: &EventRecord<'value>, - _rules_file: &str, - _data_file: &str, + root_record: &EventRecord<'value>, + rules_file: &str, + data_file: &str, _data_file_bytes: &str, _data: &Traversal<'value>, - _output_type: OutputFormatType, + output_type: OutputFormatType, ) -> crate::rules::Result<()> { - let renderer = - match _output_type { - OutputFormatType::SingleLineSummary => { - Box::new(SingleLineSummary {}) as Box - } - OutputFormatType::JSON => Box::new(StructuredSummary::new(StructureType::JSON)) - as Box, - OutputFormatType::YAML => Box::new(StructuredSummary::new(StructureType::YAML)) - as Box, - }; - super::common::report_from_events( - _root_record, - _write, - _data_file, - _rules_file, - renderer.as_ref(), - ) + let failure_repord = simplifed_json_from_root(root_record)?; + + match output_type { + OutputFormatType::JSON => serde_json::to_writer_pretty(writer, &failure_repord)?, + OutputFormatType::YAML => serde_yaml::to_writer(writer, &failure_repord)?, + OutputFormatType::SingleLineSummary => super::common::report_from_events( + root_record, + writer, + data_file, + rules_file, + &(SingleLineSummary {}), + )?, + }; + + Ok(()) } } @@ -146,7 +141,7 @@ impl Reporter for GenericSummary { struct SingleLineSummary {} fn retrieval_error_message( - rules_file: &str, + _: &str, data_file: &str, info: &NameInfo<'_>, ) -> crate::rules::Result { @@ -161,7 +156,7 @@ fn retrieval_error_message( } fn unary_error_message( - rules_file: &str, + _: &str, data_file: &str, op_msg: &str, info: &NameInfo<'_>, @@ -172,12 +167,12 @@ fn unary_error_message( op_msg=op_msg, data=data_file, rule=info.rule, - msg=info.message.replace("\n", ";"), + msg=info.message.replace('\n', ";"), )) } fn binary_error_message( - rules_file: &str, + _: &str, data_file: &str, op_msg: &str, info: &NameInfo<'_>, @@ -194,7 +189,7 @@ fn binary_error_message( op_msg = op_msg, data = data_file, rule = info.rule, - msg = info.message.replace("\n", ";"), + msg = info.message.replace('\n', ";"), expected = info .expected .as_ref() diff --git a/guard/src/commands/validate/structured.rs b/guard/src/commands/validate/structured.rs index bc4094385..b196dc0fc 100644 --- a/guard/src/commands/validate/structured.rs +++ b/guard/src/commands/validate/structured.rs @@ -73,8 +73,9 @@ impl<'eval> StructuredEvaluator<'eval> { let mut root_scope = root_scope(rule, Rc::new(each.path_value.clone()))?; if let Status::FAIL = eval_rules_file(rule, &mut root_scope, Some(&each.name))? { - self.exit_code = 5; + self.exit_code = 19; } + let root_record = root_scope.reset_recorder().extract(); let report = simplifed_json_from_root(&root_record)?; file_report.combine(report); diff --git a/guard/src/commands/validate/summary_table.rs b/guard/src/commands/validate/summary_table.rs index 3a8e8cf44..1c0fcf28e 100644 --- a/guard/src/commands/validate/summary_table.rs +++ b/guard/src/commands/validate/summary_table.rs @@ -14,6 +14,7 @@ use std::io::Write; #[bitflags] #[repr(u8)] #[derive(Debug, Copy, Clone, Eq, PartialOrd, PartialEq)] +#[allow(clippy::upper_case_acronyms)] pub(super) enum SummaryType { PASS = 0b0001, FAIL = 0b0010, @@ -27,10 +28,10 @@ pub(super) struct SummaryTable<'reporter> { } impl<'a> SummaryTable<'a> { - pub(crate) fn new<'r>( + pub(crate) fn new( summary_type: BitFlags, - next: &'r dyn Reporter, - ) -> SummaryTable<'r> { + next: &dyn Reporter, + ) -> SummaryTable<'_> { SummaryTable { summary_type, next } } } @@ -86,7 +87,7 @@ impl<'r> Reporter for SummaryTable<'r> { _data: &Traversal<'_>, _output_format_type: OutputFormatType, ) -> crate::rules::Result<()> { - let as_vec = passed_or_skipped.iter().map(|s| *s).collect_vec(); + let as_vec = passed_or_skipped.iter().copied().collect_vec(); let (skipped, passed): (Vec<&StatusContext>, Vec<&StatusContext>) = as_vec.iter().partition(|status| match status.status { // This uses the dereference deep trait of Rust diff --git a/guard/src/commands/validate/tf.rs b/guard/src/commands/validate/tf.rs index ab794f017..8b71a3a79 100644 --- a/guard/src/commands/validate/tf.rs +++ b/guard/src/commands/validate/tf.rs @@ -18,10 +18,6 @@ pub(crate) struct TfAware<'reporter> { } impl<'reporter> TfAware<'reporter> { - pub(crate) fn new() -> TfAware<'reporter> { - TfAware { next: None } - } - pub(crate) fn new_with(next: &'reporter dyn Reporter) -> TfAware { TfAware { next: Some(next) } } @@ -56,22 +52,21 @@ impl<'reporter> Reporter for TfAware<'reporter> { ) -> crate::rules::Result<()> { let root = data.root().unwrap(); let is_tf_plan = match data.at("/resource_changes", root) { - Ok(_resource_changes) => match data.at("/terraform_version", root) { - Ok(_tf_version) => true, - _ => false, - }, + Ok(_resource_changes) => matches!(data.at("/terraform_version", root), Ok(_)), _ => false, }; if is_tf_plan { let failure_report = simplifed_json_from_root(root_record)?; - Ok(match output_type { + match output_type { OutputFormatType::YAML => serde_yaml::to_writer(write, &failure_report)?, OutputFormatType::JSON => serde_json::to_writer_pretty(write, &failure_report)?, OutputFormatType::SingleLineSummary => { single_line(write, data_file, rules_file, data, root, failure_report)? } - }) + }; + + Ok(()) } else { self.next.map_or(Ok(()), |next| { next.report_eval( @@ -89,17 +84,6 @@ impl<'reporter> Reporter for TfAware<'reporter> { } } -struct PropertyError<'report, 'value: 'report> { - property: &'value str, - clause: &'report ClauseReport<'value>, -} - -struct ResourceView<'report, 'value: 'report> { - name: &'value str, - resource_type: &'value str, - errors: indexmap::IndexMap<&'value str, PropertyError<'report, 'value>>, -} - lazy_static! { static ref RESOURCE_CHANGE_EXTRACTION: Regex = Regex::new( "/resource_changes/(?P[^/]+)/change/after/(?P.*)?" @@ -138,7 +122,7 @@ fn single_line( let mut by_resources = HashMap::new(); for (key, value) in path_tree.range(String::from("/resource_changes/")..) { - let resource_ptr = match RESOURCE_CHANGE_EXTRACTION.captures(&key) { + let resource_ptr = match RESOURCE_CHANGE_EXTRACTION.captures(key) { Ok(Some(cap)) => cap.name("index_or_name").unwrap().as_str(), Ok(None) => unreachable!(), Err(e) => return Err(Error::from(e)), @@ -156,7 +140,7 @@ fn single_line( }, _ => unreachable!(), }; - let dot_sep = addr.find(".").unwrap(); + let dot_sep = addr.find('.').unwrap(); let (resource_type, resource_name) = (addr.slice(0..dot_sep), addr.slice(dot_sep + 1..)); let resource_aggr = by_resources .entry(resource_name) @@ -243,7 +227,7 @@ fn single_line( None => (resource_based, ""), }; - let property = property.slice("change/after/".len()..).replace("/", "."); + let property = property.slice("change/after/".len()..).replace('/', "."); writeln!( writer, "{prefix}{pp: (resource_based, ""), }; - let property = property.replace("/", "."); + let property = property.replace('/', "."); let width = "PropertyPath".len() + 4; writeln!( writer, diff --git a/guard/src/main.rs b/guard/src/main.rs index 9c093bdc7..6488cf45d 100644 --- a/guard/src/main.rs +++ b/guard/src/main.rs @@ -1,4 +1,3 @@ -use clap::{ArgMatches, Parser}; use std::collections::HashMap; use std::fs::File; mod command; @@ -12,9 +11,7 @@ use crate::utils::writer::WriteBuffer::Stderr; use crate::utils::writer::{WriteBuffer::File as WBFile, WriteBuffer::Stdout, Writer}; use commands::{APP_NAME, APP_VERSION}; use rules::errors::Error; -use std::io::Write; use std::process::exit; -use std::rc::Rc; fn main() -> Result<(), Error> { let mut app = clap::Command::new(APP_NAME) @@ -31,7 +28,7 @@ fn main() -> Result<(), Error> { .arg_required_else_help(true); let mut commands = utils::get_guard_commands(); - commands.push(Box::new(commands::completions::Completions::default())); + commands.push(Box::::default()); let mappings = commands.iter().map(|s| (s.name(), s)).fold( HashMap::with_capacity(commands.len()), diff --git a/guard/src/rules/display.rs b/guard/src/rules/display.rs index a6104d5dc..ab276e0f4 100644 --- a/guard/src/rules/display.rs +++ b/guard/src/rules/display.rs @@ -106,7 +106,7 @@ impl std::fmt::Display for PathAwareValue { } } -impl<'value> std::fmt::Display for QueryResult { +impl std::fmt::Display for QueryResult { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { QueryResult::Literal(l) => { diff --git a/guard/src/rules/errors.rs b/guard/src/rules/errors.rs index 86fba06ff..67dc9dde3 100644 --- a/guard/src/rules/errors.rs +++ b/guard/src/rules/errors.rs @@ -5,6 +5,7 @@ use thiserror::Error; use crate::rules::parser::{ParserError, Span}; #[derive(Debug, Error)] +#[allow(clippy::enum_variant_names)] pub enum Error { #[error("Error parsing incoming JSON context {0}")] JsonError(#[from] serde_json::Error), @@ -44,6 +45,8 @@ pub enum Error { Errors(#[from] Errors), #[error("{0}")] IllegalArguments(String), + #[error("internal error {0}")] + InternalError(String), } #[derive(Debug, Error)] diff --git a/guard/src/rules/eval.rs b/guard/src/rules/eval.rs index 7253b77a4..afd3426a9 100644 --- a/guard/src/rules/eval.rs +++ b/guard/src/rules/eval.rs @@ -58,10 +58,14 @@ is_type_fn!(is_string_operation, PathAwareValue::String(_)); is_type_fn!(is_list_operation, PathAwareValue::List(_)); is_type_fn!(is_struct_operation, PathAwareValue::Map(_)); is_type_fn!(is_int_operation, PathAwareValue::Int(_)); +#[cfg(test)] is_type_fn!(is_float_operation, PathAwareValue::Float(_)); is_type_fn!(is_bool_operation, PathAwareValue::Bool(_)); +#[cfg(test)] is_type_fn!(is_char_range_operation, PathAwareValue::RangeChar(_)); +#[cfg(test)] is_type_fn!(is_int_range_operation, PathAwareValue::RangeInt(_)); +#[cfg(test)] is_type_fn!(is_float_range_operation, PathAwareValue::RangeFloat(_)); fn not_operation(operation: O) -> impl Fn(&QueryResult) -> Result @@ -88,6 +92,7 @@ where } } +#[allow(clippy::type_complexity)] fn record_unary_clause<'eval, 'value, 'loc: 'value, O>( operation: O, cmp: (CmpOperator, bool), @@ -165,6 +170,7 @@ pub(super) enum EvaluationResult { QueryValueResult(Vec<(QueryResult, Status)>), } +#[allow(clippy::type_complexity)] fn unary_operation<'r, 'l: 'r, 'loc: 'l>( lhs_query: &'l [QueryPart<'loc>], cmp: (CmpOperator, bool), @@ -264,7 +270,7 @@ fn unary_operation<'r, 'l: 'r, 'loc: 'l>( EvaluationResult::QueryValueResult(results) } else { EvaluationResult::EmptyQueryResult({ - let result = if cmp.1 { false } else { true }; + let result = !cmp.1; let result = if inverse { !result } else { result }; match result { true => { @@ -280,7 +286,7 @@ fn unary_operation<'r, 'l: 'r, 'loc: 'l>( eval_context.end_record( &context, RecordType::ClauseValueCheck(ClauseCheck::NoValueForEmptyCheck( - custom_message.clone(), + custom_message, )), )?; Status::FAIL @@ -396,6 +402,7 @@ struct ComparisonWithRhs { pair: LhsRhsPair, } +#[allow(dead_code)] struct NotComparableWithRhs { reason: String, pair: LhsRhsPair, @@ -406,10 +413,10 @@ struct UnResolvedRhs { lhs: Rc, } -fn each_lhs_compare<'r, C>( +fn each_lhs_compare( cmp: C, lhs: Rc, - rhs: &'r [QueryResult], + rhs: &[QueryResult], ) -> Result> where C: Fn(&PathAwareValue, &PathAwareValue) -> Result, @@ -434,7 +441,7 @@ where // && each_rhs_resolved.is_scalar() { if let PathAwareValue::List((_, inner)) = &*lhs { for each in inner { - match cmp(&each, each_rhs_resolved) { + match cmp(each, each_rhs_resolved) { Ok(outcome) => { statues.push(ComparisonResult::Comparable( ComparisonWithRhs { @@ -532,18 +539,6 @@ where Ok(statues) } -fn not_cmp(cmp: F) -> impl Fn(&PathAwareValue, &PathAwareValue) -> Result -where - F: Fn(&PathAwareValue, &PathAwareValue) -> Result, -{ - move |left, right| { - Ok(match cmp(left, right)? { - true => false, - false => true, - }) - } -} - fn in_cmp(not_in: bool) -> impl Fn(&PathAwareValue, &PathAwareValue) -> Result { move |lhs, rhs| match (lhs, rhs) { (PathAwareValue::String((_, lhs_value)), PathAwareValue::String((_, rhs_value))) => { @@ -557,20 +552,8 @@ fn in_cmp(not_in: bool) -> impl Fn(&PathAwareValue, &PathAwareValue) -> Result { - if not_in { - false - } else { - true - } - } - None => { - if not_in { - true - } else { - false - } - } + Some(_) => !not_in, + None => not_in, } }), @@ -598,7 +581,7 @@ fn report_value<'r, 'value: 'r, 'loc: 'value>( }, }) => ( QueryResult::Resolved(Rc::clone(lhs_value)), - Some(QueryResult::Resolved(Rc::clone(&rhs_value))), + Some(QueryResult::Resolved(Rc::clone(rhs_value))), *outcome, None, ), @@ -640,7 +623,7 @@ fn report_value<'r, 'value: 'r, 'loc: 'value>( from: lhs_value.clone(), comparison: cmp, to: rhs_value, - custom_message: custom_message.clone(), + custom_message, message: reason, status: Status::FAIL, })), @@ -712,9 +695,11 @@ fn report_at_least_one<'r, 'value: 'r, 'loc: 'value>( } for (lhs, results) in by_lhs_value.iter() { - let found = results.iter().find(|(r, _rhs)| match r { - ComparisonResult::Comparable(ComparisonWithRhs { outcome: true, .. }) => true, - _ => false, + let found = results.iter().find(|(r, _rhs)| { + matches!( + r, + ComparisonResult::Comparable(ComparisonWithRhs { outcome: true, .. }) + ) }); match found { Some(_) => { @@ -770,7 +755,7 @@ fn binary_operation<'value, 'loc: 'value>( let lhs = eval_context.query(lhs_query)?; let results = cmp.compare(&lhs, rhs)?; match results { - operators::EvalResult::Skip => return Ok(EvaluationResult::EmptyQueryResult(Status::SKIP)), + operators::EvalResult::Skip => Ok(EvaluationResult::EmptyQueryResult(Status::SKIP)), operators::EvalResult::Result(results) => { let mut statues: Vec<(QueryResult, Status)> = Vec::with_capacity(lhs.len()); for each in results { @@ -940,7 +925,7 @@ fn binary_operation<'value, 'loc: 'value>( .rhs .iter() .cloned() - .map(|e| QueryResult::Resolved(e)) + .map(QueryResult::Resolved) .collect::>(); for lhs in qin.diff { @@ -963,8 +948,6 @@ fn binary_operation<'value, 'loc: 'value>( } } }, - - operators::ValueEvalResult::UnaryResult(_) => unreachable!(), } } Ok(EvaluationResult::QueryValueResult(statues)) @@ -1120,9 +1103,10 @@ pub(in crate::rules) fn eval_guard_access_clause<'value, 'loc: 'value>( RecordType::GuardClauseBlockCheck(BlockCheck { status: Status::FAIL, at_least_one_matches: !all, - message: Some(format!( + message: Some( "Error not RHS for binary clause when handling clause, bailing" - )), + .to_string(), + ), }), )?; return Err(Error::NotComparable(format!( @@ -1202,7 +1186,8 @@ pub(in crate::rules) fn eval_guard_access_clause<'value, 'loc: 'value>( message: Some(format!("Error {} when handling clause, bailing", e)), }), )?; - return Err(e); + + Err(e) } } } @@ -1538,7 +1523,7 @@ impl<'eval, 'value, 'loc: 'value> RecordTracer<'value> fn end_record(&mut self, context: &str, record: RecordType<'value>) -> Result<()> { let record = match record { RecordType::RuleCheck(ns) => { - if ns.name == &self.call_rule.named_rule.dependent_rule { + if ns.name == self.call_rule.named_rule.dependent_rule { RecordType::RuleCheck(NamedStatus { name: ns.name, status: ns.status, @@ -1574,13 +1559,13 @@ pub(in crate::rules) fn eval_parameterized_rule_call<'value, 'loc: 'value>( match each { LetValue::Value(val) => { resolved_parameters.insert( - (¶m_rule.parameter_names[idx]).as_str(), + (param_rule.parameter_names[idx]).as_str(), vec![QueryResult::Resolved(Rc::new(val.clone()))], ); } LetValue::AccessClause(query) => { resolved_parameters.insert( - (¶m_rule.parameter_names[idx]).as_str(), + (param_rule.parameter_names[idx]).as_str(), resolver.query(&query.query)?, ); } @@ -1610,7 +1595,7 @@ pub(in crate::rules) fn eval_guard_clause<'value, 'loc: 'value>( block, resolver, ), - GuardClause::ParameterizedNamedRule(prc) => eval_parameterized_rule_call(&prc, resolver), + GuardClause::ParameterizedNamedRule(prc) => eval_parameterized_rule_call(prc, resolver), } } @@ -1621,9 +1606,7 @@ pub(in crate::rules) fn eval_when_clause<'value, 'loc: 'value>( match when_clause { WhenGuardClause::Clause(gac) => eval_guard_access_clause(gac, resolver), WhenGuardClause::NamedRule(gnr) => eval_guard_named_clause(gnr, resolver), - WhenGuardClause::ParameterizedNamedRule(prc) => { - eval_parameterized_rule_call(&prc, resolver) - } + WhenGuardClause::ParameterizedNamedRule(prc) => eval_parameterized_rule_call(prc, resolver), } } @@ -1803,7 +1786,7 @@ pub(in crate::rules) fn eval_rule<'value, 'loc: 'value>( rule: &'value Rule<'loc>, resolver: &mut dyn EvalContext<'value, 'loc>, ) -> Result { - let context = format!("{}", rule.rule_name); + let context = rule.rule_name.to_string(); resolver.start_record(&context)?; let block = if let Some(conditions) = &rule.conditions { let when_context = format!("Rule#{}/When", context); @@ -1865,7 +1848,7 @@ pub(in crate::rules) fn eval_rule<'value, 'loc: 'value>( ..Default::default() }), )?; - return Err(e); + Err(e) } } } @@ -1924,13 +1907,11 @@ pub(crate) fn eval_rules_file<'value, 'loc: 'value>( &context, RecordType::FileCheck(NamedStatus { status: overall, - name: match data_file_name { - Some(file_name) => file_name, - None => "", - }, + name: data_file_name.unwrap_or_default(), ..Default::default() }), )?; + Ok(overall) } diff --git a/guard/src/rules/eval/operators.rs b/guard/src/rules/eval/operators.rs index a0c946895..c040539d7 100644 --- a/guard/src/rules/eval/operators.rs +++ b/guard/src/rules/eval/operators.rs @@ -4,21 +4,13 @@ use crate::rules::errors::Error; use crate::rules::path_value::*; use crate::rules::{CmpOperator, QueryResult, UnResolved}; -#[derive(Clone, Debug)] -pub(crate) enum UnaryResult { - Success, - Fail, - SuccessWith(Rc), - FailWith(Rc), -} - #[derive(Clone, Debug)] pub(crate) struct LhsRhsPair { pub(crate) lhs: Rc, pub(crate) rhs: Rc, } -impl<'value> LhsRhsPair { +impl LhsRhsPair { fn new(lhs: Rc, rhs: Rc) -> LhsRhsPair { LhsRhsPair { lhs, rhs } } @@ -31,7 +23,7 @@ pub(crate) struct QueryIn { pub(crate) rhs: Vec>, } -impl<'value> QueryIn { +impl QueryIn { fn new( diff: Vec>, lhs: Vec>, @@ -77,22 +69,10 @@ pub(crate) enum ComparisonResult { #[derive(Clone, Debug)] pub(crate) enum ValueEvalResult { LhsUnresolved(UnResolved), - UnaryResult(UnaryResult), ComparisonResult(ComparisonResult), } impl ValueEvalResult { - pub(crate) fn success(self, c: C) -> ValueEvalResult - where - C: FnOnce(ValueEvalResult) -> ValueEvalResult, - { - if let ValueEvalResult::ComparisonResult(ComparisonResult::Success(_)) = &self { - c(self) - } else { - self - } - } - pub(crate) fn fail(self, c: C) -> ValueEvalResult where C: FnOnce(ValueEvalResult) -> ValueEvalResult, @@ -117,26 +97,13 @@ pub(crate) struct NotComparable { pub(crate) pair: LhsRhsPair, } -pub(super) fn resolved(qr: &QueryResult, err: E) -> std::result::Result, R> -where - E: Fn(UnResolved) -> R, -{ - match qr { - QueryResult::Resolved(r) | QueryResult::Literal(r) => Ok(Rc::clone(r)), - QueryResult::UnResolved(ur) => Err(err(ur.clone())), - } -} - pub(crate) trait Comparator { - fn compare<'value>( - &self, - lhs: &[QueryResult], - rhs: &[QueryResult], - ) -> crate::rules::Result; + fn compare(&self, lhs: &[QueryResult], rhs: &[QueryResult]) + -> crate::rules::Result; } pub(crate) trait UnaryComparator { - fn compare<'value>(&self, lhs: &[QueryResult]) -> crate::rules::Result; + fn compare(&self, lhs: &[QueryResult]) -> crate::rules::Result; } struct CommonOperator { @@ -208,7 +175,7 @@ impl Comparator for CommonOperator { } } -fn match_value<'value, C>( +fn match_value( each_lhs: Rc, each_rhs: Rc, comparator: C, @@ -239,7 +206,7 @@ where } } -fn is_literal<'value>(query_results: &[QueryResult]) -> Option> { +fn is_literal(query_results: &[QueryResult]) -> Option> { if query_results.len() == 1 { if let QueryResult::Literal(p) = &query_results[0] { return Some(Rc::clone(p)); @@ -248,10 +215,7 @@ fn is_literal<'value>(query_results: &[QueryResult]) -> Option( - lhs_value: Rc, - rhs_value: Rc, -) -> ValueEvalResult { +fn string_in(lhs_value: Rc, rhs_value: Rc) -> ValueEvalResult { match (&*lhs_value, &*rhs_value) { (PathAwareValue::String((_, lhs)), PathAwareValue::String((_, rhs))) => { if rhs.contains(lhs) { @@ -265,7 +229,7 @@ fn string_in<'value>( } } -fn not_comparable<'value>(lhs: Rc, rhs: Rc) -> ValueEvalResult { +fn not_comparable(lhs: Rc, rhs: Rc) -> ValueEvalResult { ValueEvalResult::ComparisonResult(ComparisonResult::NotComparable(NotComparable { pair: LhsRhsPair { lhs: Rc::clone(&lhs), @@ -289,10 +253,7 @@ fn fail(lhs: Rc, rhs: Rc) -> ValueEvalResult { }))) } -fn contained_in<'value>( - lhs_value: Rc, - rhs_value: Rc, -) -> ValueEvalResult { +fn contained_in(lhs_value: Rc, rhs_value: Rc) -> ValueEvalResult { match &*lhs_value { PathAwareValue::List((_, lhsl)) => match &*rhs_value { PathAwareValue::List((_, rhsl)) => { @@ -464,11 +425,10 @@ impl Comparator for InOperation { let mut diff = Vec::with_capacity(lhs_selected.len()); 'each_lhs: for eachl in &lhs_selected { for eachr in &rhs_selected { - match contained_in(Rc::clone(eachl), Rc::clone(eachr)) { - ValueEvalResult::ComparisonResult(ComparisonResult::Success(_)) => { - continue 'each_lhs - } - _ => {} + if let ValueEvalResult::ComparisonResult(ComparisonResult::Success(_)) = + contained_in(Rc::clone(eachl), Rc::clone(eachr)) + { + continue 'each_lhs; } } @@ -666,12 +626,10 @@ impl Comparator for crate::rules::CmpOperator { comparator: compare_ge, } .compare(lhs, rhs), - _ => { - return Err(crate::rules::Error::IncompatibleError(format!( - "Operation {} NOT PERMITTED", - self - ))) - } + _ => Err(crate::rules::Error::IncompatibleError(format!( + "Operation {} NOT PERMITTED", + self + ))), } } } diff --git a/guard/src/rules/eval/operators_tests.rs b/guard/src/rules/eval/operators_tests.rs index 90e93f4a2..80d998e0c 100644 --- a/guard/src/rules/eval/operators_tests.rs +++ b/guard/src/rules/eval/operators_tests.rs @@ -53,30 +53,30 @@ Resources: le: 10 "###; -const RULES_EQ: &str = r###" -let literal1 = [10, 20, 30] -let literal2 = [10, 20] - -rule check_eq_literals_fail { - %literal1 == %literal2 -} - -rule check_in_literals_pass { - %literal2 in %literal1 -} - -let s3s = Resources[ s3_id | Type == "AWS::S3::Bucket" ] -let s3Policies = some Resources[ Type == "AWS::S3::BucketPolicy" ].Bucket.Ref -rule check_eq_queries_fail when %s3s not empty { - %s3Policies == %s3_id -} - -rule check_query_to_rhs_literal_fail { - Resources[ Type == "AWS::IAM::Role" ].Properties.Policy.Statement[*] { - Principal != '*' - } -} -"###; +// const RULES_EQ: &str = r###" +// let literal1 = [10, 20, 30] +// let literal2 = [10, 20] + +// rule check_eq_literals_fail { +// %literal1 == %literal2 +// } + +// rule check_in_literals_pass { +// %literal2 in %literal1 +// } + +// let s3s = Resources[ s3_id | Type == "AWS::S3::Bucket" ] +// let s3Policies = some Resources[ Type == "AWS::S3::BucketPolicy" ].Bucket.Ref +// rule check_eq_queries_fail when %s3s not empty { +// %s3Policies == %s3_id +// } + +// rule check_query_to_rhs_literal_fail { +// Resources[ Type == "AWS::IAM::Role" ].Properties.Policy.Statement[*] { +// Principal != '*' +// } +// } +// "###; #[test] fn test_operator_eq_literal() -> crate::rules::Result<()> { diff --git a/guard/src/rules/eval_context.rs b/guard/src/rules/eval_context.rs index 49f8b2523..efd73e082 100644 --- a/guard/src/rules/eval_context.rs +++ b/guard/src/rules/eval_context.rs @@ -7,7 +7,7 @@ use crate::rules::functions::collections::count; use crate::rules::functions::strings::{ join, json_parse, regex_replace, substring, to_lower, to_upper, url_decode, }; -use crate::rules::path_value::{MapValue, Path, PathAwareValue}; +use crate::rules::path_value::{MapValue, PathAwareValue}; use crate::rules::values::CmpOperator; use crate::rules::Result; use crate::rules::Status::SKIP; @@ -46,6 +46,7 @@ pub(crate) struct RootScope<'value, 'loc: 'value> { } impl<'value, 'loc: 'value> RootScope<'value, 'loc> { + #[cfg(test)] pub fn reset_root(self, new_root: Rc) -> Result> { root_scope_with( self.scope.literals, @@ -304,8 +305,10 @@ fn check_and_delegate<'value, 'loc: 'value>( } } +type Converters = &'static [(fn(&str) -> bool, fn(&str) -> String)]; lazy_static! { - static ref CONVERTERS: &'static [(fn(&str) -> bool, fn(&str) -> String)] = &[ + #[allow(clippy::type_complexity)] + static ref CONVERTERS: Converters = &[ (camelcase::is_camel_case, camelcase::to_camel_case), (classcase::is_class_case, classcase::to_class_case), (kebabcase::is_kebab_case, kebabcase::to_kebab_case), @@ -587,9 +590,11 @@ fn query_retrieval_with_converter<'value, 'loc: 'value>( _ => to_unresolved_result( Rc::clone(¤t), format!( - "Attempting to retrieve from index {} but type is not an array at path {}", + "Attempting to retrieve from index {} but type is not an array at path {}, \ + type {}", index, - current.self_path() + current.self_path(), + current.type_info() ), &query[query_index..], ), @@ -1197,7 +1202,7 @@ pub(crate) fn validate_number_of_params(name: &str, num_args: usize) -> Result<( _ => { return Err(Error::ParseError(format!( "no such function named {name} exists" - ))) + ))); } }; @@ -1216,10 +1221,7 @@ pub(crate) fn try_handle_function_call( args: &[Vec], ) -> Result>> { let value = match fn_name { - "count" => vec![Some(PathAwareValue::Int(( - Path::root(), - count(&args[0]) as i64, - )))], + "count" => vec![Some(count(&args[0]))], "json_parse" => json_parse(&args[0])?, "regex_replace" => { let substring_err_msg = |index| { diff --git a/guard/src/rules/eval_context_tests.rs b/guard/src/rules/eval_context_tests.rs index 0989f40ca..2e68851a6 100644 --- a/guard/src/rules/eval_context_tests.rs +++ b/guard/src/rules/eval_context_tests.rs @@ -600,7 +600,7 @@ fn test_handle_function_call() -> Result<()> { ); // invalid fn name - let res = try_handle_function_call("fn", &[query_results.clone()]); + let res = try_handle_function_call("fn", &[query_results]); assert!(res.is_err()); let err = res.unwrap_err(); assert!(matches!(err, Error::ParseError(_))); diff --git a/guard/src/rules/eval_tests.rs b/guard/src/rules/eval_tests.rs index 828d2be7e..3702283e5 100644 --- a/guard/src/rules/eval_tests.rs +++ b/guard/src/rules/eval_tests.rs @@ -285,7 +285,7 @@ fn query_empty_and_non_empty() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -352,7 +352,7 @@ fn each_lhs_value_not_comparable() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -375,11 +375,11 @@ fn each_lhs_value_not_comparable() -> Result<()> { .. }) => { let rhs_ptr = match &rhs[0] { - QueryResult::Resolved(ptr) => &*ptr, + QueryResult::Resolved(ptr) => &**ptr, _ => unreachable!(), }; - assert_eq!(&**rhs_ptr, &**value); + assert_eq!(rhs_ptr, &**value); } _ => unreachable!(), @@ -436,7 +436,7 @@ fn each_lhs_value_eq_compare() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -509,7 +509,7 @@ fn each_lhs_value_eq_compare_mixed_comparable() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -574,7 +574,7 @@ fn each_lhs_value_eq_compare_mixed_single_plus_array_form_correct_exec() -> Resu "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -588,7 +588,7 @@ fn each_lhs_value_eq_compare_mixed_single_plus_array_form_correct_exec() -> Resu assert_eq!(selected_lhs.len(), 3); // 3 selected values let rhs_value = PathAwareValue::try_from(r#""*""#)?; - let rhs_query_result = vec![QueryResult::Resolved(Rc::new(rhs_value.clone()))]; + let rhs_query_result = vec![QueryResult::Resolved(Rc::new(rhs_value))]; for each_lhs in selected_lhs { match each_lhs { QueryResult::Resolved(lhs) => { @@ -661,7 +661,7 @@ fn binary_comparisons_gt_ge() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -771,7 +771,7 @@ fn binary_comparisons_lt_le() -> Result<()> { "#, )?)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; @@ -895,7 +895,7 @@ Resources: "###; let rules = RulesFile::try_from(rulegen_created)?; let value = PathAwareValue::try_from(serde_yaml::from_str::(template)?)?; - let mut root = root_scope(&rules, Rc::new(value.clone()))?; + let mut root = root_scope(&rules, Rc::new(value))?; //let mut tracker = RecordTracker::new(&mut root); let status = eval_rules_file(&rules, &mut root, None)?; assert_eq!(status, Status::PASS); @@ -937,7 +937,7 @@ fn block_guard_pass() -> Result<()> { let mut tracker = RecordTracker::new(); let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: Some(&mut tracker), }; let status = eval_guard_clause(&block_clauses, &mut eval)?; @@ -1029,7 +1029,7 @@ fn test_guard_10_compatibility_and_diff() -> Result<()> { "###; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; @@ -1084,7 +1084,7 @@ fn test_guard_10_compatibility_and_diff() -> Result<()> { "###; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; // @@ -1127,7 +1127,7 @@ fn block_evaluation() -> Result<()> { "#; let clause = GuardClause::try_from(clause_str)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_guard_clause(&clause, &mut eval)?; @@ -1165,7 +1165,7 @@ fn block_evaluation_fail() -> Result<()> { let value = serde_yaml::from_str::(value_str)?; let value = PathAwareValue::try_from(value)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let clause_str = r#"Resources.*[ Type == 'AWS::ApiGateway::RestApi' ].Properties { @@ -1215,7 +1215,7 @@ fn variable_projections() -> Result<()> { } "#, )?; - let mut root_scope = root_scope(&rules_file, Rc::new(path_value.clone()))?; + let mut root_scope = root_scope(&rules_file, Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut root_scope, None)?; assert_eq!(status, Status::PASS); @@ -1255,7 +1255,7 @@ fn variable_projections_failures() -> Result<()> { } "#, )?; - let mut root_scope = root_scope(&rules_file, Rc::new(path_value.clone()))?; + let mut root_scope = root_scope(&rules_file, Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut root_scope, None)?; assert_eq!(status, Status::FAIL); // for s3_bucket_policy_2.Properties.Bucket == "" @@ -1354,7 +1354,7 @@ fn query_cross_joins() -> Result<()> { } "#, )?; - let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value.clone()))?; + let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value))?; let status = eval_rules_file(&rules_files, &mut root_scope, None)?; assert_eq!(status, Status::SKIP); @@ -1424,7 +1424,7 @@ fn query_cross_joins() -> Result<()> { } "#, )?; - let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value.clone()))?; + let mut root_scope = eval_context::root_scope(&rules_files, Rc::new(path_value))?; let status = eval_rules_file(&rules_files, &mut root_scope, None)?; assert_eq!(status, Status::PASS); @@ -1467,7 +1467,7 @@ fn cross_rule_clause_when_checks() -> Result<()> { let resources = PathAwareValue::try_from(input)?; let rules = RulesFile::try_from(rules_skipped)?; - let mut root = root_scope(&rules, Rc::new(resources.clone()))?; + let mut root = root_scope(&rules, Rc::new(resources))?; let status = eval_rules_file(&rules, &mut root, None)?; assert_eq!(status, Status::PASS); let mut expectations = HashMap::with_capacity(4); @@ -1501,7 +1501,7 @@ fn cross_rule_clause_when_checks() -> Result<()> { "#; let resources = PathAwareValue::try_from(input)?; - let mut root = root_scope(&rules, Rc::new(resources.clone()))?; + let mut root = root_scope(&rules, Rc::new(resources))?; let status = eval_rules_file(&rules, &mut root, None)?; assert_eq!(status, Status::PASS); expectations.clear(); @@ -1542,7 +1542,7 @@ fn test_field_type_array_or_single() -> Result<()> { let path_value = PathAwareValue::try_from(statements)?; let clause = GuardClause::try_from(r#"Statement[*].Action != '*'"#)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; let status = eval_guard_clause(&clause, &mut eval)?; @@ -1558,7 +1558,7 @@ fn test_field_type_array_or_single() -> Result<()> { "#; let path_value = PathAwareValue::try_from(statements)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; let status = eval_guard_clause(&clause, &mut eval)?; @@ -1600,7 +1600,7 @@ fn test_for_in_and_not_in() -> Result<()> { let value = PathAwareValue::try_from(serde_yaml::from_str::(statments)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; @@ -1644,7 +1644,7 @@ fn test_rule_with_range_test_and_this() -> Result<()> { "#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1660,7 +1660,7 @@ fn test_rule_with_range_test_and_this() -> Result<()> { "#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1711,7 +1711,7 @@ fn test_inner_when_skipped() -> Result<()> { "#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1733,7 +1733,7 @@ fn test_inner_when_skipped() -> Result<()> { "#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1744,7 +1744,7 @@ fn test_inner_when_skipped() -> Result<()> { "#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1753,7 +1753,7 @@ fn test_inner_when_skipped() -> Result<()> { let value_str = r#"{}"#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -1857,7 +1857,7 @@ fn test_multiple_valued_clause_reporting() -> Result<()> { "###; let rules = RulesFile::try_from(rule)?; - let mut root = root_scope(&rules, Rc::new(values.clone()))?; + let mut root = root_scope(&rules, Rc::new(values))?; let status = eval_rules_file(&rules, &mut root, None)?; assert_eq!(status, Status::FAIL); @@ -1910,7 +1910,7 @@ fn test_in_comparison_operator_for_list_of_lists( let value = PathAwareValue::try_from(serde_yaml::from_str::(&template)?)?; let rule_eval = RulesFile::try_from(rules)?; - let mut context = root_scope(&rule_eval, Rc::new(value.clone()))?; + let mut context = root_scope(&rule_eval, Rc::new(value))?; let status = eval_rules_file(&rule_eval, &mut context, None)?; assert_eq!(status, status_arg); @@ -1944,7 +1944,7 @@ fn test_type_conversions(#[case] ttl_arg: &str, #[case] status_arg: Status) -> R let value = PathAwareValue::try_from(serde_yaml::from_str::(&template)?)?; let rule_eval = RulesFile::try_from(rules)?; - let mut context = root_scope(&rule_eval, Rc::new(value.clone()))?; + let mut context = root_scope(&rule_eval, Rc::new(value))?; let status = eval_rules_file(&rule_eval, &mut context, None)?; assert_eq!(status, status_arg); @@ -1968,7 +1968,7 @@ fn is_bool() -> Result<()> { let value = PathAwareValue::try_from(resources_str)?; let rules_file = RulesFile::try_from(rule_str)?; println!("{:?}", rules_file); - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::PASS); @@ -1978,7 +1978,7 @@ fn is_bool() -> Result<()> { } "###; let value = PathAwareValue::try_from(resources_str)?; - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::FAIL); @@ -2002,7 +2002,7 @@ fn is_int() -> Result<()> { let value = PathAwareValue::try_from(resources_str)?; let rules_file = RulesFile::try_from(rule_str)?; println!("{:?}", rules_file); - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::PASS); @@ -2012,7 +2012,7 @@ fn is_int() -> Result<()> { } "###; let value = PathAwareValue::try_from(resources_str)?; - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::FAIL); @@ -2074,7 +2074,7 @@ fn double_projection_tests() -> Result<()> { let value = PathAwareValue::try_from(resources_str)?; let rules_file = RulesFile::try_from(rule_str)?; - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::PASS); @@ -2168,7 +2168,7 @@ fn test_rules_with_some_clauses() -> Result<()> { "#; let value = PathAwareValue::try_from(resources)?; let parsed = RulesFile::try_from(query)?; - let mut eval = root_scope(&parsed, Rc::new(value.clone()))?; + let mut eval = root_scope(&parsed, Rc::new(value))?; let selected = eval.resolve_variable("x")?; println!("{:?}", selected); assert_eq!(selected.len(), 1); @@ -2199,7 +2199,7 @@ fn test_support_for_atleast_one_match_clause() -> Result<()> { "#; let values = PathAwareValue::try_from(values_str)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; @@ -2212,7 +2212,7 @@ fn test_support_for_atleast_one_match_clause() -> Result<()> { let values_str = r#"{ Tags: [] }"#; let values = PathAwareValue::try_from(values_str)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_guard_clause(&clause_some, &mut eval)?; @@ -2261,7 +2261,7 @@ fn test_support_for_atleast_one_match_clause() -> Result<()> { let _resources = PathAwareValue::try_from(resources_str)?; let selection_query = AccessQuery::try_from(selection_str)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let selected = eval.query(&selection_query.query)?; @@ -2299,7 +2299,7 @@ rule check_rest_api_is_private_and_has_access { } }"#; let rule = RulesFile::try_from(rule_str)?; - let mut root = root_scope(&rule, Rc::new(value.clone()))?; + let mut root = root_scope(&rule, Rc::new(value))?; let status = eval_rules_file(&rule, &mut root, None)?; assert_eq!(status, Status::FAIL); @@ -2320,7 +2320,7 @@ rule check_rest_api_is_private_and_has_access { "#; let value = serde_yaml::from_str::(value_str)?; let value = PathAwareValue::try_from(value)?; - let mut root = root_scope(&rule, Rc::new(value.clone()))?; + let mut root = root_scope(&rule, Rc::new(value))?; let status = eval_rules_file(&rule, &mut root, None)?; assert_eq!(status, Status::PASS); @@ -2389,7 +2389,7 @@ fn ensure_all_list_value_access_on_empty_fails() -> Result<()> { let claused_failure_spec = GuardClause::try_from(r#"Tags[*] empty"#)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_guard_clause(&claused_failure_spec, &mut eval)?; @@ -2439,7 +2439,7 @@ fn ensure_all_map_values_access_on_empty_fails() -> Result<()> { "#; let _value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_guard_clause(&clause_failure_spec, &mut eval)?; @@ -2451,7 +2451,7 @@ fn ensure_all_map_values_access_on_empty_fails() -> Result<()> { let resources = r#"{}"#; let values = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let clause_failure_spec = GuardClause::try_from(r#"Resources exists"#)?; @@ -2532,7 +2532,7 @@ fn filter_based_join_clauses_failures_and_skips() -> Result<()> { let rules_file = RulesFile::try_from(rules)?; let path_value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; - let mut eval = root_scope(&rules_file, Rc::new(path_value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::FAIL); @@ -2585,7 +2585,7 @@ fn filter_based_join_clauses_failures_and_skips() -> Result<()> { "#; let path_value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; - let mut eval = root_scope(&rules_file, Rc::new(path_value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::SKIP); @@ -2611,7 +2611,7 @@ fn filter_based_join_clauses_failures_and_skips() -> Result<()> { "#; let path_value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; - let mut eval = eval.reset_root(Rc::new(path_value.clone()))?; + let mut eval = eval.reset_root(Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::SKIP); @@ -2640,7 +2640,7 @@ fn filter_based_join_clauses_failures_and_skips() -> Result<()> { let path_value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; - let mut eval = eval.reset_root(Rc::new(path_value.clone()))?; + let mut eval = eval.reset_root(Rc::new(path_value))?; // // Let us track failures and assert on what must be observed @@ -2713,7 +2713,7 @@ fn filter_based_with_join_pass_use_cases() -> Result<()> { let path_value = PathAwareValue::try_from(serde_yaml::from_str::(resources)?)?; let rules_file = RulesFile::try_from(rules)?; - let mut eval = root_scope(&rules_file, Rc::new(path_value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(path_value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; assert_eq!(status, Status::PASS); @@ -2752,7 +2752,7 @@ fn rule_clause_tests() -> Result<()> { "#; let value = PathAwareValue::try_from(v)?; - let mut eval = root_scope(&rule, Rc::new(value.clone()))?; + let mut eval = root_scope(&rule, Rc::new(value))?; let status = eval_rules_file(&rule, &mut eval, None)?; assert_eq!(Status::PASS, status); @@ -2774,7 +2774,7 @@ fn rule_clause_tests() -> Result<()> { "#; let value = PathAwareValue::try_from(v)?; - let mut eval = eval.reset_root(Rc::new(value.clone()))?; + let mut eval = eval.reset_root(Rc::new(value))?; let status = eval_rules_file(&rule, &mut eval, None)?; assert_eq!(Status::FAIL, status); @@ -2835,7 +2835,7 @@ fn rule_test_type_blocks() -> Result<()> { let root = PathAwareValue::try_from(serde_yaml::from_str::(value)?)?; let rules_file = RulesFile::try_from(r)?; - let mut root_context = root_scope(&rules_file, Rc::new(root.clone()))?; + let mut root_context = root_scope(&rules_file, Rc::new(root))?; let status = eval_rules_file(&rules_file, &mut root_context, None)?; assert_eq!(Status::FAIL, status); @@ -2928,7 +2928,7 @@ rule iam_basic_checks when iam_resources_exists { let root = PathAwareValue::try_from(value)?; let rules_file = RulesFile::try_from(file)?; - let mut root_context = root_scope(&rules_file, Rc::new(root.clone()))?; + let mut root_context = root_scope(&rules_file, Rc::new(root))?; let status = eval_rules_file(&rules_file, &mut root_context, None)?; assert_eq!(Status::PASS, status); @@ -2996,7 +2996,7 @@ rule iam_basic_checks { let root = PathAwareValue::try_from(value)?; let rules_file = RulesFile::try_from(file)?; - let mut root_context = root_scope(&rules_file, Rc::new(root.clone()))?; + let mut root_context = root_scope(&rules_file, Rc::new(root))?; let status = eval_rules_file(&rules_file, &mut root_context, None)?; assert_eq!(Status::FAIL, status); @@ -3064,7 +3064,7 @@ rule iam_basic_checks { "###; let root = PathAwareValue::try_from(value)?; - let mut root_context = root_context.reset_root(Rc::new(root.clone()))?; + let mut root_context = root_context.reset_root(Rc::new(root))?; let status = eval_rules_file(&rules_file, &mut root_context, None)?; assert_eq!(Status::FAIL, status); @@ -3152,7 +3152,7 @@ fn test_iam_statement_clauses() -> Result<()> { "###; let values = PathAwareValue::try_from(sample)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; @@ -3189,7 +3189,7 @@ fn test_iam_statement_clauses() -> Result<()> { }"###; let value = PathAwareValue::try_from(sample)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_guard_clause(&parsed, &mut eval)?; @@ -3210,7 +3210,7 @@ fn test_iam_statement_clauses() -> Result<()> { }"###; let value = PathAwareValue::try_from(sample)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_guard_clause(&parsed, &mut eval)?; @@ -3232,7 +3232,7 @@ fn test_iam_statement_clauses() -> Result<()> { }"###; let value = PathAwareValue::try_from(sample)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_guard_clause(&parsed, &mut eval)?; @@ -3241,7 +3241,7 @@ fn test_iam_statement_clauses() -> Result<()> { let value = PathAwareValue::try_from(SAMPLE)?; let parsed = GuardClause::try_from(clause)?; let mut eval = BasicQueryTesting { - root: Rc::new(value.clone()), + root: Rc::new(value), recorder: None, }; let status = eval_guard_clause(&parsed, &mut eval)?; @@ -3304,7 +3304,7 @@ rule check_rest_api_private { let values = PathAwareValue::try_from(resources)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -3369,7 +3369,7 @@ rule check_rest_api_private { let values = PathAwareValue::try_from(resources)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -3413,7 +3413,7 @@ rule check_rest_api_private { let values = PathAwareValue::try_from(resources)?; let mut eval = BasicQueryTesting { - root: Rc::new(values.clone()), + root: Rc::new(values), recorder: None, }; let status = eval_rule(&rule, &mut eval)?; @@ -3477,7 +3477,7 @@ rule deny_task_role_no_permission_boundary when %ecs_tasks !EMPTY { let rules_file = RulesFile::try_from(rules)?; let value = PathAwareValue::try_from(resources)?; - let mut eval = root_scope(&rules_file, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_file, Rc::new(value))?; let status = eval_rules_file(&rules_file, &mut eval, None)?; println!("{}", status); @@ -3808,7 +3808,7 @@ fn match_lhs_with_rhs_single_element_pass() -> Result<()> { let path_value = PathAwareValue::try_from(serde_yaml::from_str::(value)?)?; let guard_clause = GuardClause::try_from(clause)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; let status = eval_guard_clause(&guard_clause, &mut eval)?; @@ -3819,7 +3819,7 @@ fn match_lhs_with_rhs_single_element_pass() -> Result<()> { let path_value = PathAwareValue::try_from(serde_yaml::from_str::(value)?)?; let guard_clause = GuardClause::try_from(clause)?; let mut eval = BasicQueryTesting { - root: Rc::new(path_value.clone()), + root: Rc::new(path_value), recorder: None, }; let status = eval_guard_clause(&guard_clause, &mut eval)?; @@ -3868,7 +3868,7 @@ fn parameterized_evaluations() -> Result<()> { let template = PathAwareValue::try_from(serde_yaml::from_str::(template_value)?)?; - let mut eval = root_scope(&rules_files, Rc::new(template.clone()))?; + let mut eval = root_scope(&rules_files, Rc::new(template))?; let status = eval_rules_file(&rules_files, &mut eval, None)?; let top = eval.reset_recorder().extract(); let mut writer = Writer::new(Stdout(stdout()), Stderr(stderr())); @@ -3889,7 +3889,7 @@ fn parameterized_evaluations() -> Result<()> { let config_value = PathAwareValue::try_from(serde_yaml::from_str::(aws_config_value)?)?; - let mut eval = root_scope(&rules_files, Rc::new(config_value.clone()))?; + let mut eval = root_scope(&rules_files, Rc::new(config_value))?; let status = eval_rules_file(&rules_files, &mut eval, None)?; let top = eval.reset_recorder().extract(); crate::commands::validate::print_verbose_tree(&top, &mut writer); @@ -3929,7 +3929,7 @@ fn using_resource_names_for_assessment() -> Result<()> { "###; let rules = RulesFile::try_from(rules_file)?; - let mut eval = root_scope(&rules, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules, Rc::new(value))?; let status = eval_rules_file(&rules, &mut eval, None)?; assert_eq!(status, Status::FAIL); @@ -3965,7 +3965,7 @@ fn test_string_in_comparison() -> Result<()> { "###; let rules_files = RulesFile::try_from(rules)?; - let mut eval = root_scope(&rules_files, Rc::new(value.clone()))?; + let mut eval = root_scope(&rules_files, Rc::new(value))?; let status = eval_rules_file(&rules_files, &mut eval, None)?; assert_eq!(status, Status::PASS); diff --git a/guard/src/rules/evaluate.rs b/guard/src/rules/evaluate.rs index 5795fd413..1cff28327 100644 --- a/guard/src/rules/evaluate.rs +++ b/guard/src/rules/evaluate.rs @@ -47,9 +47,9 @@ pub(super) fn resolve_variable_query<'s>( Ok(acc) } -pub(super) fn resolve_query<'s, 'loc>( +pub(super) fn resolve_query<'s>( all: bool, - query: &[QueryPart<'loc>], + query: &[QueryPart<'_>], context: &'s PathAwareValue, var_resolver: &'s dyn EvaluationContext, ) -> Result> { @@ -80,6 +80,7 @@ fn negation_status(r: bool, clause_not: bool, not: bool) -> Status { } } +#[allow(clippy::type_complexity)] fn compare_loop_all( lhs: &Vec<&PathAwareValue>, rhs: &Vec<&PathAwareValue>, @@ -97,7 +98,7 @@ where 'lhs: for lhs_value in lhs { let mut acc = Vec::with_capacity(lhs.len()); for rhs_value in rhs { - let check = compare(*lhs_value, *rhs_value)?; + let check = compare(lhs_value, rhs_value)?; if check { if any_one_rhs { acc.clear(); @@ -124,7 +125,7 @@ where Ok((lhs_cmp, results)) } -#[allow(clippy::never_loop)] +#[allow(clippy::never_loop, clippy::type_complexity)] fn compare_loop( lhs: &Vec<&PathAwareValue>, rhs: &Vec<&PathAwareValue>, @@ -216,6 +217,7 @@ fn merge_mixed_results<'a>(incoming: &'a [&PathAwareValue]) -> Vec<&'a PathAware merged } +#[allow(clippy::type_complexity)] fn compare( lhs: &Vec<&PathAwareValue>, _lhs_query: &[QueryPart<'_>], @@ -288,32 +290,6 @@ where } } -//impl<'loc> std::fmt::Display for GuardAccessClause<'loc> { -// fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { -// f.write_fmt( -// format_args!( -// "Clause({}, Check: {} {} {} {})", -// self.access_clause.location, -// SliceDisplay(&self.access_clause.query.query), -// if self.access_clause.comparator.1 { "NOT" } else { "" }, -// self.access_clause.comparator.0, -// match &self.access_clause.compare_with { -// Some(v) => { -// match v { -// // TODO add Display for Value -// LetValue::Value(val) => format!("{:?}", val), -// LetValue::AccessClause(qry) => format!("{}", SliceDisplay(&qry.query)), -// -// } -// }, -// None => "".to_string() -// }, -// ) -// )?; -// Ok(()) -// } -//} - pub(super) fn invert_closure( f: F, clause_not: bool, @@ -411,13 +387,7 @@ impl<'loc> Evaluate for GuardAccessClause<'loc> { None => Some(negation_status(false, not, clause.negation)), Some(l) => Some(negation_status( l.iter() - .find(|p| { - if let PathAwareValue::String(_) = **p { - false - } else { - true - } - }) + .find(|p| !matches!(**p, PathAwareValue::String(_))) .map_or(true, |_i| false), not, clause.negation, @@ -428,13 +398,7 @@ impl<'loc> Evaluate for GuardAccessClause<'loc> { None => Some(negation_status(false, not, clause.negation)), Some(l) => Some(negation_status( l.iter() - .find(|p| { - if let PathAwareValue::List(_) = **p { - false - } else { - true - } - }) + .find(|p| !matches!(**p, PathAwareValue::List(_))) .map_or(true, |_i| false), not, clause.negation, @@ -445,13 +409,7 @@ impl<'loc> Evaluate for GuardAccessClause<'loc> { None => Some(negation_status(false, not, clause.negation)), Some(l) => Some(negation_status( l.iter() - .find(|p| { - if let PathAwareValue::Map(_) = **p { - false - } else { - true - } - }) + .find(|p| !matches!(**p, PathAwareValue::Map(_))) .map_or(true, |_i| false), not, clause.negation, @@ -771,7 +729,7 @@ impl<'loc, T: Evaluate + 'loc> Evaluate for Block<'loc, T> { context: &'s PathAwareValue, var_resolver: &'s dyn EvaluationContext, ) -> Result { - let block = BlockScope::new(&self, context, var_resolver)?; + let block = BlockScope::new(self, context, var_resolver)?; self.conjunctions.evaluate(context, &block) } } @@ -967,7 +925,7 @@ impl<'loc> Evaluate for TypeBlock<'loc> { let mut each_type_report = AutoReport::new(EvaluationType::Type, var_resolver, &type_context); match each_type_report - .status(self.block.evaluate(*each, var_resolver)?) + .status(self.block.evaluate(each, var_resolver)?) .get_status() { Status::PASS => { @@ -1118,6 +1076,7 @@ pub(crate) struct RootScope<'s, 'loc> { rule_statues: std::cell::RefCell>, } +#[cfg(test)] impl<'s, 'loc> RootScope<'s, 'loc> { pub(crate) fn new(rules: &'s RulesFile<'loc>, value: &'s PathAwareValue) -> Result { let mut literals = HashMap::new(); @@ -1127,6 +1086,7 @@ impl<'s, 'loc> RootScope<'s, 'loc> { for rule in &rules.guard_rules { lookup_cache.insert(rule.rule_name.as_str(), rule); } + Ok(RootScope { rules, input_context: value, @@ -1149,7 +1109,7 @@ impl<'s, 'loc> EvaluationContext for RootScope<'s, 'loc> { return Ok(value.clone()); } return if let Some((key, query)) = self.pending_queries.get_key_value(variable) { - let all = (*query).match_all; + let all = query.match_all; let query = &query.query; let values = match query[0].variable() { Some(var) => resolve_variable_query(all, var, query, self)?, @@ -1246,7 +1206,7 @@ impl<'s, T> EvaluationContext for BlockScope<'s, T> { return Ok(value.clone()); } return if let Some((key, query)) = self.pending_queries.get_key_value(variable) { - let all = (*query).match_all; + let all = query.match_all; let query = &query.query; let values = match query[0].variable() { Some(var) => resolve_variable_query(all, var, query, self)?, @@ -1331,20 +1291,6 @@ impl<'s> AutoReport<'s> { self } - pub(super) fn comparison( - &mut self, - status: Status, - from: Option, - to: Option, - cmp: (CmpOperator, bool), - ) -> &mut Self { - self.status = Some(status); - self.from = from; - self.to = to; - self.cmp = Some(cmp); - self - } - pub(super) fn from(&mut self, from: Option) -> &mut Self { self.from = from; self diff --git a/guard/src/rules/evaluate_tests.rs b/guard/src/rules/evaluate_tests.rs index 983ef463f..881eb7c67 100644 --- a/guard/src/rules/evaluate_tests.rs +++ b/guard/src/rules/evaluate_tests.rs @@ -88,10 +88,9 @@ fn guard_access_clause_tests() -> Result<()> { Principal.Service EXISTS Principal.Service == /^notexists/ ].Action == "sts:AssumeRole""#, )?; - match clause.evaluate(&root, &dummy) { - Ok(Status::FAIL) => {} - _rest => assert!(false), - } + + assert!(matches!(clause.evaluate(&root, &dummy)?, Status::FAIL)); + Ok(()) } @@ -559,7 +558,7 @@ rule deny_egress when %sgs NOT EMPTY { let root_context = RootScope::new(&rules_file, each)?; let reporter = Reporter(&root_context); let status = rules_file.evaluate(each, &reporter)?; - println!("{}", format!("Status {} = {}", index, status)); + println!("Status {} = {}", index, status); } let sample = r#"{ "Resources": {} }"#; @@ -1216,7 +1215,7 @@ fn test_support_for_atleast_one_match_clause() -> Result<()> { assert_eq!(status, Status::FAIL); let r = clause_some.evaluate(&values, &dummy); - assert_eq!(r.is_err(), false); + assert!(r.is_ok()); assert_eq!(r.unwrap(), Status::FAIL); // @@ -1394,7 +1393,7 @@ fn test_compare_loop_atleast_one_eq() -> Result<()> { PathAwareValue::String((root.clone(), "aws:sourceVpc".to_string())), ]; let rhs = [PathAwareValue::Regex(( - root.clone(), + root, "aws:[sS]ource(Vpc|VPC|Vpce|VPCE)".to_string(), ))]; @@ -1411,7 +1410,7 @@ fn test_compare_loop_atleast_one_eq() -> Result<()> { false, false, )?; - assert_eq!(result, false); + assert!(!result); // // match any one rhs = false, at-least-one = true @@ -1423,7 +1422,7 @@ fn test_compare_loop_atleast_one_eq() -> Result<()> { false, true, )?; - assert_eq!(result, true); + assert!(result); // // match any one rhs = true, at-least-one = false @@ -1435,7 +1434,7 @@ fn test_compare_loop_atleast_one_eq() -> Result<()> { true, false, )?; - assert_eq!(result, false); + assert!(!result); Ok(()) } @@ -1448,7 +1447,7 @@ fn test_compare_loop_all() -> Result<()> { PathAwareValue::String((root.clone(), "aws:sourceVpc".to_string())), ]; let rhs = [PathAwareValue::Regex(( - root.clone(), + root, "aws:[sS]ource(Vpc|VPC|Vpce|VPCE)".to_string(), ))]; @@ -1461,12 +1460,12 @@ fn test_compare_loop_all() -> Result<()> { // assert_eq!(results.1.len(), 2); let (outcome, from, to) = &results.1[0]; - assert_eq!(*outcome, false); + assert!(!*outcome); assert_eq!(from, &Some(lhs[0].clone())); assert_eq!(to, &Some(rhs[0].clone())); let (outcome, from, to) = &results.1[1]; - assert_eq!(*outcome, true); + assert!(*outcome); assert_eq!(from, &None); assert_eq!(to, &None); @@ -1480,7 +1479,7 @@ fn test_compare_lists() -> Result<()> { root.clone(), vec![ PathAwareValue::Int((root.clone(), 1)), - PathAwareValue::Int((root.clone(), 2)), + PathAwareValue::Int((root, 2)), ], )); let lhs = vec![&value]; @@ -1853,7 +1852,7 @@ impl<'a> EvaluationContext for Tracker<'a> { cmp: Option<(CmpOperator, bool)>, ) { self.root - .end_evaluation(eval_type, context, msg, from, to, status.clone(), cmp); + .end_evaluation(eval_type, context, msg, from, to, status, cmp); if eval_type == EvaluationType::Rule { match self.expected.get(context) { Some(e) => { @@ -2168,19 +2167,16 @@ fn test_multiple_valued_clause_reporting() -> Result<()> { if eval_type == EvaluationType::Clause { match &status { Some(Status::FAIL) => { - assert_eq!(from.is_some(), true); - assert_eq!(to.is_some(), true); + assert!(from.is_some()); + assert!(to.is_some()); let path_val = from.unwrap(); let path = path_val.self_path(); - assert_eq!( - path.0.contains("/second") || path.0.contains("/failed"), - true - ); + assert!(path.0.contains("/second") || path.0.contains("/failed")); } Some(Status::PASS) => { assert_eq!(from, None); assert_eq!(to, None); - assert_eq!(msg.contains("DEFAULT"), true); + assert!(msg.contains("DEFAULT")); } _ => {} } @@ -2247,19 +2243,16 @@ fn test_multiple_valued_clause_reporting_var_access() -> Result<()> { if eval_type == EvaluationType::Clause { match &status { Some(Status::FAIL) => { - assert_eq!(from.is_some(), true); - assert_eq!(to.is_some(), true); + assert!(from.is_some()); + assert!(to.is_some()); let path_val = from.as_ref().unwrap(); let path = path_val.self_path(); - assert_eq!( - path.0.contains("/second") || path.0.contains("/failed"), - true - ); + assert!(path.0.contains("/second") || path.0.contains("/failed")); } Some(Status::PASS) => { assert_eq!(from, None); assert_eq!(to, None); - assert_eq!(msg.contains("DEFAULT"), true); + assert!(msg.contains("DEFAULT")); } _ => {} } diff --git a/guard/src/rules/functions/collections.rs b/guard/src/rules/functions/collections.rs index 67bbbe267..74d168048 100644 --- a/guard/src/rules/functions/collections.rs +++ b/guard/src/rules/functions/collections.rs @@ -1,10 +1,25 @@ -use crate::rules::QueryResult; +use crate::rules::{ + path_value::{Path, PathAwareValue}, + QueryResult, +}; -pub(crate) fn count(args: &[QueryResult]) -> u32 { - args.iter().fold(0, |each, entry| match entry { - QueryResult::Literal(_) | QueryResult::Resolved(_) => each + 1, - _ => each, - }) +pub(crate) fn count(args: &[QueryResult]) -> PathAwareValue { + let count = args + .iter() + .filter(|query| !matches!(query, QueryResult::UnResolved(_))) + .count(); + + match args.is_empty() { + true => PathAwareValue::Int((Path::root(), 0)), + false => { + let path = match &args[0] { + QueryResult::Literal(val) | QueryResult::Resolved(val) => val.self_path().clone(), + QueryResult::UnResolved(val) => val.traversed_to.self_path().clone(), + }; + + PathAwareValue::Int((path, count as i64)) + } + } } #[cfg(test)] diff --git a/guard/src/rules/functions/collections_tests.rs b/guard/src/rules/functions/collections_tests.rs index 0845af342..59ae85c66 100644 --- a/guard/src/rules/functions/collections_tests.rs +++ b/guard/src/rules/functions/collections_tests.rs @@ -17,8 +17,11 @@ fn test_count_function() -> crate::rules::Result<()> { }; let query = AccessQuery::try_from(r#"Resources"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 1); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 1), + _ => unreachable!(), + } let value_str = r#"{}"#; let value = PathAwareValue::try_from(serde_yaml::from_str::(value_str)?)?; @@ -29,8 +32,11 @@ fn test_count_function() -> crate::rules::Result<()> { }; let query = AccessQuery::try_from(r#"Resources"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 0); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 0), + _ => unreachable!(), + } let value_str = r#" Resources: @@ -46,13 +52,18 @@ fn test_count_function() -> crate::rules::Result<()> { }; let query = AccessQuery::try_from(r#"Resources[ Type == 'AWS::S3::Bucket' ]"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 2); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 2), + _ => unreachable!(), + } let query = AccessQuery::try_from(r#"Resources[ Type == 'AWS::EC2::Instance' ]"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 0); + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 0), + _ => unreachable!(), + } Ok(()) } diff --git a/guard/src/rules/functions/strings.rs b/guard/src/rules/functions/strings.rs index 1613cba59..3d456a998 100644 --- a/guard/src/rules/functions/strings.rs +++ b/guard/src/rules/functions/strings.rs @@ -177,13 +177,23 @@ pub(crate) fn join(args: &[QueryResult], delimiter: &str) -> crate::rules::Resul } QueryResult::UnResolved(ur) => { return Err(Error::IncompatibleError(format!( - "Joining non unresolved values is not allowed {}, unsatisfied part {}", + "Joining unresolved values is not allowed {}, unsatisfied part {}", ur.traversed_to, ur.remaining_query ))); } } } - Ok(PathAwareValue::String((Path::root(), aggr))) + + match args.is_empty() { + true => Ok(PathAwareValue::String((Path::root(), aggr))), + false => { + let path = match &args[0] { + QueryResult::Literal(val) | QueryResult::Resolved(val) => val.self_path().clone(), + QueryResult::UnResolved(val) => val.traversed_to.self_path().clone(), + }; + Ok(PathAwareValue::String((path, aggr))) + } + } } #[cfg(test)] diff --git a/guard/src/rules/functions/strings_tests.rs b/guard/src/rules/functions/strings_tests.rs index eef8bd9d8..8c154c2ec 100644 --- a/guard/src/rules/functions/strings_tests.rs +++ b/guard/src/rules/functions/strings_tests.rs @@ -31,8 +31,12 @@ fn test_json_parse() -> crate::rules::Result<()> { let query = AccessQuery::try_from(r#"Resources[ Type == 'AWS::New::Service' ].Properties.Policy"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 1); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 1), + _ => unreachable!(), + } + let json = json_parse(&results)?; assert_eq!(json.len(), 1); let path_value = json[0].as_ref().unwrap(); @@ -71,8 +75,11 @@ fn test_regex_replace() -> crate::rules::Result<()> { let query = AccessQuery::try_from(r#"Resources[ Type == 'AWS::New::Service' ].Properties.Arn"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 1); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 1), + _ => unreachable!(), + } let replaced = regex_replace( &results, @@ -113,8 +120,11 @@ fn test_substring() -> crate::rules::Result<()> { let query = AccessQuery::try_from(r#"Resources[ Type == 'AWS::New::Service' ].Properties.Arn"#)?; let results = eval.query(&query.query)?; - let cnt = count(&results); - assert_eq!(cnt, 1); + + match count(&results) { + PathAwareValue::Int((_, cnt)) => assert_eq!(cnt, 1), + _ => unreachable!(), + } let replaced = substring(&results, 0, 3)?; assert_eq!(replaced.len(), 1); diff --git a/guard/src/rules/libyaml/event.rs b/guard/src/rules/libyaml/event.rs index e33c7a608..9bb76239c 100644 --- a/guard/src/rules/libyaml/event.rs +++ b/guard/src/rules/libyaml/event.rs @@ -1,5 +1,6 @@ use crate::rules::libyaml::{cstr, cstr::CStr, tag::Tag}; use std::{borrow::Cow, fmt, fmt::Debug, ptr::NonNull, slice}; +#[allow(clippy::unsafe_removed_from_name)] use unsafe_libyaml as sys; #[derive(Debug)] @@ -61,6 +62,7 @@ pub(crate) unsafe fn convert_event<'input>( } } +#[allow(dead_code)] pub(crate) struct Scalar<'input> { pub anchor: Option, pub tag: Option, @@ -70,12 +72,14 @@ pub(crate) struct Scalar<'input> { } #[derive(Debug)] +#[allow(dead_code)] pub(crate) struct SequenceStart { pub anchor: Option, pub tag: Option, } #[derive(Debug)] +#[allow(dead_code)] pub(crate) struct MappingStart { pub anchor: Option, pub tag: Option, diff --git a/guard/src/rules/libyaml/mod.rs b/guard/src/rules/libyaml/mod.rs index 1c5a557f6..9aa79fe00 100644 --- a/guard/src/rules/libyaml/mod.rs +++ b/guard/src/rules/libyaml/mod.rs @@ -1,3 +1,4 @@ +#![allow(clippy::all)] mod cstr; mod event; pub mod loader; diff --git a/guard/src/rules/libyaml/util.rs b/guard/src/rules/libyaml/util.rs index 0efaebcdd..c8e5fb2e2 100644 --- a/guard/src/rules/libyaml/util.rs +++ b/guard/src/rules/libyaml/util.rs @@ -6,6 +6,7 @@ use std::{ }; use crate::rules::path_value::Location; +#[allow(clippy::unsafe_removed_from_name)] use unsafe_libyaml as sys; pub(crate) struct Owned { diff --git a/guard/src/rules/mod.rs b/guard/src/rules/mod.rs index 9bdc46714..f522fdde5 100644 --- a/guard/src/rules/mod.rs +++ b/guard/src/rules/mod.rs @@ -1,3 +1,4 @@ +#![allow(deprecated)] pub(crate) mod display; pub(crate) mod errors; pub(crate) mod eval; @@ -84,19 +85,15 @@ lazy_static! { }; } -#[derive(Debug, Clone, PartialEq, Copy, Serialize)] +#[derive(Debug, Clone, PartialEq, Copy, Serialize, Default)] +#[allow(clippy::upper_case_acronyms)] pub(crate) enum Status { PASS, FAIL, + #[default] SKIP, } -impl Default for Status { - fn default() -> Self { - Status::SKIP - } -} - impl std::fmt::Display for Status { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { @@ -357,10 +354,6 @@ pub(crate) enum RecordType<'value> { ClauseValueCheck(ClauseCheck<'value>), } -struct ParameterRuleResult<'value, 'loc> { - rule: &'value ParameterizedRule<'loc>, -} - pub(crate) trait RecordTracer<'value> { fn start_record(&mut self, context: &str) -> Result<()>; fn end_record(&mut self, context: &str, record: RecordType<'value>) -> Result<()>; @@ -391,6 +384,7 @@ pub(crate) trait EvaluationContext { fn rule_status(&self, rule_name: &str) -> Result; + #[allow(clippy::too_many_arguments)] fn end_evaluation( &self, eval_type: EvaluationType, diff --git a/guard/src/rules/parser.rs b/guard/src/rules/parser.rs index 29ec2c94c..b034925b9 100644 --- a/guard/src/rules/parser.rs +++ b/guard/src/rules/parser.rs @@ -44,20 +44,6 @@ pub(crate) struct ParserError<'a> { pub(crate) type IResult<'a, I, O> = nom::IResult>; -impl<'a> ParserError<'a> { - pub(crate) fn context(&self) -> &str { - &self.context - } - - pub(crate) fn span(&self) -> &Span<'a> { - &self.span - } - - pub(crate) fn kind(&self) -> ErrorKind { - self.kind - } -} - impl<'a> nom::error::ParseError> for ParserError<'a> { fn from_error_kind(input: Span<'a>, kind: ErrorKind) -> Self { ParserError { @@ -264,9 +250,9 @@ fn parse_regex_inner(input: Span) -> IResult { let validate_regex = Regex::new(regex.as_str()); return match validate_regex { - Ok(valid_regex) => Ok((remainder, Value::Regex(regex))), + Ok(_) => Ok((remainder, Value::Regex(regex))), Err(e) => Err(nom::Err::Error(ParserError { - context: format!("Could not parse regular expression: {}", e.to_string()), + context: format!("Could not parse regular expression: {}", e), kind: ErrorKind::RegexpMatch, span: input, })), @@ -539,7 +525,7 @@ pub(crate) fn var_name(input: Span) -> IResult { let (remainder, first_part) = alpha1(input)?; let (remainder, next_part) = take_while(|c: char| c.is_alphanumeric() || c == '_')(remainder)?; let mut var_name = (*first_part.fragment()).to_string(); - var_name.push_str(*next_part.fragment()); + var_name.push_str(next_part.fragment()); Ok((remainder, var_name)) } @@ -1844,8 +1830,8 @@ pub(crate) fn rules_file(input: Span) -> Result { } else { format!( "{rule_file_name}/{rule_name}", - rule_file_name = input.extra.to_string(), - rule_name = DEFAULT_RULE_NAME.to_string() + rule_file_name = input.extra, + rule_name = DEFAULT_RULE_NAME ) }; diff --git a/guard/src/rules/parser_tests.rs b/guard/src/rules/parser_tests.rs index 5f8b5908b..41946a4a8 100644 --- a/guard/src/rules/parser_tests.rs +++ b/guard/src/rules/parser_tests.rs @@ -739,7 +739,7 @@ fn test_var_name() { ]; for (idx, text) in examples.iter().enumerate() { - let span = from_str2(*text); + let span = from_str2(text); let actual = var_name(span); assert_eq!(&actual, &expectations[idx]); } @@ -798,7 +798,7 @@ fn test_var_name_access() { ]; for (idx, text) in examples.iter().enumerate() { - let span = from_str2(*text); + let span = from_str2(text); let actual = var_name_access(span); assert_eq!(&actual, &expectations[idx]); } @@ -928,7 +928,7 @@ fn test_dotted_access() { ]; for (idx, text) in examples.iter().enumerate() { - let span = from_str2(*text); + let span = from_str2(text); let actual = dotted_access(span); println!("#{} Example = {}, Result = {:?}", idx, *text, actual); assert_eq!(&actual, &expectations[idx]); @@ -1304,7 +1304,7 @@ fn test_other_operations() { ]; for (idx, each) in examples.iter().enumerate() { - let span = from_str2(*each); + let span = from_str2(each); let result = other_operations(span); assert_eq!(&result, &expectations[idx]); } @@ -1423,7 +1423,7 @@ fn test_keys_keyword() { ]; for (idx, each) in examples.iter().enumerate() { - let span = from_str2(*each); + let span = from_str2(each); let result = map_keys_match(span); assert_eq!(&result, &expectations[idx]); } @@ -1494,7 +1494,7 @@ fn test_value_cmp() { ]; for (idx, each) in examples.iter().enumerate() { - let span = from_str2(*each); + let span = from_str2(each); let result = value_cmp(span); assert_eq!(&result, &expectations[idx]); } @@ -1540,7 +1540,7 @@ fn test_clause_success() { testing_access_with_cmp( &separators, &comparators, - *each_lhs, + each_lhs, rhs, || dotted.clone(), || rhs_access.clone(), @@ -1565,7 +1565,7 @@ fn test_clause_success() { testing_access_with_cmp( &separators, &comparators, - *each_lhs, + each_lhs, "", || dotted.clone(), || None, @@ -1583,7 +1583,7 @@ fn test_clause_success() { testing_access_with_cmp( &separators, &comparators, - *each_lhs, + each_lhs, " does.not.error", // this will not error, // the fragment you are left with is the one above and // the next clause fetch will error out for either no "OR" or @@ -1610,7 +1610,7 @@ fn test_clause_success() { testing_access_with_cmp( &separators, &comparators, - *each_lhs, + each_lhs, "", || dotted.clone(), || None, @@ -1642,12 +1642,12 @@ fn test_clause_success() { }; let rhs_value = - PathAwareValue::try_from(parse_value(from_str2(*each_rhs)).unwrap().1).unwrap(); + PathAwareValue::try_from(parse_value(from_str2(each_rhs)).unwrap().1).unwrap(); testing_access_with_cmp( &separators, &comparators, - *each_lhs, - *each_rhs, + each_lhs, + each_rhs, || dotted.clone(), || Some(LetValue::Value(rhs_value.clone())), ); @@ -1875,7 +1875,7 @@ fn test_predicate_clause_success() { for (idx, each) in examples.iter().enumerate() { println!("Test # {}: {}", idx, *each); - let span = from_str2(*each); + let span = from_str2(each); let result = access(span); println!("Result for Test # {}, {:?}", idx, result); assert_eq!(&result, &expectations[idx]); @@ -2060,7 +2060,7 @@ fn test_rule_clauses() { ]; for (idx, each) in examples.iter().enumerate() { - let span = from_str2(*each); + let span = from_str2(each); let result = rule_clause(span); assert_eq!(&result, &expectations[idx]); } @@ -2205,7 +2205,7 @@ fn test_clauses() { query: AccessQuery { query: "configurations.containers[*].image" .split('.') - .map(|part| { + .flat_map(|part| { if part.contains('[') { vec![ QueryPart::Key("containers".to_string()), @@ -2215,8 +2215,6 @@ fn test_clauses() { vec![QueryPart::Key(part.to_string())] } }) - .into_iter() - .flatten() .collect(), match_all: true, }, @@ -2241,7 +2239,7 @@ fn test_clauses() { for (idx, each) in examples.iter().enumerate() { println!("Testing #{}, Case = {}", idx, each); - let span = from_str2(*each); + let span = from_str2(each); let result = clauses(span); assert_eq!(&result, &expectations[idx]); println!("{:?}", result); @@ -2713,7 +2711,7 @@ fn test_type_block() { for (idx, each) in examples.iter().enumerate() { println!("Test #{}: {}", idx, *each); - let span = from_str2(*each); + let span = from_str2(each); let result = type_block(span); println!("Result #{} = {:?}", idx, result); assert_eq!(&result, &expectations[idx]); @@ -3663,7 +3661,6 @@ fn rule_parameters_parse_test() -> Result<(), Error> { ["statements", "policy"] .iter() .map(|s| s.to_string()) - .into_iter() .collect::>() ); @@ -3675,7 +3672,6 @@ fn rule_parameters_parse_test() -> Result<(), Error> { ["statements"] .iter() .map(|s| s.to_string()) - .into_iter() .collect::>() ); @@ -3687,7 +3683,6 @@ fn rule_parameters_parse_test() -> Result<(), Error> { ["statements", "policy"] .iter() .map(|s| s.to_string()) - .into_iter() .collect::>() ); @@ -4084,7 +4079,7 @@ fn does_this_work() -> Result<(), Error> { fn unary_parse(#[case] s: &str, #[case] expected: CmpOperator) -> Result<(), Error> { let parsed = value_cmp(LocatedSpan::new_extra(s, ""))?.1 .0; assert_eq!(expected, parsed); - assert_eq!(expected.is_unary(), true); + assert!(expected.is_unary()); Ok(()) } diff --git a/guard/src/rules/path_value.rs b/guard/src/rules/path_value.rs index f40691722..cb16b622a 100644 --- a/guard/src/rules/path_value.rs +++ b/guard/src/rules/path_value.rs @@ -33,6 +33,7 @@ pub(crate) struct Location { } impl Location { + #[cfg(test)] pub(crate) fn new(line: usize, col: usize) -> Self { Location { line, col } } @@ -48,6 +49,7 @@ impl std::fmt::Display for Location { pub(crate) struct Path(pub(crate) String, pub(crate) Location); impl Path { + #[cfg(test)] pub(crate) fn new(path: String, line: usize, col: usize) -> Path { Path(path, Location::new(line, col)) } @@ -123,40 +125,14 @@ impl Path { Path(copy, self.1.clone()) } - pub(crate) fn extend_str_with_location(&self, part: &str, loc: Location) -> Path { - let mut copy = self.0.clone(); - copy.push('/'); - copy.push_str(part); - Path(copy, loc) - } - - pub(crate) fn extend_string(&self, part: &String) -> Path { - self.extend_str(part.as_str()) + pub(crate) fn extend_string(&self, part: &str) -> Path { + self.extend_str(part) } pub(crate) fn extend_usize(&self, part: usize) -> Path { let as_str = part.to_string(); self.extend_string(&as_str) } - - pub(crate) fn drop_last(&mut self) -> &mut Self { - let removed = match self.0.rfind('/') { - Some(idx) => self.0.as_str()[0..idx].to_string(), - None => return self, - }; - self.0 = removed; - self - } - - pub(crate) fn extend_with_value(&self, part: &Value) -> Result { - match part { - Value::String(s) => Ok(self.extend_string(s)), - _ => Err(Error::IncompatibleError(format!( - "Value type is not String, Value = {:?}", - part - ))), - } - } } #[derive(Debug, Clone, Deserialize)] @@ -165,15 +141,6 @@ pub(crate) struct MapValue { pub(crate) values: indexmap::IndexMap, } -impl MapValue { - pub(crate) fn new() -> MapValue { - MapValue { - keys: vec![], - values: indexmap::IndexMap::new(), - } - } -} - impl Serialize for MapValue { fn serialize(&self, serializer: S) -> Result where @@ -217,57 +184,6 @@ pub(crate) enum PathAwareValue { RangeChar((Path, RangeType)), } -impl PathAwareValue { - pub(crate) fn as_string(&self) -> Option<&str> { - match self { - PathAwareValue::String((_, v)) => Some(v), - _ => None, - } - } - - pub(crate) fn as_regex(&self) -> Option<&str> { - match self { - PathAwareValue::Regex((_, v)) => Some(v), - _ => None, - } - } - - pub(crate) fn as_bool(&self) -> Option { - match self { - PathAwareValue::Bool((_, v)) => Some(*v), - _ => None, - } - } - - pub(crate) fn as_int(&self) -> Option { - match self { - PathAwareValue::Int((_, v)) => Some(*v), - _ => None, - } - } - - pub(crate) fn as_float(&self) -> Option { - match self { - PathAwareValue::Float((_, v)) => Some(*v), - _ => None, - } - } - - pub(crate) fn as_list(&self) -> Option<&Vec> { - match self { - PathAwareValue::List((_, list)) => Some(list), - _ => None, - } - } - - pub(crate) fn as_map(&self) -> Option<&MapValue> { - match self { - PathAwareValue::Map((_, map)) => Some(map), - _ => None, - } - } -} - impl Hash for PathAwareValue { fn hash(&self, state: &mut H) { match self { @@ -367,10 +283,7 @@ impl PartialEq for PathAwareValue { } (rest, rest2) => match compare_values(rest, rest2) { - Ok(ordering) => match ordering { - Ordering::Equal => true, - _ => false, - }, + Ok(ordering) => matches!(ordering, Ordering::Equal), Err(_) => false, }, } @@ -526,14 +439,14 @@ impl TryFrom<(MarkedValue, Path)> for PathAwareValue { MarkedValue::Null(loc) => Ok(PathAwareValue::Null(path.with_location(loc))), MarkedValue::List(v, _) => { let mut result: Vec = Vec::with_capacity(v.len()); - let mut idx = 0; - for each in v { + + for (idx, each) in v.into_iter().enumerate() { let sub_path = path.extend_usize(idx); let loc = each.location().clone(); let value = PathAwareValue::try_from((each, sub_path.with_location(loc)))?; result.push(value); - idx += 1; } + Ok(PathAwareValue::List((path, result))) } @@ -556,12 +469,10 @@ impl TryFrom<(MarkedValue, Path)> for PathAwareValue { ))) } - MarkedValue::BadValue(val, loc) => { - return Err(Error::ParseError(format!( - "Bad Value encountered parsing incoming file Value = {}, Loc = {}", - val, loc - ))) - } + MarkedValue::BadValue(val, loc) => Err(Error::ParseError(format!( + "Bad Value encountered parsing incoming file Value = {}, Loc = {}", + val, loc + ))), } } } @@ -1008,24 +919,15 @@ impl PathAwareValue { } pub(crate) fn is_list(&self) -> bool { - match self { - PathAwareValue::List((_, _)) => true, - _ => false, - } + matches!(self, PathAwareValue::List((_, _))) } pub(crate) fn is_map(&self) -> bool { - match self { - PathAwareValue::Map((_, _)) => true, - _ => false, - } + matches!(self, PathAwareValue::Map((_, _))) } pub(crate) fn is_null(&self) -> bool { - match self { - PathAwareValue::Null(_) => true, - _ => false, - } + matches!(self, PathAwareValue::Null(_)) } fn map_error_or_empty(&self, all: bool, e: Error) -> Result, Error> { @@ -1033,10 +935,10 @@ impl PathAwareValue { match e { Error::IncompatibleRetrievalError(_) | Error::RetrievalError(_) => Ok(vec![]), - rest => return Err(rest), + rest => Err(rest), } } else { - return Err(e); + Err(e) } } @@ -1059,35 +961,10 @@ impl PathAwareValue { !self.is_list() && !self.is_map() } - pub(crate) fn is_string(&self) -> bool { - if let PathAwareValue::String(_) = self { - true - } else { - false - } - } - pub(crate) fn self_path(&self) -> &Path { self.self_value().0 } - pub(crate) fn self_path_mut(&mut self) -> &mut Path { - match self { - PathAwareValue::Null(path) - | PathAwareValue::String((path, _)) - | PathAwareValue::Regex((path, _)) - | PathAwareValue::Bool((path, _)) - | PathAwareValue::Int((path, _)) - | PathAwareValue::Float((path, _)) - | PathAwareValue::Char((path, _)) - | PathAwareValue::List((path, _)) - | PathAwareValue::Map((path, _)) - | PathAwareValue::RangeInt((path, _)) - | PathAwareValue::RangeFloat((path, _)) - | PathAwareValue::RangeChar((path, _)) => path, - } - } - pub(crate) fn self_value(&self) -> (&Path, &PathAwareValue) { match self { PathAwareValue::Null(path) => (path, self), @@ -1190,6 +1067,7 @@ fn compare_values(first: &PathAwareValue, other: &PathAwareValue) -> Result Result { let (reg, s) = match (first, second) { (PathAwareValue::String((_, s)), PathAwareValue::Regex((_, r))) => { @@ -1269,7 +1147,7 @@ pub(crate) fn compare_eq(first: &PathAwareValue, second: &PathAwareValue) -> Res let match_result = reg.is_match(s); match match_result { Ok(is_match) => Ok(is_match), - Err(error) => return Err(Error::from(error)), + Err(error) => Err(Error::from(error)), } } diff --git a/guard/src/rules/path_value/traversal.rs b/guard/src/rules/path_value/traversal.rs index c64ede8b2..dd8554cb7 100644 --- a/guard/src/rules/path_value/traversal.rs +++ b/guard/src/rules/path_value/traversal.rs @@ -35,6 +35,7 @@ pub(crate) enum TraversalResult<'a, 'b> { Key(&'a str), } +#[cfg(test)] impl<'a, 'b> TraversalResult<'a, 'b> { pub(crate) fn as_value(&self) -> Option<&Node<'_>> { match self { @@ -42,13 +43,6 @@ impl<'a, 'b> TraversalResult<'a, 'b> { _ => None, } } - - pub(crate) fn as_key(&self) -> Option<&str> { - match self { - Self::Key(k) => Some(*k), - _ => None, - } - } } fn from_value<'value>( @@ -86,7 +80,7 @@ fn from_value<'value>( ); let parent = Some(path.0.as_str()); for (_key, each) in map.values.iter() { - from_value(each, parent.clone(), nodes); + from_value(each, parent, nodes); } } @@ -100,7 +94,7 @@ fn from_value<'value>( ); let parent = Some(path.0.as_str()); for each in list.iter() { - from_value(each, parent.clone(), nodes); + from_value(each, parent, nodes); } } } @@ -172,14 +166,12 @@ impl<'value> Traversal<'value> { match self.nodes.get(pointer) { Some(node) => Ok(TraversalResult::Value(node)), - None => { - return Err(Error::RetrievalError(format!( - "Path {} did not yield value. Current Path {}, expected sub-paths {:?}", - pointer, - node.value().self_path().0, - self.nodes.range(pointer..) - ))) - } + None => Err(Error::RetrievalError(format!( + "Path {} did not yield value. Current Path {}, expected sub-paths {:?}", + pointer, + node.value().self_path().0, + self.nodes.range(pointer..) + ))), } } } diff --git a/guard/src/rules/path_value/traversal_tests.rs b/guard/src/rules/path_value/traversal_tests.rs index b20f6dd4c..54851d5f6 100644 --- a/guard/src/rules/path_value/traversal_tests.rs +++ b/guard/src/rules/path_value/traversal_tests.rs @@ -42,9 +42,9 @@ fn test_absolute_pointer_traversal() -> crate::rules::Result<()> { let traversal = Traversal::from(&value); let root = traversal.root().unwrap(); let result = traversal.at("/", root)?; - assert_eq!(matches!(result, TraversalResult::Value(_)), true); + assert!(matches!(result, TraversalResult::Value(_))); if let TraversalResult::Value(curr) = result { - assert_eq!(std::ptr::eq(&value, curr.value), true); + assert!(std::ptr::eq(&value, curr.value)); } let result = match result { TraversalResult::Value(val) => val, @@ -52,18 +52,18 @@ fn test_absolute_pointer_traversal() -> crate::rules::Result<()> { }; let result = traversal.at("/Resources/s3/Properties/AnalyticsConfiguration", result)?; - assert_eq!(matches!(result, TraversalResult::Value(_)), true); + assert!(matches!(result, TraversalResult::Value(_))); let result = match result { TraversalResult::Value(val) => val, _ => unreachable!(), }; - assert_eq!(matches!(result.value, PathAwareValue::List(_)), true); + assert!(matches!(result.value, PathAwareValue::List(_))); // // Testing relative // let upward = traversal.at("1/Name", result)?; - assert_eq!(matches!(upward, TraversalResult::Value(_)), true); + assert!(matches!(upward, TraversalResult::Value(_))); let upward = match upward { TraversalResult::Value(up) => up, _ => unreachable!(), diff --git a/guard/src/rules/path_value_tests.rs b/guard/src/rules/path_value_tests.rs index a5fd66fa7..56f958887 100644 --- a/guard/src/rules/path_value_tests.rs +++ b/guard/src/rules/path_value_tests.rs @@ -15,34 +15,34 @@ const SAMPLE_SINGLE: &str = r#"{ } }"#; -const SAMPLE_MULTIPLE: &str = r#"{ - "Resources": { - "vpc": { - "Type": "AWS::EC2::VPC", - "Properties": { - "CidrBlock": "10.0.0.0/12" - } - }, - "routing": { - "Type": "AWS::EC2::Route", - "Properties": { - "Acls": [ - { - "From": 0, - "To": 22, - "Allow": false - }, - { - "From": 0, - "To": 23, - "Allow": false - } - ] - } - } - } - } - "#; +// const SAMPLE_MULTIPLE: &str = r#"{ +// "Resources": { +// "vpc": { +// "Type": "AWS::EC2::VPC", +// "Properties": { +// "CidrBlock": "10.0.0.0/12" +// } +// }, +// "routing": { +// "Type": "AWS::EC2::Route", +// "Properties": { +// "Acls": [ +// { +// "From": 0, +// "To": 22, +// "Allow": false +// }, +// { +// "From": 0, +// "To": 23, +// "Allow": false +// } +// ] +// } +// } +// } +// } +// "#; #[test] fn path_value_equivalent() -> Result<(), Error> { @@ -63,7 +63,7 @@ fn path_value_equivalent() -> Result<(), Error> { vpc_props.clone(), MapValue { keys: vec![PathAwareValue::String(( - cidr_path.clone(), + cidr_path, String::from("CidrBlock"), ))], values: vpc_properties, @@ -79,8 +79,8 @@ fn path_value_equivalent() -> Result<(), Error> { vpc_path.clone(), MapValue { keys: vec![ - PathAwareValue::String((vpc_type.clone(), String::from("Type"))), - PathAwareValue::String((vpc_props.clone(), String::from("Properties"))), + PathAwareValue::String((vpc_type, String::from("Type"))), + PathAwareValue::String((vpc_props, String::from("Properties"))), ], values: vpc_block, }, @@ -91,10 +91,7 @@ fn path_value_equivalent() -> Result<(), Error> { let resources = PathAwareValue::Map(( resources_path.clone(), MapValue { - keys: vec![PathAwareValue::String(( - vpc_path.clone(), - String::from("vpc"), - ))], + keys: vec![PathAwareValue::String((vpc_path, String::from("vpc")))], values: resources, }, )); @@ -105,7 +102,7 @@ fn path_value_equivalent() -> Result<(), Error> { Path::root(), MapValue { keys: vec![PathAwareValue::String(( - resources_path.clone(), + resources_path, "Resources".to_string(), ))], values: top, @@ -221,7 +218,7 @@ fn path_value_queries() -> Result<(), Error> { &resources_with_sgs.query, &eval, )?; - assert_eq!(selected.is_empty(), true); + assert!(selected.is_empty()); let resources_with_sgs = AccessQuery::try_from("Resources.*[ Properties.SecurityGroupIds EXISTS ]")?; @@ -230,7 +227,7 @@ fn path_value_queries() -> Result<(), Error> { &resources_with_sgs.query, &eval, )?; - assert_eq!(selected.is_empty(), false); + assert!(!selected.is_empty()); let get_att_refs = r#"Resources.*[ Properties.SecurityGroupIds EXISTS ].Properties.SecurityGroupIds[ 'Fn::GetAtt' EXISTS ].'Fn::GetAtt'.*"#; let resources_with_sgs = AccessQuery::try_from(get_att_refs)?; @@ -390,16 +387,13 @@ fn merge_values_test() -> Result<(), Error> { )?)?; let resources = resources.merge(parameters)?; - assert_eq!(matches!(resources, PathAwareValue::Map(_)), true); + assert!(matches!(resources, PathAwareValue::Map(_))); let resources_map = match &resources { PathAwareValue::Map((_, map)) => map, _ => unreachable!(), }; assert_eq!(resources_map.values.len(), 2); - assert_eq!( - matches!(resources_map.values.get("PARAMETERS"), Some(_)), - true - ); + assert!(matches!(resources_map.values.get("PARAMETERS"), Some(_)),); let parameters = PathAwareValue::try_from(serde_yaml::from_str::( r#" @@ -408,7 +402,7 @@ fn merge_values_test() -> Result<(), Error> { "#, )?)?; let resources = resources.merge(parameters); - assert_eq!(resources.is_err(), true); + assert!(resources.is_err()); Ok(()) } diff --git a/guard/src/rules/values.rs b/guard/src/rules/values.rs index d622f00ba..507a68653 100644 --- a/guard/src/rules/values.rs +++ b/guard/src/rules/values.rs @@ -35,20 +35,16 @@ pub enum CmpOperator { impl CmpOperator { pub(crate) fn is_unary(&self) -> bool { - match self { + matches!( + self, CmpOperator::Exists - | CmpOperator::Empty - | CmpOperator::IsString - | CmpOperator::IsBool - | CmpOperator::IsList - | CmpOperator::IsInt - | CmpOperator::IsMap => true, - _ => false, - } - } - - pub(crate) fn is_binary(&self) -> bool { - !self.is_unary() + | CmpOperator::Empty + | CmpOperator::IsString + | CmpOperator::IsBool + | CmpOperator::IsList + | CmpOperator::IsInt + | CmpOperator::IsMap + ) } } @@ -156,8 +152,7 @@ impl Display for Value { Value::Float(float) => write!(f, "{}", float), Value::Bool(bool) => write!(f, "{}", bool), Value::List(list) => { - let result: Vec = - list.into_iter().map(|item| format!("{}", item)).collect(); + let result: Vec = list.iter().map(|item| format!("{}", item)).collect(); write!(f, "[{}]", result.join(", ")) } Value::Map(map) => { @@ -386,6 +381,7 @@ impl<'a> TryFrom<&'a str> for Value { } #[derive(PartialEq, Debug, Clone)] +#[allow(dead_code)] pub(crate) enum MarkedValue { Null(Location), BadValue(String, Location), @@ -429,10 +425,11 @@ pub(crate) fn read_from(from_reader: &str) -> crate::rules::Result let mut loader = Loader::new(); match loader.load(from_reader.to_string()) { Ok(doc) => Ok(doc), - Err(e) => Err(Error::ParseError(format!("{}", e.to_string()))), + Err(e) => Err(Error::ParseError(format!("{}", e))), } } +#[cfg(test)] pub(super) fn make_linked_hashmap<'a, I>(values: I) -> IndexMap where I: IntoIterator, diff --git a/guard/src/rules/values_tests.rs b/guard/src/rules/values_tests.rs index bbca3739b..28d7cdc66 100644 --- a/guard/src/rules/values_tests.rs +++ b/guard/src/rules/values_tests.rs @@ -136,7 +136,7 @@ fn test_query_on_value() -> Result<()> { struct DummyResolver<'a> { cache: HashMap<&'a str, Vec<&'a PathAwareValue>>, - }; + } impl<'a> EvaluationContext for DummyResolver<'a> { fn resolve_variable(&self, variable: &str) -> Result> { if let Some(v) = self.cache.get(variable) { @@ -177,7 +177,7 @@ fn test_query_on_value() -> Result<()> { if let PathAwareValue::Map(_index) = each { continue; } - assert!(false); + unreachable!() } // @@ -239,7 +239,7 @@ fn test_type_block_with_var_query_evaluation() -> Result<()> { let content = read_to_string("assets/cfn-template.json")?; let value = PathAwareValue::try_from(content.as_str())?; - struct DummyResolver {}; + struct DummyResolver {} impl EvaluationContext for DummyResolver { fn resolve_variable(&self, _variable: &str) -> Result> { unimplemented!() @@ -433,12 +433,12 @@ Resources: "/Resources/s3/Properties/TestJoinWithRef/Fn::Join/1/0", root, )?; - assert_eq!(matches!(test_join, TraversalResult::Value(_)), true); + assert!(matches!(test_join, TraversalResult::Value(_))); let condition = traversal.at("/MyNotCondition/Fn::Not/0/Fn::Equals/0/Ref", root)?; - assert_eq!(matches!(condition, TraversalResult::Value(_)), true); + assert!(matches!(condition, TraversalResult::Value(_))); match condition { TraversalResult::Value(val) => { - assert_eq!(val.value().is_scalar(), true); + assert!(val.value().is_scalar()); match val.value() { PathAwareValue::String((_, v)) => { assert_eq!(v, "EnvironmentType"); diff --git a/guard/src/utils/mod.rs b/guard/src/utils/mod.rs index e175688ed..304ffb71c 100644 --- a/guard/src/utils/mod.rs +++ b/guard/src/utils/mod.rs @@ -1,3 +1,4 @@ +#![allow(dead_code)] use crate::{command::Command, commands}; pub mod reader; @@ -20,7 +21,7 @@ pub fn get_guard_commands() -> Vec> { } impl<'buffer> ReadCursor<'buffer> { - pub(crate) fn new<'b>(buffer: &'b str) -> ReadCursor<'b> { + pub(crate) fn new(buffer: &str) -> ReadCursor { ReadCursor { line_num: 0, line_buffer: buffer.lines(), @@ -31,22 +32,23 @@ impl<'buffer> ReadCursor<'buffer> { pub(crate) fn next(&mut self) -> Option<(usize, &'buffer str)> { if self.line_num < self.previous_lines.len() { self.line_num += 1; - return Some(self.previous_lines[self.line_num - 1].clone()); + return Some(self.previous_lines[self.line_num - 1]); } match self.line_buffer.next() { Some(line) => { self.line_num += 1; self.previous_lines.push((self.line_num, line)); - return Some(self.previous_lines[self.line_num - 1].clone()); + Some(self.previous_lines[self.line_num - 1]) } None => None, } } + #[cfg(test)] pub(crate) fn prev(&mut self) -> Option<(usize, &'buffer str)> { - if self.line_num - 1 > 0 && self.previous_lines.len() > 0 { + if self.line_num - 1 > 0 && !self.previous_lines.is_empty() { self.line_num -= 1; - return Some(self.previous_lines[self.line_num].clone()); + return Some(self.previous_lines[self.line_num]); } None } @@ -54,7 +56,7 @@ impl<'buffer> ReadCursor<'buffer> { pub(crate) fn seek_line(&mut self, line: usize) -> Option<(usize, &'buffer str)> { if self.previous_lines.len() > line { self.line_num = line; - return Some(self.previous_lines[self.line_num - 1].clone()); + return Some(self.previous_lines[self.line_num - 1]); } loop { @@ -63,7 +65,7 @@ impl<'buffer> ReadCursor<'buffer> { self.line_num += 1; self.previous_lines.push((self.line_num, l)); if self.line_num == line { - return Some(self.previous_lines[self.line_num - 1].clone()); + return Some(self.previous_lines[self.line_num - 1]); } } None => return None, @@ -91,17 +93,16 @@ mod tests { Fn::Sub: "aws:arn:s3::${s3}""###; let mut cursor = ReadCursor::new(resources); - let mut line = 0; while let Some(line) = cursor.next() { println!("{}.{}", line.0, line.1); } let prev = cursor.prev(); - assert_eq!(prev.is_some(), true); + assert!(prev.is_some()); let prev = match prev { Some(p) => p, None => unreachable!(), }; - assert_eq!(prev.1.contains("${s3}"), true); + assert!(prev.1.contains("${s3}")); let _ = cursor.next(); let mut lines = Vec::with_capacity(cursor.previous_lines.len()); while let Some((line, prev)) = cursor.prev() { diff --git a/guard/tests/functional.rs b/guard/tests/functional.rs index 5400dca98..55270f6fc 100644 --- a/guard/tests/functional.rs +++ b/guard/tests/functional.rs @@ -3,10 +3,6 @@ #[cfg(test)] mod functional_tests { - use cfn_guard; - use cfn_guard::commands::validate::Validate; - use cfn_guard::commands::{DATA, INPUT_PARAMETERS, RULES, SHOW_SUMMARY, VALIDATE}; - use cfn_guard::utils::writer::{WriteBuffer, Writer}; #[test] fn test_run_check() { @@ -148,7 +144,7 @@ mod functional_tests { file_name: "functional_test.json", }, ValidateInput { - content: &rule, + content: rule, file_name: "functional_test.rule", }, verbose, diff --git a/guard/tests/parse_tree.rs b/guard/tests/parse_tree.rs index 981775dd8..f4dd0a759 100644 --- a/guard/tests/parse_tree.rs +++ b/guard/tests/parse_tree.rs @@ -5,20 +5,15 @@ pub(crate) mod utils; #[cfg(test)] mod parse_tree_tests { - use std::io::stdout; - - use rstest::rstest; - - use cfn_guard; use cfn_guard::commands::{PARSE_TREE, PRINT_JSON, PRINT_YAML, RULES}; use cfn_guard::utils::reader::ReadBuffer::Stdin; use cfn_guard::utils::reader::Reader; - use cfn_guard::utils::writer::WriteBuffer::Stderr; use cfn_guard::utils::writer::{WriteBuffer::Vec as WBVec, Writer}; use crate::utils::{get_full_path_for_resource_file, CommandTestRunner, StatusCode}; use crate::{assert_output_from_file_eq, assert_output_from_str_eq}; + #[allow(dead_code)] #[derive(Default)] struct ParseTreeTestRunner<'args> { rules: &'args str, @@ -33,11 +28,13 @@ mod parse_tree_tests { self } + #[allow(dead_code)] fn output(&'args mut self, arg: &'args str) -> &'args mut ParseTreeTestRunner { self.rules = arg; self } + #[allow(dead_code)] fn print_yaml(&'args mut self) -> &'args mut ParseTreeTestRunner { self.print_yaml = true; self @@ -83,8 +80,8 @@ mod parse_tree_tests { .run(&mut writer, &mut reader); assert_eq!(StatusCode::SUCCESS, status_code); - assert_output_from_str_eq!( - "{\"assignments\":[{\"var\":\"s3_buckets_server_side_encryption\",\"value\":{\"AccessClause\":{\"query\":[{\"Key\":\"Resources\"},{\"AllValues\":null},{\"Filter\":[null,[[{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"Type\"}],\"match_all\":true},\"comparator\":[\"Eq\",false],\"compare_with\":{\"Value\":{\"path\":\"\",\"value\":\"AWS::S3::Bucket\"}},\"custom_message\":null,\"location\":{\"line\":1,\"column\":54}},\"negation\":false}}],[{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"Metadata\"},{\"Key\":\"guard\"},{\"Key\":\"SuppressedRules\"}],\"match_all\":true},\"comparator\":[\"Exists\",true],\"compare_with\":null,\"custom_message\":null,\"location\":{\"line\":2,\"column\":3}},\"negation\":false}},{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"Metadata\"},{\"Key\":\"guard\"},{\"Key\":\"SuppressedRules\"},{\"AllValues\":null}],\"match_all\":true},\"comparator\":[\"Eq\",true],\"compare_with\":{\"Value\":{\"path\":\"\",\"value\":\"S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED\"}},\"custom_message\":null,\"location\":{\"line\":3,\"column\":3}},\"negation\":false}}]]]}],\"match_all\":true}}}],\"guard_rules\":[{\"rule_name\":\"S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED\",\"conditions\":[[{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"%s3_buckets_server_side_encryption\"}],\"match_all\":true},\"comparator\":[\"Empty\",true],\"compare_with\":null,\"custom_message\":null,\"location\":{\"line\":6,\"column\":52}},\"negation\":false}}]],\"block\":{\"assignments\":[],\"conjunctions\":[[{\"Clause\":{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"%s3_buckets_server_side_encryption\"},{\"AllIndices\":null},{\"Key\":\"Properties\"},{\"Key\":\"BucketEncryption\"}],\"match_all\":true},\"comparator\":[\"Exists\",false],\"compare_with\":null,\"custom_message\":null,\"location\":{\"line\":7,\"column\":3}},\"negation\":false}}}],[{\"Clause\":{\"Clause\":{\"access_clause\":{\"query\":{\"query\":[{\"Key\":\"%s3_buckets_server_side_encryption\"},{\"AllIndices\":null},{\"Key\":\"Properties\"},{\"Key\":\"BucketEncryption\"},{\"Key\":\"ServerSideEncryptionConfiguration\"},{\"AllIndices\":null},{\"Key\":\"ServerSideEncryptionByDefault\"},{\"Key\":\"SSEAlgorithm\"}],\"match_all\":true},\"comparator\":[\"In\",false],\"compare_with\":{\"Value\":{\"path\":\"\",\"value\":[\"aws:kms\",\"AES256\"]}},\"custom_message\":\"\\n Violation: S3 Bucket must enable server-side encryption.\\n Fix: Set the S3 Bucket property BucketEncryption.ServerSideEncryptionConfiguration.ServerSideEncryptionByDefault.SSEAlgorithm to either \\\"aws:kms\\\" or \\\"AES256\\\"\\n \",\"location\":{\"line\":8,\"column\":3}},\"negation\":false}}}]]}}],\"parameterized_rules\":[]}", + assert_output_from_file_eq!( + "resources/parse-tree/output-dir/s3_bucket_server_side_encryption_parse_tree.json", writer ) } @@ -161,6 +158,11 @@ mod parse_tree_tests { "resources/parse-tree/output-dir/test_rule_with_this_keyword.yaml", StatusCode::SUCCESS )] + #[case( + "validate/functions/rules/string_manipulation.guard", + "resources/parse-tree/output-dir/parse_tree_functions.yaml", + StatusCode::SUCCESS + )] fn test_yaml_output_compare_buffer_to_file( #[case] rules_arg: &str, #[case] expected_writer_output: &str, diff --git a/guard/tests/rulegen.rs b/guard/tests/rulegen.rs index 1c66e6a8c..1660c2d01 100644 --- a/guard/tests/rulegen.rs +++ b/guard/tests/rulegen.rs @@ -5,17 +5,13 @@ pub(crate) mod utils; #[cfg(test)] mod rulegen_tests { - use std::io::stdout; - - use rstest::rstest; use crate::assert_output_from_file_eq; use cfn_guard::commands::{OUTPUT, RULEGEN, TEMPLATE}; use cfn_guard::utils::reader::ReadBuffer::Stdin; use cfn_guard::utils::reader::Reader; use cfn_guard::utils::writer::WriteBuffer::Stderr; - use cfn_guard::utils::writer::{WriteBuffer::Stdout, WriteBuffer::Vec as WBVec, Writer}; - use cfn_guard::Error; + use cfn_guard::utils::writer::{WriteBuffer::Vec as WBVec, Writer}; use crate::utils::{get_full_path_for_resource_file, CommandTestRunner, StatusCode}; @@ -31,6 +27,7 @@ mod rulegen_tests { self } + #[allow(dead_code)] fn output(&'args mut self, arg: Option<&'args str>) -> &'args mut RulegenTestRunner { self.output = arg; self diff --git a/guard/tests/test_command.rs b/guard/tests/test_command.rs index 5f1297c95..3d2c31d20 100644 --- a/guard/tests/test_command.rs +++ b/guard/tests/test_command.rs @@ -5,23 +5,20 @@ pub(crate) mod utils; #[cfg(test)] mod test_command_tests { - use indoc::indoc; - use std::io::stdout; use rstest::rstest; use crate::assert_output_from_file_eq; use cfn_guard::commands::{ - ALPHABETICAL, DIRECTORY, LAST_MODIFIED, RULES, RULES_AND_TEST_FILE, RULES_FILE, TEST, - TEST_DATA, VERBOSE, + ALPHABETICAL, DIRECTORY, LAST_MODIFIED, RULES_AND_TEST_FILE, RULES_FILE, TEST, TEST_DATA, + VERBOSE, }; use cfn_guard::utils::reader::ReadBuffer::Stdin; use cfn_guard::utils::reader::Reader; - use cfn_guard::utils::writer::WriteBuffer::Stderr; - use cfn_guard::utils::writer::{WriteBuffer::Stdout, WriteBuffer::Vec as WBVec, Writer}; + use cfn_guard::utils::writer::{WriteBuffer::Vec as WBVec, Writer}; use cfn_guard::Error; - use crate::utils::{get_full_path_for_resource_file, CommandTestRunner, StatusCode}; + use crate::utils::{CommandTestRunner, StatusCode}; #[derive(Default)] struct TestCommandTestRunner<'args> { @@ -51,6 +48,7 @@ mod test_command_tests { self } + #[allow(dead_code)] fn rules_and_test_file( &'args mut self, arg: Option<&'args str>, @@ -64,11 +62,13 @@ mod test_command_tests { self } + #[allow(dead_code)] fn alphabetical(&'args mut self) -> &'args mut TestCommandTestRunner { self.alphabetical = true; self } + #[allow(dead_code)] fn last_modified(&'args mut self) -> &'args mut TestCommandTestRunner { self.last_modified = true; self @@ -216,9 +216,9 @@ mod test_command_tests { let mut reader = Reader::new(Stdin(std::io::stdin())); let mut writer = Writer::new(WBVec(vec![]), WBVec(vec![])); let status_code = TestCommandTestRunner::default() - .test_data(Some(&format!( + .test_data(Some( "resources/test-command/data-dir/s3_bucket_server_side_encryption_enabled.yaml", - ))) + )) .rules(Some( "resources/validate/rules-dir/s3_bucket_server_side_encryption_enabled.guard", )) @@ -262,9 +262,25 @@ mod test_command_tests { .rules(Some( "resources/test-command/functions/rules/json_parse.guard", )) - .verbose() .run(&mut writer, &mut reader); assert_eq!(StatusCode::SUCCESS, status_code); + assert_output_from_file_eq!("resources/test-command/output-dir/functions.out", writer); + } + + #[test] + fn test_with_failure() { + let mut reader = Reader::new(Stdin(std::io::stdin())); + let mut writer = Writer::new(WBVec(vec![]), WBVec(vec![])); + let status_code = TestCommandTestRunner::default() + .test_data(Option::from( + "resources/test-command/data-dir/failing_test.yaml", + )) + .rules(Some( + "resources/validate/rules-dir/s3_bucket_server_side_encryption_enabled.guard", + )) + .run(&mut writer, &mut reader); + + assert_eq!(StatusCode::TEST_COMMAND_FAILURE, status_code); } } diff --git a/guard/tests/utils.rs b/guard/tests/utils.rs index 7e950f7bc..ad91cf1bf 100644 --- a/guard/tests/utils.rs +++ b/guard/tests/utils.rs @@ -3,20 +3,20 @@ use std::collections::HashMap; use std::fs::File; -use std::io::Write; -use std::io::{stdout, BufReader, Read}; +use std::io::{BufReader, Read}; use std::path::PathBuf; use cfn_guard::utils; use cfn_guard::utils::reader::ReadBuffer::File as ReadFile; use cfn_guard::utils::reader::Reader; -use cfn_guard::utils::writer::{WriteBuffer, Writer}; +use cfn_guard::utils::writer::Writer; #[non_exhaustive] pub struct StatusCode; const GUARD_TEST_APP_NAME: &str = "cfn-guard-test"; +#[allow(dead_code)] impl StatusCode { pub const SUCCESS: i32 = 0; pub const INTERNAL_FAILURE: i32 = -1; @@ -25,6 +25,7 @@ impl StatusCode { pub const INCORRECT_STATUS_ERROR: i32 = 1; pub const TEST_COMMAND_FAILURE: i32 = 7; pub const PARSING_ERROR: i32 = 5; + pub const VALIDATION_ERROR: i32 = 19; } pub fn read_from_resource_file(path: &str) -> String { @@ -33,7 +34,8 @@ pub fn read_from_resource_file(path: &str) -> String { let mut content = String::new(); let mut reader = BufReader::new(File::open(resource.as_path()).unwrap()); reader.read_to_string(&mut content).unwrap(); - return content; + + content } pub fn get_full_path_for_resource_file(path: &str) -> String { @@ -53,6 +55,7 @@ pub fn compare_write_buffer_with_file( assert_eq!(expected_output, actual_output) } +#[allow(dead_code)] pub fn compare_write_buffer_with_string(expected_output: &str, actual_output_writer: Writer) { let actual_output = actual_output_writer.stripped().unwrap(); assert_eq!(expected_output, actual_output) @@ -61,12 +64,12 @@ pub fn compare_write_buffer_with_string(expected_output: &str, actual_output_wri pub trait CommandTestRunner { fn build_args(&self) -> Vec; - fn run(&self, mut writer: &mut Writer, mut reader: &mut Reader) -> i32 { + fn run(&self, writer: &mut Writer, reader: &mut Reader) -> i32 { let mut app = clap::Command::new(GUARD_TEST_APP_NAME); let args = self.build_args(); - let mut command_options = + let command_options = args.iter() .fold(vec![String::from(GUARD_TEST_APP_NAME)], |mut res, arg| { res.push(arg.to_string()); @@ -92,7 +95,7 @@ pub trait CommandTestRunner { match app.subcommand() { Some((name, value)) => { if let Some(command) = mappings.get(name) { - match (*command).execute(value, &mut writer, &mut reader) { + match (*command).execute(value, writer, reader) { Err(e) => { writer .write_err(format!("Error occurred {e}")) @@ -115,7 +118,7 @@ pub trait CommandTestRunner { #[macro_export] macro_rules! assert_output_from_file_eq { ($expected_output_relative_file_path: expr, $actual_output_writer: expr) => { - crate::utils::compare_write_buffer_with_file( + $crate::utils::compare_write_buffer_with_file( $expected_output_relative_file_path, $actual_output_writer, ) @@ -125,10 +128,11 @@ macro_rules! assert_output_from_file_eq { #[macro_export] macro_rules! assert_output_from_str_eq { ($expected_output: expr, $actual_output_writer: expr) => { - crate::utils::compare_write_buffer_with_string($expected_output, $actual_output_writer) + $crate::utils::compare_write_buffer_with_string($expected_output, $actual_output_writer) }; } +#[allow(dead_code)] pub fn get_reader(path: &str) -> Reader { let file = File::open(path).expect("failed to find mocked file"); diff --git a/guard/tests/validate.rs b/guard/tests/validate.rs index 0f5a99616..a4c5de911 100644 --- a/guard/tests/validate.rs +++ b/guard/tests/validate.rs @@ -5,29 +5,20 @@ pub(crate) mod utils; #[cfg(test)] mod validate_tests { - use std::fmt::format; - use std::fs::File; - use std::io::{stderr, stdout, Cursor, Read}; + use std::io::{stderr, stdout, Cursor}; use indoc::indoc; - use rstest::rstest; - use strip_ansi_escapes; - use cfn_guard; - use cfn_guard::commands::validate::Validate; use cfn_guard::commands::{ ALPHABETICAL, DATA, INPUT_PARAMETERS, LAST_MODIFIED, OUTPUT_FORMAT, PAYLOAD, PRINT_JSON, RULES, SHOW_SUMMARY, STRUCTURED, VALIDATE, VERBOSE, }; - use cfn_guard::utils::reader::ReadBuffer::{Cursor as ReadCursor, File as ReadFile, Stdin}; - use cfn_guard::utils::reader::{ReadBuffer, Reader}; + use cfn_guard::utils::reader::ReadBuffer::{Cursor as ReadCursor, Stdin}; + use cfn_guard::utils::reader::Reader; use cfn_guard::utils::writer::WriteBuffer::Stderr; use cfn_guard::utils::writer::{WriteBuffer::Stdout, WriteBuffer::Vec as WBVec, Writer}; - use crate::utils::{ - compare_write_buffer_with_file, compare_write_buffer_with_string, - get_full_path_for_resource_file, CommandTestRunner, StatusCode, - }; + use crate::utils::{get_full_path_for_resource_file, CommandTestRunner, StatusCode}; use crate::{assert_output_from_file_eq, assert_output_from_str_eq, utils}; #[derive(Default)] @@ -82,11 +73,13 @@ mod validate_tests { self } + #[allow(dead_code)] fn alphabetical(&'args mut self) -> &'args mut ValidateTestRunner { self.alphabetical = true; self } + #[allow(dead_code)] fn last_modified(&'args mut self) -> &'args mut ValidateTestRunner { self.last_modified = true; self @@ -97,6 +90,7 @@ mod validate_tests { self } + #[allow(dead_code)] fn print_json(&'args mut self) -> &'args mut ValidateTestRunner { self.print_json = true; self @@ -189,7 +183,7 @@ mod validate_tests { #[case( vec!["data-dir/s3-public-read-prohibited-template-non-compliant.yaml"], vec!["rules-dir/s3_bucket_public_read_prohibited.guard"], - StatusCode::PARSING_ERROR + StatusCode::VALIDATION_ERROR )] #[case(vec!["s3-server-side-encryption-template-non-compliant-2.yaml"], vec!["malformed-rule.guard"], StatusCode::INTERNAL_FAILURE)] #[case(vec!["malformed-template.yaml"], vec!["s3_bucket_server_side_encryption_enabled_2.guard"], StatusCode::INTERNAL_FAILURE)] @@ -207,6 +201,7 @@ mod validate_tests { )] #[case(vec!["dne.yaml"], vec!["rules-dir/s3_bucket_public_read_prohibited.guard"], StatusCode::INTERNAL_FAILURE)] #[case(vec!["data-dir/s3-public-read-prohibited-template-non-compliant.yaml"], vec!["dne.guard"], StatusCode::INTERNAL_FAILURE)] + #[case(vec!["blank.yaml"], vec!["rules-dir/s3_bucket_public_read_prohibited.guard"], StatusCode::INTERNAL_FAILURE)] fn test_single_data_file_single_rules_file_status( #[case] data_arg: Vec<&str>, #[case] rules_arg: Vec<&str>, @@ -257,7 +252,19 @@ mod validate_tests { vec!["data-dir/s3-public-read-prohibited-template-non-compliant.yaml"], vec!["rules-dir/s3_bucket_public_read_prohibited.guard"], "resources/validate/output-dir/test_single_data_file_single_rules_file_verbose_non_compliant.out", - StatusCode::PARSING_ERROR + StatusCode::VALIDATION_ERROR + )] + #[case( + vec!["template_where_resources_isnt_root.json"], + vec!["workshop.guard"], + "resources/validate/output-dir/failing_template_without_resources_at_root.out", + StatusCode::VALIDATION_ERROR + )] + #[case( + vec!["failing_template_with_slash_in_key.yaml"], + vec!["rules-dir/s3_bucket_server_side_encryption_enabled.guard"], + "resources/validate/output-dir/failing_template_with_slash_in_key.out", + StatusCode::VALIDATION_ERROR )] fn test_single_data_file_single_rules_file_verbose( #[case] data_arg: Vec<&str>, @@ -283,13 +290,13 @@ mod validate_tests { vec!["data-dir/s3-public-read-prohibited-template-non-compliant.yaml"], vec!["rules-dir/s3_bucket_public_read_prohibited.guard"], "resources/validate/output-dir/test_single_data_file_single_rules_file_verbose.out", - StatusCode::PARSING_ERROR + StatusCode::VALIDATION_ERROR )] #[case( vec!["data-dir/advanced_regex_negative_lookbehind_non_compliant.yaml"], vec!["rules-dir/advanced_regex_negative_lookbehind_rule.guard"], "resources/validate/output-dir/advanced_regex_negative_lookbehind_non_compliant.out", - StatusCode::PARSING_ERROR + StatusCode::VALIDATION_ERROR )] #[case( vec!["data-dir/advanced_regex_negative_lookbehind_compliant.yaml"], @@ -375,7 +382,7 @@ mod validate_tests { .rules(rules_arg) .run(&mut writer, &mut reader); - assert_eq!(StatusCode::PARSING_ERROR, status_code); + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); } #[rstest::rstest] @@ -383,7 +390,7 @@ mod validate_tests { vec!["db_resource.yaml"], vec!["db_param_port_rule.guard"], vec!["input-parameters-dir/db_params.yaml"], - StatusCode::PARSING_ERROR + StatusCode::VALIDATION_ERROR )] #[case( vec!["db_resource.yaml"], @@ -478,7 +485,7 @@ mod validate_tests { "resources/validate/output-dir/payload_verbose_non_compliant.out", writer ); - assert_eq!(StatusCode::PARSING_ERROR, status_code); + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); } #[test] @@ -539,7 +546,7 @@ mod validate_tests { "# }; - assert_eq!(StatusCode::PARSING_ERROR, status_code); + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); assert_eq!(expected, result); } @@ -559,7 +566,7 @@ mod validate_tests { .run(&mut writer, &mut reader); assert_output_from_file_eq!("resources/validate/output-dir/structured.json", writer); - assert_eq!(StatusCode::PARSING_ERROR, status_code); + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); } #[test] @@ -578,7 +585,7 @@ mod validate_tests { .run(&mut writer, &mut reader); assert_output_from_file_eq!("resources/validate/output-dir/structured.yaml", writer); - assert_eq!(StatusCode::PARSING_ERROR, status_code); + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); } #[test] @@ -643,4 +650,40 @@ mod validate_tests { assert_eq!(StatusCode::SUCCESS, status_code); } + + #[test] + fn test_validate_with_failing_count_and_compare_output() { + let mut reader = Reader::new(Stdin(std::io::stdin())); + let mut writer = Writer::new(WBVec(vec![]), WBVec(vec![])); + + let status_code = ValidateTestRunner::default() + .rules(vec!["/functions/rules/count_with_message.guard"]) + .data(vec!["/functions/data/template.yaml"]) + .show_summary(vec!["all"]) + .run(&mut writer, &mut reader); + + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); + assert_output_from_file_eq!( + "resources/validate/functions/output/failing_count_show_summary_all.out", + writer + ); + } + + #[test] + fn test_validate_with_failing_join_and_compare_output() { + let mut reader = Reader::new(Stdin(std::io::stdin())); + let mut writer = Writer::new(WBVec(vec![]), WBVec(vec![])); + + let status_code = ValidateTestRunner::default() + .rules(vec!["/functions/rules/join_with_message.guard"]) + .data(vec!["/functions/data/template.yaml"]) + .show_summary(vec!["all"]) + .run(&mut writer, &mut reader); + + assert_eq!(StatusCode::VALIDATION_ERROR, status_code); + assert_output_from_file_eq!( + "resources/validate/functions/output/failing_join_show_summary_all.out", + writer + ); + } }