diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b950c7dd2..9b207afe7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -2,9 +2,9 @@ name: Rust on: push: - branches: [ main, development ] + branches: [ main, development, rogue_one ] pull_request: - branches: [ main, development ] + branches: [ main, development, rogue_one ] env: CARGO_TERM_COLOR: always @@ -19,7 +19,7 @@ jobs: run: cargo build --release --verbose - name: Run unit tests run: cargo test --verbose - + shellcheck: name: Shellcheck runs-on: ubuntu-latest @@ -39,9 +39,12 @@ jobs: - name: Rustfmt Check uses: actions-rust-lang/rustfmt@v1 - aws-guard-rules-registry-ubuntu-integration-tests: - name: Integration tests against aws-guard-rules-registry on Ubuntu - runs-on: ubuntu-latest + aws-guard-rules-registry-integration-tests-linux: + strategy: + matrix: + os: [ ubuntu-latest, macos-latest ] + runs-on: ${{ matrix.os }} + name: Integration tests against aws-guard-rules-registry steps: - uses: actions/checkout@v3 name: Checkout cfn-guard @@ -69,11 +72,11 @@ jobs: - name: Run integration tests using parse-tree command run: | cd aws-guard-rules-registry/rules - + FAILED_RULES=() SKIPPED_RULES=() rules=( $(find . -type f -name "*.guard") ) - + for rule in "${rules[@]}" do if [ $(sed -e '/^[ \s]*#.*$/d' $rule | sed -r '/^\s*$/d' | wc -l) -eq 0 ]; then @@ -84,7 +87,7 @@ jobs: FAILED_RULES+=("$rule") fi done - + SKIPPED_RULE_COUNT=${#SKIPPED_RULES[@]} if [ $SKIPPED_RULE_COUNT -gt 0 ]; then echo "The following $SKIPPED_RULE_COUNT rule(s) were skipped because they contained only comments:" @@ -93,9 +96,9 @@ jobs: echo "$skipped_rule" done fi - + FAILED_RULE_COUNT=${#FAILED_RULES[@]} - + if [ $FAILED_RULE_COUNT -gt 0 ]; then echo "The following $FAILED_RULE_COUNT rule(s) have failed the parse-tree integration tests with a non-zero error code:" for failed_rule in "${FAILED_RULES[@]}" @@ -106,5 +109,68 @@ jobs: else echo "All the rules have succeeded the parse-tree integration tests." fi + + aws-guard-rules-registry-integration-tests-windows: + runs-on: windows-latest + name: Integration tests against aws-guard-rules-registry for Windows + steps: + - uses: actions/checkout@v3 + name: Checkout cfn-guard + with: + path: cloudformation-guard + - name: Build binary + run: | + cd cloudformation-guard/guard/ + cargo build --release + - uses: actions/checkout@v3 + name: Checkout aws-guard-rules-registry + with: + repository: aws-cloudformation/aws-guard-rules-registry + path: aws-guard-rules-registry + ref: main + - name: Run integration tests using test command + run: | + if (cloudformation-guard/target/release/cfn-guard test -d aws-guard-rules-registry/rules) { + echo "The integration tests for test command have passed." + } + else { + echo "The integration tests for test command have failed." + exit 1 + } + + - 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) { + $rule_files_without_comments = (Get-Content $rule.FullName) -replace '^[ \s]*#.*$', '' + if ([String]::IsNullOrWhiteSpace($rule_files_without_comments)){ + $SKIPPED_RULES += "$rule" + } + elseif (../../cloudformation-guard/target/release/cfn-guard parse-tree --rules $rule.FullName) { + continue + } else { + $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/.github/workflows/publish_docker.yml b/.github/workflows/publish_docker.yml index 667fbaf4d..d01aee3cb 100644 --- a/.github/workflows/publish_docker.yml +++ b/.github/workflows/publish_docker.yml @@ -2,7 +2,7 @@ name: Deploy to ECR Public Gallery on: push: - branches: [ main ] + branches: [ main, rogue_one ] jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97c052bbc..79319ab60 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,17 +27,17 @@ jobs: macos: | rustup target add x86_64-apple-darwin cargo build --release --target x86_64-apple-darwin - mkdir cfn-guard-v2-${{ matrix.os }} - cp ./target/x86_64-apple-darwin/release/cfn-guard ./cfn-guard-v2-${{ matrix.os }}/ - cp README.md ./cfn-guard-v2-${{ matrix.os }}/ - tar czvf ./cfn-guard-v2-${{ matrix.os }}.tar.gz ./cfn-guard-v2-${{ matrix.os }} + mkdir cfn-guard-v3-${{ matrix.os }} + cp ./target/x86_64-apple-darwin/release/cfn-guard ./cfn-guard-v3-${{ matrix.os }}/ + cp README.md ./cfn-guard-v3-${{ matrix.os }}/ + tar czvf ./cfn-guard-v3-${{ matrix.os }}.tar.gz ./cfn-guard-v3-${{ matrix.os }} linux: | rustup target add x86_64-unknown-linux-musl cargo build --release --target x86_64-unknown-linux-musl - mkdir cfn-guard-v2-${{ matrix.os }} - cp ./target/x86_64-unknown-linux-musl/release/cfn-guard ./cfn-guard-v2-${{ matrix.os }}/ - cp README.md ./cfn-guard-v2-${{ matrix.os }}/ - tar czvf ./cfn-guard-v2-${{ matrix.os }}.tar.gz ./cfn-guard-v2-${{ matrix.os }} + mkdir cfn-guard-v3-${{ matrix.os }} + cp ./target/x86_64-unknown-linux-musl/release/cfn-guard ./cfn-guard-v3-${{ matrix.os }}/ + cp README.md ./cfn-guard-v3-${{ matrix.os }}/ + tar czvf ./cfn-guard-v3-${{ matrix.os }}.tar.gz ./cfn-guard-v3-${{ matrix.os }} - name: Upload Release Asset id: upload-release-asset uses: actions/upload-release-asset@v1 @@ -45,6 +45,6 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./cfn-guard-v2-${{ matrix.os }}.tar.gz - asset_name: cfn-guard-v2-${{ matrix.os }}.tar.gz + asset_path: ./cfn-guard-v3-${{ matrix.os }}.tar.gz + asset_name: cfn-guard-v3-${{ matrix.os }}.tar.gz asset_content_type: application/octet-stream diff --git a/Cargo.lock b/Cargo.lock index a22e91155..36f9381b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,7 +45,7 @@ checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -124,10 +124,11 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cfn-guard" -version = "2.1.3" +version = "3.0.0-beta" dependencies = [ "Inflector", "clap", + "clap_complete", "colored", "enumflags2", "enumflags2_derive", @@ -156,7 +157,7 @@ dependencies = [ [[package]] name = "cfn-guard-ffi" -version = "2.1.3" +version = "3.0.0-beta" dependencies = [ "cfn-guard", "ffi-support", @@ -164,7 +165,7 @@ dependencies = [ [[package]] name = "cfn-guard-lambda" -version = "2.1.3" +version = "3.0.0-beta" dependencies = [ "cfn-guard", "lambda_runtime", @@ -189,6 +190,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "clap_complete" +version = "4.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd125be87bf4c255ebc50de0b7f4d2a6201e8ac3dc86e39c0ad081dc5e7236fe" +dependencies = [ + "clap", +] + [[package]] name = "clap_lex" version = "0.3.2" @@ -229,22 +239,22 @@ dependencies = [ [[package]] name = "enumflags2" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +checksum = "c041f5090df68b32bcd905365fd51769c8b9d553fe87fde0b683534f10c01bd2" dependencies = [ "enumflags2_derive", ] [[package]] name = "enumflags2_derive" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.18", ] [[package]] @@ -350,7 +360,7 @@ checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -729,18 +739,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "dec2b086b7a862cf4de201096214fa870344cf922b2b30c167badb3af3195406" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -790,7 +800,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn", + "syn 1.0.107", ] [[package]] @@ -854,7 +864,7 @@ checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -961,6 +971,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -987,7 +1008,7 @@ checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1054,7 +1075,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -1095,7 +1116,7 @@ checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..17a2e8063 --- /dev/null +++ b/Makefile @@ -0,0 +1,7 @@ +build-CloudFormationGuardLambda: +# installing rust every time you build is not great, but it's better than having +# to install a toolchain yourself. In most cases builds will be infrequent. + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source ${HOME}/.cargo/env && rustup target add x86_64-unknown-linux-musl + source ${HOME}/.cargo/env && cd guard-lambda && cargo build --release --target x86_64-unknown-linux-musl + cp -r /tmp/samcli/scratch/target/x86_64-unknown-linux-musl/release/cfn-guard-lambda $(ARTIFACTS_DIR)/bootstrap diff --git a/README.md b/README.md index 6f78e8934..b71e6ab7f 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,25 @@ **Validate Cloud Environments with Policy-as-Code** -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. +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. -Guard 2.0 release is a complete re-write of the earlier 1.0 version to make the tool general-purpose. With Guard 2.0, developers can continue writing policies for CloudFormation Templates. In addition, developers can use Guard in the following business domains: +The Guard 3.0 release introduces support for stateful rules through built-in functions, SAM CLI as an alternative deployment method for `cfn-guard-lambda`, command auto-completions for shell, advanced regular expressions, improved handling of intrinsic functions for the test command, as well as the `--structured` flag to the validate command to emit JSON/YAML parseable output. Note that previously written tests may have to be reviewed due to the corrected behavior of intrinsic function handling. Please see the release notes for full details and examples. + +Guard can be used for the following domains: 1. **Preventative Governance and Compliance (shift left):** validate Infrastructure-as-code (IaC) or infrastructure/service compositions such as CloudFormation Templates, CloudFormation ChangeSets, Terraform JSON configuration files, Kubernetes configurations, and more against Guard policies representing your organizational best practices for security, compliance, and more. For example, developers can use Guard policies with - 1. Terraform plan (in JSON format) for deployment safety assessment checks or Terraform state files to detect live state deviations. + 1. Terraform plan (**in JSON format**) for deployment safety assessment checks or Terraform state files to detect live state deviations. 2. Static assessment of IaC templates to determine network reachability like Amazon Redshift cluster deployed inside a VPC and prevent the provision of such stacks. 2. **Detective Governance and Compliance:** validate conformity of Configuration Management Database (CMDB) resources such as AWS Config-based configuration items (CIs). For example, developers can use Guard policies against AWS Config CIs to continuously monitor state of deployed AWS and non-AWS resources, detect violations from policies, and trigger remediation. -3. **Deployment Safety:** validate CloudFormation ChangeSets to ensure changes are safe before deployment. For example, renaming an Amazon DynamoDB Table will cause a replacement of the Table. With Guard 2.0, you can prevent such changes in your CI/CD pipelines. +3. **Deployment Safety:** validate CloudFormation ChangeSets to ensure changes are safe before deployment. For example, renaming an Amazon DynamoDB Table will cause a replacement of the Table. With Guard 3.0, you can prevent such changes in your CI/CD pipelines. + +> **NOTE**: If you are using Guard 1.0 rules, we highly recommend adopting the latest version of Guard to simplify your current policy-as-code experience. Guard 2.0 and higher versions are backward incompatible with your Guard 1.0 rules and can result in breaking changes. The Guard 2.0 release was a complete re-write of the earlier 1.0 version to make the tool general-purpose. +> +> To migrate from Guard 1.0 rules to use the updated grammar, follow the steps given below. +> 1. Pull the release artifacts for the latest `2.x.x` release from the corresponding release page listed [here](https://github.com/aws-cloudformation/cloudformation-guard/releases). +> 2. Use `migrate` command to transition your existing 1.0 rules to use the updated grammar +> 3. Read about all new Guard features from the latest release, and modify your rules for enhanced experience -> **NOTE**: If you are using Guard 1.0, we highly recommend adopting Guard 2.0 because Guard 2.0 is a major release that introduces multiple features to simplify your current policy-as-code experience. Guard 2.0 and higher versions are backward incompatible with your Guard 1.0 rules and can result in breaking changes. To migrate from Guard 1.0 to Guard 2.0, 1) use migrate command to transition your existing 1.0 rules to 2.0 rules and 2) read all new Guard 2.0 features. -> -> You can find code related to Guard 2.0 on the main branch of the repo and code related to Guard 1.0 on [Guard1.0 branch](https://github.com/aws-cloudformation/cloudformation-guard/tree/Guard1.0) of the repo. **Guard In Action** @@ -37,7 +43,7 @@ Guard 2.0 release is a complete re-write of the earlier 1.0 version to make the > Guard is an open-source command line interface (CLI) that provides developers a general purpose domain-specific language (DSL) to express policy-as-code and then validate their JSON- and YAML-formatted data against that code. Guard’s DSL is a simple, powerful, and expressive declarative language to define policies. It is built on the foundation of clauses, which are assertions that evaluate to `true` or `false`. Examples clauses can include simple validations like all Amazon Simple Storage Service (S3) buckets must have versioning enabled, or combined to express complex validations like preventing public network reachability of Amazon Redshift clusters placed in a subnet. Guard has support for looping, queries with filtering, cross query joins, single shot variable assignments, conditional executions, and composable rules. These features help developers to express simple and advanced policies for various domains. **2) What Guard is not?** -> Guard **is not** a general-purpose programming language. It is a purpose-built DSL that is designed for policy definition and evaluation. Both non-technical people and developers can easily pick up Guard. Guard is human-readable and machine enforceable. +> Guard **is not** a general-purpose programming language. It is a **purpose-built** DSL that is designed for policy definition and evaluation. Both non-technical people and developers can easily pick up Guard. Guard is human-readable and machine enforceable. **3) Where can I use Guard?** > You can use Guard to define any type of policy for evaluation. You can apply Guard in the context of multiple domains: a) validating IaC/service compositions such as [CloudFormation Templates](https://aws.amazon.com/cloudformation/resources/templates/), Terraform JSON configuration files, and Kubernetes configurations, b) verifying conformity of CMDB resources such as AWS Config-based CIs, and c) assessing security postures across resources like AWS Security Hub. The policy language and expression is common to all of them, based on simple Guard clauses. @@ -46,7 +52,7 @@ Guard 2.0 release is a complete re-write of the earlier 1.0 version to make the > Clause is an assertion that evaluates to true or false. Clauses can either use binary operations to compare two values (e.g `==, >` and `in`), or unary operations that takes only one value (e.g. `exists, empty,` and `is_list`). Here is a sample clause that compares `Type` to be a `AWS::S3::Bucket` : ``` -Type == /AWS::S3::Bucket/ +Type == /AWS::S3::Bucket/ ``` **4) What are the supported** **types** **that can I use to define clauses?** @@ -56,12 +62,12 @@ Type == /AWS::S3::Bucket/ > *Unary Operators:* `exists, empty, is_string, is_list, is_struct, is_bool, is_int, is_float, not(!)` > *Binary Operators:* `==, !=, >, >=, <, <=, IN ` > -> Most operators are self-explanatory. A few important points: +> Most operators are self-explanatory. A few important points: > > 1. Refer [Guard: Clauses](docs/CLAUSES.md) to understand the usage of `exists` and `empty` operators > 2. Clause `ports >= [10, 20, 30]` implies that every element for `ports` is `>= 30`. If your intention is range, then express it as `r[10, 30]` . > 3. Clause `ports >= 100` can have ports resolve to an array `[121, 200, 443]`. This check ensures that every element returned was >= 100, and in the example shown this evaluates to `true.` -> 4. `IN` operator for collections (does not work for `string` type) to check if any value matches. For example: +> 4. `IN` operator for collections (does not work for `string` type) to check if any value matches. For example: ``` Properties.SslPolicy IN ["ELBSecurityPolicy-TLS-1-2-2017-01", "ELBSecurityPolicy-TLS-1-2-Ext-2018-06"] @@ -78,13 +84,13 @@ rule s3_bucket_name_encryption_check when %s3_buckets !empty { %s3_buckets { Properties { # common prefix - BucketName == /^MyCompanyPrefix/ - + BucketName == /^MyCompanyPrefix/ + # encryption MUST BE on BucketEncryption.ServerSideEncryptionConfiguration[*] { - # only KMS - ServerSideEncryptionByDefault.SSEAlgorithm IN - ["aws:KMS"] + # only KMS + ServerSideEncryptionByDefault.SSEAlgorithm IN + ["aws:KMS"] } } } @@ -104,14 +110,14 @@ rule s3_bucket_name_encryption_check when %s3_buckets !empty { > Guard is a DSL and an accompanying CLI tool that allows easy-to-use definitions for declaring and enforcing policies. Today the tool supports local file-based execution of a category of policies. Guard doesn’t support the following things today, along with workarounds for some: > > 1. Sourcing of rules from external locations such as GitHub Release and S3 bucket. If you want this feature natively in Guard, please raise an issue or +1 an existing issue. -> 2. Ability to import Guard policy file by reference (local file or GitHub, S3, etc.). It currently only supports a directory on disk of policy files, that it would execute. +> 2. Ability to import Guard policy file by reference (local file or GitHub, S3, etc.). It currently only supports a directory on disk of policy files, that it would execute. > 3. Parameter/Vault resolution for IaC tools such as CloudFormation or Terraform. Before you ask, the answer is NO. We will not add native support in Guard as the engine is general-purpose. If you need CloudFormation resolution support, raise an issue and we might have a solution for you. We do not support HCL natively. We do, however, support Terraform Plan in JSON to run policies against for deployment safety. If you need HCL support, raise an issue as well. -> 4. Ability to reference variables like `%s3_buckets`, inside error messages. Both JSON/Console output for evaluation results contain some of this information for inference. We also do not support using variable references to create dynamic regex expressions. However, we support variable references inside queries for cross join support, like `Resources.%references.Properties.Tags`. +> 4. Ability to reference variables like `%s3_buckets`, inside error messages. Both JSON/Console output for evaluation results contain some of this information for inference. We also do not support using variable references to create dynamic regex expressions. However, we support variable references inside queries for cross join support, like `Resources.%references.Properties.Tags`. > 5. Support for specifying variable names when accessing map or list elements to cature these values. For example, consider this check `Resources[resource_name].Properties.Tags not empty`, here `resource_name` captures the key or index value. The information is tracked as a part of the evaluation context today and present in both console/JSON outputs. This support will be extended to regex expression variable captures as well. -> 6. There are [known issues](docs/KNOWN_ISSUES.md) with potential workarounds that we are tracking towards resolution +> 6. There are [known issues](docs/KNOWN_ISSUES.md) with potential workarounds that we are tracking towards resolution **11) What are we really thankful about?** -> Where do we start? Hmm.... we want to thank Rust [language’s forums](https://users.rust-lang.org/), [build management, and amazing ecosystem](https://crates.io/) without which none of this would have been possible. We are not the greatest Rust practitioners, so if we did something that is not idiomatic Rust, please raise a PR. +> Where do we start? Hmm.... we want to thank Rust [language’s forums](https://users.rust-lang.org/), [build management, and amazing ecosystem](https://crates.io/) without which none of this would have been possible. We are not the greatest Rust practitioners, so if we did something that is not idiomatic Rust, please raise a PR. > > We want to make a special mention to [nom](https://github.com/Geal/nom) combinator parser framework to write our language parser in. This was an excellent decision that improved readability, testability, and composition. We highly recommend it. There are some rough edges, but it’s just a wonderful, awesome library. Thank you. Apart from that, we are consumers of many crates including [hyper](https://crates.io/crates/hyper) for HTTP handling, [simple logger](https://crates.io/crates/simple_logger), and many more. We also want to thank the open-source community for sharing their feedback with us through GitHub issues/PRs. > @@ -119,13 +125,13 @@ rule s3_bucket_name_encryption_check when %s3_buckets !empty { ## Guard DSL -### Tenets +### Tenets **(Unless you know better ones)** These tenets help guide the development of the Guard DSL: -* **Simple**: The language must be simple for customers to author policy rules, simple to integrate with an integrated development environment (IDE), readable for human comprehension, and machine enforceable. +* **Simple**: The language must be simple for customers to author policy rules, simple to integrate with an integrated development environment (IDE), readable for human comprehension, and machine enforceable. * **Unambiguous**: The language must not allow for ambiguous interpretations that make it hard for customers to comprehend the policy evaluation. The tool is targeted for security and compliance related attestations that need the auditor to consistently and unambiguously understand rules and their evaluations. @@ -153,7 +159,7 @@ These tenets help guide the development of the Guard DSL: ##### MacOS -By default this is built for macOS-10 (Catalina). It has been tested to work on macOS-11 (Big Sur). See [OS Matrix](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#github-hosted-runners) +By default this is built for macOS-12 (Monterey). See [OS Matrix](https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#github-hosted-runners) 1. Open terminal of your choice. Default `Cmd+Space`, type `terminal` 2. Cut-n-paste the commands below (change version=X for other versions) @@ -206,7 +212,7 @@ If building on `Ubuntu`, it is recommended to run `sudo apt-get update; sudo apt 10. Run it and accept the defaults. #### Cargo-based Installation -Now that you have [rust and cargo installed](https://doc.rust-lang.org/cargo/getting-started/installation.html), installation of cfn-guard is easy: +Now that you have [rust and cargo installed](https://doc.rust-lang.org/cargo/getting-started/installation.html), installation of cfn-guard is easy: ```bash $ cargo install cfn-guard @@ -216,50 +222,51 @@ Check `help` to see if it is working. ```bash $ cfn-guard help -cfn-guard 2.1.3 +cfn-guard 3.0.0-beta - Guard is a general-purpose tool that provides a simple declarative syntax to define + 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). Rules are composed of clauses expressed using Conjunctive Normal Form (fancy way of saying it is a logical AND of OR clauses). Guard has deep integration with CloudFormation templates for evaluation but is a general tool that equally works for any JSON- and YAML- data. -USAGE: - cfn-guard [SUBCOMMAND] - -FLAGS: - -h, --help Prints help information - -V, --version Prints version information - -SUBCOMMANDS: - help Prints this message or the help of the given subcommand(s) - migrate Migrates 1.0 rules to 2.0 compatible rules. - parse-tree Prints out the parse tree for the rules defined in the file. - rulegen Autogenerate rules from an existing JSON- or YAML- formatted data. (Currently works with only - CloudFormation templates) - test Built in unit testing capability to validate a Guard rules file against - unit tests specified in YAML format to determine each individual rule's success - or failure testing. - validate Evaluates rules against the data files to determine success or failure. - You can point rules flag to a rules directory and point data flag to a data directory. - When pointed to a directory it will read all rules in the directory file and evaluate - them against the data files found in the directory. The command can also point to a - single file and it would work as well. - Note - When pointing the command to a directory, the directory may not contain a mix of - rules and data files. The directory being pointed to must contain only data files, - or rules files. +Usage: cfn-guard [COMMAND] + +Commands: + parse-tree Prints out the parse tree for the rules defined in the file. + test Built in unit testing capability to validate a Guard rules file against + unit tests specified in YAML format to determine each individual rule's success + or failure testing. + + validate Evaluates rules against the data files to determine success or failure. + You can point rules flag to a rules directory and point data flag to a data directory. + When pointed to a directory it will read all rules in the directory file and evaluate + them against the data files found in the directory. The command can also point to a + single file and it would work as well. + Note - When pointing the command to a directory, the directory may not contain a mix of + rules and data files. The directory being pointed to must contain only data files, + or rules files. + + rulegen Autogenerate rules from an existing JSON- or YAML- formatted data. (Currently works with only CloudFormation templates) + completions Generate auto-completions for all the sub-commands in shell. + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help + -V, --version Print version + ``` ### How does Guard CLI work? -The two common Guard CLI commands are `validate` and `test`. +The two common Guard CLI commands are `validate` and `test`. #### Validate -Validate command is used when you need to assess the compliance or security posture as defined by a set of policy files against incoming JSON/YAML data. Common data payloads used are CloudFormation Templates, CloudFormation ChangeSets, Kubernetes Pod policies, Terraform Plan/Configuration in JSON format, and more. Here is an example run of the `validate` command for assessing Kubernetes Pod Container configurations +Validate command is used when you need to assess the compliance or security posture as defined by a set of policy files against incoming JSON/YAML data. Common data payloads used are CloudFormation Templates, CloudFormation ChangeSets, Kubernetes Pod policies, Terraform Plan/Configuration in JSON format, and more. Here is an example run of the `validate` command for assessing Kubernetes Pod Container configurations -1. Save the sample policy rules file below as `k8s-pod-containers-limits.guard`: +1. Save the sample policy rules file below as `k8s-pod-containers-limits.guard`: ``` # @@ -267,29 +274,29 @@ Validate command is used when you need to assess the compliance or security post # # -# These set of rules primarily apply to the version 1 of the API spec (including v1Beta) and +# These set of rules primarily apply to the version 1 of the API spec (including v1Beta) and # the kind of document is a 'Pod' # -rule version_and_kind_match +rule version_and_kind_match { apiVersion == /v1/ kind == 'Pod' } # -# For the 'Pod' document ensure that containers have resource limits set +# For the 'Pod' document ensure that containers have resource limits set # for memory # -rule ensure_container_has_memory_limits when version_and_kind_match +rule ensure_container_has_memory_limits when version_and_kind_match { - spec.containers[*] + spec.containers[*] { - resources.limits + resources.limits { # # Ensure that memory attribute is set # - memory exists + memory exists << Id: K8S_REC_22 Description: Memory limit must be set for the container @@ -300,19 +307,19 @@ rule ensure_container_has_memory_limits when version_and_kind_match } # -# For the 'Pod' document ensure that containers have resource limits set +# For the 'Pod' document ensure that containers have resource limits set # for cpu # -rule ensure_container_has_cpu_limits when version_and_kind_match +rule ensure_container_has_cpu_limits when version_and_kind_match { - spec.containers[*] + spec.containers[*] { - resources.limits + resources.limits { # # Ensure that cpu attribute is set # - cpu exists + cpu exists << Id: K8S_REC_24 Description: Cpu limit must be set for the container @@ -320,8 +327,8 @@ rule ensure_container_has_cpu_limits when version_and_kind_match } } } -``` - +``` + 2. Paste the command below and hit `enter` ```bash @@ -329,8 +336,8 @@ cfn-guard validate -r k8s-pod-containers-limits.guard ``` 3. Cut-n-paste the sample configuration below for k8s pods on STDIN and then hit `CTRL+D`: - -```yaml + +```yaml apiVersion: v1 kind: Pod metadata: @@ -358,7 +365,7 @@ spec: ![Execution of validate](images/guard-validate.png) -The container `app` does not contain CPU limits specified, which fails the overall evaluation as shown in the screenshot. +The container `app` does not contain CPU limits specified, which fails the overall evaluation as shown in the screenshot. #### Test @@ -368,19 +375,19 @@ Test command is used during the development of guard policy rules files. Test pr ``` # -# Select from Resources section of the template all ApiGateway resources -# present in the template. +# Select from Resources section of the template all ApiGateway resources +# present in the template. # let api_gws = Resources.*[ Type == 'AWS::ApiGateway::RestApi' ] # -# Rule intent -# a) All ApiGateway instances deployed must be private +# Rule intent +# a) All ApiGateway instances deployed must be private # b) All ApiGateway instances must have atleast one IAM policy condition key to allow access from a VPC # # Expectations: # 1) SKIP when there are not API Gateway instances in the template -# 2) PASS when ALL ApiGateway instances MUST be "PRIVATE" and +# 2) PASS when ALL ApiGateway instances MUST be "PRIVATE" and # ALL ApiGateway instances MUST have one IAM Condition key with aws:sourceVpc or aws:SourceVpc # 3) FAIL otherwise # @@ -396,9 +403,9 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { %api_gws { Properties { # - # ALL ApiGateways must have atleast one IAM statement that has Condition keys with + # ALL ApiGateways must have atleast one IAM statement that has Condition keys with # aws:sourceVpc - # + # some Policy.Statement[*] { Condition.*[ keys == /aws:[sS]ource(Vpc|VPC|Vpce|VPCE)/ ] !empty } @@ -423,7 +430,7 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { check_rest_api_is_private: SKIP check_rest_api_has_vpc_access: SKIP - input: - Resources: + Resources: apiGw: Type: AWS::ApiGateway::RestApi expectations: @@ -431,7 +438,7 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { check_rest_api_is_private: FAIL check_rest_api_has_vpc_access: SKIP - input: - Resources: + Resources: apiGw: Type: AWS::ApiGateway::RestApi Properties: @@ -442,7 +449,7 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { check_rest_api_is_private: PASS check_rest_api_has_vpc_access: FAIL - input: - Resources: + Resources: apiGw: Type: AWS::ApiGateway::RestApi Properties: @@ -453,7 +460,7 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { check_rest_api_is_private: FAIL check_rest_api_has_vpc_access: SKIP - input: - Resources: + Resources: apiGw: Type: AWS::ApiGateway::RestApi Properties: @@ -472,7 +479,7 @@ rule check_rest_api_has_vpc_access when check_rest_api_is_private { check_rest_api_has_vpc_access: PASS ``` -3. Run the test command +3. Run the test command ```bash cfn-guard test -r api_gateway_private_access.guard -t api_gateway_private_access_tests.yaml @@ -503,8 +510,8 @@ As a starting point for writing Guard rules for yourself or your organisation we ## 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 +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). diff --git a/docs/FUNCTIONS.md b/docs/FUNCTIONS.md new file mode 100644 index 000000000..bc295ff93 --- /dev/null +++ b/docs/FUNCTIONS.md @@ -0,0 +1,241 @@ +# Guard built-in functions and stateful rules + +As of version 3.0.0 guard now supplies some builtin functions, allowing for stateful rules + +NOTE: all examples are operating off the following yaml template + +```yaml +Resources: + 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"]}}}]}' + + s3: + Type: AWS::S3::Bucket + Properties: + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + bucket: + Type: AWS::S3::Bucket + Properties: + PublicAccessBlockConfiguration: + BlockPublicAcls: false + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true +``` + +## String Manipulation + +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 + +This function accepts a single argument: + +- this argument can either be a query that resolves to a string or a string literal. + +The return value for this function is a query where each string that was resolved from the input is parsed into its json value + +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'] +rule TEST_JSON_PARSE when %template !empty { + let policy = %template.Properties.Policy + + let res = json_parse(%policy) + + %res !empty + %res == %expected + + let policy_text = %template.BucketPolicy.PolicyText + let res2 = json_parse(%policy_text) + + %res2.Statement[*] + { + Effect == "Deny" + Resource == "arn:aws:s3:::s3-test-123/*" + } +} +``` + +### regex_replace + +The regex_replace function adds support for replacing one regular expression with another + +This function accepts 3 arguments: + +- 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 + - 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 + +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 + +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: `///-/` + +``` +let template = Resources.*[ Type == 'AWS::New::Service'] + +rule TEST_REGEX_REPLACE when %template !empty { + %template.Properties.Arn exists + let arn = %template.Properties.Arn + + let arn_partition_regex = "^arn:(\w+):(\w+):([\w0-9-]+):(\d+):(.+)$" + let capture_group_reordering = "${1}/${4}/${3}/${2}-${5}" + let res = regex_replace(%arn, %arn_partition_regex, %capture_group_reordering) + + %res == "aws/123456789012/us-west-2/newservice-Table/extracted" +} +``` + +### join + +The join function adds support to collect a query, and then join their values using the provided delimiter. + +This function accepts 2 arguments: + +- 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 + +The return value for this function is query where each string that was resolved from the input is joined with the provided delimiter + +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 + +``` +let template = Resources.*[ Type == 'AWS::New::Service'] + +rule TEST_COLLECTION when %template !empty { + let collection = %template.Collection.* + + let res = join(%collection, ",") + %res == "a,b,c" +} +``` + +### to_lower and to_upper + +Both functions accept a single argument: + +- This argument is a query that resolves to a string(s) - all strings resolved will have the operation applied on them + +Both these functions are very similar, one manipulates all resolved strings from a query to lower case, and the other to upper case + +``` +let type = Resources.newServer.Type + +rule STRING_MANIPULATION when %type !empty { + let lower = to_lower(%type) + %lower == "aws::new::service" + %lower == /aws::new::service/ + + let upper = to_upper(%type) + %upper == "AWS::NEW::SERVICE" + %upper == /AWS::NEW::SERVICE/ +} +``` + +### substring + +The substring function adds support to collect a part of all strings resolved from a query + +This function accepts 3 arguments: + +- 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) + +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 + +``` +let template = Resources.*[ Type == 'AWS::New::Service'] + +rule TEST_SUBSTRING when %template !empty { + %template.Properties.Arn exists + let arn = %template.Properties.Arn + + let res = substring(%arn, 0, 3) + + %res == "arn" +} +``` + +### url_decode + +This function accepts a single argument: + +- this argument can either be a query that resolves to a string or a string literal. + +The return value for this function is a query that contains each url decoded version of every string value from the input + +The following rule shows how you could url_decode the string `This%20string%20will%20be%20URL%20encoded` + +``` +let template = Resources.*[ Type == 'AWS::New::Service'] + +rule SOME_RULE when %template !empty { + %template.Properties.Encoded exists + let encoded = %template.Properties.Encoded + + let res = url_decode(%encoded) + %res == "This string will be URL encoded" +} +``` + +## Collection functions + +### count + +The count function adds support to count the number of items that a query resolves to + +This function accepts a single argument: + +- This argument is a query that can resolve to any type - the number of resolved values from this query is returned as the result + +The following rules show different ways we can use the count function. + +- One queries a struct, and counts the number of properties. +- 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 + +``` +let template = Resources.*[ Type == 'AWS::New::Service' ] +rule SOME_RULE when %template !empty { + let props = %template.Properties.* + let res = count(%props) + %res == 3 + + let collection = %template.Collection.* + let res2 = count(%collection) + %res2 == 3 + + let buckets = Resources.*[ Type == 'AWS::S3::Bucket' ] + let b = %buckets[ Properties.PublicAccessBlockConfiguration exists ] + let res3 = count(%b) + %res3 == 2 + +} +``` diff --git a/guard-ffi/Cargo.toml b/guard-ffi/Cargo.toml index 9d58ea765..f8da6780d 100644 --- a/guard-ffi/Cargo.toml +++ b/guard-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard-ffi" -version = "2.1.3" +version = "3.0.0-beta" 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,6 +14,5 @@ keywords = ["policy-as-code", "guard", "cfn-guard", "security", "compliance"] crate-type = ["rlib", "dylib"] [dependencies] -cfn-guard = { version = "2.1.3", path = "../guard" } +cfn-guard = { version = "3.0.0-beta", path = "../guard" } ffi-support = "0.4.4" - diff --git a/guard-lambda/.gitignore b/guard-lambda/.gitignore index d0af2acc3..39a0140d5 100644 --- a/guard-lambda/.gitignore +++ b/guard-lambda/.gitignore @@ -1,4 +1,6 @@ /target +.aws-sam/ +samconfig.toml **/*.rs.bk lambda.zip output.json diff --git a/guard-lambda/Cargo.toml b/guard-lambda/Cargo.toml index e97247312..e7c9000f5 100644 --- a/guard-lambda/Cargo.toml +++ b/guard-lambda/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard-lambda" -version = "2.1.3" +version = "3.0.0-beta" 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 = "2.1.3", path = "../guard" } \ No newline at end of file +cfn-guard = { version = "3.0.0-beta", path = "../guard" } diff --git a/guard-lambda/README.md b/guard-lambda/README.md index e5aaccf74..d42379589 100644 --- a/guard-lambda/README.md +++ b/guard-lambda/README.md @@ -1,15 +1,15 @@ # AWS CloudFormation Guard as a Lambda -The Lambda version of the tool is a lightweight wrapper around the core [cfn-guard](../guard) code that can simply be invoked as a Lambda. +The Lambda version of the tool is a lightweight wrapper around the core [cfn-guard](../guard) code that can simply be invoked as a Lambda. We currently support 2 methods for deploying the Lambda. ## Table of Contents -* [Installation](#installation) -* [Build and run post-install](#to-build-and-run-post-install) -* [Calling the Lambda Function](#calling-the-lambda-function) +* [Method 1: Installation using AWS CLI](#method-1-installation-using-aws-cli) +* [Method 2: Installation using SAM CLI](#method-2-installation-using-sam-cli) +* [Calling the AWS Lambda Function](#calling-the-aws-lambda-function) * [FAQs](#faqs) -## Installation +## Method 1: Installation using AWS CLI ### Dependencies @@ -61,6 +61,75 @@ The Lambda version of the tool is a lightweight wrapper around the core [cfn-gua --region $REGION ``` +## Method 2: Installation using SAM CLI + +### Dependencies + +* [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-getting-started.html) installed +* AWS CLI [installed](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) and [configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html) with permissions to deploy via CloudFormation. SAM CLI will internally use the credentials you setup AWS CLI with. You may use the following IAM policy as a reference for least privileged access. + +
+ + IAM Policy for SAM CLI User + + + ```js + { + "Version": "2012-10-17", + "Statement": + [ + { + "Effect": "Allow", + "Action": + [ + "cloudformation:CreateChangeSet", + "cloudformation:CreateStack", + "cloudformation:DeleteChangeSet", + "cloudformation:DeleteStack", + "cloudformation:DescribeChangeSet", + "cloudformation:DescribeStackEvents", + "cloudformation:DescribeStackResource", + "cloudformation:DescribeStackResources", + "cloudformation:DescribeStacks", + "cloudformation:ExecuteChangeSet", + "cloudformation:GetTemplate", + "cloudformation:GetTemplateSummary", + "cloudformation:ListStackResources", + "cloudformation:SetStackPolicy", + "cloudformation:UpdateStack", + "cloudformation:UpdateTerminationProtection", + "iam:AttachRolePolicy", + "iam:CreateRole", + "iam:DeleteRole", + "iam:DetachRolePolicy", + "iam:GetRole", + "iam:PassRole", + "lambda:CreateFunction", + "lambda:DeleteFunction", + "lambda:GetFunction", + "lambda:TagResource", + "s3:GetObject", + "s3:PutObject" + ], + "Resource": "*" + } + ] + } + ``` +
+ + +* [Docker](https://docs.docker.com/get-docker/) installed + +### Building and deploying + +1. Make sure docker is running +2. Navigate to `guard-lambda` directory and run `sam build --use-container` to build the code for the Lambda function +3. Run `sam deploy --guided` and complete the interactive workflow. This workflow will create a CloudFormation changeset and deploy it +4. Once it succeeds, the name of the function will be shown in the `CloudFormationGuardLambdaFunctionName` output +5. For subsequent updates, build the code again (step 2) and run `sam deploy` (without `--guided`) + + ## Calling the AWS Lambda Function ## Payload Structure @@ -72,7 +141,7 @@ The payload JSON to `cfn-guard-lambda` requires the following two fields: ## Invoking `cfn-guard-lambda` -To invoke the submitted `cfn-guard` as an AWS Lambda function run: +Initialize the variable `LAMBDA_FUNCTION_NAME` to the name of the deployed AWS Lambda Function, and invoke it using the following syntax: ```bash aws lambda invoke \ @@ -82,8 +151,8 @@ aws lambda invoke \ output.json ``` -**Note:** `--cli-binary-format` option is only required to override the default configuration setting to perform the parsing of -JSON input. If the command doesn't work with this option, try running it without this configuration override. Your current +**Note:** `--cli-binary-format` option is only required to override the default configuration setting to perform the parsing of +JSON input. If the command doesn't work with this option, try running it without this configuration override. Your current AWS CLI version may have this configuration set to the required value. ### Example diff --git a/guard-lambda/template.yaml b/guard-lambda/template.yaml new file mode 100644 index 000000000..a79a90eab --- /dev/null +++ b/guard-lambda/template.yaml @@ -0,0 +1,20 @@ +Transform: AWS::Serverless-2016-10-31 + +Resources: + CloudFormationGuardLambda: + Type: AWS::Serverless::Function + Properties: + Runtime: provided.al2 + Handler: guard.handler + # We need to point to the parent directory, so we can use ../guard/* + CodeUri: .. + Environment: + Variables: + RUST_BACKTRACE: "1" + Tracing: Active + Metadata: + BuildMethod: makefile + +Outputs: + CloudFormationGuardLambdaFunctionName: + Value: !Ref CloudFormationGuardLambda diff --git a/guard/Cargo.toml b/guard/Cargo.toml index a3e18dade..3c8a0aaf3 100644 --- a/guard/Cargo.toml +++ b/guard/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cfn-guard" -version = "2.1.3" +version = "3.0.0-beta" edition = "2018" authors = ["Diwakar Chakravarthy", "John Tompkins", "Omkar Hegde", "Priya Padmanaban", "Bryan Ayala", "Kexiang Wang", "Akshay Rane", "Tyler Southwick", "Josh Fried", "aws-cloudformation-developers "] @@ -20,9 +20,10 @@ crate-type = ["rlib"] nom = "5.1.2" nom_locate = "2.0.0" indexmap = { version = "1.6.0", features = ["serde-1"] } -clap = "4.1.4" +clap_complete = "4.1.2" +clap = { version = "4.1.4" } strip-ansi-escapes = "0.1.1" -serde = { version = "1.0", features = ["derive"] } +serde = { version = "1.0", features = ["derive", "rc"] } serde_yaml = "0.9.10" walkdir = "2.3.1" colored = "2.0.0" @@ -30,8 +31,8 @@ heck = "0.3.1" lazy_static = "1.4.0" itertools = "0.4.7" string-builder = "0.2.0" -enumflags2 = "0.7.1" -enumflags2_derive = "0.7.0" +enumflags2 = "0.7.7" +enumflags2_derive = "0.7.7" Inflector = "0.11.4" urlencoding = "2.1.0" grep-searcher = "0.1.8" diff --git a/guard/README.md b/guard/README.md index e0254b850..27b46a109 100644 --- a/guard/README.md +++ b/guard/README.md @@ -1,32 +1,32 @@ # AWS CloudFormation Guard 2.0'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. +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. As an example of how to use AWS CloudFormation Guard (cfn-guard), given a CloudFormation template (template.json): ```json { - "Resources":{ - "NewVolume":{ - "Type":"AWS::EC2::Volume", - "Properties":{ - "Size":500, - "Encrypted":false, - "AvailabilityZone":"us-west-2b" - } - }, - "NewVolume2":{ - "Type":"AWS::EC2::Volume", - "Properties":{ - "Size":100, - "Encrypted":false, - "AvailabilityZone":"us-west-2c" - } - } + "Resources": { + "NewVolume": { + "Type": "AWS::EC2::Volume", + "Properties": { + "Size": 500, + "Encrypted": false, + "AvailabilityZone": "us-west-2b" + } }, - "Parameters":{ - "InstanceName":"NewInstance" + "NewVolume2": { + "Type": "AWS::EC2::Volume", + "Properties": { + "Size": 100, + "Encrypted": false, + "AvailabilityZone": "us-west-2c" + } } + }, + "Parameters": { + "InstanceName": "NewInstance" + } } ``` @@ -34,7 +34,7 @@ And a rules file (rules.guard): ``` # Create a variable named 'aws_ec2_volume_resources' that selects all resources of type "AWS::EC2::Volume" -# in the input resource template +# in the input resource template let aws_ec2_volume_resources = Resources.*[ Type == 'AWS::EC2::Volume' ] # Create a rule named aws_template_parameters for validation in the "Parameters" section of the template @@ -42,7 +42,7 @@ rule aws_template_parameters { Parameters.InstanceName == "TestInstance" } -# Create a rule named aws_ec2_volume that filters on "AWS::EC2::Volume" type being present in the template +# Create a rule named aws_ec2_volume that filters on "AWS::EC2::Volume" type being present in the template rule aws_ec2_volume when %aws_ec2_volume_resources !empty { %aws_ec2_volume_resources.Properties.Encrypted == true %aws_ec2_volume_resources.Properties.Size IN [50, 500] @@ -61,7 +61,7 @@ aws_template_parameters FAIL aws_ec2_volume FAIL ``` -We designed `cfn-guard` to be plugged into your build processes. +We designed `cfn-guard` to be plugged into your build processes. If CloudFormation Guard validates the templates successfully, it gives you an exit status (`$?` in bash) of `0`. If CloudFormation Guard identifies a rule violation, it gives you a status report of the rules that failed. Use the verbose flag `-v` to see the detailed evaluation tree that shows how CloudFormation Guard evaluated each rule. @@ -70,38 +70,59 @@ Use the verbose flag `-v` to see the detailed evaluation tree that shows how Clo `cfn-guard` has five modes of operation: - ### Validate `validate` (like the example above) validates data against rules. -```bash -cfn-guard-validate -Evaluates rules against the data files to determine success or failure. -You can point rules flag to a rules directory and point data flag to a data directory. -When pointed to a directory it will read all rules in the directory file and evaluate -them against the data files found in the directory. The command can also point to a -single file and it would work as well. -Note - When pointing the command to a directory, the directory may not contain a mix of -rules and data files. The directory being pointed to must contain only data files, -or rules files. - -USAGE: - cfn-guard validate [FLAGS] [OPTIONS] --rules - -FLAGS: - -a, --alphabetical Validate files in a directory ordered alphabetically - -h, --help Prints help information - -m, --last-modified Validate files in a directory ordered by last modified times - -p, --print-json Print output in json format - -s, --show-clause-failures Show clause failure along with summary - -V, --version Prints version information - -v, --verbose Verbose logging - -OPTIONS: - -d, --data Provide a file or dir for data files in JSON or YAML - -r, --rules Provide a rules file or a directory of rules files - +````bash +Usage: cfn-guard validate [OPTIONS] <--rules [...]|--payload> + +Options: + -r, --rules [...] + Provide a rules file or a directory of rules files. Supports passing multiple values by using this option repeatedly. + Example: + --rules rule1.guard --rules ./rules-dir1 --rules rule2.guard + For directory arguments such as `rules-dir1` above, scanning is only supported for files with following extensions: .guard, .ruleset + -d, --data [...] + Provide a data file or directory of data files in JSON or YAML. Supports passing multiple values by using this option repeatedly. + Example: + --data template1.yaml --data ./data-dir1 --data template2.yaml + For directory arguments such as `data-dir1` above, scanning is only supported for files with following extensions: .yaml, .yml, .json, .jsn, .template + -i, --input-parameters [...] + Provide a data file or directory of data files in JSON or YAML that specifies any additional parameters to use along with data files to be used as a combined context. All the parameter files passed as input get merged and this combined context is again merged with each file passed as an argument for `data`. Due to this, every file is expected to contain mutually exclusive properties, without any overlap. Supports passing multiple values by using this option repeatedly. + Example: + --input-parameters param1.yaml --input-parameters ./param-dir1 --input-parameters param2.yaml + For directory arguments such as `param-dir1` above, scanning is only supported for files with following extensions: .yaml, .yml, .json, .jsn, .template + -t, --type + Specify the type of data file used for improved messaging - ex: CFNTemplate [possible values: CFNTemplate] + -o, --output-format + Specify the format in which the output should be displayed [default: single-line-summary] [possible values: json, yaml, single-line-summary] + -S, --show-summary + Controls if the summary table needs to be displayed. --show-summary fail (default) or --show-summary pass,fail (only show rules that did pass/fail) or --show-summary none (to turn it off) or --show-summary all (to show all the rules that pass, fail or skip) [default: fail] [possible values: none, all, pass, fail, skip] + -s, --show-clause-failures + Show clause failure along with summary + -a, --alphabetical + Validate files in a directory ordered alphabetically + -m, --last-modified + Validate files in a directory ordered by last modified times + -v, --verbose + Verbose logging + -p, --print-json + Print output in json format + -P, --payload + Provide rules and data in the following JSON format via STDIN, + {"rules":["", "", ...], "data":["", "", ...]}, where, + - "rules" takes a list of string version of rules files as its value and + - "data" takes a list of string version of data files as it value. + When --payload is specified --rules and --data cannot be specified. + -z, --structured + Print out a list of structured and valid JSON/YAML. This argument conflicts with the following arguments: + verbose + print-json + show-summary: all/fail/pass/skip + output-format: single-line-summary + -h, --help + Print help ``` ### Rulegen @@ -122,11 +143,12 @@ FLAGS: OPTIONS: -o, --output Write to output file -t, --template